separate storage for order + important styles

This commit is contained in:
tophf 2022-01-23 23:46:43 +03:00
parent da0918f49a
commit 8fb09f1a03
9 changed files with 261 additions and 134 deletions

View File

@ -1697,10 +1697,14 @@
"message": "Style injection order", "message": "Style injection order",
"description": "Tooltip for the button in the manager to open the dialog and also the title of this dialog" "description": "Tooltip for the button in the manager to open the dialog and also the title of this dialog"
}, },
"styleInjectionOrderHint": { "styleInjectionOrderHint_main": {
"message": "Drag'n'drop a style to change its position. Styles are injected sequentially in the order shown below so a style further down the list can override the earlier styles.", "message": "Drag'n'drop a style to change its position. Styles are injected sequentially in the order shown below so a style further down the list can override the earlier styles.",
"description": "Hint in the injection order dialog in the manager" "description": "Hint in the injection order dialog in the manager"
}, },
"styleInjectionOrderHint_prio": {
"message": "Important styles are listed below and are always injected last so they can override any newly installed styles. Click the style's mark to toggle its importance.",
"description": "Hint at the bottom of the injection order dialog in the manager"
},
"styleExcludeLabel": { "styleExcludeLabel": {
"message": "Custom excluded sites" "message": "Custom excluded sites"
}, },

View File

@ -9,8 +9,10 @@
addAPI addAPI
bgReady bgReady
createCache createCache
uuidIndex
*/ */
const uuidIndex = new Map();
const bgReady = {}; const bgReady = {};
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r)); bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
bgReady.all = new Promise(r => (bgReady._resolveAll = r)); bgReady.all = new Promise(r => (bgReady._resolveAll = r));

View File

