Skip to content

Commit 11b749e

Browse files
committed
[mv3] Support copying/pasting custom filters
As a convenience for filter list authors who may want to freely copy filters from/to uBO, custom filters can now be copied to the clipboard as an alternative to "export" operation, or pasted directly in empty hostname field as an alternative to "import" operation.
1 parent d187e51 commit 11b749e

3 files changed

Lines changed: 88 additions & 40 deletions

File tree

platform/mv3/extension/css/settings.css

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
@keyframes spin {
2-
0% { transform: rotate(0deg); }
3-
100% { transform: rotate(360deg); }
2+
from { transform: rotate(0deg); }
3+
to { transform: rotate(360deg); }
4+
}
5+
@keyframes copied {
6+
from { opacity: 0; }
7+
to { opacity: 1; }
48
}
59

610
:root {
@@ -278,23 +282,31 @@ section[data-pane="filters"] li [contenteditable] {
278282
section[data-pane="filters"] li [contenteditable]:focus {
279283
background-color: var(--surface-0);
280284
}
281-
section[data-pane="filters"] li .fa-icon {
285+
section[data-pane="filters"] li > div span.fa-icon {
282286
align-self: baseline;
283287
font-size: calc(var(--font-size) + 2px);
284-
padding: 0 0.5em;
288+
padding-inline: 0.3em;
285289
top: 2px;
286290
}
291+
section[data-pane="filters"] li > div span.remove,
292+
section[data-pane="filters"] li > div span.undo {
293+
padding-inline-end: 0.5em;
294+
}
287295
section[data-pane="filters"] li.selector:not(:first-of-type) {
288296
border-top: 1px dotted var(--border-1);
289297
}
290298
section[data-pane="filters"] li.removed > div [contenteditable] {
291299
color: red;
292300
text-decoration-line: line-through;
293301
}
302+
section[data-pane="filters"] li.removed > div span.copy,
294303
section[data-pane="filters"] li.removed > div span.remove,
295304
section[data-pane="filters"] li:not(.removed) > div span.undo {
296305
display: none;
297306
}
307+
section[data-pane="filters"] li > div span.copy.copied {
308+
animation: copied 1s ease-out;
309+
}
298310
section[data-pane="filters"] span[contenteditable]:empty:not(:focus)::before {
299311
color: gray;
300312
content: '\2026';

platform/mv3/extension/dashboard.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,12 @@ <h3 class="listname"></h3>
220220
<div class="listEntries"></div>
221221
</template>
222222
<template id="customFiltersHostname">
223-
<li class="hostname" data-pretty="" data-ugly=""><div><span class="hostname" spellcheck="false" contenteditable="plaintext-only"></span><span class="remove fa-icon">trash-o</span><span class="undo fa-icon">undo</span></div>
223+
<li class="hostname" data-pretty="" data-ugly=""><div><span class="hostname" spellcheck="false" contenteditable="plaintext-only"></span><span class="copy fa-icon">files-o</span><span class="remove fa-icon">trash-o</span><span class="undo fa-icon">undo</span></div>
224224
<ul class="selectors"></ul>
225225
</li>
226226
</template>
227227
<template id="customFiltersSelector">
228-
<li class="selector" data-pretty="" data-ugly=""><div><span class="selector" spellcheck="false" contenteditable="plaintext-only"></span><span class="remove fa-icon">trash-o</span><span class="undo fa-icon">undo</span></div></li>
228+
<li class="selector" data-pretty="" data-ugly=""><div><span class="selector" spellcheck="false" contenteditable="plaintext-only"></span><span class="copy fa-icon">files-o</span><span class="remove fa-icon">trash-o</span><span class="undo fa-icon">undo</span></div></li>
229229
</template>
230230
<template class="io-panel">
231231
<span class="io-panel">

platform/mv3/extension/js/filter-manager-ui.js

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,10 @@ async function removeSelectorsFromHostname(node) {
9696
qsa$(hostnameNode, 'li.selector.removed:not([data-ugly=""])')
9797
).map(a => a.dataset.ugly);
9898
if ( selectors.length === 0 ) { return; }
99-
dom.cl.add(dom.body, 'readonly');
100-
updateContentEditability();
99+
updateContentEditability(false);
101100
await sendMessage({ what: 'removeCustomFilters', hostname, selectors });
102101
await debounceRenderCustomFilters();
103-
dom.cl.remove(dom.body, 'readonly');
104-
updateContentEditability();
102+
updateContentEditability(true);
105103
}
106104

107105
async function unremoveSelectorsFromHostname(node) {
@@ -111,12 +109,10 @@ async function unremoveSelectorsFromHostname(node) {
111109
if ( hostname === undefined ) { return; }
112110
const selectors = selectorsFromNode(hostnameNode);
113111
if ( selectors.length === 0 ) { return; }
114-
dom.cl.add(dom.body, 'readonly');
115-
updateContentEditability();
112+
updateContentEditability(false);
116113
await sendMessage({ what: 'addCustomFilters', hostname, selectors });
117114
await debounceRenderCustomFilters();
118-
dom.cl.remove(dom.body, 'readonly');
119-
updateContentEditability();
115+
updateContentEditability(true);
120116
}
121117

122118
/******************************************************************************/
@@ -225,8 +221,9 @@ debounceRenderCustomFilters.debouncer = undefined;
225221

226222
/******************************************************************************/
227223

228-
function updateContentEditability() {
229-
if ( dom.cl.has(dom.body, 'readonly') ) {
224+
function updateContentEditability(readWrite) {
225+
dom.cl.toggle(dom.body, 'readonly', readWrite === false);
226+
if ( readWrite === false ) {
230227
dom.attr('section[data-pane="filters"] [contenteditable]', 'contenteditable', 'false');
231228
return;
232229
}
@@ -267,18 +264,23 @@ async function validateSelector(target, selector) {
267264
/******************************************************************************/
268265

269266
async function onHostnameChanged(target, before, after) {
267+
const hostnameNode = target.closest('li.hostname');
268+
if ( hostnameNode === null ) { return; }
269+
if ( before === '' ) {
270+
const succeeded = await importFromText(after);
271+
if ( succeeded ) {
272+
return debounceRenderCustomFilters();
273+
}
274+
}
275+
276+
after = after.replace('\n', '');
277+
270278
const uglyAfter = punycode.toASCII(after);
271279
if ( isValidHostname(uglyAfter) === false ) {
272280
target.textContent = before;
273281
return;
274282
}
275283

276-
const hostnameNode = target.closest('li.hostname');
277-
if ( hostnameNode === null ) { return; }
278-
279-
dom.cl.add(dom.body, 'readonly');
280-
updateContentEditability();
281-
282284
// Remove old hostname from storage
283285
if ( hostnameNode.dataset.ugly ) {
284286
await sendMessage({ what: 'removeAllCustomFilters',
@@ -295,11 +297,11 @@ async function onHostnameChanged(target, before, after) {
295297
});
296298

297299
await debounceRenderCustomFilters();
298-
dom.cl.remove(dom.body, 'readonly');
299-
updateContentEditability();
300300
}
301301

302302
async function onSelectorChanged(target, before, after) {
303+
after = after.replace('\n', '');
304+
303305
const selectorNode = target.closest('li.selector');
304306
if ( selectorNode === null ) { return; }
305307

@@ -310,9 +312,6 @@ async function onSelectorChanged(target, before, after) {
310312
return;
311313
}
312314

313-
dom.cl.add(dom.body, 'readonly');
314-
updateContentEditability();
315-
316315
const hostname = hostnameFromNode(target);
317316

318317
// Remove old selector from storage
@@ -330,16 +329,14 @@ async function onSelectorChanged(target, before, after) {
330329
});
331330

332331
await debounceRenderCustomFilters();
333-
dom.cl.remove(dom.body, 'readonly');
334-
updateContentEditability();
335332
}
336333

337-
function onTextChanged(target) {
334+
async function onTextChanged(target) {
338335
const itemNode = target.closest('[data-pretty]');
339336
if ( itemNode === null ) { return; }
340337
dom.cl.remove(itemNode, 'error');
341338
const before = itemNode.dataset.pretty;
342-
const after = target.textContent.trim().replace('\n', '');
339+
const after = target.textContent.trim();
343340
if ( after !== target.textContent ) {
344341
target.textContent = after;
345342
}
@@ -348,11 +345,16 @@ function onTextChanged(target) {
348345
target.textContent = before;
349346
return;
350347
}
348+
349+
updateContentEditability(false);
350+
351351
if ( target.matches('.hostname') ) {
352-
onHostnameChanged(target, before, after);
352+
await onHostnameChanged(target, before, after);
353353
} else if ( target.matches('.selector') ) {
354-
onSelectorChanged(target, before, after);
354+
await onSelectorChanged(target, before, after);
355355
}
356+
357+
updateContentEditability(true);
356358
}
357359

358360
/******************************************************************************/
@@ -388,20 +390,53 @@ function validateEdit(ev) {
388390
if ( itemNode === null ) { return; }
389391
const after = target.textContent.trim().replace('\n', '');
390392
if ( after === '' ) { return; }
393+
const before = itemNode.dataset.ugly;
391394
if ( target.matches('.selector') ) {
392395
validateSelector(target, after).then(({ pretty }) => {
393396
if ( focusedEditableContent !== target ) { return; }
394397
dom.cl.toggle(itemNode, 'error', after !== '' && Boolean(pretty) === false);
395398
});
396399
} else if ( target.matches('.hostname') ) {
397-
dom.cl.toggle(itemNode, 'error', isValidHostname(punycode.toASCII(after)) === false);
400+
dom.cl.toggle(itemNode, 'error',
401+
before !== '' && isValidHostname(punycode.toASCII(after)) === false
402+
);
398403
}
399404
}
400405

401406
let focusedEditableContent = null;
402407

403408
/******************************************************************************/
404409

410+
function onCopyClicked(ev) {
411+
const { target } = ev;
412+
const selectorNode = target.closest('li.selector:not(.removed):not([data-ugly=""])');
413+
const hostnameNode = target.closest('li.hostname:not(.removed):not([data-ugly=""])');
414+
const hostname = hostnameFromNode(hostnameNode);
415+
if ( Boolean(hostname) === false ) { return; }
416+
const selectorNodes = [];
417+
let copyNode;
418+
if ( selectorNode ) {
419+
selectorNodes.push(selectorNode);
420+
copyNode = selectorNode;
421+
} else {
422+
selectorNodes.push(...qsa$(hostnameNode, 'li.selector:not(.removed):not([data-ugly=""])'));
423+
copyNode = hostnameNode;
424+
}
425+
const text = [];
426+
for ( const node of selectorNodes ) {
427+
const selector = node.dataset.pretty;
428+
if ( Boolean(selector) === false ) { continue; }
429+
text.push(`${hostname}##${selector}`);
430+
}
431+
if ( text.length === 0 ) { return; }
432+
text.push('\n');
433+
const item = new ClipboardItem({ 'text/plain': text.join('\n') });
434+
navigator.clipboard.write([ item ]);
435+
const copyNodes = qsa$(copyNode, '.copy');
436+
dom.cl.add(copyNodes, 'copied');
437+
self.setTimeout(( ) => { dom.cl.remove(copyNodes, 'copied'); }, 1000);
438+
}
439+
405440
function onTrashClicked(ev) {
406441
const { target } = ev;
407442
const selectorNode = target.closest('li.selector');
@@ -474,10 +509,9 @@ async function importFromText(text) {
474509
}
475510
}
476511

477-
if ( hostnameToSelectorsMap.size === 0 ) { return; }
512+
if ( hostnameToSelectorsMap.size === 0 ) { return false; }
478513

479-
dom.cl.add(dom.body, 'readonly');
480-
updateContentEditability();
514+
updateContentEditability(false);
481515

482516
const promises = [];
483517
for ( const [ hostname, selectors ] of hostnameToSelectorsMap ) {
@@ -489,10 +523,11 @@ async function importFromText(text) {
489523
);
490524
}
491525
await Promise.all(promises);
492-
493526
await debounceRenderCustomFilters();
494-
dom.cl.remove(dom.body, 'readonly');
495-
updateContentEditability();
527+
528+
updateContentEditability(true);
529+
530+
return true;
496531
}
497532

498533
/******************************************************************************/
@@ -558,6 +593,7 @@ async function start() {
558593
dom.on(dataContainer, 'focusin', 'section[data-pane="filters"] [contenteditable]', startEdit);
559594
dom.on(dataContainer, 'focusout', 'section[data-pane="filters"] [contenteditable]', endEdit);
560595
dom.on(dataContainer, 'input', 'section[data-pane="filters"] [contenteditable]', commitEdit);
596+
dom.on(dataContainer, 'click', 'section[data-pane="filters"] .copy', onCopyClicked);
561597
dom.on(dataContainer, 'click', 'section[data-pane="filters"] .remove', onTrashClicked);
562598
dom.on(dataContainer, 'click', 'section[data-pane="filters"] .undo', onUndoClicked);
563599
dom.on('section[data-pane="filters"] [data-i18n="addButton"]', 'click', importFromTextarea);

0 commit comments

Comments
 (0)