diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/shared/SplitView.jsm | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/shared/SplitView.jsm')
-rw-r--r-- | devtools/client/shared/SplitView.jsm | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/devtools/client/shared/SplitView.jsm b/devtools/client/shared/SplitView.jsm new file mode 100644 index 000000000..f72aad2ac --- /dev/null +++ b/devtools/client/shared/SplitView.jsm @@ -0,0 +1,312 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +const Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +this.EXPORTED_SYMBOLS = ["SplitView"]; + +/* this must be kept in sync with CSS (ie. splitview.css) */ +const LANDSCAPE_MEDIA_QUERY = "(min-width: 701px)"; + +var bindings = new WeakMap(); + +/** + * SplitView constructor + * + * Initialize the split view UI on an existing DOM element. + * + * A split view contains items, each of those having one summary and one details + * elements. + * It is adaptive as it behaves similarly to a richlistbox when there the aspect + * ratio is narrow or as a pair listbox-box otherwise. + * + * @param DOMElement aRoot + * @see appendItem + */ +this.SplitView = function SplitView(aRoot) +{ + this._root = aRoot; + this._controller = aRoot.querySelector(".splitview-controller"); + this._nav = aRoot.querySelector(".splitview-nav"); + this._side = aRoot.querySelector(".splitview-side-details"); + this._activeSummary = null; + + this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY); + + // items list focus and search-on-type handling + this._nav.addEventListener("keydown", (aEvent) => { + function getFocusedItemWithin(nav) { + let node = nav.ownerDocument.activeElement; + while (node && node.parentNode != nav) { + node = node.parentNode; + } + return node; + } + + // do not steal focus from inside iframes or textboxes + if (aEvent.target.ownerDocument != this._nav.ownerDocument || + aEvent.target.tagName == "input" || + aEvent.target.tagName == "textbox" || + aEvent.target.tagName == "textarea" || + aEvent.target.classList.contains("textbox")) { + return false; + } + + // handle keyboard navigation within the items list + let newFocusOrdinal; + if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_UP || + aEvent.keyCode == KeyCodes.DOM_VK_HOME) { + newFocusOrdinal = 0; + } else if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_DOWN || + aEvent.keyCode == KeyCodes.DOM_VK_END) { + newFocusOrdinal = this._nav.childNodes.length - 1; + } else if (aEvent.keyCode == KeyCodes.DOM_VK_UP) { + newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal"); + newFocusOrdinal--; + } else if (aEvent.keyCode == KeyCodes.DOM_VK_DOWN) { + newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal"); + newFocusOrdinal++; + } + if (newFocusOrdinal !== undefined) { + aEvent.stopPropagation(); + let el = this.getSummaryElementByOrdinal(newFocusOrdinal); + if (el) { + el.focus(); + } + return false; + } + }, false); +}; + +SplitView.prototype = { + /** + * Retrieve whether the UI currently has a landscape orientation. + * + * @return boolean + */ + get isLandscape() + { + return this._mql.matches; + }, + + /** + * Retrieve the root element. + * + * @return DOMElement + */ + get rootElement() + { + return this._root; + }, + + /** + * Retrieve the active item's summary element or null if there is none. + * + * @return DOMElement + */ + get activeSummary() + { + return this._activeSummary; + }, + + /** + * Set the active item's summary element. + * + * @param DOMElement aSummary + */ + set activeSummary(aSummary) + { + if (aSummary == this._activeSummary) { + return; + } + + if (this._activeSummary) { + let binding = bindings.get(this._activeSummary); + + if (binding.onHide) { + binding.onHide(this._activeSummary, binding._details, binding.data); + } + + this._activeSummary.classList.remove("splitview-active"); + binding._details.classList.remove("splitview-active"); + } + + if (!aSummary) { + return; + } + + let binding = bindings.get(aSummary); + aSummary.classList.add("splitview-active"); + binding._details.classList.add("splitview-active"); + + this._activeSummary = aSummary; + + if (binding.onShow) { + binding.onShow(aSummary, binding._details, binding.data); + } + }, + + /** + * Retrieve the active item's details element or null if there is none. + * @return DOMElement + */ + get activeDetails() + { + let summary = this.activeSummary; + return summary ? bindings.get(summary)._details : null; + }, + + /** + * Retrieve the summary element for a given ordinal. + * + * @param number aOrdinal + * @return DOMElement + * Summary element with given ordinal or null if not found. + * @see appendItem + */ + getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal) + { + return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']"); + }, + + /** + * Append an item to the split view. + * + * @param DOMElement aSummary + * The summary element for the item. + * @param DOMElement aDetails + * The details element for the item. + * @param object aOptions + * Optional object that defines custom behavior and data for the item. + * All properties are optional : + * - function(DOMElement summary, DOMElement details, object data) onCreate + * Called when the item has been added. + * - function(summary, details, data) onShow + * Called when the item is shown/active. + * - function(summary, details, data) onHide + * Called when the item is hidden/inactive. + * - function(summary, details, data) onDestroy + * Called when the item has been removed. + * - object data + * Object to pass to the callbacks above. + * - number ordinal + * Items with a lower ordinal are displayed before those with a + * higher ordinal. + */ + appendItem: function ASV_appendItem(aSummary, aDetails, aOptions) + { + let binding = aOptions || {}; + + binding._summary = aSummary; + binding._details = aDetails; + bindings.set(aSummary, binding); + + this._nav.appendChild(aSummary); + + aSummary.addEventListener("click", (aEvent) => { + aEvent.stopPropagation(); + this.activeSummary = aSummary; + }, false); + + this._side.appendChild(aDetails); + + if (binding.onCreate) { + binding.onCreate(aSummary, aDetails, binding.data); + } + }, + + /** + * Append an item to the split view according to two template elements + * (one for the item's summary and the other for the item's details). + * + * @param string aName + * Name of the template elements to instantiate. + * Requires two (hidden) DOM elements with id "splitview-tpl-summary-" + * and "splitview-tpl-details-" suffixed with aName. + * @param object aOptions + * Optional object that defines custom behavior and data for the item. + * See appendItem for full description. + * @return object{summary:,details:} + * Object with the new DOM elements created for summary and details. + * @see appendItem + */ + appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions) + { + aOptions = aOptions || {}; + let summary = this._root.querySelector("#splitview-tpl-summary-" + aName); + let details = this._root.querySelector("#splitview-tpl-details-" + aName); + + summary = summary.cloneNode(true); + summary.id = ""; + if (aOptions.ordinal !== undefined) { // can be zero + summary.style.MozBoxOrdinalGroup = aOptions.ordinal; + summary.setAttribute("data-ordinal", aOptions.ordinal); + } + details = details.cloneNode(true); + details.id = ""; + + this.appendItem(summary, details, aOptions); + return {summary: summary, details: details}; + }, + + /** + * Remove an item from the split view. + * + * @param DOMElement aSummary + * Summary element of the item to remove. + */ + removeItem: function ASV_removeItem(aSummary) + { + if (aSummary == this._activeSummary) { + this.activeSummary = null; + } + + let binding = bindings.get(aSummary); + aSummary.parentNode.removeChild(aSummary); + binding._details.parentNode.removeChild(binding._details); + + if (binding.onDestroy) { + binding.onDestroy(aSummary, binding._details, binding.data); + } + }, + + /** + * Remove all items from the split view. + */ + removeAll: function ASV_removeAll() + { + while (this._nav.hasChildNodes()) { + this.removeItem(this._nav.firstChild); + } + }, + + /** + * Set the item's CSS class name. + * This sets the class on both the summary and details elements, retaining + * any SplitView-specific classes. + * + * @param DOMElement aSummary + * Summary element of the item to set. + * @param string aClassName + * One or more space-separated CSS classes. + */ + setItemClassName: function ASV_setItemClassName(aSummary, aClassName) + { + let binding = bindings.get(aSummary); + let viewSpecific; + + viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g); + viewSpecific = viewSpecific ? viewSpecific.join(" ") : ""; + aSummary.className = viewSpecific + " " + aClassName; + + viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g); + viewSpecific = viewSpecific ? viewSpecific.join(" ") : ""; + binding._details.className = viewSpecific + " " + aClassName; + }, +}; |