cryptpad/www/common/common-interface.js

1491 lines
47 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

if (!document.querySelector("#alertifyCSS")) {
// Prevent alertify from injecting CSS, we create our own in alertify.less.
// see: https://github.com/alertifyjs/alertify.js/blob/v1.0.11/src/js/alertify.js#L414
var head = document.getElementsByTagName("head")[0];
var css = document.createElement("span");
css.id = "alertifyCSS";
css.setAttribute('data-but-why', 'see: common-interface.js');
head.insertBefore(css, head.firstChild);
}
define([
'jquery',
'/customize/messages.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-notifier.js',
'/customize/application_config.js',
'/bower_components/alertifyjs/dist/js/alertify.js',
'/lib/tippy/tippy.min.js',
'/common/hyperscript.js',
'/customize/loading.js',
//'/common/test.js',
'/lib/jquery-ui/jquery-ui.min.js', // autocomplete widget
'/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js',
'css!/lib/tippy/tippy.css',
'css!/lib/jquery-ui/jquery-ui.min.css'
], function ($, Messages, Util, Hash, Notifier, AppConfig,
Alertify, Tippy, h, Loading/*, Test */) {
var UI = {};
/*
* Alertifyjs
*/
UI.Alertify = Alertify;
// set notification timeout
Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000;
var setHTML = UI.setHTML = function (e, html) {
e.innerHTML = html;
return e;
};
UI.getDisplayName = function (name) {
return (typeof(name) === 'string'? name: "").trim() || Messages.anonymous;
};
// FIXME almost everywhere this is used would also be
// a good candidate for sframe-common's getMediatagFromHref
UI.mediaTag = function (src, key) {
return h('media-tag', {
src: src,
'data-crypto-key': 'cryptpad:' + key,
});
};
var findCancelButton = UI.findCancelButton = function (root) {
if (root) {
return $(root).find('button.cancel').last();
}
return $('button.cancel').last();
};
var findOKButton = UI.findOKButton = function (root) {
if (root) {
return $(root).find('button.ok').last();
}
return $('button.ok').last();
};
UI.removeModals = function () {
$('div.alertify').remove();
};
var listenForKeys = UI.listenForKeys = function (yes, no, el) {
var handler = function (e) {
e.stopPropagation();
switch (e.which) {
case 27: // cancel
if (typeof(no) === 'function') { no(e); }
$(el || window).off('keydown', handler);
break;
case 13: // enter
if (typeof(yes) === 'function') { yes(e); }
$(el || window).off('keydown', handler);
break;
}
};
$(el || window).keydown(handler);
return handler;
};
var customListenForKeys = function (keys, cb, el) {
if (!keys || !keys.length || typeof cb !== "function") { return; }
var handler = function (e) {
e.stopPropagation();
keys.some(function (k) {
// k is number or array
// if it's an array, it should be [keyCode, "{ctrl|alt|shift|meta}"]
if (Array.isArray(k) && e.which === k[0] && e[k[1] + 'Key']) {
cb();
return true;
}
if (e.which === k && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
cb();
return true;
}
});
};
$(el || window).keydown(handler);
return handler;
};
var stopListening = UI.stopListening = function (handler) {
if (!handler) { return; } // we don't want to stop all the 'keyup' listeners
$(window).off('keyup', handler);
};
var dialog = UI.dialog = {};
var merge = function (a, b) {
var c = {};
if (a) {
Object.keys(a).forEach(function (k) {
c[k] = a[k];
});
}
if (b) {
Object.keys(b).forEach(function (k) {
c[k] = b[k];
});
}
return c;
};
dialog.selectable = function (value, opt) {
var attrs = merge({
type: 'text',
readonly: 'readonly',
}, opt);
var input = h('input', attrs);
$(input).val(value).click(function () {
input.select();
});
return input;
};
dialog.selectableArea = function (value, opt) {
var attrs = merge({
readonly: 'readonly',
}, opt);
var input = h('textarea', attrs);
$(input).val(value).click(function () {
input.select();
});
return input;
};
dialog.okButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.btn.ok.primary';
return h(sel, { tabindex: '2', }, content || Messages.okButton);
};
dialog.cancelButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.' + classString:'button.btn.cancel';
return h(sel, { tabindex: '1'}, content || Messages.cancelButton);
};
dialog.message = function (text) {
return h('p.msg', text);
};
dialog.textInput = function (opt) {
var attrs = merge({
type: 'text',
'class': 'cp-text-input',
}, opt);
return h('p.msg', h('input', attrs));
};
dialog.textTypeInput = function (dropdown) {
var attrs = {
type: 'text',
'class': 'cp-text-type-input',
};
return h('p.msg.cp-alertify-type-container', h('div.cp-alertify-type', [
h('input', attrs),
dropdown // must be a "span"
]));
};
dialog.nav = function (content) {
return h('nav', content || [
dialog.cancelButton(),
dialog.okButton(),
]);
};
dialog.frame = function (content, opt) {
opt = opt || {};
var cls = opt.wide ? '.wide' : '';
var frame = h('div.alertify', {
tabindex: 1,
}, [
h('div.dialog', [
h('div'+cls, content),
])
]);
var $frame = $(frame);
frame.closeModal = function (cb) {
frame.closeModal = function () {}; // Prevent further calls
$frame.fadeOut(150, function () {
$frame.detach();
if (typeof(cb) === "function") { cb(); }
});
};
return $frame.click(function (e) {
$frame.find('.cp-dropdown-content').hide();
e.stopPropagation();
})[0];
};
/**
* tabs is an array containing objects
* each object must have the following attributes:
* - title: String
* - content: DOMElement
*/
dialog.tabs = function (tabs) {
var contents = [];
var titles = [];
var active = 0;
tabs.forEach(function (tab, i) {
if (!(tab.content || tab.disabled) || !tab.title) { return; }
var content = h('div.alertify-tabs-content', tab.content);
var title = h('span.alertify-tabs-title'+ (tab.disabled ? '.disabled' : ''), h('span.tab-title-text',{id: 'cp-tab-' + tab.title.toLowerCase(), 'aria-hidden':"true"}, tab.title));
if (tab.icon) {
var icon = h('i', {class: tab.icon, 'aria-labelledby': 'cp-tab-' + tab.title.toLowerCase()});
$(title).prepend(' ').prepend(icon);
}
$(title).click(function () {
if (tab.disabled) { return; }
var old = tabs[active];
if (old.onHide) { old.onHide(); }
titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); });
contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); });
if (tab.onShow) {
tab.onShow();
}
$(title).addClass('alertify-tabs-active');
$(content).addClass('alertify-tabs-content-active');
active = i;
});
titles.push(title);
contents.push(content);
if (tab.active && !tab.disabled) { active = i; }
});
if (contents.length) {
$(contents[active]).addClass('alertify-tabs-content-active');
$(titles[active]).addClass('alertify-tabs-active');
}
return h('div.alertify-tabs', [
h('div.alertify-tabs-titles', titles),
h('div.alertify-tabs-contents', contents),
]);
};
UI.tokenField = function (target, autocomplete) {
var t = {
element: target || h('input'),
};
var $t = t.tokenfield = $(t.element).tokenfield({
autocomplete: {
source: autocomplete,
delay: 100
},
showAutocompleteOnFocus: false
});
t.getTokens = function (ignorePending) {
var tokens = $t.tokenfield('getTokens').map(function (token) {
return token.value.toLowerCase();
});
if (ignorePending) { return tokens; }
var $pendingEl = $($t.parent().find('.token-input')[0]);
var val = ($pendingEl.val() || "").trim();
if (val && tokens.indexOf(val) === -1) {
return tokens.concat(val);
}
return tokens;
};
var $root = $t.parent();
var $input = $root.find('.token-input');
var $button = $(h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', Messages.tag_add)
]));
$button.click(function () {
$t.tokenfield('createToken', $input.val());
});
var $container = $(h('span.cp-tokenfield-container'));
var $form = $(h('span.cp-tokenfield-form'));
$container.insertAfter($input);
// Fix the UI to keep the "add" or "edit" button at the correct location
var isEdit = false;
var called = false;
var resetUI = function () {
called = true;
setTimeout(function () {
$container.find('.tokenfield-empty').remove();
var $tokens = $root.find('.token').prependTo($container);
if (!$tokens.length) {
$container.prepend(h('span.tokenfield-empty', Messages.kanban_noTags));
}
$form.append($input);
$form.append($button);
if (isEdit) { $button.find('span').text(Messages.tag_edit); }
else { $button.find('span').text(Messages.add); }
$container.append($form);
$input.focus();
isEdit = false;
called = false;
});
};
resetUI();
$t.on('tokenfield:removedtoken', function () {
resetUI();
});
$t.on('tokenfield:editedtoken', function () {
resetUI();
});
$t.on('tokenfield:createdtoken', function () {
$input.val('');
resetUI();
});
$t.on('tokenfield:edittoken', function () {
isEdit = true;
});
// Fix UI issue where the input could go outside of the container
var MutationObserver = window.MutationObserver;
var observer = new MutationObserver(function(mutations) {
if (called) { return; }
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].classList &&
mutation.addedNodes[i].classList.contains('token-input')) {
resetUI();
break;
}
}
});
});
observer.observe($root[0], {
childList: true,
subtree: false
});
$t.on('tokenfield:removetoken', function () {
$input.focus();
});
t.preventDuplicates = function (cb) {
$t.on('tokenfield:createtoken', function (ev) {
// Close the suggest list when a token is added because we're going to wipe the input
var $input = $t.closest('.tokenfield').find('.token-input');
$input.autocomplete('close');
var val;
ev.attrs.value = ev.attrs.value.toLowerCase();
if (t.getTokens(true).some(function (t) {
if (t === ev.attrs.value) {
ev.preventDefault();
return ((val = t));
}
})) {
ev.preventDefault();
if (typeof(cb) === 'function') { cb(val); }
}
});
return t;
};
t.setTokens = function (tokens) {
$t.tokenfield('setTokens',
tokens.map(function (token) {
return {
value: token.toLowerCase(),
label: token.toLowerCase(),
};
}));
};
$t.closest('.tokenfield').removeClass('form-control');
t.focus = function () {
var $temp = $t.closest('.tokenfield').find('.token-input');
$temp.css('width', '20%');
$t.tokenfield('focusInput', $temp[0]);
};
return t;
};
dialog.tagPrompt = function (tags, existing, cb) {
var input = dialog.textInput();
var tagger = dialog.frame([
dialog.message([ Messages.tags_add ]),
input,
h('center', h('small', Messages.tags_notShared)),
dialog.nav(),
]);
var field = UI.tokenField(input, existing).preventDuplicates(function (val) {
UI.warn(Messages._getKey('tags_duplicate', [val]));
});
var listener;
var close = Util.once(function (result, ev) {
ev.stopPropagation();
ev.preventDefault();
var $frame = $(tagger).fadeOut(150, function () {
stopListening(listener);
$frame.remove();
cb(result, ev);
});
});
var $ok = findOKButton(tagger).click(function (e) {
var tokens = field.getTokens();
close(tokens, e);
});
var $cancel = findCancelButton(tagger).click(function (e) {
close(null, e);
});
$(tagger).on('keydown', function (e) {
if (e.which === 27) {
$cancel.click();
return;
}
if (e.which === 13) {
$ok.click();
}
});
$(tagger).on('click submit', function (e) {
e.stopPropagation();
});
document.body.appendChild(tagger);
// :(
setTimeout(function () {
field.setTokens(tags);
field.focus();
});
var $field = field.tokenfield.closest('.tokenfield').find('.token-input');
$field.on('keypress', function (e) {
if (!$field.val() && e.which === 13) { return void $ok.click(); }
});
$field.on('keydown', function (e) {
if (!$field.val() && e.which === 27) { return void $cancel.click(); }
});
return tagger;
};
dialog.getButtons = function (buttons, onClose) {
if (!buttons) { return; }
if (!Array.isArray(buttons)) { return void console.error('Not an array'); }
if (!buttons.length) { return; }
var navs = [];
buttons.forEach(function (b) {
if (!b.name || !b.onClick) { return; }
var button = h('button', { tabindex: '1', 'class': b.className || '' }, [
b.iconClass ? h('i' + b.iconClass) : undefined,
b.name
]);
button.classList.add('btn');
var todo = function () {
var noClose = b.onClick();
if (noClose) { return; }
var $modal = $(button).parents('.alertify').first();
if ($modal.length && $modal[0].closeModal) {
$modal[0].closeModal(function () {
if (onClose) {
onClose();
}
});
}
};
if (b.confirm) {
UI.confirmButton(button, {
classes: 'danger',
divClasses: 'left'
}, todo);
} else {
$(button).click(function () {
todo();
});
}
if (b.keys && b.keys.length) { $(button).attr('data-keys', JSON.stringify(b.keys)); }
navs.push(button);
});
return dialog.nav(navs);
};
dialog.customModal = function (msg, opt) {
var force = false;
if (typeof(opt) === 'object') {
force = opt.force || false;
} else if (typeof(opt) === 'boolean') {
force = opt;
}
if (typeof(opt) !== 'object') {
opt = {};
}
var message;
if (typeof(msg) === 'string') {
// sanitize
if (!force) { msg = Util.fixHTML(msg); }
message = dialog.message();
message.innerHTML = msg;
} else {
message = dialog.message(msg);
}
var frame = h('div', [
message,
dialog.getButtons(opt.buttons, opt.onClose)
]);
if (opt.forefront) { $(frame).addClass('forefront'); }
return frame;
};
UI.openCustomModal = function (content, opt) {
var frame = dialog.frame([
content
], opt);
$(frame).find('button[data-keys]').each(function (i, el) {
var keys = JSON.parse($(el).attr('data-keys'));
customListenForKeys(keys, function () {
if (!$(el).is(':visible')) { return; }
$(el).click();
}, frame);
});
document.body.appendChild(frame);
$(frame).focus();
setTimeout(function () {
Notifier.notify();
});
return frame;
};
UI.createModal = function (cfg) {
var $body = cfg.$body || $('body');
var $blockContainer = cfg.id && $body.find('#'+cfg.id);
if (!$blockContainer || !$blockContainer.length) {
var id = '';
if (cfg.id) { id = '#'+cfg.id; }
$blockContainer = $(h('div.cp-modal-container'+id, {
tabindex: 1
}));
}
var deleted = false;
var hide = function () {
if (deleted) { return; }
$blockContainer.hide();
if (!cfg.id) {
deleted = true;
$blockContainer.remove();
}
if (cfg.onClose) { cfg.onClose(); }
};
$blockContainer.html('').appendTo($body);
var $block = $(h('div.cp-modal')).appendTo($blockContainer);
$(h('span.cp-modal-close.fa.fa-times', {
title: Messages.filePicker_close
})).click(hide).appendTo($block);
$body.click(hide);
$block.click(function (e) {
e.stopPropagation();
});
$body.keydown(function (e) {
if (e.which === 27) {
hide();
}
});
return {
$modal: $blockContainer,
show: function () {
$blockContainer.css('display', 'flex');
},
hide: hide
};
};
UI.alert = function (msg, cb, opt) {
var force = false;
if (typeof(opt) === 'object') {
force = opt.force || false;
} else if (typeof(opt) === 'boolean') {
force = opt;
}
if (typeof(opt) !== 'object') {
opt = {};
}
cb = cb || function () {};
var message;
if (typeof(msg) === 'string') {
// sanitize
if (!force) { msg = Util.fixHTML(msg); }
message = dialog.message();
message.innerHTML = msg;
} else {
message = dialog.message(msg);
}
var ok = dialog.okButton();
var frame = dialog.frame([
message,
dialog.nav(ok),
]);
if (opt.forefront) { $(frame).addClass('forefront'); }
var listener;
var close = Util.once(function () {
$(frame).fadeOut(150, function () { $(this).remove(); });
stopListening(listener);
cb();
});
listener = listenForKeys(close, close, frame);
var $ok = $(ok).click(close);
document.body.appendChild(frame);
setTimeout(function () {
$ok.focus();
Notifier.notify();
});
return {
element: frame,
delete: close
};
};
UI.prompt = function (msg, def, cb, opt, force) {
cb = cb || function () {};
opt = opt || {};
var inputBlock = opt.password ? UI.passwordInput() :
(opt.typeInput ? dialog.textTypeInput(opt.typeInput) : dialog.textInput());
var input = $(inputBlock).is('input') ? inputBlock : $(inputBlock).find('input')[0];
input.value = typeof(def) === 'string'? def: '';
var message;
if (typeof(msg) === 'string') {
if (!force) { msg = Util.fixHTML(msg); }
message = dialog.message();
message.innerHTML = msg;
} else {
message = dialog.message(msg);
}
var ok = dialog.okButton(opt.ok);
var cancel = dialog.cancelButton(opt.cancel);
var frame = dialog.frame([
message,
inputBlock,
dialog.nav([ cancel, ok, ]),
]);
var listener;
var close = Util.once(function (result, ev) {
var $frame = $(frame).fadeOut(150, function () {
stopListening(listener);
$frame.remove();
cb(result, ev);
});
});
var $ok = $(ok).click(function (ev) { close(input.value, ev); });
var $cancel = $(cancel).click(function (ev) { close(null, ev); });
listener = listenForKeys(function () { // yes
$ok.click();
}, function () { // no
$cancel.click();
}, input);
document.body.appendChild(frame);
setTimeout(function () {
$(input).select().focus();
Notifier.notify();
});
};
UI.confirm = function (msg, cb, opt, force) {
cb = cb || function () {};
opt = opt || {};
var message;
if (typeof(msg) === 'string') {
if (!force) { msg = Util.fixHTML(msg); }
message = dialog.message();
message.innerHTML = msg;
} else {
message = dialog.message(msg);
}
var ok = dialog.okButton(opt.ok, opt.okClass);
var cancel = dialog.cancelButton(opt.cancel, opt.cancelClass);
var frame = dialog.frame([
message,
dialog.nav(opt.reverseOrder?
[ok, cancel]: [cancel, ok]),
]);
var listener;
var close = Util.once(function (bool, ev) {
$(frame).fadeOut(150, function () { $(this).remove(); });
stopListening(listener);
cb(bool, ev);
});
var $ok = $(ok).click(function (ev) { close(true, ev); });
var $cancel = $(cancel).click(function (ev) { close(false, ev); });
listener = listenForKeys(function () {
$ok.click();
}, function () {
$cancel.click();
}, frame);
document.body.appendChild(frame);
setTimeout(function () {
Notifier.notify();
$(frame).find('.ok').focus();
if (typeof(opt.done) === 'function') {
opt.done($ok.closest('.dialog'));
}
});
};
// TODO: make it such that the confirmButton's width does not change
UI.confirmButton = function (originalBtn, config, _cb) {
config = config || {};
var cb = Util.mkAsync(_cb);
if (!config.multiple) {
cb = Util.once(cb);
}
var classes = 'btn ' + (config.classes || 'btn-primary');
var newCls = config.new ? '.new' : '';
var button = h('button', {
"class": classes,
title: config.title || ''
}, Messages.areYouSure);
var $button = $(button);
var div = h('div', {
"class": config.classes || ''
});
var timer = h('div.cp-button-timer', div);
var content = h('div.cp-button-confirm'+newCls, [
button,
timer
]);
if (config.divClasses) {
$(content).addClass(config.divClasses);
}
var to;
var done = function (res) {
if (res) { cb(res); }
clearTimeout(to);
$(content).detach();
$(originalBtn).show();
};
$button.click(function (e) {
e.stopPropagation();
done(true);
});
var TIMEOUT = 3000;
var INTERVAL = 10;
var i = 1;
var todo = function () {
var p = 100 * ((TIMEOUT - (i * INTERVAL)) / TIMEOUT);
if (i++ * INTERVAL >= TIMEOUT) {
done(false);
return;
}
$(div).css('width', p+'%');
to = setTimeout(todo, INTERVAL);
};
var newCls2 = config.new ? 'new' : '';
$(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).click(function (e) {
e.stopPropagation();
// If we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; }
i = 1;
to = setTimeout(todo, INTERVAL);
$(originalBtn).hide().after(content);
});
return {
reset: function () {
done(false);
}
};
};
UI.proposal = function (content, cb) {
var clicked = false;
var buttons = [{
name: Messages.friendRequest_later,
onClick: function () {
if (clicked) { return; }
clicked = true;
},
keys: [27]
}, {
className: 'primary',
name: Messages.friendRequest_accept,
onClick: function () {
if (clicked) { return; }
clicked = true;
cb(true);
},
keys: [13]
}, {
className: 'primary',
name: Messages.friendRequest_decline,
onClick: function () {
if (clicked) { return; }
clicked = true;
cb(false);
},
keys: [[13, 'ctrl']]
}];
var modal = dialog.customModal(content, {buttons: buttons});
UI.openCustomModal(modal);
return modal;
};
UI.log = function (msg) {
Alertify.success(Util.fixHTML(msg));
};
UI.warn = function (msg) {
Alertify.error(Util.fixHTML(msg));
};
UI.passwordInput = function (opts, displayEye) {
opts = opts || {};
var attributes = merge({
type: 'password',
autocomplete: 'new-password', // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values
}, opts);
var input = h('input.cp-password-input', attributes);
var eye = h('span.fa.fa-eye.cp-password-reveal');
var $eye = $(eye);
var $input = $(input);
if (displayEye) {
$eye.mousedown(function () {
$input.prop('type', 'text');
$input.focus();
}).mouseup(function(){
$input.prop('type', 'password');
$input.focus();
}).mouseout(function(){
$input.prop('type', 'password');
$input.focus();
});
} else {
$eye.click(function () {
if ($eye.hasClass('fa-eye')) {
$input.prop('type', 'text');
$input.focus();
$eye.removeClass('fa-eye').addClass('fa-eye-slash');
return;
}
$input.prop('type', 'password');
$input.focus();
$eye.removeClass('fa-eye-slash').addClass('fa-eye');
});
}
return h('span.cp-password-container', [
input,
eye
]);
};
UI.createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
'data-cptippy-html': true,
style: 'text-decoration: none !important;',
title: text,
href: href,
target: "_blank",
'data-tippy-placement': "right"
});
return q;
};
/*
* spinner
*/
UI.spinner = function (parent) {
var $target = $('<span>', {
'class': 'fa fa-circle-o-notch fa-spin fa-4x fa-fw',
}).hide();
$(parent).append($target);
return {
show: function () {
$target.css('display', 'inline');
return this;
},
hide: function () {
$target.hide();
return this;
},
get: function () {
return $target;
},
};
};
var LOADING = 'cp-loading';
UI.addLoadingScreen = function (config) {
config = config || {};
var loadingText = config.loadingText;
var todo = function () {
var $loading = $('#' + LOADING);
// Show the loading screen
$loading.css('display', '');
$loading.removeClass('cp-loading-hidden');
$loading.removeClass('cp-loading-transparent');
if (config.newProgress) {
var progress = h('div.cp-loading-progress', [
h('p.cp-loading-progress-list'),
h('p.cp-loading-progress-container')
]);
$loading.find('.cp-loading-spinner-container').after(progress);
}
if (!$loading.find('.cp-loading-progress').length) {
// Add spinner
$('.cp-loading-spinner-container').show();
}
// Add loading text
if (loadingText) {
$('#' + LOADING).find('#cp-loading-message').show().text(loadingText);
} else {
$('#' + LOADING).find('#cp-loading-message').hide().text('');
}
};
if ($('#' + LOADING).length) {
todo();
} else {
Loading();
todo();
}
};
UI.updateLoadingProgress = function (data) {
if (window.CryptPad_updateLoadingProgress) {
window.CryptPad_updateLoadingProgress(data);
}
};
UI.removeLoadingScreen = function (cb) {
// Release the test blocker, hopefully every test has been registered.
// This test is created in sframe-boot2.js
cb = cb || function () {};
//if (Test.__ASYNC_BLOCKER__) { Test.__ASYNC_BLOCKER__.pass(); }
var $loading = $('#' + LOADING);
$loading.addClass("cp-loading-hidden"); // Hide the loading screen
$loading.find('.cp-loading-progress').remove(); // Remove the progress list
setTimeout(cb, 750);
};
UI.errorLoadingScreen = function (error, transparent, exitable) {
if (error === 'Error: XDR encoding failure') {
console.warn(error);
return;
}
var $loading = $('#' + LOADING);
if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) {
UI.addLoadingScreen();
}
// Remove the progress list
$loading.find('.cp-loading-progress').remove();
// Hide the spinner
$('.cp-loading-spinner-container').hide();
$loading.removeClass('cp-loading-transparent');
if (transparent) { $loading.addClass('cp-loading-transparent'); }
// Add the error message
var $error = $loading.find('#cp-loading-message').show();
if (error instanceof Element) {
$error.html('').append(error);
} else {
$error.html(error || Messages.error);
}
$error.find('a[href]').click(function (e) {
e.preventDefault();
var href = $(this).prop('href');
if (!href) { return; }
if (e && e.ctrlKey) {
window.open('/bounce/#'+encodeURIComponent(href));
return;
}
window.parent.location = href;
});
if (exitable) {
$(window).focus();
$(window).keydown(function (e) {
if (e.which === 27) {
$loading.hide();
if (typeof(exitable) === "function") { exitable(); }
}
});
}
};
var $defaultIcon = $('<span>', {"class": "fa fa-file-text-o"});
UI.getIcon = function (type) {
var $icon = $defaultIcon.clone();
if (AppConfig.applicationsIcon && AppConfig.applicationsIcon[type]) {
var icon = AppConfig.applicationsIcon[type];
var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa';
if (type === 'fileupload') { type = 'file'; }
if (type === 'folderupload') { type = 'file'; }
if (type === 'link') { type = 'drive'; }
var appClass = ' cp-icon cp-icon-color-'+type;
$icon = $('<span>', {'class': font + ' ' + icon + appClass});
}
return $icon;
};
UI.getFileIcon = function (data) {
var $icon = UI.getIcon();
if (!data) { return $icon; }
var href = data.href || data.roHref;
var type = data.type;
if (data.static) { type = 'link'; }
if (!href && !type) { return $icon; }
if (!type) { type = Hash.parsePadUrl(href).type; }
$icon = UI.getIcon(type);
return $icon;
};
// Tooltips
UI.clearTooltips = function () {
// If an element is removed from the UI while a tooltip is applied on that element, the tooltip will get hung
// forever, this is a solution which just searches for tooltips which have no corrisponding element and removes
// them.
$('.tippy-popper').each(function (i, el) {
if (el._tippy && el._tippy.reference && document.body.contains(el._tippy.reference)) {
el._tippy.destroy();
el.remove();
return;
}
if ($('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) {
el.remove();
}
});
};
var delay = typeof(AppConfig.tooltipDelay) === "number" ? AppConfig.tooltipDelay : 500;
$.extend(true, Tippy.defaults, {
placement: 'bottom',
performance: true,
delay: [delay, 0],
//sticky: true,
theme: 'cryptpad',
arrow: true,
maxWidth: '200px',
flip: true,
popperOptions: {
modifiers: {
preventOverflow: { boundariesElement: 'window' }
}
},
//arrowType: 'round',
dynamicTitle: false,
arrowTransform: 'scale(2)',
zIndex: 100000001
});
UI.addTooltips = function () {
var MutationObserver = window.MutationObserver;
var addTippy = function (i, el) {
if (el._tippy) { return; }
if (!el.getAttribute('title')) { return; }
if (el.nodeName === 'IFRAME') { return; }
var opts = {
distance: 15
};
Array.prototype.slice.apply(el.attributes).filter(function (obj) {
return /^data-tippy-/.test(obj.name);
}).forEach(function (obj) {
opts[obj.name.slice(11)] = obj.value;
});
if (!el.getAttribute('data-cptippy-html') && !el.fixHTML) {
el.setAttribute('title', Util.fixHTML(el.getAttribute('title'))); // fixHTML
el.fixHTML = true; // Don't clean HTML twice on the same element
}
Tippy(el, opts);
};
// This is the robust solution to remove dangling tooltips
// The mutation observer does not always find removed nodes.
//setInterval(UI.clearTooltips, delay);
$('[title]').each(addTippy);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
for (var i = 0; i < mutation.addedNodes.length; i++) {
if ($(mutation.addedNodes[i]).attr('title')) {
addTippy(0, mutation.addedNodes[i]);
}
$(mutation.addedNodes[i]).find('[title]').each(addTippy);
}
if (mutation.removedNodes.length !== 0) {
UI.clearTooltips();
}
}
if (mutation.type === "attributes" && mutation.attributeName === "title") {
mutation.target.fixHTML = false;
addTippy(0, mutation.target);
}
});
});
observer.observe($('body')[0], {
attributes: true,
childList: true,
characterData: false,
subtree: true
});
};
UI.createCheckbox = function (id, labelTxt, checked, opts) {
opts = opts|| {};
// Input properties
var inputOpts = {
type: 'checkbox',
id: id
};
if (checked) { inputOpts.checked = 'checked'; }
$.extend(inputOpts, opts.input || {});
// Label properties
var labelOpts = {};
$.extend(labelOpts, opts.label || {});
if (labelOpts.class) { labelOpts.class += ' cp-checkmark'; }
// Mark properties
var markOpts = { tabindex: 0 };
$.extend(markOpts, opts.mark || {});
var input = h('input', inputOpts);
var $input = $(input);
var mark = h('span.cp-checkmark-mark', markOpts);
var $mark = $(mark);
var label = h('span.cp-checkmark-label', labelTxt);
$mark.keydown(function (e) {
if ($input.is(':disabled')) { return; }
if (e.which === 32) {
e.stopPropagation();
e.preventDefault();
$input.prop('checked', !$input.is(':checked'));
$input.change();
}
});
$input.change(function () {
if (!opts.labelAlt) { return; }
if ($input.is(':checked') !== checked) {
$(label).text(opts.labelAlt);
} else {
$(label).text(labelTxt);
}
});
return h('label.cp-checkmark', labelOpts, [
input,
mark,
label
]);
};
UI.createRadio = function (name, id, labelTxt, checked, opts) {
opts = opts|| {};
// Input properties
var inputOpts = {
type: 'radio',
id: id,
name: name
};
if (checked) { inputOpts.checked = 'checked'; }
$.extend(inputOpts, opts.input || {});
// Label properties
var labelOpts = {};
$.extend(labelOpts, opts.label || {});
if (labelOpts.class) { labelOpts.class += ' cp-checkmark'; }
// Mark properties
var markOpts = { tabindex: 0 };
$.extend(markOpts, opts.mark || {});
var input = h('input', inputOpts);
var $input = $(input);
var mark = h('span.cp-radio-mark', markOpts);
var label = h('span.cp-checkmark-label', labelTxt);
$(mark).keydown(function (e) {
if ($input.is(':disabled')) { return; }
if (e.which === 32) {
e.stopPropagation();
e.preventDefault();
if ($input.is(':checked')) { return; }
$input.prop('checked', !$input.is(':checked'));
$input.change();
}
});
$input.change(function () { $(mark).focus(); });
var radio = h('label', labelOpts, [
input,
mark,
label
]);
$(radio).addClass('cp-radio');
return radio;
};
var corner = {
queue: [],
state: false
};
UI.cornerPopup = function (text, actions, footer, opts) {
opts = opts || {};
var dontShowAgain = h('div.cp-corner-dontshow', [
h('span.fa.fa-times'),
Messages.dontShowAgain
]);
var footerSel = 'div.cp-corner-footer';
var popup = h('div.cp-corner-container', [
setHTML(h('div.cp-corner-text'), text),
h('div.cp-corner-actions', actions),
(typeof(footer) === 'string'?
setHTML(h(footerSel), footer):
h(footerSel, footer)),
opts.dontShowAgain ? dontShowAgain : undefined
]);
var $popup = $(popup);
if (opts.big) {
$popup.addClass('cp-corner-big');
}
if (opts.alt) {
$popup.addClass('cp-corner-alt');
}
var hide = function () {
$popup.hide();
};
var show = function () {
$popup.show();
};
var deletePopup = function () {
$popup.remove();
if (!corner.queue.length) {
// Make sure no other popup is displayed in the next 5s
setTimeout(function () {
if (corner.queue.length) {
$('body').append(corner.queue.pop());
return;
}
corner.state = false;
}, 5000);
return;
}
setTimeout(function () {
$('body').append(corner.queue.pop());
}, 5000);
};
$(dontShowAgain).click(function () {
deletePopup();
if (typeof(opts.dontShowAgain) === "function") {
opts.dontShowAgain();
}
});
if (corner.state) {
corner.queue.push(popup);
} else {
corner.state = true;
$('body').append(popup);
}
return {
popup: popup,
hide: hide,
show: show,
delete: deletePopup
};
};
UI.makeSpinner = function ($container) {
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide();
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide();
var state = false;
var to;
var spin = function () {
clearTimeout(to);
state = true;
$ok.hide();
$spinner.show();
};
var hide = function () {
clearTimeout(to);
state = false;
$ok.hide();
$spinner.hide();
};
var done = function () {
clearTimeout(to);
state = false;
$ok.show();
$spinner.hide();
to = setTimeout(function () {
$ok.hide();
}, 500);
};
if ($container && $container.append) {
$container.append($ok);
$container.append($spinner);
}
return {
getState: function () { return state; },
ok: $ok[0],
spinner: $spinner[0],
spin: spin,
hide: hide,
done: done
};
};
UI.createContextMenu = function (menu) {
var $menu = $(menu).appendTo($('body'));
var display = function (e) {
$menu.css({ display: "block" });
var h = $menu.outerHeight();
var w = $menu.outerWidth();
var wH = window.innerHeight;
var wW = window.innerWidth;
if (h > wH) {
$menu.css({
top: '0px',
bottom: ''
});
} else if (e.pageY + h <= wH) {
$menu.css({
top: e.pageY+'px',
bottom: ''
});
} else {
$menu.css({
bottom: '0px',
top: ''
});
}
if(w > wW) {
$menu.css({
left: '0px',
right: ''
});
} else if (e.pageX + w <= wW) {
$menu.css({
left: e.pageX+'px',
right: ''
});
} else {
$menu.css({
left: '',
right: '0px',
});
}
};
var hide = function () {
$menu.hide();
};
var remove = function () {
$menu.remove();
};
$('body').click(hide);
return {
menu: menu,
show: display,
hide: hide,
remove: remove
};
};
/* Given two jquery objects (a 'button' and a 'drawer')
add handlers to make it such that clicking the button
displays the drawer contents, and blurring the button
hides the drawer content. Used for toolbar buttons at the moment.
*/
UI.createDrawer = function ($button, $content) {
$button.click(function () {
var topPos = $button[0].getBoundingClientRect().bottom;
$content.toggle();
$button.removeClass('cp-toolbar-button-active');
if ($content.is(':visible')) {
$button.addClass('cp-toolbar-button-active');
$content.focus();
var wh = $(window).height();
$content.css('max-height', Math.floor(wh - topPos - 1)+'px');
}
});
var onBlur = function (e) {
if (e.relatedTarget) {
var $relatedTarget = $(e.relatedTarget);
if ($relatedTarget.is('.cp-toolbar-drawer-button')) { return; }
if ($relatedTarget.parents('.cp-toolbar-drawer-content').length) {
$relatedTarget.blur(onBlur);
return;
}
}
$button.removeClass('cp-toolbar-button-active');
$content.hide();
};
$content.blur(onBlur).appendTo($button);
$('body').keydown(function (e) {
if (e.which === 27) {
$content.blur();
}
});
};
return UI;
});