simplify localization
This commit is contained in:
parent
151805f1bb
commit
b8322ecf01
|
@ -1,4 +1,10 @@
|
||||||
/* global CodeMirror prefs editor $ template */
|
/* global
|
||||||
|
$
|
||||||
|
CodeMirror
|
||||||
|
editor
|
||||||
|
prefs
|
||||||
|
t
|
||||||
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -183,7 +189,7 @@
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// $('.CodeMirror-dialog', section).focus();
|
// $('.CodeMirror-dialog', section).focus();
|
||||||
// });
|
// });
|
||||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
|
||||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
setupLivePrefs
|
setupLivePrefs
|
||||||
SourceEditor
|
SourceEditor
|
||||||
t
|
t
|
||||||
tHTML
|
|
||||||
tryCatch
|
tryCatch
|
||||||
tryJSONparse
|
tryJSONparse
|
||||||
*/
|
*/
|
||||||
|
@ -449,7 +448,7 @@ function showHelp(title = '', body) {
|
||||||
const contents = $('.contents', div);
|
const contents = $('.contents', div);
|
||||||
contents.textContent = '';
|
contents.textContent = '';
|
||||||
if (body) {
|
if (body) {
|
||||||
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
|
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.title', div).textContent = title;
|
$('.title', div).textContent = title;
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
/* global CodeMirror focusAccessibility colorMimicry editor chromeLocal
|
/* global
|
||||||
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
|
$
|
||||||
|
$$
|
||||||
|
$create
|
||||||
|
chromeLocal
|
||||||
|
CodeMirror
|
||||||
|
colorMimicry
|
||||||
|
debounce
|
||||||
|
editor
|
||||||
|
focusAccessibility
|
||||||
|
onDOMready
|
||||||
|
stringAsRegExp
|
||||||
|
t
|
||||||
|
tryRegExp
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
@ -563,14 +576,14 @@ onDOMready().then(() => {
|
||||||
state.originalFocus = document.activeElement;
|
state.originalFocus = document.activeElement;
|
||||||
state.firstRun = true;
|
state.firstRun = true;
|
||||||
|
|
||||||
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true);
|
const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
|
||||||
Object.assign(dialog, DIALOG_PROPS.dialog);
|
Object.assign(dialog, DIALOG_PROPS.dialog);
|
||||||
dialog.addEventListener('focusout', EVENTS.onfocusout);
|
dialog.addEventListener('focusout', EVENTS.onfocusout);
|
||||||
dialog.dataset.type = type;
|
dialog.dataset.type = type;
|
||||||
dialog.style.pointerEvents = 'auto';
|
dialog.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
const content = $('[data-type="content"]', dialog);
|
const content = $('[data-type="content"]', dialog);
|
||||||
content.parentNode.replaceChild(template[type].cloneNode(true), content);
|
content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
|
||||||
|
|
||||||
createInput(0, 'input', state.find);
|
createInput(0, 'input', state.find);
|
||||||
createInput(1, 'input2', state.replace);
|
createInput(1, 'input2', state.replace);
|
||||||
|
@ -633,7 +646,7 @@ onDOMready().then(() => {
|
||||||
input.value = value;
|
input.value = value;
|
||||||
Object.assign(input, DIALOG_PROPS[name]);
|
Object.assign(input, DIALOG_PROPS[name]);
|
||||||
|
|
||||||
input.parentElement.appendChild(template.clearSearch.cloneNode(true));
|
input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
|
||||||
$('[data-action]', input.parentElement)._input = input;
|
$('[data-action]', input.parentElement)._input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
prefs
|
prefs
|
||||||
regExpTester
|
regExpTester
|
||||||
t
|
t
|
||||||
template
|
|
||||||
tryCatch
|
tryCatch
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
@ -55,7 +54,7 @@ function MozSectionWidget(
|
||||||
$create('ul' + C_LIST),
|
$create('ul' + C_LIST),
|
||||||
]),
|
]),
|
||||||
listItem:
|
listItem:
|
||||||
template.appliesTo.cloneNode(true),
|
t.template.appliesTo.cloneNode(true),
|
||||||
appliesToEverything:
|
appliesToEverything:
|
||||||
$create('li.applies-to-everything', t('appliesToEverything')),
|
$create('li.applies-to-everything', t('appliesToEverything')),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
/* global showHelp $ $create tryRegExp URLS t template openURL */
|
/* global
|
||||||
|
$
|
||||||
|
$create
|
||||||
|
openURL
|
||||||
|
showHelp
|
||||||
|
t
|
||||||
|
tryRegExp
|
||||||
|
URLS
|
||||||
|
*/
|
||||||
/* exported regExpTester */
|
/* exported regExpTester */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -86,7 +94,7 @@ const regExpTester = (() => {
|
||||||
full: {data: [], label: t('styleRegexpTestFull')},
|
full: {data: [], label: t('styleRegexpTestFull')},
|
||||||
partial: {data: [], label: [
|
partial: {data: [], label: [
|
||||||
t('styleRegexpTestPartial'),
|
t('styleRegexpTestPartial'),
|
||||||
template.regexpTestPartial.cloneNode(true),
|
t.template.regexpTestPartial.cloneNode(true),
|
||||||
]},
|
]},
|
||||||
none: {data: [], label: t('styleRegexpTestNone')},
|
none: {data: [], label: t('styleRegexpTestNone')},
|
||||||
invalid: {data: [], label: t('styleRegexpTestInvalid')},
|
invalid: {data: [], label: t('styleRegexpTestInvalid')},
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
prefs
|
prefs
|
||||||
regExpTester
|
regExpTester
|
||||||
t
|
t
|
||||||
template
|
|
||||||
trimCommentLabel
|
trimCommentLabel
|
||||||
tryRegExp
|
tryRegExp
|
||||||
*/
|
*/
|
||||||
|
@ -26,7 +25,7 @@
|
||||||
function createSection(originalSection, genId, si) {
|
function createSection(originalSection, genId, si) {
|
||||||
const {dirty} = editor;
|
const {dirty} = editor;
|
||||||
const sectionId = genId();
|
const sectionId = genId();
|
||||||
const el = template.section.cloneNode(true);
|
const el = t.template.section.cloneNode(true);
|
||||||
const elLabel = $('.code-label', el);
|
const elLabel = $('.code-label', el);
|
||||||
const cm = cmFactory.create(wrapper => {
|
const cm = cmFactory.create(wrapper => {
|
||||||
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
|
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
|
||||||
|
@ -265,8 +264,8 @@ function createSection(originalSection, genId, si) {
|
||||||
function createApply({type = 'url', value, all = false}) {
|
function createApply({type = 'url', value, all = false}) {
|
||||||
const applyId = genId();
|
const applyId = genId();
|
||||||
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
|
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
|
||||||
const el = all ? template.appliesToEverything.cloneNode(true) :
|
const el = all ? t.template.appliesToEverything.cloneNode(true) :
|
||||||
template.appliesTo.cloneNode(true);
|
t.template.appliesTo.cloneNode(true);
|
||||||
|
|
||||||
const selectEl = !all && $('.applies-type', el);
|
const selectEl = !all && $('.applies-type', el);
|
||||||
if (selectEl) {
|
if (selectEl) {
|
||||||
|
@ -359,7 +358,7 @@ function createSection(originalSection, genId, si) {
|
||||||
function createResizeGrip(cm) {
|
function createResizeGrip(cm) {
|
||||||
const wrapper = cm.display.wrapper;
|
const wrapper = cm.display.wrapper;
|
||||||
wrapper.classList.add('resize-grip-enabled');
|
wrapper.classList.add('resize-grip-enabled');
|
||||||
const resizeGrip = template.resizeGrip.cloneNode(true);
|
const resizeGrip = t.template.resizeGrip.cloneNode(true);
|
||||||
wrapper.appendChild(resizeGrip);
|
wrapper.appendChild(resizeGrip);
|
||||||
let lastClickTime = 0;
|
let lastClickTime = 0;
|
||||||
let initHeight;
|
let initHeight;
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
/* global CodeMirror showHelp onDOMready $ $$ $create template t
|
/* global
|
||||||
prefs stringAsRegExp */
|
$
|
||||||
|
$$
|
||||||
|
$create
|
||||||
|
CodeMirror
|
||||||
|
onDOMready
|
||||||
|
prefs
|
||||||
|
showHelp
|
||||||
|
stringAsRegExp
|
||||||
|
t
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
@ -11,7 +20,7 @@ function showKeyMapHelp() {
|
||||||
const keyMapSorted = Object.keys(keyMap)
|
const keyMapSorted = Object.keys(keyMap)
|
||||||
.map(key => ({key, cmd: keyMap[key]}))
|
.map(key => ({key, cmd: keyMap[key]}))
|
||||||
.sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
|
.sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
|
||||||
const table = template.keymapHelp.cloneNode(true);
|
const table = t.template.keymapHelp.cloneNode(true);
|
||||||
const tBody = table.tBodies[0];
|
const tBody = table.tBodies[0];
|
||||||
const row = tBody.rows[0];
|
const row = tBody.rows[0];
|
||||||
const cellA = row.children[0];
|
const cellA = row.children[0];
|
||||||
|
|
|
@ -1,97 +1,90 @@
|
||||||
/* global tryCatch */
|
|
||||||
/* exported tHTML formatDate */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const template = {};
|
|
||||||
tDocLoader();
|
|
||||||
|
|
||||||
|
|
||||||
function t(key, params) {
|
function t(key, params) {
|
||||||
const s = chrome.i18n.getMessage(key, params);
|
const s = chrome.i18n.getMessage(key, params);
|
||||||
if (!s) throw `Missing string "${key}"`;
|
if (!s) throw `Missing string "${key}"`;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(t, {
|
||||||
|
template: {},
|
||||||
|
DOMParser: new DOMParser(),
|
||||||
|
ALLOWED_TAGS: 'a,b,code,i,sub,sup,wbr'.split(','),
|
||||||
|
RX_WORD_BREAK: new RegExp([
|
||||||
|
'(',
|
||||||
|
/[\d\w\u007B-\uFFFF]{10}/,
|
||||||
|
'|',
|
||||||
|
/[\d\w\u007B-\uFFFF]{5,10}[!-/]/,
|
||||||
|
'|',
|
||||||
|
/((?!\s)\W){10}/,
|
||||||
|
')',
|
||||||
|
/(?!\b|\s|$)/,
|
||||||
|
].map(rx => rx.source || rx).join(''), 'g'),
|
||||||
|
|
||||||
function tHTML(html, tag) {
|
HTML(html) {
|
||||||
// body is a text node without HTML tags
|
return typeof html !== 'string'
|
||||||
if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) {
|
? html
|
||||||
return document.createTextNode(html);
|
: /<\w+/.test(html) // check for html tags
|
||||||
}
|
? t.createHtml(html.replace(/>\n\s*</g, '><').trim())
|
||||||
if (typeof html === 'string') {
|
: document.createTextNode(html);
|
||||||
// spaces are removed; use for an explicit space
|
},
|
||||||
html = html.replace(/>\s+</g, '><').trim();
|
|
||||||
if (tag) {
|
|
||||||
html = `<${tag}>${html}</${tag}>`;
|
|
||||||
}
|
|
||||||
const body = t.DOMParser.parseFromString(html, 'text/html').body;
|
|
||||||
if (html.includes('i18n-')) {
|
|
||||||
tNodeList(body.getElementsByTagName('*'));
|
|
||||||
}
|
|
||||||
// the html string may contain more than one top-level node
|
|
||||||
if (!body.childNodes[1]) {
|
|
||||||
return body.firstChild;
|
|
||||||
}
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
while (body.firstChild) {
|
|
||||||
fragment.appendChild(body.firstChild);
|
|
||||||
}
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
NodeList(nodes) {
|
||||||
function tNodeList(nodes) {
|
const PREFIX = 'i18n-';
|
||||||
const PREFIX = 'i18n-';
|
for (let n = nodes.length; --n >= 0;) {
|
||||||
|
const node = nodes[n];
|
||||||
for (let n = nodes.length; --n >= 0;) {
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||||
const node = nodes[n];
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (node.localName === 'template') {
|
|
||||||
createTemplate(node);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let a = node.attributes.length; --a >= 0;) {
|
|
||||||
const attr = node.attributes[a];
|
|
||||||
const name = attr.nodeName;
|
|
||||||
if (!name.startsWith(PREFIX)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const type = name.substr(PREFIX.length);
|
if (node.localName === 'template') {
|
||||||
const value = t(attr.value);
|
t.createTemplate(node);
|
||||||
let toInsert, before;
|
continue;
|
||||||
switch (type) {
|
}
|
||||||
case 'word-break':
|
for (let a = node.attributes.length; --a >= 0;) {
|
||||||
// we already know that: hasWordBreak
|
const attr = node.attributes[a];
|
||||||
break;
|
const name = attr.nodeName;
|
||||||
case 'text':
|
if (!name.startsWith(PREFIX)) {
|
||||||
before = node.firstChild;
|
continue;
|
||||||
// fallthrough to text-append
|
|
||||||
case 'text-append':
|
|
||||||
toInsert = createText(value);
|
|
||||||
break;
|
|
||||||
case 'html': {
|
|
||||||
toInsert = createHtml(value);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
default:
|
const type = name.substr(PREFIX.length);
|
||||||
node.setAttribute(type, value);
|
const value = t(attr.value);
|
||||||
|
let toInsert, before;
|
||||||
|
switch (type) {
|
||||||
|
case 'word-break':
|
||||||
|
// we already know that: hasWordBreak
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
before = node.firstChild;
|
||||||
|
// fallthrough to text-append
|
||||||
|
case 'text-append':
|
||||||
|
toInsert = t.createText(value);
|
||||||
|
break;
|
||||||
|
case 'html': {
|
||||||
|
toInsert = t.createHtml(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
node.setAttribute(type, value);
|
||||||
|
}
|
||||||
|
t.stopObserver();
|
||||||
|
if (toInsert) {
|
||||||
|
node.insertBefore(toInsert, before || null);
|
||||||
|
}
|
||||||
|
node.removeAttribute(name);
|
||||||
}
|
}
|
||||||
tDocLoader.pause();
|
|
||||||
if (toInsert) {
|
|
||||||
node.insertBefore(toInsert, before || null);
|
|
||||||
}
|
|
||||||
node.removeAttribute(name);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function createTemplate(node) {
|
/** Adds soft hyphens every 10 characters to ensure the long words break before breaking the layout */
|
||||||
|
breakWord(text) {
|
||||||
|
return text.length <= 10 ? text :
|
||||||
|
text.replace(t.RX_WORD_BREAK, '$&\u00AD');
|
||||||
|
},
|
||||||
|
|
||||||
|
createTemplate(node) {
|
||||||
const elements = node.content.querySelectorAll('*');
|
const elements = node.content.querySelectorAll('*');
|
||||||
tNodeList(elements);
|
t.NodeList(elements);
|
||||||
template[node.dataset.id] = elements[0];
|
t.template[node.dataset.id] = elements[0];
|
||||||
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
|
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
|
||||||
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
|
||||||
const toRemove = [];
|
const toRemove = [];
|
||||||
|
@ -101,104 +94,96 @@ function tNodeList(nodes) {
|
||||||
toRemove.push(textNode);
|
toRemove.push(textNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tDocLoader.pause();
|
t.stopObserver();
|
||||||
toRemove.forEach(el => el.remove());
|
toRemove.forEach(el => el.remove());
|
||||||
}
|
},
|
||||||
|
|
||||||
function createText(str) {
|
createText(str) {
|
||||||
return document.createTextNode(tWordBreak(str));
|
return document.createTextNode(t.breakWord(str));
|
||||||
}
|
},
|
||||||
|
|
||||||
function createHtml(value) {
|
createHtml(str, trusted) {
|
||||||
// <a> and <code> are the only acceptable HTML elements,
|
const root = t.DOMParser.parseFromString(str, 'text/html').body;
|
||||||
// <a> also allows `href` attribute with an http/https URL
|
if (!trusted) {
|
||||||
const rx = /(<)(a|code)(\s[^>]*|)>(.*?)<\/\2>/i;
|
t.sanitizeHtml(root);
|
||||||
|
} else if (str.includes('i18n-')) {
|
||||||
|
t.NodeList(root.getElementsByTagName('*'));
|
||||||
|
}
|
||||||
const bin = document.createDocumentFragment();
|
const bin = document.createDocumentFragment();
|
||||||
for (let parts = value.split(rx), i = 0; i < parts.length; i++) {
|
while (root.firstChild) {
|
||||||
const s = parts[i];
|
bin.appendChild(root.firstChild);
|
||||||
if (s === '<') {
|
|
||||||
const tag = parts[++i].toLowerCase();
|
|
||||||
const el = bin.appendChild(document.createElement(tag));
|
|
||||||
const attrs = parts[++i];
|
|
||||||
const href = tag === 'a' && /(?:^|\s)href\s*=\s*(["'])?(https?:\/\/\S*?)\1/i.exec(attrs);
|
|
||||||
if (href) el.href = href[2];
|
|
||||||
el.appendChild(createText(parts[++i]));
|
|
||||||
} else {
|
|
||||||
bin.appendChild(createText(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return bin;
|
return bin;
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
|
sanitizeHtml(root) {
|
||||||
function tDocLoader() {
|
const toRemove = [];
|
||||||
t.DOMParser = new DOMParser();
|
const walker = document.createTreeWalker(root);
|
||||||
t.RX_WORD_BREAK = new RegExp([
|
for (let n; (n = walker.nextNode());) {
|
||||||
'(',
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
/[\d\w\u007B-\uFFFF]{10}/,
|
n.nodeValue = t.breakWord(n.nodeValue);
|
||||||
'|',
|
} else if (t.ALLOWED_TAGS.includes(n.localName)) {
|
||||||
/[\d\w\u007B-\uFFFF]{5,10}[!-/]/,
|
for (const attr of n.attributes) {
|
||||||
'|',
|
if (n.localName !== 'a' || attr.localName !== 'href' || !/^https?:/.test(n.href)) {
|
||||||
/((?!\s)\W){10}/,
|
n.removeAttribute(attr.name);
|
||||||
')',
|
}
|
||||||
/(?!\b|\s|$)/,
|
}
|
||||||
].map(rx => rx.source || rx).join(''), 'g');
|
} else {
|
||||||
|
toRemove.push(n);
|
||||||
Object.assign(tDocLoader, {
|
|
||||||
observer: new MutationObserver(process),
|
|
||||||
start() {
|
|
||||||
if (!tDocLoader.observing) {
|
|
||||||
tDocLoader.observing = true;
|
|
||||||
tDocLoader.observer.observe(document, {subtree: true, childList: true});
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
stop() {
|
for (const n of toRemove) {
|
||||||
tDocLoader.pause();
|
const parent = n.parentNode;
|
||||||
document.removeEventListener('DOMContentLoaded', onLoad);
|
if (parent) parent.removeChild(n); // not using .remove() as there may be a non-element
|
||||||
},
|
}
|
||||||
pause() {
|
},
|
||||||
if (tDocLoader.observing) {
|
|
||||||
tDocLoader.observing = false;
|
formatDate(date) {
|
||||||
tDocLoader.observer.disconnect();
|
if (!date) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newDate = new Date(Number(date) || date);
|
||||||
|
const string = newDate.toLocaleDateString([chrome.i18n.getUILanguage(), 'en'], {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: newDate.getYear() === new Date().getYear() ? undefined : '2-digit',
|
||||||
|
});
|
||||||
|
return string === 'Invalid Date' ? '' : string;
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const observer = new MutationObserver(process);
|
||||||
|
let observing = false;
|
||||||
|
Object.assign(t, {
|
||||||
|
stopObserver() {
|
||||||
|
if (observing) {
|
||||||
|
observing = false;
|
||||||
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
process(observer.takeRecords());
|
||||||
|
t.stopObserver();
|
||||||
|
}, {once: true});
|
||||||
|
|
||||||
tNodeList(document.getElementsByTagName('*'));
|
t.NodeList(document.getElementsByTagName('*'));
|
||||||
tDocLoader.start();
|
start();
|
||||||
document.addEventListener('DOMContentLoaded', onLoad);
|
|
||||||
|
|
||||||
function process(mutations) {
|
function process(mutations) {
|
||||||
for (const mutation of mutations) {
|
mutations.forEach(m => t.NodeList(m.addedNodes));
|
||||||
tNodeList(mutation.addedNodes);
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (!observing) {
|
||||||
|
observing = true;
|
||||||
|
observer.observe(document, {subtree: true, childList: true});
|
||||||
}
|
}
|
||||||
tDocLoader.start();
|
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
function onLoad() {
|
|
||||||
document.removeEventListener('DOMContentLoaded', onLoad);
|
|
||||||
process(tDocLoader.observer.takeRecords());
|
|
||||||
tDocLoader.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function tWordBreak(text) {
|
|
||||||
// adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
|
|
||||||
return text.length <= 10 ? text :
|
|
||||||
text.replace(t.RX_WORD_BREAK, '$&\u00AD');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function formatDate(date) {
|
|
||||||
return !date ? '' : tryCatch(() => {
|
|
||||||
const newDate = new Date(Number(date) || date);
|
|
||||||
const string = newDate.toLocaleDateString([t.cache.browserUIlanguage, 'en'], {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: newDate.getYear() === new Date().getYear() ? undefined : '2-digit',
|
|
||||||
});
|
|
||||||
return string === 'Invalid Date' ? '' : string;
|
|
||||||
}) || '';
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
/* global messageBox deepCopy $create $createLink $ t tWordBreak
|
/* global
|
||||||
prefs setupLivePrefs debounce API */
|
$
|
||||||
|
$create
|
||||||
|
$createLink
|
||||||
|
API
|
||||||
|
debounce
|
||||||
|
deepCopy
|
||||||
|
messageBox
|
||||||
|
prefs
|
||||||
|
setupLivePrefs
|
||||||
|
t
|
||||||
|
*/
|
||||||
/* exported configDialog */
|
/* exported configDialog */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -305,7 +315,7 @@ function configDialog(style) {
|
||||||
|
|
||||||
elements.push(
|
elements.push(
|
||||||
$create(`label.config-${va.type}`, [
|
$create(`label.config-${va.type}`, [
|
||||||
$create('span.config-name', tWordBreak(va.label)),
|
$create('span.config-name', t.breakWord(va.label)),
|
||||||
...children,
|
...children,
|
||||||
resetter,
|
resetter,
|
||||||
]));
|
]));
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
configDialog
|
configDialog
|
||||||
debounce
|
debounce
|
||||||
filterAndAppend
|
filterAndAppend
|
||||||
formatDate
|
|
||||||
getOwnTab
|
getOwnTab
|
||||||
getStyleWithNoCode
|
getStyleWithNoCode
|
||||||
handleUpdateInstalled
|
handleUpdateInstalled
|
||||||
|
@ -25,8 +24,6 @@
|
||||||
showFiltersStats
|
showFiltersStats
|
||||||
sorter
|
sorter
|
||||||
t
|
t
|
||||||
template
|
|
||||||
tWordBreak
|
|
||||||
VIVALDI
|
VIVALDI
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
@ -187,7 +184,7 @@ function showStyles(styles = [], matchUrlIds) {
|
||||||
function createStyleElement({style, name: nameLC}) {
|
function createStyleElement({style, name: nameLC}) {
|
||||||
// query the sub-elements just once, then reuse the references
|
// query the sub-elements just once, then reuse the references
|
||||||
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
|
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
|
||||||
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
|
const entry = t.template[`style${newUI.enabled ? 'Compact' : ''}`];
|
||||||
createStyleElement.parts = {
|
createStyleElement.parts = {
|
||||||
newUI: newUI.enabled,
|
newUI: newUI.enabled,
|
||||||
entry,
|
entry,
|
||||||
|
@ -197,7 +194,7 @@ function createStyleElement({style, name: nameLC}) {
|
||||||
editLink: $('.style-edit-link', entry) || {},
|
editLink: $('.style-edit-link', entry) || {},
|
||||||
editHrefBase: 'edit.html?id=',
|
editHrefBase: 'edit.html?id=',
|
||||||
homepage: $('.homepage', entry),
|
homepage: $('.homepage', entry),
|
||||||
homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
|
homepageIcon: t.template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
|
||||||
appliesTo: $('.applies-to', entry),
|
appliesTo: $('.applies-to', entry),
|
||||||
targets: $('.targets', entry),
|
targets: $('.targets', entry),
|
||||||
expander: $('.expander', entry),
|
expander: $('.expander', entry),
|
||||||
|
@ -215,7 +212,7 @@ function createStyleElement({style, name: nameLC}) {
|
||||||
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||||
const name = style.customName || style.name;
|
const name = style.customName || style.name;
|
||||||
parts.checker.checked = style.enabled;
|
parts.checker.checked = style.enabled;
|
||||||
parts.nameLink.textContent = tWordBreak(name);
|
parts.nameLink.textContent = t.breakWord(name);
|
||||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||||
parts.homepage.href = parts.homepage.title = style.url || '';
|
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||||
if (!newUI.enabled) {
|
if (!newUI.enabled) {
|
||||||
|
@ -243,10 +240,10 @@ function createStyleElement({style, name: nameLC}) {
|
||||||
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
||||||
}
|
}
|
||||||
if (style.updateUrl && newUI.enabled) {
|
if (style.updateUrl && newUI.enabled) {
|
||||||
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
$('.actions', entry).appendChild(t.template.updaterIcons.cloneNode(true));
|
||||||
}
|
}
|
||||||
if (configurable && newUI.enabled) {
|
if (configurable && newUI.enabled) {
|
||||||
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
|
$('.actions', entry).appendChild(t.template.configureIcon.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
createStyleTargetsElement({entry, style});
|
createStyleTargetsElement({entry, style});
|
||||||
|
@ -287,12 +284,12 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
|
||||||
el = next;
|
el = next;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const element = template.appliesToTarget.cloneNode(true);
|
const element = t.template.appliesToTarget.cloneNode(true);
|
||||||
if (!newUI.enabled) {
|
if (!newUI.enabled) {
|
||||||
if (numTargets === maxTargets) {
|
if (numTargets === maxTargets) {
|
||||||
container = container.appendChild(template.extraAppliesTo.cloneNode(true));
|
container = container.appendChild(t.template.extraAppliesTo.cloneNode(true));
|
||||||
} else if (numTargets > 0) {
|
} else if (numTargets > 0) {
|
||||||
container.appendChild(template.appliesToSeparator.cloneNode(true));
|
container.appendChild(t.template.appliesToSeparator.cloneNode(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
element.dataset.type = type;
|
element.dataset.type = type;
|
||||||
|
@ -311,7 +308,7 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
|
||||||
if (entryTargets.firstElementChild) {
|
if (entryTargets.firstElementChild) {
|
||||||
entryTargets.textContent = '';
|
entryTargets.textContent = '';
|
||||||
}
|
}
|
||||||
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
|
entryTargets.appendChild(t.template.appliesToEverything.cloneNode(true));
|
||||||
}
|
}
|
||||||
entry.classList.toggle('global', !numTargets);
|
entry.classList.toggle('global', !numTargets);
|
||||||
entry._allTargetsRendered = allTargetsRendered;
|
entry._allTargetsRendered = allTargetsRendered;
|
||||||
|
@ -525,7 +522,7 @@ Object.assign(handleEvent, {
|
||||||
{prop: 'installDate', name: 'dateInstalled'},
|
{prop: 'installDate', name: 'dateInstalled'},
|
||||||
{prop: 'updateDate', name: 'dateUpdated'},
|
{prop: 'updateDate', name: 'dateUpdated'},
|
||||||
].map(({prop, name}) =>
|
].map(({prop, name}) =>
|
||||||
t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
|
t(name) + ': ' + (t.formatDate(entry.styleMeta[prop]) || '—')).join('\n');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
/* global focusAccessibility moveFocus $ $create t tHTML animateElement */
|
/* global
|
||||||
|
$
|
||||||
|
$create
|
||||||
|
animateElement
|
||||||
|
focusAccessibility
|
||||||
|
moveFocus
|
||||||
|
t
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {String} params.title
|
* @param {String} params.title
|
||||||
* @param {String|Node|Object|Array<String|Node|Object>} params.contents
|
* @param {String|Node|Object|Array<String|Node|Object>} params.contents
|
||||||
* a string gets parsed via tHTML,
|
* a string gets parsed via t.HTML,
|
||||||
* a non-string is passed as is to $create()
|
* a non-string is passed as is to $create()
|
||||||
* @param {String} [params.className]
|
* @param {String} [params.className]
|
||||||
* CSS class name of the message box element
|
* CSS class name of the message box element
|
||||||
|
@ -116,7 +123,7 @@ function messageBox({
|
||||||
$create('SVG:path', {d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
|
$create('SVG:path', {d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
|
||||||
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
|
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
|
||||||
}))),
|
}))),
|
||||||
$create(`#${id}-contents`, tHTML(contents)),
|
$create(`#${id}-contents`, t.HTML(contents)),
|
||||||
$create(`#${id}-buttons`,
|
$create(`#${id}-buttons`,
|
||||||
buttons.map((content, buttonIndex) => content &&
|
buttons.map((content, buttonIndex) => content &&
|
||||||
$create('button', Object.assign({
|
$create('button', Object.assign({
|
||||||
|
|
|
@ -1,7 +1,23 @@
|
||||||
/* global configDialog hotkeys msg
|
/* global
|
||||||
getActiveTab CHROME FIREFOX URLS API onDOMready $ $$ prefs
|
$
|
||||||
setupLivePrefs template t $create animateElement
|
$$
|
||||||
tryJSONparse CHROME_HAS_BORDER_BUG */
|
$create
|
||||||
|
animateElement
|
||||||
|
API
|
||||||
|
CHROME
|
||||||
|
CHROME_HAS_BORDER_BUG
|
||||||
|
configDialog
|
||||||
|
FIREFOX
|
||||||
|
getActiveTab
|
||||||
|
hotkeys
|
||||||
|
msg
|
||||||
|
onDOMready
|
||||||
|
prefs
|
||||||
|
setupLivePrefs
|
||||||
|
t
|
||||||
|
tryJSONparse
|
||||||
|
URLS
|
||||||
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -165,7 +181,7 @@ function initPopup(frames) {
|
||||||
setTimeout(ping, 100, tab, --retryCountdown);
|
setTimeout(ping, 100, tab, --retryCountdown);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const info = template.unreachableInfo;
|
const info = t.template.unreachableInfo;
|
||||||
if (!FIREFOX) {
|
if (!FIREFOX) {
|
||||||
// Chrome "Allow access to file URLs" in chrome://extensions message
|
// Chrome "Allow access to file URLs" in chrome://extensions message
|
||||||
info.appendChild($create('p', t('unreachableFileHint')));
|
info.appendChild($create('p', t('unreachableFileHint')));
|
||||||
|
@ -204,7 +220,7 @@ function createWriterElement(frame) {
|
||||||
const targets = $create('span');
|
const targets = $create('span');
|
||||||
|
|
||||||
// For this URL
|
// For this URL
|
||||||
const urlLink = template.writeStyle.cloneNode(true);
|
const urlLink = t.template.writeStyle.cloneNode(true);
|
||||||
const isAboutBlank = url === ABOUT_BLANK;
|
const isAboutBlank = url === ABOUT_BLANK;
|
||||||
Object.assign(urlLink, {
|
Object.assign(urlLink, {
|
||||||
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
|
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
|
||||||
|
@ -233,7 +249,7 @@ function createWriterElement(frame) {
|
||||||
if (domains.length > 1 && numParts === 1) {
|
if (domains.length > 1 && numParts === 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const domainLink = template.writeStyle.cloneNode(true);
|
const domainLink = t.template.writeStyle.cloneNode(true);
|
||||||
Object.assign(domainLink, {
|
Object.assign(domainLink, {
|
||||||
href: 'edit.html?domain=' + encodeURIComponent(domain),
|
href: 'edit.html?domain=' + encodeURIComponent(domain),
|
||||||
textContent: numParts > 2 ? domain.split('.')[0] : domain,
|
textContent: numParts > 2 ? domain.split('.')[0] : domain,
|
||||||
|
@ -322,7 +338,7 @@ function showStyles(frameResults) {
|
||||||
if (entries.size) {
|
if (entries.size) {
|
||||||
resortEntries([...entries.values()]);
|
resortEntries([...entries.values()]);
|
||||||
} else {
|
} else {
|
||||||
installed.appendChild(template.noStyles);
|
installed.appendChild(t.template.noStyles);
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new Event('showStyles:done'));
|
window.dispatchEvent(new Event('showStyles:done'));
|
||||||
}
|
}
|
||||||
|
@ -337,7 +353,7 @@ function resortEntries(entries) {
|
||||||
function createStyleElement(style) {
|
function createStyleElement(style) {
|
||||||
let entry = $.entry(style);
|
let entry = $.entry(style);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = template.style.cloneNode(true);
|
entry = t.template.style.cloneNode(true);
|
||||||
entry.setAttribute('style-id', style.id);
|
entry.setAttribute('style-id', style.id);
|
||||||
Object.assign(entry, {
|
Object.assign(entry, {
|
||||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||||
|
@ -384,7 +400,7 @@ function createStyleElement(style) {
|
||||||
|
|
||||||
$('.delete', entry).onclick = handleEvent.delete;
|
$('.delete', entry).onclick = handleEvent.delete;
|
||||||
|
|
||||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
const indicator = t.template.regexpProblemIndicator.cloneNode(true);
|
||||||
indicator.appendChild(document.createTextNode('!'));
|
indicator.appendChild(document.createTextNode('!'));
|
||||||
indicator.onclick = handleEvent.indicator;
|
indicator.onclick = handleEvent.indicator;
|
||||||
$('.main-controls', entry).appendChild(indicator);
|
$('.main-controls', entry).appendChild(indicator);
|
||||||
|
@ -587,7 +603,7 @@ Object.assign(handleEvent, {
|
||||||
|
|
||||||
indicator(event) {
|
indicator(event) {
|
||||||
const entry = handleEvent.getClickedStyleElement(event);
|
const entry = handleEvent.getClickedStyleElement(event);
|
||||||
const info = template.regexpProblemExplanation.cloneNode(true);
|
const info = t.template.regexpProblemExplanation.cloneNode(true);
|
||||||
$.remove('#' + info.id);
|
$.remove('#' + info.id);
|
||||||
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
|
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
|
||||||
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
|
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
|
||||||
|
@ -684,7 +700,7 @@ function handleDelete(id) {
|
||||||
const el = $.entry(id);
|
const el = $.entry(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.remove();
|
el.remove();
|
||||||
if (!$('.entry')) installed.appendChild(template.noStyles);
|
if (!$('.entry')) installed.appendChild(t.template.noStyles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,9 +730,9 @@ async function getStyleDataMerged(url, id) {
|
||||||
function blockPopup(isBlocked = true) {
|
function blockPopup(isBlocked = true) {
|
||||||
document.body.classList.toggle('blocked', isBlocked);
|
document.body.classList.toggle('blocked', isBlocked);
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
document.body.prepend(template.unavailableInfo);
|
document.body.prepend(t.template.unavailableInfo);
|
||||||
} else {
|
} else {
|
||||||
template.unavailableInfo.remove();
|
t.template.unavailableInfo.remove();
|
||||||
template.noStyles.remove();
|
t.template.noStyles.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
/* global URLS tabURL handleEvent $ $$ prefs template FIREFOX debounce
|
/* global
|
||||||
$create t API tWordBreak formatDate tryCatch download */
|
$
|
||||||
|
$$
|
||||||
|
$create
|
||||||
|
API
|
||||||
|
debounce
|
||||||
|
download
|
||||||
|
FIREFOX
|
||||||
|
handleEvent
|
||||||
|
prefs
|
||||||
|
t
|
||||||
|
tabURL
|
||||||
|
tryCatch
|
||||||
|
URLS
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
window.addEventListener('showStyles:done', () => {
|
window.addEventListener('showStyles:done', () => {
|
||||||
|
@ -103,7 +116,7 @@ window.addEventListener('showStyles:done', () => {
|
||||||
const navOnClick = {prev, next};
|
const navOnClick = {prev, next};
|
||||||
for (const place of ['top', 'bottom']) {
|
for (const place of ['top', 'bottom']) {
|
||||||
const nav = $(`.search-results-nav[data-type="${place}"]`);
|
const nav = $(`.search-results-nav[data-type="${place}"]`);
|
||||||
nav.appendChild(template.searchNav.cloneNode(true));
|
nav.appendChild(t.template.searchNav.cloneNode(true));
|
||||||
dom.nav[place] = nav;
|
dom.nav[place] = nav;
|
||||||
for (const child of $$('[data-type]', nav)) {
|
for (const child of $$('[data-type]', nav)) {
|
||||||
const type = child.dataset.type;
|
const type = child.dataset.type;
|
||||||
|
@ -257,7 +270,7 @@ window.addEventListener('showStyles:done', () => {
|
||||||
* @returns {Node}
|
* @returns {Node}
|
||||||
*/
|
*/
|
||||||
function createSearchResultNode(result) {
|
function createSearchResultNode(result) {
|
||||||
const entry = template.searchResult.cloneNode(true);
|
const entry = t.template.searchResult.cloneNode(true);
|
||||||
const {
|
const {
|
||||||
i: id,
|
i: id,
|
||||||
n: name,
|
n: name,
|
||||||
|
@ -276,7 +289,7 @@ window.addEventListener('showStyles:done', () => {
|
||||||
href: URLS.usoArchive + `?category=${category}&style=${id}`,
|
href: URLS.usoArchive + `?category=${category}&style=${id}`,
|
||||||
});
|
});
|
||||||
$('.search-result-title span', entry).textContent =
|
$('.search-result-title span', entry).textContent =
|
||||||
tWordBreak(name.length < 300 ? name : name.slice(0, 300) + '...');
|
t.breakWord(name.length < 300 ? name : name.slice(0, 300) + '...');
|
||||||
// screenshot
|
// screenshot
|
||||||
const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`;
|
const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`;
|
||||||
Object.assign($('.search-result-screenshot', entry), {
|
Object.assign($('.search-result-screenshot', entry), {
|
||||||
|
@ -303,7 +316,7 @@ window.addEventListener('showStyles:done', () => {
|
||||||
// time
|
// time
|
||||||
Object.assign($('[data-type="updated"] time', entry), {
|
Object.assign($('[data-type="updated"] time', entry), {
|
||||||
dateTime: updateTime * 1000,
|
dateTime: updateTime * 1000,
|
||||||
textContent: formatDate(updateTime * 1000),
|
textContent: t.formatDate(updateTime * 1000),
|
||||||
});
|
});
|
||||||
// totals
|
// totals
|
||||||
$('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls);
|
$('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user