From d26ce3238e9beea602b4b47c4fd0184107712ce6 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 10 Oct 2018 14:49:37 +0800 Subject: [PATCH] Add: codemirror-factory --- edit/codemirror-default.js | 93 ++++----- edit/codemirror-editing-hooks.js | 345 +------------------------------ edit/codemirror-factory.js | 311 ++++++++++++++++++++++++++++ edit/colorpicker-helper.js | 8 +- edit/edit.js | 13 +- edit/global-search.js | 2 +- edit/linter-config-dialog.js | 7 +- edit/reroute-hotkeys.js | 50 +++++ edit/sections-editor.js | 9 +- edit/source-editor.js | 1 - 10 files changed, 415 insertions(+), 424 deletions(-) create mode 100644 edit/codemirror-factory.js create mode 100644 edit/reroute-hotkeys.js diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 1b9b15b3..274042fe 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -228,69 +228,48 @@ return isBlank; }); - // doubleclick option - if (typeof editors !== 'undefined') { - const fn = (cm, repeat) => - repeat === 'double' ? - {unit: selectTokenOnDoubleclick} : - {}; - const configure = (_, enabled) => { - editors.forEach(cm => cm.setOption('configureMouse', enabled ? fn : null)); - CodeMirror.defaults.configureMouse = enabled ? fn : null; - }; - configure(null, prefs.get('editor.selectByTokens')); - prefs.subscribe(['editor.selectByTokens'], configure); + // editor commands + for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) { + CodeMirror.commands[name] = () => editor[name](); } - function selectTokenOnDoubleclick(cm, pos) { - let {ch} = pos; - const {line, sticky} = pos; - const {text, styles} = cm.getLineHandle(line); + // CodeMirror convenience commands + Object.assign(CodeMirror.commands, { + toggleEditorFocus, + jumpToLine, + commentSelection, + }); - const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text); - const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text); - const atWord = ch => at(/\w/y, ch); - const atSpace = ch => at(/\s/y, ch); - - const atTokenEnd = styles.indexOf(ch, 1); - ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1; - ch = Math.min(text.length, ch); - const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)}); - if (atTokenEnd > 0) ch--; - - const isCss = type && !/^(comment|string)/.test(type); - const isNumber = type === 'number'; - const isSpace = atSpace(ch); - let wordChars = - isNumber ? /[-+\w.%]/y : - isCss ? /[-\w@]/y : - isSpace ? /\s/y : - atWord(ch) ? /\w/y : /[^\w\s]/y; - - let a = ch; - while (a && at(wordChars, a)) a--; - a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1); - - let b, found; - - if (isNumber) { - b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length; - found = b >= ch; - if (!found) { - a = b; - ch = a; + function jumpToLine(cm) { + const cur = cm.getCursor(); + const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper); + if (oldDialog) { + // close the currently opened minidialog + cm.focus(); + } + // make sure to focus the input in newly opened minidialog + // setTimeout(() => { + // $('.CodeMirror-dialog', section).focus(); + // }); + cm.openDialog(template.jumpToLine.cloneNode(true), str => { + const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); + if (m) { + cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); } - } + }, {value: cur.line + 1}); + } - if (!found) { - wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy'); - b = ch + execAt(wordChars, ch)[0].length; - } + function commentSelection(cm) { + cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); + } - return { - from: {line, ch: a}, - to: {line, ch: b}, - }; + function toggleEditorFocus(cm) { + if (!cm) return; + if (cm.hasFocus()) { + setTimeout(() => cm.display.input.blur()); + } else { + cm.focus(); + } } })(); diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index a5eccd27..8b1f0c5e 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -6,356 +6,13 @@ global messageBox 'use strict'; onDOMscriptReady('/codemirror.js').then(() => { - const COMMANDS = { - save, - toggleStyle, - toggleEditorFocus, - jumpToLine, - nextEditor, prevEditor, - commentSelection, - }; - const ORIGINAL_COMMANDS = { - insertTab: CodeMirror.commands.insertTab, - }; - // reroute handling to nearest editor when keypress resolves to one of these commands - const REROUTED = new Set([ - 'save', - 'toggleStyle', - 'jumpToLine', - 'nextEditor', 'prevEditor', - 'toggleEditorFocus', - 'find', 'findNext', 'findPrev', 'replace', 'replaceAll', - 'colorpicker', - ]); - Object.assign(CodeMirror.prototype, { - // getSection, - rerouteHotkeys, - }); - Object.assign(CodeMirror.commands, COMMANDS); - rerouteHotkeys(true); - CodeMirror.defineInitHook(cm => { - if (!cm.display.wrapper.closest('#sections')) { - return; - } - if (prefs.get('editor.autocompleteOnTyping')) { - setupAutocomplete(cm); - } - const wrapper = cm.display.wrapper; - cm.on('blur', () => { - cm.rerouteHotkeys(true); - setTimeout(() => { - wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); - }); - }); - cm.on('focus', () => { - cm.rerouteHotkeys(false); - wrapper.classList.add('CodeMirror-active'); - }); - }); - - // FIXME: pull this into a module - window.rerouteHotkeys = rerouteHotkeys; - - prefs.subscribe(null, onPrefChanged); - - //////////////////////////////////////////////// - - function getOption(o) { - return CodeMirror.defaults[o]; - } - - function setOption(o, v) { - CodeMirror.defaults[o] = v; - if (!editor) { - return; - } - const editors = editor.getEditors(); - if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) { - throttleSetOption({key: o, value: v, index: 0}); - return; - } - editors.forEach(editor => { - editor.setOption(o, v); - }); - } - - function throttleSetOption({ - key, - value, - index, - timeStart = performance.now(), - cmStart = editor.getLastActivatedEditor(), - editorsCopy = editor.getEditors().slice(), - progress, - }) { - if (index === 0) { - if (!cmStart) { - return; - } - cmStart.setOption(key, value); - } - - const THROTTLE_AFTER_MS = 100; - const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100; - - const t0 = performance.now(); - const total = editorsCopy.length; - const editors = editor.getEditors(); - while (index < total) { - const cm = editorsCopy[index++]; - if (cm === cmStart || - cm !== editors[index] && !editors.includes(cm)) { - continue; - } - cm.setOption(key, value); - if (performance.now() - t0 > THROTTLE_AFTER_MS) { - break; - } - } - if (index >= total) { - $.remove(progress); - return; - } - if (!progress && - index < total / 2 && - t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) { - let option = $('#editor.' + key); - if (option) { - if (option.type === 'checkbox') { - option = (option.labels || [])[0] || option.nextElementSibling || option; - } - progress = document.body.appendChild( - $create('.set-option-progress', {targetElement: option})); - } - } - if (progress) { - const optionBounds = progress.targetElement.getBoundingClientRect(); - const bounds = { - top: optionBounds.top + window.scrollY + 1, - left: optionBounds.left + window.scrollX + 1, - width: (optionBounds.width - 2) * index / total | 0, - height: optionBounds.height - 2, - }; - const style = progress.style; - for (const prop in bounds) { - if (bounds[prop] !== parseFloat(style[prop])) { - style[prop] = bounds[prop] + 'px'; - } - } - } - setTimeout(throttleSetOption, 0, { - key, - value, - index, - timeStart, - cmStart, - editorsCopy, - progress, - }); - } - - function getSection() { - return this.display.wrapper.parentNode; - } - - function nextEditor(cm) { - return editor.nextEditor(cm); - } - - function prevEditor(cm) { - return editor.prevEditor(cm); - } - - function jumpToLine(cm) { - const cur = cm.getCursor(); - refocusMinidialog(cm); - cm.openDialog(template.jumpToLine.cloneNode(true), str => { - const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); - if (m) { - cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); - } - }, {value: cur.line + 1}); - } - - function commentSelection(cm) { - cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); - } - - function toggleEditorFocus(cm) { - if (!cm) return; - if (cm.hasFocus()) { - setTimeout(() => cm.display.input.blur()); - } else { - cm.focus(); - } - } - - function refocusMinidialog(cm) { - const section = cm.getSection(); - if (!$('.CodeMirror-dialog', section)) { - return; - } - // close the currently opened minidialog - cm.focus(); - // make sure to focus the input in newly opened minidialog - setTimeout(() => { - $('.CodeMirror-dialog', section).focus(); - }); - } - - function onPrefChanged(key, value) { - let option = key.replace(/^editor\./, ''); - if (!option) { - console.error('no "cm_option"', key); - return; - } - switch (option) { - case 'tabSize': - value = Number(value); - CodeMirror.setOption('indentUnit', value); - break; - - case 'indentWithTabs': - CodeMirror.commands.insertTab = value ? - ORIGINAL_COMMANDS.insertTab : - CodeMirror.commands.insertSoftTab; - break; - - case 'theme': { - const themeLink = $('#cm-theme'); - // use non-localized 'default' internally - if (!value || value === 'default' || value === t('defaultTheme')) { - value = 'default'; - if (prefs.get(key) !== value) { - prefs.set(key, value); - } - themeLink.href = ''; - $('#editor.theme').value = value; - break; - } - const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); - if (themeLink.href === url) { - // preloaded in initCodeMirror() - break; - } - // avoid flicker: wait for the second stylesheet to load, then apply the theme - document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url})); - setTimeout(() => { - CodeMirror.setOption(option, value); - themeLink.remove(); - $('#cm-theme2').id = 'cm-theme'; - }, 100); - return; - } - - case 'autocompleteOnTyping': - if (editor) { - // FIXME: this won't work with removed sections - editor.getEditors().forEach(cm => setupAutocomplete(cm, value)); - } - return; - - case 'autoCloseBrackets': - Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => { - CodeMirror.setOption(option, value); - }); - return; - - case 'matchHighlight': - switch (value) { - case 'token': - case 'selection': - document.body.dataset[option] = value; - value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true}; - break; - default: - value = null; - } - option = 'highlightSelectionMatches'; - break; - - case 'colorpicker': - return; - } - CodeMirror.setOption(option, value); - } - - //////////////////////////////////////////////// - - function rerouteHotkeys(enable, immediately) { - if (!immediately) { - debounce(rerouteHotkeys, 0, enable, true); - } else if (enable) { - document.addEventListener('keydown', rerouteHandler); - } else { - document.removeEventListener('keydown', rerouteHandler); - } - } - - function rerouteHandler(event) { - const keyName = CodeMirror.keyName(event); - if (!keyName) { - return; - } - const rerouteCommand = name => { - if (REROUTED.has(name)) { - CodeMirror.commands[name](editor.closestVisible(event.target)); - return true; - } - }; - if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' || - CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') { - event.preventDefault(); - event.stopPropagation(); - } - } //////////////////////////////////////////////// //////////////////////////////////////////////// - function setupAutocomplete(cm, enable = true) { - const onOff = enable ? 'on' : 'off'; - cm[onOff]('changes', autocompleteOnTyping); - cm[onOff]('pick', autocompletePicked); - } + //////////////////////////////////////////////// - function autocompleteOnTyping(cm, [info], debounced) { - if ( - cm.state.completionActive || - info.origin && !info.origin.includes('input') || - !info.text.last - ) { - return; - } - if (cm.state.autocompletePicked) { - cm.state.autocompletePicked = false; - return; - } - if (!debounced) { - debounce(autocompleteOnTyping, 100, cm, [info], true); - return; - } - if (info.text.last.match(/[-a-z!]+$/i)) { - cm.state.autocompletePicked = false; - cm.options.hintOptions.completeSingle = false; - cm.execCommand('autocomplete'); - setTimeout(() => { - cm.options.hintOptions.completeSingle = true; - }); - } - } - function autocompletePicked(cm) { - cm.state.autocompletePicked = true; - } - - function save() { - editor.save(); - } - - function toggleStyle() { - editor.toggleStyle(); - } }); diff --git a/edit/codemirror-factory.js b/edit/codemirror-factory.js new file mode 100644 index 00000000..0150be5b --- /dev/null +++ b/edit/codemirror-factory.js @@ -0,0 +1,311 @@ +/* global CodeMirror loadScript rerouteHotkeys */ +'use strict'; +/* +All cm instances created by this module are collected so we can broadcast prefs +settings to them. You should `cmFactory.destroy(cm)` to unregister the listener +when the instance is not used anymore. +*/ +const cmFactory = (() => { + const editors = new Set(); + // used by `indentWithTabs` option + const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab; + const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab; + prefs.subscribe(null, onPrefChanged); + return {create, destroy, setOption}; + + function onPrefChanged(key, value) { + let option = key.replace(/^editor\./, ''); + if (!option) { + console.error('no "cm_option"', key); + return; + } + switch (option) { + case 'tabSize': + value = Number(value); + setOption('indentUnit', value); + break; + + case 'indentWithTabs': + CodeMirror.commands.insertTab = value ? + INSERT_TAB_COMMAND : + INSERT_SOFT_TAB_COMMAND; + break; + + case 'theme': { + const themeLink = $('#cm-theme'); + // use non-localized 'default' internally + if (!value || value === 'default' || value === t('defaultTheme')) { + value = 'default'; + if (prefs.get(key) !== value) { + prefs.set(key, value); + } + themeLink.href = ''; + $('#editor.theme').value = value; + break; + } + const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); + if (themeLink.href === url) { + // preloaded in initCodeMirror() + break; + } + // avoid flicker: wait for the second stylesheet to load, then apply the theme + document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url})); + setTimeout(() => { + setOption(option, value); + themeLink.remove(); + $('#cm-theme2').id = 'cm-theme'; + }, 100); + return; + } + + case 'autocompleteOnTyping': + for (const cm of editors) { + setupAutocomplete(cm, value); + } + return; + + case 'autoCloseBrackets': + Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => { + setOption(option, value); + }); + return; + + case 'matchHighlight': + switch (value) { + case 'token': + case 'selection': + document.body.dataset[option] = value; + value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true}; + break; + default: + value = null; + } + option = 'highlightSelectionMatches'; + break; + + case 'colorpicker': + // FIXME: this is implemented in `colorpicker-helper.js`. + return; + + case 'selectByTokens': + option = 'configureMouse'; + value = value ? configureMouseFn : null; + break; + } + setOption(option, value); + } + + function configureMouseFn(cm, repeat) { + return repeat === 'double' ? + {unit: selectTokenOnDoubleclick} : + {}; + } + + function selectTokenOnDoubleclick(cm, pos) { + let {ch} = pos; + const {line, sticky} = pos; + const {text, styles} = cm.getLineHandle(line); + + const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text); + const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text); + const atWord = ch => at(/\w/y, ch); + const atSpace = ch => at(/\s/y, ch); + + const atTokenEnd = styles.indexOf(ch, 1); + ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1; + ch = Math.min(text.length, ch); + const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)}); + if (atTokenEnd > 0) ch--; + + const isCss = type && !/^(comment|string)/.test(type); + const isNumber = type === 'number'; + const isSpace = atSpace(ch); + let wordChars = + isNumber ? /[-+\w.%]/y : + isCss ? /[-\w@]/y : + isSpace ? /\s/y : + atWord(ch) ? /\w/y : /[^\w\s]/y; + + let a = ch; + while (a && at(wordChars, a)) a--; + a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1); + + let b, found; + + if (isNumber) { + b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length; + found = b >= ch; + if (!found) { + a = b; + ch = a; + } + } + + if (!found) { + wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy'); + b = ch + execAt(wordChars, ch)[0].length; + } + + return { + from: {line, ch: a}, + to: {line, ch: b}, + }; + } + + function setupAutocomplete(cm, enable = true) { + const onOff = enable ? 'on' : 'off'; + cm[onOff]('changes', autocompleteOnTyping); + cm[onOff]('pick', autocompletePicked); + } + + function autocompleteOnTyping(cm, [info], debounced) { + if ( + cm.state.completionActive || + info.origin && !info.origin.includes('input') || + !info.text.last + ) { + return; + } + if (cm.state.autocompletePicked) { + cm.state.autocompletePicked = false; + return; + } + if (!debounced) { + debounce(autocompleteOnTyping, 100, cm, [info], true); + return; + } + if (info.text.last.match(/[-a-z!]+$/i)) { + cm.state.autocompletePicked = false; + cm.options.hintOptions.completeSingle = false; + cm.execCommand('autocomplete'); + setTimeout(() => { + cm.options.hintOptions.completeSingle = true; + }); + } + } + + function autocompletePicked(cm) { + cm.state.autocompletePicked = true; + } + + function destroy(cm) { + editors.delete(cm); + } + + function create(init, options) { + const cm = CodeMirror(init, options); + if (prefs.get('editor.autocompleteOnTyping')) { + setupAutocomplete(cm); + } + cm.lastActive = 0; + const wrapper = cm.display.wrapper; + cm.on('blur', () => { + rerouteHotkeys(true); + setTimeout(() => { + wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); + }); + }); + cm.on('focus', () => { + rerouteHotkeys(false); + wrapper.classList.add('CodeMirror-active'); + cm.lastActive = Date.now(); + }); + editors.add(cm); + cm.distroy = () => editors.delete(cm); + return cm; + } + + function getLastActivated() { + let result; + for (const cm of editors) { + if (!result || result.lastActive < cm.lastActive) { + result = cm; + } + } + return result; + } + + function setOption(key, value) { + CodeMirror.defaults[key] = value; + if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) { + throttleSetOption({key, value, index: 0}); + return; + } + for (const editor of editors) { + editor.setOption(key, value); + } + } + + function throttleSetOption({ + key, + value, + index, + timeStart = performance.now(), + editorsCopy = [...editors], + cmStart = getLastActivated(), + progress, + }) { + if (index === 0) { + if (!cmStart) { + return; + } + cmStart.setOption(key, value); + } + + const THROTTLE_AFTER_MS = 100; + const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100; + + const t0 = performance.now(); + const total = editorsCopy.length; + while (index < total) { + const cm = editorsCopy[index++]; + if (cm === cmStart || !editors.has(cm)) { + continue; + } + cm.setOption(key, value); + if (performance.now() - t0 > THROTTLE_AFTER_MS) { + break; + } + } + if (index >= total) { + $.remove(progress); + return; + } + if (!progress && + index < total / 2 && + t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) { + let option = $('#editor.' + key); + if (option) { + if (option.type === 'checkbox') { + option = (option.labels || [])[0] || option.nextElementSibling || option; + } + progress = document.body.appendChild( + $create('.set-option-progress', {targetElement: option})); + } + } + if (progress) { + const optionBounds = progress.targetElement.getBoundingClientRect(); + const bounds = { + top: optionBounds.top + window.scrollY + 1, + left: optionBounds.left + window.scrollX + 1, + width: (optionBounds.width - 2) * index / total | 0, + height: optionBounds.height - 2, + }; + const style = progress.style; + for (const prop in bounds) { + if (bounds[prop] !== parseFloat(style[prop])) { + style[prop] = bounds[prop] + 'px'; + } + } + } + setTimeout(throttleSetOption, 0, { + key, + value, + index, + timeStart, + cmStart, + editorsCopy, + progress, + }); + } +})(); diff --git a/edit/colorpicker-helper.js b/edit/colorpicker-helper.js index d368a7fd..3d1bde61 100644 --- a/edit/colorpicker-helper.js +++ b/edit/colorpicker-helper.js @@ -1,4 +1,4 @@ -/* global CodeMirror loadScript editors showHelp */ +/* global CodeMirror loadScript showHelp cmFactory */ 'use strict'; onDOMscriptReady('/colorview.js').then(() => { @@ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => { defaults.extraKeys[keyName] = 'colorpicker'; } defaults.colorpicker = { - forceUpdate: editors.length > 0, + // FIXME: who uses this? + // forceUpdate: editor.getEditors().length > 0, tooltip: t('colorpickerTooltip'), popup: { tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), @@ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => { delete defaults.extraKeys[keyName]; } } - // on page load runs before CodeMirror.setOption is defined - editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker)); + cmFactory.setOption('colorpicker', defaults.colorpicker); } function registerHotkey(id, hotkey) { diff --git a/edit/edit.js b/edit/edit.js index 7067ae28..14780a9e 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -5,7 +5,7 @@ global closeCurrentTab regExpTester messageBox global setupCodeMirror global beautify global sectionsToMozFormat -global moveFocus editorWorker msg createSectionEditor +global moveFocus editorWorker msg createSectionsEditor rerouteHotkeys */ 'use strict'; @@ -229,7 +229,7 @@ preinit(); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); - editor = usercss ? createSourceEditor(style) : createSectionEditor(style); + editor = usercss ? createSourceEditor(style) : createSectionsEditor(style); if (editor.ready) { return editor.ready(); } @@ -362,11 +362,6 @@ function onRuntimeMessage(request) { break; } break; - case 'prefChanged': - if ('editor.smartIndent' in request.prefs) { - CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']); - } - break; case 'editDeleteText': document.execCommand('delete'); break; @@ -531,7 +526,7 @@ function showCodeMirrorPopup(title, html, options) { keyMap: prefs.get('editor.keyMap') }, options)); cm.focus(); - cm.rerouteHotkeys(false); + rerouteHotkeys(false); document.documentElement.style.pointerEvents = 'none'; popup.style.pointerEvents = 'auto'; @@ -550,7 +545,7 @@ function showCodeMirrorPopup(title, html, options) { window.removeEventListener('closeHelp', _); window.removeEventListener('keydown', onKeyDown, true); document.documentElement.style.removeProperty('pointer-events'); - cm.rerouteHotkeys(true); + rerouteHotkeys(true); cm = popup.codebox = null; }); diff --git a/edit/global-search.js b/edit/global-search.js index 7fe232f8..d364ec60 100644 --- a/edit/global-search.js +++ b/edit/global-search.js @@ -212,7 +212,7 @@ onDOMready().then(() => { state.activeAppliesTo || state.cm); const cmExtra = $('body > :not(#sections) .CodeMirror'); - state.editors = cmExtra ? [cmExtra.CodeMirror] : editors; + state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors(); } diff --git a/edit/linter-config-dialog.js b/edit/linter-config-dialog.js index c1ccf51f..c06ce9ba 100644 --- a/edit/linter-config-dialog.js +++ b/edit/linter-config-dialog.js @@ -1,4 +1,5 @@ -/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/ +/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox + LINTER_DEFAULTS rerouteHotkeys */ 'use strict'; (() => { @@ -50,10 +51,10 @@ }); cm.on('changes', updateButtonState); - cm.rerouteHotkeys(false); + rerouteHotkeys(false); window.addEventListener('closeHelp', function _() { window.removeEventListener('closeHelp', _); - cm.rerouteHotkeys(true); + rerouteHotkeys(true); cm = null; }); diff --git a/edit/reroute-hotkeys.js b/edit/reroute-hotkeys.js new file mode 100644 index 00000000..9d4d5b50 --- /dev/null +++ b/edit/reroute-hotkeys.js @@ -0,0 +1,50 @@ +/* global CodeMirror editor */ +'use strict'; + +const rerouteHotkeys = (() => { + // reroute handling to nearest editor when keypress resolves to one of these commands + const REROUTED = new Set([ + 'save', + 'toggleStyle', + 'jumpToLine', + 'nextEditor', 'prevEditor', + 'toggleEditorFocus', + 'find', 'findNext', 'findPrev', 'replace', 'replaceAll', + 'colorpicker', + ]); + + rerouteHotkeys(true); + + return rerouteHotkeys; + + function rerouteHotkeys(enable, immediately) { + if (!immediately) { + debounce(rerouteHotkeys, 0, enable, true); + } else if (enable) { + document.addEventListener('keydown', rerouteHandler); + } else { + document.removeEventListener('keydown', rerouteHandler); + } + } + + function rerouteHandler(event) { + if (!editor) { + return; + } + const keyName = CodeMirror.keyName(event); + if (!keyName) { + return; + } + const rerouteCommand = name => { + if (REROUTED.has(name)) { + CodeMirror.commands[name](editor.closestVisible(event.target)); + return true; + } + }; + if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' || + CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') { + event.preventDefault(); + event.stopPropagation(); + } + } +})(); diff --git a/edit/sections-editor.js b/edit/sections-editor.js index aa3fe69a..45299c9b 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -134,7 +134,6 @@ function createSectionsEditor(style) { isDirty: dirty.isDirty, getStyle: () => style, getEditors, - getLastActivatedEditor, scrollToEditor, getStyleId: () => style.id, getEditorTitle: cm => { @@ -162,7 +161,7 @@ function createSectionsEditor(style) { nearbyElement instanceof CodeMirror ? nearbyElement : nearbyElement instanceof Node && (nearbyElement.closest('#sections > .section') || {}).CodeMirror || - editor.getLastActivatedEditor(); + getLastActivatedEditor(); if (nearbyElement instanceof Node && cm) { const {left, top} = nearbyElement.getBoundingClientRect(); const bounds = cm.display.wrapper.getBoundingClientRect(); @@ -172,7 +171,7 @@ function createSectionsEditor(style) { } } // closest editor should have at least 2 lines visible - const lineHeight = editor.getEditors()[0].defaultTextHeight(); + const lineHeight = sections[0].cm.defaultTextHeight(); const scrollY = window.scrollY; const windowBottom = scrollY + window.innerHeight - 2 * lineHeight; const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top; @@ -203,7 +202,7 @@ function createSectionsEditor(style) { } function findClosest() { - const editors = editor.getEditors(); + const editors = getEditors(); const last = editors.length - 1; let a = 0; let b = last; @@ -228,7 +227,7 @@ function createSectionsEditor(style) { } const cm = editors[b]; if (distances[b] > 0) { - editor.scrollToEditor(cm); + scrollToEditor(cm); } return cm; } diff --git a/edit/source-editor.js b/edit/source-editor.js index 10604551..34bddbdb 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -400,7 +400,6 @@ function createSourceEditor(style) { isDirty: dirty.isDirty, getStyle: () => style, getEditors: () => [cm], - getLastActivatedEditor: () => cm, scrollToEditor: () => {}, getStyleId: () => style.id, getEditorTitle: () => '',