Merge pull request #119 from openstyles/innerHTML

dealing with innerHTML
This commit is contained in:
tophf 2017-07-22 16:49:52 +03:00 committed by GitHub
commit 93ff6d0f85
9 changed files with 126 additions and 91 deletions

View File

@ -281,7 +281,10 @@ function updateIcon(tab, styles) {
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
chrome.browserAction.setBadgeBackgroundColor({color}); chrome.browserAction.setBadgeBackgroundColor({color});
getTab(tab.id).then(() => { getTab(tab.id).then(() => {
// skip pre-rendered tabs
if (tab.index >= 0) {
chrome.browserAction.setBadgeText({text, tabId: tab.id}); chrome.browserAction.setBadgeText({text, tabId: tab.id});
}
}); });
}); });
} }

View File

@ -61,7 +61,7 @@
</li> </li>
</template> </template>
<template data-id="appliesToEverything"> <template data-id="appliesToEverything">
<li class="applies-to-everything" i18n-html="appliesToEverything"> <li class="applies-to-everything" i18n-text="appliesToEverything">
<button class="add-applies-to" i18n-text="appliesSpecify"></button> <button class="add-applies-to" i18n-text="appliesSpecify"></button>
</li> </li>
</template> </template>

View File

@ -330,8 +330,6 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
position: absolute; position: absolute;
margin-left: -20px; margin-left: -20px;
margin-top: -1px; margin-top: -1px;
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
} }
/************ help popup ************/ /************ help popup ************/
#help-popup { #help-popup {

View File

