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-ui-elements.js

900 lines
35 KiB
JavaScript

define([
'jquery',
'/api/config',
'/common/cryptpad-common.js',
'/common/common-util.js',
'/common/common-language.js',
'/common/common-interface.js',
'/common/media-tag.js',
'/common/tippy.min.js',
'/customize/application_config.js',
'css!/common/tippy.css',
], function ($, Config, Cryptpad, Util, UI, Language, MediaTag, Tippy, AppConfig) {
var UIElements = {};
var Messages = Cryptpad.Messages;
/**
* Requirements from cryptpad-common.js
* getFileSize
* - hrefToHexChannelId
* displayAvatar
* - getFirstEmojiOrCharacter
* - parsePadUrl
* - getSecrets
* - base64ToHex
* - getBlobPathFromHex
* - bytesToMegabytes
*/
UIElements.updateTags = function (common, href) {
var sframeChan = common.getSframeChannel();
sframeChan.query('Q_TAGS_GET', href || null, function (err, res) {
if (err || res.error) {
if (res.error === 'NO_ENTRY') {
UI.alert(Messages.tags_noentry);
}
return void console.error(err || res.error);
}
Cryptpad.dialog.tagPrompt(res.data, function (tags) {
if (!Array.isArray(tags)) { return; }
sframeChan.event('EV_TAGS_SET', {
tags: tags,
href: href,
});
});
});
};
UIElements.createButton = function (common, type, rightside, data, callback) {
var AppConfig = common.getAppConfig();
var button;
var size = "17px";
var sframeChan = common.getSframeChannel();
switch (type) {
case 'export':
button = $('<button>', {
'class': 'fa fa-download',
title: Messages.exportButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.exportButton));
button.click(common.prepareFeedback(type));
if (callback) {
button.click(callback);
}
break;
case 'import':
button = $('<button>', {
'class': 'fa fa-upload',
title: Messages.importButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.importButton));
if (callback) {
button
.click(common.prepareFeedback(type))
.click(Cryptpad.importContent('text/plain', function (content, file) {
callback(content, file);
}, {accept: data ? data.accept : undefined}));
}
break;
case 'upload':
button = $('<button>', {
'class': 'btn btn-primary new',
title: Messages.uploadButtonTitle,
}).append($('<span>', {'class':'fa fa-upload'})).append(' '+Messages.uploadButton);
if (!data.FM) { return; }
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;'
}).on('change', function (e) {
var file = e.target.files[0];
var ev = {
target: data.target
};
if (data.filter && !data.filter(file)) {
UI.log('Invalid avatar (type or size)');
return;
}
data.FM.handleFile(file, ev);
if (callback) { callback(); }
});
if (data.accept) { $input.attr('accept', data.accept); }
button.click(function () { $input.click(); });
break;
case 'template':
if (!AppConfig.enableTemplates) { return; }
if (!common.isLoggedIn()) { return; }
button = $('<button>', {
title: Messages.saveTemplateButton,
}).append($('<span>', {'class':'fa fa-bookmark', style: 'font:'+size+' FontAwesome'}));
if (data.rt) {
button
.click(function () {
var title = data.getTitle() || document.title;
var todo = function (val) {
if (typeof(val) !== "string") { return; }
var toSave = data.rt.getUserDoc();
if (val.trim()) {
val = val.trim();
title = val;
try {
var parsed = JSON.parse(toSave);
var meta;
if (Array.isArray(parsed) && typeof(parsed[3]) === "object") {
meta = parsed[3].metadata; // pad
} else if (parsed.info) {
meta = parsed.info; // poll
} else {
meta = parsed.metadata;
}
if (typeof(meta) === "object") {
meta.title = val;
meta.defaultTitle = val;
delete meta.users;
}
toSave = JSON.stringify(parsed);
} catch(e) {
console.error("Parse error while setting the title", e);
}
}
sframeChan.query('Q_SAVE_AS_TEMPLATE', {
title: title,
toSave: toSave
}, function () {
UI.alert(Messages.templateSaved);
common.feedback('TEMPLATE_CREATED');
});
};
UI.prompt(Messages.saveTemplatePrompt, title, todo);
});
}
break;
case 'forget':
button = $('<button>', {
id: 'cryptpad-forget',
title: Messages.forgetButtonTitle,
'class': "fa fa-trash cryptpad-forget",
style: 'font:'+size+' FontAwesome'
});
if (callback) {
button
.click(common.prepareFeedback(type))
.click(function() {
var msg = common.isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
UI.confirm(msg, function (yes) {
if (!yes) { return; }
sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) {
if (err) { return void callback(err); }
var cMsg = common.isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
UI.alert(cMsg, undefined, true);
callback();
return;
});
});
});
}
break;
case 'present':
button = $('<button>', {
title: Messages.presentButtonTitle,
'class': "fa fa-play-circle cp-app-slide-present-button", // used in slide.js
style: 'font:'+size+' FontAwesome'
});
break;
case 'history':
if (!AppConfig.enableHistory) {
button = $('<span>');
break;
}
button = $('<button>', {
title: Messages.historyButton,
'class': "fa fa-history history",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.historyText));
if (data.histConfig) {
button
.click(common.prepareFeedback(type))
.on('click', function () {
common.getHistory(data.histConfig);
});
}
break;
case 'more':
button = $('<button>', {
title: Messages.moreActions,
'class': "cp-toolbar-drawer-button fa fa-ellipsis-h",
style: 'font:'+size+' FontAwesome'
});
break;
case 'savetodrive':
button = $('<button>', {
'class': 'fa fa-cloud-upload',
title: Messages.canvas_saveToDrive,
})
.click(common.prepareFeedback(type));
break;
case 'hashtag':
button = $('<button>', {
'class': 'fa fa-hashtag',
title: Messages.tags_title,
})
.click(common.prepareFeedback(type))
.click(function () { UIElements.updateTags(common, null); });
break;
default:
button = $('<button>', {
'class': "fa fa-question",
style: 'font:'+size+' FontAwesome'
})
.click(common.prepareFeedback(type));
}
if (rightside) {
button.addClass('cp-toolbar-rightside-button');
}
return button;
};
// Avatars
UIElements.displayMediatagImage = function (Common, $tag, cb) {
if (!$tag.length || !$tag.is('media-tag')) { return void cb('NOT_MEDIATAG'); }
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
if (mutation.addedNodes.length > 1 ||
mutation.addedNodes[0].nodeName !== 'IMG') {
return void cb('NOT_IMAGE');
}
var $image = $tag.find('img');
var onLoad = function () {
var img = new Image();
img.onload = function () {
var _cb = cb;
cb = $.noop;
_cb(null, $image, img);
};
img.src = $image.attr('src');
};
if ($image[0].complete) { onLoad(); }
$image.on('load', onLoad);
}
});
});
observer.observe($tag[0], {
attributes: false,
childList: true,
characterData: false
});
MediaTag($tag[0]);
};
var emoji_patt = /([\uD800-\uDBFF][\uDC00-\uDFFF])/;
var isEmoji = function (str) {
return emoji_patt.test(str);
};
var emojiStringToArray = function (str) {
var split = str.split(emoji_patt);
var arr = [];
for (var i=0; i<split.length; i++) {
var char = split[i];
if (char !== "") {
arr.push(char);
}
}
return arr;
};
var getFirstEmojiOrCharacter = function (str) {
if (!str || !str.trim()) { return '?'; }
var emojis = emojiStringToArray(str);
return isEmoji(emojis[0])? emojis[0]: str[0];
};
UIElements.displayAvatar = function (Common, $container, href, name, cb) {
var displayDefault = function () {
var text = getFirstEmojiOrCharacter(name);
var $avatar = $('<span>', {'class': 'cp-avatar-default'}).text(text);
$container.append($avatar);
if (cb) { cb(); }
};
if (!href) { return void displayDefault(); }
var parsed = Cryptpad.parsePadUrl(href);
var secret = Cryptpad.getSecrets('file', parsed.hash);
if (secret.keys && secret.channel) {
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var hexFileName = Cryptpad.base64ToHex(secret.channel);
var src = Cryptpad.getBlobPathFromHex(hexFileName);
Common.getFileSize(href, function (e, data) {
if (e) {
displayDefault();
return void console.error(e);
}
if (typeof data !== "number") { return void displayDefault(); }
if (Cryptpad.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var $img = $('<media-tag>').appendTo($container);
$img.attr('src', src);
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
UIElements.displayMediatagImage(Common, $img, function (err, $image, img) {
if (err) { return void console.error(err); }
var w = img.width;
var h = img.height;
if (w>h) {
$image.css('max-height', '100%');
$img.css('flex-direction', 'column');
if (cb) { cb($img); }
return;
}
$image.css('max-width', '100%');
$img.css('flex-direction', 'row');
if (cb) { cb($img); }
});
});
}
};
/* Create a usage bar which keeps track of how much storage space is used
by your CryptDrive. The getPinnedUsage RPC is one of the heavier calls,
so we throttle its usage. Clients will not update more than once per
LIMIT_REFRESH_RATE. It will be update at least once every three such intervals
If changes are made to your drive in the interim, they will trigger an
update.
*/
var LIMIT_REFRESH_RATE = 30000; // milliseconds
UIElements.createUsageBar = function (common, cb) {
if (!common.isLoggedIn()) { return cb("NOT_LOGGED_IN"); }
// getPinnedUsage updates common.account.usage, and other values
// so we can just use those and only check for errors
var $container = $('<span>', {'class':'cp-limit-container'});
var todo;
var updateUsage = Cryptpad.notAgainForAnother(function () {
common.getPinUsage(todo);
}, LIMIT_REFRESH_RATE);
todo = function (err, data) {
if (err) { return void console.error(err); }
var usage = data.usage;
var limit = data.limit;
var plan = data.plan;
$container.html('');
var unit = Util.magnitudeOfBytes(limit);
7 years ago
usage = unit === 'GB'? Util.bytesToGigabytes(usage):
Util.bytesToMegabytes(usage);
7 years ago
limit = unit === 'GB'? Util.bytesToGigabytes(limit):
Util.bytesToMegabytes(limit);
var $limit = $('<span>', {'class': 'cp-limit-bar'}).appendTo($container);
var quota = usage/limit;
var $usage = $('<span>', {'class': 'cp-limit-usage'}).css('width', quota*100+'%');
var urls = common.getMetadataMgr().getPrivateData().accounts;
var makeDonateButton = function () {
$('<a>', {
'class': 'cp-limit-upgrade btn btn-success',
href: urls.donateURL,
rel: "noreferrer noopener",
target: "_blank",
}).text(Messages.supportCryptpad).appendTo($container);
};
var makeUpgradeButton = function () {
$('<a>', {
'class': 'cp-limit-upgrade btn btn-success',
href: urls.upgradeURL,
rel: "noreferrer noopener",
target: "_blank",
}).text(Messages.upgradeAccount).appendTo($container);
};
if (!Config.removeDonateButton) {
if (!common.isLoggedIn() || !Config.allowSubscriptions) {
// user is not logged in, or subscriptions are disallowed
makeDonateButton();
} else if (!plan) {
// user is logged in and subscriptions are allowed
// and they don't have one. show upgrades
makeUpgradeButton();
} else {
// they have a plan. show nothing
}
}
var prettyUsage;
var prettyLimit;
if (unit === 'GB') {
prettyUsage = Messages._getKey('formattedGB', [usage]);
prettyLimit = Messages._getKey('formattedGB', [limit]);
} else {
prettyUsage = Messages._getKey('formattedMB', [usage]);
prettyLimit = Messages._getKey('formattedMB', [limit]);
}
if (quota < 0.8) { $usage.addClass('cp-limit-usage-normal'); }
else if (quota < 1) { $usage.addClass('cp-limit-usage-warning'); }
else { $usage.addClass('cp-limit-usage-above'); }
var $text = $('<span>', {'class': 'cp-limit-usage-text'});
$text.text(usage + ' / ' + prettyLimit);
$limit.append($usage).append($text);
};
setInterval(function () {
updateUsage();
}, LIMIT_REFRESH_RATE * 3);
updateUsage();
cb(null, $container);
};
// Create a button with a dropdown menu
// input is a config object with parameters:
// - container (optional): the dropdown container (span)
// - text (optional): the button text value
// - options: array of {tag: "", attributes: {}, content: "string"}
//
// allowed options tags: ['a', 'hr', 'p']
UIElements.createDropdown = function (config) {
if (typeof config !== "object" || !Array.isArray(config.options)) { return; }
var allowedTags = ['a', 'p', 'hr'];
var isValidOption = function (o) {
if (typeof o !== "object") { return false; }
if (!o.tag || allowedTags.indexOf(o.tag) === -1) { return false; }
return true;
};
// Container
var $container = $(config.container);
var containerConfig = {
'class': 'cp-dropdown-container'
};
if (config.buttonTitle) {
containerConfig.title = config.buttonTitle;
}
if (!config.container) {
$container = $('<span>', containerConfig);
}
// Button
var $button = $('<button>', {
'class': ''
}).append($('<span>', {'class': 'cp-dropdown-button-title'}).html(config.text || ""));
/*$('<span>', {
'class': 'fa fa-caret-down',
}).appendTo($button);*/
// Menu
var $innerblock = $('<div>', {'class': 'cp-dropdown-content'});
if (config.left) { $innerblock.addClass('cp-dropdown-left'); }
config.options.forEach(function (o) {
if (!isValidOption(o)) { return; }
$('<' + o.tag + '>', o.attributes || {}).html(o.content || '').appendTo($innerblock);
});
$container.append($button).append($innerblock);
var value = config.initialValue || '';
var setActive = function ($el) {
if ($el.length !== 1) { return; }
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
$el.addClass('cp-dropdown-element-active');
var scroll = $el.position().top + $innerblock.scrollTop();
if (scroll < $innerblock.scrollTop()) {
$innerblock.scrollTop(scroll);
} else if (scroll > ($innerblock.scrollTop() + 280)) {
$innerblock.scrollTop(scroll-270);
}
};
var hide = function () {
window.setTimeout(function () { $innerblock.hide(); }, 0);
};
var show = function () {
$innerblock.show();
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
if (config.isSelect && value) {
var $val = $innerblock.find('[data-value="'+value+'"]');
setActive($val);
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop());
}
if (config.feedback) { Cryptpad.feedback(config.feedback); }
};
$container.click(function (e) {
e.stopPropagation();
var state = $innerblock.is(':visible');
$('.cp-dropdown-content').hide();
try {
$('iframe').each(function (idx, ifrw) {
$(ifrw).contents().find('.cp-dropdown-content').hide();
});
} catch (er) {
// empty try catch in case this iframe is problematic (cross-origin)
}
if (state) {
hide();
return;
}
show();
});
if (config.isSelect) {
var pressed = '';
var to;
$container.keydown(function (e) {
var $value = $innerblock.find('[data-value].cp-dropdown-element-active');
if (e.which === 38) { // Up
if ($value.length) {
var $prev = $value.prev();
setActive($prev);
}
}
if (e.which === 40) { // Down
if ($value.length) {
var $next = $value.next();
setActive($next);
}
}
if (e.which === 13) { //Enter
if ($value.length) {
$value.click();
hide();
}
}
if (e.which === 27) { // Esc
hide();
}
});
$container.keypress(function (e) {
window.clearTimeout(to);
var c = String.fromCharCode(e.which);
pressed += c;
var $value = $innerblock.find('[data-value^="'+pressed+'"]:first');
if ($value.length) {
setActive($value);
$innerblock.scrollTop($value.position().top + $innerblock.scrollTop());
}
to = window.setTimeout(function () {
pressed = '';
}, 1000);
});
$container.setValue = function (val, name) {
value = val;
var $val = $innerblock.find('[data-value="'+val+'"]');
var textValue = name || $val.html() || val;
$button.find('.cp-dropdown-button-title').html(textValue);
};
$container.getValue = function () {
return value || '';
};
}
return $container;
};
UIElements.createUserAdminMenu = function (Common, config) {
var metadataMgr = Common.getMetadataMgr();
var displayNameCls = config.displayNameCls || 'displayName';
var $displayedName = $('<span>', {'class': displayNameCls});
var accountName = metadataMgr.getPrivateData().accountName;
var origin = metadataMgr.getPrivateData().origin;
var padType = metadataMgr.getMetadata().type;
var $userName = $('<span>', {'class': 'userDisplayName'});
var options = [];
if (config.displayNameCls) {
var $userAdminContent = $('<p>');
if (accountName) {
var $userAccount = $('<span>', {'class': 'userAccount'}).append(Messages.user_accountName + ': ' + Util.fixHTML(accountName));
$userAdminContent.append($userAccount);
$userAdminContent.append($('<br>'));
}
if (config.displayName) {
// Hide "Display name:" in read only mode
$userName.append(Messages.user_displayName + ': ');
$userName.append($displayedName);
}
$userAdminContent.append($userName);
options.push({
tag: 'p',
attributes: {'class': 'cp-toolbar-account'},
content: $userAdminContent.html()
});
}
if (padType !== 'drive') {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/drive/'
},
content: Messages.login_accessDrive
});
}
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls},
content: Messages.user_rename
});
}
if (accountName) {
options.push({
tag: 'a',
attributes: {'class': 'profile'},
content: Messages.profileButton
});
}
if (padType !== 'settings') {
options.push({
tag: 'a',
attributes: {'class': 'settings'},
content: Messages.settingsButton
});
}
// Add login or logout button depending on the current status
if (accountName) {
options.push({
tag: 'a',
attributes: {'class': 'logout'},
content: Messages.logoutButton
});
} else {
options.push({
tag: 'a',
attributes: {'class': 'login'},
content: Messages.login_login
});
options.push({
tag: 'a',
attributes: {'class': 'register'},
content: Messages.login_register
});
}
var $icon = $('<span>', {'class': 'fa fa-user-secret'});
//var $userbig = $('<span>', {'class': 'big'}).append($displayedName.clone());
var $userButton = $('<div>').append($icon);//.append($userbig);
if (accountName) {
$userButton = $('<div>').append(accountName);
}
/*if (account && config.displayNameCls) {
$userbig.append($('<span>', {'class': 'account-name'}).text('(' + accountName + ')'));
} else if (account) {
// If no display name, do not display the parentheses
$userbig.append($('<span>', {'class': 'account-name'}).text(accountName));
}*/
var dropdownConfigUser = {
text: $userButton.html(), // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
container: config.$initBlock, // optional
feedback: "USER_ADMIN",
};
var $userAdmin = UIElements.createDropdown(dropdownConfigUser);
var $displayName = $userAdmin.find('.'+displayNameCls);
var $avatar = $userAdmin.find('.cp-dropdown-button-title');
var loadingAvatar;
var to;
var oldUrl = '';
var updateButton = function () {
var myData = metadataMgr.getUserData();
if (!myData) { return; }
if (loadingAvatar) {
// Try again in 200ms
window.clearTimeout(to);
to = window.setTimeout(updateButton, 200);
return;
}
loadingAvatar = true;
var newName = myData.name;
var url = myData.avatar;
$displayName.text(newName || Messages.anonymous);
if (accountName && oldUrl !== url) {
$avatar.html('');
UIElements.displayAvatar(Common, $avatar, url, newName || Messages.anonymous, function ($img) {
oldUrl = url;
if ($img) {
$userAdmin.find('button').addClass('cp-avatar');
}
loadingAvatar = false;
});
return;
}
loadingAvatar = false;
};
metadataMgr.onChange(updateButton);
updateButton();
$userAdmin.find('a.logout').click(function () {
Common.logout(function () {
window.parent.location = origin+'/';
});
});
$userAdmin.find('a.settings').click(function () {
if (padType) {
window.open(origin+'/settings/');
} else {
window.parent.location = origin+'/settings/';
}
});
$userAdmin.find('a.profile').click(function () {
if (padType) {
window.open(origin+'/profile/');
} else {
window.parent.location = origin+'/profile/';
}
});
$userAdmin.find('a.login').click(function () {
Common.setLoginRedirect(function () {
window.parent.location = origin+'/login/';
});
});
$userAdmin.find('a.register').click(function () {
Common.setLoginRedirect(function () {
window.parent.location = origin+'/register/';
});
});
return $userAdmin;
};
// Provide $container if you want to put the generated block in another element
// Provide $initBlock if you already have the menu block and you want the content inserted in it
UIElements.createLanguageSelector = function (common, $container, $initBlock) {
var options = [];
var languages = Messages._languages;
var keys = Object.keys(languages).sort();
keys.forEach(function (l) {
options.push({
tag: 'a',
attributes: {
'class': 'cp-language-value',
'data-value': l,
'href': '#',
},
content: languages[l] // Pretty name of the language value
});
});
var dropdownConfig = {
text: Messages.language, // Button initial text
options: options, // Entries displayed in the menu
//left: true, // Open to the left of the button
container: $initBlock, // optional
isSelect: true
};
var $block = UIElements.createDropdown(dropdownConfig);
$block.attr('id', 'cp-language-selector');
if ($container) {
$block.appendTo($container);
}
Language.initSelector($block, common);
return $block;
};
UIElements.initFilePicker = function (common, cfg) {
var onSelect = cfg.onSelect || $.noop;
var sframeChan = common.getSframeChannel();
sframeChan.on("EV_FILE_PICKED", function (data) {
onSelect(data);
});
};
UIElements.openFilePicker = function (common, types) {
var sframeChan = common.getSframeChannel();
sframeChan.event("EV_FILE_PICKER_OPEN", types);
};
UIElements.openTemplatePicker = function (common) {
var metadataMgr = common.getMetadataMgr();
var type = metadataMgr.getMetadataLazy().type;
var sframeChan = common.getSframeChannel();
var focus;
var pickerCfg = {
types: [type],
where: ['template'],
hidden: true
};
var onConfirm = function (yes) {
if (!yes) {
if (focus) { focus.focus(); }
return;
}
delete pickerCfg.hidden;
common.openFilePicker(pickerCfg);
var first = true; // We can only pick a template once (for a new document)
var fileDialogCfg = {
onSelect: function (data) {
if (data.type === type && first) {
Cryptpad.addLoadingScreen({hideTips: true});
sframeChan.query('Q_TEMPLATE_USE', data.href, function () {
first = false;
Cryptpad.removeLoadingScreen();
common.feedback('TEMPLATE_USED');
});
if (focus) { focus.focus(); }
return;
}
}
};
common.initFilePicker(fileDialogCfg);
};
sframeChan.query("Q_TEMPLATE_EXIST", type, function (err, data) {
if (data) {
common.openFilePicker(pickerCfg);
focus = document.activeElement;
UI.confirm(Messages.useTemplate, onConfirm, {
ok: Messages.useTemplateOK,
cancel: Messages.useTemplateCancel,
});
}
});
};
UIElements.addTooltips = function () {
var MutationObserver = window.MutationObserver;
var delay = typeof(AppConfig.tooltipDelay) === "number" ? AppConfig.tooltipDelay : 500;
var addTippy = function (i, el) {
if (el.nodeName === 'IFRAME') { return; }
Tippy(el, {
position: 'bottom',
distance: 0,
performance: true,
dynamicTitle: true,
delay: [delay, 0]
});
};
var clearTooltips = function () {
$('.tippy-popper').each(function (i, el) {
if ($('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) {
el.remove();
}
});
};
// This is the robust solution to remove dangling tooltips
// The mutation observer does not always find removed nodes.
setInterval(clearTooltips, delay);
var checkRemoved = function (x) {
var out = false;
$(x).find('[aria-describedby]').each(function (i, el) {
var id = el.getAttribute('aria-describedby');
if (id.indexOf('tippy-tooltip-') !== 0) { return; }
out = true;
});
return out;
};
$('[title]').each(addTippy);
var observer = new MutationObserver(function(mutations) {
var removed = false;
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
$(mutation.addedNodes[i]).find('[title]').each(addTippy);
}
for (var j = 0; j < mutation.removedNodes.length; j++) {
removed |= checkRemoved(mutation.removedNodes[j]);
}
});
if (removed) { clearTooltips(); }
});
observer.observe($('body')[0], {
attributes: false,
childList: true,
characterData: false,
subtree: true
});
};
return UIElements;
});