Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* vim:set ts=2 sw=2 sts=2 et: */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 this.EXPORTED_SYMBOLS = ["SplitView"];
10 /* this must be kept in sync with CSS (ie. splitview.css) */
11 const LANDSCAPE_MEDIA_QUERY = "(min-width: 551px)";
13 let bindings = new WeakMap();
15 /**
16 * SplitView constructor
17 *
18 * Initialize the split view UI on an existing DOM element.
19 *
20 * A split view contains items, each of those having one summary and one details
21 * elements.
22 * It is adaptive as it behaves similarly to a richlistbox when there the aspect
23 * ratio is narrow or as a pair listbox-box otherwise.
24 *
25 * @param DOMElement aRoot
26 * @see appendItem
27 */
28 this.SplitView = function SplitView(aRoot)
29 {
30 this._root = aRoot;
31 this._controller = aRoot.querySelector(".splitview-controller");
32 this._nav = aRoot.querySelector(".splitview-nav");
33 this._side = aRoot.querySelector(".splitview-side-details");
34 this._activeSummary = null
36 this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
38 // items list focus and search-on-type handling
39 this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) {
40 function getFocusedItemWithin(nav) {
41 let node = nav.ownerDocument.activeElement;
42 while (node && node.parentNode != nav) {
43 node = node.parentNode;
44 }
45 return node;
46 }
48 // do not steal focus from inside iframes or textboxes
49 if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
50 aEvent.target.tagName == "input" ||
51 aEvent.target.tagName == "textbox" ||
52 aEvent.target.tagName == "textarea" ||
53 aEvent.target.classList.contains("textbox")) {
54 return false;
55 }
57 // handle keyboard navigation within the items list
58 let newFocusOrdinal;
59 if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP ||
60 aEvent.keyCode == aEvent.DOM_VK_HOME) {
61 newFocusOrdinal = 0;
62 } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
63 aEvent.keyCode == aEvent.DOM_VK_END) {
64 newFocusOrdinal = this._nav.childNodes.length - 1;
65 } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
66 newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
67 newFocusOrdinal--;
68 } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
69 newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
70 newFocusOrdinal++;
71 }
72 if (newFocusOrdinal !== undefined) {
73 aEvent.stopPropagation();
74 let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
75 if (el) {
76 el.focus();
77 }
78 return false;
79 }
80 }.bind(this), false);
81 }
83 SplitView.prototype = {
84 /**
85 * Retrieve whether the UI currently has a landscape orientation.
86 *
87 * @return boolean
88 */
89 get isLandscape() this._mql.matches,
91 /**
92 * Retrieve the root element.
93 *
94 * @return DOMElement
95 */
96 get rootElement() this._root,
98 /**
99 * Retrieve the active item's summary element or null if there is none.
100 *
101 * @return DOMElement
102 */
103 get activeSummary() this._activeSummary,
105 /**
106 * Set the active item's summary element.
107 *
108 * @param DOMElement aSummary
109 */
110 set activeSummary(aSummary)
111 {
112 if (aSummary == this._activeSummary) {
113 return;
114 }
116 if (this._activeSummary) {
117 let binding = bindings.get(this._activeSummary);
119 if (binding.onHide) {
120 binding.onHide(this._activeSummary, binding._details, binding.data);
121 }
123 this._activeSummary.classList.remove("splitview-active");
124 binding._details.classList.remove("splitview-active");
125 }
127 if (!aSummary) {
128 return;
129 }
131 let binding = bindings.get(aSummary);
132 aSummary.classList.add("splitview-active");
133 binding._details.classList.add("splitview-active");
135 this._activeSummary = aSummary;
137 if (binding.onShow) {
138 binding.onShow(aSummary, binding._details, binding.data);
139 }
140 },
142 /**
143 * Retrieve the active item's details element or null if there is none.
144 * @return DOMElement
145 */
146 get activeDetails()
147 {
148 let summary = this.activeSummary;
149 return summary ? bindings.get(summary)._details : null;
150 },
152 /**
153 * Retrieve the summary element for a given ordinal.
154 *
155 * @param number aOrdinal
156 * @return DOMElement
157 * Summary element with given ordinal or null if not found.
158 * @see appendItem
159 */
160 getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
161 {
162 return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
163 },
165 /**
166 * Append an item to the split view.
167 *
168 * @param DOMElement aSummary
169 * The summary element for the item.
170 * @param DOMElement aDetails
171 * The details element for the item.
172 * @param object aOptions
173 * Optional object that defines custom behavior and data for the item.
174 * All properties are optional :
175 * - function(DOMElement summary, DOMElement details, object data) onCreate
176 * Called when the item has been added.
177 * - function(summary, details, data) onShow
178 * Called when the item is shown/active.
179 * - function(summary, details, data) onHide
180 * Called when the item is hidden/inactive.
181 * - function(summary, details, data) onDestroy
182 * Called when the item has been removed.
183 * - object data
184 * Object to pass to the callbacks above.
185 * - number ordinal
186 * Items with a lower ordinal are displayed before those with a
187 * higher ordinal.
188 */
189 appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
190 {
191 let binding = aOptions || {};
193 binding._summary = aSummary;
194 binding._details = aDetails;
195 bindings.set(aSummary, binding);
197 this._nav.appendChild(aSummary);
199 aSummary.addEventListener("click", function onSummaryClick(aEvent) {
200 aEvent.stopPropagation();
201 this.activeSummary = aSummary;
202 }.bind(this), false);
204 this._side.appendChild(aDetails);
206 if (binding.onCreate) {
207 // queue onCreate handler
208 this._root.ownerDocument.defaultView.setTimeout(function () {
209 binding.onCreate(aSummary, aDetails, binding.data);
210 }, 0);
211 }
212 },
214 /**
215 * Append an item to the split view according to two template elements
216 * (one for the item's summary and the other for the item's details).
217 *
218 * @param string aName
219 * Name of the template elements to instantiate.
220 * Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
221 * and "splitview-tpl-details-" suffixed with aName.
222 * @param object aOptions
223 * Optional object that defines custom behavior and data for the item.
224 * See appendItem for full description.
225 * @return object{summary:,details:}
226 * Object with the new DOM elements created for summary and details.
227 * @see appendItem
228 */
229 appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
230 {
231 aOptions = aOptions || {};
232 let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
233 let details = this._root.querySelector("#splitview-tpl-details-" + aName);
235 summary = summary.cloneNode(true);
236 summary.id = "";
237 if (aOptions.ordinal !== undefined) { // can be zero
238 summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
239 summary.setAttribute("data-ordinal", aOptions.ordinal);
240 }
241 details = details.cloneNode(true);
242 details.id = "";
244 this.appendItem(summary, details, aOptions);
245 return {summary: summary, details: details};
246 },
248 /**
249 * Remove an item from the split view.
250 *
251 * @param DOMElement aSummary
252 * Summary element of the item to remove.
253 */
254 removeItem: function ASV_removeItem(aSummary)
255 {
256 if (aSummary == this._activeSummary) {
257 this.activeSummary = null;
258 }
260 let binding = bindings.get(aSummary);
261 aSummary.parentNode.removeChild(aSummary);
262 binding._details.parentNode.removeChild(binding._details);
264 if (binding.onDestroy) {
265 binding.onDestroy(aSummary, binding._details, binding.data);
266 }
267 },
269 /**
270 * Remove all items from the split view.
271 */
272 removeAll: function ASV_removeAll()
273 {
274 while (this._nav.hasChildNodes()) {
275 this.removeItem(this._nav.firstChild);
276 }
277 },
279 /**
280 * Set the item's CSS class name.
281 * This sets the class on both the summary and details elements, retaining
282 * any SplitView-specific classes.
283 *
284 * @param DOMElement aSummary
285 * Summary element of the item to set.
286 * @param string aClassName
287 * One or more space-separated CSS classes.
288 */
289 setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
290 {
291 let binding = bindings.get(aSummary);
292 let viewSpecific;
294 viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
295 viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
296 aSummary.className = viewSpecific + " " + aClassName;
298 viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
299 viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
300 binding._details.className = viewSpecific + " " + aClassName;
301 },
302 };