You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/common-interface.js

1458 lines
45 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

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',
'/common/tippy/tippy.min.js',
'/common/hyperscript.js',
'/customize/loading.js',
'/common/test.js',
'/common/jquery-ui/jquery-ui.min.js',
'/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js',
'css!/common/tippy/tippy.css',
'css!/common/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;
};
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.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'));
}
});
};
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 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', [
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);
};
$(originalBtn).addClass('cp-button-confirm-placeholder').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'
}, 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);
}
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'; }
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 (!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 (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 mark = h('span.cp-radio-mark', markOpts);
var label = h('span.cp-checkmark-label', labelTxt);
$(mark).keydown(function (e) {
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 popup = h('div.cp-corner-container', [
setHTML(h('div.cp-corner-text'), text),
h('div.cp-corner-actions', actions),
setHTML(h('div.cp-corner-footer'), footer),
opts.dontShowAgain ? dontShowAgain : undefined
]);
var $popup = $(popup);
if (opts.hidden) {
$popup.addClass('cp-minimized');
}
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 () {
$content.toggle();
$button.removeClass('cp-toolbar-button-active');
if ($content.is(':visible')) {
$button.addClass('cp-toolbar-button-active');
$content.focus();
var wh = $(window).height();
var topPos = $button[0].getBoundingClientRect().bottom;
$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;
});