stylus/manage/manage-actions.js
2020-03-10 19:33:13 -05:00

503 lines
14 KiB
JavaScript

/*
global messageBox getStyleWithNoCode
filterAndAppend urlFilterParam showFiltersStats
checkUpdate handleUpdateInstalled
objectDiff
configDialog
sorter msg prefs API onDOMready $ $$ setupLivePrefs
URLS enforceInputRange t
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView FIREFOX
UI bulk
*/
'use strict';
let installed;
const handleEvent = {};
Promise.all([
API.getAllStyles(true),
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
Promise.all([
onDOMready(),
prefs.initializing,
]).then(() => {
initGlobalEvents();
if (FIREFOX && 'update' in (chrome.commands || {})) {
const btn = $('#manage-shortcuts-button');
btn.classList.remove('chromium-only');
btn.onclick = API.optionsCustomizeHotkeys;
}
}),
]).then(args => {
UI.init();
UI.showStyles(...args);
lazyLoad();
});
msg.onExtension(onRuntimeMessage);
function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleUpdated':
case 'styleAdded':
API.getStyle(msg.style.id, true).then(style => handleUpdate(style, msg));
break;
case 'styleDeleted':
handleDelete(msg.style.id);
break;
case 'styleApply':
case 'styleReplaceAll':
break;
default:
return;
}
setTimeout(() => {
sorter.updateStripes({onlyWhenColumnsChanged: true});
}, 0);
}
function initGlobalEvents() {
installed = $('#installed');
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = event => {
event.preventDefault();
chrome.runtime.openOptionsPage();
};
$('#manage-shortcuts-button').onclick = event => {
event.preventDefault();
openURL({url: URLS.configureCommands});
};
$('#update-all').onclick = event => {
event.preventDefault();
handleEvent.toggleBulkActions({hidden: false});
bulk.updateAll();
}
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
document.addEventListener('visibilitychange', onVisibilityChange);
document.addEventListener('keydown', event => {
if (event.which === 27) {
// close all open "applies-to" details
$$('.applies-to-extra[open]').forEach(el => {
el.removeAttribute('open');
});
// Close bulk actions
handleEvent.toggleBulkActions({hidden: true});
} else if (event.which === 32 && event.target.classList.contains('checkmate')) {
// pressing space toggles the containing checkbox
$('input[type="checkbox"]', event.target).click();
}
});
$$('[data-toggle-on-click]').forEach(el => {
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
const target = $(el.getAttribute('data-toggle-on-click'));
el.onclick = event => {
event.preventDefault();
target.classList.toggle('hidden');
if (target.classList.contains('hidden')) {
el.removeAttribute('open');
} else {
el.setAttribute('open', '');
}
};
});
// triggered automatically by setupLivePrefs() below
enforceInputRange($('#manage.newUI.targets'));
// N.B. triggers existing onchange listeners
setupLivePrefs();
bulk.init();
sorter.init();
prefs.subscribe([
'manage.newUI.favicons',
'manage.newUI.faviconsGray',
'manage.newUI.targets',
], () => switchUI());
switchUI({styleOnly: true});
}
Object.assign(handleEvent, {
ENTRY_ROUTES: {
'.entry-state-toggle': 'toggle',
'.entry-style-name': 'name',
'.entry-homepage': 'external',
'.entry-support': 'external',
'.check-update': 'check',
'.update': 'update',
'.entry-delete': 'delete',
'.entry-configure-usercss': 'config',
'.header-filter': 'toggleBulkActions',
'.sortable': 'updateSort',
'#applies-to-config': 'appliesConfig',
'.applies-to-extra-expander': 'toggleExtraAppliesTo'
},
entryClicked(event) {
const target = event.target;
const entry = target.closest('.entry');
for (const selector in handleEvent.ENTRY_ROUTES) {
for (let el = target; el && el !== entry; el = el.parentElement) {
if (el.matches(selector)) {
const handler = handleEvent.ENTRY_ROUTES[selector];
return handleEvent[handler].call(el, event, entry);
}
}
}
},
name(event) {
handleEvent.edit(event);
},
edit(event) {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
const left = event.button === 0;
const middle = event.button === 1;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = $('[href]', event.target.closest('.entry')).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
} else {
getOwnTab().then(({index}) => {
openURL({
url,
index: index + 1,
active: openForegroundTab
});
});
}
} else {
onVisibilityChange();
getActiveTab().then(tab => {
sessionStorageHash('manageStylesHistory').set(tab.id, url);
location.href = url;
});
}
},
toggle(event, entry) {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
UI.addLabels(entry);
},
toggleBulkActions({hidden}) {
const tools = $('#tools-wrapper');
tools.classList.toggle('hidden', hidden);
$('.header-filter').classList.toggle('active', !tools.classList.contains('hidden'));
},
toggleExtraAppliesTo(event, entry) {
event.preventDefault();
entry.classList.toggle('hide-extra');
if (event.shiftKey) {
const state = entry.classList.contains('hide-extra');
$$('.entry').forEach(entry => entry.classList.toggle('hide-extra', state));
}
},
check(event, entry) {
event.preventDefault();
checkUpdate(entry, {single: true});
},
update(event, entry) {
event.preventDefault();
const json = entry.updatedCode;
json.id = entry.styleId;
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
},
updateSort(event) {
event.preventDefault();
sorter.updateSort(event);
removeSelection();
},
delete(event, entry) {
event.preventDefault();
const id = entry.styleId;
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
.then(({button}) => {
if (button === 0) {
API.deleteStyle(id);
}
});
},
external(event) {
if (event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
// Shift-click = the built-in 'open in a new window' action
return;
}
getOwnTab().then(({index}) => {
openURL({
url: event.target.closest('a').href,
index: index + 1,
active: !event.ctrlKey || event.shiftKey,
});
});
event.preventDefault();
},
loadFavicons({all = false} = {}) {
if (!installed.firstElementChild) return;
let favicons = [];
if (all) {
favicons = $$('img[data-src]', installed);
} else {
const {left, top} = installed.firstElementChild.getBoundingClientRect();
const x = Math.max(0, left);
const y = Math.max(0, top);
const first = document.elementFromPoint(x, y);
const lastOffset = first.offsetTop + window.innerHeight;
const numTargets = prefs.get('manage.newUI.targets');
let entry = first && first.closest('.entry') || installed.children[0];
while (entry && entry.offsetTop <= lastOffset) {
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
entry = entry.nextElementSibling;
}
}
let i = 0;
for (const img of favicons) {
img.src = img.dataset.src;
delete img.dataset.src;
// loading too many icons at once will block the page while the new layout is recalculated
if (++i > 100) break;
}
if ($('img[data-src]', installed)) {
debounce(handleEvent.loadFavicons, 1, {all: true});
}
},
config(event, {styleMeta}) {
event.preventDefault();
configDialog(styleMeta);
},
appliesConfig() {
messageBox({
title: t('configureStyle'),
className: 'config-dialog',
contents: [
$('#appliesToConfig').cloneNode(true)
],
buttons: [{
textContent: t('confirmClose'),
dataset: {cmd: 'close'},
}],
onshow: box => {
box.addEventListener('change', handleEvent.manageFavicons);
box.addEventListener('input', handleEvent.manageFavicons);
$$('input', box).forEach(el => {
el.dataset.id = el.id;
el.id = null;
});
}
}).then(() => {
const box = $('#message-box');
box.removeEventListener('change', handleEvent.manageFavicons);
box.removeEventListener('input', handleEvent.manageFavicons);
});
},
manageFavicons(event) {
event.stopPropagation();
const box = $('#message-box-contents');
let value = $('[data-id="manage.newUI.favicons"]', box).checked;
prefs.set('manage.newUI.favicons', value);
// Updating the hidden inputs; not the inputs in the message box
$('#manage.newUI.favicons').checked = value;
value = $('[data-id="manage.newUI.faviconsGray"]', box).checked;
prefs.set('manage.newUI.faviconsGray', value);
$('#manage.newUI.faviconsGray').checked = value;
value = $('[data-id="manage.newUI.targets"]', box).value;
prefs.set('manage.newUI.targets', value);
},
});
function handleUpdate(style, {reason, method} = {}) {
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
let entry;
let oldEntry = $(UI.ENTRY_ID_PREFIX + style.id);
if (oldEntry && method === 'styleUpdated') {
handleToggledOrCodeOnly();
}
entry = entry || UI.createStyleElement({style});
if (oldEntry) {
// Make sure to update the filter checkbox since it's state isn't saved to the style
$('.entry-filter-toggle', entry).checked = $('.entry-filter-toggle', oldEntry).checked;
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
installed.replaceChild(entry, oldEntry);
} else {
oldEntry.remove();
}
}
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
handleUpdateInstalled(entry, reason);
}
filterAndAppend({entry}).then(sorter.update);
if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
UI.getFaviconImgSrc(entry);
function handleToggledOrCodeOnly() {
const newStyleMeta = getStyleWithNoCode(style);
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta)
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
if (diff.length === 0) {
// only code was modified
entry = oldEntry;
oldEntry = null;
}
if (diff.length === 1 && diff[0].key === 'enabled') {
oldEntry.classList.toggle('enabled', style.enabled);
oldEntry.classList.toggle('disabled', !style.enabled);
$$('.entry-state-toggle', oldEntry).forEach(el => (el.checked = style.enabled));
oldEntry.styleMeta = newStyleMeta;
entry = oldEntry;
UI.addLabels(entry);
oldEntry = null;
}
}
}
function handleDelete(id) {
const node = $(UI.ENTRY_ID_PREFIX + id);
if (node) {
node.remove();
if (node.matches('.can-update')) {
const btnApply = $('#apply-all-updates');
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
}
showFiltersStats();
}
}
function switchUI({styleOnly} = {}) {
const current = {enabled: true};
const changed = {};
let someChanged = false;
// ensure the global option is processed first
for (const el of $$('[id^="manage.newUI."]')) {
const id = el.id.replace(/^manage\.newUI\.?/, '');
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
const valueChanged = value !== UI[id];
current[id] = value;
changed[id] = valueChanged;
someChanged |= valueChanged;
}
if (!styleOnly && !someChanged) {
return;
}
Object.assign(UI, current);
installed.classList.toggle('has-favicons', UI.favicons);
installed.classList.toggle('faviconsGray', UI.faviconsGray);
if (styleOnly) {
return;
}
const missingFavicons = UI.favicons && !$('.entry-applies-to img[src]');
if (changed.targets) {
for (const targetWrapper of $$('.entry .targets')) {
const targets = $$('.target', targetWrapper);
targets.forEach((target, indx) => {
target.classList.toggle('extra', indx >= UI.targets);
});
$('.applies-to-extra-expander', targetWrapper)
.classList.toggle('hidden', targets.length <= UI.targets);
}
return;
}
if (missingFavicons) {
debounce(UI.getFaviconImgSrc);
return;
}
}
function onVisibilityChange() {
switch (document.visibilityState) {
// page restored without reloading via history navigation (currently only in FF)
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
// assuming other changes aren't important enough to justify making a complicated DOM sync
case 'visible':
if (sessionStorage.justEditedStyleId) {
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
.then(style => {
handleUpdate(style, {method: 'styleUpdated'});
});
delete sessionStorage.justEditedStyleId;
}
break;
// going away
case 'hidden':
history.replaceState({scrollY: window.scrollY}, document.title);
break;
}
}
function removeSelection() {
const sel = window.getSelection ? window.getSelection() : document.selection;
if (sel) {
if (sel.removeAllRanges) {
sel.removeAllRanges();
} else if (sel.empty) {
sel.empty();
}
}
}
function lazyLoad() {
setTimeout(() => {
$$('link[data-href]').forEach(link => {
link.href = link.dataset.href;
link.removeAttribute('data-href');
});
$$('script[data-src]').forEach(script => {
script.src = script.dataset.src;
script.removeAttribute('data-src');
});
}, 500);
}