Add: codemirror-factory

This commit is contained in:
eight 2018-10-10 14:49:37 +08:00
parent 15a1f552f6
commit d26ce3238e
10 changed files with 415 additions and 424 deletions

View File

@ -228,69 +228,48 @@
return isBlank; return isBlank;
}); });
// doubleclick option // editor commands
if (typeof editors !== 'undefined') { for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
const fn = (cm, repeat) => CodeMirror.commands[name] = () => editor[name]();
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);
} }
function selectTokenOnDoubleclick(cm, pos) { // CodeMirror convenience commands
let {ch} = pos; Object.assign(CodeMirror.commands, {
const {line, sticky} = pos; toggleEditorFocus,
const {text, styles} = cm.getLineHandle(line); jumpToLine,
commentSelection,
});
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text); function jumpToLine(cm) {
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text); const cur = cm.getCursor();
const atWord = ch => at(/\w/y, ch); const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
const atSpace = ch => at(/\s/y, ch); if (oldDialog) {
// close the currently opened minidialog
const atTokenEnd = styles.indexOf(ch, 1); cm.focus();
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1; }
ch = Math.min(text.length, ch); // make sure to focus the input in newly opened minidialog
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)}); // setTimeout(() => {
if (atTokenEnd > 0) ch--; // $('.CodeMirror-dialog', section).focus();
// });
const isCss = type && !/^(comment|string)/.test(type); cm.openDialog(template.jumpToLine.cloneNode(true), str => {
const isNumber = type === 'number'; const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
const isSpace = atSpace(ch); if (m) {
let wordChars = cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
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;
} }
} }, {value: cur.line + 1});
}
if (!found) { function commentSelection(cm) {
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy'); cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
b = ch + execAt(wordChars, ch)[0].length; }
}
return { function toggleEditorFocus(cm) {
from: {line, ch: a}, if (!cm) return;
to: {line, ch: b}, if (cm.hasFocus()) {
}; setTimeout(() => cm.display.input.blur());
} else {
cm.focus();
}
} }
})(); })();

View File

@ -6,356 +6,13 @@ global messageBox
'use strict'; 'use strict';
onDOMscriptReady('/codemirror.js').then(() => { 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();
}
}); });

311
edit/codemirror-factory.js Normal file
View File

@ -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,
});
}
})();

View File

@ -1,4 +1,4 @@
/* global CodeMirror loadScript editors showHelp */ /* global CodeMirror loadScript showHelp cmFactory */
'use strict'; 'use strict';
onDOMscriptReady('/colorview.js').then(() => { onDOMscriptReady('/colorview.js').then(() => {
@ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => {
defaults.extraKeys[keyName] = 'colorpicker'; defaults.extraKeys[keyName] = 'colorpicker';
} }
defaults.colorpicker = { defaults.colorpicker = {
forceUpdate: editors.length > 0, // FIXME: who uses this?
// forceUpdate: editor.getEditors().length > 0,
tooltip: t('colorpickerTooltip'), tooltip: t('colorpickerTooltip'),
popup: { popup: {
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
@ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => {
delete defaults.extraKeys[keyName]; delete defaults.extraKeys[keyName];
} }
} }
// on page load runs before CodeMirror.setOption is defined cmFactory.setOption('colorpicker', defaults.colorpicker);
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
} }
function registerHotkey(id, hotkey) { function registerHotkey(id, hotkey) {

View File

@ -5,7 +5,7 @@ global closeCurrentTab regExpTester messageBox
global setupCodeMirror global setupCodeMirror
global beautify global beautify
global sectionsToMozFormat global sectionsToMozFormat
global moveFocus editorWorker msg createSectionEditor global moveFocus editorWorker msg createSectionsEditor rerouteHotkeys
*/ */
'use strict'; 'use strict';
@ -229,7 +229,7 @@ preinit();
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
editor = usercss ? createSourceEditor(style) : createSectionEditor(style); editor = usercss ? createSourceEditor(style) : createSectionsEditor(style);
if (editor.ready) { if (editor.ready) {
return editor.ready(); return editor.ready();
} }
@ -362,11 +362,6 @@ function onRuntimeMessage(request) {
break; break;
} }
break; break;
case 'prefChanged':
if ('editor.smartIndent' in request.prefs) {
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
}
break;
case 'editDeleteText': case 'editDeleteText':
document.execCommand('delete'); document.execCommand('delete');
break; break;
@ -531,7 +526,7 @@ function showCodeMirrorPopup(title, html, options) {
keyMap: prefs.get('editor.keyMap') keyMap: prefs.get('editor.keyMap')
}, options)); }, options));
cm.focus(); cm.focus();
cm.rerouteHotkeys(false); rerouteHotkeys(false);
document.documentElement.style.pointerEvents = 'none'; document.documentElement.style.pointerEvents = 'none';
popup.style.pointerEvents = 'auto'; popup.style.pointerEvents = 'auto';
@ -550,7 +545,7 @@ function showCodeMirrorPopup(title, html, options) {
window.removeEventListener('closeHelp', _); window.removeEventListener('closeHelp', _);
window.removeEventListener('keydown', onKeyDown, true); window.removeEventListener('keydown', onKeyDown, true);
document.documentElement.style.removeProperty('pointer-events'); document.documentElement.style.removeProperty('pointer-events');
cm.rerouteHotkeys(true); rerouteHotkeys(true);
cm = popup.codebox = null; cm = popup.codebox = null;
}); });

