diff options
| author | Alex Yatskov <alex@foosoft.net> | 2020-02-24 21:31:14 -0800 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2020-02-24 21:31:14 -0800 | 
| commit | d32f4def0eeed1599857bc04c973337a2a13dd8b (patch) | |
| tree | 61149656f361dd2d9998d67d68249dc184b73fbb /ext/fg/js | |
| parent | 0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff) | |
| parent | 706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff) | |
Merge branch 'master' into testing
Diffstat (limited to 'ext/fg/js')
| -rw-r--r-- | ext/fg/js/document.js | 8 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 85 | ||||
| -rw-r--r-- | ext/fg/js/frontend-api-sender.js | 10 | ||||
| -rw-r--r-- | ext/fg/js/frontend-initialize.js | 12 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 3 | ||||
| -rw-r--r-- | ext/fg/js/popup-nested.js | 1 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy-host.js | 65 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy.js | 9 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 262 | ||||
| -rw-r--r-- | ext/fg/js/source.js | 16 | 
10 files changed, 321 insertions, 150 deletions
| diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 71654b29..35861475 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global TextSourceElement, TextSourceRange, DOM*/  const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/; @@ -49,7 +50,9 @@ function docImposterCreate(element, isTextarea) {      const imposter = document.createElement('div');      const imposterStyle = imposter.style; -    imposter.innerText = element.value; +    let value = element.value; +    if (value.endsWith('\n')) { value += '\n'; } +    imposter.textContent = value;      for (let i = 0, ii = elementStyle.length; i < ii; ++i) {          const property = elementStyle[i]; @@ -191,8 +194,7 @@ function docSentenceExtract(source, extent) {              if (terminators.includes(c)) {                  endPos = i + 1;                  break; -            } -            else if (c in quotesBwd) { +            } else if (c in quotesBwd) {                  endPos = i;                  break;              } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 8d61d8f6..8f21a9c5 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global popupNestedInitialize, apiForward, apiGetMessageToken, Display*/  class DisplayFloat extends Display {      constructor() { @@ -28,11 +29,33 @@ class DisplayFloat extends Display {          };          this._orphaned = false; +        this._prepareInvoked = false; +        this._messageToken = null; +        this._messageTokenPromise = null;          yomichan.on('orphaned', () => this.onOrphaned());          window.addEventListener('message', (e) => this.onMessage(e), false);      } +    async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) { +        if (this._prepareInvoked) { return; } +        this._prepareInvoked = true; + +        await super.prepare(options); + +        const {id, depth, parentFrameId} = popupInfo; +        this.optionsContext.depth = depth; +        this.optionsContext.url = url; + +        if (childrenSupported) { +            popupNestedInitialize(id, depth, parentFrameId, url); +        } + +        this.setContentScale(scale); + +        apiForward('popupPrepareCompleted', {uniqueId}); +    } +      onError(error) {          if (this._orphaned) {              this.setContent('orphaned'); @@ -54,11 +77,23 @@ class DisplayFloat extends Display {      }      onMessage(e) { -        const {action, params} = e.data; -        const handler = DisplayFloat._messageHandlers.get(action); -        if (typeof handler !== 'function') { return; } - -        handler(this, params); +        const data = e.data; +        if (typeof data !== 'object' || data === null) { return; } // Invalid data + +        const token = data.token; +        if (typeof token !== 'string') { return; } // Invalid data + +        if (this._messageToken === null) { +            // Async +            this.getMessageToken() +                .then( +                    () => { this.handleAction(token, data); }, +                    () => {} +                ); +        } else { +            // Sync +            this.handleAction(token, data); +        }      }      onKeyDown(e) { @@ -73,6 +108,30 @@ class DisplayFloat extends Display {          return super.onKeyDown(e);      } +    async getMessageToken() { +        // this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made. +        if (this._messageTokenPromise === null) { +            this._messageTokenPromise = apiGetMessageToken(); +        } +        const messageToken = await this._messageTokenPromise; +        if (this._messageToken === null) { +            this._messageToken = messageToken; +        } +        this._messageTokenPromise = null; +    } + +    handleAction(token, {action, params}) { +        if (token !== this._messageToken) { +            // Invalid token +            return; +        } + +        const handler = DisplayFloat._messageHandlers.get(action); +        if (typeof handler !== 'function') { return; } + +        handler(this, params); +    } +      getOptionsContext() {          return this.optionsContext;      } @@ -92,20 +151,6 @@ class DisplayFloat extends Display {      setContentScale(scale) {          document.body.style.fontSize = `${scale}em`;      } - -    async initialize(options, popupInfo, url, childrenSupported, scale) { -        await super.initialize(options); - -        const {id, depth, parentFrameId} = popupInfo; -        this.optionsContext.depth = depth; -        this.optionsContext.url = url; - -        if (childrenSupported) { -            popupNestedInitialize(id, depth, parentFrameId, url); -        } - -        this.setContentScale(scale); -    }  }  DisplayFloat._onKeyDownHandlers = new Map([ @@ -122,7 +167,7 @@ DisplayFloat._messageHandlers = new Map([      ['setContent', (self, {type, details}) => self.setContent(type, details)],      ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()],      ['setCustomCss', (self, {css}) => self.setCustomCss(css)], -    ['initialize', (self, {options, popupInfo, url, childrenSupported, scale}) => self.initialize(options, popupInfo, url, childrenSupported, scale)], +    ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],      ['setContentScale', (self, {scale}) => self.setContentScale(scale)]  ]); diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index 93c2e593..8dc6aaf3 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -19,7 +19,7 @@  class FrontendApiSender {      constructor() { -        this.senderId = FrontendApiSender.generateId(16); +        this.senderId = yomichan.generateId(16);          this.ackTimeout = 3000; // 3 seconds          this.responseTimeout = 10000; // 10 seconds          this.callbacks = new Map(); @@ -123,12 +123,4 @@ class FrontendApiSender {          info.timer = null;          info.reject(new Error(reason));      } - -    static generateId(length) { -        let id = ''; -        for (let i = 0; i < length; ++i) { -            id += Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); -        } -        return id; -    }  } diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 9c923fea..54b874f2 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -16,18 +16,22 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global PopupProxyHost, PopupProxy, Frontend*/  async function main() {      const data = window.frontendInitializationData || {};      const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; -    let popupHost = null; -    if (!proxy) { -        popupHost = new PopupProxyHost(); +    let popup; +    if (proxy) { +        popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); +    } else { +        const popupHost = new PopupProxyHost();          await popupHost.prepare(); + +        popup = popupHost.getOrCreatePopup();      } -    const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : popupHost.createPopup(null, depth);      const frontend = new Frontend(popup, ignoreNodes);      await frontend.prepare();  } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 2286bf19..67045241 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global apiGetZoom, apiOptionsGet, apiTermsFind, apiKanjiFind, docSentenceExtract, TextScanner*/  class Frontend extends TextScanner {      constructor(popup, ignoreNodes) { @@ -55,7 +56,7 @@ class Frontend extends TextScanner {              }              yomichan.on('orphaned', () => this.onOrphaned()); -            yomichan.on('optionsUpdate', () => this.updateOptions()); +            yomichan.on('optionsUpdated', () => this.updateOptions());              yomichan.on('zoomChanged', (e) => this.onZoomChanged(e));              chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 3f3c945e..3e5f5b80 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global apiOptionsGet*/  let popupNestedInitialized = false; diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 427172c6..e55801ff 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global apiFrameInformationGet, FrontendApiReceiver, Popup*/  class PopupProxyHost {      constructor() { @@ -33,7 +34,7 @@ class PopupProxyHost {          if (typeof frameId !== 'number') { return; }          this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([ -            ['createNestedPopup', ({parentId}) => this._onApiCreateNestedPopup(parentId)], +            ['getOrCreatePopup', ({id, parentId}) => this._onApiGetOrCreatePopup(id, parentId)],              ['setOptions', ({id, options}) => this._onApiSetOptions(id, options)],              ['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)],              ['isVisible', ({id}) => this._onApiIsVisibleAsync(id)], @@ -46,14 +47,51 @@ class PopupProxyHost {          ]));      } -    createPopup(parentId, depth) { -        return this._createPopupInternal(parentId, depth).popup; +    getOrCreatePopup(id=null, parentId=null) { +        // Find by existing id +        if (id !== null) { +            const popup = this._popups.get(id); +            if (typeof popup !== 'undefined') { +                return popup; +            } +        } + +        // Find by existing parent id +        let parent = null; +        if (parentId !== null) { +            parent = this._popups.get(parentId); +            if (typeof parent !== 'undefined') { +                const popup = parent.child; +                if (popup !== null) { +                    return popup; +                } +            } else { +                parent = null; +            } +        } + +        // New unique id +        if (id === null) { +            id = this._nextId++; +        } + +        // Create new popup +        const depth = (parent !== null ? parent.depth + 1 : 0); +        const popup = new Popup(id, depth, this._frameIdPromise); +        if (parent !== null) { +            popup.setParent(parent); +        } +        this._popups.set(id, popup); +        return popup;      }      // Message handlers -    async _onApiCreateNestedPopup(parentId) { -        return this._createPopupInternal(parentId, 0).id; +    async _onApiGetOrCreatePopup(id, parentId) { +        const popup = this.getOrCreatePopup(id, parentId); +        return { +            id: popup.id +        };      }      async _onApiSetOptions(id, options) { @@ -105,25 +143,10 @@ class PopupProxyHost {      // Private functions -    _createPopupInternal(parentId, depth) { -        const parent = (typeof parentId === 'string' && this._popups.has(parentId) ? this._popups.get(parentId) : null); -        const id = `${this._nextId}`; -        if (parent !== null) { -            depth = parent.depth + 1; -        } -        ++this._nextId; -        const popup = new Popup(id, depth, this._frameIdPromise); -        if (parent !== null) { -            popup.setParent(parent); -        } -        this._popups.set(id, popup); -        return {popup, id}; -    } -      _getPopup(id) {          const popup = this._popups.get(id);          if (typeof popup === 'undefined') { -            throw new Error('Invalid popup ID'); +            throw new Error(`Invalid popup ID ${id}`);          }          return popup;      } diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 4cacee53..093cdd2e 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -16,12 +16,13 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global FrontendApiSender*/  class PopupProxy { -    constructor(depth, parentId, parentFrameId, url) { +    constructor(id, depth, parentId, parentFrameId, url) {          this._parentId = parentId;          this._parentFrameId = parentFrameId; -        this._id = null; +        this._id = id;          this._idPromise = null;          this._depth = depth;          this._url = url; @@ -69,7 +70,7 @@ class PopupProxy {          if (this._id === null) {              return;          } -        this._invokeHostApi('setVisibleOverride', {id, visible}); +        this._invokeHostApi('setVisibleOverride', {id: this._id, visible});      }      async containsPoint(x, y) { @@ -112,7 +113,7 @@ class PopupProxy {      }      async _getPopupIdAsync() { -        const id = await this._invokeHostApi('createNestedPopup', {parentId: this._parentId}); +        const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId});          this._id = id;          return id;      } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index e7dae93e..4927f4bd 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -16,6 +16,7 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/*global apiInjectStylesheet, apiGetMessageToken*/  class Popup {      constructor(id, depth, frameIdPromise) { @@ -27,32 +28,40 @@ class Popup {          this._child = null;          this._childrenSupported = true;          this._injectPromise = null; -        this._isInjected = false; -        this._isInjectedAndLoaded = false;          this._visible = false;          this._visibleOverride = null;          this._options = null; -        this._stylesheetInjectedViaApi = false;          this._contentScale = 1.0;          this._containerSizeContentScale = null; +        this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); +        this._messageToken = null;          this._container = document.createElement('iframe');          this._container.className = 'yomichan-float';          this._container.addEventListener('mousedown', (e) => e.stopPropagation());          this._container.addEventListener('scroll', (e) => e.stopPropagation()); -        this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));          this._container.style.width = '0px';          this._container.style.height = '0px'; +        this._fullscreenEventListeners = new EventListenerCollection(); +          this._updateVisibility();      }      // Public properties +    get id() { +        return this._id; +    } +      get parent() {          return this._parent;      } +    get child() { +        return this._child; +    } +      get depth() {          return this._depth;      } @@ -117,16 +126,12 @@ class Popup {      }      clearAutoPlayTimer() { -        if (this._isInjectedAndLoaded) { -            this._invokeApi('clearAutoPlayTimer'); -        } +        this._invokeApi('clearAutoPlayTimer');      }      setContentScale(scale) {          this._contentScale = scale; -        if (this._isInjectedAndLoaded) { -            this._invokeApi('setContentScale', {scale}); -        } +        this._invokeApi('setContentScale', {scale});      }      // Popup-only public functions @@ -146,7 +151,7 @@ class Popup {      }      isVisibleSync() { -        return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible); +        return (this._visibleOverride !== null ? this._visibleOverride : this._visible);      }      updateTheme() { @@ -154,21 +159,13 @@ class Popup {          this._container.dataset.yomichanSiteColor = this._getSiteColor();      } -    async setCustomOuterCss(css, injectDirectly) { -        // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. -        if (this._stylesheetInjectedViaApi) { return; } - -        if (injectDirectly || Popup._isOnExtensionPage()) { -            Popup.injectOuterStylesheet(css); -        } else { -            if (!css) { return; } -            try { -                await apiInjectStylesheet(css); -                this._stylesheetInjectedViaApi = true; -            } catch (e) { -                // NOP -            } -        } +    async setCustomOuterCss(css, useWebExtensionApi) { +        return await Popup._injectStylesheet( +            'yomichan-popup-outer-user-stylesheet', +            'code', +            css, +            useWebExtensionApi +        );      }      setChildrenSupported(value) { @@ -183,26 +180,6 @@ class Popup {          return this._container.getBoundingClientRect();      } -    static injectOuterStylesheet(css) { -        if (Popup.outerStylesheet === null) { -            if (!css) { return; } -            Popup.outerStylesheet = document.createElement('style'); -            Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet'; -        } - -        const outerStylesheet = Popup.outerStylesheet; -        if (css) { -            outerStylesheet.textContent = css; - -            const par = document.head; -            if (par && outerStylesheet.parentNode !== par) { -                par.appendChild(outerStylesheet); -            } -        } else { -            outerStylesheet.textContent = ''; -        } -    } -      // Private functions      _inject() { @@ -222,11 +199,18 @@ class Popup {              // NOP          } +        if (this._messageToken === null) { +            this._messageToken = await apiGetMessageToken(); +        } +          return new Promise((resolve) => {              const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); +            this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));              this._container.addEventListener('load', () => { -                this._isInjectedAndLoaded = true; -                this._invokeApi('initialize', { +                const uniqueId = yomichan.generateId(32); +                Popup._listenForDisplayPrepareCompleted(uniqueId, resolve); + +                this._invokeApi('prepare', {                      options: this._options,                      popupInfo: {                          id: this._id, @@ -235,17 +219,60 @@ class Popup {                      },                      url: this.url,                      childrenSupported: this._childrenSupported, -                    scale: this._contentScale +                    scale: this._contentScale, +                    uniqueId                  }); -                resolve();              }); -            this._observeFullscreen(); +            this._observeFullscreen(true);              this._onFullscreenChanged(); -            this.setCustomOuterCss(this._options.general.customPopupOuterCss, false); -            this._isInjected = true; +            this._injectStyles();          });      } +    async _injectStyles() { +        try { +            await Popup._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); +        } catch (e) { +            // NOP +        } + +        try { +            await this.setCustomOuterCss(this._options.general.customPopupOuterCss, true); +        } catch (e) { +            // NOP +        } +    } + +    _observeFullscreen(observe) { +        if (!observe) { +            this._fullscreenEventListeners.removeAllEventListeners(); +            return; +        } + +        if (this._fullscreenEventListeners.size > 0) { +            // Already observing +            return; +        } + +        const fullscreenEvents = [ +            'fullscreenchange', +            'MSFullscreenChange', +            'mozfullscreenchange', +            'webkitfullscreenchange' +        ]; +        const onFullscreenChanged = () => this._onFullscreenChanged(); +        for (const eventName of fullscreenEvents) { +            this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false); +        } +    } + +    _onFullscreenChanged() { +        const parent = (Popup._getFullscreenElement() || document.body || null); +        if (parent !== null && this._container.parentNode !== parent) { +            parent.appendChild(this._container); +        } +    } +      async _show(elementRect, writingMode) {          await this._inject(); @@ -327,38 +354,38 @@ class Popup {      }      _invokeApi(action, params={}) { -        if (!this._isInjectedAndLoaded) { -            throw new Error('Frame not loaded'); -        } -        this._container.contentWindow.postMessage({action, params}, '*'); -    } +        const token = this._messageToken; +        const contentWindow = this._container.contentWindow; +        if (token === null || contentWindow === null) { return; } -    _observeFullscreen() { -        const fullscreenEvents = [ -            'fullscreenchange', -            'MSFullscreenChange', -            'mozfullscreenchange', -            'webkitfullscreenchange' -        ]; -        for (const eventName of fullscreenEvents) { -            document.addEventListener(eventName, () => this._onFullscreenChanged(), false); -        } +        contentWindow.postMessage({action, params, token}, this._targetOrigin);      } -    _getFullscreenElement() { +    static _getFullscreenElement() {          return (              document.fullscreenElement ||              document.msFullscreenElement ||              document.mozFullScreenElement || -            document.webkitFullscreenElement +            document.webkitFullscreenElement || +            null          );      } -    _onFullscreenChanged() { -        const parent = (this._getFullscreenElement() || document.body || null); -        if (parent !== null && this._container.parentNode !== parent) { -            parent.appendChild(this._container); -        } +    static _listenForDisplayPrepareCompleted(uniqueId, resolve) { +        const runtimeMessageCallback = ({action, params}, sender, callback) => { +            if ( +                action === 'popupPrepareCompleted' && +                typeof params === 'object' && +                params !== null && +                params.uniqueId === uniqueId +            ) { +                chrome.runtime.onMessage.removeListener(runtimeMessageCallback); +                callback(); +                resolve(); +                return false; +            } +        }; +        chrome.runtime.onMessage.addListener(runtimeMessageCallback);      }      static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { @@ -492,15 +519,6 @@ class Popup {          ];      } -    static _isOnExtensionPage() { -        try { -            const url = chrome.runtime.getURL('/'); -            return window.location.href.substring(0, url.length) === url; -        } catch (e) { -            // NOP -        } -    } -      static _getViewport(useVisualViewport) {          const visualViewport = window.visualViewport;          if (visualViewport !== null && typeof visualViewport === 'object') { @@ -533,6 +551,80 @@ class Popup {              bottom: window.innerHeight          };      } + +    static _isOnExtensionPage() { +        try { +            const url = chrome.runtime.getURL('/'); +            return window.location.href.substring(0, url.length) === url; +        } catch (e) { +            // NOP +        } +    } + +    static async _injectStylesheet(id, type, value, useWebExtensionApi) { +        const injectedStylesheets = Popup._injectedStylesheets; + +        if (Popup._isOnExtensionPage()) { +            // Permissions error will occur if trying to use the WebExtension API to inject +            // into an extension page. +            useWebExtensionApi = false; +        } + +        let styleNode = injectedStylesheets.get(id); +        if (typeof styleNode !== 'undefined') { +            if (styleNode === null) { +                // Previously injected via WebExtension API +                throw new Error(`Stylesheet with id ${id} has already been injected using the WebExtension API`); +            } +        } else { +            styleNode = null; +        } + +        if (useWebExtensionApi) { +            // Inject via WebExtension API +            if (styleNode !== null && styleNode.parentNode !== null) { +                styleNode.parentNode.removeChild(styleNode); +            } + +            await apiInjectStylesheet(type, value); + +            injectedStylesheets.set(id, null); +            return null; +        } + +        // Create node in document +        const parentNode = document.head; +        if (parentNode === null) { +            throw new Error('No parent node'); +        } + +        // Create or reuse node +        const isFile = (type === 'file'); +        const tagName = isFile ? 'link' : 'style'; +        if (styleNode === null || styleNode.nodeName.toLowerCase() !== tagName) { +            if (styleNode !== null && styleNode.parentNode !== null) { +                styleNode.parentNode.removeChild(styleNode); +            } +            styleNode = document.createElement(tagName); +            styleNode.id = id; +        } + +        // Update node style +        if (isFile) { +            styleNode.rel = value; +        } else { +            styleNode.textContent = value; +        } + +        // Update parent +        if (styleNode.parentNode !== parentNode) { +            parentNode.appendChild(styleNode); +        } + +        // Add to map +        injectedStylesheets.set(id, styleNode); +        return styleNode; +    }  } -Popup.outerStylesheet = null; +Popup._injectedStylesheets = new Map(); diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 11d3ff0e..6dc482bd 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -82,7 +82,11 @@ class TextSourceRange {      }      equals(other) { -        if (other === null) { +        if (!( +            typeof other === 'object' && +            other !== null && +            other instanceof TextSourceRange +        )) {              return false;          }          if (this.imposterSourceElement !== null) { @@ -362,7 +366,7 @@ class TextSourceElement {      setEndOffset(length) {          switch (this.element.nodeName.toUpperCase()) {              case 'BUTTON': -                this.content = this.element.innerHTML; +                this.content = this.element.textContent;                  break;              case 'IMG':                  this.content = this.element.getAttribute('alt'); @@ -409,6 +413,12 @@ class TextSourceElement {      }      equals(other) { -        return other && other.element === this.element && other.content === this.content; +        return ( +            typeof other === 'object' && +            other !== null && +            other instanceof TextSourceElement && +            other.element === this.element && +            other.content === this.content +        );      }  } |