1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 // ---------------------------------------------------
  6 // A page control implementation
  7 // ---------------------------------------------------
  8 
  9 /**
 10  *  @class
 11  *  @name TKPageControlData
 12  *  @since TuneKit 1.0
 13  */
 14 
 15 /**
 16  *  @name TKPageControlData.prototype
 17  *
 18  *  @property {int} distanceBetweenPageIndicators The distance in pixels between the center points of each indicator, essentially the overall
 19  *  width of each indicator.
 20  *
 21  *  @property {bool} showPageElements Indicates whether individual elements for each page should be shown, or only the pill. Defaults to <code>false</code.
 22  *
 23  *  @property {Element} indicatorElement The element to be used for the active element indicator.
 24  *
 25  *  @property {Element} pageElement The template element to be used to represent each of the pages.
 26  *
 27  *  @property {bool} incrementalJumpsOnly Indicates whether only interactive jumps of one page at a time are allowed.
 28  *
 29  *  @property {bool} allowsDragging Indicates whether dragging is allowed.
 30  *
 31  */
 32 
 33 // data source method names
 34 const TKPageControlIndicatorElement = 'pageControlIndicatorElement';
 35 const TKPageControlPageElement = 'pageControlPageElement';
 36 
 37 // delegate method names
 38 const TKPageControlDidUpdateCurrentPage = 'pageControlDidUpdateCurrentPage';
 39 
 40 // css protocol
 41 const TKPageControlCSSClass = 'page-control';
 42 const TKPageControlCSSIndicatorElementClass = 'page-control-indicator-element';
 43 const TKPageControlCSSPageElementClass = 'page-control-page-element';
 44 
 45 // TODO: orientations
 46 
 47 TKPageControl.synthetizes = ['dataSource',
 48                              'delegate',
 49                              'interactive', // whether or not this view will listen for mouse events
 50                              'currentPage', // the index of the current page
 51                              'numPages', // the number of pages
 52                              'distanceBetweenPageIndicators', // the distance between the page ticks - FIXME: this is the wrong term
 53                              'showPageElements', // whether or not to show the page indicators
 54                              'incrementalJumpsOnly', // if true, the page control only allows interactive jumps of one page at a time
 55                              'allowsDragging', // if true, the pages control allows dragging
 56 							 'uniqueClass', // adds a unique class name to each page indicator element
 57                              'deferCurrentPageDisplay']; // whether or not to update the display as soon as currentPage changes - needed for syncing with an external control when interactive
 58 
 59 function TKPageControl (element) {
 60   this.callSuper();
 61   //
 62   this._interactive = true;
 63   this._currentPage = 0;
 64   this._numPages = 1;
 65   this._distanceBetweenPageIndicators = 50;
 66   this._showPageElements = true;
 67   this._incrementalJumpsOnly = true;
 68   this._allowsDragging = false;
 69   this._deferCurrentPageDisplay = false;
 70 	this._uniqueClass = false;
 71   
 72   if (element) {
 73     this.element = element;
 74   } else {
 75     // create the element we'll use as a container
 76     this.element = document.createElement("div");
 77   }
 78   this.element.addClassName(TKPageControlCSSClass);
 79 }
 80 
 81 TKPageControl.prototype.init = function () {
 82 
 83   if (!this.dataSource ||
 84       !TKUtils.objectHasMethod(this.dataSource, TKPageControlIndicatorElement) ||
 85       (this._showPageElements && !TKUtils.objectHasMethod(this.dataSource, TKPageControlPageElement))) {
 86     return;
 87   }
 88 
 89   // add page elements if needed
 90   if (this.showPageElements) {
 91     // get the page element
 92     var pageElement = this.dataSource[TKPageControlPageElement](this);
 93     for (var i=0; i < this._numPages; i++) {
 94       var el = pageElement.cloneNode();
 95       el.addClassName(TKPageControlCSSPageElementClass);
 96 		if(this._uniqueClass) el.addClassName('thumb-'+i);
 97       el.style.webkitTransform = "translate(" + (i * this._distanceBetweenPageIndicators) + "px, 0px)";
 98       el._pageControlIndex = i;
 99       this.element.appendChild(el);
100     }
101   }
102   
103   // add indicator element
104   var indicatorElement = this.dataSource[TKPageControlIndicatorElement](this);
105   indicatorElement.addClassName(TKPageControlCSSIndicatorElementClass);
106   indicatorElement.style.webkitTransform = "translate(" + (this._currentPage * this._distanceBetweenPageIndicators) + "px, 0px)";
107   this.element.appendChild(indicatorElement);
108   
109   if (this._interactive) {
110     if (this._allowsDragging) {
111       this.element.addEventListener("mousedown", this, false);
112     } else {
113       this.element.addEventListener("click", this, false);
114     }
115   }
116 };
117 
118 TKPageControl.prototype.setCurrentPage = function (newCurrentPage) {
119   if (this._currentPage == newCurrentPage ||
120       newCurrentPage < 0 ||
121       newCurrentPage >= this._numPages) {
122     return;
123   }
124   
125   this._currentPage = newCurrentPage;
126   
127   if (TKUtils.objectHasMethod(this.delegate, TKPageControlDidUpdateCurrentPage)) {
128     this.delegate[TKPageControlDidUpdateCurrentPage](this, this._currentPage);
129   }
130   
131   if (!this._deferCurrentPageDisplay) {
132     this.updateCurrentPageDisplay();
133   }
134 };
135 
136 TKPageControl.prototype.updateCurrentPageDisplay = function () {
137   var indicatorElement = this.dataSource[TKPageControlIndicatorElement](this);
138   indicatorElement.style.webkitTransform = "translate(" + (this._currentPage * this._distanceBetweenPageIndicators) + "px, 0px)";
139 };
140 
141 TKPageControl.prototype.handleEvent = function (event) {
142   switch (event.type) {
143     case "mousedown":
144       this.handleDragBegan(event);
145       break;
146     case "mousemove":
147       this.handleDragMove(event);
148       break;
149     case "mouseup":
150       this.handleDragEnded(event);
151       break;
152     case "click":
153       this.handleClick(event);
154       break;
155     default:
156       debug("unhandled event type in TKPageControl: " + event.type);
157   }
158 };
159 
160 TKPageControl.prototype.setupMouseInteraction = function () {
161   // in case we have page elements, let's look at the position of the first element as
162   // the minimum x value that we'll use for interactions from then on
163   if (this._showPageElements) {
164     // get the elements
165     var page_elements = this.element.querySelectorAll('.' + TKPageControlCSSPageElementClass);
166     var page_element_bounds = page_elements[0].getBounds();
167     this.minX = page_element_bounds.x + (page_element_bounds.width - this._distanceBetweenPageIndicators) / 2;
168   }
169   // otherwise, use the bounds of the container
170   else {
171     this.minX = this.element.getBounds().x;
172   }
173   // now convert this value into the coordinate system of our element
174   var point = window.webkitConvertPointFromPageToNode(this.element, new WebKitPoint(this.minX, 0));
175   this.minX = point.x;
176 };
177 
178 TKPageControl.prototype.pageIndexAtXY = function (x, y) {
179   var point = window.webkitConvertPointFromPageToNode(this.element, new WebKitPoint(x, y));
180   var page_index = Math.floor((point.x - this.minX) / this._distanceBetweenPageIndicators);
181   return Math.max(Math.min(page_index, this._numPages), 0);
182 };
183 
184 TKPageControl.prototype.handleClick = function (event) {
185   // set up the mouse interaction
186   this.setupMouseInteraction();
187   // mark that we are interacting
188   // XXX: should we really be doing this? This class is not being removed it seems.
189   this.element.addClassName("interactive");
190   // set the current page based on that right from the get go
191   this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY);
192 };
193 
194 TKPageControl.prototype.handleDragBegan = function (event) {
195   if (!this._allowsDragging) {
196     return;
197   }
198   // ensure we cancel the default web page behavior for a dragging interaction
199   event.preventDefault();
200   // set up the mouse interaction
201   this.setupMouseInteraction();
202   // mark that we are interacting
203   this.element.addClassName("interactive");
204   // set the current page based on that right from the get go
205   this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY);
206   // track mouse moves
207   window.addEventListener("mousemove", this, false);
208   window.addEventListener("mouseup", this, false);
209 };
210 
211 TKPageControl.prototype.handleDragMove = function (event) {
212   // ensure we cancel the default web page behavior for a dragging interaction
213   event.preventDefault();
214   // update the page
215   this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY);
216 };
217 
218 TKPageControl.prototype.handleDragEnded = function (event) {
219   // ensure we cancel the default web page behavior for a dragging interaction
220   event.preventDefault();
221   // mark that we are not interacting anymore
222   this.element.removeClassName("interactive");
223   // stop tracking events
224   window.removeEventListener("mousemove", this);
225   window.removeEventListener("mouseup", this);
226 };
227 
228 TKClass(TKPageControl);
229 
230 /* ====================== Datasource helper ====================== */
231 
232 function TKPageControlDataSourceHelper(data) {
233   this.data = data;
234   this.pageElement = null;
235   this.indicatorElement = null;
236 };
237 
238 TKPageControlDataSourceHelper.prototype.pageControlPageElement = function(pageControl) {
239   if (!this.data) {
240     return null;
241   }
242   if (!this.pageElement) {
243     this.pageElement = TKUtils.buildElement(this.data.pageElement);
244   }
245   return this.pageElement;
246 };
247 
248 TKPageControlDataSourceHelper.prototype.pageControlIndicatorElement = function(pageControl) {
249   if (!this.data) {
250     return null;
251   }
252   if (!this.indicatorElement) {
253     this.indicatorElement = TKUtils.buildElement(this.data.indicatorElement);
254   }
255   return this.indicatorElement;
256 };
257 
258 /* ====================== Declarative helper ====================== */
259 
260 TKPageControl.buildPageControl = function(element, data) {
261   if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKPageControl") {
262     return null;
263   }
264 
265   var pageControl = new TKPageControl(element);
266 
267   pageControl.dataSource = new TKPageControlDataSourceHelper(data);
268 
269   TKPageControl.synthetizes.forEach(function(prop) {
270     if (prop != "dataSource" && prop != "delegate") {
271       if (!TKUtils.objectIsUndefined(data[prop])) {
272         pageControl[prop] = data[prop];
273       }
274     }
275   });
276 
277   return pageControl;
278 };
279 
280