[483] | 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 | }); |
---|