1 | define([ |
---|
2 | "dojo/_base/array", |
---|
3 | "dojo/_base/connect", |
---|
4 | "dojo/_base/declare", |
---|
5 | "dojo/_base/event", |
---|
6 | "dojo/_base/lang", |
---|
7 | "dojo/sniff", |
---|
8 | "dojo/dom-class", |
---|
9 | "dojo/dom-construct", |
---|
10 | "dojo/dom-style", |
---|
11 | "dijit/registry", |
---|
12 | "dijit/_Contained", |
---|
13 | "dijit/_Container", |
---|
14 | "dijit/_WidgetBase", |
---|
15 | "./lazyLoadUtils", |
---|
16 | "./CarouselItem", |
---|
17 | "./PageIndicator", |
---|
18 | "./SwapView", |
---|
19 | "require", |
---|
20 | "dojo/has!dojo-bidi?dojox/mobile/bidi/Carousel" |
---|
21 | ], function(array, connect, declare, event, lang, has, domClass, domConstruct, domStyle, registry, Contained, Container, WidgetBase, lazyLoadUtils, CarouselItem, PageIndicator, SwapView, require, BidiCarousel){ |
---|
22 | |
---|
23 | // module: |
---|
24 | // dojox/mobile/Carousel |
---|
25 | |
---|
26 | var Carousel = declare(has("dojo-bidi") ? "dojox.mobile.NonBidiCarousel" : "dojox.mobile.Carousel", [WidgetBase, Container, Contained], { |
---|
27 | // summary: |
---|
28 | // A carousel widget that manages a list of images. |
---|
29 | // description: |
---|
30 | // The carousel widget manages a list of images that can be |
---|
31 | // displayed horizontally, and allows the user to scroll through |
---|
32 | // the list and select a single item. |
---|
33 | // |
---|
34 | // This widget itself has no data store support, but there are two |
---|
35 | // subclasses, dojox/mobile/DataCarousel and dojox/mobile/StoreCarousel, |
---|
36 | // available for generating the contents from a data store. |
---|
37 | // To feed data into a Carousel through a dojo/data, use DataCarousel. |
---|
38 | // To feed data into a Carousel through a dojo/store, use StoreCarousel. |
---|
39 | // |
---|
40 | // The Carousel widget loads and instantiates its item contents in |
---|
41 | // a lazy manner. For example, if the number of visible items |
---|
42 | // (see the property numVisible) is 2, the widget creates 4 items, 2 for the |
---|
43 | // initial pane and 2 for the next page, at startup time. If you |
---|
44 | // swipe the page to open the second page, the widget creates 2 more |
---|
45 | // items for the third page. If the item to create is a dojo widget, |
---|
46 | // its module is dynamically loaded automatically before instantiation. |
---|
47 | |
---|
48 | // numVisible: Number |
---|
49 | // The number of visible items. |
---|
50 | numVisible: 2, |
---|
51 | |
---|
52 | // itemWidth: Number |
---|
53 | // The number of visible items (=numVisible) is determined by |
---|
54 | // (carousel_width / itemWidth). |
---|
55 | // If itemWidth is specified, numVisible is automatically calculated. |
---|
56 | // If resize() is called, numVisible is recalculated and the layout |
---|
57 | // is changed accordingly. |
---|
58 | itemWidth: 0, |
---|
59 | |
---|
60 | // title: String |
---|
61 | // A title of the carousel to be displayed on the title bar. |
---|
62 | title: "", |
---|
63 | |
---|
64 | // pageIndicator: [const] Boolean |
---|
65 | // If true, a page indicator, a series of small dots that indicate |
---|
66 | // the current page, is displayed on the title bar. |
---|
67 | // Note that changing the value of the property after the widget |
---|
68 | // creation has no effect. |
---|
69 | pageIndicator: true, |
---|
70 | |
---|
71 | // navButton: [const] Boolean |
---|
72 | // If true, navigation buttons are displyed on the title bar. |
---|
73 | // Note that changing the value of the property after the widget |
---|
74 | // creation has no effect. |
---|
75 | navButton: false, |
---|
76 | |
---|
77 | // height: [const] String |
---|
78 | // Explicitly specified height of the widget (ex. "300px"). If |
---|
79 | // "inherit" is specified, the height is inherited from its offset |
---|
80 | // parent. |
---|
81 | // Note that changing the value of the property after the widget |
---|
82 | // creation has no effect. |
---|
83 | height: "", |
---|
84 | |
---|
85 | // selectable: Boolean |
---|
86 | // If true, an item can be selected by clicking it. |
---|
87 | selectable: true, |
---|
88 | |
---|
89 | /* internal properties */ |
---|
90 | |
---|
91 | // baseClass: String |
---|
92 | // The name of the CSS class of this widget. |
---|
93 | baseClass: "mblCarousel", |
---|
94 | |
---|
95 | buildRendering: function(){ |
---|
96 | this.containerNode = domConstruct.create("div", {className: "mblCarouselPages"}); |
---|
97 | this.inherited(arguments); |
---|
98 | var i; |
---|
99 | if(this.srcNodeRef){ |
---|
100 | // reparent |
---|
101 | for(i = 0, len = this.srcNodeRef.childNodes.length; i < len; i++){ |
---|
102 | this.containerNode.appendChild(this.srcNodeRef.firstChild); |
---|
103 | } |
---|
104 | } |
---|
105 | |
---|
106 | this.headerNode = domConstruct.create("div", {className: "mblCarouselHeaderBar"}, this.domNode); |
---|
107 | |
---|
108 | if(this.navButton){ |
---|
109 | this.btnContainerNode = domConstruct.create("div", { |
---|
110 | className: "mblCarouselBtnContainer" |
---|
111 | }, this.headerNode); |
---|
112 | domStyle.set(this.btnContainerNode, "float", "right"); // workaround for webkit rendering problem |
---|
113 | this.prevBtnNode = domConstruct.create("button", { |
---|
114 | className: "mblCarouselBtn", |
---|
115 | title: "Previous", |
---|
116 | innerHTML: "<" |
---|
117 | }, this.btnContainerNode); |
---|
118 | this.nextBtnNode = domConstruct.create("button", { |
---|
119 | className: "mblCarouselBtn", |
---|
120 | title: "Next", |
---|
121 | innerHTML: ">" |
---|
122 | }, this.btnContainerNode); |
---|
123 | this._prevHandle = this.connect(this.prevBtnNode, "onclick", "onPrevBtnClick"); |
---|
124 | this._nextHandle = this.connect(this.nextBtnNode, "onclick", "onNextBtnClick"); |
---|
125 | } |
---|
126 | |
---|
127 | if(this.pageIndicator){ |
---|
128 | if(!this.title){ |
---|
129 | this.title = " "; |
---|
130 | } |
---|
131 | this.piw = new PageIndicator(); |
---|
132 | this.headerNode.appendChild(this.piw.domNode); |
---|
133 | } |
---|
134 | |
---|
135 | this.titleNode = domConstruct.create("div", { |
---|
136 | className: "mblCarouselTitle" |
---|
137 | }, this.headerNode); |
---|
138 | |
---|
139 | this.domNode.appendChild(this.containerNode); |
---|
140 | this.subscribe("/dojox/mobile/viewChanged", "handleViewChanged"); |
---|
141 | this.connect(this.domNode, "onclick", "_onClick"); |
---|
142 | this.connect(this.domNode, "onkeydown", "_onClick"); |
---|
143 | this._dragstartHandle = this.connect(this.domNode, "ondragstart", event.stop); |
---|
144 | this.selectedItemIndex = -1; |
---|
145 | this.items = []; |
---|
146 | }, |
---|
147 | |
---|
148 | startup: function(){ |
---|
149 | if(this._started){ return; } |
---|
150 | |
---|
151 | var h; |
---|
152 | if(this.height === "inherit"){ |
---|
153 | if(this.domNode.offsetParent){ |
---|
154 | h = this.domNode.offsetParent.offsetHeight + "px"; |
---|
155 | } |
---|
156 | }else if(this.height){ |
---|
157 | h = this.height; |
---|
158 | } |
---|
159 | if(h){ |
---|
160 | this.domNode.style.height = h; |
---|
161 | } |
---|
162 | |
---|
163 | if(this.store){ |
---|
164 | if(!this.setStore){ |
---|
165 | throw new Error("Use StoreCarousel or DataCarousel instead of Carousel."); |
---|
166 | } |
---|
167 | var store = this.store; |
---|
168 | this.store = null; |
---|
169 | this.setStore(store, this.query, this.queryOptions); |
---|
170 | }else{ |
---|
171 | this.resizeItems(); |
---|
172 | } |
---|
173 | this.inherited(arguments); |
---|
174 | |
---|
175 | this.currentView = array.filter(this.getChildren(), function(view){ |
---|
176 | return view.isVisible(); |
---|
177 | })[0]; |
---|
178 | }, |
---|
179 | |
---|
180 | resizeItems: function(){ |
---|
181 | // summary: |
---|
182 | // Resizes the child items of the carousel. |
---|
183 | var idx = 0, i; |
---|
184 | var h = this.domNode.offsetHeight - (this.headerNode ? this.headerNode.offsetHeight : 0); |
---|
185 | var m = (has("ie") < 10) ? 5 / this.numVisible - 1 : 5 / this.numVisible; |
---|
186 | var node, item; |
---|
187 | array.forEach(this.getChildren(), function(view){ |
---|
188 | if(!(view instanceof SwapView)){ return; } |
---|
189 | if(!(view.lazy)){ |
---|
190 | view._instantiated = true; |
---|
191 | } |
---|
192 | var ch = view.containerNode.childNodes; |
---|
193 | for(i = 0, len = ch.length; i < len; i++){ |
---|
194 | node = ch[i]; |
---|
195 | if(node.nodeType !== 1){ continue; } |
---|
196 | item = this.items[idx] || {}; |
---|
197 | domStyle.set(node, { |
---|
198 | width: item.width || (90 / this.numVisible + "%"), |
---|
199 | height: item.height || h + "px", |
---|
200 | margin: "0 " + (item.margin || m + "%") |
---|
201 | }); |
---|
202 | domClass.add(node, "mblCarouselSlot"); |
---|
203 | idx++; |
---|
204 | } |
---|
205 | }, this); |
---|
206 | |
---|
207 | if(this.piw){ |
---|
208 | this.piw.refId = this.containerNode.firstChild; |
---|
209 | this.piw.reset(); |
---|
210 | } |
---|
211 | }, |
---|
212 | |
---|
213 | resize: function(){ |
---|
214 | if(!this.itemWidth){ return; } |
---|
215 | var num = Math.floor(this.domNode.offsetWidth / this.itemWidth); |
---|
216 | if(num === this.numVisible){ return; } |
---|
217 | this.selectedItemIndex = this.getIndexByItemWidget(this.selectedItem); |
---|
218 | this.numVisible = num; |
---|
219 | if(this.items.length > 0){ |
---|
220 | this.onComplete(this.items); |
---|
221 | this.select(this.selectedItemIndex); |
---|
222 | } |
---|
223 | }, |
---|
224 | |
---|
225 | fillPages: function(){ |
---|
226 | array.forEach(this.getChildren(), function(child, i){ |
---|
227 | var s = ""; |
---|
228 | var j; |
---|
229 | for(j = 0; j < this.numVisible; j++){ |
---|
230 | var type, props = "", mixins; |
---|
231 | var idx = i * this.numVisible + j; |
---|
232 | var item = {}; |
---|
233 | if(idx < this.items.length){ |
---|
234 | item = this.items[idx]; |
---|
235 | type = this.store.getValue(item, "type"); |
---|
236 | if(type){ |
---|
237 | props = this.store.getValue(item, "props"); |
---|
238 | mixins = this.store.getValue(item, "mixins"); |
---|
239 | }else{ |
---|
240 | type = "dojox.mobile.CarouselItem"; |
---|
241 | array.forEach(["alt", "src", "headerText", "footerText"], function(p){ |
---|
242 | var v = this.store.getValue(item, p); |
---|
243 | if(v !== undefined){ |
---|
244 | if(props){ props += ','; } |
---|
245 | props += p + ':"' + v + '"'; |
---|
246 | } |
---|
247 | }, this); |
---|
248 | } |
---|
249 | }else{ |
---|
250 | type = "dojox.mobile.CarouselItem"; |
---|
251 | props = 'src:"' + require.toUrl("dojo/resources/blank.gif") + '"' + |
---|
252 | ', className:"mblCarouselItemBlank"'; |
---|
253 | } |
---|
254 | |
---|
255 | s += '<div data-dojo-type="' + type + '"'; |
---|
256 | if(props){ |
---|
257 | s += ' data-dojo-props=\'' + props + '\''; |
---|
258 | } |
---|
259 | if(mixins){ |
---|
260 | s += ' data-dojo-mixins=\'' + mixins + '\''; |
---|
261 | } |
---|
262 | s += '></div>'; |
---|
263 | } |
---|
264 | child.containerNode.innerHTML = s; |
---|
265 | }, this); |
---|
266 | }, |
---|
267 | |
---|
268 | onComplete: function(/*Array*/items){ |
---|
269 | // summary: |
---|
270 | // A handler that is called after the fetch completes. |
---|
271 | array.forEach(this.getChildren(), function(child){ |
---|
272 | if(child instanceof SwapView){ |
---|
273 | child.destroyRecursive(); |
---|
274 | } |
---|
275 | }); |
---|
276 | this.selectedItem = null; |
---|
277 | this.items = items; |
---|
278 | var nPages = Math.ceil(items.length / this.numVisible), |
---|
279 | i, h = this.domNode.offsetHeight - this.headerNode.offsetHeight, |
---|
280 | idx = this.selectedItemIndex === -1 ? 0 : this.selectedItemIndex; |
---|
281 | pg = Math.floor(idx / this.numVisible); // current page |
---|
282 | for(i = 0; i < nPages; i++){ |
---|
283 | var w = new SwapView({height: h + "px", lazy:true}); |
---|
284 | this.addChild(w); |
---|
285 | if(i === pg){ |
---|
286 | w.show(); |
---|
287 | this.currentView = w; |
---|
288 | }else{ |
---|
289 | w.hide(); |
---|
290 | } |
---|
291 | } |
---|
292 | this.fillPages(); |
---|
293 | this.resizeItems(); |
---|
294 | var children = this.getChildren(); |
---|
295 | var from = pg - 1 < 0 ? 0 : pg - 1; |
---|
296 | var to = pg + 1 > nPages - 1 ? nPages - 1 : pg + 1; |
---|
297 | for(i = from; i <= to; i++){ |
---|
298 | this.instantiateView(children[i]); |
---|
299 | } |
---|
300 | }, |
---|
301 | |
---|
302 | onError: function(/*String*/ /*===== errText =====*/){ |
---|
303 | // summary: |
---|
304 | // An error handler. |
---|
305 | }, |
---|
306 | |
---|
307 | onUpdate: function(/*Object*/ /*===== item, =====*/ /*Number*/ /*===== insertedInto =====*/){ |
---|
308 | // summary: |
---|
309 | // Adds a new item or updates an existing item. |
---|
310 | }, |
---|
311 | |
---|
312 | onDelete: function(/*Object*/ /*===== item, =====*/ /*Number*/ /*===== removedFrom =====*/){ |
---|
313 | // summary: |
---|
314 | // Deletes an existing item. |
---|
315 | }, |
---|
316 | |
---|
317 | onSet: function(item, attribute, oldValue, newValue){ |
---|
318 | }, |
---|
319 | |
---|
320 | onNew: function(newItem, parentInfo){ |
---|
321 | }, |
---|
322 | |
---|
323 | onStoreClose: function(request){ |
---|
324 | // summary: |
---|
325 | // Called when the store is closed. |
---|
326 | }, |
---|
327 | |
---|
328 | getParentView: function(/*DomNode*/node){ |
---|
329 | // summary: |
---|
330 | // Returns the parent view of the given DOM node. |
---|
331 | var w; |
---|
332 | for(w = registry.getEnclosingWidget(node); w; w = w.getParent()){ |
---|
333 | if(w.getParent() instanceof SwapView){ return w; } |
---|
334 | } |
---|
335 | return null; |
---|
336 | }, |
---|
337 | |
---|
338 | getIndexByItemWidget: function(/*Widget*/w){ |
---|
339 | // summary: |
---|
340 | // Returns the index of a given item widget. |
---|
341 | if(!w){ return -1; } |
---|
342 | var view = w.getParent(); |
---|
343 | return array.indexOf(this.getChildren(), view) * this.numVisible + |
---|
344 | array.indexOf(view.getChildren(), w); |
---|
345 | }, |
---|
346 | |
---|
347 | getItemWidgetByIndex: function(/*Number*/index){ |
---|
348 | // summary: |
---|
349 | // Returns the index of an item widget at a given index. |
---|
350 | if(index === -1){ return null; } |
---|
351 | var view = this.getChildren()[Math.floor(index / this.numVisible)]; |
---|
352 | return view.getChildren()[index % this.numVisible]; |
---|
353 | }, |
---|
354 | |
---|
355 | onPrevBtnClick: function(/*Event*/ /*===== e =====*/){ |
---|
356 | // summary: |
---|
357 | // Called when the "previous" button is clicked. |
---|
358 | if(this.currentView){ |
---|
359 | this.currentView.goTo(-1); |
---|
360 | } |
---|
361 | }, |
---|
362 | |
---|
363 | onNextBtnClick: function(/*Event*/ /*===== e =====*/){ |
---|
364 | // summary: |
---|
365 | // Called when the "next" button is clicked. |
---|
366 | if(this.currentView){ |
---|
367 | this.currentView.goTo(1); |
---|
368 | } |
---|
369 | }, |
---|
370 | |
---|
371 | _onClick: function(e){ |
---|
372 | // summary: |
---|
373 | // Internal handler for click events. |
---|
374 | // tags: |
---|
375 | // private |
---|
376 | if(this.onClick(e) === false){ return; } // user's click action |
---|
377 | if(e && e.type === "keydown"){ // keyboard navigation for accessibility |
---|
378 | if(e.keyCode === 39){ // right arrow |
---|
379 | this.onNextBtnClick(); |
---|
380 | }else if(e.keyCode === 37){ // left arrow |
---|
381 | this.onPrevBtnClick(); |
---|
382 | }else if(e.keyCode !== 13){ // !Enter |
---|
383 | return; |
---|
384 | } |
---|
385 | } |
---|
386 | |
---|
387 | var w; |
---|
388 | for(w = registry.getEnclosingWidget(e.target); ; w = w.getParent()){ |
---|
389 | if(!w){ return; } |
---|
390 | if(w.getParent() instanceof SwapView){ break; } |
---|
391 | } |
---|
392 | this.select(w); |
---|
393 | var idx = this.getIndexByItemWidget(w); |
---|
394 | connect.publish("/dojox/mobile/carouselSelect", [this, w, this.items[idx], idx]); |
---|
395 | }, |
---|
396 | |
---|
397 | select: function(/*Widget|Number*/itemWidget){ |
---|
398 | // summary: |
---|
399 | // Selects the given widget. |
---|
400 | if(typeof(itemWidget) === "number"){ |
---|
401 | itemWidget = this.getItemWidgetByIndex(itemWidget); |
---|
402 | } |
---|
403 | if(this.selectable){ |
---|
404 | if(this.selectedItem){ |
---|
405 | this.selectedItem.set("selected", false); |
---|
406 | domClass.remove(this.selectedItem.domNode, "mblCarouselSlotSelected"); |
---|
407 | } |
---|
408 | if(itemWidget){ |
---|
409 | itemWidget.set("selected", true); |
---|
410 | domClass.add(itemWidget.domNode, "mblCarouselSlotSelected"); |
---|
411 | } |
---|
412 | this.selectedItem = itemWidget; |
---|
413 | } |
---|
414 | }, |
---|
415 | |
---|
416 | onClick: function(/*Event*/ /*===== e =====*/){ |
---|
417 | // summary: |
---|
418 | // User-defined function to handle clicks. |
---|
419 | // tags: |
---|
420 | // callback |
---|
421 | }, |
---|
422 | |
---|
423 | instantiateView: function(view){ |
---|
424 | // summary: |
---|
425 | // Instantiates the given view. |
---|
426 | if(view && !view._instantiated){ |
---|
427 | var isHidden = (domStyle.get(view.domNode, "display") === "none"); |
---|
428 | if(isHidden){ |
---|
429 | domStyle.set(view.domNode, {visibility:"hidden", display:""}); |
---|
430 | } |
---|
431 | lazyLoadUtils.instantiateLazyWidgets(view.containerNode, null, function(root){ |
---|
432 | if(isHidden){ |
---|
433 | domStyle.set(view.domNode, {visibility:"visible", display:"none"}); |
---|
434 | } |
---|
435 | }); |
---|
436 | view._instantiated = true; |
---|
437 | } |
---|
438 | }, |
---|
439 | |
---|
440 | handleViewChanged: function(view){ |
---|
441 | // summary: |
---|
442 | // Listens to "/dojox/mobile/viewChanged" events. |
---|
443 | if(view.getParent() !== this){ return; } |
---|
444 | if(this.currentView.nextView(this.currentView.domNode) === view){ |
---|
445 | this.instantiateView(view.nextView(view.domNode)); |
---|
446 | }else{ |
---|
447 | this.instantiateView(view.previousView(view.domNode)); |
---|
448 | } |
---|
449 | this.currentView = view; |
---|
450 | }, |
---|
451 | |
---|
452 | _setTitleAttr: function(/*String*/title){ |
---|
453 | // tags: |
---|
454 | // private |
---|
455 | this.titleNode.innerHTML = this._cv ? this._cv(title) : title; |
---|
456 | this._set("title", title); |
---|
457 | } |
---|
458 | }); |
---|
459 | |
---|
460 | Carousel.ChildSwapViewProperties = { |
---|
461 | // summary: |
---|
462 | // This property can be specified for the SwapView children of a dojox/mobile/Carousel. |
---|
463 | |
---|
464 | // lazy: Boolean |
---|
465 | // Specifies that the Carousel child must be lazily loaded. |
---|
466 | lazy: false |
---|
467 | }; |
---|
468 | |
---|
469 | // Since any widget can be specified as an Accordion child, mix ChildWidgetProperties |
---|
470 | // into the base widget class. (This is a hack, but it's effective.) |
---|
471 | // This is for the benefit of the parser. Remove for 2.0. Also, hide from doc viewer. |
---|
472 | lang.extend(SwapView, /*===== {} || =====*/ Carousel.ChildSwapViewProperties); |
---|
473 | |
---|
474 | return has("dojo-bidi") ? declare("dojox.mobile.Carousel", [Carousel, BidiCarousel]) : Carousel; |
---|
475 | }); |
---|