1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/SplitView.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,302 @@ 1.4 +/* vim:set ts=2 sw=2 sts=2 et: */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +this.EXPORTED_SYMBOLS = ["SplitView"]; 1.12 + 1.13 +/* this must be kept in sync with CSS (ie. splitview.css) */ 1.14 +const LANDSCAPE_MEDIA_QUERY = "(min-width: 551px)"; 1.15 + 1.16 +let bindings = new WeakMap(); 1.17 + 1.18 +/** 1.19 + * SplitView constructor 1.20 + * 1.21 + * Initialize the split view UI on an existing DOM element. 1.22 + * 1.23 + * A split view contains items, each of those having one summary and one details 1.24 + * elements. 1.25 + * It is adaptive as it behaves similarly to a richlistbox when there the aspect 1.26 + * ratio is narrow or as a pair listbox-box otherwise. 1.27 + * 1.28 + * @param DOMElement aRoot 1.29 + * @see appendItem 1.30 + */ 1.31 +this.SplitView = function SplitView(aRoot) 1.32 +{ 1.33 + this._root = aRoot; 1.34 + this._controller = aRoot.querySelector(".splitview-controller"); 1.35 + this._nav = aRoot.querySelector(".splitview-nav"); 1.36 + this._side = aRoot.querySelector(".splitview-side-details"); 1.37 + this._activeSummary = null 1.38 + 1.39 + this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY); 1.40 + 1.41 + // items list focus and search-on-type handling 1.42 + this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) { 1.43 + function getFocusedItemWithin(nav) { 1.44 + let node = nav.ownerDocument.activeElement; 1.45 + while (node && node.parentNode != nav) { 1.46 + node = node.parentNode; 1.47 + } 1.48 + return node; 1.49 + } 1.50 + 1.51 + // do not steal focus from inside iframes or textboxes 1.52 + if (aEvent.target.ownerDocument != this._nav.ownerDocument || 1.53 + aEvent.target.tagName == "input" || 1.54 + aEvent.target.tagName == "textbox" || 1.55 + aEvent.target.tagName == "textarea" || 1.56 + aEvent.target.classList.contains("textbox")) { 1.57 + return false; 1.58 + } 1.59 + 1.60 + // handle keyboard navigation within the items list 1.61 + let newFocusOrdinal; 1.62 + if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP || 1.63 + aEvent.keyCode == aEvent.DOM_VK_HOME) { 1.64 + newFocusOrdinal = 0; 1.65 + } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN || 1.66 + aEvent.keyCode == aEvent.DOM_VK_END) { 1.67 + newFocusOrdinal = this._nav.childNodes.length - 1; 1.68 + } else if (aEvent.keyCode == aEvent.DOM_VK_UP) { 1.69 + newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal"); 1.70 + newFocusOrdinal--; 1.71 + } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) { 1.72 + newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal"); 1.73 + newFocusOrdinal++; 1.74 + } 1.75 + if (newFocusOrdinal !== undefined) { 1.76 + aEvent.stopPropagation(); 1.77 + let el = this.getSummaryElementByOrdinal(newFocusOrdinal); 1.78 + if (el) { 1.79 + el.focus(); 1.80 + } 1.81 + return false; 1.82 + } 1.83 + }.bind(this), false); 1.84 +} 1.85 + 1.86 +SplitView.prototype = { 1.87 + /** 1.88 + * Retrieve whether the UI currently has a landscape orientation. 1.89 + * 1.90 + * @return boolean 1.91 + */ 1.92 + get isLandscape() this._mql.matches, 1.93 + 1.94 + /** 1.95 + * Retrieve the root element. 1.96 + * 1.97 + * @return DOMElement 1.98 + */ 1.99 + get rootElement() this._root, 1.100 + 1.101 + /** 1.102 + * Retrieve the active item's summary element or null if there is none. 1.103 + * 1.104 + * @return DOMElement 1.105 + */ 1.106 + get activeSummary() this._activeSummary, 1.107 + 1.108 + /** 1.109 + * Set the active item's summary element. 1.110 + * 1.111 + * @param DOMElement aSummary 1.112 + */ 1.113 + set activeSummary(aSummary) 1.114 + { 1.115 + if (aSummary == this._activeSummary) { 1.116 + return; 1.117 + } 1.118 + 1.119 + if (this._activeSummary) { 1.120 + let binding = bindings.get(this._activeSummary); 1.121 + 1.122 + if (binding.onHide) { 1.123 + binding.onHide(this._activeSummary, binding._details, binding.data); 1.124 + } 1.125 + 1.126 + this._activeSummary.classList.remove("splitview-active"); 1.127 + binding._details.classList.remove("splitview-active"); 1.128 + } 1.129 + 1.130 + if (!aSummary) { 1.131 + return; 1.132 + } 1.133 + 1.134 + let binding = bindings.get(aSummary); 1.135 + aSummary.classList.add("splitview-active"); 1.136 + binding._details.classList.add("splitview-active"); 1.137 + 1.138 + this._activeSummary = aSummary; 1.139 + 1.140 + if (binding.onShow) { 1.141 + binding.onShow(aSummary, binding._details, binding.data); 1.142 + } 1.143 + }, 1.144 + 1.145 + /** 1.146 + * Retrieve the active item's details element or null if there is none. 1.147 + * @return DOMElement 1.148 + */ 1.149 + get activeDetails() 1.150 + { 1.151 + let summary = this.activeSummary; 1.152 + return summary ? bindings.get(summary)._details : null; 1.153 + }, 1.154 + 1.155 + /** 1.156 + * Retrieve the summary element for a given ordinal. 1.157 + * 1.158 + * @param number aOrdinal 1.159 + * @return DOMElement 1.160 + * Summary element with given ordinal or null if not found. 1.161 + * @see appendItem 1.162 + */ 1.163 + getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal) 1.164 + { 1.165 + return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']"); 1.166 + }, 1.167 + 1.168 + /** 1.169 + * Append an item to the split view. 1.170 + * 1.171 + * @param DOMElement aSummary 1.172 + * The summary element for the item. 1.173 + * @param DOMElement aDetails 1.174 + * The details element for the item. 1.175 + * @param object aOptions 1.176 + * Optional object that defines custom behavior and data for the item. 1.177 + * All properties are optional : 1.178 + * - function(DOMElement summary, DOMElement details, object data) onCreate 1.179 + * Called when the item has been added. 1.180 + * - function(summary, details, data) onShow 1.181 + * Called when the item is shown/active. 1.182 + * - function(summary, details, data) onHide 1.183 + * Called when the item is hidden/inactive. 1.184 + * - function(summary, details, data) onDestroy 1.185 + * Called when the item has been removed. 1.186 + * - object data 1.187 + * Object to pass to the callbacks above. 1.188 + * - number ordinal 1.189 + * Items with a lower ordinal are displayed before those with a 1.190 + * higher ordinal. 1.191 + */ 1.192 + appendItem: function ASV_appendItem(aSummary, aDetails, aOptions) 1.193 + { 1.194 + let binding = aOptions || {}; 1.195 + 1.196 + binding._summary = aSummary; 1.197 + binding._details = aDetails; 1.198 + bindings.set(aSummary, binding); 1.199 + 1.200 + this._nav.appendChild(aSummary); 1.201 + 1.202 + aSummary.addEventListener("click", function onSummaryClick(aEvent) { 1.203 + aEvent.stopPropagation(); 1.204 + this.activeSummary = aSummary; 1.205 + }.bind(this), false); 1.206 + 1.207 + this._side.appendChild(aDetails); 1.208 + 1.209 + if (binding.onCreate) { 1.210 + // queue onCreate handler 1.211 + this._root.ownerDocument.defaultView.setTimeout(function () { 1.212 + binding.onCreate(aSummary, aDetails, binding.data); 1.213 + }, 0); 1.214 + } 1.215 + }, 1.216 + 1.217 + /** 1.218 + * Append an item to the split view according to two template elements 1.219 + * (one for the item's summary and the other for the item's details). 1.220 + * 1.221 + * @param string aName 1.222 + * Name of the template elements to instantiate. 1.223 + * Requires two (hidden) DOM elements with id "splitview-tpl-summary-" 1.224 + * and "splitview-tpl-details-" suffixed with aName. 1.225 + * @param object aOptions 1.226 + * Optional object that defines custom behavior and data for the item. 1.227 + * See appendItem for full description. 1.228 + * @return object{summary:,details:} 1.229 + * Object with the new DOM elements created for summary and details. 1.230 + * @see appendItem 1.231 + */ 1.232 + appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions) 1.233 + { 1.234 + aOptions = aOptions || {}; 1.235 + let summary = this._root.querySelector("#splitview-tpl-summary-" + aName); 1.236 + let details = this._root.querySelector("#splitview-tpl-details-" + aName); 1.237 + 1.238 + summary = summary.cloneNode(true); 1.239 + summary.id = ""; 1.240 + if (aOptions.ordinal !== undefined) { // can be zero 1.241 + summary.style.MozBoxOrdinalGroup = aOptions.ordinal; 1.242 + summary.setAttribute("data-ordinal", aOptions.ordinal); 1.243 + } 1.244 + details = details.cloneNode(true); 1.245 + details.id = ""; 1.246 + 1.247 + this.appendItem(summary, details, aOptions); 1.248 + return {summary: summary, details: details}; 1.249 + }, 1.250 + 1.251 + /** 1.252 + * Remove an item from the split view. 1.253 + * 1.254 + * @param DOMElement aSummary 1.255 + * Summary element of the item to remove. 1.256 + */ 1.257 + removeItem: function ASV_removeItem(aSummary) 1.258 + { 1.259 + if (aSummary == this._activeSummary) { 1.260 + this.activeSummary = null; 1.261 + } 1.262 + 1.263 + let binding = bindings.get(aSummary); 1.264 + aSummary.parentNode.removeChild(aSummary); 1.265 + binding._details.parentNode.removeChild(binding._details); 1.266 + 1.267 + if (binding.onDestroy) { 1.268 + binding.onDestroy(aSummary, binding._details, binding.data); 1.269 + } 1.270 + }, 1.271 + 1.272 + /** 1.273 + * Remove all items from the split view. 1.274 + */ 1.275 + removeAll: function ASV_removeAll() 1.276 + { 1.277 + while (this._nav.hasChildNodes()) { 1.278 + this.removeItem(this._nav.firstChild); 1.279 + } 1.280 + }, 1.281 + 1.282 + /** 1.283 + * Set the item's CSS class name. 1.284 + * This sets the class on both the summary and details elements, retaining 1.285 + * any SplitView-specific classes. 1.286 + * 1.287 + * @param DOMElement aSummary 1.288 + * Summary element of the item to set. 1.289 + * @param string aClassName 1.290 + * One or more space-separated CSS classes. 1.291 + */ 1.292 + setItemClassName: function ASV_setItemClassName(aSummary, aClassName) 1.293 + { 1.294 + let binding = bindings.get(aSummary); 1.295 + let viewSpecific; 1.296 + 1.297 + viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g); 1.298 + viewSpecific = viewSpecific ? viewSpecific.join(" ") : ""; 1.299 + aSummary.className = viewSpecific + " " + aClassName; 1.300 + 1.301 + viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g); 1.302 + viewSpecific = viewSpecific ? viewSpecific.join(" ") : ""; 1.303 + binding._details.className = viewSpecific + " " + aClassName; 1.304 + }, 1.305 +};