WIP: edit page

This commit is contained in:
eight 2018-10-10 02:43:09 +08:00
parent fd9ab5d6e5
commit ba6159e067
4 changed files with 279 additions and 239 deletions

View File

@ -1,7 +1,6 @@
/* /*
global CodeMirror loadScript global CodeMirror loadScript
global editor ownTabId global editor ownTabId
global save toggleStyle makeSectionVisible
global messageBox global messageBox
*/ */
'use strict'; 'use strict';
@ -37,6 +36,8 @@ onDOMscriptReady('/codemirror.js').then(() => {
getSection, getSection,
rerouteHotkeys, rerouteHotkeys,
}); });
Object.assign(CodeMirror.commands, COMMANDS);
rerouteHotkeys(true);
CodeMirror.defineInitHook(cm => { CodeMirror.defineInitHook(cm => {
if (!cm.display.wrapper.closest('#sections')) { if (!cm.display.wrapper.closest('#sections')) {
@ -58,27 +59,10 @@ onDOMscriptReady('/codemirror.js').then(() => {
}); });
}); });
new MutationObserver((mutations, observer) => { // FIXME: pull this into a module
if (!$('#sections')) { window.rerouteHotkeys = rerouteHotkeys;
return;
}
observer.disconnect();
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip); prefs.subscribe(null, onPrefChanged);
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip();
// N.B. the onchange event listeners should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged);
buildThemeElement();
buildKeymapElement();
setupLivePrefs();
Object.assign(CodeMirror.commands, COMMANDS);
rerouteHotkeys(true);
}).observe(document, {childList: true, subtree: true});
return;
//////////////////////////////////////////////// ////////////////////////////////////////////////
@ -88,6 +72,9 @@ onDOMscriptReady('/codemirror.js').then(() => {
function setOption(o, v) { function setOption(o, v) {
CodeMirror.defaults[o] = v; CodeMirror.defaults[o] = v;
if (!editor) {
return;
}
const editors = editor.getEditors(); const editors = editor.getEditors();
if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) { if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
throttleSetOption({key: o, value: v, index: 0}); throttleSetOption({key: o, value: v, index: 0});
@ -178,19 +165,11 @@ onDOMscriptReady('/codemirror.js').then(() => {
} }
function nextEditor(cm) { function nextEditor(cm) {
return nextPrevEditor(cm, 1); return editor.nextEditor(cm);
} }
function prevEditor(cm) { function prevEditor(cm) {
return nextPrevEditor(cm, -1); return editor.prevEditor(cm);
}
function nextPrevEditor(cm, direction) {
const editors = editor.getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
editor.scrollToEditor(cm);
cm.focus();
return cm;
} }
function jumpToLine(cm) { function jumpToLine(cm) {
@ -230,14 +209,12 @@ onDOMscriptReady('/codemirror.js').then(() => {
}); });
} }
function onOptionElementChanged(event) { function onPrefChanged(key, value) {
const el = event.target; let option = key.replace(/^editor\./, '');
let option = el.id.replace(/^editor\./, '');
if (!option) { if (!option) {
console.error('no "cm_option"', el); console.error('no "cm_option"', key);
return; return;
} }
let value = el.type === 'checkbox' ? el.checked : el.value;
switch (option) { switch (option) {
case 'tabSize': case 'tabSize':
value = Number(value); value = Number(value);
@ -255,11 +232,11 @@ onDOMscriptReady('/codemirror.js').then(() => {
// use non-localized 'default' internally // use non-localized 'default' internally
if (!value || value === 'default' || value === t('defaultTheme')) { if (!value || value === 'default' || value === t('defaultTheme')) {
value = 'default'; value = 'default';
if (prefs.get(el.id) !== value) { if (prefs.get(key) !== value) {
prefs.set(el.id, value); prefs.set(key, value);
} }
themeLink.href = ''; themeLink.href = '';
el.selectedIndex = 0; $('#editor.theme').value = value;
break; break;
} }
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
@ -278,7 +255,9 @@ onDOMscriptReady('/codemirror.js').then(() => {
} }
case 'autocompleteOnTyping': case 'autocompleteOnTyping':
editor.getEditors().forEach(cm => setupAutocomplete(cm, el.checked)); if (editor) {
editor.getEditors().forEach(cm => setupAutocomplete(cm, value));
}
return; return;
case 'autoCloseBrackets': case 'autoCloseBrackets':
@ -306,62 +285,6 @@ onDOMscriptReady('/codemirror.js').then(() => {
CodeMirror.setOption(option, value); CodeMirror.setOption(option, value);
} }
function buildThemeElement() {
const themeElement = $('#editor.theme');
const themeList = localStorage.codeMirrorThemes;
const optionsFromArray = options => {
const fragment = document.createDocumentFragment();
options.forEach(opt => fragment.appendChild($create('option', opt)));
themeElement.appendChild(fragment);
};
if (themeList) {
optionsFromArray(themeList.split(/\s+/));
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
const theme = prefs.get('editor.theme');
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
optionsFromArray(themes);
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
}
function buildKeymapElement() {
// move 'pc' or 'mac' prefix to the end of the displayed label
const maps = Object.keys(CodeMirror.keyMap)
.map(name => ({
value: name,
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
}))
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
const fragment = document.createDocumentFragment();
let bin = fragment;
let groupName;
// group suffixed maps in <optgroup>
maps.forEach(({value, name}, i) => {
groupName = !name.includes('-') ? name : groupName;
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
if (groupWithNext) {
if (bin === fragment) {
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
}
}
const el = bin.appendChild($create('option', {value}, name));
if (value === prefs.defaults['editor.keyMap']) {
el.dataset.default = '';
el.title = t('defaultTheme');
}
if (!groupWithNext) bin = fragment;
});
$('#editor.keyMap').appendChild(fragment);
}
//////////////////////////////////////////////// ////////////////////////////////////////////////
function rerouteHotkeys(enable, immediately) { function rerouteHotkeys(enable, immediately) {
@ -477,121 +400,6 @@ onDOMscriptReady('/codemirror.js').then(() => {
//////////////////////////////////////////////// ////////////////////////////////////////////////
function getCodeMirrorThemes() {
if (!chrome.runtime.getPackageDirectoryEntry) {
const themes = [
chrome.i18n.getMessage('defaultTheme'),
/* populate-theme-start */
'3024-day',
'3024-night',
'abcdef',
'ambiance',
'ambiance-mobile',
'base16-dark',
'base16-light',
'bespin',
'blackboard',
'cobalt',
'colorforth',
'darcula',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'gruvbox-dark',
'hopscotch',
'icecoder',
'idea',
'isotope',
'lesser-dark',
'liquibyte',
'lucario',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'oceanic-next',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'shadowfox',
'solarized',
'ssms',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'yeti',
'zenburn',
/* populate-theme-end */
];
localStorage.codeMirrorThemes = themes.join(' ');
return Promise.resolve(themes);
}
return new Promise(resolve => {
chrome.runtime.getPackageDirectoryEntry(rootDir => {
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
themeDir.createReader().readEntries(entries => {
const themes = [
chrome.i18n.getMessage('defaultTheme')
].concat(
entries.filter(entry => entry.isFile)
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map(entry => entry.name.replace(/\.css$/, ''))
);
localStorage.codeMirrorThemes = themes.join(' ');
resolve(themes);
});
});
});
});
}
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
const extraKeys = CodeMirror.defaults.extraKeys;
for (const el of $$('[data-hotkey-tooltip]')) {
if (el._hotkeyTooltipKeyMap !== mapName) {
el._hotkeyTooltipKeyMap = mapName;
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
const cmd = el.dataset.hotkeyTooltip;
const key = cmd[0] === '=' ? cmd.slice(1) :
findKeyForCommand(cmd, mapName) ||
extraKeys && findKeyForCommand(cmd, extraKeys);
const newTitle = title + (title && key ? '\n' : '') + (key || '');
if (el.title !== newTitle) el.title = newTitle;
}
}
}
function findKeyForCommand(command, map) {
if (typeof map === 'string') map = CodeMirror.keyMap[map];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
function setupAutocomplete(cm, enable = true) { function setupAutocomplete(cm, enable = true) {
const onOff = enable ? 'on' : 'off'; const onOff = enable ? 'on' : 'off';
cm[onOff]('changes', autocompleteOnTyping); cm[onOff]('changes', autocompleteOnTyping);
@ -627,4 +435,12 @@ onDOMscriptReady('/codemirror.js').then(() => {
function autocompletePicked(cm) { function autocompletePicked(cm) {
cm.state.autocompletePicked = true; cm.state.autocompletePicked = true;
} }
function save() {
editor.save();
}
function toggleStyle() {
editor.toggleStyle();
}
}); });

View File

@ -29,11 +29,197 @@ msg.onExtension(onRuntimeMessage);
preinit(); preinit();
Promise.all([ (() => {
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip();
buildThemeElement();
buildKeymapElement();
setupLivePrefs();
});
initEditor();
function getCodeMirrorThemes() {
if (!chrome.runtime.getPackageDirectoryEntry) {
const themes = [
chrome.i18n.getMessage('defaultTheme'),
/* populate-theme-start */
'3024-day',
'3024-night',
'abcdef',
'ambiance',
'ambiance-mobile',
'base16-dark',
'base16-light',
'bespin',
'blackboard',
'cobalt',
'colorforth',
'darcula',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'gruvbox-dark',
'hopscotch',
'icecoder',
'idea',
'isotope',
'lesser-dark',
'liquibyte',
'lucario',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'oceanic-next',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'shadowfox',
'solarized',
'ssms',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'yeti',
'zenburn',
/* populate-theme-end */
];
localStorage.codeMirrorThemes = themes.join(' ');
return Promise.resolve(themes);
}
return new Promise(resolve => {
chrome.runtime.getPackageDirectoryEntry(rootDir => {
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
themeDir.createReader().readEntries(entries => {
const themes = [
chrome.i18n.getMessage('defaultTheme')
].concat(
entries.filter(entry => entry.isFile)
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map(entry => entry.name.replace(/\.css$/, ''))
);
localStorage.codeMirrorThemes = themes.join(' ');
resolve(themes);
});
});
});
});
}
function findKeyForCommand(command, map) {
if (typeof map === 'string') map = CodeMirror.keyMap[map];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
function buildThemeElement() {
const themeElement = $('#editor.theme');
const themeList = localStorage.codeMirrorThemes;
const optionsFromArray = options => {
const fragment = document.createDocumentFragment();
options.forEach(opt => fragment.appendChild($create('option', opt)));
themeElement.appendChild(fragment);
};
if (themeList) {
optionsFromArray(themeList.split(/\s+/));
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
const theme = prefs.get('editor.theme');
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
optionsFromArray(themes);
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
}
function buildKeymapElement() {
// move 'pc' or 'mac' prefix to the end of the displayed label
const maps = Object.keys(CodeMirror.keyMap)
.map(name => ({
value: name,
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
}))
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
const fragment = document.createDocumentFragment();
let bin = fragment;
let groupName;
// group suffixed maps in <optgroup>
maps.forEach(({value, name}, i) => {
groupName = !name.includes('-') ? name : groupName;
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
if (groupWithNext) {
if (bin === fragment) {
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
}
}
const el = bin.appendChild($create('option', {value}, name));
if (value === prefs.defaults['editor.keyMap']) {
el.dataset.default = '';
el.title = t('defaultTheme');
}
if (!groupWithNext) bin = fragment;
});
$('#editor.keyMap').appendChild(fragment);
}
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
const extraKeys = CodeMirror.defaults.extraKeys;
for (const el of $$('[data-hotkey-tooltip]')) {
if (el._hotkeyTooltipKeyMap !== mapName) {
el._hotkeyTooltipKeyMap = mapName;
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
const cmd = el.dataset.hotkeyTooltip;
const key = cmd[0] === '=' ? cmd.slice(1) :
findKeyForCommand(cmd, mapName) ||
extraKeys && findKeyForCommand(cmd, extraKeys);
const newTitle = title + (title && key ? '\n' : '') + (key || '');
if (el.title !== newTitle) el.title = newTitle;
}
}
}
function initEditor() {
return Promise.all([
initStyleData(), initStyleData(),
onDOMready(), onDOMready(),
]) ])
.then(([style]) => { .then(([style]) => {
const usercss = isUsercss(style); const usercss = isUsercss(style);
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
@ -46,7 +232,12 @@ Promise.all([
window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
editor = usercss ? createSourceEditor(style) : createSectionEditor(style); editor = usercss ? createSourceEditor(style) : createSectionEditor(style);
}); if (editor.ready) {
return editor.ready();
}
});
}
})();
function preinit() { function preinit() {
// make querySelectorAll enumeration code readable // make querySelectorAll enumeration code readable

View File

@ -3,6 +3,7 @@
CodeMirror nextPrevEditorOnKeydown showAppliesToHelp propertyToCss CodeMirror nextPrevEditorOnKeydown showAppliesToHelp propertyToCss
regExpTester linter cssToProperty createLivePreview showCodeMirrorPopup regExpTester linter cssToProperty createLivePreview showCodeMirrorPopup
sectionsToMozFormat editorWorker messageBox clipString beautify sectionsToMozFormat editorWorker messageBox clipString beautify
rerouteHotkeys
*/ */
'use strict'; 'use strict';
@ -110,15 +111,16 @@ function createSectionsEditor(style) {
} }
let sectionOrder = ''; let sectionOrder = '';
initSection({ const initializing = new Promise(resolve => initSection({
sections: style.sections.slice(), sections: style.sections.slice(),
done:() => { done:() => {
// FIXME: implement this with CSS? // FIXME: implement this with CSS?
// https://github.com/openstyles/stylus/commit/2895ce11e271788df0e4f7314b3b981fde086574 // https://github.com/openstyles/stylus/commit/2895ce11e271788df0e4f7314b3b981fde086574
// maximizeCodeHeight(sections[sections.length - 1], true);
dirty.clear(); dirty.clear();
rerouteHotkeys(true);
resolve();
} }
}); }));
const livePreview = createLivePreview(); const livePreview = createLivePreview();
livePreview.show(Boolean(style.id)); livePreview.show(Boolean(style.id));
@ -126,20 +128,51 @@ function createSectionsEditor(style) {
updateHeader(); updateHeader();
return { return {
ready: () => initializing,
replaceStyle, replaceStyle,
isDirty: dirty.isDirty, isDirty: dirty.isDirty,
getStyle: () => style, getStyle: () => style,
getEditors: () => getEditors,
sections.filter(s => !s.isRemoved()).map(s => s.cm),
getLastActivatedEditor, getLastActivatedEditor,
scrollToEditor, scrollToEditor,
getStyleId: () => style.id, getStyleId: () => style.id,
getEditorTitle: cm => { getEditorTitle: cm => {
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm) + 1; const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm) + 1;
return `${t('sectionCode')} ${index + 1}`; return `${t('sectionCode')} ${index + 1}`;
} },
save: saveStyle,
toggleStyle,
nextEditor,
prevEditor
}; };
function getEditors() {
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
}
function toggleStyle() {
const newValue = !style.enabled;
dirty.modify('enabled', style.enabled, newValue);
style.enabled = newValue;
enabledEl.checked = newValue;
}
function nextEditor(cm) {
return nextPrevEditor(cm, 1);
}
function prevEditor(cm) {
return nextPrevEditor(cm, -1);
}
function nextPrevEditor(cm, direction) {
const editors = getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
scrollToEditor(cm);
cm.focus();
return cm;
}
function scrollToEditor(cm) { function scrollToEditor(cm) {
const section = sections.find(s => s.cm === cm); const section = sections.find(s => s.cm === cm);
const bounds = section.getBoundingClientRect(); const bounds = section.getBoundingClientRect();
@ -188,7 +221,7 @@ function createSectionsEditor(style) {
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
cm = CodeMirror.commands.prevEditor(cm); cm = prevEditor(cm);
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
break; break;
case 39: case 39:
@ -204,7 +237,7 @@ function createSectionsEditor(style) {
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
cm = CodeMirror.commands.nextEditor(cm); cm = nextEditor(cm);
cm.setCursor(0, 0); cm.setCursor(0, 0);
break; break;
} }

View File

@ -52,10 +52,6 @@ function createSourceEditor(style) {
updateLivePreview(); updateLivePreview();
}); });
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
CodeMirror.commands.nextEditor = cm => nextPrevMozDocument(cm, 1);
CodeMirror.commands.toggleStyle = toggleStyle;
CodeMirror.commands.save = save;
CodeMirror.closestVisible = () => cm; CodeMirror.closestVisible = () => cm;
cm.operation(initAppliesToLineWidget); cm.operation(initAppliesToLineWidget);
@ -409,6 +405,10 @@ function createSourceEditor(style) {
getLastActivatedEditor: () => cm, getLastActivatedEditor: () => cm,
scrollToEditor: () => {}, scrollToEditor: () => {},
getStyleId: () => style.id, getStyleId: () => style.id,
getEditorTitle: () => '' getEditorTitle: () => '',
save,
toggleStyle,
prevEditor: cm => nextPrevMozDocument(cm, -1),
nextEditor: cm => nextPrevMozDocument(cm, 1)
}; };
} }