View File

@ -212,7 +212,7 @@ onDOMready().then(() => {
state.activeAppliesTo || state.activeAppliesTo ||
state.cm); state.cm);
const cmExtra = $('body > :not(#sections) .CodeMirror'); const cmExtra = $('body > :not(#sections) .CodeMirror');
state.editors = cmExtra ? [cmExtra.CodeMirror] : editors; state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
} }

View File

@ -1,4 +1,5 @@
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/ /* global memoize editorWorker showCodeMirrorPopup loadScript messageBox
LINTER_DEFAULTS rerouteHotkeys */
'use strict'; 'use strict';
(() => { (() => {
@ -50,10 +51,10 @@
}); });
cm.on('changes', updateButtonState); cm.on('changes', updateButtonState);
cm.rerouteHotkeys(false); rerouteHotkeys(false);
window.addEventListener('closeHelp', function _() { window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _); window.removeEventListener('closeHelp', _);
cm.rerouteHotkeys(true); rerouteHotkeys(true);
cm = null; cm = null;
}); });

50
edit/reroute-hotkeys.js Normal file
View File

@ -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();
}
}
})();

View File

@ -134,7 +134,6 @@ function createSectionsEditor(style) {
isDirty: dirty.isDirty, isDirty: dirty.isDirty,
getStyle: () => style, getStyle: () => style,
getEditors, getEditors,
getLastActivatedEditor,
scrollToEditor, scrollToEditor,
getStyleId: () => style.id, getStyleId: () => style.id,
getEditorTitle: cm => { getEditorTitle: cm => {
@ -162,7 +161,7 @@ function createSectionsEditor(style) {
nearbyElement instanceof CodeMirror ? nearbyElement : nearbyElement instanceof CodeMirror ? nearbyElement :
nearbyElement instanceof Node && nearbyElement instanceof Node &&
(nearbyElement.closest('#sections > .section') || {}).CodeMirror || (nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
editor.getLastActivatedEditor(); getLastActivatedEditor();
if (nearbyElement instanceof Node && cm) { if (nearbyElement instanceof Node && cm) {
const {left, top} = nearbyElement.getBoundingClientRect(); const {left, top} = nearbyElement.getBoundingClientRect();
const bounds = cm.display.wrapper.getBoundingClientRect(); const bounds = cm.display.wrapper.getBoundingClientRect();
@ -172,7 +171,7 @@ function createSectionsEditor(style) {
} }
} }
// closest editor should have at least 2 lines visible // 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 scrollY = window.scrollY;
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight; const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top; const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
@ -203,7 +202,7 @@ function createSectionsEditor(style) {
} }
function findClosest() { function findClosest() {
const editors = editor.getEditors(); const editors = getEditors();
const last = editors.length - 1; const last = editors.length - 1;
let a = 0; let a = 0;
let b = last; let b = last;
@ -228,7 +227,7 @@ function createSectionsEditor(style) {
} }
const cm = editors[b]; const cm = editors[b];
if (distances[b] > 0) { if (distances[b] > 0) {
editor.scrollToEditor(cm); scrollToEditor(cm);
} }
return cm; return cm;
} }

View File

@ -400,7 +400,6 @@ function createSourceEditor(style) {
isDirty: dirty.isDirty, isDirty: dirty.isDirty,
getStyle: () => style, getStyle: () => style,
getEditors: () => [cm], getEditors: () => [cm],
getLastActivatedEditor: () => cm,
scrollToEditor: () => {}, scrollToEditor: () => {},
getStyleId: () => style.id, getStyleId: () => style.id,
getEditorTitle: () => '', getEditorTitle: () => '',