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 = $('', { '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 = $('', {"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 = $('', {'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 = $('', {'class': 'fa fa-check', title: Messages.saved}).hide(); var $spinner = $('', {'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; });