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