Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(290)

Side by Side Diff: chrome/browser/resources/md_history/app.crisper.js

Issue 2257723002: MD WebUI: Uglify vulcanized javascript bundles to remove comments/whitespace (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Reparent and rebase Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/resources/md_downloads/crisper.js ('k') | chrome/browser/resources/vulcanize.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4
5 /**
6 * @fileoverview PromiseResolver is a helper class that allows creating a
7 * Promise that will be fulfilled (resolved or rejected) some time later.
8 *
9 * Example:
10 * var resolver = new PromiseResolver();
11 * resolver.promise.then(function(result) {
12 * console.log('resolved with', result);
13 * });
14 * ...
15 * ...
16 * resolver.resolve({hello: 'world'});
17 */
18
19 /**
20 * @constructor @struct
21 * @template T
22 */
23 function PromiseResolver() { 4 function PromiseResolver() {
24 /** @private {function(T=): void} */
25 this.resolve_; 5 this.resolve_;
26
27 /** @private {function(*=): void} */
28 this.reject_; 6 this.reject_;
29
30 /** @private {!Promise<T>} */
31 this.promise_ = new Promise(function(resolve, reject) { 7 this.promise_ = new Promise(function(resolve, reject) {
32 this.resolve_ = resolve; 8 this.resolve_ = resolve;
33 this.reject_ = reject; 9 this.reject_ = reject;
34 }.bind(this)); 10 }.bind(this));
35 } 11 }
36 12
37 PromiseResolver.prototype = { 13 PromiseResolver.prototype = {
38 /** @return {!Promise<T>} */ 14 get promise() {
39 get promise() { return this.promise_; }, 15 return this.promise_;
40 set promise(p) { assertNotReached(); }, 16 },
17 set promise(p) {
18 assertNotReached();
19 },
20 get resolve() {
21 return this.resolve_;
22 },
23 set resolve(r) {
24 assertNotReached();
25 },
26 get reject() {
27 return this.reject_;
28 },
29 set reject(s) {
30 assertNotReached();
31 }
32 };
41 33
42 /** @return {function(T=): void} */
43 get resolve() { return this.resolve_; },
44 set resolve(r) { assertNotReached(); },
45
46 /** @return {function(*=): void} */
47 get reject() { return this.reject_; },
48 set reject(s) { assertNotReached(); },
49 };
50 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 34 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
51 // Use of this source code is governed by a BSD-style license that can be 35 // Use of this source code is governed by a BSD-style license that can be
52 // found in the LICENSE file. 36 // found in the LICENSE file.
53
54 /**
55 * The global object.
56 * @type {!Object}
57 * @const
58 */
59 var global = this; 37 var global = this;
60 38
61 /** @typedef {{eventName: string, uid: number}} */
62 var WebUIListener; 39 var WebUIListener;
63 40
64 /** Platform, package, object property, and Event support. **/
65 var cr = cr || function() { 41 var cr = cr || function() {
66 'use strict'; 42 'use strict';
67
68 /**
69 * Builds an object structure for the provided namespace path,
70 * ensuring that names that already exist are not overwritten. For
71 * example:
72 * "a.b.c" -> a = {};a.b={};a.b.c={};
73 * @param {string} name Name of the object that this file defines.
74 * @param {*=} opt_object The object to expose at the end of the path.
75 * @param {Object=} opt_objectToExportTo The object to add the path to;
76 * default is {@code global}.
77 * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
78 * returns a reference to the ui property of window.cr).
79 * @private
80 */
81 function exportPath(name, opt_object, opt_objectToExportTo) { 43 function exportPath(name, opt_object, opt_objectToExportTo) {
82 var parts = name.split('.'); 44 var parts = name.split('.');
83 var cur = opt_objectToExportTo || global; 45 var cur = opt_objectToExportTo || global;
84 46 for (var part; parts.length && (part = parts.shift()); ) {
85 for (var part; parts.length && (part = parts.shift());) {
86 if (!parts.length && opt_object !== undefined) { 47 if (!parts.length && opt_object !== undefined) {
87 // last part and we have an object; use it
88 cur[part] = opt_object; 48 cur[part] = opt_object;
89 } else if (part in cur) { 49 } else if (part in cur) {
90 cur = cur[part]; 50 cur = cur[part];
91 } else { 51 } else {
92 cur = cur[part] = {}; 52 cur = cur[part] = {};
93 } 53 }
94 } 54 }
95 return cur; 55 return cur;
96 } 56 }
97
98 /**
99 * Fires a property change event on the target.
100 * @param {EventTarget} target The target to dispatch the event on.
101 * @param {string} propertyName The name of the property that changed.
102 * @param {*} newValue The new value for the property.
103 * @param {*} oldValue The old value for the property.
104 */
105 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { 57 function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
106 var e = new Event(propertyName + 'Change'); 58 var e = new Event(propertyName + 'Change');
107 e.propertyName = propertyName; 59 e.propertyName = propertyName;
108 e.newValue = newValue; 60 e.newValue = newValue;
109 e.oldValue = oldValue; 61 e.oldValue = oldValue;
110 target.dispatchEvent(e); 62 target.dispatchEvent(e);
111 } 63 }
112
113 /**
114 * Converts a camelCase javascript property name to a hyphenated-lower-case
115 * attribute name.
116 * @param {string} jsName The javascript camelCase property name.
117 * @return {string} The equivalent hyphenated-lower-case attribute name.
118 */
119 function getAttributeName(jsName) { 64 function getAttributeName(jsName) {
120 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); 65 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
121 } 66 }
122
123 /**
124 * The kind of property to define in {@code defineProperty}.
125 * @enum {string}
126 * @const
127 */
128 var PropertyKind = { 67 var PropertyKind = {
129 /**
130 * Plain old JS property where the backing data is stored as a "private"
131 * field on the object.
132 * Use for properties of any type. Type will not be checked.
133 */
134 JS: 'js', 68 JS: 'js',
135
136 /**
137 * The property backing data is stored as an attribute on an element.
138 * Use only for properties of type {string}.
139 */
140 ATTR: 'attr', 69 ATTR: 'attr',
141
142 /**
143 * The property backing data is stored as an attribute on an element. If the
144 * element has the attribute then the value is true.
145 * Use only for properties of type {boolean}.
146 */
147 BOOL_ATTR: 'boolAttr' 70 BOOL_ATTR: 'boolAttr'
148 }; 71 };
149
150 /**
151 * Helper function for defineProperty that returns the getter to use for the
152 * property.
153 * @param {string} name The name of the property.
154 * @param {PropertyKind} kind The kind of the property.
155 * @return {function():*} The getter for the property.
156 */
157 function getGetter(name, kind) { 72 function getGetter(name, kind) {
158 switch (kind) { 73 switch (kind) {
159 case PropertyKind.JS: 74 case PropertyKind.JS:
160 var privateName = name + '_'; 75 var privateName = name + '_';
161 return function() { 76 return function() {
162 return this[privateName]; 77 return this[privateName];
163 }; 78 };
164 case PropertyKind.ATTR: 79
165 var attributeName = getAttributeName(name); 80 case PropertyKind.ATTR:
166 return function() { 81 var attributeName = getAttributeName(name);
167 return this.getAttribute(attributeName); 82 return function() {
168 }; 83 return this.getAttribute(attributeName);
169 case PropertyKind.BOOL_ATTR: 84 };
170 var attributeName = getAttributeName(name); 85
171 return function() { 86 case PropertyKind.BOOL_ATTR:
172 return this.hasAttribute(attributeName); 87 var attributeName = getAttributeName(name);
173 }; 88 return function() {
89 return this.hasAttribute(attributeName);
90 };
174 } 91 }
175
176 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
177 // the browser/unit tests to preprocess this file through grit.
178 throw 'not reached'; 92 throw 'not reached';
179 } 93 }
180
181 /**
182 * Helper function for defineProperty that returns the setter of the right
183 * kind.
184 * @param {string} name The name of the property we are defining the setter
185 * for.
186 * @param {PropertyKind} kind The kind of property we are getting the
187 * setter for.
188 * @param {function(*, *):void=} opt_setHook A function to run after the
189 * property is set, but before the propertyChange event is fired.
190 * @return {function(*):void} The function to use as a setter.
191 */
192 function getSetter(name, kind, opt_setHook) { 94 function getSetter(name, kind, opt_setHook) {
193 switch (kind) { 95 switch (kind) {
194 case PropertyKind.JS: 96 case PropertyKind.JS:
195 var privateName = name + '_'; 97 var privateName = name + '_';
196 return function(value) { 98 return function(value) {
197 var oldValue = this[name]; 99 var oldValue = this[name];
198 if (value !== oldValue) { 100 if (value !== oldValue) {
199 this[privateName] = value; 101 this[privateName] = value;
200 if (opt_setHook) 102 if (opt_setHook) opt_setHook.call(this, value, oldValue);
201 opt_setHook.call(this, value, oldValue); 103 dispatchPropertyChange(this, name, value, oldValue);
202 dispatchPropertyChange(this, name, value, oldValue); 104 }
203 } 105 };
204 };
205 106
206 case PropertyKind.ATTR: 107 case PropertyKind.ATTR:
207 var attributeName = getAttributeName(name); 108 var attributeName = getAttributeName(name);
208 return function(value) { 109 return function(value) {
209 var oldValue = this[name]; 110 var oldValue = this[name];
210 if (value !== oldValue) { 111 if (value !== oldValue) {
211 if (value == undefined) 112 if (value == undefined) this.removeAttribute(attributeName); else this .setAttribute(attributeName, value);
212 this.removeAttribute(attributeName); 113 if (opt_setHook) opt_setHook.call(this, value, oldValue);
213 else 114 dispatchPropertyChange(this, name, value, oldValue);
214 this.setAttribute(attributeName, value); 115 }
215 if (opt_setHook) 116 };
216 opt_setHook.call(this, value, oldValue);
217 dispatchPropertyChange(this, name, value, oldValue);
218 }
219 };
220 117
221 case PropertyKind.BOOL_ATTR: 118 case PropertyKind.BOOL_ATTR:
222 var attributeName = getAttributeName(name); 119 var attributeName = getAttributeName(name);
223 return function(value) { 120 return function(value) {
224 var oldValue = this[name]; 121 var oldValue = this[name];
225 if (value !== oldValue) { 122 if (value !== oldValue) {
226 if (value) 123 if (value) this.setAttribute(attributeName, name); else this.removeAtt ribute(attributeName);
227 this.setAttribute(attributeName, name); 124 if (opt_setHook) opt_setHook.call(this, value, oldValue);
228 else 125 dispatchPropertyChange(this, name, value, oldValue);
229 this.removeAttribute(attributeName); 126 }
230 if (opt_setHook) 127 };
231 opt_setHook.call(this, value, oldValue);
232 dispatchPropertyChange(this, name, value, oldValue);
233 }
234 };
235 } 128 }
236
237 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
238 // the browser/unit tests to preprocess this file through grit.
239 throw 'not reached'; 129 throw 'not reached';
240 } 130 }
241
242 /**
243 * Defines a property on an object. When the setter changes the value a
244 * property change event with the type {@code name + 'Change'} is fired.
245 * @param {!Object} obj The object to define the property for.
246 * @param {string} name The name of the property.
247 * @param {PropertyKind=} opt_kind What kind of underlying storage to use.
248 * @param {function(*, *):void=} opt_setHook A function to run after the
249 * property is set, but before the propertyChange event is fired.
250 */
251 function defineProperty(obj, name, opt_kind, opt_setHook) { 131 function defineProperty(obj, name, opt_kind, opt_setHook) {
252 if (typeof obj == 'function') 132 if (typeof obj == 'function') obj = obj.prototype;
253 obj = obj.prototype; 133 var kind = opt_kind || PropertyKind.JS;
254 134 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name, kind));
255 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); 135 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
256
257 if (!obj.__lookupGetter__(name))
258 obj.__defineGetter__(name, getGetter(name, kind));
259
260 if (!obj.__lookupSetter__(name))
261 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
262 } 136 }
263
264 /**
265 * Counter for use with createUid
266 */
267 var uidCounter = 1; 137 var uidCounter = 1;
268
269 /**
270 * @return {number} A new unique ID.
271 */
272 function createUid() { 138 function createUid() {
273 return uidCounter++; 139 return uidCounter++;
274 } 140 }
275
276 /**
277 * Returns a unique ID for the item. This mutates the item so it needs to be
278 * an object
279 * @param {!Object} item The item to get the unique ID for.
280 * @return {number} The unique ID for the item.
281 */
282 function getUid(item) { 141 function getUid(item) {
283 if (item.hasOwnProperty('uid')) 142 if (item.hasOwnProperty('uid')) return item.uid;
284 return item.uid;
285 return item.uid = createUid(); 143 return item.uid = createUid();
286 } 144 }
287
288 /**
289 * Dispatches a simple event on an event target.
290 * @param {!EventTarget} target The event target to dispatch the event on.
291 * @param {string} type The type of the event.
292 * @param {boolean=} opt_bubbles Whether the event bubbles or not.
293 * @param {boolean=} opt_cancelable Whether the default action of the event
294 * can be prevented. Default is true.
295 * @return {boolean} If any of the listeners called {@code preventDefault}
296 * during the dispatch this will return false.
297 */
298 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { 145 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
299 var e = new Event(type, { 146 var e = new Event(type, {
300 bubbles: opt_bubbles, 147 bubbles: opt_bubbles,
301 cancelable: opt_cancelable === undefined || opt_cancelable 148 cancelable: opt_cancelable === undefined || opt_cancelable
302 }); 149 });
303 return target.dispatchEvent(e); 150 return target.dispatchEvent(e);
304 } 151 }
305
306 /**
307 * Calls |fun| and adds all the fields of the returned object to the object
308 * named by |name|. For example, cr.define('cr.ui', function() {
309 * function List() {
310 * ...
311 * }
312 * function ListItem() {
313 * ...
314 * }
315 * return {
316 * List: List,
317 * ListItem: ListItem,
318 * };
319 * });
320 * defines the functions cr.ui.List and cr.ui.ListItem.
321 * @param {string} name The name of the object that we are adding fields to.
322 * @param {!Function} fun The function that will return an object containing
323 * the names and values of the new fields.
324 */
325 function define(name, fun) { 152 function define(name, fun) {
326 var obj = exportPath(name); 153 var obj = exportPath(name);
327 var exports = fun(); 154 var exports = fun();
328 for (var propertyName in exports) { 155 for (var propertyName in exports) {
329 // Maybe we should check the prototype chain here? The current usage 156 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property Name);
330 // pattern is always using an object literal so we only care about own 157 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD escriptor);
331 // properties.
332 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
333 propertyName);
334 if (propertyDescriptor)
335 Object.defineProperty(obj, propertyName, propertyDescriptor);
336 } 158 }
337 } 159 }
338
339 /**
340 * Adds a {@code getInstance} static method that always return the same
341 * instance object.
342 * @param {!Function} ctor The constructor for the class to add the static
343 * method to.
344 */
345 function addSingletonGetter(ctor) { 160 function addSingletonGetter(ctor) {
346 ctor.getInstance = function() { 161 ctor.getInstance = function() {
347 return ctor.instance_ || (ctor.instance_ = new ctor()); 162 return ctor.instance_ || (ctor.instance_ = new ctor());
348 }; 163 };
349 } 164 }
350
351 /**
352 * Forwards public APIs to private implementations.
353 * @param {Function} ctor Constructor that have private implementations in its
354 * prototype.
355 * @param {Array<string>} methods List of public method names that have their
356 * underscored counterparts in constructor's prototype.
357 * @param {string=} opt_target Selector for target node.
358 */
359 function makePublic(ctor, methods, opt_target) { 165 function makePublic(ctor, methods, opt_target) {
360 methods.forEach(function(method) { 166 methods.forEach(function(method) {
361 ctor[method] = function() { 167 ctor[method] = function() {
362 var target = opt_target ? document.getElementById(opt_target) : 168 var target = opt_target ? document.getElementById(opt_target) : ctor.get Instance();
363 ctor.getInstance();
364 return target[method + '_'].apply(target, arguments); 169 return target[method + '_'].apply(target, arguments);
365 }; 170 };
366 }); 171 });
367 } 172 }
368
369 /**
370 * The mapping used by the sendWithPromise mechanism to tie the Promise
371 * returned to callers with the corresponding WebUI response. The mapping is
372 * from ID to the PromiseResolver helper; the ID is generated by
373 * sendWithPromise and is unique across all invocations of said method.
374 * @type {!Object<!PromiseResolver>}
375 */
376 var chromeSendResolverMap = {}; 173 var chromeSendResolverMap = {};
377
378 /**
379 * The named method the WebUI handler calls directly in response to a
380 * chrome.send call that expects a response. The handler requires no knowledge
381 * of the specific name of this method, as the name is passed to the handler
382 * as the first argument in the arguments list of chrome.send. The handler
383 * must pass the ID, also sent via the chrome.send arguments list, as the
384 * first argument of the JS invocation; additionally, the handler may
385 * supply any number of other arguments that will be included in the response.
386 * @param {string} id The unique ID identifying the Promise this response is
387 * tied to.
388 * @param {boolean} isSuccess Whether the request was successful.
389 * @param {*} response The response as sent from C++.
390 */
391 function webUIResponse(id, isSuccess, response) { 174 function webUIResponse(id, isSuccess, response) {
392 var resolver = chromeSendResolverMap[id]; 175 var resolver = chromeSendResolverMap[id];
393 delete chromeSendResolverMap[id]; 176 delete chromeSendResolverMap[id];
394 177 if (isSuccess) resolver.resolve(response); else resolver.reject(response);
395 if (isSuccess)
396 resolver.resolve(response);
397 else
398 resolver.reject(response);
399 } 178 }
400
401 /**
402 * A variation of chrome.send, suitable for messages that expect a single
403 * response from C++.
404 * @param {string} methodName The name of the WebUI handler API.
405 * @param {...*} var_args Varibale number of arguments to be forwarded to the
406 * C++ call.
407 * @return {!Promise}
408 */
409 function sendWithPromise(methodName, var_args) { 179 function sendWithPromise(methodName, var_args) {
410 var args = Array.prototype.slice.call(arguments, 1); 180 var args = Array.prototype.slice.call(arguments, 1);
411 var promiseResolver = new PromiseResolver(); 181 var promiseResolver = new PromiseResolver();
412 var id = methodName + '_' + createUid(); 182 var id = methodName + '_' + createUid();
413 chromeSendResolverMap[id] = promiseResolver; 183 chromeSendResolverMap[id] = promiseResolver;
414 chrome.send(methodName, [id].concat(args)); 184 chrome.send(methodName, [ id ].concat(args));
415 return promiseResolver.promise; 185 return promiseResolver.promise;
416 } 186 }
417
418 /**
419 * A map of maps associating event names with listeners. The 2nd level map
420 * associates a listener ID with the callback function, such that individual
421 * listeners can be removed from an event without affecting other listeners of
422 * the same event.
423 * @type {!Object<!Object<!Function>>}
424 */
425 var webUIListenerMap = {}; 187 var webUIListenerMap = {};
426
427 /**
428 * The named method the WebUI handler calls directly when an event occurs.
429 * The WebUI handler must supply the name of the event as the first argument
430 * of the JS invocation; additionally, the handler may supply any number of
431 * other arguments that will be forwarded to the listener callbacks.
432 * @param {string} event The name of the event that has occurred.
433 * @param {...*} var_args Additional arguments passed from C++.
434 */
435 function webUIListenerCallback(event, var_args) { 188 function webUIListenerCallback(event, var_args) {
436 var eventListenersMap = webUIListenerMap[event]; 189 var eventListenersMap = webUIListenerMap[event];
437 if (!eventListenersMap) { 190 if (!eventListenersMap) {
438 // C++ event sent for an event that has no listeners.
439 // TODO(dpapad): Should a warning be displayed here?
440 return; 191 return;
441 } 192 }
442
443 var args = Array.prototype.slice.call(arguments, 1); 193 var args = Array.prototype.slice.call(arguments, 1);
444 for (var listenerId in eventListenersMap) { 194 for (var listenerId in eventListenersMap) {
445 eventListenersMap[listenerId].apply(null, args); 195 eventListenersMap[listenerId].apply(null, args);
446 } 196 }
447 } 197 }
448
449 /**
450 * Registers a listener for an event fired from WebUI handlers. Any number of
451 * listeners may register for a single event.
452 * @param {string} eventName The event to listen to.
453 * @param {!Function} callback The callback run when the event is fired.
454 * @return {!WebUIListener} An object to be used for removing a listener via
455 * cr.removeWebUIListener. Should be treated as read-only.
456 */
457 function addWebUIListener(eventName, callback) { 198 function addWebUIListener(eventName, callback) {
458 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; 199 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
459 var uid = createUid(); 200 var uid = createUid();
460 webUIListenerMap[eventName][uid] = callback; 201 webUIListenerMap[eventName][uid] = callback;
461 return {eventName: eventName, uid: uid}; 202 return {
203 eventName: eventName,
204 uid: uid
205 };
462 } 206 }
463
464 /**
465 * Removes a listener. Does nothing if the specified listener is not found.
466 * @param {!WebUIListener} listener The listener to be removed (as returned by
467 * addWebUIListener).
468 * @return {boolean} Whether the given listener was found and actually
469 * removed.
470 */
471 function removeWebUIListener(listener) { 207 function removeWebUIListener(listener) {
472 var listenerExists = webUIListenerMap[listener.eventName] && 208 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa p[listener.eventName][listener.uid];
473 webUIListenerMap[listener.eventName][listener.uid];
474 if (listenerExists) { 209 if (listenerExists) {
475 delete webUIListenerMap[listener.eventName][listener.uid]; 210 delete webUIListenerMap[listener.eventName][listener.uid];
476 return true; 211 return true;
477 } 212 }
478 return false; 213 return false;
479 } 214 }
480
481 return { 215 return {
482 addSingletonGetter: addSingletonGetter, 216 addSingletonGetter: addSingletonGetter,
483 createUid: createUid, 217 createUid: createUid,
484 define: define, 218 define: define,
485 defineProperty: defineProperty, 219 defineProperty: defineProperty,
486 dispatchPropertyChange: dispatchPropertyChange, 220 dispatchPropertyChange: dispatchPropertyChange,
487 dispatchSimpleEvent: dispatchSimpleEvent, 221 dispatchSimpleEvent: dispatchSimpleEvent,
488 exportPath: exportPath, 222 exportPath: exportPath,
489 getUid: getUid, 223 getUid: getUid,
490 makePublic: makePublic, 224 makePublic: makePublic,
491 PropertyKind: PropertyKind, 225 PropertyKind: PropertyKind,
492
493 // C++ <-> JS communication related methods.
494 addWebUIListener: addWebUIListener, 226 addWebUIListener: addWebUIListener,
495 removeWebUIListener: removeWebUIListener, 227 removeWebUIListener: removeWebUIListener,
496 sendWithPromise: sendWithPromise, 228 sendWithPromise: sendWithPromise,
497 webUIListenerCallback: webUIListenerCallback, 229 webUIListenerCallback: webUIListenerCallback,
498 webUIResponse: webUIResponse, 230 webUIResponse: webUIResponse,
499
500 get doc() { 231 get doc() {
501 return document; 232 return document;
502 }, 233 },
503
504 /** Whether we are using a Mac or not. */
505 get isMac() { 234 get isMac() {
506 return /Mac/.test(navigator.platform); 235 return /Mac/.test(navigator.platform);
507 }, 236 },
508
509 /** Whether this is on the Windows platform or not. */
510 get isWindows() { 237 get isWindows() {
511 return /Win/.test(navigator.platform); 238 return /Win/.test(navigator.platform);
512 }, 239 },
513
514 /** Whether this is on chromeOS or not. */
515 get isChromeOS() { 240 get isChromeOS() {
516 return /CrOS/.test(navigator.userAgent); 241 return /CrOS/.test(navigator.userAgent);
517 }, 242 },
518
519 /** Whether this is on vanilla Linux (not chromeOS). */
520 get isLinux() { 243 get isLinux() {
521 return /Linux/.test(navigator.userAgent); 244 return /Linux/.test(navigator.userAgent);
522 }, 245 },
523
524 /** Whether this is on Android. */
525 get isAndroid() { 246 get isAndroid() {
526 return /Android/.test(navigator.userAgent); 247 return /Android/.test(navigator.userAgent);
527 }, 248 },
528
529 /** Whether this is on iOS. */
530 get isIOS() { 249 get isIOS() {
531 return /iPad|iPhone|iPod/.test(navigator.platform); 250 return /iPad|iPhone|iPod/.test(navigator.platform);
532 } 251 }
533 }; 252 };
534 }(); 253 }();
254
535 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 255 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
536 // Use of this source code is governed by a BSD-style license that can be 256 // Use of this source code is governed by a BSD-style license that can be
537 // found in the LICENSE file. 257 // found in the LICENSE file.
538
539 cr.define('cr.ui', function() { 258 cr.define('cr.ui', function() {
540
541 /**
542 * Decorates elements as an instance of a class.
543 * @param {string|!Element} source The way to find the element(s) to decorate.
544 * If this is a string then {@code querySeletorAll} is used to find the
545 * elements to decorate.
546 * @param {!Function} constr The constructor to decorate with. The constr
547 * needs to have a {@code decorate} function.
548 */
549 function decorate(source, constr) { 259 function decorate(source, constr) {
550 var elements; 260 var elements;
551 if (typeof source == 'string') 261 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e lse elements = [ source ];
552 elements = cr.doc.querySelectorAll(source);
553 else
554 elements = [source];
555
556 for (var i = 0, el; el = elements[i]; i++) { 262 for (var i = 0, el; el = elements[i]; i++) {
557 if (!(el instanceof constr)) 263 if (!(el instanceof constr)) constr.decorate(el);
558 constr.decorate(el);
559 } 264 }
560 } 265 }
561
562 /**
563 * Helper function for creating new element for define.
564 */
565 function createElementHelper(tagName, opt_bag) { 266 function createElementHelper(tagName, opt_bag) {
566 // Allow passing in ownerDocument to create in a different document.
567 var doc; 267 var doc;
568 if (opt_bag && opt_bag.ownerDocument) 268 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc = cr.doc;
569 doc = opt_bag.ownerDocument;
570 else
571 doc = cr.doc;
572 return doc.createElement(tagName); 269 return doc.createElement(tagName);
573 } 270 }
574
575 /**
576 * Creates the constructor for a UI element class.
577 *
578 * Usage:
579 * <pre>
580 * var List = cr.ui.define('list');
581 * List.prototype = {
582 * __proto__: HTMLUListElement.prototype,
583 * decorate: function() {
584 * ...
585 * },
586 * ...
587 * };
588 * </pre>
589 *
590 * @param {string|Function} tagNameOrFunction The tagName or
591 * function to use for newly created elements. If this is a function it
592 * needs to return a new element when called.
593 * @return {function(Object=):Element} The constructor function which takes
594 * an optional property bag. The function also has a static
595 * {@code decorate} method added to it.
596 */
597 function define(tagNameOrFunction) { 271 function define(tagNameOrFunction) {
598 var createFunction, tagName; 272 var createFunction, tagName;
599 if (typeof tagNameOrFunction == 'function') { 273 if (typeof tagNameOrFunction == 'function') {
600 createFunction = tagNameOrFunction; 274 createFunction = tagNameOrFunction;
601 tagName = ''; 275 tagName = '';
602 } else { 276 } else {
603 createFunction = createElementHelper; 277 createFunction = createElementHelper;
604 tagName = tagNameOrFunction; 278 tagName = tagNameOrFunction;
605 } 279 }
606
607 /**
608 * Creates a new UI element constructor.
609 * @param {Object=} opt_propertyBag Optional bag of properties to set on the
610 * object after created. The property {@code ownerDocument} is special
611 * cased and it allows you to create the element in a different
612 * document than the default.
613 * @constructor
614 */
615 function f(opt_propertyBag) { 280 function f(opt_propertyBag) {
616 var el = createFunction(tagName, opt_propertyBag); 281 var el = createFunction(tagName, opt_propertyBag);
617 f.decorate(el); 282 f.decorate(el);
618 for (var propertyName in opt_propertyBag) { 283 for (var propertyName in opt_propertyBag) {
619 el[propertyName] = opt_propertyBag[propertyName]; 284 el[propertyName] = opt_propertyBag[propertyName];
620 } 285 }
621 return el; 286 return el;
622 } 287 }
623
624 /**
625 * Decorates an element as a UI element class.
626 * @param {!Element} el The element to decorate.
627 */
628 f.decorate = function(el) { 288 f.decorate = function(el) {
629 el.__proto__ = f.prototype; 289 el.__proto__ = f.prototype;
630 el.decorate(); 290 el.decorate();
631 }; 291 };
632
633 return f; 292 return f;
634 } 293 }
635
636 /**
637 * Input elements do not grow and shrink with their content. This is a simple
638 * (and not very efficient) way of handling shrinking to content with support
639 * for min width and limited by the width of the parent element.
640 * @param {!HTMLElement} el The element to limit the width for.
641 * @param {!HTMLElement} parentEl The parent element that should limit the
642 * size.
643 * @param {number} min The minimum width.
644 * @param {number=} opt_scale Optional scale factor to apply to the width.
645 */
646 function limitInputWidth(el, parentEl, min, opt_scale) { 294 function limitInputWidth(el, parentEl, min, opt_scale) {
647 // Needs a size larger than borders
648 el.style.width = '10px'; 295 el.style.width = '10px';
649 var doc = el.ownerDocument; 296 var doc = el.ownerDocument;
650 var win = doc.defaultView; 297 var win = doc.defaultView;
651 var computedStyle = win.getComputedStyle(el); 298 var computedStyle = win.getComputedStyle(el);
652 var parentComputedStyle = win.getComputedStyle(parentEl); 299 var parentComputedStyle = win.getComputedStyle(parentEl);
653 var rtl = computedStyle.direction == 'rtl'; 300 var rtl = computedStyle.direction == 'rtl';
654 301 var inputRect = el.getBoundingClientRect();
655 // To get the max width we get the width of the treeItem minus the position
656 // of the input.
657 var inputRect = el.getBoundingClientRect(); // box-sizing
658 var parentRect = parentEl.getBoundingClientRect(); 302 var parentRect = parentEl.getBoundingClientRect();
659 var startPos = rtl ? parentRect.right - inputRect.right : 303 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p arentRect.left;
660 inputRect.left - parentRect.left; 304 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp utedStyle.borderRightWidth, 10);
661 305 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa rseInt(parentComputedStyle.paddingRight, 10);
662 // Add up border and padding of the input.
663 var inner = parseInt(computedStyle.borderLeftWidth, 10) +
664 parseInt(computedStyle.paddingLeft, 10) +
665 parseInt(computedStyle.paddingRight, 10) +
666 parseInt(computedStyle.borderRightWidth, 10);
667
668 // We also need to subtract the padding of parent to prevent it to overflow.
669 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
670 parseInt(parentComputedStyle.paddingRight, 10);
671
672 var max = parentEl.clientWidth - startPos - inner - parentPadding; 306 var max = parentEl.clientWidth - startPos - inner - parentPadding;
673 if (opt_scale) 307 if (opt_scale) max *= opt_scale;
674 max *= opt_scale;
675
676 function limit() { 308 function limit() {
677 if (el.scrollWidth > max) { 309 if (el.scrollWidth > max) {
678 el.style.width = max + 'px'; 310 el.style.width = max + 'px';
679 } else { 311 } else {
680 el.style.width = 0; 312 el.style.width = 0;
681 var sw = el.scrollWidth; 313 var sw = el.scrollWidth;
682 if (sw < min) { 314 if (sw < min) {
683 el.style.width = min + 'px'; 315 el.style.width = min + 'px';
684 } else { 316 } else {
685 el.style.width = sw + 'px'; 317 el.style.width = sw + 'px';
686 } 318 }
687 } 319 }
688 } 320 }
689
690 el.addEventListener('input', limit); 321 el.addEventListener('input', limit);
691 limit(); 322 limit();
692 } 323 }
693
694 /**
695 * Takes a number and spits out a value CSS will be happy with. To avoid
696 * subpixel layout issues, the value is rounded to the nearest integral value.
697 * @param {number} pixels The number of pixels.
698 * @return {string} e.g. '16px'.
699 */
700 function toCssPx(pixels) { 324 function toCssPx(pixels) {
701 if (!window.isFinite(pixels)) 325 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: ' + pixels);
702 console.error('Pixel value is not a number: ' + pixels);
703 return Math.round(pixels) + 'px'; 326 return Math.round(pixels) + 'px';
704 } 327 }
705
706 /**
707 * Users complain they occasionaly use doubleclicks instead of clicks
708 * (http://crbug.com/140364). To fix it we freeze click handling for
709 * the doubleclick time interval.
710 * @param {MouseEvent} e Initial click event.
711 */
712 function swallowDoubleClick(e) { 328 function swallowDoubleClick(e) {
713 var doc = e.target.ownerDocument; 329 var doc = e.target.ownerDocument;
714 var counter = Math.min(1, e.detail); 330 var counter = Math.min(1, e.detail);
715 function swallow(e) { 331 function swallow(e) {
716 e.stopPropagation(); 332 e.stopPropagation();
717 e.preventDefault(); 333 e.preventDefault();
718 } 334 }
719 function onclick(e) { 335 function onclick(e) {
720 if (e.detail > counter) { 336 if (e.detail > counter) {
721 counter = e.detail; 337 counter = e.detail;
722 // Swallow the click since it's a click inside the doubleclick timeout.
723 swallow(e); 338 swallow(e);
724 } else { 339 } else {
725 // Stop tracking clicks and let regular handling.
726 doc.removeEventListener('dblclick', swallow, true); 340 doc.removeEventListener('dblclick', swallow, true);
727 doc.removeEventListener('click', onclick, true); 341 doc.removeEventListener('click', onclick, true);
728 } 342 }
729 } 343 }
730 // The following 'click' event (if e.type == 'mouseup') mustn't be taken
731 // into account (it mustn't stop tracking clicks). Start event listening
732 // after zero timeout.
733 setTimeout(function() { 344 setTimeout(function() {
734 doc.addEventListener('click', onclick, true); 345 doc.addEventListener('click', onclick, true);
735 doc.addEventListener('dblclick', swallow, true); 346 doc.addEventListener('dblclick', swallow, true);
736 }, 0); 347 }, 0);
737 } 348 }
738
739 return { 349 return {
740 decorate: decorate, 350 decorate: decorate,
741 define: define, 351 define: define,
742 limitInputWidth: limitInputWidth, 352 limitInputWidth: limitInputWidth,
743 toCssPx: toCssPx, 353 toCssPx: toCssPx,
744 swallowDoubleClick: swallowDoubleClick 354 swallowDoubleClick: swallowDoubleClick
745 }; 355 };
746 }); 356 });
357
747 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 358 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
748 // Use of this source code is governed by a BSD-style license that can be 359 // Use of this source code is governed by a BSD-style license that can be
749 // found in the LICENSE file. 360 // found in the LICENSE file.
750
751 /**
752 * @fileoverview A command is an abstraction of an action a user can do in the
753 * UI.
754 *
755 * When the focus changes in the document for each command a canExecute event
756 * is dispatched on the active element. By listening to this event you can
757 * enable and disable the command by setting the event.canExecute property.
758 *
759 * When a command is executed a command event is dispatched on the active
760 * element. Note that you should stop the propagation after you have handled the
761 * command if there might be other command listeners higher up in the DOM tree.
762 */
763
764 cr.define('cr.ui', function() { 361 cr.define('cr.ui', function() {
765
766 /**
767 * This is used to identify keyboard shortcuts.
768 * @param {string} shortcut The text used to describe the keys for this
769 * keyboard shortcut.
770 * @constructor
771 */
772 function KeyboardShortcut(shortcut) { 362 function KeyboardShortcut(shortcut) {
773 var mods = {}; 363 var mods = {};
774 var ident = ''; 364 var ident = '';
775 shortcut.split('|').forEach(function(part) { 365 shortcut.split('|').forEach(function(part) {
776 var partLc = part.toLowerCase(); 366 var partLc = part.toLowerCase();
777 switch (partLc) { 367 switch (partLc) {
778 case 'alt': 368 case 'alt':
779 case 'ctrl': 369 case 'ctrl':
780 case 'meta': 370 case 'meta':
781 case 'shift': 371 case 'shift':
782 mods[partLc + 'Key'] = true; 372 mods[partLc + 'Key'] = true;
783 break; 373 break;
784 default: 374
785 if (ident) 375 default:
786 throw Error('Invalid shortcut'); 376 if (ident) throw Error('Invalid shortcut');
787 ident = part; 377 ident = part;
788 } 378 }
789 }); 379 });
790
791 this.ident_ = ident; 380 this.ident_ = ident;
792 this.mods_ = mods; 381 this.mods_ = mods;
793 } 382 }
794
795 KeyboardShortcut.prototype = { 383 KeyboardShortcut.prototype = {
796 /**
797 * Whether the keyboard shortcut object matches a keyboard event.
798 * @param {!Event} e The keyboard event object.
799 * @return {boolean} Whether we found a match or not.
800 */
801 matchesEvent: function(e) { 384 matchesEvent: function(e) {
802 if (e.key == this.ident_) { 385 if (e.key == this.ident_) {
803 // All keyboard modifiers needs to match.
804 var mods = this.mods_; 386 var mods = this.mods_;
805 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { 387 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k) {
806 return e[k] == !!mods[k]; 388 return e[k] == !!mods[k];
807 }); 389 });
808 } 390 }
809 return false; 391 return false;
810 } 392 }
811 }; 393 };
812
813 /**
814 * Creates a new command element.
815 * @constructor
816 * @extends {HTMLElement}
817 */
818 var Command = cr.ui.define('command'); 394 var Command = cr.ui.define('command');
819
820 Command.prototype = { 395 Command.prototype = {
821 __proto__: HTMLElement.prototype, 396 __proto__: HTMLElement.prototype,
822
823 /**
824 * Initializes the command.
825 */
826 decorate: function() { 397 decorate: function() {
827 CommandManager.init(assert(this.ownerDocument)); 398 CommandManager.init(assert(this.ownerDocument));
828 399 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor tcut');
829 if (this.hasAttribute('shortcut'))
830 this.shortcut = this.getAttribute('shortcut');
831 }, 400 },
832
833 /**
834 * Executes the command by dispatching a command event on the given element.
835 * If |element| isn't given, the active element is used instead.
836 * If the command is {@code disabled} this does nothing.
837 * @param {HTMLElement=} opt_element Optional element to dispatch event on.
838 */
839 execute: function(opt_element) { 401 execute: function(opt_element) {
840 if (this.disabled) 402 if (this.disabled) return;
841 return;
842 var doc = this.ownerDocument; 403 var doc = this.ownerDocument;
843 if (doc.activeElement) { 404 if (doc.activeElement) {
844 var e = new Event('command', {bubbles: true}); 405 var e = new Event('command', {
406 bubbles: true
407 });
845 e.command = this; 408 e.command = this;
846
847 (opt_element || doc.activeElement).dispatchEvent(e); 409 (opt_element || doc.activeElement).dispatchEvent(e);
848 } 410 }
849 }, 411 },
850
851 /**
852 * Call this when there have been changes that might change whether the
853 * command can be executed or not.
854 * @param {Node=} opt_node Node for which to actuate command state.
855 */
856 canExecuteChange: function(opt_node) { 412 canExecuteChange: function(opt_node) {
857 dispatchCanExecuteEvent(this, 413 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement );
858 opt_node || this.ownerDocument.activeElement);
859 }, 414 },
860
861 /**
862 * The keyboard shortcut that triggers the command. This is a string
863 * consisting of a key (as reported by WebKit in keydown) as
864 * well as optional key modifiers joinded with a '|'.
865 *
866 * Multiple keyboard shortcuts can be provided by separating them by
867 * whitespace.
868 *
869 * For example:
870 * "F1"
871 * "Backspace|Meta" for Apple command backspace.
872 * "a|Ctrl" for Control A
873 * "Delete Backspace|Meta" for Delete and Command Backspace
874 *
875 * @type {string}
876 */
877 shortcut_: '', 415 shortcut_: '',
878 get shortcut() { 416 get shortcut() {
879 return this.shortcut_; 417 return this.shortcut_;
880 }, 418 },
881 set shortcut(shortcut) { 419 set shortcut(shortcut) {
882 var oldShortcut = this.shortcut_; 420 var oldShortcut = this.shortcut_;
883 if (shortcut !== oldShortcut) { 421 if (shortcut !== oldShortcut) {
884 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { 422 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
885 return new KeyboardShortcut(shortcut); 423 return new KeyboardShortcut(shortcut);
886 }); 424 });
887
888 // Set this after the keyboardShortcuts_ since that might throw.
889 this.shortcut_ = shortcut; 425 this.shortcut_ = shortcut;
890 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, 426 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut) ;
891 oldShortcut);
892 } 427 }
893 }, 428 },
894
895 /**
896 * Whether the event object matches the shortcut for this command.
897 * @param {!Event} e The key event object.
898 * @return {boolean} Whether it matched or not.
899 */
900 matchesEvent: function(e) { 429 matchesEvent: function(e) {
901 if (!this.keyboardShortcuts_) 430 if (!this.keyboardShortcuts_) return false;
902 return false;
903
904 return this.keyboardShortcuts_.some(function(keyboardShortcut) { 431 return this.keyboardShortcuts_.some(function(keyboardShortcut) {
905 return keyboardShortcut.matchesEvent(e); 432 return keyboardShortcut.matchesEvent(e);
906 }); 433 });
907 }, 434 }
908 }; 435 };
909
910 /**
911 * The label of the command.
912 */
913 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); 436 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
914
915 /**
916 * Whether the command is disabled or not.
917 */
918 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); 437 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
919
920 /**
921 * Whether the command is hidden or not.
922 */
923 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); 438 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
924
925 /**
926 * Whether the command is checked or not.
927 */
928 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); 439 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
929
930 /**
931 * The flag that prevents the shortcut text from being displayed on menu.
932 *
933 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
934 * is displayed in menu when the command is assosiated with a menu item.
935 * Otherwise, no text is displayed.
936 */
937 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); 440 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
938
939 /**
940 * Dispatches a canExecute event on the target.
941 * @param {!cr.ui.Command} command The command that we are testing for.
942 * @param {EventTarget} target The target element to dispatch the event on.
943 */
944 function dispatchCanExecuteEvent(command, target) { 441 function dispatchCanExecuteEvent(command, target) {
945 var e = new CanExecuteEvent(command); 442 var e = new CanExecuteEvent(command);
946 target.dispatchEvent(e); 443 target.dispatchEvent(e);
947 command.disabled = !e.canExecute; 444 command.disabled = !e.canExecute;
948 } 445 }
949
950 /**
951 * The command managers for different documents.
952 */
953 var commandManagers = {}; 446 var commandManagers = {};
954
955 /**
956 * Keeps track of the focused element and updates the commands when the focus
957 * changes.
958 * @param {!Document} doc The document that we are managing the commands for.
959 * @constructor
960 */
961 function CommandManager(doc) { 447 function CommandManager(doc) {
962 doc.addEventListener('focus', this.handleFocus_.bind(this), true); 448 doc.addEventListener('focus', this.handleFocus_.bind(this), true);
963 // Make sure we add the listener to the bubbling phase so that elements can
964 // prevent the command.
965 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); 449 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
966 } 450 }
967
968 /**
969 * Initializes a command manager for the document as needed.
970 * @param {!Document} doc The document to manage the commands for.
971 */
972 CommandManager.init = function(doc) { 451 CommandManager.init = function(doc) {
973 var uid = cr.getUid(doc); 452 var uid = cr.getUid(doc);
974 if (!(uid in commandManagers)) { 453 if (!(uid in commandManagers)) {
975 commandManagers[uid] = new CommandManager(doc); 454 commandManagers[uid] = new CommandManager(doc);
976 } 455 }
977 }; 456 };
978
979 CommandManager.prototype = { 457 CommandManager.prototype = {
980
981 /**
982 * Handles focus changes on the document.
983 * @param {Event} e The focus event object.
984 * @private
985 * @suppress {checkTypes}
986 * TODO(vitalyp): remove the suppression.
987 */
988 handleFocus_: function(e) { 458 handleFocus_: function(e) {
989 var target = e.target; 459 var target = e.target;
990 460 if (target.menu || target.command) return;
991 // Ignore focus on a menu button or command item. 461 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect orAll('command'));
992 if (target.menu || target.command)
993 return;
994
995 var commands = Array.prototype.slice.call(
996 target.ownerDocument.querySelectorAll('command'));
997
998 commands.forEach(function(command) { 462 commands.forEach(function(command) {
999 dispatchCanExecuteEvent(command, target); 463 dispatchCanExecuteEvent(command, target);
1000 }); 464 });
1001 }, 465 },
1002
1003 /**
1004 * Handles the keydown event and routes it to the right command.
1005 * @param {!Event} e The keydown event.
1006 */
1007 handleKeyDown_: function(e) { 466 handleKeyDown_: function(e) {
1008 var target = e.target; 467 var target = e.target;
1009 var commands = Array.prototype.slice.call( 468 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect orAll('command'));
1010 target.ownerDocument.querySelectorAll('command'));
1011
1012 for (var i = 0, command; command = commands[i]; i++) { 469 for (var i = 0, command; command = commands[i]; i++) {
1013 if (command.matchesEvent(e)) { 470 if (command.matchesEvent(e)) {
1014 // When invoking a command via a shortcut, we have to manually check
1015 // if it can be executed, since focus might not have been changed
1016 // what would have updated the command's state.
1017 command.canExecuteChange(); 471 command.canExecuteChange();
1018
1019 if (!command.disabled) { 472 if (!command.disabled) {
1020 e.preventDefault(); 473 e.preventDefault();
1021 // We do not want any other element to handle this.
1022 e.stopPropagation(); 474 e.stopPropagation();
1023 command.execute(); 475 command.execute();
1024 return; 476 return;
1025 } 477 }
1026 } 478 }
1027 } 479 }
1028 } 480 }
1029 }; 481 };
1030
1031 /**
1032 * The event type used for canExecute events.
1033 * @param {!cr.ui.Command} command The command that we are evaluating.
1034 * @extends {Event}
1035 * @constructor
1036 * @class
1037 */
1038 function CanExecuteEvent(command) { 482 function CanExecuteEvent(command) {
1039 var e = new Event('canExecute', {bubbles: true, cancelable: true}); 483 var e = new Event('canExecute', {
484 bubbles: true,
485 cancelable: true
486 });
1040 e.__proto__ = CanExecuteEvent.prototype; 487 e.__proto__ = CanExecuteEvent.prototype;
1041 e.command = command; 488 e.command = command;
1042 return e; 489 return e;
1043 } 490 }
1044
1045 CanExecuteEvent.prototype = { 491 CanExecuteEvent.prototype = {
1046 __proto__: Event.prototype, 492 __proto__: Event.prototype,
1047
1048 /**
1049 * The current command
1050 * @type {cr.ui.Command}
1051 */
1052 command: null, 493 command: null,
1053
1054 /**
1055 * Whether the target can execute the command. Setting this also stops the
1056 * propagation and prevents the default. Callers can tell if an event has
1057 * been handled via |this.defaultPrevented|.
1058 * @type {boolean}
1059 */
1060 canExecute_: false, 494 canExecute_: false,
1061 get canExecute() { 495 get canExecute() {
1062 return this.canExecute_; 496 return this.canExecute_;
1063 }, 497 },
1064 set canExecute(canExecute) { 498 set canExecute(canExecute) {
1065 this.canExecute_ = !!canExecute; 499 this.canExecute_ = !!canExecute;
1066 this.stopPropagation(); 500 this.stopPropagation();
1067 this.preventDefault(); 501 this.preventDefault();
1068 } 502 }
1069 }; 503 };
1070
1071 // Export
1072 return { 504 return {
1073 Command: Command, 505 Command: Command,
1074 CanExecuteEvent: CanExecuteEvent 506 CanExecuteEvent: CanExecuteEvent
1075 }; 507 };
1076 }); 508 });
509
1077 Polymer({ 510 Polymer({
1078 is: 'app-drawer', 511 is: 'app-drawer',
1079 512 properties: {
1080 properties: { 513 opened: {
1081 /** 514 type: Boolean,
1082 * The opened state of the drawer. 515 value: false,
1083 */ 516 notify: true,
1084 opened: { 517 reflectToAttribute: true
1085 type: Boolean, 518 },
1086 value: false, 519 persistent: {
1087 notify: true, 520 type: Boolean,
1088 reflectToAttribute: true 521 value: false,
1089 }, 522 reflectToAttribute: true
1090 523 },
1091 /** 524 align: {
1092 * The drawer does not have a scrim and cannot be swiped close. 525 type: String,
1093 */ 526 value: 'left'
1094 persistent: { 527 },
1095 type: Boolean, 528 position: {
1096 value: false, 529 type: String,
1097 reflectToAttribute: true 530 readOnly: true,
1098 }, 531 value: 'left',
1099 532 reflectToAttribute: true
1100 /** 533 },
1101 * The alignment of the drawer on the screen ('left', 'right', 'start' o r 'end'). 534 swipeOpen: {
1102 * 'start' computes to left and 'end' to right in LTR layout and vice ve rsa in RTL 535 type: Boolean,
1103 * layout. 536 value: false,
1104 */ 537 reflectToAttribute: true
1105 align: { 538 },
1106 type: String, 539 noFocusTrap: {
1107 value: 'left' 540 type: Boolean,
1108 }, 541 value: false
1109 542 }
1110 /** 543 },
1111 * The computed, read-only position of the drawer on the screen ('left' or 'right'). 544 observers: [ 'resetLayout(position)', '_resetPosition(align, isAttached)' ],
1112 */ 545 _translateOffset: 0,
1113 position: { 546 _trackDetails: null,
1114 type: String, 547 _drawerState: 0,
1115 readOnly: true, 548 _boundEscKeydownHandler: null,
1116 value: 'left', 549 _firstTabStop: null,
1117 reflectToAttribute: true 550 _lastTabStop: null,
1118 }, 551 ready: function() {
1119 552 this.setScrollDirection('y');
1120 /** 553 this._setTransitionDuration('0s');
1121 * Create an area at the edge of the screen to swipe open the drawer. 554 },
1122 */ 555 attached: function() {
1123 swipeOpen: { 556 Polymer.RenderStatus.afterNextRender(this, function() {
1124 type: Boolean, 557 this._setTransitionDuration('');
1125 value: false, 558 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
1126 reflectToAttribute: true 559 this._resetDrawerState();
1127 }, 560 this.listen(this, 'track', '_track');
1128 561 this.addEventListener('transitionend', this._transitionend.bind(this));
1129 /** 562 this.addEventListener('keydown', this._tabKeydownHandler.bind(this));
1130 * Trap keyboard focus when the drawer is opened and not persistent. 563 });
1131 */ 564 },
1132 noFocusTrap: { 565 detached: function() {
1133 type: Boolean, 566 document.removeEventListener('keydown', this._boundEscKeydownHandler);
1134 value: false 567 },
1135 } 568 open: function() {
1136 }, 569 this.opened = true;
1137 570 },
1138 observers: [ 571 close: function() {
1139 'resetLayout(position)', 572 this.opened = false;
1140 '_resetPosition(align, isAttached)' 573 },
1141 ], 574 toggle: function() {
1142 575 this.opened = !this.opened;
1143 _translateOffset: 0, 576 },
1144 577 getWidth: function() {
1145 _trackDetails: null, 578 return this.$.contentContainer.offsetWidth;
1146 579 },
1147 _drawerState: 0, 580 resetLayout: function() {
1148 581 this.debounce('_resetLayout', function() {
1149 _boundEscKeydownHandler: null, 582 this.fire('app-drawer-reset-layout');
1150 583 }, 1);
1151 _firstTabStop: null, 584 },
1152 585 _isRTL: function() {
1153 _lastTabStop: null, 586 return window.getComputedStyle(this).direction === 'rtl';
1154 587 },
1155 ready: function() { 588 _resetPosition: function() {
1156 // Set the scroll direction so you can vertically scroll inside the draw er. 589 switch (this.align) {
1157 this.setScrollDirection('y'); 590 case 'start':
1158 591 this._setPosition(this._isRTL() ? 'right' : 'left');
1159 // Only transition the drawer after its first render (e.g. app-drawer-la yout 592 return;
1160 // may need to set the initial opened state which should not be transiti oned). 593
1161 this._setTransitionDuration('0s'); 594 case 'end':
1162 }, 595 this._setPosition(this._isRTL() ? 'left' : 'right');
1163 596 return;
1164 attached: function() { 597 }
1165 // Only transition the drawer after its first render (e.g. app-drawer-la yout 598 this._setPosition(this.align);
1166 // may need to set the initial opened state which should not be transiti oned). 599 },
1167 Polymer.RenderStatus.afterNextRender(this, function() { 600 _escKeydownHandler: function(event) {
1168 this._setTransitionDuration(''); 601 var ESC_KEYCODE = 27;
1169 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); 602 if (event.keyCode === ESC_KEYCODE) {
1170 this._resetDrawerState(); 603 event.preventDefault();
1171 604 this.close();
1172 this.listen(this, 'track', '_track'); 605 }
1173 this.addEventListener('transitionend', this._transitionend.bind(this)) ; 606 },
1174 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) 607 _track: function(event) {
1175 }); 608 if (this.persistent) {
1176 }, 609 return;
1177 610 }
1178 detached: function() { 611 event.preventDefault();
612 switch (event.detail.state) {
613 case 'start':
614 this._trackStart(event);
615 break;
616
617 case 'track':
618 this._trackMove(event);
619 break;
620
621 case 'end':
622 this._trackEnd(event);
623 break;
624 }
625 },
626 _trackStart: function(event) {
627 this._drawerState = this._DRAWER_STATE.TRACKING;
628 this._setTransitionDuration('0s');
629 this.style.visibility = 'visible';
630 var rect = this.$.contentContainer.getBoundingClientRect();
631 if (this.position === 'left') {
632 this._translateOffset = rect.left;
633 } else {
634 this._translateOffset = rect.right - window.innerWidth;
635 }
636 this._trackDetails = [];
637 },
638 _trackMove: function(event) {
639 this._translateDrawer(event.detail.dx + this._translateOffset);
640 this._trackDetails.push({
641 dx: event.detail.dx,
642 timeStamp: Date.now()
643 });
644 },
645 _trackEnd: function(event) {
646 var x = event.detail.dx + this._translateOffset;
647 var drawerWidth = this.getWidth();
648 var isPositionLeft = this.position === 'left';
649 var isInEndState = isPositionLeft ? x >= 0 || x <= -drawerWidth : x <= 0 || x >= drawerWidth;
650 if (!isInEndState) {
651 var trackDetails = this._trackDetails;
652 this._trackDetails = null;
653 this._flingDrawer(event, trackDetails);
654 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
655 return;
656 }
657 }
658 var halfWidth = drawerWidth / 2;
659 if (event.detail.dx < -halfWidth) {
660 this.opened = this.position === 'right';
661 } else if (event.detail.dx > halfWidth) {
662 this.opened = this.position === 'left';
663 }
664 if (isInEndState) {
665 this._resetDrawerState();
666 }
667 this._setTransitionDuration('');
668 this._resetDrawerTranslate();
669 this.style.visibility = '';
670 },
671 _calculateVelocity: function(event, trackDetails) {
672 var now = Date.now();
673 var timeLowerBound = now - 100;
674 var trackDetail;
675 var min = 0;
676 var max = trackDetails.length - 1;
677 while (min <= max) {
678 var mid = min + max >> 1;
679 var d = trackDetails[mid];
680 if (d.timeStamp >= timeLowerBound) {
681 trackDetail = d;
682 max = mid - 1;
683 } else {
684 min = mid + 1;
685 }
686 }
687 if (trackDetail) {
688 var dx = event.detail.dx - trackDetail.dx;
689 var dt = now - trackDetail.timeStamp || 1;
690 return dx / dt;
691 }
692 return 0;
693 },
694 _flingDrawer: function(event, trackDetails) {
695 var velocity = this._calculateVelocity(event, trackDetails);
696 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
697 return;
698 }
699 this._drawerState = this._DRAWER_STATE.FLINGING;
700 var x = event.detail.dx + this._translateOffset;
701 var drawerWidth = this.getWidth();
702 var isPositionLeft = this.position === 'left';
703 var isVelocityPositive = velocity > 0;
704 var isClosingLeft = !isVelocityPositive && isPositionLeft;
705 var isClosingRight = isVelocityPositive && !isPositionLeft;
706 var dx;
707 if (isClosingLeft) {
708 dx = -(x + drawerWidth);
709 } else if (isClosingRight) {
710 dx = drawerWidth - x;
711 } else {
712 dx = -x;
713 }
714 if (isVelocityPositive) {
715 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
716 this.opened = this.position === 'left';
717 } else {
718 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
719 this.opened = this.position === 'right';
720 }
721 this._setTransitionDuration(this._FLING_INITIAL_SLOPE * dx / velocity + 'ms' );
722 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);
723 this._resetDrawerTranslate();
724 },
725 _transitionend: function(event) {
726 var target = Polymer.dom(event).rootTarget;
727 if (target === this.$.contentContainer || target === this.$.scrim) {
728 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
729 this._setTransitionDuration('');
730 this._setTransitionTimingFunction('');
731 this.style.visibility = '';
732 }
733 this._resetDrawerState();
734 }
735 },
736 _setTransitionDuration: function(duration) {
737 this.$.contentContainer.style.transitionDuration = duration;
738 this.$.scrim.style.transitionDuration = duration;
739 },
740 _setTransitionTimingFunction: function(timingFunction) {
741 this.$.contentContainer.style.transitionTimingFunction = timingFunction;
742 this.$.scrim.style.transitionTimingFunction = timingFunction;
743 },
744 _translateDrawer: function(x) {
745 var drawerWidth = this.getWidth();
746 if (this.position === 'left') {
747 x = Math.max(-drawerWidth, Math.min(x, 0));
748 this.$.scrim.style.opacity = 1 + x / drawerWidth;
749 } else {
750 x = Math.max(0, Math.min(x, drawerWidth));
751 this.$.scrim.style.opacity = 1 - x / drawerWidth;
752 }
753 this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
754 },
755 _resetDrawerTranslate: function() {
756 this.$.scrim.style.opacity = '';
757 this.transform('', this.$.contentContainer);
758 },
759 _resetDrawerState: function() {
760 var oldState = this._drawerState;
761 if (this.opened) {
762 this._drawerState = this.persistent ? this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED;
763 } else {
764 this._drawerState = this._DRAWER_STATE.CLOSED;
765 }
766 if (oldState !== this._drawerState) {
767 if (this._drawerState === this._DRAWER_STATE.OPENED) {
768 this._setKeyboardFocusTrap();
769 document.addEventListener('keydown', this._boundEscKeydownHandler);
770 document.body.style.overflow = 'hidden';
771 } else {
1179 document.removeEventListener('keydown', this._boundEscKeydownHandler); 772 document.removeEventListener('keydown', this._boundEscKeydownHandler);
1180 }, 773 document.body.style.overflow = '';
1181 774 }
1182 /** 775 if (oldState !== this._DRAWER_STATE.INIT) {
1183 * Opens the drawer. 776 this.fire('app-drawer-transitioned');
1184 */ 777 }
1185 open: function() { 778 }
1186 this.opened = true; 779 },
1187 }, 780 _setKeyboardFocusTrap: function() {
1188 781 if (this.noFocusTrap) {
1189 /** 782 return;
1190 * Closes the drawer. 783 }
1191 */ 784 var focusableElementsSelector = [ 'a[href]:not([tabindex="-1"])', 'area[href ]:not([tabindex="-1"])', 'input:not([disabled]):not([tabindex="-1"])', 'select:n ot([disabled]):not([tabindex="-1"])', 'textarea:not([disabled]):not([tabindex="- 1"])', 'button:not([disabled]):not([tabindex="-1"])', 'iframe:not([tabindex="-1" ])', '[tabindex]:not([tabindex="-1"])', '[contentEditable=true]:not([tabindex="- 1"])' ].join(',');
1192 close: function() { 785 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElements Selector);
1193 this.opened = false; 786 if (focusableElements.length > 0) {
1194 }, 787 this._firstTabStop = focusableElements[0];
1195 788 this._lastTabStop = focusableElements[focusableElements.length - 1];
1196 /** 789 } else {
1197 * Toggles the drawer open and close. 790 this._firstTabStop = null;
1198 */ 791 this._lastTabStop = null;
1199 toggle: function() { 792 }
1200 this.opened = !this.opened; 793 var tabindex = this.getAttribute('tabindex');
1201 }, 794 if (tabindex && parseInt(tabindex, 10) > -1) {
1202 795 this.focus();
1203 /** 796 } else if (this._firstTabStop) {
1204 * Gets the width of the drawer. 797 this._firstTabStop.focus();
1205 * 798 }
1206 * @return {number} The width of the drawer in pixels. 799 },
1207 */ 800 _tabKeydownHandler: function(event) {
1208 getWidth: function() { 801 if (this.noFocusTrap) {
1209 return this.$.contentContainer.offsetWidth; 802 return;
1210 }, 803 }
1211 804 var TAB_KEYCODE = 9;
1212 /** 805 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB _KEYCODE) {
1213 * Resets the layout. If you changed the size of app-header via CSS 806 if (event.shiftKey) {
1214 * you can notify the changes by either firing the `iron-resize` event 807 if (this._firstTabStop && Polymer.dom(event).localTarget === this._first TabStop) {
1215 * or calling `resetLayout` directly.
1216 *
1217 * @method resetLayout
1218 */
1219 resetLayout: function() {
1220 this.debounce('_resetLayout', function() {
1221 this.fire('app-drawer-reset-layout');
1222 }, 1);
1223 },
1224
1225 _isRTL: function() {
1226 return window.getComputedStyle(this).direction === 'rtl';
1227 },
1228
1229 _resetPosition: function() {
1230 switch (this.align) {
1231 case 'start':
1232 this._setPosition(this._isRTL() ? 'right' : 'left');
1233 return;
1234 case 'end':
1235 this._setPosition(this._isRTL() ? 'left' : 'right');
1236 return;
1237 }
1238 this._setPosition(this.align);
1239 },
1240
1241 _escKeydownHandler: function(event) {
1242 var ESC_KEYCODE = 27;
1243 if (event.keyCode === ESC_KEYCODE) {
1244 // Prevent any side effects if app-drawer closes.
1245 event.preventDefault(); 808 event.preventDefault();
1246 this.close(); 809 this._lastTabStop.focus();
1247 } 810 }
1248 }, 811 } else {
1249 812 if (this._lastTabStop && Polymer.dom(event).localTarget === this._lastTa bStop) {
1250 _track: function(event) { 813 event.preventDefault();
1251 if (this.persistent) {
1252 return;
1253 }
1254
1255 // Disable user selection on desktop.
1256 event.preventDefault();
1257
1258 switch (event.detail.state) {
1259 case 'start':
1260 this._trackStart(event);
1261 break;
1262 case 'track':
1263 this._trackMove(event);
1264 break;
1265 case 'end':
1266 this._trackEnd(event);
1267 break;
1268 }
1269 },
1270
1271 _trackStart: function(event) {
1272 this._drawerState = this._DRAWER_STATE.TRACKING;
1273
1274 // Disable transitions since style attributes will reflect user track ev ents.
1275 this._setTransitionDuration('0s');
1276 this.style.visibility = 'visible';
1277
1278 var rect = this.$.contentContainer.getBoundingClientRect();
1279 if (this.position === 'left') {
1280 this._translateOffset = rect.left;
1281 } else {
1282 this._translateOffset = rect.right - window.innerWidth;
1283 }
1284
1285 this._trackDetails = [];
1286 },
1287
1288 _trackMove: function(event) {
1289 this._translateDrawer(event.detail.dx + this._translateOffset);
1290
1291 // Use Date.now() since event.timeStamp is inconsistent across browsers (e.g. most
1292 // browsers use milliseconds but FF 44 uses microseconds).
1293 this._trackDetails.push({
1294 dx: event.detail.dx,
1295 timeStamp: Date.now()
1296 });
1297 },
1298
1299 _trackEnd: function(event) {
1300 var x = event.detail.dx + this._translateOffset;
1301 var drawerWidth = this.getWidth();
1302 var isPositionLeft = this.position === 'left';
1303 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
1304 (x <= 0 || x >= drawerWidth);
1305
1306 if (!isInEndState) {
1307 // No longer need the track events after this method returns - allow t hem to be GC'd.
1308 var trackDetails = this._trackDetails;
1309 this._trackDetails = null;
1310
1311 this._flingDrawer(event, trackDetails);
1312 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1313 return;
1314 }
1315 }
1316
1317 // If the drawer is not flinging, toggle the opened state based on the p osition of
1318 // the drawer.
1319 var halfWidth = drawerWidth / 2;
1320 if (event.detail.dx < -halfWidth) {
1321 this.opened = this.position === 'right';
1322 } else if (event.detail.dx > halfWidth) {
1323 this.opened = this.position === 'left';
1324 }
1325
1326 // Trigger app-drawer-transitioned now since there will be no transition end event.
1327 if (isInEndState) {
1328 this._resetDrawerState();
1329 }
1330
1331 this._setTransitionDuration('');
1332 this._resetDrawerTranslate();
1333 this.style.visibility = '';
1334 },
1335
1336 _calculateVelocity: function(event, trackDetails) {
1337 // Find the oldest track event that is within 100ms using binary search.
1338 var now = Date.now();
1339 var timeLowerBound = now - 100;
1340 var trackDetail;
1341 var min = 0;
1342 var max = trackDetails.length - 1;
1343
1344 while (min <= max) {
1345 // Floor of average of min and max.
1346 var mid = (min + max) >> 1;
1347 var d = trackDetails[mid];
1348 if (d.timeStamp >= timeLowerBound) {
1349 trackDetail = d;
1350 max = mid - 1;
1351 } else {
1352 min = mid + 1;
1353 }
1354 }
1355
1356 if (trackDetail) {
1357 var dx = event.detail.dx - trackDetail.dx;
1358 var dt = (now - trackDetail.timeStamp) || 1;
1359 return dx / dt;
1360 }
1361 return 0;
1362 },
1363
1364 _flingDrawer: function(event, trackDetails) {
1365 var velocity = this._calculateVelocity(event, trackDetails);
1366
1367 // Do not fling if velocity is not above a threshold.
1368 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
1369 return;
1370 }
1371
1372 this._drawerState = this._DRAWER_STATE.FLINGING;
1373
1374 var x = event.detail.dx + this._translateOffset;
1375 var drawerWidth = this.getWidth();
1376 var isPositionLeft = this.position === 'left';
1377 var isVelocityPositive = velocity > 0;
1378 var isClosingLeft = !isVelocityPositive && isPositionLeft;
1379 var isClosingRight = isVelocityPositive && !isPositionLeft;
1380 var dx;
1381 if (isClosingLeft) {
1382 dx = -(x + drawerWidth);
1383 } else if (isClosingRight) {
1384 dx = (drawerWidth - x);
1385 } else {
1386 dx = -x;
1387 }
1388
1389 // Enforce a minimum transition velocity to make the drawer feel snappy.
1390 if (isVelocityPositive) {
1391 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
1392 this.opened = this.position === 'left';
1393 } else {
1394 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
1395 this.opened = this.position === 'right';
1396 }
1397
1398 // Calculate the amount of time needed to finish the transition based on the
1399 // initial slope of the timing function.
1400 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms');
1401 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);
1402
1403 this._resetDrawerTranslate();
1404 },
1405
1406 _transitionend: function(event) {
1407 // contentContainer will transition on opened state changed, and scrim w ill
1408 // transition on persistent state changed when opened - these are the
1409 // transitions we are interested in.
1410 var target = Polymer.dom(event).rootTarget;
1411 if (target === this.$.contentContainer || target === this.$.scrim) {
1412
1413 // If the drawer was flinging, we need to reset the style attributes.
1414 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1415 this._setTransitionDuration('');
1416 this._setTransitionTimingFunction('');
1417 this.style.visibility = '';
1418 }
1419
1420 this._resetDrawerState();
1421 }
1422 },
1423
1424 _setTransitionDuration: function(duration) {
1425 this.$.contentContainer.style.transitionDuration = duration;
1426 this.$.scrim.style.transitionDuration = duration;
1427 },
1428
1429 _setTransitionTimingFunction: function(timingFunction) {
1430 this.$.contentContainer.style.transitionTimingFunction = timingFunction;
1431 this.$.scrim.style.transitionTimingFunction = timingFunction;
1432 },
1433
1434 _translateDrawer: function(x) {
1435 var drawerWidth = this.getWidth();
1436
1437 if (this.position === 'left') {
1438 x = Math.max(-drawerWidth, Math.min(x, 0));
1439 this.$.scrim.style.opacity = 1 + x / drawerWidth;
1440 } else {
1441 x = Math.max(0, Math.min(x, drawerWidth));
1442 this.$.scrim.style.opacity = 1 - x / drawerWidth;
1443 }
1444
1445 this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
1446 },
1447
1448 _resetDrawerTranslate: function() {
1449 this.$.scrim.style.opacity = '';
1450 this.transform('', this.$.contentContainer);
1451 },
1452
1453 _resetDrawerState: function() {
1454 var oldState = this._drawerState;
1455 if (this.opened) {
1456 this._drawerState = this.persistent ?
1457 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED;
1458 } else {
1459 this._drawerState = this._DRAWER_STATE.CLOSED;
1460 }
1461
1462 if (oldState !== this._drawerState) {
1463 if (this._drawerState === this._DRAWER_STATE.OPENED) {
1464 this._setKeyboardFocusTrap();
1465 document.addEventListener('keydown', this._boundEscKeydownHandler);
1466 document.body.style.overflow = 'hidden';
1467 } else {
1468 document.removeEventListener('keydown', this._boundEscKeydownHandler );
1469 document.body.style.overflow = '';
1470 }
1471
1472 // Don't fire the event on initial load.
1473 if (oldState !== this._DRAWER_STATE.INIT) {
1474 this.fire('app-drawer-transitioned');
1475 }
1476 }
1477 },
1478
1479 _setKeyboardFocusTrap: function() {
1480 if (this.noFocusTrap) {
1481 return;
1482 }
1483
1484 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated) , this will
1485 // not select focusable elements inside shadow roots.
1486 var focusableElementsSelector = [
1487 'a[href]:not([tabindex="-1"])',
1488 'area[href]:not([tabindex="-1"])',
1489 'input:not([disabled]):not([tabindex="-1"])',
1490 'select:not([disabled]):not([tabindex="-1"])',
1491 'textarea:not([disabled]):not([tabindex="-1"])',
1492 'button:not([disabled]):not([tabindex="-1"])',
1493 'iframe:not([tabindex="-1"])',
1494 '[tabindex]:not([tabindex="-1"])',
1495 '[contentEditable=true]:not([tabindex="-1"])'
1496 ].join(',');
1497 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem entsSelector);
1498
1499 if (focusableElements.length > 0) {
1500 this._firstTabStop = focusableElements[0];
1501 this._lastTabStop = focusableElements[focusableElements.length - 1];
1502 } else {
1503 // Reset saved tab stops when there are no focusable elements in the d rawer.
1504 this._firstTabStop = null;
1505 this._lastTabStop = null;
1506 }
1507
1508 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the first focusable
1509 // element in the drawer, if it exists. Use the tabindex attribute since the this.tabIndex
1510 // property in IE/Edge returns 0 (instead of -1) when the attribute is n ot set.
1511 var tabindex = this.getAttribute('tabindex');
1512 if (tabindex && parseInt(tabindex, 10) > -1) {
1513 this.focus();
1514 } else if (this._firstTabStop) {
1515 this._firstTabStop.focus(); 814 this._firstTabStop.focus();
1516 } 815 }
1517 }, 816 }
1518 817 }
1519 _tabKeydownHandler: function(event) { 818 },
1520 if (this.noFocusTrap) { 819 _MIN_FLING_THRESHOLD: .2,
1521 return; 820 _MIN_TRANSITION_VELOCITY: 1.2,
1522 } 821 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
1523 822 _FLING_INITIAL_SLOPE: 1.5,
1524 var TAB_KEYCODE = 9; 823 _DRAWER_STATE: {
1525 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB_KEYCODE) { 824 INIT: 0,
1526 if (event.shiftKey) { 825 OPENED: 1,
1527 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f irstTabStop) { 826 OPENED_PERSISTENT: 2,
1528 event.preventDefault(); 827 CLOSED: 3,
1529 this._lastTabStop.focus(); 828 TRACKING: 4,
1530 } 829 FLINGING: 5
1531 } else { 830 }
1532 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la stTabStop) { 831 });
1533 event.preventDefault(); 832
1534 this._firstTabStop.focus();
1535 }
1536 }
1537 }
1538 },
1539
1540 _MIN_FLING_THRESHOLD: 0.2,
1541
1542 _MIN_TRANSITION_VELOCITY: 1.2,
1543
1544 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
1545
1546 _FLING_INITIAL_SLOPE: 1.5,
1547
1548 _DRAWER_STATE: {
1549 INIT: 0,
1550 OPENED: 1,
1551 OPENED_PERSISTENT: 2,
1552 CLOSED: 3,
1553 TRACKING: 4,
1554 FLINGING: 5
1555 }
1556
1557 /**
1558 * Fired when the layout of app-drawer has changed.
1559 *
1560 * @event app-drawer-reset-layout
1561 */
1562
1563 /**
1564 * Fired when app-drawer has finished transitioning.
1565 *
1566 * @event app-drawer-transitioned
1567 */
1568 });
1569 (function() { 833 (function() {
1570 'use strict'; 834 'use strict';
1571
1572 Polymer({
1573 is: 'iron-location',
1574 properties: {
1575 /**
1576 * The pathname component of the URL.
1577 */
1578 path: {
1579 type: String,
1580 notify: true,
1581 value: function() {
1582 return window.decodeURIComponent(window.location.pathname);
1583 }
1584 },
1585 /**
1586 * The query string portion of the URL.
1587 */
1588 query: {
1589 type: String,
1590 notify: true,
1591 value: function() {
1592 return window.decodeURIComponent(window.location.search.slice(1));
1593 }
1594 },
1595 /**
1596 * The hash component of the URL.
1597 */
1598 hash: {
1599 type: String,
1600 notify: true,
1601 value: function() {
1602 return window.decodeURIComponent(window.location.hash.slice(1));
1603 }
1604 },
1605 /**
1606 * If the user was on a URL for less than `dwellTime` milliseconds, it
1607 * won't be added to the browser's history, but instead will be replaced
1608 * by the next entry.
1609 *
1610 * This is to prevent large numbers of entries from clogging up the user 's
1611 * browser history. Disable by setting to a negative number.
1612 */
1613 dwellTime: {
1614 type: Number,
1615 value: 2000
1616 },
1617
1618 /**
1619 * A regexp that defines the set of URLs that should be considered part
1620 * of this web app.
1621 *
1622 * Clicking on a link that matches this regex won't result in a full pag e
1623 * navigation, but will instead just update the URL state in place.
1624 *
1625 * This regexp is given everything after the origin in an absolute
1626 * URL. So to match just URLs that start with /search/ do:
1627 * url-space-regex="^/search/"
1628 *
1629 * @type {string|RegExp}
1630 */
1631 urlSpaceRegex: {
1632 type: String,
1633 value: ''
1634 },
1635
1636 /**
1637 * urlSpaceRegex, but coerced into a regexp.
1638 *
1639 * @type {RegExp}
1640 */
1641 _urlSpaceRegExp: {
1642 computed: '_makeRegExp(urlSpaceRegex)'
1643 },
1644
1645 _lastChangedAt: {
1646 type: Number
1647 },
1648
1649 _initialized: {
1650 type: Boolean,
1651 value: false
1652 }
1653 },
1654 hostAttributes: {
1655 hidden: true
1656 },
1657 observers: [
1658 '_updateUrl(path, query, hash)'
1659 ],
1660 attached: function() {
1661 this.listen(window, 'hashchange', '_hashChanged');
1662 this.listen(window, 'location-changed', '_urlChanged');
1663 this.listen(window, 'popstate', '_urlChanged');
1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g lobalOnClick');
1665 // Give a 200ms grace period to make initial redirects without any
1666 // additions to the user's history.
1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
1668
1669 this._initialized = true;
1670 this._urlChanged();
1671 },
1672 detached: function() {
1673 this.unlisten(window, 'hashchange', '_hashChanged');
1674 this.unlisten(window, 'location-changed', '_urlChanged');
1675 this.unlisten(window, 'popstate', '_urlChanged');
1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', ' _globalOnClick');
1677 this._initialized = false;
1678 },
1679 _hashChanged: function() {
1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1)) ;
1681 },
1682 _urlChanged: function() {
1683 // We want to extract all info out of the updated URL before we
1684 // try to write anything back into it.
1685 //
1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
1687 // one when we set this.hash. Likewise for query.
1688 this._dontUpdateUrl = true;
1689 this._hashChanged();
1690 this.path = window.decodeURIComponent(window.location.pathname);
1691 this.query = window.decodeURIComponent(
1692 window.location.search.substring(1));
1693 this._dontUpdateUrl = false;
1694 this._updateUrl();
1695 },
1696 _getUrl: function() {
1697 var partiallyEncodedPath = window.encodeURI(
1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
1699 var partiallyEncodedQuery = '';
1700 if (this.query) {
1701 partiallyEncodedQuery = '?' + window.encodeURI(
1702 this.query).replace(/\#/g, '%23');
1703 }
1704 var partiallyEncodedHash = '';
1705 if (this.hash) {
1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash);
1707 }
1708 return (
1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash) ;
1710 },
1711 _updateUrl: function() {
1712 if (this._dontUpdateUrl || !this._initialized) {
1713 return;
1714 }
1715 if (this.path === window.decodeURIComponent(window.location.pathname) &&
1716 this.query === window.decodeURIComponent(
1717 window.location.search.substring(1)) &&
1718 this.hash === window.decodeURIComponent(
1719 window.location.hash.substring(1))) {
1720 // Nothing to do, the current URL is a representation of our propertie s.
1721 return;
1722 }
1723 var newUrl = this._getUrl();
1724 // Need to use a full URL in case the containing page has a base URI.
1725 var fullNewUrl = new URL(
1726 newUrl, window.location.protocol + '//' + window.location.host).href ;
1727 var now = window.performance.now();
1728 var shouldReplace =
1729 this._lastChangedAt + this.dwellTime > now;
1730 this._lastChangedAt = now;
1731 if (shouldReplace) {
1732 window.history.replaceState({}, '', fullNewUrl);
1733 } else {
1734 window.history.pushState({}, '', fullNewUrl);
1735 }
1736 this.fire('location-changed', {}, {node: window});
1737 },
1738 /**
1739 * A necessary evil so that links work as expected. Does its best to
1740 * bail out early if possible.
1741 *
1742 * @param {MouseEvent} event .
1743 */
1744 _globalOnClick: function(event) {
1745 // If another event handler has stopped this event then there's nothing
1746 // for us to do. This can happen e.g. when there are multiple
1747 // iron-location elements in a page.
1748 if (event.defaultPrevented) {
1749 return;
1750 }
1751 var href = this._getSameOriginLinkHref(event);
1752 if (!href) {
1753 return;
1754 }
1755 event.preventDefault();
1756 // If the navigation is to the current page we shouldn't add a history
1757 // entry or fire a change event.
1758 if (href === window.location.href) {
1759 return;
1760 }
1761 window.history.pushState({}, '', href);
1762 this.fire('location-changed', {}, {node: window});
1763 },
1764 /**
1765 * Returns the absolute URL of the link (if any) that this click event
1766 * is clicking on, if we can and should override the resulting full
1767 * page navigation. Returns null otherwise.
1768 *
1769 * @param {MouseEvent} event .
1770 * @return {string?} .
1771 */
1772 _getSameOriginLinkHref: function(event) {
1773 // We only care about left-clicks.
1774 if (event.button !== 0) {
1775 return null;
1776 }
1777 // We don't want modified clicks, where the intent is to open the page
1778 // in a new tab.
1779 if (event.metaKey || event.ctrlKey) {
1780 return null;
1781 }
1782 var eventPath = Polymer.dom(event).path;
1783 var anchor = null;
1784 for (var i = 0; i < eventPath.length; i++) {
1785 var element = eventPath[i];
1786 if (element.tagName === 'A' && element.href) {
1787 anchor = element;
1788 break;
1789 }
1790 }
1791
1792 // If there's no link there's nothing to do.
1793 if (!anchor) {
1794 return null;
1795 }
1796
1797 // Target blank is a new tab, don't intercept.
1798 if (anchor.target === '_blank') {
1799 return null;
1800 }
1801 // If the link is for an existing parent frame, don't intercept.
1802 if ((anchor.target === '_top' ||
1803 anchor.target === '_parent') &&
1804 window.top !== window) {
1805 return null;
1806 }
1807
1808 var href = anchor.href;
1809
1810 // It only makes sense for us to intercept same-origin navigations.
1811 // pushState/replaceState don't work with cross-origin links.
1812 var url;
1813 if (document.baseURI != null) {
1814 url = new URL(href, /** @type {string} */(document.baseURI));
1815 } else {
1816 url = new URL(href);
1817 }
1818
1819 var origin;
1820
1821 // IE Polyfill
1822 if (window.location.origin) {
1823 origin = window.location.origin;
1824 } else {
1825 origin = window.location.protocol + '//' + window.location.hostname;
1826
1827 if (window.location.port) {
1828 origin += ':' + window.location.port;
1829 }
1830 }
1831
1832 if (url.origin !== origin) {
1833 return null;
1834 }
1835 var normalizedHref = url.pathname + url.search + url.hash;
1836
1837 // If we've been configured not to handle this url... don't handle it!
1838 if (this._urlSpaceRegExp &&
1839 !this._urlSpaceRegExp.test(normalizedHref)) {
1840 return null;
1841 }
1842 // Need to use a full URL in case the containing page has a base URI.
1843 var fullNormalizedHref = new URL(
1844 normalizedHref, window.location.href).href;
1845 return fullNormalizedHref;
1846 },
1847 _makeRegExp: function(urlSpaceRegex) {
1848 return RegExp(urlSpaceRegex);
1849 }
1850 });
1851 })();
1852 'use strict';
1853
1854 Polymer({ 835 Polymer({
1855 is: 'iron-query-params', 836 is: 'iron-location',
1856 properties: { 837 properties: {
1857 paramsString: { 838 path: {
1858 type: String, 839 type: String,
1859 notify: true, 840 notify: true,
1860 observer: 'paramsStringChanged', 841 value: function() {
1861 }, 842 return window.decodeURIComponent(window.location.pathname);
1862 paramsObject: { 843 }
1863 type: Object, 844 },
845 query: {
846 type: String,
1864 notify: true, 847 notify: true,
1865 value: function() { 848 value: function() {
1866 return {}; 849 return window.decodeURIComponent(window.location.search.slice(1));
1867 } 850 }
1868 }, 851 },
1869 _dontReact: { 852 hash: {
853 type: String,
854 notify: true,
855 value: function() {
856 return window.decodeURIComponent(window.location.hash.slice(1));
857 }
858 },
859 dwellTime: {
860 type: Number,
861 value: 2e3
862 },
863 urlSpaceRegex: {
864 type: String,
865 value: ''
866 },
867 _urlSpaceRegExp: {
868 computed: '_makeRegExp(urlSpaceRegex)'
869 },
870 _lastChangedAt: {
871 type: Number
872 },
873 _initialized: {
1870 type: Boolean, 874 type: Boolean,
1871 value: false 875 value: false
1872 } 876 }
1873 }, 877 },
1874 hostAttributes: { 878 hostAttributes: {
1875 hidden: true 879 hidden: true
1876 }, 880 },
1877 observers: [ 881 observers: [ '_updateUrl(path, query, hash)' ],
1878 'paramsObjectChanged(paramsObject.*)' 882 attached: function() {
1879 ], 883 this.listen(window, 'hashchange', '_hashChanged');
1880 paramsStringChanged: function() { 884 this.listen(window, 'location-changed', '_urlChanged');
1881 this._dontReact = true; 885 this.listen(window, 'popstate', '_urlChanged');
1882 this.paramsObject = this._decodeParams(this.paramsString); 886 this.listen(document.body, 'click', '_globalOnClick');
1883 this._dontReact = false; 887 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
1884 }, 888 this._initialized = true;
1885 paramsObjectChanged: function() { 889 this._urlChanged();
1886 if (this._dontReact) { 890 },
891 detached: function() {
892 this.unlisten(window, 'hashchange', '_hashChanged');
893 this.unlisten(window, 'location-changed', '_urlChanged');
894 this.unlisten(window, 'popstate', '_urlChanged');
895 this.unlisten(document.body, 'click', '_globalOnClick');
896 this._initialized = false;
897 },
898 _hashChanged: function() {
899 this.hash = window.decodeURIComponent(window.location.hash.substring(1));
900 },
901 _urlChanged: function() {
902 this._dontUpdateUrl = true;
903 this._hashChanged();
904 this.path = window.decodeURIComponent(window.location.pathname);
905 this.query = window.decodeURIComponent(window.location.search.substring(1) );
906 this._dontUpdateUrl = false;
907 this._updateUrl();
908 },
909 _getUrl: function() {
910 var partiallyEncodedPath = window.encodeURI(this.path).replace(/\#/g, '%23 ').replace(/\?/g, '%3F');
911 var partiallyEncodedQuery = '';
912 if (this.query) {
913 partiallyEncodedQuery = '?' + window.encodeURI(this.query).replace(/\#/g , '%23');
914 }
915 var partiallyEncodedHash = '';
916 if (this.hash) {
917 partiallyEncodedHash = '#' + window.encodeURI(this.hash);
918 }
919 return partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash ;
920 },
921 _updateUrl: function() {
922 if (this._dontUpdateUrl || !this._initialized) {
1887 return; 923 return;
1888 } 924 }
1889 this.paramsString = this._encodeParams(this.paramsObject); 925 if (this.path === window.decodeURIComponent(window.location.pathname) && t his.query === window.decodeURIComponent(window.location.search.substring(1)) && this.hash === window.decodeURIComponent(window.location.hash.substring(1))) {
1890 }, 926 return;
1891 _encodeParams: function(params) { 927 }
1892 var encodedParams = []; 928 var newUrl = this._getUrl();
1893 for (var key in params) { 929 var fullNewUrl = new URL(newUrl, window.location.protocol + '//' + window. location.host).href;
1894 var value = params[key]; 930 var now = window.performance.now();
1895 if (value === '') { 931 var shouldReplace = this._lastChangedAt + this.dwellTime > now;
1896 encodedParams.push(encodeURIComponent(key)); 932 this._lastChangedAt = now;
1897 } else if (value) { 933 if (shouldReplace) {
1898 encodedParams.push( 934 window.history.replaceState({}, '', fullNewUrl);
1899 encodeURIComponent(key) + 935 } else {
1900 '=' + 936 window.history.pushState({}, '', fullNewUrl);
1901 encodeURIComponent(value.toString()) 937 }
1902 ); 938 this.fire('location-changed', {}, {
939 node: window
940 });
941 },
942 _globalOnClick: function(event) {
943 if (event.defaultPrevented) {
944 return;
945 }
946 var href = this._getSameOriginLinkHref(event);
947 if (!href) {
948 return;
949 }
950 event.preventDefault();
951 if (href === window.location.href) {
952 return;
953 }
954 window.history.pushState({}, '', href);
955 this.fire('location-changed', {}, {
956 node: window
957 });
958 },
959 _getSameOriginLinkHref: function(event) {
960 if (event.button !== 0) {
961 return null;
962 }
963 if (event.metaKey || event.ctrlKey) {
964 return null;
965 }
966 var eventPath = Polymer.dom(event).path;
967 var anchor = null;
968 for (var i = 0; i < eventPath.length; i++) {
969 var element = eventPath[i];
970 if (element.tagName === 'A' && element.href) {
971 anchor = element;
972 break;
1903 } 973 }
1904 } 974 }
1905 return encodedParams.join('&'); 975 if (!anchor) {
1906 }, 976 return null;
1907 _decodeParams: function(paramString) { 977 }
1908 var params = {}; 978 if (anchor.target === '_blank') {
1909 979 return null;
1910 // Work around a bug in decodeURIComponent where + is not 980 }
1911 // converted to spaces: 981 if ((anchor.target === '_top' || anchor.target === '_parent') && window.to p !== window) {
1912 paramString = (paramString || '').replace(/\+/g, '%20'); 982 return null;
1913 983 }
1914 var paramList = paramString.split('&'); 984 var href = anchor.href;
1915 for (var i = 0; i < paramList.length; i++) { 985 var url;
1916 var param = paramList[i].split('='); 986 if (document.baseURI != null) {
1917 if (param[0]) { 987 url = new URL(href, document.baseURI);
1918 params[decodeURIComponent(param[0])] = 988 } else {
1919 decodeURIComponent(param[1] || ''); 989 url = new URL(href);
990 }
991 var origin;
992 if (window.location.origin) {
993 origin = window.location.origin;
994 } else {
995 origin = window.location.protocol + '//' + window.location.hostname;
996 if (window.location.port) {
997 origin += ':' + window.location.port;
1920 } 998 }
1921 } 999 }
1922 return params; 1000 if (url.origin !== origin) {
1001 return null;
1002 }
1003 var normalizedHref = url.pathname + url.search + url.hash;
1004 if (this._urlSpaceRegExp && !this._urlSpaceRegExp.test(normalizedHref)) {
1005 return null;
1006 }
1007 var fullNormalizedHref = new URL(normalizedHref, window.location.href).hre f;
1008 return fullNormalizedHref;
1009 },
1010 _makeRegExp: function(urlSpaceRegex) {
1011 return RegExp(urlSpaceRegex);
1923 } 1012 }
1924 }); 1013 });
1014 })();
1015
1925 'use strict'; 1016 'use strict';
1926 1017
1927 /** 1018 Polymer({
1928 * Provides bidirectional mapping between `path` and `queryParams` and a 1019 is: 'iron-query-params',
1929 * app-route compatible `route` object. 1020 properties: {
1930 * 1021 paramsString: {
1931 * For more information, see the docs for `app-route-converter`. 1022 type: String,
1932 * 1023 notify: true,
1933 * @polymerBehavior 1024 observer: 'paramsStringChanged'
1934 */ 1025 },
1935 Polymer.AppRouteConverterBehavior = { 1026 paramsObject: {
1027 type: Object,
1028 notify: true,
1029 value: function() {
1030 return {};
1031 }
1032 },
1033 _dontReact: {
1034 type: Boolean,
1035 value: false
1036 }
1037 },
1038 hostAttributes: {
1039 hidden: true
1040 },
1041 observers: [ 'paramsObjectChanged(paramsObject.*)' ],
1042 paramsStringChanged: function() {
1043 this._dontReact = true;
1044 this.paramsObject = this._decodeParams(this.paramsString);
1045 this._dontReact = false;
1046 },
1047 paramsObjectChanged: function() {
1048 if (this._dontReact) {
1049 return;
1050 }
1051 this.paramsString = this._encodeParams(this.paramsObject);
1052 },
1053 _encodeParams: function(params) {
1054 var encodedParams = [];
1055 for (var key in params) {
1056 var value = params[key];
1057 if (value === '') {
1058 encodedParams.push(encodeURIComponent(key));
1059 } else if (value) {
1060 encodedParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(va lue.toString()));
1061 }
1062 }
1063 return encodedParams.join('&');
1064 },
1065 _decodeParams: function(paramString) {
1066 var params = {};
1067 paramString = (paramString || '').replace(/\+/g, '%20');
1068 var paramList = paramString.split('&');
1069 for (var i = 0; i < paramList.length; i++) {
1070 var param = paramList[i].split('=');
1071 if (param[0]) {
1072 params[decodeURIComponent(param[0])] = decodeURIComponent(param[1] || '' );
1073 }
1074 }
1075 return params;
1076 }
1077 });
1078
1079 'use strict';
1080
1081 Polymer.AppRouteConverterBehavior = {
1082 properties: {
1083 route: {
1084 type: Object,
1085 notify: true
1086 },
1087 queryParams: {
1088 type: Object,
1089 notify: true
1090 },
1091 path: {
1092 type: String,
1093 notify: true
1094 }
1095 },
1096 observers: [ '_locationChanged(path, queryParams)', '_routeChanged(route.prefi x, route.path)', '_routeQueryParamsChanged(route.__queryParams)' ],
1097 created: function() {
1098 this.linkPaths('route.__queryParams', 'queryParams');
1099 this.linkPaths('queryParams', 'route.__queryParams');
1100 },
1101 _locationChanged: function() {
1102 if (this.route && this.route.path === this.path && this.queryParams === this .route.__queryParams) {
1103 return;
1104 }
1105 this.route = {
1106 prefix: '',
1107 path: this.path,
1108 __queryParams: this.queryParams
1109 };
1110 },
1111 _routeChanged: function() {
1112 if (!this.route) {
1113 return;
1114 }
1115 this.path = this.route.prefix + this.route.path;
1116 },
1117 _routeQueryParamsChanged: function(queryParams) {
1118 if (!this.route) {
1119 return;
1120 }
1121 this.queryParams = queryParams;
1122 }
1123 };
1124
1125 'use strict';
1126
1127 Polymer({
1128 is: 'app-location',
1129 properties: {
1130 route: {
1131 type: Object,
1132 notify: true
1133 },
1134 useHashAsPath: {
1135 type: Boolean,
1136 value: false
1137 },
1138 urlSpaceRegex: {
1139 type: String,
1140 notify: true
1141 },
1142 __queryParams: {
1143 type: Object
1144 },
1145 __path: {
1146 type: String
1147 },
1148 __query: {
1149 type: String
1150 },
1151 __hash: {
1152 type: String
1153 },
1154 path: {
1155 type: String,
1156 observer: '__onPathChanged'
1157 }
1158 },
1159 behaviors: [ Polymer.AppRouteConverterBehavior ],
1160 observers: [ '__computeRoutePath(useHashAsPath, __hash, __path)' ],
1161 __computeRoutePath: function() {
1162 this.path = this.useHashAsPath ? this.__hash : this.__path;
1163 },
1164 __onPathChanged: function() {
1165 if (!this._readied) {
1166 return;
1167 }
1168 if (this.useHashAsPath) {
1169 this.__hash = this.path;
1170 } else {
1171 this.__path = this.path;
1172 }
1173 }
1174 });
1175
1176 'use strict';
1177
1178 Polymer({
1179 is: 'app-route',
1180 properties: {
1181 route: {
1182 type: Object,
1183 notify: true
1184 },
1185 pattern: {
1186 type: String
1187 },
1188 data: {
1189 type: Object,
1190 value: function() {
1191 return {};
1192 },
1193 notify: true
1194 },
1195 queryParams: {
1196 type: Object,
1197 value: function() {
1198 return {};
1199 },
1200 notify: true
1201 },
1202 tail: {
1203 type: Object,
1204 value: function() {
1205 return {
1206 path: null,
1207 prefix: null,
1208 __queryParams: null
1209 };
1210 },
1211 notify: true
1212 },
1213 active: {
1214 type: Boolean,
1215 notify: true,
1216 readOnly: true
1217 },
1218 _queryParamsUpdating: {
1219 type: Boolean,
1220 value: false
1221 },
1222 _matched: {
1223 type: String,
1224 value: ''
1225 }
1226 },
1227 observers: [ '__tryToMatch(route.path, pattern)', '__updatePathOnDataChange(da ta.*)', '__tailPathChanged(tail.path)', '__routeQueryParamsChanged(route.__query Params)', '__tailQueryParamsChanged(tail.__queryParams)', '__queryParamsChanged( queryParams.*)' ],
1228 created: function() {
1229 this.linkPaths('route.__queryParams', 'tail.__queryParams');
1230 this.linkPaths('tail.__queryParams', 'route.__queryParams');
1231 },
1232 __routeQueryParamsChanged: function(queryParams) {
1233 if (queryParams && this.tail) {
1234 this.set('tail.__queryParams', queryParams);
1235 if (!this.active || this._queryParamsUpdating) {
1236 return;
1237 }
1238 var copyOfQueryParams = {};
1239 var anythingChanged = false;
1240 for (var key in queryParams) {
1241 copyOfQueryParams[key] = queryParams[key];
1242 if (anythingChanged || !this.queryParams || queryParams[key] !== this.qu eryParams[key]) {
1243 anythingChanged = true;
1244 }
1245 }
1246 for (var key in this.queryParams) {
1247 if (anythingChanged || !(key in queryParams)) {
1248 anythingChanged = true;
1249 break;
1250 }
1251 }
1252 if (!anythingChanged) {
1253 return;
1254 }
1255 this._queryParamsUpdating = true;
1256 this.set('queryParams', copyOfQueryParams);
1257 this._queryParamsUpdating = false;
1258 }
1259 },
1260 __tailQueryParamsChanged: function(queryParams) {
1261 if (queryParams && this.route) {
1262 this.set('route.__queryParams', queryParams);
1263 }
1264 },
1265 __queryParamsChanged: function(changes) {
1266 if (!this.active || this._queryParamsUpdating) {
1267 return;
1268 }
1269 this.set('route.__' + changes.path, changes.value);
1270 },
1271 __resetProperties: function() {
1272 this._setActive(false);
1273 this._matched = null;
1274 },
1275 __tryToMatch: function() {
1276 if (!this.route) {
1277 return;
1278 }
1279 var path = this.route.path;
1280 var pattern = this.pattern;
1281 if (!pattern) {
1282 return;
1283 }
1284 if (!path) {
1285 this.__resetProperties();
1286 return;
1287 }
1288 var remainingPieces = path.split('/');
1289 var patternPieces = pattern.split('/');
1290 var matched = [];
1291 var namedMatches = {};
1292 for (var i = 0; i < patternPieces.length; i++) {
1293 var patternPiece = patternPieces[i];
1294 if (!patternPiece && patternPiece !== '') {
1295 break;
1296 }
1297 var pathPiece = remainingPieces.shift();
1298 if (!pathPiece && pathPiece !== '') {
1299 this.__resetProperties();
1300 return;
1301 }
1302 matched.push(pathPiece);
1303 if (patternPiece.charAt(0) == ':') {
1304 namedMatches[patternPiece.slice(1)] = pathPiece;
1305 } else if (patternPiece !== pathPiece) {
1306 this.__resetProperties();
1307 return;
1308 }
1309 }
1310 this._matched = matched.join('/');
1311 var propertyUpdates = {};
1312 if (!this.active) {
1313 propertyUpdates.active = true;
1314 }
1315 var tailPrefix = this.route.prefix + this._matched;
1316 var tailPath = remainingPieces.join('/');
1317 if (remainingPieces.length > 0) {
1318 tailPath = '/' + tailPath;
1319 }
1320 if (!this.tail || this.tail.prefix !== tailPrefix || this.tail.path !== tail Path) {
1321 propertyUpdates.tail = {
1322 prefix: tailPrefix,
1323 path: tailPath,
1324 __queryParams: this.route.__queryParams
1325 };
1326 }
1327 propertyUpdates.data = namedMatches;
1328 this._dataInUrl = {};
1329 for (var key in namedMatches) {
1330 this._dataInUrl[key] = namedMatches[key];
1331 }
1332 this.__setMulti(propertyUpdates);
1333 },
1334 __tailPathChanged: function() {
1335 if (!this.active) {
1336 return;
1337 }
1338 var tailPath = this.tail.path;
1339 var newPath = this._matched;
1340 if (tailPath) {
1341 if (tailPath.charAt(0) !== '/') {
1342 tailPath = '/' + tailPath;
1343 }
1344 newPath += tailPath;
1345 }
1346 this.set('route.path', newPath);
1347 },
1348 __updatePathOnDataChange: function() {
1349 if (!this.route || !this.active) {
1350 return;
1351 }
1352 var newPath = this.__getLink({});
1353 var oldPath = this.__getLink(this._dataInUrl);
1354 if (newPath === oldPath) {
1355 return;
1356 }
1357 this.set('route.path', newPath);
1358 },
1359 __getLink: function(overrideValues) {
1360 var values = {
1361 tail: null
1362 };
1363 for (var key in this.data) {
1364 values[key] = this.data[key];
1365 }
1366 for (var key in overrideValues) {
1367 values[key] = overrideValues[key];
1368 }
1369 var patternPieces = this.pattern.split('/');
1370 var interp = patternPieces.map(function(value) {
1371 if (value[0] == ':') {
1372 value = values[value.slice(1)];
1373 }
1374 return value;
1375 }, this);
1376 if (values.tail && values.tail.path) {
1377 if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
1378 interp.push(values.tail.path.slice(1));
1379 } else {
1380 interp.push(values.tail.path);
1381 }
1382 }
1383 return interp.join('/');
1384 },
1385 __setMulti: function(setObj) {
1386 for (var property in setObj) {
1387 this._propertySetter(property, setObj[property]);
1388 }
1389 for (var property in setObj) {
1390 this._pathEffector(property, this[property]);
1391 this._notifyPathUp(property, this[property]);
1392 }
1393 }
1394 });
1395
1396 Polymer({
1397 is: 'iron-media-query',
1398 properties: {
1399 queryMatches: {
1400 type: Boolean,
1401 value: false,
1402 readOnly: true,
1403 notify: true
1404 },
1405 query: {
1406 type: String,
1407 observer: 'queryChanged'
1408 },
1409 full: {
1410 type: Boolean,
1411 value: false
1412 },
1413 _boundMQHandler: {
1414 value: function() {
1415 return this.queryHandler.bind(this);
1416 }
1417 },
1418 _mq: {
1419 value: null
1420 }
1421 },
1422 attached: function() {
1423 this.style.display = 'none';
1424 this.queryChanged();
1425 },
1426 detached: function() {
1427 this._remove();
1428 },
1429 _add: function() {
1430 if (this._mq) {
1431 this._mq.addListener(this._boundMQHandler);
1432 }
1433 },
1434 _remove: function() {
1435 if (this._mq) {
1436 this._mq.removeListener(this._boundMQHandler);
1437 }
1438 this._mq = null;
1439 },
1440 queryChanged: function() {
1441 this._remove();
1442 var query = this.query;
1443 if (!query) {
1444 return;
1445 }
1446 if (!this.full && query[0] !== '(') {
1447 query = '(' + query + ')';
1448 }
1449 this._mq = window.matchMedia(query);
1450 this._add();
1451 this.queryHandler(this._mq);
1452 },
1453 queryHandler: function(mq) {
1454 this._setQueryMatches(mq.matches);
1455 }
1456 });
1457
1458 Polymer.IronResizableBehavior = {
1459 properties: {
1460 _parentResizable: {
1461 type: Object,
1462 observer: '_parentResizableChanged'
1463 },
1464 _notifyingDescendant: {
1465 type: Boolean,
1466 value: false
1467 }
1468 },
1469 listeners: {
1470 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
1471 },
1472 created: function() {
1473 this._interestedResizables = [];
1474 this._boundNotifyResize = this.notifyResize.bind(this);
1475 },
1476 attached: function() {
1477 this.fire('iron-request-resize-notifications', null, {
1478 node: this,
1479 bubbles: true,
1480 cancelable: true
1481 });
1482 if (!this._parentResizable) {
1483 window.addEventListener('resize', this._boundNotifyResize);
1484 this.notifyResize();
1485 }
1486 },
1487 detached: function() {
1488 if (this._parentResizable) {
1489 this._parentResizable.stopResizeNotificationsFor(this);
1490 } else {
1491 window.removeEventListener('resize', this._boundNotifyResize);
1492 }
1493 this._parentResizable = null;
1494 },
1495 notifyResize: function() {
1496 if (!this.isAttached) {
1497 return;
1498 }
1499 this._interestedResizables.forEach(function(resizable) {
1500 if (this.resizerShouldNotify(resizable)) {
1501 this._notifyDescendant(resizable);
1502 }
1503 }, this);
1504 this._fireResize();
1505 },
1506 assignParentResizable: function(parentResizable) {
1507 this._parentResizable = parentResizable;
1508 },
1509 stopResizeNotificationsFor: function(target) {
1510 var index = this._interestedResizables.indexOf(target);
1511 if (index > -1) {
1512 this._interestedResizables.splice(index, 1);
1513 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
1514 }
1515 },
1516 resizerShouldNotify: function(element) {
1517 return true;
1518 },
1519 _onDescendantIronResize: function(event) {
1520 if (this._notifyingDescendant) {
1521 event.stopPropagation();
1522 return;
1523 }
1524 if (!Polymer.Settings.useShadow) {
1525 this._fireResize();
1526 }
1527 },
1528 _fireResize: function() {
1529 this.fire('iron-resize', null, {
1530 node: this,
1531 bubbles: false
1532 });
1533 },
1534 _onIronRequestResizeNotifications: function(event) {
1535 var target = event.path ? event.path[0] : event.target;
1536 if (target === this) {
1537 return;
1538 }
1539 if (this._interestedResizables.indexOf(target) === -1) {
1540 this._interestedResizables.push(target);
1541 this.listen(target, 'iron-resize', '_onDescendantIronResize');
1542 }
1543 target.assignParentResizable(this);
1544 this._notifyDescendant(target);
1545 event.stopPropagation();
1546 },
1547 _parentResizableChanged: function(parentResizable) {
1548 if (parentResizable) {
1549 window.removeEventListener('resize', this._boundNotifyResize);
1550 }
1551 },
1552 _notifyDescendant: function(descendant) {
1553 if (!this.isAttached) {
1554 return;
1555 }
1556 this._notifyingDescendant = true;
1557 descendant.notifyResize();
1558 this._notifyingDescendant = false;
1559 }
1560 };
1561
1562 Polymer.IronSelection = function(selectCallback) {
1563 this.selection = [];
1564 this.selectCallback = selectCallback;
1565 };
1566
1567 Polymer.IronSelection.prototype = {
1568 get: function() {
1569 return this.multi ? this.selection.slice() : this.selection[0];
1570 },
1571 clear: function(excludes) {
1572 this.selection.slice().forEach(function(item) {
1573 if (!excludes || excludes.indexOf(item) < 0) {
1574 this.setItemSelected(item, false);
1575 }
1576 }, this);
1577 },
1578 isSelected: function(item) {
1579 return this.selection.indexOf(item) >= 0;
1580 },
1581 setItemSelected: function(item, isSelected) {
1582 if (item != null) {
1583 if (isSelected !== this.isSelected(item)) {
1584 if (isSelected) {
1585 this.selection.push(item);
1586 } else {
1587 var i = this.selection.indexOf(item);
1588 if (i >= 0) {
1589 this.selection.splice(i, 1);
1590 }
1591 }
1592 if (this.selectCallback) {
1593 this.selectCallback(item, isSelected);
1594 }
1595 }
1596 }
1597 },
1598 select: function(item) {
1599 if (this.multi) {
1600 this.toggle(item);
1601 } else if (this.get() !== item) {
1602 this.setItemSelected(this.get(), false);
1603 this.setItemSelected(item, true);
1604 }
1605 },
1606 toggle: function(item) {
1607 this.setItemSelected(item, !this.isSelected(item));
1608 }
1609 };
1610
1611 Polymer.IronSelectableBehavior = {
1612 properties: {
1613 attrForSelected: {
1614 type: String,
1615 value: null
1616 },
1617 selected: {
1618 type: String,
1619 notify: true
1620 },
1621 selectedItem: {
1622 type: Object,
1623 readOnly: true,
1624 notify: true
1625 },
1626 activateEvent: {
1627 type: String,
1628 value: 'tap',
1629 observer: '_activateEventChanged'
1630 },
1631 selectable: String,
1632 selectedClass: {
1633 type: String,
1634 value: 'iron-selected'
1635 },
1636 selectedAttribute: {
1637 type: String,
1638 value: null
1639 },
1640 fallbackSelection: {
1641 type: String,
1642 value: null
1643 },
1644 items: {
1645 type: Array,
1646 readOnly: true,
1647 notify: true,
1648 value: function() {
1649 return [];
1650 }
1651 },
1652 _excludedLocalNames: {
1653 type: Object,
1654 value: function() {
1655 return {
1656 template: 1
1657 };
1658 }
1659 }
1660 },
1661 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec ted)', '_checkFallback(fallbackSelection)' ],
1662 created: function() {
1663 this._bindFilterItem = this._filterItem.bind(this);
1664 this._selection = new Polymer.IronSelection(this._applySelection.bind(this)) ;
1665 },
1666 attached: function() {
1667 this._observer = this._observeItems(this);
1668 this._updateItems();
1669 if (!this._shouldUpdateSelection) {
1670 this._updateSelected();
1671 }
1672 this._addListener(this.activateEvent);
1673 },
1674 detached: function() {
1675 if (this._observer) {
1676 Polymer.dom(this).unobserveNodes(this._observer);
1677 }
1678 this._removeListener(this.activateEvent);
1679 },
1680 indexOf: function(item) {
1681 return this.items.indexOf(item);
1682 },
1683 select: function(value) {
1684 this.selected = value;
1685 },
1686 selectPrevious: function() {
1687 var length = this.items.length;
1688 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt h;
1689 this.selected = this._indexToValue(index);
1690 },
1691 selectNext: function() {
1692 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len gth;
1693 this.selected = this._indexToValue(index);
1694 },
1695 selectIndex: function(index) {
1696 this.select(this._indexToValue(index));
1697 },
1698 forceSynchronousItemUpdate: function() {
1699 this._updateItems();
1700 },
1701 get _shouldUpdateSelection() {
1702 return this.selected != null;
1703 },
1704 _checkFallback: function() {
1705 if (this._shouldUpdateSelection) {
1706 this._updateSelected();
1707 }
1708 },
1709 _addListener: function(eventName) {
1710 this.listen(this, eventName, '_activateHandler');
1711 },
1712 _removeListener: function(eventName) {
1713 this.unlisten(this, eventName, '_activateHandler');
1714 },
1715 _activateEventChanged: function(eventName, old) {
1716 this._removeListener(old);
1717 this._addListener(eventName);
1718 },
1719 _updateItems: function() {
1720 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '* ');
1721 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
1722 this._setItems(nodes);
1723 },
1724 _updateAttrForSelected: function() {
1725 if (this._shouldUpdateSelection) {
1726 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
1727 }
1728 },
1729 _updateSelected: function() {
1730 this._selectSelected(this.selected);
1731 },
1732 _selectSelected: function(selected) {
1733 this._selection.select(this._valueToItem(this.selected));
1734 if (this.fallbackSelection && this.items.length && this._selection.get() === undefined) {
1735 this.selected = this.fallbackSelection;
1736 }
1737 },
1738 _filterItem: function(node) {
1739 return !this._excludedLocalNames[node.localName];
1740 },
1741 _valueToItem: function(value) {
1742 return value == null ? null : this.items[this._valueToIndex(value)];
1743 },
1744 _valueToIndex: function(value) {
1745 if (this.attrForSelected) {
1746 for (var i = 0, item; item = this.items[i]; i++) {
1747 if (this._valueForItem(item) == value) {
1748 return i;
1749 }
1750 }
1751 } else {
1752 return Number(value);
1753 }
1754 },
1755 _indexToValue: function(index) {
1756 if (this.attrForSelected) {
1757 var item = this.items[index];
1758 if (item) {
1759 return this._valueForItem(item);
1760 }
1761 } else {
1762 return index;
1763 }
1764 },
1765 _valueForItem: function(item) {
1766 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
1767 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe lected);
1768 },
1769 _applySelection: function(item, isSelected) {
1770 if (this.selectedClass) {
1771 this.toggleClass(this.selectedClass, isSelected, item);
1772 }
1773 if (this.selectedAttribute) {
1774 this.toggleAttribute(this.selectedAttribute, isSelected, item);
1775 }
1776 this._selectionChange();
1777 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {
1778 item: item
1779 });
1780 },
1781 _selectionChange: function() {
1782 this._setSelectedItem(this._selection.get());
1783 },
1784 _observeItems: function(node) {
1785 return Polymer.dom(node).observeNodes(function(mutation) {
1786 this._updateItems();
1787 if (this._shouldUpdateSelection) {
1788 this._updateSelected();
1789 }
1790 this.fire('iron-items-changed', mutation, {
1791 bubbles: false,
1792 cancelable: false
1793 });
1794 });
1795 },
1796 _activateHandler: function(e) {
1797 var t = e.target;
1798 var items = this.items;
1799 while (t && t != this) {
1800 var i = items.indexOf(t);
1801 if (i >= 0) {
1802 var value = this._indexToValue(i);
1803 this._itemActivate(value, t);
1804 return;
1805 }
1806 t = t.parentNode;
1807 }
1808 },
1809 _itemActivate: function(value, item) {
1810 if (!this.fire('iron-activate', {
1811 selected: value,
1812 item: item
1813 }, {
1814 cancelable: true
1815 }).defaultPrevented) {
1816 this.select(value);
1817 }
1818 }
1819 };
1820
1821 Polymer({
1822 is: 'iron-pages',
1823 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronSelectableBehavior ],
1824 properties: {
1825 activateEvent: {
1826 type: String,
1827 value: null
1828 }
1829 },
1830 observers: [ '_selectedPageChanged(selected)' ],
1831 _selectedPageChanged: function(selected, old) {
1832 this.async(this.notifyResize);
1833 }
1834 });
1835
1836 (function() {
1837 'use strict';
1838 var KEY_IDENTIFIER = {
1839 'U+0008': 'backspace',
1840 'U+0009': 'tab',
1841 'U+001B': 'esc',
1842 'U+0020': 'space',
1843 'U+007F': 'del'
1844 };
1845 var KEY_CODE = {
1846 8: 'backspace',
1847 9: 'tab',
1848 13: 'enter',
1849 27: 'esc',
1850 33: 'pageup',
1851 34: 'pagedown',
1852 35: 'end',
1853 36: 'home',
1854 32: 'space',
1855 37: 'left',
1856 38: 'up',
1857 39: 'right',
1858 40: 'down',
1859 46: 'del',
1860 106: '*'
1861 };
1862 var MODIFIER_KEYS = {
1863 shift: 'shiftKey',
1864 ctrl: 'ctrlKey',
1865 alt: 'altKey',
1866 meta: 'metaKey'
1867 };
1868 var KEY_CHAR = /[a-z0-9*]/;
1869 var IDENT_CHAR = /U\+/;
1870 var ARROW_KEY = /^arrow/;
1871 var SPACE_KEY = /^space(bar)?/;
1872 var ESC_KEY = /^escape$/;
1873 function transformKey(key, noSpecialChars) {
1874 var validKey = '';
1875 if (key) {
1876 var lKey = key.toLowerCase();
1877 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
1878 validKey = 'space';
1879 } else if (ESC_KEY.test(lKey)) {
1880 validKey = 'esc';
1881 } else if (lKey.length == 1) {
1882 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
1883 validKey = lKey;
1884 }
1885 } else if (ARROW_KEY.test(lKey)) {
1886 validKey = lKey.replace('arrow', '');
1887 } else if (lKey == 'multiply') {
1888 validKey = '*';
1889 } else {
1890 validKey = lKey;
1891 }
1892 }
1893 return validKey;
1894 }
1895 function transformKeyIdentifier(keyIdent) {
1896 var validKey = '';
1897 if (keyIdent) {
1898 if (keyIdent in KEY_IDENTIFIER) {
1899 validKey = KEY_IDENTIFIER[keyIdent];
1900 } else if (IDENT_CHAR.test(keyIdent)) {
1901 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1902 validKey = String.fromCharCode(keyIdent).toLowerCase();
1903 } else {
1904 validKey = keyIdent.toLowerCase();
1905 }
1906 }
1907 return validKey;
1908 }
1909 function transformKeyCode(keyCode) {
1910 var validKey = '';
1911 if (Number(keyCode)) {
1912 if (keyCode >= 65 && keyCode <= 90) {
1913 validKey = String.fromCharCode(32 + keyCode);
1914 } else if (keyCode >= 112 && keyCode <= 123) {
1915 validKey = 'f' + (keyCode - 112);
1916 } else if (keyCode >= 48 && keyCode <= 57) {
1917 validKey = String(keyCode - 48);
1918 } else if (keyCode >= 96 && keyCode <= 105) {
1919 validKey = String(keyCode - 96);
1920 } else {
1921 validKey = KEY_CODE[keyCode];
1922 }
1923 }
1924 return validKey;
1925 }
1926 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1927 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier( keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
1928 }
1929 function keyComboMatchesEvent(keyCombo, event) {
1930 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1931 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event .altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey);
1932 }
1933 function parseKeyComboString(keyComboString) {
1934 if (keyComboString.length === 1) {
1935 return {
1936 combo: keyComboString,
1937 key: keyComboString,
1938 event: 'keydown'
1939 };
1940 }
1941 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar t) {
1942 var eventParts = keyComboPart.split(':');
1943 var keyName = eventParts[0];
1944 var event = eventParts[1];
1945 if (keyName in MODIFIER_KEYS) {
1946 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1947 parsedKeyCombo.hasModifiers = true;
1948 } else {
1949 parsedKeyCombo.key = keyName;
1950 parsedKeyCombo.event = event || 'keydown';
1951 }
1952 return parsedKeyCombo;
1953 }, {
1954 combo: keyComboString.split(':').shift()
1955 });
1956 }
1957 function parseEventString(eventString) {
1958 return eventString.trim().split(' ').map(function(keyComboString) {
1959 return parseKeyComboString(keyComboString);
1960 });
1961 }
1962 Polymer.IronA11yKeysBehavior = {
1936 properties: { 1963 properties: {
1937 /** 1964 keyEventTarget: {
1938 * A model representing the deserialized path through the route tree, as
1939 * well as the current queryParams.
1940 *
1941 * A route object is the kernel of the routing system. It is intended to
1942 * be fed into consuming elements such as `app-route`.
1943 *
1944 * @type {?Object}
1945 */
1946 route: {
1947 type: Object, 1965 type: Object,
1948 notify: true 1966 value: function() {
1949 }, 1967 return this;
1950
1951 /**
1952 * A set of key/value pairs that are universally accessible to branches of
1953 * the route tree.
1954 *
1955 * @type {?Object}
1956 */
1957 queryParams: {
1958 type: Object,
1959 notify: true
1960 },
1961
1962 /**
1963 * The serialized path through the route tree. This corresponds to the
1964 * `window.location.pathname` value, and will update to reflect changes
1965 * to that value.
1966 */
1967 path: {
1968 type: String,
1969 notify: true,
1970 }
1971 },
1972
1973 observers: [
1974 '_locationChanged(path, queryParams)',
1975 '_routeChanged(route.prefix, route.path)',
1976 '_routeQueryParamsChanged(route.__queryParams)'
1977 ],
1978
1979 created: function() {
1980 this.linkPaths('route.__queryParams', 'queryParams');
1981 this.linkPaths('queryParams', 'route.__queryParams');
1982 },
1983
1984 /**
1985 * Handler called when the path or queryParams change.
1986 */
1987 _locationChanged: function() {
1988 if (this.route &&
1989 this.route.path === this.path &&
1990 this.queryParams === this.route.__queryParams) {
1991 return;
1992 }
1993 this.route = {
1994 prefix: '',
1995 path: this.path,
1996 __queryParams: this.queryParams
1997 };
1998 },
1999
2000 /**
2001 * Handler called when the route prefix and route path change.
2002 */
2003 _routeChanged: function() {
2004 if (!this.route) {
2005 return;
2006 }
2007
2008 this.path = this.route.prefix + this.route.path;
2009 },
2010
2011 /**
2012 * Handler called when the route queryParams change.
2013 *
2014 * @param {Object} queryParams A set of key/value pairs that are
2015 * universally accessible to branches of the route tree.
2016 */
2017 _routeQueryParamsChanged: function(queryParams) {
2018 if (!this.route) {
2019 return;
2020 }
2021 this.queryParams = queryParams;
2022 }
2023 };
2024 'use strict';
2025
2026 Polymer({
2027 is: 'app-location',
2028
2029 properties: {
2030 /**
2031 * A model representing the deserialized path through the route tree, as
2032 * well as the current queryParams.
2033 */
2034 route: {
2035 type: Object,
2036 notify: true
2037 },
2038
2039 /**
2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in
2041 * alternative to the `path`. For example, if deploying an app to a stat ic
2042 * web server (e.g., Github Pages) - where one does not have control ove r
2043 * server-side routing - it is usually a better experience to use the ha sh
2044 * to represent paths through one's app.
2045 *
2046 * When this property is set to true, the `hash` will be used in place o f
2047
2048 * the `path` for generating a `route`.
2049 */
2050 useHashAsPath: {
2051 type: Boolean,
2052 value: false
2053 },
2054
2055 /**
2056 * A regexp that defines the set of URLs that should be considered part
2057 * of this web app.
2058 *
2059 * Clicking on a link that matches this regex won't result in a full pag e
2060 * navigation, but will instead just update the URL state in place.
2061 *
2062 * This regexp is given everything after the origin in an absolute
2063 * URL. So to match just URLs that start with /search/ do:
2064 * url-space-regex="^/search/"
2065 *
2066 * @type {string|RegExp}
2067 */
2068 urlSpaceRegex: {
2069 type: String,
2070 notify: true
2071 },
2072
2073 /**
2074 * A set of key/value pairs that are universally accessible to branches
2075 * of the route tree.
2076 */
2077 __queryParams: {
2078 type: Object
2079 },
2080
2081 /**
2082 * The pathname component of the current URL.
2083 */
2084 __path: {
2085 type: String
2086 },
2087
2088 /**
2089 * The query string portion of the current URL.
2090 */
2091 __query: {
2092 type: String
2093 },
2094
2095 /**
2096 * The hash portion of the current URL.
2097 */
2098 __hash: {
2099 type: String
2100 },
2101
2102 /**
2103 * The route path, which will be either the hash or the path, depending
2104 * on useHashAsPath.
2105 */
2106 path: {
2107 type: String,
2108 observer: '__onPathChanged'
2109 } 1968 }
2110 }, 1969 },
2111 1970 stopKeyboardEventPropagation: {
2112 behaviors: [Polymer.AppRouteConverterBehavior],
2113
2114 observers: [
2115 '__computeRoutePath(useHashAsPath, __hash, __path)'
2116 ],
2117
2118 __computeRoutePath: function() {
2119 this.path = this.useHashAsPath ? this.__hash : this.__path;
2120 },
2121
2122 __onPathChanged: function() {
2123 if (!this._readied) {
2124 return;
2125 }
2126
2127 if (this.useHashAsPath) {
2128 this.__hash = this.path;
2129 } else {
2130 this.__path = this.path;
2131 }
2132 }
2133 });
2134 'use strict';
2135
2136 Polymer({
2137 is: 'app-route',
2138
2139 properties: {
2140 /**
2141 * The URL component managed by this element.
2142 */
2143 route: {
2144 type: Object,
2145 notify: true
2146 },
2147
2148 /**
2149 * The pattern of slash-separated segments to match `path` against.
2150 *
2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar"
2152 * but not "/foobar".
2153 *
2154 * Path segments like `/:named` are mapped to properties on the `data` obj ect.
2155 */
2156 pattern: {
2157 type: String
2158 },
2159
2160 /**
2161 * The parameterized values that are extracted from the route as
2162 * described by `pattern`.
2163 */
2164 data: {
2165 type: Object,
2166 value: function() {return {};},
2167 notify: true
2168 },
2169
2170 /**
2171 * @type {?Object}
2172 */
2173 queryParams: {
2174 type: Object,
2175 value: function() {
2176 return {};
2177 },
2178 notify: true
2179 },
2180
2181 /**
2182 * The part of `path` NOT consumed by `pattern`.
2183 */
2184 tail: {
2185 type: Object,
2186 value: function() {return {path: null, prefix: null, __queryParams: null };},
2187 notify: true
2188 },
2189
2190 active: {
2191 type: Boolean,
2192 notify: true,
2193 readOnly: true
2194 },
2195
2196 _queryParamsUpdating: {
2197 type: Boolean, 1971 type: Boolean,
2198 value: false 1972 value: false
2199 }, 1973 },
2200 /** 1974 _boundKeyHandlers: {
2201 * @type {?string}
2202 */
2203 _matched: {
2204 type: String,
2205 value: ''
2206 }
2207 },
2208
2209 observers: [
2210 '__tryToMatch(route.path, pattern)',
2211 '__updatePathOnDataChange(data.*)',
2212 '__tailPathChanged(tail.path)',
2213 '__routeQueryParamsChanged(route.__queryParams)',
2214 '__tailQueryParamsChanged(tail.__queryParams)',
2215 '__queryParamsChanged(queryParams.*)'
2216 ],
2217
2218 created: function() {
2219 this.linkPaths('route.__queryParams', 'tail.__queryParams');
2220 this.linkPaths('tail.__queryParams', 'route.__queryParams');
2221 },
2222
2223 /**
2224 * Deal with the query params object being assigned to wholesale.
2225 * @export
2226 */
2227 __routeQueryParamsChanged: function(queryParams) {
2228 if (queryParams && this.tail) {
2229 this.set('tail.__queryParams', queryParams);
2230
2231 if (!this.active || this._queryParamsUpdating) {
2232 return;
2233 }
2234
2235 // Copy queryParams and track whether there are any differences compared
2236 // to the existing query params.
2237 var copyOfQueryParams = {};
2238 var anythingChanged = false;
2239 for (var key in queryParams) {
2240 copyOfQueryParams[key] = queryParams[key];
2241 if (anythingChanged ||
2242 !this.queryParams ||
2243 queryParams[key] !== this.queryParams[key]) {
2244 anythingChanged = true;
2245 }
2246 }
2247 // Need to check whether any keys were deleted
2248 for (var key in this.queryParams) {
2249 if (anythingChanged || !(key in queryParams)) {
2250 anythingChanged = true;
2251 break;
2252 }
2253 }
2254
2255 if (!anythingChanged) {
2256 return;
2257 }
2258 this._queryParamsUpdating = true;
2259 this.set('queryParams', copyOfQueryParams);
2260 this._queryParamsUpdating = false;
2261 }
2262 },
2263
2264 /**
2265 * @export
2266 */
2267 __tailQueryParamsChanged: function(queryParams) {
2268 if (queryParams && this.route) {
2269 this.set('route.__queryParams', queryParams);
2270 }
2271 },
2272
2273 /**
2274 * @export
2275 */
2276 __queryParamsChanged: function(changes) {
2277 if (!this.active || this._queryParamsUpdating) {
2278 return;
2279 }
2280
2281 this.set('route.__' + changes.path, changes.value);
2282 },
2283
2284 __resetProperties: function() {
2285 this._setActive(false);
2286 this._matched = null;
2287 //this.tail = { path: null, prefix: null, queryParams: null };
2288 //this.data = {};
2289 },
2290
2291 /**
2292 * @export
2293 */
2294 __tryToMatch: function() {
2295 if (!this.route) {
2296 return;
2297 }
2298 var path = this.route.path;
2299 var pattern = this.pattern;
2300 if (!pattern) {
2301 return;
2302 }
2303
2304 if (!path) {
2305 this.__resetProperties();
2306 return;
2307 }
2308
2309 var remainingPieces = path.split('/');
2310 var patternPieces = pattern.split('/');
2311
2312 var matched = [];
2313 var namedMatches = {};
2314
2315 for (var i=0; i < patternPieces.length; i++) {
2316 var patternPiece = patternPieces[i];
2317 if (!patternPiece && patternPiece !== '') {
2318 break;
2319 }
2320 var pathPiece = remainingPieces.shift();
2321
2322 // We don't match this path.
2323 if (!pathPiece && pathPiece !== '') {
2324 this.__resetProperties();
2325 return;
2326 }
2327 matched.push(pathPiece);
2328
2329 if (patternPiece.charAt(0) == ':') {
2330 namedMatches[patternPiece.slice(1)] = pathPiece;
2331 } else if (patternPiece !== pathPiece) {
2332 this.__resetProperties();
2333 return;
2334 }
2335 }
2336
2337 this._matched = matched.join('/');
2338
2339 // Properties that must be updated atomically.
2340 var propertyUpdates = {};
2341
2342 //this.active
2343 if (!this.active) {
2344 propertyUpdates.active = true;
2345 }
2346
2347 // this.tail
2348 var tailPrefix = this.route.prefix + this._matched;
2349 var tailPath = remainingPieces.join('/');
2350 if (remainingPieces.length > 0) {
2351 tailPath = '/' + tailPath;
2352 }
2353 if (!this.tail ||
2354 this.tail.prefix !== tailPrefix ||
2355 this.tail.path !== tailPath) {
2356 propertyUpdates.tail = {
2357 prefix: tailPrefix,
2358 path: tailPath,
2359 __queryParams: this.route.__queryParams
2360 };
2361 }
2362
2363 // this.data
2364 propertyUpdates.data = namedMatches;
2365 this._dataInUrl = {};
2366 for (var key in namedMatches) {
2367 this._dataInUrl[key] = namedMatches[key];
2368 }
2369
2370 this.__setMulti(propertyUpdates);
2371 },
2372
2373 /**
2374 * @export
2375 */
2376 __tailPathChanged: function() {
2377 if (!this.active) {
2378 return;
2379 }
2380 var tailPath = this.tail.path;
2381 var newPath = this._matched;
2382 if (tailPath) {
2383 if (tailPath.charAt(0) !== '/') {
2384 tailPath = '/' + tailPath;
2385 }
2386 newPath += tailPath;
2387 }
2388 this.set('route.path', newPath);
2389 },
2390
2391 /**
2392 * @export
2393 */
2394 __updatePathOnDataChange: function() {
2395 if (!this.route || !this.active) {
2396 return;
2397 }
2398 var newPath = this.__getLink({});
2399 var oldPath = this.__getLink(this._dataInUrl);
2400 if (newPath === oldPath) {
2401 return;
2402 }
2403 this.set('route.path', newPath);
2404 },
2405
2406 __getLink: function(overrideValues) {
2407 var values = {tail: null};
2408 for (var key in this.data) {
2409 values[key] = this.data[key];
2410 }
2411 for (var key in overrideValues) {
2412 values[key] = overrideValues[key];
2413 }
2414 var patternPieces = this.pattern.split('/');
2415 var interp = patternPieces.map(function(value) {
2416 if (value[0] == ':') {
2417 value = values[value.slice(1)];
2418 }
2419 return value;
2420 }, this);
2421 if (values.tail && values.tail.path) {
2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
2423 interp.push(values.tail.path.slice(1));
2424 } else {
2425 interp.push(values.tail.path);
2426 }
2427 }
2428 return interp.join('/');
2429 },
2430
2431 __setMulti: function(setObj) {
2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at
2433 // internal data structures. I would not advise that you copy this
2434 // example.
2435 //
2436 // In the future this will be a feature of Polymer itself.
2437 // See: https://github.com/Polymer/polymer/issues/3640
2438 //
2439 // Hacking around with private methods like this is juggling footguns,
2440 // and is likely to have unexpected and unsupported rough edges.
2441 //
2442 // Be ye so warned.
2443 for (var property in setObj) {
2444 this._propertySetter(property, setObj[property]);
2445 }
2446
2447 for (var property in setObj) {
2448 this._pathEffector(property, this[property]);
2449 this._notifyPathUp(property, this[property]);
2450 }
2451 }
2452 });
2453 Polymer({
2454
2455 is: 'iron-media-query',
2456
2457 properties: {
2458
2459 /**
2460 * The Boolean return value of the media query.
2461 */
2462 queryMatches: {
2463 type: Boolean,
2464 value: false,
2465 readOnly: true,
2466 notify: true
2467 },
2468
2469 /**
2470 * The CSS media query to evaluate.
2471 */
2472 query: {
2473 type: String,
2474 observer: 'queryChanged'
2475 },
2476
2477 /**
2478 * If true, the query attribute is assumed to be a complete media query
2479 * string rather than a single media feature.
2480 */
2481 full: {
2482 type: Boolean,
2483 value: false
2484 },
2485
2486 /**
2487 * @type {function(MediaQueryList)}
2488 */
2489 _boundMQHandler: {
2490 value: function() {
2491 return this.queryHandler.bind(this);
2492 }
2493 },
2494
2495 /**
2496 * @type {MediaQueryList}
2497 */
2498 _mq: {
2499 value: null
2500 }
2501 },
2502
2503 attached: function() {
2504 this.style.display = 'none';
2505 this.queryChanged();
2506 },
2507
2508 detached: function() {
2509 this._remove();
2510 },
2511
2512 _add: function() {
2513 if (this._mq) {
2514 this._mq.addListener(this._boundMQHandler);
2515 }
2516 },
2517
2518 _remove: function() {
2519 if (this._mq) {
2520 this._mq.removeListener(this._boundMQHandler);
2521 }
2522 this._mq = null;
2523 },
2524
2525 queryChanged: function() {
2526 this._remove();
2527 var query = this.query;
2528 if (!query) {
2529 return;
2530 }
2531 if (!this.full && query[0] !== '(') {
2532 query = '(' + query + ')';
2533 }
2534 this._mq = window.matchMedia(query);
2535 this._add();
2536 this.queryHandler(this._mq);
2537 },
2538
2539 queryHandler: function(mq) {
2540 this._setQueryMatches(mq.matches);
2541 }
2542
2543 });
2544 /**
2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
2546 * coordinate the flow of resize events between "resizers" (elements that cont rol the
2547 * size or hidden state of their children) and "resizables" (elements that nee d to be
2548 * notified when they are resized or un-hidden by their parents in order to ta ke
2549 * action on their new measurements).
2550 *
2551 * Elements that perform measurement should add the `IronResizableBehavior` be havior to
2552 * their element definition and listen for the `iron-resize` event on themselv es.
2553 * This event will be fired when they become showing after having been hidden,
2554 * when they are resized explicitly by another resizable, or when the window h as been
2555 * resized.
2556 *
2557 * Note, the `iron-resize` event is non-bubbling.
2558 *
2559 * @polymerBehavior Polymer.IronResizableBehavior
2560 * @demo demo/index.html
2561 **/
2562 Polymer.IronResizableBehavior = {
2563 properties: {
2564 /**
2565 * The closest ancestor element that implements `IronResizableBehavior`.
2566 */
2567 _parentResizable: {
2568 type: Object,
2569 observer: '_parentResizableChanged'
2570 },
2571
2572 /**
2573 * True if this element is currently notifying its descedant elements of
2574 * resize.
2575 */
2576 _notifyingDescendant: {
2577 type: Boolean,
2578 value: false
2579 }
2580 },
2581
2582 listeners: {
2583 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
2584 },
2585
2586 created: function() {
2587 // We don't really need property effects on these, and also we want them
2588 // to be created before the `_parentResizable` observer fires:
2589 this._interestedResizables = [];
2590 this._boundNotifyResize = this.notifyResize.bind(this);
2591 },
2592
2593 attached: function() {
2594 this.fire('iron-request-resize-notifications', null, {
2595 node: this,
2596 bubbles: true,
2597 cancelable: true
2598 });
2599
2600 if (!this._parentResizable) {
2601 window.addEventListener('resize', this._boundNotifyResize);
2602 this.notifyResize();
2603 }
2604 },
2605
2606 detached: function() {
2607 if (this._parentResizable) {
2608 this._parentResizable.stopResizeNotificationsFor(this);
2609 } else {
2610 window.removeEventListener('resize', this._boundNotifyResize);
2611 }
2612
2613 this._parentResizable = null;
2614 },
2615
2616 /**
2617 * Can be called to manually notify a resizable and its descendant
2618 * resizables of a resize change.
2619 */
2620 notifyResize: function() {
2621 if (!this.isAttached) {
2622 return;
2623 }
2624
2625 this._interestedResizables.forEach(function(resizable) {
2626 if (this.resizerShouldNotify(resizable)) {
2627 this._notifyDescendant(resizable);
2628 }
2629 }, this);
2630
2631 this._fireResize();
2632 },
2633
2634 /**
2635 * Used to assign the closest resizable ancestor to this resizable
2636 * if the ancestor detects a request for notifications.
2637 */
2638 assignParentResizable: function(parentResizable) {
2639 this._parentResizable = parentResizable;
2640 },
2641
2642 /**
2643 * Used to remove a resizable descendant from the list of descendants
2644 * that should be notified of a resize change.
2645 */
2646 stopResizeNotificationsFor: function(target) {
2647 var index = this._interestedResizables.indexOf(target);
2648
2649 if (index > -1) {
2650 this._interestedResizables.splice(index, 1);
2651 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
2652 }
2653 },
2654
2655 /**
2656 * This method can be overridden to filter nested elements that should or
2657 * should not be notified by the current element. Return true if an element
2658 * should be notified, or false if it should not be notified.
2659 *
2660 * @param {HTMLElement} element A candidate descendant element that
2661 * implements `IronResizableBehavior`.
2662 * @return {boolean} True if the `element` should be notified of resize.
2663 */
2664 resizerShouldNotify: function(element) { return true; },
2665
2666 _onDescendantIronResize: function(event) {
2667 if (this._notifyingDescendant) {
2668 event.stopPropagation();
2669 return;
2670 }
2671
2672 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
2673 // otherwise non-bubbling event "just work." We do it manually here for
2674 // the case where Polymer is not using shadow roots for whatever reason:
2675 if (!Polymer.Settings.useShadow) {
2676 this._fireResize();
2677 }
2678 },
2679
2680 _fireResize: function() {
2681 this.fire('iron-resize', null, {
2682 node: this,
2683 bubbles: false
2684 });
2685 },
2686
2687 _onIronRequestResizeNotifications: function(event) {
2688 var target = event.path ? event.path[0] : event.target;
2689
2690 if (target === this) {
2691 return;
2692 }
2693
2694 if (this._interestedResizables.indexOf(target) === -1) {
2695 this._interestedResizables.push(target);
2696 this.listen(target, 'iron-resize', '_onDescendantIronResize');
2697 }
2698
2699 target.assignParentResizable(this);
2700 this._notifyDescendant(target);
2701
2702 event.stopPropagation();
2703 },
2704
2705 _parentResizableChanged: function(parentResizable) {
2706 if (parentResizable) {
2707 window.removeEventListener('resize', this._boundNotifyResize);
2708 }
2709 },
2710
2711 _notifyDescendant: function(descendant) {
2712 // NOTE(cdata): In IE10, attached is fired on children first, so it's
2713 // important not to notify them if the parent is not attached yet (or
2714 // else they will get redundantly notified when the parent attaches).
2715 if (!this.isAttached) {
2716 return;
2717 }
2718
2719 this._notifyingDescendant = true;
2720 descendant.notifyResize();
2721 this._notifyingDescendant = false;
2722 }
2723 };
2724 /**
2725 * @param {!Function} selectCallback
2726 * @constructor
2727 */
2728 Polymer.IronSelection = function(selectCallback) {
2729 this.selection = [];
2730 this.selectCallback = selectCallback;
2731 };
2732
2733 Polymer.IronSelection.prototype = {
2734
2735 /**
2736 * Retrieves the selected item(s).
2737 *
2738 * @method get
2739 * @returns Returns the selected item(s). If the multi property is true,
2740 * `get` will return an array, otherwise it will return
2741 * the selected item or undefined if there is no selection.
2742 */
2743 get: function() {
2744 return this.multi ? this.selection.slice() : this.selection[0];
2745 },
2746
2747 /**
2748 * Clears all the selection except the ones indicated.
2749 *
2750 * @method clear
2751 * @param {Array} excludes items to be excluded.
2752 */
2753 clear: function(excludes) {
2754 this.selection.slice().forEach(function(item) {
2755 if (!excludes || excludes.indexOf(item) < 0) {
2756 this.setItemSelected(item, false);
2757 }
2758 }, this);
2759 },
2760
2761 /**
2762 * Indicates if a given item is selected.
2763 *
2764 * @method isSelected
2765 * @param {*} item The item whose selection state should be checked.
2766 * @returns Returns true if `item` is selected.
2767 */
2768 isSelected: function(item) {
2769 return this.selection.indexOf(item) >= 0;
2770 },
2771
2772 /**
2773 * Sets the selection state for a given item to either selected or deselecte d.
2774 *
2775 * @method setItemSelected
2776 * @param {*} item The item to select.
2777 * @param {boolean} isSelected True for selected, false for deselected.
2778 */
2779 setItemSelected: function(item, isSelected) {
2780 if (item != null) {
2781 if (isSelected !== this.isSelected(item)) {
2782 // proceed to update selection only if requested state differs from cu rrent
2783 if (isSelected) {
2784 this.selection.push(item);
2785 } else {
2786 var i = this.selection.indexOf(item);
2787 if (i >= 0) {
2788 this.selection.splice(i, 1);
2789 }
2790 }
2791 if (this.selectCallback) {
2792 this.selectCallback(item, isSelected);
2793 }
2794 }
2795 }
2796 },
2797
2798 /**
2799 * Sets the selection state for a given item. If the `multi` property
2800 * is true, then the selected state of `item` will be toggled; otherwise
2801 * the `item` will be selected.
2802 *
2803 * @method select
2804 * @param {*} item The item to select.
2805 */
2806 select: function(item) {
2807 if (this.multi) {
2808 this.toggle(item);
2809 } else if (this.get() !== item) {
2810 this.setItemSelected(this.get(), false);
2811 this.setItemSelected(item, true);
2812 }
2813 },
2814
2815 /**
2816 * Toggles the selection state for `item`.
2817 *
2818 * @method toggle
2819 * @param {*} item The item to toggle.
2820 */
2821 toggle: function(item) {
2822 this.setItemSelected(item, !this.isSelected(item));
2823 }
2824
2825 };
2826 /** @polymerBehavior */
2827 Polymer.IronSelectableBehavior = {
2828
2829 /**
2830 * Fired when iron-selector is activated (selected or deselected).
2831 * It is fired before the selected items are changed.
2832 * Cancel the event to abort selection.
2833 *
2834 * @event iron-activate
2835 */
2836
2837 /**
2838 * Fired when an item is selected
2839 *
2840 * @event iron-select
2841 */
2842
2843 /**
2844 * Fired when an item is deselected
2845 *
2846 * @event iron-deselect
2847 */
2848
2849 /**
2850 * Fired when the list of selectable items changes (e.g., items are
2851 * added or removed). The detail of the event is a mutation record that
2852 * describes what changed.
2853 *
2854 * @event iron-items-changed
2855 */
2856
2857 properties: {
2858
2859 /**
2860 * If you want to use an attribute value or property of an element for
2861 * `selected` instead of the index, set this to the name of the attribute
2862 * or property. Hyphenated values are converted to camel case when used to
2863 * look up the property of a selectable element. Camel cased values are
2864 * *not* converted to hyphenated values for attribute lookup. It's
2865 * recommended that you provide the hyphenated form of the name so that
2866 * selection works in both cases. (Use `attr-or-property-name` instead of
2867 * `attrOrPropertyName`.)
2868 */
2869 attrForSelected: {
2870 type: String,
2871 value: null
2872 },
2873
2874 /**
2875 * Gets or sets the selected element. The default is to use the index of t he item.
2876 * @type {string|number}
2877 */
2878 selected: {
2879 type: String,
2880 notify: true
2881 },
2882
2883 /**
2884 * Returns the currently selected item.
2885 *
2886 * @type {?Object}
2887 */
2888 selectedItem: {
2889 type: Object,
2890 readOnly: true,
2891 notify: true
2892 },
2893
2894 /**
2895 * The event that fires from items when they are selected. Selectable
2896 * will listen for this event from items and update the selection state.
2897 * Set to empty string to listen to no events.
2898 */
2899 activateEvent: {
2900 type: String,
2901 value: 'tap',
2902 observer: '_activateEventChanged'
2903 },
2904
2905 /**
2906 * This is a CSS selector string. If this is set, only items that match t he CSS selector
2907 * are selectable.
2908 */
2909 selectable: String,
2910
2911 /**
2912 * The class to set on elements when selected.
2913 */
2914 selectedClass: {
2915 type: String,
2916 value: 'iron-selected'
2917 },
2918
2919 /**
2920 * The attribute to set on elements when selected.
2921 */
2922 selectedAttribute: {
2923 type: String,
2924 value: null
2925 },
2926
2927 /**
2928 * Default fallback if the selection based on selected with `attrForSelect ed`
2929 * is not found.
2930 */
2931 fallbackSelection: {
2932 type: String,
2933 value: null
2934 },
2935
2936 /**
2937 * The list of items from which a selection can be made.
2938 */
2939 items: {
2940 type: Array, 1975 type: Array,
2941 readOnly: true,
2942 notify: true,
2943 value: function() { 1976 value: function() {
2944 return []; 1977 return [];
2945 } 1978 }
2946 }, 1979 },
2947 1980 _imperativeKeyBindings: {
2948 /**
2949 * The set of excluded elements where the key is the `localName`
2950 * of the element that will be ignored from the item list.
2951 *
2952 * @default {template: 1}
2953 */
2954 _excludedLocalNames: {
2955 type: Object, 1981 type: Object,
2956 value: function() { 1982 value: function() {
2957 return { 1983 return {};
2958 'template': 1
2959 };
2960 } 1984 }
2961 } 1985 }
2962 }, 1986 },
2963 1987 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ],
2964 observers: [ 1988 keyBindings: {},
2965 '_updateAttrForSelected(attrForSelected)', 1989 registered: function() {
2966 '_updateSelected(selected)', 1990 this._prepKeyBindings();
2967 '_checkFallback(fallbackSelection)' 1991 },
2968 ],
2969
2970 created: function() {
2971 this._bindFilterItem = this._filterItem.bind(this);
2972 this._selection = new Polymer.IronSelection(this._applySelection.bind(this ));
2973 },
2974
2975 attached: function() { 1992 attached: function() {
2976 this._observer = this._observeItems(this); 1993 this._listenKeyEventListeners();
2977 this._updateItems(); 1994 },
2978 if (!this._shouldUpdateSelection) {
2979 this._updateSelected();
2980 }
2981 this._addListener(this.activateEvent);
2982 },
2983
2984 detached: function() { 1995 detached: function() {
2985 if (this._observer) { 1996 this._unlistenKeyEventListeners();
2986 Polymer.dom(this).unobserveNodes(this._observer); 1997 },
2987 } 1998 addOwnKeyBinding: function(eventString, handlerName) {
2988 this._removeListener(this.activateEvent); 1999 this._imperativeKeyBindings[eventString] = handlerName;
2989 }, 2000 this._prepKeyBindings();
2990 2001 this._resetKeyEventListeners();
2991 /** 2002 },
2992 * Returns the index of the given item. 2003 removeOwnKeyBindings: function() {
2993 * 2004 this._imperativeKeyBindings = {};
2994 * @method indexOf 2005 this._prepKeyBindings();
2995 * @param {Object} item 2006 this._resetKeyEventListeners();
2996 * @returns Returns the index of the item 2007 },
2997 */ 2008 keyboardEventMatchesKeys: function(event, eventString) {
2998 indexOf: function(item) { 2009 var keyCombos = parseEventString(eventString);
2999 return this.items.indexOf(item); 2010 for (var i = 0; i < keyCombos.length; ++i) {
3000 }, 2011 if (keyComboMatchesEvent(keyCombos[i], event)) {
3001 2012 return true;
3002 /** 2013 }
3003 * Selects the given value. 2014 }
3004 * 2015 return false;
3005 * @method select 2016 },
3006 * @param {string|number} value the value to select. 2017 _collectKeyBindings: function() {
3007 */ 2018 var keyBindings = this.behaviors.map(function(behavior) {
3008 select: function(value) { 2019 return behavior.keyBindings;
3009 this.selected = value; 2020 });
3010 }, 2021 if (keyBindings.indexOf(this.keyBindings) === -1) {
3011 2022 keyBindings.push(this.keyBindings);
3012 /** 2023 }
3013 * Selects the previous item. 2024 return keyBindings;
3014 * 2025 },
3015 * @method selectPrevious 2026 _prepKeyBindings: function() {
3016 */ 2027 this._keyBindings = {};
3017 selectPrevious: function() { 2028 this._collectKeyBindings().forEach(function(keyBindings) {
3018 var length = this.items.length; 2029 for (var eventString in keyBindings) {
3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth; 2030 this._addKeyBinding(eventString, keyBindings[eventString]);
3020 this.selected = this._indexToValue(index); 2031 }
3021 }, 2032 }, this);
3022 2033 for (var eventString in this._imperativeKeyBindings) {
3023 /** 2034 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString ]);
3024 * Selects the next item. 2035 }
3025 * 2036 for (var eventName in this._keyBindings) {
3026 * @method selectNext 2037 this._keyBindings[eventName].sort(function(kb1, kb2) {
3027 */ 2038 var b1 = kb1[0].hasModifiers;
3028 selectNext: function() { 2039 var b2 = kb2[0].hasModifiers;
3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength; 2040 return b1 === b2 ? 0 : b1 ? -1 : 1;
3030 this.selected = this._indexToValue(index); 2041 });
3031 }, 2042 }
3032 2043 },
3033 /** 2044 _addKeyBinding: function(eventString, handlerName) {
3034 * Selects the item at the given index. 2045 parseEventString(eventString).forEach(function(keyCombo) {
3035 * 2046 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] || [];
3036 * @method selectIndex 2047 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]);
3037 */ 2048 }, this);
3038 selectIndex: function(index) { 2049 },
3039 this.select(this._indexToValue(index)); 2050 _resetKeyEventListeners: function() {
3040 }, 2051 this._unlistenKeyEventListeners();
3041 2052 if (this.isAttached) {
3042 /** 2053 this._listenKeyEventListeners();
3043 * Force a synchronous update of the `items` property. 2054 }
3044 * 2055 },
3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to 2056 _listenKeyEventListeners: function() {
3046 * updates to the set of selectable items after updates to the DOM list and 2057 if (!this.keyEventTarget) {
3047 * selection state have been made. 2058 return;
3048 * 2059 }
3049 * WARNING: If you are using this method, you should probably consider an 2060 Object.keys(this._keyBindings).forEach(function(eventName) {
3050 * alternate approach. Synchronously querying for items is potentially 2061 var keyBindings = this._keyBindings[eventName];
3051 * slow for many use cases. The `items` property will update asynchronously 2062 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
3052 * on its own to reflect selectable items in the DOM. 2063 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa ndler ]);
3053 */ 2064 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
3054 forceSynchronousItemUpdate: function() { 2065 }, this);
3055 this._updateItems(); 2066 },
3056 }, 2067 _unlistenKeyEventListeners: function() {
3057 2068 var keyHandlerTuple;
3058 get _shouldUpdateSelection() { 2069 var keyEventTarget;
3059 return this.selected != null; 2070 var eventName;
3060 }, 2071 var boundKeyHandler;
3061 2072 while (this._boundKeyHandlers.length) {
3062 _checkFallback: function() { 2073 keyHandlerTuple = this._boundKeyHandlers.pop();
3063 if (this._shouldUpdateSelection) { 2074 keyEventTarget = keyHandlerTuple[0];
3064 this._updateSelected(); 2075 eventName = keyHandlerTuple[1];
3065 } 2076 boundKeyHandler = keyHandlerTuple[2];
3066 }, 2077 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
3067 2078 }
3068 _addListener: function(eventName) { 2079 },
3069 this.listen(this, eventName, '_activateHandler'); 2080 _onKeyBindingEvent: function(keyBindings, event) {
3070 }, 2081 if (this.stopKeyboardEventPropagation) {
3071 2082 event.stopPropagation();
3072 _removeListener: function(eventName) { 2083 }
3073 this.unlisten(this, eventName, '_activateHandler'); 2084 if (event.defaultPrevented) {
3074 }, 2085 return;
3075 2086 }
3076 _activateEventChanged: function(eventName, old) { 2087 for (var i = 0; i < keyBindings.length; i++) {
3077 this._removeListener(old); 2088 var keyCombo = keyBindings[i][0];
3078 this._addListener(eventName); 2089 var handlerName = keyBindings[i][1];
3079 }, 2090 if (keyComboMatchesEvent(keyCombo, event)) {
3080 2091 this._triggerKeyHandler(keyCombo, handlerName, event);
3081 _updateItems: function() { 2092 if (event.defaultPrevented) {
3082 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*'); 2093 return;
3083 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
3084 this._setItems(nodes);
3085 },
3086
3087 _updateAttrForSelected: function() {
3088 if (this._shouldUpdateSelection) {
3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3090 }
3091 },
3092
3093 _updateSelected: function() {
3094 this._selectSelected(this.selected);
3095 },
3096
3097 _selectSelected: function(selected) {
3098 this._selection.select(this._valueToItem(this.selected));
3099 // Check for items, since this array is populated only when attached
3100 // Since Number(0) is falsy, explicitly check for undefined
3101 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
3102 this.selected = this.fallbackSelection;
3103 }
3104 },
3105
3106 _filterItem: function(node) {
3107 return !this._excludedLocalNames[node.localName];
3108 },
3109
3110 _valueToItem: function(value) {
3111 return (value == null) ? null : this.items[this._valueToIndex(value)];
3112 },
3113
3114 _valueToIndex: function(value) {
3115 if (this.attrForSelected) {
3116 for (var i = 0, item; item = this.items[i]; i++) {
3117 if (this._valueForItem(item) == value) {
3118 return i;
3119 } 2094 }
3120 } 2095 }
2096 }
2097 },
2098 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
2099 var detail = Object.create(keyCombo);
2100 detail.keyboardEvent = keyboardEvent;
2101 var event = new CustomEvent(keyCombo.event, {
2102 detail: detail,
2103 cancelable: true
2104 });
2105 this[handlerName].call(this, event);
2106 if (event.defaultPrevented) {
2107 keyboardEvent.preventDefault();
2108 }
2109 }
2110 };
2111 })();
2112
2113 Polymer.IronControlState = {
2114 properties: {
2115 focused: {
2116 type: Boolean,
2117 value: false,
2118 notify: true,
2119 readOnly: true,
2120 reflectToAttribute: true
2121 },
2122 disabled: {
2123 type: Boolean,
2124 value: false,
2125 notify: true,
2126 observer: '_disabledChanged',
2127 reflectToAttribute: true
2128 },
2129 _oldTabIndex: {
2130 type: Number
2131 },
2132 _boundFocusBlurHandler: {
2133 type: Function,
2134 value: function() {
2135 return this._focusBlurHandler.bind(this);
2136 }
2137 }
2138 },
2139 observers: [ '_changedControlState(focused, disabled)' ],
2140 ready: function() {
2141 this.addEventListener('focus', this._boundFocusBlurHandler, true);
2142 this.addEventListener('blur', this._boundFocusBlurHandler, true);
2143 },
2144 _focusBlurHandler: function(event) {
2145 if (event.target === this) {
2146 this._setFocused(event.type === 'focus');
2147 } else if (!this.shadowRoot) {
2148 var target = Polymer.dom(event).localTarget;
2149 if (!this.isLightDescendant(target)) {
2150 this.fire(event.type, {
2151 sourceEvent: event
2152 }, {
2153 node: this,
2154 bubbles: event.bubbles,
2155 cancelable: event.cancelable
2156 });
2157 }
2158 }
2159 },
2160 _disabledChanged: function(disabled, old) {
2161 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
2162 this.style.pointerEvents = disabled ? 'none' : '';
2163 if (disabled) {
2164 this._oldTabIndex = this.tabIndex;
2165 this._setFocused(false);
2166 this.tabIndex = -1;
2167 this.blur();
2168 } else if (this._oldTabIndex !== undefined) {
2169 this.tabIndex = this._oldTabIndex;
2170 }
2171 },
2172 _changedControlState: function() {
2173 if (this._controlStateChanged) {
2174 this._controlStateChanged();
2175 }
2176 }
2177 };
2178
2179 Polymer.IronButtonStateImpl = {
2180 properties: {
2181 pressed: {
2182 type: Boolean,
2183 readOnly: true,
2184 value: false,
2185 reflectToAttribute: true,
2186 observer: '_pressedChanged'
2187 },
2188 toggles: {
2189 type: Boolean,
2190 value: false,
2191 reflectToAttribute: true
2192 },
2193 active: {
2194 type: Boolean,
2195 value: false,
2196 notify: true,
2197 reflectToAttribute: true
2198 },
2199 pointerDown: {
2200 type: Boolean,
2201 readOnly: true,
2202 value: false
2203 },
2204 receivedFocusFromKeyboard: {
2205 type: Boolean,
2206 readOnly: true
2207 },
2208 ariaActiveAttribute: {
2209 type: String,
2210 value: 'aria-pressed',
2211 observer: '_ariaActiveAttributeChanged'
2212 }
2213 },
2214 listeners: {
2215 down: '_downHandler',
2216 up: '_upHandler',
2217 tap: '_tapHandler'
2218 },
2219 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi veAttribute)' ],
2220 keyBindings: {
2221 'enter:keydown': '_asyncClick',
2222 'space:keydown': '_spaceKeyDownHandler',
2223 'space:keyup': '_spaceKeyUpHandler'
2224 },
2225 _mouseEventRe: /^mouse/,
2226 _tapHandler: function() {
2227 if (this.toggles) {
2228 this._userActivate(!this.active);
2229 } else {
2230 this.active = false;
2231 }
2232 },
2233 _detectKeyboardFocus: function(focused) {
2234 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
2235 },
2236 _userActivate: function(active) {
2237 if (this.active !== active) {
2238 this.active = active;
2239 this.fire('change');
2240 }
2241 },
2242 _downHandler: function(event) {
2243 this._setPointerDown(true);
2244 this._setPressed(true);
2245 this._setReceivedFocusFromKeyboard(false);
2246 },
2247 _upHandler: function() {
2248 this._setPointerDown(false);
2249 this._setPressed(false);
2250 },
2251 _spaceKeyDownHandler: function(event) {
2252 var keyboardEvent = event.detail.keyboardEvent;
2253 var target = Polymer.dom(keyboardEvent).localTarget;
2254 if (this.isLightDescendant(target)) return;
2255 keyboardEvent.preventDefault();
2256 keyboardEvent.stopImmediatePropagation();
2257 this._setPressed(true);
2258 },
2259 _spaceKeyUpHandler: function(event) {
2260 var keyboardEvent = event.detail.keyboardEvent;
2261 var target = Polymer.dom(keyboardEvent).localTarget;
2262 if (this.isLightDescendant(target)) return;
2263 if (this.pressed) {
2264 this._asyncClick();
2265 }
2266 this._setPressed(false);
2267 },
2268 _asyncClick: function() {
2269 this.async(function() {
2270 this.click();
2271 }, 1);
2272 },
2273 _pressedChanged: function(pressed) {
2274 this._changedButtonState();
2275 },
2276 _ariaActiveAttributeChanged: function(value, oldValue) {
2277 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
2278 this.removeAttribute(oldValue);
2279 }
2280 },
2281 _activeChanged: function(active, ariaActiveAttribute) {
2282 if (this.toggles) {
2283 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false');
2284 } else {
2285 this.removeAttribute(this.ariaActiveAttribute);
2286 }
2287 this._changedButtonState();
2288 },
2289 _controlStateChanged: function() {
2290 if (this.disabled) {
2291 this._setPressed(false);
2292 } else {
2293 this._changedButtonState();
2294 }
2295 },
2296 _changedButtonState: function() {
2297 if (this._buttonStateChanged) {
2298 this._buttonStateChanged();
2299 }
2300 }
2301 };
2302
2303 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat eImpl ];
2304
2305 (function() {
2306 var Utility = {
2307 distance: function(x1, y1, x2, y2) {
2308 var xDelta = x1 - x2;
2309 var yDelta = y1 - y2;
2310 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
2311 },
2312 now: window.performance && window.performance.now ? window.performance.now.b ind(window.performance) : Date.now
2313 };
2314 function ElementMetrics(element) {
2315 this.element = element;
2316 this.width = this.boundingRect.width;
2317 this.height = this.boundingRect.height;
2318 this.size = Math.max(this.width, this.height);
2319 }
2320 ElementMetrics.prototype = {
2321 get boundingRect() {
2322 return this.element.getBoundingClientRect();
2323 },
2324 furthestCornerDistanceFrom: function(x, y) {
2325 var topLeft = Utility.distance(x, y, 0, 0);
2326 var topRight = Utility.distance(x, y, this.width, 0);
2327 var bottomLeft = Utility.distance(x, y, 0, this.height);
2328 var bottomRight = Utility.distance(x, y, this.width, this.height);
2329 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
2330 }
2331 };
2332 function Ripple(element) {
2333 this.element = element;
2334 this.color = window.getComputedStyle(element).color;
2335 this.wave = document.createElement('div');
2336 this.waveContainer = document.createElement('div');
2337 this.wave.style.backgroundColor = this.color;
2338 this.wave.classList.add('wave');
2339 this.waveContainer.classList.add('wave-container');
2340 Polymer.dom(this.waveContainer).appendChild(this.wave);
2341 this.resetInteractionState();
2342 }
2343 Ripple.MAX_RADIUS = 300;
2344 Ripple.prototype = {
2345 get recenters() {
2346 return this.element.recenters;
2347 },
2348 get center() {
2349 return this.element.center;
2350 },
2351 get mouseDownElapsed() {
2352 var elapsed;
2353 if (!this.mouseDownStart) {
2354 return 0;
2355 }
2356 elapsed = Utility.now() - this.mouseDownStart;
2357 if (this.mouseUpStart) {
2358 elapsed -= this.mouseUpElapsed;
2359 }
2360 return elapsed;
2361 },
2362 get mouseUpElapsed() {
2363 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0;
2364 },
2365 get mouseDownElapsedSeconds() {
2366 return this.mouseDownElapsed / 1e3;
2367 },
2368 get mouseUpElapsedSeconds() {
2369 return this.mouseUpElapsed / 1e3;
2370 },
2371 get mouseInteractionSeconds() {
2372 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
2373 },
2374 get initialOpacity() {
2375 return this.element.initialOpacity;
2376 },
2377 get opacityDecayVelocity() {
2378 return this.element.opacityDecayVelocity;
2379 },
2380 get radius() {
2381 var width2 = this.containerMetrics.width * this.containerMetrics.width;
2382 var height2 = this.containerMetrics.height * this.containerMetrics.height;
2383 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS) * 1.1 + 5;
2384 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS);
2385 var timeNow = this.mouseInteractionSeconds / duration;
2386 var size = waveRadius * (1 - Math.pow(80, -timeNow));
2387 return Math.abs(size);
2388 },
2389 get opacity() {
2390 if (!this.mouseUpStart) {
2391 return this.initialOpacity;
2392 }
2393 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this .opacityDecayVelocity);
2394 },
2395 get outerOpacity() {
2396 var outerOpacity = this.mouseUpElapsedSeconds * .3;
2397 var waveOpacity = this.opacity;
2398 return Math.max(0, Math.min(outerOpacity, waveOpacity));
2399 },
2400 get isOpacityFullyDecayed() {
2401 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl e.MAX_RADIUS);
2402 },
2403 get isRestingAtMaxRadius() {
2404 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this .maxRadius, Ripple.MAX_RADIUS);
2405 },
2406 get isAnimationComplete() {
2407 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa xRadius;
2408 },
2409 get translationFraction() {
2410 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr t(2));
2411 },
2412 get xNow() {
2413 if (this.xEnd) {
2414 return this.xStart + this.translationFraction * (this.xEnd - this.xStart );
2415 }
2416 return this.xStart;
2417 },
2418 get yNow() {
2419 if (this.yEnd) {
2420 return this.yStart + this.translationFraction * (this.yEnd - this.yStart );
2421 }
2422 return this.yStart;
2423 },
2424 get isMouseDown() {
2425 return this.mouseDownStart && !this.mouseUpStart;
2426 },
2427 resetInteractionState: function() {
2428 this.maxRadius = 0;
2429 this.mouseDownStart = 0;
2430 this.mouseUpStart = 0;
2431 this.xStart = 0;
2432 this.yStart = 0;
2433 this.xEnd = 0;
2434 this.yEnd = 0;
2435 this.slideDistance = 0;
2436 this.containerMetrics = new ElementMetrics(this.element);
2437 },
2438 draw: function() {
2439 var scale;
2440 var translateString;
2441 var dx;
2442 var dy;
2443 this.wave.style.opacity = this.opacity;
2444 scale = this.radius / (this.containerMetrics.size / 2);
2445 dx = this.xNow - this.containerMetrics.width / 2;
2446 dy = this.yNow - this.containerMetrics.height / 2;
2447 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
2448 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + ' px, 0)';
2449 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
2450 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
2451 },
2452 downAction: function(event) {
2453 var xCenter = this.containerMetrics.width / 2;
2454 var yCenter = this.containerMetrics.height / 2;
2455 this.resetInteractionState();
2456 this.mouseDownStart = Utility.now();
2457 if (this.center) {
2458 this.xStart = xCenter;
2459 this.yStart = yCenter;
2460 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
3121 } else { 2461 } else {
3122 return Number(value); 2462 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec t.left : this.containerMetrics.width / 2;
3123 } 2463 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec t.top : this.containerMetrics.height / 2;
3124 }, 2464 }
3125 2465 if (this.recenters) {
3126 _indexToValue: function(index) { 2466 this.xEnd = xCenter;
3127 if (this.attrForSelected) { 2467 this.yEnd = yCenter;
3128 var item = this.items[index]; 2468 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
3129 if (item) { 2469 }
3130 return this._valueForItem(item); 2470 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt art, this.yStart);
2471 this.waveContainer.style.top = (this.containerMetrics.height - this.contai nerMetrics.size) / 2 + 'px';
2472 this.waveContainer.style.left = (this.containerMetrics.width - this.contai nerMetrics.size) / 2 + 'px';
2473 this.waveContainer.style.width = this.containerMetrics.size + 'px';
2474 this.waveContainer.style.height = this.containerMetrics.size + 'px';
2475 },
2476 upAction: function(event) {
2477 if (!this.isMouseDown) {
2478 return;
2479 }
2480 this.mouseUpStart = Utility.now();
2481 },
2482 remove: function() {
2483 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer) ;
2484 }
2485 };
2486 Polymer({
2487 is: 'paper-ripple',
2488 behaviors: [ Polymer.IronA11yKeysBehavior ],
2489 properties: {
2490 initialOpacity: {
2491 type: Number,
2492 value: .25
2493 },
2494 opacityDecayVelocity: {
2495 type: Number,
2496 value: .8
2497 },
2498 recenters: {
2499 type: Boolean,
2500 value: false
2501 },
2502 center: {
2503 type: Boolean,
2504 value: false
2505 },
2506 ripples: {
2507 type: Array,
2508 value: function() {
2509 return [];
3131 } 2510 }
2511 },
2512 animating: {
2513 type: Boolean,
2514 readOnly: true,
2515 reflectToAttribute: true,
2516 value: false
2517 },
2518 holdDown: {
2519 type: Boolean,
2520 value: false,
2521 observer: '_holdDownChanged'
2522 },
2523 noink: {
2524 type: Boolean,
2525 value: false
2526 },
2527 _animating: {
2528 type: Boolean
2529 },
2530 _boundAnimate: {
2531 type: Function,
2532 value: function() {
2533 return this.animate.bind(this);
2534 }
2535 }
2536 },
2537 get target() {
2538 return this.keyEventTarget;
2539 },
2540 keyBindings: {
2541 'enter:keydown': '_onEnterKeydown',
2542 'space:keydown': '_onSpaceKeydown',
2543 'space:keyup': '_onSpaceKeyup'
2544 },
2545 attached: function() {
2546 if (this.parentNode.nodeType == 11) {
2547 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
3132 } else { 2548 } else {
3133 return index; 2549 this.keyEventTarget = this.parentNode;
3134 } 2550 }
3135 }, 2551 var keyEventTarget = this.keyEventTarget;
3136 2552 this.listen(keyEventTarget, 'up', 'uiUpAction');
3137 _valueForItem: function(item) { 2553 this.listen(keyEventTarget, 'down', 'uiDownAction');
3138 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ]; 2554 },
3139 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected); 2555 detached: function() {
3140 }, 2556 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
3141 2557 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
3142 _applySelection: function(item, isSelected) { 2558 this.keyEventTarget = null;
3143 if (this.selectedClass) { 2559 },
3144 this.toggleClass(this.selectedClass, isSelected, item); 2560 get shouldKeepAnimating() {
3145 } 2561 for (var index = 0; index < this.ripples.length; ++index) {
3146 if (this.selectedAttribute) { 2562 if (!this.ripples[index].isAnimationComplete) {
3147 this.toggleAttribute(this.selectedAttribute, isSelected, item); 2563 return true;
3148 }
3149 this._selectionChange();
3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
3151 },
3152
3153 _selectionChange: function() {
3154 this._setSelectedItem(this._selection.get());
3155 },
3156
3157 // observe items change under the given node.
3158 _observeItems: function(node) {
3159 return Polymer.dom(node).observeNodes(function(mutation) {
3160 this._updateItems();
3161
3162 if (this._shouldUpdateSelection) {
3163 this._updateSelected();
3164 } 2564 }
3165 2565 }
3166 // Let other interested parties know about the change so that 2566 return false;
3167 // we don't have to recreate mutation observers everywhere. 2567 },
3168 this.fire('iron-items-changed', mutation, { 2568 simulatedRipple: function() {
3169 bubbles: false, 2569 this.downAction(null);
3170 cancelable: false 2570 this.async(function() {
3171 }); 2571 this.upAction();
2572 }, 1);
2573 },
2574 uiDownAction: function(event) {
2575 if (!this.noink) {
2576 this.downAction(event);
2577 }
2578 },
2579 downAction: function(event) {
2580 if (this.holdDown && this.ripples.length > 0) {
2581 return;
2582 }
2583 var ripple = this.addRipple();
2584 ripple.downAction(event);
2585 if (!this._animating) {
2586 this._animating = true;
2587 this.animate();
2588 }
2589 },
2590 uiUpAction: function(event) {
2591 if (!this.noink) {
2592 this.upAction(event);
2593 }
2594 },
2595 upAction: function(event) {
2596 if (this.holdDown) {
2597 return;
2598 }
2599 this.ripples.forEach(function(ripple) {
2600 ripple.upAction(event);
3172 }); 2601 });
3173 }, 2602 this._animating = true;
3174 2603 this.animate();
3175 _activateHandler: function(e) { 2604 },
3176 var t = e.target; 2605 onAnimationComplete: function() {
3177 var items = this.items; 2606 this._animating = false;
3178 while (t && t != this) { 2607 this.$.background.style.backgroundColor = null;
3179 var i = items.indexOf(t); 2608 this.fire('transitionend');
3180 if (i >= 0) { 2609 },
3181 var value = this._indexToValue(i); 2610 addRipple: function() {
3182 this._itemActivate(value, t); 2611 var ripple = new Ripple(this);
3183 return; 2612 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
2613 this.$.background.style.backgroundColor = ripple.color;
2614 this.ripples.push(ripple);
2615 this._setAnimating(true);
2616 return ripple;
2617 },
2618 removeRipple: function(ripple) {
2619 var rippleIndex = this.ripples.indexOf(ripple);
2620 if (rippleIndex < 0) {
2621 return;
2622 }
2623 this.ripples.splice(rippleIndex, 1);
2624 ripple.remove();
2625 if (!this.ripples.length) {
2626 this._setAnimating(false);
2627 }
2628 },
2629 animate: function() {
2630 if (!this._animating) {
2631 return;
2632 }
2633 var index;
2634 var ripple;
2635 for (index = 0; index < this.ripples.length; ++index) {
2636 ripple = this.ripples[index];
2637 ripple.draw();
2638 this.$.background.style.opacity = ripple.outerOpacity;
2639 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
2640 this.removeRipple(ripple);
3184 } 2641 }
3185 t = t.parentNode; 2642 }
3186 } 2643 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
3187 }, 2644 this.onAnimationComplete();
3188 2645 } else {
3189 _itemActivate: function(value, item) { 2646 window.requestAnimationFrame(this._boundAnimate);
3190 if (!this.fire('iron-activate', 2647 }
3191 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { 2648 },
3192 this.select(value); 2649 _onEnterKeydown: function() {
3193 } 2650 this.uiDownAction();
3194 } 2651 this.async(this.uiUpAction, 1);
3195 2652 },
3196 }; 2653 _onSpaceKeydown: function() {
2654 this.uiDownAction();
2655 },
2656 _onSpaceKeyup: function() {
2657 this.uiUpAction();
2658 },
2659 _holdDownChanged: function(newVal, oldVal) {
2660 if (oldVal === undefined) {
2661 return;
2662 }
2663 if (newVal) {
2664 this.downAction();
2665 } else {
2666 this.upAction();
2667 }
2668 }
2669 });
2670 })();
2671
2672 Polymer.PaperRippleBehavior = {
2673 properties: {
2674 noink: {
2675 type: Boolean,
2676 observer: '_noinkChanged'
2677 },
2678 _rippleContainer: {
2679 type: Object
2680 }
2681 },
2682 _buttonStateChanged: function() {
2683 if (this.focused) {
2684 this.ensureRipple();
2685 }
2686 },
2687 _downHandler: function(event) {
2688 Polymer.IronButtonStateImpl._downHandler.call(this, event);
2689 if (this.pressed) {
2690 this.ensureRipple(event);
2691 }
2692 },
2693 ensureRipple: function(optTriggeringEvent) {
2694 if (!this.hasRipple()) {
2695 this._ripple = this._createRipple();
2696 this._ripple.noink = this.noink;
2697 var rippleContainer = this._rippleContainer || this.root;
2698 if (rippleContainer) {
2699 Polymer.dom(rippleContainer).appendChild(this._ripple);
2700 }
2701 if (optTriggeringEvent) {
2702 var domContainer = Polymer.dom(this._rippleContainer || this);
2703 var target = Polymer.dom(optTriggeringEvent).rootTarget;
2704 if (domContainer.deepContains(target)) {
2705 this._ripple.uiDownAction(optTriggeringEvent);
2706 }
2707 }
2708 }
2709 },
2710 getRipple: function() {
2711 this.ensureRipple();
2712 return this._ripple;
2713 },
2714 hasRipple: function() {
2715 return Boolean(this._ripple);
2716 },
2717 _createRipple: function() {
2718 return document.createElement('paper-ripple');
2719 },
2720 _noinkChanged: function(noink) {
2721 if (this.hasRipple()) {
2722 this._ripple.noink = noink;
2723 }
2724 }
2725 };
2726
2727 Polymer.PaperButtonBehaviorImpl = {
2728 properties: {
2729 elevation: {
2730 type: Number,
2731 reflectToAttribute: true,
2732 readOnly: true
2733 }
2734 },
2735 observers: [ '_calculateElevation(focused, disabled, active, pressed, received FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ],
2736 hostAttributes: {
2737 role: 'button',
2738 tabindex: '0',
2739 animated: true
2740 },
2741 _calculateElevation: function() {
2742 var e = 1;
2743 if (this.disabled) {
2744 e = 0;
2745 } else if (this.active || this.pressed) {
2746 e = 4;
2747 } else if (this.receivedFocusFromKeyboard) {
2748 e = 3;
2749 }
2750 this._setElevation(e);
2751 },
2752 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
2753 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
2754 },
2755 _spaceKeyDownHandler: function(event) {
2756 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
2757 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
2758 this._ripple.uiDownAction();
2759 }
2760 },
2761 _spaceKeyUpHandler: function(event) {
2762 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
2763 if (this.hasRipple()) {
2764 this._ripple.uiUpAction();
2765 }
2766 }
2767 };
2768
2769 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ];
2770
3197 Polymer({ 2771 Polymer({
3198 2772 is: 'paper-button',
3199 is: 'iron-pages', 2773 behaviors: [ Polymer.PaperButtonBehavior ],
3200 2774 properties: {
3201 behaviors: [ 2775 raised: {
3202 Polymer.IronResizableBehavior, 2776 type: Boolean,
3203 Polymer.IronSelectableBehavior 2777 reflectToAttribute: true,
3204 ], 2778 value: false,
3205 2779 observer: '_calculateElevation'
3206 properties: { 2780 }
3207 2781 },
3208 // as the selected page is the only one visible, activateEvent 2782 _calculateElevation: function() {
3209 // is both non-sensical and problematic; e.g. in cases where a user 2783 if (!this.raised) {
3210 // handler attempts to change the page and the activateEvent 2784 this._setElevation(0);
3211 // handler immediately changes it back 2785 } else {
3212 activateEvent: { 2786 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
3213 type: String, 2787 }
3214 value: null 2788 }
3215 } 2789 });
3216 2790
2791 (function() {
2792 var metaDatas = {};
2793 var metaArrays = {};
2794 var singleton = null;
2795 Polymer.IronMeta = Polymer({
2796 is: 'iron-meta',
2797 properties: {
2798 type: {
2799 type: String,
2800 value: 'default',
2801 observer: '_typeChanged'
3217 }, 2802 },
3218 2803 key: {
3219 observers: [ 2804 type: String,
3220 '_selectedPageChanged(selected)' 2805 observer: '_keyChanged'
3221 ], 2806 },
3222 2807 value: {
3223 _selectedPageChanged: function(selected, old) { 2808 type: Object,
3224 this.async(this.notifyResize); 2809 notify: true,
3225 } 2810 observer: '_valueChanged'
3226 }); 2811 },
3227 (function() { 2812 self: {
3228 'use strict'; 2813 type: Boolean,
3229 2814 observer: '_selfChanged'
3230 /** 2815 },
3231 * Chrome uses an older version of DOM Level 3 Keyboard Events 2816 list: {
3232 * 2817 type: Array,
3233 * Most keys are labeled as text, but some are Unicode codepoints. 2818 notify: true
3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set 2819 }
3235 */ 2820 },
3236 var KEY_IDENTIFIER = { 2821 hostAttributes: {
3237 'U+0008': 'backspace', 2822 hidden: true
3238 'U+0009': 'tab', 2823 },
3239 'U+001B': 'esc', 2824 factoryImpl: function(config) {
3240 'U+0020': 'space', 2825 if (config) {
3241 'U+007F': 'del' 2826 for (var n in config) {
3242 }; 2827 switch (n) {
3243 2828 case 'type':
3244 /** 2829 case 'key':
3245 * Special table for KeyboardEvent.keyCode. 2830 case 'value':
3246 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er 2831 this[n] = config[n];
3247 * than that. 2832 break;
3248 *
3249 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode
3250 */
3251 var KEY_CODE = {
3252 8: 'backspace',
3253 9: 'tab',
3254 13: 'enter',
3255 27: 'esc',
3256 33: 'pageup',
3257 34: 'pagedown',
3258 35: 'end',
3259 36: 'home',
3260 32: 'space',
3261 37: 'left',
3262 38: 'up',
3263 39: 'right',
3264 40: 'down',
3265 46: 'del',
3266 106: '*'
3267 };
3268
3269 /**
3270 * MODIFIER_KEYS maps the short name for modifier keys used in a key
3271 * combo string to the property name that references those same keys
3272 * in a KeyboardEvent instance.
3273 */
3274 var MODIFIER_KEYS = {
3275 'shift': 'shiftKey',
3276 'ctrl': 'ctrlKey',
3277 'alt': 'altKey',
3278 'meta': 'metaKey'
3279 };
3280
3281 /**
3282 * KeyboardEvent.key is mostly represented by printable character made by
3283 * the keyboard, with unprintable keys labeled nicely.
3284 *
3285 * However, on OS X, Alt+char can make a Unicode character that follows an
3286 * Apple-specific mapping. In this case, we fall back to .keyCode.
3287 */
3288 var KEY_CHAR = /[a-z0-9*]/;
3289
3290 /**
3291 * Matches a keyIdentifier string.
3292 */
3293 var IDENT_CHAR = /U\+/;
3294
3295 /**
3296 * Matches arrow keys in Gecko 27.0+
3297 */
3298 var ARROW_KEY = /^arrow/;
3299
3300 /**
3301 * Matches space keys everywhere (notably including IE10's exceptional name
3302 * `spacebar`).
3303 */
3304 var SPACE_KEY = /^space(bar)?/;
3305
3306 /**
3307 * Matches ESC key.
3308 *
3309 * Value from: http://w3c.github.io/uievents-key/#key-Escape
3310 */
3311 var ESC_KEY = /^escape$/;
3312
3313 /**
3314 * Transforms the key.
3315 * @param {string} key The KeyBoardEvent.key
3316 * @param {Boolean} [noSpecialChars] Limits the transformation to
3317 * alpha-numeric characters.
3318 */
3319 function transformKey(key, noSpecialChars) {
3320 var validKey = '';
3321 if (key) {
3322 var lKey = key.toLowerCase();
3323 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
3324 validKey = 'space';
3325 } else if (ESC_KEY.test(lKey)) {
3326 validKey = 'esc';
3327 } else if (lKey.length == 1) {
3328 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
3329 validKey = lKey;
3330 }
3331 } else if (ARROW_KEY.test(lKey)) {
3332 validKey = lKey.replace('arrow', '');
3333 } else if (lKey == 'multiply') {
3334 // numpad '*' can map to Multiply on IE/Windows
3335 validKey = '*';
3336 } else {
3337 validKey = lKey;
3338 }
3339 }
3340 return validKey;
3341 }
3342
3343 function transformKeyIdentifier(keyIdent) {
3344 var validKey = '';
3345 if (keyIdent) {
3346 if (keyIdent in KEY_IDENTIFIER) {
3347 validKey = KEY_IDENTIFIER[keyIdent];
3348 } else if (IDENT_CHAR.test(keyIdent)) {
3349 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
3350 validKey = String.fromCharCode(keyIdent).toLowerCase();
3351 } else {
3352 validKey = keyIdent.toLowerCase();
3353 }
3354 }
3355 return validKey;
3356 }
3357
3358 function transformKeyCode(keyCode) {
3359 var validKey = '';
3360 if (Number(keyCode)) {
3361 if (keyCode >= 65 && keyCode <= 90) {
3362 // ascii a-z
3363 // lowercase is 32 offset from uppercase
3364 validKey = String.fromCharCode(32 + keyCode);
3365 } else if (keyCode >= 112 && keyCode <= 123) {
3366 // function keys f1-f12
3367 validKey = 'f' + (keyCode - 112);
3368 } else if (keyCode >= 48 && keyCode <= 57) {
3369 // top 0-9 keys
3370 validKey = String(keyCode - 48);
3371 } else if (keyCode >= 96 && keyCode <= 105) {
3372 // num pad 0-9
3373 validKey = String(keyCode - 96);
3374 } else {
3375 validKey = KEY_CODE[keyCode];
3376 }
3377 }
3378 return validKey;
3379 }
3380
3381 /**
3382 * Calculates the normalized key for a KeyboardEvent.
3383 * @param {KeyboardEvent} keyEvent
3384 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
3385 * transformation to alpha-numeric chars. This is useful with key
3386 * combinations like shift + 2, which on FF for MacOS produces
3387 * keyEvent.key = @
3388 * To get 2 returned, set noSpecialChars = true
3389 * To get @ returned, set noSpecialChars = false
3390 */
3391 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
3392 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
3393 // .detail.key to support artificial keyboard events.
3394 return transformKey(keyEvent.key, noSpecialChars) ||
3395 transformKeyIdentifier(keyEvent.keyIdentifier) ||
3396 transformKeyCode(keyEvent.keyCode) ||
3397 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
3398 }
3399
3400 function keyComboMatchesEvent(keyCombo, event) {
3401 // For combos with modifiers we support only alpha-numeric keys
3402 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
3403 return keyEvent === keyCombo.key &&
3404 (!keyCombo.hasModifiers || (
3405 !!event.shiftKey === !!keyCombo.shiftKey &&
3406 !!event.ctrlKey === !!keyCombo.ctrlKey &&
3407 !!event.altKey === !!keyCombo.altKey &&
3408 !!event.metaKey === !!keyCombo.metaKey)
3409 );
3410 }
3411
3412 function parseKeyComboString(keyComboString) {
3413 if (keyComboString.length === 1) {
3414 return {
3415 combo: keyComboString,
3416 key: keyComboString,
3417 event: 'keydown'
3418 };
3419 }
3420 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP art) {
3421 var eventParts = keyComboPart.split(':');
3422 var keyName = eventParts[0];
3423 var event = eventParts[1];
3424
3425 if (keyName in MODIFIER_KEYS) {
3426 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
3427 parsedKeyCombo.hasModifiers = true;
3428 } else {
3429 parsedKeyCombo.key = keyName;
3430 parsedKeyCombo.event = event || 'keydown';
3431 }
3432
3433 return parsedKeyCombo;
3434 }, {
3435 combo: keyComboString.split(':').shift()
3436 });
3437 }
3438
3439 function parseEventString(eventString) {
3440 return eventString.trim().split(' ').map(function(keyComboString) {
3441 return parseKeyComboString(keyComboString);
3442 });
3443 }
3444
3445 /**
3446 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
3447 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
3448 * The element takes care of browser differences with respect to Keyboard ev ents
3449 * and uses an expressive syntax to filter key presses.
3450 *
3451 * Use the `keyBindings` prototype property to express what combination of k eys
3452 * will trigger the callback. A key binding has the format
3453 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
3454 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
3455 *
3456 * keyBindings: {
3457 * 'space': '_onKeydown', // same as 'space:keydown'
3458 * 'shift+tab': '_onKeydown',
3459 * 'enter:keypress': '_onKeypress',
3460 * 'esc:keyup': '_onKeyup'
3461 * }
3462 *
3463 * The callback will receive with an event containing the following informat ion in `event.detail`:
3464 *
3465 * _onKeydown: function(event) {
3466 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
3467 * console.log(event.detail.key); // KEY only, e.g. "tab"
3468 * console.log(event.detail.event); // EVENT, e.g. "keydown"
3469 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
3470 * }
3471 *
3472 * Use the `keyEventTarget` attribute to set up event handlers on a specific
3473 * node.
3474 *
3475 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
3476 * for an example.
3477 *
3478 * @demo demo/index.html
3479 * @polymerBehavior
3480 */
3481 Polymer.IronA11yKeysBehavior = {
3482 properties: {
3483 /**
3484 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
3485 * `null` to disable the listeners.
3486 * @type {?EventTarget}
3487 */
3488 keyEventTarget: {
3489 type: Object,
3490 value: function() {
3491 return this;
3492 }
3493 },
3494
3495 /**
3496 * If true, this property will cause the implementing element to
3497 * automatically stop propagation on any handled KeyboardEvents.
3498 */
3499 stopKeyboardEventPropagation: {
3500 type: Boolean,
3501 value: false
3502 },
3503
3504 _boundKeyHandlers: {
3505 type: Array,
3506 value: function() {
3507 return [];
3508 }
3509 },
3510
3511 // We use this due to a limitation in IE10 where instances will have
3512 // own properties of everything on the "prototype".
3513 _imperativeKeyBindings: {
3514 type: Object,
3515 value: function() {
3516 return {};
3517 } 2833 }
3518 } 2834 }
2835 }
2836 },
2837 created: function() {
2838 this._metaDatas = metaDatas;
2839 this._metaArrays = metaArrays;
2840 },
2841 _keyChanged: function(key, old) {
2842 this._resetRegistration(old);
2843 },
2844 _valueChanged: function(value) {
2845 this._resetRegistration(this.key);
2846 },
2847 _selfChanged: function(self) {
2848 if (self) {
2849 this.value = this;
2850 }
2851 },
2852 _typeChanged: function(type) {
2853 this._unregisterKey(this.key);
2854 if (!metaDatas[type]) {
2855 metaDatas[type] = {};
2856 }
2857 this._metaData = metaDatas[type];
2858 if (!metaArrays[type]) {
2859 metaArrays[type] = [];
2860 }
2861 this.list = metaArrays[type];
2862 this._registerKeyValue(this.key, this.value);
2863 },
2864 byKey: function(key) {
2865 return this._metaData && this._metaData[key];
2866 },
2867 _resetRegistration: function(oldKey) {
2868 this._unregisterKey(oldKey);
2869 this._registerKeyValue(this.key, this.value);
2870 },
2871 _unregisterKey: function(key) {
2872 this._unregister(key, this._metaData, this.list);
2873 },
2874 _registerKeyValue: function(key, value) {
2875 this._register(key, value, this._metaData, this.list);
2876 },
2877 _register: function(key, value, data, list) {
2878 if (key && data && value !== undefined) {
2879 data[key] = value;
2880 list.push(value);
2881 }
2882 },
2883 _unregister: function(key, data, list) {
2884 if (key && data) {
2885 if (key in data) {
2886 var value = data[key];
2887 delete data[key];
2888 this.arrayDelete(list, value);
2889 }
2890 }
2891 }
2892 });
2893 Polymer.IronMeta.getIronMeta = function getIronMeta() {
2894 if (singleton === null) {
2895 singleton = new Polymer.IronMeta();
2896 }
2897 return singleton;
2898 };
2899 Polymer.IronMetaQuery = Polymer({
2900 is: 'iron-meta-query',
2901 properties: {
2902 type: {
2903 type: String,
2904 value: 'default',
2905 observer: '_typeChanged'
3519 }, 2906 },
3520 2907 key: {
3521 observers: [ 2908 type: String,
3522 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' 2909 observer: '_keyChanged'
3523 ],
3524
3525
3526 /**
3527 * To be used to express what combination of keys will trigger the relati ve
3528 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
3529 * @type {Object}
3530 */
3531 keyBindings: {},
3532
3533 registered: function() {
3534 this._prepKeyBindings();
3535 }, 2910 },
3536 2911 value: {
3537 attached: function() { 2912 type: Object,
3538 this._listenKeyEventListeners(); 2913 notify: true,
2914 readOnly: true
3539 }, 2915 },
3540 2916 list: {
3541 detached: function() { 2917 type: Array,
3542 this._unlistenKeyEventListeners(); 2918 notify: true
3543 }, 2919 }
3544 2920 },
3545 /** 2921 factoryImpl: function(config) {
3546 * Can be used to imperatively add a key binding to the implementing 2922 if (config) {
3547 * element. This is the imperative equivalent of declaring a keybinding 2923 for (var n in config) {
3548 * in the `keyBindings` prototype property. 2924 switch (n) {
3549 */ 2925 case 'type':
3550 addOwnKeyBinding: function(eventString, handlerName) { 2926 case 'key':
3551 this._imperativeKeyBindings[eventString] = handlerName; 2927 this[n] = config[n];
3552 this._prepKeyBindings(); 2928 break;
3553 this._resetKeyEventListeners();
3554 },
3555
3556 /**
3557 * When called, will remove all imperatively-added key bindings.
3558 */
3559 removeOwnKeyBindings: function() {
3560 this._imperativeKeyBindings = {};
3561 this._prepKeyBindings();
3562 this._resetKeyEventListeners();
3563 },
3564
3565 /**
3566 * Returns true if a keyboard event matches `eventString`.
3567 *
3568 * @param {KeyboardEvent} event
3569 * @param {string} eventString
3570 * @return {boolean}
3571 */
3572 keyboardEventMatchesKeys: function(event, eventString) {
3573 var keyCombos = parseEventString(eventString);
3574 for (var i = 0; i < keyCombos.length; ++i) {
3575 if (keyComboMatchesEvent(keyCombos[i], event)) {
3576 return true;
3577 } 2929 }
3578 } 2930 }
3579 return false; 2931 }
3580 }, 2932 },
3581 2933 created: function() {
3582 _collectKeyBindings: function() { 2934 this._metaDatas = metaDatas;
3583 var keyBindings = this.behaviors.map(function(behavior) { 2935 this._metaArrays = metaArrays;
3584 return behavior.keyBindings; 2936 },
3585 }); 2937 _keyChanged: function(key) {
3586 2938 this._setValue(this._metaData && this._metaData[key]);
3587 if (keyBindings.indexOf(this.keyBindings) === -1) { 2939 },
3588 keyBindings.push(this.keyBindings); 2940 _typeChanged: function(type) {
2941 this._metaData = metaDatas[type];
2942 this.list = metaArrays[type];
2943 if (this.key) {
2944 this._keyChanged(this.key);
2945 }
2946 },
2947 byKey: function(key) {
2948 return this._metaData && this._metaData[key];
2949 }
2950 });
2951 })();
2952
2953 Polymer({
2954 is: 'iron-icon',
2955 properties: {
2956 icon: {
2957 type: String,
2958 observer: '_iconChanged'
2959 },
2960 theme: {
2961 type: String,
2962 observer: '_updateIcon'
2963 },
2964 src: {
2965 type: String,
2966 observer: '_srcChanged'
2967 },
2968 _meta: {
2969 value: Polymer.Base.create('iron-meta', {
2970 type: 'iconset'
2971 }),
2972 observer: '_updateIcon'
2973 }
2974 },
2975 _DEFAULT_ICONSET: 'icons',
2976 _iconChanged: function(icon) {
2977 var parts = (icon || '').split(':');
2978 this._iconName = parts.pop();
2979 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
2980 this._updateIcon();
2981 },
2982 _srcChanged: function(src) {
2983 this._updateIcon();
2984 },
2985 _usesIconset: function() {
2986 return this.icon || !this.src;
2987 },
2988 _updateIcon: function() {
2989 if (this._usesIconset()) {
2990 if (this._img && this._img.parentNode) {
2991 Polymer.dom(this.root).removeChild(this._img);
2992 }
2993 if (this._iconName === "") {
2994 if (this._iconset) {
2995 this._iconset.removeIcon(this);
3589 } 2996 }
3590 2997 } else if (this._iconsetName && this._meta) {
3591 return keyBindings; 2998 this._iconset = this._meta.byKey(this._iconsetName);
3592 }, 2999 if (this._iconset) {
3593 3000 this._iconset.applyIcon(this, this._iconName, this.theme);
3594 _prepKeyBindings: function() { 3001 this.unlisten(window, 'iron-iconset-added', '_updateIcon');
3595 this._keyBindings = {}; 3002 } else {
3596 3003 this.listen(window, 'iron-iconset-added', '_updateIcon');
3597 this._collectKeyBindings().forEach(function(keyBindings) {
3598 for (var eventString in keyBindings) {
3599 this._addKeyBinding(eventString, keyBindings[eventString]);
3600 }
3601 }, this);
3602
3603 for (var eventString in this._imperativeKeyBindings) {
3604 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
3605 } 3004 }
3606 3005 }
3607 // Give precedence to combos with modifiers to be checked first. 3006 } else {
3608 for (var eventName in this._keyBindings) { 3007 if (this._iconset) {
3609 this._keyBindings[eventName].sort(function (kb1, kb2) { 3008 this._iconset.removeIcon(this);
3610 var b1 = kb1[0].hasModifiers; 3009 }
3611 var b2 = kb2[0].hasModifiers; 3010 if (!this._img) {
3612 return (b1 === b2) ? 0 : b1 ? -1 : 1; 3011 this._img = document.createElement('img');
3613 }) 3012 this._img.style.width = '100%';
3013 this._img.style.height = '100%';
3014 this._img.draggable = false;
3015 }
3016 this._img.src = this.src;
3017 Polymer.dom(this.root).appendChild(this._img);
3018 }
3019 }
3020 });
3021
3022 Polymer.PaperInkyFocusBehaviorImpl = {
3023 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ],
3024 _focusedChanged: function(receivedFocusFromKeyboard) {
3025 if (receivedFocusFromKeyboard) {
3026 this.ensureRipple();
3027 }
3028 if (this.hasRipple()) {
3029 this._ripple.holdDown = receivedFocusFromKeyboard;
3030 }
3031 },
3032 _createRipple: function() {
3033 var ripple = Polymer.PaperRippleBehavior._createRipple();
3034 ripple.id = 'ink';
3035 ripple.setAttribute('center', '');
3036 ripple.classList.add('circle');
3037 return ripple;
3038 }
3039 };
3040
3041 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ];
3042
3043 Polymer({
3044 is: 'paper-icon-button',
3045 hostAttributes: {
3046 role: 'button',
3047 tabindex: '0'
3048 },
3049 behaviors: [ Polymer.PaperInkyFocusBehavior ],
3050 properties: {
3051 src: {
3052 type: String
3053 },
3054 icon: {
3055 type: String
3056 },
3057 alt: {
3058 type: String,
3059 observer: "_altChanged"
3060 }
3061 },
3062 _altChanged: function(newValue, oldValue) {
3063 var label = this.getAttribute('aria-label');
3064 if (!label || oldValue == label) {
3065 this.setAttribute('aria-label', newValue);
3066 }
3067 }
3068 });
3069
3070 Polymer({
3071 is: 'paper-tab',
3072 behaviors: [ Polymer.IronControlState, Polymer.IronButtonState, Polymer.PaperR ippleBehavior ],
3073 properties: {
3074 link: {
3075 type: Boolean,
3076 value: false,
3077 reflectToAttribute: true
3078 }
3079 },
3080 hostAttributes: {
3081 role: 'tab'
3082 },
3083 listeners: {
3084 down: '_updateNoink',
3085 tap: '_onTap'
3086 },
3087 attached: function() {
3088 this._updateNoink();
3089 },
3090 get _parentNoink() {
3091 var parent = Polymer.dom(this).parentNode;
3092 return !!parent && !!parent.noink;
3093 },
3094 _updateNoink: function() {
3095 this.noink = !!this.noink || !!this._parentNoink;
3096 },
3097 _onTap: function(event) {
3098 if (this.link) {
3099 var anchor = this.queryEffectiveChildren('a');
3100 if (!anchor) {
3101 return;
3102 }
3103 if (event.target === anchor) {
3104 return;
3105 }
3106 anchor.click();
3107 }
3108 }
3109 });
3110
3111 Polymer.IronMultiSelectableBehaviorImpl = {
3112 properties: {
3113 multi: {
3114 type: Boolean,
3115 value: false,
3116 observer: 'multiChanged'
3117 },
3118 selectedValues: {
3119 type: Array,
3120 notify: true
3121 },
3122 selectedItems: {
3123 type: Array,
3124 readOnly: true,
3125 notify: true
3126 }
3127 },
3128 observers: [ '_updateSelected(selectedValues.splices)' ],
3129 select: function(value) {
3130 if (this.multi) {
3131 if (this.selectedValues) {
3132 this._toggleSelected(value);
3133 } else {
3134 this.selectedValues = [ value ];
3135 }
3136 } else {
3137 this.selected = value;
3138 }
3139 },
3140 multiChanged: function(multi) {
3141 this._selection.multi = multi;
3142 },
3143 get _shouldUpdateSelection() {
3144 return this.selected != null || this.selectedValues != null && this.selected Values.length;
3145 },
3146 _updateAttrForSelected: function() {
3147 if (!this.multi) {
3148 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
3149 } else if (this._shouldUpdateSelection) {
3150 this.selectedValues = this.selectedItems.map(function(selectedItem) {
3151 return this._indexToValue(this.indexOf(selectedItem));
3152 }, this).filter(function(unfilteredValue) {
3153 return unfilteredValue != null;
3154 }, this);
3155 }
3156 },
3157 _updateSelected: function() {
3158 if (this.multi) {
3159 this._selectMulti(this.selectedValues);
3160 } else {
3161 this._selectSelected(this.selected);
3162 }
3163 },
3164 _selectMulti: function(values) {
3165 if (values) {
3166 var selectedItems = this._valuesToItems(values);
3167 this._selection.clear(selectedItems);
3168 for (var i = 0; i < selectedItems.length; i++) {
3169 this._selection.setItemSelected(selectedItems[i], true);
3170 }
3171 if (this.fallbackSelection && this.items.length && !this._selection.get(). length) {
3172 var fallback = this._valueToItem(this.fallbackSelection);
3173 if (fallback) {
3174 this.selectedValues = [ this.fallbackSelection ];
3614 } 3175 }
3615 }, 3176 }
3616 3177 } else {
3617 _addKeyBinding: function(eventString, handlerName) { 3178 this._selection.clear();
3618 parseEventString(eventString).forEach(function(keyCombo) { 3179 }
3619 this._keyBindings[keyCombo.event] = 3180 },
3620 this._keyBindings[keyCombo.event] || []; 3181 _selectionChange: function() {
3621 3182 var s = this._selection.get();
3622 this._keyBindings[keyCombo.event].push([ 3183 if (this.multi) {
3623 keyCombo, 3184 this._setSelectedItems(s);
3624 handlerName 3185 } else {
3625 ]); 3186 this._setSelectedItems([ s ]);
3626 }, this); 3187 this._setSelectedItem(s);
3627 }, 3188 }
3628 3189 },
3629 _resetKeyEventListeners: function() { 3190 _toggleSelected: function(value) {
3630 this._unlistenKeyEventListeners(); 3191 var i = this.selectedValues.indexOf(value);
3631 3192 var unselected = i < 0;
3632 if (this.isAttached) { 3193 if (unselected) {
3633 this._listenKeyEventListeners(); 3194 this.push('selectedValues', value);
3634 } 3195 } else {
3635 }, 3196 this.splice('selectedValues', i, 1);
3636 3197 }
3637 _listenKeyEventListeners: function() { 3198 },
3638 if (!this.keyEventTarget) { 3199 _valuesToItems: function(values) {
3639 return; 3200 return values == null ? null : values.map(function(value) {
3640 } 3201 return this._valueToItem(value);
3641 Object.keys(this._keyBindings).forEach(function(eventName) { 3202 }, this);
3642 var keyBindings = this._keyBindings[eventName]; 3203 }
3643 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); 3204 };
3644 3205
3645 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH andler]); 3206 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer. IronMultiSelectableBehaviorImpl ];
3646 3207
3647 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); 3208 Polymer.IronMenuBehaviorImpl = {
3648 }, this); 3209 properties: {
3649 }, 3210 focusedItem: {
3650 3211 observer: '_focusedItemChanged',
3651 _unlistenKeyEventListeners: function() { 3212 readOnly: true,
3652 var keyHandlerTuple; 3213 type: Object
3653 var keyEventTarget; 3214 },
3654 var eventName; 3215 attrForItemTitle: {
3655 var boundKeyHandler; 3216 type: String
3656 3217 }
3657 while (this._boundKeyHandlers.length) { 3218 },
3658 // My kingdom for block-scope binding and destructuring assignment.. 3219 hostAttributes: {
3659 keyHandlerTuple = this._boundKeyHandlers.pop(); 3220 role: 'menu',
3660 keyEventTarget = keyHandlerTuple[0]; 3221 tabindex: '0'
3661 eventName = keyHandlerTuple[1]; 3222 },
3662 boundKeyHandler = keyHandlerTuple[2]; 3223 observers: [ '_updateMultiselectable(multi)' ],
3663 3224 listeners: {
3664 keyEventTarget.removeEventListener(eventName, boundKeyHandler); 3225 focus: '_onFocus',
3665 } 3226 keydown: '_onKeydown',
3666 }, 3227 'iron-items-changed': '_onIronItemsChanged'
3667 3228 },
3668 _onKeyBindingEvent: function(keyBindings, event) { 3229 keyBindings: {
3669 if (this.stopKeyboardEventPropagation) { 3230 up: '_onUpKey',
3670 event.stopPropagation(); 3231 down: '_onDownKey',
3671 } 3232 esc: '_onEscKey',
3672 3233 'shift+tab:keydown': '_onShiftTabDown'
3673 // if event has been already prevented, don't do anything 3234 },
3674 if (event.defaultPrevented) { 3235 attached: function() {
3675 return; 3236 this._resetTabindices();
3676 } 3237 },
3677 3238 select: function(value) {
3678 for (var i = 0; i < keyBindings.length; i++) { 3239 if (this._defaultFocusAsync) {
3679 var keyCombo = keyBindings[i][0]; 3240 this.cancelAsync(this._defaultFocusAsync);
3680 var handlerName = keyBindings[i][1]; 3241 this._defaultFocusAsync = null;
3681 if (keyComboMatchesEvent(keyCombo, event)) { 3242 }
3682 this._triggerKeyHandler(keyCombo, handlerName, event); 3243 var item = this._valueToItem(value);
3683 // exit the loop if eventDefault was prevented 3244 if (item && item.hasAttribute('disabled')) return;
3684 if (event.defaultPrevented) { 3245 this._setFocusedItem(item);
3685 return; 3246 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
3686 } 3247 },
3687 } 3248 _resetTabindices: function() {
3688 } 3249 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0] : this.selectedItem;
3689 }, 3250 this.items.forEach(function(item) {
3690 3251 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
3691 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { 3252 }, this);
3692 var detail = Object.create(keyCombo); 3253 },
3693 detail.keyboardEvent = keyboardEvent; 3254 _updateMultiselectable: function(multi) {
3694 var event = new CustomEvent(keyCombo.event, { 3255 if (multi) {
3695 detail: detail, 3256 this.setAttribute('aria-multiselectable', 'true');
3696 cancelable: true 3257 } else {
3697 }); 3258 this.removeAttribute('aria-multiselectable');
3698 this[handlerName].call(this, event); 3259 }
3699 if (event.defaultPrevented) { 3260 },
3700 keyboardEvent.preventDefault(); 3261 _focusWithKeyboardEvent: function(event) {
3701 } 3262 for (var i = 0, item; item = this.items[i]; i++) {
3702 } 3263 var attr = this.attrForItemTitle || 'textContent';
3703 }; 3264 var title = item[attr] || item.getAttribute(attr);
3704 })(); 3265 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo werCase() === String.fromCharCode(event.keyCode).toLowerCase()) {
3705 /** 3266 this._setFocusedItem(item);
3706 * @demo demo/index.html 3267 break;
3707 * @polymerBehavior 3268 }
3708 */ 3269 }
3709 Polymer.IronControlState = { 3270 },
3710 3271 _focusPrevious: function() {
3711 properties: { 3272 var length = this.items.length;
3712 3273 var curFocusIndex = Number(this.indexOf(this.focusedItem));
3713 /** 3274 for (var i = 1; i < length + 1; i++) {
3714 * If true, the element currently has focus. 3275 var item = this.items[(curFocusIndex - i + length) % length];
3715 */ 3276 if (!item.hasAttribute('disabled')) {
3716 focused: { 3277 this._setFocusedItem(item);
3717 type: Boolean,
3718 value: false,
3719 notify: true,
3720 readOnly: true,
3721 reflectToAttribute: true
3722 },
3723
3724 /**
3725 * If true, the user cannot interact with this element.
3726 */
3727 disabled: {
3728 type: Boolean,
3729 value: false,
3730 notify: true,
3731 observer: '_disabledChanged',
3732 reflectToAttribute: true
3733 },
3734
3735 _oldTabIndex: {
3736 type: Number
3737 },
3738
3739 _boundFocusBlurHandler: {
3740 type: Function,
3741 value: function() {
3742 return this._focusBlurHandler.bind(this);
3743 }
3744 }
3745
3746 },
3747
3748 observers: [
3749 '_changedControlState(focused, disabled)'
3750 ],
3751
3752 ready: function() {
3753 this.addEventListener('focus', this._boundFocusBlurHandler, true);
3754 this.addEventListener('blur', this._boundFocusBlurHandler, true);
3755 },
3756
3757 _focusBlurHandler: function(event) {
3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
3759 // eventually become `this` due to retargeting; if we are not in
3760 // ShadowDOM land, `event.target` will eventually become `this` due
3761 // to the second conditional which fires a synthetic event (that is also
3762 // handled). In either case, we can disregard `event.path`.
3763
3764 if (event.target === this) {
3765 this._setFocused(event.type === 'focus');
3766 } else if (!this.shadowRoot) {
3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget);
3768 if (!this.isLightDescendant(target)) {
3769 this.fire(event.type, {sourceEvent: event}, {
3770 node: this,
3771 bubbles: event.bubbles,
3772 cancelable: event.cancelable
3773 });
3774 }
3775 }
3776 },
3777
3778 _disabledChanged: function(disabled, old) {
3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
3780 this.style.pointerEvents = disabled ? 'none' : '';
3781 if (disabled) {
3782 this._oldTabIndex = this.tabIndex;
3783 this._setFocused(false);
3784 this.tabIndex = -1;
3785 this.blur();
3786 } else if (this._oldTabIndex !== undefined) {
3787 this.tabIndex = this._oldTabIndex;
3788 }
3789 },
3790
3791 _changedControlState: function() {
3792 // _controlStateChanged is abstract, follow-on behaviors may implement it
3793 if (this._controlStateChanged) {
3794 this._controlStateChanged();
3795 }
3796 }
3797
3798 };
3799 /**
3800 * @demo demo/index.html
3801 * @polymerBehavior Polymer.IronButtonState
3802 */
3803 Polymer.IronButtonStateImpl = {
3804
3805 properties: {
3806
3807 /**
3808 * If true, the user is currently holding down the button.
3809 */
3810 pressed: {
3811 type: Boolean,
3812 readOnly: true,
3813 value: false,
3814 reflectToAttribute: true,
3815 observer: '_pressedChanged'
3816 },
3817
3818 /**
3819 * If true, the button toggles the active state with each tap or press
3820 * of the spacebar.
3821 */
3822 toggles: {
3823 type: Boolean,
3824 value: false,
3825 reflectToAttribute: true
3826 },
3827
3828 /**
3829 * If true, the button is a toggle and is currently in the active state.
3830 */
3831 active: {
3832 type: Boolean,
3833 value: false,
3834 notify: true,
3835 reflectToAttribute: true
3836 },
3837
3838 /**
3839 * True if the element is currently being pressed by a "pointer," which
3840 * is loosely defined as mouse or touch input (but specifically excluding
3841 * keyboard input).
3842 */
3843 pointerDown: {
3844 type: Boolean,
3845 readOnly: true,
3846 value: false
3847 },
3848
3849 /**
3850 * True if the input device that caused the element to receive focus
3851 * was a keyboard.
3852 */
3853 receivedFocusFromKeyboard: {
3854 type: Boolean,
3855 readOnly: true
3856 },
3857
3858 /**
3859 * The aria attribute to be set if the button is a toggle and in the
3860 * active state.
3861 */
3862 ariaActiveAttribute: {
3863 type: String,
3864 value: 'aria-pressed',
3865 observer: '_ariaActiveAttributeChanged'
3866 }
3867 },
3868
3869 listeners: {
3870 down: '_downHandler',
3871 up: '_upHandler',
3872 tap: '_tapHandler'
3873 },
3874
3875 observers: [
3876 '_detectKeyboardFocus(focused)',
3877 '_activeChanged(active, ariaActiveAttribute)'
3878 ],
3879
3880 keyBindings: {
3881 'enter:keydown': '_asyncClick',
3882 'space:keydown': '_spaceKeyDownHandler',
3883 'space:keyup': '_spaceKeyUpHandler',
3884 },
3885
3886 _mouseEventRe: /^mouse/,
3887
3888 _tapHandler: function() {
3889 if (this.toggles) {
3890 // a tap is needed to toggle the active state
3891 this._userActivate(!this.active);
3892 } else {
3893 this.active = false;
3894 }
3895 },
3896
3897 _detectKeyboardFocus: function(focused) {
3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
3899 },
3900
3901 // to emulate native checkbox, (de-)activations from a user interaction fire
3902 // 'change' events
3903 _userActivate: function(active) {
3904 if (this.active !== active) {
3905 this.active = active;
3906 this.fire('change');
3907 }
3908 },
3909
3910 _downHandler: function(event) {
3911 this._setPointerDown(true);
3912 this._setPressed(true);
3913 this._setReceivedFocusFromKeyboard(false);
3914 },
3915
3916 _upHandler: function() {
3917 this._setPointerDown(false);
3918 this._setPressed(false);
3919 },
3920
3921 /**
3922 * @param {!KeyboardEvent} event .
3923 */
3924 _spaceKeyDownHandler: function(event) {
3925 var keyboardEvent = event.detail.keyboardEvent;
3926 var target = Polymer.dom(keyboardEvent).localTarget;
3927
3928 // Ignore the event if this is coming from a focused light child, since th at
3929 // element will deal with it.
3930 if (this.isLightDescendant(/** @type {Node} */(target)))
3931 return; 3278 return;
3932 3279 }
3933 keyboardEvent.preventDefault(); 3280 }
3934 keyboardEvent.stopImmediatePropagation(); 3281 },
3935 this._setPressed(true); 3282 _focusNext: function() {
3936 }, 3283 var length = this.items.length;
3937 3284 var curFocusIndex = Number(this.indexOf(this.focusedItem));
3938 /** 3285 for (var i = 1; i < length + 1; i++) {
3939 * @param {!KeyboardEvent} event . 3286 var item = this.items[(curFocusIndex + i) % length];
3940 */ 3287 if (!item.hasAttribute('disabled')) {
3941 _spaceKeyUpHandler: function(event) { 3288 this._setFocusedItem(item);
3942 var keyboardEvent = event.detail.keyboardEvent;
3943 var target = Polymer.dom(keyboardEvent).localTarget;
3944
3945 // Ignore the event if this is coming from a focused light child, since th at
3946 // element will deal with it.
3947 if (this.isLightDescendant(/** @type {Node} */(target)))
3948 return; 3289 return;
3949 3290 }
3950 if (this.pressed) { 3291 }
3951 this._asyncClick(); 3292 },
3952 } 3293 _applySelection: function(item, isSelected) {
3953 this._setPressed(false); 3294 if (isSelected) {
3954 }, 3295 item.setAttribute('aria-selected', 'true');
3955 3296 } else {
3956 // trigger click asynchronously, the asynchrony is useful to allow one 3297 item.removeAttribute('aria-selected');
3957 // event handler to unwind before triggering another event 3298 }
3958 _asyncClick: function() { 3299 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
3959 this.async(function() { 3300 },
3960 this.click(); 3301 _focusedItemChanged: function(focusedItem, old) {
3961 }, 1); 3302 old && old.setAttribute('tabindex', '-1');
3962 }, 3303 if (focusedItem) {
3963 3304 focusedItem.setAttribute('tabindex', '0');
3964 // any of these changes are considered a change to button state 3305 focusedItem.focus();
3965 3306 }
3966 _pressedChanged: function(pressed) { 3307 },
3967 this._changedButtonState(); 3308 _onIronItemsChanged: function(event) {
3968 }, 3309 if (event.detail.addedNodes.length) {
3969 3310 this._resetTabindices();
3970 _ariaActiveAttributeChanged: function(value, oldValue) { 3311 }
3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { 3312 },
3972 this.removeAttribute(oldValue); 3313 _onShiftTabDown: function(event) {
3973 } 3314 var oldTabIndex = this.getAttribute('tabindex');
3974 }, 3315 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
3975 3316 this._setFocusedItem(null);
3976 _activeChanged: function(active, ariaActiveAttribute) { 3317 this.setAttribute('tabindex', '-1');
3977 if (this.toggles) { 3318 this.async(function() {
3978 this.setAttribute(this.ariaActiveAttribute, 3319 this.setAttribute('tabindex', oldTabIndex);
3979 active ? 'true' : 'false'); 3320 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
3980 } else { 3321 }, 1);
3981 this.removeAttribute(this.ariaActiveAttribute); 3322 },
3982 } 3323 _onFocus: function(event) {
3983 this._changedButtonState(); 3324 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
3984 }, 3325 return;
3985 3326 }
3986 _controlStateChanged: function() { 3327 var rootTarget = Polymer.dom(event).rootTarget;
3987 if (this.disabled) { 3328 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th is.isLightDescendant(rootTarget)) {
3988 this._setPressed(false); 3329 return;
3989 } else { 3330 }
3990 this._changedButtonState(); 3331 this._defaultFocusAsync = this.async(function() {
3991 } 3332 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0 ] : this.selectedItem;
3992 }, 3333 this._setFocusedItem(null);
3993 3334 if (selectedItem) {
3994 // provide hook for follow-on behaviors to react to button-state 3335 this._setFocusedItem(selectedItem);
3995 3336 } else if (this.items[0]) {
3996 _changedButtonState: function() { 3337 this._focusNext();
3997 if (this._buttonStateChanged) { 3338 }
3998 this._buttonStateChanged(); // abstract
3999 }
4000 }
4001
4002 };
4003
4004 /** @polymerBehavior */
4005 Polymer.IronButtonState = [
4006 Polymer.IronA11yKeysBehavior,
4007 Polymer.IronButtonStateImpl
4008 ];
4009 (function() {
4010 var Utility = {
4011 distance: function(x1, y1, x2, y2) {
4012 var xDelta = (x1 - x2);
4013 var yDelta = (y1 - y2);
4014
4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
4016 },
4017
4018 now: window.performance && window.performance.now ?
4019 window.performance.now.bind(window.performance) : Date.now
4020 };
4021
4022 /**
4023 * @param {HTMLElement} element
4024 * @constructor
4025 */
4026 function ElementMetrics(element) {
4027 this.element = element;
4028 this.width = this.boundingRect.width;
4029 this.height = this.boundingRect.height;
4030
4031 this.size = Math.max(this.width, this.height);
4032 }
4033
4034 ElementMetrics.prototype = {
4035 get boundingRect () {
4036 return this.element.getBoundingClientRect();
4037 },
4038
4039 furthestCornerDistanceFrom: function(x, y) {
4040 var topLeft = Utility.distance(x, y, 0, 0);
4041 var topRight = Utility.distance(x, y, this.width, 0);
4042 var bottomLeft = Utility.distance(x, y, 0, this.height);
4043 var bottomRight = Utility.distance(x, y, this.width, this.height);
4044
4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
4046 }
4047 };
4048
4049 /**
4050 * @param {HTMLElement} element
4051 * @constructor
4052 */
4053 function Ripple(element) {
4054 this.element = element;
4055 this.color = window.getComputedStyle(element).color;
4056
4057 this.wave = document.createElement('div');
4058 this.waveContainer = document.createElement('div');
4059 this.wave.style.backgroundColor = this.color;
4060 this.wave.classList.add('wave');
4061 this.waveContainer.classList.add('wave-container');
4062 Polymer.dom(this.waveContainer).appendChild(this.wave);
4063
4064 this.resetInteractionState();
4065 }
4066
4067 Ripple.MAX_RADIUS = 300;
4068
4069 Ripple.prototype = {
4070 get recenters() {
4071 return this.element.recenters;
4072 },
4073
4074 get center() {
4075 return this.element.center;
4076 },
4077
4078 get mouseDownElapsed() {
4079 var elapsed;
4080
4081 if (!this.mouseDownStart) {
4082 return 0;
4083 }
4084
4085 elapsed = Utility.now() - this.mouseDownStart;
4086
4087 if (this.mouseUpStart) {
4088 elapsed -= this.mouseUpElapsed;
4089 }
4090
4091 return elapsed;
4092 },
4093
4094 get mouseUpElapsed() {
4095 return this.mouseUpStart ?
4096 Utility.now () - this.mouseUpStart : 0;
4097 },
4098
4099 get mouseDownElapsedSeconds() {
4100 return this.mouseDownElapsed / 1000;
4101 },
4102
4103 get mouseUpElapsedSeconds() {
4104 return this.mouseUpElapsed / 1000;
4105 },
4106
4107 get mouseInteractionSeconds() {
4108 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
4109 },
4110
4111 get initialOpacity() {
4112 return this.element.initialOpacity;
4113 },
4114
4115 get opacityDecayVelocity() {
4116 return this.element.opacityDecayVelocity;
4117 },
4118
4119 get radius() {
4120 var width2 = this.containerMetrics.width * this.containerMetrics.width;
4121 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
4122 var waveRadius = Math.min(
4123 Math.sqrt(width2 + height2),
4124 Ripple.MAX_RADIUS
4125 ) * 1.1 + 5;
4126
4127 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
4128 var timeNow = this.mouseInteractionSeconds / duration;
4129 var size = waveRadius * (1 - Math.pow(80, -timeNow));
4130
4131 return Math.abs(size);
4132 },
4133
4134 get opacity() {
4135 if (!this.mouseUpStart) {
4136 return this.initialOpacity;
4137 }
4138
4139 return Math.max(
4140 0,
4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
4142 );
4143 },
4144
4145 get outerOpacity() {
4146 // Linear increase in background opacity, capped at the opacity
4147 // of the wavefront (waveOpacity).
4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
4149 var waveOpacity = this.opacity;
4150
4151 return Math.max(
4152 0,
4153 Math.min(outerOpacity, waveOpacity)
4154 );
4155 },
4156
4157 get isOpacityFullyDecayed() {
4158 return this.opacity < 0.01 &&
4159 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
4160 },
4161
4162 get isRestingAtMaxRadius() {
4163 return this.opacity >= this.initialOpacity &&
4164 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
4165 },
4166
4167 get isAnimationComplete() {
4168 return this.mouseUpStart ?
4169 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
4170 },
4171
4172 get translationFraction() {
4173 return Math.min(
4174 1,
4175 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
4176 );
4177 },
4178
4179 get xNow() {
4180 if (this.xEnd) {
4181 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
4182 }
4183
4184 return this.xStart;
4185 },
4186
4187 get yNow() {
4188 if (this.yEnd) {
4189 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
4190 }
4191
4192 return this.yStart;
4193 },
4194
4195 get isMouseDown() {
4196 return this.mouseDownStart && !this.mouseUpStart;
4197 },
4198
4199 resetInteractionState: function() {
4200 this.maxRadius = 0;
4201 this.mouseDownStart = 0;
4202 this.mouseUpStart = 0;
4203
4204 this.xStart = 0;
4205 this.yStart = 0;
4206 this.xEnd = 0;
4207 this.yEnd = 0;
4208 this.slideDistance = 0;
4209
4210 this.containerMetrics = new ElementMetrics(this.element);
4211 },
4212
4213 draw: function() {
4214 var scale;
4215 var translateString;
4216 var dx;
4217 var dy;
4218
4219 this.wave.style.opacity = this.opacity;
4220
4221 scale = this.radius / (this.containerMetrics.size / 2);
4222 dx = this.xNow - (this.containerMetrics.width / 2);
4223 dy = this.yNow - (this.containerMetrics.height / 2);
4224
4225
4226 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
4227 // https://bugs.webkit.org/show_bug.cgi?id=98538
4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
4232 },
4233
4234 /** @param {Event=} event */
4235 downAction: function(event) {
4236 var xCenter = this.containerMetrics.width / 2;
4237 var yCenter = this.containerMetrics.height / 2;
4238
4239 this.resetInteractionState();
4240 this.mouseDownStart = Utility.now();
4241
4242 if (this.center) {
4243 this.xStart = xCenter;
4244 this.yStart = yCenter;
4245 this.slideDistance = Utility.distance(
4246 this.xStart, this.yStart, this.xEnd, this.yEnd
4247 );
4248 } else {
4249 this.xStart = event ?
4250 event.detail.x - this.containerMetrics.boundingRect.left :
4251 this.containerMetrics.width / 2;
4252 this.yStart = event ?
4253 event.detail.y - this.containerMetrics.boundingRect.top :
4254 this.containerMetrics.height / 2;
4255 }
4256
4257 if (this.recenters) {
4258 this.xEnd = xCenter;
4259 this.yEnd = yCenter;
4260 this.slideDistance = Utility.distance(
4261 this.xStart, this.yStart, this.xEnd, this.yEnd
4262 );
4263 }
4264
4265 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
4266 this.xStart,
4267 this.yStart
4268 );
4269
4270 this.waveContainer.style.top =
4271 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
4272 this.waveContainer.style.left =
4273 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
4274
4275 this.waveContainer.style.width = this.containerMetrics.size + 'px';
4276 this.waveContainer.style.height = this.containerMetrics.size + 'px';
4277 },
4278
4279 /** @param {Event=} event */
4280 upAction: function(event) {
4281 if (!this.isMouseDown) {
4282 return;
4283 }
4284
4285 this.mouseUpStart = Utility.now();
4286 },
4287
4288 remove: function() {
4289 Polymer.dom(this.waveContainer.parentNode).removeChild(
4290 this.waveContainer
4291 );
4292 }
4293 };
4294
4295 Polymer({
4296 is: 'paper-ripple',
4297
4298 behaviors: [
4299 Polymer.IronA11yKeysBehavior
4300 ],
4301
4302 properties: {
4303 /**
4304 * The initial opacity set on the wave.
4305 *
4306 * @attribute initialOpacity
4307 * @type number
4308 * @default 0.25
4309 */
4310 initialOpacity: {
4311 type: Number,
4312 value: 0.25
4313 },
4314
4315 /**
4316 * How fast (opacity per second) the wave fades out.
4317 *
4318 * @attribute opacityDecayVelocity
4319 * @type number
4320 * @default 0.8
4321 */
4322 opacityDecayVelocity: {
4323 type: Number,
4324 value: 0.8
4325 },
4326
4327 /**
4328 * If true, ripples will exhibit a gravitational pull towards
4329 * the center of their container as they fade away.
4330 *
4331 * @attribute recenters
4332 * @type boolean
4333 * @default false
4334 */
4335 recenters: {
4336 type: Boolean,
4337 value: false
4338 },
4339
4340 /**
4341 * If true, ripples will center inside its container
4342 *
4343 * @attribute recenters
4344 * @type boolean
4345 * @default false
4346 */
4347 center: {
4348 type: Boolean,
4349 value: false
4350 },
4351
4352 /**
4353 * A list of the visual ripples.
4354 *
4355 * @attribute ripples
4356 * @type Array
4357 * @default []
4358 */
4359 ripples: {
4360 type: Array,
4361 value: function() {
4362 return [];
4363 }
4364 },
4365
4366 /**
4367 * True when there are visible ripples animating within the
4368 * element.
4369 */
4370 animating: {
4371 type: Boolean,
4372 readOnly: true,
4373 reflectToAttribute: true,
4374 value: false
4375 },
4376
4377 /**
4378 * If true, the ripple will remain in the "down" state until `holdDown`
4379 * is set to false again.
4380 */
4381 holdDown: {
4382 type: Boolean,
4383 value: false,
4384 observer: '_holdDownChanged'
4385 },
4386
4387 /**
4388 * If true, the ripple will not generate a ripple effect
4389 * via pointer interaction.
4390 * Calling ripple's imperative api like `simulatedRipple` will
4391 * still generate the ripple effect.
4392 */
4393 noink: {
4394 type: Boolean,
4395 value: false
4396 },
4397
4398 _animating: {
4399 type: Boolean
4400 },
4401
4402 _boundAnimate: {
4403 type: Function,
4404 value: function() {
4405 return this.animate.bind(this);
4406 }
4407 }
4408 },
4409
4410 get target () {
4411 return this.keyEventTarget;
4412 },
4413
4414 keyBindings: {
4415 'enter:keydown': '_onEnterKeydown',
4416 'space:keydown': '_onSpaceKeydown',
4417 'space:keyup': '_onSpaceKeyup'
4418 },
4419
4420 attached: function() {
4421 // Set up a11yKeysBehavior to listen to key events on the target,
4422 // so that space and enter activate the ripple even if the target doesn' t
4423 // handle key events. The key handlers deal with `noink` themselves.
4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
4426 } else {
4427 this.keyEventTarget = this.parentNode;
4428 }
4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
4430 this.listen(keyEventTarget, 'up', 'uiUpAction');
4431 this.listen(keyEventTarget, 'down', 'uiDownAction');
4432 },
4433
4434 detached: function() {
4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
4437 this.keyEventTarget = null;
4438 },
4439
4440 get shouldKeepAnimating () {
4441 for (var index = 0; index < this.ripples.length; ++index) {
4442 if (!this.ripples[index].isAnimationComplete) {
4443 return true;
4444 }
4445 }
4446
4447 return false;
4448 },
4449
4450 simulatedRipple: function() {
4451 this.downAction(null);
4452
4453 // Please see polymer/polymer#1305
4454 this.async(function() {
4455 this.upAction();
4456 }, 1);
4457 },
4458
4459 /**
4460 * Provokes a ripple down effect via a UI event,
4461 * respecting the `noink` property.
4462 * @param {Event=} event
4463 */
4464 uiDownAction: function(event) {
4465 if (!this.noink) {
4466 this.downAction(event);
4467 }
4468 },
4469
4470 /**
4471 * Provokes a ripple down effect via a UI event,
4472 * *not* respecting the `noink` property.
4473 * @param {Event=} event
4474 */
4475 downAction: function(event) {
4476 if (this.holdDown && this.ripples.length > 0) {
4477 return;
4478 }
4479
4480 var ripple = this.addRipple();
4481
4482 ripple.downAction(event);
4483
4484 if (!this._animating) {
4485 this._animating = true;
4486 this.animate();
4487 }
4488 },
4489
4490 /**
4491 * Provokes a ripple up effect via a UI event,
4492 * respecting the `noink` property.
4493 * @param {Event=} event
4494 */
4495 uiUpAction: function(event) {
4496 if (!this.noink) {
4497 this.upAction(event);
4498 }
4499 },
4500
4501 /**
4502 * Provokes a ripple up effect via a UI event,
4503 * *not* respecting the `noink` property.
4504 * @param {Event=} event
4505 */
4506 upAction: function(event) {
4507 if (this.holdDown) {
4508 return;
4509 }
4510
4511 this.ripples.forEach(function(ripple) {
4512 ripple.upAction(event);
4513 });
4514
4515 this._animating = true;
4516 this.animate();
4517 },
4518
4519 onAnimationComplete: function() {
4520 this._animating = false;
4521 this.$.background.style.backgroundColor = null;
4522 this.fire('transitionend');
4523 },
4524
4525 addRipple: function() {
4526 var ripple = new Ripple(this);
4527
4528 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
4529 this.$.background.style.backgroundColor = ripple.color;
4530 this.ripples.push(ripple);
4531
4532 this._setAnimating(true);
4533
4534 return ripple;
4535 },
4536
4537 removeRipple: function(ripple) {
4538 var rippleIndex = this.ripples.indexOf(ripple);
4539
4540 if (rippleIndex < 0) {
4541 return;
4542 }
4543
4544 this.ripples.splice(rippleIndex, 1);
4545
4546 ripple.remove();
4547
4548 if (!this.ripples.length) {
4549 this._setAnimating(false);
4550 }
4551 },
4552
4553 animate: function() {
4554 if (!this._animating) {
4555 return;
4556 }
4557 var index;
4558 var ripple;
4559
4560 for (index = 0; index < this.ripples.length; ++index) {
4561 ripple = this.ripples[index];
4562
4563 ripple.draw();
4564
4565 this.$.background.style.opacity = ripple.outerOpacity;
4566
4567 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
4568 this.removeRipple(ripple);
4569 }
4570 }
4571
4572 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
4573 this.onAnimationComplete();
4574 } else {
4575 window.requestAnimationFrame(this._boundAnimate);
4576 }
4577 },
4578
4579 _onEnterKeydown: function() {
4580 this.uiDownAction();
4581 this.async(this.uiUpAction, 1);
4582 },
4583
4584 _onSpaceKeydown: function() {
4585 this.uiDownAction();
4586 },
4587
4588 _onSpaceKeyup: function() {
4589 this.uiUpAction();
4590 },
4591
4592 // note: holdDown does not respect noink since it can be a focus based
4593 // effect.
4594 _holdDownChanged: function(newVal, oldVal) {
4595 if (oldVal === undefined) {
4596 return;
4597 }
4598 if (newVal) {
4599 this.downAction();
4600 } else {
4601 this.upAction();
4602 }
4603 }
4604
4605 /**
4606 Fired when the animation finishes.
4607 This is useful if you want to wait until
4608 the ripple animation finishes to perform some action.
4609
4610 @event transitionend
4611 @param {{node: Object}} detail Contains the animated node.
4612 */
4613 }); 3339 });
4614 })(); 3340 },
4615 /** 3341 _onUpKey: function(event) {
4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple 3342 this._focusPrevious();
4617 * when the element has focus via pointer or keyboard. 3343 event.detail.keyboardEvent.preventDefault();
4618 * 3344 },
4619 * NOTE: This behavior is intended to be used in conjunction with and after 3345 _onDownKey: function(event) {
4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`. 3346 this._focusNext();
4621 * 3347 event.detail.keyboardEvent.preventDefault();
4622 * @polymerBehavior Polymer.PaperRippleBehavior 3348 },
4623 */ 3349 _onEscKey: function(event) {
4624 Polymer.PaperRippleBehavior = { 3350 this.focusedItem.blur();
4625 properties: { 3351 },
4626 /** 3352 _onKeydown: function(event) {
4627 * If true, the element will not produce a ripple effect when interacted 3353 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
4628 * with via the pointer. 3354 this._focusWithKeyboardEvent(event);
4629 */ 3355 }
4630 noink: { 3356 event.stopPropagation();
4631 type: Boolean, 3357 },
4632 observer: '_noinkChanged' 3358 _activateHandler: function(event) {
4633 }, 3359 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
4634 3360 event.stopPropagation();
4635 /** 3361 }
4636 * @type {Element|undefined} 3362 };
4637 */ 3363
4638 _rippleContainer: { 3364 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
4639 type: Object, 3365
4640 } 3366 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1 1yKeysBehavior, Polymer.IronMenuBehaviorImpl ];
4641 }, 3367
4642 3368 Polymer.IronMenubarBehaviorImpl = {
4643 /** 3369 hostAttributes: {
4644 * Ensures a `<paper-ripple>` element is available when the element is 3370 role: 'menubar'
4645 * focused. 3371 },
4646 */ 3372 keyBindings: {
4647 _buttonStateChanged: function() { 3373 left: '_onLeftKey',
4648 if (this.focused) { 3374 right: '_onRightKey'
4649 this.ensureRipple(); 3375 },
4650 } 3376 _onUpKey: function(event) {
4651 }, 3377 this.focusedItem.click();
4652 3378 event.detail.keyboardEvent.preventDefault();
4653 /** 3379 },
4654 * In addition to the functionality provided in `IronButtonState`, ensures 3380 _onDownKey: function(event) {
4655 * a ripple effect is created when the element is in a `pressed` state. 3381 this.focusedItem.click();
4656 */ 3382 event.detail.keyboardEvent.preventDefault();
4657 _downHandler: function(event) { 3383 },
4658 Polymer.IronButtonStateImpl._downHandler.call(this, event); 3384 get _isRTL() {
4659 if (this.pressed) { 3385 return window.getComputedStyle(this)['direction'] === 'rtl';
4660 this.ensureRipple(event); 3386 },
4661 } 3387 _onLeftKey: function(event) {
4662 }, 3388 if (this._isRTL) {
4663 3389 this._focusNext();
4664 /** 3390 } else {
4665 * Ensures this element contains a ripple effect. For startup efficiency 3391 this._focusPrevious();
4666 * the ripple effect is dynamically on demand when needed. 3392 }
4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the 3393 event.detail.keyboardEvent.preventDefault();
4668 * ripple. 3394 },
4669 */ 3395 _onRightKey: function(event) {
4670 ensureRipple: function(optTriggeringEvent) { 3396 if (this._isRTL) {
4671 if (!this.hasRipple()) { 3397 this._focusPrevious();
4672 this._ripple = this._createRipple(); 3398 } else {
4673 this._ripple.noink = this.noink; 3399 this._focusNext();
4674 var rippleContainer = this._rippleContainer || this.root; 3400 }
4675 if (rippleContainer) { 3401 event.detail.keyboardEvent.preventDefault();
4676 Polymer.dom(rippleContainer).appendChild(this._ripple); 3402 },
4677 } 3403 _onKeydown: function(event) {
4678 if (optTriggeringEvent) { 3404 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
4679 // Check if the event happened inside of the ripple container 3405 return;
4680 // Fall back to host instead of the root because distributed text 3406 }
4681 // nodes are not valid event targets 3407 this._focusWithKeyboardEvent(event);
4682 var domContainer = Polymer.dom(this._rippleContainer || this); 3408 }
4683 var target = Polymer.dom(optTriggeringEvent).rootTarget; 3409 };
4684 if (domContainer.deepContains( /** @type {Node} */(target))) { 3410
4685 this._ripple.uiDownAction(optTriggeringEvent); 3411 Polymer.IronMenubarBehavior = [ Polymer.IronMenuBehavior, Polymer.IronMenubarBeh aviorImpl ];
4686 } 3412
4687 }
4688 }
4689 },
4690
4691 /**
4692 * Returns the `<paper-ripple>` element used by this element to create
4693 * ripple effects. The element's ripple is created on demand, when
4694 * necessary, and calling this method will force the
4695 * ripple to be created.
4696 */
4697 getRipple: function() {
4698 this.ensureRipple();
4699 return this._ripple;
4700 },
4701
4702 /**
4703 * Returns true if this element currently contains a ripple effect.
4704 * @return {boolean}
4705 */
4706 hasRipple: function() {
4707 return Boolean(this._ripple);
4708 },
4709
4710 /**
4711 * Create the element's ripple effect via creating a `<paper-ripple>`.
4712 * Override this method to customize the ripple element.
4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
4714 */
4715 _createRipple: function() {
4716 return /** @type {!PaperRippleElement} */ (
4717 document.createElement('paper-ripple'));
4718 },
4719
4720 _noinkChanged: function(noink) {
4721 if (this.hasRipple()) {
4722 this._ripple.noink = noink;
4723 }
4724 }
4725 };
4726 /** @polymerBehavior Polymer.PaperButtonBehavior */
4727 Polymer.PaperButtonBehaviorImpl = {
4728 properties: {
4729 /**
4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the
4731 * shadow, and each increasing number greater than 0 will be "deeper"
4732 * than the last.
4733 *
4734 * @attribute elevation
4735 * @type number
4736 * @default 1
4737 */
4738 elevation: {
4739 type: Number,
4740 reflectToAttribute: true,
4741 readOnly: true
4742 }
4743 },
4744
4745 observers: [
4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
4747 '_computeKeyboardClass(receivedFocusFromKeyboard)'
4748 ],
4749
4750 hostAttributes: {
4751 role: 'button',
4752 tabindex: '0',
4753 animated: true
4754 },
4755
4756 _calculateElevation: function() {
4757 var e = 1;
4758 if (this.disabled) {
4759 e = 0;
4760 } else if (this.active || this.pressed) {
4761 e = 4;
4762 } else if (this.receivedFocusFromKeyboard) {
4763 e = 3;
4764 }
4765 this._setElevation(e);
4766 },
4767
4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
4770 },
4771
4772 /**
4773 * In addition to `IronButtonState` behavior, when space key goes down,
4774 * create a ripple down effect.
4775 *
4776 * @param {!KeyboardEvent} event .
4777 */
4778 _spaceKeyDownHandler: function(event) {
4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
4780 // Ensure that there is at most one ripple when the space key is held down .
4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
4782 this._ripple.uiDownAction();
4783 }
4784 },
4785
4786 /**
4787 * In addition to `IronButtonState` behavior, when space key goes up,
4788 * create a ripple up effect.
4789 *
4790 * @param {!KeyboardEvent} event .
4791 */
4792 _spaceKeyUpHandler: function(event) {
4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
4794 if (this.hasRipple()) {
4795 this._ripple.uiUpAction();
4796 }
4797 }
4798 };
4799
4800 /** @polymerBehavior */
4801 Polymer.PaperButtonBehavior = [
4802 Polymer.IronButtonState,
4803 Polymer.IronControlState,
4804 Polymer.PaperRippleBehavior,
4805 Polymer.PaperButtonBehaviorImpl
4806 ];
4807 Polymer({ 3413 Polymer({
4808 is: 'paper-button', 3414 is: 'iron-iconset-svg',
4809 3415 properties: {
4810 behaviors: [ 3416 name: {
4811 Polymer.PaperButtonBehavior 3417 type: String,
4812 ], 3418 observer: '_nameChanged'
4813 3419 },
4814 properties: { 3420 size: {
4815 /** 3421 type: Number,
4816 * If true, the button should be styled with a shadow. 3422 value: 24
4817 */ 3423 }
4818 raised: { 3424 },
4819 type: Boolean, 3425 attached: function() {
4820 reflectToAttribute: true, 3426 this.style.display = 'none';
4821 value: false, 3427 },
4822 observer: '_calculateElevation' 3428 getIconNames: function() {
4823 } 3429 this._icons = this._createIconMap();
4824 }, 3430 return Object.keys(this._icons).map(function(n) {
4825 3431 return this.name + ':' + n;
4826 _calculateElevation: function() { 3432 }, this);
4827 if (!this.raised) { 3433 },
4828 this._setElevation(0); 3434 applyIcon: function(element, iconName) {
4829 } else { 3435 element = element.root || element;
4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); 3436 this.removeIcon(element);
4831 } 3437 var svg = this._cloneIcon(iconName);
4832 } 3438 if (svg) {
4833 3439 var pde = Polymer.dom(element);
4834 /** 3440 pde.insertBefore(svg, pde.childNodes[0]);
4835 Fired when the animation finishes. 3441 return element._svgIcon = svg;
4836 This is useful if you want to wait until 3442 }
4837 the ripple animation finishes to perform some action. 3443 return null;
4838 3444 },
4839 @event transitionend 3445 removeIcon: function(element) {
4840 Event param: {{node: Object}} detail Contains the animated node. 3446 if (element._svgIcon) {
4841 */ 3447 Polymer.dom(element).removeChild(element._svgIcon);
3448 element._svgIcon = null;
3449 }
3450 },
3451 _nameChanged: function() {
3452 new Polymer.IronMeta({
3453 type: 'iconset',
3454 key: this.name,
3455 value: this
4842 }); 3456 });
4843 (function() { 3457 this.async(function() {
4844 3458 this.fire('iron-iconset-added', this, {
4845 // monostate data 3459 node: window
4846 var metaDatas = {}; 3460 });
4847 var metaArrays = {};
4848 var singleton = null;
4849
4850 Polymer.IronMeta = Polymer({
4851
4852 is: 'iron-meta',
4853
4854 properties: {
4855
4856 /**
4857 * The type of meta-data. All meta-data of the same type is stored
4858 * together.
4859 */
4860 type: {
4861 type: String,
4862 value: 'default',
4863 observer: '_typeChanged'
4864 },
4865
4866 /**
4867 * The key used to store `value` under the `type` namespace.
4868 */
4869 key: {
4870 type: String,
4871 observer: '_keyChanged'
4872 },
4873
4874 /**
4875 * The meta-data to store or retrieve.
4876 */
4877 value: {
4878 type: Object,
4879 notify: true,
4880 observer: '_valueChanged'
4881 },
4882
4883 /**
4884 * If true, `value` is set to the iron-meta instance itself.
4885 */
4886 self: {
4887 type: Boolean,
4888 observer: '_selfChanged'
4889 },
4890
4891 /**
4892 * Array of all meta-data values for the given type.
4893 */
4894 list: {
4895 type: Array,
4896 notify: true
4897 }
4898
4899 },
4900
4901 hostAttributes: {
4902 hidden: true
4903 },
4904
4905 /**
4906 * Only runs if someone invokes the factory/constructor directly
4907 * e.g. `new Polymer.IronMeta()`
4908 *
4909 * @param {{type: (string|undefined), key: (string|undefined), value}=} co nfig
4910 */
4911 factoryImpl: function(config) {
4912 if (config) {
4913 for (var n in config) {
4914 switch(n) {
4915 case 'type':
4916 case 'key':
4917 case 'value':
4918 this[n] = config[n];
4919 break;
4920 }
4921 }
4922 }
4923 },
4924
4925 created: function() {
4926 // TODO(sjmiles): good for debugging?
4927 this._metaDatas = metaDatas;
4928 this._metaArrays = metaArrays;
4929 },
4930
4931 _keyChanged: function(key, old) {
4932 this._resetRegistration(old);
4933 },
4934
4935 _valueChanged: function(value) {
4936 this._resetRegistration(this.key);
4937 },
4938
4939 _selfChanged: function(self) {
4940 if (self) {
4941 this.value = this;
4942 }
4943 },
4944
4945 _typeChanged: function(type) {
4946 this._unregisterKey(this.key);
4947 if (!metaDatas[type]) {
4948 metaDatas[type] = {};
4949 }
4950 this._metaData = metaDatas[type];
4951 if (!metaArrays[type]) {
4952 metaArrays[type] = [];
4953 }
4954 this.list = metaArrays[type];
4955 this._registerKeyValue(this.key, this.value);
4956 },
4957
4958 /**
4959 * Retrieves meta data value by key.
4960 *
4961 * @method byKey
4962 * @param {string} key The key of the meta-data to be returned.
4963 * @return {*}
4964 */
4965 byKey: function(key) {
4966 return this._metaData && this._metaData[key];
4967 },
4968
4969 _resetRegistration: function(oldKey) {
4970 this._unregisterKey(oldKey);
4971 this._registerKeyValue(this.key, this.value);
4972 },
4973
4974 _unregisterKey: function(key) {
4975 this._unregister(key, this._metaData, this.list);
4976 },
4977
4978 _registerKeyValue: function(key, value) {
4979 this._register(key, value, this._metaData, this.list);
4980 },
4981
4982 _register: function(key, value, data, list) {
4983 if (key && data && value !== undefined) {
4984 data[key] = value;
4985 list.push(value);
4986 }
4987 },
4988
4989 _unregister: function(key, data, list) {
4990 if (key && data) {
4991 if (key in data) {
4992 var value = data[key];
4993 delete data[key];
4994 this.arrayDelete(list, value);
4995 }
4996 }
4997 }
4998
4999 }); 3461 });
5000 3462 },
5001 Polymer.IronMeta.getIronMeta = function getIronMeta() { 3463 _createIconMap: function() {
5002 if (singleton === null) { 3464 var icons = Object.create(null);
5003 singleton = new Polymer.IronMeta(); 3465 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) {
5004 } 3466 icons[icon.id] = icon;
5005 return singleton;
5006 };
5007
5008 /**
5009 `iron-meta-query` can be used to access infomation stored in `iron-meta`.
5010
5011 Examples:
5012
5013 If I create an instance like this:
5014
5015 <iron-meta key="info" value="foo/bar"></iron-meta>
5016
5017 Note that value="foo/bar" is the metadata I've defined. I could define more
5018 attributes or use child nodes to define additional metadata.
5019
5020 Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
5021
5022 var value = new Polymer.IronMetaQuery({key: 'info'}).value;
5023
5024 @group Polymer Iron Elements
5025 @element iron-meta-query
5026 */
5027 Polymer.IronMetaQuery = Polymer({
5028
5029 is: 'iron-meta-query',
5030
5031 properties: {
5032
5033 /**
5034 * The type of meta-data. All meta-data of the same type is stored
5035 * together.
5036 */
5037 type: {
5038 type: String,
5039 value: 'default',
5040 observer: '_typeChanged'
5041 },
5042
5043 /**
5044 * Specifies a key to use for retrieving `value` from the `type`
5045 * namespace.
5046 */
5047 key: {
5048 type: String,
5049 observer: '_keyChanged'
5050 },
5051
5052 /**
5053 * The meta-data to store or retrieve.
5054 */
5055 value: {
5056 type: Object,
5057 notify: true,
5058 readOnly: true
5059 },
5060
5061 /**
5062 * Array of all meta-data values for the given type.
5063 */
5064 list: {
5065 type: Array,
5066 notify: true
5067 }
5068
5069 },
5070
5071 /**
5072 * Actually a factory method, not a true constructor. Only runs if
5073 * someone invokes it directly (via `new Polymer.IronMeta()`);
5074 *
5075 * @param {{type: (string|undefined), key: (string|undefined)}=} config
5076 */
5077 factoryImpl: function(config) {
5078 if (config) {
5079 for (var n in config) {
5080 switch(n) {
5081 case 'type':
5082 case 'key':
5083 this[n] = config[n];
5084 break;
5085 }
5086 }
5087 }
5088 },
5089
5090 created: function() {
5091 // TODO(sjmiles): good for debugging?
5092 this._metaDatas = metaDatas;
5093 this._metaArrays = metaArrays;
5094 },
5095
5096 _keyChanged: function(key) {
5097 this._setValue(this._metaData && this._metaData[key]);
5098 },
5099
5100 _typeChanged: function(type) {
5101 this._metaData = metaDatas[type];
5102 this.list = metaArrays[type];
5103 if (this.key) {
5104 this._keyChanged(this.key);
5105 }
5106 },
5107
5108 /**
5109 * Retrieves meta data value by key.
5110 * @param {string} key The key of the meta-data to be returned.
5111 * @return {*}
5112 */
5113 byKey: function(key) {
5114 return this._metaData && this._metaData[key];
5115 }
5116
5117 }); 3467 });
5118 3468 return icons;
5119 })(); 3469 },
3470 _cloneIcon: function(id) {
3471 this._icons = this._icons || this._createIconMap();
3472 return this._prepareSvgClone(this._icons[id], this.size);
3473 },
3474 _prepareSvgClone: function(sourceSvg, size) {
3475 if (sourceSvg) {
3476 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
3477 svg.setAttribute('viewBox', viewBox);
3478 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
3479 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he ight: 100%;';
3480 svg.appendChild(content).removeAttribute('id');
3481 return svg;
3482 }
3483 return null;
3484 }
3485 });
3486
5120 Polymer({ 3487 Polymer({
5121 3488 is: 'paper-tabs',
5122 is: 'iron-icon', 3489 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronMenubarBehavior ],
5123 3490 properties: {
5124 properties: { 3491 noink: {
5125 3492 type: Boolean,
5126 /** 3493 value: false,
5127 * The name of the icon to use. The name should be of the form: 3494 observer: '_noinkChanged'
5128 * `iconset_name:icon_name`. 3495 },
5129 */ 3496 noBar: {
5130 icon: { 3497 type: Boolean,
5131 type: String, 3498 value: false
5132 observer: '_iconChanged' 3499 },
5133 }, 3500 noSlide: {
5134 3501 type: Boolean,
5135 /** 3502 value: false
5136 * The name of the theme to used, if one is specified by the 3503 },
5137 * iconset. 3504 scrollable: {
5138 */ 3505 type: Boolean,
5139 theme: { 3506 value: false
5140 type: String, 3507 },
5141 observer: '_updateIcon' 3508 fitContainer: {
5142 }, 3509 type: Boolean,
5143 3510 value: false
5144 /** 3511 },
5145 * If using iron-icon without an iconset, you can set the src to be 3512 disableDrag: {
5146 * the URL of an individual icon image file. Note that this will take 3513 type: Boolean,
5147 * precedence over a given icon attribute. 3514 value: false
5148 */ 3515 },
5149 src: { 3516 hideScrollButtons: {
5150 type: String, 3517 type: Boolean,
5151 observer: '_srcChanged' 3518 value: false
5152 }, 3519 },
5153 3520 alignBottom: {
5154 /** 3521 type: Boolean,
5155 * @type {!Polymer.IronMeta} 3522 value: false
5156 */ 3523 },
5157 _meta: { 3524 selectable: {
5158 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), 3525 type: String,
5159 observer: '_updateIcon' 3526 value: 'paper-tab'
5160 } 3527 },
5161 3528 autoselect: {
5162 }, 3529 type: Boolean,
5163 3530 value: false
5164 _DEFAULT_ICONSET: 'icons', 3531 },
5165 3532 autoselectDelay: {
5166 _iconChanged: function(icon) { 3533 type: Number,
5167 var parts = (icon || '').split(':'); 3534 value: 0
5168 this._iconName = parts.pop(); 3535 },
5169 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; 3536 _step: {
5170 this._updateIcon(); 3537 type: Number,
5171 }, 3538 value: 10
5172 3539 },
5173 _srcChanged: function(src) { 3540 _holdDelay: {
5174 this._updateIcon(); 3541 type: Number,
5175 }, 3542 value: 1
5176 3543 },
5177 _usesIconset: function() { 3544 _leftHidden: {
5178 return this.icon || !this.src; 3545 type: Boolean,
5179 }, 3546 value: false
5180 3547 },
5181 /** @suppress {visibility} */ 3548 _rightHidden: {
5182 _updateIcon: function() { 3549 type: Boolean,
5183 if (this._usesIconset()) { 3550 value: false
5184 if (this._img && this._img.parentNode) { 3551 },
5185 Polymer.dom(this.root).removeChild(this._img); 3552 _previousTab: {
5186 } 3553 type: Object
5187 if (this._iconName === "") { 3554 }
5188 if (this._iconset) { 3555 },
5189 this._iconset.removeIcon(this); 3556 hostAttributes: {
5190 } 3557 role: 'tablist'
5191 } else if (this._iconsetName && this._meta) { 3558 },
5192 this._iconset = /** @type {?Polymer.Iconset} */ ( 3559 listeners: {
5193 this._meta.byKey(this._iconsetName)); 3560 'iron-resize': '_onTabSizingChanged',
5194 if (this._iconset) { 3561 'iron-items-changed': '_onTabSizingChanged',
5195 this._iconset.applyIcon(this, this._iconName, this.theme); 3562 'iron-select': '_onIronSelect',
5196 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); 3563 'iron-deselect': '_onIronDeselect'
5197 } else { 3564 },
5198 this.listen(window, 'iron-iconset-added', '_updateIcon'); 3565 keyBindings: {
5199 } 3566 'left:keyup right:keyup': '_onArrowKeyup'
5200 } 3567 },
5201 } else { 3568 created: function() {
5202 if (this._iconset) { 3569 this._holdJob = null;
5203 this._iconset.removeIcon(this); 3570 this._pendingActivationItem = undefined;
5204 } 3571 this._pendingActivationTimeout = undefined;
5205 if (!this._img) { 3572 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind(thi s);
5206 this._img = document.createElement('img'); 3573 this.addEventListener('blur', this._onBlurCapture.bind(this), true);
5207 this._img.style.width = '100%'; 3574 },
5208 this._img.style.height = '100%'; 3575 ready: function() {
5209 this._img.draggable = false; 3576 this.setScrollDirection('y', this.$.tabsContainer);
5210 } 3577 },
5211 this._img.src = this.src; 3578 detached: function() {
5212 Polymer.dom(this.root).appendChild(this._img); 3579 this._cancelPendingActivation();
5213 } 3580 },
5214 } 3581 _noinkChanged: function(noink) {
5215 3582 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab');
3583 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAttribu te);
3584 },
3585 _setNoinkAttribute: function(element) {
3586 element.setAttribute('noink', '');
3587 },
3588 _removeNoinkAttribute: function(element) {
3589 element.removeAttribute('noink');
3590 },
3591 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScrollButt ons) {
3592 if (!scrollable || hideScrollButtons) {
3593 return 'hidden';
3594 }
3595 if (hideThisButton) {
3596 return 'not-visible';
3597 }
3598 return '';
3599 },
3600 _computeTabsContentClass: function(scrollable, fitContainer) {
3601 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '') : ' fit-container';
3602 },
3603 _computeSelectionBarClass: function(noBar, alignBottom) {
3604 if (noBar) {
3605 return 'hidden';
3606 } else if (alignBottom) {
3607 return 'align-bottom';
3608 }
3609 return '';
3610 },
3611 _onTabSizingChanged: function() {
3612 this.debounce('_onTabSizingChanged', function() {
3613 this._scroll();
3614 this._tabChanged(this.selectedItem);
3615 }, 10);
3616 },
3617 _onIronSelect: function(event) {
3618 this._tabChanged(event.detail.item, this._previousTab);
3619 this._previousTab = event.detail.item;
3620 this.cancelDebouncer('tab-changed');
3621 },
3622 _onIronDeselect: function(event) {
3623 this.debounce('tab-changed', function() {
3624 this._tabChanged(null, this._previousTab);
3625 this._previousTab = null;
3626 }, 1);
3627 },
3628 _activateHandler: function() {
3629 this._cancelPendingActivation();
3630 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
3631 },
3632 _scheduleActivation: function(item, delay) {
3633 this._pendingActivationItem = item;
3634 this._pendingActivationTimeout = this.async(this._bindDelayedActivationHandl er, delay);
3635 },
3636 _delayedActivationHandler: function() {
3637 var item = this._pendingActivationItem;
3638 this._pendingActivationItem = undefined;
3639 this._pendingActivationTimeout = undefined;
3640 item.fire(this.activateEvent, null, {
3641 bubbles: true,
3642 cancelable: true
5216 }); 3643 });
5217 /** 3644 },
5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus. 3645 _cancelPendingActivation: function() {
5219 * 3646 if (this._pendingActivationTimeout !== undefined) {
5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior 3647 this.cancelAsync(this._pendingActivationTimeout);
5221 */ 3648 this._pendingActivationItem = undefined;
5222 Polymer.PaperInkyFocusBehaviorImpl = { 3649 this._pendingActivationTimeout = undefined;
5223 observers: [ 3650 }
5224 '_focusedChanged(receivedFocusFromKeyboard)' 3651 },
5225 ], 3652 _onArrowKeyup: function(event) {
5226 3653 if (this.autoselect) {
5227 _focusedChanged: function(receivedFocusFromKeyboard) { 3654 this._scheduleActivation(this.focusedItem, this.autoselectDelay);
5228 if (receivedFocusFromKeyboard) { 3655 }
5229 this.ensureRipple(); 3656 },
5230 } 3657 _onBlurCapture: function(event) {
5231 if (this.hasRipple()) { 3658 if (event.target === this._pendingActivationItem) {
5232 this._ripple.holdDown = receivedFocusFromKeyboard; 3659 this._cancelPendingActivation();
5233 } 3660 }
5234 }, 3661 },
5235 3662 get _tabContainerScrollSize() {
5236 _createRipple: function() { 3663 return Math.max(0, this.$.tabsContainer.scrollWidth - this.$.tabsContainer.o ffsetWidth);
5237 var ripple = Polymer.PaperRippleBehavior._createRipple(); 3664 },
5238 ripple.id = 'ink'; 3665 _scroll: function(e, detail) {
5239 ripple.setAttribute('center', ''); 3666 if (!this.scrollable) {
5240 ripple.classList.add('circle'); 3667 return;
5241 return ripple; 3668 }
5242 } 3669 var ddx = detail && -detail.ddx || 0;
5243 }; 3670 this._affectScroll(ddx);
5244 3671 },
5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ 3672 _down: function(e) {
5246 Polymer.PaperInkyFocusBehavior = [ 3673 this.async(function() {
5247 Polymer.IronButtonState,
5248 Polymer.IronControlState,
5249 Polymer.PaperRippleBehavior,
5250 Polymer.PaperInkyFocusBehaviorImpl
5251 ];
5252 Polymer({
5253 is: 'paper-icon-button',
5254
5255 hostAttributes: {
5256 role: 'button',
5257 tabindex: '0'
5258 },
5259
5260 behaviors: [
5261 Polymer.PaperInkyFocusBehavior
5262 ],
5263
5264 properties: {
5265 /**
5266 * The URL of an image for the icon. If the src property is specified,
5267 * the icon property should not be.
5268 */
5269 src: {
5270 type: String
5271 },
5272
5273 /**
5274 * Specifies the icon name or index in the set of icons available in
5275 * the icon's icon set. If the icon property is specified,
5276 * the src property should not be.
5277 */
5278 icon: {
5279 type: String
5280 },
5281
5282 /**
5283 * Specifies the alternate text for the button, for accessibility.
5284 */
5285 alt: {
5286 type: String,
5287 observer: "_altChanged"
5288 }
5289 },
5290
5291 _altChanged: function(newValue, oldValue) {
5292 var label = this.getAttribute('aria-label');
5293
5294 // Don't stomp over a user-set aria-label.
5295 if (!label || oldValue == label) {
5296 this.setAttribute('aria-label', newValue);
5297 }
5298 }
5299 });
5300 Polymer({
5301 is: 'paper-tab',
5302
5303 behaviors: [
5304 Polymer.IronControlState,
5305 Polymer.IronButtonState,
5306 Polymer.PaperRippleBehavior
5307 ],
5308
5309 properties: {
5310
5311 /**
5312 * If true, the tab will forward keyboard clicks (enter/space) to
5313 * the first anchor element found in its descendants
5314 */
5315 link: {
5316 type: Boolean,
5317 value: false,
5318 reflectToAttribute: true
5319 }
5320
5321 },
5322
5323 hostAttributes: {
5324 role: 'tab'
5325 },
5326
5327 listeners: {
5328 down: '_updateNoink',
5329 tap: '_onTap'
5330 },
5331
5332 attached: function() {
5333 this._updateNoink();
5334 },
5335
5336 get _parentNoink () {
5337 var parent = Polymer.dom(this).parentNode;
5338 return !!parent && !!parent.noink;
5339 },
5340
5341 _updateNoink: function() {
5342 this.noink = !!this.noink || !!this._parentNoink;
5343 },
5344
5345 _onTap: function(event) {
5346 if (this.link) {
5347 var anchor = this.queryEffectiveChildren('a');
5348
5349 if (!anchor) {
5350 return;
5351 }
5352
5353 // Don't get stuck in a loop delegating
5354 // the listener from the child anchor
5355 if (event.target === anchor) {
5356 return;
5357 }
5358
5359 anchor.click();
5360 }
5361 }
5362
5363 });
5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
5365 Polymer.IronMultiSelectableBehaviorImpl = {
5366 properties: {
5367
5368 /**
5369 * If true, multiple selections are allowed.
5370 */
5371 multi: {
5372 type: Boolean,
5373 value: false,
5374 observer: 'multiChanged'
5375 },
5376
5377 /**
5378 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
5379 * is true.
5380 */
5381 selectedValues: {
5382 type: Array,
5383 notify: true
5384 },
5385
5386 /**
5387 * Returns an array of currently selected items.
5388 */
5389 selectedItems: {
5390 type: Array,
5391 readOnly: true,
5392 notify: true
5393 },
5394
5395 },
5396
5397 observers: [
5398 '_updateSelected(selectedValues.splices)'
5399 ],
5400
5401 /**
5402 * Selects the given value. If the `multi` property is true, then the select ed state of the
5403 * `value` will be toggled; otherwise the `value` will be selected.
5404 *
5405 * @method select
5406 * @param {string|number} value the value to select.
5407 */
5408 select: function(value) {
5409 if (this.multi) {
5410 if (this.selectedValues) {
5411 this._toggleSelected(value);
5412 } else {
5413 this.selectedValues = [value];
5414 }
5415 } else {
5416 this.selected = value;
5417 }
5418 },
5419
5420 multiChanged: function(multi) {
5421 this._selection.multi = multi;
5422 },
5423
5424 get _shouldUpdateSelection() {
5425 return this.selected != null ||
5426 (this.selectedValues != null && this.selectedValues.length);
5427 },
5428
5429 _updateAttrForSelected: function() {
5430 if (!this.multi) {
5431 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
5432 } else if (this._shouldUpdateSelection) {
5433 this.selectedValues = this.selectedItems.map(function(selectedItem) {
5434 return this._indexToValue(this.indexOf(selectedItem));
5435 }, this).filter(function(unfilteredValue) {
5436 return unfilteredValue != null;
5437 }, this);
5438 }
5439 },
5440
5441 _updateSelected: function() {
5442 if (this.multi) {
5443 this._selectMulti(this.selectedValues);
5444 } else {
5445 this._selectSelected(this.selected);
5446 }
5447 },
5448
5449 _selectMulti: function(values) {
5450 if (values) {
5451 var selectedItems = this._valuesToItems(values);
5452 // clear all but the current selected items
5453 this._selection.clear(selectedItems);
5454 // select only those not selected yet
5455 for (var i = 0; i < selectedItems.length; i++) {
5456 this._selection.setItemSelected(selectedItems[i], true);
5457 }
5458 // Check for items, since this array is populated only when attached
5459 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
5460 var fallback = this._valueToItem(this.fallbackSelection);
5461 if (fallback) {
5462 this.selectedValues = [this.fallbackSelection];
5463 }
5464 }
5465 } else {
5466 this._selection.clear();
5467 }
5468 },
5469
5470 _selectionChange: function() {
5471 var s = this._selection.get();
5472 if (this.multi) {
5473 this._setSelectedItems(s);
5474 } else {
5475 this._setSelectedItems([s]);
5476 this._setSelectedItem(s);
5477 }
5478 },
5479
5480 _toggleSelected: function(value) {
5481 var i = this.selectedValues.indexOf(value);
5482 var unselected = i < 0;
5483 if (unselected) {
5484 this.push('selectedValues',value);
5485 } else {
5486 this.splice('selectedValues',i,1);
5487 }
5488 },
5489
5490 _valuesToItems: function(values) {
5491 return (values == null) ? null : values.map(function(value) {
5492 return this._valueToItem(value);
5493 }, this);
5494 }
5495 };
5496
5497 /** @polymerBehavior */
5498 Polymer.IronMultiSelectableBehavior = [
5499 Polymer.IronSelectableBehavior,
5500 Polymer.IronMultiSelectableBehaviorImpl
5501 ];
5502 /**
5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
5504 *
5505 * @demo demo/index.html
5506 * @polymerBehavior Polymer.IronMenuBehavior
5507 */
5508 Polymer.IronMenuBehaviorImpl = {
5509
5510 properties: {
5511
5512 /**
5513 * Returns the currently focused item.
5514 * @type {?Object}
5515 */
5516 focusedItem: {
5517 observer: '_focusedItemChanged',
5518 readOnly: true,
5519 type: Object
5520 },
5521
5522 /**
5523 * The attribute to use on menu items to look up the item title. Typing th e first
5524 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
5525 * will be used.
5526 */
5527 attrForItemTitle: {
5528 type: String
5529 }
5530 },
5531
5532 hostAttributes: {
5533 'role': 'menu',
5534 'tabindex': '0'
5535 },
5536
5537 observers: [
5538 '_updateMultiselectable(multi)'
5539 ],
5540
5541 listeners: {
5542 'focus': '_onFocus',
5543 'keydown': '_onKeydown',
5544 'iron-items-changed': '_onIronItemsChanged'
5545 },
5546
5547 keyBindings: {
5548 'up': '_onUpKey',
5549 'down': '_onDownKey',
5550 'esc': '_onEscKey',
5551 'shift+tab:keydown': '_onShiftTabDown'
5552 },
5553
5554 attached: function() {
5555 this._resetTabindices();
5556 },
5557
5558 /**
5559 * Selects the given value. If the `multi` property is true, then the select ed state of the
5560 * `value` will be toggled; otherwise the `value` will be selected.
5561 *
5562 * @param {string|number} value the value to select.
5563 */
5564 select: function(value) {
5565 // Cancel automatically focusing a default item if the menu received focus
5566 // through a user action selecting a particular item.
5567 if (this._defaultFocusAsync) { 3674 if (this._defaultFocusAsync) {
5568 this.cancelAsync(this._defaultFocusAsync); 3675 this.cancelAsync(this._defaultFocusAsync);
5569 this._defaultFocusAsync = null; 3676 this._defaultFocusAsync = null;
5570 } 3677 }
5571 var item = this._valueToItem(value); 3678 }, 1);
5572 if (item && item.hasAttribute('disabled')) return; 3679 },
5573 this._setFocusedItem(item); 3680 _affectScroll: function(dx) {
5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); 3681 this.$.tabsContainer.scrollLeft += dx;
5575 }, 3682 var scrollLeft = this.$.tabsContainer.scrollLeft;
5576 3683 this._leftHidden = scrollLeft === 0;
5577 /** 3684 this._rightHidden = scrollLeft === this._tabContainerScrollSize;
5578 * Resets all tabindex attributes to the appropriate value based on the 3685 },
5579 * current selection state. The appropriate value is `0` (focusable) for 3686 _onLeftScrollButtonDown: function() {
5580 * the default selected item, and `-1` (not keyboard focusable) for all 3687 this._scrollToLeft();
5581 * other items. 3688 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDelay);
5582 */ 3689 },
5583 _resetTabindices: function() { 3690 _onRightScrollButtonDown: function() {
5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem; 3691 this._scrollToRight();
5585 3692 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDelay) ;
5586 this.items.forEach(function(item) { 3693 },
5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); 3694 _onScrollButtonUp: function() {
5588 }, this); 3695 clearInterval(this._holdJob);
5589 }, 3696 this._holdJob = null;
5590 3697 },
5591 /** 3698 _scrollToLeft: function() {
5592 * Sets appropriate ARIA based on whether or not the menu is meant to be 3699 this._affectScroll(-this._step);
5593 * multi-selectable. 3700 },
5594 * 3701 _scrollToRight: function() {
5595 * @param {boolean} multi True if the menu should be multi-selectable. 3702 this._affectScroll(this._step);
5596 */ 3703 },
5597 _updateMultiselectable: function(multi) { 3704 _tabChanged: function(tab, old) {
5598 if (multi) { 3705 if (!tab) {
5599 this.setAttribute('aria-multiselectable', 'true'); 3706 this.$.selectionBar.classList.remove('expand');
5600 } else { 3707 this.$.selectionBar.classList.remove('contract');
5601 this.removeAttribute('aria-multiselectable'); 3708 this._positionBar(0, 0);
5602 } 3709 return;
5603 }, 3710 }
5604 3711 var r = this.$.tabsContent.getBoundingClientRect();
5605 /** 3712 var w = r.width;
5606 * Given a KeyboardEvent, this method will focus the appropriate item in the 3713 var tabRect = tab.getBoundingClientRect();
5607 * menu (if there is a relevant item, and it is possible to focus it). 3714 var tabOffsetLeft = tabRect.left - r.left;
5608 * 3715 this._pos = {
5609 * @param {KeyboardEvent} event A KeyboardEvent. 3716 width: this._calcPercent(tabRect.width, w),
5610 */ 3717 left: this._calcPercent(tabOffsetLeft, w)
5611 _focusWithKeyboardEvent: function(event) { 3718 };
5612 for (var i = 0, item; item = this.items[i]; i++) { 3719 if (this.noSlide || old == null) {
5613 var attr = this.attrForItemTitle || 'textContent'; 3720 this.$.selectionBar.classList.remove('expand');
5614 var title = item[attr] || item.getAttribute(attr); 3721 this.$.selectionBar.classList.remove('contract');
5615 3722 this._positionBar(this._pos.width, this._pos.left);
5616 if (!item.hasAttribute('disabled') && title && 3723 return;
5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) { 3724 }
5618 this._setFocusedItem(item); 3725 var oldRect = old.getBoundingClientRect();
5619 break; 3726 var oldIndex = this.items.indexOf(old);
5620 } 3727 var index = this.items.indexOf(tab);
5621 } 3728 var m = 5;
5622 }, 3729 this.$.selectionBar.classList.add('expand');
5623 3730 var moveRight = oldIndex < index;
5624 /** 3731 var isRTL = this._isRTL;
5625 * Focuses the previous item (relative to the currently focused item) in the 3732 if (isRTL) {
5626 * menu, disabled items will be skipped. 3733 moveRight = !moveRight;
5627 * Loop until length + 1 to handle case of single item in menu. 3734 }
5628 */ 3735 if (moveRight) {
5629 _focusPrevious: function() { 3736 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - oldRect .left, w) - m, this._left);
5630 var length = this.items.length; 3737 } else {
5631 var curFocusIndex = Number(this.indexOf(this.focusedItem)); 3738 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tabRect .left, w) - m, this._calcPercent(tabOffsetLeft, w) + m);
5632 for (var i = 1; i < length + 1; i++) { 3739 }
5633 var item = this.items[(curFocusIndex - i + length) % length]; 3740 if (this.scrollable) {
5634 if (!item.hasAttribute('disabled')) { 3741 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft);
5635 this._setFocusedItem(item); 3742 }
5636 return; 3743 },
5637 } 3744 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) {
5638 } 3745 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft;
5639 }, 3746 if (l < 0) {
5640 3747 this.$.tabsContainer.scrollLeft += l;
5641 /** 3748 } else {
5642 * Focuses the next item (relative to the currently focused item) in the 3749 l += tabWidth - this.$.tabsContainer.offsetWidth;
5643 * menu, disabled items will be skipped. 3750 if (l > 0) {
5644 * Loop until length + 1 to handle case of single item in menu. 3751 this.$.tabsContainer.scrollLeft += l;
5645 */ 3752 }
5646 _focusNext: function() { 3753 }
5647 var length = this.items.length; 3754 },
5648 var curFocusIndex = Number(this.indexOf(this.focusedItem)); 3755 _calcPercent: function(w, w0) {
5649 for (var i = 1; i < length + 1; i++) { 3756 return 100 * w / w0;
5650 var item = this.items[(curFocusIndex + i) % length]; 3757 },
5651 if (!item.hasAttribute('disabled')) { 3758 _positionBar: function(width, left) {
5652 this._setFocusedItem(item); 3759 width = width || 0;
5653 return; 3760 left = left || 0;
5654 } 3761 this._width = width;
5655 } 3762 this._left = left;
5656 }, 3763 this.transform('translateX(' + left + '%) scaleX(' + width / 100 + ')', this .$.selectionBar);
5657 3764 },
5658 /** 3765 _onBarTransitionEnd: function(e) {
5659 * Mutates items in the menu based on provided selection details, so that 3766 var cl = this.$.selectionBar.classList;
5660 * all items correctly reflect selection state. 3767 if (cl.contains('expand')) {
5661 * 3768 cl.remove('expand');
5662 * @param {Element} item An item in the menu. 3769 cl.add('contract');
5663 * @param {boolean} isSelected True if the item should be shown in a 3770 this._positionBar(this._pos.width, this._pos.left);
5664 * selected state, otherwise false. 3771 } else if (cl.contains('contract')) {
5665 */ 3772 cl.remove('contract');
5666 _applySelection: function(item, isSelected) { 3773 }
5667 if (isSelected) { 3774 }
5668 item.setAttribute('aria-selected', 'true'); 3775 });
5669 } else { 3776
5670 item.removeAttribute('aria-selected'); 3777 (function() {
5671 } 3778 'use strict';
5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); 3779 Polymer.IronA11yAnnouncer = Polymer({
5673 }, 3780 is: 'iron-a11y-announcer',
5674
5675 /**
5676 * Discretely updates tabindex values among menu items as the focused item
5677 * changes.
5678 *
5679 * @param {Element} focusedItem The element that is currently focused.
5680 * @param {?Element} old The last element that was considered focused, if
5681 * applicable.
5682 */
5683 _focusedItemChanged: function(focusedItem, old) {
5684 old && old.setAttribute('tabindex', '-1');
5685 if (focusedItem) {
5686 focusedItem.setAttribute('tabindex', '0');
5687 focusedItem.focus();
5688 }
5689 },
5690
5691 /**
5692 * A handler that responds to mutation changes related to the list of items
5693 * in the menu.
5694 *
5695 * @param {CustomEvent} event An event containing mutation records as its
5696 * detail.
5697 */
5698 _onIronItemsChanged: function(event) {
5699 if (event.detail.addedNodes.length) {
5700 this._resetTabindices();
5701 }
5702 },
5703
5704 /**
5705 * Handler that is called when a shift+tab keypress is detected by the menu.
5706 *
5707 * @param {CustomEvent} event A key combination event.
5708 */
5709 _onShiftTabDown: function(event) {
5710 var oldTabIndex = this.getAttribute('tabindex');
5711
5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
5713
5714 this._setFocusedItem(null);
5715
5716 this.setAttribute('tabindex', '-1');
5717
5718 this.async(function() {
5719 this.setAttribute('tabindex', oldTabIndex);
5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5721 // NOTE(cdata): polymer/polymer#1305
5722 }, 1);
5723 },
5724
5725 /**
5726 * Handler that is called when the menu receives focus.
5727 *
5728 * @param {FocusEvent} event A focus event.
5729 */
5730 _onFocus: function(event) {
5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
5732 // do not focus the menu itself
5733 return;
5734 }
5735
5736 // Do not focus the selected tab if the deepest target is part of the
5737 // menu element's local DOM and is focusable.
5738 var rootTarget = /** @type {?HTMLElement} */(
5739 Polymer.dom(event).rootTarget);
5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
5741 return;
5742 }
5743
5744 // clear the cached focus item
5745 this._defaultFocusAsync = this.async(function() {
5746 // focus the selected item when the menu receives focus, or the first it em
5747 // if no item is selected
5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
5749
5750 this._setFocusedItem(null);
5751
5752 if (selectedItem) {
5753 this._setFocusedItem(selectedItem);
5754 } else if (this.items[0]) {
5755 // We find the first none-disabled item (if one exists)
5756 this._focusNext();
5757 }
5758 });
5759 },
5760
5761 /**
5762 * Handler that is called when the up key is pressed.
5763 *
5764 * @param {CustomEvent} event A key combination event.
5765 */
5766 _onUpKey: function(event) {
5767 // up and down arrows moves the focus
5768 this._focusPrevious();
5769 event.detail.keyboardEvent.preventDefault();
5770 },
5771
5772 /**
5773 * Handler that is called when the down key is pressed.
5774 *
5775 * @param {CustomEvent} event A key combination event.
5776 */
5777 _onDownKey: function(event) {
5778 this._focusNext();
5779 event.detail.keyboardEvent.preventDefault();
5780 },
5781
5782 /**
5783 * Handler that is called when the esc key is pressed.
5784 *
5785 * @param {CustomEvent} event A key combination event.
5786 */
5787 _onEscKey: function(event) {
5788 // esc blurs the control
5789 this.focusedItem.blur();
5790 },
5791
5792 /**
5793 * Handler that is called when a keydown event is detected.
5794 *
5795 * @param {KeyboardEvent} event A keyboard event.
5796 */
5797 _onKeydown: function(event) {
5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
5799 // all other keys focus the menu item starting with that character
5800 this._focusWithKeyboardEvent(event);
5801 }
5802 event.stopPropagation();
5803 },
5804
5805 // override _activateHandler
5806 _activateHandler: function(event) {
5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
5808 event.stopPropagation();
5809 }
5810 };
5811
5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5813
5814 /** @polymerBehavior Polymer.IronMenuBehavior */
5815 Polymer.IronMenuBehavior = [
5816 Polymer.IronMultiSelectableBehavior,
5817 Polymer.IronA11yKeysBehavior,
5818 Polymer.IronMenuBehaviorImpl
5819 ];
5820 /**
5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior.
5822 *
5823 * @polymerBehavior Polymer.IronMenubarBehavior
5824 */
5825 Polymer.IronMenubarBehaviorImpl = {
5826
5827 hostAttributes: {
5828 'role': 'menubar'
5829 },
5830
5831 keyBindings: {
5832 'left': '_onLeftKey',
5833 'right': '_onRightKey'
5834 },
5835
5836 _onUpKey: function(event) {
5837 this.focusedItem.click();
5838 event.detail.keyboardEvent.preventDefault();
5839 },
5840
5841 _onDownKey: function(event) {
5842 this.focusedItem.click();
5843 event.detail.keyboardEvent.preventDefault();
5844 },
5845
5846 get _isRTL() {
5847 return window.getComputedStyle(this)['direction'] === 'rtl';
5848 },
5849
5850 _onLeftKey: function(event) {
5851 if (this._isRTL) {
5852 this._focusNext();
5853 } else {
5854 this._focusPrevious();
5855 }
5856 event.detail.keyboardEvent.preventDefault();
5857 },
5858
5859 _onRightKey: function(event) {
5860 if (this._isRTL) {
5861 this._focusPrevious();
5862 } else {
5863 this._focusNext();
5864 }
5865 event.detail.keyboardEvent.preventDefault();
5866 },
5867
5868 _onKeydown: function(event) {
5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
5870 return;
5871 }
5872
5873 // all other keys focus the menu item starting with that character
5874 this._focusWithKeyboardEvent(event);
5875 }
5876
5877 };
5878
5879 /** @polymerBehavior Polymer.IronMenubarBehavior */
5880 Polymer.IronMenubarBehavior = [
5881 Polymer.IronMenuBehavior,
5882 Polymer.IronMenubarBehaviorImpl
5883 ];
5884 /**
5885 * The `iron-iconset-svg` element allows users to define their own icon sets
5886 * that contain svg icons. The svg icon elements should be children of the
5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
5888 *
5889 * Using svg elements to create icons has a few advantages over traditional
5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so
5891 * they are resolution independent and should look good on any device. They
5892 * are stylable via css. Icons can be themed, colorized, and even animated.
5893 *
5894 * Example:
5895 *
5896 * <iron-iconset-svg name="my-svg-icons" size="24">
5897 * <svg>
5898 * <defs>
5899 * <g id="shape">
5900 * <rect x="12" y="0" width="12" height="24" />
5901 * <circle cx="12" cy="12" r="12" />
5902 * </g>
5903 * </defs>
5904 * </svg>
5905 * </iron-iconset-svg>
5906 *
5907 * This will automatically register the icon set "my-svg-icons" to the iconset
5908 * database. To use these icons from within another element, make a
5909 * `iron-iconset` element and call the `byId` method
5910 * to retrieve a given iconset. To apply a particular icon inside an
5911 * element use the `applyIcon` method. For example:
5912 *
5913 * iconset.applyIcon(iconNode, 'car');
5914 *
5915 * @element iron-iconset-svg
5916 * @demo demo/index.html
5917 * @implements {Polymer.Iconset}
5918 */
5919 Polymer({
5920 is: 'iron-iconset-svg',
5921
5922 properties: { 3781 properties: {
5923 3782 mode: {
5924 /**
5925 * The name of the iconset.
5926 */
5927 name: {
5928 type: String, 3783 type: String,
5929 observer: '_nameChanged' 3784 value: 'polite'
5930 }, 3785 },
5931 3786 _text: {
5932 /**
5933 * The size of an individual icon. Note that icons must be square.
5934 */
5935 size: {
5936 type: Number,
5937 value: 24
5938 }
5939
5940 },
5941
5942 attached: function() {
5943 this.style.display = 'none';
5944 },
5945
5946 /**
5947 * Construct an array of all icon names in this iconset.
5948 *
5949 * @return {!Array} Array of icon names.
5950 */
5951 getIconNames: function() {
5952 this._icons = this._createIconMap();
5953 return Object.keys(this._icons).map(function(n) {
5954 return this.name + ':' + n;
5955 }, this);
5956 },
5957
5958 /**
5959 * Applies an icon to the given element.
5960 *
5961 * An svg icon is prepended to the element's shadowRoot if it exists,
5962 * otherwise to the element itself.
5963 *
5964 * @method applyIcon
5965 * @param {Element} element Element to which the icon is applied.
5966 * @param {string} iconName Name of the icon to apply.
5967 * @return {?Element} The svg element which renders the icon.
5968 */
5969 applyIcon: function(element, iconName) {
5970 // insert svg element into shadow root, if it exists
5971 element = element.root || element;
5972 // Remove old svg element
5973 this.removeIcon(element);
5974 // install new svg element
5975 var svg = this._cloneIcon(iconName);
5976 if (svg) {
5977 var pde = Polymer.dom(element);
5978 pde.insertBefore(svg, pde.childNodes[0]);
5979 return element._svgIcon = svg;
5980 }
5981 return null;
5982 },
5983
5984 /**
5985 * Remove an icon from the given element by undoing the changes effected
5986 * by `applyIcon`.
5987 *
5988 * @param {Element} element The element from which the icon is removed.
5989 */
5990 removeIcon: function(element) {
5991 // Remove old svg element
5992 if (element._svgIcon) {
5993 Polymer.dom(element).removeChild(element._svgIcon);
5994 element._svgIcon = null;
5995 }
5996 },
5997
5998 /**
5999 *
6000 * When name is changed, register iconset metadata
6001 *
6002 */
6003 _nameChanged: function() {
6004 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
6005 this.async(function() {
6006 this.fire('iron-iconset-added', this, {node: window});
6007 });
6008 },
6009
6010 /**
6011 * Create a map of child SVG elements by id.
6012 *
6013 * @return {!Object} Map of id's to SVG elements.
6014 */
6015 _createIconMap: function() {
6016 // Objects chained to Object.prototype (`{}`) have members. Specifically,
6017 // on FF there is a `watch` method that confuses the icon map, so we
6018 // need to use a null-based object here.
6019 var icons = Object.create(null);
6020 Polymer.dom(this).querySelectorAll('[id]')
6021 .forEach(function(icon) {
6022 icons[icon.id] = icon;
6023 });
6024 return icons;
6025 },
6026
6027 /**
6028 * Produce installable clone of the SVG element matching `id` in this
6029 * iconset, or `undefined` if there is no matching element.
6030 *
6031 * @return {Element} Returns an installable clone of the SVG element
6032 * matching `id`.
6033 */
6034 _cloneIcon: function(id) {
6035 // create the icon map on-demand, since the iconset itself has no discrete
6036 // signal to know when it's children are fully parsed
6037 this._icons = this._icons || this._createIconMap();
6038 return this._prepareSvgClone(this._icons[id], this.size);
6039 },
6040
6041 /**
6042 * @param {Element} sourceSvg
6043 * @param {number} size
6044 * @return {Element}
6045 */
6046 _prepareSvgClone: function(sourceSvg, size) {
6047 if (sourceSvg) {
6048 var content = sourceSvg.cloneNode(true),
6049 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
6050 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s ize;
6051 svg.setAttribute('viewBox', viewBox);
6052 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136
6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6056 svg.appendChild(content).removeAttribute('id');
6057 return svg;
6058 }
6059 return null;
6060 }
6061
6062 });
6063 Polymer({
6064 is: 'paper-tabs',
6065
6066 behaviors: [
6067 Polymer.IronResizableBehavior,
6068 Polymer.IronMenubarBehavior
6069 ],
6070
6071 properties: {
6072 /**
6073 * If true, ink ripple effect is disabled. When this property is changed ,
6074 * all descendant `<paper-tab>` elements have their `noink` property
6075 * changed to the new value as well.
6076 */
6077 noink: {
6078 type: Boolean,
6079 value: false,
6080 observer: '_noinkChanged'
6081 },
6082
6083 /**
6084 * If true, the bottom bar to indicate the selected tab will not be show n.
6085 */
6086 noBar: {
6087 type: Boolean,
6088 value: false
6089 },
6090
6091 /**
6092 * If true, the slide effect for the bottom bar is disabled.
6093 */
6094 noSlide: {
6095 type: Boolean,
6096 value: false
6097 },
6098
6099 /**
6100 * If true, tabs are scrollable and the tab width is based on the label width.
6101 */
6102 scrollable: {
6103 type: Boolean,
6104 value: false
6105 },
6106
6107 /**
6108 * If true, tabs expand to fit their container. This currently only appl ies when
6109 * scrollable is true.
6110 */
6111 fitContainer: {
6112 type: Boolean,
6113 value: false
6114 },
6115
6116 /**
6117 * If true, dragging on the tabs to scroll is disabled.
6118 */
6119 disableDrag: {
6120 type: Boolean,
6121 value: false
6122 },
6123
6124 /**
6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla ble tabs.
6126 */
6127 hideScrollButtons: {
6128 type: Boolean,
6129 value: false
6130 },
6131
6132 /**
6133 * If true, the tabs are aligned to bottom (the selection bar appears at the top).
6134 */
6135 alignBottom: {
6136 type: Boolean,
6137 value: false
6138 },
6139
6140 selectable: {
6141 type: String,
6142 value: 'paper-tab'
6143 },
6144
6145 /**
6146 * If true, tabs are automatically selected when focused using the
6147 * keyboard.
6148 */
6149 autoselect: {
6150 type: Boolean,
6151 value: false
6152 },
6153
6154 /**
6155 * The delay (in milliseconds) between when the user stops interacting
6156 * with the tabs through the keyboard and when the focused item is
6157 * automatically selected (if `autoselect` is true).
6158 */
6159 autoselectDelay: {
6160 type: Number,
6161 value: 0
6162 },
6163
6164 _step: {
6165 type: Number,
6166 value: 10
6167 },
6168
6169 _holdDelay: {
6170 type: Number,
6171 value: 1
6172 },
6173
6174 _leftHidden: {
6175 type: Boolean,
6176 value: false
6177 },
6178
6179 _rightHidden: {
6180 type: Boolean,
6181 value: false
6182 },
6183
6184 _previousTab: {
6185 type: Object
6186 }
6187 },
6188
6189 hostAttributes: {
6190 role: 'tablist'
6191 },
6192
6193 listeners: {
6194 'iron-resize': '_onTabSizingChanged',
6195 'iron-items-changed': '_onTabSizingChanged',
6196 'iron-select': '_onIronSelect',
6197 'iron-deselect': '_onIronDeselect'
6198 },
6199
6200 keyBindings: {
6201 'left:keyup right:keyup': '_onArrowKeyup'
6202 },
6203
6204 created: function() {
6205 this._holdJob = null;
6206 this._pendingActivationItem = undefined;
6207 this._pendingActivationTimeout = undefined;
6208 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind (this);
6209 this.addEventListener('blur', this._onBlurCapture.bind(this), true);
6210 },
6211
6212 ready: function() {
6213 this.setScrollDirection('y', this.$.tabsContainer);
6214 },
6215
6216 detached: function() {
6217 this._cancelPendingActivation();
6218 },
6219
6220 _noinkChanged: function(noink) {
6221 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab');
6222 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt ribute);
6223 },
6224
6225 _setNoinkAttribute: function(element) {
6226 element.setAttribute('noink', '');
6227 },
6228
6229 _removeNoinkAttribute: function(element) {
6230 element.removeAttribute('noink');
6231 },
6232
6233 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll Buttons) {
6234 if (!scrollable || hideScrollButtons) {
6235 return 'hidden';
6236 }
6237
6238 if (hideThisButton) {
6239 return 'not-visible';
6240 }
6241
6242 return '';
6243 },
6244
6245 _computeTabsContentClass: function(scrollable, fitContainer) {
6246 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '' ) : ' fit-container';
6247 },
6248
6249 _computeSelectionBarClass: function(noBar, alignBottom) {
6250 if (noBar) {
6251 return 'hidden';
6252 } else if (alignBottom) {
6253 return 'align-bottom';
6254 }
6255
6256 return '';
6257 },
6258
6259 // TODO(cdata): Add `track` response back in when gesture lands.
6260
6261 _onTabSizingChanged: function() {
6262 this.debounce('_onTabSizingChanged', function() {
6263 this._scroll();
6264 this._tabChanged(this.selectedItem);
6265 }, 10);
6266 },
6267
6268 _onIronSelect: function(event) {
6269 this._tabChanged(event.detail.item, this._previousTab);
6270 this._previousTab = event.detail.item;
6271 this.cancelDebouncer('tab-changed');
6272 },
6273
6274 _onIronDeselect: function(event) {
6275 this.debounce('tab-changed', function() {
6276 this._tabChanged(null, this._previousTab);
6277 this._previousTab = null;
6278 // See polymer/polymer#1305
6279 }, 1);
6280 },
6281
6282 _activateHandler: function() {
6283 // Cancel item activations scheduled by keyboard events when any other
6284 // action causes an item to be activated (e.g. clicks).
6285 this._cancelPendingActivation();
6286
6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
6288 },
6289
6290 /**
6291 * Activates an item after a delay (in milliseconds).
6292 */
6293 _scheduleActivation: function(item, delay) {
6294 this._pendingActivationItem = item;
6295 this._pendingActivationTimeout = this.async(
6296 this._bindDelayedActivationHandler, delay);
6297 },
6298
6299 /**
6300 * Activates the last item given to `_scheduleActivation`.
6301 */
6302 _delayedActivationHandler: function() {
6303 var item = this._pendingActivationItem;
6304 this._pendingActivationItem = undefined;
6305 this._pendingActivationTimeout = undefined;
6306 item.fire(this.activateEvent, null, {
6307 bubbles: true,
6308 cancelable: true
6309 });
6310 },
6311
6312 /**
6313 * Cancels a previously scheduled item activation made with
6314 * `_scheduleActivation`.
6315 */
6316 _cancelPendingActivation: function() {
6317 if (this._pendingActivationTimeout !== undefined) {
6318 this.cancelAsync(this._pendingActivationTimeout);
6319 this._pendingActivationItem = undefined;
6320 this._pendingActivationTimeout = undefined;
6321 }
6322 },
6323
6324 _onArrowKeyup: function(event) {
6325 if (this.autoselect) {
6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay);
6327 }
6328 },
6329
6330 _onBlurCapture: function(event) {
6331 // Cancel a scheduled item activation (if any) when that item is
6332 // blurred.
6333 if (event.target === this._pendingActivationItem) {
6334 this._cancelPendingActivation();
6335 }
6336 },
6337
6338 get _tabContainerScrollSize () {
6339 return Math.max(
6340 0,
6341 this.$.tabsContainer.scrollWidth -
6342 this.$.tabsContainer.offsetWidth
6343 );
6344 },
6345
6346 _scroll: function(e, detail) {
6347 if (!this.scrollable) {
6348 return;
6349 }
6350
6351 var ddx = (detail && -detail.ddx) || 0;
6352 this._affectScroll(ddx);
6353 },
6354
6355 _down: function(e) {
6356 // go one beat async to defeat IronMenuBehavior
6357 // autorefocus-on-no-selection timeout
6358 this.async(function() {
6359 if (this._defaultFocusAsync) {
6360 this.cancelAsync(this._defaultFocusAsync);
6361 this._defaultFocusAsync = null;
6362 }
6363 }, 1);
6364 },
6365
6366 _affectScroll: function(dx) {
6367 this.$.tabsContainer.scrollLeft += dx;
6368
6369 var scrollLeft = this.$.tabsContainer.scrollLeft;
6370
6371 this._leftHidden = scrollLeft === 0;
6372 this._rightHidden = scrollLeft === this._tabContainerScrollSize;
6373 },
6374
6375 _onLeftScrollButtonDown: function() {
6376 this._scrollToLeft();
6377 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel ay);
6378 },
6379
6380 _onRightScrollButtonDown: function() {
6381 this._scrollToRight();
6382 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe lay);
6383 },
6384
6385 _onScrollButtonUp: function() {
6386 clearInterval(this._holdJob);
6387 this._holdJob = null;
6388 },
6389
6390 _scrollToLeft: function() {
6391 this._affectScroll(-this._step);
6392 },
6393
6394 _scrollToRight: function() {
6395 this._affectScroll(this._step);
6396 },
6397
6398 _tabChanged: function(tab, old) {
6399 if (!tab) {
6400 // Remove the bar without animation.
6401 this.$.selectionBar.classList.remove('expand');
6402 this.$.selectionBar.classList.remove('contract');
6403 this._positionBar(0, 0);
6404 return;
6405 }
6406
6407 var r = this.$.tabsContent.getBoundingClientRect();
6408 var w = r.width;
6409 var tabRect = tab.getBoundingClientRect();
6410 var tabOffsetLeft = tabRect.left - r.left;
6411
6412 this._pos = {
6413 width: this._calcPercent(tabRect.width, w),
6414 left: this._calcPercent(tabOffsetLeft, w)
6415 };
6416
6417 if (this.noSlide || old == null) {
6418 // Position the bar without animation.
6419 this.$.selectionBar.classList.remove('expand');
6420 this.$.selectionBar.classList.remove('contract');
6421 this._positionBar(this._pos.width, this._pos.left);
6422 return;
6423 }
6424
6425 var oldRect = old.getBoundingClientRect();
6426 var oldIndex = this.items.indexOf(old);
6427 var index = this.items.indexOf(tab);
6428 var m = 5;
6429
6430 // bar animation: expand
6431 this.$.selectionBar.classList.add('expand');
6432
6433 var moveRight = oldIndex < index;
6434 var isRTL = this._isRTL;
6435 if (isRTL) {
6436 moveRight = !moveRight;
6437 }
6438
6439 if (moveRight) {
6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old Rect.left, w) - m,
6441 this._left);
6442 } else {
6443 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab Rect.left, w) - m,
6444 this._calcPercent(tabOffsetLeft, w) + m);
6445 }
6446
6447 if (this.scrollable) {
6448 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft);
6449 }
6450 },
6451
6452 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) {
6453 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft;
6454 if (l < 0) {
6455 this.$.tabsContainer.scrollLeft += l;
6456 } else {
6457 l += (tabWidth - this.$.tabsContainer.offsetWidth);
6458 if (l > 0) {
6459 this.$.tabsContainer.scrollLeft += l;
6460 }
6461 }
6462 },
6463
6464 _calcPercent: function(w, w0) {
6465 return 100 * w / w0;
6466 },
6467
6468 _positionBar: function(width, left) {
6469 width = width || 0;
6470 left = left || 0;
6471
6472 this._width = width;
6473 this._left = left;
6474 this.transform(
6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')',
6476 this.$.selectionBar);
6477 },
6478
6479 _onBarTransitionEnd: function(e) {
6480 var cl = this.$.selectionBar.classList;
6481 // bar animation: expand -> contract
6482 if (cl.contains('expand')) {
6483 cl.remove('expand');
6484 cl.add('contract');
6485 this._positionBar(this._pos.width, this._pos.left);
6486 // bar animation done
6487 } else if (cl.contains('contract')) {
6488 cl.remove('contract');
6489 }
6490 }
6491 });
6492 (function() {
6493 'use strict';
6494
6495 Polymer.IronA11yAnnouncer = Polymer({
6496 is: 'iron-a11y-announcer',
6497
6498 properties: {
6499
6500 /**
6501 * The value of mode is used to set the `aria-live` attribute
6502 * for the element that will be announced. Valid values are: `off`,
6503 * `polite` and `assertive`.
6504 */
6505 mode: {
6506 type: String,
6507 value: 'polite'
6508 },
6509
6510 _text: {
6511 type: String,
6512 value: ''
6513 }
6514 },
6515
6516 created: function() {
6517 if (!Polymer.IronA11yAnnouncer.instance) {
6518 Polymer.IronA11yAnnouncer.instance = this;
6519 }
6520
6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
6522 },
6523
6524 /**
6525 * Cause a text string to be announced by screen readers.
6526 *
6527 * @param {string} text The text that should be announced.
6528 */
6529 announce: function(text) {
6530 this._text = '';
6531 this.async(function() {
6532 this._text = text;
6533 }, 100);
6534 },
6535
6536 _onIronAnnounce: function(event) {
6537 if (event.detail && event.detail.text) {
6538 this.announce(event.detail.text);
6539 }
6540 }
6541 });
6542
6543 Polymer.IronA11yAnnouncer.instance = null;
6544
6545 Polymer.IronA11yAnnouncer.requestAvailability = function() {
6546 if (!Polymer.IronA11yAnnouncer.instance) {
6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
6548 }
6549
6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
6551 };
6552 })();
6553 /**
6554 * Singleton IronMeta instance.
6555 */
6556 Polymer.IronValidatableBehaviorMeta = null;
6557
6558 /**
6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
6561 *
6562 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
6563 * To validate a form imperatively, call the form's `validate()` method, which in turn will
6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
6565 * custom element will get a public `validate()`, which
6566 * will return the validity of the element, and a corresponding `invalid` attr ibute,
6567 * which can be used for styling.
6568 *
6569 * To implement the custom validation logic of your element, you must override
6570 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
6572 * for an example.
6573 *
6574 * ### Accessibility
6575 *
6576 * Changing the `invalid` property, either manually or by calling `validate()` will update the
6577 * `aria-invalid` attribute.
6578 *
6579 * @demo demo/index.html
6580 * @polymerBehavior
6581 */
6582 Polymer.IronValidatableBehavior = {
6583
6584 properties: {
6585
6586 /**
6587 * Name of the validator to use.
6588 */
6589 validator: {
6590 type: String
6591 },
6592
6593 /**
6594 * True if the last call to `validate` is invalid.
6595 */
6596 invalid: {
6597 notify: true,
6598 reflectToAttribute: true,
6599 type: Boolean,
6600 value: false
6601 },
6602
6603 /**
6604 * This property is deprecated and should not be used. Use the global
6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
6606 */
6607 _validatorMeta: {
6608 type: Object
6609 },
6610
6611 /**
6612 * Namespace for this validator. This property is deprecated and should
6613 * not be used. For all intents and purposes, please consider it a
6614 * read-only, config-time property.
6615 */
6616 validatorType: {
6617 type: String,
6618 value: 'validator'
6619 },
6620
6621 _validator: {
6622 type: Object,
6623 computed: '__computeValidator(validator)'
6624 }
6625 },
6626
6627 observers: [
6628 '_invalidChanged(invalid)'
6629 ],
6630
6631 registered: function() {
6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
6633 },
6634
6635 _invalidChanged: function() {
6636 if (this.invalid) {
6637 this.setAttribute('aria-invalid', 'true');
6638 } else {
6639 this.removeAttribute('aria-invalid');
6640 }
6641 },
6642
6643 /**
6644 * @return {boolean} True if the validator `validator` exists.
6645 */
6646 hasValidator: function() {
6647 return this._validator != null;
6648 },
6649
6650 /**
6651 * Returns true if the `value` is valid, and updates `invalid`. If you want
6652 * your element to have custom validation logic, do not override this method ;
6653 * override `_getValidity(value)` instead.
6654
6655 * @param {Object} value The value to be validated. By default, it is passed
6656 * to the validator's `validate()` function, if a validator is set.
6657 * @return {boolean} True if `value` is valid.
6658 */
6659 validate: function(value) {
6660 this.invalid = !this._getValidity(value);
6661 return !this.invalid;
6662 },
6663
6664 /**
6665 * Returns true if `value` is valid. By default, it is passed
6666 * to the validator's `validate()` function, if a validator is set. You
6667 * should override this method if you want to implement custom validity
6668 * logic for your element.
6669 *
6670 * @param {Object} value The value to be validated.
6671 * @return {boolean} True if `value` is valid.
6672 */
6673
6674 _getValidity: function(value) {
6675 if (this.hasValidator()) {
6676 return this._validator.validate(value);
6677 }
6678 return true;
6679 },
6680
6681 __computeValidator: function() {
6682 return Polymer.IronValidatableBehaviorMeta &&
6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
6684 }
6685 };
6686 /*
6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
6688 to `<input>`.
6689
6690 ### Two-way binding
6691
6692 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
6693
6694 <input value="{{myValue::input}}">
6695
6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
6697 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
6698
6699 <input is="iron-input" bind-value="{{myValue}}">
6700
6701 ### Custom validators
6702
6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
6704
6705 <input is="iron-input" validator="my-custom-validator">
6706
6707 ### Stopping invalid input
6708
6709 It may be desirable to only allow users to enter certain characters. You can use the
6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
6711 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
6712
6713 \x3c!-- only allow characters that match [0-9] --\x3e
6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
6715
6716 @hero hero.svg
6717 @demo demo/index.html
6718 */
6719
6720 Polymer({
6721
6722 is: 'iron-input',
6723
6724 extends: 'input',
6725
6726 behaviors: [
6727 Polymer.IronValidatableBehavior
6728 ],
6729
6730 properties: {
6731
6732 /**
6733 * Use this property instead of `value` for two-way data binding.
6734 */
6735 bindValue: {
6736 observer: '_bindValueChanged',
6737 type: String
6738 },
6739
6740 /**
6741 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
6742 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
6743 * Pasted input will have each character checked individually; if any char acter
6744 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
6745 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
6746 */
6747 preventInvalidInput: {
6748 type: Boolean
6749 },
6750
6751 /**
6752 * Regular expression that list the characters allowed as input.
6753 * This pattern represents the allowed characters for the field; as the us er inputs text,
6754 * each individual character will be checked against the pattern (rather t han checking
6755 * the entire value as a whole). The recommended format should be a list o f allowed characters;
6756 * for example, `[a-zA-Z0-9.+-!;:]`
6757 */
6758 allowedPattern: {
6759 type: String,
6760 observer: "_allowedPatternChanged"
6761 },
6762
6763 _previousValidInput: {
6764 type: String, 3787 type: String,
6765 value: '' 3788 value: ''
6766 }, 3789 }
6767 3790 },
6768 _patternAlreadyChecked: {
6769 type: Boolean,
6770 value: false
6771 }
6772
6773 },
6774
6775 listeners: {
6776 'input': '_onInput',
6777 'keypress': '_onKeypress'
6778 },
6779
6780 /** @suppress {checkTypes} */
6781 registered: function() {
6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
6783 if (!this._canDispatchEventOnDisabled()) {
6784 this._origDispatchEvent = this.dispatchEvent;
6785 this.dispatchEvent = this._dispatchEventFirefoxIE;
6786 }
6787 },
6788
6789 created: function() { 3791 created: function() {
6790 Polymer.IronA11yAnnouncer.requestAvailability(); 3792 if (!Polymer.IronA11yAnnouncer.instance) {
6791 }, 3793 Polymer.IronA11yAnnouncer.instance = this;
6792 3794 }
6793 _canDispatchEventOnDisabled: function() { 3795 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind( this));
6794 var input = document.createElement('input'); 3796 },
6795 var canDispatch = false; 3797 announce: function(text) {
6796 input.disabled = true; 3798 this._text = '';
6797 3799 this.async(function() {
6798 input.addEventListener('feature-check-dispatch-event', function() { 3800 this._text = text;
6799 canDispatch = true; 3801 }, 100);
6800 }); 3802 },
6801 3803 _onIronAnnounce: function(event) {
6802 try { 3804 if (event.detail && event.detail.text) {
6803 input.dispatchEvent(new Event('feature-check-dispatch-event')); 3805 this.announce(event.detail.text);
6804 } catch(e) {} 3806 }
6805 3807 }
6806 return canDispatch; 3808 });
6807 }, 3809 Polymer.IronA11yAnnouncer.instance = null;
6808 3810 Polymer.IronA11yAnnouncer.requestAvailability = function() {
6809 _dispatchEventFirefoxIE: function() { 3811 if (!Polymer.IronA11yAnnouncer.instance) {
6810 // Due to Firefox bug, events fired on disabled form controls can throw 3812 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann ouncer');
6811 // errors; furthermore, neither IE nor Firefox will actually dispatch 3813 }
6812 // events from disabled form controls; as such, we toggle disable around 3814 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
6813 // the dispatch to allow notifying properties to notify 3815 };
6814 // See issue #47 for details 3816 })();
6815 var disabled = this.disabled; 3817
6816 this.disabled = false; 3818 Polymer.IronValidatableBehaviorMeta = null;
6817 this._origDispatchEvent.apply(this, arguments); 3819
6818 this.disabled = disabled; 3820 Polymer.IronValidatableBehavior = {
6819 }, 3821 properties: {
6820 3822 validator: {
6821 get _patternRegExp() { 3823 type: String
6822 var pattern; 3824 },
6823 if (this.allowedPattern) { 3825 invalid: {
6824 pattern = new RegExp(this.allowedPattern); 3826 notify: true,
3827 reflectToAttribute: true,
3828 type: Boolean,
3829 value: false
3830 },
3831 _validatorMeta: {
3832 type: Object
3833 },
3834 validatorType: {
3835 type: String,
3836 value: 'validator'
3837 },
3838 _validator: {
3839 type: Object,
3840 computed: '__computeValidator(validator)'
3841 }
3842 },
3843 observers: [ '_invalidChanged(invalid)' ],
3844 registered: function() {
3845 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({
3846 type: 'validator'
3847 });
3848 },
3849 _invalidChanged: function() {
3850 if (this.invalid) {
3851 this.setAttribute('aria-invalid', 'true');
3852 } else {
3853 this.removeAttribute('aria-invalid');
3854 }
3855 },
3856 hasValidator: function() {
3857 return this._validator != null;
3858 },
3859 validate: function(value) {
3860 this.invalid = !this._getValidity(value);
3861 return !this.invalid;
3862 },
3863 _getValidity: function(value) {
3864 if (this.hasValidator()) {
3865 return this._validator.validate(value);
3866 }
3867 return true;
3868 },
3869 __computeValidator: function() {
3870 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio rMeta.byKey(this.validator);
3871 }
3872 };
3873
3874 Polymer({
3875 is: 'iron-input',
3876 "extends": 'input',
3877 behaviors: [ Polymer.IronValidatableBehavior ],
3878 properties: {
3879 bindValue: {
3880 observer: '_bindValueChanged',
3881 type: String
3882 },
3883 preventInvalidInput: {
3884 type: Boolean
3885 },
3886 allowedPattern: {
3887 type: String,
3888 observer: "_allowedPatternChanged"
3889 },
3890 _previousValidInput: {
3891 type: String,
3892 value: ''
3893 },
3894 _patternAlreadyChecked: {
3895 type: Boolean,
3896 value: false
3897 }
3898 },
3899 listeners: {
3900 input: '_onInput',
3901 keypress: '_onKeypress'
3902 },
3903 registered: function() {
3904 if (!this._canDispatchEventOnDisabled()) {
3905 this._origDispatchEvent = this.dispatchEvent;
3906 this.dispatchEvent = this._dispatchEventFirefoxIE;
3907 }
3908 },
3909 created: function() {
3910 Polymer.IronA11yAnnouncer.requestAvailability();
3911 },
3912 _canDispatchEventOnDisabled: function() {
3913 var input = document.createElement('input');
3914 var canDispatch = false;
3915 input.disabled = true;
3916 input.addEventListener('feature-check-dispatch-event', function() {
3917 canDispatch = true;
3918 });
3919 try {
3920 input.dispatchEvent(new Event('feature-check-dispatch-event'));
3921 } catch (e) {}
3922 return canDispatch;
3923 },
3924 _dispatchEventFirefoxIE: function() {
3925 var disabled = this.disabled;
3926 this.disabled = false;
3927 this._origDispatchEvent.apply(this, arguments);
3928 this.disabled = disabled;
3929 },
3930 get _patternRegExp() {
3931 var pattern;
3932 if (this.allowedPattern) {
3933 pattern = new RegExp(this.allowedPattern);
3934 } else {
3935 switch (this.type) {
3936 case 'number':
3937 pattern = /[0-9.,e-]/;
3938 break;
3939 }
3940 }
3941 return pattern;
3942 },
3943 ready: function() {
3944 this.bindValue = this.value;
3945 },
3946 _bindValueChanged: function() {
3947 if (this.value !== this.bindValue) {
3948 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue == = false) ? '' : this.bindValue;
3949 }
3950 this.fire('bind-value-changed', {
3951 value: this.bindValue
3952 });
3953 },
3954 _allowedPatternChanged: function() {
3955 this.preventInvalidInput = this.allowedPattern ? true : false;
3956 },
3957 _onInput: function() {
3958 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
3959 var valid = this._checkPatternValidity();
3960 if (!valid) {
3961 this._announceInvalidCharacter('Invalid string of characters not entered .');
3962 this.value = this._previousValidInput;
3963 }
3964 }
3965 this.bindValue = this.value;
3966 this._previousValidInput = this.value;
3967 this._patternAlreadyChecked = false;
3968 },
3969 _isPrintable: function(event) {
3970 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC ode == 13 || event.keyCode == 27;
3971 var mozNonPrintable = event.keyCode == 19 || event.keyCode == 20 || event.ke yCode == 45 || event.keyCode == 46 || event.keyCode == 144 || event.keyCode == 1 45 || event.keyCode > 32 && event.keyCode < 41 || event.keyCode > 111 && event.k eyCode < 124;
3972 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
3973 },
3974 _onKeypress: function(event) {
3975 if (!this.preventInvalidInput && this.type !== 'number') {
3976 return;
3977 }
3978 var regexp = this._patternRegExp;
3979 if (!regexp) {
3980 return;
3981 }
3982 if (event.metaKey || event.ctrlKey || event.altKey) return;
3983 this._patternAlreadyChecked = true;
3984 var thisChar = String.fromCharCode(event.charCode);
3985 if (this._isPrintable(event) && !regexp.test(thisChar)) {
3986 event.preventDefault();
3987 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent ered.');
3988 }
3989 },
3990 _checkPatternValidity: function() {
3991 var regexp = this._patternRegExp;
3992 if (!regexp) {
3993 return true;
3994 }
3995 for (var i = 0; i < this.value.length; i++) {
3996 if (!regexp.test(this.value[i])) {
3997 return false;
3998 }
3999 }
4000 return true;
4001 },
4002 validate: function() {
4003 var valid = this.checkValidity();
4004 if (valid) {
4005 if (this.required && this.value === '') {
4006 valid = false;
4007 } else if (this.hasValidator()) {
4008 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
4009 }
4010 }
4011 this.invalid = !valid;
4012 this.fire('iron-input-validate');
4013 return valid;
4014 },
4015 _announceInvalidCharacter: function(message) {
4016 this.fire('iron-announce', {
4017 text: message
4018 });
4019 }
4020 });
4021
4022 Polymer({
4023 is: 'paper-input-container',
4024 properties: {
4025 noLabelFloat: {
4026 type: Boolean,
4027 value: false
4028 },
4029 alwaysFloatLabel: {
4030 type: Boolean,
4031 value: false
4032 },
4033 attrForValue: {
4034 type: String,
4035 value: 'bind-value'
4036 },
4037 autoValidate: {
4038 type: Boolean,
4039 value: false
4040 },
4041 invalid: {
4042 observer: '_invalidChanged',
4043 type: Boolean,
4044 value: false
4045 },
4046 focused: {
4047 readOnly: true,
4048 type: Boolean,
4049 value: false,
4050 notify: true
4051 },
4052 _addons: {
4053 type: Array
4054 },
4055 _inputHasContent: {
4056 type: Boolean,
4057 value: false
4058 },
4059 _inputSelector: {
4060 type: String,
4061 value: 'input,textarea,.paper-input-input'
4062 },
4063 _boundOnFocus: {
4064 type: Function,
4065 value: function() {
4066 return this._onFocus.bind(this);
4067 }
4068 },
4069 _boundOnBlur: {
4070 type: Function,
4071 value: function() {
4072 return this._onBlur.bind(this);
4073 }
4074 },
4075 _boundOnInput: {
4076 type: Function,
4077 value: function() {
4078 return this._onInput.bind(this);
4079 }
4080 },
4081 _boundValueChanged: {
4082 type: Function,
4083 value: function() {
4084 return this._onValueChanged.bind(this);
4085 }
4086 }
4087 },
4088 listeners: {
4089 'addon-attached': '_onAddonAttached',
4090 'iron-input-validate': '_onIronInputValidate'
4091 },
4092 get _valueChangedEvent() {
4093 return this.attrForValue + '-changed';
4094 },
4095 get _propertyForValue() {
4096 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
4097 },
4098 get _inputElement() {
4099 return Polymer.dom(this).querySelector(this._inputSelector);
4100 },
4101 get _inputElementValue() {
4102 return this._inputElement[this._propertyForValue] || this._inputElement.valu e;
4103 },
4104 ready: function() {
4105 if (!this._addons) {
4106 this._addons = [];
4107 }
4108 this.addEventListener('focus', this._boundOnFocus, true);
4109 this.addEventListener('blur', this._boundOnBlur, true);
4110 },
4111 attached: function() {
4112 if (this.attrForValue) {
4113 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa lueChanged);
4114 } else {
4115 this.addEventListener('input', this._onInput);
4116 }
4117 if (this._inputElementValue != '') {
4118 this._handleValueAndAutoValidate(this._inputElement);
4119 } else {
4120 this._handleValue(this._inputElement);
4121 }
4122 },
4123 _onAddonAttached: function(event) {
4124 if (!this._addons) {
4125 this._addons = [];
4126 }
4127 var target = event.target;
4128 if (this._addons.indexOf(target) === -1) {
4129 this._addons.push(target);
4130 if (this.isAttached) {
4131 this._handleValue(this._inputElement);
4132 }
4133 }
4134 },
4135 _onFocus: function() {
4136 this._setFocused(true);
4137 },
4138 _onBlur: function() {
4139 this._setFocused(false);
4140 this._handleValueAndAutoValidate(this._inputElement);
4141 },
4142 _onInput: function(event) {
4143 this._handleValueAndAutoValidate(event.target);
4144 },
4145 _onValueChanged: function(event) {
4146 this._handleValueAndAutoValidate(event.target);
4147 },
4148 _handleValue: function(inputElement) {
4149 var value = this._inputElementValue;
4150 if (value || value === 0 || inputElement.type === 'number' && !inputElement. checkValidity()) {
4151 this._inputHasContent = true;
4152 } else {
4153 this._inputHasContent = false;
4154 }
4155 this.updateAddons({
4156 inputElement: inputElement,
4157 value: value,
4158 invalid: this.invalid
4159 });
4160 },
4161 _handleValueAndAutoValidate: function(inputElement) {
4162 if (this.autoValidate) {
4163 var valid;
4164 if (inputElement.validate) {
4165 valid = inputElement.validate(this._inputElementValue);
6825 } else { 4166 } else {
6826 switch (this.type) { 4167 valid = inputElement.checkValidity();
6827 case 'number': 4168 }
6828 pattern = /[0-9.,e-]/;
6829 break;
6830 }
6831 }
6832 return pattern;
6833 },
6834
6835 ready: function() {
6836 this.bindValue = this.value;
6837 },
6838
6839 /**
6840 * @suppress {checkTypes}
6841 */
6842 _bindValueChanged: function() {
6843 if (this.value !== this.bindValue) {
6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
6845 }
6846 // manually notify because we don't want to notify until after setting val ue
6847 this.fire('bind-value-changed', {value: this.bindValue});
6848 },
6849
6850 _allowedPatternChanged: function() {
6851 // Force to prevent invalid input when an `allowed-pattern` is set
6852 this.preventInvalidInput = this.allowedPattern ? true : false;
6853 },
6854
6855 _onInput: function() {
6856 // Need to validate each of the characters pasted if they haven't
6857 // been validated inside `_onKeypress` already.
6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
6859 var valid = this._checkPatternValidity();
6860 if (!valid) {
6861 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
6862 this.value = this._previousValidInput;
6863 }
6864 }
6865
6866 this.bindValue = this.value;
6867 this._previousValidInput = this.value;
6868 this._patternAlreadyChecked = false;
6869 },
6870
6871 _isPrintable: function(event) {
6872 // What a control/printable character is varies wildly based on the browse r.
6873 // - most control characters (arrows, backspace) do not send a `keypress` event
6874 // in Chrome, but the *do* on Firefox
6875 // - in Firefox, when they do send a `keypress` event, control chars have
6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
6877 // - printable characters always send a keypress event.
6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
6879 // always matches the charCode.
6880 // None of this makes any sense.
6881
6882 // For these keys, ASCII code == browser keycode.
6883 var anyNonPrintable =
6884 (event.keyCode == 8) || // backspace
6885 (event.keyCode == 9) || // tab
6886 (event.keyCode == 13) || // enter
6887 (event.keyCode == 27); // escape
6888
6889 // For these keys, make sure it's a browser keycode and not an ASCII code.
6890 var mozNonPrintable =
6891 (event.keyCode == 19) || // pause
6892 (event.keyCode == 20) || // caps lock
6893 (event.keyCode == 45) || // insert
6894 (event.keyCode == 46) || // delete
6895 (event.keyCode == 144) || // num lock
6896 (event.keyCode == 145) || // scroll lock
6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys
6899
6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
6901 },
6902
6903 _onKeypress: function(event) {
6904 if (!this.preventInvalidInput && this.type !== 'number') {
6905 return;
6906 }
6907 var regexp = this._patternRegExp;
6908 if (!regexp) {
6909 return;
6910 }
6911
6912 // Handle special keys and backspace
6913 if (event.metaKey || event.ctrlKey || event.altKey)
6914 return;
6915
6916 // Check the pattern either here or in `_onInput`, but not in both.
6917 this._patternAlreadyChecked = true;
6918
6919 var thisChar = String.fromCharCode(event.charCode);
6920 if (this._isPrintable(event) && !regexp.test(thisChar)) {
6921 event.preventDefault();
6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
6923 }
6924 },
6925
6926 _checkPatternValidity: function() {
6927 var regexp = this._patternRegExp;
6928 if (!regexp) {
6929 return true;
6930 }
6931 for (var i = 0; i < this.value.length; i++) {
6932 if (!regexp.test(this.value[i])) {
6933 return false;
6934 }
6935 }
6936 return true;
6937 },
6938
6939 /**
6940 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
6941 * then any constraints.
6942 * @return {boolean} True if the value is valid.
6943 */
6944 validate: function() {
6945 // First, check what the browser thinks. Some inputs (like type=number)
6946 // behave weirdly and will set the value to "" if something invalid is
6947 // entered, but will set the validity correctly.
6948 var valid = this.checkValidity();
6949
6950 // Only do extra checking if the browser thought this was valid.
6951 if (valid) {
6952 // Empty, required input is invalid
6953 if (this.required && this.value === '') {
6954 valid = false;
6955 } else if (this.hasValidator()) {
6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
6957 }
6958 }
6959
6960 this.invalid = !valid; 4169 this.invalid = !valid;
6961 this.fire('iron-input-validate'); 4170 }
6962 return valid; 4171 this._handleValue(inputElement);
6963 }, 4172 },
6964 4173 _onIronInputValidate: function(event) {
6965 _announceInvalidCharacter: function(message) { 4174 this.invalid = this._inputElement.invalid;
6966 this.fire('iron-announce', { text: message }); 4175 },
6967 } 4176 _invalidChanged: function() {
6968 }); 4177 if (this._addons) {
6969
6970 /*
6971 The `iron-input-validate` event is fired whenever `validate()` is called.
6972 @event iron-input-validate
6973 */
6974 Polymer({
6975 is: 'paper-input-container',
6976
6977 properties: {
6978 /**
6979 * Set to true to disable the floating label. The label disappears when th e input value is
6980 * not null.
6981 */
6982 noLabelFloat: {
6983 type: Boolean,
6984 value: false
6985 },
6986
6987 /**
6988 * Set to true to always float the floating label.
6989 */
6990 alwaysFloatLabel: {
6991 type: Boolean,
6992 value: false
6993 },
6994
6995 /**
6996 * The attribute to listen for value changes on.
6997 */
6998 attrForValue: {
6999 type: String,
7000 value: 'bind-value'
7001 },
7002
7003 /**
7004 * Set to true to auto-validate the input value when it changes.
7005 */
7006 autoValidate: {
7007 type: Boolean,
7008 value: false
7009 },
7010
7011 /**
7012 * True if the input is invalid. This property is set automatically when t he input value
7013 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
7014 */
7015 invalid: {
7016 observer: '_invalidChanged',
7017 type: Boolean,
7018 value: false
7019 },
7020
7021 /**
7022 * True if the input has focus.
7023 */
7024 focused: {
7025 readOnly: true,
7026 type: Boolean,
7027 value: false,
7028 notify: true
7029 },
7030
7031 _addons: {
7032 type: Array
7033 // do not set a default value here intentionally - it will be initialize d lazily when a
7034 // distributed child is attached, which may occur before configuration f or this element
7035 // in polyfill.
7036 },
7037
7038 _inputHasContent: {
7039 type: Boolean,
7040 value: false
7041 },
7042
7043 _inputSelector: {
7044 type: String,
7045 value: 'input,textarea,.paper-input-input'
7046 },
7047
7048 _boundOnFocus: {
7049 type: Function,
7050 value: function() {
7051 return this._onFocus.bind(this);
7052 }
7053 },
7054
7055 _boundOnBlur: {
7056 type: Function,
7057 value: function() {
7058 return this._onBlur.bind(this);
7059 }
7060 },
7061
7062 _boundOnInput: {
7063 type: Function,
7064 value: function() {
7065 return this._onInput.bind(this);
7066 }
7067 },
7068
7069 _boundValueChanged: {
7070 type: Function,
7071 value: function() {
7072 return this._onValueChanged.bind(this);
7073 }
7074 }
7075 },
7076
7077 listeners: {
7078 'addon-attached': '_onAddonAttached',
7079 'iron-input-validate': '_onIronInputValidate'
7080 },
7081
7082 get _valueChangedEvent() {
7083 return this.attrForValue + '-changed';
7084 },
7085
7086 get _propertyForValue() {
7087 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
7088 },
7089
7090 get _inputElement() {
7091 return Polymer.dom(this).querySelector(this._inputSelector);
7092 },
7093
7094 get _inputElementValue() {
7095 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
7096 },
7097
7098 ready: function() {
7099 if (!this._addons) {
7100 this._addons = [];
7101 }
7102 this.addEventListener('focus', this._boundOnFocus, true);
7103 this.addEventListener('blur', this._boundOnBlur, true);
7104 },
7105
7106 attached: function() {
7107 if (this.attrForValue) {
7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
7109 } else {
7110 this.addEventListener('input', this._onInput);
7111 }
7112
7113 // Only validate when attached if the input already has a value.
7114 if (this._inputElementValue != '') {
7115 this._handleValueAndAutoValidate(this._inputElement);
7116 } else {
7117 this._handleValue(this._inputElement);
7118 }
7119 },
7120
7121 _onAddonAttached: function(event) {
7122 if (!this._addons) {
7123 this._addons = [];
7124 }
7125 var target = event.target;
7126 if (this._addons.indexOf(target) === -1) {
7127 this._addons.push(target);
7128 if (this.isAttached) {
7129 this._handleValue(this._inputElement);
7130 }
7131 }
7132 },
7133
7134 _onFocus: function() {
7135 this._setFocused(true);
7136 },
7137
7138 _onBlur: function() {
7139 this._setFocused(false);
7140 this._handleValueAndAutoValidate(this._inputElement);
7141 },
7142
7143 _onInput: function(event) {
7144 this._handleValueAndAutoValidate(event.target);
7145 },
7146
7147 _onValueChanged: function(event) {
7148 this._handleValueAndAutoValidate(event.target);
7149 },
7150
7151 _handleValue: function(inputElement) {
7152 var value = this._inputElementValue;
7153
7154 // type="number" hack needed because this.value is empty until it's valid
7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
7156 this._inputHasContent = true;
7157 } else {
7158 this._inputHasContent = false;
7159 }
7160
7161 this.updateAddons({ 4178 this.updateAddons({
7162 inputElement: inputElement,
7163 value: value,
7164 invalid: this.invalid 4179 invalid: this.invalid
7165 }); 4180 });
7166 }, 4181 }
7167 4182 },
7168 _handleValueAndAutoValidate: function(inputElement) { 4183 updateAddons: function(state) {
7169 if (this.autoValidate) { 4184 for (var addon, index = 0; addon = this._addons[index]; index++) {
7170 var valid; 4185 addon.update(state);
7171 if (inputElement.validate) { 4186 }
7172 valid = inputElement.validate(this._inputElementValue); 4187 },
7173 } else { 4188 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i nvalid, _inputHasContent) {
7174 valid = inputElement.checkValidity(); 4189 var cls = 'input-content';
7175 } 4190 if (!noLabelFloat) {
7176 this.invalid = !valid; 4191 var label = this.querySelector('label');
7177 } 4192 if (alwaysFloatLabel || _inputHasContent) {
7178 4193 cls += ' label-is-floating';
7179 // Call this last to notify the add-ons. 4194 this.$.labelAndInputContainer.style.position = 'static';
7180 this._handleValue(inputElement); 4195 if (invalid) {
7181 }, 4196 cls += ' is-invalid';
7182 4197 } else if (focused) {
7183 _onIronInputValidate: function(event) { 4198 cls += " label-is-highlighted";
7184 this.invalid = this._inputElement.invalid;
7185 },
7186
7187 _invalidChanged: function() {
7188 if (this._addons) {
7189 this.updateAddons({invalid: this.invalid});
7190 }
7191 },
7192
7193 /**
7194 * Call this to update the state of add-ons.
7195 * @param {Object} state Add-on state.
7196 */
7197 updateAddons: function(state) {
7198 for (var addon, index = 0; addon = this._addons[index]; index++) {
7199 addon.update(state);
7200 }
7201 },
7202
7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
7204 var cls = 'input-content';
7205 if (!noLabelFloat) {
7206 var label = this.querySelector('label');
7207
7208 if (alwaysFloatLabel || _inputHasContent) {
7209 cls += ' label-is-floating';
7210 // If the label is floating, ignore any offsets that may have been
7211 // applied from a prefix element.
7212 this.$.labelAndInputContainer.style.position = 'static';
7213
7214 if (invalid) {
7215 cls += ' is-invalid';
7216 } else if (focused) {
7217 cls += " label-is-highlighted";
7218 }
7219 } else {
7220 // When the label is not floating, it should overlap the input element .
7221 if (label) {
7222 this.$.labelAndInputContainer.style.position = 'relative';
7223 }
7224 } 4199 }
7225 } else { 4200 } else {
7226 if (_inputHasContent) { 4201 if (label) {
7227 cls += ' label-is-hidden'; 4202 this.$.labelAndInputContainer.style.position = 'relative';
7228 } 4203 }
7229 } 4204 }
7230 return cls; 4205 } else {
7231 }, 4206 if (_inputHasContent) {
7232 4207 cls += ' label-is-hidden';
7233 _computeUnderlineClass: function(focused, invalid) { 4208 }
7234 var cls = 'underline'; 4209 }
7235 if (invalid) { 4210 return cls;
7236 cls += ' is-invalid'; 4211 },
7237 } else if (focused) { 4212 _computeUnderlineClass: function(focused, invalid) {
7238 cls += ' is-highlighted' 4213 var cls = 'underline';
7239 } 4214 if (invalid) {
7240 return cls; 4215 cls += ' is-invalid';
7241 }, 4216 } else if (focused) {
7242 4217 cls += ' is-highlighted';
7243 _computeAddOnContentClass: function(focused, invalid) { 4218 }
7244 var cls = 'add-on-content'; 4219 return cls;
7245 if (invalid) { 4220 },
7246 cls += ' is-invalid'; 4221 _computeAddOnContentClass: function(focused, invalid) {
7247 } else if (focused) { 4222 var cls = 'add-on-content';
7248 cls += ' is-highlighted' 4223 if (invalid) {
7249 } 4224 cls += ' is-invalid';
7250 return cls; 4225 } else if (focused) {
7251 } 4226 cls += ' is-highlighted';
7252 }); 4227 }
7253 /** @polymerBehavior */ 4228 return cls;
7254 Polymer.PaperSpinnerBehavior = { 4229 }
7255 4230 });
7256 listeners: { 4231
7257 'animationend': '__reset', 4232 Polymer.PaperSpinnerBehavior = {
7258 'webkitAnimationEnd': '__reset' 4233 listeners: {
7259 }, 4234 animationend: '__reset',
7260 4235 webkitAnimationEnd: '__reset'
7261 properties: { 4236 },
7262 /** 4237 properties: {
7263 * Displays the spinner. 4238 active: {
7264 */ 4239 type: Boolean,
7265 active: { 4240 value: false,
7266 type: Boolean, 4241 reflectToAttribute: true,
7267 value: false, 4242 observer: '__activeChanged'
7268 reflectToAttribute: true, 4243 },
7269 observer: '__activeChanged' 4244 alt: {
7270 }, 4245 type: String,
7271 4246 value: 'loading',
7272 /** 4247 observer: '__altChanged'
7273 * Alternative text content for accessibility support. 4248 },
7274 * If alt is present, it will add an aria-label whose content matches alt when active. 4249 __coolingDown: {
7275 * If alt is not present, it will default to 'loading' as the alt value. 4250 type: Boolean,
7276 */ 4251 value: false
7277 alt: { 4252 }
7278 type: String, 4253 },
7279 value: 'loading', 4254 __computeContainerClasses: function(active, coolingDown) {
7280 observer: '__altChanged' 4255 return [ active || coolingDown ? 'active' : '', coolingDown ? 'cooldown' : ' ' ].join(' ');
7281 }, 4256 },
7282 4257 __activeChanged: function(active, old) {
7283 __coolingDown: { 4258 this.__setAriaHidden(!active);
7284 type: Boolean, 4259 this.__coolingDown = !active && old;
7285 value: false 4260 },
7286 } 4261 __altChanged: function(alt) {
7287 }, 4262 if (alt === this.getPropertyInfo('alt').value) {
7288 4263 this.alt = this.getAttribute('aria-label') || alt;
7289 __computeContainerClasses: function(active, coolingDown) { 4264 } else {
7290 return [ 4265 this.__setAriaHidden(alt === '');
7291 active || coolingDown ? 'active' : '', 4266 this.setAttribute('aria-label', alt);
7292 coolingDown ? 'cooldown' : '' 4267 }
7293 ].join(' '); 4268 },
7294 }, 4269 __setAriaHidden: function(hidden) {
7295 4270 var attr = 'aria-hidden';
7296 __activeChanged: function(active, old) { 4271 if (hidden) {
7297 this.__setAriaHidden(!active); 4272 this.setAttribute(attr, 'true');
7298 this.__coolingDown = !active && old; 4273 } else {
7299 }, 4274 this.removeAttribute(attr);
7300 4275 }
7301 __altChanged: function(alt) { 4276 },
7302 // user-provided `aria-label` takes precedence over prototype default 4277 __reset: function() {
7303 if (alt === this.getPropertyInfo('alt').value) { 4278 this.active = false;
7304 this.alt = this.getAttribute('aria-label') || alt; 4279 this.__coolingDown = false;
7305 } else { 4280 }
7306 this.__setAriaHidden(alt===''); 4281 };
7307 this.setAttribute('aria-label', alt); 4282
7308 }
7309 },
7310
7311 __setAriaHidden: function(hidden) {
7312 var attr = 'aria-hidden';
7313 if (hidden) {
7314 this.setAttribute(attr, 'true');
7315 } else {
7316 this.removeAttribute(attr);
7317 }
7318 },
7319
7320 __reset: function() {
7321 this.active = false;
7322 this.__coolingDown = false;
7323 }
7324 };
7325 Polymer({ 4283 Polymer({
7326 is: 'paper-spinner-lite', 4284 is: 'paper-spinner-lite',
7327 4285 behaviors: [ Polymer.PaperSpinnerBehavior ]
7328 behaviors: [ 4286 });
7329 Polymer.PaperSpinnerBehavior 4287
7330 ]
7331 });
7332 // Copyright 2016 The Chromium Authors. All rights reserved. 4288 // Copyright 2016 The Chromium Authors. All rights reserved.
7333 // Use of this source code is governed by a BSD-style license that can be 4289 // Use of this source code is governed by a BSD-style license that can be
7334 // found in the LICENSE file. 4290 // found in the LICENSE file.
7335
7336 /**
7337 * Implements an incremental search field which can be shown and hidden.
7338 * Canonical implementation is <cr-search-field>.
7339 * @polymerBehavior
7340 */
7341 var CrSearchFieldBehavior = { 4291 var CrSearchFieldBehavior = {
7342 properties: { 4292 properties: {
7343 label: { 4293 label: {
7344 type: String, 4294 type: String,
7345 value: '', 4295 value: ''
7346 }, 4296 },
7347
7348 clearLabel: { 4297 clearLabel: {
7349 type: String, 4298 type: String,
7350 value: '', 4299 value: ''
7351 }, 4300 },
7352
7353 showingSearch: { 4301 showingSearch: {
7354 type: Boolean, 4302 type: Boolean,
7355 value: false, 4303 value: false,
7356 notify: true, 4304 notify: true,
7357 observer: 'showingSearchChanged_', 4305 observer: 'showingSearchChanged_',
7358 reflectToAttribute: true 4306 reflectToAttribute: true
7359 }, 4307 },
7360
7361 /** @private */
7362 lastValue_: { 4308 lastValue_: {
7363 type: String, 4309 type: String,
7364 value: '', 4310 value: ''
7365 }, 4311 }
7366 }, 4312 },
7367
7368 /**
7369 * @abstract
7370 * @return {!HTMLInputElement} The input field element the behavior should
7371 * use.
7372 */
7373 getSearchInput: function() {}, 4313 getSearchInput: function() {},
7374
7375 /**
7376 * @return {string} The value of the search field.
7377 */
7378 getValue: function() { 4314 getValue: function() {
7379 return this.getSearchInput().value; 4315 return this.getSearchInput().value;
7380 }, 4316 },
7381
7382 /**
7383 * Sets the value of the search field.
7384 * @param {string} value
7385 */
7386 setValue: function(value) { 4317 setValue: function(value) {
7387 // Use bindValue when setting the input value so that changes propagate
7388 // correctly.
7389 this.getSearchInput().bindValue = value; 4318 this.getSearchInput().bindValue = value;
7390 this.onValueChanged_(value); 4319 this.onValueChanged_(value);
7391 }, 4320 },
7392
7393 showAndFocus: function() { 4321 showAndFocus: function() {
7394 this.showingSearch = true; 4322 this.showingSearch = true;
7395 this.focus_(); 4323 this.focus_();
7396 }, 4324 },
7397
7398 /** @private */
7399 focus_: function() { 4325 focus_: function() {
7400 this.getSearchInput().focus(); 4326 this.getSearchInput().focus();
7401 }, 4327 },
7402
7403 onSearchTermSearch: function() { 4328 onSearchTermSearch: function() {
7404 this.onValueChanged_(this.getValue()); 4329 this.onValueChanged_(this.getValue());
7405 }, 4330 },
7406
7407 /**
7408 * Updates the internal state of the search field based on a change that has
7409 * already happened.
7410 * @param {string} newValue
7411 * @private
7412 */
7413 onValueChanged_: function(newValue) { 4331 onValueChanged_: function(newValue) {
7414 if (newValue == this.lastValue_) 4332 if (newValue == this.lastValue_) return;
7415 return;
7416
7417 this.fire('search-changed', newValue); 4333 this.fire('search-changed', newValue);
7418 this.lastValue_ = newValue; 4334 this.lastValue_ = newValue;
7419 }, 4335 },
7420
7421 onSearchTermKeydown: function(e) { 4336 onSearchTermKeydown: function(e) {
7422 if (e.key == 'Escape') 4337 if (e.key == 'Escape') this.showingSearch = false;
7423 this.showingSearch = false; 4338 },
7424 },
7425
7426 /** @private */
7427 showingSearchChanged_: function() { 4339 showingSearchChanged_: function() {
7428 if (this.showingSearch) { 4340 if (this.showingSearch) {
7429 this.focus_(); 4341 this.focus_();
7430 return; 4342 return;
7431 } 4343 }
7432
7433 this.setValue(''); 4344 this.setValue('');
7434 this.getSearchInput().blur(); 4345 this.getSearchInput().blur();
7435 } 4346 }
7436 }; 4347 };
4348
7437 // Copyright 2016 The Chromium Authors. All rights reserved. 4349 // Copyright 2016 The Chromium Authors. All rights reserved.
7438 // Use of this source code is governed by a BSD-style license that can be 4350 // Use of this source code is governed by a BSD-style license that can be
7439 // found in the LICENSE file. 4351 // found in the LICENSE file.
7440
7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field.
7442 Polymer({ 4352 Polymer({
7443 is: 'cr-toolbar-search-field', 4353 is: 'cr-toolbar-search-field',
7444 4354 behaviors: [ CrSearchFieldBehavior ],
7445 behaviors: [CrSearchFieldBehavior],
7446
7447 properties: { 4355 properties: {
7448 narrow: { 4356 narrow: {
7449 type: Boolean, 4357 type: Boolean,
7450 reflectToAttribute: true, 4358 reflectToAttribute: true
7451 }, 4359 },
7452
7453 // Prompt text to display in the search field.
7454 label: String, 4360 label: String,
7455
7456 // Tooltip to display on the clear search button.
7457 clearLabel: String, 4361 clearLabel: String,
7458
7459 // When true, show a loading spinner to indicate that the backend is
7460 // processing the search. Will only show if the search field is open.
7461 spinnerActive: { 4362 spinnerActive: {
7462 type: Boolean, 4363 type: Boolean,
7463 reflectToAttribute: true 4364 reflectToAttribute: true
7464 }, 4365 },
7465 4366 hasSearchText_: Boolean
7466 /** @private */ 4367 },
7467 hasSearchText_: Boolean,
7468 },
7469
7470 listeners: { 4368 listeners: {
7471 'tap': 'showSearch_', 4369 tap: 'showSearch_',
7472 'searchInput.bind-value-changed': 'onBindValueChanged_', 4370 'searchInput.bind-value-changed': 'onBindValueChanged_'
7473 }, 4371 },
7474
7475 /** @return {!HTMLInputElement} */
7476 getSearchInput: function() { 4372 getSearchInput: function() {
7477 return this.$.searchInput; 4373 return this.$.searchInput;
7478 }, 4374 },
7479
7480 /** @return {boolean} */
7481 isSearchFocused: function() { 4375 isSearchFocused: function() {
7482 return this.$.searchTerm.focused; 4376 return this.$.searchTerm.focused;
7483 }, 4377 },
7484
7485 /**
7486 * @param {boolean} narrow
7487 * @return {number}
7488 * @private
7489 */
7490 computeIconTabIndex_: function(narrow) { 4378 computeIconTabIndex_: function(narrow) {
7491 return narrow ? 0 : -1; 4379 return narrow ? 0 : -1;
7492 }, 4380 },
7493
7494 /**
7495 * @param {boolean} spinnerActive
7496 * @param {boolean} showingSearch
7497 * @return {boolean}
7498 * @private
7499 */
7500 isSpinnerShown_: function(spinnerActive, showingSearch) { 4381 isSpinnerShown_: function(spinnerActive, showingSearch) {
7501 return spinnerActive && showingSearch; 4382 return spinnerActive && showingSearch;
7502 }, 4383 },
7503
7504 /** @private */
7505 onInputBlur_: function() { 4384 onInputBlur_: function() {
7506 if (!this.hasSearchText_) 4385 if (!this.hasSearchText_) this.showingSearch = false;
7507 this.showingSearch = false; 4386 },
7508 },
7509
7510 /**
7511 * Update the state of the search field whenever the underlying input value
7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately
7513 * after any change, whether the result of user input or JS modification.
7514 * @private
7515 */
7516 onBindValueChanged_: function() { 4387 onBindValueChanged_: function() {
7517 var newValue = this.$.searchInput.bindValue; 4388 var newValue = this.$.searchInput.bindValue;
7518 this.hasSearchText_ = newValue != ''; 4389 this.hasSearchText_ = newValue != '';
7519 if (newValue != '') 4390 if (newValue != '') this.showingSearch = true;
7520 this.showingSearch = true; 4391 },
7521 },
7522
7523 /**
7524 * @param {Event} e
7525 * @private
7526 */
7527 showSearch_: function(e) { 4392 showSearch_: function(e) {
7528 if (e.target != this.$.clearSearch) 4393 if (e.target != this.$.clearSearch) this.showingSearch = true;
7529 this.showingSearch = true; 4394 },
7530 },
7531
7532 /**
7533 * @param {Event} e
7534 * @private
7535 */
7536 hideSearch_: function(e) { 4395 hideSearch_: function(e) {
7537 this.showingSearch = false; 4396 this.showingSearch = false;
7538 e.stopPropagation(); 4397 e.stopPropagation();
7539 } 4398 }
7540 }); 4399 });
4400
7541 // Copyright 2016 The Chromium Authors. All rights reserved. 4401 // Copyright 2016 The Chromium Authors. All rights reserved.
7542 // Use of this source code is governed by a BSD-style license that can be 4402 // Use of this source code is governed by a BSD-style license that can be
7543 // found in the LICENSE file. 4403 // found in the LICENSE file.
7544
7545 Polymer({ 4404 Polymer({
7546 is: 'cr-toolbar', 4405 is: 'cr-toolbar',
7547
7548 properties: { 4406 properties: {
7549 // Name to display in the toolbar, in titlecase.
7550 pageName: String, 4407 pageName: String,
7551
7552 // Prompt text to display in the search field.
7553 searchPrompt: String, 4408 searchPrompt: String,
7554
7555 // Tooltip to display on the clear search button.
7556 clearLabel: String, 4409 clearLabel: String,
7557
7558 // Tooltip to display on the menu button.
7559 menuLabel: String, 4410 menuLabel: String,
7560
7561 // Value is proxied through to cr-toolbar-search-field. When true,
7562 // the search field will show a processing spinner.
7563 spinnerActive: Boolean, 4411 spinnerActive: Boolean,
7564
7565 // Controls whether the menu button is shown at the start of the menu.
7566 showMenu: { 4412 showMenu: {
7567 type: Boolean, 4413 type: Boolean,
7568 reflectToAttribute: true, 4414 reflectToAttribute: true,
7569 value: true 4415 value: true
7570 }, 4416 },
7571
7572 /** @private */
7573 narrow_: { 4417 narrow_: {
7574 type: Boolean, 4418 type: Boolean,
7575 reflectToAttribute: true 4419 reflectToAttribute: true
7576 }, 4420 },
7577
7578 /** @private */
7579 showingSearch_: { 4421 showingSearch_: {
7580 type: Boolean, 4422 type: Boolean,
7581 reflectToAttribute: true, 4423 reflectToAttribute: true
7582 }, 4424 }
7583 }, 4425 },
7584
7585 /** @return {!CrToolbarSearchFieldElement} */
7586 getSearchField: function() { 4426 getSearchField: function() {
7587 return this.$.search; 4427 return this.$.search;
7588 }, 4428 },
7589
7590 /** @private */
7591 onMenuTap_: function(e) { 4429 onMenuTap_: function(e) {
7592 this.fire('cr-menu-tap'); 4430 this.fire('cr-menu-tap');
7593 } 4431 }
7594 }); 4432 });
4433
7595 // Copyright 2015 The Chromium Authors. All rights reserved. 4434 // Copyright 2015 The Chromium Authors. All rights reserved.
7596 // Use of this source code is governed by a BSD-style license that can be 4435 // Use of this source code is governed by a BSD-style license that can be
7597 // found in the LICENSE file. 4436 // found in the LICENSE file.
7598
7599 Polymer({ 4437 Polymer({
7600 is: 'history-toolbar', 4438 is: 'history-toolbar',
7601 properties: { 4439 properties: {
7602 // Number of history items currently selected.
7603 // TODO(calamity): bind this to
7604 // listContainer.selectedItem.selectedPaths.length.
7605 count: { 4440 count: {
7606 type: Number, 4441 type: Number,
7607 value: 0, 4442 value: 0,
7608 observer: 'changeToolbarView_' 4443 observer: 'changeToolbarView_'
7609 }, 4444 },
7610
7611 // True if 1 or more history items are selected. When this value changes
7612 // the background colour changes.
7613 itemsSelected_: { 4445 itemsSelected_: {
7614 type: Boolean, 4446 type: Boolean,
7615 value: false, 4447 value: false,
7616 reflectToAttribute: true 4448 reflectToAttribute: true
7617 }, 4449 },
7618
7619 // The most recent term entered in the search field. Updated incrementally
7620 // as the user types.
7621 searchTerm: { 4450 searchTerm: {
7622 type: String, 4451 type: String,
7623 notify: true, 4452 notify: true
7624 }, 4453 },
7625
7626 // True if the backend is processing and a spinner should be shown in the
7627 // toolbar.
7628 spinnerActive: { 4454 spinnerActive: {
7629 type: Boolean, 4455 type: Boolean,
7630 value: false 4456 value: false
7631 }, 4457 },
7632
7633 hasDrawer: { 4458 hasDrawer: {
7634 type: Boolean, 4459 type: Boolean,
7635 observer: 'hasDrawerChanged_', 4460 observer: 'hasDrawerChanged_',
7636 reflectToAttribute: true, 4461 reflectToAttribute: true
7637 }, 4462 },
7638
7639 // Whether domain-grouped history is enabled.
7640 isGroupedMode: { 4463 isGroupedMode: {
7641 type: Boolean, 4464 type: Boolean,
7642 reflectToAttribute: true, 4465 reflectToAttribute: true
7643 }, 4466 },
7644
7645 // The period to search over. Matches BrowsingHistoryHandler::Range.
7646 groupedRange: { 4467 groupedRange: {
7647 type: Number, 4468 type: Number,
7648 value: 0, 4469 value: 0,
7649 reflectToAttribute: true, 4470 reflectToAttribute: true,
7650 notify: true 4471 notify: true
7651 }, 4472 },
7652
7653 // The start time of the query range.
7654 queryStartTime: String, 4473 queryStartTime: String,
7655 4474 queryEndTime: String
7656 // The end time of the query range. 4475 },
7657 queryEndTime: String,
7658 },
7659
7660 /**
7661 * Changes the toolbar background color depending on whether any history items
7662 * are currently selected.
7663 * @private
7664 */
7665 changeToolbarView_: function() { 4476 changeToolbarView_: function() {
7666 this.itemsSelected_ = this.count > 0; 4477 this.itemsSelected_ = this.count > 0;
7667 }, 4478 },
7668
7669 /**
7670 * When changing the search term externally, update the search field to
7671 * reflect the new search term.
7672 * @param {string} search
7673 */
7674 setSearchTerm: function(search) { 4479 setSearchTerm: function(search) {
7675 if (this.searchTerm == search) 4480 if (this.searchTerm == search) return;
7676 return;
7677
7678 this.searchTerm = search; 4481 this.searchTerm = search;
7679 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) 4482 var searchField = this.$['main-toolbar'].getSearchField();
7680 .getSearchField();
7681 searchField.showAndFocus(); 4483 searchField.showAndFocus();
7682 searchField.setValue(search); 4484 searchField.setValue(search);
7683 }, 4485 },
7684
7685 /**
7686 * @param {!CustomEvent} event
7687 * @private
7688 */
7689 onSearchChanged_: function(event) { 4486 onSearchChanged_: function(event) {
7690 this.searchTerm = /** @type {string} */ (event.detail); 4487 this.searchTerm = event.detail;
7691 }, 4488 },
7692
7693 onClearSelectionTap_: function() { 4489 onClearSelectionTap_: function() {
7694 this.fire('unselect-all'); 4490 this.fire('unselect-all');
7695 }, 4491 },
7696
7697 onDeleteTap_: function() { 4492 onDeleteTap_: function() {
7698 this.fire('delete-selected'); 4493 this.fire('delete-selected');
7699 }, 4494 },
7700
7701 get searchBar() { 4495 get searchBar() {
7702 return this.$['main-toolbar'].getSearchField(); 4496 return this.$['main-toolbar'].getSearchField();
7703 }, 4497 },
7704
7705 showSearchField: function() { 4498 showSearchField: function() {
7706 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) 4499 this.$['main-toolbar'].getSearchField().showAndFocus();
7707 .getSearchField() 4500 },
7708 .showAndFocus();
7709 },
7710
7711 /**
7712 * If the user is a supervised user the delete button is not shown.
7713 * @private
7714 */
7715 deletingAllowed_: function() { 4501 deletingAllowed_: function() {
7716 return loadTimeData.getBoolean('allowDeletingHistory'); 4502 return loadTimeData.getBoolean('allowDeletingHistory');
7717 }, 4503 },
7718
7719 numberOfItemsSelected_: function(count) { 4504 numberOfItemsSelected_: function(count) {
7720 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; 4505 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : '';
7721 }, 4506 },
7722
7723 getHistoryInterval_: function(queryStartTime, queryEndTime) { 4507 getHistoryInterval_: function(queryStartTime, queryEndTime) {
7724 // TODO(calamity): Fix the format of these dates. 4508 return loadTimeData.getStringF('historyInterval', queryStartTime, queryEndTi me);
7725 return loadTimeData.getStringF( 4509 },
7726 'historyInterval', queryStartTime, queryEndTime);
7727 },
7728
7729 /** @private */
7730 hasDrawerChanged_: function() { 4510 hasDrawerChanged_: function() {
7731 this.updateStyles(); 4511 this.updateStyles();
7732 }, 4512 }
7733 }); 4513 });
4514
7734 // Copyright 2016 The Chromium Authors. All rights reserved. 4515 // Copyright 2016 The Chromium Authors. All rights reserved.
7735 // Use of this source code is governed by a BSD-style license that can be 4516 // Use of this source code is governed by a BSD-style license that can be
7736 // found in the LICENSE file. 4517 // found in the LICENSE file.
7737
7738 /**
7739 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the
7740 * dialog is closed via close(), a 'close' event is fired. If the dialog is
7741 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event.
7742 * Additionally clients can inspect the dialog's |returnValue| property inside
7743 * the 'close' event listener to determine whether it was canceled or just
7744 * closed, where a truthy value means success, and a falsy value means it was
7745 * canceled.
7746 */
7747 Polymer({ 4518 Polymer({
7748 is: 'cr-dialog', 4519 is: 'cr-dialog',
7749 extends: 'dialog', 4520 "extends": 'dialog',
7750
7751 /** @override */
7752 created: function() { 4521 created: function() {
7753 // If the active history entry changes (i.e. user clicks back button),
7754 // all open dialogs should be cancelled.
7755 window.addEventListener('popstate', function() { 4522 window.addEventListener('popstate', function() {
7756 if (this.open) 4523 if (this.open) this.cancel();
7757 this.cancel();
7758 }.bind(this)); 4524 }.bind(this));
7759 }, 4525 },
7760
7761 cancel: function() { 4526 cancel: function() {
7762 this.fire('cancel'); 4527 this.fire('cancel');
7763 HTMLDialogElement.prototype.close.call(this, ''); 4528 HTMLDialogElement.prototype.close.call(this, '');
7764 }, 4529 },
7765
7766 /**
7767 * @param {string=} opt_returnValue
7768 * @override
7769 */
7770 close: function(opt_returnValue) { 4530 close: function(opt_returnValue) {
7771 HTMLDialogElement.prototype.close.call(this, 'success'); 4531 HTMLDialogElement.prototype.close.call(this, 'success');
7772 }, 4532 },
7773
7774 /** @return {!PaperIconButtonElement} */
7775 getCloseButton: function() { 4533 getCloseButton: function() {
7776 return this.$.close; 4534 return this.$.close;
7777 }, 4535 }
7778 }); 4536 });
7779 /**
7780 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
7781 optionally centers it in the window or another element.
7782 4537
7783 The element will only be sized and/or positioned if it has not already been size d and/or positioned 4538 Polymer.IronFitBehavior = {
7784 by CSS. 4539 properties: {
4540 sizingTarget: {
4541 type: Object,
4542 value: function() {
4543 return this;
4544 }
4545 },
4546 fitInto: {
4547 type: Object,
4548 value: window
4549 },
4550 noOverlap: {
4551 type: Boolean
4552 },
4553 positionTarget: {
4554 type: Element
4555 },
4556 horizontalAlign: {
4557 type: String
4558 },
4559 verticalAlign: {
4560 type: String
4561 },
4562 dynamicAlign: {
4563 type: Boolean
4564 },
4565 horizontalOffset: {
4566 type: Number,
4567 value: 0,
4568 notify: true
4569 },
4570 verticalOffset: {
4571 type: Number,
4572 value: 0,
4573 notify: true
4574 },
4575 autoFitOnAttach: {
4576 type: Boolean,
4577 value: false
4578 },
4579 _fitInfo: {
4580 type: Object
4581 }
4582 },
4583 get _fitWidth() {
4584 var fitWidth;
4585 if (this.fitInto === window) {
4586 fitWidth = this.fitInto.innerWidth;
4587 } else {
4588 fitWidth = this.fitInto.getBoundingClientRect().width;
4589 }
4590 return fitWidth;
4591 },
4592 get _fitHeight() {
4593 var fitHeight;
4594 if (this.fitInto === window) {
4595 fitHeight = this.fitInto.innerHeight;
4596 } else {
4597 fitHeight = this.fitInto.getBoundingClientRect().height;
4598 }
4599 return fitHeight;
4600 },
4601 get _fitLeft() {
4602 var fitLeft;
4603 if (this.fitInto === window) {
4604 fitLeft = 0;
4605 } else {
4606 fitLeft = this.fitInto.getBoundingClientRect().left;
4607 }
4608 return fitLeft;
4609 },
4610 get _fitTop() {
4611 var fitTop;
4612 if (this.fitInto === window) {
4613 fitTop = 0;
4614 } else {
4615 fitTop = this.fitInto.getBoundingClientRect().top;
4616 }
4617 return fitTop;
4618 },
4619 get _defaultPositionTarget() {
4620 var parent = Polymer.dom(this).parentNode;
4621 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
4622 parent = parent.host;
4623 }
4624 return parent;
4625 },
4626 get _localeHorizontalAlign() {
4627 if (this._isRTL) {
4628 if (this.horizontalAlign === 'right') {
4629 return 'left';
4630 }
4631 if (this.horizontalAlign === 'left') {
4632 return 'right';
4633 }
4634 }
4635 return this.horizontalAlign;
4636 },
4637 attached: function() {
4638 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
4639 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
4640 if (this.autoFitOnAttach) {
4641 if (window.getComputedStyle(this).display === 'none') {
4642 setTimeout(function() {
4643 this.fit();
4644 }.bind(this));
4645 } else {
4646 this.fit();
4647 }
4648 }
4649 },
4650 fit: function() {
4651 this.position();
4652 this.constrain();
4653 this.center();
4654 },
4655 _discoverInfo: function() {
4656 if (this._fitInfo) {
4657 return;
4658 }
4659 var target = window.getComputedStyle(this);
4660 var sizer = window.getComputedStyle(this.sizingTarget);
4661 this._fitInfo = {
4662 inlineStyle: {
4663 top: this.style.top || '',
4664 left: this.style.left || '',
4665 position: this.style.position || ''
4666 },
4667 sizerInlineStyle: {
4668 maxWidth: this.sizingTarget.style.maxWidth || '',
4669 maxHeight: this.sizingTarget.style.maxHeight || '',
4670 boxSizing: this.sizingTarget.style.boxSizing || ''
4671 },
4672 positionedBy: {
4673 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? ' bottom' : null,
4674 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto' ? 'right' : null
4675 },
4676 sizedBy: {
4677 height: sizer.maxHeight !== 'none',
4678 width: sizer.maxWidth !== 'none',
4679 minWidth: parseInt(sizer.minWidth, 10) || 0,
4680 minHeight: parseInt(sizer.minHeight, 10) || 0
4681 },
4682 margin: {
4683 top: parseInt(target.marginTop, 10) || 0,
4684 right: parseInt(target.marginRight, 10) || 0,
4685 bottom: parseInt(target.marginBottom, 10) || 0,
4686 left: parseInt(target.marginLeft, 10) || 0
4687 }
4688 };
4689 if (this.verticalOffset) {
4690 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs et;
4691 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
4692 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
4693 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px ';
4694 }
4695 if (this.horizontalOffset) {
4696 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf fset;
4697 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
4698 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
4699 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + ' px';
4700 }
4701 },
4702 resetFit: function() {
4703 var info = this._fitInfo || {};
4704 for (var property in info.sizerInlineStyle) {
4705 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
4706 }
4707 for (var property in info.inlineStyle) {
4708 this.style[property] = info.inlineStyle[property];
4709 }
4710 this._fitInfo = null;
4711 },
4712 refit: function() {
4713 var scrollLeft = this.sizingTarget.scrollLeft;
4714 var scrollTop = this.sizingTarget.scrollTop;
4715 this.resetFit();
4716 this.fit();
4717 this.sizingTarget.scrollLeft = scrollLeft;
4718 this.sizingTarget.scrollTop = scrollTop;
4719 },
4720 position: function() {
4721 if (!this.horizontalAlign && !this.verticalAlign) {
4722 return;
4723 }
4724 this._discoverInfo();
4725 this.style.position = 'fixed';
4726 this.sizingTarget.style.boxSizing = 'border-box';
4727 this.style.left = '0px';
4728 this.style.top = '0px';
4729 var rect = this.getBoundingClientRect();
4730 var positionRect = this.__getNormalizedRect(this.positionTarget);
4731 var fitRect = this.__getNormalizedRect(this.fitInto);
4732 var margin = this._fitInfo.margin;
4733 var size = {
4734 width: rect.width + margin.left + margin.right,
4735 height: rect.height + margin.top + margin.bottom
4736 };
4737 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical Align, size, positionRect, fitRect);
4738 var left = position.left + margin.left;
4739 var top = position.top + margin.top;
4740 var right = Math.min(fitRect.right - margin.right, left + rect.width);
4741 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
4742 var minWidth = this._fitInfo.sizedBy.minWidth;
4743 var minHeight = this._fitInfo.sizedBy.minHeight;
4744 if (left < margin.left) {
4745 left = margin.left;
4746 if (right - left < minWidth) {
4747 left = right - minWidth;
4748 }
4749 }
4750 if (top < margin.top) {
4751 top = margin.top;
4752 if (bottom - top < minHeight) {
4753 top = bottom - minHeight;
4754 }
4755 }
4756 this.sizingTarget.style.maxWidth = right - left + 'px';
4757 this.sizingTarget.style.maxHeight = bottom - top + 'px';
4758 this.style.left = left - rect.left + 'px';
4759 this.style.top = top - rect.top + 'px';
4760 },
4761 constrain: function() {
4762 if (this.horizontalAlign || this.verticalAlign) {
4763 return;
4764 }
4765 this._discoverInfo();
4766 var info = this._fitInfo;
4767 if (!info.positionedBy.vertically) {
4768 this.style.position = 'fixed';
4769 this.style.top = '0px';
4770 }
4771 if (!info.positionedBy.horizontally) {
4772 this.style.position = 'fixed';
4773 this.style.left = '0px';
4774 }
4775 this.sizingTarget.style.boxSizing = 'border-box';
4776 var rect = this.getBoundingClientRect();
4777 if (!info.sizedBy.height) {
4778 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
4779 }
4780 if (!info.sizedBy.width) {
4781 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right' , 'Width');
4782 }
4783 },
4784 _sizeDimension: function(rect, positionedBy, start, end, extent) {
4785 this.__sizeDimension(rect, positionedBy, start, end, extent);
4786 },
4787 __sizeDimension: function(rect, positionedBy, start, end, extent) {
4788 var info = this._fitInfo;
4789 var fitRect = this.__getNormalizedRect(this.fitInto);
4790 var max = extent === 'Width' ? fitRect.width : fitRect.height;
4791 var flip = positionedBy === end;
4792 var offset = flip ? max - rect[end] : rect[start];
4793 var margin = info.margin[flip ? start : end];
4794 var offsetExtent = 'offset' + extent;
4795 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
4796 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs et + 'px';
4797 },
4798 center: function() {
4799 if (this.horizontalAlign || this.verticalAlign) {
4800 return;
4801 }
4802 this._discoverInfo();
4803 var positionedBy = this._fitInfo.positionedBy;
4804 if (positionedBy.vertically && positionedBy.horizontally) {
4805 return;
4806 }
4807 this.style.position = 'fixed';
4808 if (!positionedBy.vertically) {
4809 this.style.top = '0px';
4810 }
4811 if (!positionedBy.horizontally) {
4812 this.style.left = '0px';
4813 }
4814 var rect = this.getBoundingClientRect();
4815 var fitRect = this.__getNormalizedRect(this.fitInto);
4816 if (!positionedBy.vertically) {
4817 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
4818 this.style.top = top + 'px';
4819 }
4820 if (!positionedBy.horizontally) {
4821 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
4822 this.style.left = left + 'px';
4823 }
4824 },
4825 __getNormalizedRect: function(target) {
4826 if (target === document.documentElement || target === window) {
4827 return {
4828 top: 0,
4829 left: 0,
4830 width: window.innerWidth,
4831 height: window.innerHeight,
4832 right: window.innerWidth,
4833 bottom: window.innerHeight
4834 };
4835 }
4836 return target.getBoundingClientRect();
4837 },
4838 __getCroppedArea: function(position, size, fitRect) {
4839 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
4840 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
4841 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size .height;
4842 },
4843 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
4844 var positions = [ {
4845 verticalAlign: 'top',
4846 horizontalAlign: 'left',
4847 top: positionRect.top,
4848 left: positionRect.left
4849 }, {
4850 verticalAlign: 'top',
4851 horizontalAlign: 'right',
4852 top: positionRect.top,
4853 left: positionRect.right - size.width
4854 }, {
4855 verticalAlign: 'bottom',
4856 horizontalAlign: 'left',
4857 top: positionRect.bottom - size.height,
4858 left: positionRect.left
4859 }, {
4860 verticalAlign: 'bottom',
4861 horizontalAlign: 'right',
4862 top: positionRect.bottom - size.height,
4863 left: positionRect.right - size.width
4864 } ];
4865 if (this.noOverlap) {
4866 for (var i = 0, l = positions.length; i < l; i++) {
4867 var copy = {};
4868 for (var key in positions[i]) {
4869 copy[key] = positions[i][key];
4870 }
4871 positions.push(copy);
4872 }
4873 positions[0].top = positions[1].top += positionRect.height;
4874 positions[2].top = positions[3].top -= positionRect.height;
4875 positions[4].left = positions[6].left += positionRect.width;
4876 positions[5].left = positions[7].left -= positionRect.width;
4877 }
4878 vAlign = vAlign === 'auto' ? null : vAlign;
4879 hAlign = hAlign === 'auto' ? null : hAlign;
4880 var position;
4881 for (var i = 0; i < positions.length; i++) {
4882 var pos = positions[i];
4883 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
4884 position = pos;
4885 break;
4886 }
4887 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos .horizontalAlign === hAlign);
4888 if (!this.dynamicAlign && !alignOk) {
4889 continue;
4890 }
4891 position = position || pos;
4892 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
4893 var diff = pos.croppedArea - position.croppedArea;
4894 if (diff < 0 || diff === 0 && alignOk) {
4895 position = pos;
4896 }
4897 if (position.croppedArea === 0 && alignOk) {
4898 break;
4899 }
4900 }
4901 return position;
4902 }
4903 };
7785 4904
7786 CSS properties | Action 4905 (function() {
7787 -----------------------------|------------------------------------------- 4906 'use strict';
7788 `position` set | Element is not centered horizontally or verticall y 4907 Polymer({
7789 `top` or `bottom` set | Element is not vertically centered 4908 is: 'iron-overlay-backdrop',
7790 `left` or `right` set | Element is not horizontally centered
7791 `max-height` set | Element respects `max-height`
7792 `max-width` set | Element respects `max-width`
7793
7794 `Polymer.IronFitBehavior` can position an element into another element using
7795 `verticalAlign` and `horizontalAlign`. This will override the element's css posi tion.
7796
7797 <div class="container">
7798 <iron-fit-impl vertical-align="top" horizontal-align="auto">
7799 Positioned into the container
7800 </iron-fit-impl>
7801 </div>
7802
7803 Use `noOverlap` to position the element around another element without overlappi ng it.
7804
7805 <div class="container">
7806 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
7807 Positioned around the container
7808 </iron-fit-impl>
7809 </div>
7810
7811 @demo demo/index.html
7812 @polymerBehavior
7813 */
7814
7815 Polymer.IronFitBehavior = {
7816
7817 properties: { 4909 properties: {
7818
7819 /**
7820 * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
7821 * but it can be set to a child element. This is useful, for example, for implementing a
7822 * scrolling region inside the element.
7823 * @type {!Element}
7824 */
7825 sizingTarget: {
7826 type: Object,
7827 value: function() {
7828 return this;
7829 }
7830 },
7831
7832 /**
7833 * The element to fit `this` into.
7834 */
7835 fitInto: {
7836 type: Object,
7837 value: window
7838 },
7839
7840 /**
7841 * Will position the element around the positionTarget without overlapping it.
7842 */
7843 noOverlap: {
7844 type: Boolean
7845 },
7846
7847 /**
7848 * The element that should be used to position the element. If not set, it will
7849 * default to the parent node.
7850 * @type {!Element}
7851 */
7852 positionTarget: {
7853 type: Element
7854 },
7855
7856 /**
7857 * The orientation against which to align the element horizontally
7858 * relative to the `positionTarget`. Possible values are "left", "right", "auto".
7859 */
7860 horizontalAlign: {
7861 type: String
7862 },
7863
7864 /**
7865 * The orientation against which to align the element vertically
7866 * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
7867 */
7868 verticalAlign: {
7869 type: String
7870 },
7871
7872 /**
7873 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr eferred alignment
7874 * and if there's not enough space, it will pick the values which minimize the cropping.
7875 */
7876 dynamicAlign: {
7877 type: Boolean
7878 },
7879
7880 /**
7881 * The same as setting margin-left and margin-right css properties.
7882 * @deprecated
7883 */
7884 horizontalOffset: {
7885 type: Number,
7886 value: 0,
7887 notify: true
7888 },
7889
7890 /**
7891 * The same as setting margin-top and margin-bottom css properties.
7892 * @deprecated
7893 */
7894 verticalOffset: {
7895 type: Number,
7896 value: 0,
7897 notify: true
7898 },
7899
7900 /**
7901 * Set to true to auto-fit on attach.
7902 */
7903 autoFitOnAttach: {
7904 type: Boolean,
7905 value: false
7906 },
7907
7908 /** @type {?Object} */
7909 _fitInfo: {
7910 type: Object
7911 }
7912 },
7913
7914 get _fitWidth() {
7915 var fitWidth;
7916 if (this.fitInto === window) {
7917 fitWidth = this.fitInto.innerWidth;
7918 } else {
7919 fitWidth = this.fitInto.getBoundingClientRect().width;
7920 }
7921 return fitWidth;
7922 },
7923
7924 get _fitHeight() {
7925 var fitHeight;
7926 if (this.fitInto === window) {
7927 fitHeight = this.fitInto.innerHeight;
7928 } else {
7929 fitHeight = this.fitInto.getBoundingClientRect().height;
7930 }
7931 return fitHeight;
7932 },
7933
7934 get _fitLeft() {
7935 var fitLeft;
7936 if (this.fitInto === window) {
7937 fitLeft = 0;
7938 } else {
7939 fitLeft = this.fitInto.getBoundingClientRect().left;
7940 }
7941 return fitLeft;
7942 },
7943
7944 get _fitTop() {
7945 var fitTop;
7946 if (this.fitInto === window) {
7947 fitTop = 0;
7948 } else {
7949 fitTop = this.fitInto.getBoundingClientRect().top;
7950 }
7951 return fitTop;
7952 },
7953
7954 /**
7955 * The element that should be used to position the element,
7956 * if no position target is configured.
7957 */
7958 get _defaultPositionTarget() {
7959 var parent = Polymer.dom(this).parentNode;
7960
7961 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
7962 parent = parent.host;
7963 }
7964
7965 return parent;
7966 },
7967
7968 /**
7969 * The horizontal align value, accounting for the RTL/LTR text direction.
7970 */
7971 get _localeHorizontalAlign() {
7972 if (this._isRTL) {
7973 // In RTL, "left" becomes "right".
7974 if (this.horizontalAlign === 'right') {
7975 return 'left';
7976 }
7977 if (this.horizontalAlign === 'left') {
7978 return 'right';
7979 }
7980 }
7981 return this.horizontalAlign;
7982 },
7983
7984 attached: function() {
7985 // Memoize this to avoid expensive calculations & relayouts.
7986 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
7987 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
7988 if (this.autoFitOnAttach) {
7989 if (window.getComputedStyle(this).display === 'none') {
7990 setTimeout(function() {
7991 this.fit();
7992 }.bind(this));
7993 } else {
7994 this.fit();
7995 }
7996 }
7997 },
7998
7999 /**
8000 * Positions and fits the element into the `fitInto` element.
8001 */
8002 fit: function() {
8003 this.position();
8004 this.constrain();
8005 this.center();
8006 },
8007
8008 /**
8009 * Memoize information needed to position and size the target element.
8010 * @suppress {deprecated}
8011 */
8012 _discoverInfo: function() {
8013 if (this._fitInfo) {
8014 return;
8015 }
8016 var target = window.getComputedStyle(this);
8017 var sizer = window.getComputedStyle(this.sizingTarget);
8018
8019 this._fitInfo = {
8020 inlineStyle: {
8021 top: this.style.top || '',
8022 left: this.style.left || '',
8023 position: this.style.position || ''
8024 },
8025 sizerInlineStyle: {
8026 maxWidth: this.sizingTarget.style.maxWidth || '',
8027 maxHeight: this.sizingTarget.style.maxHeight || '',
8028 boxSizing: this.sizingTarget.style.boxSizing || ''
8029 },
8030 positionedBy: {
8031 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
8032 'bottom' : null),
8033 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut o' ?
8034 'right' : null)
8035 },
8036 sizedBy: {
8037 height: sizer.maxHeight !== 'none',
8038 width: sizer.maxWidth !== 'none',
8039 minWidth: parseInt(sizer.minWidth, 10) || 0,
8040 minHeight: parseInt(sizer.minHeight, 10) || 0
8041 },
8042 margin: {
8043 top: parseInt(target.marginTop, 10) || 0,
8044 right: parseInt(target.marginRight, 10) || 0,
8045 bottom: parseInt(target.marginBottom, 10) || 0,
8046 left: parseInt(target.marginLeft, 10) || 0
8047 }
8048 };
8049
8050 // Support these properties until they are removed.
8051 if (this.verticalOffset) {
8052 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf fset;
8053 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
8054 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
8055 this.style.marginTop = this.style.marginBottom = this.verticalOffset + ' px';
8056 }
8057 if (this.horizontalOffset) {
8058 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal Offset;
8059 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
8060 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
8061 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
8062 }
8063 },
8064
8065 /**
8066 * Resets the target element's position and size constraints, and clear
8067 * the memoized data.
8068 */
8069 resetFit: function() {
8070 var info = this._fitInfo || {};
8071 for (var property in info.sizerInlineStyle) {
8072 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
8073 }
8074 for (var property in info.inlineStyle) {
8075 this.style[property] = info.inlineStyle[property];
8076 }
8077
8078 this._fitInfo = null;
8079 },
8080
8081 /**
8082 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
8083 * the element or the `fitInto` element has been resized, or if any of the
8084 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated .
8085 * It preserves the scroll position of the sizingTarget.
8086 */
8087 refit: function() {
8088 var scrollLeft = this.sizingTarget.scrollLeft;
8089 var scrollTop = this.sizingTarget.scrollTop;
8090 this.resetFit();
8091 this.fit();
8092 this.sizingTarget.scrollLeft = scrollLeft;
8093 this.sizingTarget.scrollTop = scrollTop;
8094 },
8095
8096 /**
8097 * Positions the element according to `horizontalAlign, verticalAlign`.
8098 */
8099 position: function() {
8100 if (!this.horizontalAlign && !this.verticalAlign) {
8101 // needs to be centered, and it is done after constrain.
8102 return;
8103 }
8104 this._discoverInfo();
8105
8106 this.style.position = 'fixed';
8107 // Need border-box for margin/padding.
8108 this.sizingTarget.style.boxSizing = 'border-box';
8109 // Set to 0, 0 in order to discover any offset caused by parent stacking c ontexts.
8110 this.style.left = '0px';
8111 this.style.top = '0px';
8112
8113 var rect = this.getBoundingClientRect();
8114 var positionRect = this.__getNormalizedRect(this.positionTarget);
8115 var fitRect = this.__getNormalizedRect(this.fitInto);
8116
8117 var margin = this._fitInfo.margin;
8118
8119 // Consider the margin as part of the size for position calculations.
8120 var size = {
8121 width: rect.width + margin.left + margin.right,
8122 height: rect.height + margin.top + margin.bottom
8123 };
8124
8125 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic alAlign, size, positionRect, fitRect);
8126
8127 var left = position.left + margin.left;
8128 var top = position.top + margin.top;
8129
8130 // Use original size (without margin).
8131 var right = Math.min(fitRect.right - margin.right, left + rect.width);
8132 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
8133
8134 var minWidth = this._fitInfo.sizedBy.minWidth;
8135 var minHeight = this._fitInfo.sizedBy.minHeight;
8136 if (left < margin.left) {
8137 left = margin.left;
8138 if (right - left < minWidth) {
8139 left = right - minWidth;
8140 }
8141 }
8142 if (top < margin.top) {
8143 top = margin.top;
8144 if (bottom - top < minHeight) {
8145 top = bottom - minHeight;
8146 }
8147 }
8148
8149 this.sizingTarget.style.maxWidth = (right - left) + 'px';
8150 this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
8151
8152 // Remove the offset caused by any stacking context.
8153 this.style.left = (left - rect.left) + 'px';
8154 this.style.top = (top - rect.top) + 'px';
8155 },
8156
8157 /**
8158 * Constrains the size of the element to `fitInto` by setting `max-height`
8159 * and/or `max-width`.
8160 */
8161 constrain: function() {
8162 if (this.horizontalAlign || this.verticalAlign) {
8163 return;
8164 }
8165 this._discoverInfo();
8166
8167 var info = this._fitInfo;
8168 // position at (0px, 0px) if not already positioned, so we can measure the natural size.
8169 if (!info.positionedBy.vertically) {
8170 this.style.position = 'fixed';
8171 this.style.top = '0px';
8172 }
8173 if (!info.positionedBy.horizontally) {
8174 this.style.position = 'fixed';
8175 this.style.left = '0px';
8176 }
8177
8178 // need border-box for margin/padding
8179 this.sizingTarget.style.boxSizing = 'border-box';
8180 // constrain the width and height if not already set
8181 var rect = this.getBoundingClientRect();
8182 if (!info.sizedBy.height) {
8183 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom' , 'Height');
8184 }
8185 if (!info.sizedBy.width) {
8186 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ t', 'Width');
8187 }
8188 },
8189
8190 /**
8191 * @protected
8192 * @deprecated
8193 */
8194 _sizeDimension: function(rect, positionedBy, start, end, extent) {
8195 this.__sizeDimension(rect, positionedBy, start, end, extent);
8196 },
8197
8198 /**
8199 * @private
8200 */
8201 __sizeDimension: function(rect, positionedBy, start, end, extent) {
8202 var info = this._fitInfo;
8203 var fitRect = this.__getNormalizedRect(this.fitInto);
8204 var max = extent === 'Width' ? fitRect.width : fitRect.height;
8205 var flip = (positionedBy === end);
8206 var offset = flip ? max - rect[end] : rect[start];
8207 var margin = info.margin[flip ? start : end];
8208 var offsetExtent = 'offset' + extent;
8209 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
8210 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO ffset) + 'px';
8211 },
8212
8213 /**
8214 * Centers horizontally and vertically if not already positioned. This also sets
8215 * `position:fixed`.
8216 */
8217 center: function() {
8218 if (this.horizontalAlign || this.verticalAlign) {
8219 return;
8220 }
8221 this._discoverInfo();
8222
8223 var positionedBy = this._fitInfo.positionedBy;
8224 if (positionedBy.vertically && positionedBy.horizontally) {
8225 // Already positioned.
8226 return;
8227 }
8228 // Need position:fixed to center
8229 this.style.position = 'fixed';
8230 // Take into account the offset caused by parents that create stacking
8231 // contexts (e.g. with transform: translate3d). Translate to 0,0 and
8232 // measure the bounding rect.
8233 if (!positionedBy.vertically) {
8234 this.style.top = '0px';
8235 }
8236 if (!positionedBy.horizontally) {
8237 this.style.left = '0px';
8238 }
8239 // It will take in consideration margins and transforms
8240 var rect = this.getBoundingClientRect();
8241 var fitRect = this.__getNormalizedRect(this.fitInto);
8242 if (!positionedBy.vertically) {
8243 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
8244 this.style.top = top + 'px';
8245 }
8246 if (!positionedBy.horizontally) {
8247 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
8248 this.style.left = left + 'px';
8249 }
8250 },
8251
8252 __getNormalizedRect: function(target) {
8253 if (target === document.documentElement || target === window) {
8254 return {
8255 top: 0,
8256 left: 0,
8257 width: window.innerWidth,
8258 height: window.innerHeight,
8259 right: window.innerWidth,
8260 bottom: window.innerHeight
8261 };
8262 }
8263 return target.getBoundingClientRect();
8264 },
8265
8266 __getCroppedArea: function(position, size, fitRect) {
8267 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
8268 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ t - (position.left + size.width));
8269 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si ze.height;
8270 },
8271
8272
8273 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
8274 // All the possible configurations.
8275 // Ordered as top-left, top-right, bottom-left, bottom-right.
8276 var positions = [{
8277 verticalAlign: 'top',
8278 horizontalAlign: 'left',
8279 top: positionRect.top,
8280 left: positionRect.left
8281 }, {
8282 verticalAlign: 'top',
8283 horizontalAlign: 'right',
8284 top: positionRect.top,
8285 left: positionRect.right - size.width
8286 }, {
8287 verticalAlign: 'bottom',
8288 horizontalAlign: 'left',
8289 top: positionRect.bottom - size.height,
8290 left: positionRect.left
8291 }, {
8292 verticalAlign: 'bottom',
8293 horizontalAlign: 'right',
8294 top: positionRect.bottom - size.height,
8295 left: positionRect.right - size.width
8296 }];
8297
8298 if (this.noOverlap) {
8299 // Duplicate.
8300 for (var i = 0, l = positions.length; i < l; i++) {
8301 var copy = {};
8302 for (var key in positions[i]) {
8303 copy[key] = positions[i][key];
8304 }
8305 positions.push(copy);
8306 }
8307 // Horizontal overlap only.
8308 positions[0].top = positions[1].top += positionRect.height;
8309 positions[2].top = positions[3].top -= positionRect.height;
8310 // Vertical overlap only.
8311 positions[4].left = positions[6].left += positionRect.width;
8312 positions[5].left = positions[7].left -= positionRect.width;
8313 }
8314
8315 // Consider auto as null for coding convenience.
8316 vAlign = vAlign === 'auto' ? null : vAlign;
8317 hAlign = hAlign === 'auto' ? null : hAlign;
8318
8319 var position;
8320 for (var i = 0; i < positions.length; i++) {
8321 var pos = positions[i];
8322
8323 // If both vAlign and hAlign are defined, return exact match.
8324 // For dynamicAlign and noOverlap we'll have more than one candidate, so
8325 // we'll have to check the croppedArea to make the best choice.
8326 if (!this.dynamicAlign && !this.noOverlap &&
8327 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
8328 position = pos;
8329 break;
8330 }
8331
8332 // Align is ok if alignment preferences are respected. If no preferences ,
8333 // it is considered ok.
8334 var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
8335 (!hAlign || pos.horizontalAlign === hAlign);
8336
8337 // Filter out elements that don't match the alignment (if defined).
8338 // With dynamicAlign, we need to consider all the positions to find the
8339 // one that minimizes the cropped area.
8340 if (!this.dynamicAlign && !alignOk) {
8341 continue;
8342 }
8343
8344 position = position || pos;
8345 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
8346 var diff = pos.croppedArea - position.croppedArea;
8347 // Check which crops less. If it crops equally, check if align is ok.
8348 if (diff < 0 || (diff === 0 && alignOk)) {
8349 position = pos;
8350 }
8351 // If not cropped and respects the align requirements, keep it.
8352 // This allows to prefer positions overlapping horizontally over the
8353 // ones overlapping vertically.
8354 if (position.croppedArea === 0 && alignOk) {
8355 break;
8356 }
8357 }
8358
8359 return position;
8360 }
8361
8362 };
8363 (function() {
8364 'use strict';
8365
8366 Polymer({
8367
8368 is: 'iron-overlay-backdrop',
8369
8370 properties: {
8371
8372 /**
8373 * Returns true if the backdrop is opened.
8374 */
8375 opened: { 4910 opened: {
8376 reflectToAttribute: true, 4911 reflectToAttribute: true,
8377 type: Boolean, 4912 type: Boolean,
8378 value: false, 4913 value: false,
8379 observer: '_openedChanged' 4914 observer: '_openedChanged'
8380 } 4915 }
8381 4916 },
8382 },
8383
8384 listeners: { 4917 listeners: {
8385 'transitionend': '_onTransitionend' 4918 transitionend: '_onTransitionend'
8386 }, 4919 },
8387
8388 created: function() { 4920 created: function() {
8389 // Used to cancel previous requestAnimationFrame calls when opened changes .
8390 this.__openedRaf = null; 4921 this.__openedRaf = null;
8391 }, 4922 },
8392
8393 attached: function() { 4923 attached: function() {
8394 this.opened && this._openedChanged(this.opened); 4924 this.opened && this._openedChanged(this.opened);
8395 }, 4925 },
8396
8397 /**
8398 * Appends the backdrop to document body if needed.
8399 */
8400 prepare: function() { 4926 prepare: function() {
8401 if (this.opened && !this.parentNode) { 4927 if (this.opened && !this.parentNode) {
8402 Polymer.dom(document.body).appendChild(this); 4928 Polymer.dom(document.body).appendChild(this);
8403 } 4929 }
8404 }, 4930 },
8405
8406 /**
8407 * Shows the backdrop.
8408 */
8409 open: function() { 4931 open: function() {
8410 this.opened = true; 4932 this.opened = true;
8411 }, 4933 },
8412
8413 /**
8414 * Hides the backdrop.
8415 */
8416 close: function() { 4934 close: function() {
8417 this.opened = false; 4935 this.opened = false;
8418 }, 4936 },
8419
8420 /**
8421 * Removes the backdrop from document body if needed.
8422 */
8423 complete: function() { 4937 complete: function() {
8424 if (!this.opened && this.parentNode === document.body) { 4938 if (!this.opened && this.parentNode === document.body) {
8425 Polymer.dom(this.parentNode).removeChild(this); 4939 Polymer.dom(this.parentNode).removeChild(this);
8426 } 4940 }
8427 }, 4941 },
8428
8429 _onTransitionend: function(event) { 4942 _onTransitionend: function(event) {
8430 if (event && event.target === this) { 4943 if (event && event.target === this) {
8431 this.complete(); 4944 this.complete();
8432 } 4945 }
8433 }, 4946 },
8434
8435 /**
8436 * @param {boolean} opened
8437 * @private
8438 */
8439 _openedChanged: function(opened) { 4947 _openedChanged: function(opened) {
8440 if (opened) { 4948 if (opened) {
8441 // Auto-attach.
8442 this.prepare(); 4949 this.prepare();
8443 } else { 4950 } else {
8444 // Animation might be disabled via the mixin or opacity custom property.
8445 // If it is disabled in other ways, it's up to the user to call complete .
8446 var cs = window.getComputedStyle(this); 4951 var cs = window.getComputedStyle(this);
8447 if (cs.transitionDuration === '0s' || cs.opacity == 0) { 4952 if (cs.transitionDuration === '0s' || cs.opacity == 0) {
8448 this.complete(); 4953 this.complete();
8449 } 4954 }
8450 } 4955 }
8451
8452 if (!this.isAttached) { 4956 if (!this.isAttached) {
8453 return; 4957 return;
8454 } 4958 }
8455
8456 // Always cancel previous requestAnimationFrame.
8457 if (this.__openedRaf) { 4959 if (this.__openedRaf) {
8458 window.cancelAnimationFrame(this.__openedRaf); 4960 window.cancelAnimationFrame(this.__openedRaf);
8459 this.__openedRaf = null; 4961 this.__openedRaf = null;
8460 } 4962 }
8461 // Force relayout to ensure proper transitions.
8462 this.scrollTop = this.scrollTop; 4963 this.scrollTop = this.scrollTop;
8463 this.__openedRaf = window.requestAnimationFrame(function() { 4964 this.__openedRaf = window.requestAnimationFrame(function() {
8464 this.__openedRaf = null; 4965 this.__openedRaf = null;
8465 this.toggleClass('opened', this.opened); 4966 this.toggleClass('opened', this.opened);
8466 }.bind(this)); 4967 }.bind(this));
8467 } 4968 }
8468 }); 4969 });
8469
8470 })(); 4970 })();
8471 /** 4971
8472 * @struct 4972 Polymer.IronOverlayManagerClass = function() {
8473 * @constructor 4973 this._overlays = [];
8474 * @private 4974 this._minimumZ = 101;
8475 */ 4975 this._backdropElement = null;
8476 Polymer.IronOverlayManagerClass = function() { 4976 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
8477 /** 4977 document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
8478 * Used to keep track of the opened overlays. 4978 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
8479 * @private {Array<Element>} 4979 };
8480 */ 4980
8481 this._overlays = []; 4981 Polymer.IronOverlayManagerClass.prototype = {
8482 4982 constructor: Polymer.IronOverlayManagerClass,
8483 /** 4983 get backdropElement() {
8484 * iframes have a default z-index of 100, 4984 if (!this._backdropElement) {
8485 * so this default should be at least that. 4985 this._backdropElement = document.createElement('iron-overlay-backdrop');
8486 * @private {number} 4986 }
8487 */ 4987 return this._backdropElement;
8488 this._minimumZ = 101; 4988 },
8489 4989 get deepActiveElement() {
8490 /** 4990 var active = document.activeElement || document.body;
8491 * Memoized backdrop element. 4991 while (active.root && Polymer.dom(active.root).activeElement) {
8492 * @private {Element|null} 4992 active = Polymer.dom(active.root).activeElement;
8493 */ 4993 }
8494 this._backdropElement = null; 4994 return active;
8495 4995 },
8496 // Enable document-wide tap recognizer. 4996 _bringOverlayAtIndexToFront: function(i) {
8497 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); 4997 var overlay = this._overlays[i];
8498 4998 if (!overlay) {
8499 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); 4999 return;
8500 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true ); 5000 }
8501 }; 5001 var lastI = this._overlays.length - 1;
8502 5002 var currentOverlay = this._overlays[lastI];
8503 Polymer.IronOverlayManagerClass.prototype = { 5003 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8504 5004 lastI--;
8505 constructor: Polymer.IronOverlayManagerClass, 5005 }
8506 5006 if (i >= lastI) {
8507 /** 5007 return;
8508 * The shared backdrop element. 5008 }
8509 * @type {!Element} backdropElement 5009 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8510 */ 5010 if (this._getZ(overlay) <= minimumZ) {
8511 get backdropElement() { 5011 this._applyOverlayZ(overlay, minimumZ);
8512 if (!this._backdropElement) { 5012 }
8513 this._backdropElement = document.createElement('iron-overlay-backdrop'); 5013 while (i < lastI) {
8514 } 5014 this._overlays[i] = this._overlays[i + 1];
8515 return this._backdropElement; 5015 i++;
8516 }, 5016 }
8517 5017 this._overlays[lastI] = overlay;
8518 /** 5018 },
8519 * The deepest active element. 5019 addOrRemoveOverlay: function(overlay) {
8520 * @type {!Element} activeElement the active element 5020 if (overlay.opened) {
8521 */ 5021 this.addOverlay(overlay);
8522 get deepActiveElement() { 5022 } else {
8523 // document.activeElement can be null 5023 this.removeOverlay(overlay);
8524 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement 5024 }
8525 // In case of null, default it to document.body. 5025 },
8526 var active = document.activeElement || document.body; 5026 addOverlay: function(overlay) {
8527 while (active.root && Polymer.dom(active.root).activeElement) { 5027 var i = this._overlays.indexOf(overlay);
8528 active = Polymer.dom(active.root).activeElement; 5028 if (i >= 0) {
8529 } 5029 this._bringOverlayAtIndexToFront(i);
8530 return active;
8531 },
8532
8533 /**
8534 * Brings the overlay at the specified index to the front.
8535 * @param {number} i
8536 * @private
8537 */
8538 _bringOverlayAtIndexToFront: function(i) {
8539 var overlay = this._overlays[i];
8540 if (!overlay) {
8541 return;
8542 }
8543 var lastI = this._overlays.length - 1;
8544 var currentOverlay = this._overlays[lastI];
8545 // Ensure always-on-top overlay stays on top.
8546 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8547 lastI--;
8548 }
8549 // If already the top element, return.
8550 if (i >= lastI) {
8551 return;
8552 }
8553 // Update z-index to be on top.
8554 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8555 if (this._getZ(overlay) <= minimumZ) {
8556 this._applyOverlayZ(overlay, minimumZ);
8557 }
8558
8559 // Shift other overlays behind the new on top.
8560 while (i < lastI) {
8561 this._overlays[i] = this._overlays[i + 1];
8562 i++;
8563 }
8564 this._overlays[lastI] = overlay;
8565 },
8566
8567 /**
8568 * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
8569 * Also updates the backdrop z-index.
8570 * @param {!Element} overlay
8571 */
8572 addOrRemoveOverlay: function(overlay) {
8573 if (overlay.opened) {
8574 this.addOverlay(overlay);
8575 } else {
8576 this.removeOverlay(overlay);
8577 }
8578 },
8579
8580 /**
8581 * Tracks overlays for z-index and focus management.
8582 * Ensures the last added overlay with always-on-top remains on top.
8583 * @param {!Element} overlay
8584 */
8585 addOverlay: function(overlay) {
8586 var i = this._overlays.indexOf(overlay);
8587 if (i >= 0) {
8588 this._bringOverlayAtIndexToFront(i);
8589 this.trackBackdrop();
8590 return;
8591 }
8592 var insertionIndex = this._overlays.length;
8593 var currentOverlay = this._overlays[insertionIndex - 1];
8594 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8595 var newZ = this._getZ(overlay);
8596
8597 // Ensure always-on-top overlay stays on top.
8598 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8599 // This bumps the z-index of +2.
8600 this._applyOverlayZ(currentOverlay, minimumZ);
8601 insertionIndex--;
8602 // Update minimumZ to match previous overlay's z-index.
8603 var previousOverlay = this._overlays[insertionIndex - 1];
8604 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8605 }
8606
8607 // Update z-index and insert overlay.
8608 if (newZ <= minimumZ) {
8609 this._applyOverlayZ(overlay, minimumZ);
8610 }
8611 this._overlays.splice(insertionIndex, 0, overlay);
8612
8613 this.trackBackdrop(); 5030 this.trackBackdrop();
8614 }, 5031 return;
8615 5032 }
8616 /** 5033 var insertionIndex = this._overlays.length;
8617 * @param {!Element} overlay 5034 var currentOverlay = this._overlays[insertionIndex - 1];
8618 */ 5035 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8619 removeOverlay: function(overlay) { 5036 var newZ = this._getZ(overlay);
8620 var i = this._overlays.indexOf(overlay); 5037 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8621 if (i === -1) { 5038 this._applyOverlayZ(currentOverlay, minimumZ);
8622 return; 5039 insertionIndex--;
8623 } 5040 var previousOverlay = this._overlays[insertionIndex - 1];
8624 this._overlays.splice(i, 1); 5041 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8625 5042 }
8626 this.trackBackdrop(); 5043 if (newZ <= minimumZ) {
8627 }, 5044 this._applyOverlayZ(overlay, minimumZ);
8628 5045 }
8629 /** 5046 this._overlays.splice(insertionIndex, 0, overlay);
8630 * Returns the current overlay. 5047 this.trackBackdrop();
8631 * @return {Element|undefined} 5048 },
8632 */ 5049 removeOverlay: function(overlay) {
8633 currentOverlay: function() { 5050 var i = this._overlays.indexOf(overlay);
8634 var i = this._overlays.length - 1; 5051 if (i === -1) {
8635 return this._overlays[i]; 5052 return;
8636 }, 5053 }
8637 5054 this._overlays.splice(i, 1);
8638 /** 5055 this.trackBackdrop();
8639 * Returns the current overlay z-index. 5056 },
8640 * @return {number} 5057 currentOverlay: function() {
8641 */ 5058 var i = this._overlays.length - 1;
8642 currentOverlayZ: function() { 5059 return this._overlays[i];
8643 return this._getZ(this.currentOverlay()); 5060 },
8644 }, 5061 currentOverlayZ: function() {
8645 5062 return this._getZ(this.currentOverlay());
8646 /** 5063 },
8647 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. 5064 ensureMinimumZ: function(minimumZ) {
8648 * This does not effect the z-index of any existing overlays. 5065 this._minimumZ = Math.max(this._minimumZ, minimumZ);
8649 * @param {number} minimumZ 5066 },
8650 */ 5067 focusOverlay: function() {
8651 ensureMinimumZ: function(minimumZ) { 5068 var current = this.currentOverlay();
8652 this._minimumZ = Math.max(this._minimumZ, minimumZ); 5069 if (current) {
8653 }, 5070 current._applyFocus();
8654 5071 }
8655 focusOverlay: function() { 5072 },
8656 var current = /** @type {?} */ (this.currentOverlay()); 5073 trackBackdrop: function() {
8657 if (current) { 5074 var overlay = this._overlayWithBackdrop();
8658 current._applyFocus(); 5075 if (!overlay && !this._backdropElement) {
8659 } 5076 return;
8660 }, 5077 }
8661 5078 this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
8662 /** 5079 this.backdropElement.opened = !!overlay;
8663 * Updates the backdrop z-index. 5080 },
8664 */ 5081 getBackdrops: function() {
8665 trackBackdrop: function() { 5082 var backdrops = [];
8666 var overlay = this._overlayWithBackdrop(); 5083 for (var i = 0; i < this._overlays.length; i++) {
8667 // Avoid creating the backdrop if there is no overlay with backdrop. 5084 if (this._overlays[i].withBackdrop) {
8668 if (!overlay && !this._backdropElement) { 5085 backdrops.push(this._overlays[i]);
8669 return; 5086 }
8670 } 5087 }
8671 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; 5088 return backdrops;
8672 this.backdropElement.opened = !!overlay; 5089 },
8673 }, 5090 backdropZ: function() {
8674 5091 return this._getZ(this._overlayWithBackdrop()) - 1;
8675 /** 5092 },
8676 * @return {Array<Element>} 5093 _overlayWithBackdrop: function() {
8677 */ 5094 for (var i = 0; i < this._overlays.length; i++) {
8678 getBackdrops: function() { 5095 if (this._overlays[i].withBackdrop) {
8679 var backdrops = []; 5096 return this._overlays[i];
8680 for (var i = 0; i < this._overlays.length; i++) { 5097 }
8681 if (this._overlays[i].withBackdrop) { 5098 }
8682 backdrops.push(this._overlays[i]); 5099 },
8683 } 5100 _getZ: function(overlay) {
8684 } 5101 var z = this._minimumZ;
8685 return backdrops; 5102 if (overlay) {
8686 }, 5103 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z Index);
8687 5104 if (z1 === z1) {
8688 /** 5105 z = z1;
8689 * Returns the z-index for the backdrop. 5106 }
8690 * @return {number} 5107 }
8691 */ 5108 return z;
8692 backdropZ: function() { 5109 },
8693 return this._getZ(this._overlayWithBackdrop()) - 1; 5110 _setZ: function(element, z) {
8694 }, 5111 element.style.zIndex = z;
8695 5112 },
8696 /** 5113 _applyOverlayZ: function(overlay, aboveZ) {
8697 * Returns the first opened overlay that has a backdrop. 5114 this._setZ(overlay, aboveZ + 2);
8698 * @return {Element|undefined} 5115 },
8699 * @private 5116 _overlayInPath: function(path) {
8700 */ 5117 path = path || [];
8701 _overlayWithBackdrop: function() { 5118 for (var i = 0; i < path.length; i++) {
8702 for (var i = 0; i < this._overlays.length; i++) { 5119 if (path[i]._manager === this) {
8703 if (this._overlays[i].withBackdrop) { 5120 return path[i];
8704 return this._overlays[i]; 5121 }
8705 } 5122 }
8706 } 5123 },
8707 }, 5124 _onCaptureClick: function(event) {
8708 5125 var overlay = this.currentOverlay();
8709 /** 5126 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8710 * Calculates the minimum z-index for the overlay. 5127 overlay._onCaptureClick(event);
8711 * @param {Element=} overlay 5128 }
8712 * @private 5129 },
8713 */ 5130 _onCaptureFocus: function(event) {
8714 _getZ: function(overlay) { 5131 var overlay = this.currentOverlay();
8715 var z = this._minimumZ; 5132 if (overlay) {
8716 if (overlay) { 5133 overlay._onCaptureFocus(event);
8717 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay) .zIndex); 5134 }
8718 // Check if is a number 5135 },
8719 // Number.isNaN not supported in IE 10+ 5136 _onCaptureKeyDown: function(event) {
8720 if (z1 === z1) { 5137 var overlay = this.currentOverlay();
8721 z = z1; 5138 if (overlay) {
8722 } 5139 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8723 } 5140 overlay._onCaptureEsc(event);
8724 return z; 5141 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't ab')) {
8725 }, 5142 overlay._onCaptureTab(event);
8726 5143 }
8727 /** 5144 }
8728 * @param {!Element} element 5145 },
8729 * @param {number|string} z 5146 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8730 * @private 5147 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8731 */ 5148 }
8732 _setZ: function(element, z) { 5149 };
8733 element.style.zIndex = z; 5150
8734 }, 5151 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8735 5152
8736 /**
8737 * @param {!Element} overlay
8738 * @param {number} aboveZ
8739 * @private
8740 */
8741 _applyOverlayZ: function(overlay, aboveZ) {
8742 this._setZ(overlay, aboveZ + 2);
8743 },
8744
8745 /**
8746 * Returns the deepest overlay in the path.
8747 * @param {Array<Element>=} path
8748 * @return {Element|undefined}
8749 * @suppress {missingProperties}
8750 * @private
8751 */
8752 _overlayInPath: function(path) {
8753 path = path || [];
8754 for (var i = 0; i < path.length; i++) {
8755 if (path[i]._manager === this) {
8756 return path[i];
8757 }
8758 }
8759 },
8760
8761 /**
8762 * Ensures the click event is delegated to the right overlay.
8763 * @param {!Event} event
8764 * @private
8765 */
8766 _onCaptureClick: function(event) {
8767 var overlay = /** @type {?} */ (this.currentOverlay());
8768 // Check if clicked outside of top overlay.
8769 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8770 overlay._onCaptureClick(event);
8771 }
8772 },
8773
8774 /**
8775 * Ensures the focus event is delegated to the right overlay.
8776 * @param {!Event} event
8777 * @private
8778 */
8779 _onCaptureFocus: function(event) {
8780 var overlay = /** @type {?} */ (this.currentOverlay());
8781 if (overlay) {
8782 overlay._onCaptureFocus(event);
8783 }
8784 },
8785
8786 /**
8787 * Ensures TAB and ESC keyboard events are delegated to the right overlay.
8788 * @param {!Event} event
8789 * @private
8790 */
8791 _onCaptureKeyDown: function(event) {
8792 var overlay = /** @type {?} */ (this.currentOverlay());
8793 if (overlay) {
8794 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8795 overlay._onCaptureEsc(event);
8796 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
8797 overlay._onCaptureTab(event);
8798 }
8799 }
8800 },
8801
8802 /**
8803 * Returns if the overlay1 should be behind overlay2.
8804 * @param {!Element} overlay1
8805 * @param {!Element} overlay2
8806 * @return {boolean}
8807 * @suppress {missingProperties}
8808 * @private
8809 */
8810 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8811 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8812 }
8813 };
8814
8815 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8816 (function() { 5153 (function() {
8817 'use strict'; 5154 'use strict';
8818
8819 /**
8820 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
8821 on top of other content. It includes an optional backdrop, and can be used to im plement a variety
8822 of UI controls including dialogs and drop downs. Multiple overlays may be displa yed at once.
8823
8824 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav ior/blob/master/demo/simple-overlay.html)
8825 for an example.
8826
8827 ### Closing and canceling
8828
8829 An overlay may be hidden by closing or canceling. The difference between close a nd cancel is user
8830 intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
8831 it will cancel whenever the user taps outside it or presses the escape key. This behavior is
8832 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click ` properties.
8833 `close()` should be called explicitly by the implementer when the user interacts with a control
8834 in the overlay element. When the dialog is canceled, the overlay fires an 'iron- overlay-canceled'
8835 event. Call `preventDefault` on this event to prevent the overlay from closing.
8836
8837 ### Positioning
8838
8839 By default the element is sized and positioned to fit and centered inside the wi ndow. You can
8840 position and size it manually using CSS. See `Polymer.IronFitBehavior`.
8841
8842 ### Backdrop
8843
8844 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
8845 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
8846 options.
8847
8848 In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
8849 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f ocusableNodes)
8850 to achieve a different behavior.
8851
8852 ### Limitations
8853
8854 The element is styled to appear on top of other content by setting its `z-index` property. You
8855 must ensure no element has a stacking context with a higher `z-index` than its p arent stacking
8856 context. You should place this element as a child of `<body>` whenever possible.
8857
8858 @demo demo/index.html
8859 @polymerBehavior Polymer.IronOverlayBehavior
8860 */
8861
8862 Polymer.IronOverlayBehaviorImpl = { 5155 Polymer.IronOverlayBehaviorImpl = {
8863
8864 properties: { 5156 properties: {
8865
8866 /**
8867 * True if the overlay is currently displayed.
8868 */
8869 opened: { 5157 opened: {
8870 observer: '_openedChanged', 5158 observer: '_openedChanged',
8871 type: Boolean, 5159 type: Boolean,
8872 value: false, 5160 value: false,
8873 notify: true 5161 notify: true
8874 }, 5162 },
8875
8876 /**
8877 * True if the overlay was canceled when it was last closed.
8878 */
8879 canceled: { 5163 canceled: {
8880 observer: '_canceledChanged', 5164 observer: '_canceledChanged',
8881 readOnly: true, 5165 readOnly: true,
8882 type: Boolean, 5166 type: Boolean,
8883 value: false 5167 value: false
8884 }, 5168 },
8885
8886 /**
8887 * Set to true to display a backdrop behind the overlay. It traps the focu s
8888 * within the light DOM of the overlay.
8889 */
8890 withBackdrop: { 5169 withBackdrop: {
8891 observer: '_withBackdropChanged', 5170 observer: '_withBackdropChanged',
8892 type: Boolean 5171 type: Boolean
8893 }, 5172 },
8894
8895 /**
8896 * Set to true to disable auto-focusing the overlay or child nodes with
8897 * the `autofocus` attribute` when the overlay is opened.
8898 */
8899 noAutoFocus: { 5173 noAutoFocus: {
8900 type: Boolean, 5174 type: Boolean,
8901 value: false 5175 value: false
8902 }, 5176 },
8903
8904 /**
8905 * Set to true to disable canceling the overlay with the ESC key.
8906 */
8907 noCancelOnEscKey: { 5177 noCancelOnEscKey: {
8908 type: Boolean, 5178 type: Boolean,
8909 value: false 5179 value: false
8910 }, 5180 },
8911
8912 /**
8913 * Set to true to disable canceling the overlay by clicking outside it.
8914 */
8915 noCancelOnOutsideClick: { 5181 noCancelOnOutsideClick: {
8916 type: Boolean, 5182 type: Boolean,
8917 value: false 5183 value: false
8918 }, 5184 },
8919
8920 /**
8921 * Contains the reason(s) this overlay was last closed (see `iron-overlay- closed`).
8922 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t he
8923 * behavior can provide other reasons in addition to `canceled`.
8924 */
8925 closingReason: { 5185 closingReason: {
8926 // was a getter before, but needs to be a property so other
8927 // behaviors can override this.
8928 type: Object 5186 type: Object
8929 }, 5187 },
8930
8931 /**
8932 * Set to true to enable restoring of focus when overlay is closed.
8933 */
8934 restoreFocusOnClose: { 5188 restoreFocusOnClose: {
8935 type: Boolean, 5189 type: Boolean,
8936 value: false 5190 value: false
8937 }, 5191 },
8938
8939 /**
8940 * Set to true to keep overlay always on top.
8941 */
8942 alwaysOnTop: { 5192 alwaysOnTop: {
8943 type: Boolean 5193 type: Boolean
8944 }, 5194 },
8945
8946 /**
8947 * Shortcut to access to the overlay manager.
8948 * @private
8949 * @type {Polymer.IronOverlayManagerClass}
8950 */
8951 _manager: { 5195 _manager: {
8952 type: Object, 5196 type: Object,
8953 value: Polymer.IronOverlayManager 5197 value: Polymer.IronOverlayManager
8954 }, 5198 },
8955
8956 /**
8957 * The node being focused.
8958 * @type {?Node}
8959 */
8960 _focusedChild: { 5199 _focusedChild: {
8961 type: Object 5200 type: Object
8962 } 5201 }
8963 5202 },
8964 },
8965
8966 listeners: { 5203 listeners: {
8967 'iron-resize': '_onIronResize' 5204 'iron-resize': '_onIronResize'
8968 }, 5205 },
8969
8970 /**
8971 * The backdrop element.
8972 * @type {Element}
8973 */
8974 get backdropElement() { 5206 get backdropElement() {
8975 return this._manager.backdropElement; 5207 return this._manager.backdropElement;
8976 }, 5208 },
8977
8978 /**
8979 * Returns the node to give focus to.
8980 * @type {Node}
8981 */
8982 get _focusNode() { 5209 get _focusNode() {
8983 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this; 5210 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this;
8984 }, 5211 },
8985
8986 /**
8987 * Array of nodes that can receive focus (overlay included), ordered by `tab index`.
8988 * This is used to retrieve which is the first and last focusable nodes in o rder
8989 * to wrap the focus for overlays `with-backdrop`.
8990 *
8991 * If you know what is your content (specifically the first and last focusab le children),
8992 * you can override this method to return only `[firstFocusable, lastFocusab le];`
8993 * @type {Array<Node>}
8994 * @protected
8995 */
8996 get _focusableNodes() { 5212 get _focusableNodes() {
8997 // Elements that can be focused even if they have [disabled] attribute. 5213 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin dex]', '[contentEditable=true]' ];
8998 var FOCUSABLE_WITH_DISABLED = [ 5214 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button' ];
8999 'a[href]', 5215 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + ': not([tabindex="-1"]),' + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([ tabindex="-1"]),') + ':not([disabled]):not([tabindex="-1"])';
9000 'area[href]',
9001 'iframe',
9002 '[tabindex]',
9003 '[contentEditable=true]'
9004 ];
9005
9006 // Elements that cannot be focused if they have [disabled] attribute.
9007 var FOCUSABLE_WITHOUT_DISABLED = [
9008 'input',
9009 'select',
9010 'textarea',
9011 'button'
9012 ];
9013
9014 // Discard elements with tabindex=-1 (makes them not focusable).
9015 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
9016 ':not([tabindex="-1"]),' +
9017 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),' ) +
9018 ':not([disabled]):not([tabindex="-1"])';
9019
9020 var focusables = Polymer.dom(this).querySelectorAll(selector); 5216 var focusables = Polymer.dom(this).querySelectorAll(selector);
9021 if (this.tabIndex >= 0) { 5217 if (this.tabIndex >= 0) {
9022 // Insert at the beginning because we might have all elements with tabIn dex = 0,
9023 // and the overlay should be the first of the list.
9024 focusables.splice(0, 0, this); 5218 focusables.splice(0, 0, this);
9025 } 5219 }
9026 // Sort by tabindex. 5220 return focusables.sort(function(a, b) {
9027 return focusables.sort(function (a, b) {
9028 if (a.tabIndex === b.tabIndex) { 5221 if (a.tabIndex === b.tabIndex) {
9029 return 0; 5222 return 0;
9030 } 5223 }
9031 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { 5224 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
9032 return 1; 5225 return 1;
9033 } 5226 }
9034 return -1; 5227 return -1;
9035 }); 5228 });
9036 }, 5229 },
9037
9038 ready: function() { 5230 ready: function() {
9039 // Used to skip calls to notifyResize and refit while the overlay is anima ting.
9040 this.__isAnimating = false; 5231 this.__isAnimating = false;
9041 // with-backdrop needs tabindex to be set in order to trap the focus.
9042 // If it is not set, IronOverlayBehavior will set it, and remove it if wit h-backdrop = false.
9043 this.__shouldRemoveTabIndex = false; 5232 this.__shouldRemoveTabIndex = false;
9044 // Used for wrapping the focus on TAB / Shift+TAB.
9045 this.__firstFocusableNode = this.__lastFocusableNode = null; 5233 this.__firstFocusableNode = this.__lastFocusableNode = null;
9046 // Used by __onNextAnimationFrame to cancel any previous callback.
9047 this.__raf = null; 5234 this.__raf = null;
9048 // Focused node before overlay gets opened. Can be restored on close.
9049 this.__restoreFocusNode = null; 5235 this.__restoreFocusNode = null;
9050 this._ensureSetup(); 5236 this._ensureSetup();
9051 }, 5237 },
9052
9053 attached: function() { 5238 attached: function() {
9054 // Call _openedChanged here so that position can be computed correctly.
9055 if (this.opened) { 5239 if (this.opened) {
9056 this._openedChanged(this.opened); 5240 this._openedChanged(this.opened);
9057 } 5241 }
9058 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); 5242 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
9059 }, 5243 },
9060
9061 detached: function() { 5244 detached: function() {
9062 Polymer.dom(this).unobserveNodes(this._observer); 5245 Polymer.dom(this).unobserveNodes(this._observer);
9063 this._observer = null; 5246 this._observer = null;
9064 if (this.__raf) { 5247 if (this.__raf) {
9065 window.cancelAnimationFrame(this.__raf); 5248 window.cancelAnimationFrame(this.__raf);
9066 this.__raf = null; 5249 this.__raf = null;
9067 } 5250 }
9068 this._manager.removeOverlay(this); 5251 this._manager.removeOverlay(this);
9069 }, 5252 },
9070
9071 /**
9072 * Toggle the opened state of the overlay.
9073 */
9074 toggle: function() { 5253 toggle: function() {
9075 this._setCanceled(false); 5254 this._setCanceled(false);
9076 this.opened = !this.opened; 5255 this.opened = !this.opened;
9077 }, 5256 },
9078
9079 /**
9080 * Open the overlay.
9081 */
9082 open: function() { 5257 open: function() {
9083 this._setCanceled(false); 5258 this._setCanceled(false);
9084 this.opened = true; 5259 this.opened = true;
9085 }, 5260 },
9086
9087 /**
9088 * Close the overlay.
9089 */
9090 close: function() { 5261 close: function() {
9091 this._setCanceled(false); 5262 this._setCanceled(false);
9092 this.opened = false; 5263 this.opened = false;
9093 }, 5264 },
9094
9095 /**
9096 * Cancels the overlay.
9097 * @param {Event=} event The original event
9098 */
9099 cancel: function(event) { 5265 cancel: function(event) {
9100 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t rue}); 5266 var cancelEvent = this.fire('iron-overlay-canceled', event, {
5267 cancelable: true
5268 });
9101 if (cancelEvent.defaultPrevented) { 5269 if (cancelEvent.defaultPrevented) {
9102 return; 5270 return;
9103 } 5271 }
9104
9105 this._setCanceled(true); 5272 this._setCanceled(true);
9106 this.opened = false; 5273 this.opened = false;
9107 }, 5274 },
9108
9109 _ensureSetup: function() { 5275 _ensureSetup: function() {
9110 if (this._overlaySetup) { 5276 if (this._overlaySetup) {
9111 return; 5277 return;
9112 } 5278 }
9113 this._overlaySetup = true; 5279 this._overlaySetup = true;
9114 this.style.outline = 'none'; 5280 this.style.outline = 'none';
9115 this.style.display = 'none'; 5281 this.style.display = 'none';
9116 }, 5282 },
9117
9118 /**
9119 * Called when `opened` changes.
9120 * @param {boolean=} opened
9121 * @protected
9122 */
9123 _openedChanged: function(opened) { 5283 _openedChanged: function(opened) {
9124 if (opened) { 5284 if (opened) {
9125 this.removeAttribute('aria-hidden'); 5285 this.removeAttribute('aria-hidden');
9126 } else { 5286 } else {
9127 this.setAttribute('aria-hidden', 'true'); 5287 this.setAttribute('aria-hidden', 'true');
9128 } 5288 }
9129
9130 // Defer any animation-related code on attached
9131 // (_openedChanged gets called again on attached).
9132 if (!this.isAttached) { 5289 if (!this.isAttached) {
9133 return; 5290 return;
9134 } 5291 }
9135
9136 this.__isAnimating = true; 5292 this.__isAnimating = true;
9137
9138 // Use requestAnimationFrame for non-blocking rendering.
9139 this.__onNextAnimationFrame(this.__openedChanged); 5293 this.__onNextAnimationFrame(this.__openedChanged);
9140 }, 5294 },
9141
9142 _canceledChanged: function() { 5295 _canceledChanged: function() {
9143 this.closingReason = this.closingReason || {}; 5296 this.closingReason = this.closingReason || {};
9144 this.closingReason.canceled = this.canceled; 5297 this.closingReason.canceled = this.canceled;
9145 }, 5298 },
9146
9147 _withBackdropChanged: function() { 5299 _withBackdropChanged: function() {
9148 // If tabindex is already set, no need to override it.
9149 if (this.withBackdrop && !this.hasAttribute('tabindex')) { 5300 if (this.withBackdrop && !this.hasAttribute('tabindex')) {
9150 this.setAttribute('tabindex', '-1'); 5301 this.setAttribute('tabindex', '-1');
9151 this.__shouldRemoveTabIndex = true; 5302 this.__shouldRemoveTabIndex = true;
9152 } else if (this.__shouldRemoveTabIndex) { 5303 } else if (this.__shouldRemoveTabIndex) {
9153 this.removeAttribute('tabindex'); 5304 this.removeAttribute('tabindex');
9154 this.__shouldRemoveTabIndex = false; 5305 this.__shouldRemoveTabIndex = false;
9155 } 5306 }
9156 if (this.opened && this.isAttached) { 5307 if (this.opened && this.isAttached) {
9157 this._manager.trackBackdrop(); 5308 this._manager.trackBackdrop();
9158 } 5309 }
9159 }, 5310 },
9160
9161 /**
9162 * tasks which must occur before opening; e.g. making the element visible.
9163 * @protected
9164 */
9165 _prepareRenderOpened: function() { 5311 _prepareRenderOpened: function() {
9166 // Store focused node.
9167 this.__restoreFocusNode = this._manager.deepActiveElement; 5312 this.__restoreFocusNode = this._manager.deepActiveElement;
9168
9169 // Needed to calculate the size of the overlay so that transitions on its size
9170 // will have the correct starting points.
9171 this._preparePositioning(); 5313 this._preparePositioning();
9172 this.refit(); 5314 this.refit();
9173 this._finishPositioning(); 5315 this._finishPositioning();
9174
9175 // Safari will apply the focus to the autofocus element when displayed
9176 // for the first time, so we make sure to return the focus where it was.
9177 if (this.noAutoFocus && document.activeElement === this._focusNode) { 5316 if (this.noAutoFocus && document.activeElement === this._focusNode) {
9178 this._focusNode.blur(); 5317 this._focusNode.blur();
9179 this.__restoreFocusNode.focus(); 5318 this.__restoreFocusNode.focus();
9180 } 5319 }
9181 }, 5320 },
9182
9183 /**
9184 * Tasks which cause the overlay to actually open; typically play an animati on.
9185 * @protected
9186 */
9187 _renderOpened: function() { 5321 _renderOpened: function() {
9188 this._finishRenderOpened(); 5322 this._finishRenderOpened();
9189 }, 5323 },
9190
9191 /**
9192 * Tasks which cause the overlay to actually close; typically play an animat ion.
9193 * @protected
9194 */
9195 _renderClosed: function() { 5324 _renderClosed: function() {
9196 this._finishRenderClosed(); 5325 this._finishRenderClosed();
9197 }, 5326 },
9198
9199 /**
9200 * Tasks to be performed at the end of open action. Will fire `iron-overlay- opened`.
9201 * @protected
9202 */
9203 _finishRenderOpened: function() { 5327 _finishRenderOpened: function() {
9204 this.notifyResize(); 5328 this.notifyResize();
9205 this.__isAnimating = false; 5329 this.__isAnimating = false;
9206
9207 // Store it so we don't query too much.
9208 var focusableNodes = this._focusableNodes; 5330 var focusableNodes = this._focusableNodes;
9209 this.__firstFocusableNode = focusableNodes[0]; 5331 this.__firstFocusableNode = focusableNodes[0];
9210 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; 5332 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
9211
9212 this.fire('iron-overlay-opened'); 5333 this.fire('iron-overlay-opened');
9213 }, 5334 },
9214
9215 /**
9216 * Tasks to be performed at the end of close action. Will fire `iron-overlay -closed`.
9217 * @protected
9218 */
9219 _finishRenderClosed: function() { 5335 _finishRenderClosed: function() {
9220 // Hide the overlay.
9221 this.style.display = 'none'; 5336 this.style.display = 'none';
9222 // Reset z-index only at the end of the animation.
9223 this.style.zIndex = ''; 5337 this.style.zIndex = '';
9224 this.notifyResize(); 5338 this.notifyResize();
9225 this.__isAnimating = false; 5339 this.__isAnimating = false;
9226 this.fire('iron-overlay-closed', this.closingReason); 5340 this.fire('iron-overlay-closed', this.closingReason);
9227 }, 5341 },
9228
9229 _preparePositioning: function() { 5342 _preparePositioning: function() {
9230 this.style.transition = this.style.webkitTransition = 'none'; 5343 this.style.transition = this.style.webkitTransition = 'none';
9231 this.style.transform = this.style.webkitTransform = 'none'; 5344 this.style.transform = this.style.webkitTransform = 'none';
9232 this.style.display = ''; 5345 this.style.display = '';
9233 }, 5346 },
9234
9235 _finishPositioning: function() { 5347 _finishPositioning: function() {
9236 // First, make it invisible & reactivate animations.
9237 this.style.display = 'none'; 5348 this.style.display = 'none';
9238 // Force reflow before re-enabling animations so that they don't start.
9239 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9240 this.scrollTop = this.scrollTop; 5349 this.scrollTop = this.scrollTop;
9241 this.style.transition = this.style.webkitTransition = ''; 5350 this.style.transition = this.style.webkitTransition = '';
9242 this.style.transform = this.style.webkitTransform = ''; 5351 this.style.transform = this.style.webkitTransform = '';
9243 // Now that animations are enabled, make it visible again
9244 this.style.display = ''; 5352 this.style.display = '';
9245 // Force reflow, so that following animations are properly started.
9246 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9247 this.scrollTop = this.scrollTop; 5353 this.scrollTop = this.scrollTop;
9248 }, 5354 },
9249
9250 /**
9251 * Applies focus according to the opened state.
9252 * @protected
9253 */
9254 _applyFocus: function() { 5355 _applyFocus: function() {
9255 if (this.opened) { 5356 if (this.opened) {
9256 if (!this.noAutoFocus) { 5357 if (!this.noAutoFocus) {
9257 this._focusNode.focus(); 5358 this._focusNode.focus();
9258 } 5359 }
9259 } 5360 } else {
9260 else {
9261 this._focusNode.blur(); 5361 this._focusNode.blur();
9262 this._focusedChild = null; 5362 this._focusedChild = null;
9263 // Restore focus.
9264 if (this.restoreFocusOnClose && this.__restoreFocusNode) { 5363 if (this.restoreFocusOnClose && this.__restoreFocusNode) {
9265 this.__restoreFocusNode.focus(); 5364 this.__restoreFocusNode.focus();
9266 } 5365 }
9267 this.__restoreFocusNode = null; 5366 this.__restoreFocusNode = null;
9268 // If many overlays get closed at the same time, one of them would still
9269 // be the currentOverlay even if already closed, and would call _applyFo cus
9270 // infinitely, so we check for this not to be the current overlay.
9271 var currentOverlay = this._manager.currentOverlay(); 5367 var currentOverlay = this._manager.currentOverlay();
9272 if (currentOverlay && this !== currentOverlay) { 5368 if (currentOverlay && this !== currentOverlay) {
9273 currentOverlay._applyFocus(); 5369 currentOverlay._applyFocus();
9274 } 5370 }
9275 } 5371 }
9276 }, 5372 },
9277
9278 /**
9279 * Cancels (closes) the overlay. Call when click happens outside the overlay .
9280 * @param {!Event} event
9281 * @protected
9282 */
9283 _onCaptureClick: function(event) { 5373 _onCaptureClick: function(event) {
9284 if (!this.noCancelOnOutsideClick) { 5374 if (!this.noCancelOnOutsideClick) {
9285 this.cancel(event); 5375 this.cancel(event);
9286 } 5376 }
9287 }, 5377 },
9288 5378 _onCaptureFocus: function(event) {
9289 /**
9290 * Keeps track of the focused child. If withBackdrop, traps focus within ove rlay.
9291 * @param {!Event} event
9292 * @protected
9293 */
9294 _onCaptureFocus: function (event) {
9295 if (!this.withBackdrop) { 5379 if (!this.withBackdrop) {
9296 return; 5380 return;
9297 } 5381 }
9298 var path = Polymer.dom(event).path; 5382 var path = Polymer.dom(event).path;
9299 if (path.indexOf(this) === -1) { 5383 if (path.indexOf(this) === -1) {
9300 event.stopPropagation(); 5384 event.stopPropagation();
9301 this._applyFocus(); 5385 this._applyFocus();
9302 } else { 5386 } else {
9303 this._focusedChild = path[0]; 5387 this._focusedChild = path[0];
9304 } 5388 }
9305 }, 5389 },
9306
9307 /**
9308 * Handles the ESC key event and cancels (closes) the overlay.
9309 * @param {!Event} event
9310 * @protected
9311 */
9312 _onCaptureEsc: function(event) { 5390 _onCaptureEsc: function(event) {
9313 if (!this.noCancelOnEscKey) { 5391 if (!this.noCancelOnEscKey) {
9314 this.cancel(event); 5392 this.cancel(event);
9315 } 5393 }
9316 }, 5394 },
9317
9318 /**
9319 * Handles TAB key events to track focus changes.
9320 * Will wrap focus for overlays withBackdrop.
9321 * @param {!Event} event
9322 * @protected
9323 */
9324 _onCaptureTab: function(event) { 5395 _onCaptureTab: function(event) {
9325 if (!this.withBackdrop) { 5396 if (!this.withBackdrop) {
9326 return; 5397 return;
9327 } 5398 }
9328 // TAB wraps from last to first focusable.
9329 // Shift + TAB wraps from first to last focusable.
9330 var shift = event.shiftKey; 5399 var shift = event.shiftKey;
9331 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node; 5400 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node;
9332 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de; 5401 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de;
9333 var shouldWrap = false; 5402 var shouldWrap = false;
9334 if (nodeToCheck === nodeToSet) { 5403 if (nodeToCheck === nodeToSet) {
9335 // If nodeToCheck is the same as nodeToSet, it means we have an overlay
9336 // with 0 or 1 focusables; in either case we still need to trap the
9337 // focus within the overlay.
9338 shouldWrap = true; 5404 shouldWrap = true;
9339 } else { 5405 } else {
9340 // In dom=shadow, the manager will receive focus changes on the main
9341 // root but not the ones within other shadow roots, so we can't rely on
9342 // _focusedChild, but we should check the deepest active element.
9343 var focusedNode = this._manager.deepActiveElement; 5406 var focusedNode = this._manager.deepActiveElement;
9344 // If the active element is not the nodeToCheck but the overlay itself, 5407 shouldWrap = focusedNode === nodeToCheck || focusedNode === this;
9345 // it means the focus is about to go outside the overlay, hence we 5408 }
9346 // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
9347 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
9348 }
9349
9350 if (shouldWrap) { 5409 if (shouldWrap) {
9351 // When the overlay contains the last focusable element of the document
9352 // and it's already focused, pressing TAB would move the focus outside
9353 // the document (e.g. to the browser search bar). Similarly, when the
9354 // overlay contains the first focusable element of the document and it's
9355 // already focused, pressing Shift+TAB would move the focus outside the
9356 // document (e.g. to the browser search bar).
9357 // In both cases, we would not receive a focus event, but only a blur.
9358 // In order to achieve focus wrapping, we prevent this TAB event and
9359 // force the focus. This will also prevent the focus to temporarily move
9360 // outside the overlay, which might cause scrolling.
9361 event.preventDefault(); 5410 event.preventDefault();
9362 this._focusedChild = nodeToSet; 5411 this._focusedChild = nodeToSet;
9363 this._applyFocus(); 5412 this._applyFocus();
9364 } 5413 }
9365 }, 5414 },
9366
9367 /**
9368 * Refits if the overlay is opened and not animating.
9369 * @protected
9370 */
9371 _onIronResize: function() { 5415 _onIronResize: function() {
9372 if (this.opened && !this.__isAnimating) { 5416 if (this.opened && !this.__isAnimating) {
9373 this.__onNextAnimationFrame(this.refit); 5417 this.__onNextAnimationFrame(this.refit);
9374 } 5418 }
9375 }, 5419 },
9376
9377 /**
9378 * Will call notifyResize if overlay is opened.
9379 * Can be overridden in order to avoid multiple observers on the same node.
9380 * @protected
9381 */
9382 _onNodesChange: function() { 5420 _onNodesChange: function() {
9383 if (this.opened && !this.__isAnimating) { 5421 if (this.opened && !this.__isAnimating) {
9384 this.notifyResize(); 5422 this.notifyResize();
9385 } 5423 }
9386 }, 5424 },
9387
9388 /**
9389 * Tasks executed when opened changes: prepare for the opening, move the
9390 * focus, update the manager, render opened/closed.
9391 * @private
9392 */
9393 __openedChanged: function() { 5425 __openedChanged: function() {
9394 if (this.opened) { 5426 if (this.opened) {
9395 // Make overlay visible, then add it to the manager.
9396 this._prepareRenderOpened(); 5427 this._prepareRenderOpened();
9397 this._manager.addOverlay(this); 5428 this._manager.addOverlay(this);
9398 // Move the focus to the child node with [autofocus].
9399 this._applyFocus(); 5429 this._applyFocus();
9400
9401 this._renderOpened(); 5430 this._renderOpened();
9402 } else { 5431 } else {
9403 // Remove overlay, then restore the focus before actually closing.
9404 this._manager.removeOverlay(this); 5432 this._manager.removeOverlay(this);
9405 this._applyFocus(); 5433 this._applyFocus();
9406
9407 this._renderClosed(); 5434 this._renderClosed();
9408 } 5435 }
9409 }, 5436 },
9410
9411 /**
9412 * Executes a callback on the next animation frame, overriding any previous
9413 * callback awaiting for the next animation frame. e.g.
9414 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
9415 * `callback1` will never be invoked.
9416 * @param {!Function} callback Its `this` parameter is the overlay itself.
9417 * @private
9418 */
9419 __onNextAnimationFrame: function(callback) { 5437 __onNextAnimationFrame: function(callback) {
9420 if (this.__raf) { 5438 if (this.__raf) {
9421 window.cancelAnimationFrame(this.__raf); 5439 window.cancelAnimationFrame(this.__raf);
9422 } 5440 }
9423 var self = this; 5441 var self = this;
9424 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { 5442 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
9425 self.__raf = null; 5443 self.__raf = null;
9426 callback.call(self); 5444 callback.call(self);
9427 }); 5445 });
9428 } 5446 }
9429
9430 }; 5447 };
9431 5448 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable Behavior, Polymer.IronOverlayBehaviorImpl ];
9432 /** @polymerBehavior */
9433 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB ehavior, Polymer.IronOverlayBehaviorImpl];
9434
9435 /**
9436 * Fired after the overlay opens.
9437 * @event iron-overlay-opened
9438 */
9439
9440 /**
9441 * Fired when the overlay is canceled, but before it is closed.
9442 * @event iron-overlay-canceled
9443 * @param {Event} event The closing of the overlay can be prevented
9444 * by calling `event.preventDefault()`. The `event.detail` is the original eve nt that
9445 * originated the canceling (e.g. ESC keyboard event or click event outside th e overlay).
9446 */
9447
9448 /**
9449 * Fired after the overlay closes.
9450 * @event iron-overlay-closed
9451 * @param {Event} event The `event.detail` is the `closingReason` property
9452 * (contains `canceled`, whether the overlay was canceled).
9453 */
9454
9455 })(); 5449 })();
9456 /** 5450
9457 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim ations for use with 5451 Polymer.NeonAnimatableBehavior = {
9458 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. 5452 properties: {
9459 * @polymerBehavior 5453 animationConfig: {
9460 */ 5454 type: Object
9461 Polymer.NeonAnimatableBehavior = { 5455 },
9462 5456 entryAnimation: {
5457 observer: '_entryAnimationChanged',
5458 type: String
5459 },
5460 exitAnimation: {
5461 observer: '_exitAnimationChanged',
5462 type: String
5463 }
5464 },
5465 _entryAnimationChanged: function() {
5466 this.animationConfig = this.animationConfig || {};
5467 this.animationConfig['entry'] = [ {
5468 name: this.entryAnimation,
5469 node: this
5470 } ];
5471 },
5472 _exitAnimationChanged: function() {
5473 this.animationConfig = this.animationConfig || {};
5474 this.animationConfig['exit'] = [ {
5475 name: this.exitAnimation,
5476 node: this
5477 } ];
5478 },
5479 _copyProperties: function(config1, config2) {
5480 for (var property in config2) {
5481 config1[property] = config2[property];
5482 }
5483 },
5484 _cloneConfig: function(config) {
5485 var clone = {
5486 isClone: true
5487 };
5488 this._copyProperties(clone, config);
5489 return clone;
5490 },
5491 _getAnimationConfigRecursive: function(type, map, allConfigs) {
5492 if (!this.animationConfig) {
5493 return;
5494 }
5495 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun ction') {
5496 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid e of your components 'properties' object instead of outside of it."));
5497 return;
5498 }
5499 var thisConfig;
5500 if (type) {
5501 thisConfig = this.animationConfig[type];
5502 } else {
5503 thisConfig = this.animationConfig;
5504 }
5505 if (!Array.isArray(thisConfig)) {
5506 thisConfig = [ thisConfig ];
5507 }
5508 if (thisConfig) {
5509 for (var config, index = 0; config = thisConfig[index]; index++) {
5510 if (config.animatable) {
5511 config.animatable._getAnimationConfigRecursive(config.type || type, ma p, allConfigs);
5512 } else {
5513 if (config.id) {
5514 var cachedConfig = map[config.id];
5515 if (cachedConfig) {
5516 if (!cachedConfig.isClone) {
5517 map[config.id] = this._cloneConfig(cachedConfig);
5518 cachedConfig = map[config.id];
5519 }
5520 this._copyProperties(cachedConfig, config);
5521 } else {
5522 map[config.id] = config;
5523 }
5524 } else {
5525 allConfigs.push(config);
5526 }
5527 }
5528 }
5529 }
5530 },
5531 getAnimationConfig: function(type) {
5532 var map = {};
5533 var allConfigs = [];
5534 this._getAnimationConfigRecursive(type, map, allConfigs);
5535 for (var key in map) {
5536 allConfigs.push(map[key]);
5537 }
5538 return allConfigs;
5539 }
5540 };
5541
5542 Polymer.NeonAnimationRunnerBehaviorImpl = {
5543 _configureAnimations: function(configs) {
5544 var results = [];
5545 if (configs.length > 0) {
5546 for (var config, index = 0; config = configs[index]; index++) {
5547 var neonAnimation = document.createElement(config.name);
5548 if (neonAnimation.isNeonAnimation) {
5549 var result = null;
5550 try {
5551 result = neonAnimation.configure(config);
5552 if (typeof result.cancel != 'function') {
5553 result = document.timeline.play(result);
5554 }
5555 } catch (e) {
5556 result = null;
5557 console.warn('Couldnt play', '(', config.name, ').', e);
5558 }
5559 if (result) {
5560 results.push({
5561 neonAnimation: neonAnimation,
5562 config: config,
5563 animation: result
5564 });
5565 }
5566 } else {
5567 console.warn(this.is + ':', config.name, 'not found!');
5568 }
5569 }
5570 }
5571 return results;
5572 },
5573 _shouldComplete: function(activeEntries) {
5574 var finished = true;
5575 for (var i = 0; i < activeEntries.length; i++) {
5576 if (activeEntries[i].animation.playState != 'finished') {
5577 finished = false;
5578 break;
5579 }
5580 }
5581 return finished;
5582 },
5583 _complete: function(activeEntries) {
5584 for (var i = 0; i < activeEntries.length; i++) {
5585 activeEntries[i].neonAnimation.complete(activeEntries[i].config);
5586 }
5587 for (var i = 0; i < activeEntries.length; i++) {
5588 activeEntries[i].animation.cancel();
5589 }
5590 },
5591 playAnimation: function(type, cookie) {
5592 var configs = this.getAnimationConfig(type);
5593 if (!configs) {
5594 return;
5595 }
5596 this._active = this._active || {};
5597 if (this._active[type]) {
5598 this._complete(this._active[type]);
5599 delete this._active[type];
5600 }
5601 var activeEntries = this._configureAnimations(configs);
5602 if (activeEntries.length == 0) {
5603 this.fire('neon-animation-finish', cookie, {
5604 bubbles: false
5605 });
5606 return;
5607 }
5608 this._active[type] = activeEntries;
5609 for (var i = 0; i < activeEntries.length; i++) {
5610 activeEntries[i].animation.onfinish = function() {
5611 if (this._shouldComplete(activeEntries)) {
5612 this._complete(activeEntries);
5613 delete this._active[type];
5614 this.fire('neon-animation-finish', cookie, {
5615 bubbles: false
5616 });
5617 }
5618 }.bind(this);
5619 }
5620 },
5621 cancelAnimation: function() {
5622 for (var k in this._animations) {
5623 this._animations[k].cancel();
5624 }
5625 this._animations = {};
5626 }
5627 };
5628
5629 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer. NeonAnimationRunnerBehaviorImpl ];
5630
5631 Polymer.NeonAnimationBehavior = {
5632 properties: {
5633 animationTiming: {
5634 type: Object,
5635 value: function() {
5636 return {
5637 duration: 500,
5638 easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
5639 fill: 'both'
5640 };
5641 }
5642 }
5643 },
5644 isNeonAnimation: true,
5645 timingFromConfig: function(config) {
5646 if (config.timing) {
5647 for (var property in config.timing) {
5648 this.animationTiming[property] = config.timing[property];
5649 }
5650 }
5651 return this.animationTiming;
5652 },
5653 setPrefixedProperty: function(node, property, value) {
5654 var map = {
5655 transform: [ 'webkitTransform' ],
5656 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ]
5657 };
5658 var prefixes = map[property];
5659 for (var prefix, index = 0; prefix = prefixes[index]; index++) {
5660 node.style[prefix] = value;
5661 }
5662 node.style[property] = value;
5663 },
5664 complete: function() {}
5665 };
5666
5667 Polymer({
5668 is: 'opaque-animation',
5669 behaviors: [ Polymer.NeonAnimationBehavior ],
5670 configure: function(config) {
5671 var node = config.node;
5672 this._effect = new KeyframeEffect(node, [ {
5673 opacity: '1'
5674 }, {
5675 opacity: '1'
5676 } ], this.timingFromConfig(config));
5677 node.style.opacity = '0';
5678 return this._effect;
5679 },
5680 complete: function(config) {
5681 config.node.style.opacity = '';
5682 }
5683 });
5684
5685 (function() {
5686 'use strict';
5687 var LAST_TOUCH_POSITION = {
5688 pageX: 0,
5689 pageY: 0
5690 };
5691 var ROOT_TARGET = null;
5692 var SCROLLABLE_NODES = [];
5693 Polymer.IronDropdownScrollManager = {
5694 get currentLockingElement() {
5695 return this._lockingElements[this._lockingElements.length - 1];
5696 },
5697 elementIsScrollLocked: function(element) {
5698 var currentLockingElement = this.currentLockingElement;
5699 if (currentLockingElement === undefined) return false;
5700 var scrollLocked;
5701 if (this._hasCachedLockedElement(element)) {
5702 return true;
5703 }
5704 if (this._hasCachedUnlockedElement(element)) {
5705 return false;
5706 }
5707 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen t && !this._composedTreeContains(currentLockingElement, element);
5708 if (scrollLocked) {
5709 this._lockedElementCache.push(element);
5710 } else {
5711 this._unlockedElementCache.push(element);
5712 }
5713 return scrollLocked;
5714 },
5715 pushScrollLock: function(element) {
5716 if (this._lockingElements.indexOf(element) >= 0) {
5717 return;
5718 }
5719 if (this._lockingElements.length === 0) {
5720 this._lockScrollInteractions();
5721 }
5722 this._lockingElements.push(element);
5723 this._lockedElementCache = [];
5724 this._unlockedElementCache = [];
5725 },
5726 removeScrollLock: function(element) {
5727 var index = this._lockingElements.indexOf(element);
5728 if (index === -1) {
5729 return;
5730 }
5731 this._lockingElements.splice(index, 1);
5732 this._lockedElementCache = [];
5733 this._unlockedElementCache = [];
5734 if (this._lockingElements.length === 0) {
5735 this._unlockScrollInteractions();
5736 }
5737 },
5738 _lockingElements: [],
5739 _lockedElementCache: null,
5740 _unlockedElementCache: null,
5741 _hasCachedLockedElement: function(element) {
5742 return this._lockedElementCache.indexOf(element) > -1;
5743 },
5744 _hasCachedUnlockedElement: function(element) {
5745 return this._unlockedElementCache.indexOf(element) > -1;
5746 },
5747 _composedTreeContains: function(element, child) {
5748 var contentElements;
5749 var distributedNodes;
5750 var contentIndex;
5751 var nodeIndex;
5752 if (element.contains(child)) {
5753 return true;
5754 }
5755 contentElements = Polymer.dom(element).querySelectorAll('content');
5756 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd ex) {
5757 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib utedNodes();
5758 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
5759 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
5760 return true;
5761 }
5762 }
5763 }
5764 return false;
5765 },
5766 _scrollInteractionHandler: function(event) {
5767 if (event.cancelable && this._shouldPreventScrolling(event)) {
5768 event.preventDefault();
5769 }
5770 if (event.targetTouches) {
5771 var touch = event.targetTouches[0];
5772 LAST_TOUCH_POSITION.pageX = touch.pageX;
5773 LAST_TOUCH_POSITION.pageY = touch.pageY;
5774 }
5775 },
5776 _lockScrollInteractions: function() {
5777 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac tionHandler.bind(this);
5778 document.addEventListener('wheel', this._boundScrollHandler, true);
5779 document.addEventListener('mousewheel', this._boundScrollHandler, true);
5780 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true );
5781 document.addEventListener('touchstart', this._boundScrollHandler, true);
5782 document.addEventListener('touchmove', this._boundScrollHandler, true);
5783 },
5784 _unlockScrollInteractions: function() {
5785 document.removeEventListener('wheel', this._boundScrollHandler, true);
5786 document.removeEventListener('mousewheel', this._boundScrollHandler, true) ;
5787 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t rue);
5788 document.removeEventListener('touchstart', this._boundScrollHandler, true) ;
5789 document.removeEventListener('touchmove', this._boundScrollHandler, true);
5790 },
5791 _shouldPreventScrolling: function(event) {
5792 var target = Polymer.dom(event).rootTarget;
5793 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
5794 ROOT_TARGET = target;
5795 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
5796 }
5797 if (!SCROLLABLE_NODES.length) {
5798 return true;
5799 }
5800 if (event.type === 'touchstart') {
5801 return false;
5802 }
5803 var info = this._getScrollInfo(event);
5804 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY) ;
5805 },
5806 _getScrollableNodes: function(nodes) {
5807 var scrollables = [];
5808 var lockingIndex = nodes.indexOf(this.currentLockingElement);
5809 for (var i = 0; i <= lockingIndex; i++) {
5810 var node = nodes[i];
5811 if (node.nodeType === 11) {
5812 continue;
5813 }
5814 var style = node.style;
5815 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
5816 style = window.getComputedStyle(node);
5817 }
5818 if (style.overflow === 'scroll' || style.overflow === 'auto') {
5819 scrollables.push(node);
5820 }
5821 }
5822 return scrollables;
5823 },
5824 _getScrollingNode: function(nodes, deltaX, deltaY) {
5825 if (!deltaX && !deltaY) {
5826 return;
5827 }
5828 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
5829 for (var i = 0; i < nodes.length; i++) {
5830 var node = nodes[i];
5831 var canScroll = false;
5832 if (verticalScroll) {
5833 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc rollHeight - node.clientHeight;
5834 } else {
5835 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node. scrollWidth - node.clientWidth;
5836 }
5837 if (canScroll) {
5838 return node;
5839 }
5840 }
5841 },
5842 _getScrollInfo: function(event) {
5843 var info = {
5844 deltaX: event.deltaX,
5845 deltaY: event.deltaY
5846 };
5847 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) {
5848 info.deltaX = -event.wheelDeltaX;
5849 info.deltaY = -event.wheelDeltaY;
5850 } else if ('axis' in event) {
5851 info.deltaX = event.axis === 1 ? event.detail : 0;
5852 info.deltaY = event.axis === 2 ? event.detail : 0;
5853 } else if (event.targetTouches) {
5854 var touch = event.targetTouches[0];
5855 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
5856 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
5857 }
5858 return info;
5859 }
5860 };
5861 })();
5862
5863 (function() {
5864 'use strict';
5865 Polymer({
5866 is: 'iron-dropdown',
5867 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer .IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ],
9463 properties: { 5868 properties: {
9464 5869 horizontalAlign: {
9465 /** 5870 type: String,
9466 * Animation configuration. See README for more info. 5871 value: 'left',
9467 */ 5872 reflectToAttribute: true
9468 animationConfig: { 5873 },
5874 verticalAlign: {
5875 type: String,
5876 value: 'top',
5877 reflectToAttribute: true
5878 },
5879 openAnimationConfig: {
9469 type: Object 5880 type: Object
9470 }, 5881 },
9471 5882 closeAnimationConfig: {
9472 /** 5883 type: Object
9473 * Convenience property for setting an 'entry' animation. Do not set `anim ationConfig.entry`
9474 * manually if using this. The animated node is set to `this` if using thi s property.
9475 */
9476 entryAnimation: {
9477 observer: '_entryAnimationChanged',
9478 type: String
9479 }, 5884 },
9480 5885 focusTarget: {
9481 /** 5886 type: Object
9482 * Convenience property for setting an 'exit' animation. Do not set `anima tionConfig.exit` 5887 },
9483 * manually if using this. The animated node is set to `this` if using thi s property. 5888 noAnimations: {
9484 */ 5889 type: Boolean,
9485 exitAnimation: { 5890 value: false
9486 observer: '_exitAnimationChanged', 5891 },
9487 type: String 5892 allowOutsideScroll: {
9488 } 5893 type: Boolean,
9489 5894 value: false
9490 }, 5895 },
9491 5896 _boundOnCaptureScroll: {
9492 _entryAnimationChanged: function() { 5897 type: Function,
9493 this.animationConfig = this.animationConfig || {}; 5898 value: function() {
9494 this.animationConfig['entry'] = [{ 5899 return this._onCaptureScroll.bind(this);
9495 name: this.entryAnimation, 5900 }
9496 node: this 5901 }
9497 }]; 5902 },
9498 }, 5903 listeners: {
9499 5904 'neon-animation-finish': '_onNeonAnimationFinish'
9500 _exitAnimationChanged: function() { 5905 },
9501 this.animationConfig = this.animationConfig || {}; 5906 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont alAlign, verticalOffset, horizontalOffset)' ],
9502 this.animationConfig['exit'] = [{ 5907 get containedElement() {
9503 name: this.exitAnimation, 5908 return Polymer.dom(this.$.content).getDistributedNodes()[0];
9504 node: this 5909 },
9505 }]; 5910 get _focusTarget() {
9506 }, 5911 return this.focusTarget || this.containedElement;
9507 5912 },
9508 _copyProperties: function(config1, config2) { 5913 ready: function() {
9509 // shallowly copy properties from config2 to config1 5914 this._scrollTop = 0;
9510 for (var property in config2) { 5915 this._scrollLeft = 0;
9511 config1[property] = config2[property]; 5916 this._refitOnScrollRAF = null;
9512 } 5917 },
9513 }, 5918 detached: function() {
9514 5919 this.cancelAnimation();
9515 _cloneConfig: function(config) { 5920 Polymer.IronDropdownScrollManager.removeScrollLock(this);
9516 var clone = { 5921 },
9517 isClone: true 5922 _openedChanged: function() {
5923 if (this.opened && this.disabled) {
5924 this.cancel();
5925 } else {
5926 this.cancelAnimation();
5927 this.sizingTarget = this.containedElement || this.sizingTarget;
5928 this._updateAnimationConfig();
5929 this._saveScrollPosition();
5930 if (this.opened) {
5931 document.addEventListener('scroll', this._boundOnCaptureScroll);
5932 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro llLock(this);
5933 } else {
5934 document.removeEventListener('scroll', this._boundOnCaptureScroll);
5935 Polymer.IronDropdownScrollManager.removeScrollLock(this);
5936 }
5937 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
5938 }
5939 },
5940 _renderOpened: function() {
5941 if (!this.noAnimations && this.animationConfig.open) {
5942 this.$.contentWrapper.classList.add('animating');
5943 this.playAnimation('open');
5944 } else {
5945 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
5946 }
5947 },
5948 _renderClosed: function() {
5949 if (!this.noAnimations && this.animationConfig.close) {
5950 this.$.contentWrapper.classList.add('animating');
5951 this.playAnimation('close');
5952 } else {
5953 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
5954 }
5955 },
5956 _onNeonAnimationFinish: function() {
5957 this.$.contentWrapper.classList.remove('animating');
5958 if (this.opened) {
5959 this._finishRenderOpened();
5960 } else {
5961 this._finishRenderClosed();
5962 }
5963 },
5964 _onCaptureScroll: function() {
5965 if (!this.allowOutsideScroll) {
5966 this._restoreScrollPosition();
5967 } else {
5968 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol lRAF);
5969 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th is));
5970 }
5971 },
5972 _saveScrollPosition: function() {
5973 if (document.scrollingElement) {
5974 this._scrollTop = document.scrollingElement.scrollTop;
5975 this._scrollLeft = document.scrollingElement.scrollLeft;
5976 } else {
5977 this._scrollTop = Math.max(document.documentElement.scrollTop, document. body.scrollTop);
5978 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen t.body.scrollLeft);
5979 }
5980 },
5981 _restoreScrollPosition: function() {
5982 if (document.scrollingElement) {
5983 document.scrollingElement.scrollTop = this._scrollTop;
5984 document.scrollingElement.scrollLeft = this._scrollLeft;
5985 } else {
5986 document.documentElement.scrollTop = this._scrollTop;
5987 document.documentElement.scrollLeft = this._scrollLeft;
5988 document.body.scrollTop = this._scrollTop;
5989 document.body.scrollLeft = this._scrollLeft;
5990 }
5991 },
5992 _updateAnimationConfig: function() {
5993 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati onConfig || []);
5994 for (var i = 0; i < animations.length; i++) {
5995 animations[i].node = this.containedElement;
5996 }
5997 this.animationConfig = {
5998 open: this.openAnimationConfig,
5999 close: this.closeAnimationConfig
9518 }; 6000 };
9519 this._copyProperties(clone, config); 6001 },
9520 return clone; 6002 _updateOverlayPosition: function() {
9521 }, 6003 if (this.isAttached) {
9522 6004 this.notifyResize();
9523 _getAnimationConfigRecursive: function(type, map, allConfigs) { 6005 }
9524 if (!this.animationConfig) { 6006 },
9525 return; 6007 _applyFocus: function() {
9526 } 6008 var focusTarget = this.focusTarget || this.containedElement;
9527 6009 if (focusTarget && this.opened && !this.noAutoFocus) {
9528 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu nction') { 6010 focusTarget.focus();
9529 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins ide of your components 'properties' object instead of outside of it.")); 6011 } else {
9530 » return; 6012 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
9531 } 6013 }
9532 6014 }
9533 // type is optional 6015 });
9534 var thisConfig; 6016 })();
9535 if (type) { 6017
9536 thisConfig = this.animationConfig[type];
9537 } else {
9538 thisConfig = this.animationConfig;
9539 }
9540
9541 if (!Array.isArray(thisConfig)) {
9542 thisConfig = [thisConfig];
9543 }
9544
9545 // iterate animations and recurse to process configurations from child nod es
9546 if (thisConfig) {
9547 for (var config, index = 0; config = thisConfig[index]; index++) {
9548 if (config.animatable) {
9549 config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
9550 } else {
9551 if (config.id) {
9552 var cachedConfig = map[config.id];
9553 if (cachedConfig) {
9554 // merge configurations with the same id, making a clone lazily
9555 if (!cachedConfig.isClone) {
9556 map[config.id] = this._cloneConfig(cachedConfig)
9557 cachedConfig = map[config.id];
9558 }
9559 this._copyProperties(cachedConfig, config);
9560 } else {
9561 // put any configs with an id into a map
9562 map[config.id] = config;
9563 }
9564 } else {
9565 allConfigs.push(config);
9566 }
9567 }
9568 }
9569 }
9570 },
9571
9572 /**
9573 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
9574 * an animation with an optional type. Elements implementing `Polymer.NeonAn imatableBehavior`
9575 * should define the property `animationConfig`, which is either a configura tion object
9576 * or a map of animation type to array of configuration objects.
9577 */
9578 getAnimationConfig: function(type) {
9579 var map = {};
9580 var allConfigs = [];
9581 this._getAnimationConfigRecursive(type, map, allConfigs);
9582 // append the configurations saved in the map to the array
9583 for (var key in map) {
9584 allConfigs.push(map[key]);
9585 }
9586 return allConfigs;
9587 }
9588
9589 };
9590 /**
9591 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
9592 *
9593 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
9594 */
9595 Polymer.NeonAnimationRunnerBehaviorImpl = {
9596
9597 _configureAnimations: function(configs) {
9598 var results = [];
9599 if (configs.length > 0) {
9600 for (var config, index = 0; config = configs[index]; index++) {
9601 var neonAnimation = document.createElement(config.name);
9602 // is this element actually a neon animation?
9603 if (neonAnimation.isNeonAnimation) {
9604 var result = null;
9605 // configuration or play could fail if polyfills aren't loaded
9606 try {
9607 result = neonAnimation.configure(config);
9608 // Check if we have an Effect rather than an Animation
9609 if (typeof result.cancel != 'function') {
9610 result = document.timeline.play(result);
9611 }
9612 } catch (e) {
9613 result = null;
9614 console.warn('Couldnt play', '(', config.name, ').', e);
9615 }
9616 if (result) {
9617 results.push({
9618 neonAnimation: neonAnimation,
9619 config: config,
9620 animation: result,
9621 });
9622 }
9623 } else {
9624 console.warn(this.is + ':', config.name, 'not found!');
9625 }
9626 }
9627 }
9628 return results;
9629 },
9630
9631 _shouldComplete: function(activeEntries) {
9632 var finished = true;
9633 for (var i = 0; i < activeEntries.length; i++) {
9634 if (activeEntries[i].animation.playState != 'finished') {
9635 finished = false;
9636 break;
9637 }
9638 }
9639 return finished;
9640 },
9641
9642 _complete: function(activeEntries) {
9643 for (var i = 0; i < activeEntries.length; i++) {
9644 activeEntries[i].neonAnimation.complete(activeEntries[i].config);
9645 }
9646 for (var i = 0; i < activeEntries.length; i++) {
9647 activeEntries[i].animation.cancel();
9648 }
9649 },
9650
9651 /**
9652 * Plays an animation with an optional `type`.
9653 * @param {string=} type
9654 * @param {!Object=} cookie
9655 */
9656 playAnimation: function(type, cookie) {
9657 var configs = this.getAnimationConfig(type);
9658 if (!configs) {
9659 return;
9660 }
9661 this._active = this._active || {};
9662 if (this._active[type]) {
9663 this._complete(this._active[type]);
9664 delete this._active[type];
9665 }
9666
9667 var activeEntries = this._configureAnimations(configs);
9668
9669 if (activeEntries.length == 0) {
9670 this.fire('neon-animation-finish', cookie, {bubbles: false});
9671 return;
9672 }
9673
9674 this._active[type] = activeEntries;
9675
9676 for (var i = 0; i < activeEntries.length; i++) {
9677 activeEntries[i].animation.onfinish = function() {
9678 if (this._shouldComplete(activeEntries)) {
9679 this._complete(activeEntries);
9680 delete this._active[type];
9681 this.fire('neon-animation-finish', cookie, {bubbles: false});
9682 }
9683 }.bind(this);
9684 }
9685 },
9686
9687 /**
9688 * Cancels the currently running animations.
9689 */
9690 cancelAnimation: function() {
9691 for (var k in this._animations) {
9692 this._animations[k].cancel();
9693 }
9694 this._animations = {};
9695 }
9696 };
9697
9698 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */
9699 Polymer.NeonAnimationRunnerBehavior = [
9700 Polymer.NeonAnimatableBehavior,
9701 Polymer.NeonAnimationRunnerBehaviorImpl
9702 ];
9703 /**
9704 * Use `Polymer.NeonAnimationBehavior` to implement an animation.
9705 * @polymerBehavior
9706 */
9707 Polymer.NeonAnimationBehavior = {
9708
9709 properties: {
9710
9711 /**
9712 * Defines the animation timing.
9713 */
9714 animationTiming: {
9715 type: Object,
9716 value: function() {
9717 return {
9718 duration: 500,
9719 easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
9720 fill: 'both'
9721 }
9722 }
9723 }
9724
9725 },
9726
9727 /**
9728 * Can be used to determine that elements implement this behavior.
9729 */
9730 isNeonAnimation: true,
9731
9732 /**
9733 * Do any animation configuration here.
9734 */
9735 // configure: function(config) {
9736 // },
9737
9738 /**
9739 * Returns the animation timing by mixing in properties from `config` to the defaults defined
9740 * by the animation.
9741 */
9742 timingFromConfig: function(config) {
9743 if (config.timing) {
9744 for (var property in config.timing) {
9745 this.animationTiming[property] = config.timing[property];
9746 }
9747 }
9748 return this.animationTiming;
9749 },
9750
9751 /**
9752 * Sets `transform` and `transformOrigin` properties along with the prefixed versions.
9753 */
9754 setPrefixedProperty: function(node, property, value) {
9755 var map = {
9756 'transform': ['webkitTransform'],
9757 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin']
9758 };
9759 var prefixes = map[property];
9760 for (var prefix, index = 0; prefix = prefixes[index]; index++) {
9761 node.style[prefix] = value;
9762 }
9763 node.style[property] = value;
9764 },
9765
9766 /**
9767 * Called when the animation finishes.
9768 */
9769 complete: function() {}
9770
9771 };
9772 Polymer({ 6018 Polymer({
9773 6019 is: 'fade-in-animation',
9774 is: 'opaque-animation', 6020 behaviors: [ Polymer.NeonAnimationBehavior ],
9775 6021 configure: function(config) {
9776 behaviors: [ 6022 var node = config.node;
9777 Polymer.NeonAnimationBehavior 6023 this._effect = new KeyframeEffect(node, [ {
9778 ], 6024 opacity: '0'
9779 6025 }, {
9780 configure: function(config) { 6026 opacity: '1'
9781 var node = config.node; 6027 } ], this.timingFromConfig(config));
9782 this._effect = new KeyframeEffect(node, [ 6028 return this._effect;
9783 {'opacity': '1'}, 6029 }
9784 {'opacity': '1'} 6030 });
9785 ], this.timingFromConfig(config)); 6031
9786 node.style.opacity = '0';
9787 return this._effect;
9788 },
9789
9790 complete: function(config) {
9791 config.node.style.opacity = '';
9792 }
9793
9794 });
9795 (function() {
9796 'use strict';
9797 // Used to calculate the scroll direction during touch events.
9798 var LAST_TOUCH_POSITION = {
9799 pageX: 0,
9800 pageY: 0
9801 };
9802 // Used to avoid computing event.path and filter scrollable nodes (better pe rf).
9803 var ROOT_TARGET = null;
9804 var SCROLLABLE_NODES = [];
9805
9806 /**
9807 * The IronDropdownScrollManager is intended to provide a central source
9808 * of authority and control over which elements in a document are currently
9809 * allowed to scroll.
9810 */
9811
9812 Polymer.IronDropdownScrollManager = {
9813
9814 /**
9815 * The current element that defines the DOM boundaries of the
9816 * scroll lock. This is always the most recently locking element.
9817 */
9818 get currentLockingElement() {
9819 return this._lockingElements[this._lockingElements.length - 1];
9820 },
9821
9822 /**
9823 * Returns true if the provided element is "scroll locked", which is to
9824 * say that it cannot be scrolled via pointer or keyboard interactions.
9825 *
9826 * @param {HTMLElement} element An HTML element instance which may or may
9827 * not be scroll locked.
9828 */
9829 elementIsScrollLocked: function(element) {
9830 var currentLockingElement = this.currentLockingElement;
9831
9832 if (currentLockingElement === undefined)
9833 return false;
9834
9835 var scrollLocked;
9836
9837 if (this._hasCachedLockedElement(element)) {
9838 return true;
9839 }
9840
9841 if (this._hasCachedUnlockedElement(element)) {
9842 return false;
9843 }
9844
9845 scrollLocked = !!currentLockingElement &&
9846 currentLockingElement !== element &&
9847 !this._composedTreeContains(currentLockingElement, element);
9848
9849 if (scrollLocked) {
9850 this._lockedElementCache.push(element);
9851 } else {
9852 this._unlockedElementCache.push(element);
9853 }
9854
9855 return scrollLocked;
9856 },
9857
9858 /**
9859 * Push an element onto the current scroll lock stack. The most recently
9860 * pushed element and its children will be considered scrollable. All
9861 * other elements will not be scrollable.
9862 *
9863 * Scroll locking is implemented as a stack so that cases such as
9864 * dropdowns within dropdowns are handled well.
9865 *
9866 * @param {HTMLElement} element The element that should lock scroll.
9867 */
9868 pushScrollLock: function(element) {
9869 // Prevent pushing the same element twice
9870 if (this._lockingElements.indexOf(element) >= 0) {
9871 return;
9872 }
9873
9874 if (this._lockingElements.length === 0) {
9875 this._lockScrollInteractions();
9876 }
9877
9878 this._lockingElements.push(element);
9879
9880 this._lockedElementCache = [];
9881 this._unlockedElementCache = [];
9882 },
9883
9884 /**
9885 * Remove an element from the scroll lock stack. The element being
9886 * removed does not need to be the most recently pushed element. However,
9887 * the scroll lock constraints only change when the most recently pushed
9888 * element is removed.
9889 *
9890 * @param {HTMLElement} element The element to remove from the scroll
9891 * lock stack.
9892 */
9893 removeScrollLock: function(element) {
9894 var index = this._lockingElements.indexOf(element);
9895
9896 if (index === -1) {
9897 return;
9898 }
9899
9900 this._lockingElements.splice(index, 1);
9901
9902 this._lockedElementCache = [];
9903 this._unlockedElementCache = [];
9904
9905 if (this._lockingElements.length === 0) {
9906 this._unlockScrollInteractions();
9907 }
9908 },
9909
9910 _lockingElements: [],
9911
9912 _lockedElementCache: null,
9913
9914 _unlockedElementCache: null,
9915
9916 _hasCachedLockedElement: function(element) {
9917 return this._lockedElementCache.indexOf(element) > -1;
9918 },
9919
9920 _hasCachedUnlockedElement: function(element) {
9921 return this._unlockedElementCache.indexOf(element) > -1;
9922 },
9923
9924 _composedTreeContains: function(element, child) {
9925 // NOTE(cdata): This method iterates over content elements and their
9926 // corresponding distributed nodes to implement a contains-like method
9927 // that pierces through the composed tree of the ShadowDOM. Results of
9928 // this operation are cached (elsewhere) on a per-scroll-lock basis, to
9929 // guard against potentially expensive lookups happening repeatedly as
9930 // a user scrolls / touchmoves.
9931 var contentElements;
9932 var distributedNodes;
9933 var contentIndex;
9934 var nodeIndex;
9935
9936 if (element.contains(child)) {
9937 return true;
9938 }
9939
9940 contentElements = Polymer.dom(element).querySelectorAll('content');
9941
9942 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI ndex) {
9943
9944 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr ibutedNodes();
9945
9946 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
9947
9948 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
9949 return true;
9950 }
9951 }
9952 }
9953
9954 return false;
9955 },
9956
9957 _scrollInteractionHandler: function(event) {
9958 // Avoid canceling an event with cancelable=false, e.g. scrolling is in
9959 // progress and cannot be interrupted.
9960 if (event.cancelable && this._shouldPreventScrolling(event)) {
9961 event.preventDefault();
9962 }
9963 // If event has targetTouches (touch event), update last touch position.
9964 if (event.targetTouches) {
9965 var touch = event.targetTouches[0];
9966 LAST_TOUCH_POSITION.pageX = touch.pageX;
9967 LAST_TOUCH_POSITION.pageY = touch.pageY;
9968 }
9969 },
9970
9971 _lockScrollInteractions: function() {
9972 this._boundScrollHandler = this._boundScrollHandler ||
9973 this._scrollInteractionHandler.bind(this);
9974 // Modern `wheel` event for mouse wheel scrolling:
9975 document.addEventListener('wheel', this._boundScrollHandler, true);
9976 // Older, non-standard `mousewheel` event for some FF:
9977 document.addEventListener('mousewheel', this._boundScrollHandler, true);
9978 // IE:
9979 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr ue);
9980 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
9981 document.addEventListener('touchstart', this._boundScrollHandler, true);
9982 // Mobile devices can scroll on touch move:
9983 document.addEventListener('touchmove', this._boundScrollHandler, true);
9984 },
9985
9986 _unlockScrollInteractions: function() {
9987 document.removeEventListener('wheel', this._boundScrollHandler, true);
9988 document.removeEventListener('mousewheel', this._boundScrollHandler, tru e);
9989 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
9990 document.removeEventListener('touchstart', this._boundScrollHandler, tru e);
9991 document.removeEventListener('touchmove', this._boundScrollHandler, true );
9992 },
9993
9994 /**
9995 * Returns true if the event causes scroll outside the current locking
9996 * element, e.g. pointer/keyboard interactions, or scroll "leaking"
9997 * outside the locking element when it is already at its scroll boundaries .
9998 * @param {!Event} event
9999 * @return {boolean}
10000 * @private
10001 */
10002 _shouldPreventScrolling: function(event) {
10003
10004 // Update if root target changed. For touch events, ensure we don't
10005 // update during touchmove.
10006 var target = Polymer.dom(event).rootTarget;
10007 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
10008 ROOT_TARGET = target;
10009 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
10010 }
10011
10012 // Prevent event if no scrollable nodes.
10013 if (!SCROLLABLE_NODES.length) {
10014 return true;
10015 }
10016 // Don't prevent touchstart event inside the locking element when it has
10017 // scrollable nodes.
10018 if (event.type === 'touchstart') {
10019 return false;
10020 }
10021 // Get deltaX/Y.
10022 var info = this._getScrollInfo(event);
10023 // Prevent if there is no child that can scroll.
10024 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta Y);
10025 },
10026
10027 /**
10028 * Returns an array of scrollable nodes up to the current locking element,
10029 * which is included too if scrollable.
10030 * @param {!Array<Node>} nodes
10031 * @return {Array<Node>} scrollables
10032 * @private
10033 */
10034 _getScrollableNodes: function(nodes) {
10035 var scrollables = [];
10036 var lockingIndex = nodes.indexOf(this.currentLockingElement);
10037 // Loop from root target to locking element (included).
10038 for (var i = 0; i <= lockingIndex; i++) {
10039 var node = nodes[i];
10040 // Skip document fragments.
10041 if (node.nodeType === 11) {
10042 continue;
10043 }
10044 // Check inline style before checking computed style.
10045 var style = node.style;
10046 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
10047 style = window.getComputedStyle(node);
10048 }
10049 if (style.overflow === 'scroll' || style.overflow === 'auto') {
10050 scrollables.push(node);
10051 }
10052 }
10053 return scrollables;
10054 },
10055
10056 /**
10057 * Returns the node that is scrolling. If there is no scrolling,
10058 * returns undefined.
10059 * @param {!Array<Node>} nodes
10060 * @param {number} deltaX Scroll delta on the x-axis
10061 * @param {number} deltaY Scroll delta on the y-axis
10062 * @return {Node|undefined}
10063 * @private
10064 */
10065 _getScrollingNode: function(nodes, deltaX, deltaY) {
10066 // No scroll.
10067 if (!deltaX && !deltaY) {
10068 return;
10069 }
10070 // Check only one axis according to where there is more scroll.
10071 // Prefer vertical to horizontal.
10072 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
10073 for (var i = 0; i < nodes.length; i++) {
10074 var node = nodes[i];
10075 var canScroll = false;
10076 if (verticalScroll) {
10077 // delta < 0 is scroll up, delta > 0 is scroll down.
10078 canScroll = deltaY < 0 ? node.scrollTop > 0 :
10079 node.scrollTop < node.scrollHeight - node.clientHeight;
10080 } else {
10081 // delta < 0 is scroll left, delta > 0 is scroll right.
10082 canScroll = deltaX < 0 ? node.scrollLeft > 0 :
10083 node.scrollLeft < node.scrollWidth - node.clientWidth;
10084 }
10085 if (canScroll) {
10086 return node;
10087 }
10088 }
10089 },
10090
10091 /**
10092 * Returns scroll `deltaX` and `deltaY`.
10093 * @param {!Event} event The scroll event
10094 * @return {{
10095 * deltaX: number The x-axis scroll delta (positive: scroll right,
10096 * negative: scroll left, 0: no scroll),
10097 * deltaY: number The y-axis scroll delta (positive: scroll down,
10098 * negative: scroll up, 0: no scroll)
10099 * }} info
10100 * @private
10101 */
10102 _getScrollInfo: function(event) {
10103 var info = {
10104 deltaX: event.deltaX,
10105 deltaY: event.deltaY
10106 };
10107 // Already available.
10108 if ('deltaX' in event) {
10109 // do nothing, values are already good.
10110 }
10111 // Safari has scroll info in `wheelDeltaX/Y`.
10112 else if ('wheelDeltaX' in event) {
10113 info.deltaX = -event.wheelDeltaX;
10114 info.deltaY = -event.wheelDeltaY;
10115 }
10116 // Firefox has scroll info in `detail` and `axis`.
10117 else if ('axis' in event) {
10118 info.deltaX = event.axis === 1 ? event.detail : 0;
10119 info.deltaY = event.axis === 2 ? event.detail : 0;
10120 }
10121 // On mobile devices, calculate scroll direction.
10122 else if (event.targetTouches) {
10123 var touch = event.targetTouches[0];
10124 // Touch moves from right to left => scrolling goes right.
10125 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
10126 // Touch moves from down to up => scrolling goes down.
10127 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
10128 }
10129 return info;
10130 }
10131 };
10132 })();
10133 (function() {
10134 'use strict';
10135
10136 Polymer({
10137 is: 'iron-dropdown',
10138
10139 behaviors: [
10140 Polymer.IronControlState,
10141 Polymer.IronA11yKeysBehavior,
10142 Polymer.IronOverlayBehavior,
10143 Polymer.NeonAnimationRunnerBehavior
10144 ],
10145
10146 properties: {
10147 /**
10148 * The orientation against which to align the dropdown content
10149 * horizontally relative to the dropdown trigger.
10150 * Overridden from `Polymer.IronFitBehavior`.
10151 */
10152 horizontalAlign: {
10153 type: String,
10154 value: 'left',
10155 reflectToAttribute: true
10156 },
10157
10158 /**
10159 * The orientation against which to align the dropdown content
10160 * vertically relative to the dropdown trigger.
10161 * Overridden from `Polymer.IronFitBehavior`.
10162 */
10163 verticalAlign: {
10164 type: String,
10165 value: 'top',
10166 reflectToAttribute: true
10167 },
10168
10169 /**
10170 * An animation config. If provided, this will be used to animate the
10171 * opening of the dropdown.
10172 */
10173 openAnimationConfig: {
10174 type: Object
10175 },
10176
10177 /**
10178 * An animation config. If provided, this will be used to animate the
10179 * closing of the dropdown.
10180 */
10181 closeAnimationConfig: {
10182 type: Object
10183 },
10184
10185 /**
10186 * If provided, this will be the element that will be focused when
10187 * the dropdown opens.
10188 */
10189 focusTarget: {
10190 type: Object
10191 },
10192
10193 /**
10194 * Set to true to disable animations when opening and closing the
10195 * dropdown.
10196 */
10197 noAnimations: {
10198 type: Boolean,
10199 value: false
10200 },
10201
10202 /**
10203 * By default, the dropdown will constrain scrolling on the page
10204 * to itself when opened.
10205 * Set to true in order to prevent scroll from being constrained
10206 * to the dropdown when it opens.
10207 */
10208 allowOutsideScroll: {
10209 type: Boolean,
10210 value: false
10211 },
10212
10213 /**
10214 * Callback for scroll events.
10215 * @type {Function}
10216 * @private
10217 */
10218 _boundOnCaptureScroll: {
10219 type: Function,
10220 value: function() {
10221 return this._onCaptureScroll.bind(this);
10222 }
10223 }
10224 },
10225
10226 listeners: {
10227 'neon-animation-finish': '_onNeonAnimationFinish'
10228 },
10229
10230 observers: [
10231 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign , verticalOffset, horizontalOffset)'
10232 ],
10233
10234 /**
10235 * The element that is contained by the dropdown, if any.
10236 */
10237 get containedElement() {
10238 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10239 },
10240
10241 /**
10242 * The element that should be focused when the dropdown opens.
10243 * @deprecated
10244 */
10245 get _focusTarget() {
10246 return this.focusTarget || this.containedElement;
10247 },
10248
10249 ready: function() {
10250 // Memoized scrolling position, used to block scrolling outside.
10251 this._scrollTop = 0;
10252 this._scrollLeft = 0;
10253 // Used to perform a non-blocking refit on scroll.
10254 this._refitOnScrollRAF = null;
10255 },
10256
10257 detached: function() {
10258 this.cancelAnimation();
10259 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10260 },
10261
10262 /**
10263 * Called when the value of `opened` changes.
10264 * Overridden from `IronOverlayBehavior`
10265 */
10266 _openedChanged: function() {
10267 if (this.opened && this.disabled) {
10268 this.cancel();
10269 } else {
10270 this.cancelAnimation();
10271 this.sizingTarget = this.containedElement || this.sizingTarget;
10272 this._updateAnimationConfig();
10273 this._saveScrollPosition();
10274 if (this.opened) {
10275 document.addEventListener('scroll', this._boundOnCaptureScroll);
10276 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push ScrollLock(this);
10277 } else {
10278 document.removeEventListener('scroll', this._boundOnCaptureScroll) ;
10279 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10280 }
10281 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments );
10282 }
10283 },
10284
10285 /**
10286 * Overridden from `IronOverlayBehavior`.
10287 */
10288 _renderOpened: function() {
10289 if (!this.noAnimations && this.animationConfig.open) {
10290 this.$.contentWrapper.classList.add('animating');
10291 this.playAnimation('open');
10292 } else {
10293 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments) ;
10294 }
10295 },
10296
10297 /**
10298 * Overridden from `IronOverlayBehavior`.
10299 */
10300 _renderClosed: function() {
10301
10302 if (!this.noAnimations && this.animationConfig.close) {
10303 this.$.contentWrapper.classList.add('animating');
10304 this.playAnimation('close');
10305 } else {
10306 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments) ;
10307 }
10308 },
10309
10310 /**
10311 * Called when animation finishes on the dropdown (when opening or
10312 * closing). Responsible for "completing" the process of opening or
10313 * closing the dropdown by positioning it or setting its display to
10314 * none.
10315 */
10316 _onNeonAnimationFinish: function() {
10317 this.$.contentWrapper.classList.remove('animating');
10318 if (this.opened) {
10319 this._finishRenderOpened();
10320 } else {
10321 this._finishRenderClosed();
10322 }
10323 },
10324
10325 _onCaptureScroll: function() {
10326 if (!this.allowOutsideScroll) {
10327 this._restoreScrollPosition();
10328 } else {
10329 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS crollRAF);
10330 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin d(this));
10331 }
10332 },
10333
10334 /**
10335 * Memoizes the scroll position of the outside scrolling element.
10336 * @private
10337 */
10338 _saveScrollPosition: function() {
10339 if (document.scrollingElement) {
10340 this._scrollTop = document.scrollingElement.scrollTop;
10341 this._scrollLeft = document.scrollingElement.scrollLeft;
10342 } else {
10343 // Since we don't know if is the body or html, get max.
10344 this._scrollTop = Math.max(document.documentElement.scrollTop, docum ent.body.scrollTop);
10345 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc ument.body.scrollLeft);
10346 }
10347 },
10348
10349 /**
10350 * Resets the scroll position of the outside scrolling element.
10351 * @private
10352 */
10353 _restoreScrollPosition: function() {
10354 if (document.scrollingElement) {
10355 document.scrollingElement.scrollTop = this._scrollTop;
10356 document.scrollingElement.scrollLeft = this._scrollLeft;
10357 } else {
10358 // Since we don't know if is the body or html, set both.
10359 document.documentElement.scrollTop = this._scrollTop;
10360 document.documentElement.scrollLeft = this._scrollLeft;
10361 document.body.scrollTop = this._scrollTop;
10362 document.body.scrollLeft = this._scrollLeft;
10363 }
10364 },
10365
10366 /**
10367 * Constructs the final animation config from different properties used
10368 * to configure specific parts of the opening and closing animations.
10369 */
10370 _updateAnimationConfig: function() {
10371 var animations = (this.openAnimationConfig || []).concat(this.closeAni mationConfig || []);
10372 for (var i = 0; i < animations.length; i++) {
10373 animations[i].node = this.containedElement;
10374 }
10375 this.animationConfig = {
10376 open: this.openAnimationConfig,
10377 close: this.closeAnimationConfig
10378 };
10379 },
10380
10381 /**
10382 * Updates the overlay position based on configured horizontal
10383 * and vertical alignment.
10384 */
10385 _updateOverlayPosition: function() {
10386 if (this.isAttached) {
10387 // This triggers iron-resize, and iron-overlay-behavior will call re fit if needed.
10388 this.notifyResize();
10389 }
10390 },
10391
10392 /**
10393 * Apply focus to focusTarget or containedElement
10394 */
10395 _applyFocus: function () {
10396 var focusTarget = this.focusTarget || this.containedElement;
10397 if (focusTarget && this.opened && !this.noAutoFocus) {
10398 focusTarget.focus();
10399 } else {
10400 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
10401 }
10402 }
10403 });
10404 })();
10405 Polymer({ 6032 Polymer({
10406 6033 is: 'fade-out-animation',
10407 is: 'fade-in-animation', 6034 behaviors: [ Polymer.NeonAnimationBehavior ],
10408 6035 configure: function(config) {
10409 behaviors: [ 6036 var node = config.node;
10410 Polymer.NeonAnimationBehavior 6037 this._effect = new KeyframeEffect(node, [ {
10411 ], 6038 opacity: '1'
10412 6039 }, {
10413 configure: function(config) { 6040 opacity: '0'
10414 var node = config.node; 6041 } ], this.timingFromConfig(config));
10415 this._effect = new KeyframeEffect(node, [ 6042 return this._effect;
10416 {'opacity': '0'}, 6043 }
10417 {'opacity': '1'} 6044 });
10418 ], this.timingFromConfig(config)); 6045
10419 return this._effect;
10420 }
10421
10422 });
10423 Polymer({ 6046 Polymer({
10424 6047 is: 'paper-menu-grow-height-animation',
10425 is: 'fade-out-animation', 6048 behaviors: [ Polymer.NeonAnimationBehavior ],
10426 6049 configure: function(config) {
10427 behaviors: [ 6050 var node = config.node;
10428 Polymer.NeonAnimationBehavior 6051 var rect = node.getBoundingClientRect();
10429 ], 6052 var height = rect.height;
10430 6053 this._effect = new KeyframeEffect(node, [ {
10431 configure: function(config) { 6054 height: height / 2 + 'px'
10432 var node = config.node; 6055 }, {
10433 this._effect = new KeyframeEffect(node, [ 6056 height: height + 'px'
10434 {'opacity': '1'}, 6057 } ], this.timingFromConfig(config));
10435 {'opacity': '0'} 6058 return this._effect;
10436 ], this.timingFromConfig(config)); 6059 }
10437 return this._effect; 6060 });
10438 } 6061
10439
10440 });
10441 Polymer({ 6062 Polymer({
10442 is: 'paper-menu-grow-height-animation', 6063 is: 'paper-menu-grow-width-animation',
10443 6064 behaviors: [ Polymer.NeonAnimationBehavior ],
10444 behaviors: [ 6065 configure: function(config) {
10445 Polymer.NeonAnimationBehavior 6066 var node = config.node;
10446 ], 6067 var rect = node.getBoundingClientRect();
10447 6068 var width = rect.width;
10448 configure: function(config) { 6069 this._effect = new KeyframeEffect(node, [ {
10449 var node = config.node; 6070 width: width / 2 + 'px'
10450 var rect = node.getBoundingClientRect(); 6071 }, {
10451 var height = rect.height; 6072 width: width + 'px'
10452 6073 } ], this.timingFromConfig(config));
10453 this._effect = new KeyframeEffect(node, [{ 6074 return this._effect;
10454 height: (height / 2) + 'px' 6075 }
10455 }, { 6076 });
10456 height: height + 'px' 6077
10457 }], this.timingFromConfig(config)); 6078 Polymer({
10458 6079 is: 'paper-menu-shrink-width-animation',
10459 return this._effect; 6080 behaviors: [ Polymer.NeonAnimationBehavior ],
10460 } 6081 configure: function(config) {
10461 }); 6082 var node = config.node;
10462 6083 var rect = node.getBoundingClientRect();
10463 Polymer({ 6084 var width = rect.width;
10464 is: 'paper-menu-grow-width-animation', 6085 this._effect = new KeyframeEffect(node, [ {
10465 6086 width: width + 'px'
10466 behaviors: [ 6087 }, {
10467 Polymer.NeonAnimationBehavior 6088 width: width - width / 20 + 'px'
10468 ], 6089 } ], this.timingFromConfig(config));
10469 6090 return this._effect;
10470 configure: function(config) { 6091 }
10471 var node = config.node; 6092 });
10472 var rect = node.getBoundingClientRect(); 6093
10473 var width = rect.width; 6094 Polymer({
10474 6095 is: 'paper-menu-shrink-height-animation',
10475 this._effect = new KeyframeEffect(node, [{ 6096 behaviors: [ Polymer.NeonAnimationBehavior ],
10476 width: (width / 2) + 'px' 6097 configure: function(config) {
10477 }, { 6098 var node = config.node;
10478 width: width + 'px' 6099 var rect = node.getBoundingClientRect();
10479 }], this.timingFromConfig(config)); 6100 var height = rect.height;
10480 6101 var top = rect.top;
10481 return this._effect; 6102 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
10482 } 6103 this._effect = new KeyframeEffect(node, [ {
10483 }); 6104 height: height + 'px',
10484 6105 transform: 'translateY(0)'
10485 Polymer({ 6106 }, {
10486 is: 'paper-menu-shrink-width-animation', 6107 height: height / 2 + 'px',
10487 6108 transform: 'translateY(-20px)'
10488 behaviors: [ 6109 } ], this.timingFromConfig(config));
10489 Polymer.NeonAnimationBehavior 6110 return this._effect;
10490 ], 6111 }
10491 6112 });
10492 configure: function(config) { 6113
10493 var node = config.node;
10494 var rect = node.getBoundingClientRect();
10495 var width = rect.width;
10496
10497 this._effect = new KeyframeEffect(node, [{
10498 width: width + 'px'
10499 }, {
10500 width: width - (width / 20) + 'px'
10501 }], this.timingFromConfig(config));
10502
10503 return this._effect;
10504 }
10505 });
10506
10507 Polymer({
10508 is: 'paper-menu-shrink-height-animation',
10509
10510 behaviors: [
10511 Polymer.NeonAnimationBehavior
10512 ],
10513
10514 configure: function(config) {
10515 var node = config.node;
10516 var rect = node.getBoundingClientRect();
10517 var height = rect.height;
10518 var top = rect.top;
10519
10520 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
10521
10522 this._effect = new KeyframeEffect(node, [{
10523 height: height + 'px',
10524 transform: 'translateY(0)'
10525 }, {
10526 height: height / 2 + 'px',
10527 transform: 'translateY(-20px)'
10528 }], this.timingFromConfig(config));
10529
10530 return this._effect;
10531 }
10532 });
10533 // Copyright 2016 The Chromium Authors. All rights reserved. 6114 // Copyright 2016 The Chromium Authors. All rights reserved.
10534 // Use of this source code is governed by a BSD-style license that can be 6115 // Use of this source code is governed by a BSD-style license that can be
10535 // found in the LICENSE file. 6116 // found in the LICENSE file.
10536
10537 /** Same as paper-menu-button's custom easing cubic-bezier param. */
10538 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; 6117 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)';
10539 6118
10540 Polymer({ 6119 Polymer({
10541 is: 'cr-shared-menu', 6120 is: 'cr-shared-menu',
10542 6121 behaviors: [ Polymer.IronA11yKeysBehavior ],
10543 behaviors: [Polymer.IronA11yKeysBehavior],
10544
10545 properties: { 6122 properties: {
10546 menuOpen: { 6123 menuOpen: {
10547 type: Boolean, 6124 type: Boolean,
10548 observer: 'menuOpenChanged_', 6125 observer: 'menuOpenChanged_',
10549 value: false, 6126 value: false,
10550 notify: true, 6127 notify: true
10551 }, 6128 },
10552
10553 /**
10554 * The contextual item that this menu was clicked for.
10555 * e.g. the data used to render an item in an <iron-list> or <dom-repeat>
10556 * @type {?Object}
10557 */
10558 itemData: { 6129 itemData: {
10559 type: Object, 6130 type: Object,
10560 value: null, 6131 value: null
10561 }, 6132 },
10562
10563 /** @override */
10564 keyEventTarget: { 6133 keyEventTarget: {
10565 type: Object, 6134 type: Object,
10566 value: function() { 6135 value: function() {
10567 return this.$.menu; 6136 return this.$.menu;
10568 } 6137 }
10569 }, 6138 },
10570
10571 openAnimationConfig: { 6139 openAnimationConfig: {
10572 type: Object, 6140 type: Object,
10573 value: function() { 6141 value: function() {
10574 return [{ 6142 return [ {
10575 name: 'fade-in-animation', 6143 name: 'fade-in-animation',
10576 timing: { 6144 timing: {
10577 delay: 50, 6145 delay: 50,
10578 duration: 200 6146 duration: 200
10579 } 6147 }
10580 }, { 6148 }, {
10581 name: 'paper-menu-grow-width-animation', 6149 name: 'paper-menu-grow-width-animation',
10582 timing: { 6150 timing: {
10583 delay: 50, 6151 delay: 50,
10584 duration: 150, 6152 duration: 150,
10585 easing: SLIDE_CUBIC_BEZIER 6153 easing: SLIDE_CUBIC_BEZIER
10586 } 6154 }
10587 }, { 6155 }, {
10588 name: 'paper-menu-grow-height-animation', 6156 name: 'paper-menu-grow-height-animation',
10589 timing: { 6157 timing: {
10590 delay: 100, 6158 delay: 100,
10591 duration: 275, 6159 duration: 275,
10592 easing: SLIDE_CUBIC_BEZIER 6160 easing: SLIDE_CUBIC_BEZIER
10593 } 6161 }
10594 }]; 6162 } ];
10595 } 6163 }
10596 }, 6164 },
10597
10598 closeAnimationConfig: { 6165 closeAnimationConfig: {
10599 type: Object, 6166 type: Object,
10600 value: function() { 6167 value: function() {
10601 return [{ 6168 return [ {
10602 name: 'fade-out-animation', 6169 name: 'fade-out-animation',
10603 timing: { 6170 timing: {
10604 duration: 150 6171 duration: 150
10605 } 6172 }
10606 }]; 6173 } ];
10607 } 6174 }
10608 } 6175 }
10609 }, 6176 },
10610
10611 keyBindings: { 6177 keyBindings: {
10612 'tab': 'onTabPressed_', 6178 tab: 'onTabPressed_'
10613 }, 6179 },
10614
10615 listeners: { 6180 listeners: {
10616 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', 6181 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_'
10617 }, 6182 },
10618
10619 /**
10620 * The last anchor that was used to open a menu. It's necessary for toggling.
10621 * @private {?Element}
10622 */
10623 lastAnchor_: null, 6183 lastAnchor_: null,
10624
10625 /**
10626 * The first focusable child in the menu's light DOM.
10627 * @private {?Element}
10628 */
10629 firstFocus_: null, 6184 firstFocus_: null,
10630
10631 /**
10632 * The last focusable child in the menu's light DOM.
10633 * @private {?Element}
10634 */
10635 lastFocus_: null, 6185 lastFocus_: null,
10636
10637 /** @override */
10638 attached: function() { 6186 attached: function() {
10639 window.addEventListener('resize', this.closeMenu.bind(this)); 6187 window.addEventListener('resize', this.closeMenu.bind(this));
10640 }, 6188 },
10641
10642 /** Closes the menu. */
10643 closeMenu: function() { 6189 closeMenu: function() {
10644 if (this.root.activeElement == null) { 6190 if (this.root.activeElement == null) {
10645 // Something else has taken focus away from the menu. Do not attempt to
10646 // restore focus to the button which opened the menu.
10647 this.$.dropdown.restoreFocusOnClose = false; 6191 this.$.dropdown.restoreFocusOnClose = false;
10648 } 6192 }
10649 this.menuOpen = false; 6193 this.menuOpen = false;
10650 }, 6194 },
10651
10652 /**
10653 * Opens the menu at the anchor location.
10654 * @param {!Element} anchor The location to display the menu.
10655 * @param {!Object} itemData The contextual item's data.
10656 */
10657 openMenu: function(anchor, itemData) { 6195 openMenu: function(anchor, itemData) {
10658 if (this.lastAnchor_ == anchor && this.menuOpen) 6196 if (this.lastAnchor_ == anchor && this.menuOpen) return;
10659 return; 6197 if (this.menuOpen) this.closeMenu();
10660
10661 if (this.menuOpen)
10662 this.closeMenu();
10663
10664 this.itemData = itemData; 6198 this.itemData = itemData;
10665 this.lastAnchor_ = anchor; 6199 this.lastAnchor_ = anchor;
10666 this.$.dropdown.restoreFocusOnClose = true; 6200 this.$.dropdown.restoreFocusOnClose = true;
10667 6201 var focusableChildren = Polymer.dom(this).querySelectorAll('[tabindex]:not([ hidden]),button:not([hidden])');
10668 var focusableChildren = Polymer.dom(this).querySelectorAll(
10669 '[tabindex]:not([hidden]),button:not([hidden])');
10670 if (focusableChildren.length > 0) { 6202 if (focusableChildren.length > 0) {
10671 this.$.dropdown.focusTarget = focusableChildren[0]; 6203 this.$.dropdown.focusTarget = focusableChildren[0];
10672 this.firstFocus_ = focusableChildren[0]; 6204 this.firstFocus_ = focusableChildren[0];
10673 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; 6205 this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
10674 } 6206 }
10675
10676 // Move the menu to the anchor.
10677 this.$.dropdown.positionTarget = anchor; 6207 this.$.dropdown.positionTarget = anchor;
10678 this.menuOpen = true; 6208 this.menuOpen = true;
10679 }, 6209 },
10680
10681 /**
10682 * Toggles the menu for the anchor that is passed in.
10683 * @param {!Element} anchor The location to display the menu.
10684 * @param {!Object} itemData The contextual item's data.
10685 */
10686 toggleMenu: function(anchor, itemData) { 6210 toggleMenu: function(anchor, itemData) {
10687 if (anchor == this.lastAnchor_ && this.menuOpen) 6211 if (anchor == this.lastAnchor_ && this.menuOpen) this.closeMenu(); else this .openMenu(anchor, itemData);
10688 this.closeMenu(); 6212 },
10689 else
10690 this.openMenu(anchor, itemData);
10691 },
10692
10693 /**
10694 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
10695 * the first element with a nonzero tabindex to the last such element.
10696 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
10697 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
10698 * @param {CustomEvent} e
10699 */
10700 onTabPressed_: function(e) { 6213 onTabPressed_: function(e) {
10701 if (!this.firstFocus_ || !this.lastFocus_) 6214 if (!this.firstFocus_ || !this.lastFocus_) return;
10702 return;
10703
10704 var toFocus; 6215 var toFocus;
10705 var keyEvent = e.detail.keyboardEvent; 6216 var keyEvent = e.detail.keyboardEvent;
10706 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) 6217 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) toFocus = this .lastFocus_; else if (keyEvent.target == this.lastFocus_) toFocus = this.firstFo cus_;
10707 toFocus = this.lastFocus_; 6218 if (!toFocus) return;
10708 else if (keyEvent.target == this.lastFocus_)
10709 toFocus = this.firstFocus_;
10710
10711 if (!toFocus)
10712 return;
10713
10714 e.preventDefault(); 6219 e.preventDefault();
10715 toFocus.focus(); 6220 toFocus.focus();
10716 }, 6221 },
10717
10718 /**
10719 * Ensure the menu is reset properly when it is closed by the dropdown (eg,
10720 * clicking outside).
10721 * @private
10722 */
10723 menuOpenChanged_: function() { 6222 menuOpenChanged_: function() {
10724 if (!this.menuOpen) { 6223 if (!this.menuOpen) {
10725 this.itemData = null; 6224 this.itemData = null;
10726 this.lastAnchor_ = null; 6225 this.lastAnchor_ = null;
10727 } 6226 }
10728 }, 6227 },
10729
10730 /**
10731 * Prevent focus restoring when tapping outside the menu. This stops the
10732 * focus moving around unexpectedly when closing the menu with the mouse.
10733 * @param {CustomEvent} e
10734 * @private
10735 */
10736 onOverlayCanceled_: function(e) { 6228 onOverlayCanceled_: function(e) {
10737 if (e.detail.type == 'tap') 6229 if (e.detail.type == 'tap') this.$.dropdown.restoreFocusOnClose = false;
10738 this.$.dropdown.restoreFocusOnClose = false; 6230 }
10739 }, 6231 });
10740 }); 6232
10741 /** @polymerBehavior Polymer.PaperItemBehavior */ 6233 Polymer.PaperItemBehaviorImpl = {
10742 Polymer.PaperItemBehaviorImpl = { 6234 hostAttributes: {
10743 hostAttributes: { 6235 role: 'option',
10744 role: 'option', 6236 tabindex: '0'
10745 tabindex: '0' 6237 }
10746 } 6238 };
10747 }; 6239
10748 6240 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState, Polymer.PaperItemBehaviorImpl ];
10749 /** @polymerBehavior */ 6241
10750 Polymer.PaperItemBehavior = [
10751 Polymer.IronButtonState,
10752 Polymer.IronControlState,
10753 Polymer.PaperItemBehaviorImpl
10754 ];
10755 Polymer({ 6242 Polymer({
10756 is: 'paper-item', 6243 is: 'paper-item',
10757 6244 behaviors: [ Polymer.PaperItemBehavior ]
10758 behaviors: [ 6245 });
10759 Polymer.PaperItemBehavior 6246
10760 ]
10761 });
10762 Polymer({ 6247 Polymer({
10763 6248 is: 'iron-collapse',
10764 is: 'iron-collapse', 6249 behaviors: [ Polymer.IronResizableBehavior ],
10765 6250 properties: {
10766 behaviors: [ 6251 horizontal: {
10767 Polymer.IronResizableBehavior 6252 type: Boolean,
10768 ], 6253 value: false,
10769 6254 observer: '_horizontalChanged'
10770 properties: { 6255 },
10771 6256 opened: {
10772 /** 6257 type: Boolean,
10773 * If true, the orientation is horizontal; otherwise is vertical. 6258 value: false,
10774 * 6259 notify: true,
10775 * @attribute horizontal 6260 observer: '_openedChanged'
10776 */ 6261 },
10777 horizontal: { 6262 noAnimation: {
10778 type: Boolean, 6263 type: Boolean
10779 value: false, 6264 }
10780 observer: '_horizontalChanged' 6265 },
10781 }, 6266 get dimension() {
10782 6267 return this.horizontal ? 'width' : 'height';
10783 /** 6268 },
10784 * Set opened to true to show the collapse element and to false to hide it . 6269 get _dimensionMax() {
10785 * 6270 return this.horizontal ? 'maxWidth' : 'maxHeight';
10786 * @attribute opened 6271 },
10787 */ 6272 get _dimensionMaxCss() {
10788 opened: { 6273 return this.horizontal ? 'max-width' : 'max-height';
10789 type: Boolean, 6274 },
10790 value: false, 6275 hostAttributes: {
10791 notify: true, 6276 role: 'group',
10792 observer: '_openedChanged' 6277 'aria-hidden': 'true',
10793 }, 6278 'aria-expanded': 'false'
10794 6279 },
10795 /** 6280 listeners: {
10796 * Set noAnimation to true to disable animations 6281 transitionend: '_transitionEnd'
10797 * 6282 },
10798 * @attribute noAnimation 6283 attached: function() {
10799 */ 6284 this._transitionEnd();
10800 noAnimation: { 6285 },
10801 type: Boolean 6286 toggle: function() {
10802 }, 6287 this.opened = !this.opened;
10803 6288 },
10804 }, 6289 show: function() {
10805 6290 this.opened = true;
10806 get dimension() { 6291 },
10807 return this.horizontal ? 'width' : 'height'; 6292 hide: function() {
10808 }, 6293 this.opened = false;
10809 6294 },
10810 /** 6295 updateSize: function(size, animated) {
10811 * `maxWidth` or `maxHeight`. 6296 var curSize = this.style[this._dimensionMax];
10812 * @private 6297 if (curSize === size || size === 'auto' && !curSize) {
10813 */ 6298 return;
10814 get _dimensionMax() { 6299 }
10815 return this.horizontal ? 'maxWidth' : 'maxHeight'; 6300 this._updateTransition(false);
10816 }, 6301 if (animated && !this.noAnimation && this._isDisplayed) {
10817 6302 var startSize = this._calcSize();
10818 /**
10819 * `max-width` or `max-height`.
10820 * @private
10821 */
10822 get _dimensionMaxCss() {
10823 return this.horizontal ? 'max-width' : 'max-height';
10824 },
10825
10826 hostAttributes: {
10827 role: 'group',
10828 'aria-hidden': 'true',
10829 'aria-expanded': 'false'
10830 },
10831
10832 listeners: {
10833 transitionend: '_transitionEnd'
10834 },
10835
10836 attached: function() {
10837 // It will take care of setting correct classes and styles.
10838 this._transitionEnd();
10839 },
10840
10841 /**
10842 * Toggle the opened state.
10843 *
10844 * @method toggle
10845 */
10846 toggle: function() {
10847 this.opened = !this.opened;
10848 },
10849
10850 show: function() {
10851 this.opened = true;
10852 },
10853
10854 hide: function() {
10855 this.opened = false;
10856 },
10857
10858 /**
10859 * Updates the size of the element.
10860 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop erty value, usually `auto` or `0px`.
10861 * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
10862 */
10863 updateSize: function(size, animated) {
10864 // No change!
10865 var curSize = this.style[this._dimensionMax];
10866 if (curSize === size || (size === 'auto' && !curSize)) {
10867 return;
10868 }
10869
10870 this._updateTransition(false);
10871 // If we can animate, must do some prep work.
10872 if (animated && !this.noAnimation && this._isDisplayed) {
10873 // Animation will start at the current size.
10874 var startSize = this._calcSize();
10875 // For `auto` we must calculate what is the final size for the animation .
10876 // After the transition is done, _transitionEnd will set the size back t o `auto`.
10877 if (size === 'auto') {
10878 this.style[this._dimensionMax] = '';
10879 size = this._calcSize();
10880 }
10881 // Go to startSize without animation.
10882 this.style[this._dimensionMax] = startSize;
10883 // Force layout to ensure transition will go. Set scrollTop to itself
10884 // so that compilers won't remove it.
10885 this.scrollTop = this.scrollTop;
10886 // Enable animation.
10887 this._updateTransition(true);
10888 }
10889 // Set the final size.
10890 if (size === 'auto') { 6303 if (size === 'auto') {
10891 this.style[this._dimensionMax] = ''; 6304 this.style[this._dimensionMax] = '';
6305 size = this._calcSize();
6306 }
6307 this.style[this._dimensionMax] = startSize;
6308 this.scrollTop = this.scrollTop;
6309 this._updateTransition(true);
6310 }
6311 if (size === 'auto') {
6312 this.style[this._dimensionMax] = '';
6313 } else {
6314 this.style[this._dimensionMax] = size;
6315 }
6316 },
6317 enableTransition: function(enabled) {
6318 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` in stead.');
6319 this.noAnimation = !enabled;
6320 },
6321 _updateTransition: function(enabled) {
6322 this.style.transitionDuration = enabled && !this.noAnimation ? '' : '0s';
6323 },
6324 _horizontalChanged: function() {
6325 this.style.transitionProperty = this._dimensionMaxCss;
6326 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxW idth';
6327 this.style[otherDimension] = '';
6328 this.updateSize(this.opened ? 'auto' : '0px', false);
6329 },
6330 _openedChanged: function() {
6331 this.setAttribute('aria-expanded', this.opened);
6332 this.setAttribute('aria-hidden', !this.opened);
6333 this.toggleClass('iron-collapse-closed', false);
6334 this.toggleClass('iron-collapse-opened', false);
6335 this.updateSize(this.opened ? 'auto' : '0px', true);
6336 if (this.opened) {
6337 this.focus();
6338 }
6339 if (this.noAnimation) {
6340 this._transitionEnd();
6341 }
6342 },
6343 _transitionEnd: function() {
6344 if (this.opened) {
6345 this.style[this._dimensionMax] = '';
6346 }
6347 this.toggleClass('iron-collapse-closed', !this.opened);
6348 this.toggleClass('iron-collapse-opened', this.opened);
6349 this._updateTransition(false);
6350 this.notifyResize();
6351 },
6352 get _isDisplayed() {
6353 var rect = this.getBoundingClientRect();
6354 for (var prop in rect) {
6355 if (rect[prop] !== 0) return true;
6356 }
6357 return false;
6358 },
6359 _calcSize: function() {
6360 return this.getBoundingClientRect()[this.dimension] + 'px';
6361 }
6362 });
6363
6364 Polymer.IronFormElementBehavior = {
6365 properties: {
6366 name: {
6367 type: String
6368 },
6369 value: {
6370 notify: true,
6371 type: String
6372 },
6373 required: {
6374 type: Boolean,
6375 value: false
6376 },
6377 _parentForm: {
6378 type: Object
6379 }
6380 },
6381 attached: function() {
6382 this.fire('iron-form-element-register');
6383 },
6384 detached: function() {
6385 if (this._parentForm) {
6386 this._parentForm.fire('iron-form-element-unregister', {
6387 target: this
6388 });
6389 }
6390 }
6391 };
6392
6393 Polymer.IronCheckedElementBehaviorImpl = {
6394 properties: {
6395 checked: {
6396 type: Boolean,
6397 value: false,
6398 reflectToAttribute: true,
6399 notify: true,
6400 observer: '_checkedChanged'
6401 },
6402 toggles: {
6403 type: Boolean,
6404 value: true,
6405 reflectToAttribute: true
6406 },
6407 value: {
6408 type: String,
6409 value: 'on',
6410 observer: '_valueChanged'
6411 }
6412 },
6413 observers: [ '_requiredChanged(required)' ],
6414 created: function() {
6415 this._hasIronCheckedElementBehavior = true;
6416 },
6417 _getValidity: function(_value) {
6418 return this.disabled || !this.required || this.checked;
6419 },
6420 _requiredChanged: function() {
6421 if (this.required) {
6422 this.setAttribute('aria-required', 'true');
6423 } else {
6424 this.removeAttribute('aria-required');
6425 }
6426 },
6427 _checkedChanged: function() {
6428 this.active = this.checked;
6429 this.fire('iron-change');
6430 },
6431 _valueChanged: function() {
6432 if (this.value === undefined || this.value === null) {
6433 this.value = 'on';
6434 }
6435 }
6436 };
6437
6438 Polymer.IronCheckedElementBehavior = [ Polymer.IronFormElementBehavior, Polymer. IronValidatableBehavior, Polymer.IronCheckedElementBehaviorImpl ];
6439
6440 Polymer.PaperCheckedElementBehaviorImpl = {
6441 _checkedChanged: function() {
6442 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
6443 if (this.hasRipple()) {
6444 if (this.checked) {
6445 this._ripple.setAttribute('checked', '');
10892 } else { 6446 } else {
10893 this.style[this._dimensionMax] = size; 6447 this._ripple.removeAttribute('checked');
10894 } 6448 }
10895 }, 6449 }
10896 6450 },
10897 /** 6451 _buttonStateChanged: function() {
10898 * enableTransition() is deprecated, but left over so it doesn't break exist ing code. 6452 Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
10899 * Please use `noAnimation` property instead. 6453 if (this.disabled) {
10900 * 6454 return;
10901 * @method enableTransition 6455 }
10902 * @deprecated since version 1.0.4 6456 if (this.isAttached) {
10903 */ 6457 this.checked = this.active;
10904 enableTransition: function(enabled) { 6458 }
10905 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.'); 6459 }
10906 this.noAnimation = !enabled; 6460 };
10907 }, 6461
10908 6462 Polymer.PaperCheckedElementBehavior = [ Polymer.PaperInkyFocusBehavior, Polymer. IronCheckedElementBehavior, Polymer.PaperCheckedElementBehaviorImpl ];
10909 _updateTransition: function(enabled) { 6463
10910 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s' ;
10911 },
10912
10913 _horizontalChanged: function() {
10914 this.style.transitionProperty = this._dimensionMaxCss;
10915 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma xWidth';
10916 this.style[otherDimension] = '';
10917 this.updateSize(this.opened ? 'auto' : '0px', false);
10918 },
10919
10920 _openedChanged: function() {
10921 this.setAttribute('aria-expanded', this.opened);
10922 this.setAttribute('aria-hidden', !this.opened);
10923
10924 this.toggleClass('iron-collapse-closed', false);
10925 this.toggleClass('iron-collapse-opened', false);
10926 this.updateSize(this.opened ? 'auto' : '0px', true);
10927
10928 // Focus the current collapse.
10929 if (this.opened) {
10930 this.focus();
10931 }
10932 if (this.noAnimation) {
10933 this._transitionEnd();
10934 }
10935 },
10936
10937 _transitionEnd: function() {
10938 if (this.opened) {
10939 this.style[this._dimensionMax] = '';
10940 }
10941 this.toggleClass('iron-collapse-closed', !this.opened);
10942 this.toggleClass('iron-collapse-opened', this.opened);
10943 this._updateTransition(false);
10944 this.notifyResize();
10945 },
10946
10947 /**
10948 * Simplistic heuristic to detect if element has a parent with display: none
10949 *
10950 * @private
10951 */
10952 get _isDisplayed() {
10953 var rect = this.getBoundingClientRect();
10954 for (var prop in rect) {
10955 if (rect[prop] !== 0) return true;
10956 }
10957 return false;
10958 },
10959
10960 _calcSize: function() {
10961 return this.getBoundingClientRect()[this.dimension] + 'px';
10962 }
10963
10964 });
10965 /**
10966 Polymer.IronFormElementBehavior enables a custom element to be included
10967 in an `iron-form`.
10968
10969 @demo demo/index.html
10970 @polymerBehavior
10971 */
10972 Polymer.IronFormElementBehavior = {
10973
10974 properties: {
10975 /**
10976 * Fired when the element is added to an `iron-form`.
10977 *
10978 * @event iron-form-element-register
10979 */
10980
10981 /**
10982 * Fired when the element is removed from an `iron-form`.
10983 *
10984 * @event iron-form-element-unregister
10985 */
10986
10987 /**
10988 * The name of this element.
10989 */
10990 name: {
10991 type: String
10992 },
10993
10994 /**
10995 * The value for this element.
10996 */
10997 value: {
10998 notify: true,
10999 type: String
11000 },
11001
11002 /**
11003 * Set to true to mark the input as required. If used in a form, a
11004 * custom element that uses this behavior should also use
11005 * Polymer.IronValidatableBehavior and define a custom validation method.
11006 * Otherwise, a `required` element will always be considered valid.
11007 * It's also strongly recommended to provide a visual style for the elemen t
11008 * when its value is invalid.
11009 */
11010 required: {
11011 type: Boolean,
11012 value: false
11013 },
11014
11015 /**
11016 * The form that the element is registered to.
11017 */
11018 _parentForm: {
11019 type: Object
11020 }
11021 },
11022
11023 attached: function() {
11024 // Note: the iron-form that this element belongs to will set this
11025 // element's _parentForm property when handling this event.
11026 this.fire('iron-form-element-register');
11027 },
11028
11029 detached: function() {
11030 if (this._parentForm) {
11031 this._parentForm.fire('iron-form-element-unregister', {target: this});
11032 }
11033 }
11034
11035 };
11036 /**
11037 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element
11038 * that has a `checked` property, which can be used for validation if the
11039 * element is also `required`. Element instances implementing this behavior
11040 * will also be registered for use in an `iron-form` element.
11041 *
11042 * @demo demo/index.html
11043 * @polymerBehavior Polymer.IronCheckedElementBehavior
11044 */
11045 Polymer.IronCheckedElementBehaviorImpl = {
11046
11047 properties: {
11048 /**
11049 * Fired when the checked state changes.
11050 *
11051 * @event iron-change
11052 */
11053
11054 /**
11055 * Gets or sets the state, `true` is checked and `false` is unchecked.
11056 */
11057 checked: {
11058 type: Boolean,
11059 value: false,
11060 reflectToAttribute: true,
11061 notify: true,
11062 observer: '_checkedChanged'
11063 },
11064
11065 /**
11066 * If true, the button toggles the active state with each tap or press
11067 * of the spacebar.
11068 */
11069 toggles: {
11070 type: Boolean,
11071 value: true,
11072 reflectToAttribute: true
11073 },
11074
11075 /* Overriden from Polymer.IronFormElementBehavior */
11076 value: {
11077 type: String,
11078 value: 'on',
11079 observer: '_valueChanged'
11080 }
11081 },
11082
11083 observers: [
11084 '_requiredChanged(required)'
11085 ],
11086
11087 created: function() {
11088 // Used by `iron-form` to handle the case that an element with this behavi or
11089 // doesn't have a role of 'checkbox' or 'radio', but should still only be
11090 // included when the form is serialized if `this.checked === true`.
11091 this._hasIronCheckedElementBehavior = true;
11092 },
11093
11094 /**
11095 * Returns false if the element is required and not checked, and true otherw ise.
11096 * @param {*=} _value Ignored.
11097 * @return {boolean} true if `required` is false or if `checked` is true.
11098 */
11099 _getValidity: function(_value) {
11100 return this.disabled || !this.required || this.checked;
11101 },
11102
11103 /**
11104 * Update the aria-required label when `required` is changed.
11105 */
11106 _requiredChanged: function() {
11107 if (this.required) {
11108 this.setAttribute('aria-required', 'true');
11109 } else {
11110 this.removeAttribute('aria-required');
11111 }
11112 },
11113
11114 /**
11115 * Fire `iron-changed` when the checked state changes.
11116 */
11117 _checkedChanged: function() {
11118 this.active = this.checked;
11119 this.fire('iron-change');
11120 },
11121
11122 /**
11123 * Reset value to 'on' if it is set to `undefined`.
11124 */
11125 _valueChanged: function() {
11126 if (this.value === undefined || this.value === null) {
11127 this.value = 'on';
11128 }
11129 }
11130 };
11131
11132 /** @polymerBehavior Polymer.IronCheckedElementBehavior */
11133 Polymer.IronCheckedElementBehavior = [
11134 Polymer.IronFormElementBehavior,
11135 Polymer.IronValidatableBehavior,
11136 Polymer.IronCheckedElementBehaviorImpl
11137 ];
11138 /**
11139 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element
11140 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio r`
11141 * and is compatible with having a ripple effect.
11142 * @polymerBehavior Polymer.PaperCheckedElementBehavior
11143 */
11144 Polymer.PaperCheckedElementBehaviorImpl = {
11145 /**
11146 * Synchronizes the element's checked state with its ripple effect.
11147 */
11148 _checkedChanged: function() {
11149 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
11150 if (this.hasRipple()) {
11151 if (this.checked) {
11152 this._ripple.setAttribute('checked', '');
11153 } else {
11154 this._ripple.removeAttribute('checked');
11155 }
11156 }
11157 },
11158
11159 /**
11160 * Synchronizes the element's `active` and `checked` state.
11161 */
11162 _buttonStateChanged: function() {
11163 Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
11164 if (this.disabled) {
11165 return;
11166 }
11167 if (this.isAttached) {
11168 this.checked = this.active;
11169 }
11170 }
11171 };
11172
11173 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */
11174 Polymer.PaperCheckedElementBehavior = [
11175 Polymer.PaperInkyFocusBehavior,
11176 Polymer.IronCheckedElementBehavior,
11177 Polymer.PaperCheckedElementBehaviorImpl
11178 ];
11179 Polymer({ 6464 Polymer({
11180 is: 'paper-checkbox', 6465 is: 'paper-checkbox',
11181 6466 behaviors: [ Polymer.PaperCheckedElementBehavior ],
11182 behaviors: [ 6467 hostAttributes: {
11183 Polymer.PaperCheckedElementBehavior 6468 role: 'checkbox',
11184 ], 6469 'aria-checked': false,
11185 6470 tabindex: 0
11186 hostAttributes: { 6471 },
11187 role: 'checkbox', 6472 properties: {
11188 'aria-checked': false, 6473 ariaActiveAttribute: {
11189 tabindex: 0 6474 type: String,
11190 }, 6475 value: 'aria-checked'
11191 6476 }
11192 properties: { 6477 },
11193 /** 6478 _computeCheckboxClass: function(checked, invalid) {
11194 * Fired when the checked state changes due to user interaction. 6479 var className = '';
11195 * 6480 if (checked) {
11196 * @event change 6481 className += 'checked ';
11197 */ 6482 }
11198 6483 if (invalid) {
11199 /** 6484 className += 'invalid';
11200 * Fired when the checked state changes. 6485 }
11201 * 6486 return className;
11202 * @event iron-change 6487 },
11203 */ 6488 _computeCheckmarkClass: function(checked) {
11204 ariaActiveAttribute: { 6489 return checked ? '' : 'hidden';
11205 type: String, 6490 },
11206 value: 'aria-checked' 6491 _createRipple: function() {
11207 } 6492 this._rippleContainer = this.$.checkboxContainer;
11208 }, 6493 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
11209 6494 }
11210 _computeCheckboxClass: function(checked, invalid) { 6495 });
11211 var className = ''; 6496
11212 if (checked) {
11213 className += 'checked ';
11214 }
11215 if (invalid) {
11216 className += 'invalid';
11217 }
11218 return className;
11219 },
11220
11221 _computeCheckmarkClass: function(checked) {
11222 return checked ? '' : 'hidden';
11223 },
11224
11225 // create ripple inside the checkboxContainer
11226 _createRipple: function() {
11227 this._rippleContainer = this.$.checkboxContainer;
11228 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
11229 }
11230
11231 });
11232 Polymer({ 6497 Polymer({
11233 is: 'paper-icon-button-light', 6498 is: 'paper-icon-button-light',
11234 extends: 'button', 6499 "extends": 'button',
11235 6500 behaviors: [ Polymer.PaperRippleBehavior ],
11236 behaviors: [ 6501 listeners: {
11237 Polymer.PaperRippleBehavior 6502 down: '_rippleDown',
11238 ], 6503 up: '_rippleUp',
11239 6504 focus: '_rippleDown',
11240 listeners: { 6505 blur: '_rippleUp'
11241 'down': '_rippleDown', 6506 },
11242 'up': '_rippleUp', 6507 _rippleDown: function() {
11243 'focus': '_rippleDown', 6508 this.getRipple().downAction();
11244 'blur': '_rippleUp', 6509 },
11245 }, 6510 _rippleUp: function() {
11246 6511 this.getRipple().upAction();
11247 _rippleDown: function() { 6512 },
11248 this.getRipple().downAction(); 6513 ensureRipple: function(var_args) {
11249 }, 6514 var lastRipple = this._ripple;
11250 6515 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
11251 _rippleUp: function() { 6516 if (this._ripple && this._ripple !== lastRipple) {
11252 this.getRipple().upAction(); 6517 this._ripple.center = true;
11253 }, 6518 this._ripple.classList.add('circle');
11254 6519 }
11255 /** 6520 }
11256 * @param {...*} var_args 6521 });
11257 */ 6522
11258 ensureRipple: function(var_args) {
11259 var lastRipple = this._ripple;
11260 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
11261 if (this._ripple && this._ripple !== lastRipple) {
11262 this._ripple.center = true;
11263 this._ripple.classList.add('circle');
11264 }
11265 }
11266 });
11267 // Copyright 2016 The Chromium Authors. All rights reserved. 6523 // Copyright 2016 The Chromium Authors. All rights reserved.
11268 // Use of this source code is governed by a BSD-style license that can be 6524 // Use of this source code is governed by a BSD-style license that can be
11269 // found in the LICENSE file. 6525 // found in the LICENSE file.
11270
11271 cr.define('cr.icon', function() { 6526 cr.define('cr.icon', function() {
11272 /**
11273 * @return {!Array<number>} The scale factors supported by this platform for
11274 * webui resources.
11275 */
11276 function getSupportedScaleFactors() { 6527 function getSupportedScaleFactors() {
11277 var supportedScaleFactors = []; 6528 var supportedScaleFactors = [];
11278 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { 6529 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
11279 // All desktop platforms support zooming which also updates the
11280 // renderer's device scale factors (a.k.a devicePixelRatio), and
11281 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
11282 // image-set on these platforms so that the renderer can pick the
11283 // closest image for the current device scale factor.
11284 supportedScaleFactors.push(1); 6530 supportedScaleFactors.push(1);
11285 supportedScaleFactors.push(2); 6531 supportedScaleFactors.push(2);
11286 } else { 6532 } else {
11287 // For other platforms that use fixed device scale factor, use
11288 // the window's device pixel ratio.
11289 // TODO(oshima): Investigate if Android/iOS need to use image-set.
11290 supportedScaleFactors.push(window.devicePixelRatio); 6533 supportedScaleFactors.push(window.devicePixelRatio);
11291 } 6534 }
11292 return supportedScaleFactors; 6535 return supportedScaleFactors;
11293 } 6536 }
11294
11295 /**
11296 * Returns the URL of the image, or an image set of URLs for the profile
11297 * avatar. Default avatars have resources available for multiple scalefactors,
11298 * whereas the GAIA profile image only comes in one size.
11299 *
11300 * @param {string} path The path of the image.
11301 * @return {string} The url, or an image set of URLs of the avatar image.
11302 */
11303 function getProfileAvatarIcon(path) { 6537 function getProfileAvatarIcon(path) {
11304 var chromeThemePath = 'chrome://theme'; 6538 var chromeThemePath = 'chrome://theme';
11305 var isDefaultAvatar = 6539 var isDefaultAvatar = path.slice(0, chromeThemePath.length) == chromeThemePa th;
11306 (path.slice(0, chromeThemePath.length) == chromeThemePath); 6540 return isDefaultAvatar ? imageset(path + '@scalefactorx') : url(path);
11307 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); 6541 }
11308 }
11309
11310 /**
11311 * Generates a CSS -webkit-image-set for a chrome:// url.
11312 * An entry in the image set is added for each of getSupportedScaleFactors().
11313 * The scale-factor-specific url is generated by replacing the first instance
11314 * of 'scalefactor' in |path| with the numeric scale factor.
11315 * @param {string} path The URL to generate an image set for.
11316 * 'scalefactor' should be a substring of |path|.
11317 * @return {string} The CSS -webkit-image-set.
11318 */
11319 function imageset(path) { 6542 function imageset(path) {
11320 var supportedScaleFactors = getSupportedScaleFactors(); 6543 var supportedScaleFactors = getSupportedScaleFactors();
11321
11322 var replaceStartIndex = path.indexOf('scalefactor'); 6544 var replaceStartIndex = path.indexOf('scalefactor');
11323 if (replaceStartIndex < 0) 6545 if (replaceStartIndex < 0) return url(path);
11324 return url(path);
11325
11326 var s = ''; 6546 var s = '';
11327 for (var i = 0; i < supportedScaleFactors.length; ++i) { 6547 for (var i = 0; i < supportedScaleFactors.length; ++i) {
11328 var scaleFactor = supportedScaleFactors[i]; 6548 var scaleFactor = supportedScaleFactors[i];
11329 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + 6549 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
11330 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
11331
11332 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; 6550 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
11333 6551 if (i != supportedScaleFactors.length - 1) s += ', ';
11334 if (i != supportedScaleFactors.length - 1)
11335 s += ', ';
11336 } 6552 }
11337 return '-webkit-image-set(' + s + ')'; 6553 return '-webkit-image-set(' + s + ')';
11338 } 6554 }
11339
11340 /**
11341 * A regular expression for identifying favicon URLs.
11342 * @const {!RegExp}
11343 */
11344 var FAVICON_URL_REGEX = /\.ico$/i; 6555 var FAVICON_URL_REGEX = /\.ico$/i;
11345
11346 /**
11347 * Creates a CSS -webkit-image-set for a favicon request.
11348 * @param {string} url Either the URL of the original page or of the favicon
11349 * itself.
11350 * @param {number=} opt_size Optional preferred size of the favicon.
11351 * @param {string=} opt_type Optional type of favicon to request. Valid values
11352 * are 'favicon' and 'touch-icon'. Default is 'favicon'.
11353 * @return {string} -webkit-image-set for the favicon.
11354 */
11355 function getFaviconImageSet(url, opt_size, opt_type) { 6556 function getFaviconImageSet(url, opt_size, opt_type) {
11356 var size = opt_size || 16; 6557 var size = opt_size || 16;
11357 var type = opt_type || 'favicon'; 6558 var type = opt_type || 'favicon';
11358 6559 return imageset('chrome://' + type + '/size/' + size + '@scalefactorx/' + (F AVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
11359 return imageset( 6560 }
11360 'chrome://' + type + '/size/' + size + '@scalefactorx/' +
11361 // Note: Literal 'iconurl' must match |kIconURLParameter| in
11362 // components/favicon_base/favicon_url_parser.cc.
11363 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
11364 }
11365
11366 return { 6561 return {
11367 getSupportedScaleFactors: getSupportedScaleFactors, 6562 getSupportedScaleFactors: getSupportedScaleFactors,
11368 getProfileAvatarIcon: getProfileAvatarIcon, 6563 getProfileAvatarIcon: getProfileAvatarIcon,
11369 getFaviconImageSet: getFaviconImageSet, 6564 getFaviconImageSet: getFaviconImageSet
11370 }; 6565 };
11371 }); 6566 });
6567
11372 // Copyright 2016 The Chromium Authors. All rights reserved. 6568 // Copyright 2016 The Chromium Authors. All rights reserved.
11373 // Use of this source code is governed by a BSD-style license that can be 6569 // Use of this source code is governed by a BSD-style license that can be
11374 // found in the LICENSE file. 6570 // found in the LICENSE file.
11375
11376 /**
11377 * @fileoverview Defines a singleton object, md_history.BrowserService, which
11378 * provides access to chrome.send APIs.
11379 */
11380
11381 cr.define('md_history', function() { 6571 cr.define('md_history', function() {
11382 /** @constructor */
11383 function BrowserService() { 6572 function BrowserService() {
11384 /** @private {Array<!HistoryEntry>} */
11385 this.pendingDeleteItems_ = null; 6573 this.pendingDeleteItems_ = null;
11386 /** @private {PromiseResolver} */
11387 this.pendingDeletePromise_ = null; 6574 this.pendingDeletePromise_ = null;
11388 } 6575 }
11389
11390 BrowserService.prototype = { 6576 BrowserService.prototype = {
11391 /**
11392 * @param {!Array<!HistoryEntry>} items
11393 * @return {Promise<!Array<!HistoryEntry>>}
11394 */
11395 deleteItems: function(items) { 6577 deleteItems: function(items) {
11396 if (this.pendingDeleteItems_ != null) { 6578 if (this.pendingDeleteItems_ != null) {
11397 // There's already a deletion in progress, reject immediately. 6579 return new Promise(function(resolve, reject) {
11398 return new Promise(function(resolve, reject) { reject(items); }); 6580 reject(items);
6581 });
11399 } 6582 }
11400
11401 var removalList = items.map(function(item) { 6583 var removalList = items.map(function(item) {
11402 return { 6584 return {
11403 url: item.url, 6585 url: item.url,
11404 timestamps: item.allTimestamps 6586 timestamps: item.allTimestamps
11405 }; 6587 };
11406 }); 6588 });
11407
11408 this.pendingDeleteItems_ = items; 6589 this.pendingDeleteItems_ = items;
11409 this.pendingDeletePromise_ = new PromiseResolver(); 6590 this.pendingDeletePromise_ = new PromiseResolver();
11410
11411 chrome.send('removeVisits', removalList); 6591 chrome.send('removeVisits', removalList);
11412
11413 return this.pendingDeletePromise_.promise; 6592 return this.pendingDeletePromise_.promise;
11414 }, 6593 },
11415
11416 /**
11417 * @param {!string} url
11418 */
11419 removeBookmark: function(url) { 6594 removeBookmark: function(url) {
11420 chrome.send('removeBookmark', [url]); 6595 chrome.send('removeBookmark', [ url ]);
11421 }, 6596 },
11422
11423 /**
11424 * @param {string} sessionTag
11425 */
11426 openForeignSessionAllTabs: function(sessionTag) { 6597 openForeignSessionAllTabs: function(sessionTag) {
11427 chrome.send('openForeignSession', [sessionTag]); 6598 chrome.send('openForeignSession', [ sessionTag ]);
11428 }, 6599 },
11429
11430 /**
11431 * @param {string} sessionTag
11432 * @param {number} windowId
11433 * @param {number} tabId
11434 * @param {MouseEvent} e
11435 */
11436 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { 6600 openForeignSessionTab: function(sessionTag, windowId, tabId, e) {
11437 chrome.send('openForeignSession', [ 6601 chrome.send('openForeignSession', [ sessionTag, String(windowId), String(t abId), e.button || 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey ]);
11438 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, 6602 },
11439 e.ctrlKey, e.metaKey, e.shiftKey
11440 ]);
11441 },
11442
11443 /**
11444 * @param {string} sessionTag
11445 */
11446 deleteForeignSession: function(sessionTag) { 6603 deleteForeignSession: function(sessionTag) {
11447 chrome.send('deleteForeignSession', [sessionTag]); 6604 chrome.send('deleteForeignSession', [ sessionTag ]);
11448 }, 6605 },
11449
11450 openClearBrowsingData: function() { 6606 openClearBrowsingData: function() {
11451 chrome.send('clearBrowsingData'); 6607 chrome.send('clearBrowsingData');
11452 }, 6608 },
11453
11454 /**
11455 * Record an action in UMA.
11456 * @param {string} actionDesc The name of the action to be logged.
11457 */
11458 recordAction: function(actionDesc) { 6609 recordAction: function(actionDesc) {
11459 chrome.send('metricsHandler:recordAction', [actionDesc]); 6610 chrome.send('metricsHandler:recordAction', [ actionDesc ]);
11460 }, 6611 },
11461
11462 /**
11463 * @param {boolean} successful
11464 * @private
11465 */
11466 resolveDelete_: function(successful) { 6612 resolveDelete_: function(successful) {
11467 if (this.pendingDeleteItems_ == null || 6613 if (this.pendingDeleteItems_ == null || this.pendingDeletePromise_ == null ) {
11468 this.pendingDeletePromise_ == null) {
11469 return; 6614 return;
11470 } 6615 }
11471 6616 if (successful) this.pendingDeletePromise_.resolve(this.pendingDeleteItems _); else this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
11472 if (successful)
11473 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_);
11474 else
11475 this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
11476
11477 this.pendingDeleteItems_ = null; 6617 this.pendingDeleteItems_ = null;
11478 this.pendingDeletePromise_ = null; 6618 this.pendingDeletePromise_ = null;
11479 }, 6619 }
11480 }; 6620 };
11481
11482 cr.addSingletonGetter(BrowserService); 6621 cr.addSingletonGetter(BrowserService);
11483 6622 return {
11484 return {BrowserService: BrowserService}; 6623 BrowserService: BrowserService
11485 }); 6624 };
11486 6625 });
11487 /** 6626
11488 * Called by the history backend when deletion was succesful.
11489 */
11490 function deleteComplete() { 6627 function deleteComplete() {
11491 md_history.BrowserService.getInstance().resolveDelete_(true); 6628 md_history.BrowserService.getInstance().resolveDelete_(true);
11492 } 6629 }
11493 6630
11494 /**
11495 * Called by the history backend when the deletion failed.
11496 */
11497 function deleteFailed() { 6631 function deleteFailed() {
11498 md_history.BrowserService.getInstance().resolveDelete_(false); 6632 md_history.BrowserService.getInstance().resolveDelete_(false);
11499 }; 6633 }
6634
11500 // Copyright 2016 The Chromium Authors. All rights reserved. 6635 // Copyright 2016 The Chromium Authors. All rights reserved.
11501 // Use of this source code is governed by a BSD-style license that can be 6636 // Use of this source code is governed by a BSD-style license that can be
11502 // found in the LICENSE file. 6637 // found in the LICENSE file.
11503
11504 Polymer({ 6638 Polymer({
11505 is: 'history-searched-label', 6639 is: 'history-searched-label',
11506
11507 properties: { 6640 properties: {
11508 // The text to show in this label.
11509 title: String, 6641 title: String,
11510 6642 searchTerm: String
11511 // The search term to bold within the title. 6643 },
11512 searchTerm: String, 6644 observers: [ 'setSearchedTextToBold_(title, searchTerm)' ],
11513 },
11514
11515 observers: ['setSearchedTextToBold_(title, searchTerm)'],
11516
11517 /**
11518 * Updates the page title. If a search term is specified, highlights any
11519 * occurrences of the search term in bold.
11520 * @private
11521 */
11522 setSearchedTextToBold_: function() { 6645 setSearchedTextToBold_: function() {
11523 var i = 0; 6646 var i = 0;
11524 var titleElem = this.$.container; 6647 var titleElem = this.$.container;
11525 var titleText = this.title; 6648 var titleText = this.title;
11526
11527 if (this.searchTerm == '' || this.searchTerm == null) { 6649 if (this.searchTerm == '' || this.searchTerm == null) {
11528 titleElem.textContent = titleText; 6650 titleElem.textContent = titleText;
11529 return; 6651 return;
11530 } 6652 }
11531
11532 var re = new RegExp(quoteString(this.searchTerm), 'gim'); 6653 var re = new RegExp(quoteString(this.searchTerm), 'gim');
11533 var match; 6654 var match;
11534 titleElem.textContent = ''; 6655 titleElem.textContent = '';
11535 while (match = re.exec(titleText)) { 6656 while (match = re.exec(titleText)) {
11536 if (match.index > i) 6657 if (match.index > i) titleElem.appendChild(document.createTextNode(titleTe xt.slice(i, match.index)));
11537 titleElem.appendChild(document.createTextNode(
11538 titleText.slice(i, match.index)));
11539 i = re.lastIndex; 6658 i = re.lastIndex;
11540 // Mark the highlighted text in bold.
11541 var b = document.createElement('b'); 6659 var b = document.createElement('b');
11542 b.textContent = titleText.substring(match.index, i); 6660 b.textContent = titleText.substring(match.index, i);
11543 titleElem.appendChild(b); 6661 titleElem.appendChild(b);
11544 } 6662 }
11545 if (i < titleText.length) 6663 if (i < titleText.length) titleElem.appendChild(document.createTextNode(titl eText.slice(i)));
11546 titleElem.appendChild( 6664 }
11547 document.createTextNode(titleText.slice(i))); 6665 });
11548 }, 6666
11549 });
11550 // Copyright 2015 The Chromium Authors. All rights reserved. 6667 // Copyright 2015 The Chromium Authors. All rights reserved.
11551 // Use of this source code is governed by a BSD-style license that can be 6668 // Use of this source code is governed by a BSD-style license that can be
11552 // found in the LICENSE file. 6669 // found in the LICENSE file.
11553
11554 cr.define('md_history', function() { 6670 cr.define('md_history', function() {
11555 var HistoryItem = Polymer({ 6671 var HistoryItem = Polymer({
11556 is: 'history-item', 6672 is: 'history-item',
11557
11558 properties: { 6673 properties: {
11559 // Underlying HistoryEntry data for this item. Contains read-only fields 6674 item: {
11560 // from the history backend, as well as fields computed by history-list. 6675 type: Object,
11561 item: {type: Object, observer: 'showIcon_'}, 6676 observer: 'showIcon_'
11562 6677 },
11563 // Search term used to obtain this history-item. 6678 searchTerm: {
11564 searchTerm: {type: String}, 6679 type: String
11565 6680 },
11566 selected: {type: Boolean, notify: true}, 6681 selected: {
11567 6682 type: Boolean,
11568 isFirstItem: {type: Boolean, reflectToAttribute: true}, 6683 notify: true
11569 6684 },
11570 isCardStart: {type: Boolean, reflectToAttribute: true}, 6685 isFirstItem: {
11571 6686 type: Boolean,
11572 isCardEnd: {type: Boolean, reflectToAttribute: true}, 6687 reflectToAttribute: true
11573 6688 },
11574 // True if the item is being displayed embedded in another element and 6689 isCardStart: {
11575 // should not manage its own borders or size. 6690 type: Boolean,
11576 embedded: {type: Boolean, reflectToAttribute: true}, 6691 reflectToAttribute: true
11577 6692 },
11578 hasTimeGap: {type: Boolean}, 6693 isCardEnd: {
11579 6694 type: Boolean,
11580 numberOfItems: {type: Number}, 6695 reflectToAttribute: true
11581 6696 },
11582 // The path of this history item inside its parent. 6697 embedded: {
11583 path: String, 6698 type: Boolean,
11584 }, 6699 reflectToAttribute: true
11585 6700 },
11586 /** 6701 hasTimeGap: {
11587 * When a history-item is selected the toolbar is notified and increases 6702 type: Boolean
11588 * or decreases its count of selected items accordingly. 6703 },
11589 * @param {MouseEvent} e 6704 numberOfItems: {
11590 * @private 6705 type: Number
11591 */ 6706 },
6707 path: String
6708 },
11592 onCheckboxSelected_: function(e) { 6709 onCheckboxSelected_: function(e) {
11593 // TODO(calamity): Fire this event whenever |selected| changes.
11594 this.fire('history-checkbox-select', { 6710 this.fire('history-checkbox-select', {
11595 element: this, 6711 element: this,
11596 shiftKey: e.shiftKey, 6712 shiftKey: e.shiftKey
11597 }); 6713 });
11598 e.preventDefault(); 6714 e.preventDefault();
11599 }, 6715 },
11600
11601 /**
11602 * @param {MouseEvent} e
11603 * @private
11604 */
11605 onCheckboxMousedown_: function(e) { 6716 onCheckboxMousedown_: function(e) {
11606 // Prevent shift clicking a checkbox from selecting text. 6717 if (e.shiftKey) e.preventDefault();
11607 if (e.shiftKey) 6718 },
11608 e.preventDefault();
11609 },
11610
11611 /**
11612 * Remove bookmark of current item when bookmark-star is clicked.
11613 * @private
11614 */
11615 onRemoveBookmarkTap_: function() { 6719 onRemoveBookmarkTap_: function() {
11616 if (!this.item.starred) 6720 if (!this.item.starred) return;
11617 return; 6721 if (this.$$('#bookmark-star') == this.root.activeElement) this.$['menu-but ton'].focus();
11618 6722 md_history.BrowserService.getInstance().removeBookmark(this.item.url);
11619 if (this.$$('#bookmark-star') == this.root.activeElement)
11620 this.$['menu-button'].focus();
11621
11622 md_history.BrowserService.getInstance()
11623 .removeBookmark(this.item.url);
11624 this.fire('remove-bookmark-stars', this.item.url); 6723 this.fire('remove-bookmark-stars', this.item.url);
11625 }, 6724 },
11626
11627 /**
11628 * Fires a custom event when the menu button is clicked. Sends the details
11629 * of the history item and where the menu should appear.
11630 */
11631 onMenuButtonTap_: function(e) { 6725 onMenuButtonTap_: function(e) {
11632 this.fire('toggle-menu', { 6726 this.fire('toggle-menu', {
11633 target: Polymer.dom(e).localTarget, 6727 target: Polymer.dom(e).localTarget,
11634 item: this.item, 6728 item: this.item,
11635 path: this.path, 6729 path: this.path
11636 }); 6730 });
11637
11638 // Stops the 'tap' event from closing the menu when it opens.
11639 e.stopPropagation(); 6731 e.stopPropagation();
11640 }, 6732 },
11641
11642 /**
11643 * Set the favicon image, based on the URL of the history item.
11644 * @private
11645 */
11646 showIcon_: function() { 6733 showIcon_: function() {
11647 this.$.icon.style.backgroundImage = 6734 this.$.icon.style.backgroundImage = cr.icon.getFaviconImageSet(this.item.u rl);
11648 cr.icon.getFaviconImageSet(this.item.url); 6735 },
11649 },
11650
11651 selectionNotAllowed_: function() { 6736 selectionNotAllowed_: function() {
11652 return !loadTimeData.getBoolean('allowDeletingHistory'); 6737 return !loadTimeData.getBoolean('allowDeletingHistory');
11653 }, 6738 },
11654
11655 /**
11656 * Generates the title for this history card.
11657 * @param {number} numberOfItems The number of items in the card.
11658 * @param {string} search The search term associated with these results.
11659 * @private
11660 */
11661 cardTitle_: function(numberOfItems, historyDate, search) { 6739 cardTitle_: function(numberOfItems, historyDate, search) {
11662 if (!search) 6740 if (!search) return this.item.dateRelativeDay;
11663 return this.item.dateRelativeDay;
11664
11665 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; 6741 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults';
11666 return loadTimeData.getStringF('foundSearchResults', numberOfItems, 6742 return loadTimeData.getStringF('foundSearchResults', numberOfItems, loadTi meData.getString(resultId), search);
11667 loadTimeData.getString(resultId), search); 6743 },
11668 },
11669
11670 /**
11671 * Crop long item titles to reduce their effect on layout performance. See
11672 * crbug.com/621347.
11673 * @param {string} title
11674 * @return {string}
11675 */
11676 cropItemTitle_: function(title) { 6744 cropItemTitle_: function(title) {
11677 return (title.length > TITLE_MAX_LENGTH) ? 6745 return title.length > TITLE_MAX_LENGTH ? title.substr(0, TITLE_MAX_LENGTH) : title;
11678 title.substr(0, TITLE_MAX_LENGTH) :
11679 title;
11680 } 6746 }
11681 }); 6747 });
11682
11683 /**
11684 * Check whether the time difference between the given history item and the
11685 * next one is large enough for a spacer to be required.
11686 * @param {Array<HistoryEntry>} visits
11687 * @param {number} currentIndex
11688 * @param {string} searchedTerm
11689 * @return {boolean} Whether or not time gap separator is required.
11690 * @private
11691 */
11692 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { 6748 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) {
11693 if (currentIndex >= visits.length - 1 || visits.length == 0) 6749 if (currentIndex >= visits.length - 1 || visits.length == 0) return false;
11694 return false;
11695
11696 var currentItem = visits[currentIndex]; 6750 var currentItem = visits[currentIndex];
11697 var nextItem = visits[currentIndex + 1]; 6751 var nextItem = visits[currentIndex + 1];
11698 6752 if (searchedTerm) return currentItem.dateShort != nextItem.dateShort;
11699 if (searchedTerm) 6753 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && currentItem.d ateRelativeDay == nextItem.dateRelativeDay;
11700 return currentItem.dateShort != nextItem.dateShort;
11701
11702 return currentItem.time - nextItem.time > BROWSING_GAP_TIME &&
11703 currentItem.dateRelativeDay == nextItem.dateRelativeDay;
11704 }; 6754 };
11705 6755 return {
11706 return { HistoryItem: HistoryItem }; 6756 HistoryItem: HistoryItem
11707 }); 6757 };
6758 });
6759
11708 // Copyright 2016 The Chromium Authors. All rights reserved. 6760 // Copyright 2016 The Chromium Authors. All rights reserved.
11709 // Use of this source code is governed by a BSD-style license that can be 6761 // Use of this source code is governed by a BSD-style license that can be
11710 // found in the LICENSE file. 6762 // found in the LICENSE file.
11711
11712 /**
11713 * @constructor
11714 * @param {string} currentPath
11715 */
11716 var SelectionTreeNode = function(currentPath) { 6763 var SelectionTreeNode = function(currentPath) {
11717 /** @type {string} */
11718 this.currentPath = currentPath; 6764 this.currentPath = currentPath;
11719 /** @type {boolean} */
11720 this.leaf = false; 6765 this.leaf = false;
11721 /** @type {Array<number>} */
11722 this.indexes = []; 6766 this.indexes = [];
11723 /** @type {Array<SelectionTreeNode>} */
11724 this.children = []; 6767 this.children = [];
11725 }; 6768 };
11726 6769
11727 /**
11728 * @param {number} index
11729 * @param {string} path
11730 */
11731 SelectionTreeNode.prototype.addChild = function(index, path) { 6770 SelectionTreeNode.prototype.addChild = function(index, path) {
11732 this.indexes.push(index); 6771 this.indexes.push(index);
11733 this.children[index] = new SelectionTreeNode(path); 6772 this.children[index] = new SelectionTreeNode(path);
11734 }; 6773 };
11735 6774
11736 /** @polymerBehavior */
11737 var HistoryListBehavior = { 6775 var HistoryListBehavior = {
11738 properties: { 6776 properties: {
11739 /**
11740 * Polymer paths to the history items contained in this list.
11741 * @type {!Set<string>} selectedPaths
11742 */
11743 selectedPaths: { 6777 selectedPaths: {
11744 type: Object, 6778 type: Object,
11745 value: /** @return {!Set<string>} */ function() { return new Set(); } 6779 value: function() {
11746 }, 6780 return new Set();
11747 6781 }
11748 lastSelectedPath: String, 6782 },
11749 }, 6783 lastSelectedPath: String
11750 6784 },
11751 listeners: { 6785 listeners: {
11752 'history-checkbox-select': 'itemSelected_', 6786 'history-checkbox-select': 'itemSelected_'
11753 }, 6787 },
11754 6788 hasResults: function(historyDataLength) {
11755 /** 6789 return historyDataLength > 0;
11756 * @param {number} historyDataLength 6790 },
11757 * @return {boolean}
11758 * @private
11759 */
11760 hasResults: function(historyDataLength) { return historyDataLength > 0; },
11761
11762 /**
11763 * @param {string} searchedTerm
11764 * @param {boolean} isLoading
11765 * @return {string}
11766 * @private
11767 */
11768 noResultsMessage: function(searchedTerm, isLoading) { 6791 noResultsMessage: function(searchedTerm, isLoading) {
11769 if (isLoading) 6792 if (isLoading) return '';
11770 return '';
11771
11772 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; 6793 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
11773 return loadTimeData.getString(messageId); 6794 return loadTimeData.getString(messageId);
11774 }, 6795 },
11775
11776 /**
11777 * Deselect each item in |selectedPaths|.
11778 */
11779 unselectAllItems: function() { 6796 unselectAllItems: function() {
11780 this.selectedPaths.forEach(function(path) { 6797 this.selectedPaths.forEach(function(path) {
11781 this.set(path + '.selected', false); 6798 this.set(path + '.selected', false);
11782 }.bind(this)); 6799 }.bind(this));
11783
11784 this.selectedPaths.clear(); 6800 this.selectedPaths.clear();
11785 }, 6801 },
11786
11787 /**
11788 * Performs a request to the backend to delete all selected items. If
11789 * successful, removes them from the view. Does not prompt the user before
11790 * deleting -- see <history-list-container> for a version of this method which
11791 * does prompt.
11792 */
11793 deleteSelected: function() { 6802 deleteSelected: function() {
11794 var toBeRemoved = 6803 var toBeRemoved = Array.from(this.selectedPaths.values()).map(function(path) {
11795 Array.from(this.selectedPaths.values()).map(function(path) { 6804 return this.get(path);
11796 return this.get(path); 6805 }.bind(this));
11797 }.bind(this)); 6806 md_history.BrowserService.getInstance().deleteItems(toBeRemoved).then(functi on() {
11798 6807 this.removeItemsByPath(Array.from(this.selectedPaths));
11799 md_history.BrowserService.getInstance() 6808 this.fire('unselect-all');
11800 .deleteItems(toBeRemoved) 6809 }.bind(this));
11801 .then(function() { 6810 },
11802 this.removeItemsByPath(Array.from(this.selectedPaths));
11803 this.fire('unselect-all');
11804 }.bind(this));
11805 },
11806
11807 /**
11808 * Removes the history items in |paths|. Assumes paths are of a.0.b.0...
11809 * structure.
11810 *
11811 * We want to use notifySplices to update the arrays for performance reasons
11812 * which requires manually batching and sending the notifySplices for each
11813 * level. To do this, we build a tree where each node is an array and then
11814 * depth traverse it to remove items. Each time a node has all children
11815 * deleted, we can also remove the node.
11816 *
11817 * @param {Array<string>} paths
11818 * @private
11819 */
11820 removeItemsByPath: function(paths) { 6811 removeItemsByPath: function(paths) {
11821 if (paths.length == 0) 6812 if (paths.length == 0) return;
11822 return;
11823
11824 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); 6813 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths));
11825 }, 6814 },
11826
11827 /**
11828 * Creates the tree to traverse in order to remove |paths| from this list.
11829 * Assumes paths are of a.0.b.0...
11830 * structure.
11831 *
11832 * @param {Array<string>} paths
11833 * @return {SelectionTreeNode}
11834 * @private
11835 */
11836 buildRemovalTree_: function(paths) { 6815 buildRemovalTree_: function(paths) {
11837 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); 6816 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]);
11838
11839 // Build a tree to each history item specified in |paths|.
11840 paths.forEach(function(path) { 6817 paths.forEach(function(path) {
11841 var components = path.split('.'); 6818 var components = path.split('.');
11842 var node = rootNode; 6819 var node = rootNode;
11843 components.shift(); 6820 components.shift();
11844 while (components.length > 1) { 6821 while (components.length > 1) {
11845 var index = Number(components.shift()); 6822 var index = Number(components.shift());
11846 var arrayName = components.shift(); 6823 var arrayName = components.shift();
11847 6824 if (!node.children[index]) node.addChild(index, [ node.currentPath, inde x, arrayName ].join('.'));
11848 if (!node.children[index])
11849 node.addChild(index, [node.currentPath, index, arrayName].join('.'));
11850
11851 node = node.children[index]; 6825 node = node.children[index];
11852 } 6826 }
11853 node.leaf = true; 6827 node.leaf = true;
11854 node.indexes.push(Number(components.shift())); 6828 node.indexes.push(Number(components.shift()));
11855 }); 6829 });
11856
11857 return rootNode; 6830 return rootNode;
11858 }, 6831 },
11859
11860 /**
11861 * Removes the history items underneath |node| and deletes container arrays as
11862 * they become empty.
11863 * @param {SelectionTreeNode} node
11864 * @return {boolean} Whether this node's array should be deleted.
11865 * @private
11866 */
11867 removeItemsBeneathNode_: function(node) { 6832 removeItemsBeneathNode_: function(node) {
11868 var array = this.get(node.currentPath); 6833 var array = this.get(node.currentPath);
11869 var splices = []; 6834 var splices = [];
11870 6835 node.indexes.sort(function(a, b) {
11871 node.indexes.sort(function(a, b) { return b - a; }); 6836 return b - a;
6837 });
11872 node.indexes.forEach(function(index) { 6838 node.indexes.forEach(function(index) {
11873 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { 6839 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) {
11874 var item = array.splice(index, 1); 6840 var item = array.splice(index, 1);
11875 splices.push({ 6841 splices.push({
11876 index: index, 6842 index: index,
11877 removed: [item], 6843 removed: [ item ],
11878 addedCount: 0, 6844 addedCount: 0,
11879 object: array, 6845 object: array,
11880 type: 'splice' 6846 type: 'splice'
11881 }); 6847 });
11882 } 6848 }
11883 }.bind(this)); 6849 }.bind(this));
11884 6850 if (array.length == 0) return true;
11885 if (array.length == 0)
11886 return true;
11887
11888 // notifySplices gives better performance than individually splicing as it
11889 // batches all of the updates together.
11890 this.notifySplices(node.currentPath, splices); 6851 this.notifySplices(node.currentPath, splices);
11891 return false; 6852 return false;
11892 }, 6853 },
11893
11894 /**
11895 * @param {Event} e
11896 * @private
11897 */
11898 itemSelected_: function(e) { 6854 itemSelected_: function(e) {
11899 var item = e.detail.element; 6855 var item = e.detail.element;
11900 var paths = []; 6856 var paths = [];
11901 var itemPath = item.path; 6857 var itemPath = item.path;
11902
11903 // Handle shift selection. Change the selection state of all items between
11904 // |path| and |lastSelected| to the selection state of |item|.
11905 if (e.detail.shiftKey && this.lastSelectedPath) { 6858 if (e.detail.shiftKey && this.lastSelectedPath) {
11906 var itemPathComponents = itemPath.split('.'); 6859 var itemPathComponents = itemPath.split('.');
11907 var itemIndex = Number(itemPathComponents.pop()); 6860 var itemIndex = Number(itemPathComponents.pop());
11908 var itemArrayPath = itemPathComponents.join('.'); 6861 var itemArrayPath = itemPathComponents.join('.');
11909
11910 var lastItemPathComponents = this.lastSelectedPath.split('.'); 6862 var lastItemPathComponents = this.lastSelectedPath.split('.');
11911 var lastItemIndex = Number(lastItemPathComponents.pop()); 6863 var lastItemIndex = Number(lastItemPathComponents.pop());
11912 if (itemArrayPath == lastItemPathComponents.join('.')) { 6864 if (itemArrayPath == lastItemPathComponents.join('.')) {
11913 for (var i = Math.min(itemIndex, lastItemIndex); 6865 for (var i = Math.min(itemIndex, lastItemIndex); i <= Math.max(itemIndex , lastItemIndex); i++) {
11914 i <= Math.max(itemIndex, lastItemIndex); i++) {
11915 paths.push(itemArrayPath + '.' + i); 6866 paths.push(itemArrayPath + '.' + i);
11916 } 6867 }
11917 } 6868 }
11918 } 6869 }
11919 6870 if (paths.length == 0) paths.push(item.path);
11920 if (paths.length == 0)
11921 paths.push(item.path);
11922
11923 paths.forEach(function(path) { 6871 paths.forEach(function(path) {
11924 this.set(path + '.selected', item.selected); 6872 this.set(path + '.selected', item.selected);
11925
11926 if (item.selected) { 6873 if (item.selected) {
11927 this.selectedPaths.add(path); 6874 this.selectedPaths.add(path);
11928 return; 6875 return;
11929 } 6876 }
11930
11931 this.selectedPaths.delete(path); 6877 this.selectedPaths.delete(path);
11932 }.bind(this)); 6878 }.bind(this));
6879 this.lastSelectedPath = itemPath;
6880 }
6881 };
11933 6882
11934 this.lastSelectedPath = itemPath;
11935 },
11936 };
11937 // Copyright 2016 The Chromium Authors. All rights reserved. 6883 // Copyright 2016 The Chromium Authors. All rights reserved.
11938 // Use of this source code is governed by a BSD-style license that can be 6884 // Use of this source code is governed by a BSD-style license that can be
11939 // found in the LICENSE file. 6885 // found in the LICENSE file.
11940
11941 /**
11942 * @typedef {{domain: string,
11943 * visits: !Array<HistoryEntry>,
11944 * rendered: boolean,
11945 * expanded: boolean}}
11946 */
11947 var HistoryDomain; 6886 var HistoryDomain;
11948 6887
11949 /**
11950 * @typedef {{title: string,
11951 * domains: !Array<HistoryDomain>}}
11952 */
11953 var HistoryGroup; 6888 var HistoryGroup;
11954 6889
11955 Polymer({ 6890 Polymer({
11956 is: 'history-grouped-list', 6891 is: 'history-grouped-list',
11957 6892 behaviors: [ HistoryListBehavior ],
11958 behaviors: [HistoryListBehavior],
11959
11960 properties: { 6893 properties: {
11961 // An array of history entries in reverse chronological order.
11962 historyData: { 6894 historyData: {
11963 type: Array, 6895 type: Array
11964 }, 6896 },
11965
11966 /**
11967 * @type {Array<HistoryGroup>}
11968 */
11969 groupedHistoryData_: { 6897 groupedHistoryData_: {
11970 type: Array, 6898 type: Array
11971 }, 6899 },
11972
11973 searchedTerm: { 6900 searchedTerm: {
11974 type: String, 6901 type: String,
11975 value: '' 6902 value: ''
11976 }, 6903 },
11977
11978 range: { 6904 range: {
11979 type: Number, 6905 type: Number
11980 }, 6906 },
11981
11982 queryStartTime: String, 6907 queryStartTime: String,
11983 queryEndTime: String, 6908 queryEndTime: String
11984 }, 6909 },
11985 6910 observers: [ 'updateGroupedHistoryData_(range, historyData)' ],
11986 observers: [
11987 'updateGroupedHistoryData_(range, historyData)'
11988 ],
11989
11990 /**
11991 * Make a list of domains from visits.
11992 * @param {!Array<!HistoryEntry>} visits
11993 * @return {!Array<!HistoryDomain>}
11994 */
11995 createHistoryDomains_: function(visits) { 6911 createHistoryDomains_: function(visits) {
11996 var domainIndexes = {}; 6912 var domainIndexes = {};
11997 var domains = []; 6913 var domains = [];
11998
11999 // Group the visits into a dictionary and generate a list of domains.
12000 for (var i = 0, visit; visit = visits[i]; i++) { 6914 for (var i = 0, visit; visit = visits[i]; i++) {
12001 var domain = visit.domain; 6915 var domain = visit.domain;
12002 if (domainIndexes[domain] == undefined) { 6916 if (domainIndexes[domain] == undefined) {
12003 domainIndexes[domain] = domains.length; 6917 domainIndexes[domain] = domains.length;
12004 domains.push({ 6918 domains.push({
12005 domain: domain, 6919 domain: domain,
12006 visits: [], 6920 visits: [],
12007 expanded: false, 6921 expanded: false,
12008 rendered: false, 6922 rendered: false
12009 }); 6923 });
12010 } 6924 }
12011 domains[domainIndexes[domain]].visits.push(visit); 6925 domains[domainIndexes[domain]].visits.push(visit);
12012 } 6926 }
12013 var sortByVisits = function(a, b) { 6927 var sortByVisits = function(a, b) {
12014 return b.visits.length - a.visits.length; 6928 return b.visits.length - a.visits.length;
12015 }; 6929 };
12016 domains.sort(sortByVisits); 6930 domains.sort(sortByVisits);
12017
12018 return domains; 6931 return domains;
12019 }, 6932 },
12020
12021 updateGroupedHistoryData_: function() { 6933 updateGroupedHistoryData_: function() {
12022 if (this.historyData.length == 0) { 6934 if (this.historyData.length == 0) {
12023 this.groupedHistoryData_ = []; 6935 this.groupedHistoryData_ = [];
12024 return; 6936 return;
12025 } 6937 }
12026
12027 if (this.range == HistoryRange.WEEK) { 6938 if (this.range == HistoryRange.WEEK) {
12028 // Group each day into a list of results.
12029 var days = []; 6939 var days = [];
12030 var currentDayVisits = [this.historyData[0]]; 6940 var currentDayVisits = [ this.historyData[0] ];
12031
12032 var pushCurrentDay = function() { 6941 var pushCurrentDay = function() {
12033 days.push({ 6942 days.push({
12034 title: this.searchedTerm ? currentDayVisits[0].dateShort : 6943 title: this.searchedTerm ? currentDayVisits[0].dateShort : currentDayV isits[0].dateRelativeDay,
12035 currentDayVisits[0].dateRelativeDay, 6944 domains: this.createHistoryDomains_(currentDayVisits)
12036 domains: this.createHistoryDomains_(currentDayVisits),
12037 }); 6945 });
12038 }.bind(this); 6946 }.bind(this);
12039
12040 var visitsSameDay = function(a, b) { 6947 var visitsSameDay = function(a, b) {
12041 if (this.searchedTerm) 6948 if (this.searchedTerm) return a.dateShort == b.dateShort;
12042 return a.dateShort == b.dateShort;
12043
12044 return a.dateRelativeDay == b.dateRelativeDay; 6949 return a.dateRelativeDay == b.dateRelativeDay;
12045 }.bind(this); 6950 }.bind(this);
12046
12047 for (var i = 1; i < this.historyData.length; i++) { 6951 for (var i = 1; i < this.historyData.length; i++) {
12048 var visit = this.historyData[i]; 6952 var visit = this.historyData[i];
12049 if (!visitsSameDay(visit, currentDayVisits[0])) { 6953 if (!visitsSameDay(visit, currentDayVisits[0])) {
12050 pushCurrentDay(); 6954 pushCurrentDay();
12051 currentDayVisits = []; 6955 currentDayVisits = [];
12052 } 6956 }
12053 currentDayVisits.push(visit); 6957 currentDayVisits.push(visit);
12054 } 6958 }
12055 pushCurrentDay(); 6959 pushCurrentDay();
12056
12057 this.groupedHistoryData_ = days; 6960 this.groupedHistoryData_ = days;
12058 } else if (this.range == HistoryRange.MONTH) { 6961 } else if (this.range == HistoryRange.MONTH) {
12059 // Group each all visits into a single list. 6962 this.groupedHistoryData_ = [ {
12060 this.groupedHistoryData_ = [{
12061 title: this.queryStartTime + ' – ' + this.queryEndTime, 6963 title: this.queryStartTime + ' – ' + this.queryEndTime,
12062 domains: this.createHistoryDomains_(this.historyData) 6964 domains: this.createHistoryDomains_(this.historyData)
12063 }]; 6965 } ];
12064 } 6966 }
12065 }, 6967 },
12066
12067 /**
12068 * @param {{model:Object, currentTarget:IronCollapseElement}} e
12069 */
12070 toggleDomainExpanded_: function(e) { 6968 toggleDomainExpanded_: function(e) {
12071 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); 6969 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse');
12072 e.model.set('domain.rendered', true); 6970 e.model.set('domain.rendered', true);
12073 6971 setTimeout(function() {
12074 // Give the history-items time to render. 6972 collapse.toggle();
12075 setTimeout(function() { collapse.toggle() }, 0); 6973 }, 0);
12076 }, 6974 },
12077
12078 /**
12079 * Check whether the time difference between the given history item and the
12080 * next one is large enough for a spacer to be required.
12081 * @param {number} groupIndex
12082 * @param {number} domainIndex
12083 * @param {number} itemIndex
12084 * @return {boolean} Whether or not time gap separator is required.
12085 * @private
12086 */
12087 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { 6975 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) {
12088 var visits = 6976 var visits = this.groupedHistoryData_[groupIndex].domains[domainIndex].visit s;
12089 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; 6977 return md_history.HistoryItem.needsTimeGap(visits, itemIndex, this.searchedT erm);
12090 6978 },
12091 return md_history.HistoryItem.needsTimeGap(
12092 visits, itemIndex, this.searchedTerm);
12093 },
12094
12095 /**
12096 * @param {number} groupIndex
12097 * @param {number} domainIndex
12098 * @param {number} itemIndex
12099 * @return {string}
12100 * @private
12101 */
12102 pathForItem_: function(groupIndex, domainIndex, itemIndex) { 6979 pathForItem_: function(groupIndex, domainIndex, itemIndex) {
12103 return [ 6980 return [ 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits' , itemIndex ].join('.');
12104 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', 6981 },
12105 itemIndex
12106 ].join('.');
12107 },
12108
12109 /**
12110 * @param {HistoryDomain} domain
12111 * @return {string}
12112 * @private
12113 */
12114 getWebsiteIconStyle_: function(domain) { 6982 getWebsiteIconStyle_: function(domain) {
12115 return 'background-image: ' + 6983 return 'background-image: ' + cr.icon.getFaviconImageSet(domain.visits[0].ur l);
12116 cr.icon.getFaviconImageSet(domain.visits[0].url); 6984 },
12117 },
12118
12119 /**
12120 * @param {boolean} expanded
12121 * @return {string}
12122 * @private
12123 */
12124 getDropdownIcon_: function(expanded) { 6985 getDropdownIcon_: function(expanded) {
12125 return expanded ? 'cr:expand-less' : 'cr:expand-more'; 6986 return expanded ? 'cr:expand-less' : 'cr:expand-more';
12126 }, 6987 }
12127 }); 6988 });
12128 /**
12129 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
12130 * designated scroll target.
12131 *
12132 * Elements that consume this behavior can override the `_scrollHandler`
12133 * method to add logic on the scroll event.
12134 *
12135 * @demo demo/scrolling-region.html Scrolling Region
12136 * @demo demo/document.html Document Element
12137 * @polymerBehavior
12138 */
12139 Polymer.IronScrollTargetBehavior = {
12140 6989
12141 properties: { 6990 Polymer.IronScrollTargetBehavior = {
6991 properties: {
6992 scrollTarget: {
6993 type: HTMLElement,
6994 value: function() {
6995 return this._defaultScrollTarget;
6996 }
6997 }
6998 },
6999 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ],
7000 _scrollTargetChanged: function(scrollTarget, isAttached) {
7001 var eventTarget;
7002 if (this._oldScrollTarget) {
7003 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro llTarget;
7004 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
7005 this._oldScrollTarget = null;
7006 }
7007 if (!isAttached) {
7008 return;
7009 }
7010 if (scrollTarget === 'document') {
7011 this.scrollTarget = this._doc;
7012 } else if (typeof scrollTarget === 'string') {
7013 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer. dom(this.ownerDocument).querySelector('#' + scrollTarget);
7014 } else if (this._isValidScrollTarget()) {
7015 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
7016 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler .bind(this);
7017 this._oldScrollTarget = scrollTarget;
7018 eventTarget.addEventListener('scroll', this._boundScrollHandler);
7019 }
7020 },
7021 _scrollHandler: function scrollHandler() {},
7022 get _defaultScrollTarget() {
7023 return this._doc;
7024 },
7025 get _doc() {
7026 return this.ownerDocument.documentElement;
7027 },
7028 get _scrollTop() {
7029 if (this._isValidScrollTarget()) {
7030 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT arget.scrollTop;
7031 }
7032 return 0;
7033 },
7034 get _scrollLeft() {
7035 if (this._isValidScrollTarget()) {
7036 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT arget.scrollLeft;
7037 }
7038 return 0;
7039 },
7040 set _scrollTop(top) {
7041 if (this.scrollTarget === this._doc) {
7042 window.scrollTo(window.pageXOffset, top);
7043 } else if (this._isValidScrollTarget()) {
7044 this.scrollTarget.scrollTop = top;
7045 }
7046 },
7047 set _scrollLeft(left) {
7048 if (this.scrollTarget === this._doc) {
7049 window.scrollTo(left, window.pageYOffset);
7050 } else if (this._isValidScrollTarget()) {
7051 this.scrollTarget.scrollLeft = left;
7052 }
7053 },
7054 scroll: function(left, top) {
7055 if (this.scrollTarget === this._doc) {
7056 window.scrollTo(left, top);
7057 } else if (this._isValidScrollTarget()) {
7058 this.scrollTarget.scrollLeft = left;
7059 this.scrollTarget.scrollTop = top;
7060 }
7061 },
7062 get _scrollTargetWidth() {
7063 if (this._isValidScrollTarget()) {
7064 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa rget.offsetWidth;
7065 }
7066 return 0;
7067 },
7068 get _scrollTargetHeight() {
7069 if (this._isValidScrollTarget()) {
7070 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT arget.offsetHeight;
7071 }
7072 return 0;
7073 },
7074 _isValidScrollTarget: function() {
7075 return this.scrollTarget instanceof HTMLElement;
7076 }
7077 };
12142 7078
12143 /**
12144 * Specifies the element that will handle the scroll event
12145 * on the behalf of the current element. This is typically a reference to an element,
12146 * but there are a few more posibilities:
12147 *
12148 * ### Elements id
12149 *
12150 *```html
12151 * <div id="scrollable-element" style="overflow: auto;">
12152 * <x-element scroll-target="scrollable-element">
12153 * \x3c!-- Content--\x3e
12154 * </x-element>
12155 * </div>
12156 *```
12157 * In this case, the `scrollTarget` will point to the outer div element.
12158 *
12159 * ### Document scrolling
12160 *
12161 * For document scrolling, you can use the reserved word `document`:
12162 *
12163 *```html
12164 * <x-element scroll-target="document">
12165 * \x3c!-- Content --\x3e
12166 * </x-element>
12167 *```
12168 *
12169 * ### Elements reference
12170 *
12171 *```js
12172 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
12173 *```
12174 *
12175 * @type {HTMLElement}
12176 */
12177 scrollTarget: {
12178 type: HTMLElement,
12179 value: function() {
12180 return this._defaultScrollTarget;
12181 }
12182 }
12183 },
12184
12185 observers: [
12186 '_scrollTargetChanged(scrollTarget, isAttached)'
12187 ],
12188
12189 _scrollTargetChanged: function(scrollTarget, isAttached) {
12190 var eventTarget;
12191
12192 if (this._oldScrollTarget) {
12193 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
12194 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
12195 this._oldScrollTarget = null;
12196 }
12197
12198 if (!isAttached) {
12199 return;
12200 }
12201 // Support element id references
12202 if (scrollTarget === 'document') {
12203
12204 this.scrollTarget = this._doc;
12205
12206 } else if (typeof scrollTarget === 'string') {
12207
12208 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
12209 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
12210
12211 } else if (this._isValidScrollTarget()) {
12212
12213 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
12214 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
12215 this._oldScrollTarget = scrollTarget;
12216
12217 eventTarget.addEventListener('scroll', this._boundScrollHandler);
12218 }
12219 },
12220
12221 /**
12222 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
12223 *
12224 * @protected
12225 */
12226 _scrollHandler: function scrollHandler() {},
12227
12228 /**
12229 * The default scroll target. Consumers of this behavior may want to customi ze
12230 * the default scroll target.
12231 *
12232 * @type {Element}
12233 */
12234 get _defaultScrollTarget() {
12235 return this._doc;
12236 },
12237
12238 /**
12239 * Shortcut for the document element
12240 *
12241 * @type {Element}
12242 */
12243 get _doc() {
12244 return this.ownerDocument.documentElement;
12245 },
12246
12247 /**
12248 * Gets the number of pixels that the content of an element is scrolled upwa rd.
12249 *
12250 * @type {number}
12251 */
12252 get _scrollTop() {
12253 if (this._isValidScrollTarget()) {
12254 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
12255 }
12256 return 0;
12257 },
12258
12259 /**
12260 * Gets the number of pixels that the content of an element is scrolled to t he left.
12261 *
12262 * @type {number}
12263 */
12264 get _scrollLeft() {
12265 if (this._isValidScrollTarget()) {
12266 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
12267 }
12268 return 0;
12269 },
12270
12271 /**
12272 * Sets the number of pixels that the content of an element is scrolled upwa rd.
12273 *
12274 * @type {number}
12275 */
12276 set _scrollTop(top) {
12277 if (this.scrollTarget === this._doc) {
12278 window.scrollTo(window.pageXOffset, top);
12279 } else if (this._isValidScrollTarget()) {
12280 this.scrollTarget.scrollTop = top;
12281 }
12282 },
12283
12284 /**
12285 * Sets the number of pixels that the content of an element is scrolled to t he left.
12286 *
12287 * @type {number}
12288 */
12289 set _scrollLeft(left) {
12290 if (this.scrollTarget === this._doc) {
12291 window.scrollTo(left, window.pageYOffset);
12292 } else if (this._isValidScrollTarget()) {
12293 this.scrollTarget.scrollLeft = left;
12294 }
12295 },
12296
12297 /**
12298 * Scrolls the content to a particular place.
12299 *
12300 * @method scroll
12301 * @param {number} left The left position
12302 * @param {number} top The top position
12303 */
12304 scroll: function(left, top) {
12305 if (this.scrollTarget === this._doc) {
12306 window.scrollTo(left, top);
12307 } else if (this._isValidScrollTarget()) {
12308 this.scrollTarget.scrollLeft = left;
12309 this.scrollTarget.scrollTop = top;
12310 }
12311 },
12312
12313 /**
12314 * Gets the width of the scroll target.
12315 *
12316 * @type {number}
12317 */
12318 get _scrollTargetWidth() {
12319 if (this._isValidScrollTarget()) {
12320 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
12321 }
12322 return 0;
12323 },
12324
12325 /**
12326 * Gets the height of the scroll target.
12327 *
12328 * @type {number}
12329 */
12330 get _scrollTargetHeight() {
12331 if (this._isValidScrollTarget()) {
12332 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
12333 }
12334 return 0;
12335 },
12336
12337 /**
12338 * Returns true if the scroll target is a valid HTMLElement.
12339 *
12340 * @return {boolean}
12341 */
12342 _isValidScrollTarget: function() {
12343 return this.scrollTarget instanceof HTMLElement;
12344 }
12345 };
12346 (function() { 7079 (function() {
12347
12348 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); 7080 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
12349 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; 7081 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
12350 var DEFAULT_PHYSICAL_COUNT = 3; 7082 var DEFAULT_PHYSICAL_COUNT = 3;
12351 var HIDDEN_Y = '-10000px'; 7083 var HIDDEN_Y = '-10000px';
12352 var DEFAULT_GRID_SIZE = 200; 7084 var DEFAULT_GRID_SIZE = 200;
12353 var SECRET_TABINDEX = -100; 7085 var SECRET_TABINDEX = -100;
12354
12355 Polymer({ 7086 Polymer({
12356
12357 is: 'iron-list', 7087 is: 'iron-list',
12358
12359 properties: { 7088 properties: {
12360
12361 /**
12362 * An array containing items determining how many instances of the templat e
12363 * to stamp and that that each template instance should bind to.
12364 */
12365 items: { 7089 items: {
12366 type: Array 7090 type: Array
12367 }, 7091 },
12368
12369 /**
12370 * The max count of physical items the pool can extend to.
12371 */
12372 maxPhysicalCount: { 7092 maxPhysicalCount: {
12373 type: Number, 7093 type: Number,
12374 value: 500 7094 value: 500
12375 }, 7095 },
12376
12377 /**
12378 * The name of the variable to add to the binding scope for the array
12379 * element associated with a given template instance.
12380 */
12381 as: { 7096 as: {
12382 type: String, 7097 type: String,
12383 value: 'item' 7098 value: 'item'
12384 }, 7099 },
12385
12386 /**
12387 * The name of the variable to add to the binding scope with the index
12388 * for the row.
12389 */
12390 indexAs: { 7100 indexAs: {
12391 type: String, 7101 type: String,
12392 value: 'index' 7102 value: 'index'
12393 }, 7103 },
12394
12395 /**
12396 * The name of the variable to add to the binding scope to indicate
12397 * if the row is selected.
12398 */
12399 selectedAs: { 7104 selectedAs: {
12400 type: String, 7105 type: String,
12401 value: 'selected' 7106 value: 'selected'
12402 }, 7107 },
12403
12404 /**
12405 * When true, the list is rendered as a grid. Grid items must have
12406 * fixed width and height set via CSS. e.g.
12407 *
12408 * ```html
12409 * <iron-list grid>
12410 * <template>
12411 * <div style="width: 100px; height: 100px;"> 100x100 </div>
12412 * </template>
12413 * </iron-list>
12414 * ```
12415 */
12416 grid: { 7108 grid: {
12417 type: Boolean, 7109 type: Boolean,
12418 value: false, 7110 value: false,
12419 reflectToAttribute: true 7111 reflectToAttribute: true
12420 }, 7112 },
12421
12422 /**
12423 * When true, tapping a row will select the item, placing its data model
12424 * in the set of selected items retrievable via the selection property.
12425 *
12426 * Note that tapping focusable elements within the list item will not
12427 * result in selection, since they are presumed to have their * own action .
12428 */
12429 selectionEnabled: { 7113 selectionEnabled: {
12430 type: Boolean, 7114 type: Boolean,
12431 value: false 7115 value: false
12432 }, 7116 },
12433
12434 /**
12435 * When `multiSelection` is false, this is the currently selected item, or `null`
12436 * if no item is selected.
12437 */
12438 selectedItem: { 7117 selectedItem: {
12439 type: Object, 7118 type: Object,
12440 notify: true 7119 notify: true
12441 }, 7120 },
12442
12443 /**
12444 * When `multiSelection` is true, this is an array that contains the selec ted items.
12445 */
12446 selectedItems: { 7121 selectedItems: {
12447 type: Object, 7122 type: Object,
12448 notify: true 7123 notify: true
12449 }, 7124 },
12450
12451 /**
12452 * When `true`, multiple items may be selected at once (in this case,
12453 * `selected` is an array of currently selected items). When `false`,
12454 * only one item may be selected at a time.
12455 */
12456 multiSelection: { 7125 multiSelection: {
12457 type: Boolean, 7126 type: Boolean,
12458 value: false 7127 value: false
12459 } 7128 }
12460 }, 7129 },
12461 7130 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)' ],
12462 observers: [ 7131 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ],
12463 '_itemsChanged(items.*)',
12464 '_selectionEnabledChanged(selectionEnabled)',
12465 '_multiSelectionChanged(multiSelection)',
12466 '_setOverflow(scrollTarget)'
12467 ],
12468
12469 behaviors: [
12470 Polymer.Templatizer,
12471 Polymer.IronResizableBehavior,
12472 Polymer.IronA11yKeysBehavior,
12473 Polymer.IronScrollTargetBehavior
12474 ],
12475
12476 keyBindings: { 7132 keyBindings: {
12477 'up': '_didMoveUp', 7133 up: '_didMoveUp',
12478 'down': '_didMoveDown', 7134 down: '_didMoveDown',
12479 'enter': '_didEnter' 7135 enter: '_didEnter'
12480 }, 7136 },
12481 7137 _ratio: .5,
12482 /**
12483 * The ratio of hidden tiles that should remain in the scroll direction.
12484 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
12485 */
12486 _ratio: 0.5,
12487
12488 /**
12489 * The padding-top value for the list.
12490 */
12491 _scrollerPaddingTop: 0, 7138 _scrollerPaddingTop: 0,
12492
12493 /**
12494 * This value is the same as `scrollTop`.
12495 */
12496 _scrollPosition: 0, 7139 _scrollPosition: 0,
12497
12498 /**
12499 * The sum of the heights of all the tiles in the DOM.
12500 */
12501 _physicalSize: 0, 7140 _physicalSize: 0,
12502
12503 /**
12504 * The average `offsetHeight` of the tiles observed till now.
12505 */
12506 _physicalAverage: 0, 7141 _physicalAverage: 0,
12507
12508 /**
12509 * The number of tiles which `offsetHeight` > 0 observed until now.
12510 */
12511 _physicalAverageCount: 0, 7142 _physicalAverageCount: 0,
12512
12513 /**
12514 * The Y position of the item rendered in the `_physicalStart`
12515 * tile relative to the scrolling list.
12516 */
12517 _physicalTop: 0, 7143 _physicalTop: 0,
12518
12519 /**
12520 * The number of items in the list.
12521 */
12522 _virtualCount: 0, 7144 _virtualCount: 0,
12523
12524 /**
12525 * A map between an item key and its physical item index
12526 */
12527 _physicalIndexForKey: null, 7145 _physicalIndexForKey: null,
12528
12529 /**
12530 * The estimated scroll height based on `_physicalAverage`
12531 */
12532 _estScrollHeight: 0, 7146 _estScrollHeight: 0,
12533
12534 /**
12535 * The scroll height of the dom node
12536 */
12537 _scrollHeight: 0, 7147 _scrollHeight: 0,
12538
12539 /**
12540 * The height of the list. This is referred as the viewport in the context o f list.
12541 */
12542 _viewportHeight: 0, 7148 _viewportHeight: 0,
12543
12544 /**
12545 * The width of the list. This is referred as the viewport in the context of list.
12546 */
12547 _viewportWidth: 0, 7149 _viewportWidth: 0,
12548
12549 /**
12550 * An array of DOM nodes that are currently in the tree
12551 * @type {?Array<!TemplatizerNode>}
12552 */
12553 _physicalItems: null, 7150 _physicalItems: null,
12554
12555 /**
12556 * An array of heights for each item in `_physicalItems`
12557 * @type {?Array<number>}
12558 */
12559 _physicalSizes: null, 7151 _physicalSizes: null,
12560
12561 /**
12562 * A cached value for the first visible index.
12563 * See `firstVisibleIndex`
12564 * @type {?number}
12565 */
12566 _firstVisibleIndexVal: null, 7152 _firstVisibleIndexVal: null,
12567
12568 /**
12569 * A cached value for the last visible index.
12570 * See `lastVisibleIndex`
12571 * @type {?number}
12572 */
12573 _lastVisibleIndexVal: null, 7153 _lastVisibleIndexVal: null,
12574
12575 /**
12576 * A Polymer collection for the items.
12577 * @type {?Polymer.Collection}
12578 */
12579 _collection: null, 7154 _collection: null,
12580
12581 /**
12582 * True if the current item list was rendered for the first time
12583 * after attached.
12584 */
12585 _itemsRendered: false, 7155 _itemsRendered: false,
12586
12587 /**
12588 * The page that is currently rendered.
12589 */
12590 _lastPage: null, 7156 _lastPage: null,
12591
12592 /**
12593 * The max number of pages to render. One page is equivalent to the height o f the list.
12594 */
12595 _maxPages: 3, 7157 _maxPages: 3,
12596
12597 /**
12598 * The currently focused physical item.
12599 */
12600 _focusedItem: null, 7158 _focusedItem: null,
12601
12602 /**
12603 * The index of the `_focusedItem`.
12604 */
12605 _focusedIndex: -1, 7159 _focusedIndex: -1,
12606
12607 /**
12608 * The the item that is focused if it is moved offscreen.
12609 * @private {?TemplatizerNode}
12610 */
12611 _offscreenFocusedItem: null, 7160 _offscreenFocusedItem: null,
12612
12613 /**
12614 * The item that backfills the `_offscreenFocusedItem` in the physical items
12615 * list when that item is moved offscreen.
12616 */
12617 _focusBackfillItem: null, 7161 _focusBackfillItem: null,
12618
12619 /**
12620 * The maximum items per row
12621 */
12622 _itemsPerRow: 1, 7162 _itemsPerRow: 1,
12623
12624 /**
12625 * The width of each grid item
12626 */
12627 _itemWidth: 0, 7163 _itemWidth: 0,
12628
12629 /**
12630 * The height of the row in grid layout.
12631 */
12632 _rowHeight: 0, 7164 _rowHeight: 0,
12633
12634 /**
12635 * The bottom of the physical content.
12636 */
12637 get _physicalBottom() { 7165 get _physicalBottom() {
12638 return this._physicalTop + this._physicalSize; 7166 return this._physicalTop + this._physicalSize;
12639 }, 7167 },
12640
12641 /**
12642 * The bottom of the scroll.
12643 */
12644 get _scrollBottom() { 7168 get _scrollBottom() {
12645 return this._scrollPosition + this._viewportHeight; 7169 return this._scrollPosition + this._viewportHeight;
12646 }, 7170 },
12647
12648 /**
12649 * The n-th item rendered in the last physical item.
12650 */
12651 get _virtualEnd() { 7171 get _virtualEnd() {
12652 return this._virtualStart + this._physicalCount - 1; 7172 return this._virtualStart + this._physicalCount - 1;
12653 }, 7173 },
12654
12655 /**
12656 * The height of the physical content that isn't on the screen.
12657 */
12658 get _hiddenContentSize() { 7174 get _hiddenContentSize() {
12659 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize; 7175 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
12660 return size - this._viewportHeight; 7176 return size - this._viewportHeight;
12661 }, 7177 },
12662
12663 /**
12664 * The maximum scroll top value.
12665 */
12666 get _maxScrollTop() { 7178 get _maxScrollTop() {
12667 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop; 7179 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
12668 }, 7180 },
12669
12670 /**
12671 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
12672 */
12673 _minVirtualStart: 0, 7181 _minVirtualStart: 0,
12674
12675 /**
12676 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
12677 */
12678 get _maxVirtualStart() { 7182 get _maxVirtualStart() {
12679 return Math.max(0, this._virtualCount - this._physicalCount); 7183 return Math.max(0, this._virtualCount - this._physicalCount);
12680 }, 7184 },
12681
12682 /**
12683 * The n-th item rendered in the `_physicalStart` tile.
12684 */
12685 _virtualStartVal: 0, 7185 _virtualStartVal: 0,
12686
12687 set _virtualStart(val) { 7186 set _virtualStart(val) {
12688 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); 7187 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
12689 }, 7188 },
12690
12691 get _virtualStart() { 7189 get _virtualStart() {
12692 return this._virtualStartVal || 0; 7190 return this._virtualStartVal || 0;
12693 }, 7191 },
12694
12695 /**
12696 * The k-th tile that is at the top of the scrolling list.
12697 */
12698 _physicalStartVal: 0, 7192 _physicalStartVal: 0,
12699
12700 set _physicalStart(val) { 7193 set _physicalStart(val) {
12701 this._physicalStartVal = val % this._physicalCount; 7194 this._physicalStartVal = val % this._physicalCount;
12702 if (this._physicalStartVal < 0) { 7195 if (this._physicalStartVal < 0) {
12703 this._physicalStartVal = this._physicalCount + this._physicalStartVal; 7196 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
12704 } 7197 }
12705 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 7198 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12706 }, 7199 },
12707
12708 get _physicalStart() { 7200 get _physicalStart() {
12709 return this._physicalStartVal || 0; 7201 return this._physicalStartVal || 0;
12710 }, 7202 },
12711
12712 /**
12713 * The number of tiles in the DOM.
12714 */
12715 _physicalCountVal: 0, 7203 _physicalCountVal: 0,
12716
12717 set _physicalCount(val) { 7204 set _physicalCount(val) {
12718 this._physicalCountVal = val; 7205 this._physicalCountVal = val;
12719 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 7206 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12720 }, 7207 },
12721
12722 get _physicalCount() { 7208 get _physicalCount() {
12723 return this._physicalCountVal; 7209 return this._physicalCountVal;
12724 }, 7210 },
12725
12726 /**
12727 * The k-th tile that is at the bottom of the scrolling list.
12728 */
12729 _physicalEnd: 0, 7211 _physicalEnd: 0,
12730
12731 /**
12732 * An optimal physical size such that we will have enough physical items
12733 * to fill up the viewport and recycle when the user scrolls.
12734 *
12735 * This default value assumes that we will at least have the equivalent
12736 * to a viewport of physical items above and below the user's viewport.
12737 */
12738 get _optPhysicalSize() { 7212 get _optPhysicalSize() {
12739 if (this.grid) { 7213 if (this.grid) {
12740 return this._estRowsInView * this._rowHeight * this._maxPages; 7214 return this._estRowsInView * this._rowHeight * this._maxPages;
12741 } 7215 }
12742 return this._viewportHeight * this._maxPages; 7216 return this._viewportHeight * this._maxPages;
12743 }, 7217 },
12744
12745 get _optPhysicalCount() { 7218 get _optPhysicalCount() {
12746 return this._estRowsInView * this._itemsPerRow * this._maxPages; 7219 return this._estRowsInView * this._itemsPerRow * this._maxPages;
12747 }, 7220 },
12748
12749 /**
12750 * True if the current list is visible.
12751 */
12752 get _isVisible() { 7221 get _isVisible() {
12753 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight); 7222 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
12754 }, 7223 },
12755
12756 /**
12757 * Gets the index of the first visible item in the viewport.
12758 *
12759 * @type {number}
12760 */
12761 get firstVisibleIndex() { 7224 get firstVisibleIndex() {
12762 if (this._firstVisibleIndexVal === null) { 7225 if (this._firstVisibleIndexVal === null) {
12763 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 7226 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
12764 7227 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) {
12765 this._firstVisibleIndexVal = this._iterateItems( 7228 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12766 function(pidx, vidx) { 7229 if (physicalOffset > this._scrollPosition) {
12767 physicalOffset += this._getPhysicalSizeIncrement(pidx); 7230 return this.grid ? vidx - vidx % this._itemsPerRow : vidx;
12768 7231 }
12769 if (physicalOffset > this._scrollPosition) { 7232 if (this.grid && this._virtualCount - 1 === vidx) {
12770 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; 7233 return vidx - vidx % this._itemsPerRow;
12771 } 7234 }
12772 // Handle a partially rendered final row in grid mode 7235 }) || 0;
12773 if (this.grid && this._virtualCount - 1 === vidx) {
12774 return vidx - (vidx % this._itemsPerRow);
12775 }
12776 }) || 0;
12777 } 7236 }
12778 return this._firstVisibleIndexVal; 7237 return this._firstVisibleIndexVal;
12779 }, 7238 },
12780
12781 /**
12782 * Gets the index of the last visible item in the viewport.
12783 *
12784 * @type {number}
12785 */
12786 get lastVisibleIndex() { 7239 get lastVisibleIndex() {
12787 if (this._lastVisibleIndexVal === null) { 7240 if (this._lastVisibleIndexVal === null) {
12788 if (this.grid) { 7241 if (this.grid) {
12789 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 7242 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
12790 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 7243 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
12791 } else { 7244 } else {
12792 var physicalOffset = this._physicalTop; 7245 var physicalOffset = this._physicalTop;
12793 this._iterateItems(function(pidx, vidx) { 7246 this._iterateItems(function(pidx, vidx) {
12794 if (physicalOffset < this._scrollBottom) { 7247 if (physicalOffset < this._scrollBottom) {
12795 this._lastVisibleIndexVal = vidx; 7248 this._lastVisibleIndexVal = vidx;
12796 } else { 7249 } else {
12797 // Break _iterateItems
12798 return true; 7250 return true;
12799 } 7251 }
12800 physicalOffset += this._getPhysicalSizeIncrement(pidx); 7252 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12801 }); 7253 });
12802 } 7254 }
12803 } 7255 }
12804 return this._lastVisibleIndexVal; 7256 return this._lastVisibleIndexVal;
12805 }, 7257 },
12806
12807 get _defaultScrollTarget() { 7258 get _defaultScrollTarget() {
12808 return this; 7259 return this;
12809 }, 7260 },
12810 get _virtualRowCount() { 7261 get _virtualRowCount() {
12811 return Math.ceil(this._virtualCount / this._itemsPerRow); 7262 return Math.ceil(this._virtualCount / this._itemsPerRow);
12812 }, 7263 },
12813
12814 get _estRowsInView() { 7264 get _estRowsInView() {
12815 return Math.ceil(this._viewportHeight / this._rowHeight); 7265 return Math.ceil(this._viewportHeight / this._rowHeight);
12816 }, 7266 },
12817
12818 get _physicalRows() { 7267 get _physicalRows() {
12819 return Math.ceil(this._physicalCount / this._itemsPerRow); 7268 return Math.ceil(this._physicalCount / this._itemsPerRow);
12820 }, 7269 },
12821
12822 ready: function() { 7270 ready: function() {
12823 this.addEventListener('focus', this._didFocus.bind(this), true); 7271 this.addEventListener('focus', this._didFocus.bind(this), true);
12824 }, 7272 },
12825
12826 attached: function() { 7273 attached: function() {
12827 this.updateViewportBoundaries(); 7274 this.updateViewportBoundaries();
12828 this._render(); 7275 this._render();
12829 // `iron-resize` is fired when the list is attached if the event is added
12830 // before attached causing unnecessary work.
12831 this.listen(this, 'iron-resize', '_resizeHandler'); 7276 this.listen(this, 'iron-resize', '_resizeHandler');
12832 }, 7277 },
12833
12834 detached: function() { 7278 detached: function() {
12835 this._itemsRendered = false; 7279 this._itemsRendered = false;
12836 this.unlisten(this, 'iron-resize', '_resizeHandler'); 7280 this.unlisten(this, 'iron-resize', '_resizeHandler');
12837 }, 7281 },
12838
12839 /**
12840 * Set the overflow property if this element has its own scrolling region
12841 */
12842 _setOverflow: function(scrollTarget) { 7282 _setOverflow: function(scrollTarget) {
12843 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; 7283 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
12844 this.style.overflow = scrollTarget === this ? 'auto' : ''; 7284 this.style.overflow = scrollTarget === this ? 'auto' : '';
12845 }, 7285 },
12846
12847 /**
12848 * Invoke this method if you dynamically update the viewport's
12849 * size or CSS padding.
12850 *
12851 * @method updateViewportBoundaries
12852 */
12853 updateViewportBoundaries: function() { 7286 updateViewportBoundaries: function() {
12854 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : 7287 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo w.getComputedStyle(this)['padding-top'], 10);
12855 parseInt(window.getComputedStyle(this)['padding-top'], 10);
12856
12857 this._viewportHeight = this._scrollTargetHeight; 7288 this._viewportHeight = this._scrollTargetHeight;
12858 if (this.grid) { 7289 if (this.grid) {
12859 this._updateGridMetrics(); 7290 this._updateGridMetrics();
12860 } 7291 }
12861 }, 7292 },
12862
12863 /**
12864 * Update the models, the position of the
12865 * items in the viewport and recycle tiles as needed.
12866 */
12867 _scrollHandler: function() { 7293 _scrollHandler: function() {
12868 // clamp the `scrollTop` value
12869 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; 7294 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
12870 var delta = scrollTop - this._scrollPosition; 7295 var delta = scrollTop - this._scrollPosition;
12871 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; 7296 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
12872 var ratio = this._ratio; 7297 var ratio = this._ratio;
12873 var recycledTiles = 0; 7298 var recycledTiles = 0;
12874 var hiddenContentSize = this._hiddenContentSize; 7299 var hiddenContentSize = this._hiddenContentSize;
12875 var currentRatio = ratio; 7300 var currentRatio = ratio;
12876 var movingUp = []; 7301 var movingUp = [];
12877
12878 // track the last `scrollTop`
12879 this._scrollPosition = scrollTop; 7302 this._scrollPosition = scrollTop;
12880
12881 // clear cached visible indexes
12882 this._firstVisibleIndexVal = null; 7303 this._firstVisibleIndexVal = null;
12883 this._lastVisibleIndexVal = null; 7304 this._lastVisibleIndexVal = null;
12884
12885 scrollBottom = this._scrollBottom; 7305 scrollBottom = this._scrollBottom;
12886 physicalBottom = this._physicalBottom; 7306 physicalBottom = this._physicalBottom;
12887
12888 // random access
12889 if (Math.abs(delta) > this._physicalSize) { 7307 if (Math.abs(delta) > this._physicalSize) {
12890 this._physicalTop += delta; 7308 this._physicalTop += delta;
12891 recycledTiles = Math.round(delta / this._physicalAverage); 7309 recycledTiles = Math.round(delta / this._physicalAverage);
12892 } 7310 } else if (delta < 0) {
12893 // scroll up
12894 else if (delta < 0) {
12895 var topSpace = scrollTop - this._physicalTop; 7311 var topSpace = scrollTop - this._physicalTop;
12896 var virtualStart = this._virtualStart; 7312 var virtualStart = this._virtualStart;
12897
12898 recycledTileSet = []; 7313 recycledTileSet = [];
12899
12900 kth = this._physicalEnd; 7314 kth = this._physicalEnd;
12901 currentRatio = topSpace / hiddenContentSize; 7315 currentRatio = topSpace / hiddenContentSize;
12902 7316 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen t(kth) > scrollBottom) {
12903 // move tiles from bottom to top
12904 while (
12905 // approximate `currentRatio` to `ratio`
12906 currentRatio < ratio &&
12907 // recycle less physical items than the total
12908 recycledTiles < this._physicalCount &&
12909 // ensure that these recycled tiles are needed
12910 virtualStart - recycledTiles > 0 &&
12911 // ensure that the tile is not visible
12912 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
12913 ) {
12914
12915 tileHeight = this._getPhysicalSizeIncrement(kth); 7317 tileHeight = this._getPhysicalSizeIncrement(kth);
12916 currentRatio += tileHeight / hiddenContentSize; 7318 currentRatio += tileHeight / hiddenContentSize;
12917 physicalBottom -= tileHeight; 7319 physicalBottom -= tileHeight;
12918 recycledTileSet.push(kth); 7320 recycledTileSet.push(kth);
12919 recycledTiles++; 7321 recycledTiles++;
12920 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; 7322 kth = kth === 0 ? this._physicalCount - 1 : kth - 1;
12921 } 7323 }
12922
12923 movingUp = recycledTileSet; 7324 movingUp = recycledTileSet;
12924 recycledTiles = -recycledTiles; 7325 recycledTiles = -recycledTiles;
12925 } 7326 } else if (delta > 0) {
12926 // scroll down
12927 else if (delta > 0) {
12928 var bottomSpace = physicalBottom - scrollBottom; 7327 var bottomSpace = physicalBottom - scrollBottom;
12929 var virtualEnd = this._virtualEnd; 7328 var virtualEnd = this._virtualEnd;
12930 var lastVirtualItemIndex = this._virtualCount-1; 7329 var lastVirtualItemIndex = this._virtualCount - 1;
12931
12932 recycledTileSet = []; 7330 recycledTileSet = [];
12933
12934 kth = this._physicalStart; 7331 kth = this._physicalStart;
12935 currentRatio = bottomSpace / hiddenContentSize; 7332 currentRatio = bottomSpace / hiddenContentSize;
12936 7333 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get PhysicalSizeIncrement(kth) < scrollTop) {
12937 // move tiles from top to bottom
12938 while (
12939 // approximate `currentRatio` to `ratio`
12940 currentRatio < ratio &&
12941 // recycle less physical items than the total
12942 recycledTiles < this._physicalCount &&
12943 // ensure that these recycled tiles are needed
12944 virtualEnd + recycledTiles < lastVirtualItemIndex &&
12945 // ensure that the tile is not visible
12946 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
12947 ) {
12948
12949 tileHeight = this._getPhysicalSizeIncrement(kth); 7334 tileHeight = this._getPhysicalSizeIncrement(kth);
12950 currentRatio += tileHeight / hiddenContentSize; 7335 currentRatio += tileHeight / hiddenContentSize;
12951
12952 this._physicalTop += tileHeight; 7336 this._physicalTop += tileHeight;
12953 recycledTileSet.push(kth); 7337 recycledTileSet.push(kth);
12954 recycledTiles++; 7338 recycledTiles++;
12955 kth = (kth + 1) % this._physicalCount; 7339 kth = (kth + 1) % this._physicalCount;
12956 } 7340 }
12957 } 7341 }
12958
12959 if (recycledTiles === 0) { 7342 if (recycledTiles === 0) {
12960 // Try to increase the pool if the list's client height isn't filled up with physical items
12961 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { 7343 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
12962 this._increasePoolIfNeeded(); 7344 this._increasePoolIfNeeded();
12963 } 7345 }
12964 } else { 7346 } else {
12965 this._virtualStart = this._virtualStart + recycledTiles; 7347 this._virtualStart = this._virtualStart + recycledTiles;
12966 this._physicalStart = this._physicalStart + recycledTiles; 7348 this._physicalStart = this._physicalStart + recycledTiles;
12967 this._update(recycledTileSet, movingUp); 7349 this._update(recycledTileSet, movingUp);
12968 } 7350 }
12969 }, 7351 },
12970
12971 /**
12972 * Update the list of items, starting from the `_virtualStart` item.
12973 * @param {!Array<number>=} itemSet
12974 * @param {!Array<number>=} movingUp
12975 */
12976 _update: function(itemSet, movingUp) { 7352 _update: function(itemSet, movingUp) {
12977 // manage focus
12978 this._manageFocus(); 7353 this._manageFocus();
12979 // update models
12980 this._assignModels(itemSet); 7354 this._assignModels(itemSet);
12981 // measure heights
12982 this._updateMetrics(itemSet); 7355 this._updateMetrics(itemSet);
12983 // adjust offset after measuring
12984 if (movingUp) { 7356 if (movingUp) {
12985 while (movingUp.length) { 7357 while (movingUp.length) {
12986 var idx = movingUp.pop(); 7358 var idx = movingUp.pop();
12987 this._physicalTop -= this._getPhysicalSizeIncrement(idx); 7359 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
12988 } 7360 }
12989 } 7361 }
12990 // update the position of the items
12991 this._positionItems(); 7362 this._positionItems();
12992 // set the scroller size
12993 this._updateScrollerSize(); 7363 this._updateScrollerSize();
12994 // increase the pool of physical items
12995 this._increasePoolIfNeeded(); 7364 this._increasePoolIfNeeded();
12996 }, 7365 },
12997
12998 /**
12999 * Creates a pool of DOM elements and attaches them to the local dom.
13000 */
13001 _createPool: function(size) { 7366 _createPool: function(size) {
13002 var physicalItems = new Array(size); 7367 var physicalItems = new Array(size);
13003
13004 this._ensureTemplatized(); 7368 this._ensureTemplatized();
13005
13006 for (var i = 0; i < size; i++) { 7369 for (var i = 0; i < size; i++) {
13007 var inst = this.stamp(null); 7370 var inst = this.stamp(null);
13008 // First element child is item; Safari doesn't support children[0]
13009 // on a doc fragment
13010 physicalItems[i] = inst.root.querySelector('*'); 7371 physicalItems[i] = inst.root.querySelector('*');
13011 Polymer.dom(this).appendChild(inst.root); 7372 Polymer.dom(this).appendChild(inst.root);
13012 } 7373 }
13013 return physicalItems; 7374 return physicalItems;
13014 }, 7375 },
13015
13016 /**
13017 * Increases the pool of physical items only if needed.
13018 *
13019 * @return {boolean} True if the pool was increased.
13020 */
13021 _increasePoolIfNeeded: function() { 7376 _increasePoolIfNeeded: function() {
13022 // Base case 1: the list has no height.
13023 if (this._viewportHeight === 0) { 7377 if (this._viewportHeight === 0) {
13024 return false; 7378 return false;
13025 } 7379 }
13026 // Base case 2: If the physical size is optimal and the list's client heig ht is full
13027 // with physical items, don't increase the pool.
13028 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition; 7380 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
13029 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { 7381 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
13030 return false; 7382 return false;
13031 } 7383 }
13032 // this value should range between [0 <= `currentPage` <= `_maxPages`]
13033 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); 7384 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
13034
13035 if (currentPage === 0) { 7385 if (currentPage === 0) {
13036 // fill the first page 7386 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * .5)));
13037 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
13038 } else if (this._lastPage !== currentPage && isClientHeightFull) { 7387 } else if (this._lastPage !== currentPage && isClientHeightFull) {
13039 // paint the page and defer the next increase
13040 // wait 16ms which is rough enough to get paint cycle.
13041 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16)); 7388 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
13042 } else { 7389 } else {
13043 // fill the rest of the pages
13044 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ; 7390 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
13045 } 7391 }
13046
13047 this._lastPage = currentPage; 7392 this._lastPage = currentPage;
13048
13049 return true; 7393 return true;
13050 }, 7394 },
13051
13052 /**
13053 * Increases the pool size.
13054 */
13055 _increasePool: function(missingItems) { 7395 _increasePool: function(missingItems) {
13056 var nextPhysicalCount = Math.min( 7396 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this. _virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS ICAL_COUNT));
13057 this._physicalCount + missingItems,
13058 this._virtualCount - this._virtualStart,
13059 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
13060 );
13061 var prevPhysicalCount = this._physicalCount; 7397 var prevPhysicalCount = this._physicalCount;
13062 var delta = nextPhysicalCount - prevPhysicalCount; 7398 var delta = nextPhysicalCount - prevPhysicalCount;
13063
13064 if (delta <= 0) { 7399 if (delta <= 0) {
13065 return; 7400 return;
13066 } 7401 }
13067
13068 [].push.apply(this._physicalItems, this._createPool(delta)); 7402 [].push.apply(this._physicalItems, this._createPool(delta));
13069 [].push.apply(this._physicalSizes, new Array(delta)); 7403 [].push.apply(this._physicalSizes, new Array(delta));
13070
13071 this._physicalCount = prevPhysicalCount + delta; 7404 this._physicalCount = prevPhysicalCount + delta;
13072 7405 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this. _focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd ) {
13073 // update the physical start if we need to preserve the model of the focus ed item.
13074 // In this situation, the focused item is currently rendered and its model would
13075 // have changed after increasing the pool if the physical start remained u nchanged.
13076 if (this._physicalStart > this._physicalEnd &&
13077 this._isIndexRendered(this._focusedIndex) &&
13078 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
13079 this._physicalStart = this._physicalStart + delta; 7406 this._physicalStart = this._physicalStart + delta;
13080 } 7407 }
13081 this._update(); 7408 this._update();
13082 }, 7409 },
13083
13084 /**
13085 * Render a new list of items. This method does exactly the same as `update` ,
13086 * but it also ensures that only one `update` cycle is created.
13087 */
13088 _render: function() { 7410 _render: function() {
13089 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; 7411 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
13090
13091 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { 7412 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
13092 this._lastPage = 0; 7413 this._lastPage = 0;
13093 this._update(); 7414 this._update();
13094 this._itemsRendered = true; 7415 this._itemsRendered = true;
13095 } 7416 }
13096 }, 7417 },
13097
13098 /**
13099 * Templetizes the user template.
13100 */
13101 _ensureTemplatized: function() { 7418 _ensureTemplatized: function() {
13102 if (!this.ctor) { 7419 if (!this.ctor) {
13103 // Template instance props that should be excluded from forwarding
13104 var props = {}; 7420 var props = {};
13105 props.__key__ = true; 7421 props.__key__ = true;
13106 props[this.as] = true; 7422 props[this.as] = true;
13107 props[this.indexAs] = true; 7423 props[this.indexAs] = true;
13108 props[this.selectedAs] = true; 7424 props[this.selectedAs] = true;
13109 props.tabIndex = true; 7425 props.tabIndex = true;
13110
13111 this._instanceProps = props; 7426 this._instanceProps = props;
13112 this._userTemplate = Polymer.dom(this).querySelector('template'); 7427 this._userTemplate = Polymer.dom(this).querySelector('template');
13113
13114 if (this._userTemplate) { 7428 if (this._userTemplate) {
13115 this.templatize(this._userTemplate); 7429 this.templatize(this._userTemplate);
13116 } else { 7430 } else {
13117 console.warn('iron-list requires a template to be provided in light-do m'); 7431 console.warn('iron-list requires a template to be provided in light-do m');
13118 } 7432 }
13119 } 7433 }
13120 }, 7434 },
13121
13122 /**
13123 * Implements extension point from Templatizer mixin.
13124 */
13125 _getStampedChildren: function() { 7435 _getStampedChildren: function() {
13126 return this._physicalItems; 7436 return this._physicalItems;
13127 }, 7437 },
13128
13129 /**
13130 * Implements extension point from Templatizer
13131 * Called as a side effect of a template instance path change, responsible
13132 * for notifying items.<key-for-instance>.<path> change up to host.
13133 */
13134 _forwardInstancePath: function(inst, path, value) { 7438 _forwardInstancePath: function(inst, path, value) {
13135 if (path.indexOf(this.as + '.') === 0) { 7439 if (path.indexOf(this.as + '.') === 0) {
13136 this.notifyPath('items.' + inst.__key__ + '.' + 7440 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt h + 1), value);
13137 path.slice(this.as.length + 1), value);
13138 } 7441 }
13139 }, 7442 },
13140
13141 /**
13142 * Implements extension point from Templatizer mixin
13143 * Called as side-effect of a host property change, responsible for
13144 * notifying parent path change on each row.
13145 */
13146 _forwardParentProp: function(prop, value) { 7443 _forwardParentProp: function(prop, value) {
13147 if (this._physicalItems) { 7444 if (this._physicalItems) {
13148 this._physicalItems.forEach(function(item) { 7445 this._physicalItems.forEach(function(item) {
13149 item._templateInstance[prop] = value; 7446 item._templateInstance[prop] = value;
13150 }, this); 7447 }, this);
13151 } 7448 }
13152 }, 7449 },
13153
13154 /**
13155 * Implements extension point from Templatizer
13156 * Called as side-effect of a host path change, responsible for
13157 * notifying parent.<path> path change on each row.
13158 */
13159 _forwardParentPath: function(path, value) { 7450 _forwardParentPath: function(path, value) {
13160 if (this._physicalItems) { 7451 if (this._physicalItems) {
13161 this._physicalItems.forEach(function(item) { 7452 this._physicalItems.forEach(function(item) {
13162 item._templateInstance.notifyPath(path, value, true); 7453 item._templateInstance.notifyPath(path, value, true);
13163 }, this); 7454 }, this);
13164 } 7455 }
13165 }, 7456 },
13166
13167 /**
13168 * Called as a side effect of a host items.<key>.<path> path change,
13169 * responsible for notifying item.<path> changes.
13170 */
13171 _forwardItemPath: function(path, value) { 7457 _forwardItemPath: function(path, value) {
13172 if (!this._physicalIndexForKey) { 7458 if (!this._physicalIndexForKey) {
13173 return; 7459 return;
13174 } 7460 }
13175 var dot = path.indexOf('.'); 7461 var dot = path.indexOf('.');
13176 var key = path.substring(0, dot < 0 ? path.length : dot); 7462 var key = path.substring(0, dot < 0 ? path.length : dot);
13177 var idx = this._physicalIndexForKey[key]; 7463 var idx = this._physicalIndexForKey[key];
13178 var offscreenItem = this._offscreenFocusedItem; 7464 var offscreenItem = this._offscreenFocusedItem;
13179 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? 7465 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? offscreenItem : this._physicalItems[idx];
13180 offscreenItem : this._physicalItems[idx];
13181
13182 if (!el || el._templateInstance.__key__ !== key) { 7466 if (!el || el._templateInstance.__key__ !== key) {
13183 return; 7467 return;
13184 } 7468 }
13185 if (dot >= 0) { 7469 if (dot >= 0) {
13186 path = this.as + '.' + path.substring(dot+1); 7470 path = this.as + '.' + path.substring(dot + 1);
13187 el._templateInstance.notifyPath(path, value, true); 7471 el._templateInstance.notifyPath(path, value, true);
13188 } else { 7472 } else {
13189 // Update selection if needed
13190 var currentItem = el._templateInstance[this.as]; 7473 var currentItem = el._templateInstance[this.as];
13191 if (Array.isArray(this.selectedItems)) { 7474 if (Array.isArray(this.selectedItems)) {
13192 for (var i = 0; i < this.selectedItems.length; i++) { 7475 for (var i = 0; i < this.selectedItems.length; i++) {
13193 if (this.selectedItems[i] === currentItem) { 7476 if (this.selectedItems[i] === currentItem) {
13194 this.set('selectedItems.' + i, value); 7477 this.set('selectedItems.' + i, value);
13195 break; 7478 break;
13196 } 7479 }
13197 } 7480 }
13198 } else if (this.selectedItem === currentItem) { 7481 } else if (this.selectedItem === currentItem) {
13199 this.set('selectedItem', value); 7482 this.set('selectedItem', value);
13200 } 7483 }
13201 el._templateInstance[this.as] = value; 7484 el._templateInstance[this.as] = value;
13202 } 7485 }
13203 }, 7486 },
13204
13205 /**
13206 * Called when the items have changed. That is, ressignments
13207 * to `items`, splices or updates to a single item.
13208 */
13209 _itemsChanged: function(change) { 7487 _itemsChanged: function(change) {
13210 if (change.path === 'items') { 7488 if (change.path === 'items') {
13211 // reset items
13212 this._virtualStart = 0; 7489 this._virtualStart = 0;
13213 this._physicalTop = 0; 7490 this._physicalTop = 0;
13214 this._virtualCount = this.items ? this.items.length : 0; 7491 this._virtualCount = this.items ? this.items.length : 0;
13215 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; 7492 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
13216 this._physicalIndexForKey = {}; 7493 this._physicalIndexForKey = {};
13217 this._firstVisibleIndexVal = null; 7494 this._firstVisibleIndexVal = null;
13218 this._lastVisibleIndexVal = null; 7495 this._lastVisibleIndexVal = null;
13219
13220 this._resetScrollPosition(0); 7496 this._resetScrollPosition(0);
13221 this._removeFocusedItem(); 7497 this._removeFocusedItem();
13222 // create the initial physical items
13223 if (!this._physicalItems) { 7498 if (!this._physicalItems) {
13224 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); 7499 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
13225 this._physicalItems = this._createPool(this._physicalCount); 7500 this._physicalItems = this._createPool(this._physicalCount);
13226 this._physicalSizes = new Array(this._physicalCount); 7501 this._physicalSizes = new Array(this._physicalCount);
13227 } 7502 }
13228
13229 this._physicalStart = 0; 7503 this._physicalStart = 0;
13230
13231 } else if (change.path === 'items.splices') { 7504 } else if (change.path === 'items.splices') {
13232
13233 this._adjustVirtualIndex(change.value.indexSplices); 7505 this._adjustVirtualIndex(change.value.indexSplices);
13234 this._virtualCount = this.items ? this.items.length : 0; 7506 this._virtualCount = this.items ? this.items.length : 0;
13235
13236 } else { 7507 } else {
13237 // update a single item
13238 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); 7508 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
13239 return; 7509 return;
13240 } 7510 }
13241
13242 this._itemsRendered = false; 7511 this._itemsRendered = false;
13243 this._debounceTemplate(this._render); 7512 this._debounceTemplate(this._render);
13244 }, 7513 },
13245
13246 /**
13247 * @param {!Array<!PolymerSplice>} splices
13248 */
13249 _adjustVirtualIndex: function(splices) { 7514 _adjustVirtualIndex: function(splices) {
13250 splices.forEach(function(splice) { 7515 splices.forEach(function(splice) {
13251 // deselect removed items
13252 splice.removed.forEach(this._removeItem, this); 7516 splice.removed.forEach(this._removeItem, this);
13253 // We only need to care about changes happening above the current positi on
13254 if (splice.index < this._virtualStart) { 7517 if (splice.index < this._virtualStart) {
13255 var delta = Math.max( 7518 var delta = Math.max(splice.addedCount - splice.removed.length, splice .index - this._virtualStart);
13256 splice.addedCount - splice.removed.length,
13257 splice.index - this._virtualStart);
13258
13259 this._virtualStart = this._virtualStart + delta; 7519 this._virtualStart = this._virtualStart + delta;
13260
13261 if (this._focusedIndex >= 0) { 7520 if (this._focusedIndex >= 0) {
13262 this._focusedIndex = this._focusedIndex + delta; 7521 this._focusedIndex = this._focusedIndex + delta;
13263 } 7522 }
13264 } 7523 }
13265 }, this); 7524 }, this);
13266 }, 7525 },
13267
13268 _removeItem: function(item) { 7526 _removeItem: function(item) {
13269 this.$.selector.deselect(item); 7527 this.$.selector.deselect(item);
13270 // remove the current focused item
13271 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) { 7528 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
13272 this._removeFocusedItem(); 7529 this._removeFocusedItem();
13273 } 7530 }
13274 }, 7531 },
13275
13276 /**
13277 * Executes a provided function per every physical index in `itemSet`
13278 * `itemSet` default value is equivalent to the entire set of physical index es.
13279 *
13280 * @param {!function(number, number)} fn
13281 * @param {!Array<number>=} itemSet
13282 */
13283 _iterateItems: function(fn, itemSet) { 7532 _iterateItems: function(fn, itemSet) {
13284 var pidx, vidx, rtn, i; 7533 var pidx, vidx, rtn, i;
13285
13286 if (arguments.length === 2 && itemSet) { 7534 if (arguments.length === 2 && itemSet) {
13287 for (i = 0; i < itemSet.length; i++) { 7535 for (i = 0; i < itemSet.length; i++) {
13288 pidx = itemSet[i]; 7536 pidx = itemSet[i];
13289 vidx = this._computeVidx(pidx); 7537 vidx = this._computeVidx(pidx);
13290 if ((rtn = fn.call(this, pidx, vidx)) != null) { 7538 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13291 return rtn; 7539 return rtn;
13292 } 7540 }
13293 } 7541 }
13294 } else { 7542 } else {
13295 pidx = this._physicalStart; 7543 pidx = this._physicalStart;
13296 vidx = this._virtualStart; 7544 vidx = this._virtualStart;
13297 7545 for (;pidx < this._physicalCount; pidx++, vidx++) {
13298 for (; pidx < this._physicalCount; pidx++, vidx++) {
13299 if ((rtn = fn.call(this, pidx, vidx)) != null) { 7546 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13300 return rtn; 7547 return rtn;
13301 } 7548 }
13302 } 7549 }
13303 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { 7550 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
13304 if ((rtn = fn.call(this, pidx, vidx)) != null) { 7551 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13305 return rtn; 7552 return rtn;
13306 } 7553 }
13307 } 7554 }
13308 } 7555 }
13309 }, 7556 },
13310
13311 /**
13312 * Returns the virtual index for a given physical index
13313 *
13314 * @param {number} pidx Physical index
13315 * @return {number}
13316 */
13317 _computeVidx: function(pidx) { 7557 _computeVidx: function(pidx) {
13318 if (pidx >= this._physicalStart) { 7558 if (pidx >= this._physicalStart) {
13319 return this._virtualStart + (pidx - this._physicalStart); 7559 return this._virtualStart + (pidx - this._physicalStart);
13320 } 7560 }
13321 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; 7561 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
13322 }, 7562 },
13323
13324 /**
13325 * Assigns the data models to a given set of items.
13326 * @param {!Array<number>=} itemSet
13327 */
13328 _assignModels: function(itemSet) { 7563 _assignModels: function(itemSet) {
13329 this._iterateItems(function(pidx, vidx) { 7564 this._iterateItems(function(pidx, vidx) {
13330 var el = this._physicalItems[pidx]; 7565 var el = this._physicalItems[pidx];
13331 var inst = el._templateInstance; 7566 var inst = el._templateInstance;
13332 var item = this.items && this.items[vidx]; 7567 var item = this.items && this.items[vidx];
13333
13334 if (item != null) { 7568 if (item != null) {
13335 inst[this.as] = item; 7569 inst[this.as] = item;
13336 inst.__key__ = this._collection.getKey(item); 7570 inst.__key__ = this._collection.getKey(item);
13337 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item); 7571 inst[this.selectedAs] = this.$.selector.isSelected(item);
13338 inst[this.indexAs] = vidx; 7572 inst[this.indexAs] = vidx;
13339 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; 7573 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
13340 this._physicalIndexForKey[inst.__key__] = pidx; 7574 this._physicalIndexForKey[inst.__key__] = pidx;
13341 el.removeAttribute('hidden'); 7575 el.removeAttribute('hidden');
13342 } else { 7576 } else {
13343 inst.__key__ = null; 7577 inst.__key__ = null;
13344 el.setAttribute('hidden', ''); 7578 el.setAttribute('hidden', '');
13345 } 7579 }
13346 }, itemSet); 7580 }, itemSet);
13347 }, 7581 },
13348 7582 _updateMetrics: function(itemSet) {
13349 /**
13350 * Updates the height for a given set of items.
13351 *
13352 * @param {!Array<number>=} itemSet
13353 */
13354 _updateMetrics: function(itemSet) {
13355 // Make sure we distributed all the physical items
13356 // so we can measure them
13357 Polymer.dom.flush(); 7583 Polymer.dom.flush();
13358
13359 var newPhysicalSize = 0; 7584 var newPhysicalSize = 0;
13360 var oldPhysicalSize = 0; 7585 var oldPhysicalSize = 0;
13361 var prevAvgCount = this._physicalAverageCount; 7586 var prevAvgCount = this._physicalAverageCount;
13362 var prevPhysicalAvg = this._physicalAverage; 7587 var prevPhysicalAvg = this._physicalAverage;
13363
13364 this._iterateItems(function(pidx, vidx) { 7588 this._iterateItems(function(pidx, vidx) {
13365
13366 oldPhysicalSize += this._physicalSizes[pidx] || 0; 7589 oldPhysicalSize += this._physicalSizes[pidx] || 0;
13367 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; 7590 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
13368 newPhysicalSize += this._physicalSizes[pidx]; 7591 newPhysicalSize += this._physicalSizes[pidx];
13369 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; 7592 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
13370
13371 }, itemSet); 7593 }, itemSet);
13372
13373 this._viewportHeight = this._scrollTargetHeight; 7594 this._viewportHeight = this._scrollTargetHeight;
13374 if (this.grid) { 7595 if (this.grid) {
13375 this._updateGridMetrics(); 7596 this._updateGridMetrics();
13376 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight; 7597 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
13377 } else { 7598 } else {
13378 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize; 7599 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
13379 } 7600 }
13380
13381 // update the average if we measured something
13382 if (this._physicalAverageCount !== prevAvgCount) { 7601 if (this._physicalAverageCount !== prevAvgCount) {
13383 this._physicalAverage = Math.round( 7602 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new PhysicalSize) / this._physicalAverageCount);
13384 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
13385 this._physicalAverageCount);
13386 } 7603 }
13387 }, 7604 },
13388
13389 _updateGridMetrics: function() { 7605 _updateGridMetrics: function() {
13390 this._viewportWidth = this.$.items.offsetWidth; 7606 this._viewportWidth = this.$.items.offsetWidth;
13391 // Set item width to the value of the _physicalItems offsetWidth
13392 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE; 7607 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
13393 // Set row height to the value of the _physicalItems offsetHeight
13394 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE; 7608 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
13395 // If in grid mode compute how many items with exist in each row
13396 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow; 7609 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
13397 }, 7610 },
13398
13399 /**
13400 * Updates the position of the physical items.
13401 */
13402 _positionItems: function() { 7611 _positionItems: function() {
13403 this._adjustScrollPosition(); 7612 this._adjustScrollPosition();
13404
13405 var y = this._physicalTop; 7613 var y = this._physicalTop;
13406
13407 if (this.grid) { 7614 if (this.grid) {
13408 var totalItemWidth = this._itemsPerRow * this._itemWidth; 7615 var totalItemWidth = this._itemsPerRow * this._itemWidth;
13409 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; 7616 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
13410
13411 this._iterateItems(function(pidx, vidx) { 7617 this._iterateItems(function(pidx, vidx) {
13412
13413 var modulus = vidx % this._itemsPerRow; 7618 var modulus = vidx % this._itemsPerRow;
13414 var x = Math.floor((modulus * this._itemWidth) + rowOffset); 7619 var x = Math.floor(modulus * this._itemWidth + rowOffset);
13415
13416 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); 7620 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
13417
13418 if (this._shouldRenderNextRow(vidx)) { 7621 if (this._shouldRenderNextRow(vidx)) {
13419 y += this._rowHeight; 7622 y += this._rowHeight;
13420 } 7623 }
13421
13422 }); 7624 });
13423 } else { 7625 } else {
13424 this._iterateItems(function(pidx, vidx) { 7626 this._iterateItems(function(pidx, vidx) {
13425
13426 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); 7627 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
13427 y += this._physicalSizes[pidx]; 7628 y += this._physicalSizes[pidx];
13428
13429 }); 7629 });
13430 } 7630 }
13431 }, 7631 },
13432
13433 _getPhysicalSizeIncrement: function(pidx) { 7632 _getPhysicalSizeIncrement: function(pidx) {
13434 if (!this.grid) { 7633 if (!this.grid) {
13435 return this._physicalSizes[pidx]; 7634 return this._physicalSizes[pidx];
13436 } 7635 }
13437 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) { 7636 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
13438 return 0; 7637 return 0;
13439 } 7638 }
13440 return this._rowHeight; 7639 return this._rowHeight;
13441 }, 7640 },
13442
13443 /**
13444 * Returns, based on the current index,
13445 * whether or not the next index will need
13446 * to be rendered on a new row.
13447 *
13448 * @param {number} vidx Virtual index
13449 * @return {boolean}
13450 */
13451 _shouldRenderNextRow: function(vidx) { 7641 _shouldRenderNextRow: function(vidx) {
13452 return vidx % this._itemsPerRow === this._itemsPerRow - 1; 7642 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
13453 }, 7643 },
13454
13455 /**
13456 * Adjusts the scroll position when it was overestimated.
13457 */
13458 _adjustScrollPosition: function() { 7644 _adjustScrollPosition: function() {
13459 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : 7645 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min( this._scrollPosition + this._physicalTop, 0);
13460 Math.min(this._scrollPosition + this._physicalTop, 0);
13461
13462 if (deltaHeight) { 7646 if (deltaHeight) {
13463 this._physicalTop = this._physicalTop - deltaHeight; 7647 this._physicalTop = this._physicalTop - deltaHeight;
13464 // juking scroll position during interial scrolling on iOS is no bueno
13465 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { 7648 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
13466 this._resetScrollPosition(this._scrollTop - deltaHeight); 7649 this._resetScrollPosition(this._scrollTop - deltaHeight);
13467 } 7650 }
13468 } 7651 }
13469 }, 7652 },
13470
13471 /**
13472 * Sets the position of the scroll.
13473 */
13474 _resetScrollPosition: function(pos) { 7653 _resetScrollPosition: function(pos) {
13475 if (this.scrollTarget) { 7654 if (this.scrollTarget) {
13476 this._scrollTop = pos; 7655 this._scrollTop = pos;
13477 this._scrollPosition = this._scrollTop; 7656 this._scrollPosition = this._scrollTop;
13478 } 7657 }
13479 }, 7658 },
13480
13481 /**
13482 * Sets the scroll height, that's the height of the content,
13483 *
13484 * @param {boolean=} forceUpdate If true, updates the height no matter what.
13485 */
13486 _updateScrollerSize: function(forceUpdate) { 7659 _updateScrollerSize: function(forceUpdate) {
13487 if (this.grid) { 7660 if (this.grid) {
13488 this._estScrollHeight = this._virtualRowCount * this._rowHeight; 7661 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
13489 } else { 7662 } else {
13490 this._estScrollHeight = (this._physicalBottom + 7663 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
13491 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
13492 } 7664 }
13493
13494 forceUpdate = forceUpdate || this._scrollHeight === 0; 7665 forceUpdate = forceUpdate || this._scrollHeight === 0;
13495 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; 7666 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
13496 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight; 7667 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
13497
13498 // amortize height adjustment, so it won't trigger repaints very often
13499 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { 7668 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
13500 this.$.items.style.height = this._estScrollHeight + 'px'; 7669 this.$.items.style.height = this._estScrollHeight + 'px';
13501 this._scrollHeight = this._estScrollHeight; 7670 this._scrollHeight = this._estScrollHeight;
13502 } 7671 }
13503 }, 7672 },
13504 7673 scrollToItem: function(item) {
13505 /**
13506 * Scroll to a specific item in the virtual list regardless
13507 * of the physical items in the DOM tree.
13508 *
13509 * @method scrollToItem
13510 * @param {(Object)} item The item to be scrolled to
13511 */
13512 scrollToItem: function(item){
13513 return this.scrollToIndex(this.items.indexOf(item)); 7674 return this.scrollToIndex(this.items.indexOf(item));
13514 }, 7675 },
13515
13516 /**
13517 * Scroll to a specific index in the virtual list regardless
13518 * of the physical items in the DOM tree.
13519 *
13520 * @method scrollToIndex
13521 * @param {number} idx The index of the item
13522 */
13523 scrollToIndex: function(idx) { 7676 scrollToIndex: function(idx) {
13524 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { 7677 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
13525 return; 7678 return;
13526 } 7679 }
13527
13528 Polymer.dom.flush(); 7680 Polymer.dom.flush();
13529 7681 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1);
13530 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
13531 // update the virtual start only when needed
13532 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { 7682 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
13533 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1); 7683 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1;
13534 } 7684 }
13535 // manage focus
13536 this._manageFocus(); 7685 this._manageFocus();
13537 // assign new models
13538 this._assignModels(); 7686 this._assignModels();
13539 // measure the new sizes
13540 this._updateMetrics(); 7687 this._updateMetrics();
13541 7688 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
13542 // estimate new physical offset
13543 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
13544 this._physicalTop = estPhysicalTop; 7689 this._physicalTop = estPhysicalTop;
13545
13546 var currentTopItem = this._physicalStart; 7690 var currentTopItem = this._physicalStart;
13547 var currentVirtualItem = this._virtualStart; 7691 var currentVirtualItem = this._virtualStart;
13548 var targetOffsetTop = 0; 7692 var targetOffsetTop = 0;
13549 var hiddenContentSize = this._hiddenContentSize; 7693 var hiddenContentSize = this._hiddenContentSize;
13550
13551 // scroll to the item as much as we can
13552 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { 7694 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
13553 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem); 7695 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
13554 currentTopItem = (currentTopItem + 1) % this._physicalCount; 7696 currentTopItem = (currentTopItem + 1) % this._physicalCount;
13555 currentVirtualItem++; 7697 currentVirtualItem++;
13556 } 7698 }
13557 // update the scroller size
13558 this._updateScrollerSize(true); 7699 this._updateScrollerSize(true);
13559 // update the position of the items
13560 this._positionItems(); 7700 this._positionItems();
13561 // set the new scroll position
13562 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop); 7701 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
13563 // increase the pool of physical items if needed
13564 this._increasePoolIfNeeded(); 7702 this._increasePoolIfNeeded();
13565 // clear cached visible index
13566 this._firstVisibleIndexVal = null; 7703 this._firstVisibleIndexVal = null;
13567 this._lastVisibleIndexVal = null; 7704 this._lastVisibleIndexVal = null;
13568 }, 7705 },
13569
13570 /**
13571 * Reset the physical average and the average count.
13572 */
13573 _resetAverage: function() { 7706 _resetAverage: function() {
13574 this._physicalAverage = 0; 7707 this._physicalAverage = 0;
13575 this._physicalAverageCount = 0; 7708 this._physicalAverageCount = 0;
13576 }, 7709 },
13577
13578 /**
13579 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
13580 * when the element is resized.
13581 */
13582 _resizeHandler: function() { 7710 _resizeHandler: function() {
13583 // iOS fires the resize event when the address bar slides up
13584 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) { 7711 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
13585 return; 7712 return;
13586 } 7713 }
13587 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
13588 // changing the scroll position from a resize handler would result in
13589 // the scroll position being reset. Waiting 1ms fixes the issue.
13590 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { 7714 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
13591 this.updateViewportBoundaries(); 7715 this.updateViewportBoundaries();
13592 this._render(); 7716 this._render();
13593
13594 if (this._itemsRendered && this._physicalItems && this._isVisible) { 7717 if (this._itemsRendered && this._physicalItems && this._isVisible) {
13595 this._resetAverage(); 7718 this._resetAverage();
13596 this.scrollToIndex(this.firstVisibleIndex); 7719 this.scrollToIndex(this.firstVisibleIndex);
13597 } 7720 }
13598 }.bind(this), 1)); 7721 }.bind(this), 1));
13599 }, 7722 },
13600
13601 _getModelFromItem: function(item) { 7723 _getModelFromItem: function(item) {
13602 var key = this._collection.getKey(item); 7724 var key = this._collection.getKey(item);
13603 var pidx = this._physicalIndexForKey[key]; 7725 var pidx = this._physicalIndexForKey[key];
13604
13605 if (pidx != null) { 7726 if (pidx != null) {
13606 return this._physicalItems[pidx]._templateInstance; 7727 return this._physicalItems[pidx]._templateInstance;
13607 } 7728 }
13608 return null; 7729 return null;
13609 }, 7730 },
13610
13611 /**
13612 * Gets a valid item instance from its index or the object value.
13613 *
13614 * @param {(Object|number)} item The item object or its index
13615 */
13616 _getNormalizedItem: function(item) { 7731 _getNormalizedItem: function(item) {
13617 if (this._collection.getKey(item) === undefined) { 7732 if (this._collection.getKey(item) === undefined) {
13618 if (typeof item === 'number') { 7733 if (typeof item === 'number') {
13619 item = this.items[item]; 7734 item = this.items[item];
13620 if (!item) { 7735 if (!item) {
13621 throw new RangeError('<item> not found'); 7736 throw new RangeError('<item> not found');
13622 } 7737 }
13623 return item; 7738 return item;
13624 } 7739 }
13625 throw new TypeError('<item> should be a valid item'); 7740 throw new TypeError('<item> should be a valid item');
13626 } 7741 }
13627 return item; 7742 return item;
13628 }, 7743 },
13629
13630 /**
13631 * Select the list item at the given index.
13632 *
13633 * @method selectItem
13634 * @param {(Object|number)} item The item object or its index
13635 */
13636 selectItem: function(item) { 7744 selectItem: function(item) {
13637 item = this._getNormalizedItem(item); 7745 item = this._getNormalizedItem(item);
13638 var model = this._getModelFromItem(item); 7746 var model = this._getModelFromItem(item);
13639
13640 if (!this.multiSelection && this.selectedItem) { 7747 if (!this.multiSelection && this.selectedItem) {
13641 this.deselectItem(this.selectedItem); 7748 this.deselectItem(this.selectedItem);
13642 } 7749 }
13643 if (model) { 7750 if (model) {
13644 model[this.selectedAs] = true; 7751 model[this.selectedAs] = true;
13645 } 7752 }
13646 this.$.selector.select(item); 7753 this.$.selector.select(item);
13647 this.updateSizeForItem(item); 7754 this.updateSizeForItem(item);
13648 }, 7755 },
13649
13650 /**
13651 * Deselects the given item list if it is already selected.
13652 *
13653
13654 * @method deselect
13655 * @param {(Object|number)} item The item object or its index
13656 */
13657 deselectItem: function(item) { 7756 deselectItem: function(item) {
13658 item = this._getNormalizedItem(item); 7757 item = this._getNormalizedItem(item);
13659 var model = this._getModelFromItem(item); 7758 var model = this._getModelFromItem(item);
13660
13661 if (model) { 7759 if (model) {
13662 model[this.selectedAs] = false; 7760 model[this.selectedAs] = false;
13663 } 7761 }
13664 this.$.selector.deselect(item); 7762 this.$.selector.deselect(item);
13665 this.updateSizeForItem(item); 7763 this.updateSizeForItem(item);
13666 }, 7764 },
13667
13668 /**
13669 * Select or deselect a given item depending on whether the item
13670 * has already been selected.
13671 *
13672 * @method toggleSelectionForItem
13673 * @param {(Object|number)} item The item object or its index
13674 */
13675 toggleSelectionForItem: function(item) { 7765 toggleSelectionForItem: function(item) {
13676 item = this._getNormalizedItem(item); 7766 item = this._getNormalizedItem(item);
13677 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) { 7767 if (this.$.selector.isSelected(item)) {
13678 this.deselectItem(item); 7768 this.deselectItem(item);
13679 } else { 7769 } else {
13680 this.selectItem(item); 7770 this.selectItem(item);
13681 } 7771 }
13682 }, 7772 },
13683
13684 /**
13685 * Clears the current selection state of the list.
13686 *
13687 * @method clearSelection
13688 */
13689 clearSelection: function() { 7773 clearSelection: function() {
13690 function unselect(item) { 7774 function unselect(item) {
13691 var model = this._getModelFromItem(item); 7775 var model = this._getModelFromItem(item);
13692 if (model) { 7776 if (model) {
13693 model[this.selectedAs] = false; 7777 model[this.selectedAs] = false;
13694 } 7778 }
13695 } 7779 }
13696
13697 if (Array.isArray(this.selectedItems)) { 7780 if (Array.isArray(this.selectedItems)) {
13698 this.selectedItems.forEach(unselect, this); 7781 this.selectedItems.forEach(unselect, this);
13699 } else if (this.selectedItem) { 7782 } else if (this.selectedItem) {
13700 unselect.call(this, this.selectedItem); 7783 unselect.call(this, this.selectedItem);
13701 } 7784 }
13702 7785 this.$.selector.clearSelection();
13703 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
13704 }, 7786 },
13705
13706 /**
13707 * Add an event listener to `tap` if `selectionEnabled` is true,
13708 * it will remove the listener otherwise.
13709 */
13710 _selectionEnabledChanged: function(selectionEnabled) { 7787 _selectionEnabledChanged: function(selectionEnabled) {
13711 var handler = selectionEnabled ? this.listen : this.unlisten; 7788 var handler = selectionEnabled ? this.listen : this.unlisten;
13712 handler.call(this, this, 'tap', '_selectionHandler'); 7789 handler.call(this, this, 'tap', '_selectionHandler');
13713 }, 7790 },
13714
13715 /**
13716 * Select an item from an event object.
13717 */
13718 _selectionHandler: function(e) { 7791 _selectionHandler: function(e) {
13719 var model = this.modelForElement(e.target); 7792 var model = this.modelForElement(e.target);
13720 if (!model) { 7793 if (!model) {
13721 return; 7794 return;
13722 } 7795 }
13723 var modelTabIndex, activeElTabIndex; 7796 var modelTabIndex, activeElTabIndex;
13724 var target = Polymer.dom(e).path[0]; 7797 var target = Polymer.dom(e).path[0];
13725 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement; 7798 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
13726 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])]; 7799 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
13727 // Safari does not focus certain form controls via mouse 7800 if (target.localName === 'input' || target.localName === 'button' || targe t.localName === 'select') {
13728 // https://bugs.webkit.org/show_bug.cgi?id=118043
13729 if (target.localName === 'input' ||
13730 target.localName === 'button' ||
13731 target.localName === 'select') {
13732 return; 7801 return;
13733 } 7802 }
13734 // Set a temporary tabindex
13735 modelTabIndex = model.tabIndex; 7803 modelTabIndex = model.tabIndex;
13736 model.tabIndex = SECRET_TABINDEX; 7804 model.tabIndex = SECRET_TABINDEX;
13737 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; 7805 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
13738 model.tabIndex = modelTabIndex; 7806 model.tabIndex = modelTabIndex;
13739 // Only select the item if the tap wasn't on a focusable child
13740 // or the element bound to `tabIndex`
13741 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) { 7807 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
13742 return; 7808 return;
13743 } 7809 }
13744 this.toggleSelectionForItem(model[this.as]); 7810 this.toggleSelectionForItem(model[this.as]);
13745 }, 7811 },
13746
13747 _multiSelectionChanged: function(multiSelection) { 7812 _multiSelectionChanged: function(multiSelection) {
13748 this.clearSelection(); 7813 this.clearSelection();
13749 this.$.selector.multi = multiSelection; 7814 this.$.selector.multi = multiSelection;
13750 }, 7815 },
13751
13752 /**
13753 * Updates the size of an item.
13754 *
13755 * @method updateSizeForItem
13756 * @param {(Object|number)} item The item object or its index
13757 */
13758 updateSizeForItem: function(item) { 7816 updateSizeForItem: function(item) {
13759 item = this._getNormalizedItem(item); 7817 item = this._getNormalizedItem(item);
13760 var key = this._collection.getKey(item); 7818 var key = this._collection.getKey(item);
13761 var pidx = this._physicalIndexForKey[key]; 7819 var pidx = this._physicalIndexForKey[key];
13762
13763 if (pidx != null) { 7820 if (pidx != null) {
13764 this._updateMetrics([pidx]); 7821 this._updateMetrics([ pidx ]);
13765 this._positionItems(); 7822 this._positionItems();
13766 } 7823 }
13767 }, 7824 },
13768
13769 /**
13770 * Creates a temporary backfill item in the rendered pool of physical items
13771 * to replace the main focused item. The focused item has tabIndex = 0
13772 * and might be currently focused by the user.
13773 *
13774 * This dynamic replacement helps to preserve the focus state.
13775 */
13776 _manageFocus: function() { 7825 _manageFocus: function() {
13777 var fidx = this._focusedIndex; 7826 var fidx = this._focusedIndex;
13778
13779 if (fidx >= 0 && fidx < this._virtualCount) { 7827 if (fidx >= 0 && fidx < this._virtualCount) {
13780 // if it's a valid index, check if that index is rendered
13781 // in a physical item.
13782 if (this._isIndexRendered(fidx)) { 7828 if (this._isIndexRendered(fidx)) {
13783 this._restoreFocusedItem(); 7829 this._restoreFocusedItem();
13784 } else { 7830 } else {
13785 this._createFocusBackfillItem(); 7831 this._createFocusBackfillItem();
13786 } 7832 }
13787 } else if (this._virtualCount > 0 && this._physicalCount > 0) { 7833 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
13788 // otherwise, assign the initial focused index.
13789 this._focusedIndex = this._virtualStart; 7834 this._focusedIndex = this._virtualStart;
13790 this._focusedItem = this._physicalItems[this._physicalStart]; 7835 this._focusedItem = this._physicalItems[this._physicalStart];
13791 } 7836 }
13792 }, 7837 },
13793
13794 _isIndexRendered: function(idx) { 7838 _isIndexRendered: function(idx) {
13795 return idx >= this._virtualStart && idx <= this._virtualEnd; 7839 return idx >= this._virtualStart && idx <= this._virtualEnd;
13796 }, 7840 },
13797
13798 _isIndexVisible: function(idx) { 7841 _isIndexVisible: function(idx) {
13799 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; 7842 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
13800 }, 7843 },
13801
13802 _getPhysicalIndex: function(idx) { 7844 _getPhysicalIndex: function(idx) {
13803 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))]; 7845 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
13804 }, 7846 },
13805
13806 _focusPhysicalItem: function(idx) { 7847 _focusPhysicalItem: function(idx) {
13807 if (idx < 0 || idx >= this._virtualCount) { 7848 if (idx < 0 || idx >= this._virtualCount) {
13808 return; 7849 return;
13809 } 7850 }
13810 this._restoreFocusedItem(); 7851 this._restoreFocusedItem();
13811 // scroll to index to make sure it's rendered
13812 if (!this._isIndexRendered(idx)) { 7852 if (!this._isIndexRendered(idx)) {
13813 this.scrollToIndex(idx); 7853 this.scrollToIndex(idx);
13814 } 7854 }
13815
13816 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; 7855 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
13817 var model = physicalItem._templateInstance; 7856 var model = physicalItem._templateInstance;
13818 var focusable; 7857 var focusable;
13819
13820 // set a secret tab index
13821 model.tabIndex = SECRET_TABINDEX; 7858 model.tabIndex = SECRET_TABINDEX;
13822 // check if focusable element is the physical item
13823 if (physicalItem.tabIndex === SECRET_TABINDEX) { 7859 if (physicalItem.tabIndex === SECRET_TABINDEX) {
13824 focusable = physicalItem; 7860 focusable = physicalItem;
13825 } 7861 }
13826 // search for the element which tabindex is bound to the secret tab index
13827 if (!focusable) { 7862 if (!focusable) {
13828 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]'); 7863 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
13829 } 7864 }
13830 // restore the tab index
13831 model.tabIndex = 0; 7865 model.tabIndex = 0;
13832 // focus the focusable element
13833 this._focusedIndex = idx; 7866 this._focusedIndex = idx;
13834 focusable && focusable.focus(); 7867 focusable && focusable.focus();
13835 }, 7868 },
13836
13837 _removeFocusedItem: function() { 7869 _removeFocusedItem: function() {
13838 if (this._offscreenFocusedItem) { 7870 if (this._offscreenFocusedItem) {
13839 Polymer.dom(this).removeChild(this._offscreenFocusedItem); 7871 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
13840 } 7872 }
13841 this._offscreenFocusedItem = null; 7873 this._offscreenFocusedItem = null;
13842 this._focusBackfillItem = null; 7874 this._focusBackfillItem = null;
13843 this._focusedItem = null; 7875 this._focusedItem = null;
13844 this._focusedIndex = -1; 7876 this._focusedIndex = -1;
13845 }, 7877 },
13846
13847 _createFocusBackfillItem: function() { 7878 _createFocusBackfillItem: function() {
13848 var pidx, fidx = this._focusedIndex; 7879 var pidx, fidx = this._focusedIndex;
13849 if (this._offscreenFocusedItem || fidx < 0) { 7880 if (this._offscreenFocusedItem || fidx < 0) {
13850 return; 7881 return;
13851 } 7882 }
13852 if (!this._focusBackfillItem) { 7883 if (!this._focusBackfillItem) {
13853 // create a physical item, so that it backfills the focused item.
13854 var stampedTemplate = this.stamp(null); 7884 var stampedTemplate = this.stamp(null);
13855 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); 7885 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
13856 Polymer.dom(this).appendChild(stampedTemplate.root); 7886 Polymer.dom(this).appendChild(stampedTemplate.root);
13857 } 7887 }
13858 // get the physical index for the focused index
13859 pidx = this._getPhysicalIndex(fidx); 7888 pidx = this._getPhysicalIndex(fidx);
13860
13861 if (pidx != null) { 7889 if (pidx != null) {
13862 // set the offcreen focused physical item
13863 this._offscreenFocusedItem = this._physicalItems[pidx]; 7890 this._offscreenFocusedItem = this._physicalItems[pidx];
13864 // backfill the focused physical item
13865 this._physicalItems[pidx] = this._focusBackfillItem; 7891 this._physicalItems[pidx] = this._focusBackfillItem;
13866 // hide the focused physical
13867 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); 7892 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
13868 } 7893 }
13869 }, 7894 },
13870
13871 _restoreFocusedItem: function() { 7895 _restoreFocusedItem: function() {
13872 var pidx, fidx = this._focusedIndex; 7896 var pidx, fidx = this._focusedIndex;
13873
13874 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { 7897 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
13875 return; 7898 return;
13876 } 7899 }
13877 // assign models to the focused index
13878 this._assignModels(); 7900 this._assignModels();
13879 // get the new physical index for the focused index
13880 pidx = this._getPhysicalIndex(fidx); 7901 pidx = this._getPhysicalIndex(fidx);
13881
13882 if (pidx != null) { 7902 if (pidx != null) {
13883 // flip the focus backfill
13884 this._focusBackfillItem = this._physicalItems[pidx]; 7903 this._focusBackfillItem = this._physicalItems[pidx];
13885 // restore the focused physical item
13886 this._physicalItems[pidx] = this._offscreenFocusedItem; 7904 this._physicalItems[pidx] = this._offscreenFocusedItem;
13887 // reset the offscreen focused item
13888 this._offscreenFocusedItem = null; 7905 this._offscreenFocusedItem = null;
13889 // hide the physical item that backfills
13890 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); 7906 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
13891 } 7907 }
13892 }, 7908 },
13893
13894 _didFocus: function(e) { 7909 _didFocus: function(e) {
13895 var targetModel = this.modelForElement(e.target); 7910 var targetModel = this.modelForElement(e.target);
13896 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null; 7911 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
13897 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; 7912 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
13898 var fidx = this._focusedIndex; 7913 var fidx = this._focusedIndex;
13899
13900 if (!targetModel || !focusedModel) { 7914 if (!targetModel || !focusedModel) {
13901 return; 7915 return;
13902 } 7916 }
13903 if (focusedModel === targetModel) { 7917 if (focusedModel === targetModel) {
13904 // if the user focused the same item, then bring it into view if it's no t visible
13905 if (!this._isIndexVisible(fidx)) { 7918 if (!this._isIndexVisible(fidx)) {
13906 this.scrollToIndex(fidx); 7919 this.scrollToIndex(fidx);
13907 } 7920 }
13908 } else { 7921 } else {
13909 this._restoreFocusedItem(); 7922 this._restoreFocusedItem();
13910 // restore tabIndex for the currently focused item
13911 focusedModel.tabIndex = -1; 7923 focusedModel.tabIndex = -1;
13912 // set the tabIndex for the next focused item
13913 targetModel.tabIndex = 0; 7924 targetModel.tabIndex = 0;
13914 fidx = targetModel[this.indexAs]; 7925 fidx = targetModel[this.indexAs];
13915 this._focusedIndex = fidx; 7926 this._focusedIndex = fidx;
13916 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; 7927 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
13917
13918 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { 7928 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
13919 this._update(); 7929 this._update();
13920 } 7930 }
13921 } 7931 }
13922 }, 7932 },
13923
13924 _didMoveUp: function() { 7933 _didMoveUp: function() {
13925 this._focusPhysicalItem(this._focusedIndex - 1); 7934 this._focusPhysicalItem(this._focusedIndex - 1);
13926 }, 7935 },
13927
13928 _didMoveDown: function(e) { 7936 _didMoveDown: function(e) {
13929 // disable scroll when pressing the down key
13930 e.detail.keyboardEvent.preventDefault(); 7937 e.detail.keyboardEvent.preventDefault();
13931 this._focusPhysicalItem(this._focusedIndex + 1); 7938 this._focusPhysicalItem(this._focusedIndex + 1);
13932 }, 7939 },
13933
13934 _didEnter: function(e) { 7940 _didEnter: function(e) {
13935 this._focusPhysicalItem(this._focusedIndex); 7941 this._focusPhysicalItem(this._focusedIndex);
13936 this._selectionHandler(e.detail.keyboardEvent); 7942 this._selectionHandler(e.detail.keyboardEvent);
13937 } 7943 }
13938 }); 7944 });
7945 })();
13939 7946
13940 })();
13941 Polymer({ 7947 Polymer({
7948 is: 'iron-scroll-threshold',
7949 properties: {
7950 upperThreshold: {
7951 type: Number,
7952 value: 100
7953 },
7954 lowerThreshold: {
7955 type: Number,
7956 value: 100
7957 },
7958 upperTriggered: {
7959 type: Boolean,
7960 value: false,
7961 notify: true,
7962 readOnly: true
7963 },
7964 lowerTriggered: {
7965 type: Boolean,
7966 value: false,
7967 notify: true,
7968 readOnly: true
7969 },
7970 horizontal: {
7971 type: Boolean,
7972 value: false
7973 }
7974 },
7975 behaviors: [ Polymer.IronScrollTargetBehavior ],
7976 observers: [ '_setOverflow(scrollTarget)', '_initCheck(horizontal, isAttached) ' ],
7977 get _defaultScrollTarget() {
7978 return this;
7979 },
7980 _setOverflow: function(scrollTarget) {
7981 this.style.overflow = scrollTarget === this ? 'auto' : '';
7982 },
7983 _scrollHandler: function() {
7984 var THROTTLE_THRESHOLD = 200;
7985 if (!this.isDebouncerActive('_checkTheshold')) {
7986 this.debounce('_checkTheshold', function() {
7987 this.checkScrollThesholds();
7988 }, THROTTLE_THRESHOLD);
7989 }
7990 },
7991 _initCheck: function(horizontal, isAttached) {
7992 if (isAttached) {
7993 this.debounce('_init', function() {
7994 this.clearTriggers();
7995 this.checkScrollThesholds();
7996 });
7997 }
7998 },
7999 checkScrollThesholds: function() {
8000 if (!this.scrollTarget || this.lowerTriggered && this.upperTriggered) {
8001 return;
8002 }
8003 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTop;
8004 var lowerScrollValue = this.horizontal ? this.scrollTarget.scrollWidth - thi s._scrollTargetWidth - this._scrollLeft : this.scrollTarget.scrollHeight - this. _scrollTargetHeight - this._scrollTop;
8005 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
8006 this._setUpperTriggered(true);
8007 this.fire('upper-threshold');
8008 }
8009 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
8010 this._setLowerTriggered(true);
8011 this.fire('lower-threshold');
8012 }
8013 },
8014 clearTriggers: function() {
8015 this._setUpperTriggered(false);
8016 this._setLowerTriggered(false);
8017 }
8018 });
13942 8019
13943 is: 'iron-scroll-threshold',
13944
13945 properties: {
13946
13947 /**
13948 * Distance from the top (or left, for horizontal) bound of the scroller
13949 * where the "upper trigger" will fire.
13950 */
13951 upperThreshold: {
13952 type: Number,
13953 value: 100
13954 },
13955
13956 /**
13957 * Distance from the bottom (or right, for horizontal) bound of the scroll er
13958 * where the "lower trigger" will fire.
13959 */
13960 lowerThreshold: {
13961 type: Number,
13962 value: 100
13963 },
13964
13965 /**
13966 * Read-only value that tracks the triggered state of the upper threshold.
13967 */
13968 upperTriggered: {
13969 type: Boolean,
13970 value: false,
13971 notify: true,
13972 readOnly: true
13973 },
13974
13975 /**
13976 * Read-only value that tracks the triggered state of the lower threshold.
13977 */
13978 lowerTriggered: {
13979 type: Boolean,
13980 value: false,
13981 notify: true,
13982 readOnly: true
13983 },
13984
13985 /**
13986 * True if the orientation of the scroller is horizontal.
13987 */
13988 horizontal: {
13989 type: Boolean,
13990 value: false
13991 }
13992 },
13993
13994 behaviors: [
13995 Polymer.IronScrollTargetBehavior
13996 ],
13997
13998 observers: [
13999 '_setOverflow(scrollTarget)',
14000 '_initCheck(horizontal, isAttached)'
14001 ],
14002
14003 get _defaultScrollTarget() {
14004 return this;
14005 },
14006
14007 _setOverflow: function(scrollTarget) {
14008 this.style.overflow = scrollTarget === this ? 'auto' : '';
14009 },
14010
14011 _scrollHandler: function() {
14012 // throttle the work on the scroll event
14013 var THROTTLE_THRESHOLD = 200;
14014 if (!this.isDebouncerActive('_checkTheshold')) {
14015 this.debounce('_checkTheshold', function() {
14016 this.checkScrollThesholds();
14017 }, THROTTLE_THRESHOLD);
14018 }
14019 },
14020
14021 _initCheck: function(horizontal, isAttached) {
14022 if (isAttached) {
14023 this.debounce('_init', function() {
14024 this.clearTriggers();
14025 this.checkScrollThesholds();
14026 });
14027 }
14028 },
14029
14030 /**
14031 * Checks the scroll thresholds.
14032 * This method is automatically called by iron-scroll-threshold.
14033 *
14034 * @method checkScrollThesholds
14035 */
14036 checkScrollThesholds: function() {
14037 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
14038 return;
14039 }
14040 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo p;
14041 var lowerScrollValue = this.horizontal ?
14042 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll Left :
14043 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._ scrollTop;
14044
14045 // Detect upper threshold
14046 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
14047 this._setUpperTriggered(true);
14048 this.fire('upper-threshold');
14049 }
14050 // Detect lower threshold
14051 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
14052 this._setLowerTriggered(true);
14053 this.fire('lower-threshold');
14054 }
14055 },
14056
14057 /**
14058 * Clear the upper and lower threshold states.
14059 *
14060 * @method clearTriggers
14061 */
14062 clearTriggers: function() {
14063 this._setUpperTriggered(false);
14064 this._setLowerTriggered(false);
14065 }
14066
14067 /**
14068 * Fires when the lower threshold has been reached.
14069 *
14070 * @event lower-threshold
14071 */
14072
14073 /**
14074 * Fires when the upper threshold has been reached.
14075 *
14076 * @event upper-threshold
14077 */
14078
14079 });
14080 // Copyright 2015 The Chromium Authors. All rights reserved. 8020 // Copyright 2015 The Chromium Authors. All rights reserved.
14081 // Use of this source code is governed by a BSD-style license that can be 8021 // Use of this source code is governed by a BSD-style license that can be
14082 // found in the LICENSE file. 8022 // found in the LICENSE file.
14083
14084 Polymer({ 8023 Polymer({
14085 is: 'history-list', 8024 is: 'history-list',
14086 8025 behaviors: [ HistoryListBehavior ],
14087 behaviors: [HistoryListBehavior],
14088
14089 properties: { 8026 properties: {
14090 // The search term for the current query. Set when the query returns.
14091 searchedTerm: { 8027 searchedTerm: {
14092 type: String, 8028 type: String,
14093 value: '', 8029 value: ''
14094 }, 8030 },
14095
14096 querying: Boolean, 8031 querying: Boolean,
14097
14098 // An array of history entries in reverse chronological order.
14099 historyData_: Array, 8032 historyData_: Array,
14100
14101 resultLoadingDisabled_: { 8033 resultLoadingDisabled_: {
14102 type: Boolean, 8034 type: Boolean,
14103 value: false, 8035 value: false
14104 }, 8036 }
14105 }, 8037 },
14106
14107 listeners: { 8038 listeners: {
14108 'scroll': 'notifyListScroll_', 8039 scroll: 'notifyListScroll_',
14109 'remove-bookmark-stars': 'removeBookmarkStars_', 8040 'remove-bookmark-stars': 'removeBookmarkStars_'
14110 }, 8041 },
14111
14112 /** @override */
14113 attached: function() { 8042 attached: function() {
14114 // It is possible (eg, when middle clicking the reload button) for all other 8043 this.$['infinite-list'].notifyResize();
14115 // resize events to fire before the list is attached and can be measured.
14116 // Adding another resize here ensures it will get sized correctly.
14117 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize();
14118 this.$['infinite-list'].scrollTarget = this; 8044 this.$['infinite-list'].scrollTarget = this;
14119 this.$['scroll-threshold'].scrollTarget = this; 8045 this.$['scroll-threshold'].scrollTarget = this;
14120 }, 8046 },
14121
14122 /**
14123 * Remove bookmark star for history items with matching URLs.
14124 * @param {{detail: !string}} e
14125 * @private
14126 */
14127 removeBookmarkStars_: function(e) { 8047 removeBookmarkStars_: function(e) {
14128 var url = e.detail; 8048 var url = e.detail;
14129 8049 if (this.historyData_ === undefined) return;
14130 if (this.historyData_ === undefined)
14131 return;
14132
14133 for (var i = 0; i < this.historyData_.length; i++) { 8050 for (var i = 0; i < this.historyData_.length; i++) {
14134 if (this.historyData_[i].url == url) 8051 if (this.historyData_[i].url == url) this.set('historyData_.' + i + '.star red', false);
14135 this.set('historyData_.' + i + '.starred', false); 8052 }
14136 } 8053 },
14137 },
14138
14139 /**
14140 * Disables history result loading when there are no more history results.
14141 */
14142 disableResultLoading: function() { 8054 disableResultLoading: function() {
14143 this.resultLoadingDisabled_ = true; 8055 this.resultLoadingDisabled_ = true;
14144 }, 8056 },
14145
14146 /**
14147 * Adds the newly updated history results into historyData_. Adds new fields
14148 * for each result.
14149 * @param {!Array<!HistoryEntry>} historyResults The new history results.
14150 * @param {boolean} incremental Whether the result is from loading more
14151 * history, or a new search/list reload.
14152 */
14153 addNewResults: function(historyResults, incremental) { 8057 addNewResults: function(historyResults, incremental) {
14154 var results = historyResults.slice(); 8058 var results = historyResults.slice();
14155 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) 8059 this.$['scroll-threshold'].clearTriggers();
14156 .clearTriggers();
14157
14158 if (!incremental) { 8060 if (!incremental) {
14159 this.resultLoadingDisabled_ = false; 8061 this.resultLoadingDisabled_ = false;
14160 if (this.historyData_) 8062 if (this.historyData_) this.splice('historyData_', 0, this.historyData_.le ngth);
14161 this.splice('historyData_', 0, this.historyData_.length);
14162 this.fire('unselect-all'); 8063 this.fire('unselect-all');
14163 } 8064 }
14164
14165 if (this.historyData_) { 8065 if (this.historyData_) {
14166 // If we have previously received data, push the new items onto the
14167 // existing array.
14168 results.unshift('historyData_'); 8066 results.unshift('historyData_');
14169 this.push.apply(this, results); 8067 this.push.apply(this, results);
14170 } else { 8068 } else {
14171 // The first time we receive data, use set() to ensure the iron-list is
14172 // initialized correctly.
14173 this.set('historyData_', results); 8069 this.set('historyData_', results);
14174 } 8070 }
14175 }, 8071 },
14176
14177 /**
14178 * Called when the page is scrolled to near the bottom of the list.
14179 * @private
14180 */
14181 loadMoreData_: function() { 8072 loadMoreData_: function() {
14182 if (this.resultLoadingDisabled_ || this.querying) 8073 if (this.resultLoadingDisabled_ || this.querying) return;
14183 return;
14184
14185 this.fire('load-more-history'); 8074 this.fire('load-more-history');
14186 }, 8075 },
14187
14188 /**
14189 * Check whether the time difference between the given history item and the
14190 * next one is large enough for a spacer to be required.
14191 * @param {HistoryEntry} item
14192 * @param {number} index The index of |item| in |historyData_|.
14193 * @param {number} length The length of |historyData_|.
14194 * @return {boolean} Whether or not time gap separator is required.
14195 * @private
14196 */
14197 needsTimeGap_: function(item, index, length) { 8076 needsTimeGap_: function(item, index, length) {
14198 return md_history.HistoryItem.needsTimeGap( 8077 return md_history.HistoryItem.needsTimeGap(this.historyData_, index, this.se archedTerm);
14199 this.historyData_, index, this.searchedTerm); 8078 },
14200 },
14201
14202 /**
14203 * True if the given item is the beginning of a new card.
14204 * @param {HistoryEntry} item
14205 * @param {number} i Index of |item| within |historyData_|.
14206 * @param {number} length
14207 * @return {boolean}
14208 * @private
14209 */
14210 isCardStart_: function(item, i, length) { 8079 isCardStart_: function(item, i, length) {
14211 if (length == 0 || i > length - 1) 8080 if (length == 0 || i > length - 1) return false;
14212 return false; 8081 return i == 0 || this.historyData_[i].dateRelativeDay != this.historyData_[i - 1].dateRelativeDay;
14213 return i == 0 || 8082 },
14214 this.historyData_[i].dateRelativeDay !=
14215 this.historyData_[i - 1].dateRelativeDay;
14216 },
14217
14218 /**
14219 * True if the given item is the end of a card.
14220 * @param {HistoryEntry} item
14221 * @param {number} i Index of |item| within |historyData_|.
14222 * @param {number} length
14223 * @return {boolean}
14224 * @private
14225 */
14226 isCardEnd_: function(item, i, length) { 8083 isCardEnd_: function(item, i, length) {
14227 if (length == 0 || i > length - 1) 8084 if (length == 0 || i > length - 1) return false;
14228 return false; 8085 return i == length - 1 || this.historyData_[i].dateRelativeDay != this.histo ryData_[i + 1].dateRelativeDay;
14229 return i == length - 1 || 8086 },
14230 this.historyData_[i].dateRelativeDay !=
14231 this.historyData_[i + 1].dateRelativeDay;
14232 },
14233
14234 /**
14235 * @param {number} index
14236 * @return {boolean}
14237 * @private
14238 */
14239 isFirstItem_: function(index) { 8087 isFirstItem_: function(index) {
14240 return index == 0; 8088 return index == 0;
14241 }, 8089 },
14242
14243 /**
14244 * @private
14245 */
14246 notifyListScroll_: function() { 8090 notifyListScroll_: function() {
14247 this.fire('history-list-scrolled'); 8091 this.fire('history-list-scrolled');
14248 }, 8092 },
14249
14250 /**
14251 * @param {number} index
14252 * @return {string}
14253 * @private
14254 */
14255 pathForItem_: function(index) { 8093 pathForItem_: function(index) {
14256 return 'historyData_.' + index; 8094 return 'historyData_.' + index;
14257 }, 8095 }
14258 }); 8096 });
8097
14259 // Copyright 2016 The Chromium Authors. All rights reserved. 8098 // Copyright 2016 The Chromium Authors. All rights reserved.
14260 // Use of this source code is governed by a BSD-style license that can be 8099 // Use of this source code is governed by a BSD-style license that can be
14261 // found in the LICENSE file. 8100 // found in the LICENSE file.
14262
14263 /**
14264 * @fileoverview
14265 * history-lazy-render is a simple variant of dom-if designed for lazy rendering
14266 * of elements that are accessed imperatively.
14267 * Usage:
14268 * <template is="history-lazy-render" id="menu">
14269 * <heavy-menu></heavy-menu>
14270 * </template>
14271 *
14272 * this.$.menu.get().then(function(menu) {
14273 * menu.show();
14274 * });
14275 */
14276
14277 Polymer({ 8101 Polymer({
14278 is: 'history-lazy-render', 8102 is: 'history-lazy-render',
14279 extends: 'template', 8103 "extends": 'template',
14280 8104 behaviors: [ Polymer.Templatizer ],
14281 behaviors: [
14282 Polymer.Templatizer
14283 ],
14284
14285 /** @private {Promise<Element>} */
14286 _renderPromise: null, 8105 _renderPromise: null,
14287
14288 /** @private {TemplateInstance} */
14289 _instance: null, 8106 _instance: null,
14290
14291 /**
14292 * Stamp the template into the DOM tree asynchronously
14293 * @return {Promise<Element>} Promise which resolves when the template has
14294 * been stamped.
14295 */
14296 get: function() { 8107 get: function() {
14297 if (!this._renderPromise) { 8108 if (!this._renderPromise) {
14298 this._renderPromise = new Promise(function(resolve) { 8109 this._renderPromise = new Promise(function(resolve) {
14299 this._debounceTemplate(function() { 8110 this._debounceTemplate(function() {
14300 this._render(); 8111 this._render();
14301 this._renderPromise = null; 8112 this._renderPromise = null;
14302 resolve(this.getIfExists()); 8113 resolve(this.getIfExists());
14303 }.bind(this)); 8114 }.bind(this));
14304 }.bind(this)); 8115 }.bind(this));
14305 } 8116 }
14306 return this._renderPromise; 8117 return this._renderPromise;
14307 }, 8118 },
14308
14309 /**
14310 * @return {?Element} The element contained in the template, if it has
14311 * already been stamped.
14312 */
14313 getIfExists: function() { 8119 getIfExists: function() {
14314 if (this._instance) { 8120 if (this._instance) {
14315 var children = this._instance._children; 8121 var children = this._instance._children;
14316
14317 for (var i = 0; i < children.length; i++) { 8122 for (var i = 0; i < children.length; i++) {
14318 if (children[i].nodeType == Node.ELEMENT_NODE) 8123 if (children[i].nodeType == Node.ELEMENT_NODE) return children[i];
14319 return children[i];
14320 } 8124 }
14321 } 8125 }
14322 return null; 8126 return null;
14323 }, 8127 },
14324
14325 _render: function() { 8128 _render: function() {
14326 if (!this.ctor) 8129 if (!this.ctor) this.templatize(this);
14327 this.templatize(this);
14328 var parentNode = this.parentNode; 8130 var parentNode = this.parentNode;
14329 if (parentNode && !this._instance) { 8131 if (parentNode && !this._instance) {
14330 this._instance = /** @type {TemplateInstance} */(this.stamp({})); 8132 this._instance = this.stamp({});
14331 var root = this._instance.root; 8133 var root = this._instance.root;
14332 parentNode.insertBefore(root, this); 8134 parentNode.insertBefore(root, this);
14333 } 8135 }
14334 }, 8136 },
14335
14336 /**
14337 * @param {string} prop
14338 * @param {Object} value
14339 */
14340 _forwardParentProp: function(prop, value) { 8137 _forwardParentProp: function(prop, value) {
14341 if (this._instance) 8138 if (this._instance) this._instance.__setProperty(prop, value, true);
14342 this._instance.__setProperty(prop, value, true); 8139 },
14343 },
14344
14345 /**
14346 * @param {string} path
14347 * @param {Object} value
14348 */
14349 _forwardParentPath: function(path, value) { 8140 _forwardParentPath: function(path, value) {
14350 if (this._instance) 8141 if (this._instance) this._instance._notifyPath(path, value, true);
14351 this._instance._notifyPath(path, value, true);
14352 } 8142 }
14353 }); 8143 });
8144
14354 // Copyright 2016 The Chromium Authors. All rights reserved. 8145 // Copyright 2016 The Chromium Authors. All rights reserved.
14355 // Use of this source code is governed by a BSD-style license that can be 8146 // Use of this source code is governed by a BSD-style license that can be
14356 // found in the LICENSE file. 8147 // found in the LICENSE file.
14357
14358 Polymer({ 8148 Polymer({
14359 is: 'history-list-container', 8149 is: 'history-list-container',
14360
14361 properties: { 8150 properties: {
14362 // The path of the currently selected page.
14363 selectedPage_: String, 8151 selectedPage_: String,
14364
14365 // Whether domain-grouped history is enabled.
14366 grouped: Boolean, 8152 grouped: Boolean,
14367
14368 /** @type {!QueryState} */
14369 queryState: Object, 8153 queryState: Object,
14370 8154 queryResult: Object
14371 /** @type {!QueryResult} */ 8155 },
14372 queryResult: Object, 8156 observers: [ 'groupedRangeChanged_(queryState.range)' ],
14373 },
14374
14375 observers: [
14376 'groupedRangeChanged_(queryState.range)',
14377 ],
14378
14379 listeners: { 8157 listeners: {
14380 'history-list-scrolled': 'closeMenu_', 8158 'history-list-scrolled': 'closeMenu_',
14381 'load-more-history': 'loadMoreHistory_', 8159 'load-more-history': 'loadMoreHistory_',
14382 'toggle-menu': 'toggleMenu_', 8160 'toggle-menu': 'toggleMenu_'
14383 }, 8161 },
14384
14385 /**
14386 * @param {HistoryQuery} info An object containing information about the
14387 * query.
14388 * @param {!Array<HistoryEntry>} results A list of results.
14389 */
14390 historyResult: function(info, results) { 8162 historyResult: function(info, results) {
14391 this.initializeResults_(info, results); 8163 this.initializeResults_(info, results);
14392 this.closeMenu_(); 8164 this.closeMenu_();
14393
14394 if (this.selectedPage_ == 'grouped-list') { 8165 if (this.selectedPage_ == 'grouped-list') {
14395 this.$$('#grouped-list').historyData = results; 8166 this.$$('#grouped-list').historyData = results;
14396 return; 8167 return;
14397 } 8168 }
14398 8169 var list = this.$['infinite-list'];
14399 var list = /** @type {HistoryListElement} */(this.$['infinite-list']);
14400 list.addNewResults(results, this.queryState.incremental); 8170 list.addNewResults(results, this.queryState.incremental);
14401 if (info.finished) 8171 if (info.finished) list.disableResultLoading();
14402 list.disableResultLoading(); 8172 },
14403 },
14404
14405 /**
14406 * Queries the history backend for results based on queryState.
14407 * @param {boolean} incremental Whether the new query should continue where
14408 * the previous query stopped.
14409 */
14410 queryHistory: function(incremental) { 8173 queryHistory: function(incremental) {
14411 var queryState = this.queryState; 8174 var queryState = this.queryState;
14412 // Disable querying until the first set of results have been returned. If
14413 // there is a search, query immediately to support search query params from
14414 // the URL.
14415 var noResults = !this.queryResult || this.queryResult.results == null; 8175 var noResults = !this.queryResult || this.queryResult.results == null;
14416 if (queryState.queryingDisabled || 8176 if (queryState.queryingDisabled || !this.queryState.searchTerm && noResults) {
14417 (!this.queryState.searchTerm && noResults)) {
14418 return; 8177 return;
14419 } 8178 }
14420
14421 // Close any open dialog if a new query is initiated.
14422 var dialog = this.$.dialog.getIfExists(); 8179 var dialog = this.$.dialog.getIfExists();
14423 if (!incremental && dialog && dialog.open) 8180 if (!incremental && dialog && dialog.open) dialog.close();
14424 dialog.close();
14425
14426 this.set('queryState.querying', true); 8181 this.set('queryState.querying', true);
14427 this.set('queryState.incremental', incremental); 8182 this.set('queryState.incremental', incremental);
14428
14429 var lastVisitTime = 0; 8183 var lastVisitTime = 0;
14430 if (incremental) { 8184 if (incremental) {
14431 var lastVisit = this.queryResult.results.slice(-1)[0]; 8185 var lastVisit = this.queryResult.results.slice(-1)[0];
14432 lastVisitTime = lastVisit ? lastVisit.time : 0; 8186 lastVisitTime = lastVisit ? lastVisit.time : 0;
14433 } 8187 }
14434 8188 var maxResults = queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAG E : 0;
14435 var maxResults = 8189 chrome.send('queryHistory', [ queryState.searchTerm, queryState.groupedOffse t, queryState.range, lastVisitTime, maxResults ]);
14436 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0; 8190 },
14437 chrome.send('queryHistory', [
14438 queryState.searchTerm, queryState.groupedOffset, queryState.range,
14439 lastVisitTime, maxResults
14440 ]);
14441 },
14442
14443 historyDeleted: function() { 8191 historyDeleted: function() {
14444 // Do not reload the list when there are items checked. 8192 if (this.getSelectedItemCount() > 0) return;
14445 if (this.getSelectedItemCount() > 0)
14446 return;
14447
14448 // Reload the list with current search state.
14449 this.queryHistory(false); 8193 this.queryHistory(false);
14450 }, 8194 },
14451
14452 /** @return {number} */
14453 getSelectedItemCount: function() { 8195 getSelectedItemCount: function() {
14454 return this.getSelectedList_().selectedPaths.size; 8196 return this.getSelectedList_().selectedPaths.size;
14455 }, 8197 },
14456
14457 unselectAllItems: function(count) { 8198 unselectAllItems: function(count) {
14458 var selectedList = this.getSelectedList_(); 8199 var selectedList = this.getSelectedList_();
14459 if (selectedList) 8200 if (selectedList) selectedList.unselectAllItems(count);
14460 selectedList.unselectAllItems(count); 8201 },
14461 },
14462
14463 /**
14464 * Delete all the currently selected history items. Will prompt the user with
14465 * a dialog to confirm that the deletion should be performed.
14466 */
14467 deleteSelectedWithPrompt: function() { 8202 deleteSelectedWithPrompt: function() {
14468 if (!loadTimeData.getBoolean('allowDeletingHistory')) 8203 if (!loadTimeData.getBoolean('allowDeletingHistory')) return;
14469 return;
14470 this.$.dialog.get().then(function(dialog) { 8204 this.$.dialog.get().then(function(dialog) {
14471 dialog.showModal(); 8205 dialog.showModal();
14472 }); 8206 });
14473 }, 8207 },
14474
14475 /**
14476 * @param {HistoryRange} range
14477 * @private
14478 */
14479 groupedRangeChanged_: function(range) { 8208 groupedRangeChanged_: function(range) {
14480 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? 8209 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? 'infin ite-list' : 'grouped-list';
14481 'infinite-list' : 'grouped-list';
14482
14483 this.queryHistory(false); 8210 this.queryHistory(false);
14484 }, 8211 },
14485 8212 loadMoreHistory_: function() {
14486 /** @private */ 8213 this.queryHistory(true);
14487 loadMoreHistory_: function() { this.queryHistory(true); }, 8214 },
14488
14489 /**
14490 * @param {HistoryQuery} info
14491 * @param {!Array<HistoryEntry>} results
14492 * @private
14493 */
14494 initializeResults_: function(info, results) { 8215 initializeResults_: function(info, results) {
14495 if (results.length == 0) 8216 if (results.length == 0) return;
14496 return;
14497
14498 var currentDate = results[0].dateRelativeDay; 8217 var currentDate = results[0].dateRelativeDay;
14499
14500 for (var i = 0; i < results.length; i++) { 8218 for (var i = 0; i < results.length; i++) {
14501 // Sets the default values for these fields to prevent undefined types.
14502 results[i].selected = false; 8219 results[i].selected = false;
14503 results[i].readableTimestamp = 8220 results[i].readableTimestamp = info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
14504 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
14505
14506 if (results[i].dateRelativeDay != currentDate) { 8221 if (results[i].dateRelativeDay != currentDate) {
14507 currentDate = results[i].dateRelativeDay; 8222 currentDate = results[i].dateRelativeDay;
14508 } 8223 }
14509 } 8224 }
14510 }, 8225 },
14511
14512 /** @private */
14513 onDialogConfirmTap_: function() { 8226 onDialogConfirmTap_: function() {
14514 this.getSelectedList_().deleteSelected(); 8227 this.getSelectedList_().deleteSelected();
14515 var dialog = assert(this.$.dialog.getIfExists()); 8228 var dialog = assert(this.$.dialog.getIfExists());
14516 dialog.close(); 8229 dialog.close();
14517 }, 8230 },
14518
14519 /** @private */
14520 onDialogCancelTap_: function() { 8231 onDialogCancelTap_: function() {
14521 var dialog = assert(this.$.dialog.getIfExists()); 8232 var dialog = assert(this.$.dialog.getIfExists());
14522 dialog.close(); 8233 dialog.close();
14523 }, 8234 },
14524
14525 /**
14526 * Closes the overflow menu.
14527 * @private
14528 */
14529 closeMenu_: function() { 8235 closeMenu_: function() {
14530 var menu = this.$.sharedMenu.getIfExists(); 8236 var menu = this.$.sharedMenu.getIfExists();
14531 if (menu) 8237 if (menu) menu.closeMenu();
14532 menu.closeMenu(); 8238 },
14533 },
14534
14535 /**
14536 * Opens the overflow menu unless the menu is already open and the same button
14537 * is pressed.
14538 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e
14539 * @return {Promise<Element>}
14540 * @private
14541 */
14542 toggleMenu_: function(e) { 8239 toggleMenu_: function(e) {
14543 var target = e.detail.target; 8240 var target = e.detail.target;
14544 return this.$.sharedMenu.get().then(function(menu) { 8241 return this.$.sharedMenu.get().then(function(menu) {
14545 /** @type {CrSharedMenuElement} */(menu).toggleMenu( 8242 menu.toggleMenu(target, e.detail);
14546 target, e.detail); 8243 });
14547 }); 8244 },
14548 },
14549
14550 /** @private */
14551 onMoreFromSiteTap_: function() { 8245 onMoreFromSiteTap_: function() {
14552 var menu = assert(this.$.sharedMenu.getIfExists()); 8246 var menu = assert(this.$.sharedMenu.getIfExists());
14553 this.fire('search-domain', {domain: menu.itemData.item.domain}); 8247 this.fire('search-domain', {
8248 domain: menu.itemData.item.domain
8249 });
14554 menu.closeMenu(); 8250 menu.closeMenu();
14555 }, 8251 },
14556
14557 /** @private */
14558 onRemoveFromHistoryTap_: function() { 8252 onRemoveFromHistoryTap_: function() {
14559 var menu = assert(this.$.sharedMenu.getIfExists()); 8253 var menu = assert(this.$.sharedMenu.getIfExists());
14560 var itemData = menu.itemData; 8254 var itemData = menu.itemData;
14561 md_history.BrowserService.getInstance() 8255 md_history.BrowserService.getInstance().deleteItems([ itemData.item ]).then( function(items) {
14562 .deleteItems([itemData.item]) 8256 this.getSelectedList_().removeItemsByPath([ itemData.path ]);
14563 .then(function(items) { 8257 this.fire('unselect-all');
14564 this.getSelectedList_().removeItemsByPath([itemData.path]); 8258 }.bind(this));
14565 // This unselect-all is to reset the toolbar when deleting a selected
14566 // item. TODO(tsergeant): Make this automatic based on observing list
14567 // modifications.
14568 this.fire('unselect-all');
14569 }.bind(this));
14570 menu.closeMenu(); 8259 menu.closeMenu();
14571 }, 8260 },
14572
14573 /**
14574 * @return {HTMLElement}
14575 * @private
14576 */
14577 getSelectedList_: function() { 8261 getSelectedList_: function() {
14578 return this.$.content.selectedItem; 8262 return this.$.content.selectedItem;
14579 }, 8263 }
14580 }); 8264 });
8265
14581 // Copyright 2016 The Chromium Authors. All rights reserved. 8266 // Copyright 2016 The Chromium Authors. All rights reserved.
14582 // Use of this source code is governed by a BSD-style license that can be 8267 // Use of this source code is governed by a BSD-style license that can be
14583 // found in the LICENSE file. 8268 // found in the LICENSE file.
14584
14585 Polymer({ 8269 Polymer({
14586 is: 'history-synced-device-card', 8270 is: 'history-synced-device-card',
14587
14588 properties: { 8271 properties: {
14589 // Name of the synced device.
14590 device: String, 8272 device: String,
14591
14592 // When the device information was last updated.
14593 lastUpdateTime: String, 8273 lastUpdateTime: String,
14594
14595 /**
14596 * The list of tabs open for this device.
14597 * @type {!Array<!ForeignSessionTab>}
14598 */
14599 tabs: { 8274 tabs: {
14600 type: Array, 8275 type: Array,
14601 value: function() { return []; }, 8276 value: function() {
8277 return [];
8278 },
14602 observer: 'updateIcons_' 8279 observer: 'updateIcons_'
14603 }, 8280 },
14604
14605 /**
14606 * The indexes where a window separator should be shown. The use of a
14607 * separate array here is necessary for window separators to appear
14608 * correctly in search. See http://crrev.com/2022003002 for more details.
14609 * @type {!Array<number>}
14610 */
14611 separatorIndexes: Array, 8281 separatorIndexes: Array,
14612
14613 // Whether the card is open.
14614 opened: Boolean, 8282 opened: Boolean,
14615
14616 searchTerm: String, 8283 searchTerm: String,
14617 8284 sessionTag: String
14618 // Internal identifier for the device. 8285 },
14619 sessionTag: String,
14620 },
14621
14622 /**
14623 * Open a single synced tab. Listens to 'click' rather than 'tap'
14624 * to determine what modifier keys were pressed.
14625 * @param {DomRepeatClickEvent} e
14626 * @private
14627 */
14628 openTab_: function(e) { 8286 openTab_: function(e) {
14629 var tab = /** @type {ForeignSessionTab} */(e.model.tab); 8287 var tab = e.model.tab;
14630 md_history.BrowserService.getInstance().openForeignSessionTab( 8288 md_history.BrowserService.getInstance().openForeignSessionTab(this.sessionTa g, tab.windowId, tab.sessionId, e);
14631 this.sessionTag, tab.windowId, tab.sessionId, e);
14632 e.preventDefault(); 8289 e.preventDefault();
14633 }, 8290 },
14634
14635 /**
14636 * Toggles the dropdown display of synced tabs for each device card.
14637 */
14638 toggleTabCard: function() { 8291 toggleTabCard: function() {
14639 this.$.collapse.toggle(); 8292 this.$.collapse.toggle();
14640 this.$['dropdown-indicator'].icon = 8293 this.$['dropdown-indicator'].icon = this.$.collapse.opened ? 'cr:expand-less ' : 'cr:expand-more';
14641 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; 8294 },
14642 },
14643
14644 /**
14645 * When the synced tab information is set, the icon associated with the tab
14646 * website is also set.
14647 * @private
14648 */
14649 updateIcons_: function() { 8295 updateIcons_: function() {
14650 this.async(function() { 8296 this.async(function() {
14651 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); 8297 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon');
14652
14653 for (var i = 0; i < this.tabs.length; i++) { 8298 for (var i = 0; i < this.tabs.length; i++) {
14654 icons[i].style.backgroundImage = 8299 icons[i].style.backgroundImage = cr.icon.getFaviconImageSet(this.tabs[i] .url);
14655 cr.icon.getFaviconImageSet(this.tabs[i].url); 8300 }
14656 } 8301 });
14657 }); 8302 },
14658 },
14659
14660 /** @private */
14661 isWindowSeparatorIndex_: function(index, separatorIndexes) { 8303 isWindowSeparatorIndex_: function(index, separatorIndexes) {
14662 return this.separatorIndexes.indexOf(index) != -1; 8304 return this.separatorIndexes.indexOf(index) != -1;
14663 }, 8305 },
14664
14665 /**
14666 * @param {boolean} opened
14667 * @return {string}
14668 * @private
14669 */
14670 getCollapseIcon_: function(opened) { 8306 getCollapseIcon_: function(opened) {
14671 return opened ? 'cr:expand-less' : 'cr:expand-more'; 8307 return opened ? 'cr:expand-less' : 'cr:expand-more';
14672 }, 8308 },
14673
14674 /**
14675 * @param {boolean} opened
14676 * @return {string}
14677 * @private
14678 */
14679 getCollapseTitle_: function(opened) { 8309 getCollapseTitle_: function(opened) {
14680 return opened ? loadTimeData.getString('collapseSessionButton') : 8310 return opened ? loadTimeData.getString('collapseSessionButton') : loadTimeDa ta.getString('expandSessionButton');
14681 loadTimeData.getString('expandSessionButton'); 8311 },
14682 },
14683
14684 /**
14685 * @param {CustomEvent} e
14686 * @private
14687 */
14688 onMenuButtonTap_: function(e) { 8312 onMenuButtonTap_: function(e) {
14689 this.fire('toggle-menu', { 8313 this.fire('toggle-menu', {
14690 target: Polymer.dom(e).localTarget, 8314 target: Polymer.dom(e).localTarget,
14691 tag: this.sessionTag 8315 tag: this.sessionTag
14692 }); 8316 });
14693 e.stopPropagation(); // Prevent iron-collapse. 8317 e.stopPropagation();
14694 }, 8318 }
14695 }); 8319 });
8320
14696 // Copyright 2016 The Chromium Authors. All rights reserved. 8321 // Copyright 2016 The Chromium Authors. All rights reserved.
14697 // Use of this source code is governed by a BSD-style license that can be 8322 // Use of this source code is governed by a BSD-style license that can be
14698 // found in the LICENSE file. 8323 // found in the LICENSE file.
14699
14700 /**
14701 * @typedef {{device: string,
14702 * lastUpdateTime: string,
14703 * opened: boolean,
14704 * separatorIndexes: !Array<number>,
14705 * timestamp: number,
14706 * tabs: !Array<!ForeignSessionTab>,
14707 * tag: string}}
14708 */
14709 var ForeignDeviceInternal; 8324 var ForeignDeviceInternal;
14710 8325
14711 Polymer({ 8326 Polymer({
14712 is: 'history-synced-device-manager', 8327 is: 'history-synced-device-manager',
14713
14714 properties: { 8328 properties: {
14715 /**
14716 * @type {?Array<!ForeignSession>}
14717 */
14718 sessionList: { 8329 sessionList: {
14719 type: Array, 8330 type: Array,
14720 observer: 'updateSyncedDevices' 8331 observer: 'updateSyncedDevices'
14721 }, 8332 },
14722
14723 searchTerm: { 8333 searchTerm: {
14724 type: String, 8334 type: String,
14725 observer: 'searchTermChanged' 8335 observer: 'searchTermChanged'
14726 }, 8336 },
14727
14728 /**
14729 * An array of synced devices with synced tab data.
14730 * @type {!Array<!ForeignDeviceInternal>}
14731 */
14732 syncedDevices_: { 8337 syncedDevices_: {
14733 type: Array, 8338 type: Array,
14734 value: function() { return []; } 8339 value: function() {
14735 }, 8340 return [];
14736 8341 }
14737 /** @private */ 8342 },
14738 signInState_: { 8343 signInState_: {
14739 type: Boolean, 8344 type: Boolean,
14740 value: loadTimeData.getBoolean('isUserSignedIn'), 8345 value: loadTimeData.getBoolean('isUserSignedIn')
14741 }, 8346 },
14742
14743 /** @private */
14744 guestSession_: { 8347 guestSession_: {
14745 type: Boolean, 8348 type: Boolean,
14746 value: loadTimeData.getBoolean('isGuestSession'), 8349 value: loadTimeData.getBoolean('isGuestSession')
14747 }, 8350 },
14748
14749 /** @private */
14750 fetchingSyncedTabs_: { 8351 fetchingSyncedTabs_: {
14751 type: Boolean, 8352 type: Boolean,
14752 value: false, 8353 value: false
14753 } 8354 }
14754 }, 8355 },
14755
14756 listeners: { 8356 listeners: {
14757 'toggle-menu': 'onToggleMenu_', 8357 'toggle-menu': 'onToggleMenu_',
14758 'scroll': 'onListScroll_' 8358 scroll: 'onListScroll_'
14759 }, 8359 },
14760
14761 /** @override */
14762 attached: function() { 8360 attached: function() {
14763 // Update the sign in state.
14764 chrome.send('otherDevicesInitialized'); 8361 chrome.send('otherDevicesInitialized');
14765 }, 8362 },
14766
14767 /**
14768 * @param {!ForeignSession} session
14769 * @return {!ForeignDeviceInternal}
14770 */
14771 createInternalDevice_: function(session) { 8363 createInternalDevice_: function(session) {
14772 var tabs = []; 8364 var tabs = [];
14773 var separatorIndexes = []; 8365 var separatorIndexes = [];
14774 for (var i = 0; i < session.windows.length; i++) { 8366 for (var i = 0; i < session.windows.length; i++) {
14775 var windowId = session.windows[i].sessionId; 8367 var windowId = session.windows[i].sessionId;
14776 var newTabs = session.windows[i].tabs; 8368 var newTabs = session.windows[i].tabs;
14777 if (newTabs.length == 0) 8369 if (newTabs.length == 0) continue;
14778 continue;
14779
14780 newTabs.forEach(function(tab) { 8370 newTabs.forEach(function(tab) {
14781 tab.windowId = windowId; 8371 tab.windowId = windowId;
14782 }); 8372 });
14783
14784 var windowAdded = false; 8373 var windowAdded = false;
14785 if (!this.searchTerm) { 8374 if (!this.searchTerm) {
14786 // Add all the tabs if there is no search term.
14787 tabs = tabs.concat(newTabs); 8375 tabs = tabs.concat(newTabs);
14788 windowAdded = true; 8376 windowAdded = true;
14789 } else { 8377 } else {
14790 var searchText = this.searchTerm.toLowerCase(); 8378 var searchText = this.searchTerm.toLowerCase();
14791 for (var j = 0; j < newTabs.length; j++) { 8379 for (var j = 0; j < newTabs.length; j++) {
14792 var tab = newTabs[j]; 8380 var tab = newTabs[j];
14793 if (tab.title.toLowerCase().indexOf(searchText) != -1) { 8381 if (tab.title.toLowerCase().indexOf(searchText) != -1) {
14794 tabs.push(tab); 8382 tabs.push(tab);
14795 windowAdded = true; 8383 windowAdded = true;
14796 } 8384 }
14797 } 8385 }
14798 } 8386 }
14799 if (windowAdded && i != session.windows.length - 1) 8387 if (windowAdded && i != session.windows.length - 1) separatorIndexes.push( tabs.length - 1);
14800 separatorIndexes.push(tabs.length - 1);
14801 } 8388 }
14802 return { 8389 return {
14803 device: session.name, 8390 device: session.name,
14804 lastUpdateTime: '– ' + session.modifiedTime, 8391 lastUpdateTime: '– ' + session.modifiedTime,
14805 opened: true, 8392 opened: true,
14806 separatorIndexes: separatorIndexes, 8393 separatorIndexes: separatorIndexes,
14807 timestamp: session.timestamp, 8394 timestamp: session.timestamp,
14808 tabs: tabs, 8395 tabs: tabs,
14809 tag: session.tag, 8396 tag: session.tag
14810 }; 8397 };
14811 }, 8398 },
14812
14813 onSignInTap_: function() { 8399 onSignInTap_: function() {
14814 chrome.send('startSignInFlow'); 8400 chrome.send('startSignInFlow');
14815 }, 8401 },
14816
14817 onListScroll_: function() { 8402 onListScroll_: function() {
14818 var menu = this.$.menu.getIfExists(); 8403 var menu = this.$.menu.getIfExists();
14819 if (menu) 8404 if (menu) menu.closeMenu();
14820 menu.closeMenu(); 8405 },
14821 },
14822
14823 onToggleMenu_: function(e) { 8406 onToggleMenu_: function(e) {
14824 this.$.menu.get().then(function(menu) { 8407 this.$.menu.get().then(function(menu) {
14825 menu.toggleMenu(e.detail.target, e.detail.tag); 8408 menu.toggleMenu(e.detail.target, e.detail.tag);
14826 }); 8409 });
14827 }, 8410 },
14828
14829 onOpenAllTap_: function() { 8411 onOpenAllTap_: function() {
14830 var menu = assert(this.$.menu.getIfExists()); 8412 var menu = assert(this.$.menu.getIfExists());
14831 md_history.BrowserService.getInstance().openForeignSessionAllTabs( 8413 md_history.BrowserService.getInstance().openForeignSessionAllTabs(menu.itemD ata);
14832 menu.itemData);
14833 menu.closeMenu(); 8414 menu.closeMenu();
14834 }, 8415 },
14835
14836 onDeleteSessionTap_: function() { 8416 onDeleteSessionTap_: function() {
14837 var menu = assert(this.$.menu.getIfExists()); 8417 var menu = assert(this.$.menu.getIfExists());
14838 md_history.BrowserService.getInstance().deleteForeignSession( 8418 md_history.BrowserService.getInstance().deleteForeignSession(menu.itemData);
14839 menu.itemData);
14840 menu.closeMenu(); 8419 menu.closeMenu();
14841 }, 8420 },
14842
14843 /** @private */
14844 clearDisplayedSyncedDevices_: function() { 8421 clearDisplayedSyncedDevices_: function() {
14845 this.syncedDevices_ = []; 8422 this.syncedDevices_ = [];
14846 }, 8423 },
14847 8424 showNoSyncedMessage: function(signInState, syncedDevicesLength, guestSession) {
14848 /** 8425 if (guestSession) return true;
14849 * Decide whether or not should display no synced tabs message.
14850 * @param {boolean} signInState
14851 * @param {number} syncedDevicesLength
14852 * @param {boolean} guestSession
14853 * @return {boolean}
14854 */
14855 showNoSyncedMessage: function(
14856 signInState, syncedDevicesLength, guestSession) {
14857 if (guestSession)
14858 return true;
14859
14860 return signInState && syncedDevicesLength == 0; 8426 return signInState && syncedDevicesLength == 0;
14861 }, 8427 },
14862
14863 /**
14864 * Shows the signin guide when the user is not signed in and not in a guest
14865 * session.
14866 * @param {boolean} signInState
14867 * @param {boolean} guestSession
14868 * @return {boolean}
14869 */
14870 showSignInGuide: function(signInState, guestSession) { 8428 showSignInGuide: function(signInState, guestSession) {
14871 var show = !signInState && !guestSession; 8429 var show = !signInState && !guestSession;
14872 if (show) { 8430 if (show) {
14873 md_history.BrowserService.getInstance().recordAction( 8431 md_history.BrowserService.getInstance().recordAction('Signin_Impression_Fr omRecentTabs');
14874 'Signin_Impression_FromRecentTabs'); 8432 }
14875 }
14876
14877 return show; 8433 return show;
14878 }, 8434 },
14879
14880 /**
14881 * Decide what message should be displayed when user is logged in and there
14882 * are no synced tabs.
14883 * @param {boolean} fetchingSyncedTabs
14884 * @return {string}
14885 */
14886 noSyncedTabsMessage: function(fetchingSyncedTabs) { 8435 noSyncedTabsMessage: function(fetchingSyncedTabs) {
14887 return loadTimeData.getString( 8436 return loadTimeData.getString(fetchingSyncedTabs ? 'loading' : 'noSyncedResu lts');
14888 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); 8437 },
14889 },
14890
14891 /**
14892 * Replaces the currently displayed synced tabs with |sessionList|. It is
14893 * common for only a single session within the list to have changed, We try to
14894 * avoid doing extra work in this case. The logic could be more intelligent
14895 * about updating individual tabs rather than replacing whole sessions, but
14896 * this approach seems to have acceptable performance.
14897 * @param {?Array<!ForeignSession>} sessionList
14898 */
14899 updateSyncedDevices: function(sessionList) { 8438 updateSyncedDevices: function(sessionList) {
14900 this.fetchingSyncedTabs_ = false; 8439 this.fetchingSyncedTabs_ = false;
14901 8440 if (!sessionList) return;
14902 if (!sessionList)
14903 return;
14904
14905 // First, update any existing devices that have changed.
14906 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); 8441 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length);
14907 for (var i = 0; i < updateCount; i++) { 8442 for (var i = 0; i < updateCount; i++) {
14908 var oldDevice = this.syncedDevices_[i]; 8443 var oldDevice = this.syncedDevices_[i];
14909 if (oldDevice.tag != sessionList[i].tag || 8444 if (oldDevice.tag != sessionList[i].tag || oldDevice.timestamp != sessionL ist[i].timestamp) {
14910 oldDevice.timestamp != sessionList[i].timestamp) { 8445 this.splice('syncedDevices_', i, 1, this.createInternalDevice_(sessionLi st[i]));
14911 this.splice( 8446 }
14912 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); 8447 }
14913 }
14914 }
14915
14916 if (sessionList.length >= this.syncedDevices_.length) { 8448 if (sessionList.length >= this.syncedDevices_.length) {
14917 // The list grew; append new items.
14918 for (var i = updateCount; i < sessionList.length; i++) { 8449 for (var i = updateCount; i < sessionList.length; i++) {
14919 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); 8450 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i]));
14920 } 8451 }
14921 } else { 8452 } else {
14922 // The list shrank; remove deleted items. 8453 this.splice('syncedDevices_', updateCount, this.syncedDevices_.length - up dateCount);
14923 this.splice( 8454 }
14924 'syncedDevices_', updateCount, 8455 },
14925 this.syncedDevices_.length - updateCount);
14926 }
14927 },
14928
14929 /**
14930 * End fetching synced tabs when sync is disabled.
14931 */
14932 tabSyncDisabled: function() { 8456 tabSyncDisabled: function() {
14933 this.fetchingSyncedTabs_ = false; 8457 this.fetchingSyncedTabs_ = false;
14934 this.clearDisplayedSyncedDevices_(); 8458 this.clearDisplayedSyncedDevices_();
14935 }, 8459 },
14936
14937 /**
14938 * Get called when user's sign in state changes, this will affect UI of synced
14939 * tabs page. Sign in promo gets displayed when user is signed out, and
14940 * different messages are shown when there are no synced tabs.
14941 * @param {boolean} isUserSignedIn
14942 */
14943 updateSignInState: function(isUserSignedIn) { 8460 updateSignInState: function(isUserSignedIn) {
14944 // If user's sign in state didn't change, then don't change message or 8461 if (this.signInState_ == isUserSignedIn) return;
14945 // update UI.
14946 if (this.signInState_ == isUserSignedIn)
14947 return;
14948
14949 this.signInState_ = isUserSignedIn; 8462 this.signInState_ = isUserSignedIn;
14950
14951 // User signed out, clear synced device list and show the sign in promo.
14952 if (!isUserSignedIn) { 8463 if (!isUserSignedIn) {
14953 this.clearDisplayedSyncedDevices_(); 8464 this.clearDisplayedSyncedDevices_();
14954 return; 8465 return;
14955 } 8466 }
14956 // User signed in, show the loading message when querying for synced
14957 // devices.
14958 this.fetchingSyncedTabs_ = true; 8467 this.fetchingSyncedTabs_ = true;
14959 }, 8468 },
14960
14961 searchTermChanged: function(searchTerm) { 8469 searchTermChanged: function(searchTerm) {
14962 this.clearDisplayedSyncedDevices_(); 8470 this.clearDisplayedSyncedDevices_();
14963 this.updateSyncedDevices(this.sessionList); 8471 this.updateSyncedDevices(this.sessionList);
14964 } 8472 }
14965 }); 8473 });
14966 /** 8474
14967 `iron-selector` is an element which can be used to manage a list of elements 8475 Polymer({
14968 that can be selected. Tapping on the item will make the item selected. The ` selected` indicates 8476 is: 'iron-selector',
14969 which item is being selected. The default is to use the index of the item. 8477 behaviors: [ Polymer.IronMultiSelectableBehavior ]
14970 8478 });
14971 Example: 8479
14972
14973 <iron-selector selected="0">
14974 <div>Item 1</div>
14975 <div>Item 2</div>
14976 <div>Item 3</div>
14977 </iron-selector>
14978
14979 If you want to use the attribute value of an element for `selected` instead of the index,
14980 set `attrForSelected` to the name of the attribute. For example, if you want to select item by
14981 `name`, set `attrForSelected` to `name`.
14982
14983 Example:
14984
14985 <iron-selector attr-for-selected="name" selected="foo">
14986 <div name="foo">Foo</div>
14987 <div name="bar">Bar</div>
14988 <div name="zot">Zot</div>
14989 </iron-selector>
14990
14991 You can specify a default fallback with `fallbackSelection` in case the `selec ted` attribute does
14992 not match the `attrForSelected` attribute of any elements.
14993
14994 Example:
14995
14996 <iron-selector attr-for-selected="name" selected="non-existing"
14997 fallback-selection="default">
14998 <div name="foo">Foo</div>
14999 <div name="bar">Bar</div>
15000 <div name="default">Default</div>
15001 </iron-selector>
15002
15003 Note: When the selector is multi, the selection will set to `fallbackSelection ` iff
15004 the number of matching elements is zero.
15005
15006 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the selected element.
15007
15008 Example:
15009
15010 <style>
15011 .iron-selected {
15012 background: #eee;
15013 }
15014 </style>
15015
15016 ...
15017
15018 <iron-selector selected="0">
15019 <div>Item 1</div>
15020 <div>Item 2</div>
15021 <div>Item 3</div>
15022 </iron-selector>
15023
15024 @demo demo/index.html
15025 */
15026
15027 Polymer({
15028
15029 is: 'iron-selector',
15030
15031 behaviors: [
15032 Polymer.IronMultiSelectableBehavior
15033 ]
15034
15035 });
15036 // Copyright 2016 The Chromium Authors. All rights reserved. 8480 // Copyright 2016 The Chromium Authors. All rights reserved.
15037 // Use of this source code is governed by a BSD-style license that can be 8481 // Use of this source code is governed by a BSD-style license that can be
15038 // found in the LICENSE file. 8482 // found in the LICENSE file.
15039
15040 Polymer({ 8483 Polymer({
15041 is: 'history-side-bar', 8484 is: 'history-side-bar',
15042
15043 properties: { 8485 properties: {
15044 selectedPage: { 8486 selectedPage: {
15045 type: String, 8487 type: String,
15046 notify: true 8488 notify: true
15047 }, 8489 },
15048
15049 route: Object, 8490 route: Object,
15050
15051 showFooter: Boolean, 8491 showFooter: Boolean,
15052
15053 // If true, the sidebar is contained within an app-drawer.
15054 drawer: { 8492 drawer: {
15055 type: Boolean, 8493 type: Boolean,
15056 reflectToAttribute: true 8494 reflectToAttribute: true
15057 }, 8495 }
15058 }, 8496 },
15059
15060 /** @private */
15061 onSelectorActivate_: function() { 8497 onSelectorActivate_: function() {
15062 this.fire('history-close-drawer'); 8498 this.fire('history-close-drawer');
15063 }, 8499 },
15064
15065 /**
15066 * Relocates the user to the clear browsing data section of the settings page.
15067 * @param {Event} e
15068 * @private
15069 */
15070 onClearBrowsingDataTap_: function(e) { 8500 onClearBrowsingDataTap_: function(e) {
15071 md_history.BrowserService.getInstance().openClearBrowsingData(); 8501 md_history.BrowserService.getInstance().openClearBrowsingData();
15072 e.preventDefault(); 8502 e.preventDefault();
15073 }, 8503 },
15074
15075 /**
15076 * @param {Object} route
15077 * @private
15078 */
15079 getQueryString_: function(route) { 8504 getQueryString_: function(route) {
15080 return window.location.search; 8505 return window.location.search;
15081 } 8506 }
15082 }); 8507 });
8508
15083 // Copyright 2016 The Chromium Authors. All rights reserved. 8509 // Copyright 2016 The Chromium Authors. All rights reserved.
15084 // Use of this source code is governed by a BSD-style license that can be 8510 // Use of this source code is governed by a BSD-style license that can be
15085 // found in the LICENSE file. 8511 // found in the LICENSE file.
15086
15087 Polymer({ 8512 Polymer({
15088 is: 'history-app', 8513 is: 'history-app',
15089
15090 properties: { 8514 properties: {
15091 showSidebarFooter: Boolean, 8515 showSidebarFooter: Boolean,
15092 8516 selectedPage_: {
15093 // The id of the currently selected page. 8517 type: String,
15094 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, 8518 value: 'history',
15095 8519 observer: 'unselectAll'
15096 // Whether domain-grouped history is enabled. 8520 },
15097 grouped_: {type: Boolean, reflectToAttribute: true}, 8521 grouped_: {
15098 8522 type: Boolean,
15099 /** @type {!QueryState} */ 8523 reflectToAttribute: true
8524 },
15100 queryState_: { 8525 queryState_: {
15101 type: Object, 8526 type: Object,
15102 value: function() { 8527 value: function() {
15103 return { 8528 return {
15104 // Whether the most recent query was incremental.
15105 incremental: false, 8529 incremental: false,
15106 // A query is initiated by page load.
15107 querying: true, 8530 querying: true,
15108 queryingDisabled: false, 8531 queryingDisabled: false,
15109 _range: HistoryRange.ALL_TIME, 8532 _range: HistoryRange.ALL_TIME,
15110 searchTerm: '', 8533 searchTerm: '',
15111 // TODO(calamity): Make history toolbar buttons change the offset
15112 groupedOffset: 0, 8534 groupedOffset: 0,
15113 8535 set range(val) {
15114 set range(val) { this._range = Number(val); }, 8536 this._range = Number(val);
15115 get range() { return this._range; }, 8537 },
8538 get range() {
8539 return this._range;
8540 }
15116 }; 8541 };
15117 } 8542 }
15118 }, 8543 },
15119
15120 /** @type {!QueryResult} */
15121 queryResult_: { 8544 queryResult_: {
15122 type: Object, 8545 type: Object,
15123 value: function() { 8546 value: function() {
15124 return { 8547 return {
15125 info: null, 8548 info: null,
15126 results: null, 8549 results: null,
15127 sessionList: null, 8550 sessionList: null
15128 }; 8551 };
15129 } 8552 }
15130 }, 8553 },
15131
15132 // Route data for the current page.
15133 routeData_: Object, 8554 routeData_: Object,
15134
15135 // The query params for the page.
15136 queryParams_: Object, 8555 queryParams_: Object,
15137 8556 hasDrawer_: Boolean
15138 // True if the window is narrow enough for the page to have a drawer. 8557 },
15139 hasDrawer_: Boolean, 8558 observers: [ 'routeDataChanged_(routeData_.page)', 'selectedPageChanged_(selec tedPage_)', 'searchTermChanged_(queryState_.searchTerm)', 'searchQueryParamChang ed_(queryParams_.q)' ],
15140 },
15141
15142 observers: [
15143 // routeData_.page <=> selectedPage
15144 'routeDataChanged_(routeData_.page)',
15145 'selectedPageChanged_(selectedPage_)',
15146
15147 // queryParams_.q <=> queryState.searchTerm
15148 'searchTermChanged_(queryState_.searchTerm)',
15149 'searchQueryParamChanged_(queryParams_.q)',
15150
15151 ],
15152
15153 // TODO(calamity): Replace these event listeners with data bound properties.
15154 listeners: { 8559 listeners: {
15155 'cr-menu-tap': 'onMenuTap_', 8560 'cr-menu-tap': 'onMenuTap_',
15156 'history-checkbox-select': 'checkboxSelected', 8561 'history-checkbox-select': 'checkboxSelected',
15157 'unselect-all': 'unselectAll', 8562 'unselect-all': 'unselectAll',
15158 'delete-selected': 'deleteSelected', 8563 'delete-selected': 'deleteSelected',
15159 'search-domain': 'searchDomain_', 8564 'search-domain': 'searchDomain_',
15160 'history-close-drawer': 'closeDrawer_', 8565 'history-close-drawer': 'closeDrawer_'
15161 }, 8566 },
15162
15163 /** @override */
15164 ready: function() { 8567 ready: function() {
15165 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); 8568 this.grouped_ = loadTimeData.getBoolean('groupByDomain');
15166
15167 cr.ui.decorate('command', cr.ui.Command); 8569 cr.ui.decorate('command', cr.ui.Command);
15168 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); 8570 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
15169 document.addEventListener('command', this.onCommand_.bind(this)); 8571 document.addEventListener('command', this.onCommand_.bind(this));
15170
15171 // Redirect legacy search URLs to URLs compatible with material history.
15172 if (window.location.hash) { 8572 if (window.location.hash) {
15173 window.location.href = window.location.href.split('#')[0] + '?' + 8573 window.location.href = window.location.href.split('#')[0] + '?' + window.l ocation.hash.substr(1);
15174 window.location.hash.substr(1); 8574 }
15175 } 8575 },
15176 },
15177
15178 /** @private */
15179 onMenuTap_: function() { 8576 onMenuTap_: function() {
15180 var drawer = this.$$('#drawer'); 8577 var drawer = this.$$('#drawer');
15181 if (drawer) 8578 if (drawer) drawer.toggle();
15182 drawer.toggle(); 8579 },
15183 },
15184
15185 /**
15186 * Listens for history-item being selected or deselected (through checkbox)
15187 * and changes the view of the top toolbar.
15188 * @param {{detail: {countAddition: number}}} e
15189 */
15190 checkboxSelected: function(e) { 8580 checkboxSelected: function(e) {
15191 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); 8581 var toolbar = this.$.toolbar;
15192 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history) 8582 toolbar.count = this.$.history.getSelectedItemCount();
15193 .getSelectedItemCount(); 8583 },
15194 },
15195
15196 /**
15197 * Listens for call to cancel selection and loops through all items to set
15198 * checkbox to be unselected.
15199 * @private
15200 */
15201 unselectAll: function() { 8584 unselectAll: function() {
15202 var listContainer = 8585 var listContainer = this.$.history;
15203 /** @type {HistoryListContainerElement} */ (this.$.history); 8586 var toolbar = this.$.toolbar;
15204 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
15205 listContainer.unselectAllItems(toolbar.count); 8587 listContainer.unselectAllItems(toolbar.count);
15206 toolbar.count = 0; 8588 toolbar.count = 0;
15207 }, 8589 },
15208
15209 deleteSelected: function() { 8590 deleteSelected: function() {
15210 this.$.history.deleteSelectedWithPrompt(); 8591 this.$.history.deleteSelectedWithPrompt();
15211 }, 8592 },
15212
15213 /**
15214 * @param {HistoryQuery} info An object containing information about the
15215 * query.
15216 * @param {!Array<HistoryEntry>} results A list of results.
15217 */
15218 historyResult: function(info, results) { 8593 historyResult: function(info, results) {
15219 this.set('queryState_.querying', false); 8594 this.set('queryState_.querying', false);
15220 this.set('queryResult_.info', info); 8595 this.set('queryResult_.info', info);
15221 this.set('queryResult_.results', results); 8596 this.set('queryResult_.results', results);
15222 var listContainer = 8597 var listContainer = this.$['history'];
15223 /** @type {HistoryListContainerElement} */ (this.$['history']);
15224 listContainer.historyResult(info, results); 8598 listContainer.historyResult(info, results);
15225 }, 8599 },
15226 8600 searchDomain_: function(e) {
15227 /** 8601 this.$.toolbar.setSearchTerm(e.detail.domain);
15228 * Fired when the user presses 'More from this site'. 8602 },
15229 * @param {{detail: {domain: string}}} e
15230 */
15231 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); },
15232
15233 /**
15234 * @param {Event} e
15235 * @private
15236 */
15237 onCanExecute_: function(e) { 8603 onCanExecute_: function(e) {
15238 e = /** @type {cr.ui.CanExecuteEvent} */(e); 8604 e = e;
15239 switch (e.command.id) { 8605 switch (e.command.id) {
15240 case 'find-command': 8606 case 'find-command':
15241 e.canExecute = true; 8607 e.canExecute = true;
15242 break; 8608 break;
15243 case 'slash-command': 8609
15244 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); 8610 case 'slash-command':
15245 break; 8611 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused();
15246 case 'delete-command': 8612 break;
15247 e.canExecute = this.$.toolbar.count > 0; 8613
15248 break; 8614 case 'delete-command':
15249 } 8615 e.canExecute = this.$.toolbar.count > 0;
15250 }, 8616 break;
15251 8617 }
15252 /** 8618 },
15253 * @param {string} searchTerm
15254 * @private
15255 */
15256 searchTermChanged_: function(searchTerm) { 8619 searchTermChanged_: function(searchTerm) {
15257 this.set('queryParams_.q', searchTerm || null); 8620 this.set('queryParams_.q', searchTerm || null);
15258 this.$['history'].queryHistory(false); 8621 this.$['history'].queryHistory(false);
15259 }, 8622 },
15260
15261 /**
15262 * @param {string} searchQuery
15263 * @private
15264 */
15265 searchQueryParamChanged_: function(searchQuery) { 8623 searchQueryParamChanged_: function(searchQuery) {
15266 this.$.toolbar.setSearchTerm(searchQuery || ''); 8624 this.$.toolbar.setSearchTerm(searchQuery || '');
15267 }, 8625 },
15268
15269 /**
15270 * @param {Event} e
15271 * @private
15272 */
15273 onCommand_: function(e) { 8626 onCommand_: function(e) {
15274 if (e.command.id == 'find-command' || e.command.id == 'slash-command') 8627 if (e.command.id == 'find-command' || e.command.id == 'slash-command') this. $.toolbar.showSearchField();
15275 this.$.toolbar.showSearchField(); 8628 if (e.command.id == 'delete-command') this.deleteSelected();
15276 if (e.command.id == 'delete-command') 8629 },
15277 this.deleteSelected();
15278 },
15279
15280 /**
15281 * @param {!Array<!ForeignSession>} sessionList Array of objects describing
15282 * the sessions from other devices.
15283 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
15284 */
15285 setForeignSessions: function(sessionList, isTabSyncEnabled) { 8630 setForeignSessions: function(sessionList, isTabSyncEnabled) {
15286 if (!isTabSyncEnabled) { 8631 if (!isTabSyncEnabled) {
15287 var syncedDeviceManagerElem = 8632 var syncedDeviceManagerElem = this.$$('history-synced-device-manager');
15288 /** @type {HistorySyncedDeviceManagerElement} */this 8633 if (syncedDeviceManagerElem) syncedDeviceManagerElem.tabSyncDisabled();
15289 .$$('history-synced-device-manager');
15290 if (syncedDeviceManagerElem)
15291 syncedDeviceManagerElem.tabSyncDisabled();
15292 return; 8634 return;
15293 } 8635 }
15294
15295 this.set('queryResult_.sessionList', sessionList); 8636 this.set('queryResult_.sessionList', sessionList);
15296 }, 8637 },
15297
15298 /**
15299 * Called when browsing data is cleared.
15300 */
15301 historyDeleted: function() { 8638 historyDeleted: function() {
15302 this.$.history.historyDeleted(); 8639 this.$.history.historyDeleted();
15303 }, 8640 },
15304
15305 /**
15306 * Update sign in state of synced device manager after user logs in or out.
15307 * @param {boolean} isUserSignedIn
15308 */
15309 updateSignInState: function(isUserSignedIn) { 8641 updateSignInState: function(isUserSignedIn) {
15310 var syncedDeviceManagerElem = 8642 var syncedDeviceManagerElem = this.$$('history-synced-device-manager');
15311 /** @type {HistorySyncedDeviceManagerElement} */this 8643 if (syncedDeviceManagerElem) syncedDeviceManagerElem.updateSignInState(isUse rSignedIn);
15312 .$$('history-synced-device-manager'); 8644 },
15313 if (syncedDeviceManagerElem)
15314 syncedDeviceManagerElem.updateSignInState(isUserSignedIn);
15315 },
15316
15317 /**
15318 * @param {string} selectedPage
15319 * @return {boolean}
15320 * @private
15321 */
15322 syncedTabsSelected_: function(selectedPage) { 8645 syncedTabsSelected_: function(selectedPage) {
15323 return selectedPage == 'syncedTabs'; 8646 return selectedPage == 'syncedTabs';
15324 }, 8647 },
15325
15326 /**
15327 * @param {boolean} querying
15328 * @param {boolean} incremental
15329 * @param {string} searchTerm
15330 * @return {boolean} Whether a loading spinner should be shown (implies the
15331 * backend is querying a new search term).
15332 * @private
15333 */
15334 shouldShowSpinner_: function(querying, incremental, searchTerm) { 8648 shouldShowSpinner_: function(querying, incremental, searchTerm) {
15335 return querying && !incremental && searchTerm != ''; 8649 return querying && !incremental && searchTerm != '';
15336 }, 8650 },
15337
15338 /**
15339 * @param {string} page
15340 * @private
15341 */
15342 routeDataChanged_: function(page) { 8651 routeDataChanged_: function(page) {
15343 this.selectedPage_ = page; 8652 this.selectedPage_ = page;
15344 }, 8653 },
15345
15346 /**
15347 * @param {string} selectedPage
15348 * @private
15349 */
15350 selectedPageChanged_: function(selectedPage) { 8654 selectedPageChanged_: function(selectedPage) {
15351 this.set('routeData_.page', selectedPage); 8655 this.set('routeData_.page', selectedPage);
15352 }, 8656 },
15353
15354 /**
15355 * This computed binding is needed to make the iron-pages selector update when
15356 * the synced-device-manager is instantiated for the first time. Otherwise the
15357 * fallback selection will continue to be used after the corresponding item is
15358 * added as a child of iron-pages.
15359 * @param {string} selectedPage
15360 * @param {Array} items
15361 * @return {string}
15362 * @private
15363 */
15364 getSelectedPage_: function(selectedPage, items) { 8657 getSelectedPage_: function(selectedPage, items) {
15365 return selectedPage; 8658 return selectedPage;
15366 }, 8659 },
15367
15368 /** @private */
15369 closeDrawer_: function() { 8660 closeDrawer_: function() {
15370 var drawer = this.$$('#drawer'); 8661 var drawer = this.$$('#drawer');
15371 if (drawer) 8662 if (drawer) drawer.close();
15372 drawer.close(); 8663 }
15373 },
15374 }); 8664 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/md_downloads/crisper.js ('k') | chrome/browser/resources/vulcanize.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698