1 /* 2 * Copyright © 2009 Apple Inc. All rights reserved. 3 */ 4 5 // --------------------------------------------------- 6 // A two dimensional view with absolutely positioned elements 7 // --------------------------------------------------- 8 9 // data source method names 10 const TKComicViewNumberOfPanels = 'comicViewNumberOfPanels'; 11 const TKComicViewPanelPositionAtIndex = 'comicViewPanelPositionAtIndex'; // expects {x: y:} to be returned 12 const TKComicViewPanelScaleAtIndex = 'comicViewPanelScaleAtIndex'; // expects a float returned 13 14 const TKComicViewPanelImageAtIndex = 'comicViewPanelImageAtIndex'; 15 const TKComicViewPanelImagePositionAtIndex = 'comicViewPanelImagePositionAtIndex'; // expects {x: y:} to be returned 16 17 const TKComicViewPanelBubbleAtIndex = 'comicViewPanelBubbleAtIndex'; 18 const TKComicViewPanelBubblePositionAtIndex = 'comicViewPanelBubblePositionAtIndex'; // expects {x: y:} to be returned 19 20 // delegate method names 21 const TKComicViewDidShowPanelAtIndex = 'comicViewDidShowPanelAtIndex'; 22 23 // css protocol 24 const TKComicViewCSSRootClass = 'comic-view'; 25 const TKComicViewCSSContainerClass = 'comic-view-container'; 26 const TKComicViewCSSPanelClass = 'comic-view-panel'; 27 28 const TKComicViewCSSPanelImageClass = 'comic-view-panel-image'; 29 const TKComicViewCSSPanelBubbleClass = 'comic-view-panel-bubble'; 30 31 const TKComicViewCSSCurrentClass = 'comic-view-panel-current'; 32 const TKComicViewCSSNextClass = 'comic-view-panel-next'; 33 const TKComicViewCSSPreviousClass = 'comic-view-panel-previous'; 34 35 TKComicView.synthetizes = ['dataSource', 36 'delegate', 37 'currentPanelIndex', 38 'nextPanelOpacity', // float [0,1] 39 'previousPanelOpacity', // float [0,1] 40 'transitionDuration', // in milliseconds 41 'numberOfPanels']; 42 43 function TKComicView (element) { 44 this.callSuper(); 45 this._currentPanelIndex = 0; 46 this._nextPanelOpacity = 0.5; 47 this._previousPanelOpacity = 0.5; 48 this._transitionDuration = 500; 49 50 if (element) { 51 this.element = element; 52 } else { 53 // create the element we'll use as root 54 this.element = document.createElement("div"); 55 } 56 this.element.addClassName(TKComicViewCSSRootClass); 57 58 // create the positioner 59 this.container = document.createElement("div"); 60 this.container.style.webkitTransitionProperty = "-webkit-transform"; 61 this.container.style.webkitTransitionDuration = this._transitionDuration + "ms"; 62 this.container.addClassName(TKComicViewCSSContainerClass); 63 this.element.appendChild(this.container); 64 65 this.currentPanel = null; 66 this.nextPanel = null; 67 this.previousPanel = null; 68 } 69 70 TKComicView.prototype.init = function () { 71 72 if (!this.dataSource || 73 !TKUtils.objectHasMethod(this.dataSource, TKComicViewNumberOfPanels) || 74 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelPositionAtIndex) || 75 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelScaleAtIndex) || 76 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelImageAtIndex) || 77 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelImagePositionAtIndex) || 78 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelBubbleAtIndex) || 79 !TKUtils.objectHasMethod(this.dataSource, TKComicViewPanelBubblePositionAtIndex)) { 80 debug("TKComicView: dataSource does not exist or does not have correct methods"); 81 return; 82 } 83 84 this.moveToPanel(this._currentPanelIndex); 85 }; 86 87 TKComicView.prototype.setTransitionDuration = function (newTransitionDuration) { 88 this._transitionDuration = newTransitionDuration; 89 if (this.container) { 90 this.container.style.webkitTransitionDuration = this._transitionDuration + "ms"; 91 } 92 }; 93 94 TKComicView.prototype.getNumberOfPanels = function () { 95 return this.dataSource[TKComicViewNumberOfPanels](this); 96 }; 97 98 TKComicView.prototype.showNextPanel = function () { 99 if (this._currentPanelIndex < this.numberOfPanels - 1) { 100 this.moveToPanel(this._currentPanelIndex + 1); 101 } 102 }; 103 104 TKComicView.prototype.showPreviousPanel = function () { 105 if (this._currentPanelIndex > 0) { 106 this.moveToPanel(this._currentPanelIndex - 1); 107 } 108 }; 109 110 TKComicView.prototype.loadPanel = function (index) { 111 var newPanel = document.createElement("div"); 112 newPanel.addClassName(TKComicViewCSSPanelClass); 113 114 newPanel.style.webkitTransitionProperty = "opacity"; 115 newPanel.style.webkitTransitionDuration = this._transitionDuration + "ms"; 116 newPanel.style.opacity = 0; 117 118 var image = this.dataSource[TKComicViewPanelImageAtIndex](this, index); 119 image.addClassName(TKComicViewCSSPanelImageClass); 120 var position = this.dataSource[TKComicViewPanelImagePositionAtIndex](this, index); 121 var translate = "translate(" + position.x + "px, " + position.y + "px)"; 122 image.style.webkitTransform = translate; 123 newPanel.appendChild(image); 124 125 var bubble = this.dataSource[TKComicViewPanelBubbleAtIndex](this, index); 126 if (bubble) { 127 bubble.addClassName(TKComicViewCSSPanelBubbleClass); 128 position = this.dataSource[TKComicViewPanelBubblePositionAtIndex](this, index); 129 translate = "translate(" + position.x + "px, " + position.y + "px)"; 130 bubble.style.webkitTransform = translate; 131 newPanel.appendChild(bubble); 132 } 133 134 return newPanel; 135 }; 136 137 TKComicView.prototype.moveToPanel = function (index) { 138 // we assume we're only going one step in each direction for now 139 140 var _this = this; 141 var forwards = (index >= this._currentPanelIndex); 142 var outgoingPanel = this.previousPanel; 143 var incomingPanel = this.nextPanel; 144 var nextPanelIndexToLoad = index + 1; 145 146 if (!forwards) { 147 outgoingPanel = this.nextPanel; 148 incomingPanel = this.previousPanel; 149 nextPanelIndexToLoad = index - 1; 150 } 151 152 // Step 1. Tell the outgoing panel to fade out. Remove it from the document 153 // after it has faded 154 if (outgoingPanel) { 155 outgoingPanel.style.opacity = 0; 156 setTimeout(function() { 157 _this.container.removeChild(outgoingPanel); 158 }, this._transitionDuration + 20); 159 } 160 161 // Step 2. Remove the incoming panel from the container (we add it again later) 162 if (incomingPanel) { 163 this.container.removeChild(incomingPanel); 164 } 165 166 // Step 3. Load in the new panel, and insert it at the end of the container 167 // with the "next" class and opacity 168 var newPanel = this.loadPanel(index); 169 newPanel.style.opacity = this._nextPanelOpacity; 170 this.container.appendChild(newPanel); 171 172 // Step 3. Tell the current panel to become either "previous" or "next" panel 173 if (this.currentPanel) { 174 this.currentPanel.style.opacity = forwards ? this._previousPanelOpacity : this._nextPanelOpacity; 175 } 176 177 // Step 4. Load in the new next panel, and insert it at the start of the container 178 var nextPanel = null; 179 if (nextPanelIndexToLoad >= 0 && nextPanelIndexToLoad <= this.numberOfPanels - 1) { 180 nextPanel = this.loadPanel(nextPanelIndexToLoad); 181 this.container.insertBefore(nextPanel, this.container.firstChild); 182 } 183 184 // Step 5. Tell the new current panel to fade in completely, and the new next 185 // panel to fade up. 186 // This needs to be done on a timeout, since we just added it to the document 187 setTimeout(function() { 188 newPanel.style.opacity = 1; 189 if (nextPanel) { 190 nextPanel.style.opacity = forwards ? _this.nextPanelOpacity : _this.previousPanelOpacity; 191 } 192 }, 0); 193 194 // Step 6. Assign all the right references 195 if (forwards) { 196 this.previousPanel = this.currentPanel; 197 this.nextPanel = nextPanel; 198 } else { 199 this.previousPanel = nextPanel; 200 this.nextPanel = this.currentPanel; 201 } 202 this.currentPanel = newPanel; 203 this._currentPanelIndex = index; 204 205 // Step 7. Assign classes to elements 206 this.applyClass(this.currentPanel, TKComicViewCSSCurrentClass); 207 this.applyClass(this.previousPanel, TKComicViewCSSPreviousClass); 208 this.applyClass(this.nextPanel, TKComicViewCSSNextClass); 209 210 // Step 8. Position the container 211 var position = this.dataSource[TKComicViewPanelPositionAtIndex](this, index); 212 var scale = this.dataSource[TKComicViewPanelScaleAtIndex](this, index); 213 var transform = " scale(" + scale + ") translate(" + (-1 * position.x) + "px, " + (-1 * position.y) + "px)"; 214 this.container.style.webkitTransform = transform; 215 216 // Step 9. Tell the delegate we moved 217 if (TKUtils.objectHasMethod(this.delegate, TKComicViewDidShowPanelAtIndex)) { 218 this.delegate[TKComicViewDidShowPanelAtIndex](this, index); 219 } 220 }; 221 222 TKComicView.prototype.applyClass = function (element, className) { 223 if (element) { 224 element.removeClassName(TKComicViewCSSCurrentClass); 225 element.removeClassName(TKComicViewCSSPreviousClass); 226 element.removeClassName(TKComicViewCSSNextClass); 227 element.addClassName(className); 228 } 229 }; 230 231 TKClass(TKComicView); 232 233 /* ====================== Datasource helper ====================== */ 234 235 function TKComicViewDataSourceHelper(data) { 236 this.data = data; 237 }; 238 239 TKComicViewDataSourceHelper.prototype.comicViewNumberOfPanels = function(view) { 240 if (this.data) { 241 return this.data.length; 242 } else { 243 return 0; 244 } 245 }; 246 247 TKComicViewDataSourceHelper.prototype.comicViewPanelPositionAtIndex = function(view, index) { 248 if (!this.data || index >= this.data.length) { 249 return null; 250 } 251 var source = this.data[index]; 252 return { 253 x: TKUtils.objectIsUndefined(source.x) ? 0 : source.x, 254 y: TKUtils.objectIsUndefined(source.y) ? 0 : source.y 255 }; 256 }; 257 258 TKComicViewDataSourceHelper.prototype.comicViewPanelScaleAtIndex = function(view, index) { 259 if (!this.data || index >= this.data.length) { 260 return null; 261 } 262 var source = this.data[index]; 263 return TKUtils.objectIsUndefined(source.scale) ? 1 : source.scale; 264 }; 265 266 267 TKComicViewDataSourceHelper.prototype.comicViewPanelImageAtIndex = function(view, index) { 268 if (!this.data || index >= this.data.length) { 269 return null; 270 } 271 var source = this.data[index]; 272 return TKUtils.buildElement(source.image); 273 }; 274 275 TKComicViewDataSourceHelper.prototype.comicViewPanelImagePositionAtIndex = function(view, index) { 276 if (!this.data || index >= this.data.length) { 277 return null; 278 } 279 var source = this.data[index]; 280 return { 281 x: TKUtils.objectIsUndefined(source.image.x) ? 0 : source.image.x, 282 y: TKUtils.objectIsUndefined(source.image.y) ? 0 : source.image.y 283 }; 284 }; 285 286 TKComicViewDataSourceHelper.prototype.comicViewPanelBubbleAtIndex = function(view, index) { 287 if (!this.data || index >= this.data.length) { 288 return null; 289 } 290 var source = this.data[index]; 291 if (source.bubble) { 292 return TKUtils.buildElement(source.bubble); 293 } else { 294 return null; 295 } 296 }; 297 298 TKComicViewDataSourceHelper.prototype.comicViewPanelBubblePositionAtIndex = function(view, index) { 299 if (!this.data || index >= this.data.length) { 300 return null; 301 } 302 var source = this.data[index]; 303 if (source.bubble) { 304 return { 305 x: TKUtils.objectIsUndefined(source.bubble.x) ? 0 : source.bubble.x, 306 y: TKUtils.objectIsUndefined(source.bubble.y) ? 0 : source.bubble.y 307 }; 308 } else { 309 return { x: 0, y: 0}; 310 } 311 }; 312 313 /* ====================== Declarative helper ====================== */ 314 315 TKComicView.buildComicView = function(element, data) { 316 if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKComicView") { 317 return null; 318 } 319 320 var comicView = new TKComicView(element); 321 322 if (!TKUtils.objectIsUndefined(data.elements)) { 323 comicView.dataSource = new TKComicViewDataSourceHelper(data.elements); 324 } 325 326 TKComicView.synthetizes.forEach(function(prop) { 327 if (prop != "dataSource" && prop != "delegate") { 328 if (!TKUtils.objectIsUndefined(data[prop])) { 329 comicView[prop] = data[prop]; 330 } 331 } 332 }); 333 334 return comicView; 335 }; 336 337