diff --git a/edit/edit.js b/edit/edit.js index 75b64ba9..f9b2e841 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -3,19 +3,14 @@ define(require => { const {API, msg} = require('/js/msg'); const { - FIREFOX, closeCurrentTab, debounce, - getOwnTab, sessionStore, } = require('/js/toolbox'); const { $, $$, $create, - $remove, - getEventKeyName, - onDOMready, setupLivePrefs, } = require('/js/dom'); const t = require('/js/localization'); @@ -26,14 +21,10 @@ define(require => { const {CodeMirror, initBeautifyButton} = require('./codemirror-factory'); let headerHeight; - let isSimpleWindow; - let isWindowed; window.on('beforeunload', beforeUnload); msg.onExtension(onRuntimeMessage); - lazyInit(); - (async function init() { await preinit; buildThemeElement(); @@ -205,77 +196,6 @@ define(require => { } } - /* Stuff not needed for the main init so we can let it run at its own tempo */ - function lazyInit() { - let ownTabId; - // not using `await` so we don't block the subsequent code - getOwnTab().then(patchHistoryBack); - // no windows on android - if (chrome.windows) { - detectWindowedState(); - chrome.tabs.onAttached.addListener(onAttached); - } - async function patchHistoryBack(tab) { - ownTabId = tab.id; - // use browser history back when 'back to manage' is clicked - if (sessionStore['manageStylesHistory' + ownTabId] === location.href) { - await onDOMready(); - $('#cancel-button').onclick = event => { - event.stopPropagation(); - event.preventDefault(); - history.back(); - }; - } - } - async function detectWindowedState() { - isSimpleWindow = - (await browser.windows.getCurrent()).type === 'popup'; - isWindowed = isSimpleWindow || ( - prefs.get('openEditInWindow') && - history.length === 1 && - (await browser.windows.getAll()).length > 1 && - (await browser.tabs.query({currentWindow: true})).length === 1 - ); - if (isSimpleWindow) { - await onDOMready(); - initPopupButton(); - } - } - function initPopupButton() { - const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S'; - const btn = $create('img', { - id: 'popup-button', - title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY, - onclick: embedPopup, - }); - const onIconsetChanged = (_, val) => { - const prefix = `images/icon/${val ? 'light/' : ''}`; - btn.srcset = `${prefix}16.png 1x,${prefix}32.png 2x`; - }; - prefs.subscribe('iconset', onIconsetChanged, {runNow: true}); - document.body.appendChild(btn); - window.on('keydown', e => getEventKeyName(e) === POPUP_HOTKEY && embedPopup()); - CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup'; // adds to keymap help - } - async function onAttached(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); - } - } - function onRuntimeMessage(request) { const {style} = request; switch (request.method) { @@ -322,7 +242,7 @@ define(require => { } function canSaveWindowPos() { - return isWindowed && + return editor.isWindowed && document.visibilityState === 'visible' && prefs.get('openEditInWindow') && !isWindowMaximized(); @@ -383,63 +303,4 @@ define(require => { window.outerHeight < screen.availHeight + 10 ); } - - function embedPopup() { - const ID = 'popup-iframe'; - const SEL = '#' + ID; - if ($(SEL)) return; - const frame = $create('iframe', { - id: ID, - src: chrome.runtime.getManifest().browser_action.default_popup, - height: 600, - width: prefs.get('popupWidth'), - onload() { - frame.onload = null; - frame.focus(); - const pw = frame.contentWindow; - const body = pw.document.body; - pw.on('keydown', e => getEventKeyName(e) === 'Escape' && embedPopup._close()); - pw.close = embedPopup._close; - if (pw.IntersectionObserver) { - let loaded; - new pw.IntersectionObserver(([e]) => { - const el = pw.document.scrollingElement; - const h = e.isIntersecting && !pw.scrollY ? el.offsetHeight : el.scrollHeight; - const hasSB = h > el.offsetHeight; - const {width} = e.boundingClientRect; - frame.height = h; - if (!hasSB !== !frame._scrollbarWidth || frame.width - width) { - frame._scrollbarWidth = hasSB ? width - el.offsetWidth : 0; - frame.width = width + frame._scrollbarWidth; - } - if (!loaded) { - loaded = true; - frame.dataset.loaded = ''; - } - }).observe(body.appendChild( - $create('div', {style: {height: '1px', marginTop: '-1px'}}) - )); - } else { - frame.dataset.loaded = ''; - frame.height = body.scrollHeight; - } - new pw.MutationObserver(() => { - const bs = body.style; - const w = parseFloat(bs.minWidth || bs.width) + (frame._scrollbarWidth || 0); - const h = parseFloat(bs.minHeight || body.offsetHeight); - if (frame.width - w) frame.width = w; - if (frame.height - h) frame.height = h; - }).observe(body, {attributes: true, attributeFilter: ['style']}); - }, - }); - // saving the listener here so it's the same function reference for window.off - if (!embedPopup._close) { - embedPopup._close = () => { - $remove(SEL); - window.off('mousedown', embedPopup._close); - }; - } - window.on('mousedown', embedPopup._close); - document.body.appendChild(frame); - } }); diff --git a/edit/editor.js b/edit/editor.js index 12318e94..411946b4 100644 --- a/edit/editor.js +++ b/edit/editor.js @@ -22,6 +22,8 @@ define(require => { const editor = { dirty, isUsercss: false, + isWindowed: false, + isWindowSimple: false, /** @type {'customName'|'name'} */ nameTarget: 'name', previewDelay: 200, // Chrome devtools uses 200 diff --git a/edit/embedded-popup.js b/edit/embedded-popup.js new file mode 100644 index 00000000..8c26ed3f --- /dev/null +++ b/edit/embedded-popup.js @@ -0,0 +1,114 @@ +'use strict'; + +define(require => { + const { + $, + $create, + $remove, + getEventKeyName, + } = require('/js/dom'); + const t = require('/js/localization'); + const prefs = require('/js/prefs'); + const {CodeMirror} = require('./codemirror-factory'); + + const ID = 'popup-iframe'; + const SEL = '#' + ID; + const URL = chrome.runtime.getManifest().browser_action.default_popup; + /** @type {HTMLIFrameElement} */ + let frame; + let isLoaded; + let scrollbarWidth; + + return { + initPopupButton() { + const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S'; + const btn = $create('img', { + id: 'popup-button', + title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY, + onclick: embedPopup, + }); + const onIconsetChanged = (_, val) => { + const prefix = `images/icon/${val ? 'light/' : ''}`; + btn.srcset = `${prefix}16.png 1x,${prefix}32.png 2x`; + }; + prefs.subscribe('iconset', onIconsetChanged, {runNow: true}); + document.body.appendChild(btn); + window.on('keydown', e => getEventKeyName(e) === POPUP_HOTKEY && embedPopup()); + CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup'; // adds to keymap help + }, + }; + + function embedPopup() { + if ($(SEL)) return; + isLoaded = false; + scrollbarWidth = 0; + frame = $create('iframe', { + id: ID, + src: URL, + height: 600, + width: prefs.get('popupWidth'), + onload: initFrame, + }); + window.on('mousedown', removePopup); + document.body.appendChild(frame); + } + + function initFrame() { + frame = this; + frame.focus(); + const pw = frame.contentWindow; + const body = pw.document.body; + pw.on('keydown', removePopupOnEsc); + pw.close = removePopup; + if (pw.IntersectionObserver) { + new pw.IntersectionObserver(onIntersect).observe(body.appendChild( + $create('div', {style: {height: '1px', marginTop: '-1px'}}) + )); + } else { + frame.dataset.loaded = ''; + frame.height = body.scrollHeight; + } + new pw.MutationObserver(onMutation).observe(body, { + attributes: true, + attributeFilter: ['style'], + }); + } + + function onMutation() { + const body = frame.contentDocument.body; + const bs = body.style; + const w = parseFloat(bs.minWidth || bs.width) + (scrollbarWidth || 0); + const h = parseFloat(bs.minHeight || body.offsetHeight); + if (frame.width - w) frame.width = w; + if (frame.height - h) frame.height = h; + } + + function onIntersect([e]) { + const pw = frame.contentWindow; + const el = pw.document.scrollingElement; + const h = e.isIntersecting && !pw.scrollY ? el.offsetHeight : el.scrollHeight; + const hasSB = h > el.offsetHeight; + const {width} = e.boundingClientRect; + frame.height = h; + if (!hasSB !== !scrollbarWidth || frame.width - width) { + scrollbarWidth = hasSB ? width - el.offsetWidth : 0; + frame.width = width + scrollbarWidth; + } + if (!isLoaded) { + isLoaded = true; + frame.dataset.loaded = ''; + } + } + + function removePopup() { + frame = null; + $remove(SEL); + window.off('mousedown', removePopup); + } + + function removePopupOnEsc(e) { + if (getEventKeyName(e) === 'Escape') { + removePopup(); + } + } +}); diff --git a/edit/preinit.js b/edit/preinit.js index 2038bb3b..b651338d 100644 --- a/edit/preinit.js +++ b/edit/preinit.js @@ -2,8 +2,14 @@ define(require => { const {API} = require('/js/msg'); - const {sessionStore, tryCatch, tryJSONparse} = require('/js/toolbox'); - const {waitForSelector} = require('/js/dom'); + const { + FIREFOX, + getOwnTab, + sessionStore, + tryCatch, + tryJSONparse, + } = require('/js/toolbox'); + const {$, waitForSelector} = require('/js/dom'); const prefs = require('/js/prefs'); const editor = require('./editor'); const util = require('./util'); @@ -12,9 +18,12 @@ define(require => { emacs: '/vendor/codemirror/keymap/emacs', vim: '/vendor/codemirror/keymap/vim', }; + const domReady = waitForSelector('#sections'); + let ownTabId; // resize the window on 'undo close' if (chrome.windows) { + initWindowedMode(); const pos = tryJSONparse(sessionStore.windowPos); delete sessionStore.windowPos; if (pos && pos.left != null && chrome.windows) { @@ -22,27 +31,35 @@ define(require => { } } - async function preinit() { - const params = new URLSearchParams(location.search); - const id = Number(params.get('id')); - const style = id && await API.styles.get(id) || { - name: params.get('domain') || - tryCatch(() => new URL(params.get('url-prefix')).hostname) || - '', - enabled: true, - sections: [ - util.DocFuncMapper.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.lazyKeymaps = lazyKeymaps; - editor.style = style; - 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); + getOwnTab().then(async tab => { + ownTabId = tab.id; + // use browser history back when 'back to manage' is clicked + if (sessionStore['manageStylesHistory' + ownTabId] === location.href) { + await domReady; + $('#cancel-button').onclick = event => { + event.stopPropagation(); + event.preventDefault(); + history.back(); + }; + } + }); + + async function initWindowedMode() { + chrome.tabs.onAttached.addListener(onTabAttached); + editor.isWindowSimple = + (await browser.windows.getCurrent()).type === 'popup'; + if (editor.isWindowSimple) { + Promise.all([ + require(['./embedded-popup']), + domReady, + ]).then(([_]) => _.initPopupButton()); + } + editor.isWindowed = editor.isWindowSimple || ( + history.length === 1 && + await prefs.initializing && prefs.get('openEditInWindow') && + (await browser.windows.getAll()).length > 1 && + (await browser.tabs.query({currentWindow: true})).length === 1 + ); } /** Preloads the theme so CodeMirror can use the correct metrics in its first render */ @@ -70,6 +87,47 @@ define(require => { /vim/i.test(km) && require([lazyKeymaps.vim]); } + 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); + } + + async function preinit() { + const params = new URLSearchParams(location.search); + const id = Number(params.get('id')); + const style = id && await API.styles.get(id) || { + name: params.get('domain') || + tryCatch(() => new URL(params.get('url-prefix')).hostname) || + '', + enabled: true, + sections: [ + util.DocFuncMapper.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.lazyKeymaps = lazyKeymaps; + editor.style = style; + 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); + } + return Promise.all([ preinit(), prefs.initializing.then(() => @@ -77,6 +135,6 @@ define(require => { loadTheme(), loadKeymaps(), ])), - waitForSelector('#sections'), + domReady, ]); });