/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* 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/. */ const {Cc, Cu, Ci} = require("chrome"); // Page size for pageup/pagedown const PAGE_SIZE = 10; const PREVIEW_AREA = 700; const DEFAULT_MAX_CHILDREN = 100; const COLLAPSE_ATTRIBUTE_LENGTH = 120; const COLLAPSE_DATA_URL_REGEX = /^data.+base64/; const COLLAPSE_DATA_URL_LENGTH = 60; const CONTAINER_FLASHING_DURATION = 500; const {UndoStack} = require("devtools/shared/undo"); const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor"); const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); const {HTMLEditor} = require("devtools/markupview/html-editor"); const {OutputParser} = require("devtools/output-parser"); const promise = require("sdk/core/promise"); const {Tooltip} = require("devtools/shared/widgets/Tooltip"); Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); Cu.import("resource://gre/modules/devtools/Templater.jsm"); Cu.import("resource://gre/modules/Services.jsm"); loader.lazyGetter(this, "DOMParser", function() { return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); }); loader.lazyGetter(this, "AutocompletePopup", () => { return require("devtools/shared/autocomplete-popup").AutocompletePopup }); /** * Vocabulary for the purposes of this file: * * MarkupContainer - the structure that holds an editor and its * immediate children in the markup panel. * Node - A content node. * object.elt - A UI element in the markup panel. */ /** * The markup tree. Manages the mapping of nodes to MarkupContainers, * updating based on mutations, and the undo/redo bindings. * * @param Inspector aInspector * The inspector we're watching. * @param iframe aFrame * An iframe in which the caller has kindly loaded markup-view.xhtml. */ function MarkupView(aInspector, aFrame, aControllerWindow) { this._inspector = aInspector; this.walker = this._inspector.walker; this._frame = aFrame; this.doc = this._frame.contentDocument; this._elt = this.doc.querySelector("#root"); this._outputParser = new OutputParser(); this.htmlEditor = new HTMLEditor(this.doc); this.layoutHelpers = new LayoutHelpers(this.doc.defaultView); try { this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize"); } catch(ex) { this.maxChildren = DEFAULT_MAX_CHILDREN; } // Creating the popup to be used to show CSS suggestions. let options = { fixedWidth: true, autoSelect: true, theme: "auto" }; this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options); this.undo = new UndoStack(); this.undo.installController(aControllerWindow); this._containers = new Map(); this._boundMutationObserver = this._mutationObserver.bind(this); this.walker.on("mutations", this._boundMutationObserver); this._boundOnNewSelection = this._onNewSelection.bind(this); this._inspector.selection.on("new-node-front", this._boundOnNewSelection); this._onNewSelection(); this._boundKeyDown = this._onKeyDown.bind(this); this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false); this._boundFocus = this._onFocus.bind(this); this._frame.addEventListener("focus", this._boundFocus, false); this._handlePrefChange = this._handlePrefChange.bind(this); gDevTools.on("pref-changed", this._handlePrefChange); this._initPreview(); } exports.MarkupView = MarkupView; MarkupView.prototype = { _selectedContainer: null, template: function(aName, aDest, aOptions={stack: "markup-view.xhtml"}) { let node = this.doc.getElementById("template-" + aName).cloneNode(true); node.removeAttribute("id"); template(node, aDest, aOptions); return node; }, /** * Get the MarkupContainer object for a given node, or undefined if * none exists. */ getContainer: function(aNode) { return this._containers.get(aNode); }, _handlePrefChange: function(event, data) { if (data.pref == "devtools.defaultColorUnit") { this.update(); } }, update: function() { let updateChildren = function(node) { this.getContainer(node).update(); for (let child of node.treeChildren()) { updateChildren(child); } }.bind(this); // Start with the documentElement let documentElement; for (let node of this._rootNode.treeChildren()) { if (node.isDocumentElement === true) { documentElement = node; break; } } // Recursively update each node starting with documentElement. updateChildren(documentElement); }, /** * Highlight the inspector selected node. */ _onNewSelection: function() { this.htmlEditor.hide(); let done = this._inspector.updating("markup-view"); if (this._inspector.selection.isNode()) { this.showNode(this._inspector.selection.nodeFront, true).then(() => { this.markNodeAsSelected(this._inspector.selection.nodeFront); done(); }); } else { this.unmarkSelectedNode(); done(); } }, /** * Create a TreeWalker to find the next/previous * node for selection. */ _selectionWalker: function(aStart) { let walker = this.doc.createTreeWalker( aStart || this._elt, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, function(aElement) { if (aElement.container && aElement.container.elt === aElement && aElement.container.visible) { return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; } return Ci.nsIDOMNodeFilter.FILTER_SKIP; } ); walker.currentNode = this._selectedContainer.elt; return walker; }, /** * Key handling. */ _onKeyDown: function(aEvent) { let handled = true; // Ignore keystrokes that originated in editors. if (aEvent.target.tagName.toLowerCase() === "input" || aEvent.target.tagName.toLowerCase() === "textarea") { return; } switch(aEvent.keyCode) { case Ci.nsIDOMKeyEvent.DOM_VK_H: let node = this._selectedContainer.node; if (node.hidden) { this.walker.unhideNode(node).then(() => this.nodeChanged(node)); } else { this.walker.hideNode(node).then(() => this.nodeChanged(node)); } break; case Ci.nsIDOMKeyEvent.DOM_VK_DELETE: case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE: this.deleteNode(this._selectedContainer.node); break; case Ci.nsIDOMKeyEvent.DOM_VK_HOME: let rootContainer = this._containers.get(this._rootNode); this.navigate(rootContainer.children.firstChild.container); break; case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: if (this._selectedContainer.expanded) { this.collapseNode(this._selectedContainer.node); } else { let parent = this._selectionWalker().parentNode(); if (parent) { this.navigate(parent.container); } } break; case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: if (!this._selectedContainer.expanded && this._selectedContainer.hasChildren) { this._expandContainer(this._selectedContainer); } else { let next = this._selectionWalker().nextNode(); if (next) { this.navigate(next.container); } } break; case Ci.nsIDOMKeyEvent.DOM_VK_UP: let prev = this._selectionWalker().previousNode(); if (prev) { this.navigate(prev.container); } break; case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: let next = this._selectionWalker().nextNode(); if (next) { this.navigate(next.container); } break; case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: { let walker = this._selectionWalker(); let selection = this._selectedContainer; for (let i = 0; i < PAGE_SIZE; i++) { let prev = walker.previousNode(); if (!prev) { break; } selection = prev.container; } this.navigate(selection); break; } case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: { let walker = this._selectionWalker(); let selection = this._selectedContainer; for (let i = 0; i < PAGE_SIZE; i++) { let next = walker.nextNode(); if (!next) { break; } selection = next.container; } this.navigate(selection); break; } default: handled = false; } if (handled) { aEvent.stopPropagation(); aEvent.preventDefault(); } }, /** * Delete a node from the DOM. * This is an undoable action. */ deleteNode: function(aNode) { if (aNode.isDocumentElement || aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) { return; } let container = this._containers.get(aNode); // Retain the node so we can undo this... this.walker.retainNode(aNode).then(() => { let parent = aNode.parentNode(); let sibling = null; this.undo.do(() => { if (container.selected) { this.navigate(this._containers.get(parent)); } this.walker.removeNode(aNode).then(nextSibling => { sibling = nextSibling; }); }, () => { this.walker.insertBefore(aNode, parent, sibling); }); }).then(null, console.error); }, /** * If an editable item is focused, select its container. */ _onFocus: function(aEvent) { let parent = aEvent.target; while (!parent.container) { parent = parent.parentNode; } if (parent) { this.navigate(parent.container, true); } }, /** * Handle a user-requested navigation to a given MarkupContainer, * updating the inspector's currently-selected node. * * @param MarkupContainer aContainer * The container we're navigating to. * @param aIgnoreFocus aIgnoreFocus * If falsy, keyboard focus will be moved to the container too. */ navigate: function(aContainer, aIgnoreFocus) { if (!aContainer) { return; } let node = aContainer.node; this.markNodeAsSelected(node, "treepanel"); // This event won't be fired if the node is the same. But the highlighter // need to lock the node if it wasn't. this._inspector.selection.emit("new-node"); this._inspector.selection.emit("new-node-front"); if (!aIgnoreFocus) { aContainer.focus(); } }, /** * Make sure a node is included in the markup tool. * * @param DOMNode aNode * The node in the content document. * @param boolean aFlashNode * Whether the newly imported node should be flashed * @returns MarkupContainer The MarkupContainer object for this element. */ importNode: function(aNode, aFlashNode) { if (!aNode) { return null; } if (this._containers.has(aNode)) { return this._containers.get(aNode); } if (aNode === this.walker.rootNode) { var container = new RootContainer(this, aNode); this._elt.appendChild(container.elt); this._rootNode = aNode; } else { var container = new MarkupContainer(this, aNode, this._inspector); if (aFlashNode) { container.flashMutation(); } } this._containers.set(aNode, container); container.childrenDirty = true; this._updateChildren(container); return container; }, /** * Mutation observer used for included nodes. */ _mutationObserver: function(aMutations) { let requiresLayoutChange = false; let reselectParent; let reselectChildIndex; for (let mutation of aMutations) { let type = mutation.type; let target = mutation.target; if (mutation.type === "documentUnload") { // Treat this as a childList change of the child (maybe the protocol // should do this). type = "childList"; target = mutation.targetParent; if (!target) { continue; } } let container = this._containers.get(target); if (!container) { // Container might not exist if this came from a load event for a node // we're not viewing. continue; } if (type === "attributes" || type === "characterData") { container.update(); // Auto refresh style properties on selected node when they change. if (type === "attributes" && container.selected) { requiresLayoutChange = true; } } else if (type === "childList") { let isFromOuterHTML = mutation.removed.some((n) => { return n === this._outerHTMLNode; }); // Keep track of which node should be reselected after mutations. if (isFromOuterHTML) { reselectParent = target; reselectChildIndex = this._outerHTMLChildIndex; delete this._outerHTMLNode; delete this._outerHTMLChildIndex; } container.childrenDirty = true; // Update the children to take care of changes in the markup view DOM. this._updateChildren(container, {flash: !isFromOuterHTML}); } } if (requiresLayoutChange) { this._inspector.immediateLayoutChange(); } this._waitForChildren().then((nodes) => { this._flashMutatedNodes(aMutations); this._inspector.emit("markupmutation", aMutations); // Since the htmlEditor is absolutely positioned, a mutation may change // the location in which it should be shown. this.htmlEditor.refresh(); // If a node has had its outerHTML set, the parent node will be selected. // Reselect the original node immediately. if (this._inspector.selection.nodeFront === reselectParent) { this.walker.children(reselectParent).then((o) => { let node = o.nodes[reselectChildIndex]; let container = this._containers.get(node); if (node && container) { this.markNodeAsSelected(node, "outerhtml"); if (container.hasChildren) { this.expandNode(node); } } }); } }); }, /** * Given a list of mutations returned by the mutation observer, flash the * corresponding containers to attract attention. */ _flashMutatedNodes: function(aMutations) { let addedOrEditedContainers = new Set(); let removedContainers = new Set(); for (let {type, target, added, removed} of aMutations) { let container = this._containers.get(target); if (container) { if (type === "attributes" || type === "characterData") { addedOrEditedContainers.add(container); } else if (type === "childList") { // If there has been removals, flash the parent if (removed.length) { removedContainers.add(container); } // If there has been additions, flash the nodes added.forEach(added => { let addedContainer = this._containers.get(added); addedOrEditedContainers.add(addedContainer); // The node may be added as a result of an append, in which case it // it will have been removed from another container first, but in // these cases we don't want to flash both the removal and the // addition removedContainers.delete(container); }); } } } for (let container of removedContainers) { container.flashMutation(); } for (let container of addedOrEditedContainers) { container.flashMutation(); } }, /** * Make sure the given node's parents are expanded and the * node is scrolled on to screen. */ showNode: function(aNode, centered) { let container = this.importNode(aNode); let parent = aNode; while ((parent = parent.parentNode())) { this.importNode(parent); this.expandNode(parent); } return this._waitForChildren().then(() => { return this._ensureVisible(aNode); }).then(() => { // Why is this not working? this.layoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered); }); }, /** * Expand the container's children. */ _expandContainer: function(aContainer) { return this._updateChildren(aContainer, {expand: true}).then(() => { aContainer.expanded = true; }); }, /** * Expand the node's children. */ expandNode: function(aNode) { let container = this._containers.get(aNode); this._expandContainer(container); }, /** * Expand the entire tree beneath a container. * * @param aContainer The container to expand. */ _expandAll: function(aContainer) { return this._expandContainer(aContainer).then(() => { let child = aContainer.children.firstChild; let promises = []; while (child) { promises.push(this._expandAll(child.container)); child = child.nextSibling; } return promise.all(promises); }).then(null, console.error); }, /** * Expand the entire tree beneath a node. * * @param aContainer The node to expand, or null * to start from the top. */ expandAll: function(aNode) { aNode = aNode || this._rootNode; return this._expandAll(this._containers.get(aNode)); }, /** * Collapse the node's children. */ collapseNode: function(aNode) { let container = this._containers.get(aNode); container.expanded = false; }, /** * Retrieve the outerHTML for a remote node. * @param aNode The NodeFront to get the outerHTML for. * @returns A promise that will be resolved with the outerHTML. */ getNodeOuterHTML: function(aNode) { let def = promise.defer(); this.walker.outerHTML(aNode).then(longstr => { longstr.string().then(outerHTML => { longstr.release().then(null, console.error); def.resolve(outerHTML); }); }); return def.promise; }, /** * Retrieve the index of a child within its parent's children list. * @param aNode The NodeFront to find the index of. * @returns A promise that will be resolved with the integer index. * If the child cannot be found, returns -1 */ getNodeChildIndex: function(aNode) { let def = promise.defer(); let parentNode = aNode.parentNode(); // Node may have been removed from the DOM, instead of throwing an error, // return -1 indicating that it isn't inside of its parent children list. if (!parentNode) { def.resolve(-1); } else { this.walker.children(parentNode).then(children => { def.resolve(children.nodes.indexOf(aNode)); }); } return def.promise; }, /** * Retrieve the index of a child within its parent's children collection. * @param aNode The NodeFront to find the index of. * @param newValue The new outerHTML to set on the node. * @param oldValue The old outerHTML that will be reverted to find the index of. * @returns A promise that will be resolved with the integer index. * If the child cannot be found, returns -1 */ updateNodeOuterHTML: function(aNode, newValue, oldValue) { let container = this._containers.get(aNode); if (!container) { return; } this.getNodeChildIndex(aNode).then((i) => { this._outerHTMLChildIndex = i; this._outerHTMLNode = aNode; container.undo.do(() => { this.walker.setOuterHTML(aNode, newValue); }, () => { this.walker.setOuterHTML(aNode, oldValue); }); }); }, /** * Open an editor in the UI to allow editing of a node's outerHTML. * @param aNode The NodeFront to edit. */ beginEditingOuterHTML: function(aNode) { this.getNodeOuterHTML(aNode).then((oldValue)=> { let container = this._containers.get(aNode); if (!container) { return; } this.htmlEditor.show(container.tagLine, oldValue); this.htmlEditor.once("popup-hidden", (e, aCommit, aValue) => { if (aCommit) { this.updateNodeOuterHTML(aNode, aValue, oldValue); } }); }); }, /** * Mark the given node expanded. * @param aNode The NodeFront to mark as expanded. */ setNodeExpanded: function(aNode, aExpanded) { if (aExpanded) { this.expandNode(aNode); } else { this.collapseNode(aNode); } }, /** * Mark the given node selected, and update the inspector.selection * object's NodeFront to keep consistent state between UI and selection. * @param aNode The NodeFront to mark as selected. */ markNodeAsSelected: function(aNode, reason) { let container = this._containers.get(aNode); if (this._selectedContainer === container) { return false; } if (this._selectedContainer) { this._selectedContainer.selected = false; } this._selectedContainer = container; if (aNode) { this._selectedContainer.selected = true; } this._inspector.selection.setNodeFront(aNode, reason || "nodeselected"); return true; }, /** * Make sure that every ancestor of the selection are updated * and included in the list of visible children. */ _ensureVisible: function(node) { while (node) { let container = this._containers.get(node); let parent = node.parentNode(); if (!container.elt.parentNode) { let parentContainer = this._containers.get(parent); parentContainer.childrenDirty = true; this._updateChildren(parentContainer, {expand: node}); } node = parent; } return this._waitForChildren(); }, /** * Unmark selected node (no node selected). */ unmarkSelectedNode: function() { if (this._selectedContainer) { this._selectedContainer.selected = false; this._selectedContainer = null; } }, /** * Called when the markup panel initiates a change on a node. */ nodeChanged: function(aNode) { if (aNode === this._inspector.selection.nodeFront) { this._inspector.change("markupview"); } }, /** * Check if the current selection is a descendent of the container. * if so, make sure it's among the visible set for the container, * and set the dirty flag if needed. * @returns The node that should be made visible, if any. */ _checkSelectionVisible: function(aContainer) { let centered = null; let node = this._inspector.selection.nodeFront; while (node) { if (node.parentNode() === aContainer.node) { centered = node; break; } node = node.parentNode(); } return centered; }, /** * Make sure all children of the given container's node are * imported and attached to the container in the right order. * * Children need to be updated only in the following circumstances: * a) We just imported this node and have never seen its children. * container.childrenDirty will be set by importNode in this case. * b) We received a childList mutation on the node. * container.childrenDirty will be set in that case too. * c) We have changed the selection, and the path to that selection * wasn't loaded in a previous children request (because we only * grab a subset). * container.childrenDirty should be set in that case too! * * @param MarkupContainer aContainer * The markup container whose children need updating * @param Object options * Options are {expand:boolean,flash:boolean} * @return a promise that will be resolved when the children are ready * (which may be immediately). */ _updateChildren: function(aContainer, options) { let expand = options && options.expand; let flash = options && options.flash; aContainer.hasChildren = aContainer.node.hasChildren; if (!this._queuedChildUpdates) { this._queuedChildUpdates = new Map(); } if (this._queuedChildUpdates.has(aContainer)) { return this._queuedChildUpdates.get(aContainer); } if (!aContainer.childrenDirty) { return promise.resolve(aContainer); } if (!aContainer.hasChildren) { while (aContainer.children.firstChild) { aContainer.children.removeChild(aContainer.children.firstChild); } aContainer.childrenDirty = false; return promise.resolve(aContainer); } // If we're not expanded (or asked to update anyway), we're done for // now. Note that this will leave the childrenDirty flag set, so when // expanded we'll refresh the child list. if (!(aContainer.expanded || expand)) { return promise.resolve(aContainer); } // We're going to issue a children request, make sure it includes the // centered node. let centered = this._checkSelectionVisible(aContainer); // Children aren't updated yet, but clear the childrenDirty flag anyway. // If the dirty flag is re-set while we're fetching we'll need to fetch // again. aContainer.childrenDirty = false; let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => { if (!this._containers) { return promise.reject("markup view destroyed"); } this._queuedChildUpdates.delete(aContainer); // If children are dirty, we got a change notification for this node // while the request was in progress, we need to do it again. if (aContainer.childrenDirty) { return this._updateChildren(aContainer, {expand: centered}); } let fragment = this.doc.createDocumentFragment(); for (let child of children.nodes) { let container = this.importNode(child, flash); fragment.appendChild(container.elt); } while (aContainer.children.firstChild) { aContainer.children.removeChild(aContainer.children.firstChild); } if (!(children.hasFirst && children.hasLast)) { let data = { showing: this.strings.GetStringFromName("markupView.more.showing"), showAll: this.strings.formatStringFromName( "markupView.more.showAll", [aContainer.node.numChildren.toString()], 1), allButtonClick: () => { aContainer.maxChildren = -1; aContainer.childrenDirty = true; this._updateChildren(aContainer); } }; if (!children.hasFirst) { let span = this.template("more-nodes", data); fragment.insertBefore(span, fragment.firstChild); } if (!children.hasLast) { let span = this.template("more-nodes", data); fragment.appendChild(span); } } aContainer.children.appendChild(fragment); return aContainer; }).then(null, console.error); this._queuedChildUpdates.set(aContainer, updatePromise); return updatePromise; }, _waitForChildren: function() { if (!this._queuedChildUpdates) { return promise.resolve(undefined); } return promise.all([updatePromise for (updatePromise of this._queuedChildUpdates.values())]); }, /** * Return a list of the children to display for this container. */ _getVisibleChildren: function(aContainer, aCentered) { let maxChildren = aContainer.maxChildren || this.maxChildren; if (maxChildren == -1) { maxChildren = undefined; } return this.walker.children(aContainer.node, { maxNodes: maxChildren, center: aCentered }); }, /** * Tear down the markup panel. */ destroy: function() { gDevTools.off("pref-changed", this._handlePrefChange); this.htmlEditor.destroy(); delete this.htmlEditor; this.undo.destroy(); delete this.undo; this.popup.destroy(); delete this.popup; this._frame.removeEventListener("focus", this._boundFocus, false); delete this._boundFocus; delete this._outputParser; if (this._boundUpdatePreview) { this._frame.contentWindow.removeEventListener("scroll", this._boundUpdatePreview, true); delete this._boundUpdatePreview; } if (this._boundResizePreview) { this._frame.contentWindow.removeEventListener("resize", this._boundResizePreview, true); this._frame.contentWindow.removeEventListener("overflow", this._boundResizePreview, true); this._frame.contentWindow.removeEventListener("underflow", this._boundResizePreview, true); delete this._boundResizePreview; } this._frame.contentWindow.removeEventListener("keydown", this._boundKeyDown, false); delete this._boundKeyDown; this._inspector.selection.off("new-node-front", this._boundOnNewSelection); delete this._boundOnNewSelection; this.walker.off("mutations", this._boundMutationObserver) delete this._boundMutationObserver; delete this._elt; for (let [key, container] of this._containers) { container.destroy(); } delete this._containers; }, /** * Initialize the preview panel. */ _initPreview: function() { if (!Services.prefs.getBoolPref("devtools.inspector.markupPreview")) { return; } this._previewBar = this.doc.querySelector("#previewbar"); this._preview = this.doc.querySelector("#preview"); this._viewbox = this.doc.querySelector("#viewbox"); this._previewBar.classList.remove("disabled"); this._previewWidth = this._preview.getBoundingClientRect().width; this._boundResizePreview = this._resizePreview.bind(this); this._frame.contentWindow.addEventListener("resize", this._boundResizePreview, true); this._frame.contentWindow.addEventListener("overflow", this._boundResizePreview, true); this._frame.contentWindow.addEventListener("underflow", this._boundResizePreview, true); this._boundUpdatePreview = this._updatePreview.bind(this); this._frame.contentWindow.addEventListener("scroll", this._boundUpdatePreview, true); this._updatePreview(); }, /** * Move the preview viewbox. */ _updatePreview: function() { let win = this._frame.contentWindow; if (win.scrollMaxY == 0) { this._previewBar.classList.add("disabled"); return; } this._previewBar.classList.remove("disabled"); let ratio = this._previewWidth / PREVIEW_AREA; let width = ratio * win.innerWidth; let height = ratio * (win.scrollMaxY + win.innerHeight); let scrollTo if (height >= win.innerHeight) { scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY); this._previewBar.setAttribute("style", "height:" + height + "px;transform:translateY(" + scrollTo + "px)"); } else { this._previewBar.setAttribute("style", "height:100%"); } let bgSize = ~~width + "px " + ~~height + "px"; this._preview.setAttribute("style", "background-size:" + bgSize); let height = ~~(win.innerHeight * ratio) + "px"; let top = ~~(win.scrollY * ratio) + "px"; this._viewbox.setAttribute("style", "height:" + height + ";transform: translateY(" + top + ")"); }, /** * Hide the preview while resizing, to avoid slowness. */ _resizePreview: function() { let win = this._frame.contentWindow; this._previewBar.classList.add("hide"); win.clearTimeout(this._resizePreviewTimeout); win.setTimeout(function() { this._updatePreview(); this._previewBar.classList.remove("hide"); }.bind(this), 1000); } }; /** * The main structure for storing a document node in the markup * tree. Manages creation of the editor for the node and * a