You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
583 lines
18 KiB
583 lines
18 KiB
define([ |
|
"dojo/_base/array", |
|
"dojo/_base/declare", |
|
"dojo/_base/lang", |
|
"dojo/dom-class", |
|
"dojo/dom-construct", |
|
"dojo/dom-style", |
|
"dojo/dom-attr", |
|
"dijit/registry", |
|
"dijit/_WidgetBase", |
|
"./iconUtils", |
|
"./_ItemBase", |
|
"./ProgressIndicator", |
|
"dojo/has", |
|
"dojo/has!dojo-bidi?dojox/mobile/bidi/ListItem" |
|
], function(array, declare, lang, domClass, domConstruct, domStyle, domAttr, registry, WidgetBase, iconUtils, ItemBase, ProgressIndicator, has, BidiListItem){ |
|
|
|
// module: |
|
// dojox/mobile/ListItem |
|
|
|
var ListItem = declare(has("dojo-bidi") ? "dojox.mobile.NonBidiListItem" : "dojox.mobile.ListItem", ItemBase, { |
|
// summary: |
|
// An item of either RoundRectList or EdgeToEdgeList. |
|
// description: |
|
// ListItem represents an item of either RoundRectList or |
|
// EdgeToEdgeList. There are three ways to move to a different view: |
|
// moveTo, href, and url. You can choose only one of them. |
|
// |
|
// A child DOM node (or widget) can have the layout attribute, |
|
// whose value is "left", "right", or "center". Such nodes will be |
|
// aligned as specified. |
|
// example: |
|
// | <li data-dojo-type="dojox.mobile.ListItem"> |
|
// | <div layout="left">Left Node</div> |
|
// | <div layout="right">Right Node</div> |
|
// | <div layout="center">Center Node</div> |
|
// | </li> |
|
// |
|
// Similarly, a child widget can have the preventTouch |
|
// attribute, whose value is a boolean (or data-mobile-prevent-touch |
|
// for children which are not widgets), such that touching such |
|
// child doesn't trigger the item action. |
|
// |
|
// Note that even if you specify variableHeight="true" for the list |
|
// and place a tall object inside the layout node as in the example |
|
// below, the layout node does not expand as you may expect, |
|
// because layout node is aligned using float:left, float:right, or |
|
// position:absolute. |
|
// example: |
|
// | <li data-dojo-type="dojox.mobile.ListItem" variableHeight="true"> |
|
// | <div layout="left"><img src="large-picture.jpg"></div> |
|
// | </li> |
|
|
|
// rightText: String |
|
// A right-aligned text to display on the item. |
|
rightText: "", |
|
|
|
// rightIcon: String |
|
// An icon to display at the right hand side of the item. The value |
|
// can be either a path for an image file or a class name of a DOM |
|
// button. |
|
rightIcon: "", |
|
|
|
// rightIcon2: String |
|
// An icon to display at the left of the rightIcon. The value can |
|
// be either a path for an image file or a class name of a DOM |
|
// button. |
|
rightIcon2: "", |
|
|
|
// deleteIcon: String |
|
// A delete icon to display at the left of the item. The value can |
|
// be either a path for an image file or a class name of a DOM |
|
// button. |
|
deleteIcon: "", |
|
|
|
// anchorLabel: Boolean |
|
// If true, the label text becomes a clickable anchor text. When |
|
// the user clicks on the text, the onAnchorLabelClicked handler is |
|
// called. You can override or connect to the handler and implement |
|
// any action. The handler has no default action. |
|
anchorLabel: false, |
|
|
|
// noArrow: Boolean |
|
// If true, the right hand side arrow is not displayed. |
|
noArrow: false, |
|
|
|
// checked: Boolean |
|
// If true, a check mark is displayed at the right of the item. |
|
checked: false, |
|
|
|
// arrowClass: String |
|
// An icon to display as an arrow. The value can be either a path |
|
// for an image file or a class name of a DOM button. |
|
arrowClass: "", |
|
|
|
// checkClass: String |
|
// An icon to display as a check mark. The value can be either a |
|
// path for an image file or a class name of a DOM button. |
|
checkClass: "", |
|
|
|
// uncheckClass: String |
|
// An icon to display as an uncheck mark. The value can be either a |
|
// path for an image file or a class name of a DOM button. |
|
uncheckClass: "", |
|
|
|
// variableHeight: Boolean |
|
// If true, the height of the item varies according to its content. |
|
variableHeight: false, |
|
|
|
// rightIconTitle: String |
|
// An alt text for the right icon. |
|
rightIconTitle: "", |
|
|
|
// rightIcon2Title: String |
|
// An alt text for the right icon2. |
|
rightIcon2Title: "", |
|
|
|
// header: Boolean |
|
// If true, this item is rendered as a category header. |
|
header: false, |
|
|
|
// tag: String |
|
// A name of html tag to create as domNode. |
|
tag: "li", |
|
|
|
// busy: Boolean |
|
// If true, a progress indicator spins. |
|
busy: false, |
|
|
|
// progStyle: String |
|
// A css class name to add to the progress indicator. |
|
progStyle: "", |
|
|
|
// layoutOnResize: Boolean |
|
// If true, a call to resize() will force computation of variable height items layout. You should not need this as in most |
|
// cases ListItem height doesn't change on container resize. Depending on number and complexity |
|
// of items in a view, setting to true may have a high impact on performance. |
|
layoutOnResize: false, |
|
|
|
/* internal properties */ |
|
// The following properties are overrides of those in _ItemBase. |
|
paramsToInherit: "variableHeight,transition,deleteIcon,icon,rightIcon,rightIcon2,uncheckIcon,arrowClass,checkClass,uncheckClass,deleteIconTitle,deleteIconRole", |
|
baseClass: "mblListItem", |
|
|
|
_selStartMethod: "touch", |
|
_selEndMethod: "timer", |
|
_delayedSelection: true, |
|
|
|
_selClass: "mblListItemSelected", |
|
|
|
buildRendering: function(){ |
|
this._templated = !!this.templateString; // true if this widget is templated |
|
if(!this._templated){ |
|
// Create root node if it wasn't created by _TemplatedMixin |
|
this.domNode = this.containerNode = this.srcNodeRef || domConstruct.create(this.tag); |
|
} |
|
this.inherited(arguments); |
|
|
|
if(this.selected){ |
|
domClass.add(this.domNode, this._selClass); |
|
} |
|
if(this.header){ |
|
domClass.replace(this.domNode, "mblEdgeToEdgeCategory", this.baseClass); |
|
} |
|
|
|
if(!this._templated){ |
|
this.labelNode = |
|
domConstruct.create("div", {className:"mblListItemLabel"}); |
|
var ref = this.srcNodeRef; |
|
if(ref && ref.childNodes.length === 1 && ref.firstChild.nodeType === 3){ |
|
// if ref has only one text node, regard it as a label |
|
this.labelNode.appendChild(ref.firstChild); |
|
} |
|
this.domNode.appendChild(this.labelNode); |
|
} |
|
this._layoutChildren = []; |
|
}, |
|
|
|
startup: function(){ |
|
if(this._started){ return; } |
|
var parent = this.getParent(); |
|
var opts = this.getTransOpts(); |
|
// When using a template, labelNode may be created via an attach point. |
|
// The attach points are not yet set when ListItem.buildRendering() |
|
// executes, hence the need to use them in startup(). |
|
if((!this._templated || this.labelNode) && this.anchorLabel){ |
|
this.labelNode.style.display = "inline"; // to narrow the text region |
|
this.labelNode.style.cursor = "pointer"; |
|
this.connect(this.labelNode, "onclick", "_onClick"); |
|
this.onTouchStart = function(e){ |
|
return (e.target !== this.labelNode); |
|
}; |
|
} |
|
|
|
this.inherited(arguments); |
|
|
|
if(domClass.contains(this.domNode, "mblVariableHeight")){ |
|
this.variableHeight = true; |
|
} |
|
if(this.variableHeight){ |
|
domClass.add(this.domNode, "mblVariableHeight"); |
|
this.defer("layoutVariableHeight"); |
|
} |
|
|
|
if(!this._isOnLine){ |
|
this._isOnLine = true; |
|
this.set({ |
|
// retry applying the attributes for which the custom setter delays the actual |
|
// work until _isOnLine is true |
|
icon: this._pending_icon !== undefined ? this._pending_icon : this.icon, |
|
deleteIcon: this._pending_deleteIcon !== undefined ? this._pending_deleteIcon : this.deleteIcon, |
|
rightIcon: this._pending_rightIcon !== undefined ? this._pending_rightIcon : this.rightIcon, |
|
rightIcon2: this._pending_rightIcon2 !== undefined ? this._pending_rightIcon2 : this.rightIcon2, |
|
uncheckIcon: this._pending_uncheckIcon !== undefined ? this._pending_uncheckIcon : this.uncheckIcon |
|
}); |
|
// Not needed anymore (this code executes only once per life cycle): |
|
delete this._pending_icon; |
|
delete this._pending_deleteIcon; |
|
delete this._pending_rightIcon; |
|
delete this._pending_rightIcon2; |
|
delete this._pending_uncheckIcon; |
|
} |
|
if(parent && parent.select){ |
|
// retry applying the attributes for which the custom setter delays the actual |
|
// work until _isOnLine is true. |
|
this.set("checked", this._pendingChecked !== undefined ? this._pendingChecked : this.checked); |
|
domAttr.set(this.domNode, "role", "option"); |
|
if(this._pendingChecked || this.checked){ |
|
domAttr.set(this.domNode, "aria-selected", "true"); |
|
} |
|
// Not needed anymore (this code executes only once per life cycle): |
|
delete this._pendingChecked; |
|
} |
|
this.setArrow(); |
|
this.layoutChildren(); |
|
}, |
|
|
|
_updateHandles: function(){ |
|
// tags: |
|
// private |
|
var parent = this.getParent(); |
|
var opts = this.getTransOpts(); |
|
if(opts.moveTo || opts.href || opts.url || this.clickable || (parent && parent.select)){ |
|
if(!this._keydownHandle){ |
|
this._keydownHandle = this.connect(this.domNode, "onkeydown", "_onClick"); // for desktop browsers |
|
} |
|
this._handleClick = true; |
|
}else{ |
|
if(this._keydownHandle){ |
|
this.disconnect(this._keydownHandle); |
|
this._keydownHandle = null; |
|
} |
|
this._handleClick = false; |
|
} |
|
this.inherited(arguments); |
|
}, |
|
|
|
layoutChildren: function(){ |
|
var centerNode; |
|
array.forEach(this.domNode.childNodes, function(n){ |
|
if(n.nodeType !== 1){ return; } |
|
var layout = n.getAttribute("layout") || // TODO: Remove the non-HTML5-compliant attribute in 2.0 |
|
n.getAttribute("data-mobile-layout") || |
|
(registry.byNode(n) || {}).layout; |
|
if(layout){ |
|
domClass.add(n, "mblListItemLayout" + |
|
layout.charAt(0).toUpperCase() + layout.substring(1)); |
|
this._layoutChildren.push(n); |
|
if(layout === "center"){ centerNode = n; } |
|
} |
|
}, this); |
|
if(centerNode){ |
|
this.domNode.insertBefore(centerNode, this.domNode.firstChild); |
|
} |
|
}, |
|
|
|
resize: function(){ |
|
if(this.layoutOnResize && this.variableHeight){ |
|
this.layoutVariableHeight(); |
|
} |
|
// labelNode may not exist only when using a template (if not created by an attach point) |
|
if(!this._templated || this.labelNode){ |
|
// If labelNode is empty, shrink it so as not to prevent user clicks. |
|
this.labelNode.style.display = this.labelNode.firstChild ? "block" : "inline"; |
|
} |
|
}, |
|
|
|
_onTouchStart: function(e){ |
|
// tags: |
|
// private |
|
if(e.target.getAttribute("preventTouch") || // TODO: Remove the non-HTML5-compliant attribute in 2.0 |
|
e.target.getAttribute("data-mobile-prevent-touch") || |
|
(registry.getEnclosingWidget(e.target) || {}).preventTouch){ |
|
return; |
|
} |
|
this.inherited(arguments); |
|
}, |
|
|
|
_onClick: function(e){ |
|
// summary: |
|
// Internal handler for click events. |
|
// tags: |
|
// private |
|
if(this.getParent().isEditing || e && e.type === "keydown" && e.keyCode !== 13){ return; } |
|
if(this.onClick(e) === false){ return; } // user's click action |
|
var n = this.labelNode; |
|
// labelNode may not exist only when using a template |
|
if((this._templated || n) && this.anchorLabel && e.currentTarget === n){ |
|
domClass.add(n, "mblListItemLabelSelected"); |
|
this.defer(function(){ |
|
domClass.remove(n, "mblListItemLabelSelected"); |
|
}, this._duration); |
|
this.onAnchorLabelClicked(e); |
|
return; |
|
} |
|
var parent = this.getParent(); |
|
if(parent.select){ |
|
if(parent.select === "single"){ |
|
if(!this.checked){ |
|
this.set("checked", true); |
|
} |
|
}else if(parent.select === "multiple"){ |
|
this.set("checked", !this.checked); |
|
} |
|
} |
|
this.defaultClickAction(e); |
|
}, |
|
|
|
onClick: function(/*Event*/ /*===== e =====*/){ |
|
// summary: |
|
// User-defined function to handle clicks. |
|
// tags: |
|
// callback |
|
}, |
|
|
|
onAnchorLabelClicked: function(e){ |
|
// summary: |
|
// Stub function to connect to from your application. |
|
}, |
|
|
|
layoutVariableHeight: function(){ |
|
// summary: |
|
// Lays out the current item with variable height. |
|
var h = this.domNode.offsetHeight; |
|
if(h === this.domNodeHeight){ return; } |
|
this.domNodeHeight = h; |
|
array.forEach(this._layoutChildren.concat([ |
|
this.rightTextNode, |
|
this.rightIcon2Node, |
|
this.rightIconNode, |
|
this.uncheckIconNode, |
|
this.iconNode, |
|
this.deleteIconNode, |
|
this.knobIconNode |
|
]), function(n){ |
|
if(n){ |
|
var domNode = this.domNode; |
|
var f = function(){ |
|
var t = Math.round((domNode.offsetHeight - n.offsetHeight) / 2) - |
|
domStyle.get(domNode, "paddingTop"); |
|
n.style.marginTop = t + "px"; |
|
}; |
|
if(n.offsetHeight === 0 && n.tagName === "IMG"){ |
|
n.onload = f; |
|
}else{ |
|
f(); |
|
} |
|
} |
|
}, this); |
|
}, |
|
|
|
setArrow: function(){ |
|
// summary: |
|
// Sets the arrow icon if necessary. |
|
if(this.checked){ return; } |
|
var c = ""; |
|
var parent = this.getParent(); |
|
var opts = this.getTransOpts(); |
|
if(opts.moveTo || opts.href || opts.url || this.clickable){ |
|
if(!this.noArrow && !(parent && parent.selectOne)){ |
|
c = this.arrowClass || "mblDomButtonArrow"; |
|
domAttr.set(this.domNode, "role", "button"); |
|
} |
|
} |
|
if(c){ |
|
this._setRightIconAttr(c); |
|
} |
|
}, |
|
|
|
_findRef: function(/*String*/type){ |
|
// summary: |
|
// Find an appropriate position to insert a new child node. |
|
// tags: |
|
// private |
|
var i, node, list = ["deleteIcon", "icon", "rightIcon", "uncheckIcon", "rightIcon2", "rightText"]; |
|
for(i = array.indexOf(list, type) + 1; i < list.length; i++){ |
|
node = this[list[i] + "Node"]; |
|
if(node){ return node; } |
|
} |
|
for(i = list.length - 1; i >= 0; i--){ |
|
node = this[list[i] + "Node"]; |
|
if(node){ return node.nextSibling; } |
|
} |
|
return this.domNode.firstChild; |
|
}, |
|
|
|
_setIcon: function(/*String*/icon, /*String*/type){ |
|
// tags: |
|
// private |
|
if(!this._isOnLine){ |
|
// record the value to be able to reapply it (see the code in the startup method) |
|
this["_pending_" + type] = icon; |
|
return; |
|
} // icon may be invalid because inheritParams is not called yet |
|
this._set(type, icon); |
|
this[type + "Node"] = iconUtils.setIcon(icon, this[type + "Pos"], |
|
this[type + "Node"], this[type + "Title"] || this.alt, this.domNode, this._findRef(type), "before"); |
|
if(this[type + "Node"]){ |
|
var cap = type.charAt(0).toUpperCase() + type.substring(1); |
|
domClass.add(this[type + "Node"], "mblListItem" + cap); |
|
} |
|
var role = this[type + "Role"]; |
|
if(role){ |
|
this[type + "Node"].setAttribute("role", role); |
|
} |
|
}, |
|
|
|
_setDeleteIconAttr: function(/*String*/icon){ |
|
// tags: |
|
// private |
|
this._setIcon(icon, "deleteIcon"); |
|
}, |
|
|
|
_setIconAttr: function(icon){ |
|
// tags: |
|
// private |
|
this._setIcon(icon, "icon"); |
|
}, |
|
|
|
_setRightTextAttr: function(/*String*/text){ |
|
// tags: |
|
// private |
|
if(!this._templated && !this.rightTextNode){ |
|
// When using a template, let the template create the element. |
|
this.rightTextNode = domConstruct.create("div", {className:"mblListItemRightText"}, this.labelNode, "before"); |
|
} |
|
this.rightText = text; |
|
this.rightTextNode.innerHTML = this._cv ? this._cv(text) : text; |
|
}, |
|
|
|
_setRightIconAttr: function(/*String*/icon){ |
|
// tags: |
|
// private |
|
this._setIcon(icon, "rightIcon"); |
|
}, |
|
|
|
_setUncheckIconAttr: function(/*String*/icon){ |
|
// tags: |
|
// private |
|
this._setIcon(icon, "uncheckIcon"); |
|
}, |
|
|
|
_setRightIcon2Attr: function(/*String*/icon){ |
|
// tags: |
|
// private |
|
this._setIcon(icon, "rightIcon2"); |
|
}, |
|
|
|
_setCheckedAttr: function(/*Boolean*/checked){ |
|
// tags: |
|
// private |
|
if(!this._isOnLine){ |
|
// record the value to be able to reapply it (see the code in the startup method) |
|
this._pendingChecked = checked; |
|
return; |
|
} // icon may be invalid because inheritParams is not called yet |
|
var parent = this.getParent(); |
|
if(parent && parent.select === "single" && checked){ |
|
array.forEach(parent.getChildren(), function(child){ |
|
child !== this && child.checked && child.set("checked", false) && domAttr.set(child.domNode, "aria-selected", "false"); |
|
}, this); |
|
} |
|
this._setRightIconAttr(this.checkClass || "mblDomButtonCheck"); |
|
this._setUncheckIconAttr(this.uncheckClass); |
|
|
|
domClass.toggle(this.domNode, "mblListItemChecked", checked); |
|
domClass.toggle(this.domNode, "mblListItemUnchecked", !checked); |
|
domClass.toggle(this.domNode, "mblListItemHasUncheck", !!this.uncheckIconNode); |
|
this.rightIconNode.style.position = (this.uncheckIconNode && !checked) ? "absolute" : ""; |
|
|
|
if(parent && this.checked !== checked){ |
|
parent.onCheckStateChanged(this, checked); |
|
} |
|
this._set("checked", checked); |
|
domAttr.set(this.domNode, "aria-selected", checked ? "true" : "false"); |
|
}, |
|
|
|
_setBusyAttr: function(/*Boolean*/busy){ |
|
// tags: |
|
// private |
|
var prog = this._prog; |
|
if(busy){ |
|
if(!this._progNode){ |
|
this._progNode = domConstruct.create("div", {className:"mblListItemIcon"}); |
|
prog = this._prog = new ProgressIndicator({size:25, center:false, removeOnStop:false}); |
|
domClass.add(prog.domNode, this.progStyle); |
|
this._progNode.appendChild(prog.domNode); |
|
} |
|
if(this.iconNode){ |
|
this.domNode.replaceChild(this._progNode, this.iconNode); |
|
}else{ |
|
domConstruct.place(this._progNode, this._findRef("icon"), "before"); |
|
} |
|
prog.start(); |
|
}else if(this._progNode){ |
|
if(this.iconNode){ |
|
this.domNode.replaceChild(this.iconNode, this._progNode); |
|
}else{ |
|
this.domNode.removeChild(this._progNode); |
|
} |
|
prog.stop(); |
|
} |
|
this._set("busy", busy); |
|
}, |
|
|
|
_setSelectedAttr: function(/*Boolean*/selected){ |
|
// summary: |
|
// Makes this widget in the selected or unselected state. |
|
// tags: |
|
// private |
|
this.inherited(arguments); |
|
domClass.toggle(this.domNode, this._selClass, selected); |
|
}, |
|
|
|
_setClickableAttr: function(/*Boolean*/clickable){ |
|
// tags: |
|
// private |
|
this._set("clickable", clickable); |
|
this._updateHandles(); |
|
}, |
|
|
|
_setMoveToAttr: function(/*String*/moveTo){ |
|
// tags: |
|
// private |
|
this._set("moveTo", moveTo); |
|
this._updateHandles(); |
|
}, |
|
|
|
_setHrefAttr: function(/*String*/href){ |
|
// tags: |
|
// private |
|
this._set("href", href); |
|
this._updateHandles(); |
|
}, |
|
|
|
_setUrlAttr: function(/*String*/url){ |
|
// tags: |
|
// private |
|
this._set("url", url); |
|
this._updateHandles(); |
|
} |
|
}); |
|
|
|
ListItem.ChildWidgetProperties = { |
|
// summary: |
|
// These properties can be specified for the children of a dojox/mobile/ListItem. |
|
|
|
// layout: String |
|
// Specifies the position of the ListItem child ("left", "center" or "right"). |
|
layout: "", |
|
|
|
// preventTouch: Boolean |
|
// Disables touch events on the ListItem child. |
|
preventTouch: false |
|
}; |
|
|
|
// Since any widget can be specified as a ListItem child, mix ChildWidgetProperties |
|
// into the base widget class. (This is a hack, but it's effective.) |
|
// This is for the benefit of the parser. Remove for 2.0. Also, hide from doc viewer. |
|
lang.extend(WidgetBase, /*===== {} || =====*/ ListItem.ChildWidgetProperties); |
|
|
|
return has("dojo-bidi") ? declare("dojox.mobile.ListItem", [ListItem, BidiListItem]) : ListItem; |
|
});
|
|
|