@ -1,6 +1,6 @@
/* global API msg */// msg.js /* global API msg */// msg.js
/* global CHROME URLS isEmptyObj stringAsRegExp tryRegExp tryURL */// toolbox.js /* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global bgReady createCache */// common.js /* global bgReady createCache uuidIndex */// common.js
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js /* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
/* global db */ /* global db */
/* global prefs */ /* global prefs */
@ -20,6 +20,7 @@ to cleanup the temporary code. See livePreview in /edit.
const styleUtil = {}; const styleUtil = {};
/* exported styleMan */
const styleMan = (() => { const styleMan = (() => {
Object.assign(styleUtil, { Object.assign(styleUtil, {
@ -30,9 +31,9 @@ const styleMan = (() => {
//#region Declarations //#region Declarations
/** @typedef {{ /** @typedef {{
style: StyleObj style: StyleObj,
preview?: StyleObj preview?: StyleObj,
appliesTo: Set<string> appliesTo: Set<string>,
}} StyleMapData */ }} StyleMapData */
/** @type {Map<number,StyleMapData>} */ /** @type {Map<number,StyleMapData>} */
const dataMap = new Map(); const dataMap = new Map();
@ -63,9 +64,20 @@ const styleMan = (() => {
_rev: () => Date.now(), _rev: () => Date.now(),
}; };
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5']; const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
const INJ_ORDER = 'injectionOrder';
const order = {main: {}, prio: {}};
const orderForDb = {
id: INJ_ORDER,
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
get value() {
return mapObj(order, group => sortObjectKeysByValue(group, id2uuid));
},
set value(val) {
setOrderFromArray(val);
},
};
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */ /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = init(); let ready = init();
let order = {};
chrome.runtime.onConnect.addListener(port => { chrome.runtime.onConnect.addListener(port => {
if (port.name === 'livePreview') { if (port.name === 'livePreview') {
@ -83,18 +95,6 @@ const styleMan = (() => {
} }
}); });
// TODO: will fix in subsequent commit
// prefs.subscribe(['injectionOrder'], (key, value) => {
// order = {};
// value.forEach((uid, i) => {
// const id = uuidIndex.get(uid);
// if (id) {
// order[id] = i;
// }
// });
// msg.broadcast({method: 'styleSort', order});
// });
//#endregion //#endregion
//#region Exports //#region Exports
@ -103,17 +103,17 @@ const styleMan = (() => {
/** @returns {Promise<number>} style id */ /** @returns {Promise<number>} style id */
async delete(id, reason) { async delete(id, reason) {
if (ready.then) await ready; if (ready.then) await ready;
const data = id2data(id); const {style, appliesTo} = dataMap.get(id);
const {style, appliesTo} = data; const sync = reason !== 'sync';
db.styles.delete(id); db.styles.delete(id);
if (reason !== 'sync') { if (sync) API.sync.delete(style._id, Date.now());
API.sync.delete(style._id, Date.now());
}
for (const url of appliesTo) { for (const url of appliesTo) {
const cache = cachedStyleForUrl.get(url); const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id]; if (cache) delete cache.sections[id];
} }
dataMap.delete(id); dataMap.delete(id);
mapObj(order, val => delete val[id]);
setOrder(orderForDb.value, {sync});
if (style._usw && style._usw.token) { if (style._usw && style._usw.token) {
// Must be called after the style is deleted from dataMap // Must be called after the style is deleted from dataMap
API.usw.revoke(id); API.usw.revoke(id);
@ -149,7 +149,21 @@ const styleMan = (() => {
/** @returns {Promise<StyleObj[]>} */ /** @returns {Promise<StyleObj[]>} */
async getAll() { async getAll() {
if (ready.then) await ready; if (ready.then) await ready;
return Array.from(dataMap.values(), data2style); return getAllAsArray();
},
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
async getAllOrdered() {
if (ready.then) await ready;
const res = mapObj(order, group => sortObjectKeysByValue(group, id2style));
if (res.main.length + res.prio.length < dataMap.size) {
for (const {style} of dataMap.values()) {
if (!(style.id in order.main) && !(style.id in order.prio)) {
res.main.push(style);
}
}
}
return res;
}, },
/** @returns {Promise<StyleSectionsToApply>} */ /** @returns {Promise<StyleSectionsToApply>} */
@ -199,7 +213,7 @@ const styleMan = (() => {
const result = []; const result = [];
const styles = id const styles = id
? [id2style(id)].filter(Boolean) ? [id2style(id)].filter(Boolean)
: Array.from(dataMap.values(), data2style); : getAllAsArray();
const query = createMatchQuery(url); const query = createMatchQuery(url);
for (const style of styles) { for (const style of styles) {
let excluded = false; let excluded = false;
@ -269,6 +283,13 @@ const styleMan = (() => {
save: saveStyle, save: saveStyle,
async setOrder(val) {
if (ready.then) await ready;
return val &&
!deepEqual(val, order) &&
setOrder(val, {broadcast: true, sync: true});
},
/** @returns {Promise<number>} style id */ /** @returns {Promise<number>} style id */
async toggle(id, enabled) { async toggle(id, enabled) {
if (ready.then) await ready; if (ready.then) await ready;
@ -306,12 +327,12 @@ const styleMan = (() => {
/** @returns {?StyleObj} */ /** @returns {?StyleObj} */
function id2style(id) { function id2style(id) {
return (dataMap.get(id) || {}).style; return (dataMap.get(Number(id)) || {}).style;
} }
/** @returns {?StyleObj} */ /** @returns {?string} */
function data2style(data) { function id2uuid(id) {
return data && data.style; return (id2style(id) || {})._id;
} }
/** @returns {StyleObj} */ /** @returns {StyleObj} */
@ -332,6 +353,7 @@ const styleMan = (() => {
style, style,
appliesTo: new Set(), appliesTo: new Set(),
}); });
uuidIndex.set(style._id, style.id);
} }
/** @returns {StyleObj} */ /** @returns {StyleObj} */
@ -451,7 +473,7 @@ const styleMan = (() => {
data.style = style; data.style = style;
} }
if (reason !== 'sync') { if (reason !== 'sync') {
API.sync.putStyle(style); API.sync.putDoc(style);
} }
if (broadcast) broadcastStyleUpdated(style, reason, method); if (broadcast) broadcastStyleUpdated(style, reason, method);
return style; return style;
@ -477,12 +499,18 @@ const styleMan = (() => {
} }
async function init() { async function init() {
const orderPromise = db.open(prefs.STORAGE_KEY).get(INJ_ORDER);
const styles = await db.styles.getAll() || []; const styles = await db.styles.getAll() || [];
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean)); const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
if (updated.length) { if (updated.length) {
await db.styles.putMany(updated); await db.styles.putMany(updated);
} }
styles.forEach(storeInMap); styles.forEach(storeInMap);
Object.assign(orderForDb, await orderPromise);
API.sync.registerDoc(orderForDb, doc => {
Object.assign(orderForDb, doc);
setOrder();
});
ready = true; ready = true;
bgReady._resolveStyles(); bgReady._resolveStyles();
} }
@ -680,10 +708,40 @@ const styleMan = (() => {
} }
} }
/** @returns {StyleObj[]} */
function getAllAsArray() {
return Array.from(dataMap.values(), v => v.style);
}
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */ /** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
function hex4dashed(num, i) { function hex4dashed(num, i) {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : ''); return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
} }
async function setOrder(val, {broadcast, store = true, sync} = {}) {
if (val) {
setOrderFromArray(val);
orderForDb._rev = Date.now();
}
if (broadcast) msg.broadcast({method: 'styleSort', order});
if (store) await db.open(prefs.STORAGE_KEY).put(orderForDb);
if (sync) API.sync.putDoc(orderForDb);
}
function setOrderFromArray(newOrder) {
mapObj(order, (_, type) => {
const res = order[type] = {};
(newOrder && newOrder[type] || []).forEach((uid, i) => {
const id = uuidIndex.get(uid);
if (id) res[id] = i;
});
});
}
/** Since JS object's numeric keys are sorted in ascending order, we have to re-sort by value */
function sortObjectKeysByValue(obj, map) {
return Object.entries(obj).sort((a, b) => a[1] - b[1]).map(e => map(e[0]));
}
//#endregion //#endregion
})(); })();

View File

@ -1,4 +1,5 @@
/* global API msg */// msg.js /* global API msg */// msg.js
/* global uuidIndex */// common.js
/* global chromeLocal chromeSync */// storage-util.js /* global chromeLocal chromeSync */// storage-util.js
/* global db */ /* global db */
/* global iconMan */ /* global iconMan */
@ -29,17 +30,13 @@ const syncMan = (() => {
errorMessage: null, errorMessage: null,
login: false, login: false,
}; };
const uuidIndex = new Map(); const customDocs = {};
const uuid2style = uuid => styleUtil.id2style(uuidIndex.get(uuid));
const compareRevision = (rev1, rev2) => rev1 - rev2; const compareRevision = (rev1, rev2) => rev1 - rev2;
let lastError = null; let lastError = null;
let ctrl; let ctrl;
let currentDrive; let currentDrive;
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */ /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = prefs.ready.then(async () => { let ready = prefs.ready.then(async () => {
for (const {id, _id} of await API.styles.getAll()) {
uuidIndex.set(_id, id);
}
ready = true; ready = true;
prefs.subscribe('sync.enabled', prefs.subscribe('sync.enabled',
(_, val) => val === 'none' (_, val) => val === 'none'
@ -87,16 +84,20 @@ const syncMan = (() => {
} }
}, },
async put(...args) { async putDoc({id, _id, _rev}) {
if (ready.then) await ready; if (ready.then) await ready;
uuidIndex.set(_id, id);
if (!currentDrive) return; if (!currentDrive) return;
schedule(); schedule();
return ctrl.put(...args); return ctrl.put(_id, _rev);
}, },
putStyle({id, _id, _rev}) { registerDoc(doc, setter) {
uuidIndex.set(_id, id); uuidIndex.set(doc._id, doc.id);
return syncMan.put(_id, _rev); Object.defineProperty(customDocs, doc.id, {
get: () => doc,
set: setter,
});
}, },
async setDriveOptions(driveName, options) { async setDriveOptions(driveName, options) {
@ -191,10 +192,13 @@ const syncMan = (() => {
async function initController() { async function initController() {
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */ await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
ctrl = dbToCloud.dbToCloud({ ctrl = dbToCloud.dbToCloud({
onGet: uuid2style, onGet(uuid) {
return styleUtil.id2style(uuidIndex.get(uuid));
},
async onPut(doc) { async onPut(doc) {
const id = uuidIndex.get(doc._id); const id = uuidIndex.get(doc._id);
const oldDoc = uuid2style(doc._id); const style = styleUtil.id2style(id);
const oldDoc = style || customDocs[id];
if (id) { if (id) {
doc.id = id; doc.id = id;
} else { } else {
@ -204,27 +208,30 @@ const syncMan = (() => {
if (oldDoc) { if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev); diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) { if (diff > 0) {
syncMan.put(oldDoc); syncMan.putDoc(oldDoc);
return; return;
} }
} }
if (diff < 0) { if (diff >= 0) {
return;
}
if (style) {
doc.id = await db.styles.put(doc); doc.id = await db.styles.put(doc);
uuidIndex.set(doc._id, doc.id); uuidIndex.set(doc._id, doc.id);
return styleUtil.handleSave(doc, {reason: 'sync'}); return styleUtil.handleSave(doc, {reason: 'sync'});
} }
if (oldDoc) oldDoc[id] = doc;
}, },
onDelete(_id, rev) { onDelete(_id, rev) {
const id = uuidIndex.get(_id); const id = uuidIndex.get(_id);
const oldDoc = uuid2style(_id); const oldDoc = styleUtil.id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) { if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
uuidIndex.delete(id); uuidIndex.delete(id);
return API.styles.delete(id, 'sync'); return API.styles.delete(id, 'sync');
} }
}, },
async onFirstSync() { async onFirstSync() {
for (const i of await API.styles.getAll()) { for (const i of Object.values(customDocs).concat(await API.styles.getAll())) {
ctrl.put(i._id, i._rev); ctrl.put(i._id, i._rev);
} }
}, },

View File

@ -13,19 +13,16 @@
let hasStyles = false; let hasStyles = false;
let isDisabled = false; let isDisabled = false;
let isTab = !chrome.tabs || location.pathname !== '/popup.html'; let isTab = !chrome.tabs || location.pathname !== '/popup.html';
let order = {}; const order = {main: [], prio: []};
const calcOrder = ({id}) =>
(order.prio[id] || 0) * 1e6 ||
order.main[id] ||
id + .5e6; // no order = at the end of `main`
const isFrame = window !== parent; const isFrame = window !== parent;
const isFrameAboutBlank = isFrame && location.href === 'about:blank'; const isFrameAboutBlank = isFrame && location.href === 'about:blank';
const isUnstylable = !chrome.app && document instanceof XMLDocument; const isUnstylable = !chrome.app && document instanceof XMLDocument;
const styleInjector = StyleInjector({ const styleInjector = StyleInjector({
compare: (a, b) => { compare: (a, b) => calcOrder(a) - calcOrder(b),
const ia = order[a.id];
const ib = order[b.id];
if (ia === ib) return 0;
if (ia == null) return 1;
if (ib == null) return -1;
return ia - ib;
},
onUpdate: onInjectorUpdate, onUpdate: onInjectorUpdate,
}); });
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited) // dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
@ -110,7 +107,7 @@
await API.styles.getSectionsByUrl(matchUrl, null, true); await API.styles.getSectionsByUrl(matchUrl, null, true);
if (styles.cfg) { if (styles.cfg) {
isDisabled = styles.cfg.disableAll; isDisabled = styles.cfg.disableAll;
order = styles.cfg.order || {}; Object.assign(order, styles.cfg.order);
delete styles.cfg; delete styles.cfg;
} }
hasStyles = !isDisabled; hasStyles = !isDisabled;
@ -179,7 +176,7 @@
break; break;
case 'styleSort': case 'styleSort':
order = request.order; Object.assign(order, request.order);
styleInjector.sort(); styleInjector.sort();
break; break;

View File

@ -1,13 +1,16 @@
#message-box.injection-order > div { .injection-order > div {
height: 100%; height: 100%;
max-width: 80vw; max-width: 80vw;
} }
#message-box.injection-order #message-box-contents { .injection-order #message-box-contents,
.injection-order section {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
--border: 1px solid rgba(128, 128, 128, .25); }
.injection-order section[data-main] {
flex: 1 0;
} }
.injection-order header { .injection-order header {
padding: 1rem; padding: 1rem;
@ -16,39 +19,81 @@
box-sizing: border-box; box-sizing: border-box;
} }
.injection-order ol { .injection-order ol {
height: 100%; padding: 1px 0; /* 1px for keyboard-focused element's outline */
padding: 1px 0 0; /* 1px for keyboard-focused element's outline */
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
overflow-y: auto; overflow-y: auto;
border-top: var(--border);
} }
.injection-order a { .injection-order ol:empty {
display: block; display: none;
}
.injection-order [data-prio] header {
background-color: hsla(40, 80%, 50%, 0.4);
}
.injection-order [data-prio] {
height: min-content;
min-height: 2em;
max-height: 50%;
}
.injection-order-entry {
display: flex;
justify-content: space-between;
color: #000; color: #000;
text-decoration: none;
transition: transform .25s ease-in-out; transition: transform .25s ease-in-out;
z-index: 1; z-index: 1;
user-select: none; user-select: none;
padding: 0.3em .5em;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: move; cursor: move;
} }
.injection-order a.enabled { .injection-order-entry a[href] {
padding: .4em 0 .4em 1rem;
cursor: inherit;
}
.injection-order-entry.enabled a[href] {
font-weight: bold; font-weight: bold;
} }
.injection-order a:hover { .injection-order-entry a {
text-decoration: none;
}
.injection-order-entry a[href]:hover {
text-decoration: underline; text-decoration: underline;
} }
.injection-order a:not(:first-child) { .injection-order-toggle {
border-top: var(--border); display: flex;
align-items: center;
padding: 0 .5rem;
cursor: pointer;
opacity: .33;
transition: .2s;
} }
.injection-order a::before { .injection-order-toggle::after {
content: "\2261"; content: '';
padding: 0.6em; width: .75em;
font-weight: normal; height: .75em;
border-radius: 100%;
border: 2px solid currentColor;
}
.injection-order-entry:hover .injection-order-toggle {
opacity: 1;
}
[data-prio] .injection-order-toggle::after {
background-color: currentColor;
background-clip: content-box;
width: .5em;
height: .5em;
padding: 2px;
transition: .2s;
}
.injection-order-toggle:hover::after {
background-color: hsl(40, 80%, 50%);
}
.injection-order [data-prio] header,
.injection-order ol,
.injection-order #message-box-buttons,
.injection-order-entry:nth-child(n + 2) {
border-top: 1px solid rgba(128, 128, 128, .25);
} }
.injection-order .draggable-list-target { .injection-order .draggable-list-target {
position: relative; position: relative;

View File

@ -1,7 +1,6 @@
/* global $create messageBoxProxy */// dom.js /* global $create messageBoxProxy */// dom.js
/* global API */// msg.js /* global API */// msg.js
/* global DraggableList */ /* global DraggableList */
/* global prefs */
/* global t */// localization.js /* global t */// localization.js
'use strict'; 'use strict';
@ -10,70 +9,68 @@ async function InjectionOrder(show = true) {
if (!show) { if (!show) {
return messageBoxProxy.close(); return messageBoxProxy.close();
} }
const entries = (await getOrderedStyles()).map(makeEntry); const SEL_ENTRY = '.injection-order-entry';
const ol = $create('ol'); const groups = await API.styles.getAllOrdered();
let maxTranslateY; const ols = {};
ol.append(...entries.map(l => l.el));
ol.on('d:dragstart', ({detail: d}) => {
d.origin.dataTransfer.setDragImage(new Image(), 0, 0);
maxTranslateY = ol.scrollHeight + ol.offsetTop - d.dragTarget.offsetHeight - d.dragTarget.offsetTop;
});
ol.on('d:dragmove', ({detail: d}) => {
d.origin.stopPropagation(); // preserves dropEffect
d.origin.dataTransfer.dropEffect = 'move';
const y = Math.min(d.currentPos.y - d.startPos.y, maxTranslateY);
d.dragTarget.style.transform = `translateY(${y}px)`;
});
ol.on('d:dragend', ({detail: d}) => {
const [item] = entries.splice(d.originalIndex, 1);
entries.splice(d.spliceIndex, 0, item);
ol.insertBefore(d.dragTarget, d.insertBefore);
prefs.set('injectionOrder', entries.map(l => l.style._id));
});
DraggableList(ol, {scrollContainer: ol});
await messageBoxProxy.show({ await messageBoxProxy.show({
title: t('styleInjectionOrder'), title: t('styleInjectionOrder'),
contents: $create('fragment', [ contents: $create('fragment', Object.entries(groups).map(makeList)),
$create('header', t('styleInjectionOrderHint')),
ol,
]),
className: 'injection-order center-dialog', className: 'injection-order center-dialog',
blockScroll: true, blockScroll: true,
buttons: [t('confirmClose')], buttons: [t('confirmClose')],
}); });
async function getOrderedStyles() {
const [styles] = await Promise.all([
API.styles.getAll(),
prefs.ready,
]);
const styleSet = new Set(styles);
const uuidIndex = new Map();
for (const s of styleSet) {
uuidIndex.set(s._id, s);
}
const orderedStyles = [];
for (const uid of prefs.get('injectionOrder')) {
const s = uuidIndex.get(uid);
if (s) {
uuidIndex.delete(uid);
orderedStyles.push(s);
styleSet.delete(s);
}
}
orderedStyles.push(...styleSet);
return orderedStyles;
}
function makeEntry(style) { function makeEntry(style) {
return { return $create('li' + SEL_ENTRY + (style.enabled ? '.enabled' : ''), [
style, $create('a', {
el: $create('a', {
className: style.enabled ? 'enabled' : '',
href: '/edit.html?id=' + style.id, href: '/edit.html?id=' + style.id,
target: '_blank', target: '_blank',
draggable: false,
}, style.name), }, style.name),
}; $create('a.injection-order-toggle', {
tabIndex: 0,
draggable: false,
}),
]);
}
function makeList([type, styles]) {
const ids = groups[type] = styles.map(s => s._id);
const ol = ols[type] = $create('ol');
let maxTranslateY;
ol.append(...styles.map(makeEntry));
ol.on('d:dragstart', ({detail: d}) => {
d.origin.dataTransfer.setDragImage(new Image(), 0, 0);
maxTranslateY =
ol.scrollHeight + ol.offsetTop - d.dragTarget.offsetHeight - d.dragTarget.offsetTop;
});
ol.on('d:dragmove', ({detail: d}) => {
d.origin.stopPropagation(); // preserves dropEffect
d.origin.dataTransfer.dropEffect = 'move';
const y = Math.min(d.currentPos.y - d.startPos.y, maxTranslateY);
d.dragTarget.style.transform = `translateY(${y}px)`;
});
ol.on('d:dragend', ({detail: d}) => {
const [item] = ids.splice(d.originalIndex, 1);
ids.splice(d.spliceIndex, 0, item);
ol.insertBefore(d.dragTarget, d.insertBefore);
API.styles.setOrder(groups);
});
ol.on('click', e => {
if (e.target.closest('.injection-order-toggle')) {
const el = e.target.closest(SEL_ENTRY);
const i = [].indexOf.call(el.parentNode.children, el);
const [item] = ids.splice(i, 1);
const type2 = type === 'main' ? 'prio' : 'main';
groups[type2].push(item);
ols[type2].appendChild(el);
API.styles.setOrder(groups);
}
});
DraggableList(ol, {scrollContainer: ol});
return $create('section', {dataset: {[type]: ''}}, [
$create('header', t('styleInjectionOrderHint_' + type)),
ol,
]);
} }
} }

View File

@ -134,8 +134,6 @@
'popupWidth': 246, // popup width in pixels 'popupWidth': 246, // popup width in pixels
'updateInterval': 24, // user-style automatic update interval, hours (0 = disable) 'updateInterval': 24, // user-style automatic update interval, hours (0 = disable)
'injectionOrder': [],
}; };
const knownKeys = Object.keys(defaults); const knownKeys = Object.keys(defaults);
/** @type {PrefsValues} */ /** @type {PrefsValues} */

View File

@ -13,6 +13,7 @@
getTab getTab
ignoreChromeError ignoreChromeError
isEmptyObj isEmptyObj
mapObj
onTabReady onTabReady
openURL openURL
sessionStore sessionStore
@ -272,6 +273,24 @@ function isEmptyObj(obj) {
return true; return true;
} }
/**
* @param {?Object} obj
* @param {function(val:?, key:string, obj:Object):T} [fn]
* @param {string[]} [keys]
* @returns {?Object<string,T>}
* @template T
*/
function mapObj(obj, fn, keys) {
if (!obj) return obj;
const res = {};
for (const k of keys || Object.keys(obj)) {
if (!keys || k in obj) {
res[k] = fn ? fn(obj[k], k, obj) : obj[k];
}
}
return res;
}
/** /**
* js engine can't optimize the entire function if it contains try-catch * js engine can't optimize the entire function if it contains try-catch
* so we should keep it isolated from normal code in a minimal wrapper * so we should keep it isolated from normal code in a minimal wrapper