From c0c60fb7a2ae27964bed389dd980236ef0359963 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 00:12:09 +0300 Subject: [PATCH] add a terse invocation syntax for $element and rename it to $create --- .eslintrc | 5 +- edit/applies-to-line-widget.js | 68 +++++---------- edit/codemirror-editing-hooks.js | 15 +--- edit/colorpicker-helper.js | 3 +- edit/edit.js | 18 ++-- edit/lint.js | 134 +++++++++-------------------- edit/regexp-tester.js | 50 +++++------ edit/show-keymap-help.js | 2 +- edit/source-editor.js | 13 +-- install-usercss/install-usercss.js | 74 ++++++---------- js/dom.js | 87 +++++++++++++++---- manage/config-dialog.js | 88 ++++++++----------- manage/import-export.js | 24 +++--- manage/incremental-search.js | 3 +- manage/manage.js | 4 +- manage/updater-ui.js | 5 +- msgbox/msgbox.js | 40 ++++----- popup/hotkeys.js | 8 +- 18 files changed, 275 insertions(+), 366 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3d844c56..6eb52b33 100644 --- a/.eslintrc +++ b/.eslintrc @@ -55,7 +55,8 @@ globals: animateElement: false $: false $$: false - $element: false + $create: false + $createLink: false # prefs.js prefs: false setupLivePrefs: false @@ -236,7 +237,7 @@ rules: one-var: [0] operator-assignment: [2, always] operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] - padded-blocks: [2, never] + padded-blocks: [0] prefer-numeric-literals: [2] prefer-rest-params: [0] prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}] diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js index 34340626..f3687213 100644 --- a/edit/applies-to-line-widget.js +++ b/edit/applies-to-line-widget.js @@ -25,53 +25,25 @@ function createAppliesToLineWidget(cm) { TPL = { container: - $element({className: 'applies-to', appendChild: [ - $element({tag: 'label', appendChild: t('appliesLabel')}), - $element({tag: 'ul', className: 'applies-to-list'}), - ]}), - listItem: $element({ - tag: 'li', - className: 'applies-to-item', - appendChild: [ - $element({ - tag: 'select', - className: 'applies-type', - appendChild: [ - [t('appliesUrlOption'), 'url'], - [t('appliesUrlPrefixOption'), 'url-prefix'], - [t('appliesDomainOption'), 'domain'], - [t('appliesRegexpOption'), 'regexp'] - ].map(([textContent, value]) => $element({ - tag: 'option', - value, - textContent, - })), - }), - $element({ - tag: 'input', - className: 'applies-value', - }), - $element({ - tag: 'button', - className: 'test-regexp', - textContent: t('styleRegexpTestButton'), - }), - $element({ - tag: 'button', - className: 'remove-applies-to', - textContent: t('appliesRemove'), - }), - $element({ - tag: 'button', - className: 'add-applies-to', - textContent: t('appliesAdd'), - }) - ]}), - appliesToEverything: $element({ - tag: 'li', - className: 'applies-to-everything', - textContent: t('appliesToEverything'), - }), + $create('div.applies-to', [ + $create('label', t('appliesLabel')), + $create('ul.applies-to-list'), + ]), + listItem: + $create('li.applies-to-item', [ + $create('select.applies-type', [ + $create('option', {value: 'url'}, t('appliesUrlOption')), + $create('option', {value: 'url-prefix'}, t('appliesUrlPrefixOption')), + $create('option', {value: 'domain'}, t('appliesDomainOption')), + $create('option', {value: 'regexp'}, t('appliesRegexpOption')), + ]), + $create('input.applies-value'), + $create('button.test-regexp', t('styleRegexpTestButton')), + $create('button.remove-applies-to', t('appliesRemove')), + $create('button.add-applies-to', t('appliesAdd')), + ]), + appliesToEverything: + $create('li.applies-to-everything', t('appliesToEverything')), }; CLICK_ROUTE = { @@ -164,7 +136,7 @@ function createAppliesToLineWidget(cm) { } }; - styleVariables = $element({tag: 'style'}); + styleVariables = $create('style'); fromLine = 0; toLine = cm.doc.size; diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index a47b2e12..8071d7d2 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -115,10 +115,8 @@ onDOMready().then(() => { if (option.type === 'checkbox') { option = (option.labels || [])[0] || option.nextElementSibling || option; } - progress = document.body.appendChild($element({ - className: 'set-option-progress', - targetElement: option, - })); + progress = document.body.appendChild( + $create('.set-option-progress', {targetElement: option})); } } if (progress) { @@ -222,12 +220,7 @@ onDOMready().then(() => { break; } // avoid flicker: wait for the second stylesheet to load, then apply the theme - document.head.appendChild($element({ - tag: 'link', - id: 'cm-theme2', - rel: 'stylesheet', - href: url - })); + document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url})); setTimeout(() => { CodeMirror.setOption(option, value); themeLink.remove(); @@ -287,7 +280,7 @@ onDOMready().then(() => { function optionsFromArray(parent, options) { const fragment = document.createDocumentFragment(); for (const opt of options) { - fragment.appendChild($element({tag: 'option', textContent: opt})); + fragment.appendChild($create('option', opt)); } parent.appendChild(fragment); } diff --git a/edit/colorpicker-helper.js b/edit/colorpicker-helper.js index 51d95e3b..ef53452a 100644 --- a/edit/colorpicker-helper.js +++ b/edit/colorpicker-helper.js @@ -69,8 +69,7 @@ var initColorpicker = () => { } function configureColorpicker() { - const input = $element({ - tag: 'input', + const input = $create('input', { type: 'search', spellcheck: false, value: prefs.get('editor.colorpicker.hotkey'), diff --git a/edit/edit.js b/edit/edit.js index 78378c13..492d8958 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -457,22 +457,20 @@ function toMozillaFormat() { function fromMozillaFormat() { const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'), - $element({appendChild: [ - $element({ - tag: 'button', + $create([ + $create('button', { name: 'import-append', textContent: t('importAppendLabel'), title: 'Ctrl-Enter:\n' + t('importAppendTooltip'), onclick: doImport, }), - $element({ - tag: 'button', + $create('button', { name: 'import-replace', textContent: t('importReplaceLabel'), title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'), onclick: () => doImport({replaceOldStyle: true}), }), - ]})); + ])); const contents = $('.contents', popup); contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); popup.codebox.focus(); @@ -522,10 +520,8 @@ function fromMozillaFormat() { } function showError(errors) { - showHelp(t('styleFromMozillaFormatError'), $element({ - tag: 'pre', - textContent: Array.isArray(errors) ? errors.join('\n') : errors, - })); + showHelp(t('styleFromMozillaFormatError'), + $create('pre', Array.isArray(errors) ? errors.join('\n') : errors)); } } @@ -617,7 +613,7 @@ function showCodeMirrorPopup(title, html, options) { function setGlobalProgress(done, total) { const progressElement = $('#global-progress') || - total && document.body.appendChild($element({id: 'global-progress'})); + total && document.body.appendChild($create('#global-progress')); if (total) { const progress = (done / Math.max(done, total) * 100).toFixed(1); progressElement.style.borderLeftWidth = progress + 'vw'; diff --git a/edit/lint.js b/edit/lint.js index f39ec685..814807b2 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -1,7 +1,6 @@ /* global CodeMirror messageBox */ /* global editors makeSectionVisible showCodeMirrorPopup showHelp */ /* global loadScript require CSSLint stylelint */ -/* global makeLink */ 'use strict'; onDOMready().then(loadLinterAssets); @@ -231,9 +230,7 @@ function updateLinter({immediately, linter = linterConfig.getName()} = {}) { cm.options.gutters = guttersOption; const el = $('.' + GUTTERS_CLASS, cm.display.gutters); if (linter && !el) { - cm.display.gutters.appendChild($element({ - className: 'CodeMirror-gutter ' + GUTTERS_CLASS - })); + cm.display.gutters.appendChild($create('.CodeMirror-gutter ' + GUTTERS_CLASS)); } else if (!linter && el) { el.remove(); } @@ -281,9 +278,8 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) { const newMarkers = lintState.stylusMarkers = new Map(); const oldText = (lintState.body || {}).textContentCached || ''; const activeLine = cm.getCursor().line; - const body = !(lintState.marked || {}).length ? {} : $element({ - tag: 'tbody', - appendChild: lintState.marked.map(mark => { + const body = !(lintState.marked || {}).length ? {} : + $create('tbody', lintState.marked.map(mark => { const info = mark.__annotation; const {line, ch} = info.from; const isActiveLine = line === activeLine; @@ -294,27 +290,15 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) { oldMarkers.delete(pos); } newMarkers.set(pos, message); - return $element({ - tag: 'tr', - className: info.severity, - appendChild: [ - $element({ - tag: 'td', - attributes: {role: 'severity'}, - dataset: {rule: info.rule}, - appendChild: $element({ - className: 'CodeMirror-lint-marker-' + info.severity, - textContent: info.severity, - }), - }), - $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}), - $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}), - $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}), - $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}), - ], - }); - }) - }); + return $create(`tr.${info.severity}`, [ + $create('td', {attributes: {role: 'severity'}, dataset: {rule: info.rule}}, + $create('.CodeMirror-lint-marker-' + info.severity, info.severity)), + $create('td', {attributes: {role: 'line'}}, line + 1), + $create('td', {attributes: {role: 'sep'}}, ':'), + $create('td', {attributes: {role: 'col'}}, ch + 1), + $create('td', {attributes: {role: 'message'}, title}, message), + ]); + })); body.textContentCached = body.textContent || ''; lintState.body = body.textContentCached && body; result.changed |= oldText !== body.textContentCached; @@ -340,14 +324,10 @@ function renderLintReport(someBlockChanged) { if (!body) { return; } - const newBlock = $element({ - tag: 'table', - appendChild: [ - $element({tag: 'caption', textContent: label + ' ' + (index + 1)}), - body, - ], - cm, - }); + const newBlock = $create('table', {cm}, [ + $create('caption', label + ' ' + (index + 1)), + body, + ]); newContent.appendChild(newBlock); issueCount += newBlock.rows.length; @@ -388,35 +368,29 @@ function showLintHelp() { : 'https://github.com/CSSLint/csslint/issues/535'; let headerLink, template; if (linter === 'csslint') { - headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'); + headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'); template = ruleID => { const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID); return rule && - $element({tag: 'li', appendChild: [ - $element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}), - $element({tag: 'br'}), + $create('li', [ + $create('b', $createLink(rule.url || baseUrl, rule.name)), + $create('br'), rule.desc, - ]}); + ]); }; } else { - headerLink = makeLink(baseUrl, 'stylelint'); + headerLink = $createLink(baseUrl, 'stylelint'); template = rule => - $element({ - tag: 'li', - appendChild: makeLink(baseUrl + rule, rule), - }); + $create('li', + $createLink(baseUrl + rule, rule)); } const header = t('linterIssuesHelp', '\x01').split('\x01'); const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule)); return showHelp(t('linterIssues'), - $element({appendChild: [ + $create([ header[0], headerLink, header[1], - $element({ - tag: 'ul', - className: 'rules', - appendChild: [...activeRules.values()].map(template), - }), - ]}) + $create('ul.rules', [...activeRules.values()].map(template)), + ]) ); } @@ -465,41 +439,21 @@ function setupLinterPopup(config) { }); function makeFooter() { - const makeButton = (className, onclick, text, options = {}) => - $element(Object.assign(options, { - className, - onclick, - tag: 'button', - type: 'button', - textContent: t(text), - })); - return $element({ - appendChild: [ - $element({ - tag: 'p', - appendChild: [ - t('linterRulesLink') + ' ', - $element({ - tag: 'a', - target: '_blank', - href: linter === 'stylelint' - ? 'https://stylelint.io/user-guide/rules/' - : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', - textContent: linterTitle - }), - linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '' - ] - }), - makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}), - makeButton('cancel', cancel, 'confirmClose'), - makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}), - $element({ - tag: 'span', - className: 'saved-message', - textContent: t('genericSavedMessage') - }) - ] - }); + return $create('div', [ + $create('p', [ + t('linterRulesLink') + ' ', + $createLink( + linter === 'stylelint' + ? 'https://stylelint.io/user-guide/rules/' + : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', + linterTitle), + linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '', + ]), + $create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')), + $create('button.cancel', {onclick: cancel}, t('confirmClose')), + $create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')), + $create('span.saved-message', t('genericSavedMessage')), + ]); } function save(event) { @@ -516,9 +470,7 @@ function setupLinterPopup(config) { if (invalid.length) { showLinterErrorMessage(linter, [ t('linterInvalidConfigError'), - $element({tag: 'ul', appendChild: invalid.map(name => - $element({tag: 'li', textContent: name})), - }), + $create('ul', invalid.map(name => $create('li', name))), ], popup); return; } diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index a11527c1..4c5e63ff 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -34,7 +34,7 @@ var regExpTester = (() => { if (!isInit) { init(); } - showHelp('', $element({className: 'regexp-report'})); + showHelp('', $create('.regexp-report')); } else if (!state && isShown()) { if (isInit) { uninit(); @@ -108,19 +108,19 @@ var regExpTester = (() => { const faviconUrl = url.startsWith(URLS.ownOrigin) ? OWN_ICON : GET_FAVICON_URL + new URL(url).hostname; - const icon = $element({tag: 'img', src: faviconUrl}); + const icon = $create('img', {src: faviconUrl}); if (match.text.length === url.length) { - full.push($element({appendChild: [ + full.push($create('div', [ icon, url, - ]})); + ])); } else { - partial.push($element({appendChild: [ + partial.push($create('div', [ icon, url.substr(0, match.pos), - $element({tag: 'mark', textContent: match.text}), + $create('mark', match.text), url.substr(match.pos + match.text.length), - ]})); + ])); } } if (full.length) { @@ -131,33 +131,28 @@ var regExpTester = (() => { } } // render stats - const report = $element({className: 'regexp-report'}); - const br = $element({tag: 'br'}); + const report = $create('.regexp-report'); + const br = $create('br'); for (const type in stats) { // top level groups: full, partial, none, invalid const {label, data} = stats[type]; if (!data.length) { continue; } - const block = report.appendChild($element({ - tag: 'details', - open: true, - dataset: {type}, - appendChild: $element({tag: 'summary', appendChild: label}), - })); + const block = report.appendChild( + $create('details', {open: true, dataset: {type}}, [ + $create('summary', label), + ])); // 2nd level: regexp text for (const {text, urls} of data) { if (urls) { // type is partial or full - block.appendChild($element({ - tag: 'details', - open: true, - appendChild: [ - $element({tag: 'summary', textContent: text}), + block.appendChild( + $create('details', {open: true}, [ + $create('summary', text), // 3rd level: tab urls ...urls, - ], - })); + ])); } else { // type is none or invalid block.appendChild(document.createTextNode(text)); @@ -165,12 +160,11 @@ var regExpTester = (() => { } } } - report.appendChild($element({ - tag: 'p', - className: 'regexp-report-note', - appendChild: t('styleRegexpTestNote').split(/(\\+)/) - .map(s => s.startsWith('\\') ? $element({tag: 'code', textContent: s}) : s), - })); + report.appendChild( + $create('p.regexp-report-note', + t('styleRegexpTestNote') + .split(/(\\+)/) + .map(s => (s.startsWith('\\') ? $create('code', s) : s)))); showHelp(t('styleRegexpTestTitle'), report); report.onclick = event => { diff --git a/edit/show-keymap-help.js b/edit/show-keymap-help.js index 8c73cbfa..0929d9d0 100644 --- a/edit/show-keymap-help.js +++ b/edit/show-keymap-help.js @@ -64,7 +64,7 @@ function showKeyMapHelp() { if (index > offset) { cell.appendChild(document.createTextNode(text.substring(offset, index))); } - cell.appendChild($element({tag: 'mark', textContent: match})); + cell.appendChild($create('mark', match)); offset = index + match.length; }); if (offset + 1 !== text.length) { diff --git a/edit/source-editor.js b/edit/source-editor.js index b09bf838..5b1d5385 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -1,7 +1,7 @@ /* global CodeMirror dirtyReporter initLint */ /* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ /* global editors linterConfig updateLinter regExpTester mozParser */ -/* global makeLink createAppliesToLineWidget messageBox */ +/* global createAppliesToLineWidget messageBox */ 'use strict'; function createSourceEditor(style) { @@ -12,9 +12,7 @@ function createSourceEditor(style) { $('#name').disabled = true; $('#mozilla-format-container').remove(); $('#sections').textContent = ''; - $('#sections').appendChild( - $element({className: 'single-editor'}) - ); + $('#sections').appendChild($create('.single-editor')); const dirty = dirtyReporter(); dirty.onChange(() => { @@ -236,15 +234,12 @@ function createSourceEditor(style) { return; } const contents = Array.isArray(err) ? - $element({tag: 'pre', textContent: err.join('\n')}) : + $create('pre', err.join('\n')) : [String(err)]; if (Number.isInteger(err.index)) { const pos = cm.posFromIndex(err.index); contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`; - contents.push($element({ - tag: 'pre', - textContent: drawLinePointer(pos) - })); + contents.push($create('pre', drawLinePointer(pos))); } messageBox.alert(contents); }); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index c3c98833..3d347955 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -1,4 +1,4 @@ -/* global CodeMirror semverCompare makeLink closeCurrentTab */ +/* global CodeMirror semverCompare closeCurrentTab */ /* global messageBox download chromeLocal */ 'use strict'; @@ -44,11 +44,8 @@ setTimeout(() => { if (!installed) { - const div = $element({}); - $('.header').appendChild($element({ - className: 'lds-spinner', - appendChild: new Array(12).fill(div).map(e => e.cloneNode()), - })); + $('.header').appendChild($create('.lds-spinner', + new Array(12).fill($create('div')).map(e => e.cloneNode()))); } }, 200); @@ -101,8 +98,7 @@ $('.applies-to').textContent = ''; getAppliesTo(style).forEach(pattern => - $('.applies-to').appendChild($element({tag: 'li', textContent: pattern})) - ); + $('.applies-to').appendChild($create('li', pattern))); $('.external-link').textContent = ''; const externalLink = makeExternalLink(); @@ -125,46 +121,35 @@ const [, name, email, url] = match; const frag = document.createDocumentFragment(); if (email) { - frag.appendChild(makeLink(`mailto:${email}`, name)); + frag.appendChild($createLink(`mailto:${email}`, name)); } else { - frag.appendChild($element({ - tag: 'span', - textContent: name - })); + frag.appendChild($create('span', name)); } if (url) { - frag.appendChild(makeLink( - url, - $element({ - tag: 'svg#svg', - viewBox: '0 0 20 20', - class: 'svg-icon', - appendChild: $element({ - tag: 'svg#path', + frag.appendChild($createLink(url, + $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, + $create('SVG:path', { d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z' - }) - }) + })) )); } return frag; } function makeExternalLink() { - const urls = []; - if (data.homepageURL) { - urls.push([data.homepageURL, t('externalHomepage')]); - } - if (data.supportURL) { - urls.push([data.supportURL, t('externalSupport')]); - } - if (urls.length) { - return $element({appendChild: [ - $element({tag: 'h3', textContent: t('externalLink')}), - $element({tag: 'ul', appendChild: urls.map(args => - $element({tag: 'li', appendChild: makeLink(...args)}) - )}) - ]}); - } + const urls = [ + data.homepageURL && [data.homepageURL, t('externalHomepage')], + data.supportURL && [data.supportURL, t('externalSupport')], + ]; + return (data.homepageURL || data.supportURL) && ( + $create('div', [ + $create('h3', t('externalLink')), + $create('ul', urls.map(args => args && + $create('li', + $createLink(...args) + ) + )) + ])); } function installButtonClass() { @@ -230,26 +215,23 @@ function buildWarning(err) { const contents = Array.isArray(err) ? - $element({tag: 'pre', textContent: err.join('\n')}) : + $create('pre', err.join('\n')) : [err && err.message || err || 'Unknown error']; if (Number.isInteger(err.index)) { const pos = cm.posFromIndex(err.index); contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0]; - contents.push($element({ - tag: 'pre', - textContent: drawLinePointer(pos) - })); + contents.push($create('pre', drawLinePointer(pos))); setTimeout(() => { cm.scrollIntoView({line: pos.line + 1, ch: pos.ch}, window.innerHeight / 4); cm.setCursor(pos.line, pos.ch + 1); cm.focus(); }); } - return $element({className: 'warning', appendChild: [ + return $create('.warning', [ t('parseUsercssError'), '\n', ...contents, - ]}); + ]); } function drawLinePointer(pos) { @@ -281,7 +263,7 @@ // update UI if (versionTest < 0) { $('.actions').parentNode.insertBefore( - $element({className: 'warning', textContent: t('versionInvalidOlder')}), + $create('.warning', t('versionInvalidOlder')), $('.actions') ); } diff --git a/js/dom.js b/js/dom.js index 969dc5c0..d81c2bc7 100644 --- a/js/dom.js +++ b/js/dom.js @@ -73,8 +73,7 @@ if (!chrome.app && chrome.windows) { } const iconset = ['', 'light/'][prefs.get('iconset')] || ''; for (const size of [38, 32, 19, 16]) { - document.head.appendChild($element({ - tag: 'link', + document.head.appendChild($create('link', { rel: 'icon', href: `/images/icon/${iconset}${size}.png`, sizes: size + 'x' + size, @@ -167,35 +166,88 @@ function $$(selector, base = document) { } -function $element(opt) { - // tag: string, default 'div', may include namespace like 'ns#tag' - // appendChild: element/string or an array of elements/strings - // dataset: object - // any DOM property: assigned as is - const [ns, tag] = opt.tag && opt.tag.includes('#') - ? opt.tag.split('#') - : [null, opt.tag]; +function $create(selector = 'div', properties, children) { +/* + $create('tag#id.class.class', ?[children]) + $create('tag#id.class.class', ?textContentOrChildNode) + $create('tag#id.class.class', {properties}, ?[children]) + $create('tag#id.class.class', {properties}, ?textContentOrChildNode) + tag is 'div' by default, #id and .class are optional + + $create([children]) + + $create({propertiesAndOptions}) + $create({propertiesAndOptions}, ?[children]) + tag: string, default 'div' + appendChild: element/string or an array of elements/strings + dataset: object + any DOM property: assigned as is + + tag may include namespace like 'ns:tag' +*/ + let ns, tag, opt; + + if (typeof selector === 'string') { + if (Array.isArray(properties) || + properties instanceof Node || + typeof properties !== 'object') { + opt = {}; + children = properties; + } else { + opt = properties || {}; + } + const idStart = (selector.indexOf('#') + 1 || selector.length + 1) - 1; + const classStart = (selector.indexOf('.') + 1 || selector.length + 1) - 1; + const id = selector.slice(idStart + 1, classStart); + if (id) { + opt.id = id; + } + const cls = selector.slice(classStart + 1); + if (cls) { + opt[selector.includes(':') ? 'class' : 'className'] = + cls.includes('.') ? cls.replace(/\./g, ' ') : cls; + } + tag = selector.slice(0, Math.min(idStart, classStart)); + + } else if (Array.isArray(selector)) { + tag = 'div'; + opt = {}; + children = selector; + + } else { + opt = selector; + tag = opt.tag; + delete opt.tag; + children = opt.appendChild || properties; + delete opt.appendChild; + } + + if (tag && tag.includes(':')) { + ([ns, tag] = tag.split(':')); + } + const element = ns ? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) : document.createElement(tag || 'div'); - const children = Array.isArray(opt.appendChild) ? opt.appendChild : [opt.appendChild]; - for (const child of children) { + + for (const child of Array.isArray(children) ? children : [children]) { if (child) { element.appendChild(child instanceof Node ? child : document.createTextNode(child)); } } - delete opt.appendChild; - delete opt.tag; + if (opt.dataset) { Object.assign(element.dataset, opt.dataset); delete opt.dataset; } + if (opt.attributes) { for (const attr in opt.attributes) { element.setAttribute(attr, opt.attributes[attr]); } delete opt.attributes; } + if (ns) { for (const attr in opt) { element.setAttributeNS(null, attr, opt[attr]); @@ -203,11 +255,12 @@ function $element(opt) { } else { Object.assign(element, opt); } + return element; } -function makeLink(href = '', content) { +function $createLink(href = '', content) { const opt = { tag: 'a', target: '_blank', @@ -217,9 +270,9 @@ function makeLink(href = '', content) { Object.assign(opt, href); } else { opt.href = href; - opt.appendChild = content; } - return $element(opt); + opt.appendChild = opt.appendChild || content; + return $create(opt); } diff --git a/manage/config-dialog.js b/manage/config-dialog.js index ebb9dbe0..18ddf0eb 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -1,8 +1,9 @@ -/* global messageBox makeLink */ +/* global messageBox */ 'use strict'; function configDialog(style) { - const varsHash = deepCopy(style.usercssData.vars) || {}; + const data = style.usercssData; + const varsHash = deepCopy(data.vars) || {}; const varNames = Object.keys(varsHash); const vars = varNames.map(name => varsHash[name]); const elements = []; @@ -12,21 +13,12 @@ function configDialog(style) { renderValues(); return messageBox({ - title: `${style.name} v${style.usercssData.version}`, + title: `${style.name} v${data.version}`, className: 'config-dialog', contents: [ - $element({ - className: 'config-heading', - appendChild: style.usercssData.supportURL && makeLink({ - className: 'external-support', - href: style.usercssData.supportURL, - textContent: t('externalFeedback') - }) - }), - $element({ - className: 'config-body', - appendChild: elements - }) + $create('.config-heading', data.supportURL && + $createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))), + $create('.config-body', elements) ], buttons: [ t('confirmSave'), @@ -71,19 +63,17 @@ function configDialog(style) { continue; } invalid.push(['*' + va.name, ': ', ...error].map(e => - e[0] === '*' && $element({tag: 'b', textContent: e.slice(1)}) || e)); + e[0] === '*' && $create('b', e.slice(1)) || e)); if (bgva) { styleVars[va.name].value = deepCopy(bgva); } } if (invalid.length) { messageBox.alert([ - $element({textContent: t('usercssConfigIncomplete'), style: 'max-width: 34em'}), - $element({ - tag: 'ol', - style: 'text-align: left', - appendChild: invalid.map(msg => $element({tag: 'li', appendChild: msg})), - }), + $create('div', {style: 'max-width: 34em'}, t('usercssConfigIncomplete')), + $create('ol', {style: 'text-align: left'}, + invalid.map(msg => + $create({tag: 'li', appendChild: msg}))), ]); } return numValid && BG.usercssHelper.save(style); @@ -91,30 +81,28 @@ function configDialog(style) { function buildConfigForm() { for (const va of vars) { - let appendChild; + let children; switch (va.type) { case 'color': - appendChild = [$element({ - className: 'cm-colorview', - appendChild: va.inputColor = $element({ - va, - className: 'color-swatch', - onclick: showColorpicker, - }) - })]; + va.inputColor = $create('.color-swatch', {va, onclick: showColorpicker}); + children = [ + $create('.cm-colorview', [ + va.inputColor, + ]), + ]; break; case 'checkbox': - va.input = $element({tag: 'input', type: 'checkbox', className: 'slider'}); + va.input = $create('input.slider', {type: 'checkbox'}); va.input.onchange = () => { va.dirty = true; va.value = String(Number(va.input.checked)); }; - appendChild = [ - $element({tag: 'span', className: 'onoffswitch', appendChild: [ + children = [ + $create('span.onoffswitch', [ va.input, - $element({tag: 'span'}) - ]}) + $create('span'), + ]) ]; break; @@ -122,36 +110,30 @@ function configDialog(style) { case 'dropdown': case 'image': // TODO: a image picker input? - va.input = $element({ - tag: 'select', - appendChild: va.options.map(o => $element({ - tag: 'option', value: o.name, textContent: o.label - })) - }); + va.input = $create('select', + va.options.map(o => + $create('option', {value: o.name}, o.label))); va.input.onchange = () => { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.input]; + children = [va.input]; break; default: - va.input = $element({tag: 'input', type: 'text'}); + va.input = $create('input', {type: 'text'}); va.input.oninput = () => { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.input]; + children = [va.input]; break; } - elements.push($element({ - tag: 'label', - className: `config-${va.type}`, - appendChild: [ - $element({tag: 'span', appendChild: va.label}), - ...appendChild, - ], - })); + elements.push( + $create(`label.config-${va.type}`, [ + $create('span', va.label), + ...children, + ])); } } diff --git a/manage/import-export.js b/manage/import-export.js index b7e80031..a4c91a70 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -175,18 +175,16 @@ function importFromString(jsonString) { .map(kind => { const {ids, names, legend} = stats[kind]; const listItemsWithId = (name, i) => - $element({dataset: {id: ids[i]}, textContent: name}); + $create('div', {dataset: {id: ids[i]}}, name); const listItems = name => - $element({textContent: name}); + $create('div', name); const block = - $element({tag: 'details', dataset: {id: kind}, appendChild: [ - $element({tag: 'summary', appendChild: - $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) - }), - $element({tag: 'small', appendChild: - names.map(ids ? listItemsWithId : listItems) - }), - ]}); + $create('details', {dataset: {id: kind}}, [ + $create('summary', + $create('b', names.length + ' ' + t(legend))), + $create('small', + names.map(ids ? listItemsWithId : listItems)), + ]); return block; }); scrollTo(0, 0); @@ -308,8 +306,7 @@ $('#file-all-styles').onclick = () => { const text = JSON.stringify(styles, null, '\t'); const blob = new Blob([text], {type: 'application/json'}); const objectURL = URL.createObjectURL(blob); - let link = $element({ - tag:'a', + let link = $create('a', { href: objectURL, type: 'application/json', download: generateFileName(), @@ -319,8 +316,7 @@ $('#file-all-styles').onclick = () => { link.dispatchEvent(new MouseEvent('click')); setTimeout(() => URL.revokeObjectURL(objectURL)); } else { - const iframe = document.body.appendChild($element({ - tag: 'iframe', + const iframe = document.body.appendChild($create('iframe', { style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), })); doTimeout() diff --git a/manage/incremental-search.js b/manage/incremental-search.js index 4f61ff6e..9dc5312c 100644 --- a/manage/incremental-search.js +++ b/manage/incremental-search.js @@ -5,8 +5,7 @@ onDOMready().then(() => { let prevText, focusedLink, focusedEntry; let prevTime = performance.now(); let focusedName = ''; - const input = $element({ - tag: 'textarea', + const input = $create('textarea', { spellcheck: false, oninput: incrementalSearch, }); diff --git a/manage/manage.js b/manage/manage.js index e8fc983a..86cc6b18 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -84,14 +84,14 @@ function initGlobalEvents() { switchUI({styleOnly: true}); // translate CSS manually - document.head.appendChild($element({tag: 'style', textContent: ` + document.head.appendChild($create('style', ` .disabled h2::after { content: "${t('genericDisabledLabel')}"; } #update-all-no-updates[data-skipped-edited="true"]:after { content: " ${t('updateAllCheckSucceededSomeEdited')}"; } - `})); + `)); } diff --git a/manage/updater-ui.js b/manage/updater-ui.js index 3f4b5292..7b5360dd 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -174,10 +174,7 @@ function showUpdateHistory() { BG.chromeLocal.getValue('updateLog').then((lines = []) => { messageBox({ title: t('updateCheckHistory'), - contents: $element({ - className: 'update-history-log', - textContent: lines.join('\n'), - }), + contents: $create('.update-history-log', lines.join('\n')), buttons: [t('confirmOK')], onshow: () => ($('#message-box-contents').scrollTop = 1e9), }); diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 4354d895..cbe3b44f 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -58,27 +58,25 @@ function messageBox({ removeSelf(); } const id = 'message-box'; - messageBox.element = $element({id, className, appendChild: [ - $element({appendChild: [ - $element({id: `${id}-title`, textContent: title}), - $element({id: `${id}-close-icon`, appendChild: - $element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', appendChild: - $element({tag: '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', - }) - }), - onclick: messageBox.listeners.closeIcon}), - $element({id: `${id}-contents`, appendChild: tHTML(contents)}), - $element({id: `${id}-buttons`, appendChild: - buttons.map((content, buttonIndex) => content && $element({ - tag: 'button', - buttonIndex, - textContent: content.textContent || content, - onclick: content.onclick || messageBox.listeners.button, - })) - }), - ]}), - ]}); + messageBox.element = + $create({id, className}, [ + $create([ + $create(`#${id}-title`, title), + $create(`#${id}-close-icon`, {onclick: messageBox.listeners.closeIcon}, + $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, + $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', + }))), + $create(`#${id}-contents`, tHTML(contents)), + $create(`#${id}-buttons`, + buttons.map((content, buttonIndex) => content && + $create('button', { + buttonIndex, + textContent: content.textContent || content, + onclick: content.onclick || messageBox.listeners.button, + }))), + ]), + ]); } function bindGlobalListeners() { diff --git a/popup/hotkeys.js b/popup/hotkeys.js index 4508d151..053d7e1c 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -142,13 +142,13 @@ window.addEventListener('showStyles:done', function _() { line .split(/(<.*?>)/) .map(s => (!s.startsWith('<') ? s : - $element({tag: 'mark', textContent: s.slice(1, -1)}))); + $create('mark', s.slice(1, -1)))); const linesToElements = text => text .trim() .split('\n') .map((line, i, array) => - $element(i < array.length - 1 ? { + $create(i < array.length - 1 ? { tag: 'p', appendChild: keysToElements(line), } : { @@ -159,9 +159,9 @@ window.addEventListener('showStyles:done', function _() { })); [ linesToElements(t('popupHotkeysInfo')), - $element({tag: 'button', textContent: t('confirmOK')}), + $create('button', t('confirmOK')), ].forEach(child => { - container.appendChild($element({appendChild: child})); + container.appendChild($create('div', child)); }); } }