Add: codemirror-factory
This commit is contained in:
parent
15a1f552f6
commit
d26ce3238e
|
@ -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();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
311
edit/codemirror-factory.js
Normal file
311
edit/codemirror-factory.js
Normal 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,
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -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) {
|
||||
|
|
13
edit/edit.js
13
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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
50
edit/reroute-hotkeys.js
Normal file
50
edit/reroute-hotkeys.js
Normal 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();
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -400,7 +400,6 @@ function createSourceEditor(style) {
|
|||
isDirty: dirty.isDirty,
|
||||
getStyle: () => style,
|
||||
getEditors: () => [cm],
|
||||
getLastActivatedEditor: () => cm,
|
||||
scrollToEditor: () => {},
|
||||
getStyleId: () => style.id,
|
||||
getEditorTitle: () => '',
|
||||
|
|
Loading…
Reference in New Issue
Block a user