edgeconnect-cryptpad-deploy.../www/common/common-interface.js
Richard Robert Reitz addc3725ea
Some checks failed
build / build (push) Has been cancelled
first commit
2025-08-28 15:13:14 +02:00

1720 lines
56 KiB
JavaScript
Raw Permalink 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.

// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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',
'/components/alertify.js/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
'/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, content || Messages.okButton);
};
dialog.cancelButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.' + classString:'button.btn.cancel';
return h(sel, 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 dialogContent = frame.querySelector('div' + cls);
dialogContent.setAttribute('aria-live', 'assertive');
dialogContent.setAttribute('role', 'alertdialog');
dialogContent.setAttribute('aria-modal', 'true');
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));
$(title).attr('tabindex', '0');
if (tab.icon) {
var icon = h('i', {class: tab.icon, 'aria-labelledby': 'cp-tab-' + tab.title.toLowerCase()});
$(title).prepend(' ').prepend(icon);
}
Util.onClickEnter($(title), function (event) {
event.preventDefault();
event.stopPropagation();
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');
$input.attr('tabindex', 0);
var $button = $(h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', Messages.tag_add)
]));
Util.onClickEnter($button, function (e) {
$t.tokenfield('createToken', $input.val());
e.stopPropagation();
});
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));
}
$tokens.find('.close').attr('tabindex', 0).on('keydown', e => {
e.stopPropagation();
});
$tokens.find('.token-label').attr('tabindex', 0).on('keydown', function (e) {
if (e.which === 13 || e.which === 32) {
$(this).dblclick();
}
e.stopPropagation();
});
$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);
isEdit = false;
called = false;
});
};
resetUI();
const focusInput = () => {
let active = document.activeElement;
if ($.contains($container[0], active)) {
setTimeout(() => {
$input.focus();
});
}
};
$t.on('tokenfield:removedtoken', function () {
resetUI();
focusInput();
});
$t.on('tokenfield:editedtoken', function () {
resetUI();
focusInput();
});
$t.on('tokenfield:createdtoken', function () {
$input.val('');
resetUI();
focusInput();
});
$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', { '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 {
Util.onClickEnter($(button), function (e) {
e.stopPropagation();
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 cls = opt.scrollable ? '.cp-alertify-scrollable' : '';
var frame = h('div'+cls, [
message,
dialog.getButtons(opt.buttons, opt.onClose)
]);
if (opt.forefront) { $(frame).addClass('forefront'); }
return frame;
};
let addTabListener = UI.addTabListener = frame => {
$(frame).attr('role', 'dialog').attr('aria-modal', 'true');
// find focusable elements
let modalElements = $(frame).find('a, button, input, [tabindex]:not([tabindex="-1"]), textarea').filter(':visible').filter(':not(:disabled)');
if (modalElements.length === 0) {
// there are no focusable elements -> nothing to do for us here
return;
}
// intialize with focus on first element
modalElements[0].focus();
$(frame).on('keydown', function (e) {
modalElements = $(frame).find('a, button, input, [tabindex]:not([tabindex="-1"]), textarea').filter(':visible').filter(':not(:disabled)'); // for modals with dynamic content
if (e.which === 9) { // Tab
if (e.shiftKey) {
// On the first element, shift+tab goes to last
if (document.activeElement === modalElements[0]) {
e.preventDefault();
modalElements[modalElements.length - 1].focus();
}
} else {
// On the last element, tab goes to first
if (document.activeElement === modalElements[modalElements.length - 1]) {
e.preventDefault();
modalElements[0].focus();
}
}
}
});
};
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();
});
addTabListener(frame);
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');
addTabListener($blockContainer);
},
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),
], opt);
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();
});
addTabListener(frame);
return {
element: frame,
delete: close
};
};
UI.alertPromise = function (msg, opt) {
return new Promise((resolve) => {
UI.alert(msg, resolve, opt);
});
};
/**
* @callback promptCallback
* @param {string} value - the value the user chose
*/
/**
* Optional parameters for UI.prompt()
* @typedef {Object} PromptParams
* @property {boolean} [password] - if true: ask the user for a password
* @property {Element} [typeInput] - if set: add a dropdown next to the text input field (create it with UIElements.createDropdown())
* @property {Object} [inputOpts] - parameters for dialog.textInput()
* @property {string} [ok] - caption for the OK button
* @property {string} [cancel] - caption for the cancel button
*/
/**
* Show a popup to ask something.
*
* @param {(string|Element)} [msg] - Message/title to show
* @param {string} [def] - the default value
* @param {promptCallback} [cb] - called when the used selected a value
* @param {PromptParams} [opt] - optional settings for the prompt
* @param {boolean} [force] - if true: do not HTML escape msg
*/
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(opt && opt.inputOpts));
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 () {
addTabListener(frame);
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]),
], opt);
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); });
document.body.appendChild(frame);
addTabListener(frame);
listener = listenForKeys(function () {
// Only trigger OK if cancel is not focused
if (document.activeElement === $cancel[0]) {
return void $cancel.click();
}
$ok.click();
}, function () {
$cancel.click();
}, 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).on('click keydown', function (e) {
if (e.type === 'click' || (e.type === 'keydown' && e.key === 'Enter')) {
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);
$(button).focus();
}
});
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',
tabindex: '0',
autocomplete: 'one-time-code', // 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', {
tabindex: 0,
role: 'button',
'aria-label': Messages.show_password,
'aria-pressed': 'false'
});
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 {
Util.onClickEnter($eye, function (e) {
e.stopPropagation();
if ($eye.hasClass('fa-eye')) {
$input.prop('type', 'text');
$input.focus();
$eye.removeClass('fa-eye').addClass('fa-eye-slash').attr('aria-label', Messages.hide_password).attr('aria-pressed', 'true');
return;
}
$input.prop('type', 'password');
$input.focus();
$eye.removeClass('fa-eye-slash').addClass('fa-eye').attr('aria-label', Messages.show_password).attr('aria-pressed', 'false');
});
}
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",
'aria-label': text
});
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');
$loading.attr('aria-live','polite');
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();
}
$('html').toggleClass('cp-loading-noscroll', true);
// Remove the inner placeholder (iframe)
$('#placeholder').remove();
};
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);
$('head > link[href^="/customize/src/pre-loading.css"]').remove();
$('html').toggleClass('cp-loading-noscroll', false);
};
UI.emptyLoadingScreen = function (content) {
UI.addLoadingScreen();
var $loading = $('#' + LOADING);
$loading.find('.cp-loading-container').hide();
$loading.find('.cp-loading-logo').hide();
$loading.append(content);
};
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;
}
// XXX
window.parent.location = href;
});
if (exitable) {
// if true or function, ALSO add a button to leave
$(window).focus();
$(window).keydown(function (e) { // what if they don't have a keyboard?
if (e.which === 27) {
e.preventDefault();
e.stopPropagation();
// Function: call the function (should be a redirect)
if (typeof(exitable) === "function") { return void exitable(); }
// Otherwise remove the loading screen
$loading.hide();
$('html').toggleClass('cp-loading-noscroll', false);
}
});
}
};
UI.getNewIcon = function (type) {
var icon = h('i.fa.fa-file-text-o');
if (AppConfig.applicationsIcon && AppConfig.applicationsIcon[type]) {
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 = h('i', {'class': font + ' ' + icon + appClass});
}
return icon;
};
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();
}
});
};
UI.clearTooltipsDelay = function () {
setTimeout(UI.clearTooltips, 500);
};
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,
onShow: () => {
// Hide other tooltips
$('body').find('.tippy-popper').hide();
},
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.getAttribute('data-notippy')) { 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, role: 'checkbox', 'aria-checked': checked, 'aria-labelledby': inputOpts.id + '-label' };
$.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', {id: inputOpts.id + '-label'}, labelTxt);
$mark.keydown(function (e) {
if ($input.is(':disabled')) { return; }
if (e.which === 32 || e.which === 13){
e.stopPropagation();
e.preventDefault();
$input.prop('checked', !$input.is(':checked'));
$input.change();
}
});
$input.change(function () {
$mark.attr('aria-checked', $input.is(':checked'));
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 === 13 || 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
};
};
/* QR code generation is synchronous once the library is loaded
so this could be syncronous if we load the library separately. */
UI.createQRCode = function (data, _cb) {
var cb = Util.once(Util.mkAsync(_cb || Util.noop));
require(['/lib/qrcode.min.js'], function () {
var div = h('div');
/*var code =*/ new window.QRCode(div, data);
cb(void 0, div);
}, function (err) {
cb(err);
});
};
UI.getOTPScreen = function (cb, exitable, err) {
var btn, input;
var error;
if (err) {
error = h('p.cp-password-error', Messages.settings_otp_invalid);
}
var block = h('div#cp-loading-password-prompt', [
error,
h('p.cp-password-info', Messages.loading_enter_otp),
h('p.cp-password-form', [
input = h('input', {
placeholder: Messages.settings_otp_code,
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: false,
}),
btn = h('button.btn.btn-primary', Messages.ui_confirm)
]),
UI.setHTML(h('p.cp-password-recovery'), Messages.loading_recover)
]);
var $input = $(input);
var $btn = $(btn).click(function () {
var val = $input.val();
if (!val) { return void UI.getOTPScreen(cb, exitable, 'INVALID_CODE'); }
cb(val);
});
$(input).on('keydown', function (e) {
if (e.which !== 13) { return; } // enter
$btn.click();
});
UI.errorLoadingScreen(block, false, exitable);
// set the user's cursor in the OTP input field
$(block).find('.cp-password-form input').focus();
};
UI.getDestroyedPlaceholderMessage = (code, isAccount, isTemplate) => {
var account = {
ARCHIVE_OWNED: Messages.dph_account_destroyed,
INACTIVE: Messages.dph_account_inactive,
MODERATION_ACCOUNT: Messages.dph_account_moderated,
MODERATION_BLOCK: Messages.dph_account_moderated,
PASSWORD_CHANGE: Messages.dph_account_pw,
};
var template = {
ARCHIVE_OWNED: Messages.dph_tmp_destroyed,
MODERATION_PAD: Messages.dph_tmp_moderated,
MODERATION_ACCOUNT: Messages.dph_tmp_moderated_account,
PASSWORD_CHANGE: Messages.dph_tmp_pw
};
var pad = {
ARCHIVE_OWNED: Messages.dph_pad_destroyed,
INACTIVE: Messages.dph_pad_inactive,
MODERATION_PAD: Messages.dph_pad_moderated,
MODERATION_DESTROY: Messages.dph_pad_moderated,
MODERATION_ACCOUNT: Messages.dph_pad_moderated_account,
PASSWORD_CHANGE: Messages.dph_pad_pw
};
var msg = pad[code];
if (isAccount) {
msg = account[code];
} else if (isTemplate) {
msg = template[code];
}
if (!msg) { msg = Messages.dph_default; }
return msg;
};
UI.getDestroyedPlaceholder = function (reason, isAccount) {
if (typeof(reason) !== "string") { return; }
var split = reason.split(':');
var code = split[0]; // Generated code
var input = split[1]; // User/admin manual input
var text = UI.getDestroyedPlaceholderMessage(code, isAccount);
var reasonBlock = input ? h('p', Messages._getKey('dph_reason', [input])) : undefined;
return h('div', [
h('p', text),
reasonBlock
]);
};
return UI;
});