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