view-helpers.js 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  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/. */
  5. "use strict";
  6. const {KeyCodes} = require("devtools/client/shared/keycodes");
  7. const PANE_APPEARANCE_DELAY = 50;
  8. const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
  9. const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
  10. var namedTimeoutsStore = new Map();
  11. /**
  12. * Inheritance helpers from the addon SDK's core/heritage.
  13. * Remove these when all devtools are loadered.
  14. */
  15. exports.Heritage = {
  16. /**
  17. * @see extend in sdk/core/heritage.
  18. */
  19. extend: function (prototype, properties = {}) {
  20. return Object.create(prototype, this.getOwnPropertyDescriptors(properties));
  21. },
  22. /**
  23. * @see getOwnPropertyDescriptors in sdk/core/heritage.
  24. */
  25. getOwnPropertyDescriptors: function (object) {
  26. return Object.getOwnPropertyNames(object).reduce((descriptor, name) => {
  27. descriptor[name] = Object.getOwnPropertyDescriptor(object, name);
  28. return descriptor;
  29. }, {});
  30. }
  31. };
  32. /**
  33. * Helper for draining a rapid succession of events and invoking a callback
  34. * once everything settles down.
  35. *
  36. * @param string id
  37. * A string identifier for the named timeout.
  38. * @param number wait
  39. * The amount of milliseconds to wait after no more events are fired.
  40. * @param function callback
  41. * Invoked when no more events are fired after the specified time.
  42. */
  43. const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
  44. clearNamedTimeout(id);
  45. namedTimeoutsStore.set(id, setTimeout(() =>
  46. namedTimeoutsStore.delete(id) && callback(), wait));
  47. };
  48. exports.setNamedTimeout = setNamedTimeout;
  49. /**
  50. * Clears a named timeout.
  51. * @see setNamedTimeout
  52. *
  53. * @param string id
  54. * A string identifier for the named timeout.
  55. */
  56. const clearNamedTimeout = function clearNamedTimeout(id) {
  57. if (!namedTimeoutsStore) {
  58. return;
  59. }
  60. clearTimeout(namedTimeoutsStore.get(id));
  61. namedTimeoutsStore.delete(id);
  62. };
  63. exports.clearNamedTimeout = clearNamedTimeout;
  64. /**
  65. * Same as `setNamedTimeout`, but invokes the callback only if the provided
  66. * predicate function returns true. Otherwise, the timeout is re-triggered.
  67. *
  68. * @param string id
  69. * A string identifier for the conditional timeout.
  70. * @param number wait
  71. * The amount of milliseconds to wait after no more events are fired.
  72. * @param function predicate
  73. * The predicate function used to determine whether the timeout restarts.
  74. * @param function callback
  75. * Invoked when no more events are fired after the specified time, and
  76. * the provided predicate function returns true.
  77. */
  78. const setConditionalTimeout = function setConditionalTimeout(id, wait,
  79. predicate,
  80. callback) {
  81. setNamedTimeout(id, wait, function maybeCallback() {
  82. if (predicate()) {
  83. callback();
  84. return;
  85. }
  86. setConditionalTimeout(id, wait, predicate, callback);
  87. });
  88. };
  89. exports.setConditionalTimeout = setConditionalTimeout;
  90. /**
  91. * Clears a conditional timeout.
  92. * @see setConditionalTimeout
  93. *
  94. * @param string id
  95. * A string identifier for the conditional timeout.
  96. */
  97. const clearConditionalTimeout = function clearConditionalTimeout(id) {
  98. clearNamedTimeout(id);
  99. };
  100. exports.clearConditionalTimeout = clearConditionalTimeout;
  101. /**
  102. * Helpers for creating and messaging between UI components.
  103. */
  104. const ViewHelpers = exports.ViewHelpers = {
  105. /**
  106. * Convenience method, dispatching a custom event.
  107. *
  108. * @param nsIDOMNode target
  109. * A custom target element to dispatch the event from.
  110. * @param string type
  111. * The name of the event.
  112. * @param any detail
  113. * The data passed when initializing the event.
  114. * @return boolean
  115. * True if the event was cancelled or a registered handler
  116. * called preventDefault.
  117. */
  118. dispatchEvent: function (target, type, detail) {
  119. if (!(target instanceof Node)) {
  120. // Event cancelled.
  121. return true;
  122. }
  123. let document = target.ownerDocument || target;
  124. let dispatcher = target.ownerDocument ? target : document.documentElement;
  125. let event = document.createEvent("CustomEvent");
  126. event.initCustomEvent(type, true, true, detail);
  127. return dispatcher.dispatchEvent(event);
  128. },
  129. /**
  130. * Helper delegating some of the DOM attribute methods of a node to a widget.
  131. *
  132. * @param object widget
  133. * The widget to assign the methods to.
  134. * @param nsIDOMNode node
  135. * A node to delegate the methods to.
  136. */
  137. delegateWidgetAttributeMethods: function (widget, node) {
  138. widget.getAttribute =
  139. widget.getAttribute || node.getAttribute.bind(node);
  140. widget.setAttribute =
  141. widget.setAttribute || node.setAttribute.bind(node);
  142. widget.removeAttribute =
  143. widget.removeAttribute || node.removeAttribute.bind(node);
  144. },
  145. /**
  146. * Helper delegating some of the DOM event methods of a node to a widget.
  147. *
  148. * @param object widget
  149. * The widget to assign the methods to.
  150. * @param nsIDOMNode node
  151. * A node to delegate the methods to.
  152. */
  153. delegateWidgetEventMethods: function (widget, node) {
  154. widget.addEventListener =
  155. widget.addEventListener || node.addEventListener.bind(node);
  156. widget.removeEventListener =
  157. widget.removeEventListener || node.removeEventListener.bind(node);
  158. },
  159. /**
  160. * Checks if the specified object looks like it's been decorated by an
  161. * event emitter.
  162. *
  163. * @return boolean
  164. * True if it looks, walks and quacks like an event emitter.
  165. */
  166. isEventEmitter: function (object) {
  167. return object && object.on && object.off && object.once && object.emit;
  168. },
  169. /**
  170. * Checks if the specified object is an instance of a DOM node.
  171. *
  172. * @return boolean
  173. * True if it's a node, false otherwise.
  174. */
  175. isNode: function (object) {
  176. return object instanceof Node ||
  177. object instanceof Element ||
  178. object instanceof DocumentFragment;
  179. },
  180. /**
  181. * Prevents event propagation when navigation keys are pressed.
  182. *
  183. * @param Event e
  184. * The event to be prevented.
  185. */
  186. preventScrolling: function (e) {
  187. switch (e.keyCode) {
  188. case KeyCodes.DOM_VK_UP:
  189. case KeyCodes.DOM_VK_DOWN:
  190. case KeyCodes.DOM_VK_LEFT:
  191. case KeyCodes.DOM_VK_RIGHT:
  192. case KeyCodes.DOM_VK_PAGE_UP:
  193. case KeyCodes.DOM_VK_PAGE_DOWN:
  194. case KeyCodes.DOM_VK_HOME:
  195. case KeyCodes.DOM_VK_END:
  196. e.preventDefault();
  197. e.stopPropagation();
  198. }
  199. },
  200. /**
  201. * Check if the enter key or space was pressed
  202. *
  203. * @param event event
  204. * The event triggered by a keypress on an element
  205. */
  206. isSpaceOrReturn: function (event) {
  207. return event.keyCode === KeyCodes.DOM_VK_SPACE ||
  208. event.keyCode === KeyCodes.DOM_VK_RETURN;
  209. },
  210. /**
  211. * Sets a toggled pane hidden or visible. The pane can either be displayed on
  212. * the side (right or left depending on the locale) or at the bottom.
  213. *
  214. * @param object flags
  215. * An object containing some of the following properties:
  216. * - visible: true if the pane should be shown, false to hide
  217. * - animated: true to display an animation on toggle
  218. * - delayed: true to wait a few cycles before toggle
  219. * - callback: a function to invoke when the toggle finishes
  220. * @param nsIDOMNode pane
  221. * The element representing the pane to toggle.
  222. */
  223. togglePane: function (flags, pane) {
  224. // Make sure a pane is actually available first.
  225. if (!pane) {
  226. return;
  227. }
  228. // Hiding is always handled via margins, not the hidden attribute.
  229. pane.removeAttribute("hidden");
  230. // Add a class to the pane to handle min-widths, margins and animations.
  231. pane.classList.add("generic-toggled-pane");
  232. // Avoid toggles in the middle of animation.
  233. if (pane.hasAttribute("animated")) {
  234. return;
  235. }
  236. // Avoid useless toggles.
  237. if (flags.visible == !pane.classList.contains("pane-collapsed")) {
  238. if (flags.callback) {
  239. flags.callback();
  240. }
  241. return;
  242. }
  243. // The "animated" attributes enables animated toggles (slide in-out).
  244. if (flags.animated) {
  245. pane.setAttribute("animated", "");
  246. } else {
  247. pane.removeAttribute("animated");
  248. }
  249. // Computes and sets the pane margins in order to hide or show it.
  250. let doToggle = () => {
  251. // Negative margins are applied to "right" and "left" to support RTL and
  252. // LTR directions, as well as to "bottom" to support vertical layouts.
  253. // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
  254. if (flags.visible) {
  255. pane.style.marginLeft = "0";
  256. pane.style.marginRight = "0";
  257. pane.style.marginBottom = "0";
  258. pane.classList.remove("pane-collapsed");
  259. } else {
  260. let width = Math.floor(pane.getAttribute("width")) + 1;
  261. let height = Math.floor(pane.getAttribute("height")) + 1;
  262. pane.style.marginLeft = -width + "px";
  263. pane.style.marginRight = -width + "px";
  264. pane.style.marginBottom = -height + "px";
  265. }
  266. // Wait for the animation to end before calling afterToggle()
  267. if (flags.animated) {
  268. let options = {
  269. useCapture: false,
  270. once: true
  271. };
  272. pane.addEventListener("transitionend", () => {
  273. // Prevent unwanted transitions: if the panel is hidden and the layout
  274. // changes margins will be updated and the panel will pop out.
  275. pane.removeAttribute("animated");
  276. if (!flags.visible) {
  277. pane.classList.add("pane-collapsed");
  278. }
  279. if (flags.callback) {
  280. flags.callback();
  281. }
  282. }, options);
  283. } else {
  284. if (!flags.visible) {
  285. pane.classList.add("pane-collapsed");
  286. }
  287. // Invoke the callback immediately since there's no transition.
  288. if (flags.callback) {
  289. flags.callback();
  290. }
  291. }
  292. };
  293. // Sometimes it's useful delaying the toggle a few ticks to ensure
  294. // a smoother slide in-out animation.
  295. if (flags.delayed) {
  296. pane.ownerDocument.defaultView.setTimeout(doToggle,
  297. PANE_APPEARANCE_DELAY);
  298. } else {
  299. doToggle();
  300. }
  301. }
  302. };
  303. /**
  304. * A generic Item is used to describe children present in a Widget.
  305. *
  306. * This is basically a very thin wrapper around an nsIDOMNode, with a few
  307. * characteristics, like a `value` and an `attachment`.
  308. *
  309. * The characteristics are optional, and their meaning is entirely up to you.
  310. * - The `value` should be a string, passed as an argument.
  311. * - The `attachment` is any kind of primitive or object, passed as an argument.
  312. *
  313. * Iterable via "for (let childItem of parentItem) { }".
  314. *
  315. * @param object ownerView
  316. * The owner view creating this item.
  317. * @param nsIDOMNode element
  318. * A prebuilt node to be wrapped.
  319. * @param string value
  320. * A string identifying the node.
  321. * @param any attachment
  322. * Some attached primitive/object.
  323. */
  324. function Item(ownerView, element, value, attachment) {
  325. this.ownerView = ownerView;
  326. this.attachment = attachment;
  327. this._value = value + "";
  328. this._prebuiltNode = element;
  329. this._itemsByElement = new Map();
  330. }
  331. Item.prototype = {
  332. get value() {
  333. return this._value;
  334. },
  335. get target() {
  336. return this._target;
  337. },
  338. get prebuiltNode() {
  339. return this._prebuiltNode;
  340. },
  341. /**
  342. * Immediately appends a child item to this item.
  343. *
  344. * @param nsIDOMNode element
  345. * An nsIDOMNode representing the child element to append.
  346. * @param object options [optional]
  347. * Additional options or flags supported by this operation:
  348. * - attachment: some attached primitive/object for the item
  349. * - attributes: a batch of attributes set to the displayed element
  350. * - finalize: function invoked when the child item is removed
  351. * @return Item
  352. * The item associated with the displayed element.
  353. */
  354. append: function (element, options = {}) {
  355. let item = new Item(this, element, "", options.attachment);
  356. // Entangle the item with the newly inserted child node.
  357. // Make sure this is done with the value returned by appendChild(),
  358. // to avoid storing a potential DocumentFragment.
  359. this._entangleItem(item, this._target.appendChild(element));
  360. // Handle any additional options after entangling the item.
  361. if (options.attributes) {
  362. options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
  363. }
  364. if (options.finalize) {
  365. item.finalize = options.finalize;
  366. }
  367. // Return the item associated with the displayed element.
  368. return item;
  369. },
  370. /**
  371. * Immediately removes the specified child item from this item.
  372. *
  373. * @param Item item
  374. * The item associated with the element to remove.
  375. */
  376. remove: function (item) {
  377. if (!item) {
  378. return;
  379. }
  380. this._target.removeChild(item._target);
  381. this._untangleItem(item);
  382. },
  383. /**
  384. * Entangles an item (model) with a displayed node element (view).
  385. *
  386. * @param Item item
  387. * The item describing a target element.
  388. * @param nsIDOMNode element
  389. * The element displaying the item.
  390. */
  391. _entangleItem: function (item, element) {
  392. this._itemsByElement.set(element, item);
  393. item._target = element;
  394. },
  395. /**
  396. * Untangles an item (model) from a displayed node element (view).
  397. *
  398. * @param Item item
  399. * The item describing a target element.
  400. */
  401. _untangleItem: function (item) {
  402. if (item.finalize) {
  403. item.finalize(item);
  404. }
  405. for (let childItem of item) {
  406. item.remove(childItem);
  407. }
  408. this._unlinkItem(item);
  409. item._target = null;
  410. },
  411. /**
  412. * Deletes an item from the its parent's storage maps.
  413. *
  414. * @param Item item
  415. * The item describing a target element.
  416. */
  417. _unlinkItem: function (item) {
  418. this._itemsByElement.delete(item._target);
  419. },
  420. /**
  421. * Returns a string representing the object.
  422. * Avoid using `toString` to avoid accidental JSONification.
  423. * @return string
  424. */
  425. stringify: function () {
  426. return JSON.stringify({
  427. value: this._value,
  428. target: this._target + "",
  429. prebuiltNode: this._prebuiltNode + "",
  430. attachment: this.attachment
  431. }, null, 2);
  432. },
  433. _value: "",
  434. _target: null,
  435. _prebuiltNode: null,
  436. finalize: null,
  437. attachment: null
  438. };
  439. /**
  440. * Some generic Widget methods handling Item instances.
  441. * Iterable via "for (let childItem of wrappedView) { }".
  442. *
  443. * Usage:
  444. * function MyView() {
  445. * this.widget = new MyWidget(document.querySelector(".my-node"));
  446. * }
  447. *
  448. * MyView.prototype = Heritage.extend(WidgetMethods, {
  449. * myMethod: function() {},
  450. * ...
  451. * });
  452. *
  453. * See https://gist.github.com/victorporof/5749386 for more details.
  454. * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation
  455. * example.
  456. *
  457. * Language:
  458. * - An "item" is an instance of an Item.
  459. * - An "element" or "node" is a nsIDOMNode.
  460. *
  461. * The supplied widget can be any object implementing the following
  462. * methods:
  463. * - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode,
  464. * aValue:string)
  465. * - function:nsIDOMNode getItemAtIndex(aIndex:number)
  466. * - function removeChild(aChild:nsIDOMNode)
  467. * - function removeAllItems()
  468. * - get:nsIDOMNode selectedItem()
  469. * - set selectedItem(aChild:nsIDOMNode)
  470. * - function getAttribute(aName:string)
  471. * - function setAttribute(aName:string, aValue:string)
  472. * - function removeAttribute(aName:string)
  473. * - function addEventListener(aName:string, aCallback:function,
  474. * aBubbleFlag:boolean)
  475. * - function removeEventListener(aName:string, aCallback:function,
  476. * aBubbleFlag:boolean)
  477. *
  478. * Optional methods that can be implemented by the widget:
  479. * - function ensureElementIsVisible(aChild:nsIDOMNode)
  480. *
  481. * Optional attributes that may be handled (when calling
  482. * get/set/removeAttribute):
  483. * - "emptyText": label temporarily added when there are no items present
  484. * - "headerText": label permanently added as a header
  485. *
  486. * For automagical keyboard and mouse accessibility, the widget should be an
  487. * event emitter with the following events:
  488. * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
  489. * - "mousePress" -> (aName:string, aEvent:MouseEvent)
  490. */
  491. const WidgetMethods = exports.WidgetMethods = {
  492. /**
  493. * Sets the element node or widget associated with this container.
  494. * @param nsIDOMNode | object widget
  495. */
  496. set widget(widget) {
  497. this._widget = widget;
  498. // Can't use a WeakMap for _itemsByValue because keys are strings, and
  499. // can't use one for _itemsByElement either, since it needs to be iterable.
  500. this._itemsByValue = new Map();
  501. this._itemsByElement = new Map();
  502. this._stagedItems = [];
  503. // Handle internal events emitted by the widget if necessary.
  504. if (ViewHelpers.isEventEmitter(widget)) {
  505. widget.on("keyPress", this._onWidgetKeyPress.bind(this));
  506. widget.on("mousePress", this._onWidgetMousePress.bind(this));
  507. }
  508. },
  509. /**
  510. * Gets the element node or widget associated with this container.
  511. * @return nsIDOMNode | object
  512. */
  513. get widget() {
  514. return this._widget;
  515. },
  516. /**
  517. * Prepares an item to be added to this container. This allows, for example,
  518. * for a large number of items to be batched up before being sorted & added.
  519. *
  520. * If the "staged" flag is *not* set to true, the item will be immediately
  521. * inserted at the correct position in this container, so that all the items
  522. * still remain sorted. This can (possibly) be much slower than batching up
  523. * multiple items.
  524. *
  525. * By default, this container assumes that all the items should be displayed
  526. * sorted by their value. This can be overridden with the "index" flag,
  527. * specifying on which position should an item be appended. The "staged" and
  528. * "index" flags are mutually exclusive, meaning that all staged items
  529. * will always be appended.
  530. *
  531. * @param nsIDOMNode element
  532. * A prebuilt node to be wrapped.
  533. * @param string value
  534. * A string identifying the node.
  535. * @param object options [optional]
  536. * Additional options or flags supported by this operation:
  537. * - attachment: some attached primitive/object for the item
  538. * - staged: true to stage the item to be appended later
  539. * - index: specifies on which position should the item be appended
  540. * - attributes: a batch of attributes set to the displayed element
  541. * - finalize: function invoked when the item is removed
  542. * @return Item
  543. * The item associated with the displayed element if an unstaged push,
  544. * undefined if the item was staged for a later commit.
  545. */
  546. push: function ([element, value], options = {}) {
  547. let item = new Item(this, element, value, options.attachment);
  548. // Batch the item to be added later.
  549. if (options.staged) {
  550. // An ulterior commit operation will ignore any specified index, so
  551. // no reason to keep it around.
  552. options.index = undefined;
  553. return void this._stagedItems.push({ item: item, options: options });
  554. }
  555. // Find the target position in this container and insert the item there.
  556. if (!("index" in options)) {
  557. return this._insertItemAt(this._findExpectedIndexFor(item), item,
  558. options);
  559. }
  560. // Insert the item at the specified index. If negative or out of bounds,
  561. // the item will be simply appended.
  562. return this._insertItemAt(options.index, item, options);
  563. },
  564. /**
  565. * Flushes all the prepared items into this container.
  566. * Any specified index on the items will be ignored. Everything is appended.
  567. *
  568. * @param object options [optional]
  569. * Additional options or flags supported by this operation:
  570. * - sorted: true to sort all the items before adding them
  571. */
  572. commit: function (options = {}) {
  573. let stagedItems = this._stagedItems;
  574. // Sort the items before adding them to this container, if preferred.
  575. if (options.sorted) {
  576. stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
  577. }
  578. // Append the prepared items to this container.
  579. for (let { item, opt } of stagedItems) {
  580. this._insertItemAt(-1, item, opt);
  581. }
  582. // Recreate the temporary items list for ulterior pushes.
  583. this._stagedItems.length = 0;
  584. },
  585. /**
  586. * Immediately removes the specified item from this container.
  587. *
  588. * @param Item item
  589. * The item associated with the element to remove.
  590. */
  591. remove: function (item) {
  592. if (!item) {
  593. return;
  594. }
  595. this._widget.removeChild(item._target);
  596. this._untangleItem(item);
  597. if (!this._itemsByElement.size) {
  598. this._preferredValue = this.selectedValue;
  599. this._widget.selectedItem = null;
  600. this._widget.setAttribute("emptyText", this._emptyText);
  601. }
  602. },
  603. /**
  604. * Removes the item at the specified index from this container.
  605. *
  606. * @param number index
  607. * The index of the item to remove.
  608. */
  609. removeAt: function (index) {
  610. this.remove(this.getItemAtIndex(index));
  611. },
  612. /**
  613. * Removes the items in this container based on a predicate.
  614. */
  615. removeForPredicate: function (predicate) {
  616. let item;
  617. while ((item = this.getItemForPredicate(predicate))) {
  618. this.remove(item);
  619. }
  620. },
  621. /**
  622. * Removes all items from this container.
  623. */
  624. empty: function () {
  625. this._preferredValue = this.selectedValue;
  626. this._widget.selectedItem = null;
  627. this._widget.removeAllItems();
  628. this._widget.setAttribute("emptyText", this._emptyText);
  629. for (let [, item] of this._itemsByElement) {
  630. this._untangleItem(item);
  631. }
  632. this._itemsByValue.clear();
  633. this._itemsByElement.clear();
  634. this._stagedItems.length = 0;
  635. },
  636. /**
  637. * Ensures the specified item is visible in this container.
  638. *
  639. * @param Item item
  640. * The item to bring into view.
  641. */
  642. ensureItemIsVisible: function (item) {
  643. this._widget.ensureElementIsVisible(item._target);
  644. },
  645. /**
  646. * Ensures the item at the specified index is visible in this container.
  647. *
  648. * @param number index
  649. * The index of the item to bring into view.
  650. */
  651. ensureIndexIsVisible: function (index) {
  652. this.ensureItemIsVisible(this.getItemAtIndex(index));
  653. },
  654. /**
  655. * Sugar for ensuring the selected item is visible in this container.
  656. */
  657. ensureSelectedItemIsVisible: function () {
  658. this.ensureItemIsVisible(this.selectedItem);
  659. },
  660. /**
  661. * If supported by the widget, the label string temporarily added to this
  662. * container when there are no child items present.
  663. */
  664. set emptyText(value) {
  665. this._emptyText = value;
  666. // Apply the emptyText attribute right now if there are no child items.
  667. if (!this._itemsByElement.size) {
  668. this._widget.setAttribute("emptyText", value);
  669. }
  670. },
  671. /**
  672. * If supported by the widget, the label string permanently added to this
  673. * container as a header.
  674. * @param string value
  675. */
  676. set headerText(value) {
  677. this._headerText = value;
  678. this._widget.setAttribute("headerText", value);
  679. },
  680. /**
  681. * Toggles all the items in this container hidden or visible.
  682. *
  683. * This does not change the default filtering predicate, so newly inserted
  684. * items will always be visible. Use WidgetMethods.filterContents if you care.
  685. *
  686. * @param boolean visibleFlag
  687. * Specifies the intended visibility.
  688. */
  689. toggleContents: function (visibleFlag) {
  690. for (let [element] of this._itemsByElement) {
  691. element.hidden = !visibleFlag;
  692. }
  693. },
  694. /**
  695. * Toggles all items in this container hidden or visible based on a predicate.
  696. *
  697. * @param function predicate [optional]
  698. * Items are toggled according to the return value of this function,
  699. * which will become the new default filtering predicate in this
  700. * container.
  701. * If unspecified, all items will be toggled visible.
  702. */
  703. filterContents: function (predicate = this._currentFilterPredicate) {
  704. this._currentFilterPredicate = predicate;
  705. for (let [element, item] of this._itemsByElement) {
  706. element.hidden = !predicate(item);
  707. }
  708. },
  709. /**
  710. * Sorts all the items in this container based on a predicate.
  711. *
  712. * @param function predicate [optional]
  713. * Items are sorted according to the return value of the function,
  714. * which will become the new default sorting predicate in this
  715. * container. If unspecified, all items will be sorted by their value.
  716. */
  717. sortContents: function (predicate = this._currentSortPredicate) {
  718. let sortedItems = this.items.sort(this._currentSortPredicate = predicate);
  719. for (let i = 0, len = sortedItems.length; i < len; i++) {
  720. this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
  721. }
  722. },
  723. /**
  724. * Visually swaps two items in this container.
  725. *
  726. * @param Item first
  727. * The first item to be swapped.
  728. * @param Item second
  729. * The second item to be swapped.
  730. */
  731. swapItems: function (first, second) {
  732. if (first == second) {
  733. // We're just dandy, thank you.
  734. return;
  735. }
  736. let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = first;
  737. let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = second;
  738. // If the two items were constructed with prebuilt nodes as
  739. // DocumentFragments, then those DocumentFragments are now
  740. // empty and need to be reassembled.
  741. if (firstPrebuiltTarget instanceof DocumentFragment) {
  742. for (let node of firstTarget.childNodes) {
  743. firstPrebuiltTarget.appendChild(node.cloneNode(true));
  744. }
  745. }
  746. if (secondPrebuiltTarget instanceof DocumentFragment) {
  747. for (let node of secondTarget.childNodes) {
  748. secondPrebuiltTarget.appendChild(node.cloneNode(true));
  749. }
  750. }
  751. // 1. Get the indices of the two items to swap.
  752. let i = this._indexOfElement(firstTarget);
  753. let j = this._indexOfElement(secondTarget);
  754. // 2. Remeber the selection index, to reselect an item, if necessary.
  755. let selectedTarget = this._widget.selectedItem;
  756. let selectedIndex = -1;
  757. if (selectedTarget == firstTarget) {
  758. selectedIndex = i;
  759. } else if (selectedTarget == secondTarget) {
  760. selectedIndex = j;
  761. }
  762. // 3. Silently nuke both items, nobody needs to know about this.
  763. this._widget.removeChild(firstTarget);
  764. this._widget.removeChild(secondTarget);
  765. this._unlinkItem(first);
  766. this._unlinkItem(second);
  767. // 4. Add the items again, but reversing their indices.
  768. this._insertItemAt.apply(this, i < j ? [i, second] : [j, first]);
  769. this._insertItemAt.apply(this, i < j ? [j, first] : [i, second]);
  770. // 5. Restore the previous selection, if necessary.
  771. if (selectedIndex == i) {
  772. this._widget.selectedItem = first._target;
  773. } else if (selectedIndex == j) {
  774. this._widget.selectedItem = second._target;
  775. }
  776. // 6. Let the outside world know that these two items were swapped.
  777. ViewHelpers.dispatchEvent(first.target, "swap", [second, first]);
  778. },
  779. /**
  780. * Visually swaps two items in this container at specific indices.
  781. *
  782. * @param number first
  783. * The index of the first item to be swapped.
  784. * @param number second
  785. * The index of the second item to be swapped.
  786. */
  787. swapItemsAtIndices: function (first, second) {
  788. this.swapItems(this.getItemAtIndex(first), this.getItemAtIndex(second));
  789. },
  790. /**
  791. * Checks whether an item with the specified value is among the elements
  792. * shown in this container.
  793. *
  794. * @param string value
  795. * The item's value.
  796. * @return boolean
  797. * True if the value is known, false otherwise.
  798. */
  799. containsValue: function (value) {
  800. return this._itemsByValue.has(value) ||
  801. this._stagedItems.some(({ item }) => item._value == value);
  802. },
  803. /**
  804. * Gets the "preferred value". This is the latest selected item's value,
  805. * remembered just before emptying this container.
  806. * @return string
  807. */
  808. get preferredValue() {
  809. return this._preferredValue;
  810. },
  811. /**
  812. * Retrieves the item associated with the selected element.
  813. * @return Item | null
  814. */
  815. get selectedItem() {
  816. let selectedElement = this._widget.selectedItem;
  817. if (selectedElement) {
  818. return this._itemsByElement.get(selectedElement);
  819. }
  820. return null;
  821. },
  822. /**
  823. * Retrieves the selected element's index in this container.
  824. * @return number
  825. */
  826. get selectedIndex() {
  827. let selectedElement = this._widget.selectedItem;
  828. if (selectedElement) {
  829. return this._indexOfElement(selectedElement);
  830. }
  831. return -1;
  832. },
  833. /**
  834. * Retrieves the value of the selected element.
  835. * @return string
  836. */
  837. get selectedValue() {
  838. let selectedElement = this._widget.selectedItem;
  839. if (selectedElement) {
  840. return this._itemsByElement.get(selectedElement)._value;
  841. }
  842. return "";
  843. },
  844. /**
  845. * Retrieves the attachment of the selected element.
  846. * @return object | null
  847. */
  848. get selectedAttachment() {
  849. let selectedElement = this._widget.selectedItem;
  850. if (selectedElement) {
  851. return this._itemsByElement.get(selectedElement).attachment;
  852. }
  853. return null;
  854. },
  855. _selectItem: function (item) {
  856. // A falsy item is allowed to invalidate the current selection.
  857. let targetElement = item ? item._target : null;
  858. let prevElement = this._widget.selectedItem;
  859. // Make sure the selected item's target element is focused and visible.
  860. if (this.autoFocusOnSelection && targetElement) {
  861. targetElement.focus();
  862. }
  863. if (targetElement != prevElement) {
  864. this._widget.selectedItem = targetElement;
  865. }
  866. },
  867. /**
  868. * Selects the element with the entangled item in this container.
  869. * @param Item | function item
  870. */
  871. set selectedItem(item) {
  872. // A predicate is allowed to select a specific item.
  873. // If no item is matched, then the current selection is removed.
  874. if (typeof item == "function") {
  875. item = this.getItemForPredicate(item);
  876. }
  877. let targetElement = item ? item._target : null;
  878. let prevElement = this._widget.selectedItem;
  879. if (this.maintainSelectionVisible && targetElement) {
  880. // Some methods are optional. See the WidgetMethods object documentation
  881. // for a comprehensive list.
  882. if ("ensureElementIsVisible" in this._widget) {
  883. this._widget.ensureElementIsVisible(targetElement);
  884. }
  885. }
  886. this._selectItem(item);
  887. // Prevent selecting the same item again and avoid dispatching
  888. // a redundant selection event, so return early.
  889. if (targetElement != prevElement) {
  890. let dispTarget = targetElement || prevElement;
  891. let dispName = this.suppressSelectionEvents ? "suppressed-select"
  892. : "select";
  893. ViewHelpers.dispatchEvent(dispTarget, dispName, item);
  894. }
  895. },
  896. /**
  897. * Selects the element at the specified index in this container.
  898. * @param number index
  899. */
  900. set selectedIndex(index) {
  901. let targetElement = this._widget.getItemAtIndex(index);
  902. if (targetElement) {
  903. this.selectedItem = this._itemsByElement.get(targetElement);
  904. return;
  905. }
  906. this.selectedItem = null;
  907. },
  908. /**
  909. * Selects the element with the specified value in this container.
  910. * @param string value
  911. */
  912. set selectedValue(value) {
  913. this.selectedItem = this._itemsByValue.get(value);
  914. },
  915. /**
  916. * Deselects and re-selects an item in this container.
  917. *
  918. * Useful when you want a "select" event to be emitted, even though
  919. * the specified item was already selected.
  920. *
  921. * @param Item | function item
  922. * @see `set selectedItem`
  923. */
  924. forceSelect: function (item) {
  925. this.selectedItem = null;
  926. this.selectedItem = item;
  927. },
  928. /**
  929. * Specifies if this container should try to keep the selected item visible.
  930. * (For example, when new items are added the selection is brought into view).
  931. */
  932. maintainSelectionVisible: true,
  933. /**
  934. * Specifies if "select" events dispatched from the elements in this container
  935. * when their respective items are selected should be suppressed or not.
  936. *
  937. * If this flag is set to true, then consumers of this container won't
  938. * be normally notified when items are selected.
  939. */
  940. suppressSelectionEvents: false,
  941. /**
  942. * Focus this container the first time an element is inserted?
  943. *
  944. * If this flag is set to true, then when the first item is inserted in
  945. * this container (and thus it's the only item available), its corresponding
  946. * target element is focused as well.
  947. */
  948. autoFocusOnFirstItem: true,
  949. /**
  950. * Focus on selection?
  951. *
  952. * If this flag is set to true, then whenever an item is selected in
  953. * this container (e.g. via the selectedIndex or selectedItem setters),
  954. * its corresponding target element is focused as well.
  955. *
  956. * You can disable this flag, for example, to maintain a certain node
  957. * focused but visually indicate a different selection in this container.
  958. */
  959. autoFocusOnSelection: true,
  960. /**
  961. * Focus on input (e.g. mouse click)?
  962. *
  963. * If this flag is set to true, then whenever an item receives user input in
  964. * this container, its corresponding target element is focused as well.
  965. */
  966. autoFocusOnInput: true,
  967. /**
  968. * When focusing on input, allow right clicks?
  969. * @see WidgetMethods.autoFocusOnInput
  970. */
  971. allowFocusOnRightClick: false,
  972. /**
  973. * The number of elements in this container to jump when Page Up or Page Down
  974. * keys are pressed. If falsy, then the page size will be based on the
  975. * number of visible items in the container.
  976. */
  977. pageSize: 0,
  978. /**
  979. * Focuses the first visible item in this container.
  980. */
  981. focusFirstVisibleItem: function () {
  982. this.focusItemAtDelta(-this.itemCount);
  983. },
  984. /**
  985. * Focuses the last visible item in this container.
  986. */
  987. focusLastVisibleItem: function () {
  988. this.focusItemAtDelta(+this.itemCount);
  989. },
  990. /**
  991. * Focuses the next item in this container.
  992. */
  993. focusNextItem: function () {
  994. this.focusItemAtDelta(+1);
  995. },
  996. /**
  997. * Focuses the previous item in this container.
  998. */
  999. focusPrevItem: function () {
  1000. this.focusItemAtDelta(-1);
  1001. },
  1002. /**
  1003. * Focuses another item in this container based on the index distance
  1004. * from the currently focused item.
  1005. *
  1006. * @param number delta
  1007. * A scalar specifying by how many items should the selection change.
  1008. */
  1009. focusItemAtDelta: function (delta) {
  1010. // Make sure the currently selected item is also focused, so that the
  1011. // command dispatcher mechanism has a relative node to work with.
  1012. // If there's no selection, just select an item at a corresponding index
  1013. // (e.g. the first item in this container if delta <= 1).
  1014. let selectedElement = this._widget.selectedItem;
  1015. if (selectedElement) {
  1016. selectedElement.focus();
  1017. } else {
  1018. this.selectedIndex = Math.max(0, delta - 1);
  1019. return;
  1020. }
  1021. let direction = delta > 0 ? "advanceFocus" : "rewindFocus";
  1022. let distance = Math.abs(Math[delta > 0 ? "ceil" : "floor"](delta));
  1023. while (distance--) {
  1024. if (!this._focusChange(direction)) {
  1025. // Out of bounds.
  1026. break;
  1027. }
  1028. }
  1029. // Synchronize the selected item as being the currently focused element.
  1030. this.selectedItem = this.getItemForElement(this._focusedElement);
  1031. },
  1032. /**
  1033. * Focuses the next or previous item in this container.
  1034. *
  1035. * @param string direction
  1036. * Either "advanceFocus" or "rewindFocus".
  1037. * @return boolean
  1038. * False if the focus went out of bounds and the first or last item
  1039. * in this container was focused instead.
  1040. */
  1041. _focusChange: function (direction) {
  1042. let commandDispatcher = this._commandDispatcher;
  1043. let prevFocusedElement = commandDispatcher.focusedElement;
  1044. let currFocusedElement;
  1045. do {
  1046. commandDispatcher.suppressFocusScroll = true;
  1047. commandDispatcher[direction]();
  1048. currFocusedElement = commandDispatcher.focusedElement;
  1049. // Make sure the newly focused item is a part of this container. If the
  1050. // focus goes out of bounds, revert the previously focused item.
  1051. if (!this.getItemForElement(currFocusedElement)) {
  1052. prevFocusedElement.focus();
  1053. return false;
  1054. }
  1055. } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
  1056. // Focus remained within bounds.
  1057. return true;
  1058. },
  1059. /**
  1060. * Gets the command dispatcher instance associated with this container's DOM.
  1061. * If there are no items displayed in this container, null is returned.
  1062. * @return nsIDOMXULCommandDispatcher | null
  1063. */
  1064. get _commandDispatcher() {
  1065. if (this._cachedCommandDispatcher) {
  1066. return this._cachedCommandDispatcher;
  1067. }
  1068. let someElement = this._widget.getItemAtIndex(0);
  1069. if (someElement) {
  1070. let commandDispatcher = someElement.ownerDocument.commandDispatcher;
  1071. this._cachedCommandDispatcher = commandDispatcher;
  1072. return commandDispatcher;
  1073. }
  1074. return null;
  1075. },
  1076. /**
  1077. * Gets the currently focused element in this container.
  1078. *
  1079. * @return nsIDOMNode
  1080. * The focused element, or null if nothing is found.
  1081. */
  1082. get _focusedElement() {
  1083. let commandDispatcher = this._commandDispatcher;
  1084. if (commandDispatcher) {
  1085. return commandDispatcher.focusedElement;
  1086. }
  1087. return null;
  1088. },
  1089. /**
  1090. * Gets the item in the container having the specified index.
  1091. *
  1092. * @param number index
  1093. * The index used to identify the element.
  1094. * @return Item
  1095. * The matched item, or null if nothing is found.
  1096. */
  1097. getItemAtIndex: function (index) {
  1098. return this.getItemForElement(this._widget.getItemAtIndex(index));
  1099. },
  1100. /**
  1101. * Gets the item in the container having the specified value.
  1102. *
  1103. * @param string value
  1104. * The value used to identify the element.
  1105. * @return Item
  1106. * The matched item, or null if nothing is found.
  1107. */
  1108. getItemByValue: function (value) {
  1109. return this._itemsByValue.get(value);
  1110. },
  1111. /**
  1112. * Gets the item in the container associated with the specified element.
  1113. *
  1114. * @param nsIDOMNode element
  1115. * The element used to identify the item.
  1116. * @param object flags [optional]
  1117. * Additional options for showing the source. Supported options:
  1118. * - noSiblings: if siblings shouldn't be taken into consideration
  1119. * when searching for the associated item.
  1120. * @return Item
  1121. * The matched item, or null if nothing is found.
  1122. */
  1123. getItemForElement: function (element, flags = {}) {
  1124. while (element) {
  1125. let item = this._itemsByElement.get(element);
  1126. // Also search the siblings if allowed.
  1127. if (!flags.noSiblings) {
  1128. item = item ||
  1129. this._itemsByElement.get(element.nextElementSibling) ||
  1130. this._itemsByElement.get(element.previousElementSibling);
  1131. }
  1132. if (item) {
  1133. return item;
  1134. }
  1135. element = element.parentNode;
  1136. }
  1137. return null;
  1138. },
  1139. /**
  1140. * Gets a visible item in this container validating a specified predicate.
  1141. *
  1142. * @param function predicate
  1143. * The first item which validates this predicate is returned
  1144. * @return Item
  1145. * The matched item, or null if nothing is found.
  1146. */
  1147. getItemForPredicate: function (predicate, owner = this) {
  1148. // Recursively check the items in this widget for a predicate match.
  1149. for (let [element, item] of owner._itemsByElement) {
  1150. let match;
  1151. if (predicate(item) && !element.hidden) {
  1152. match = item;
  1153. } else {
  1154. match = this.getItemForPredicate(predicate, item);
  1155. }
  1156. if (match) {
  1157. return match;
  1158. }
  1159. }
  1160. // Also check the staged items. No need to do this recursively since
  1161. // they're not even appended to the view yet.
  1162. for (let { item } of this._stagedItems) {
  1163. if (predicate(item)) {
  1164. return item;
  1165. }
  1166. }
  1167. return null;
  1168. },
  1169. /**
  1170. * Shortcut function for getItemForPredicate which works on item attachments.
  1171. * @see getItemForPredicate
  1172. */
  1173. getItemForAttachment: function (predicate, owner = this) {
  1174. return this.getItemForPredicate(e => predicate(e.attachment));
  1175. },
  1176. /**
  1177. * Finds the index of an item in the container.
  1178. *
  1179. * @param Item item
  1180. * The item get the index for.
  1181. * @return number
  1182. * The index of the matched item, or -1 if nothing is found.
  1183. */
  1184. indexOfItem: function (item) {
  1185. return this._indexOfElement(item._target);
  1186. },
  1187. /**
  1188. * Finds the index of an element in the container.
  1189. *
  1190. * @param nsIDOMNode element
  1191. * The element get the index for.
  1192. * @return number
  1193. * The index of the matched element, or -1 if nothing is found.
  1194. */
  1195. _indexOfElement: function (element) {
  1196. for (let i = 0; i < this._itemsByElement.size; i++) {
  1197. if (this._widget.getItemAtIndex(i) == element) {
  1198. return i;
  1199. }
  1200. }
  1201. return -1;
  1202. },
  1203. /**
  1204. * Gets the total number of items in this container.
  1205. * @return number
  1206. */
  1207. get itemCount() {
  1208. return this._itemsByElement.size;
  1209. },
  1210. /**
  1211. * Returns a list of items in this container, in the displayed order.
  1212. * @return array
  1213. */
  1214. get items() {
  1215. let store = [];
  1216. let itemCount = this.itemCount;
  1217. for (let i = 0; i < itemCount; i++) {
  1218. store.push(this.getItemAtIndex(i));
  1219. }
  1220. return store;
  1221. },
  1222. /**
  1223. * Returns a list of values in this container, in the displayed order.
  1224. * @return array
  1225. */
  1226. get values() {
  1227. return this.items.map(e => e._value);
  1228. },
  1229. /**
  1230. * Returns a list of attachments in this container, in the displayed order.
  1231. * @return array
  1232. */
  1233. get attachments() {
  1234. return this.items.map(e => e.attachment);
  1235. },
  1236. /**
  1237. * Returns a list of all the visible (non-hidden) items in this container,
  1238. * in the displayed order
  1239. * @return array
  1240. */
  1241. get visibleItems() {
  1242. return this.items.filter(e => !e._target.hidden);
  1243. },
  1244. /**
  1245. * Checks if an item is unique in this container. If an item's value is an
  1246. * empty string, "undefined" or "null", it is considered unique.
  1247. *
  1248. * @param Item item
  1249. * The item for which to verify uniqueness.
  1250. * @return boolean
  1251. * True if the item is unique, false otherwise.
  1252. */
  1253. isUnique: function (item) {
  1254. let value = item._value;
  1255. if (value == "" || value == "undefined" || value == "null") {
  1256. return true;
  1257. }
  1258. return !this._itemsByValue.has(value);
  1259. },
  1260. /**
  1261. * Checks if an item is eligible for this container. By default, this checks
  1262. * whether an item is unique and has a prebuilt target node.
  1263. *
  1264. * @param Item item
  1265. * The item for which to verify eligibility.
  1266. * @return boolean
  1267. * True if the item is eligible, false otherwise.
  1268. */
  1269. isEligible: function (item) {
  1270. return this.isUnique(item) && item._prebuiltNode;
  1271. },
  1272. /**
  1273. * Finds the expected item index in this container based on the default
  1274. * sort predicate.
  1275. *
  1276. * @param Item item
  1277. * The item for which to get the expected index.
  1278. * @return number
  1279. * The expected item index.
  1280. */
  1281. _findExpectedIndexFor: function (item) {
  1282. let itemCount = this.itemCount;
  1283. for (let i = 0; i < itemCount; i++) {
  1284. if (this._currentSortPredicate(this.getItemAtIndex(i), item) > 0) {
  1285. return i;
  1286. }
  1287. }
  1288. return itemCount;
  1289. },
  1290. /**
  1291. * Immediately inserts an item in this container at the specified index.
  1292. *
  1293. * @param number index
  1294. * The position in the container intended for this item.
  1295. * @param Item item
  1296. * The item describing a target element.
  1297. * @param object options [optional]
  1298. * Additional options or flags supported by this operation:
  1299. * - attributes: a batch of attributes set to the displayed element
  1300. * - finalize: function when the item is untangled (removed)
  1301. * @return Item
  1302. * The item associated with the displayed element, null if rejected.
  1303. */
  1304. _insertItemAt: function (index, item, options = {}) {
  1305. if (!this.isEligible(item)) {
  1306. return null;
  1307. }
  1308. // Entangle the item with the newly inserted node.
  1309. // Make sure this is done with the value returned by insertItemAt(),
  1310. // to avoid storing a potential DocumentFragment.
  1311. let node = item._prebuiltNode;
  1312. let attachment = item.attachment;
  1313. this._entangleItem(item,
  1314. this._widget.insertItemAt(index, node, attachment));
  1315. // Handle any additional options after entangling the item.
  1316. if (!this._currentFilterPredicate(item)) {
  1317. item._target.hidden = true;
  1318. }
  1319. if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
  1320. item._target.focus();
  1321. }
  1322. if (options.attributes) {
  1323. options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
  1324. }
  1325. if (options.finalize) {
  1326. item.finalize = options.finalize;
  1327. }
  1328. // Hide the empty text if the selection wasn't lost.
  1329. this._widget.removeAttribute("emptyText");
  1330. // Return the item associated with the displayed element.
  1331. return item;
  1332. },
  1333. /**
  1334. * Entangles an item (model) with a displayed node element (view).
  1335. *
  1336. * @param Item item
  1337. * The item describing a target element.
  1338. * @param nsIDOMNode element
  1339. * The element displaying the item.
  1340. */
  1341. _entangleItem: function (item, element) {
  1342. this._itemsByValue.set(item._value, item);
  1343. this._itemsByElement.set(element, item);
  1344. item._target = element;
  1345. },
  1346. /**
  1347. * Untangles an item (model) from a displayed node element (view).
  1348. *
  1349. * @param Item item
  1350. * The item describing a target element.
  1351. */
  1352. _untangleItem: function (item) {
  1353. if (item.finalize) {
  1354. item.finalize(item);
  1355. }
  1356. for (let childItem of item) {
  1357. item.remove(childItem);
  1358. }
  1359. this._unlinkItem(item);
  1360. item._target = null;
  1361. },
  1362. /**
  1363. * Deletes an item from the its parent's storage maps.
  1364. *
  1365. * @param Item item
  1366. * The item describing a target element.
  1367. */
  1368. _unlinkItem: function (item) {
  1369. this._itemsByValue.delete(item._value);
  1370. this._itemsByElement.delete(item._target);
  1371. },
  1372. /**
  1373. * The keyPress event listener for this container.
  1374. * @param string name
  1375. * @param KeyboardEvent event
  1376. */
  1377. _onWidgetKeyPress: function (name, event) {
  1378. // Prevent scrolling when pressing navigation keys.
  1379. ViewHelpers.preventScrolling(event);
  1380. switch (event.keyCode) {
  1381. case KeyCodes.DOM_VK_UP:
  1382. case KeyCodes.DOM_VK_LEFT:
  1383. this.focusPrevItem();
  1384. return;
  1385. case KeyCodes.DOM_VK_DOWN:
  1386. case KeyCodes.DOM_VK_RIGHT:
  1387. this.focusNextItem();
  1388. return;
  1389. case KeyCodes.DOM_VK_PAGE_UP:
  1390. this.focusItemAtDelta(-(this.pageSize ||
  1391. (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
  1392. return;
  1393. case KeyCodes.DOM_VK_PAGE_DOWN:
  1394. this.focusItemAtDelta(+(this.pageSize ||
  1395. (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
  1396. return;
  1397. case KeyCodes.DOM_VK_HOME:
  1398. this.focusFirstVisibleItem();
  1399. return;
  1400. case KeyCodes.DOM_VK_END:
  1401. this.focusLastVisibleItem();
  1402. return;
  1403. }
  1404. },
  1405. /**
  1406. * The mousePress event listener for this container.
  1407. * @param string name
  1408. * @param MouseEvent event
  1409. */
  1410. _onWidgetMousePress: function (name, event) {
  1411. if (event.button != 0 && !this.allowFocusOnRightClick) {
  1412. // Only allow left-click to trigger this event.
  1413. return;
  1414. }
  1415. let item = this.getItemForElement(event.target);
  1416. if (item) {
  1417. // The container is not empty and we clicked on an actual item.
  1418. this.selectedItem = item;
  1419. // Make sure the current event's target element is also focused.
  1420. this.autoFocusOnInput && item._target.focus();
  1421. }
  1422. },
  1423. /**
  1424. * The predicate used when filtering items. By default, all items in this
  1425. * view are visible.
  1426. *
  1427. * @param Item item
  1428. * The item passing through the filter.
  1429. * @return boolean
  1430. * True if the item should be visible, false otherwise.
  1431. */
  1432. _currentFilterPredicate: function (item) {
  1433. return true;
  1434. },
  1435. /**
  1436. * The predicate used when sorting items. By default, items in this view
  1437. * are sorted by their label.
  1438. *
  1439. * @param Item first
  1440. * The first item used in the comparison.
  1441. * @param Item second
  1442. * The second item used in the comparison.
  1443. * @return number
  1444. * -1 to sort first to a lower index than second
  1445. * 0 to leave first and second unchanged with respect to each other
  1446. * 1 to sort second to a lower index than first
  1447. */
  1448. _currentSortPredicate: function (first, second) {
  1449. return +(first._value.toLowerCase() > second._value.toLowerCase());
  1450. },
  1451. /**
  1452. * Call a method on this widget named `methodName`. Any further arguments are
  1453. * passed on to the method. Returns the result of the method call.
  1454. *
  1455. * @param String methodName
  1456. * The name of the method you want to call.
  1457. * @param args
  1458. * Optional. Any arguments you want to pass through to the method.
  1459. */
  1460. callMethod: function (methodName, ...args) {
  1461. return this._widget[methodName].apply(this._widget, args);
  1462. },
  1463. _widget: null,
  1464. _emptyText: "",
  1465. _headerText: "",
  1466. _preferredValue: "",
  1467. _cachedCommandDispatcher: null
  1468. };
  1469. /**
  1470. * A generator-iterator over all the items in this container.
  1471. */
  1472. Item.prototype[Symbol.iterator] =
  1473. WidgetMethods[Symbol.iterator] = function* () {
  1474. yield* this._itemsByElement.values();
  1475. };