/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = ["FinderHighlighter"]; const { interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm"); XPCOMUtils.defineLazyGetter(this, "kDebug", () => { const kDebugPref = "findbar.modalHighlight.debug"; return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref); }); const kContentChangeThresholdPx = 5; const kModalHighlightRepaintFreqMs = 10; const kHighlightAllPref = "findbar.highlightAll"; const kModalHighlightPref = "findbar.modalHighlight"; const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size", "font-size-adjust", "font-stretch", "font-variant", "font-weight", "line-height", "letter-spacing", "text-emphasis", "text-orientation", "text-transform", "word-spacing"]; const kFontPropsCamelCase = kFontPropsCSS.map(prop => { let parts = prop.split("-"); return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(""); }); const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i // This uuid is used to prefix HTML element IDs and classNames in order to make // them unique and hard to clash with IDs and classNames content authors come up // with, since the stylesheet for modal highlighting is inserted as an agent-sheet // in the active HTML document. const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463"; const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline"; const kModalStyle = ` .findbar-modalHighlight-outline { position: absolute; background: #ffc535; border-radius: 3px; box-shadow: 0 2px 0 0 rgba(0,0,0,.1); color: #000; display: -moz-box; margin: -2px 0 0 -2px !important; padding: 2px !important; pointer-events: none; z-index: 2; } .findbar-modalHighlight-outline.findbar-debug { z-index: 2147483647; } .findbar-modalHighlight-outline[grow] { animation-name: findbar-modalHighlight-outlineAnim; } @keyframes findbar-modalHighlight-outlineAnim { from { transform: scaleX(0) scaleY(0); } 50% { transform: scaleX(1.5) scaleY(1.5); } to { transform: scaleX(0) scaleY(0); } } .findbar-modalHighlight-outline[hidden] { opacity: 0; } .findbar-modalHighlight-outline:not([disable-transitions]) { transition-property: opacity, transform, top, left; transition-duration: 50ms; transition-timing-function: linear; } .findbar-modalHighlight-outline-text { margin: 0 !important; padding: 0 !important; vertical-align: top !important; } .findbar-modalHighlight-outlineMask { background: #000; mix-blend-mode: multiply; opacity: .35; pointer-events: none; position: absolute; z-index: 1; } .findbar-modalHighlight-outlineMask.findbar-debug { z-index: 2147483646; top: 0; left: 0; } .findbar-modalHighlight-outlineMask[brighttext] { background: #fff; } .findbar-modalHighlight-rect { background: #fff; margin: -1px 0 0 -1px !important; padding: 0 1px 2px 1px !important; position: absolute; } .findbar-modalHighlight-outlineMask[brighttext] > .findbar-modalHighlight-rect { background: #000; }`; function mockAnonymousContentNode(domNode) { return { setTextContentForElement(id, text) { (domNode.querySelector("#" + id) || domNode).textContent = text; }, getAttributeForElement(id, attrName) { let node = domNode.querySelector("#" + id) || domNode; if (!node.hasAttribute(attrName)) return undefined; return node.getAttribute(attrName); }, setAttributeForElement(id, attrName, attrValue) { (domNode.querySelector("#" + id) || domNode).setAttribute(attrName, attrValue); }, removeAttributeForElement(id, attrName) { let node = domNode.querySelector("#" + id) || domNode; if (!node.hasAttribute(attrName)) return; node.removeAttribute(attrName); }, remove() { try { domNode.parentNode.removeChild(domNode); } catch (ex) {} } }; } /** * FinderHighlighter class that is used by Finder.jsm to take care of the * 'Highlight All' feature, which can highlight all find occurrences in a page. * * @param {Finder} finder Finder.jsm instance */ function FinderHighlighter(finder) { this._currentFoundRange = null; this._modal = Services.prefs.getBoolPref(kModalHighlightPref); this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref); this._lastIteratorParams = null; this.finder = finder; this.visible = false; } FinderHighlighter.prototype = { get iterator() { if (this._iterator) return this._iterator; this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator; return this._iterator; }, get modalStyleSheet() { if (!this._modalStyleSheet) { this._modalStyleSheet = kModalStyle.replace(/findbar-/g, kModalIdPrefix + "-findbar-"); } return this._modalStyleSheet; }, get modalStyleSheetURI() { if (!this._modalStyleSheetURI) { this._modalStyleSheetURI = "data:text/css;charset=utf-8," + encodeURIComponent(this.modalStyleSheet.replace(/[\n]+/g, " ")); } return this._modalStyleSheetURI; }, /** * Notify all registered listeners that the 'Highlight All' operation finished. * * @param {Boolean} highlight Whether highlighting was turned on */ notifyFinished(highlight) { for (let l of this.finder._listeners) { try { l.onHighlightFinished(highlight); } catch (ex) {} } }, /** * Toggle highlighting all occurrences of a word in a page. This method will * be called recursively for each (i)frame inside a page. * * @param {Booolean} highlight Whether highlighting should be turned on * @param {String} word Needle to search for and highlight when found * @param {Boolean} linksOnly Only consider nodes that are links for the search * @yield {Promise} that resolves once the operation has finished */ highlight: Task.async(function* (highlight, word, linksOnly) { let window = this.finder._getWindow(); let controller = this.finder._getSelectionController(window); let doc = window.document; this._found = false; if (!controller || !doc || !doc.documentElement) { // Without the selection controller, // we are unable to (un)highlight any matches return this._found; } if (highlight) { let params = { caseSensitive: this.finder._fastFind.caseSensitive, entireWord: this.finder._fastFind.entireWord, linksOnly, word, finder: this.finder, listener: this, useCache: true }; if (this.iterator._areParamsEqual(params, this._lastIteratorParams)) return this._found; if (params) { yield this.iterator.start(params); if (this._found) this.finder._outlineLink(true); } } else { this.hide(window); // Removing the highlighting always succeeds, so return true. this._found = true; } return this._found; }), // FinderIterator listener implementation onIteratorRangeFound(range) { this.highlightRange(range); this._found = true; }, onIteratorReset() { this.clear(); }, onIteratorRestart() {}, onIteratorStart(params) { // Save a clean params set for use later in the `update()` method. this._lastIteratorParams = params; this.clear(); if (!this._modal) this.hide(this.finder._getWindow(), this.finder._fastFind.getFoundRange()); }, /** * Add a range to the find selection, i.e. highlight it, and if it's inside an * editable node, track it. * * @param {nsIDOMRange} range Range object to be highlighted */ highlightRange(range) { let node = range.startContainer; let editableNode = this._getEditableNode(node); let window = node.ownerDocument.defaultView; let controller = this.finder._getSelectionController(window); if (editableNode) { controller = editableNode.editor.selectionController; } if (this._modal) { this._modalHighlight(range, controller, window); } else { let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); findSelection.addRange(range); } if (editableNode) { // Highlighting added, so cache this editor, and hook up listeners // to ensure we deal properly with edits within the highlighting this._addEditorListeners(editableNode.editor); } }, /** * If modal highlighting is enabled, show the dimmed background that will overlay * the page. * * @param {nsIDOMWindow} window The dimmed background will overlay this window. * Optional, defaults to the finder window. */ show(window = null) { if (!this._modal || this.visible) return; this.visible = true; window = window || this.finder._getWindow(); this._maybeCreateModalHighlightNodes(window); this._addModalHighlightListeners(window); }, /** * Clear all highlighted matches. If modal highlighting is enabled and * the outline + dimmed background is currently visible, both will be hidden. * * @param {nsIDOMWindow} window The dimmed background will overlay this window. * Optional, defaults to the finder window. * @param {nsIDOMRange} skipRange A range that should not be removed from the * find selection. * @param {nsIDOMEvent} event When called from an event handler, this will * be the triggering event. */ hide(window = null, skipRange = null, event = null) { // Do not hide on anything but a left-click. if (event && event.type == "click" && event.button !== 0) return; window = window || this.finder._getWindow(); let doc = window.document; this._clearSelection(this.finder._getSelectionController(window), skipRange); // Next, check our editor cache, for editors belonging to this // document if (this._editors) { for (let x = this._editors.length - 1; x >= 0; --x) { if (this._editors[x].document == doc) { this._clearSelection(this._editors[x].selectionController, skipRange); // We don't need to listen to this editor any more this._unhookListenersAtIndex(x); } } } if (this._modalRepaintScheduler) { window.clearTimeout(this._modalRepaintScheduler); this._modalRepaintScheduler = null; } this._lastWindowDimensions = null; if (this._modalHighlightOutline) this._modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true"); this._removeHighlightAllMask(window); this._removeModalHighlightListeners(window); delete this._brightText; this.visible = false; }, /** * Called by the Finder after a find result comes in; update the position and * content of the outline to the newly found occurrence. * To make sure that the outline covers the found range completely, all the * CSS styles that influence the text are copied and applied to the outline. * * @param {Object} data Dictionary coming from Finder that contains the * following properties: * {Number} result One of the nsITypeAheadFind.FIND_* constants * indicating the result of a search operation. * {Boolean} findBackwards If TRUE, the search was performed backwards, * FALSE if forwards. * {Boolean} findAgain If TRUE, the search was performed using the same * search string as before. * {String} linkURL If a link was hit, this will contain a URL string. * {Rect} rect An object with top, left, width and height * coordinates of the current selection. * {String} searchString The string the search was performed with. * {Boolean} storeResult Indicator if the search string should be stored * by the consumer of the Finder. */ update(data) { let window = this.finder._getWindow(); let foundRange = this.finder._fastFind.getFoundRange(); if (!this._modal) { if (this._highlightAll) { this._currentFoundRange = foundRange; let params = this.iterator.params; if (this.iterator._areParamsEqual(params, this._lastIteratorParams)) return; if (params) this.highlight(true, params.word, params.linksOnly); } return; } // Place the match placeholder on top of the current found range. if (data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND || !foundRange) { this.hide(); return; } let outlineNode; if (foundRange !== this._currentFoundRange || data.findAgain) { this._currentFoundRange = foundRange; let textContent = this._getRangeContentArray(foundRange); if (!textContent.length) { this.hide(window); return; } let rect = foundRange.getClientRects()[0]; let fontStyle = this._getRangeFontStyle(foundRange); if (typeof this._brightText == "undefined") { this._brightText = this._isColorBright(fontStyle.color); } // Text color in the outline is determined by our stylesheet. delete fontStyle.color; if (!this.visible) this.show(window); else this._maybeCreateModalHighlightNodes(window); outlineNode = this._modalHighlightOutline; outlineNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" ")); // Correct the line-height to align the text in the middle of the box. fontStyle.lineHeight = rect.height + "px"; outlineNode.setAttributeForElement(kModalOutlineId + "-text", "style", this._getHTMLFontStyle(fontStyle)); if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "hidden") == "string") outlineNode.removeAttributeForElement(kModalOutlineId, "hidden"); let { scrollX, scrollY } = this._getScrollPosition(window); outlineNode.setAttributeForElement(kModalOutlineId, "style", `top: ${scrollY + rect.top}px; left: ${scrollX + rect.left}px; height: ${rect.height}px; width: ${rect.width}px;`); } outlineNode = this._modalHighlightOutline; try { outlineNode.removeAttributeForElement(kModalOutlineId, "grow"); } catch (ex) {} window.requestAnimationFrame(() => { outlineNode.setAttributeForElement(kModalOutlineId, "grow", true); }); }, /** * Invalidates the list by clearing the map of highglighted ranges that we * keep to build the mask for. */ clear() { this._currentFoundRange = null; // Reset the Map, because no range references a node anymore. if (this._modalHighlightRectsMap) this._modalHighlightRectsMap.clear(); }, /** * When the current page is refreshed or navigated away from, the CanvasFrame * contents is not valid anymore, i.e. all anonymous content is destroyed. * We need to clear the references we keep, which'll make sure we redraw * everything when the user starts to find in page again. */ onLocationChange() { this.clear(); if (!this._modalHighlightOutline) return; if (kDebug) this._modalHighlightOutline.remove(); try { this.finder._getWindow().document .removeAnonymousContent(this._modalHighlightOutline); } catch (ex) {} this._modalHighlightOutline = null; }, /** * When `kModalHighlightPref` pref changed during a session, this callback is * invoked. When modal highlighting is turned off, we hide the CanvasFrame * contents. * * @param {Boolean} useModalHighlight */ onModalHighlightChange(useModalHighlight) { if (this._modal && !useModalHighlight) { this.hide(); this.clear(); } this._modal = useModalHighlight; }, /** * When 'Highlight All' is toggled during a session, this callback is invoked * and when it's turned off, the found occurrences will be removed from the mask. * * @param {Boolean} highlightAll */ onHighlightAllChange(highlightAll) { this._highlightAll = highlightAll; if (this._modal && !highlightAll) { this.clear(); this._scheduleRepaintOfMask(this.finder._getWindow()); } }, /** * Utility; removes all ranges from the find selection that belongs to a * controller. Optionally skips a specific range. * * @param {nsISelectionController} controller * @param {nsIDOMRange} restoreRange */ _clearSelection(controller, restoreRange = null) { let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); sel.removeAllRanges(); if (restoreRange) { sel = controller.getSelection(Ci.nsISelectionController.SELECTION_NORMAL); sel.addRange(restoreRange); controller.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION); controller.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL); } }, /** * Utility; get the nsIDOMWindowUtils for a window. * * @param {nsIDOMWindow} window Optional, defaults to the finder window. * @return {nsIDOMWindowUtils} */ _getDWU(window = null) { return (window || this.finder._getWindow()) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); }, /** * Utility; wrapper around nsIDOMWindowUtils#getScrollXY. * * @param {nsDOMWindow} window Optional, defaults to the finder window. * @return {Object} The current scroll position. */ _getScrollPosition(window = null) { let scrollX = {}; let scrollY = {}; this._getDWU(window).getScrollXY(false, scrollX, scrollY); return { scrollX: scrollX.value, scrollY: scrollY.value }; }, /** * Utility; fetch the full width and height of the current window, excluding * scrollbars. * * @param {nsiDOMWindow} window The current finder window. * @return {Object} The current full page dimensions with `width` and `height` * properties */ _getWindowDimensions(window) { // First we'll try without flushing layout, because it's way faster. let dwu = this._getDWU(window); let {width, height} = dwu.getBoundsWithoutFlushing(window.document.body); if (!width || !height) { // We need a flush after all :'( width = window.innerWidth + window.scrollMaxX - window.scrollMinX; height = window.innerHeight + window.scrollMaxY - window.scrollMinY; } let scrollbarHeight = {}; let scrollbarWidth = {}; dwu.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); width -= scrollbarWidth.value; height -= scrollbarHeight.value; return { width, height }; }, /** * Utility; fetch the current text contents of a given range. * * @param {nsIDOMRange} range Range object to extract the contents from. * @return {Array} Snippets of text. */ _getRangeContentArray(range) { let content = range.cloneContents(); let t, textContent = []; for (let node of content.childNodes) { t = node.textContent || node.nodeValue; //if (t && t.trim()) textContent.push(t); } return textContent; }, /** * Utility; get all available font styles as applied to the content of a given * range. The CSS properties we look for can be found in `kFontPropsCSS`. * * @param {nsIDOMRange} range Range to fetch style info from. * @return {Object} Dictionary consisting of the styles that were found. */ _getRangeFontStyle(range) { let node = range.startContainer; while (node.nodeType != 1) node = node.parentNode; let style = node.ownerDocument.defaultView.getComputedStyle(node, ""); let props = {}; for (let prop of kFontPropsCamelCase) { if (prop in style && style[prop]) props[prop] = style[prop]; } return props; }, /** * Utility; transform a dictionary object as returned by `_getRangeFontStyle` * above into a HTML style attribute value. * * @param {Object} fontStyle * @return {String} */ _getHTMLFontStyle(fontStyle) { let style = []; for (let prop of Object.getOwnPropertyNames(fontStyle)) { let idx = kFontPropsCamelCase.indexOf(prop); if (idx == -1) continue; style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]};`); } return style.join(" "); }, /** * Checks whether a CSS RGB color value can be classified as being 'bright'. * * @param {String} cssColor RGB color value in the default format rgb[a](r,g,b) * @return {Boolean} */ _isColorBright(cssColor) { cssColor = cssColor.match(kRGBRE); if (!cssColor || !cssColor.length) return false; cssColor.shift(); return new Color(...cssColor).isBright; }, /** * Add a range to the list of ranges to highlight on, or cut out of, the dimmed * background. * * @param {nsIDOMRange} range Range object that should be inspected * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed */ _modalHighlight(range, controller, window) { if (!this._getRangeContentArray(range).length) return; let rects = new Set(); // Absolute positions should include the viewport scroll offset. let { scrollX, scrollY } = this._getScrollPosition(window); // A range may consist of multiple rectangles, we can also do these kind of // precise cut-outs. range.getBoundingClientRect() returns the fully // encompassing rectangle, which is too much for our purpose here. for (let dims of range.getClientRects()) { rects.add({ height: dims.bottom - dims.top, width: dims.right - dims.left, y: dims.top + scrollY, x: dims.left + scrollX }); } if (!this._modalHighlightRectsMap) this._modalHighlightRectsMap = new Map(); this._modalHighlightRectsMap.set(range, rects); this.show(window); // We don't repaint the mask right away, but pass it off to a render loop of // sorts. this._scheduleRepaintOfMask(window); }, /** * Lazily insert the nodes we need as anonymous content into the CanvasFrame * of a window. * * @param {nsIDOMWindow} window Window to draw in. */ _maybeCreateModalHighlightNodes(window) { if (this._modalHighlightOutline) { if (!this._modalHighlightAllMask) { // Make sure to at least show the dimmed background. this._repaintHighlightAllMask(window, false); this._scheduleRepaintOfMask(window); } return; } let document = window.document; // A hidden document doesn't accept insertAnonymousContent calls yet. if (document.hidden) { let onVisibilityChange = () => { document.removeEventListener("visibilitychange", onVisibilityChange); this._maybeCreateModalHighlightNodes(window); }; document.addEventListener("visibilitychange", onVisibilityChange); return; } this._maybeInstallStyleSheet(window); // The outline needs to be sitting inside a container, otherwise the anonymous // content API won't find it by its ID later... let container = document.createElement("div"); // Create the main (yellow) highlight outline box. let outlineBox = document.createElement("div"); outlineBox.setAttribute("id", kModalOutlineId); outlineBox.className = kModalOutlineId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : ""); let outlineBoxText = document.createElement("span"); let attrValue = kModalOutlineId + "-text"; outlineBoxText.setAttribute("id", attrValue); outlineBoxText.setAttribute("class", attrValue); outlineBox.appendChild(outlineBoxText); container.appendChild(outlineBox); this._modalHighlightOutline = kDebug ? mockAnonymousContentNode(document.body.appendChild(container.firstChild)) : document.insertAnonymousContent(container); // Make sure to at least show the dimmed background. this._repaintHighlightAllMask(window, false); }, /** * Build and draw the mask that takes care of the dimmed background that * overlays the current page and the mask that cuts out all the rectangles of * the ranges that were found. * * @param {nsIDOMWindow} window Window to draw in. * @param {Boolean} [paintContent] */ _repaintHighlightAllMask(window, paintContent = true) { let document = window.document; const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask"; let maskNode = document.createElement("div"); // Make sure the dimmed mask node takes the full width and height that's available. let {width, height} = this._getWindowDimensions(window); this._lastWindowDimensions = { width, height }; maskNode.setAttribute("id", kMaskId); maskNode.setAttribute("class", kMaskId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : "")); maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`); if (this._brightText) maskNode.setAttribute("brighttext", "true"); if (paintContent || this._modalHighlightAllMask) { // Create a DOM node for each rectangle representing the ranges we found. let maskContent = []; const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect"; if (this._modalHighlightRectsMap) { for (let [range, rects] of this._modalHighlightRectsMap) { for (let rect of rects) { maskContent.push(`
`); } } } maskNode.innerHTML = maskContent.join(""); } // Always remove the current mask and insert it a-fresh, because we're not // free to alter DOM nodes inside the CanvasFrame. this._removeHighlightAllMask(window); this._modalHighlightAllMask = kDebug ? mockAnonymousContentNode(document.body.appendChild(maskNode)) : document.insertAnonymousContent(maskNode); }, /** * Safely remove the mask AnoymousContent node from the CanvasFrame. * * @param {nsIDOMWindow} window */ _removeHighlightAllMask(window) { if (this._modalHighlightAllMask) { // If the current window isn't the one the content was inserted into, this // will fail, but that's fine. if (kDebug) this._modalHighlightAllMask.remove(); try { window.document.removeAnonymousContent(this._modalHighlightAllMask); } catch (ex) {} this._modalHighlightAllMask = null; } }, /** * Doing a full repaint each time a range is delivered by the highlight iterator * is way too costly, thus we pipe the frequency down to every * `kModalHighlightRepaintFreqMs` milliseconds. * * @param {nsIDOMWindow} window * @param {Boolean} contentChanged Whether the documents' content changed * in the meantime. This happens when the * DOM is updated whilst the page is loaded. */ _scheduleRepaintOfMask(window, contentChanged = false) { if (this._modalRepaintScheduler) { window.clearTimeout(this._modalRepaintScheduler); this._modalRepaintScheduler = null; } // When we request to repaint unconditionally, we mean to call // `_repaintHighlightAllMask()` right after the timeout. if (!this._unconditionalRepaintRequested) this._unconditionalRepaintRequested = !contentChanged; this._modalRepaintScheduler = window.setTimeout(() => { if (this._unconditionalRepaintRequested) { this._unconditionalRepaintRequested = false; this._repaintHighlightAllMask(window); return; } let { width, height } = this._getWindowDimensions(window); if (!this._modalHighlightRectsMap || (Math.abs(this._lastWindowDimensions.width - width) < kContentChangeThresholdPx && Math.abs(this._lastWindowDimensions.height - height) < kContentChangeThresholdPx)) { return; } this.iterator.restart(this.finder); this._lastWindowDimensions = { width, height }; this._repaintHighlightAllMask(window); }, kModalHighlightRepaintFreqMs); }, /** * The outline that shows/ highlights the current found range is styled and * animated using CSS. This style can be found in `kModalStyle`, but to have it * applied on any DOM node we insert using the AnonymousContent API we need to * inject an agent sheet into the document. * * @param {nsIDOMWindow} window */ _maybeInstallStyleSheet(window) { let document = window.document; // The WeakMap is a cheap method to make sure we don't needlessly insert the // same sheet twice. if (!this._modalInstalledSheets) this._modalInstalledSheets = new WeakMap(); if (this._modalInstalledSheets.has(document)) return; let dwu = this._getDWU(window); let uri = this.modalStyleSheetURI; try { dwu.loadSheetUsingURIString(uri, dwu.AGENT_SHEET); } catch (e) {} this._modalInstalledSheets.set(document, uri); }, /** * Add event listeners to the content which will cause the modal highlight * AnonymousContent to be re-painted or hidden. * * @param {nsIDOMWindow} window */ _addModalHighlightListeners(window) { if (this._highlightListeners) return; this._highlightListeners = [ this._scheduleRepaintOfMask.bind(this, window, true), this.hide.bind(this, window, null) ]; let target = this.iterator._getDocShell(window).chromeEventHandler; target.addEventListener("MozAfterPaint", this._highlightListeners[0]); window.addEventListener("click", this._highlightListeners[1]); }, /** * Remove event listeners from content. * * @param {nsIDOMWindow} window */ _removeModalHighlightListeners(window) { if (!this._highlightListeners) return; let target = this.iterator._getDocShell(window).chromeEventHandler; target.removeEventListener("MozAfterPaint", this._highlightListeners[0]); window.removeEventListener("click", this._highlightListeners[1]); this._highlightListeners = null; }, /** * For a given node returns its editable parent or null if there is none. * It's enough to check if node is a text node and its parent's parent is * instance of nsIDOMNSEditableElement. * * @param node the node we want to check * @returns the first node in the parent chain that is editable, * null if there is no such node */ _getEditableNode(node) { if (node.nodeType === node.TEXT_NODE && node.parentNode && node.parentNode.parentNode && node.parentNode.parentNode instanceof Ci.nsIDOMNSEditableElement) { return node.parentNode.parentNode; } return null; }, /** * Add ourselves as an nsIEditActionListener and nsIDocumentStateListener for * a given editor * * @param editor the editor we'd like to listen to */ _addEditorListeners(editor) { if (!this._editors) { this._editors = []; this._stateListeners = []; } let existingIndex = this._editors.indexOf(editor); if (existingIndex == -1) { let x = this._editors.length; this._editors[x] = editor; this._stateListeners[x] = this._createStateListener(); this._editors[x].addEditActionListener(this); this._editors[x].addDocumentStateListener(this._stateListeners[x]); } }, /** * Helper method to unhook listeners, remove cached editors * and keep the relevant arrays in sync * * @param idx the index into the array of editors/state listeners * we wish to remove */ _unhookListenersAtIndex(idx) { this._editors[idx].removeEditActionListener(this); this._editors[idx] .removeDocumentStateListener(this._stateListeners[idx]); this._editors.splice(idx, 1); this._stateListeners.splice(idx, 1); if (!this._editors.length) { delete this._editors; delete this._stateListeners; } }, /** * Remove ourselves as an nsIEditActionListener and * nsIDocumentStateListener from a given cached editor * * @param editor the editor we no longer wish to listen to */ _removeEditorListeners(editor) { // editor is an editor that we listen to, so therefore must be // cached. Find the index of this editor let idx = this._editors.indexOf(editor); if (idx == -1) { return; } // Now unhook ourselves, and remove our cached copy this._unhookListenersAtIndex(idx); }, /* * nsIEditActionListener logic follows * * We implement this interface to allow us to catch the case where * the findbar found a match in a HTML or