~ubuntu-branches/ubuntu/trusty/ubuntu-html5-theme/trusty-proposed

« back to all changes in this revision

Viewing changes to 0.1/ambiance/js/tabs.js

  • Committer: Package Import Robot
  • Author(s): Ubuntu daily release, Adnane Belmadiaf, daker, Kyle Nitzsche, Colin Watson, Alexandre Abreu, Ubuntu daily release
  • Date: 2014-01-07 23:46:53 UTC
  • mfrom: (1.1.7)
  • Revision ID: package-import@ubuntu.com-20140107234653-oagvm6guom4h9dvh
Tags: 0.1+14.04.20140107-0ubuntu1
[ Adnane Belmadiaf ]
* s/parentNode/parendNode Spaces instead of tabs. (LP: #1241215)
* Add option selector widget. (LP: #1232533)

[ daker ]
* Passe the UI variable name so we will not lock the variable. (LP:
  #1222878)
* Made variable declaration locale. (LP: #1222881)
* Fixed z-index for the list items aside. (LP: #1223973)
* s/parentNode/parendNode Spaces instead of tabs. (LP: #1241215)
* Space instead of tab. (LP: #1240682)
* Made list items with headers differentes. (LP: #1246446)
* Updated the progressbar component to match the design.
* Move tabs closer to what they should be, Expand API to match the QML
  one (at least at the Tabs level), .
* Add option selector widget. (LP: #1232533)

[ Kyle Nitzsche ]
* Add button id to "Invalid button ID" error message .
* This MR does three main things: 1) Implements yuidoc comments in all
  js files to support API doc generation, and provides yuidoc assets
  (theme dir and json file) needed to build the API docs. Bug LP:
  #1241029 3) Provides JS classes for shape and page with
  corresponding UbuntuUI prototype constructor functions. Bug LP:
  #1243248 4) Adds a getEl(UbuntuUIObject) to return the element for
  any Ubuntu class. Also LP: #1243248. (LP: #1243248, #1241029)

[ Colin Watson ]
* Make ubuntu-html5-theme Multi-Arch: foreign.

[ Alexandre Abreu ]
* Fix exec path in app-gallery app desktop file. (LP: #1235321)
* Add convenient element() function to most widgets ... (we might want
  to factor those out in a second step).
* Move tabs closer to what they should be, Expand API to match the QML
  one (at least at the Tabs level), .
* Add HTML5 webapp container package.

[ Ubuntu daily release ]
* Automatic snapshot from revision 98

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
 * <http://www.gnu.org/licenses/>
21
21
 */
22
22
 
23
 
/* Tabs */
24
 
var Tabs = function (tabs) {
 
23
/**
 
24
 * Tabs are the standard way to provide app navigation from your application Header. See the Header class for more information.
 
25
 
 
26
Declare the Header and Tabs in HTML as a direct child of the top level Page as a sibling to the content div.
 
27
 
 
28
 * @class Tabs
 
29
 * @constructor
 
30
 * @namespace UbuntuUI
 
31
 * @example
 
32
      <body>
 
33
        <div data-role="page">
 
34
          <header data-role="header" id="headerID">
 
35
            <ul data-role="tabs">
 
36
              <li data-role="tab" data-page="main">
 
37
                Main
 
38
              </li>
 
39
              <li data-role="tab" data-page="page2">
 
40
                Two
 
41
              </li>
 
42
            </ul>
 
43
          </header>
 
44
          <div data-role="content">
 
45
            [...]
 
46
          </div>
 
47
        </div>
 
48
      </body>
 
49
 
 
50
      JavaScript:
 
51
      UI.tabs.METHOD();
 
52
*/
 
53
var Tabs = (function () {
25
54
    var pageX,
26
55
        pageY,
27
56
        isScrolling,
31
60
        resistance,
32
61
        tabsWidth,
33
62
        activeTab,
34
 
        t1,
35
 
        t2;
36
 
 
37
 
    var getScroll = function () {
38
 
        var translate3d = tabs.style.webkitTransform.match(/translate3d\(([^,]*)/);
39
 
        return parseInt(translate3d ? translate3d[1] : 0)
40
 
    };
41
 
 
42
 
    var setTouchInProgress = function (val) {
43
 
 
44
 
        //Add or remove event listeners depending on touch status
45
 
        if (val === true) {
46
 
            tabs.addEventListener(UI.touchEvents.touchMove, onTouchMove);
47
 
            tabs.addEventListener(UI.touchEvents.touchEnd, onTouchEnd);
48
 
 
49
 
            // we only have leave events on desktop, we manually calcuate
50
 
            // leave on touch as its not supported in webkit
51
 
            if (UI.touchEvents.touchLeave) {
52
 
                tabs.addEventListener(UI.touchEvents.touchLeave, onTouchLeave);
53
 
            }
54
 
        } else {
55
 
            tabs.removeEventListener(UI.touchEvents.touchMove, onTouchMove, false);
56
 
            tabs.removeEventListener(UI.touchEvents.touchEnd, onTouchEnd, false);
57
 
 
58
 
            // we only have leave events on desktop, we manually calcuate
59
 
            // leave on touch as its not supported in webkit
60
 
            if (UI.touchEvents.touchLeave) {
61
 
                tabs.removeEventListener(UI.touchEvents.touchLeave, onTouchLeave, false);
62
 
            }
63
 
        }
64
 
    };
65
 
 
66
 
    var onTouchStart = function (e) {
67
 
        if (!tabs) return;
68
 
        window.clearTimeout(t1);
69
 
        window.clearTimeout(t2);
70
 
        isScrolling = undefined;
71
 
        tabsWidth = tabs.offsetWidth;
72
 
        resistance = 1;
73
 
 
74
 
        if (!UI.isTouch) {
75
 
            e.touches = [{
76
 
                pageX: e.pageX,
77
 
                pageY: e.pageY
78
 
            }];
79
 
        }
80
 
        pageX = e.touches[0].pageX;
81
 
        pageY = e.touches[0].pageY;
82
 
 
83
 
        tabs.style['-webkit-transition-duration'] = 0;
84
 
        setTouchInProgress(true);
85
 
    };
86
 
 
87
 
    var onTouchMove = function (e) {
88
 
        if (!UI.isTouch) {
89
 
            e.touches = [{
90
 
                pageX: e.pageX,
91
 
                pageY: e.pageY
92
 
            }];
93
 
        }
94
 
        deltaX = e.touches[0].pageX - pageX;
95
 
        deltaY = e.touches[0].pageY - pageY;
96
 
        pageX = e.touches[0].pageX;
97
 
        pageY = e.touches[0].pageY;
98
 
 
99
 
        if (typeof isScrolling == 'undefined') {
100
 
            isScrolling = Math.abs(deltaY) > Math.abs(deltaX);
101
 
        }
102
 
 
103
 
        if (isScrolling) return;
104
 
        offsetX = (deltaX / resistance) + getScroll();
105
 
 
106
 
        tabs.style.webkitTransform = 'translate3d(' + offsetX + 'px,0,0)';
107
 
    };
108
 
 
109
 
    var onTouchEnd = function (e) {
110
 
        if (!tabs || isScrolling) return;
111
 
        setTouchInProgress(false);
112
 
 
113
 
        activeTab = document.querySelector('[data-role="tab"].active');
114
 
        t1 = window.setTimeout(function() {
115
 
                offsetX = activeTab.offsetLeft;
116
 
                tabs.style['-webkit-transition-duration'] = '.3s';
117
 
                tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
118
 
                [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
119
 
                    el.classList.toggle('inactive');
120
 
                });
121
 
        }, 5000);
122
 
    };
123
 
 
124
 
    var onTouchLeave = function (e) {};
125
 
 
126
 
    var onClicked = function (e) {
127
 
 
128
 
        if ((this.className).indexOf('inactive') > -1) {
129
 
            window.clearTimeout(t2);
130
 
            activeTab = document.querySelector('[data-role="tab"].active');
131
 
            offsetX = this.offsetLeft;
132
 
            tabs.style['-webkit-transition-duration'] = '.3s';
133
 
            tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
134
 
            activeTab.classList.remove('inactive');
135
 
            activeTab.classList.remove('active');
136
 
            this.classList.remove('inactive');
137
 
            this.classList.add('active');
138
 
 
139
 
            [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
140
 
                el.classList.remove('inactive');
141
 
            });
142
 
 
143
 
            /*FIXME : We need to try to implement the infinite sliding
144
 
            Array.prototype.slice.call(
145
 
                    document.querySelectorAll('ul[data-role=tabs] li:nth-child(-n+3)')
146
 
                ).map(function(element) {
147
 
                    return element.cloneNode(true);
148
 
                }).forEach(function(element) {
149
 
                    element.classList.remove('active');
150
 
                    tabs.appendChild(element);
151
 
                });*/
152
 
 
153
 
        } else {
154
 
            [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
155
 
                el.classList.toggle('inactive');
156
 
            });
157
 
            t2 = window.setTimeout(function() {
158
 
                [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
159
 
                    el.classList.toggle('inactive');
160
 
                });
161
 
            }, 5000);
162
 
        }
163
 
        e.preventDefault();
164
 
    };
165
 
 
166
 
    tabs.addEventListener(UI.touchEvents.touchStart, onTouchStart);
167
 
    tabs.addEventListener(UI.touchEvents.touchMove, onTouchMove);
168
 
    tabs.addEventListener(UI.touchEvents.touchEnd, onTouchEnd);
169
 
 
170
 
    [].forEach.call(document.querySelectorAll('[data-role="tab"]'), function (el) {
171
 
        el.addEventListener('click', onClicked, false);
172
 
    });
173
 
};
 
 
b'\\ No newline at end of file'
 
63
        navigationTimer,
 
64
        t2,
 
65
        startTimestamp;
 
66
 
 
67
    var STATES = {
 
68
        basic: 0,
 
69
        transitioning_to_navigation: 1,
 
70
        navigating: 2,
 
71
    };
 
72
    var state = STATES.basic;
 
73
 
 
74
    function Tabs (tabs, touchInfoDelegate) {
 
75
        if (tabs == null || touchInfoDelegate == null) {
 
76
            return;
 
77
        }
 
78
        this._tabs = tabs;
 
79
 
 
80
        this._infos = {
 
81
            width: this.__getTabHeadersWidth(),
 
82
            count: this._tabs.querySelectorAll('li').length
 
83
        };
 
84
        this._touchInfoDelegate = touchInfoDelegate;
 
85
 
 
86
        var touchEvents = touchInfoDelegate.touchEvents;
 
87
        this._tabs.addEventListener(touchEvents.touchStart,
 
88
                                    this.__onTouchStart.bind(this));
 
89
        this._tabs.addEventListener(touchEvents.touchMove, this.__onTouchMove.bind(this));
 
90
        this._tabs.addEventListener(touchEvents.touchEnd, this.__onTouchEnd.bind(this));
 
91
 
 
92
        // we only have leave events on desktop, we manually calcuate
 
93
        // leave on touch as its not supported in webkit
 
94
        if (touchEvents.touchLeave) {
 
95
            this._tabs.addEventListener(touchEvents.touchLeave, this.__onTouchLeave.bind(this));
 
96
        }
 
97
        
 
98
        // initialize default page
 
99
        this.__setupInitialTabVisibility();
 
100
    };
 
101
 
 
102
    Tabs.prototype = {
 
103
        /**
 
104
         * Return the index of the selected tab
 
105
         * @property selectedTabIndex
 
106
         * @return {Integer} - The index of the element in the list of tabs or -1
 
107
         */
 
108
        get selectedTabIndex() {
 
109
            return [].prototype.slice.call(this._tabs.querySelector('ul').children).indexOf(activeTab);
 
110
        },
 
111
 
 
112
        /**
 
113
         * Return the currently selected tab element
 
114
         * @property selectedTab
 
115
         * @return {Element} - The currently selected element or null
 
116
         */
 
117
        get selectedTab() {
 
118
            return activeTab;
 
119
        },
 
120
 
 
121
        /**
 
122
         * Return the page associated with the currently selected tab
 
123
         * @property currentPage
 
124
         * @return {Element} - Page DOM element associated with the currently selected tab or null
 
125
         */
 
126
        get currentPage() {
 
127
            return this.selectedTab ? this.selectedTab.querySelector('page') : null;
 
128
        },
 
129
 
 
130
        /**
 
131
         * Return the number of tab elements in the header
 
132
         * @property count
 
133
         * @return {Integer} - Number of tab elements
 
134
         */
 
135
        get count() {
 
136
            return this.tabChildren.length;
 
137
        },
 
138
 
 
139
        /**
 
140
         * Return the list of DOM elements of the tab
 
141
         * @property tabChildren
 
142
         * @return {Elements} - List of DOM elements in the tab
 
143
         */
 
144
        get tabChildren() {
 
145
            return this._tabs.querySelectorAll('li');
 
146
        },
 
147
 
 
148
        /**
 
149
         * @private
 
150
         */
 
151
        __setupInitialTabVisibility: function() {
 
152
            var tab = this._tabs.querySelector('[data-role="tab"]:first-child');
 
153
            tab.classList.add('active');
 
154
            var updateDisplayStyle = function(tab, value) {
 
155
                var targetPage = document.getElementById(tab.getAttribute('data-page'));
 
156
                if (targetPage)
 
157
                    targetPage.style.display=value;
 
158
            };
 
159
            [].slice.
 
160
                call(this._tabs.querySelectorAll('[data-role="tab"]:not(:first-child)')).
 
161
                forEach(function(element) {
 
162
                    updateDisplayStyle(element, 'none');
 
163
                });
 
164
        },
 
165
 
 
166
        /**
 
167
         * @private
 
168
         */
 
169
        __getScroll: function () {
 
170
            var translate3d = this._tabs.style.webkitTransform.match(/translate3d\(([^,]*)/);
 
171
            return parseInt(translate3d ? translate3d[1] : 0)
 
172
        },
 
173
 
 
174
        /**
 
175
         * @private
 
176
         */
 
177
        __getTabHeadersWidth: function() {
 
178
            return Array.prototype.slice.call(document.querySelectorAll('header li')).reduce(function(prev, cur) { return prev + cur.clientWidth;}, 0);
 
179
        },
 
180
 
 
181
        /**
 
182
         * @private
 
183
         */
 
184
        __getHeaderWidth: function() {
 
185
            return this._tabs.clientWidth;
 
186
        },
 
187
 
 
188
        /**
 
189
         * @private
 
190
         */
 
191
        __clearInternalState: function() {
 
192
            if (navigationTimer) window.clearTimeout(navigationTimer);
 
193
            if (t2) window.clearTimeout(t2);
 
194
            navigationTimer = undefined;
 
195
            t2 = undefined;
 
196
            isScrolling = undefined;
 
197
            tabsWidth = this._tabs.offsetWidth;
 
198
            resistance = 10;
 
199
            startTimestamp = 0;
 
200
        },
 
201
 
 
202
        /**
 
203
         * @private
 
204
         */
 
205
        __onTouchStart: function (e) {
 
206
            console.log('touchstart');
 
207
            if (!this._tabs) return;
 
208
            this.__clearInternalState();
 
209
 
 
210
            if (state === STATES.basic) {
 
211
                state = STATES.transitioning_to_navigation;
 
212
                this.__showNavigation();
 
213
            }
 
214
 
 
215
            var _e = this._touchInfoDelegate.translateTouchEvent(e);
 
216
            pageX = _e.touches[0].pageX;
 
217
            pageY = _e.touches[0].pageY;
 
218
 
 
219
            startTimestamp = _e.timeStamp;
 
220
 
 
221
            this._tabs.style['-webkit-transition-duration'] = 0;
 
222
        },
 
223
 
 
224
        /**
 
225
         * @private
 
226
         */
 
227
        __onTouchMove: function (e) {
 
228
            var _e = this._touchInfoDelegate.translateTouchEvent(e);
 
229
            deltaX = _e.touches[0].pageX - pageX;
 
230
            deltaY = _e.touches[0].pageY - pageY;
 
231
 
 
232
            // TODO: account for DPR
 
233
            var MIN_JITTER_THRESHOLD = 20;
 
234
 
 
235
            var plusDeltaX = Math.abs(deltaX);
 
236
            var plusDeltaY = Math.abs(deltaY);
 
237
 
 
238
            // Account for clicks w/ slight touch jitter
 
239
            isScrolling = plusDeltaY > plusDeltaX &&
 
240
                plusDeltaY > MIN_JITTER_THRESHOLD;
 
241
            if (isScrolling) return;
 
242
 
 
243
            offsetX = (deltaX / resistance) + this.__getScroll();
 
244
 
 
245
            var maxLeftReached = this._tabs.querySelector('li:first-child').getBoundingClientRect().left >= 0 &&
 
246
                deltaX > 0;
 
247
            var maxRightReached = this._tabs.querySelector('li:last-child').getBoundingClientRect().right <= this.__getHeaderWidth() &&
 
248
                deltaX < 0;
 
249
            if (this.__getTabHeadersWidth() > this.__getHeaderWidth() && !maxLeftReached && !maxRightReached) {
 
250
                this._tabs.style.webkitTransform = 'translate3d(' + offsetX + 'px,0,0)';
 
251
            }
 
252
        },
 
253
 
 
254
        /**
 
255
         * @private
 
256
         */
 
257
        __onTouchEnd: function (e) {
 
258
            if (!this._tabs || isScrolling) return;
 
259
 
 
260
            var _e = this._touchInfoDelegate.translateTouchEvent(e);
 
261
 
 
262
            var MIN_JITTER_THRESHOLD = 20;
 
263
            if (state === STATES.transitioning_to_navigation) {
 
264
                state = STATES.navigating;
 
265
            }
 
266
            else if (state === STATES.navigating && Math.abs((_e.changedTouches[0].pageX - pageX)) < MIN_JITTER_THRESHOLD) {
 
267
                this.__onClicked(_e);
 
268
                // Timer should have been cancelled, back to basic
 
269
                state = STATES.basic;
 
270
            }
 
271
 
 
272
            var self = this;
 
273
            navigationTimer = window.setTimeout(function () {
 
274
                self.__endNavigation();
 
275
                state = STATES.basic;
 
276
            }, 3000);
 
277
        },
 
278
 
 
279
        /**
 
280
         * @private
 
281
         */
 
282
        __endNavigation: function () {
 
283
            if (state !== STATES.navigating) return;
 
284
 
 
285
            var activeTab = document.querySelector('[data-role="tab"].active');
 
286
            if (! activeTab) return;
 
287
 
 
288
            var offsetX = activeTab.offsetLeft;
 
289
            this._tabs.style['-webkit-transition-duration'] = '.3s';
 
290
            this._tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
 
291
            
 
292
            [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
 
293
                el.classList.toggle('inactive');
 
294
            });
 
295
        },
 
296
 
 
297
        /**
 
298
         * @private
 
299
         */
 
300
        __showNavigation: function () {
 
301
            if (state !== STATES.transitioning_to_navigation) return;
 
302
 
 
303
            // TODO constraint a bit the selector
 
304
            [].forEach.call(document.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
 
305
                el.classList.toggle('inactive');
 
306
            });
 
307
        },
 
308
 
 
309
        /**
 
310
         * @private
 
311
         */
 
312
        __updateActiveTab: function(newActiveTab, oldActiveTab) {
 
313
            // TODO avoid reflow
 
314
            oldActiveTab.classList.remove('inactive');
 
315
            oldActiveTab.classList.remove('active');
 
316
            newActiveTab.classList.remove('inactive');
 
317
            newActiveTab.classList.add('active');
 
318
        },
 
319
 
 
320
        /**
 
321
         * @private
 
322
         */
 
323
        __onTouchLeave: function (e) {},
 
324
 
 
325
        /**
 
326
         * @private
 
327
         */
 
328
        __dispatchTabChangedEvent: function (pageId) {
 
329
            this._evt = document.createEvent('Event');
 
330
            this._evt.initEvent('tabchanged',true,true);
 
331
            this._evt.infos = {pageId: pageId};
 
332
            this._tabs.dispatchEvent(this._evt);
 
333
        },
 
334
 
 
335
        /**
 
336
         * @private
 
337
         */
 
338
        __onClicked: function (e) {
 
339
            if (state !== STATES.navigating)
 
340
                return;
 
341
            if (e.changedTouches.length === 0)
 
342
                return;
 
343
            var touch = e.changedTouches[0];
 
344
            var selectedTab = document.elementFromPoint(touch.pageX, touch.pageY);
 
345
            if (selectedTab == null)
 
346
                return;
 
347
            if (selectedTab.getAttribute("data-role") !== 'tab')
 
348
                return;
 
349
            if ((selectedTab.className).indexOf('inactive') > -1) {
 
350
                window.clearTimeout(t2);
 
351
 
 
352
                activeTab = this._tabs.querySelector('[data-role="tab"].active');
 
353
                offsetX = this.offsetLeft;
 
354
                this._tabs.style['-webkit-transition-duration'] = '.3s';
 
355
                this._tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
 
356
 
 
357
                this.__updateActiveTab(selectedTab, activeTab);
 
358
 
 
359
                [].forEach.call(this._tabs.querySelectorAll('[data-role="tab"]:not(.active)'), function (e) {
 
360
                    e.classList.remove('inactive');
 
361
                });
 
362
 
 
363
                var targetPageId = selectedTab.getAttribute('data-page');
 
364
                this.activate(targetPageId);
 
365
                this.__dispatchTabChangedEvent(targetPageId);
 
366
            } else {
 
367
 
 
368
                [].forEach.call(this._tabs.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
 
369
                    el.classList.toggle('inactive');
 
370
                });
 
371
                t2 = window.setTimeout(function () {
 
372
                    [].forEach.call(this._tabs.querySelectorAll('[data-role="tab"]:not(.active)'), function (el) {
 
373
                        el.classList.toggle('inactive');
 
374
                    });
 
375
                }, 3000);
 
376
            }
 
377
            e.preventDefault();
 
378
        },
 
379
 
 
380
        /**
 
381
         * @private
 
382
         */
 
383
        activate: function (id) {
 
384
            if (!id || typeof (id) !== 'string')
 
385
                return;
 
386
            activeTab = this._tabs.querySelector('[data-page="'+ id +'"]');
 
387
            if (!activeTab)
 
388
                return;
 
389
 
 
390
            [].forEach.call(this._tabs.querySelectorAll('[data-role="tab"]'), function (e) {
 
391
                e.classList.remove('active');
 
392
                e.classList.remove('inactive');
 
393
            });
 
394
 
 
395
            activeTab.classList.add('active');
 
396
 
 
397
            offsetX = activeTab.offsetLeft;
 
398
            this._tabs.style['-webkit-transition-duration'] = '.3s';
 
399
            this._tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
 
400
        },
 
401
 
 
402
        /**
 
403
         * @private
 
404
         */
 
405
        onTabChanged: function(callback){
 
406
            this._tabs.addEventListener("tabchanged", callback);
 
407
        }
 
408
    };
 
409
 
 
410
 
 
411
    return Tabs;
 
412
})();