@ -260,24 +260,28 @@ function initCodeMirror() {
}; };
// initialize global editor controls // initialize global editor controls
function optionsHtmlFromArray(options) { function optionsFromArray(parent, options) {
return options.map(opt => '<option>' + opt + '</option>').join(''); const fragment = document.createDocumentFragment();
for (const opt of options) {
fragment.appendChild($element({tag: 'option', textContent: opt}));
}
parent.appendChild(fragment);
} }
const themeControl = document.getElementById('editor.theme'); const themeControl = document.getElementById('editor.theme');
const themeList = localStorage.codeMirrorThemes; const themeList = localStorage.codeMirrorThemes;
if (themeList) { if (themeList) {
themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/)); optionsFromArray(themeControl, themeList.split(/\s+/));
} else { } else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
const theme = prefs.get('editor.theme'); const theme = prefs.get('editor.theme');
themeControl.innerHTML = optionsHtmlFromArray([theme === 'default' ? t('defaultTheme') : theme]); optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
getCodeMirrorThemes().then(() => { getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
themeControl.innerHTML = optionsHtmlFromArray(themes); optionsFromArray(themeControl, themes);
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
}); });
} }
document.getElementById('editor.keyMap').innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort()); optionsFromArray($('#editor.keyMap'), Object.keys(CM.keyMap).sort());
document.getElementById('options').addEventListener('change', acmeEventListener, false); document.getElementById('options').addEventListener('change', acmeEventListener, false);
setupLivePrefs(); setupLivePrefs();
@ -314,15 +318,17 @@ function acmeEventListener(event) {
break; break;
} }
// avoid flicker: wait for the second stylesheet to load, then apply the theme // avoid flicker: wait for the second stylesheet to load, then apply the theme
document.head.insertAdjacentHTML('beforeend', document.head.appendChild($element({
'<link id="cm-theme2" rel="stylesheet" href="' + url + '">'); tag: 'link',
(() => { id: 'cm-theme2',
rel: 'stylesheet',
href: url
}));
setTimeout(() => { setTimeout(() => {
CodeMirror.setOption(option, value); CodeMirror.setOption(option, value);
themeLink.remove(); themeLink.remove();
document.getElementById('cm-theme2').id = 'cm-theme'; $('#cm-theme2').id = 'cm-theme';
}, 100); }, 100);
})();
return; return;
} }
case 'autocompleteOnTyping': case 'autocompleteOnTyping':
@ -688,11 +694,11 @@ function setupGlobalSearch() {
return cm.state.search; return cm.state.search;
} }
// temporarily overrides the original openDialog with the provided template's innerHTML // overrides the original openDialog with a clone of the provided template
function customizeOpenDialog(cm, template, callback) { function customizeOpenDialog(cm, template, callback) {
cm.openDialog = (tmpl, cb, opt) => { cm.openDialog = (tmpl, cb, opt) => {
// invoke 'callback' and bind 'this' to the original callback // invoke 'callback' and bind 'this' to the original callback
originalOpenDialog.call(cm, template.innerHTML, callback.bind(cb), opt); originalOpenDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
}; };
setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0); setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0);
refocusMinidialog(cm); refocusMinidialog(cm);
@ -871,7 +877,7 @@ function setupGlobalSearch() {
doReplace(); doReplace();
} }
}); });
originalOpenConfirm.call(cm, template.replaceConfirm.innerHTML, ovrCallbacks, opt); originalOpenConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
}; };
} }
} }
@ -890,7 +896,7 @@ function setupGlobalSearch() {
function jumpToLine(cm) { function jumpToLine(cm) {
const cur = cm.getCursor(); const cur = cm.getCursor();
refocusMinidialog(cm); refocusMinidialog(cm);
cm.openDialog(template.jumpToLine.innerHTML, str => { cm.openDialog(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);
@ -1087,9 +1093,9 @@ function renderLintReport(someBlockChanged) {
let issueCount = 0; let issueCount = 0;
editors.forEach((cm, index) => { editors.forEach((cm, index) => {
if (cm.state.lint && cm.state.lint.html) { if (cm.state.lint && cm.state.lint.html) {
const newBlock = newContent.appendChild(document.createElement('table'));
const html = '<caption>' + label + ' ' + (index + 1) + '</caption>' + cm.state.lint.html; const html = '<caption>' + label + ' ' + (index + 1) + '</caption>' + cm.state.lint.html;
newBlock.innerHTML = html; const newBlock = newContent.appendChild(tHTML(html, 'table'));
newBlock.cm = cm; newBlock.cm = cm;
issueCount += newBlock.rows.length; issueCount += newBlock.rows.length;
@ -1233,7 +1239,7 @@ function init() {
const params = getParams(); const params = getParams();
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
// This is an add // This is an add
tE('heading', 'addStyleTitle'); $('#heading').textContent = t('addStyleTitle');
const section = {code: ''}; const section = {code: ''};
for (const i in CssToProperty) { for (const i in CssToProperty) {
if (params[i]) { if (params[i]) {
@ -1251,7 +1257,7 @@ function init() {
return; return;
} }
// This is an edit // This is an edit
tE('heading', 'editStyleHeading', null, false); $('#heading').textContent = t('editStyleHeading');
getStylesSafe({id: params.id}).then(styles => { getStylesSafe({id: params.id}).then(styles => {
let style = styles[0]; let style = styles[0];
if (!style) { if (!style) {
@ -1504,7 +1510,7 @@ function saveComplete(style) {
// Go from new style URL to edit style URL // Go from new style URL to edit style URL
if (location.href.indexOf('id=') === -1) { if (location.href.indexOf('id=') === -1) {
history.replaceState({}, document.title, 'edit.html?id=' + style.id); history.replaceState({}, document.title, 'edit.html?id=' + style.id);
tE('heading', 'editStyleHeading', null, false); $('#heading').textContent = t('editStyleHeading');
} }
updateTitle(); updateTitle();
} }
@ -1534,7 +1540,7 @@ function fromMozillaFormat() {
<button name="import-append" i18n-text="importAppendLabel" i18n-title="importAppendTooltip"></button> <button name="import-append" i18n-text="importAppendLabel" i18n-title="importAppendTooltip"></button>
<button name="import-replace" i18n-text="importReplaceLabel" i18n-title="importReplaceTooltip"></button> <button name="import-replace" i18n-text="importReplaceLabel" i18n-title="importReplaceTooltip"></button>
</div>` </div>`
).innerHTML); ));
const contents = popup.querySelector('.contents'); const contents = popup.querySelector('.contents');
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
@ -1610,7 +1616,7 @@ function fromMozillaFormat() {
makeSectionVisible(firstAddedCM); makeSectionVisible(firstAddedCM);
firstAddedCM.focus(); firstAddedCM.focus();
if (errors) { if (errors.length) {
showHelp(t('issues'), $element({ showHelp(t('issues'), $element({
tag: 'pre', tag: 'pre',
textContent: errors.join('\n'), textContent: errors.join('\n'),
@ -1748,16 +1754,37 @@ function showKeyMapHelp() {
function filterTable(event) { function filterTable(event) {
const input = event.target; const input = event.target;
const query = stringAsRegExp(input.value, 'gi');
const col = input.parentNode.cellIndex; const col = input.parentNode.cellIndex;
inputs[1 - col].value = ''; inputs[1 - col].value = '';
table.tBodies[0].childNodes.forEach(row => { table.tBodies[0].childNodes.forEach(row => {
let cell = row.children[col]; const cell = row.children[col];
cell.innerHTML = cell.textContent.replace(query, '<mark>$&</mark>'); const text = cell.textContent;
row.style.display = query.test(cell.textContent) ? '' : 'none'; const query = stringAsRegExp(input.value, 'gi');
const test = query.test(text);
row.style.display = input.value && test === false ? 'none' : '';
if (input.value && test) {
cell.textContent = '';
let offset = 0;
text.replace(query, (match, index) => {
if (index > offset) {
cell.appendChild(document.createTextNode(text.substring(offset, index)));
}
cell.appendChild($element({tag: 'mark', textContent: match}));
offset = index + match.length;
});
if (offset + 1 !== text.length) {
cell.appendChild(document.createTextNode(text.substring(offset)));
}
}
else {
cell.textContent = text;
}
// clear highlight from the other column // clear highlight from the other column
cell = row.children[1 - col]; const otherCell = row.children[1 - col];
cell.innerHTML = cell.textContent; if (otherCell.children.length) {
const text = otherCell.textContent;
otherCell.textContent = text;
}
}); });
} }
function mergeKeyMaps(merged, ...more) { function mergeKeyMaps(merged, ...more) {
@ -1897,15 +1924,17 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
if (!data.length) { if (!data.length) {
continue; continue;
} }
const block = report.appendChild($element({
tag: 'details',
open: true,
dataset: {type},
appendChild: $element({tag: 'summary', appendChild: label}),
}));
// 2nd level: regexp text // 2nd level: regexp text
const summary = $element({tag: 'summary', appendChild: label});
const block = [summary];
for (const {text, urls} of data) { for (const {text, urls} of data) {
if (!urls) { if (urls) {
block.push(text, br.cloneNode()); // type is partial or full
continue; block.appendChild($element({
}
block.push($element({
tag: 'details', tag: 'details',
open: true, open: true,
appendChild: [ appendChild: [
@ -1914,13 +1943,12 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
...urls, ...urls,
], ],
})); }));
} else {
// type is none or invalid
block.appendChild(document.createTextNode(text));
block.appendChild(br.cloneNode());
}
} }
report.appendChild($element({
tag: 'details',
open: true,
dataset: {type},
appendChild: block,
}));
} }
showHelp(t('styleRegexpTestTitle'), report); showHelp(t('styleRegexpTestTitle'), report);
@ -1934,17 +1962,11 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
}); });
} }
function showHelp(title, text) { function showHelp(title, body) {
const div = $('#help-popup'); const div = $('#help-popup');
div.classList.remove('big'); div.classList.remove('big');
$('.contents', div).textContent = '';
const contents = $('.contents', div); $('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body);
if (text instanceof HTMLElement) {
contents.textContent = '';
contents.appendChild(text);
} else {
contents.innerHTML = text;
}
$('.title', div).textContent = title; $('.title', div).textContent = title;
if (getComputedStyle(div).display === 'none') { if (getComputedStyle(div).display === 'none') {
@ -1962,7 +1984,7 @@ function showHelp(title, text) {
((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) ((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
) { ) {
div.style.display = ''; div.style.display = '';
document.querySelector('.contents').innerHTML = ''; document.querySelector('.contents').textContent = '';
document.removeEventListener('keydown', closeHelp); document.removeEventListener('keydown', closeHelp);
} }
} }

View File

@ -17,24 +17,31 @@ function t(key, params) {
} }
function tE(id, key, attr, esc) { function tHTML(html, tag) {
if (attr) { // body is a text node without HTML tags
document.getElementById(id).setAttribute(attr, t(key)); if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) {
} else if (typeof esc === 'undefined' || esc) { return document.createTextNode(html);
document.getElementById(id).appendChild(document.createTextNode(t(key)));
} else {
document.getElementById(id).innerHTML = t(key);
} }
} if (typeof html === 'string') {
html = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space
if (tag) {
function tHTML(html) { html = `<${tag}>${html}</${tag}>`;
const node = document.createElement('div'); }
node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space const body = t.DOMParser.parseFromString(html, 'text/html').body;
if (html.includes('i18n-')) { if (html.includes('i18n-')) {
tNodeList(node.getElementsByTagName('*')); tNodeList(body.getElementsByTagName('*'));
} }
return node.firstElementChild; // the html string may contain more than one top-level elements
if (body.childElementCount <= 1) {
return body.firstElementChild;
}
const fragment = document.createDocumentFragment();
while (body.childElementCount) {
fragment.appendChild(body.firstElementChild);
}
return fragment;
}
return html;
} }
@ -78,7 +85,11 @@ function tNodeList(nodes) {
node.appendChild(document.createTextNode(value)); node.appendChild(document.createTextNode(value));
break; break;
case 'html': case 'html':
node.insertAdjacentHTML('afterbegin', value); // 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));
break; break;
default: default:
node.setAttribute(type, value); node.setAttribute(type, value);
@ -90,6 +101,7 @@ function tNodeList(nodes) {
function tDocLoader() { function tDocLoader() {
t.DOMParser = new DOMParser();
t.cache = tryJSONparse(localStorage.L10N) || {}; t.cache = tryJSONparse(localStorage.L10N) || {};
// reset L10N cache on UI language change // reset L10N cache on UI language change

View File

@ -117,7 +117,7 @@
<template data-id="extraAppliesTo"> <template data-id="extraAppliesTo">
<details class="applies-to-extra"> <details class="applies-to-extra">
<summary i18n-html="appliesDisplayTruncatedSuffix"></summary> <summary i18n-text="appliesDisplayTruncatedSuffix"></summary>
</details> </details>
</template> </template>

View File

@ -519,7 +519,7 @@ function switchUI({styleOnly} = {}) {
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img'); const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) { if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
installed.innerHTML = ''; installed.textContent = '';
getStylesSafe().then(showStyles); getStylesSafe().then(showStyles);
return; return;
} }

View File

@ -1,8 +1,8 @@
'use strict'; 'use strict';
function messageBox({ function messageBox({
title, // [mandatory] the title string for innerHTML title, // [mandatory] string
contents, // [mandatory] 1) DOM element 2) string for innerHTML contents, // [mandatory] 1) DOM element 2) string
className = '', // string, CSS class name of the message box element className = '', // string, CSS class name of the message box element
buttons = [], // array of strings used as labels buttons = [], // array of strings used as labels
onshow, // function(messageboxElement) invoked after the messagebox is shown onshow, // function(messageboxElement) invoked after the messagebox is shown
@ -52,10 +52,9 @@ function messageBox({
unbindAndRemoveSelf(); unbindAndRemoveSelf();
} }
const id = 'message-box'; const id = 'message-box';
const putAs = typeof contents === 'string' ? 'innerHTML' : 'appendChild';
messageBox.element = $element({id, className, appendChild: [ messageBox.element = $element({id, className, appendChild: [
$element({appendChild: [ $element({appendChild: [
$element({id: `${id}-title`, innerHTML: title}), $element({id: `${id}-title`, textContent: title}),
$element({id: `${id}-close-icon`, appendChild: $element({id: `${id}-close-icon`, appendChild:
$element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', 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,' + $element({tag: 'SVG#path', d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
@ -63,7 +62,7 @@ function messageBox({
}) })
}), }),
onclick: messageBox.listeners.closeIcon}), onclick: messageBox.listeners.closeIcon}),
$element({id: `${id}-contents`, [putAs]: contents}), $element({id: `${id}-contents`, appendChild: tHTML(contents)}),
$element({id: `${id}-buttons`, appendChild: $element({id: `${id}-buttons`, appendChild:
buttons.map((textContent, buttonIndex) => textContent && buttons.map((textContent, buttonIndex) => textContent &&
$element({ $element({

View File

@ -176,7 +176,8 @@ function showStyles(styles) {
return; return;
} }
if (!styles.length) { if (!styles.length) {
installed.innerHTML = template.noStyles.outerHTML; installed.textContent = '';
installed.appendChild(template.noStyles.cloneNode(true));
return; return;
} }