stylus/manage/manage-ui.js

337 lines
12 KiB
JavaScript
Raw Normal View History

2018-11-25 23:35:54 +00:00
/*
2018-12-01 03:56:26 +00:00
global prefs t $ $$ $create template tWordBreak
installed sorter filterAndAppend handleEvent
animateElement scrollElementIntoView formatDate
2018-11-25 23:35:54 +00:00
*/
'use strict';
const UI = {
ENTRY_ID_PREFIX_RAW: 'style-',
ENTRY_ID_PREFIX: '#style-',
TARGET_TYPES: ['domains', 'urls', 'urlPrefixes', 'regexps'],
GET_FAVICON_URL: 'https://www.google.com/s2/favicons?domain=',
OWN_ICON: chrome.runtime.getManifest().icons['16'],
favicons: prefs.get('manage.newUI.favicons'),
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
targets: prefs.get('manage.newUI.targets'),
2019-01-17 12:40:52 +00:00
injectionXref: {},
2018-11-25 23:35:54 +00:00
labels: {
'usercss': {
2018-11-29 04:21:58 +00:00
is: ({style}) => typeof style.usercssData !== 'undefined',
2018-11-25 23:35:54 +00:00
text: 'usercss'
},
'disabled': {
2018-11-29 04:21:58 +00:00
is: ({entry}) => !$('.entry-state-toggle', entry).checked,
2018-11-25 23:35:54 +00:00
text: t('genericDisabledLabel')
}
},
init: () => {
$('.ext-version').textContent = `v${chrome.runtime.getManifest().version}`;
// translate CSS manually
2019-01-02 02:10:38 +00:00
// #update-all-no-updates[data-skipped-edited="true"]::after {
// content: " ${t('updateAllCheckSucceededSomeEdited')}";
// }
2018-11-25 23:35:54 +00:00
document.head.appendChild($create('style', `
2018-12-31 05:45:14 +00:00
body.all-styles-hidden-by-filters #installed:after {
content: "${t('filteredStylesAllHidden')}";
2018-11-25 23:35:54 +00:00
}
`));
},
showStyles: (styles = [], matchUrlIds) => {
2018-12-02 06:14:59 +00:00
UI.addHeaderLabels();
2019-01-17 12:40:52 +00:00
// map injection order of styles
if (styles && styles.length && !styles.every(s => s.injectionOrder)) {
UI.injectionXref = styles
.sort((a, b) => ((a.injectionOrder || a.id) - (b.injectionOrder || b.id)))
.map((s, index) => (s.injectionOrder = index + 1))
.reduce((acc, s) => {
acc[s.id] = s.injectionOrder;
return acc;
}, {});
}
2018-11-25 23:35:54 +00:00
const sorted = sorter.sort({
styles: styles.map(style => ({
style,
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
})),
});
let index = 0;
let firstRun = true;
installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
const renderBin = document.createDocumentFragment();
if (scrollY) {
renderStyles();
} else {
requestAnimationFrame(renderStyles);
}
function renderStyles() {
const t0 = performance.now();
let rendered = 0;
while (
index < sorted.length &&
// eslint-disable-next-line no-unmodified-loop-condition
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
) {
const info = sorted[index++];
const entry = UI.createStyleElement(info);
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
entry.classList.add('not-matching');
rendered--;
}
renderBin.appendChild(entry);
}
filterAndAppend({container: renderBin}).then(sorter.updateStripes);
if (index < sorted.length) {
requestAnimationFrame(renderStyles);
if (firstRun) setTimeout(UI.getFaviconImgSrc);
firstRun = false;
return;
}
setTimeout(UI.getFaviconImgSrc);
if (sessionStorage.justEditedStyleId) {
UI.highlightEditedStyle();
} else if ('scrollY' in (history.state || {})) {
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
}
}
},
createStyleElement: ({style, name}) => {
// query the sub-elements just once, then reuse the references
2018-12-17 04:48:20 +00:00
if (!(UI._parts || {}).UI) {
2018-11-25 23:35:54 +00:00
const entry = template['style'];
UI._parts = {
2018-12-17 04:48:20 +00:00
UI: true,
2018-11-25 23:35:54 +00:00
entry,
entryClassBase: entry.className,
checker: $('.entry-state-toggle', entry) || {},
2019-01-03 04:34:29 +00:00
nameLink: $('.entry-name', entry),
2018-11-25 23:35:54 +00:00
editLink: $('.entry-edit', entry) || {},
editHrefBase: 'edit.html?id=',
appliesTo: $('.entry-applies-to', entry),
targets: $('.targets', entry),
decorations: {
urlPrefixesAfter: '*',
regexpsBefore: '/',
regexpsAfter: '/',
},
};
}
const parts = UI._parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
parts.checker.checked = style.enabled;
2019-01-03 04:34:29 +00:00
$('.entry-name-text', parts.nameLink).textContent = tWordBreak(style.name);
2018-11-25 23:35:54 +00:00
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
// clear the code to free up some memory
// (note, style is already a deep copy)
style.sourceCode = null;
style.sections.forEach(section => (section.code = null));
const entry = parts.entry.cloneNode(true);
entry.id = UI.ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : '');
2019-01-17 12:40:52 +00:00
$('.entry-id', entry).textContent = style.injectionOrder || UI.injectionXref[style.id];
2018-11-25 23:35:54 +00:00
let el = $('.entry-homepage', entry);
el.classList.toggle('invisible', !style.url);
el.href = style.url || '';
2018-12-08 14:42:29 +00:00
el.dataset.title = style.url ? `${t('externalHomepage')}: ${style.url}` : '';
2018-11-25 23:35:54 +00:00
const support = style.usercssData && style.usercssData.supportURL || '';
el = $('.entry-support', entry);
el.classList.toggle('invisible', !support);
el.href = support;
2018-12-08 14:42:29 +00:00
el.dataset.title = support ? `${t('externalSupport')}: ${support}` : '';
2018-11-25 23:35:54 +00:00
$('.entry-configure-usercss', entry).classList.toggle('invisible', !configurable);
if (style.updateUrl) {
$('.entry-updater-placeholder', entry).replaceWith(template.updaterIcons.cloneNode(true));
2018-11-25 23:35:54 +00:00
}
$('.entry-version', entry).textContent = style.usercssData && style.usercssData.version || '';
2018-12-01 03:56:26 +00:00
const lastUpdate = $('.entry-last-update', entry);
lastUpdate.textContent = UI.getDateString(style.updateDate);
// Show install & last update in title
2018-12-08 14:42:29 +00:00
lastUpdate.dataset.title = [
2018-12-01 03:56:26 +00:00
{prop: 'installDate', name: 'dateInstalled'},
{prop: 'updateDate', name: 'dateUpdated'},
].map(({prop, name}) => t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
2018-11-25 23:35:54 +00:00
UI.createStyleTargetsElement({entry, style});
2018-11-29 04:21:58 +00:00
UI.addLabels(entry);
2018-11-25 23:35:54 +00:00
return entry;
},
2018-12-01 03:56:26 +00:00
getDateString: date => {
const newDate = new Date(date);
return newDate instanceof Date && isFinite(newDate)
? newDate.toISOString().split('T')[0].replace(/-/g, '.')
: '';
},
2018-11-25 23:35:54 +00:00
createStyleTargetsElement: ({entry, style}) => {
const parts = UI._parts;
const entryTargets = $('.targets', entry);
const targets = parts.targets.cloneNode(true);
let container = targets;
let numTargets = 0;
let extraClass = '';
2018-11-25 23:35:54 +00:00
const displayed = new Set();
for (const type of UI.TARGET_TYPES) {
for (const section of style.sections) {
for (const targetValue of section[type] || []) {
if (displayed.has(targetValue)) {
continue;
}
displayed.add(targetValue);
const element = template.appliesToTarget.cloneNode(true);
if (numTargets === UI.targets) {
extraClass = ' extra';
2018-11-25 23:35:54 +00:00
}
element.dataset.type = type;
2018-12-17 04:48:20 +00:00
element.dataset.index = numTargets;
2018-12-08 14:42:29 +00:00
element.dataset.title =
2018-11-25 23:35:54 +00:00
(parts.decorations[type + 'Before'] || '') +
targetValue +
(parts.decorations[type + 'After'] || '');
element.className += extraClass;
2018-11-25 23:35:54 +00:00
container.appendChild(element);
numTargets++;
}
}
}
// Include hidden expander in case user changes UI.targets
container.appendChild(template.extraAppliesTo.cloneNode(true));
2018-12-17 04:48:20 +00:00
if (numTargets <= UI.targets) {
$('.applies-to-extra-expander', container).classList.add('hidden');
2018-11-25 23:35:54 +00:00
}
if (numTargets) {
entryTargets.parentElement.replaceChild(targets, entryTargets);
2018-12-14 13:03:49 +00:00
} else if (!entry.classList.contains('global') || !entryTargets.firstElementChild) {
2018-11-25 23:35:54 +00:00
if (entryTargets.firstElementChild) {
entryTargets.textContent = '';
}
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
}
entry.classList.toggle('global', !numTargets);
},
getFaviconImgSrc: (container = installed) => {
if (!UI.favicons) return;
const regexpRemoveNegativeLookAhead = /(\?!([^)]+\))|\(\?![\w(]+[^)]+[\w|)]+)/g;
// replace extra characters & all but the first group entry "(abc|def|ghi)xyz" => abcxyz
const regexpReplaceExtraCharacters = /[\\(]|((\|\w+)+\))/g;
const regexpMatchRegExp = /[\w-]+[.(]+(com|org|co|net|im|io|edu|gov|biz|info|de|cn|uk|nl|eu|ru)\b/g;
const regexpMatchDomain = /^.*?:\/\/([^/]+)/;
for (const target of $$('.target', container)) {
const type = target.dataset.type;
2018-12-08 14:42:29 +00:00
const targetValue = target.dataset.title;
2018-11-25 23:35:54 +00:00
if (!targetValue) continue;
let favicon = '';
if (type === 'domains') {
favicon = UI.GET_FAVICON_URL + targetValue;
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
favicon = UI.OWN_ICON;
} else if (type === 'regexps') {
favicon = targetValue
.replace(regexpRemoveNegativeLookAhead, '')
.replace(regexpReplaceExtraCharacters, '')
.match(regexpMatchRegExp);
favicon = favicon ? UI.GET_FAVICON_URL + favicon.shift() : '';
} else {
favicon = targetValue.includes('://') && targetValue.match(regexpMatchDomain);
favicon = favicon ? UI.GET_FAVICON_URL + favicon[1] : '';
}
if (favicon) {
2018-12-17 04:48:20 +00:00
const el = $('img[src], svg', target);
2018-12-14 13:03:49 +00:00
if (!el || el.localName === 'svg') {
2018-12-17 04:48:20 +00:00
const img = $('img', target);
2018-11-25 23:35:54 +00:00
img.dataset.src = favicon;
2018-12-14 13:03:49 +00:00
} else if ((target.dataset.src || target.src) !== favicon) {
2018-12-17 04:48:20 +00:00
delete el.src;
2018-12-14 13:03:49 +00:00
el.dataset.src = favicon;
2018-11-25 23:35:54 +00:00
}
}
}
handleEvent.loadFavicons();
},
highlightEditedStyle: () => {
if (!sessionStorage.justEditedStyleId) return;
const entry = $(UI.ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
delete sessionStorage.justEditedStyleId;
if (entry) {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
},
2018-12-02 06:14:59 +00:00
addHeaderLabels: () => {
const header = $('.header-name');
2018-12-08 14:42:29 +00:00
const span = document.createElement('span');
2018-12-17 04:48:20 +00:00
let labels = $('.header-labels', header);
if (labels) {
labels.textContent = '';
} else {
labels = span.cloneNode();
}
2018-12-02 06:14:59 +00:00
const label = document.createElement('a');
labels.className = 'header-labels';
2018-12-08 14:42:29 +00:00
label.className = 'header-label sortable tt-s';
2018-12-02 06:14:59 +00:00
label.href = '#';
Object.keys(UI.labels).forEach(item => {
const newLabel = label.cloneNode(true);
2018-12-03 03:06:05 +00:00
const text = UI.labels[item].text;
2018-12-02 06:14:59 +00:00
newLabel.dataset.type = item;
2018-12-03 03:06:05 +00:00
newLabel.textContent = text;
2018-12-08 14:42:29 +00:00
newLabel.appendChild(span.cloneNode());
newLabel.dataset.title = t('sortLabel', text);
2018-12-02 06:14:59 +00:00
labels.appendChild(newLabel);
});
header.appendChild(labels);
},
2018-11-29 04:21:58 +00:00
addLabels: entry => {
const style = entry.styleMeta;
2018-11-27 23:50:11 +00:00
const container = $('.entry-labels', entry);
2018-11-25 23:35:54 +00:00
const label = document.createElement('span');
2018-11-27 23:50:11 +00:00
const labels = document.createElement('span');
labels.className = 'entry-labels';
2018-12-02 06:14:59 +00:00
label.className = 'entry-label';
2018-11-25 23:35:54 +00:00
Object.keys(UI.labels).forEach(item => {
2018-11-29 04:21:58 +00:00
if (UI.labels[item].is({entry, style})) {
2018-11-25 23:35:54 +00:00
const newLabel = label.cloneNode(true);
2018-12-02 06:14:59 +00:00
newLabel.dataset.type = item;
2018-11-25 23:35:54 +00:00
newLabel.textContent = UI.labels[item].text;
2018-11-27 23:50:11 +00:00
labels.appendChild(newLabel);
2018-11-25 23:35:54 +00:00
}
});
2018-11-27 23:50:11 +00:00
container.replaceWith(labels);
2018-11-25 23:35:54 +00:00
}
};