source: Dev/trunk/src/client/dojox/mobile/ListItem.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 18.1 KB
Line 
1define([
2        "dojo/_base/array",
3        "dojo/_base/declare",
4        "dojo/_base/lang",
5        "dojo/dom-class",
6        "dojo/dom-construct",
7        "dojo/dom-style",
8        "dojo/dom-attr",
9        "dijit/registry",
10        "dijit/_WidgetBase",
11        "./iconUtils",
12        "./_ItemBase",
13        "./ProgressIndicator",
14        "dojo/has",
15        "dojo/has!dojo-bidi?dojox/mobile/bidi/ListItem"
16], function(array, declare, lang, domClass, domConstruct, domStyle, domAttr, registry, WidgetBase, iconUtils, ItemBase, ProgressIndicator, has,  BidiListItem){
17
18        // module:
19        //              dojox/mobile/ListItem
20
21        var ListItem = declare(has("dojo-bidi") ? "dojox.mobile.NonBidiListItem" : "dojox.mobile.ListItem", ItemBase, {
22                // summary:
23                //              An item of either RoundRectList or EdgeToEdgeList.
24                // description:
25                //              ListItem represents an item of either RoundRectList or
26                //              EdgeToEdgeList. There are three ways to move to a different view:
27                //              moveTo, href, and url. You can choose only one of them.
28                //
29                //              A child DOM node (or widget) can have the layout attribute,
30                //              whose value is "left", "right", or "center". Such nodes will be
31                //              aligned as specified.
32                // example:
33                // |    <li data-dojo-type="dojox.mobile.ListItem">
34                // |            <div layout="left">Left Node</div>
35                // |            <div layout="right">Right Node</div>
36                // |            <div layout="center">Center Node</div>
37                // |    </li>
38                //
39                //              Note that even if you specify variableHeight="true" for the list
40                //              and place a tall object inside the layout node as in the example
41                //              below, the layout node does not expand as you may expect,
42                //              because layout node is aligned using float:left, float:right, or
43                //              position:absolute.
44                // example:
45                // |    <li data-dojo-type="dojox.mobile.ListItem" variableHeight="true">
46                // |            <div layout="left"><img src="large-picture.jpg"></div>
47                // |    </li>
48
49                // rightText: String
50                //              A right-aligned text to display on the item.
51                rightText: "",
52
53                // rightIcon: String
54                //              An icon to display at the right hand side of the item. The value
55                //              can be either a path for an image file or a class name of a DOM
56                //              button.
57                rightIcon: "",
58
59                // rightIcon2: String
60                //              An icon to display at the left of the rightIcon. The value can
61                //              be either a path for an image file or a class name of a DOM
62                //              button.
63                rightIcon2: "",
64
65                // deleteIcon: String
66                //              A delete icon to display at the left of the item. The value can
67                //              be either a path for an image file or a class name of a DOM
68                //              button.
69                deleteIcon: "",
70
71                // anchorLabel: Boolean
72                //              If true, the label text becomes a clickable anchor text. When
73                //              the user clicks on the text, the onAnchorLabelClicked handler is
74                //              called. You can override or connect to the handler and implement
75                //              any action. The handler has no default action.
76                anchorLabel: false,
77
78                // noArrow: Boolean
79                //              If true, the right hand side arrow is not displayed.
80                noArrow: false,
81
82                // checked: Boolean
83                //              If true, a check mark is displayed at the right of the item.
84                checked: false,
85
86                // arrowClass: String
87                //              An icon to display as an arrow. The value can be either a path
88                //              for an image file or a class name of a DOM button.
89                arrowClass: "",
90
91                // checkClass: String
92                //              An icon to display as a check mark. The value can be either a
93                //              path for an image file or a class name of a DOM button.
94                checkClass: "",
95
96                // uncheckClass: String
97                //              An icon to display as an uncheck mark. The value can be either a
98                //              path for an image file or a class name of a DOM button.
99                uncheckClass: "",
100
101                // variableHeight: Boolean
102                //              If true, the height of the item varies according to its content.
103                variableHeight: false,
104
105                // rightIconTitle: String
106                //              An alt text for the right icon.
107                rightIconTitle: "",
108
109                // rightIcon2Title: String
110                //              An alt text for the right icon2.
111                rightIcon2Title: "",
112
113                // header: Boolean
114                //              If true, this item is rendered as a category header.
115                header: false,
116
117                // tag: String
118                //              A name of html tag to create as domNode.
119                tag: "li",
120
121                // busy: Boolean
122                //              If true, a progress indicator spins.
123                busy: false,
124
125                // progStyle: String
126                //              A css class name to add to the progress indicator.
127                progStyle: "",
128
129                /* internal properties */       
130                // The following properties are overrides of those in _ItemBase.
131                paramsToInherit: "variableHeight,transition,deleteIcon,icon,rightIcon,rightIcon2,uncheckIcon,arrowClass,checkClass,uncheckClass,deleteIconTitle,deleteIconRole",
132                baseClass: "mblListItem",
133
134                _selStartMethod: "touch",
135                _selEndMethod: "timer",
136                _delayedSelection: true,
137
138                _selClass: "mblListItemSelected",
139
140                buildRendering: function(){
141                        this._templated = !!this.templateString; // true if this widget is templated
142                        if(!this._templated){
143                                // Create root node if it wasn't created by _TemplatedMixin
144                                this.domNode = this.containerNode = this.srcNodeRef || domConstruct.create(this.tag);
145                        }
146                        this.inherited(arguments);
147
148                        if(this.selected){
149                                domClass.add(this.domNode, this._selClass);
150                        }
151                        if(this.header){
152                                domClass.replace(this.domNode, "mblEdgeToEdgeCategory", this.baseClass);
153                        }
154
155                        if(!this._templated){
156                                this.labelNode =
157                                        domConstruct.create("div", {className:"mblListItemLabel"});
158                                var ref = this.srcNodeRef;
159                                if(ref && ref.childNodes.length === 1 && ref.firstChild.nodeType === 3){
160                                        // if ref has only one text node, regard it as a label
161                                        this.labelNode.appendChild(ref.firstChild);
162                                }
163                                this.domNode.appendChild(this.labelNode);
164                        }
165                        this._layoutChildren = [];
166                },
167
168                startup: function(){
169                        if(this._started){ return; }
170                        var parent = this.getParent();
171                        var opts = this.getTransOpts();
172                        // When using a template, labelNode may be created via an attach point.
173                        // The attach points are not yet set when ListItem.buildRendering()
174                        // executes, hence the need to use them in startup().
175                        if((!this._templated || this.labelNode) && this.anchorLabel){
176                                this.labelNode.style.display = "inline"; // to narrow the text region
177                                this.labelNode.style.cursor = "pointer";
178                                this.connect(this.labelNode, "onclick", "_onClick");
179                                this.onTouchStart = function(e){
180                                        return (e.target !== this.labelNode);
181                                };
182                        }
183                        if(opts.moveTo || opts.href || opts.url || this.clickable || (parent && parent.select)){
184                                this.connect(this.domNode, "onkeydown", "_onClick"); // for desktop browsers
185                        }else{
186                                this._handleClick = false;
187                        }
188
189                        this.inherited(arguments);
190                       
191                        if(domClass.contains(this.domNode, "mblVariableHeight")){
192                                this.variableHeight = true;
193                        }
194                        if(this.variableHeight){
195                                domClass.add(this.domNode, "mblVariableHeight");
196                                this.defer("layoutVariableHeight");
197                        }
198
199                        if(!this._isOnLine){
200                                this._isOnLine = true;
201                                this.set({
202                                        // retry applying the attributes for which the custom setter delays the actual
203                                        // work until _isOnLine is true
204                                        icon: this._pending_icon !== undefined ? this._pending_icon : this.icon,
205                                        deleteIcon: this._pending_deleteIcon !== undefined ? this._pending_deleteIcon : this.deleteIcon,
206                                        rightIcon: this._pending_rightIcon !== undefined ? this._pending_rightIcon : this.rightIcon,
207                                        rightIcon2: this._pending_rightIcon2 !== undefined ? this._pending_rightIcon2 : this.rightIcon2,
208                                        uncheckIcon: this._pending_uncheckIcon !== undefined ? this._pending_uncheckIcon : this.uncheckIcon
209                                });
210                                // Not needed anymore (this code executes only once per life cycle):
211                                delete this._pending_icon;
212                                delete this._pending_deleteIcon;
213                                delete this._pending_rightIcon;
214                                delete this._pending_rightIcon2;
215                                delete this._pending_uncheckIcon;
216                        }
217                        if(parent && parent.select){
218                                // retry applying the attributes for which the custom setter delays the actual
219                                // work until _isOnLine is true.
220                                this.set("checked", this._pendingChecked !== undefined ? this._pendingChecked : this.checked);
221                                domAttr.set(this.domNode, "role", "option");
222                                if(this._pendingChecked || this.checked){
223                                        domAttr.set(this.domNode, "aria-selected", "true");
224                                }
225                                // Not needed anymore (this code executes only once per life cycle):
226                                delete this._pendingChecked;
227                        }
228                        this.setArrow();
229                        this.layoutChildren();
230                },
231
232                _updateHandles: function(){
233                        // tags:
234                        //              private
235                        var parent = this.getParent();
236                        var opts = this.getTransOpts();
237                        if(opts.moveTo || opts.href || opts.url || this.clickable || (parent && parent.select)){
238                                if(!this._keydownHandle){
239                                        this._keydownHandle = this.connect(this.domNode, "onkeydown", "_onClick"); // for desktop browsers
240                                }
241                                this._handleClick = true;
242                        }else{
243                                if(this._keydownHandle){
244                                        this.disconnect(this._keydownHandle);
245                                        this._keydownHandle = null;
246                                }
247                                this._handleClick = false;
248                        }
249                        this.inherited(arguments);
250                },
251
252                layoutChildren: function(){
253                        var centerNode;
254                        array.forEach(this.domNode.childNodes, function(n){
255                                if(n.nodeType !== 1){ return; }
256                                var layout = n.getAttribute("layout") || // TODO: Remove the non-HTML5-compliant attribute in 2.0
257                                        n.getAttribute("data-mobile-layout") ||
258                                        (registry.byNode(n) || {}).layout;
259                                if(layout){
260                                        domClass.add(n, "mblListItemLayout" +
261                                                layout.charAt(0).toUpperCase() + layout.substring(1));
262                                        this._layoutChildren.push(n);
263                                        if(layout === "center"){ centerNode = n; }
264                                }
265                        }, this);
266                        if(centerNode){
267                                this.domNode.insertBefore(centerNode, this.domNode.firstChild);
268                        }
269                },
270
271                resize: function(){
272                        if(this.variableHeight){
273                                this.layoutVariableHeight();
274                        }
275
276                        // labelNode may not exist only when using a template (if not created by an attach point)
277                        if(!this._templated || this.labelNode){
278                                // If labelNode is empty, shrink it so as not to prevent user clicks.
279                                this.labelNode.style.display = this.labelNode.firstChild ? "block" : "inline";
280                        }
281                },
282
283                _onTouchStart: function(e){
284                        // tags:
285                        //              private
286                        if(e.target.getAttribute("preventTouch") || // TODO: Remove the non-HTML5-compliant attribute in 2.0
287                                e.target.getAttribute("data-mobile-prevent-touch") ||
288                                (registry.getEnclosingWidget(e.target) || {}).preventTouch){
289                                return;
290                        }
291                        this.inherited(arguments);
292                },
293
294                _onClick: function(e){
295                        // summary:
296                        //              Internal handler for click events.
297                        // tags:
298                        //              private
299                        if(this.getParent().isEditing || e && e.type === "keydown" && e.keyCode !== 13){ return; }
300                        if(this.onClick(e) === false){ return; } // user's click action
301                        var n = this.labelNode;
302                        // labelNode may not exist only when using a template
303                        if((this._templated || n) && this.anchorLabel && e.currentTarget === n){
304                                domClass.add(n, "mblListItemLabelSelected");
305                                this.defer(function(){
306                                        domClass.remove(n, "mblListItemLabelSelected");
307                                }, this._duration);
308                                this.onAnchorLabelClicked(e);
309                                return;
310                        }
311                        var parent = this.getParent();
312                        if(parent.select){
313                                if(parent.select === "single"){
314                                        if(!this.checked){
315                                                this.set("checked", true);
316                                        }
317                                }else if(parent.select === "multiple"){
318                                        this.set("checked", !this.checked);
319                                }
320                        }
321                        this.defaultClickAction(e);
322                },
323
324                onClick: function(/*Event*/ /*===== e =====*/){
325                        // summary:
326                        //              User-defined function to handle clicks.
327                        // tags:
328                        //              callback
329                },
330
331                onAnchorLabelClicked: function(e){
332                        // summary:
333                        //              Stub function to connect to from your application.
334                },
335
336                layoutVariableHeight: function(){
337                        // summary:
338                        //              Lays out the current item with variable height.
339                        var h = this.domNode.offsetHeight;
340                        if(h === this.domNodeHeight){ return; }
341                        this.domNodeHeight = h;
342                        array.forEach(this._layoutChildren.concat([
343                                this.rightTextNode,
344                                this.rightIcon2Node,
345                                this.rightIconNode,
346                                this.uncheckIconNode,
347                                this.iconNode,
348                                this.deleteIconNode,
349                                this.knobIconNode
350                        ]), function(n){
351                                if(n){
352                                        var domNode = this.domNode;
353                                        var f = function(){
354                                                var t = Math.round((domNode.offsetHeight - n.offsetHeight) / 2) -
355                                                        domStyle.get(domNode, "paddingTop");
356                                                n.style.marginTop = t + "px";
357                                        }
358                                        if(n.offsetHeight === 0 && n.tagName === "IMG"){
359                                                n.onload = f;
360                                        }else{
361                                                f();
362                                        }
363                                }
364                        }, this);
365                },
366
367                setArrow: function(){
368                        // summary:
369                        //              Sets the arrow icon if necessary.
370                        if(this.checked){ return; }
371                        var c = "";
372                        var parent = this.getParent();
373                        var opts = this.getTransOpts();
374                        if(opts.moveTo || opts.href || opts.url || this.clickable){
375                                if(!this.noArrow && !(parent && parent.selectOne)){
376                                        c = this.arrowClass || "mblDomButtonArrow";
377                                        domAttr.set(this.domNode, "role", "button");
378                                }
379                        }
380                        if(c){
381                                this._setRightIconAttr(c);
382                        }
383                },
384
385                _findRef: function(/*String*/type){
386                        // summary:
387                        //              Find an appropriate position to insert a new child node.
388                        // tags:
389                        //              private
390                        var i, node, list = ["deleteIcon", "icon", "rightIcon", "uncheckIcon", "rightIcon2", "rightText"];
391                        for(i = array.indexOf(list, type) + 1; i < list.length; i++){
392                                node = this[list[i] + "Node"];
393                                if(node){ return node; }
394                        }
395                        for(i = list.length - 1; i >= 0; i--){
396                                node = this[list[i] + "Node"];
397                                if(node){ return node.nextSibling; }
398                        }
399                        return this.domNode.firstChild;
400                },
401               
402                _setIcon: function(/*String*/icon, /*String*/type){
403                        // tags:
404                        //              private
405                        if(!this._isOnLine){
406                                // record the value to be able to reapply it (see the code in the startup method)
407                                this["_pending_" + type] = icon;
408                                return;
409                        } // icon may be invalid because inheritParams is not called yet
410                        this._set(type, icon);
411                        this[type + "Node"] = iconUtils.setIcon(icon, this[type + "Pos"],
412                                this[type + "Node"], this[type + "Title"] || this.alt, this.domNode, this._findRef(type), "before");
413                        if(this[type + "Node"]){
414                                var cap = type.charAt(0).toUpperCase() + type.substring(1);
415                                domClass.add(this[type + "Node"], "mblListItem" + cap);
416                        }
417                        var role = this[type + "Role"];
418                        if(role){
419                                this[type + "Node"].setAttribute("role", role);
420                        }
421                },
422
423                _setDeleteIconAttr: function(/*String*/icon){
424                        // tags:
425                        //              private
426                        this._setIcon(icon, "deleteIcon");
427                },
428
429                _setIconAttr: function(icon){
430                        // tags:
431                        //              private
432                        this._setIcon(icon, "icon");
433                },
434
435                _setRightTextAttr: function(/*String*/text){
436                        // tags:
437                        //              private
438                        if(!this._templated && !this.rightTextNode){
439                                // When using a template, let the template create the element.
440                                this.rightTextNode = domConstruct.create("div", {className:"mblListItemRightText"}, this.labelNode, "before");
441                        }
442                        this.rightText = text;
443                        this.rightTextNode.innerHTML = this._cv ? this._cv(text) : text;
444                },
445
446                _setRightIconAttr: function(/*String*/icon){
447                        // tags:
448                        //              private
449                        this._setIcon(icon, "rightIcon");
450                },
451
452                _setUncheckIconAttr: function(/*String*/icon){
453                        // tags:
454                        //              private
455                        this._setIcon(icon, "uncheckIcon");
456                },
457
458                _setRightIcon2Attr: function(/*String*/icon){
459                        // tags:
460                        //              private
461                        this._setIcon(icon, "rightIcon2");
462                },
463
464                _setCheckedAttr: function(/*Boolean*/checked){
465                        // tags:
466                        //              private
467                        if(!this._isOnLine){
468                                // record the value to be able to reapply it (see the code in the startup method)
469                                this._pendingChecked = checked;
470                                return;
471                        } // icon may be invalid because inheritParams is not called yet
472                        var parent = this.getParent();
473                        if(parent && parent.select === "single" && checked){
474                                array.forEach(parent.getChildren(), function(child){
475                                        child !== this && child.checked && child.set("checked", false) && domAttr.set(child.domNode, "aria-selected", "false");
476                                }, this);
477                        }
478                        this._setRightIconAttr(this.checkClass || "mblDomButtonCheck");
479                        this._setUncheckIconAttr(this.uncheckClass);
480
481                        domClass.toggle(this.domNode, "mblListItemChecked", checked);
482                        domClass.toggle(this.domNode, "mblListItemUnchecked", !checked);
483                        domClass.toggle(this.domNode, "mblListItemHasUncheck", !!this.uncheckIconNode);
484                        this.rightIconNode.style.position = (this.uncheckIconNode && !checked) ? "absolute" : "";
485
486                        if(parent && this.checked !== checked){
487                                parent.onCheckStateChanged(this, checked);
488                        }
489                        this._set("checked", checked);
490                        domAttr.set(this.domNode, "aria-selected", checked ? "true" : "false");
491                },
492
493                _setBusyAttr: function(/*Boolean*/busy){
494                        // tags:
495                        //              private
496                        var prog = this._prog;
497                        if(busy){
498                                if(!this._progNode){
499                                        this._progNode = domConstruct.create("div", {className:"mblListItemIcon"});
500                                        prog = this._prog = new ProgressIndicator({size:25, center:false, removeOnStop:false});
501                                        domClass.add(prog.domNode, this.progStyle);
502                                        this._progNode.appendChild(prog.domNode);
503                                }
504                                if(this.iconNode){
505                                        this.domNode.replaceChild(this._progNode, this.iconNode);
506                                }else{
507                                        domConstruct.place(this._progNode, this._findRef("icon"), "before");
508                                }
509                                prog.start();
510                        }else if(this._progNode){
511                                if(this.iconNode){
512                                        this.domNode.replaceChild(this.iconNode, this._progNode);
513                                }else{
514                                        this.domNode.removeChild(this._progNode);
515                                }
516                                prog.stop();
517                        }
518                        this._set("busy", busy);
519                },
520
521                _setSelectedAttr: function(/*Boolean*/selected){
522                        // summary:
523                        //              Makes this widget in the selected or unselected state.
524                        // tags:
525                        //              private
526                        this.inherited(arguments);
527                        domClass.toggle(this.domNode, this._selClass, selected);
528                },
529               
530                _setClickableAttr: function(/*Boolean*/clickable){
531                        // tags:
532                        //              private
533                        this._set("clickable", clickable);
534                        this._updateHandles();
535                },
536               
537                _setMoveToAttr: function(/*String*/moveTo){
538                        // tags:
539                        //              private
540                        this._set("moveTo", moveTo);
541                        this._updateHandles();
542                },
543               
544                _setHrefAttr: function(/*String*/href){
545                        // tags:
546                        //              private
547                        this._set("href", href);
548                        this._updateHandles();
549                },
550               
551                _setUrlAttr: function(/*String*/url){
552                        // tags:
553                        //              private
554                        this._set("url", url);
555                        this._updateHandles();
556                }
557        });
558       
559        ListItem.ChildWidgetProperties = {
560                // summary:
561                //              These properties can be specified for the children of a dojox/mobile/ListItem.
562
563                // layout: String
564                //              Specifies the position of the ListItem child ("left", "center" or "right").
565                layout: "",
566
567                // preventTouch: Boolean
568                //              Disables touch events on the ListItem child.
569                preventTouch: false
570        };
571       
572        // Since any widget can be specified as a ListItem child, mix ChildWidgetProperties
573        // into the base widget class.  (This is a hack, but it's effective.)
574        // This is for the benefit of the parser.   Remove for 2.0.  Also, hide from doc viewer.
575        lang.extend(WidgetBase, /*===== {} || =====*/ ListItem.ChildWidgetProperties);
576
577        return has("dojo-bidi") ? declare("dojox.mobile.ListItem", [ListItem, BidiListItem]) : ListItem;       
578});
Note: See TracBrowser for help on using the repository browser.