| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- /* global $ $$ $create setupLivePrefs waitForSelector */// dom.js
- /* global API */// msg.js
- /* global CODEMIRROR_THEMES */
- /* global CodeMirror */
- /* global MozDocMapper */// sections-util.js
- /* global initBeautifyButton */// beautify.js
- /* global prefs */
- /* global t */// localization.js
- /* global
- FIREFOX
- debounce
- getOwnTab
- sessionStore
- tryJSONparse
- tryURL
- */// toolbox.js
- /* global EventEmitter */
- 'use strict';
- /**
- * @type Editor
- * @namespace Editor
- */
- const editor = Object.assign(EventEmitter(), {
- style: null,
- dirty: DirtyReporter(),
- isUsercss: false,
- isWindowed: false,
- lazyKeymaps: {
- emacs: '/vendor/codemirror/keymap/emacs',
- vim: '/vendor/codemirror/keymap/vim',
- },
- livePreview: null,
- /** @type {'customName'|'name'} */
- nameTarget: 'name',
- previewDelay: 200, // Chrome devtools uses 200
- scrollInfo: null,
- onStyleUpdated() {
- document.documentElement.classList.toggle('is-new-style', !editor.style.id);
- },
- updateTitle(isDirty = editor.dirty.isDirty()) {
- const {customName, name} = editor.style;
- document.title = `${
- isDirty ? '* ' : ''
- }${
- customName || name || t('styleMissingName')
- } - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
- },
- });
- //#region pre-init
- const baseInit = (() => {
- const domReady = waitForSelector('#sections');
- return {
- domReady,
- ready: Promise.all([
- domReady,
- loadStyle(),
- prefs.ready.then(() =>
- Promise.all([
- loadTheme(),
- loadKeymaps(),
- ])),
- ]),
- };
- /** Preloads vim/emacs keymap only if it's the active one, otherwise will load later */
- function loadKeymaps() {
- const km = prefs.get('editor.keyMap');
- return /emacs/i.test(km) && require([editor.lazyKeymaps.emacs]) ||
- /vim/i.test(km) && require([editor.lazyKeymaps.vim]);
- }
- async function loadStyle() {
- const params = new URLSearchParams(location.search);
- const id = Number(params.get('id'));
- const style = id && await API.styles.get(id) || {
- name: params.get('domain') ||
- tryURL(params.get('url-prefix')).hostname ||
- '',
- enabled: true,
- sections: [
- MozDocMapper.toSection([...params], {code: ''}),
- ],
- };
- // switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
- editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
- editor.style = style;
- editor.onStyleUpdated();
- editor.updateTitle(false);
- document.documentElement.classList.toggle('usercss', editor.isUsercss);
- sessionStore.justEditedStyleId = style.id || '';
- // no such style so let's clear the invalid URL parameters
- if (!style.id) history.replaceState({}, '', location.pathname);
- }
- /** Preloads the theme so CodeMirror can use the correct metrics in its first render */
- async function loadTheme() {
- const theme = prefs.get('editor.theme');
- if (!CODEMIRROR_THEMES.includes(theme)) {
- prefs.set('editor.theme', 'default');
- return;
- }
- if (theme !== 'default') {
- const el = $('#cm-theme');
- const el2 = await require([`/vendor/codemirror/theme/${theme}.css`]);
- el2.id = el.id;
- el.remove();
- // FF containers take more time to load CSS
- for (let retry = 0; !el2.sheet && ++retry <= 10;) {
- await new Promise(requestAnimationFrame);
- }
- }
- }
- })();
- //#endregion
- //#region init layout/resize
- // baseInit.domReady.then(() => {
- // let headerHeight;
- // detectLayout(true);
- // window.on('resize', () => detectLayout());
- // function detectLayout(now) {
- // const compact = window.innerWidth <= 850;
- // if (compact) {
- // document.body.classList.add('compact-layout');
- // if (!editor.isUsercss) {
- // if (now) fixedHeader();
- // else debounce(fixedHeader, 250);
- // window.on('scroll', fixedHeader, {passive: true});
- // }
- // } else {
- // document.body.classList.remove('compact-layout', 'fixed-header');
- // window.off('scroll', fixedHeader);
- // }
- // for (const el of $$('details[data-pref]')) {
- // el.open = compact ? false : prefs.get(el.dataset.pref);
- // }
- // }
- // function fixedHeader() {
- // const headerFixed = $('.fixed-header');
- // if (!headerFixed) headerHeight = $('#header').clientHeight;
- // const scrollPoint = headerHeight - 43;
- // if (window.scrollY >= scrollPoint && !headerFixed) {
- // $('body').style.setProperty('--fixed-padding', ` ${headerHeight}px`);
- // $('body').classList.add('fixed-header');
- // } else if (window.scrollY < scrollPoint && headerFixed) {
- // $('body').classList.remove('fixed-header');
- // }
- // }
- // });
- //#endregion
- //#region init header
- baseInit.ready.then(() => {
- initBeautifyButton($('#beautify'));
- initKeymapElement();
- initNameArea();
- initThemeElement();
- setupLivePrefs();
- require(Object.values(editor.lazyKeymaps), () => {
- initKeymapElement();
- prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
- window.on('showHotkeyInTooltip', showHotkeyInTooltip);
- });
- 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 initNameArea() {
- const nameEl = $('#name');
- const resetEl = $('#reset-name');
- const isCustomName = editor.style.updateUrl || editor.isUsercss;
- editor.nameTarget = isCustomName ? 'customName' : 'name';
- nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
- nameEl.title = isCustomName ? t('customNameHint') : '';
- nameEl.on('input', () => {
- editor.updateName(true);
- resetEl.hidden = false;
- });
- resetEl.hidden = !editor.style.customName;
- resetEl.onclick = () => {
- const {style} = editor;
- nameEl.focus();
- nameEl.select();
- // trying to make it undoable via Ctrl-Z
- if (!document.execCommand('insertText', false, style.name)) {
- nameEl.value = style.name;
- editor.updateName(true);
- }
- style.customName = null; // to delete it from db
- resetEl.hidden = true;
- };
- const enabledEl = $('#enabled');
- enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
- }
- function initThemeElement() {
- $('#editor.theme').append(...[
- $create('option', {value: 'default'}, t('defaultTheme')),
- ...CODEMIRROR_THEMES.map(s => $create('option', s)),
- ]);
- // move the theme after built-in CSS so that its same-specificity selectors win
- document.head.appendChild($('#cm-theme'));
- }
- function initKeymapElement() {
- // 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;
- });
- const selector = $('#editor.keyMap');
- selector.textContent = '';
- selector.appendChild(fragment);
- selector.value = prefs.get('editor.keyMap');
- }
- 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;
- }
- }
- }
- });
- //#endregion
- //#region init windowed mode
- (() => {
- let ownTabId;
- if (chrome.windows) {
- initWindowedMode();
- const pos = tryJSONparse(sessionStore.windowPos);
- delete sessionStore.windowPos;
- // resize the window on 'undo close'
- if (pos && pos.left != null) {
- chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
- }
- }
- getOwnTab().then(async tab => {
- ownTabId = tab.id;
- // use browser history back when 'back to manage' is clicked
- if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
- await baseInit.domReady;
- $('#cancel-button').onclick = event => {
- event.stopPropagation();
- event.preventDefault();
- history.back();
- };
- }
- });
- async function initWindowedMode() {
- chrome.tabs.onAttached.addListener(onTabAttached);
- const isSimple = (await browser.windows.getCurrent()).type === 'popup';
- if (isSimple) require(['/edit/embedded-popup']);
- editor.isWindowed = isSimple || (
- history.length === 1 &&
- await prefs.ready && prefs.get('openEditInWindow') &&
- (await browser.windows.getAll()).length > 1 &&
- (await browser.tabs.query({currentWindow: true})).length === 1
- );
- }
- async function onTabAttached(tabId, info) {
- if (tabId !== ownTabId) {
- return;
- }
- if (info.newPosition !== 0) {
- prefs.set('openEditInWindow', false);
- return;
- }
- const win = await browser.windows.get(info.newWindowId, {populate: true});
- // If there's only one tab in this window, it's been dragged to new window
- const openEditInWindow = win.tabs.length === 1;
- // FF-only because Chrome retardedly resets the size during dragging
- if (openEditInWindow && FIREFOX) {
- chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
- }
- prefs.set('openEditInWindow', openEditInWindow);
- }
- })();
- //#endregion
- //#region internals
- /** @returns DirtyReporter */
- function DirtyReporter() {
- const data = new Map();
- const listeners = new Set();
- const notifyChange = wasDirty => {
- if (wasDirty !== (data.size > 0)) {
- listeners.forEach(cb => cb());
- }
- };
- /** @namespace DirtyReporter */
- return {
- add(obj, value) {
- const wasDirty = data.size > 0;
- const saved = data.get(obj);
- if (!saved) {
- data.set(obj, {type: 'add', newValue: value});
- } else if (saved.type === 'remove') {
- if (saved.savedValue === value) {
- data.delete(obj);
- } else {
- saved.newValue = value;
- saved.type = 'modify';
- }
- }
- notifyChange(wasDirty);
- },
- clear(obj) {
- const wasDirty = data.size > 0;
- if (obj === undefined) {
- data.clear();
- } else {
- data.delete(obj);
- }
- notifyChange(wasDirty);
- },
- has(key) {
- return data.has(key);
- },
- isDirty() {
- return data.size > 0;
- },
- modify(obj, oldValue, newValue) {
- const wasDirty = data.size > 0;
- const saved = data.get(obj);
- if (!saved) {
- if (oldValue !== newValue) {
- data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
- }
- } else if (saved.type === 'modify') {
- if (saved.savedValue === newValue) {
- data.delete(obj);
- } else {
- saved.newValue = newValue;
- }
- } else if (saved.type === 'add') {
- saved.newValue = newValue;
- }
- notifyChange(wasDirty);
- },
- onChange(cb, add = true) {
- listeners[add ? 'add' : 'delete'](cb);
- },
- remove(obj, value) {
- const wasDirty = data.size > 0;
- const saved = data.get(obj);
- if (!saved) {
- data.set(obj, {type: 'remove', savedValue: value});
- } else if (saved.type === 'add') {
- data.delete(obj);
- } else if (saved.type === 'modify') {
- saved.type = 'remove';
- }
- notifyChange(wasDirty);
- },
- };
- }
- //#endregion
|