diff --git a/edit.html b/edit.html index 03eef893..6b7ae8b8 100644 --- a/edit.html +++ b/edit.html @@ -95,6 +95,7 @@ + diff --git a/edit/edit.js b/edit/edit.js index a5b144a4..6c861a2c 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1,9 +1,9 @@ /* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch closeCurrentTab messageBox debounce workerUtil - beautify + beautify ignoreChromeError moveFocus msg createSectionsEditor rerouteHotkeys */ -/* exported showCodeMirrorPopup editorWorker */ +/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */ 'use strict'; const editorWorker = workerUtil.createWorker({ @@ -557,3 +557,14 @@ function isWindowMaximized() { window.outerHeight < screen.availHeight + 10 ); } + +function toggleContextMenuDelete(event) { + if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) { + chrome.contextMenus.update('editor.contextDelete', { + enabled: Boolean( + this.selectionStart !== this.selectionEnd || + this.somethingSelected && this.somethingSelected() + ), + }, ignoreChromeError); + } +} diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js new file mode 100644 index 00000000..32196633 --- /dev/null +++ b/edit/sections-editor-section.js @@ -0,0 +1,416 @@ +/* global template cmFactory $ propertyToCss CssToProperty linter regExpTester + FIREFOX toggleContextMenuDelete beautify showHelp t tryRegExp */ +/* exported createSection */ +'use strict'; + +function createResizeGrip(cm) { + const wrapper = cm.display.wrapper; + wrapper.classList.add('resize-grip-enabled'); + const resizeGrip = template.resizeGrip.cloneNode(true); + wrapper.appendChild(resizeGrip); + let lastClickTime = 0; + resizeGrip.onmousedown = event => { + if (event.button !== 0) { + return; + } + event.preventDefault(); + if (Date.now() - lastClickTime < 500) { + lastClickTime = 0; + toggleSectionHeight(cm); + return; + } + lastClickTime = Date.now(); + const minHeight = cm.defaultTextHeight() + + /* .CodeMirror-lines padding */ + cm.display.lineDiv.offsetParent.offsetTop + + /* borders */ + wrapper.offsetHeight - wrapper.clientHeight; + wrapper.style.pointerEvents = 'none'; + document.body.style.cursor = 's-resize'; + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', resizeStop); + + function resize(e) { + const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY; + const height = Math.max(minHeight, e.pageY - cmPageY); + if (height !== wrapper.clientHeight) { + cm.setSize(null, height); + } + } + + function resizeStop() { + document.removeEventListener('mouseup', resizeStop); + document.removeEventListener('mousemove', resize); + wrapper.style.pointerEvents = ''; + document.body.style.cursor = ''; + } + }; + + function toggleSectionHeight(cm) { + if (cm.state.toggleHeightSaved) { + // restore previous size + cm.setSize(null, cm.state.toggleHeightSaved); + cm.state.toggleHeightSaved = 0; + } else { + // maximize + const wrapper = cm.display.wrapper; + const allBounds = $('#sections').getBoundingClientRect(); + const pageExtrasHeight = allBounds.top + window.scrollY + + parseFloat(getComputedStyle($('#sections')).paddingBottom); + const sectionEl = wrapper.parentNode; + const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight; + cm.state.toggleHeightSaved = wrapper.clientHeight; + cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight); + const bounds = sectionEl.getBoundingClientRect(); + if (bounds.top < 0 || bounds.bottom > window.innerHeight) { + window.scrollBy(0, bounds.top); + } + } + } +} + +function createSection({ + originalSection, + genId, + dirty, + showMozillaFormatImport, + removeSection, + insertSectionAfter, + moveSectionUp, + moveSectionDown, + restoreSection, + nextEditor, + prevEditor +}) { + const sectionId = genId(); + const el = template.section.cloneNode(true); + const cm = cmFactory.create(wrapper => { + el.insertBefore(wrapper, $('.code-label', el).nextSibling); + }, {value: originalSection.code}); + + const changeListeners = new Set(); + + const appliesToContainer = $('.applies-to-list', el); + const appliesTo = []; + for (const [key, fnName] of Object.entries(propertyToCss)) { + if (originalSection[key]) { + originalSection[key].forEach(value => + insertApplyAfter({type: fnName, value}) + ); + } + } + if (!appliesTo.length) { + insertApplyAfter({all: true}); + } + + let changeGeneration = cm.changeGeneration(); + let removed = false; + + registerEvents(); + updateRegexpTester(); + createResizeGrip(cm); + + linter.enableForEditor(cm); + + let lastActive = 0; + + const section = { + id: sectionId, + el, + cm, + render, + getCode, + getModel, + remove, + restore, + isRemoved: () => removed, + onChange, + off, + getLastActive: () => lastActive, + appliesTo + }; + return section; + + function onChange(fn) { + changeListeners.add(fn); + } + + function off(fn) { + changeListeners.delete(fn); + } + + function emitSectionChange() { + for (const fn of changeListeners) { + fn(); + } + } + + function getModel() { + const section = { + code: cm.getValue() + }; + for (const apply of appliesTo) { + if (apply.all) { + continue; + } + const key = CssToProperty[apply.getType()]; + if (!section[key]) { + section[key] = []; + } + section[key].push(apply.getValue()); + } + return section; + } + + function registerEvents() { + cm.on('changes', () => { + const newGeneration = cm.changeGeneration(); + dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration); + changeGeneration = newGeneration; + emitSectionChange(); + }); + cm.on('paste', (cm, event) => { + const text = event.clipboardData.getData('text') || ''; + if ( + text.includes('@-moz-document') && + text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '') + .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/) + ) { + event.preventDefault(); + showMozillaFormatImport(text); + } + // FIXME: why? + // if (editors.length === 1) { + // setTimeout(() => { + // if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) { + // maximizeCodeHeight.stats = null; + // maximizeCodeHeight(cm.getSection(), true); + // } + // }); + // } + }); + if (!FIREFOX) { + cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event)); + } + cm.on('focus', () => { + lastActive = Date.now(); + }); + + cm.display.wrapper.addEventListener('keydown', event => + handleKeydown(cm, event), true); + + $('.applies-to-help', el).addEventListener('click', showAppliesToHelp); + $('.remove-section', el).addEventListener('click', () => removeSection(section)); + $('.add-section', el).addEventListener('click', () => insertSectionAfter(undefined, section)); + $('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section)); + $('.move-section-up', el).addEventListener('click', () => moveSectionUp(section)); + $('.move-section-down', el).addEventListener('click', () => moveSectionDown(section)); + $('.beautify-section', el).addEventListener('click', () => beautify([cm])); + $('.restore-section', el).addEventListener('click', () => restoreSection(section)); + $('.test-regexp', el).addEventListener('click', () => { + regExpTester.toggle(); + updateRegexpTester(); + }); + } + + function handleKeydown(cm, event) { + const key = event.which; + if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { + return; + } + const {line, ch} = cm.getCursor(); + switch (key) { + case 37: + // arrow Left + if (line || ch) { + return; + } + // fallthrough to arrow Up + case 38: + // arrow Up + cm = line === 0 && prevEditor(cm, false); + if (!cm) { + return; + } + event.preventDefault(); + event.stopPropagation(); + cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); + break; + case 39: + // arrow Right + if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { + return; + } + // fallthrough to arrow Down + case 40: + // arrow Down + cm = line === cm.doc.size - 1 && nextEditor(cm, false); + if (!cm) { + return; + } + event.preventDefault(); + event.stopPropagation(); + cm.setCursor(0, 0); + break; + } + // FIXME: what is this? + // const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; + // if (animation) { + // animation.playbackRate = -1; + // animation.currentTime = 2000; + // animation.play(); + // } + } + + function showAppliesToHelp(event) { + event.preventDefault(); + showHelp(t('appliesLabel'), t('appliesHelp')); + } + + function getCode() { + return cm.getValue(); + } + + function remove(destroy = false) { + linter.disableForEditor(cm); + el.classList.add('removed'); + removed = true; + appliesTo.forEach(a => a.remove()); + if (destroy) { + cmFactory.destroy(cm); + } + } + + function restore() { + linter.enableForEditor(cm); + el.classList.remove('removed'); + removed = false; + appliesTo.forEach(a => a.restore()); + render(); + } + + function render() { + cm.refresh(); + } + + function updateRegexpTester() { + const regexps = appliesTo.filter(a => a.getType() === 'regexp') + .map(a => a.getValue()); + if (regexps.length) { + el.classList.add('has-regexp'); + regExpTester.update(regexps); + } else { + el.classList.remove('has-regexp'); + regExpTester.toggle(false); + } + } + + function insertApplyAfter(init, base) { + const apply = createApply(init); + if (base) { + const index = appliesTo.indexOf(base); + appliesTo.splice(index + 1, 0, apply); + appliesToContainer.insertBefore(apply.el, base.el.nextSibling); + } else { + appliesTo.push(apply); + appliesToContainer.appendChild(apply.el); + } + dirty.add(apply, apply); + if (appliesTo.length > 1 && appliesTo[0].all) { + removeApply(appliesTo[0]); + } + emitSectionChange(); + } + + function removeApply(apply) { + const index = appliesTo.indexOf(apply); + appliesTo.splice(index, 1); + apply.remove(); + apply.el.remove(); + dirty.remove(apply, apply); + if (!appliesTo.length) { + insertApplyAfter({all: true}); + } + emitSectionChange(); + } + + function createApply({type = 'url', value, all = false}) { + const applyId = genId(); + const dirtyPrefix = `section.${sectionId}.apply.${applyId}`; + const el = all ? template.appliesToEverything.cloneNode(true) : + template.appliesTo.cloneNode(true); + + const selectEl = !all && $('.applies-type', el); + if (selectEl) { + selectEl.value = type; + selectEl.addEventListener('change', () => { + const oldKey = type; + dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value); + type = selectEl.value; + if (oldKey === 'regexp' || type === 'regexp') { + updateRegexpTester(); + } + emitSectionChange(); + validate(); + }); + } + + const valueEl = !all && $('.applies-value', el); + if (valueEl) { + valueEl.value = value; + valueEl.addEventListener('input', () => { + dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value); + value = valueEl.value; + if (type === 'regexp') { + updateRegexpTester(); + } + emitSectionChange(); + }); + valueEl.addEventListener('change', validate); + } + + const apply = { + id: applyId, + all, + remove, + restore, + el, + getType: () => type, + getValue: () => value, + valueEl + }; + + const removeButton = $('.remove-applies-to', el); + if (removeButton) { + removeButton.addEventListener('click', e => { + e.preventDefault(); + removeApply(apply); + }); + } + $('.add-applies-to', el).addEventListener('click', e => { + e.preventDefault(); + insertApplyAfter({type, value: ''}, apply); + }); + + return apply; + + function validate() { + if (type !== 'regexp' || tryRegExp(value)) { + valueEl.setCustomValidity(''); + } else { + valueEl.setCustomValidity(t('styleBadRegexp')); + setTimeout(() => valueEl.reportValidity()); + } + } + + function remove() { + dirty.remove(`${dirtyPrefix}.type`, type); + dirty.remove(`${dirtyPrefix}.value`, value); + } + + function restore() { + dirty.add(`${dirtyPrefix}.type`, type); + dirty.add(`${dirtyPrefix}.value`, value); + } + } +} diff --git a/edit/sections-editor.js b/edit/sections-editor.js index ec57d80f..c9b3421f 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -1,79 +1,11 @@ -/* global dirtyReporter showHelp prefs ignoreChromeError - CodeMirror propertyToCss - regExpTester linter createLivePreview showCodeMirrorPopup - sectionsToMozFormat messageBox clipString beautify - rerouteHotkeys cmFactory CssToProperty template $ $$ $create t FIREFOX API - debounce tryRegExp -*/ +/* global dirtyReporter showHelp toggleContextMenuDelete createSection + CodeMirror linter createLivePreview showCodeMirrorPopup + sectionsToMozFormat messageBox clipString + rerouteHotkeys $ $$ $create t FIREFOX API + debounce */ /* exported createSectionsEditor */ 'use strict'; -function createResizeGrip(cm) { - const wrapper = cm.display.wrapper; - wrapper.classList.add('resize-grip-enabled'); - const resizeGrip = template.resizeGrip.cloneNode(true); - wrapper.appendChild(resizeGrip); - let lastClickTime = 0; - resizeGrip.onmousedown = event => { - if (event.button !== 0) { - return; - } - event.preventDefault(); - if (Date.now() - lastClickTime < 500) { - lastClickTime = 0; - toggleSectionHeight(cm); - return; - } - lastClickTime = Date.now(); - const minHeight = cm.defaultTextHeight() + - /* .CodeMirror-lines padding */ - cm.display.lineDiv.offsetParent.offsetTop + - /* borders */ - wrapper.offsetHeight - wrapper.clientHeight; - wrapper.style.pointerEvents = 'none'; - document.body.style.cursor = 's-resize'; - document.addEventListener('mousemove', resize); - document.addEventListener('mouseup', resizeStop); - - function resize(e) { - const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY; - const height = Math.max(minHeight, e.pageY - cmPageY); - if (height !== wrapper.clientHeight) { - cm.setSize(null, height); - } - } - - function resizeStop() { - document.removeEventListener('mouseup', resizeStop); - document.removeEventListener('mousemove', resize); - wrapper.style.pointerEvents = ''; - document.body.style.cursor = ''; - } - }; - - function toggleSectionHeight(cm) { - if (cm.state.toggleHeightSaved) { - // restore previous size - cm.setSize(null, cm.state.toggleHeightSaved); - cm.state.toggleHeightSaved = 0; - } else { - // maximize - const wrapper = cm.display.wrapper; - const allBounds = $('#sections').getBoundingClientRect(); - const pageExtrasHeight = allBounds.top + window.scrollY + - parseFloat(getComputedStyle($('#sections')).paddingBottom); - const sectionEl = wrapper.parentNode; - const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight; - cm.state.toggleHeightSaved = wrapper.clientHeight; - cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight); - const bounds = sectionEl.getBoundingClientRect(); - if (bounds.top < 0 || bounds.bottom > window.innerHeight) { - window.scrollBy(0, bounds.top); - } - } - } -} - function createSectionsEditor(style) { let INC_ID = 0; // an increment id that is used by various object to track the order const dirty = dirtyReporter(); @@ -97,7 +29,7 @@ function createSectionsEditor(style) { $('#to-mozilla').addEventListener('click', showMozillaFormat); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp); - $('#from-mozilla').addEventListener('click', () => fromMozillaFormat()); + $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); $('#save-button').addEventListener('click', saveStyle); // FIXME: this element doesn't exist? $('#sections-help').addEventListener('click', showSectionHelp); @@ -150,15 +82,8 @@ function createSectionsEditor(style) { getSearchableInputs, }; - function toggleContextMenuDelete(event) { - if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) { - chrome.contextMenus.update('editor.contextDelete', { - enabled: Boolean( - this.selectionStart !== this.selectionEnd || - this.somethingSelected && this.somethingSelected() - ), - }, ignoreChromeError); - } + function genId() { + return INC_ID++; } function setGlobalProgress(done, total) { @@ -180,11 +105,6 @@ function createSectionsEditor(style) { showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp')); } - function showAppliesToHelp(event) { - event.preventDefault(); - showHelp(t('appliesLabel'), t('appliesHelp')); - } - function showSectionHelp(event) { event.preventDefault(); showHelp(t('styleSectionsTitle'), t('sectionHelp')); @@ -287,11 +207,33 @@ function createSectionsEditor(style) { enabledEl.checked = newValue; } - function nextEditor(cm) { + function nextEditor(cm, cycle = true) { + if (!cycle) { + for (const section of sections) { + if (section.isRemoved()) { + continue; + } + if (cm === section.cm) { + return; + } + break; + } + } return nextPrevEditor(cm, 1); } - function prevEditor(cm) { + function prevEditor(cm, cycle = true) { + if (!cycle) { + for (let i = sections.length - 1; i >= 0; i--) { + if (sections[i].isRemoved()) { + continue; + } + if (cm === sections[i].cm) { + return; + } + break; + } + } return nextPrevEditor(cm, -1); } @@ -332,55 +274,6 @@ function createSectionsEditor(style) { return result; } - function nextPrevEditorOnKeydown(cm, event) { - const key = event.which; - if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { - return; - } - const {line, ch} = cm.getCursor(); - switch (key) { - case 37: - // arrow Left - if (line || ch) { - return; - } - // fallthrough to arrow Up - case 38: - // arrow Up - if (line > 0 || cm === sections[0].cm) { - return; - } - event.preventDefault(); - event.stopPropagation(); - cm = prevEditor(cm); - cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); - break; - case 39: - // arrow Right - if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { - return; - } - // fallthrough to arrow Down - case 40: - // arrow Down - if (line < cm.doc.size - 1 || cm === sections[sections.length - 1].cm) { - return; - } - event.preventDefault(); - event.stopPropagation(); - cm = nextEditor(cm); - cm.setCursor(0, 0); - break; - } - // FIXME: what is this? - // const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; - // if (animation) { - // animation.playbackRate = -1; - // animation.currentTime = 2000; - // animation.play(); - // } - } - function scrollEntirePageOnCtrlShift(event) { // make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) { @@ -396,7 +289,7 @@ function createSectionsEditor(style) { popup.codebox.execCommand('selectAll'); } - function fromMozillaFormat(text = '') { + function showMozillaFormatImport(text = '') { const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'), $create('.buttons', [ $create('button', { @@ -612,7 +505,19 @@ function createSectionsEditor(style) { if (!init) { init = {code: '', urlPrefixes: ['http://example.com']}; } - const section = createSection(init); + const section = createSection({ + originalSection: init, + genId, + dirty, + showMozillaFormatImport, + removeSection, + restoreSection, + insertSectionAfter, + moveSectionUp, + moveSectionDown, + prevEditor, + nextEditor + }); if (base) { const index = sections.indexOf(base); sections.splice(index + 1, 0, section); @@ -650,286 +555,6 @@ function createSectionsEditor(style) { updateSectionOrder(); } - function createSection(originalSection) { - const sectionId = INC_ID++; - const el = template.section.cloneNode(true); - const cm = cmFactory.create(wrapper => { - el.insertBefore(wrapper, $('.code-label', el).nextSibling); - }, {value: originalSection.code}); - - const changeListeners = new Set(); - - const appliesToContainer = $('.applies-to-list', el); - const appliesTo = []; - for (const [key, fnName] of Object.entries(propertyToCss)) { - if (originalSection[key]) { - originalSection[key].forEach(value => - insertApplyAfter({type: fnName, value}) - ); - } - } - if (!appliesTo.length) { - insertApplyAfter({all: true}); - } - - let changeGeneration = cm.changeGeneration(); - let removed = false; - - registerEvents(); - updateRegexpTester(); - createResizeGrip(cm); - - linter.enableForEditor(cm); - - let lastActive = 0; - - const section = { - id: sectionId, - el, - cm, - render, - getCode, - getModel, - remove, - restore, - isRemoved: () => removed, - onChange, - off, - getLastActive: () => lastActive, - appliesTo - }; - return section; - - function onChange(fn) { - changeListeners.add(fn); - } - - function off(fn) { - changeListeners.delete(fn); - } - - function emitSectionChange() { - for (const fn of changeListeners) { - fn(); - } - } - - function getModel() { - const section = { - code: cm.getValue() - }; - for (const apply of appliesTo) { - if (apply.all) { - continue; - } - const key = CssToProperty[apply.getType()]; - if (!section[key]) { - section[key] = []; - } - section[key].push(apply.getValue()); - } - return section; - } - - function registerEvents() { - cm.on('changes', () => { - const newGeneration = cm.changeGeneration(); - dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration); - changeGeneration = newGeneration; - emitSectionChange(); - }); - cm.on('paste', (cm, event) => { - const text = event.clipboardData.getData('text') || ''; - if ( - text.includes('@-moz-document') && - text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '') - .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/) - ) { - event.preventDefault(); - fromMozillaFormat(text); - } - // FIXME: why? - // if (editors.length === 1) { - // setTimeout(() => { - // if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) { - // maximizeCodeHeight.stats = null; - // maximizeCodeHeight(cm.getSection(), true); - // } - // }); - // } - }); - if (!FIREFOX) { - cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event)); - } - cm.on('focus', () => { - lastActive = Date.now(); - }); - - cm.display.wrapper.addEventListener('keydown', event => - nextPrevEditorOnKeydown(cm, event), true); - - $('.applies-to-help', el).addEventListener('click', showAppliesToHelp); - $('.remove-section', el).addEventListener('click', () => removeSection(section)); - $('.add-section', el).addEventListener('click', () => insertSectionAfter(undefined, section)); - $('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section)); - $('.move-section-up', el).addEventListener('click', () => moveSectionUp(section)); - $('.move-section-down', el).addEventListener('click', () => moveSectionDown(section)); - $('.beautify-section', el).addEventListener('click', () => beautify([cm])); - $('.restore-section', el).addEventListener('click', () => restoreSection(section)); - $('.test-regexp', el).addEventListener('click', () => { - regExpTester.toggle(); - updateRegexpTester(); - }); - } - - function getCode() { - return cm.getValue(); - } - - function remove(destroy = false) { - linter.disableForEditor(cm); - el.classList.add('removed'); - removed = true; - appliesTo.forEach(a => a.remove()); - if (destroy) { - cmFactory.destroy(cm); - } - } - - function restore() { - linter.enableForEditor(cm); - el.classList.remove('removed'); - removed = false; - appliesTo.forEach(a => a.restore()); - render(); - } - - function render() { - cm.refresh(); - } - - function updateRegexpTester() { - const regexps = appliesTo.filter(a => a.getType() === 'regexp') - .map(a => a.getValue()); - if (regexps.length) { - el.classList.add('has-regexp'); - regExpTester.update(regexps); - } else { - el.classList.remove('has-regexp'); - regExpTester.toggle(false); - } - } - - function insertApplyAfter(init, base) { - const apply = createApply(init); - if (base) { - const index = appliesTo.indexOf(base); - appliesTo.splice(index + 1, 0, apply); - appliesToContainer.insertBefore(apply.el, base.el.nextSibling); - } else { - appliesTo.push(apply); - appliesToContainer.appendChild(apply.el); - } - dirty.add(apply, apply); - if (appliesTo.length > 1 && appliesTo[0].all) { - removeApply(appliesTo[0]); - } - emitSectionChange(); - } - - function removeApply(apply) { - const index = appliesTo.indexOf(apply); - appliesTo.splice(index, 1); - apply.remove(); - apply.el.remove(); - dirty.remove(apply, apply); - if (!appliesTo.length) { - insertApplyAfter({all: true}); - } - emitSectionChange(); - } - - function createApply({type = 'url', value, all = false}) { - const applyId = INC_ID++; - const dirtyPrefix = `section.${sectionId}.apply.${applyId}`; - const el = all ? template.appliesToEverything.cloneNode(true) : - template.appliesTo.cloneNode(true); - - const selectEl = !all && $('.applies-type', el); - if (selectEl) { - selectEl.value = type; - selectEl.addEventListener('change', () => { - const oldKey = type; - dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value); - type = selectEl.value; - if (oldKey === 'regexp' || type === 'regexp') { - updateRegexpTester(); - } - emitSectionChange(); - validate(); - }); - } - - const valueEl = !all && $('.applies-value', el); - if (valueEl) { - valueEl.value = value; - valueEl.addEventListener('input', () => { - dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value); - value = valueEl.value; - if (type === 'regexp') { - updateRegexpTester(); - } - emitSectionChange(); - }); - valueEl.addEventListener('change', validate); - } - - const apply = { - id: applyId, - all, - remove, - restore, - el, - getType: () => type, - getValue: () => value, - valueEl - }; - - const removeButton = $('.remove-applies-to', el); - if (removeButton) { - removeButton.addEventListener('click', e => { - e.preventDefault(); - removeApply(apply); - }); - } - $('.add-applies-to', el).addEventListener('click', e => { - e.preventDefault(); - insertApplyAfter({type, value: ''}, apply); - }); - - return apply; - - function validate() { - if (type !== 'regexp' || tryRegExp(value)) { - valueEl.setCustomValidity(''); - } else { - valueEl.setCustomValidity(t('styleBadRegexp')); - setTimeout(() => valueEl.reportValidity()); - } - } - - function remove() { - dirty.remove(`${dirtyPrefix}.type`, type); - dirty.remove(`${dirtyPrefix}.value`, value); - } - - function restore() { - dirty.add(`${dirtyPrefix}.type`, type); - dirty.add(`${dirtyPrefix}.value`, value); - } - } - } - function replaceSections(originalSections) { for (const section of sections) { section.remove(true);