browser/metro/base/content/bindings/grid.xml

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:e791e2162727
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>

mercurial