diff --git a/.eslintrc b/.eslintrc index 6eb52b33..0d866fff 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ globals: tHTML: false tNodeList: false tDocLoader: false + tWordBreak: false # dom.js onDOMready: false scrollElementIntoView: false diff --git a/edit/edit.js b/edit/edit.js index 255cc975..a050e2fe 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -75,30 +75,6 @@ function preinit() { 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css' })); - // forcefully break long labels in aligned options to prevent the entire block layout from breaking - onDOMready().then(() => new Promise(requestAnimationFrame)).then(() => { - const maxWidth2ndChild = $$('#options .aligned > :nth-child(2)') - .sort((a, b) => b.offsetWidth - a.offsetWidth)[0].offsetWidth; - const widthFor1stChild = $('#options').offsetWidth - maxWidth2ndChild; - if (widthFor1stChild > 50) { - for (const el of $$('#options .aligned > :nth-child(1)')) { - if (el.offsetWidth > widthFor1stChild) { - el.style.cssText = 'word-break: break-all; hyphens: auto;'; - } - } - } else { - const width = $('#options').clientWidth; - document.head.appendChild($create('style', ` - #options .aligned > nth-child(1) { - max-width: 70px; - } - #options .aligned > nth-child(2) { - max-width: ${width - 70}px; - } - `)); - } - }); - if (chrome.windows) { queryTabs({currentWindow: true}).then(tabs => { const windowId = tabs[0].windowId; diff --git a/js/localization.js b/js/localization.js index 79b242e9..c0c5a28f 100644 --- a/js/localization.js +++ b/js/localization.js @@ -48,26 +48,14 @@ function tHTML(html, tag) { function tNodeList(nodes) { const PREFIX = 'i18n-'; + for (let n = nodes.length; --n >= 0;) { const node = nodes[n]; - // skip non-ELEMENT_NODE - if (node.nodeType !== 1) { + if (node.nodeType !== Node.ELEMENT_NODE) { continue; } if (node.localName === 'template') { - const elements = node.content.querySelectorAll('*'); - tNodeList(elements); - template[node.dataset.id] = elements[0]; - // compress inter-tag whitespace to reduce number of DOM nodes by 25% - const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT); - const toRemove = []; - while (walker.nextNode()) { - const textNode = walker.currentNode; - if (!textNode.nodeValue.trim()) { - toRemove.push(textNode); - } - } - toRemove.forEach(el => el.remove()); + createTemplate(node); continue; } for (let a = node.attributes.length; --a >= 0;) { @@ -78,26 +66,71 @@ function tNodeList(nodes) { } const type = name.substr(PREFIX.length); const value = t(attr.value); + let toInsert, before; switch (type) { + case 'word-break': + // we already know that: hasWordBreak + break; case 'text': - node.insertBefore(document.createTextNode(value), node.firstChild); - break; + before = node.firstChild; + // fallthrough to text-append case 'text-append': - node.appendChild(document.createTextNode(value)); + toInsert = createText(value); break; - case 'html': - // localized strings only allow having text nodes and links - node.textContent = ''; - [...tHTML(value, 'div').childNodes] - .filter(a => a.nodeType === a.TEXT_NODE || a.tagName === 'A') - .forEach(n => node.appendChild(n)); + case 'html': { + toInsert = createHtml(value); break; + } default: node.setAttribute(type, value); } + tDocLoader.pause(); + if (toInsert) { + node.insertBefore(toInsert, before || null); + } node.removeAttribute(name); } } + + function createTemplate(node) { + const elements = node.content.querySelectorAll('*'); + tNodeList(elements); + template[node.dataset.id] = elements[0]; + // compress inter-tag whitespace to reduce number of DOM nodes by 25% + const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT); + const toRemove = []; + while (walker.nextNode()) { + const textNode = walker.currentNode; + if (!textNode.nodeValue.trim()) { + toRemove.push(textNode); + } + } + tDocLoader.pause(); + toRemove.forEach(el => el.remove()); + } + + function createText(str) { + return document.createTextNode(tWordBreak(str)); + } + + function createHtml(value) { + // bar are the only recognizable HTML elements + const rx = /(?:]*)>([^<]*)<\/a>)?([^<]*)/gi; + const bin = document.createDocumentFragment(); + for (let m; (m = rx.exec(value)) && m[0];) { + const [, linkParams, linkText, nextText] = m; + if (linkText) { + const href = /\bhref\s*=\s*(\S+)/.exec(linkParams); + const a = bin.appendChild(document.createElement('a')); + a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || ''; + a.appendChild(createText(linkText)); + } + if (nextText) { + bin.appendChild(createText(nextText)); + } + } + return bin; + } } @@ -115,33 +148,50 @@ function tDocLoader() { t.cache = {browserUIlanguage: UIlang}; localStorage.L10N = JSON.stringify(t.cache); } - const cacheLength = Object.keys(t.cache).length; - // localize HEAD - tNodeList(document.getElementsByTagName('*')); + Object.assign(tDocLoader, { + observer: new MutationObserver(process), + start() { + if (!tDocLoader.observing) { + tDocLoader.observing = true; + tDocLoader.observer.observe(document, {subtree: true, childList: true}); + } + }, + stop() { + tDocLoader.pause(); + document.removeEventListener('DOMContentLoaded', onLoad); + }, + pause() { + if (tDocLoader.observing) { + tDocLoader.observing = false; + tDocLoader.observer.disconnect(); + } + }, + }); - // localize BODY - const process = mutations => { + tNodeList(document.getElementsByTagName('*')); + tDocLoader.start(); + document.addEventListener('DOMContentLoaded', onLoad); + + function process(mutations) { for (const mutation of mutations) { tNodeList(mutation.addedNodes); } - }; - const observer = new MutationObserver(process); - const onLoad = () => { + tDocLoader.start(); + } + + function onLoad() { tDocLoader.stop(); - process(observer.takeRecords()); + process(tDocLoader.observer.takeRecords()); if (cacheLength !== Object.keys(t.cache).length) { localStorage.L10N = JSON.stringify(t.cache); } - }; - tDocLoader.start = () => { - observer.observe(document, {subtree: true, childList: true}); - }; - tDocLoader.stop = () => { - observer.disconnect(); - document.removeEventListener('DOMContentLoaded', onLoad); - }; - tDocLoader.start(); - document.addEventListener('DOMContentLoaded', onLoad); + } +} + + +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(/[\d\w\u0080-\uFFFF]{10}|((?!\s)\W){10}/g, '$&\u00AD'); } diff --git a/manage/manage.js b/manage/manage.js index 4069337a..506bdc31 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -179,7 +179,7 @@ function createStyleElement({style, name}) { } const parts = createStyleElement.parts; parts.checker.checked = style.enabled; - parts.nameLink.textContent = style.name; + parts.nameLink.textContent = tWordBreak(style.name); parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.homepage.href = parts.homepage.title = style.url || '';