/* global $$ $ $create */// dom.js /* global API msg */// msg.js /* global CodeMirror */ /* global SectionsEditor */ /* global SourceEditor */ /* global clipString createHotkeyInput helpPopup */// util.js /* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js /* global cmFactory */ /* global editor EditorHeader */// base.js /* global linterMan */ /* global prefs */ /* global t */// localization.js 'use strict'; //#region init document.body.appendChild(t.template.body); EditorMethods(); editor.styleReady.then(async () => { EditorHeader(); dispatchEvent(new Event('domReady')); await (editor.isUsercss ? SourceEditor : SectionsEditor)(); editor.dirty.onChange(editor.updateDirty); prefs.subscribe('editor.linter', () => linterMan.run()); // enabling after init to prevent flash of validation failure on an empty name $('#name').required = !editor.isUsercss; $('#save-button').onclick = editor.save; $('#cancel-button').onclick = editor.cancel; const elSec = $('#sections-list'); // editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work if (elSec.open) editor.updateToc(); // and we also toggle `open` directly in other places e.g. in detectLayout() new MutationObserver(() => elSec.open && editor.updateToc()) .observe(elSec, {attributes: true, attributeFilter: ['open']}); $('#toc').onclick = e => editor.jumpToEditor([...$('#toc').children].indexOf(e.target)); $('#keyMap-help').onclick = () => require(['/edit/show-keymap-help'], () => showKeymapHelp()); /* global showKeymapHelp */ $('#linter-settings').onclick = () => require(['/edit/linter-dialogs'], () => linterMan.showLintConfig()); $('#lint-help').onclick = () => require(['/edit/linter-dialogs'], () => linterMan.showLintHelp()); $('#style-settings-btn').onclick = () => require([ '/edit/settings.css', '/edit/settings', /* global StyleSettings */ ], () => StyleSettings()); require([ '/edit/autocomplete', '/edit/drafts', '/edit/global-search', ]); }); editor.styleReady.then(async () => { // Set up mini-header on scroll const {isUsercss} = editor; const el = $create({ style: ` top: 0; height: 1px; position: absolute; visibility: hidden; `.replace(/;/g, '!important;'), }); const scroller = isUsercss ? $('.CodeMirror-scroll') : document.body; const xoRoot = isUsercss ? scroller : undefined; const xo = new IntersectionObserver(onScrolled, {root: xoRoot}); scroller.appendChild(el); onCompactToggled(editor.mqCompact); editor.mqCompact.on('change', onCompactToggled); /** @param {MediaQueryList} mq */ function onCompactToggled(mq) { for (const el of $$('details[data-pref]')) { el.open = mq.matches ? false : prefs.get(el.dataset.pref); } if (mq.matches) { xo.observe(el); } else { xo.disconnect(); } } /** @param {IntersectionObserverEntry[]} entries */ function onScrolled(entries) { const h = $('#header'); const sticky = !entries.pop().isIntersecting; if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : ''; h.classList.toggle('sticky', sticky); } }); //#endregion //#region events msg.onExtension(request => { const {style} = request; switch (request.method) { case 'styleUpdated': if (editor.style.id === style.id) { handleExternalUpdate(request); } break; case 'styleDeleted': if (editor.style.id === style.id) { closeCurrentTab(); } break; } }); async function handleExternalUpdate({style, reason}) { if (reason === 'editPreview' || reason === 'editPreviewEnd') { return; } if (reason === 'editSave' && editor.saving) { editor.saving = false; return; } if (reason === 'toggle') { if (editor.dirty.isDirty()) { editor.toggleStyle(style.enabled); } else { Object.assign(editor.style, style); } editor.updateMeta(); editor.updateLivePreview(); return; } style = await API.styles.get(style.id); if (reason === 'config') { delete style.sourceCode; delete style.sections; delete style.name; delete style.enabled; Object.assign(editor.style, style); } else { await editor.replaceStyle(style); } window.dispatchEvent(new Event('styleSettings')); } window.on('beforeunload', e => { let pos; if (editor.isWindowed && document.visibilityState === 'visible' && prefs.get('openEditInWindow') && screenX !== -32000 && // Chrome uses this value for minimized windows ( // only if not maximized screenX > 0 || outerWidth < screen.availWidth || screenY > 0 || outerHeight < screen.availHeight || screenX <= -10 || outerWidth >= screen.availWidth + 10 || screenY <= -10 || outerHeight >= screen.availHeight + 10 ) ) { pos = { left: screenX, top: screenY, width: outerWidth, height: outerHeight, }; prefs.set('windowPosition', pos); } sessionStore.windowPos = JSON.stringify(pos || {}); sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo()); const activeElement = document.activeElement; if (activeElement) { // blurring triggers 'change' or 'input' event if needed activeElement.blur(); // refocus if unloading was canceled setTimeout(() => activeElement.focus()); } if (editor.dirty.isDirty()) { // neither confirm() nor custom messages work in modern browsers but just in case e.returnValue = t('styleChangesNotSaved'); } }); //#endregion //#region editor methods function EditorMethods() { const toc = []; const {dirty} = editor; let {style} = editor; let wasDirty = false; Object.defineProperties(editor, { scrollInfo: { get: () => style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]) || {}, }, style: { get: () => style, set: val => (style = val), }, }); /** @namespace Editor */ Object.assign(editor, { applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) { if (si && si.sel) { const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js cm.setSelections(...si.sel, {scroll: false}); cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts)); Object.assign(cm.display.scroller, si.scroll); // for source editor Object.assign(cm.doc, si.scroll); // for sectioned editor } }, makeScrollInfo() { return { scrollY: window.scrollY, cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({ bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()), focus: cm.hasFocus(), height: cm.display.wrapper.style.height.replace('100vh', ''), parentHeight: cm.display.wrapper.parentElement.offsetHeight, scroll: mapObj(cm.doc, null, ['scrollLeft', 'scrollTop']), sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex], })), }; }, async save() { if (dirty.isDirty()) { editor.saving = true; await editor.saveImpl(); } }, toggleStyle(enabled = !style.enabled) { $('#enabled').checked = enabled; editor.updateEnabledness(enabled); }, updateDirty() { const isDirty = dirty.isDirty(); if (wasDirty !== isDirty) { wasDirty = isDirty; document.body.classList.toggle('dirty', isDirty); $('#save-button').disabled = !isDirty; } editor.updateTitle(); }, updateEnabledness(enabled) { dirty.modify('enabled', style.enabled, enabled); style.enabled = enabled; editor.updateLivePreview(); }, updateName(isUserInput) { if (!editor) return; if (isUserInput) { const {value} = $('#name'); dirty.modify('name', style[editor.nameTarget] || style.name, value); style[editor.nameTarget] = value; } editor.updateTitle(); }, updateToc(added = editor.sections) { if (!toc.el) { toc.el = $('#toc'); toc.elDetails = toc.el.closest('details'); } if (!toc.elDetails.open) return; const {sections} = editor; const first = sections.indexOf(added[0]); const elFirst = toc.el.children[first]; if (first >= 0 && (!added.focus || !elFirst)) { for (let el = elFirst, i = first; i < sections.length; i++) { const entry = sections[i].tocEntry; if (!deepEqual(entry, toc[i])) { if (!el) el = toc.el.appendChild($create('li', {tabIndex: 0})); el.tabIndex = entry.removed ? -1 : 0; toc[i] = Object.assign({}, entry); const s = el.textContent = clipString(entry.label) || ( entry.target == null ? t('appliesToEverything') : clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : '')); if (s.length > 30) el.title = s; } el = el.nextElementSibling; } } while (toc.length > sections.length) { toc.el.lastElementChild.remove(); toc.length--; } if (added.focus) { const cls = 'current'; const old = $('.' + cls, toc.el); const el = elFirst || toc.el.children[first]; if (old && old !== el) old.classList.remove(cls); el.classList.add(cls); } }, useSavedStyle(newStyle) { if (style.id !== newStyle.id) { history.replaceState({}, '', `?id=${newStyle.id}`); } sessionStore.justEditedStyleId = newStyle.id; Object.assign(style, newStyle); editor.updateClass(); editor.updateMeta(); }, }); } //#endregion //#region colorpickerHelper (async function colorpickerHelper() { prefs.subscribe('editor.colorpicker.hotkey', (id, hotkey) => { CodeMirror.commands.colorpicker = invokeColorpicker; const extraKeys = CodeMirror.defaults.extraKeys; for (const key in extraKeys) { if (extraKeys[key] === 'colorpicker') { delete extraKeys[key]; break; } } if (hotkey) { extraKeys[hotkey] = 'colorpicker'; } }); prefs.subscribe('editor.colorpicker', (id, enabled) => { const defaults = CodeMirror.defaults; const keyName = prefs.get('editor.colorpicker.hotkey'); defaults.colorpicker = enabled; if (enabled) { if (keyName) { CodeMirror.commands.colorpicker = invokeColorpicker; defaults.extraKeys = defaults.extraKeys || {}; defaults.extraKeys[keyName] = 'colorpicker'; } defaults.colorpicker = { tooltip: t('colorpickerTooltip'), popup: { tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), paletteLine: t('numberedLine'), paletteHint: t('colorpickerPaletteHint'), hexUppercase: prefs.get('editor.colorpicker.hexUppercase'), embedderCallback: state => { ['hexUppercase', 'color'] .filter(name => state[name] !== prefs.get('editor.colorpicker.' + name)) .forEach(name => prefs.set('editor.colorpicker.' + name, state[name])); }, get maxHeight() { return prefs.get('editor.colorpicker.maxHeight'); }, set maxHeight(h) { prefs.set('editor.colorpicker.maxHeight', h); }, }, }; } else { if (defaults.extraKeys) { delete defaults.extraKeys[keyName]; } } cmFactory.globalSetOption('colorpicker', defaults.colorpicker); }, {runNow: true}); $('#colorpicker-settings').onclick = function (event) { event.preventDefault(); const input = createHotkeyInput('editor.colorpicker.hotkey', {onDone: () => helpPopup.close()}); const popup = helpPopup.show(t('helpKeyMapHotkey'), input); const bounds = this.getBoundingClientRect(); popup.style.left = bounds.right + 10 + 'px'; popup.style.top = bounds.top - popup.clientHeight / 2 + 'px'; popup.style.right = 'auto'; $('input', popup).focus(); }; function invokeColorpicker(cm) { cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color')); } })(); //#endregion