From 0e92e6d0c767be8f242595018a70c73e9ee26393 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 13 Sep 2017 16:58:03 +0800 Subject: [PATCH] Add applies-to line widget --- edit/edit.css | 19 +++++ edit/source-editor.js | 175 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/edit/edit.css b/edit/edit.css index 7f033529..9297b20f 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -539,6 +539,25 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar height: 100%; } +/************ line widget *************/ +.CodeMirror-linewidget .applies-to { + margin: 1em 0; + padding: 1em; + padding-right: calc(1em + 20px); +} + +.CodeMirror-linewidget .applies-to li { + margin: 0; +} + +.CodeMirror-linewidget .applies-to li + li { + margin-top: 0.35rem; +} + +.CodeMirror-linewidget li.applies-to-everything { + margin-top: 0.2rem; +} + /************ reponsive layouts ************/ @media(max-width:737px) { #header { diff --git a/edit/source-editor.js b/edit/source-editor.js index e2c7ac89..55370998 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -37,6 +37,181 @@ function createSourceEditor(style) { updateMetas(); initHooks(); initLint(); + initAppliesToReport(cm); + + function initAppliesToReport(cm) { + const DELAY = 500; + let widgets = []; + let timer; + let fromLine = null; + let toLine = null; + let style = getComputedStyle(cm.getGutterElement()); + + update(); + + cm.on('change', (cm, {from, to}) => { + if (fromLine === null || toLine === null) { + fromLine = from.line; + toLine = to.line; + } else { + fromLine = Math.min(fromLine, from.line); + toLine = Math.max(toLine, to.line); + } + clearTimeout(timer); + timer = setTimeout(update, DELAY); + }); + + cm.on('optionChange', (cm, option) => { + if (option === 'theme') { + updateStyle(); + } + }); + + // is it possible to avoid flickering? + window.addEventListener('load', updateStyle); + + function update() { + cm.operation(doUpdate); + } + + function updateStyle() { + style = getComputedStyle(cm.getGutterElement()); + widgets.forEach(setWidgetStyle); + } + + function setWidgetStyle(widget) { + const borderStyle = style.borderRightWidth !== '0px' ? + style.borderRight : '1px solid ' + style.color; + widget.node.style.backgroundColor = style.backgroundColor; + widget.node.style.borderTop = borderStyle; + widget.node.style.borderBottom = borderStyle; + } + + function doUpdate() { + // find which widgets needs to be update + // some widgets (lines) might be deleted + widgets = widgets.filter(w => w.line.lineNo() !== null); + let i = fromLine === null ? 0 : widgets.findIndex(w => w.line.lineNo() > fromLine) - 1; + let j = toLine === null ? 0 : widgets.findIndex(w => w.line.lineNo() > toLine); + if (i === -2) { + i = widgets.length - 1; + } + if (j < 0) { + j = widgets.length; + } + + // decide search range + const fromIndex = widgets[i] ? cm.indexFromPos({line: widgets[i].line.lineNo(), ch: 0}) : 0; + const toIndex = widgets[j] ? cm.indexFromPos({line: widgets[j].line.lineNo(), ch: 0}) : cm.getValue().length; + + // splice + if (i < 0) { + i = 0; + } + + widgets.splice(i, 0, ...createWidgets(fromIndex, toIndex, widgets.splice(i, j - i))); + + fromLine = null; + toLine = null; + } + + function *createWidgets(start, end, removed) { + let i = 0; + for (const section of findAppliesTo(start, end)) { + while (removed[i] && removed[i].line.lineNo() < section.pos.line) { + removed[i++].clear(); + } + if (removed[i] && removed[i].line.lineNo() === section.pos.line) { + // reuse old widget + const newNode = buildElement(section); + removed[i].node.parentNode.replaceChild(newNode, removed[i].node); + removed[i].node = newNode; + setWidgetStyle(removed[i]); + removed[i].changed(); + yield removed[i]; + i++; + continue; + } + // new widget + const widget = cm.addLineWidget(section.pos.line, buildElement(section), { + coverGutter: true, + noHScroll: true, + above: true + }); + setWidgetStyle(widget); + yield widget; + } + removed.slice(i).forEach(w => w.clear()); + } + + function buildElement({applies}) { + const el = $element({className: 'applies-to', appendChild: [ + $element({tag: 'label', appendChild: [ + t('appliesLabel'), + // $element({tag: 'svg'}) + ]}), + $element({tag: 'ul', className: 'applies-to-list', appendChild: applies.map(apply => + $element({tag: 'li', appendChild: [ + $element({tag: 'input', className: 'applies-type', value: typeLabel(apply.type), readOnly: true}), + $element({tag: 'input', className: 'applies-value', value: apply.value, readOnly: true}) + ]}) + )}) + ]}); + if (!$$('li', el).length) { + $('ul', el).appendChild($element({ + tag: 'li', + className: 'applies-to-everything', + textContent: t('appliesToEverything') + })); + } + return el; + } + + function typeLabel(type) { + switch (type.toLowerCase()) { + case 'url': + return t('appliesUrlOption'); + case 'url-prefix': + return t('appliesUrlPrefixOption'); + case 'domain': + return t('appliesDomainOption'); + case 'regexp': + return t('appliesRegexpOption'); + } + } + + function *findAppliesTo(posStart, posEnd) { + const text = cm.getValue(); + const re = /^[\t ]*@-moz-document\s+/mg; + const applyRe = /^(url|url-prefix|domain|regexp)\(((['"])(?:\\\3|[\s\S])*?\3|[^)]*)\)[\s,]*/i; + let preIndex = re.lastIndex = posStart; + let match; + let pos = cm.posFromIndex(preIndex); + while ((match = re.exec(text))) { + if (match.index >= posEnd) { + return; + } + pos = cm.findPosH(pos, match.index - preIndex, 'char'); + const applies = []; + let t = text.slice(re.lastIndex); + let m; + while ((m = t.match(applyRe))) { + applies.push({type: m[1], value: normalizeString(m[2])}); + t = t.slice(m[0].length); + } + yield {pos, applies}; + preIndex = match.index; + re.lastIndex = text.length - t.length; + } + } + + function normalizeString(s) { + if (/^(['"])[\s\S]*\1$/.test(s)) { + return s.slice(1, -1); + } + return s; + } + } function initHooks() { // sidebar commands