|
1 <?xml version="1.0"?> |
|
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 |
|
6 <bindings |
|
7 xmlns="http://www.mozilla.org/xbl" |
|
8 xmlns:xbl="http://www.mozilla.org/xbl" |
|
9 xmlns:html="http://www.w3.org/1999/xhtml" |
|
10 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> |
|
11 |
|
12 <binding id="richgrid" |
|
13 extends="chrome://global/content/bindings/general.xml#basecontrol"> |
|
14 |
|
15 <content> |
|
16 <html:div id="grid-div" anonid="grid" class="richgrid-grid" xbl:inherits="compact"> |
|
17 <children/> |
|
18 </html:div> |
|
19 </content> |
|
20 |
|
21 <implementation implements="nsIDOMXULSelectControlElement"> |
|
22 <property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/> |
|
23 |
|
24 <property name="isBound" readonly="true" onget="return !!this._grid"/> |
|
25 <property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/> |
|
26 |
|
27 <field name="controller">null</field> |
|
28 |
|
29 <!-- collection of child items excluding empty tiles --> |
|
30 <property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/> |
|
31 <property name="itemCount" readonly="true" onget="return this.items.length;"/> |
|
32 |
|
33 <method name="isItem"> |
|
34 <parameter name="anItem"/> |
|
35 <body> |
|
36 <![CDATA[ |
|
37 // only non-empty child nodes are considered items |
|
38 return anItem && anItem.hasAttribute("value") && |
|
39 anItem.parentNode == this; |
|
40 ]]> |
|
41 </body> |
|
42 </method> |
|
43 |
|
44 <!-- nsIDOMXULMultiSelectControlElement (not fully implemented) --> |
|
45 |
|
46 <method name="clearSelection"> |
|
47 <body> |
|
48 <![CDATA[ |
|
49 // 'selection' and 'selected' are confusingly overloaded here |
|
50 // as richgrid is adopting multi-select behavior, but select/selected are already being |
|
51 // used to describe triggering the default action of a tile |
|
52 if (this._selectedItem){ |
|
53 this._selectedItem.removeAttribute("selected"); |
|
54 this._selectedItem = null; |
|
55 } |
|
56 |
|
57 for (let childItem of this.selectedItems) { |
|
58 childItem.removeAttribute("selected"); |
|
59 } |
|
60 ]]> |
|
61 </body> |
|
62 </method> |
|
63 |
|
64 <method name="toggleItemSelection"> |
|
65 <parameter name="anItem"/> |
|
66 <body> |
|
67 <![CDATA[ |
|
68 if (!this.isItem(anItem)) |
|
69 return; |
|
70 |
|
71 let wasSelected = anItem.selected; |
|
72 if ("single" == this.getAttribute("seltype")) { |
|
73 this.clearSelection(); |
|
74 } |
|
75 this._selectedItem = wasSelected ? null : anItem; |
|
76 if (wasSelected) |
|
77 anItem.removeAttribute("selected"); |
|
78 else |
|
79 anItem.setAttribute("selected", true); |
|
80 this._fireEvent("selectionchange"); |
|
81 ]]> |
|
82 </body> |
|
83 </method> |
|
84 |
|
85 <method name="selectItem"> |
|
86 <parameter name="anItem"/> |
|
87 <body> |
|
88 <![CDATA[ |
|
89 if (!this.isItem(anItem)) |
|
90 return; |
|
91 let wasSelected = anItem.selected, |
|
92 isSingleMode = ("single" == this.getAttribute("seltype")); |
|
93 if (isSingleMode) { |
|
94 this.clearSelection(); |
|
95 } |
|
96 this._selectedItem = anItem; |
|
97 if (wasSelected) { |
|
98 return; |
|
99 } |
|
100 anItem.setAttribute("selected", true); |
|
101 if (isSingleMode) { |
|
102 this._fireEvent("select"); |
|
103 } else { |
|
104 this._fireEvent("selectionchange"); |
|
105 } |
|
106 ]]> |
|
107 </body> |
|
108 </method> |
|
109 |
|
110 <method name="selectNone"> |
|
111 <body> |
|
112 <![CDATA[ |
|
113 let selectedCount = this.selectedItems.length; |
|
114 this.clearSelection(); |
|
115 if (selectedCount && "single" != this.getAttribute("seltype")) { |
|
116 this._fireEvent("selectionchange"); |
|
117 } |
|
118 ]]> |
|
119 </body> |
|
120 </method> |
|
121 |
|
122 <method name="handleItemClick"> |
|
123 <parameter name="aItem"/> |
|
124 <parameter name="aEvent"/> |
|
125 <body> |
|
126 <![CDATA[ |
|
127 if (!(this.isBound && this.isItem(aItem))) |
|
128 return; |
|
129 |
|
130 if ("single" == this.getAttribute("seltype")) { |
|
131 // we'll republish this as a selectionchange event on the grid |
|
132 aEvent.stopPropagation(); |
|
133 this.selectItem(aItem); |
|
134 } |
|
135 |
|
136 if (this.controller && this.controller.handleItemClick) |
|
137 this.controller.handleItemClick(aItem, aEvent); |
|
138 ]]> |
|
139 </body> |
|
140 </method> |
|
141 |
|
142 <method name="handleItemContextMenu"> |
|
143 <parameter name="aItem"/> |
|
144 <parameter name="aEvent"/> |
|
145 <body> |
|
146 <![CDATA[ |
|
147 if (!this.isBound || this.noContext || !this.isItem(aItem)) |
|
148 return; |
|
149 // we'll republish this as a selectionchange event on the grid |
|
150 aEvent.stopPropagation(); |
|
151 this.toggleItemSelection(aItem); |
|
152 ]]> |
|
153 </body> |
|
154 </method> |
|
155 |
|
156 <property name="contextSetName" readonly="true" |
|
157 onget="return this.getAttribute('set-name');"/> |
|
158 |
|
159 <property name="contextActions"> |
|
160 <getter> |
|
161 <![CDATA[ |
|
162 // return the subset of verbs that apply to all selected tiles |
|
163 let tileNodes = this.selectedItems; |
|
164 if (!tileNodes.length) { |
|
165 return new Set(); |
|
166 } |
|
167 |
|
168 // given one or more sets of values, |
|
169 // return a set with only those values present in each |
|
170 let initialItem = tileNodes[0]; |
|
171 |
|
172 let verbSet = new Set(initialItem.contextActions); |
|
173 for (let i=1; i<tileNodes.length; i++){ |
|
174 let set = tileNodes[i].contextActions; |
|
175 for (let item of verbSet) { |
|
176 if (!set.has(item)){ |
|
177 verbSet.delete(item); |
|
178 } |
|
179 } |
|
180 } |
|
181 // add the clear-selection button if more than one tiles are selected |
|
182 if (tileNodes.length > 1) { |
|
183 verbSet.add('clear'); |
|
184 } |
|
185 // returns Set |
|
186 return verbSet; |
|
187 ]]> |
|
188 </getter> |
|
189 </property> |
|
190 |
|
191 <!-- nsIDOMXULSelectControlElement --> |
|
192 |
|
193 <field name="_selectedItem">null</field> |
|
194 <property name="selectedItem" onget="return this._selectedItem;"> |
|
195 <setter> |
|
196 <![CDATA[ |
|
197 this.selectItem(val); |
|
198 ]]> |
|
199 </setter> |
|
200 </property> |
|
201 |
|
202 <!-- partial implementation of multiple selection interface --> |
|
203 <property name="selectedItems"> |
|
204 <getter> |
|
205 <![CDATA[ |
|
206 return this.querySelectorAll("richgriditem[value][selected]"); |
|
207 ]]> |
|
208 </getter> |
|
209 </property> |
|
210 |
|
211 <property name="selectedIndex"> |
|
212 <getter> |
|
213 <![CDATA[ |
|
214 return this.getIndexOfItem(this._selectedItem); |
|
215 ]]> |
|
216 </getter> |
|
217 <setter> |
|
218 <![CDATA[ |
|
219 if (val >= 0) { |
|
220 let selected = this.getItemAtIndex(val); |
|
221 this.selectItem(selected); |
|
222 } else { |
|
223 this.selectNone(); |
|
224 } |
|
225 ]]> |
|
226 </setter> |
|
227 </property> |
|
228 |
|
229 <method name="appendItem"> |
|
230 <parameter name="aLabel"/> |
|
231 <parameter name="aValue"/> |
|
232 <parameter name="aSkipArrange"/> |
|
233 <body> |
|
234 <![CDATA[ |
|
235 let item = this.nextSlot(); |
|
236 item.setAttribute("value", aValue); |
|
237 item.setAttribute("label", aLabel); |
|
238 |
|
239 if (!aSkipArrange) |
|
240 this.arrangeItems(); |
|
241 return item; |
|
242 ]]> |
|
243 </body> |
|
244 </method> |
|
245 |
|
246 <method name="_slotValues"> |
|
247 <body><![CDATA[ |
|
248 return Array.map(this.children, (cnode) => cnode.getAttribute("value")); |
|
249 ]]></body> |
|
250 </method> |
|
251 |
|
252 <property name="minSlots" readonly="true" |
|
253 onget="return this.getAttribute('minSlots') || 3;"/> |
|
254 |
|
255 <method name="clearAll"> |
|
256 <parameter name="aSkipArrange"/> |
|
257 <body> |
|
258 <![CDATA[ |
|
259 const ELEMENT_NODE_TYPE = Components.interfaces.nsIDOMNode.ELEMENT_NODE; |
|
260 let slotCount = this.minSlots; |
|
261 let childIndex = 0; |
|
262 let child = this.firstChild; |
|
263 while (child) { |
|
264 // remove excess elements and non-element nodes |
|
265 if (child.nodeType !== ELEMENT_NODE_TYPE || childIndex+1 > slotCount) { |
|
266 let orphanNode = child; |
|
267 child = orphanNode.nextSibling; |
|
268 this.removeChild(orphanNode); |
|
269 continue; |
|
270 } |
|
271 if (child.hasAttribute("value")) { |
|
272 this._releaseSlot(child); |
|
273 } |
|
274 child = child.nextSibling; |
|
275 childIndex++; |
|
276 } |
|
277 // create our quota of item slots |
|
278 for (let count = this.childElementCount; count < slotCount; count++) { |
|
279 this.appendChild( this._createItemElement() ); |
|
280 } |
|
281 |
|
282 if (!aSkipArrange) |
|
283 this.arrangeItems(); |
|
284 ]]> |
|
285 </body> |
|
286 </method> |
|
287 |
|
288 <method name="_slotAt"> |
|
289 <parameter name="anIndex"/> |
|
290 <body> |
|
291 <![CDATA[ |
|
292 // backfill with new slots as necessary |
|
293 let count = Math.max(1+anIndex, this.minSlots) - this.childElementCount; |
|
294 for (; count > 0; count--) { |
|
295 this.appendChild( this._createItemElement() ); |
|
296 } |
|
297 return this.children[anIndex]; |
|
298 ]]> |
|
299 </body> |
|
300 </method> |
|
301 |
|
302 <method name="nextSlot"> |
|
303 <body> |
|
304 <![CDATA[ |
|
305 if (!this.itemCount) { |
|
306 return this._slotAt(0); |
|
307 } |
|
308 let lastItem = this.items[this.itemCount-1]; |
|
309 let nextIndex = 1 + Array.indexOf(this.children, lastItem); |
|
310 return this._slotAt(nextIndex); |
|
311 ]]> |
|
312 </body> |
|
313 </method> |
|
314 |
|
315 <method name="_releaseSlot"> |
|
316 <parameter name="anItem"/> |
|
317 <body> |
|
318 <![CDATA[ |
|
319 // Flush out data and state attributes so we can recycle this slot/element |
|
320 let exclude = { value: 1, tiletype: 1 }; |
|
321 let attrNames = [attr.name for (attr of anItem.attributes)]; |
|
322 for (let attrName of attrNames) { |
|
323 if (!(attrName in exclude)) |
|
324 anItem.removeAttribute(attrName); |
|
325 } |
|
326 // clear out inline styles |
|
327 anItem.removeAttribute("style"); |
|
328 // finally clear the value, which should apply the richgrid-empty-item binding |
|
329 anItem.removeAttribute("value"); |
|
330 ]]> |
|
331 </body> |
|
332 </method> |
|
333 |
|
334 <method name="insertItemAt"> |
|
335 <parameter name="anIndex"/> |
|
336 <parameter name="aLabel"/> |
|
337 <parameter name="aValue"/> |
|
338 <parameter name="aSkipArrange"/> |
|
339 <body> |
|
340 <![CDATA[ |
|
341 anIndex = Math.min(this.itemCount, anIndex); |
|
342 let insertedItem; |
|
343 let existing = this.getItemAtIndex(anIndex); |
|
344 if (existing) { |
|
345 // use an empty slot if we have one, otherwise insert it |
|
346 let childIndex = Array.indexOf(this.children, existing); |
|
347 if (childIndex > 0 && !this.children[childIndex-1].hasAttribute("value")) { |
|
348 insertedItem = this.children[childIndex-1]; |
|
349 } else { |
|
350 insertedItem = this.insertBefore(this._createItemElement(),existing); |
|
351 } |
|
352 } |
|
353 if (!insertedItem) { |
|
354 insertedItem = this._slotAt(anIndex); |
|
355 } |
|
356 insertedItem.setAttribute("value", aValue); |
|
357 insertedItem.setAttribute("label", aLabel); |
|
358 if (!aSkipArrange) |
|
359 this.arrangeItems(); |
|
360 return insertedItem; |
|
361 ]]> |
|
362 </body> |
|
363 </method> |
|
364 |
|
365 <method name="removeItemAt"> |
|
366 <parameter name="anIndex"/> |
|
367 <parameter name="aSkipArrange"/> |
|
368 <body> |
|
369 <![CDATA[ |
|
370 let item = this.getItemAtIndex(anIndex); |
|
371 if (!item) |
|
372 return null; |
|
373 return this.removeItem(item, aSkipArrange); |
|
374 ]]> |
|
375 </body> |
|
376 </method> |
|
377 |
|
378 <method name="removeItem"> |
|
379 <parameter name="aItem"/> |
|
380 <parameter name="aSkipArrange"/> |
|
381 <body> |
|
382 <![CDATA[ |
|
383 if (!this.isItem(aItem)) |
|
384 return null; |
|
385 |
|
386 let removal = this.removeChild(aItem); |
|
387 // replace the slot if necessary |
|
388 if (this.childElementCount < this.minSlots) { |
|
389 this.nextSlot(); |
|
390 } |
|
391 |
|
392 if (removal && !aSkipArrange) |
|
393 this.arrangeItems(); |
|
394 |
|
395 // note that after removal the node is unbound |
|
396 // so none of the richgriditem binding methods & properties are available |
|
397 return removal; |
|
398 ]]> |
|
399 </body> |
|
400 </method> |
|
401 |
|
402 <method name="getIndexOfItem"> |
|
403 <parameter name="anItem"/> |
|
404 <body> |
|
405 <![CDATA[ |
|
406 if (!this.isItem(anItem)) |
|
407 return -1; |
|
408 |
|
409 return Array.indexOf(this.items, anItem); |
|
410 ]]> |
|
411 </body> |
|
412 </method> |
|
413 |
|
414 <method name="getItemAtIndex"> |
|
415 <parameter name="anIndex"/> |
|
416 <body> |
|
417 <![CDATA[ |
|
418 if (!this._isIndexInBounds(anIndex)) |
|
419 return null; |
|
420 return this.items.item(anIndex); |
|
421 ]]> |
|
422 </body> |
|
423 </method> |
|
424 |
|
425 <method name="getItemsByUrl"> |
|
426 <parameter name="aUrl"/> |
|
427 <body> |
|
428 <![CDATA[ |
|
429 return this.querySelectorAll('richgriditem[value="'+aUrl+'"]'); |
|
430 ]]> |
|
431 </body> |
|
432 </method> |
|
433 |
|
434 <!-- Interface for offsetting selection and checking bounds --> |
|
435 |
|
436 <property name="isSelectionAtStart" readonly="true" |
|
437 onget="return this.selectedIndex == 0;"/> |
|
438 |
|
439 <property name="isSelectionAtEnd" readonly="true" |
|
440 onget="return this.selectedIndex == (this.itemCount - 1);"/> |
|
441 |
|
442 <property name="isSelectionInStartRow" readonly="true"> |
|
443 <getter> |
|
444 <![CDATA[ |
|
445 return this.selectedIndex < this.columnCount; |
|
446 ]]> |
|
447 </getter> |
|
448 </property> |
|
449 |
|
450 <property name="isSelectionInEndRow" readonly="true"> |
|
451 <getter> |
|
452 <![CDATA[ |
|
453 let lowerBound = (this.rowCount - 1) * this.columnCount; |
|
454 let higherBound = this.rowCount * this.columnCount; |
|
455 |
|
456 return this.selectedIndex >= lowerBound && |
|
457 this.selectedIndex < higherBound; |
|
458 ]]> |
|
459 </getter> |
|
460 </property> |
|
461 |
|
462 <method name="offsetSelection"> |
|
463 <parameter name="aOffset"/> |
|
464 <body> |
|
465 <![CDATA[ |
|
466 let newIndex = this.selectedIndex + aOffset; |
|
467 if (this._isIndexInBounds(newIndex)) |
|
468 this.selectedIndex = newIndex; |
|
469 ]]> |
|
470 </body> |
|
471 </method> |
|
472 |
|
473 <method name="offsetSelectionByRow"> |
|
474 <parameter name="aRowOffset"/> |
|
475 <body> |
|
476 <![CDATA[ |
|
477 let newIndex = this.selectedIndex + (this.columnCount * aRowOffset); |
|
478 if (this._isIndexInBounds(newIndex)) |
|
479 this.selectedIndex -= this.columnCount; |
|
480 ]]> |
|
481 </body> |
|
482 </method> |
|
483 |
|
484 <!-- Interface for grid layout management --> |
|
485 |
|
486 <field name="_rowCount">0</field> |
|
487 <property name="rowCount" readonly="true" onget="return this._rowCount;"/> |
|
488 <field name="_columnCount">0</field> |
|
489 <property name="columnCount" readonly="true" onget="return this._columnCount;"/> |
|
490 <property name="_containerSize"> |
|
491 <getter><![CDATA[ |
|
492 // return the rect that represents our bounding box |
|
493 let containerNode = this.hasAttribute("flex") ? this : this.parentNode; |
|
494 let rect = containerNode.getBoundingClientRect(); |
|
495 // return falsy if the container has no height |
|
496 return rect.height ? { |
|
497 width: rect.width, |
|
498 height: rect.height |
|
499 } : null; |
|
500 ]]></getter> |
|
501 </property> |
|
502 |
|
503 <property name="_itemSize"> |
|
504 <getter><![CDATA[ |
|
505 // return the dimensions that represent an item in the grid |
|
506 |
|
507 // grab tile/item dimensions |
|
508 this._tileSizes = this._getTileSizes(); |
|
509 |
|
510 let type = this.getAttribute("tiletype") || "default"; |
|
511 let dims = this._tileSizes && this._tileSizes[type]; |
|
512 if (!dims) { |
|
513 throw new Error("Missing tile sizes for '" + type + "' type"); |
|
514 } |
|
515 return dims; |
|
516 ]]></getter> |
|
517 </property> |
|
518 |
|
519 <!-- do conditions allow layout/arrange of the grid? --> |
|
520 <property name="_canLayout" readonly="true"> |
|
521 <getter> |
|
522 <![CDATA[ |
|
523 if (!(this._grid && this._grid.style)) { |
|
524 return false; |
|
525 } |
|
526 |
|
527 let gridItemSize = this._itemSize; |
|
528 |
|
529 // If we don't have valid item dimensions we can't arrange yet |
|
530 if (!(gridItemSize && gridItemSize.height)) { |
|
531 return false; |
|
532 } |
|
533 |
|
534 let container = this._containerSize; |
|
535 // If we don't have valid container dimensions we can't arrange yet |
|
536 if (!(container && container.height)) { |
|
537 return false; |
|
538 } |
|
539 return true; |
|
540 ]]> |
|
541 </getter> |
|
542 </property> |
|
543 |
|
544 <field name="_scheduledArrangeItemsTimerId">null</field> |
|
545 <field name="_scheduledArrangeItemsTries">0</field> |
|
546 <field name="_maxArrangeItemsRetries">5</field> |
|
547 |
|
548 <method name="_scheduleArrangeItems"> |
|
549 <parameter name="aTime"/> |
|
550 <body> |
|
551 <![CDATA[ |
|
552 // cap the number of times we reschedule calling arrangeItems |
|
553 if ( |
|
554 !this._scheduledArrangeItemsTimerId && |
|
555 this._maxArrangeItemsRetries > this._scheduledArrangeItemsTries |
|
556 ) { |
|
557 this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), aTime || 0); |
|
558 // track how many times we've attempted arrangeItems |
|
559 this._scheduledArrangeItemsTries++; |
|
560 } |
|
561 ]]> |
|
562 </body> |
|
563 </method> |
|
564 |
|
565 <method name="arrangeItems"> |
|
566 <body> |
|
567 <![CDATA[ |
|
568 if (this.hasAttribute("deferlayout")) { |
|
569 return; |
|
570 } |
|
571 if (!this._canLayout) { |
|
572 // try again later |
|
573 this._scheduleArrangeItems(); |
|
574 return; |
|
575 } |
|
576 |
|
577 let itemDims = this._itemSize; |
|
578 let containerDims = this._containerSize; |
|
579 let slotsCount = this.childElementCount; |
|
580 |
|
581 // reset the flags |
|
582 if (this._scheduledArrangeItemsTimerId) { |
|
583 clearTimeout(this._scheduledArrangeItemsTimerId); |
|
584 delete this._scheduledArrangeItemsTimerId; |
|
585 } |
|
586 this._scheduledArrangeItemsTries = 0; |
|
587 |
|
588 // clear explicit width and columns before calculating from avail. height again |
|
589 let gridStyle = this._grid.style; |
|
590 gridStyle.removeProperty("min-width"); |
|
591 gridStyle.removeProperty("-moz-column-count"); |
|
592 |
|
593 if (this.hasAttribute("vertical")) { |
|
594 this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1; |
|
595 this._rowCount = Math.floor(slotsCount / this._columnCount); |
|
596 } else { |
|
597 // rows attribute is fixed number of rows |
|
598 let maxRows = Math.floor(containerDims.height / itemDims.height); |
|
599 this._rowCount = this.getAttribute("rows") ? |
|
600 // fit indicated rows when possible |
|
601 Math.min(maxRows, this.getAttribute("rows")) : |
|
602 // at least 1 row |
|
603 Math.min(maxRows, slotsCount) || 1; |
|
604 |
|
605 // columns attribute is min number of cols |
|
606 this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1; |
|
607 if (this.getAttribute("columns")) { |
|
608 this._columnCount = Math.max(this._columnCount, this.getAttribute("columns")); |
|
609 } |
|
610 } |
|
611 |
|
612 // width is typically auto, cap max columns by truncating items collection |
|
613 // or, setting max-width style property with overflow hidden |
|
614 if (this._columnCount) { |
|
615 gridStyle.MozColumnCount = this._columnCount; |
|
616 } |
|
617 this._fireEvent("arranged"); |
|
618 ]]> |
|
619 </body> |
|
620 </method> |
|
621 <method name="arrangeItemsNow"> |
|
622 <body> |
|
623 <![CDATA[ |
|
624 this.removeAttribute("deferlayout"); |
|
625 // cancel any scheduled arrangeItems and reset flags |
|
626 if (this._scheduledArrangeItemsTimerId) { |
|
627 clearTimeout(this._scheduledArrangeItemsTimerId); |
|
628 delete this._scheduledArrangeItemsTimerId; |
|
629 } |
|
630 this._scheduledArrangeItemsTries = 0; |
|
631 // pass over any params |
|
632 return this.arrangeItems.apply(this, arguments); |
|
633 ]]> |
|
634 </body> |
|
635 </method> |
|
636 |
|
637 <!-- Inteface to suppress selection events --> |
|
638 <property name="suppressOnSelect" |
|
639 onget="return this.getAttribute('suppressonselect') == 'true';" |
|
640 onset="this.setAttribute('suppressonselect', val);"/> |
|
641 <property name="noContext" |
|
642 onget="return this.hasAttribute('nocontext');" |
|
643 onset="if (val) this.setAttribute('nocontext', true); else this.removeAttribute('nocontext');"/> |
|
644 <property name="crossSlideBoundary" |
|
645 onget="return this.hasAttribute('crossslideboundary')? this.getAttribute('crossslideboundary') : Infinity;"/> |
|
646 |
|
647 <!-- Internal methods --> |
|
648 <field name="_xslideHandler"/> |
|
649 <constructor> |
|
650 <![CDATA[ |
|
651 // create our quota of item slots |
|
652 for (let count = this.childElementCount, slotCount = this.minSlots; |
|
653 count < slotCount; count++) { |
|
654 this.appendChild( this._createItemElement() ); |
|
655 } |
|
656 if (this.controller && this.controller.gridBoundCallback != undefined) |
|
657 this.controller.gridBoundCallback(); |
|
658 // XXX This event was never actually implemented (bug 223411). |
|
659 let event = document.createEvent("Events"); |
|
660 event.initEvent("contentgenerated", true, true); |
|
661 this.dispatchEvent(event); |
|
662 ]]> |
|
663 </constructor> |
|
664 |
|
665 <destructor> |
|
666 <![CDATA[ |
|
667 this.disableCrossSlide(); |
|
668 ]]> |
|
669 </destructor> |
|
670 <method name="enableCrossSlide"> |
|
671 <body> |
|
672 <![CDATA[ |
|
673 // set up cross-slide gesture handling for multiple-selection grids |
|
674 if (!this._xslideHandler && |
|
675 "undefined" !== typeof CrossSlide && !this.noContext) { |
|
676 this._xslideHandler = new CrossSlide.Handler(this, { |
|
677 REARRANGESTART: this.crossSlideBoundary |
|
678 }); |
|
679 } |
|
680 ]]> |
|
681 </body> |
|
682 </method> |
|
683 |
|
684 <method name="disableCrossSlide"> |
|
685 <body> |
|
686 <![CDATA[ |
|
687 if (this._xslideHandler) { |
|
688 this.removeEventListener("touchstart", this._xslideHandler); |
|
689 this.removeEventListener("touchmove", this._xslideHandler); |
|
690 this.removeEventListener("touchend", this._xslideHandler); |
|
691 this._xslideHandler = null; |
|
692 } |
|
693 ]]> |
|
694 </body> |
|
695 </method> |
|
696 |
|
697 <property name="tileWidth" readonly="true" onget="return this._itemSize.width"/> |
|
698 <property name="tileHeight" readonly="true" onget="return this._itemSize.height"/> |
|
699 <field name="_tileStyleSheetName">"tiles.css"</field> |
|
700 <method name="_getTileSizes"> |
|
701 <body> |
|
702 <![CDATA[ |
|
703 // Tile sizes are constants, this avoids the need to measure a rendered item before grid layout |
|
704 // The defines.inc used by the theme CSS is the single source of truth for these values |
|
705 // This method locates and parses out (just) those dimensions from the stylesheet |
|
706 |
|
707 let typeSizes = this.ownerDocument.defaultView._richgridTileSizes; |
|
708 if (typeSizes && typeSizes["default"]) { |
|
709 return typeSizes; |
|
710 } |
|
711 |
|
712 // cache sizes on the global window object, for reuse between bound nodes |
|
713 typeSizes = this.ownerDocument.defaultView._richgridTileSizes = {}; |
|
714 |
|
715 let sheets = this.ownerDocument.styleSheets; |
|
716 // The (first matching) rules that will give us tile type => width/height values |
|
717 // The keys in this object are string-matched against the selectorText |
|
718 // of rules in our stylesheet. Quoted values in a selector will always use " not ' |
|
719 let typeSelectors = { |
|
720 'richgriditem' : "default", |
|
721 'richgriditem[tiletype="thumbnail"]': "thumbnail", |
|
722 'richgriditem[search]': "search", |
|
723 'richgriditem[compact]': "compact" |
|
724 }; |
|
725 let rules, sheet; |
|
726 for (let i=0; (sheet=sheets[i]); i++) { |
|
727 if (sheet.href && sheet.href.endsWith( this._tileStyleSheetName )) { |
|
728 rules = sheet.cssRules; |
|
729 break; |
|
730 } |
|
731 } |
|
732 if (rules) { |
|
733 // walk the stylesheet rules until we've matched all our selectors |
|
734 for (let i=0, rule;(rule=rules[i]); i++) { |
|
735 let type = rule.selectorText && typeSelectors[rule.selectorText]; |
|
736 if (type) { |
|
737 let sizes = typeSizes[type] = {}; |
|
738 typeSelectors[type] = null; |
|
739 delete typeSelectors[type]; |
|
740 // we assume px unit for tile dimension values |
|
741 sizes.width = parseInt(rule.style.getPropertyValue("width")); |
|
742 sizes.height = parseInt(rule.style.getPropertyValue("height")); |
|
743 } |
|
744 if (!Object.keys(typeSelectors).length) |
|
745 break; |
|
746 } |
|
747 } else { |
|
748 throw new Error("Failed to find stylesheet to parse out richgriditem dimensions\n"); |
|
749 } |
|
750 return typeSizes; |
|
751 ]]> |
|
752 </body> |
|
753 </method> |
|
754 |
|
755 <method name="_isIndexInBounds"> |
|
756 <parameter name="anIndex"/> |
|
757 <body> |
|
758 <![CDATA[ |
|
759 return anIndex >= 0 && anIndex < this.itemCount; |
|
760 ]]> |
|
761 </body> |
|
762 </method> |
|
763 |
|
764 <method name="_createItemElement"> |
|
765 <parameter name="aLabel"/> |
|
766 <parameter name="aValue"/> |
|
767 <body> |
|
768 <![CDATA[ |
|
769 let item = this.ownerDocument.createElement("richgriditem"); |
|
770 if (aValue) { |
|
771 item.setAttribute("value", aValue); |
|
772 } |
|
773 if (aLabel) { |
|
774 item.setAttribute("label", aLabel); |
|
775 } |
|
776 if (this.hasAttribute("tiletype")) { |
|
777 item.setAttribute("tiletype", this.getAttribute("tiletype")); |
|
778 } |
|
779 return item; |
|
780 ]]> |
|
781 </body> |
|
782 </method> |
|
783 |
|
784 <method name="_fireEvent"> |
|
785 <parameter name="aType"/> |
|
786 <body> |
|
787 <![CDATA[ |
|
788 switch (aType) { |
|
789 case "select" : |
|
790 case "selectionchange" : |
|
791 if (this.suppressOnSelect) |
|
792 return; |
|
793 break; |
|
794 case "arranged" : |
|
795 break; |
|
796 } |
|
797 |
|
798 let event = document.createEvent("Events"); |
|
799 event.initEvent(aType, true, true); |
|
800 this.dispatchEvent(event); |
|
801 ]]> |
|
802 </body> |
|
803 </method> |
|
804 |
|
805 <method name="bendItem"> |
|
806 <parameter name="aItem"/> |
|
807 <parameter name="aEvent"/> |
|
808 <body><![CDATA[ |
|
809 // apply the transform to the contentBox element of the item |
|
810 let bendNode = this.isItem(aItem) ? aItem._contentBox : null; |
|
811 if (!bendNode || aItem.hasAttribute("bending")) |
|
812 return; |
|
813 |
|
814 let event = aEvent; |
|
815 let rect = bendNode.getBoundingClientRect(); |
|
816 let angle; |
|
817 let x = (event.clientX - rect.left) / rect.width; |
|
818 let y = (event.clientY - rect.top) / rect.height; |
|
819 let perspective = '450px'; |
|
820 // scaling factors for the angle of deflection, |
|
821 // based on the aspect-ratio of the tile |
|
822 let aspectRatio = rect.width/rect.height; |
|
823 let deflectX = 10 * Math.ceil(1/aspectRatio); |
|
824 let deflectY = 10 * Math.ceil(aspectRatio); |
|
825 |
|
826 if (Math.abs(x - .5) < .1 && Math.abs(y - .5) < .1) { |
|
827 bendNode.style.transform = "perspective("+perspective+") translateZ(-10px)"; |
|
828 } |
|
829 else if (x > y) { |
|
830 if (1 - y > x) { |
|
831 angle = Math.ceil((.5 - y) * deflectY); |
|
832 bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)"; |
|
833 bendNode.style.transformOrigin = "center bottom"; |
|
834 } else { |
|
835 angle = Math.ceil((x - .5) * deflectX); |
|
836 bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)"; |
|
837 bendNode.style.transformOrigin = "left center"; |
|
838 } |
|
839 } else { |
|
840 if (1 - y < x) { |
|
841 angle = -Math.ceil((y - .5) * deflectY); |
|
842 bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)"; |
|
843 bendNode.style.transformOrigin = "center top"; |
|
844 } else { |
|
845 angle = -Math.ceil((.5 - x) * deflectX); |
|
846 bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)"; |
|
847 bendNode.style.transformOrigin = "right center"; |
|
848 } |
|
849 } |
|
850 // mark when bend effect is applied |
|
851 aItem.setAttribute("bending", true); |
|
852 ]]></body> |
|
853 </method> |
|
854 |
|
855 <method name="unbendItem"> |
|
856 <parameter name="aItem"/> |
|
857 <body><![CDATA[ |
|
858 // clear the 'bend' transform on the contentBox element of the item |
|
859 let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox; |
|
860 if (bendNode && aItem.hasAttribute("bending")) { |
|
861 bendNode.style.removeProperty('transform'); |
|
862 bendNode.style.removeProperty('transformOrigin'); |
|
863 aItem.removeAttribute("bending"); |
|
864 } |
|
865 ]]></body> |
|
866 </method> |
|
867 </implementation> |
|
868 <handlers> |
|
869 <!-- item bend effect handlers --> |
|
870 <handler event="mousedown" button="0" phase="capturing" action="this.bendItem(event.target, event)"/> |
|
871 <handler event="touchstart" action="this.bendItem(event.target, event.touches[0])"/> |
|
872 <handler event="mouseup" button="0" action="this.unbendItem(event.target)"/> |
|
873 <handler event="mouseout" button="0" action="this.unbendItem(event.target)"/> |
|
874 <handler event="touchend" action="this.unbendItem(event.target)"/> |
|
875 <handler event="touchcancel" action="this.unbendItem(event.target)"/> |
|
876 <!-- /item bend effect handler --> |
|
877 |
|
878 <handler event="context-action"> |
|
879 <![CDATA[ |
|
880 // context-action is an event fired by the appbar typically |
|
881 // which directs us to do something to the selected tiles |
|
882 switch (event.action) { |
|
883 case "clear": |
|
884 this.selectNone(); |
|
885 break; |
|
886 default: |
|
887 if (this.controller && this.controller.doActionOnSelectedTiles) { |
|
888 this.controller.doActionOnSelectedTiles(event.action, event); |
|
889 } |
|
890 } |
|
891 ]]> |
|
892 </handler> |
|
893 <handler event="MozCrossSliding"> |
|
894 <![CDATA[ |
|
895 // MozCrossSliding is swipe gesture across a tile |
|
896 // The tile should follow the drag to reinforce the gesture |
|
897 // (with inertia/speedbump behavior) |
|
898 let state = event.crossSlidingState; |
|
899 let thresholds = this._xslideHandler.thresholds; |
|
900 let transformValue; |
|
901 switch (state) { |
|
902 case "cancelled": |
|
903 this.unbendItem(event.target); |
|
904 event.target.removeAttribute('crosssliding'); |
|
905 // hopefully nothing else is transform-ing the tile |
|
906 event.target.style.removeProperty('transform'); |
|
907 break; |
|
908 case "dragging": |
|
909 case "selecting": |
|
910 // remove bend/depress effect when a cross-slide begins |
|
911 this.unbendItem(event.target); |
|
912 |
|
913 event.target.setAttribute("crosssliding", true); |
|
914 // just track the mouse in the initial phases of the drag gesture |
|
915 transformValue = (event.direction=='x') ? |
|
916 'translateX('+event.delta+'px)' : |
|
917 'translateY('+event.delta+'px)'; |
|
918 event.target.style.transform = transformValue; |
|
919 break; |
|
920 case "selectSpeedBumping": |
|
921 case "speedBumping": |
|
922 event.target.setAttribute('crosssliding', true); |
|
923 // in speed-bump phase, we add inertia to the drag |
|
924 let offset = CrossSlide.speedbump( |
|
925 event.delta, |
|
926 thresholds.SPEEDBUMPSTART, |
|
927 thresholds.SPEEDBUMPEND |
|
928 ); |
|
929 transformValue = (event.direction=='x') ? |
|
930 'translateX('+offset+'px)' : |
|
931 'translateY('+offset+'px)'; |
|
932 event.target.style.transform = transformValue; |
|
933 break; |
|
934 // "rearranging" case not used or implemented here |
|
935 case "completed": |
|
936 event.target.removeAttribute('crosssliding'); |
|
937 event.target.style.removeProperty('transform'); |
|
938 break; |
|
939 } |
|
940 ]]> |
|
941 </handler> |
|
942 <handler event="MozCrossSlideSelect"> |
|
943 <![CDATA[ |
|
944 if (this.noContext) |
|
945 return; |
|
946 this.toggleItemSelection(event.target); |
|
947 ]]> |
|
948 </handler> |
|
949 </handlers> |
|
950 </binding> |
|
951 |
|
952 <binding id="richgrid-item"> |
|
953 <content> |
|
954 <html:div anonid="anon-tile" class="tile-content" xbl:inherits="customImage"> |
|
955 <html:div class="tile-start-container" xbl:inherits="customImage"> |
|
956 <html:div class="tile-icon-box" anonid="anon-tile-icon-box"><xul:image anonid="anon-tile-icon" xbl:inherits="src=iconURI"/></html:div> |
|
957 </html:div> |
|
958 <html:div anonid="anon-tile-label" class="tile-desc" xbl:inherits="xbl:text=label"/> |
|
959 </html:div> |
|
960 </content> |
|
961 |
|
962 <implementation> |
|
963 <property name="isBound" readonly="true" onget="return !!this._icon"/> |
|
964 <constructor> |
|
965 <![CDATA[ |
|
966 this.refresh(); |
|
967 ]]> |
|
968 </constructor> |
|
969 <property name="_contentBox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-content');"/> |
|
970 <property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-desc');"/> |
|
971 <property name="_top" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-start-container');"/> |
|
972 <property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon');"/> |
|
973 <property name="_iconBox" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon-box');"/> |
|
974 <property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-label');"/> |
|
975 <property name="iconSrc" |
|
976 onset="this._icon.src = val; this.setAttribute('iconURI', val);" |
|
977 onget="return this._icon.src;" /> |
|
978 |
|
979 <property name="selected" |
|
980 onget="return this.hasAttribute('selected');" |
|
981 onset="if (val) this.setAttribute('selected', val); else this.removeAttribute('selected');" /> |
|
982 <property name="url" |
|
983 onget="return this.getAttribute('value')" |
|
984 onset="this.setAttribute('value', val);"/> |
|
985 <property name="label" |
|
986 onget="return this._label.getAttribute('value')" |
|
987 onset="this.setAttribute('label', val); this._label.setAttribute('value', val);"/> |
|
988 <property name="pinned" |
|
989 onget="return this.hasAttribute('pinned')" |
|
990 onset="if (val) { this.setAttribute('pinned', val) } else this.removeAttribute('pinned');"/> |
|
991 |
|
992 <method name="refresh"> |
|
993 <body> |
|
994 <![CDATA[ |
|
995 // Prevent an exception in case binding is not done yet. |
|
996 if (!this.isBound) |
|
997 return; |
|
998 |
|
999 // Seed the binding properties from bound-node attribute values |
|
1000 // Usage: node.refresh() |
|
1001 // - reinitializes all binding properties from their associated attributes |
|
1002 |
|
1003 this.iconSrc = this.getAttribute('iconURI'); |
|
1004 this.color = this.getAttribute("customColor"); |
|
1005 this.label = this.getAttribute('label'); |
|
1006 // url getter just looks directly at attribute |
|
1007 // selected getter just looks directly at attribute |
|
1008 // pinned getter just looks directly at attribute |
|
1009 // value getter just looks directly at attribute |
|
1010 this._contextActions = null; |
|
1011 this.refreshBackgroundImage(); |
|
1012 ]]> |
|
1013 </body> |
|
1014 </method> |
|
1015 |
|
1016 <property name="control"> |
|
1017 <getter><![CDATA[ |
|
1018 let parent = this.parentNode; |
|
1019 while (parent && parent != this.ownerDocument.documentElement) { |
|
1020 if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement) |
|
1021 return parent; |
|
1022 parent = parent.parentNode; |
|
1023 } |
|
1024 return null; |
|
1025 ]]></getter> |
|
1026 </property> |
|
1027 |
|
1028 <property name="color" onget="return this.getAttribute('customColor');"> |
|
1029 <setter><![CDATA[ |
|
1030 if (val) { |
|
1031 this.setAttribute("customColor", val); |
|
1032 this._contentBox.style.backgroundColor = val; |
|
1033 |
|
1034 // overridden in tiles.css for non-thumbnail types |
|
1035 this._label.style.backgroundColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.8)'); |
|
1036 |
|
1037 // Small icons get a border+background-color treatment. |
|
1038 // See tiles.css for large icon overrides |
|
1039 this._iconBox.style.borderColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.6)'); |
|
1040 this._iconBox.style.backgroundColor = this.hasAttribute("tintColor") ? |
|
1041 this.getAttribute("tintColor") : "#fff"; |
|
1042 } else { |
|
1043 this.removeAttribute("customColor"); |
|
1044 this._contentBox.style.removeProperty("background-color"); |
|
1045 this._label.style.removeProperty("background-color"); |
|
1046 this._iconBox.style.removeProperty("border-color"); |
|
1047 this._iconBox.style.removeProperty("background-color"); |
|
1048 } |
|
1049 ]]></setter> |
|
1050 </property> |
|
1051 |
|
1052 <property name="backgroundImage" onget="return this.getAttribute('customImage');"> |
|
1053 <setter><![CDATA[ |
|
1054 if (val) { |
|
1055 this.setAttribute("customImage", val); |
|
1056 this._top.style.backgroundImage = val; |
|
1057 } else { |
|
1058 this.removeAttribute("customImage"); |
|
1059 this._top.style.removeProperty("background-image"); |
|
1060 } |
|
1061 ]]></setter> |
|
1062 </property> |
|
1063 |
|
1064 <method name="refreshBackgroundImage"> |
|
1065 <body><![CDATA[ |
|
1066 if (!this.isBound) |
|
1067 return; |
|
1068 if (this.backgroundImage) { |
|
1069 this._top.style.removeProperty("background-image"); |
|
1070 this._top.style.setProperty("background-image", this.backgroundImage); |
|
1071 } |
|
1072 ]]></body> |
|
1073 </method> |
|
1074 |
|
1075 <field name="_contextActions">null</field> |
|
1076 <property name="contextActions"> |
|
1077 <getter> |
|
1078 <![CDATA[ |
|
1079 if (!this._contextActions) { |
|
1080 this._contextActions = new Set(); |
|
1081 let actionSet = this._contextActions; |
|
1082 let actions = this.getAttribute("data-contextactions"); |
|
1083 if (actions) { |
|
1084 actions.split(/[,\s]+/).forEach(function(verb){ |
|
1085 actionSet.add(verb); |
|
1086 }); |
|
1087 } |
|
1088 } |
|
1089 return this._contextActions; |
|
1090 ]]> |
|
1091 </getter> |
|
1092 </property> |
|
1093 </implementation> |
|
1094 |
|
1095 <handlers> |
|
1096 <handler event="click" button="0"> |
|
1097 <![CDATA[ |
|
1098 // left-click/touch handler |
|
1099 this.control.handleItemClick(this, event); |
|
1100 // Stop this from bubbling, when the richgrid container |
|
1101 // receives click events, we blur the nav bar. |
|
1102 event.stopPropagation(); |
|
1103 ]]> |
|
1104 </handler> |
|
1105 |
|
1106 <handler event="contextmenu"> |
|
1107 <![CDATA[ |
|
1108 // fires for right-click, long-click and (keyboard) contextmenu input |
|
1109 // toggle the selected state of tiles in a grid |
|
1110 let gridParent = this.control; |
|
1111 if (!this.isBound || !gridParent) |
|
1112 return; |
|
1113 gridParent.handleItemContextMenu(this, event); |
|
1114 ]]> |
|
1115 </handler> |
|
1116 </handlers> |
|
1117 </binding> |
|
1118 |
|
1119 <binding id="richgrid-empty-item"> |
|
1120 <content> |
|
1121 <html:div anonid="anon-tile" class="tile-content"></html:div> |
|
1122 </content> |
|
1123 </binding> |
|
1124 |
|
1125 </bindings> |