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

Side by Side Diff: chrome/browser/resources/md_downloads/crisper.js

Issue 2224003003: Vulcanize MD History to improve page-load performance (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase, revulcanize, avoid duplicating files 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
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 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 Assertion support.
7 */
8
9 /**
10 * Verify |condition| is truthy and return |condition| if so.
11 * @template T
12 * @param {T} condition A condition to check for truthiness. Note that this
13 * may be used to test whether a value is defined or not, and we don't want
14 * to force a cast to Boolean.
15 * @param {string=} opt_message A message to show on failure.
16 * @return {T} A non-null |condition|.
17 */
18 function assert(condition, opt_message) { 4 function assert(condition, opt_message) {
19 if (!condition) { 5 if (!condition) {
20 var message = 'Assertion failed'; 6 var message = 'Assertion failed';
21 if (opt_message) 7 if (opt_message) message = message + ': ' + opt_message;
22 message = message + ': ' + opt_message;
23 var error = new Error(message); 8 var error = new Error(message);
24 var global = function() { return this; }(); 9 var global = function() {
25 if (global.traceAssertionsForTesting) 10 return this;
26 console.warn(error.stack); 11 }();
12 if (global.traceAssertionsForTesting) console.warn(error.stack);
27 throw error; 13 throw error;
28 } 14 }
29 return condition; 15 return condition;
30 } 16 }
31 17
32 /**
33 * Call this from places in the code that should never be reached.
34 *
35 * For example, handling all the values of enum with a switch() like this:
36 *
37 * function getValueFromEnum(enum) {
38 * switch (enum) {
39 * case ENUM_FIRST_OF_TWO:
40 * return first
41 * case ENUM_LAST_OF_TWO:
42 * return last;
43 * }
44 * assertNotReached();
45 * return document;
46 * }
47 *
48 * This code should only be hit in the case of serious programmer error or
49 * unexpected input.
50 *
51 * @param {string=} opt_message A message to show when this is hit.
52 */
53 function assertNotReached(opt_message) { 18 function assertNotReached(opt_message) {
54 assert(false, opt_message || 'Unreachable code hit'); 19 assert(false, opt_message || 'Unreachable code hit');
55 } 20 }
56 21
57 /**
58 * @param {*} value The value to check.
59 * @param {function(new: T, ...)} type A user-defined constructor.
60 * @param {string=} opt_message A message to show when this is hit.
61 * @return {T}
62 * @template T
63 */
64 function assertInstanceof(value, type, opt_message) { 22 function assertInstanceof(value, type, opt_message) {
65 // We don't use assert immediately here so that we avoid constructing an error
66 // message if we don't have to.
67 if (!(value instanceof type)) { 23 if (!(value instanceof type)) {
68 assertNotReached(opt_message || 'Value ' + value + 24 assertNotReached(opt_message || 'Value ' + value + ' is not a[n] ' + (type.n ame || typeof type));
69 ' is not a[n] ' + (type.name || typeof type));
70 } 25 }
71 return value; 26 return value;
72 }; 27 }
28
73 // Copyright 2016 The Chromium Authors. All rights reserved. 29 // Copyright 2016 The Chromium Authors. All rights reserved.
74 // Use of this source code is governed by a BSD-style license that can be 30 // Use of this source code is governed by a BSD-style license that can be
75 // found in the LICENSE file. 31 // found in the LICENSE file.
76
77 /**
78 * @fileoverview PromiseResolver is a helper class that allows creating a
79 * Promise that will be fulfilled (resolved or rejected) some time later.
80 *
81 * Example:
82 * var resolver = new PromiseResolver();
83 * resolver.promise.then(function(result) {
84 * console.log('resolved with', result);
85 * });
86 * ...
87 * ...
88 * resolver.resolve({hello: 'world'});
89 */
90
91 /**
92 * @constructor @struct
93 * @template T
94 */
95 function PromiseResolver() { 32 function PromiseResolver() {
96 /** @private {function(T=): void} */
97 this.resolve_; 33 this.resolve_;
98
99 /** @private {function(*=): void} */
100 this.reject_; 34 this.reject_;
101
102 /** @private {!Promise<T>} */
103 this.promise_ = new Promise(function(resolve, reject) { 35 this.promise_ = new Promise(function(resolve, reject) {
104 this.resolve_ = resolve; 36 this.resolve_ = resolve;
105 this.reject_ = reject; 37 this.reject_ = reject;
106 }.bind(this)); 38 }.bind(this));
107 } 39 }
108 40
109 PromiseResolver.prototype = { 41 PromiseResolver.prototype = {
110 /** @return {!Promise<T>} */ 42 get promise() {
111 get promise() { return this.promise_; }, 43 return this.promise_;
112 set promise(p) { assertNotReached(); }, 44 },
45 set promise(p) {
46 assertNotReached();
47 },
48 get resolve() {
49 return this.resolve_;
50 },
51 set resolve(r) {
52 assertNotReached();
53 },
54 get reject() {
55 return this.reject_;
56 },
57 set reject(s) {
58 assertNotReached();
59 }
60 };
113 61
114 /** @return {function(T=): void} */
115 get resolve() { return this.resolve_; },
116 set resolve(r) { assertNotReached(); },
117
118 /** @return {function(*=): void} */
119 get reject() { return this.reject_; },
120 set reject(s) { assertNotReached(); },
121 };
122 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 62 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
123 // Use of this source code is governed by a BSD-style license that can be 63 // Use of this source code is governed by a BSD-style license that can be
124 // found in the LICENSE file. 64 // found in the LICENSE file.
125
126 /**
127 * The global object.
128 * @type {!Object}
129 * @const
130 */
131 var global = this; 65 var global = this;
132 66
133 /** @typedef {{eventName: string, uid: number}} */
134 var WebUIListener; 67 var WebUIListener;
135 68
136 /** Platform, package, object property, and Event support. **/
137 var cr = cr || function() { 69 var cr = cr || function() {
138 'use strict'; 70 'use strict';
139
140 /**
141 * Builds an object structure for the provided namespace path,
142 * ensuring that names that already exist are not overwritten. For
143 * example:
144 * "a.b.c" -> a = {};a.b={};a.b.c={};
145 * @param {string} name Name of the object that this file defines.
146 * @param {*=} opt_object The object to expose at the end of the path.
147 * @param {Object=} opt_objectToExportTo The object to add the path to;
148 * default is {@code global}.
149 * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
150 * returns a reference to the ui property of window.cr).
151 * @private
152 */
153 function exportPath(name, opt_object, opt_objectToExportTo) { 71 function exportPath(name, opt_object, opt_objectToExportTo) {
154 var parts = name.split('.'); 72 var parts = name.split('.');
155 var cur = opt_objectToExportTo || global; 73 var cur = opt_objectToExportTo || global;
156 74 for (var part; parts.length && (part = parts.shift()); ) {
157 for (var part; parts.length && (part = parts.shift());) {
158 if (!parts.length && opt_object !== undefined) { 75 if (!parts.length && opt_object !== undefined) {
159 // last part and we have an object; use it
160 cur[part] = opt_object; 76 cur[part] = opt_object;
161 } else if (part in cur) { 77 } else if (part in cur) {
162 cur = cur[part]; 78 cur = cur[part];
163 } else { 79 } else {
164 cur = cur[part] = {}; 80 cur = cur[part] = {};
165 } 81 }
166 } 82 }
167 return cur; 83 return cur;
168 } 84 }
169
170 /**
171 * Fires a property change event on the target.
172 * @param {EventTarget} target The target to dispatch the event on.
173 * @param {string} propertyName The name of the property that changed.
174 * @param {*} newValue The new value for the property.
175 * @param {*} oldValue The old value for the property.
176 */
177 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { 85 function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
178 var e = new Event(propertyName + 'Change'); 86 var e = new Event(propertyName + 'Change');
179 e.propertyName = propertyName; 87 e.propertyName = propertyName;
180 e.newValue = newValue; 88 e.newValue = newValue;
181 e.oldValue = oldValue; 89 e.oldValue = oldValue;
182 target.dispatchEvent(e); 90 target.dispatchEvent(e);
183 } 91 }
184
185 /**
186 * Converts a camelCase javascript property name to a hyphenated-lower-case
187 * attribute name.
188 * @param {string} jsName The javascript camelCase property name.
189 * @return {string} The equivalent hyphenated-lower-case attribute name.
190 */
191 function getAttributeName(jsName) { 92 function getAttributeName(jsName) {
192 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); 93 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
193 } 94 }
194
195 /**
196 * The kind of property to define in {@code defineProperty}.
197 * @enum {string}
198 * @const
199 */
200 var PropertyKind = { 95 var PropertyKind = {
201 /**
202 * Plain old JS property where the backing data is stored as a "private"
203 * field on the object.
204 * Use for properties of any type. Type will not be checked.
205 */
206 JS: 'js', 96 JS: 'js',
207
208 /**
209 * The property backing data is stored as an attribute on an element.
210 * Use only for properties of type {string}.
211 */
212 ATTR: 'attr', 97 ATTR: 'attr',
213
214 /**
215 * The property backing data is stored as an attribute on an element. If the
216 * element has the attribute then the value is true.
217 * Use only for properties of type {boolean}.
218 */
219 BOOL_ATTR: 'boolAttr' 98 BOOL_ATTR: 'boolAttr'
220 }; 99 };
221
222 /**
223 * Helper function for defineProperty that returns the getter to use for the
224 * property.
225 * @param {string} name The name of the property.
226 * @param {PropertyKind} kind The kind of the property.
227 * @return {function():*} The getter for the property.
228 */
229 function getGetter(name, kind) { 100 function getGetter(name, kind) {
230 switch (kind) { 101 switch (kind) {
231 case PropertyKind.JS: 102 case PropertyKind.JS:
232 var privateName = name + '_'; 103 var privateName = name + '_';
233 return function() { 104 return function() {
234 return this[privateName]; 105 return this[privateName];
235 }; 106 };
236 case PropertyKind.ATTR: 107
237 var attributeName = getAttributeName(name); 108 case PropertyKind.ATTR:
238 return function() { 109 var attributeName = getAttributeName(name);
239 return this.getAttribute(attributeName); 110 return function() {
240 }; 111 return this.getAttribute(attributeName);
241 case PropertyKind.BOOL_ATTR: 112 };
242 var attributeName = getAttributeName(name); 113
243 return function() { 114 case PropertyKind.BOOL_ATTR:
244 return this.hasAttribute(attributeName); 115 var attributeName = getAttributeName(name);
245 }; 116 return function() {
117 return this.hasAttribute(attributeName);
118 };
246 } 119 }
247
248 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
249 // the browser/unit tests to preprocess this file through grit.
250 throw 'not reached'; 120 throw 'not reached';
251 } 121 }
252
253 /**
254 * Helper function for defineProperty that returns the setter of the right
255 * kind.
256 * @param {string} name The name of the property we are defining the setter
257 * for.
258 * @param {PropertyKind} kind The kind of property we are getting the
259 * setter for.
260 * @param {function(*, *):void=} opt_setHook A function to run after the
261 * property is set, but before the propertyChange event is fired.
262 * @return {function(*):void} The function to use as a setter.
263 */
264 function getSetter(name, kind, opt_setHook) { 122 function getSetter(name, kind, opt_setHook) {
265 switch (kind) { 123 switch (kind) {
266 case PropertyKind.JS: 124 case PropertyKind.JS:
267 var privateName = name + '_'; 125 var privateName = name + '_';
268 return function(value) { 126 return function(value) {
269 var oldValue = this[name]; 127 var oldValue = this[name];
270 if (value !== oldValue) { 128 if (value !== oldValue) {
271 this[privateName] = value; 129 this[privateName] = value;
272 if (opt_setHook) 130 if (opt_setHook) opt_setHook.call(this, value, oldValue);
273 opt_setHook.call(this, value, oldValue); 131 dispatchPropertyChange(this, name, value, oldValue);
274 dispatchPropertyChange(this, name, value, oldValue); 132 }
275 } 133 };
276 };
277 134
278 case PropertyKind.ATTR: 135 case PropertyKind.ATTR:
279 var attributeName = getAttributeName(name); 136 var attributeName = getAttributeName(name);
280 return function(value) { 137 return function(value) {
281 var oldValue = this[name]; 138 var oldValue = this[name];
282 if (value !== oldValue) { 139 if (value !== oldValue) {
283 if (value == undefined) 140 if (value == undefined) this.removeAttribute(attributeName); else this .setAttribute(attributeName, value);
284 this.removeAttribute(attributeName); 141 if (opt_setHook) opt_setHook.call(this, value, oldValue);
285 else 142 dispatchPropertyChange(this, name, value, oldValue);
286 this.setAttribute(attributeName, value); 143 }
287 if (opt_setHook) 144 };
288 opt_setHook.call(this, value, oldValue);
289 dispatchPropertyChange(this, name, value, oldValue);
290 }
291 };
292 145
293 case PropertyKind.BOOL_ATTR: 146 case PropertyKind.BOOL_ATTR:
294 var attributeName = getAttributeName(name); 147 var attributeName = getAttributeName(name);
295 return function(value) { 148 return function(value) {
296 var oldValue = this[name]; 149 var oldValue = this[name];
297 if (value !== oldValue) { 150 if (value !== oldValue) {
298 if (value) 151 if (value) this.setAttribute(attributeName, name); else this.removeAtt ribute(attributeName);
299 this.setAttribute(attributeName, name); 152 if (opt_setHook) opt_setHook.call(this, value, oldValue);
300 else 153 dispatchPropertyChange(this, name, value, oldValue);
301 this.removeAttribute(attributeName); 154 }
302 if (opt_setHook) 155 };
303 opt_setHook.call(this, value, oldValue);
304 dispatchPropertyChange(this, name, value, oldValue);
305 }
306 };
307 } 156 }
308
309 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
310 // the browser/unit tests to preprocess this file through grit.
311 throw 'not reached'; 157 throw 'not reached';
312 } 158 }
313
314 /**
315 * Defines a property on an object. When the setter changes the value a
316 * property change event with the type {@code name + 'Change'} is fired.
317 * @param {!Object} obj The object to define the property for.
318 * @param {string} name The name of the property.
319 * @param {PropertyKind=} opt_kind What kind of underlying storage to use.
320 * @param {function(*, *):void=} opt_setHook A function to run after the
321 * property is set, but before the propertyChange event is fired.
322 */
323 function defineProperty(obj, name, opt_kind, opt_setHook) { 159 function defineProperty(obj, name, opt_kind, opt_setHook) {
324 if (typeof obj == 'function') 160 if (typeof obj == 'function') obj = obj.prototype;
325 obj = obj.prototype; 161 var kind = opt_kind || PropertyKind.JS;
326 162 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name, kind));
327 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); 163 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
328
329 if (!obj.__lookupGetter__(name))
330 obj.__defineGetter__(name, getGetter(name, kind));
331
332 if (!obj.__lookupSetter__(name))
333 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
334 } 164 }
335
336 /**
337 * Counter for use with createUid
338 */
339 var uidCounter = 1; 165 var uidCounter = 1;
340
341 /**
342 * @return {number} A new unique ID.
343 */
344 function createUid() { 166 function createUid() {
345 return uidCounter++; 167 return uidCounter++;
346 } 168 }
347
348 /**
349 * Returns a unique ID for the item. This mutates the item so it needs to be
350 * an object
351 * @param {!Object} item The item to get the unique ID for.
352 * @return {number} The unique ID for the item.
353 */
354 function getUid(item) { 169 function getUid(item) {
355 if (item.hasOwnProperty('uid')) 170 if (item.hasOwnProperty('uid')) return item.uid;
356 return item.uid;
357 return item.uid = createUid(); 171 return item.uid = createUid();
358 } 172 }
359
360 /**
361 * Dispatches a simple event on an event target.
362 * @param {!EventTarget} target The event target to dispatch the event on.
363 * @param {string} type The type of the event.
364 * @param {boolean=} opt_bubbles Whether the event bubbles or not.
365 * @param {boolean=} opt_cancelable Whether the default action of the event
366 * can be prevented. Default is true.
367 * @return {boolean} If any of the listeners called {@code preventDefault}
368 * during the dispatch this will return false.
369 */
370 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { 173 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
371 var e = new Event(type, { 174 var e = new Event(type, {
372 bubbles: opt_bubbles, 175 bubbles: opt_bubbles,
373 cancelable: opt_cancelable === undefined || opt_cancelable 176 cancelable: opt_cancelable === undefined || opt_cancelable
374 }); 177 });
375 return target.dispatchEvent(e); 178 return target.dispatchEvent(e);
376 } 179 }
377
378 /**
379 * Calls |fun| and adds all the fields of the returned object to the object
380 * named by |name|. For example, cr.define('cr.ui', function() {
381 * function List() {
382 * ...
383 * }
384 * function ListItem() {
385 * ...
386 * }
387 * return {
388 * List: List,
389 * ListItem: ListItem,
390 * };
391 * });
392 * defines the functions cr.ui.List and cr.ui.ListItem.
393 * @param {string} name The name of the object that we are adding fields to.
394 * @param {!Function} fun The function that will return an object containing
395 * the names and values of the new fields.
396 */
397 function define(name, fun) { 180 function define(name, fun) {
398 var obj = exportPath(name); 181 var obj = exportPath(name);
399 var exports = fun(); 182 var exports = fun();
400 for (var propertyName in exports) { 183 for (var propertyName in exports) {
401 // Maybe we should check the prototype chain here? The current usage 184 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property Name);
402 // pattern is always using an object literal so we only care about own 185 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD escriptor);
403 // properties.
404 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
405 propertyName);
406 if (propertyDescriptor)
407 Object.defineProperty(obj, propertyName, propertyDescriptor);
408 } 186 }
409 } 187 }
410
411 /**
412 * Adds a {@code getInstance} static method that always return the same
413 * instance object.
414 * @param {!Function} ctor The constructor for the class to add the static
415 * method to.
416 */
417 function addSingletonGetter(ctor) { 188 function addSingletonGetter(ctor) {
418 ctor.getInstance = function() { 189 ctor.getInstance = function() {
419 return ctor.instance_ || (ctor.instance_ = new ctor()); 190 return ctor.instance_ || (ctor.instance_ = new ctor());
420 }; 191 };
421 } 192 }
422
423 /**
424 * Forwards public APIs to private implementations.
425 * @param {Function} ctor Constructor that have private implementations in its
426 * prototype.
427 * @param {Array<string>} methods List of public method names that have their
428 * underscored counterparts in constructor's prototype.
429 * @param {string=} opt_target Selector for target node.
430 */
431 function makePublic(ctor, methods, opt_target) { 193 function makePublic(ctor, methods, opt_target) {
432 methods.forEach(function(method) { 194 methods.forEach(function(method) {
433 ctor[method] = function() { 195 ctor[method] = function() {
434 var target = opt_target ? document.getElementById(opt_target) : 196 var target = opt_target ? document.getElementById(opt_target) : ctor.get Instance();
435 ctor.getInstance();
436 return target[method + '_'].apply(target, arguments); 197 return target[method + '_'].apply(target, arguments);
437 }; 198 };
438 }); 199 });
439 } 200 }
440
441 /**
442 * The mapping used by the sendWithPromise mechanism to tie the Promise
443 * returned to callers with the corresponding WebUI response. The mapping is
444 * from ID to the PromiseResolver helper; the ID is generated by
445 * sendWithPromise and is unique across all invocations of said method.
446 * @type {!Object<!PromiseResolver>}
447 */
448 var chromeSendResolverMap = {}; 201 var chromeSendResolverMap = {};
449
450 /**
451 * The named method the WebUI handler calls directly in response to a
452 * chrome.send call that expects a response. The handler requires no knowledge
453 * of the specific name of this method, as the name is passed to the handler
454 * as the first argument in the arguments list of chrome.send. The handler
455 * must pass the ID, also sent via the chrome.send arguments list, as the
456 * first argument of the JS invocation; additionally, the handler may
457 * supply any number of other arguments that will be included in the response.
458 * @param {string} id The unique ID identifying the Promise this response is
459 * tied to.
460 * @param {boolean} isSuccess Whether the request was successful.
461 * @param {*} response The response as sent from C++.
462 */
463 function webUIResponse(id, isSuccess, response) { 202 function webUIResponse(id, isSuccess, response) {
464 var resolver = chromeSendResolverMap[id]; 203 var resolver = chromeSendResolverMap[id];
465 delete chromeSendResolverMap[id]; 204 delete chromeSendResolverMap[id];
466 205 if (isSuccess) resolver.resolve(response); else resolver.reject(response);
467 if (isSuccess)
468 resolver.resolve(response);
469 else
470 resolver.reject(response);
471 } 206 }
472
473 /**
474 * A variation of chrome.send, suitable for messages that expect a single
475 * response from C++.
476 * @param {string} methodName The name of the WebUI handler API.
477 * @param {...*} var_args Varibale number of arguments to be forwarded to the
478 * C++ call.
479 * @return {!Promise}
480 */
481 function sendWithPromise(methodName, var_args) { 207 function sendWithPromise(methodName, var_args) {
482 var args = Array.prototype.slice.call(arguments, 1); 208 var args = Array.prototype.slice.call(arguments, 1);
483 var promiseResolver = new PromiseResolver(); 209 var promiseResolver = new PromiseResolver();
484 var id = methodName + '_' + createUid(); 210 var id = methodName + '_' + createUid();
485 chromeSendResolverMap[id] = promiseResolver; 211 chromeSendResolverMap[id] = promiseResolver;
486 chrome.send(methodName, [id].concat(args)); 212 chrome.send(methodName, [ id ].concat(args));
487 return promiseResolver.promise; 213 return promiseResolver.promise;
488 } 214 }
489
490 /**
491 * A map of maps associating event names with listeners. The 2nd level map
492 * associates a listener ID with the callback function, such that individual
493 * listeners can be removed from an event without affecting other listeners of
494 * the same event.
495 * @type {!Object<!Object<!Function>>}
496 */
497 var webUIListenerMap = {}; 215 var webUIListenerMap = {};
498
499 /**
500 * The named method the WebUI handler calls directly when an event occurs.
501 * The WebUI handler must supply the name of the event as the first argument
502 * of the JS invocation; additionally, the handler may supply any number of
503 * other arguments that will be forwarded to the listener callbacks.
504 * @param {string} event The name of the event that has occurred.
505 * @param {...*} var_args Additional arguments passed from C++.
506 */
507 function webUIListenerCallback(event, var_args) { 216 function webUIListenerCallback(event, var_args) {
508 var eventListenersMap = webUIListenerMap[event]; 217 var eventListenersMap = webUIListenerMap[event];
509 if (!eventListenersMap) { 218 if (!eventListenersMap) {
510 // C++ event sent for an event that has no listeners.
511 // TODO(dpapad): Should a warning be displayed here?
512 return; 219 return;
513 } 220 }
514
515 var args = Array.prototype.slice.call(arguments, 1); 221 var args = Array.prototype.slice.call(arguments, 1);
516 for (var listenerId in eventListenersMap) { 222 for (var listenerId in eventListenersMap) {
517 eventListenersMap[listenerId].apply(null, args); 223 eventListenersMap[listenerId].apply(null, args);
518 } 224 }
519 } 225 }
520
521 /**
522 * Registers a listener for an event fired from WebUI handlers. Any number of
523 * listeners may register for a single event.
524 * @param {string} eventName The event to listen to.
525 * @param {!Function} callback The callback run when the event is fired.
526 * @return {!WebUIListener} An object to be used for removing a listener via
527 * cr.removeWebUIListener. Should be treated as read-only.
528 */
529 function addWebUIListener(eventName, callback) { 226 function addWebUIListener(eventName, callback) {
530 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; 227 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
531 var uid = createUid(); 228 var uid = createUid();
532 webUIListenerMap[eventName][uid] = callback; 229 webUIListenerMap[eventName][uid] = callback;
533 return {eventName: eventName, uid: uid}; 230 return {
231 eventName: eventName,
232 uid: uid
233 };
534 } 234 }
535
536 /**
537 * Removes a listener. Does nothing if the specified listener is not found.
538 * @param {!WebUIListener} listener The listener to be removed (as returned by
539 * addWebUIListener).
540 * @return {boolean} Whether the given listener was found and actually
541 * removed.
542 */
543 function removeWebUIListener(listener) { 235 function removeWebUIListener(listener) {
544 var listenerExists = webUIListenerMap[listener.eventName] && 236 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa p[listener.eventName][listener.uid];
545 webUIListenerMap[listener.eventName][listener.uid];
546 if (listenerExists) { 237 if (listenerExists) {
547 delete webUIListenerMap[listener.eventName][listener.uid]; 238 delete webUIListenerMap[listener.eventName][listener.uid];
548 return true; 239 return true;
549 } 240 }
550 return false; 241 return false;
551 } 242 }
552
553 return { 243 return {
554 addSingletonGetter: addSingletonGetter, 244 addSingletonGetter: addSingletonGetter,
555 createUid: createUid, 245 createUid: createUid,
556 define: define, 246 define: define,
557 defineProperty: defineProperty, 247 defineProperty: defineProperty,
558 dispatchPropertyChange: dispatchPropertyChange, 248 dispatchPropertyChange: dispatchPropertyChange,
559 dispatchSimpleEvent: dispatchSimpleEvent, 249 dispatchSimpleEvent: dispatchSimpleEvent,
560 exportPath: exportPath, 250 exportPath: exportPath,
561 getUid: getUid, 251 getUid: getUid,
562 makePublic: makePublic, 252 makePublic: makePublic,
563 PropertyKind: PropertyKind, 253 PropertyKind: PropertyKind,
564
565 // C++ <-> JS communication related methods.
566 addWebUIListener: addWebUIListener, 254 addWebUIListener: addWebUIListener,
567 removeWebUIListener: removeWebUIListener, 255 removeWebUIListener: removeWebUIListener,
568 sendWithPromise: sendWithPromise, 256 sendWithPromise: sendWithPromise,
569 webUIListenerCallback: webUIListenerCallback, 257 webUIListenerCallback: webUIListenerCallback,
570 webUIResponse: webUIResponse, 258 webUIResponse: webUIResponse,
571
572 get doc() { 259 get doc() {
573 return document; 260 return document;
574 }, 261 },
575
576 /** Whether we are using a Mac or not. */
577 get isMac() { 262 get isMac() {
578 return /Mac/.test(navigator.platform); 263 return /Mac/.test(navigator.platform);
579 }, 264 },
580
581 /** Whether this is on the Windows platform or not. */
582 get isWindows() { 265 get isWindows() {
583 return /Win/.test(navigator.platform); 266 return /Win/.test(navigator.platform);
584 }, 267 },
585
586 /** Whether this is on chromeOS or not. */
587 get isChromeOS() { 268 get isChromeOS() {
588 return /CrOS/.test(navigator.userAgent); 269 return /CrOS/.test(navigator.userAgent);
589 }, 270 },
590
591 /** Whether this is on vanilla Linux (not chromeOS). */
592 get isLinux() { 271 get isLinux() {
593 return /Linux/.test(navigator.userAgent); 272 return /Linux/.test(navigator.userAgent);
594 }, 273 },
595
596 /** Whether this is on Android. */
597 get isAndroid() { 274 get isAndroid() {
598 return /Android/.test(navigator.userAgent); 275 return /Android/.test(navigator.userAgent);
599 }, 276 },
600
601 /** Whether this is on iOS. */
602 get isIOS() { 277 get isIOS() {
603 return /iPad|iPhone|iPod/.test(navigator.platform); 278 return /iPad|iPhone|iPod/.test(navigator.platform);
604 } 279 }
605 }; 280 };
606 }(); 281 }();
282
607 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 283 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
608 // Use of this source code is governed by a BSD-style license that can be 284 // Use of this source code is governed by a BSD-style license that can be
609 // found in the LICENSE file. 285 // found in the LICENSE file.
610
611 cr.define('cr.ui', function() { 286 cr.define('cr.ui', function() {
612
613 /**
614 * Decorates elements as an instance of a class.
615 * @param {string|!Element} source The way to find the element(s) to decorate.
616 * If this is a string then {@code querySeletorAll} is used to find the
617 * elements to decorate.
618 * @param {!Function} constr The constructor to decorate with. The constr
619 * needs to have a {@code decorate} function.
620 */
621 function decorate(source, constr) { 287 function decorate(source, constr) {
622 var elements; 288 var elements;
623 if (typeof source == 'string') 289 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e lse elements = [ source ];
624 elements = cr.doc.querySelectorAll(source);
625 else
626 elements = [source];
627
628 for (var i = 0, el; el = elements[i]; i++) { 290 for (var i = 0, el; el = elements[i]; i++) {
629 if (!(el instanceof constr)) 291 if (!(el instanceof constr)) constr.decorate(el);
630 constr.decorate(el);
631 } 292 }
632 } 293 }
633
634 /**
635 * Helper function for creating new element for define.
636 */
637 function createElementHelper(tagName, opt_bag) { 294 function createElementHelper(tagName, opt_bag) {
638 // Allow passing in ownerDocument to create in a different document.
639 var doc; 295 var doc;
640 if (opt_bag && opt_bag.ownerDocument) 296 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc = cr.doc;
641 doc = opt_bag.ownerDocument;
642 else
643 doc = cr.doc;
644 return doc.createElement(tagName); 297 return doc.createElement(tagName);
645 } 298 }
646
647 /**
648 * Creates the constructor for a UI element class.
649 *
650 * Usage:
651 * <pre>
652 * var List = cr.ui.define('list');
653 * List.prototype = {
654 * __proto__: HTMLUListElement.prototype,
655 * decorate: function() {
656 * ...
657 * },
658 * ...
659 * };
660 * </pre>
661 *
662 * @param {string|Function} tagNameOrFunction The tagName or
663 * function to use for newly created elements. If this is a function it
664 * needs to return a new element when called.
665 * @return {function(Object=):Element} The constructor function which takes
666 * an optional property bag. The function also has a static
667 * {@code decorate} method added to it.
668 */
669 function define(tagNameOrFunction) { 299 function define(tagNameOrFunction) {
670 var createFunction, tagName; 300 var createFunction, tagName;
671 if (typeof tagNameOrFunction == 'function') { 301 if (typeof tagNameOrFunction == 'function') {
672 createFunction = tagNameOrFunction; 302 createFunction = tagNameOrFunction;
673 tagName = ''; 303 tagName = '';
674 } else { 304 } else {
675 createFunction = createElementHelper; 305 createFunction = createElementHelper;
676 tagName = tagNameOrFunction; 306 tagName = tagNameOrFunction;
677 } 307 }
678
679 /**
680 * Creates a new UI element constructor.
681 * @param {Object=} opt_propertyBag Optional bag of properties to set on the
682 * object after created. The property {@code ownerDocument} is special
683 * cased and it allows you to create the element in a different
684 * document than the default.
685 * @constructor
686 */
687 function f(opt_propertyBag) { 308 function f(opt_propertyBag) {
688 var el = createFunction(tagName, opt_propertyBag); 309 var el = createFunction(tagName, opt_propertyBag);
689 f.decorate(el); 310 f.decorate(el);
690 for (var propertyName in opt_propertyBag) { 311 for (var propertyName in opt_propertyBag) {
691 el[propertyName] = opt_propertyBag[propertyName]; 312 el[propertyName] = opt_propertyBag[propertyName];
692 } 313 }
693 return el; 314 return el;
694 } 315 }
695
696 /**
697 * Decorates an element as a UI element class.
698 * @param {!Element} el The element to decorate.
699 */
700 f.decorate = function(el) { 316 f.decorate = function(el) {
701 el.__proto__ = f.prototype; 317 el.__proto__ = f.prototype;
702 el.decorate(); 318 el.decorate();
703 }; 319 };
704
705 return f; 320 return f;
706 } 321 }
707
708 /**
709 * Input elements do not grow and shrink with their content. This is a simple
710 * (and not very efficient) way of handling shrinking to content with support
711 * for min width and limited by the width of the parent element.
712 * @param {!HTMLElement} el The element to limit the width for.
713 * @param {!HTMLElement} parentEl The parent element that should limit the
714 * size.
715 * @param {number} min The minimum width.
716 * @param {number=} opt_scale Optional scale factor to apply to the width.
717 */
718 function limitInputWidth(el, parentEl, min, opt_scale) { 322 function limitInputWidth(el, parentEl, min, opt_scale) {
719 // Needs a size larger than borders
720 el.style.width = '10px'; 323 el.style.width = '10px';
721 var doc = el.ownerDocument; 324 var doc = el.ownerDocument;
722 var win = doc.defaultView; 325 var win = doc.defaultView;
723 var computedStyle = win.getComputedStyle(el); 326 var computedStyle = win.getComputedStyle(el);
724 var parentComputedStyle = win.getComputedStyle(parentEl); 327 var parentComputedStyle = win.getComputedStyle(parentEl);
725 var rtl = computedStyle.direction == 'rtl'; 328 var rtl = computedStyle.direction == 'rtl';
726 329 var inputRect = el.getBoundingClientRect();
727 // To get the max width we get the width of the treeItem minus the position
728 // of the input.
729 var inputRect = el.getBoundingClientRect(); // box-sizing
730 var parentRect = parentEl.getBoundingClientRect(); 330 var parentRect = parentEl.getBoundingClientRect();
731 var startPos = rtl ? parentRect.right - inputRect.right : 331 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p arentRect.left;
732 inputRect.left - parentRect.left; 332 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp utedStyle.borderRightWidth, 10);
733 333 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa rseInt(parentComputedStyle.paddingRight, 10);
734 // Add up border and padding of the input.
735 var inner = parseInt(computedStyle.borderLeftWidth, 10) +
736 parseInt(computedStyle.paddingLeft, 10) +
737 parseInt(computedStyle.paddingRight, 10) +
738 parseInt(computedStyle.borderRightWidth, 10);
739
740 // We also need to subtract the padding of parent to prevent it to overflow.
741 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
742 parseInt(parentComputedStyle.paddingRight, 10);
743
744 var max = parentEl.clientWidth - startPos - inner - parentPadding; 334 var max = parentEl.clientWidth - startPos - inner - parentPadding;
745 if (opt_scale) 335 if (opt_scale) max *= opt_scale;
746 max *= opt_scale;
747
748 function limit() { 336 function limit() {
749 if (el.scrollWidth > max) { 337 if (el.scrollWidth > max) {
750 el.style.width = max + 'px'; 338 el.style.width = max + 'px';
751 } else { 339 } else {
752 el.style.width = 0; 340 el.style.width = 0;
753 var sw = el.scrollWidth; 341 var sw = el.scrollWidth;
754 if (sw < min) { 342 if (sw < min) {
755 el.style.width = min + 'px'; 343 el.style.width = min + 'px';
756 } else { 344 } else {
757 el.style.width = sw + 'px'; 345 el.style.width = sw + 'px';
758 } 346 }
759 } 347 }
760 } 348 }
761
762 el.addEventListener('input', limit); 349 el.addEventListener('input', limit);
763 limit(); 350 limit();
764 } 351 }
765
766 /**
767 * Takes a number and spits out a value CSS will be happy with. To avoid
768 * subpixel layout issues, the value is rounded to the nearest integral value.
769 * @param {number} pixels The number of pixels.
770 * @return {string} e.g. '16px'.
771 */
772 function toCssPx(pixels) { 352 function toCssPx(pixels) {
773 if (!window.isFinite(pixels)) 353 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: ' + pixels);
774 console.error('Pixel value is not a number: ' + pixels);
775 return Math.round(pixels) + 'px'; 354 return Math.round(pixels) + 'px';
776 } 355 }
777
778 /**
779 * Users complain they occasionaly use doubleclicks instead of clicks
780 * (http://crbug.com/140364). To fix it we freeze click handling for
781 * the doubleclick time interval.
782 * @param {MouseEvent} e Initial click event.
783 */
784 function swallowDoubleClick(e) { 356 function swallowDoubleClick(e) {
785 var doc = e.target.ownerDocument; 357 var doc = e.target.ownerDocument;
786 var counter = Math.min(1, e.detail); 358 var counter = Math.min(1, e.detail);
787 function swallow(e) { 359 function swallow(e) {
788 e.stopPropagation(); 360 e.stopPropagation();
789 e.preventDefault(); 361 e.preventDefault();
790 } 362 }
791 function onclick(e) { 363 function onclick(e) {
792 if (e.detail > counter) { 364 if (e.detail > counter) {
793 counter = e.detail; 365 counter = e.detail;
794 // Swallow the click since it's a click inside the doubleclick timeout.
795 swallow(e); 366 swallow(e);
796 } else { 367 } else {
797 // Stop tracking clicks and let regular handling.
798 doc.removeEventListener('dblclick', swallow, true); 368 doc.removeEventListener('dblclick', swallow, true);
799 doc.removeEventListener('click', onclick, true); 369 doc.removeEventListener('click', onclick, true);
800 } 370 }
801 } 371 }
802 // The following 'click' event (if e.type == 'mouseup') mustn't be taken
803 // into account (it mustn't stop tracking clicks). Start event listening
804 // after zero timeout.
805 setTimeout(function() { 372 setTimeout(function() {
806 doc.addEventListener('click', onclick, true); 373 doc.addEventListener('click', onclick, true);
807 doc.addEventListener('dblclick', swallow, true); 374 doc.addEventListener('dblclick', swallow, true);
808 }, 0); 375 }, 0);
809 } 376 }
810
811 return { 377 return {
812 decorate: decorate, 378 decorate: decorate,
813 define: define, 379 define: define,
814 limitInputWidth: limitInputWidth, 380 limitInputWidth: limitInputWidth,
815 toCssPx: toCssPx, 381 toCssPx: toCssPx,
816 swallowDoubleClick: swallowDoubleClick 382 swallowDoubleClick: swallowDoubleClick
817 }; 383 };
818 }); 384 });
385
819 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 386 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
820 // Use of this source code is governed by a BSD-style license that can be 387 // Use of this source code is governed by a BSD-style license that can be
821 // found in the LICENSE file. 388 // found in the LICENSE file.
822
823 /**
824 * @fileoverview A command is an abstraction of an action a user can do in the
825 * UI.
826 *
827 * When the focus changes in the document for each command a canExecute event
828 * is dispatched on the active element. By listening to this event you can
829 * enable and disable the command by setting the event.canExecute property.
830 *
831 * When a command is executed a command event is dispatched on the active
832 * element. Note that you should stop the propagation after you have handled the
833 * command if there might be other command listeners higher up in the DOM tree.
834 */
835
836 cr.define('cr.ui', function() { 389 cr.define('cr.ui', function() {
837
838 /**
839 * This is used to identify keyboard shortcuts.
840 * @param {string} shortcut The text used to describe the keys for this
841 * keyboard shortcut.
842 * @constructor
843 */
844 function KeyboardShortcut(shortcut) { 390 function KeyboardShortcut(shortcut) {
845 var mods = {}; 391 var mods = {};
846 var ident = ''; 392 var ident = '';
847 shortcut.split('|').forEach(function(part) { 393 shortcut.split('|').forEach(function(part) {
848 var partLc = part.toLowerCase(); 394 var partLc = part.toLowerCase();
849 switch (partLc) { 395 switch (partLc) {
850 case 'alt': 396 case 'alt':
851 case 'ctrl': 397 case 'ctrl':
852 case 'meta': 398 case 'meta':
853 case 'shift': 399 case 'shift':
854 mods[partLc + 'Key'] = true; 400 mods[partLc + 'Key'] = true;
855 break; 401 break;
856 default: 402
857 if (ident) 403 default:
858 throw Error('Invalid shortcut'); 404 if (ident) throw Error('Invalid shortcut');
859 ident = part; 405 ident = part;
860 } 406 }
861 }); 407 });
862
863 this.ident_ = ident; 408 this.ident_ = ident;
864 this.mods_ = mods; 409 this.mods_ = mods;
865 } 410 }
866
867 KeyboardShortcut.prototype = { 411 KeyboardShortcut.prototype = {
868 /**
869 * Whether the keyboard shortcut object matches a keyboard event.
870 * @param {!Event} e The keyboard event object.
871 * @return {boolean} Whether we found a match or not.
872 */
873 matchesEvent: function(e) { 412 matchesEvent: function(e) {
874 if (e.key == this.ident_) { 413 if (e.key == this.ident_) {
875 // All keyboard modifiers needs to match.
876 var mods = this.mods_; 414 var mods = this.mods_;
877 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { 415 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k) {
878 return e[k] == !!mods[k]; 416 return e[k] == !!mods[k];
879 }); 417 });
880 } 418 }
881 return false; 419 return false;
882 } 420 }
883 }; 421 };
884
885 /**
886 * Creates a new command element.
887 * @constructor
888 * @extends {HTMLElement}
889 */
890 var Command = cr.ui.define('command'); 422 var Command = cr.ui.define('command');
891
892 Command.prototype = { 423 Command.prototype = {
893 __proto__: HTMLElement.prototype, 424 __proto__: HTMLElement.prototype,
894
895 /**
896 * Initializes the command.
897 */
898 decorate: function() { 425 decorate: function() {
899 CommandManager.init(assert(this.ownerDocument)); 426 CommandManager.init(assert(this.ownerDocument));
900 427 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor tcut');
901 if (this.hasAttribute('shortcut'))
902 this.shortcut = this.getAttribute('shortcut');
903 }, 428 },
904
905 /**
906 * Executes the command by dispatching a command event on the given element.
907 * If |element| isn't given, the active element is used instead.
908 * If the command is {@code disabled} this does nothing.
909 * @param {HTMLElement=} opt_element Optional element to dispatch event on.
910 */
911 execute: function(opt_element) { 429 execute: function(opt_element) {
912 if (this.disabled) 430 if (this.disabled) return;
913 return;
914 var doc = this.ownerDocument; 431 var doc = this.ownerDocument;
915 if (doc.activeElement) { 432 if (doc.activeElement) {
916 var e = new Event('command', {bubbles: true}); 433 var e = new Event('command', {
434 bubbles: true
435 });
917 e.command = this; 436 e.command = this;
918
919 (opt_element || doc.activeElement).dispatchEvent(e); 437 (opt_element || doc.activeElement).dispatchEvent(e);
920 } 438 }
921 }, 439 },
922
923 /**
924 * Call this when there have been changes that might change whether the
925 * command can be executed or not.
926 * @param {Node=} opt_node Node for which to actuate command state.
927 */
928 canExecuteChange: function(opt_node) { 440 canExecuteChange: function(opt_node) {
929 dispatchCanExecuteEvent(this, 441 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement );
930 opt_node || this.ownerDocument.activeElement);
931 }, 442 },
932
933 /**
934 * The keyboard shortcut that triggers the command. This is a string
935 * consisting of a key (as reported by WebKit in keydown) as
936 * well as optional key modifiers joinded with a '|'.
937 *
938 * Multiple keyboard shortcuts can be provided by separating them by
939 * whitespace.
940 *
941 * For example:
942 * "F1"
943 * "Backspace|Meta" for Apple command backspace.
944 * "a|Ctrl" for Control A
945 * "Delete Backspace|Meta" for Delete and Command Backspace
946 *
947 * @type {string}
948 */
949 shortcut_: '', 443 shortcut_: '',
950 get shortcut() { 444 get shortcut() {
951 return this.shortcut_; 445 return this.shortcut_;
952 }, 446 },
953 set shortcut(shortcut) { 447 set shortcut(shortcut) {
954 var oldShortcut = this.shortcut_; 448 var oldShortcut = this.shortcut_;
955 if (shortcut !== oldShortcut) { 449 if (shortcut !== oldShortcut) {
956 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { 450 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
957 return new KeyboardShortcut(shortcut); 451 return new KeyboardShortcut(shortcut);
958 }); 452 });
959
960 // Set this after the keyboardShortcuts_ since that might throw.
961 this.shortcut_ = shortcut; 453 this.shortcut_ = shortcut;
962 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, 454 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut) ;
963 oldShortcut);
964 } 455 }
965 }, 456 },
966
967 /**
968 * Whether the event object matches the shortcut for this command.
969 * @param {!Event} e The key event object.
970 * @return {boolean} Whether it matched or not.
971 */
972 matchesEvent: function(e) { 457 matchesEvent: function(e) {
973 if (!this.keyboardShortcuts_) 458 if (!this.keyboardShortcuts_) return false;
974 return false;
975
976 return this.keyboardShortcuts_.some(function(keyboardShortcut) { 459 return this.keyboardShortcuts_.some(function(keyboardShortcut) {
977 return keyboardShortcut.matchesEvent(e); 460 return keyboardShortcut.matchesEvent(e);
978 }); 461 });
979 }, 462 }
980 }; 463 };
981
982 /**
983 * The label of the command.
984 */
985 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); 464 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
986
987 /**
988 * Whether the command is disabled or not.
989 */
990 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); 465 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
991
992 /**
993 * Whether the command is hidden or not.
994 */
995 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); 466 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
996
997 /**
998 * Whether the command is checked or not.
999 */
1000 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); 467 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
1001
1002 /**
1003 * The flag that prevents the shortcut text from being displayed on menu.
1004 *
1005 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
1006 * is displayed in menu when the command is assosiated with a menu item.
1007 * Otherwise, no text is displayed.
1008 */
1009 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); 468 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
1010
1011 /**
1012 * Dispatches a canExecute event on the target.
1013 * @param {!cr.ui.Command} command The command that we are testing for.
1014 * @param {EventTarget} target The target element to dispatch the event on.
1015 */
1016 function dispatchCanExecuteEvent(command, target) { 469 function dispatchCanExecuteEvent(command, target) {
1017 var e = new CanExecuteEvent(command); 470 var e = new CanExecuteEvent(command);
1018 target.dispatchEvent(e); 471 target.dispatchEvent(e);
1019 command.disabled = !e.canExecute; 472 command.disabled = !e.canExecute;
1020 } 473 }
1021
1022 /**
1023 * The command managers for different documents.
1024 */
1025 var commandManagers = {}; 474 var commandManagers = {};
1026
1027 /**
1028 * Keeps track of the focused element and updates the commands when the focus
1029 * changes.
1030 * @param {!Document} doc The document that we are managing the commands for.
1031 * @constructor
1032 */
1033 function CommandManager(doc) { 475 function CommandManager(doc) {
1034 doc.addEventListener('focus', this.handleFocus_.bind(this), true); 476 doc.addEventListener('focus', this.handleFocus_.bind(this), true);
1035 // Make sure we add the listener to the bubbling phase so that elements can
1036 // prevent the command.
1037 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); 477 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
1038 } 478 }
1039
1040 /**
1041 * Initializes a command manager for the document as needed.
1042 * @param {!Document} doc The document to manage the commands for.
1043 */
1044 CommandManager.init = function(doc) { 479 CommandManager.init = function(doc) {
1045 var uid = cr.getUid(doc); 480 var uid = cr.getUid(doc);
1046 if (!(uid in commandManagers)) { 481 if (!(uid in commandManagers)) {
1047 commandManagers[uid] = new CommandManager(doc); 482 commandManagers[uid] = new CommandManager(doc);
1048 } 483 }
1049 }; 484 };
1050
1051 CommandManager.prototype = { 485 CommandManager.prototype = {
1052
1053 /**
1054 * Handles focus changes on the document.
1055 * @param {Event} e The focus event object.
1056 * @private
1057 * @suppress {checkTypes}
1058 * TODO(vitalyp): remove the suppression.
1059 */
1060 handleFocus_: function(e) { 486 handleFocus_: function(e) {
1061 var target = e.target; 487 var target = e.target;
1062 488 if (target.menu || target.command) return;
1063 // Ignore focus on a menu button or command item. 489 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect orAll('command'));
1064 if (target.menu || target.command)
1065 return;
1066
1067 var commands = Array.prototype.slice.call(
1068 target.ownerDocument.querySelectorAll('command'));
1069
1070 commands.forEach(function(command) { 490 commands.forEach(function(command) {
1071 dispatchCanExecuteEvent(command, target); 491 dispatchCanExecuteEvent(command, target);
1072 }); 492 });
1073 }, 493 },
1074
1075 /**
1076 * Handles the keydown event and routes it to the right command.
1077 * @param {!Event} e The keydown event.
1078 */
1079 handleKeyDown_: function(e) { 494 handleKeyDown_: function(e) {
1080 var target = e.target; 495 var target = e.target;
1081 var commands = Array.prototype.slice.call( 496 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect orAll('command'));
1082 target.ownerDocument.querySelectorAll('command'));
1083
1084 for (var i = 0, command; command = commands[i]; i++) { 497 for (var i = 0, command; command = commands[i]; i++) {
1085 if (command.matchesEvent(e)) { 498 if (command.matchesEvent(e)) {
1086 // When invoking a command via a shortcut, we have to manually check
1087 // if it can be executed, since focus might not have been changed
1088 // what would have updated the command's state.
1089 command.canExecuteChange(); 499 command.canExecuteChange();
1090
1091 if (!command.disabled) { 500 if (!command.disabled) {
1092 e.preventDefault(); 501 e.preventDefault();
1093 // We do not want any other element to handle this.
1094 e.stopPropagation(); 502 e.stopPropagation();
1095 command.execute(); 503 command.execute();
1096 return; 504 return;
1097 } 505 }
1098 } 506 }
1099 } 507 }
1100 } 508 }
1101 }; 509 };
1102
1103 /**
1104 * The event type used for canExecute events.
1105 * @param {!cr.ui.Command} command The command that we are evaluating.
1106 * @extends {Event}
1107 * @constructor
1108 * @class
1109 */
1110 function CanExecuteEvent(command) { 510 function CanExecuteEvent(command) {
1111 var e = new Event('canExecute', {bubbles: true, cancelable: true}); 511 var e = new Event('canExecute', {
512 bubbles: true,
513 cancelable: true
514 });
1112 e.__proto__ = CanExecuteEvent.prototype; 515 e.__proto__ = CanExecuteEvent.prototype;
1113 e.command = command; 516 e.command = command;
1114 return e; 517 return e;
1115 } 518 }
1116
1117 CanExecuteEvent.prototype = { 519 CanExecuteEvent.prototype = {
1118 __proto__: Event.prototype, 520 __proto__: Event.prototype,
1119
1120 /**
1121 * The current command
1122 * @type {cr.ui.Command}
1123 */
1124 command: null, 521 command: null,
1125
1126 /**
1127 * Whether the target can execute the command. Setting this also stops the
1128 * propagation and prevents the default. Callers can tell if an event has
1129 * been handled via |this.defaultPrevented|.
1130 * @type {boolean}
1131 */
1132 canExecute_: false, 522 canExecute_: false,
1133 get canExecute() { 523 get canExecute() {
1134 return this.canExecute_; 524 return this.canExecute_;
1135 }, 525 },
1136 set canExecute(canExecute) { 526 set canExecute(canExecute) {
1137 this.canExecute_ = !!canExecute; 527 this.canExecute_ = !!canExecute;
1138 this.stopPropagation(); 528 this.stopPropagation();
1139 this.preventDefault(); 529 this.preventDefault();
1140 } 530 }
1141 }; 531 };
1142
1143 // Export
1144 return { 532 return {
1145 Command: Command, 533 Command: Command,
1146 CanExecuteEvent: CanExecuteEvent 534 CanExecuteEvent: CanExecuteEvent
1147 }; 535 };
1148 }); 536 });
537
1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 538 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
1150 // Use of this source code is governed by a BSD-style license that can be 539 // Use of this source code is governed by a BSD-style license that can be
1151 // found in the LICENSE file. 540 // found in the LICENSE file.
1152
1153 // <include src="../../../../ui/webui/resources/js/assert.js">
1154
1155 /**
1156 * Alias for document.getElementById. Found elements must be HTMLElements.
1157 * @param {string} id The ID of the element to find.
1158 * @return {HTMLElement} The found element or null if not found.
1159 */
1160 function $(id) { 541 function $(id) {
1161 var el = document.getElementById(id); 542 var el = document.getElementById(id);
1162 return el ? assertInstanceof(el, HTMLElement) : null; 543 return el ? assertInstanceof(el, HTMLElement) : null;
1163 } 544 }
1164 545
1165 // TODO(devlin): This should return SVGElement, but closure compiler is missing
1166 // those externs.
1167 /**
1168 * Alias for document.getElementById. Found elements must be SVGElements.
1169 * @param {string} id The ID of the element to find.
1170 * @return {Element} The found element or null if not found.
1171 */
1172 function getSVGElement(id) { 546 function getSVGElement(id) {
1173 var el = document.getElementById(id); 547 var el = document.getElementById(id);
1174 return el ? assertInstanceof(el, Element) : null; 548 return el ? assertInstanceof(el, Element) : null;
1175 } 549 }
1176 550
1177 /**
1178 * Add an accessible message to the page that will be announced to
1179 * users who have spoken feedback on, but will be invisible to all
1180 * other users. It's removed right away so it doesn't clutter the DOM.
1181 * @param {string} msg The text to be pronounced.
1182 */
1183 function announceAccessibleMessage(msg) { 551 function announceAccessibleMessage(msg) {
1184 var element = document.createElement('div'); 552 var element = document.createElement('div');
1185 element.setAttribute('aria-live', 'polite'); 553 element.setAttribute('aria-live', 'polite');
1186 element.style.position = 'relative'; 554 element.style.position = 'relative';
1187 element.style.left = '-9999px'; 555 element.style.left = '-9999px';
1188 element.style.height = '0px'; 556 element.style.height = '0px';
1189 element.innerText = msg; 557 element.innerText = msg;
1190 document.body.appendChild(element); 558 document.body.appendChild(element);
1191 window.setTimeout(function() { 559 window.setTimeout(function() {
1192 document.body.removeChild(element); 560 document.body.removeChild(element);
1193 }, 0); 561 }, 0);
1194 } 562 }
1195 563
1196 /**
1197 * Generates a CSS url string.
1198 * @param {string} s The URL to generate the CSS url for.
1199 * @return {string} The CSS url string.
1200 */
1201 function url(s) { 564 function url(s) {
1202 // http://www.w3.org/TR/css3-values/#uris
1203 // Parentheses, commas, whitespace characters, single quotes (') and double
1204 // quotes (") appearing in a URI must be escaped with a backslash
1205 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); 565 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
1206 // WebKit has a bug when it comes to URLs that end with \
1207 // https://bugs.webkit.org/show_bug.cgi?id=28885
1208 if (/\\\\$/.test(s2)) { 566 if (/\\\\$/.test(s2)) {
1209 // Add a space to work around the WebKit bug.
1210 s2 += ' '; 567 s2 += ' ';
1211 } 568 }
1212 return 'url("' + s2 + '")'; 569 return 'url("' + s2 + '")';
1213 } 570 }
1214 571
1215 /**
1216 * Parses query parameters from Location.
1217 * @param {Location} location The URL to generate the CSS url for.
1218 * @return {Object} Dictionary containing name value pairs for URL
1219 */
1220 function parseQueryParams(location) { 572 function parseQueryParams(location) {
1221 var params = {}; 573 var params = {};
1222 var query = unescape(location.search.substring(1)); 574 var query = unescape(location.search.substring(1));
1223 var vars = query.split('&'); 575 var vars = query.split('&');
1224 for (var i = 0; i < vars.length; i++) { 576 for (var i = 0; i < vars.length; i++) {
1225 var pair = vars[i].split('='); 577 var pair = vars[i].split('=');
1226 params[pair[0]] = pair[1]; 578 params[pair[0]] = pair[1];
1227 } 579 }
1228 return params; 580 return params;
1229 } 581 }
1230 582
1231 /**
1232 * Creates a new URL by appending or replacing the given query key and value.
1233 * Not supporting URL with username and password.
1234 * @param {Location} location The original URL.
1235 * @param {string} key The query parameter name.
1236 * @param {string} value The query parameter value.
1237 * @return {string} The constructed new URL.
1238 */
1239 function setQueryParam(location, key, value) { 583 function setQueryParam(location, key, value) {
1240 var query = parseQueryParams(location); 584 var query = parseQueryParams(location);
1241 query[encodeURIComponent(key)] = encodeURIComponent(value); 585 query[encodeURIComponent(key)] = encodeURIComponent(value);
1242
1243 var newQuery = ''; 586 var newQuery = '';
1244 for (var q in query) { 587 for (var q in query) {
1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; 588 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q];
1246 } 589 }
1247
1248 return location.origin + location.pathname + newQuery + location.hash; 590 return location.origin + location.pathname + newQuery + location.hash;
1249 } 591 }
1250 592
1251 /**
1252 * @param {Node} el A node to search for ancestors with |className|.
1253 * @param {string} className A class to search for.
1254 * @return {Element} A node with class of |className| or null if none is found.
1255 */
1256 function findAncestorByClass(el, className) { 593 function findAncestorByClass(el, className) {
1257 return /** @type {Element} */(findAncestor(el, function(el) { 594 return findAncestor(el, function(el) {
1258 return el.classList && el.classList.contains(className); 595 return el.classList && el.classList.contains(className);
1259 })); 596 });
1260 } 597 }
1261 598
1262 /**
1263 * Return the first ancestor for which the {@code predicate} returns true.
1264 * @param {Node} node The node to check.
1265 * @param {function(Node):boolean} predicate The function that tests the
1266 * nodes.
1267 * @return {Node} The found ancestor or null if not found.
1268 */
1269 function findAncestor(node, predicate) { 599 function findAncestor(node, predicate) {
1270 var last = false; 600 var last = false;
1271 while (node != null && !(last = predicate(node))) { 601 while (node != null && !(last = predicate(node))) {
1272 node = node.parentNode; 602 node = node.parentNode;
1273 } 603 }
1274 return last ? node : null; 604 return last ? node : null;
1275 } 605 }
1276 606
1277 function swapDomNodes(a, b) { 607 function swapDomNodes(a, b) {
1278 var afterA = a.nextSibling; 608 var afterA = a.nextSibling;
1279 if (afterA == b) { 609 if (afterA == b) {
1280 swapDomNodes(b, a); 610 swapDomNodes(b, a);
1281 return; 611 return;
1282 } 612 }
1283 var aParent = a.parentNode; 613 var aParent = a.parentNode;
1284 b.parentNode.replaceChild(a, b); 614 b.parentNode.replaceChild(a, b);
1285 aParent.insertBefore(b, afterA); 615 aParent.insertBefore(b, afterA);
1286 } 616 }
1287 617
1288 /**
1289 * Disables text selection and dragging, with optional whitelist callbacks.
1290 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
1291 * is defined and returns true, the onselectionstart event will be
1292 * surpressed.
1293 * @param {function(Event):boolean=} opt_allowDragStart Unless this function
1294 * is defined and returns true, the ondragstart event will be surpressed.
1295 */
1296 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { 618 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
1297 // Disable text selection.
1298 document.onselectstart = function(e) { 619 document.onselectstart = function(e) {
1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) 620 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) e.prevent Default();
1300 e.preventDefault();
1301 }; 621 };
1302
1303 // Disable dragging.
1304 document.ondragstart = function(e) { 622 document.ondragstart = function(e) {
1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) 623 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) e.preventDefa ult();
1306 e.preventDefault();
1307 }; 624 };
1308 } 625 }
1309 626
1310 /**
1311 * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead.
1312 * Call this to stop clicks on <a href="#"> links from scrolling to the top of
1313 * the page (and possibly showing a # in the link).
1314 */
1315 function preventDefaultOnPoundLinkClicks() { 627 function preventDefaultOnPoundLinkClicks() {
1316 document.addEventListener('click', function(e) { 628 document.addEventListener('click', function(e) {
1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { 629 var anchor = findAncestor(e.target, function(el) {
1318 return el.tagName == 'A'; 630 return el.tagName == 'A';
1319 }); 631 });
1320 // Use getAttribute() to prevent URL normalization. 632 if (anchor && anchor.getAttribute('href') == '#') e.preventDefault();
1321 if (anchor && anchor.getAttribute('href') == '#')
1322 e.preventDefault();
1323 }); 633 });
1324 } 634 }
1325 635
1326 /**
1327 * Check the directionality of the page.
1328 * @return {boolean} True if Chrome is running an RTL UI.
1329 */
1330 function isRTL() { 636 function isRTL() {
1331 return document.documentElement.dir == 'rtl'; 637 return document.documentElement.dir == 'rtl';
1332 } 638 }
1333 639
1334 /**
1335 * Get an element that's known to exist by its ID. We use this instead of just
1336 * calling getElementById and not checking the result because this lets us
1337 * satisfy the JSCompiler type system.
1338 * @param {string} id The identifier name.
1339 * @return {!HTMLElement} the Element.
1340 */
1341 function getRequiredElement(id) { 640 function getRequiredElement(id) {
1342 return assertInstanceof($(id), HTMLElement, 641 return assertInstanceof($(id), HTMLElement, 'Missing required element: ' + id) ;
1343 'Missing required element: ' + id); 642 }
1344 } 643
1345
1346 /**
1347 * Query an element that's known to exist by a selector. We use this instead of
1348 * just calling querySelector and not checking the result because this lets us
1349 * satisfy the JSCompiler type system.
1350 * @param {string} selectors CSS selectors to query the element.
1351 * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional
1352 * context object for querySelector.
1353 * @return {!HTMLElement} the Element.
1354 */
1355 function queryRequiredElement(selectors, opt_context) { 644 function queryRequiredElement(selectors, opt_context) {
1356 var element = (opt_context || document).querySelector(selectors); 645 var element = (opt_context || document).querySelector(selectors);
1357 return assertInstanceof(element, HTMLElement, 646 return assertInstanceof(element, HTMLElement, 'Missing required element: ' + s electors);
1358 'Missing required element: ' + selectors); 647 }
1359 } 648
1360
1361 // Handle click on a link. If the link points to a chrome: or file: url, then
1362 // call into the browser to do the navigation.
1363 document.addEventListener('click', function(e) { 649 document.addEventListener('click', function(e) {
1364 if (e.defaultPrevented) 650 if (e.defaultPrevented) return;
1365 return;
1366
1367 var el = e.target; 651 var el = e.target;
1368 if (el.nodeType == Node.ELEMENT_NODE && 652 if (el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector('A, A *')) {
1369 el.webkitMatchesSelector('A, A *')) {
1370 while (el.tagName != 'A') { 653 while (el.tagName != 'A') {
1371 el = el.parentElement; 654 el = el.parentElement;
1372 } 655 }
1373 656 if ((el.protocol == 'file:' || el.protocol == 'about:') && (e.button == 0 || e.button == 1)) {
1374 if ((el.protocol == 'file:' || el.protocol == 'about:') && 657 chrome.send('navigateToUrl', [ el.href, el.target, e.button, e.altKey, e.c trlKey, e.metaKey, e.shiftKey ]);
1375 (e.button == 0 || e.button == 1)) {
1376 chrome.send('navigateToUrl', [
1377 el.href,
1378 el.target,
1379 e.button,
1380 e.altKey,
1381 e.ctrlKey,
1382 e.metaKey,
1383 e.shiftKey
1384 ]);
1385 e.preventDefault(); 658 e.preventDefault();
1386 } 659 }
1387 } 660 }
1388 }); 661 });
1389 662
1390 /**
1391 * Creates a new URL which is the old URL with a GET param of key=value.
1392 * @param {string} url The base URL. There is not sanity checking on the URL so
1393 * it must be passed in a proper format.
1394 * @param {string} key The key of the param.
1395 * @param {string} value The value of the param.
1396 * @return {string} The new URL.
1397 */
1398 function appendParam(url, key, value) { 663 function appendParam(url, key, value) {
1399 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); 664 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
1400 665 if (url.indexOf('?') == -1) return url + '?' + param;
1401 if (url.indexOf('?') == -1)
1402 return url + '?' + param;
1403 return url + '&' + param; 666 return url + '&' + param;
1404 } 667 }
1405 668
1406 /**
1407 * Creates an element of a specified type with a specified class name.
1408 * @param {string} type The node type.
1409 * @param {string} className The class name to use.
1410 * @return {Element} The created element.
1411 */
1412 function createElementWithClassName(type, className) { 669 function createElementWithClassName(type, className) {
1413 var elm = document.createElement(type); 670 var elm = document.createElement(type);
1414 elm.className = className; 671 elm.className = className;
1415 return elm; 672 return elm;
1416 } 673 }
1417 674
1418 /**
1419 * webkitTransitionEnd does not always fire (e.g. when animation is aborted
1420 * or when no paint happens during the animation). This function sets up
1421 * a timer and emulate the event if it is not fired when the timer expires.
1422 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
1423 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
1424 * webkitTransitionEnd to happen. If not specified, it is fetched from |el|
1425 * using the transitionDuration style value.
1426 */
1427 function ensureTransitionEndEvent(el, opt_timeOut) { 675 function ensureTransitionEndEvent(el, opt_timeOut) {
1428 if (opt_timeOut === undefined) { 676 if (opt_timeOut === undefined) {
1429 var style = getComputedStyle(el); 677 var style = getComputedStyle(el);
1430 opt_timeOut = parseFloat(style.transitionDuration) * 1000; 678 opt_timeOut = parseFloat(style.transitionDuration) * 1e3;
1431
1432 // Give an additional 50ms buffer for the animation to complete.
1433 opt_timeOut += 50; 679 opt_timeOut += 50;
1434 } 680 }
1435
1436 var fired = false; 681 var fired = false;
1437 el.addEventListener('webkitTransitionEnd', function f(e) { 682 el.addEventListener('webkitTransitionEnd', function f(e) {
1438 el.removeEventListener('webkitTransitionEnd', f); 683 el.removeEventListener('webkitTransitionEnd', f);
1439 fired = true; 684 fired = true;
1440 }); 685 });
1441 window.setTimeout(function() { 686 window.setTimeout(function() {
1442 if (!fired) 687 if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
1443 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
1444 }, opt_timeOut); 688 }, opt_timeOut);
1445 } 689 }
1446 690
1447 /**
1448 * Alias for document.scrollTop getter.
1449 * @param {!HTMLDocument} doc The document node where information will be
1450 * queried from.
1451 * @return {number} The Y document scroll offset.
1452 */
1453 function scrollTopForDocument(doc) { 691 function scrollTopForDocument(doc) {
1454 return doc.documentElement.scrollTop || doc.body.scrollTop; 692 return doc.documentElement.scrollTop || doc.body.scrollTop;
1455 } 693 }
1456 694
1457 /**
1458 * Alias for document.scrollTop setter.
1459 * @param {!HTMLDocument} doc The document node where information will be
1460 * queried from.
1461 * @param {number} value The target Y scroll offset.
1462 */
1463 function setScrollTopForDocument(doc, value) { 695 function setScrollTopForDocument(doc, value) {
1464 doc.documentElement.scrollTop = doc.body.scrollTop = value; 696 doc.documentElement.scrollTop = doc.body.scrollTop = value;
1465 } 697 }
1466 698
1467 /**
1468 * Alias for document.scrollLeft getter.
1469 * @param {!HTMLDocument} doc The document node where information will be
1470 * queried from.
1471 * @return {number} The X document scroll offset.
1472 */
1473 function scrollLeftForDocument(doc) { 699 function scrollLeftForDocument(doc) {
1474 return doc.documentElement.scrollLeft || doc.body.scrollLeft; 700 return doc.documentElement.scrollLeft || doc.body.scrollLeft;
1475 } 701 }
1476 702
1477 /**
1478 * Alias for document.scrollLeft setter.
1479 * @param {!HTMLDocument} doc The document node where information will be
1480 * queried from.
1481 * @param {number} value The target X scroll offset.
1482 */
1483 function setScrollLeftForDocument(doc, value) { 703 function setScrollLeftForDocument(doc, value) {
1484 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; 704 doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
1485 } 705 }
1486 706
1487 /**
1488 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
1489 * @param {string} original The original string.
1490 * @return {string} The string with all the characters mentioned above replaced.
1491 */
1492 function HTMLEscape(original) { 707 function HTMLEscape(original) {
1493 return original.replace(/&/g, '&amp;') 708 return original.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&g t;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
1494 .replace(/</g, '&lt;') 709 }
1495 .replace(/>/g, '&gt;') 710
1496 .replace(/"/g, '&quot;')
1497 .replace(/'/g, '&#39;');
1498 }
1499
1500 /**
1501 * Shortens the provided string (if necessary) to a string of length at most
1502 * |maxLength|.
1503 * @param {string} original The original string.
1504 * @param {number} maxLength The maximum length allowed for the string.
1505 * @return {string} The original string if its length does not exceed
1506 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
1507 * appended.
1508 */
1509 function elide(original, maxLength) { 711 function elide(original, maxLength) {
1510 if (original.length <= maxLength) 712 if (original.length <= maxLength) return original;
1511 return original; 713 return original.substring(0, maxLength - 1) + '…';
1512 return original.substring(0, maxLength - 1) + '\u2026'; 714 }
1513 } 715
1514
1515 /**
1516 * Quote a string so it can be used in a regular expression.
1517 * @param {string} str The source string.
1518 * @return {string} The escaped string.
1519 */
1520 function quoteString(str) { 716 function quoteString(str) {
1521 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); 717 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
1522 } 718 }
1523 719
1524 // <if expr="is_ios"> 720 // <if expr="is_ios">
1525 // Polyfill 'key' in KeyboardEvent for iOS.
1526 // This function is not intended to be complete but should
1527 // be sufficient enough to have iOS work correctly while
1528 // it does not support key yet.
1529 if (!('key' in KeyboardEvent.prototype)) { 721 if (!('key' in KeyboardEvent.prototype)) {
1530 Object.defineProperty(KeyboardEvent.prototype, 'key', { 722 Object.defineProperty(KeyboardEvent.prototype, 'key', {
1531 /** @this {KeyboardEvent} */ 723 get: function() {
1532 get: function () { 724 if (this.keyCode >= 48 && this.keyCode <= 57) return String.fromCharCode(t his.keyCode);
1533 // 0-9 725 if (this.keyCode >= 65 && this.keyCode <= 90) {
1534 if (this.keyCode >= 0x30 && this.keyCode <= 0x39)
1535 return String.fromCharCode(this.keyCode);
1536
1537 // A-Z
1538 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) {
1539 var result = String.fromCharCode(this.keyCode).toLowerCase(); 726 var result = String.fromCharCode(this.keyCode).toLowerCase();
1540 if (this.shiftKey) 727 if (this.shiftKey) result = result.toUpperCase();
1541 result = result.toUpperCase();
1542 return result; 728 return result;
1543 } 729 }
1544 730 switch (this.keyCode) {
1545 // Special characters 731 case 8:
1546 switch(this.keyCode) { 732 return 'Backspace';
1547 case 0x08: return 'Backspace'; 733
1548 case 0x09: return 'Tab'; 734 case 9:
1549 case 0x0d: return 'Enter'; 735 return 'Tab';
1550 case 0x10: return 'Shift'; 736
1551 case 0x11: return 'Control'; 737 case 13:
1552 case 0x12: return 'Alt'; 738 return 'Enter';
1553 case 0x1b: return 'Escape'; 739
1554 case 0x20: return ' '; 740 case 16:
1555 case 0x21: return 'PageUp'; 741 return 'Shift';
1556 case 0x22: return 'PageDown'; 742
1557 case 0x23: return 'End'; 743 case 17:
1558 case 0x24: return 'Home'; 744 return 'Control';
1559 case 0x25: return 'ArrowLeft'; 745
1560 case 0x26: return 'ArrowUp'; 746 case 18:
1561 case 0x27: return 'ArrowRight'; 747 return 'Alt';
1562 case 0x28: return 'ArrowDown'; 748
1563 case 0x2d: return 'Insert'; 749 case 27:
1564 case 0x2e: return 'Delete'; 750 return 'Escape';
1565 case 0x5b: return 'Meta'; 751
1566 case 0x70: return 'F1'; 752 case 32:
1567 case 0x71: return 'F2'; 753 return ' ';
1568 case 0x72: return 'F3'; 754
1569 case 0x73: return 'F4'; 755 case 33:
1570 case 0x74: return 'F5'; 756 return 'PageUp';
1571 case 0x75: return 'F6'; 757
1572 case 0x76: return 'F7'; 758 case 34:
1573 case 0x77: return 'F8'; 759 return 'PageDown';
1574 case 0x78: return 'F9'; 760
1575 case 0x79: return 'F10'; 761 case 35:
1576 case 0x7a: return 'F11'; 762 return 'End';
1577 case 0x7b: return 'F12'; 763
1578 case 0xbb: return '='; 764 case 36:
1579 case 0xbd: return '-'; 765 return 'Home';
1580 case 0xdb: return '['; 766
1581 case 0xdd: return ']'; 767 case 37:
768 return 'ArrowLeft';
769
770 case 38:
771 return 'ArrowUp';
772
773 case 39:
774 return 'ArrowRight';
775
776 case 40:
777 return 'ArrowDown';
778
779 case 45:
780 return 'Insert';
781
782 case 46:
783 return 'Delete';
784
785 case 91:
786 return 'Meta';
787
788 case 112:
789 return 'F1';
790
791 case 113:
792 return 'F2';
793
794 case 114:
795 return 'F3';
796
797 case 115:
798 return 'F4';
799
800 case 116:
801 return 'F5';
802
803 case 117:
804 return 'F6';
805
806 case 118:
807 return 'F7';
808
809 case 119:
810 return 'F8';
811
812 case 120:
813 return 'F9';
814
815 case 121:
816 return 'F10';
817
818 case 122:
819 return 'F11';
820
821 case 123:
822 return 'F12';
823
824 case 187:
825 return '=';
826
827 case 189:
828 return '-';
829
830 case 219:
831 return '[';
832
833 case 221:
834 return ']';
1582 } 835 }
1583 return 'Unidentified'; 836 return 'Unidentified';
1584 } 837 }
1585 }); 838 });
1586 } else { 839 } else {
1587 window.console.log("KeyboardEvent.Key polyfill not required"); 840 window.console.log("KeyboardEvent.Key polyfill not required");
1588 } 841 }
842
1589 // </if> /* is_ios */ 843 // </if> /* is_ios */
1590 /** 844 Polymer.IronResizableBehavior = {
1591 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to 845 properties: {
1592 * coordinate the flow of resize events between "resizers" (elements that cont rol the 846 _parentResizable: {
1593 * size or hidden state of their children) and "resizables" (elements that nee d to be 847 type: Object,
1594 * notified when they are resized or un-hidden by their parents in order to ta ke 848 observer: '_parentResizableChanged'
1595 * action on their new measurements). 849 },
1596 * 850 _notifyingDescendant: {
1597 * Elements that perform measurement should add the `IronResizableBehavior` be havior to 851 type: Boolean,
1598 * their element definition and listen for the `iron-resize` event on themselv es. 852 value: false
1599 * This event will be fired when they become showing after having been hidden, 853 }
1600 * when they are resized explicitly by another resizable, or when the window h as been 854 },
1601 * resized. 855 listeners: {
1602 * 856 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
1603 * Note, the `iron-resize` event is non-bubbling. 857 },
1604 * 858 created: function() {
1605 * @polymerBehavior Polymer.IronResizableBehavior 859 this._interestedResizables = [];
1606 * @demo demo/index.html 860 this._boundNotifyResize = this.notifyResize.bind(this);
1607 **/ 861 },
1608 Polymer.IronResizableBehavior = { 862 attached: function() {
863 this.fire('iron-request-resize-notifications', null, {
864 node: this,
865 bubbles: true,
866 cancelable: true
867 });
868 if (!this._parentResizable) {
869 window.addEventListener('resize', this._boundNotifyResize);
870 this.notifyResize();
871 }
872 },
873 detached: function() {
874 if (this._parentResizable) {
875 this._parentResizable.stopResizeNotificationsFor(this);
876 } else {
877 window.removeEventListener('resize', this._boundNotifyResize);
878 }
879 this._parentResizable = null;
880 },
881 notifyResize: function() {
882 if (!this.isAttached) {
883 return;
884 }
885 this._interestedResizables.forEach(function(resizable) {
886 if (this.resizerShouldNotify(resizable)) {
887 this._notifyDescendant(resizable);
888 }
889 }, this);
890 this._fireResize();
891 },
892 assignParentResizable: function(parentResizable) {
893 this._parentResizable = parentResizable;
894 },
895 stopResizeNotificationsFor: function(target) {
896 var index = this._interestedResizables.indexOf(target);
897 if (index > -1) {
898 this._interestedResizables.splice(index, 1);
899 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
900 }
901 },
902 resizerShouldNotify: function(element) {
903 return true;
904 },
905 _onDescendantIronResize: function(event) {
906 if (this._notifyingDescendant) {
907 event.stopPropagation();
908 return;
909 }
910 if (!Polymer.Settings.useShadow) {
911 this._fireResize();
912 }
913 },
914 _fireResize: function() {
915 this.fire('iron-resize', null, {
916 node: this,
917 bubbles: false
918 });
919 },
920 _onIronRequestResizeNotifications: function(event) {
921 var target = event.path ? event.path[0] : event.target;
922 if (target === this) {
923 return;
924 }
925 if (this._interestedResizables.indexOf(target) === -1) {
926 this._interestedResizables.push(target);
927 this.listen(target, 'iron-resize', '_onDescendantIronResize');
928 }
929 target.assignParentResizable(this);
930 this._notifyDescendant(target);
931 event.stopPropagation();
932 },
933 _parentResizableChanged: function(parentResizable) {
934 if (parentResizable) {
935 window.removeEventListener('resize', this._boundNotifyResize);
936 }
937 },
938 _notifyDescendant: function(descendant) {
939 if (!this.isAttached) {
940 return;
941 }
942 this._notifyingDescendant = true;
943 descendant.notifyResize();
944 this._notifyingDescendant = false;
945 }
946 };
947
948 (function() {
949 'use strict';
950 var KEY_IDENTIFIER = {
951 'U+0008': 'backspace',
952 'U+0009': 'tab',
953 'U+001B': 'esc',
954 'U+0020': 'space',
955 'U+007F': 'del'
956 };
957 var KEY_CODE = {
958 8: 'backspace',
959 9: 'tab',
960 13: 'enter',
961 27: 'esc',
962 33: 'pageup',
963 34: 'pagedown',
964 35: 'end',
965 36: 'home',
966 32: 'space',
967 37: 'left',
968 38: 'up',
969 39: 'right',
970 40: 'down',
971 46: 'del',
972 106: '*'
973 };
974 var MODIFIER_KEYS = {
975 shift: 'shiftKey',
976 ctrl: 'ctrlKey',
977 alt: 'altKey',
978 meta: 'metaKey'
979 };
980 var KEY_CHAR = /[a-z0-9*]/;
981 var IDENT_CHAR = /U\+/;
982 var ARROW_KEY = /^arrow/;
983 var SPACE_KEY = /^space(bar)?/;
984 var ESC_KEY = /^escape$/;
985 function transformKey(key, noSpecialChars) {
986 var validKey = '';
987 if (key) {
988 var lKey = key.toLowerCase();
989 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
990 validKey = 'space';
991 } else if (ESC_KEY.test(lKey)) {
992 validKey = 'esc';
993 } else if (lKey.length == 1) {
994 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
995 validKey = lKey;
996 }
997 } else if (ARROW_KEY.test(lKey)) {
998 validKey = lKey.replace('arrow', '');
999 } else if (lKey == 'multiply') {
1000 validKey = '*';
1001 } else {
1002 validKey = lKey;
1003 }
1004 }
1005 return validKey;
1006 }
1007 function transformKeyIdentifier(keyIdent) {
1008 var validKey = '';
1009 if (keyIdent) {
1010 if (keyIdent in KEY_IDENTIFIER) {
1011 validKey = KEY_IDENTIFIER[keyIdent];
1012 } else if (IDENT_CHAR.test(keyIdent)) {
1013 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1014 validKey = String.fromCharCode(keyIdent).toLowerCase();
1015 } else {
1016 validKey = keyIdent.toLowerCase();
1017 }
1018 }
1019 return validKey;
1020 }
1021 function transformKeyCode(keyCode) {
1022 var validKey = '';
1023 if (Number(keyCode)) {
1024 if (keyCode >= 65 && keyCode <= 90) {
1025 validKey = String.fromCharCode(32 + keyCode);
1026 } else if (keyCode >= 112 && keyCode <= 123) {
1027 validKey = 'f' + (keyCode - 112);
1028 } else if (keyCode >= 48 && keyCode <= 57) {
1029 validKey = String(keyCode - 48);
1030 } else if (keyCode >= 96 && keyCode <= 105) {
1031 validKey = String(keyCode - 96);
1032 } else {
1033 validKey = KEY_CODE[keyCode];
1034 }
1035 }
1036 return validKey;
1037 }
1038 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1039 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier( keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
1040 }
1041 function keyComboMatchesEvent(keyCombo, event) {
1042 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1043 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event .altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey);
1044 }
1045 function parseKeyComboString(keyComboString) {
1046 if (keyComboString.length === 1) {
1047 return {
1048 combo: keyComboString,
1049 key: keyComboString,
1050 event: 'keydown'
1051 };
1052 }
1053 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar t) {
1054 var eventParts = keyComboPart.split(':');
1055 var keyName = eventParts[0];
1056 var event = eventParts[1];
1057 if (keyName in MODIFIER_KEYS) {
1058 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1059 parsedKeyCombo.hasModifiers = true;
1060 } else {
1061 parsedKeyCombo.key = keyName;
1062 parsedKeyCombo.event = event || 'keydown';
1063 }
1064 return parsedKeyCombo;
1065 }, {
1066 combo: keyComboString.split(':').shift()
1067 });
1068 }
1069 function parseEventString(eventString) {
1070 return eventString.trim().split(' ').map(function(keyComboString) {
1071 return parseKeyComboString(keyComboString);
1072 });
1073 }
1074 Polymer.IronA11yKeysBehavior = {
1609 properties: { 1075 properties: {
1610 /** 1076 keyEventTarget: {
1611 * The closest ancestor element that implements `IronResizableBehavior`.
1612 */
1613 _parentResizable: {
1614 type: Object, 1077 type: Object,
1615 observer: '_parentResizableChanged' 1078 value: function() {
1616 }, 1079 return this;
1617 1080 }
1618 /** 1081 },
1619 * True if this element is currently notifying its descedant elements of 1082 stopKeyboardEventPropagation: {
1620 * resize.
1621 */
1622 _notifyingDescendant: {
1623 type: Boolean, 1083 type: Boolean,
1624 value: false 1084 value: false
1625 } 1085 },
1626 }, 1086 _boundKeyHandlers: {
1627 1087 type: Array,
1628 listeners: { 1088 value: function() {
1629 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' 1089 return [];
1630 }, 1090 }
1631 1091 },
1632 created: function() { 1092 _imperativeKeyBindings: {
1633 // We don't really need property effects on these, and also we want them 1093 type: Object,
1634 // to be created before the `_parentResizable` observer fires: 1094 value: function() {
1635 this._interestedResizables = []; 1095 return {};
1636 this._boundNotifyResize = this.notifyResize.bind(this); 1096 }
1637 }, 1097 }
1638 1098 },
1099 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ],
1100 keyBindings: {},
1101 registered: function() {
1102 this._prepKeyBindings();
1103 },
1639 attached: function() { 1104 attached: function() {
1640 this.fire('iron-request-resize-notifications', null, { 1105 this._listenKeyEventListeners();
1641 node: this, 1106 },
1642 bubbles: true, 1107 detached: function() {
1108 this._unlistenKeyEventListeners();
1109 },
1110 addOwnKeyBinding: function(eventString, handlerName) {
1111 this._imperativeKeyBindings[eventString] = handlerName;
1112 this._prepKeyBindings();
1113 this._resetKeyEventListeners();
1114 },
1115 removeOwnKeyBindings: function() {
1116 this._imperativeKeyBindings = {};
1117 this._prepKeyBindings();
1118 this._resetKeyEventListeners();
1119 },
1120 keyboardEventMatchesKeys: function(event, eventString) {
1121 var keyCombos = parseEventString(eventString);
1122 for (var i = 0; i < keyCombos.length; ++i) {
1123 if (keyComboMatchesEvent(keyCombos[i], event)) {
1124 return true;
1125 }
1126 }
1127 return false;
1128 },
1129 _collectKeyBindings: function() {
1130 var keyBindings = this.behaviors.map(function(behavior) {
1131 return behavior.keyBindings;
1132 });
1133 if (keyBindings.indexOf(this.keyBindings) === -1) {
1134 keyBindings.push(this.keyBindings);
1135 }
1136 return keyBindings;
1137 },
1138 _prepKeyBindings: function() {
1139 this._keyBindings = {};
1140 this._collectKeyBindings().forEach(function(keyBindings) {
1141 for (var eventString in keyBindings) {
1142 this._addKeyBinding(eventString, keyBindings[eventString]);
1143 }
1144 }, this);
1145 for (var eventString in this._imperativeKeyBindings) {
1146 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString ]);
1147 }
1148 for (var eventName in this._keyBindings) {
1149 this._keyBindings[eventName].sort(function(kb1, kb2) {
1150 var b1 = kb1[0].hasModifiers;
1151 var b2 = kb2[0].hasModifiers;
1152 return b1 === b2 ? 0 : b1 ? -1 : 1;
1153 });
1154 }
1155 },
1156 _addKeyBinding: function(eventString, handlerName) {
1157 parseEventString(eventString).forEach(function(keyCombo) {
1158 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] || [];
1159 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]);
1160 }, this);
1161 },
1162 _resetKeyEventListeners: function() {
1163 this._unlistenKeyEventListeners();
1164 if (this.isAttached) {
1165 this._listenKeyEventListeners();
1166 }
1167 },
1168 _listenKeyEventListeners: function() {
1169 if (!this.keyEventTarget) {
1170 return;
1171 }
1172 Object.keys(this._keyBindings).forEach(function(eventName) {
1173 var keyBindings = this._keyBindings[eventName];
1174 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
1175 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa ndler ]);
1176 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
1177 }, this);
1178 },
1179 _unlistenKeyEventListeners: function() {
1180 var keyHandlerTuple;
1181 var keyEventTarget;
1182 var eventName;
1183 var boundKeyHandler;
1184 while (this._boundKeyHandlers.length) {
1185 keyHandlerTuple = this._boundKeyHandlers.pop();
1186 keyEventTarget = keyHandlerTuple[0];
1187 eventName = keyHandlerTuple[1];
1188 boundKeyHandler = keyHandlerTuple[2];
1189 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
1190 }
1191 },
1192 _onKeyBindingEvent: function(keyBindings, event) {
1193 if (this.stopKeyboardEventPropagation) {
1194 event.stopPropagation();
1195 }
1196 if (event.defaultPrevented) {
1197 return;
1198 }
1199 for (var i = 0; i < keyBindings.length; i++) {
1200 var keyCombo = keyBindings[i][0];
1201 var handlerName = keyBindings[i][1];
1202 if (keyComboMatchesEvent(keyCombo, event)) {
1203 this._triggerKeyHandler(keyCombo, handlerName, event);
1204 if (event.defaultPrevented) {
1205 return;
1206 }
1207 }
1208 }
1209 },
1210 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
1211 var detail = Object.create(keyCombo);
1212 detail.keyboardEvent = keyboardEvent;
1213 var event = new CustomEvent(keyCombo.event, {
1214 detail: detail,
1643 cancelable: true 1215 cancelable: true
1644 }); 1216 });
1645 1217 this[handlerName].call(this, event);
1646 if (!this._parentResizable) { 1218 if (event.defaultPrevented) {
1647 window.addEventListener('resize', this._boundNotifyResize); 1219 keyboardEvent.preventDefault();
1648 this.notifyResize(); 1220 }
1649 }
1650 },
1651
1652 detached: function() {
1653 if (this._parentResizable) {
1654 this._parentResizable.stopResizeNotificationsFor(this);
1655 } else {
1656 window.removeEventListener('resize', this._boundNotifyResize);
1657 }
1658
1659 this._parentResizable = null;
1660 },
1661
1662 /**
1663 * Can be called to manually notify a resizable and its descendant
1664 * resizables of a resize change.
1665 */
1666 notifyResize: function() {
1667 if (!this.isAttached) {
1668 return;
1669 }
1670
1671 this._interestedResizables.forEach(function(resizable) {
1672 if (this.resizerShouldNotify(resizable)) {
1673 this._notifyDescendant(resizable);
1674 }
1675 }, this);
1676
1677 this._fireResize();
1678 },
1679
1680 /**
1681 * Used to assign the closest resizable ancestor to this resizable
1682 * if the ancestor detects a request for notifications.
1683 */
1684 assignParentResizable: function(parentResizable) {
1685 this._parentResizable = parentResizable;
1686 },
1687
1688 /**
1689 * Used to remove a resizable descendant from the list of descendants
1690 * that should be notified of a resize change.
1691 */
1692 stopResizeNotificationsFor: function(target) {
1693 var index = this._interestedResizables.indexOf(target);
1694
1695 if (index > -1) {
1696 this._interestedResizables.splice(index, 1);
1697 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
1698 }
1699 },
1700
1701 /**
1702 * This method can be overridden to filter nested elements that should or
1703 * should not be notified by the current element. Return true if an element
1704 * should be notified, or false if it should not be notified.
1705 *
1706 * @param {HTMLElement} element A candidate descendant element that
1707 * implements `IronResizableBehavior`.
1708 * @return {boolean} True if the `element` should be notified of resize.
1709 */
1710 resizerShouldNotify: function(element) { return true; },
1711
1712 _onDescendantIronResize: function(event) {
1713 if (this._notifyingDescendant) {
1714 event.stopPropagation();
1715 return;
1716 }
1717
1718 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
1719 // otherwise non-bubbling event "just work." We do it manually here for
1720 // the case where Polymer is not using shadow roots for whatever reason:
1721 if (!Polymer.Settings.useShadow) {
1722 this._fireResize();
1723 }
1724 },
1725
1726 _fireResize: function() {
1727 this.fire('iron-resize', null, {
1728 node: this,
1729 bubbles: false
1730 });
1731 },
1732
1733 _onIronRequestResizeNotifications: function(event) {
1734 var target = event.path ? event.path[0] : event.target;
1735
1736 if (target === this) {
1737 return;
1738 }
1739
1740 if (this._interestedResizables.indexOf(target) === -1) {
1741 this._interestedResizables.push(target);
1742 this.listen(target, 'iron-resize', '_onDescendantIronResize');
1743 }
1744
1745 target.assignParentResizable(this);
1746 this._notifyDescendant(target);
1747
1748 event.stopPropagation();
1749 },
1750
1751 _parentResizableChanged: function(parentResizable) {
1752 if (parentResizable) {
1753 window.removeEventListener('resize', this._boundNotifyResize);
1754 }
1755 },
1756
1757 _notifyDescendant: function(descendant) {
1758 // NOTE(cdata): In IE10, attached is fired on children first, so it's
1759 // important not to notify them if the parent is not attached yet (or
1760 // else they will get redundantly notified when the parent attaches).
1761 if (!this.isAttached) {
1762 return;
1763 }
1764
1765 this._notifyingDescendant = true;
1766 descendant.notifyResize();
1767 this._notifyingDescendant = false;
1768 } 1221 }
1769 }; 1222 };
1223 })();
1224
1225 Polymer.IronScrollTargetBehavior = {
1226 properties: {
1227 scrollTarget: {
1228 type: HTMLElement,
1229 value: function() {
1230 return this._defaultScrollTarget;
1231 }
1232 }
1233 },
1234 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ],
1235 _scrollTargetChanged: function(scrollTarget, isAttached) {
1236 var eventTarget;
1237 if (this._oldScrollTarget) {
1238 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro llTarget;
1239 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
1240 this._oldScrollTarget = null;
1241 }
1242 if (!isAttached) {
1243 return;
1244 }
1245 if (scrollTarget === 'document') {
1246 this.scrollTarget = this._doc;
1247 } else if (typeof scrollTarget === 'string') {
1248 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer. dom(this.ownerDocument).querySelector('#' + scrollTarget);
1249 } else if (this._isValidScrollTarget()) {
1250 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
1251 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler .bind(this);
1252 this._oldScrollTarget = scrollTarget;
1253 eventTarget.addEventListener('scroll', this._boundScrollHandler);
1254 }
1255 },
1256 _scrollHandler: function scrollHandler() {},
1257 get _defaultScrollTarget() {
1258 return this._doc;
1259 },
1260 get _doc() {
1261 return this.ownerDocument.documentElement;
1262 },
1263 get _scrollTop() {
1264 if (this._isValidScrollTarget()) {
1265 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT arget.scrollTop;
1266 }
1267 return 0;
1268 },
1269 get _scrollLeft() {
1270 if (this._isValidScrollTarget()) {
1271 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT arget.scrollLeft;
1272 }
1273 return 0;
1274 },
1275 set _scrollTop(top) {
1276 if (this.scrollTarget === this._doc) {
1277 window.scrollTo(window.pageXOffset, top);
1278 } else if (this._isValidScrollTarget()) {
1279 this.scrollTarget.scrollTop = top;
1280 }
1281 },
1282 set _scrollLeft(left) {
1283 if (this.scrollTarget === this._doc) {
1284 window.scrollTo(left, window.pageYOffset);
1285 } else if (this._isValidScrollTarget()) {
1286 this.scrollTarget.scrollLeft = left;
1287 }
1288 },
1289 scroll: function(left, top) {
1290 if (this.scrollTarget === this._doc) {
1291 window.scrollTo(left, top);
1292 } else if (this._isValidScrollTarget()) {
1293 this.scrollTarget.scrollLeft = left;
1294 this.scrollTarget.scrollTop = top;
1295 }
1296 },
1297 get _scrollTargetWidth() {
1298 if (this._isValidScrollTarget()) {
1299 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa rget.offsetWidth;
1300 }
1301 return 0;
1302 },
1303 get _scrollTargetHeight() {
1304 if (this._isValidScrollTarget()) {
1305 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT arget.offsetHeight;
1306 }
1307 return 0;
1308 },
1309 _isValidScrollTarget: function() {
1310 return this.scrollTarget instanceof HTMLElement;
1311 }
1312 };
1313
1770 (function() { 1314 (function() {
1771 'use strict';
1772
1773 /**
1774 * Chrome uses an older version of DOM Level 3 Keyboard Events
1775 *
1776 * Most keys are labeled as text, but some are Unicode codepoints.
1777 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set
1778 */
1779 var KEY_IDENTIFIER = {
1780 'U+0008': 'backspace',
1781 'U+0009': 'tab',
1782 'U+001B': 'esc',
1783 'U+0020': 'space',
1784 'U+007F': 'del'
1785 };
1786
1787 /**
1788 * Special table for KeyboardEvent.keyCode.
1789 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er
1790 * than that.
1791 *
1792 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode
1793 */
1794 var KEY_CODE = {
1795 8: 'backspace',
1796 9: 'tab',
1797 13: 'enter',
1798 27: 'esc',
1799 33: 'pageup',
1800 34: 'pagedown',
1801 35: 'end',
1802 36: 'home',
1803 32: 'space',
1804 37: 'left',
1805 38: 'up',
1806 39: 'right',
1807 40: 'down',
1808 46: 'del',
1809 106: '*'
1810 };
1811
1812 /**
1813 * MODIFIER_KEYS maps the short name for modifier keys used in a key
1814 * combo string to the property name that references those same keys
1815 * in a KeyboardEvent instance.
1816 */
1817 var MODIFIER_KEYS = {
1818 'shift': 'shiftKey',
1819 'ctrl': 'ctrlKey',
1820 'alt': 'altKey',
1821 'meta': 'metaKey'
1822 };
1823
1824 /**
1825 * KeyboardEvent.key is mostly represented by printable character made by
1826 * the keyboard, with unprintable keys labeled nicely.
1827 *
1828 * However, on OS X, Alt+char can make a Unicode character that follows an
1829 * Apple-specific mapping. In this case, we fall back to .keyCode.
1830 */
1831 var KEY_CHAR = /[a-z0-9*]/;
1832
1833 /**
1834 * Matches a keyIdentifier string.
1835 */
1836 var IDENT_CHAR = /U\+/;
1837
1838 /**
1839 * Matches arrow keys in Gecko 27.0+
1840 */
1841 var ARROW_KEY = /^arrow/;
1842
1843 /**
1844 * Matches space keys everywhere (notably including IE10's exceptional name
1845 * `spacebar`).
1846 */
1847 var SPACE_KEY = /^space(bar)?/;
1848
1849 /**
1850 * Matches ESC key.
1851 *
1852 * Value from: http://w3c.github.io/uievents-key/#key-Escape
1853 */
1854 var ESC_KEY = /^escape$/;
1855
1856 /**
1857 * Transforms the key.
1858 * @param {string} key The KeyBoardEvent.key
1859 * @param {Boolean} [noSpecialChars] Limits the transformation to
1860 * alpha-numeric characters.
1861 */
1862 function transformKey(key, noSpecialChars) {
1863 var validKey = '';
1864 if (key) {
1865 var lKey = key.toLowerCase();
1866 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
1867 validKey = 'space';
1868 } else if (ESC_KEY.test(lKey)) {
1869 validKey = 'esc';
1870 } else if (lKey.length == 1) {
1871 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
1872 validKey = lKey;
1873 }
1874 } else if (ARROW_KEY.test(lKey)) {
1875 validKey = lKey.replace('arrow', '');
1876 } else if (lKey == 'multiply') {
1877 // numpad '*' can map to Multiply on IE/Windows
1878 validKey = '*';
1879 } else {
1880 validKey = lKey;
1881 }
1882 }
1883 return validKey;
1884 }
1885
1886 function transformKeyIdentifier(keyIdent) {
1887 var validKey = '';
1888 if (keyIdent) {
1889 if (keyIdent in KEY_IDENTIFIER) {
1890 validKey = KEY_IDENTIFIER[keyIdent];
1891 } else if (IDENT_CHAR.test(keyIdent)) {
1892 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1893 validKey = String.fromCharCode(keyIdent).toLowerCase();
1894 } else {
1895 validKey = keyIdent.toLowerCase();
1896 }
1897 }
1898 return validKey;
1899 }
1900
1901 function transformKeyCode(keyCode) {
1902 var validKey = '';
1903 if (Number(keyCode)) {
1904 if (keyCode >= 65 && keyCode <= 90) {
1905 // ascii a-z
1906 // lowercase is 32 offset from uppercase
1907 validKey = String.fromCharCode(32 + keyCode);
1908 } else if (keyCode >= 112 && keyCode <= 123) {
1909 // function keys f1-f12
1910 validKey = 'f' + (keyCode - 112);
1911 } else if (keyCode >= 48 && keyCode <= 57) {
1912 // top 0-9 keys
1913 validKey = String(keyCode - 48);
1914 } else if (keyCode >= 96 && keyCode <= 105) {
1915 // num pad 0-9
1916 validKey = String(keyCode - 96);
1917 } else {
1918 validKey = KEY_CODE[keyCode];
1919 }
1920 }
1921 return validKey;
1922 }
1923
1924 /**
1925 * Calculates the normalized key for a KeyboardEvent.
1926 * @param {KeyboardEvent} keyEvent
1927 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
1928 * transformation to alpha-numeric chars. This is useful with key
1929 * combinations like shift + 2, which on FF for MacOS produces
1930 * keyEvent.key = @
1931 * To get 2 returned, set noSpecialChars = true
1932 * To get @ returned, set noSpecialChars = false
1933 */
1934 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1935 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
1936 // .detail.key to support artificial keyboard events.
1937 return transformKey(keyEvent.key, noSpecialChars) ||
1938 transformKeyIdentifier(keyEvent.keyIdentifier) ||
1939 transformKeyCode(keyEvent.keyCode) ||
1940 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
1941 }
1942
1943 function keyComboMatchesEvent(keyCombo, event) {
1944 // For combos with modifiers we support only alpha-numeric keys
1945 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1946 return keyEvent === keyCombo.key &&
1947 (!keyCombo.hasModifiers || (
1948 !!event.shiftKey === !!keyCombo.shiftKey &&
1949 !!event.ctrlKey === !!keyCombo.ctrlKey &&
1950 !!event.altKey === !!keyCombo.altKey &&
1951 !!event.metaKey === !!keyCombo.metaKey)
1952 );
1953 }
1954
1955 function parseKeyComboString(keyComboString) {
1956 if (keyComboString.length === 1) {
1957 return {
1958 combo: keyComboString,
1959 key: keyComboString,
1960 event: 'keydown'
1961 };
1962 }
1963 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP art) {
1964 var eventParts = keyComboPart.split(':');
1965 var keyName = eventParts[0];
1966 var event = eventParts[1];
1967
1968 if (keyName in MODIFIER_KEYS) {
1969 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1970 parsedKeyCombo.hasModifiers = true;
1971 } else {
1972 parsedKeyCombo.key = keyName;
1973 parsedKeyCombo.event = event || 'keydown';
1974 }
1975
1976 return parsedKeyCombo;
1977 }, {
1978 combo: keyComboString.split(':').shift()
1979 });
1980 }
1981
1982 function parseEventString(eventString) {
1983 return eventString.trim().split(' ').map(function(keyComboString) {
1984 return parseKeyComboString(keyComboString);
1985 });
1986 }
1987
1988 /**
1989 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
1990 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
1991 * The element takes care of browser differences with respect to Keyboard ev ents
1992 * and uses an expressive syntax to filter key presses.
1993 *
1994 * Use the `keyBindings` prototype property to express what combination of k eys
1995 * will trigger the callback. A key binding has the format
1996 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
1997 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
1998 *
1999 * keyBindings: {
2000 * 'space': '_onKeydown', // same as 'space:keydown'
2001 * 'shift+tab': '_onKeydown',
2002 * 'enter:keypress': '_onKeypress',
2003 * 'esc:keyup': '_onKeyup'
2004 * }
2005 *
2006 * The callback will receive with an event containing the following informat ion in `event.detail`:
2007 *
2008 * _onKeydown: function(event) {
2009 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
2010 * console.log(event.detail.key); // KEY only, e.g. "tab"
2011 * console.log(event.detail.event); // EVENT, e.g. "keydown"
2012 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
2013 * }
2014 *
2015 * Use the `keyEventTarget` attribute to set up event handlers on a specific
2016 * node.
2017 *
2018 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
2019 * for an example.
2020 *
2021 * @demo demo/index.html
2022 * @polymerBehavior
2023 */
2024 Polymer.IronA11yKeysBehavior = {
2025 properties: {
2026 /**
2027 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
2028 * `null` to disable the listeners.
2029 * @type {?EventTarget}
2030 */
2031 keyEventTarget: {
2032 type: Object,
2033 value: function() {
2034 return this;
2035 }
2036 },
2037
2038 /**
2039 * If true, this property will cause the implementing element to
2040 * automatically stop propagation on any handled KeyboardEvents.
2041 */
2042 stopKeyboardEventPropagation: {
2043 type: Boolean,
2044 value: false
2045 },
2046
2047 _boundKeyHandlers: {
2048 type: Array,
2049 value: function() {
2050 return [];
2051 }
2052 },
2053
2054 // We use this due to a limitation in IE10 where instances will have
2055 // own properties of everything on the "prototype".
2056 _imperativeKeyBindings: {
2057 type: Object,
2058 value: function() {
2059 return {};
2060 }
2061 }
2062 },
2063
2064 observers: [
2065 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
2066 ],
2067
2068
2069 /**
2070 * To be used to express what combination of keys will trigger the relati ve
2071 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
2072 * @type {Object}
2073 */
2074 keyBindings: {},
2075
2076 registered: function() {
2077 this._prepKeyBindings();
2078 },
2079
2080 attached: function() {
2081 this._listenKeyEventListeners();
2082 },
2083
2084 detached: function() {
2085 this._unlistenKeyEventListeners();
2086 },
2087
2088 /**
2089 * Can be used to imperatively add a key binding to the implementing
2090 * element. This is the imperative equivalent of declaring a keybinding
2091 * in the `keyBindings` prototype property.
2092 */
2093 addOwnKeyBinding: function(eventString, handlerName) {
2094 this._imperativeKeyBindings[eventString] = handlerName;
2095 this._prepKeyBindings();
2096 this._resetKeyEventListeners();
2097 },
2098
2099 /**
2100 * When called, will remove all imperatively-added key bindings.
2101 */
2102 removeOwnKeyBindings: function() {
2103 this._imperativeKeyBindings = {};
2104 this._prepKeyBindings();
2105 this._resetKeyEventListeners();
2106 },
2107
2108 /**
2109 * Returns true if a keyboard event matches `eventString`.
2110 *
2111 * @param {KeyboardEvent} event
2112 * @param {string} eventString
2113 * @return {boolean}
2114 */
2115 keyboardEventMatchesKeys: function(event, eventString) {
2116 var keyCombos = parseEventString(eventString);
2117 for (var i = 0; i < keyCombos.length; ++i) {
2118 if (keyComboMatchesEvent(keyCombos[i], event)) {
2119 return true;
2120 }
2121 }
2122 return false;
2123 },
2124
2125 _collectKeyBindings: function() {
2126 var keyBindings = this.behaviors.map(function(behavior) {
2127 return behavior.keyBindings;
2128 });
2129
2130 if (keyBindings.indexOf(this.keyBindings) === -1) {
2131 keyBindings.push(this.keyBindings);
2132 }
2133
2134 return keyBindings;
2135 },
2136
2137 _prepKeyBindings: function() {
2138 this._keyBindings = {};
2139
2140 this._collectKeyBindings().forEach(function(keyBindings) {
2141 for (var eventString in keyBindings) {
2142 this._addKeyBinding(eventString, keyBindings[eventString]);
2143 }
2144 }, this);
2145
2146 for (var eventString in this._imperativeKeyBindings) {
2147 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
2148 }
2149
2150 // Give precedence to combos with modifiers to be checked first.
2151 for (var eventName in this._keyBindings) {
2152 this._keyBindings[eventName].sort(function (kb1, kb2) {
2153 var b1 = kb1[0].hasModifiers;
2154 var b2 = kb2[0].hasModifiers;
2155 return (b1 === b2) ? 0 : b1 ? -1 : 1;
2156 })
2157 }
2158 },
2159
2160 _addKeyBinding: function(eventString, handlerName) {
2161 parseEventString(eventString).forEach(function(keyCombo) {
2162 this._keyBindings[keyCombo.event] =
2163 this._keyBindings[keyCombo.event] || [];
2164
2165 this._keyBindings[keyCombo.event].push([
2166 keyCombo,
2167 handlerName
2168 ]);
2169 }, this);
2170 },
2171
2172 _resetKeyEventListeners: function() {
2173 this._unlistenKeyEventListeners();
2174
2175 if (this.isAttached) {
2176 this._listenKeyEventListeners();
2177 }
2178 },
2179
2180 _listenKeyEventListeners: function() {
2181 if (!this.keyEventTarget) {
2182 return;
2183 }
2184 Object.keys(this._keyBindings).forEach(function(eventName) {
2185 var keyBindings = this._keyBindings[eventName];
2186 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
2187
2188 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH andler]);
2189
2190 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
2191 }, this);
2192 },
2193
2194 _unlistenKeyEventListeners: function() {
2195 var keyHandlerTuple;
2196 var keyEventTarget;
2197 var eventName;
2198 var boundKeyHandler;
2199
2200 while (this._boundKeyHandlers.length) {
2201 // My kingdom for block-scope binding and destructuring assignment..
2202 keyHandlerTuple = this._boundKeyHandlers.pop();
2203 keyEventTarget = keyHandlerTuple[0];
2204 eventName = keyHandlerTuple[1];
2205 boundKeyHandler = keyHandlerTuple[2];
2206
2207 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
2208 }
2209 },
2210
2211 _onKeyBindingEvent: function(keyBindings, event) {
2212 if (this.stopKeyboardEventPropagation) {
2213 event.stopPropagation();
2214 }
2215
2216 // if event has been already prevented, don't do anything
2217 if (event.defaultPrevented) {
2218 return;
2219 }
2220
2221 for (var i = 0; i < keyBindings.length; i++) {
2222 var keyCombo = keyBindings[i][0];
2223 var handlerName = keyBindings[i][1];
2224 if (keyComboMatchesEvent(keyCombo, event)) {
2225 this._triggerKeyHandler(keyCombo, handlerName, event);
2226 // exit the loop if eventDefault was prevented
2227 if (event.defaultPrevented) {
2228 return;
2229 }
2230 }
2231 }
2232 },
2233
2234 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
2235 var detail = Object.create(keyCombo);
2236 detail.keyboardEvent = keyboardEvent;
2237 var event = new CustomEvent(keyCombo.event, {
2238 detail: detail,
2239 cancelable: true
2240 });
2241 this[handlerName].call(this, event);
2242 if (event.defaultPrevented) {
2243 keyboardEvent.preventDefault();
2244 }
2245 }
2246 };
2247 })();
2248 /**
2249 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
2250 * designated scroll target.
2251 *
2252 * Elements that consume this behavior can override the `_scrollHandler`
2253 * method to add logic on the scroll event.
2254 *
2255 * @demo demo/scrolling-region.html Scrolling Region
2256 * @demo demo/document.html Document Element
2257 * @polymerBehavior
2258 */
2259 Polymer.IronScrollTargetBehavior = {
2260
2261 properties: {
2262
2263 /**
2264 * Specifies the element that will handle the scroll event
2265 * on the behalf of the current element. This is typically a reference to an element,
2266 * but there are a few more posibilities:
2267 *
2268 * ### Elements id
2269 *
2270 *```html
2271 * <div id="scrollable-element" style="overflow: auto;">
2272 * <x-element scroll-target="scrollable-element">
2273 * \x3c!-- Content--\x3e
2274 * </x-element>
2275 * </div>
2276 *```
2277 * In this case, the `scrollTarget` will point to the outer div element.
2278 *
2279 * ### Document scrolling
2280 *
2281 * For document scrolling, you can use the reserved word `document`:
2282 *
2283 *```html
2284 * <x-element scroll-target="document">
2285 * \x3c!-- Content --\x3e
2286 * </x-element>
2287 *```
2288 *
2289 * ### Elements reference
2290 *
2291 *```js
2292 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
2293 *```
2294 *
2295 * @type {HTMLElement}
2296 */
2297 scrollTarget: {
2298 type: HTMLElement,
2299 value: function() {
2300 return this._defaultScrollTarget;
2301 }
2302 }
2303 },
2304
2305 observers: [
2306 '_scrollTargetChanged(scrollTarget, isAttached)'
2307 ],
2308
2309 _scrollTargetChanged: function(scrollTarget, isAttached) {
2310 var eventTarget;
2311
2312 if (this._oldScrollTarget) {
2313 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
2314 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
2315 this._oldScrollTarget = null;
2316 }
2317
2318 if (!isAttached) {
2319 return;
2320 }
2321 // Support element id references
2322 if (scrollTarget === 'document') {
2323
2324 this.scrollTarget = this._doc;
2325
2326 } else if (typeof scrollTarget === 'string') {
2327
2328 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
2329 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
2330
2331 } else if (this._isValidScrollTarget()) {
2332
2333 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
2334 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
2335 this._oldScrollTarget = scrollTarget;
2336
2337 eventTarget.addEventListener('scroll', this._boundScrollHandler);
2338 }
2339 },
2340
2341 /**
2342 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
2343 *
2344 * @protected
2345 */
2346 _scrollHandler: function scrollHandler() {},
2347
2348 /**
2349 * The default scroll target. Consumers of this behavior may want to customi ze
2350 * the default scroll target.
2351 *
2352 * @type {Element}
2353 */
2354 get _defaultScrollTarget() {
2355 return this._doc;
2356 },
2357
2358 /**
2359 * Shortcut for the document element
2360 *
2361 * @type {Element}
2362 */
2363 get _doc() {
2364 return this.ownerDocument.documentElement;
2365 },
2366
2367 /**
2368 * Gets the number of pixels that the content of an element is scrolled upwa rd.
2369 *
2370 * @type {number}
2371 */
2372 get _scrollTop() {
2373 if (this._isValidScrollTarget()) {
2374 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
2375 }
2376 return 0;
2377 },
2378
2379 /**
2380 * Gets the number of pixels that the content of an element is scrolled to t he left.
2381 *
2382 * @type {number}
2383 */
2384 get _scrollLeft() {
2385 if (this._isValidScrollTarget()) {
2386 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
2387 }
2388 return 0;
2389 },
2390
2391 /**
2392 * Sets the number of pixels that the content of an element is scrolled upwa rd.
2393 *
2394 * @type {number}
2395 */
2396 set _scrollTop(top) {
2397 if (this.scrollTarget === this._doc) {
2398 window.scrollTo(window.pageXOffset, top);
2399 } else if (this._isValidScrollTarget()) {
2400 this.scrollTarget.scrollTop = top;
2401 }
2402 },
2403
2404 /**
2405 * Sets the number of pixels that the content of an element is scrolled to t he left.
2406 *
2407 * @type {number}
2408 */
2409 set _scrollLeft(left) {
2410 if (this.scrollTarget === this._doc) {
2411 window.scrollTo(left, window.pageYOffset);
2412 } else if (this._isValidScrollTarget()) {
2413 this.scrollTarget.scrollLeft = left;
2414 }
2415 },
2416
2417 /**
2418 * Scrolls the content to a particular place.
2419 *
2420 * @method scroll
2421 * @param {number} left The left position
2422 * @param {number} top The top position
2423 */
2424 scroll: function(left, top) {
2425 if (this.scrollTarget === this._doc) {
2426 window.scrollTo(left, top);
2427 } else if (this._isValidScrollTarget()) {
2428 this.scrollTarget.scrollLeft = left;
2429 this.scrollTarget.scrollTop = top;
2430 }
2431 },
2432
2433 /**
2434 * Gets the width of the scroll target.
2435 *
2436 * @type {number}
2437 */
2438 get _scrollTargetWidth() {
2439 if (this._isValidScrollTarget()) {
2440 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
2441 }
2442 return 0;
2443 },
2444
2445 /**
2446 * Gets the height of the scroll target.
2447 *
2448 * @type {number}
2449 */
2450 get _scrollTargetHeight() {
2451 if (this._isValidScrollTarget()) {
2452 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
2453 }
2454 return 0;
2455 },
2456
2457 /**
2458 * Returns true if the scroll target is a valid HTMLElement.
2459 *
2460 * @return {boolean}
2461 */
2462 _isValidScrollTarget: function() {
2463 return this.scrollTarget instanceof HTMLElement;
2464 }
2465 };
2466 (function() {
2467
2468 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); 1315 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
2469 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; 1316 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
2470 var DEFAULT_PHYSICAL_COUNT = 3; 1317 var DEFAULT_PHYSICAL_COUNT = 3;
2471 var HIDDEN_Y = '-10000px'; 1318 var HIDDEN_Y = '-10000px';
2472 var DEFAULT_GRID_SIZE = 200; 1319 var DEFAULT_GRID_SIZE = 200;
2473 var SECRET_TABINDEX = -100; 1320 var SECRET_TABINDEX = -100;
2474
2475 Polymer({ 1321 Polymer({
2476
2477 is: 'iron-list', 1322 is: 'iron-list',
2478
2479 properties: { 1323 properties: {
2480
2481 /**
2482 * An array containing items determining how many instances of the templat e
2483 * to stamp and that that each template instance should bind to.
2484 */
2485 items: { 1324 items: {
2486 type: Array 1325 type: Array
2487 }, 1326 },
2488
2489 /**
2490 * The max count of physical items the pool can extend to.
2491 */
2492 maxPhysicalCount: { 1327 maxPhysicalCount: {
2493 type: Number, 1328 type: Number,
2494 value: 500 1329 value: 500
2495 }, 1330 },
2496
2497 /**
2498 * The name of the variable to add to the binding scope for the array
2499 * element associated with a given template instance.
2500 */
2501 as: { 1331 as: {
2502 type: String, 1332 type: String,
2503 value: 'item' 1333 value: 'item'
2504 }, 1334 },
2505
2506 /**
2507 * The name of the variable to add to the binding scope with the index
2508 * for the row.
2509 */
2510 indexAs: { 1335 indexAs: {
2511 type: String, 1336 type: String,
2512 value: 'index' 1337 value: 'index'
2513 }, 1338 },
2514
2515 /**
2516 * The name of the variable to add to the binding scope to indicate
2517 * if the row is selected.
2518 */
2519 selectedAs: { 1339 selectedAs: {
2520 type: String, 1340 type: String,
2521 value: 'selected' 1341 value: 'selected'
2522 }, 1342 },
2523
2524 /**
2525 * When true, the list is rendered as a grid. Grid items must have
2526 * fixed width and height set via CSS. e.g.
2527 *
2528 * ```html
2529 * <iron-list grid>
2530 * <template>
2531 * <div style="width: 100px; height: 100px;"> 100x100 </div>
2532 * </template>
2533 * </iron-list>
2534 * ```
2535 */
2536 grid: { 1343 grid: {
2537 type: Boolean, 1344 type: Boolean,
2538 value: false, 1345 value: false,
2539 reflectToAttribute: true 1346 reflectToAttribute: true
2540 }, 1347 },
2541
2542 /**
2543 * When true, tapping a row will select the item, placing its data model
2544 * in the set of selected items retrievable via the selection property.
2545 *
2546 * Note that tapping focusable elements within the list item will not
2547 * result in selection, since they are presumed to have their * own action .
2548 */
2549 selectionEnabled: { 1348 selectionEnabled: {
2550 type: Boolean, 1349 type: Boolean,
2551 value: false 1350 value: false
2552 }, 1351 },
2553
2554 /**
2555 * When `multiSelection` is false, this is the currently selected item, or `null`
2556 * if no item is selected.
2557 */
2558 selectedItem: { 1352 selectedItem: {
2559 type: Object, 1353 type: Object,
2560 notify: true 1354 notify: true
2561 }, 1355 },
2562
2563 /**
2564 * When `multiSelection` is true, this is an array that contains the selec ted items.
2565 */
2566 selectedItems: { 1356 selectedItems: {
2567 type: Object, 1357 type: Object,
2568 notify: true 1358 notify: true
2569 }, 1359 },
2570
2571 /**
2572 * When `true`, multiple items may be selected at once (in this case,
2573 * `selected` is an array of currently selected items). When `false`,
2574 * only one item may be selected at a time.
2575 */
2576 multiSelection: { 1360 multiSelection: {
2577 type: Boolean, 1361 type: Boolean,
2578 value: false 1362 value: false
2579 } 1363 }
2580 }, 1364 },
2581 1365 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)' ],
2582 observers: [ 1366 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ],
2583 '_itemsChanged(items.*)',
2584 '_selectionEnabledChanged(selectionEnabled)',
2585 '_multiSelectionChanged(multiSelection)',
2586 '_setOverflow(scrollTarget)'
2587 ],
2588
2589 behaviors: [
2590 Polymer.Templatizer,
2591 Polymer.IronResizableBehavior,
2592 Polymer.IronA11yKeysBehavior,
2593 Polymer.IronScrollTargetBehavior
2594 ],
2595
2596 keyBindings: { 1367 keyBindings: {
2597 'up': '_didMoveUp', 1368 up: '_didMoveUp',
2598 'down': '_didMoveDown', 1369 down: '_didMoveDown',
2599 'enter': '_didEnter' 1370 enter: '_didEnter'
2600 }, 1371 },
2601 1372 _ratio: .5,
2602 /**
2603 * The ratio of hidden tiles that should remain in the scroll direction.
2604 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
2605 */
2606 _ratio: 0.5,
2607
2608 /**
2609 * The padding-top value for the list.
2610 */
2611 _scrollerPaddingTop: 0, 1373 _scrollerPaddingTop: 0,
2612
2613 /**
2614 * This value is the same as `scrollTop`.
2615 */
2616 _scrollPosition: 0, 1374 _scrollPosition: 0,
2617
2618 /**
2619 * The sum of the heights of all the tiles in the DOM.
2620 */
2621 _physicalSize: 0, 1375 _physicalSize: 0,
2622
2623 /**
2624 * The average `offsetHeight` of the tiles observed till now.
2625 */
2626 _physicalAverage: 0, 1376 _physicalAverage: 0,
2627
2628 /**
2629 * The number of tiles which `offsetHeight` > 0 observed until now.
2630 */
2631 _physicalAverageCount: 0, 1377 _physicalAverageCount: 0,
2632
2633 /**
2634 * The Y position of the item rendered in the `_physicalStart`
2635 * tile relative to the scrolling list.
2636 */
2637 _physicalTop: 0, 1378 _physicalTop: 0,
2638
2639 /**
2640 * The number of items in the list.
2641 */
2642 _virtualCount: 0, 1379 _virtualCount: 0,
2643
2644 /**
2645 * A map between an item key and its physical item index
2646 */
2647 _physicalIndexForKey: null, 1380 _physicalIndexForKey: null,
2648
2649 /**
2650 * The estimated scroll height based on `_physicalAverage`
2651 */
2652 _estScrollHeight: 0, 1381 _estScrollHeight: 0,
2653
2654 /**
2655 * The scroll height of the dom node
2656 */
2657 _scrollHeight: 0, 1382 _scrollHeight: 0,
2658
2659 /**
2660 * The height of the list. This is referred as the viewport in the context o f list.
2661 */
2662 _viewportHeight: 0, 1383 _viewportHeight: 0,
2663
2664 /**
2665 * The width of the list. This is referred as the viewport in the context of list.
2666 */
2667 _viewportWidth: 0, 1384 _viewportWidth: 0,
2668
2669 /**
2670 * An array of DOM nodes that are currently in the tree
2671 * @type {?Array<!TemplatizerNode>}
2672 */
2673 _physicalItems: null, 1385 _physicalItems: null,
2674
2675 /**
2676 * An array of heights for each item in `_physicalItems`
2677 * @type {?Array<number>}
2678 */
2679 _physicalSizes: null, 1386 _physicalSizes: null,
2680
2681 /**
2682 * A cached value for the first visible index.
2683 * See `firstVisibleIndex`
2684 * @type {?number}
2685 */
2686 _firstVisibleIndexVal: null, 1387 _firstVisibleIndexVal: null,
2687
2688 /**
2689 * A cached value for the last visible index.
2690 * See `lastVisibleIndex`
2691 * @type {?number}
2692 */
2693 _lastVisibleIndexVal: null, 1388 _lastVisibleIndexVal: null,
2694
2695 /**
2696 * A Polymer collection for the items.
2697 * @type {?Polymer.Collection}
2698 */
2699 _collection: null, 1389 _collection: null,
2700
2701 /**
2702 * True if the current item list was rendered for the first time
2703 * after attached.
2704 */
2705 _itemsRendered: false, 1390 _itemsRendered: false,
2706
2707 /**
2708 * The page that is currently rendered.
2709 */
2710 _lastPage: null, 1391 _lastPage: null,
2711
2712 /**
2713 * The max number of pages to render. One page is equivalent to the height o f the list.
2714 */
2715 _maxPages: 3, 1392 _maxPages: 3,
2716
2717 /**
2718 * The currently focused physical item.
2719 */
2720 _focusedItem: null, 1393 _focusedItem: null,
2721
2722 /**
2723 * The index of the `_focusedItem`.
2724 */
2725 _focusedIndex: -1, 1394 _focusedIndex: -1,
2726
2727 /**
2728 * The the item that is focused if it is moved offscreen.
2729 * @private {?TemplatizerNode}
2730 */
2731 _offscreenFocusedItem: null, 1395 _offscreenFocusedItem: null,
2732
2733 /**
2734 * The item that backfills the `_offscreenFocusedItem` in the physical items
2735 * list when that item is moved offscreen.
2736 */
2737 _focusBackfillItem: null, 1396 _focusBackfillItem: null,
2738
2739 /**
2740 * The maximum items per row
2741 */
2742 _itemsPerRow: 1, 1397 _itemsPerRow: 1,
2743
2744 /**
2745 * The width of each grid item
2746 */
2747 _itemWidth: 0, 1398 _itemWidth: 0,
2748
2749 /**
2750 * The height of the row in grid layout.
2751 */
2752 _rowHeight: 0, 1399 _rowHeight: 0,
2753
2754 /**
2755 * The bottom of the physical content.
2756 */
2757 get _physicalBottom() { 1400 get _physicalBottom() {
2758 return this._physicalTop + this._physicalSize; 1401 return this._physicalTop + this._physicalSize;
2759 }, 1402 },
2760
2761 /**
2762 * The bottom of the scroll.
2763 */
2764 get _scrollBottom() { 1403 get _scrollBottom() {
2765 return this._scrollPosition + this._viewportHeight; 1404 return this._scrollPosition + this._viewportHeight;
2766 }, 1405 },
2767
2768 /**
2769 * The n-th item rendered in the last physical item.
2770 */
2771 get _virtualEnd() { 1406 get _virtualEnd() {
2772 return this._virtualStart + this._physicalCount - 1; 1407 return this._virtualStart + this._physicalCount - 1;
2773 }, 1408 },
2774
2775 /**
2776 * The height of the physical content that isn't on the screen.
2777 */
2778 get _hiddenContentSize() { 1409 get _hiddenContentSize() {
2779 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize; 1410 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
2780 return size - this._viewportHeight; 1411 return size - this._viewportHeight;
2781 }, 1412 },
2782
2783 /**
2784 * The maximum scroll top value.
2785 */
2786 get _maxScrollTop() { 1413 get _maxScrollTop() {
2787 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop; 1414 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
2788 }, 1415 },
2789
2790 /**
2791 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
2792 */
2793 _minVirtualStart: 0, 1416 _minVirtualStart: 0,
2794
2795 /**
2796 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
2797 */
2798 get _maxVirtualStart() { 1417 get _maxVirtualStart() {
2799 return Math.max(0, this._virtualCount - this._physicalCount); 1418 return Math.max(0, this._virtualCount - this._physicalCount);
2800 }, 1419 },
2801
2802 /**
2803 * The n-th item rendered in the `_physicalStart` tile.
2804 */
2805 _virtualStartVal: 0, 1420 _virtualStartVal: 0,
2806
2807 set _virtualStart(val) { 1421 set _virtualStart(val) {
2808 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); 1422 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
2809 }, 1423 },
2810
2811 get _virtualStart() { 1424 get _virtualStart() {
2812 return this._virtualStartVal || 0; 1425 return this._virtualStartVal || 0;
2813 }, 1426 },
2814
2815 /**
2816 * The k-th tile that is at the top of the scrolling list.
2817 */
2818 _physicalStartVal: 0, 1427 _physicalStartVal: 0,
2819
2820 set _physicalStart(val) { 1428 set _physicalStart(val) {
2821 this._physicalStartVal = val % this._physicalCount; 1429 this._physicalStartVal = val % this._physicalCount;
2822 if (this._physicalStartVal < 0) { 1430 if (this._physicalStartVal < 0) {
2823 this._physicalStartVal = this._physicalCount + this._physicalStartVal; 1431 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
2824 } 1432 }
2825 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 1433 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2826 }, 1434 },
2827
2828 get _physicalStart() { 1435 get _physicalStart() {
2829 return this._physicalStartVal || 0; 1436 return this._physicalStartVal || 0;
2830 }, 1437 },
2831
2832 /**
2833 * The number of tiles in the DOM.
2834 */
2835 _physicalCountVal: 0, 1438 _physicalCountVal: 0,
2836
2837 set _physicalCount(val) { 1439 set _physicalCount(val) {
2838 this._physicalCountVal = val; 1440 this._physicalCountVal = val;
2839 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 1441 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2840 }, 1442 },
2841
2842 get _physicalCount() { 1443 get _physicalCount() {
2843 return this._physicalCountVal; 1444 return this._physicalCountVal;
2844 }, 1445 },
2845
2846 /**
2847 * The k-th tile that is at the bottom of the scrolling list.
2848 */
2849 _physicalEnd: 0, 1446 _physicalEnd: 0,
2850
2851 /**
2852 * An optimal physical size such that we will have enough physical items
2853 * to fill up the viewport and recycle when the user scrolls.
2854 *
2855 * This default value assumes that we will at least have the equivalent
2856 * to a viewport of physical items above and below the user's viewport.
2857 */
2858 get _optPhysicalSize() { 1447 get _optPhysicalSize() {
2859 if (this.grid) { 1448 if (this.grid) {
2860 return this._estRowsInView * this._rowHeight * this._maxPages; 1449 return this._estRowsInView * this._rowHeight * this._maxPages;
2861 } 1450 }
2862 return this._viewportHeight * this._maxPages; 1451 return this._viewportHeight * this._maxPages;
2863 }, 1452 },
2864
2865 get _optPhysicalCount() { 1453 get _optPhysicalCount() {
2866 return this._estRowsInView * this._itemsPerRow * this._maxPages; 1454 return this._estRowsInView * this._itemsPerRow * this._maxPages;
2867 }, 1455 },
2868
2869 /**
2870 * True if the current list is visible.
2871 */
2872 get _isVisible() { 1456 get _isVisible() {
2873 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight); 1457 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
2874 }, 1458 },
2875
2876 /**
2877 * Gets the index of the first visible item in the viewport.
2878 *
2879 * @type {number}
2880 */
2881 get firstVisibleIndex() { 1459 get firstVisibleIndex() {
2882 if (this._firstVisibleIndexVal === null) { 1460 if (this._firstVisibleIndexVal === null) {
2883 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 1461 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
2884 1462 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) {
2885 this._firstVisibleIndexVal = this._iterateItems( 1463 physicalOffset += this._getPhysicalSizeIncrement(pidx);
2886 function(pidx, vidx) { 1464 if (physicalOffset > this._scrollPosition) {
2887 physicalOffset += this._getPhysicalSizeIncrement(pidx); 1465 return this.grid ? vidx - vidx % this._itemsPerRow : vidx;
2888 1466 }
2889 if (physicalOffset > this._scrollPosition) { 1467 if (this.grid && this._virtualCount - 1 === vidx) {
2890 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; 1468 return vidx - vidx % this._itemsPerRow;
2891 } 1469 }
2892 // Handle a partially rendered final row in grid mode 1470 }) || 0;
2893 if (this.grid && this._virtualCount - 1 === vidx) {
2894 return vidx - (vidx % this._itemsPerRow);
2895 }
2896 }) || 0;
2897 } 1471 }
2898 return this._firstVisibleIndexVal; 1472 return this._firstVisibleIndexVal;
2899 }, 1473 },
2900
2901 /**
2902 * Gets the index of the last visible item in the viewport.
2903 *
2904 * @type {number}
2905 */
2906 get lastVisibleIndex() { 1474 get lastVisibleIndex() {
2907 if (this._lastVisibleIndexVal === null) { 1475 if (this._lastVisibleIndexVal === null) {
2908 if (this.grid) { 1476 if (this.grid) {
2909 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 1477 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
2910 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 1478 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
2911 } else { 1479 } else {
2912 var physicalOffset = this._physicalTop; 1480 var physicalOffset = this._physicalTop;
2913 this._iterateItems(function(pidx, vidx) { 1481 this._iterateItems(function(pidx, vidx) {
2914 if (physicalOffset < this._scrollBottom) { 1482 if (physicalOffset < this._scrollBottom) {
2915 this._lastVisibleIndexVal = vidx; 1483 this._lastVisibleIndexVal = vidx;
2916 } else { 1484 } else {
2917 // Break _iterateItems
2918 return true; 1485 return true;
2919 } 1486 }
2920 physicalOffset += this._getPhysicalSizeIncrement(pidx); 1487 physicalOffset += this._getPhysicalSizeIncrement(pidx);
2921 }); 1488 });
2922 } 1489 }
2923 } 1490 }
2924 return this._lastVisibleIndexVal; 1491 return this._lastVisibleIndexVal;
2925 }, 1492 },
2926
2927 get _defaultScrollTarget() { 1493 get _defaultScrollTarget() {
2928 return this; 1494 return this;
2929 }, 1495 },
2930 get _virtualRowCount() { 1496 get _virtualRowCount() {
2931 return Math.ceil(this._virtualCount / this._itemsPerRow); 1497 return Math.ceil(this._virtualCount / this._itemsPerRow);
2932 }, 1498 },
2933
2934 get _estRowsInView() { 1499 get _estRowsInView() {
2935 return Math.ceil(this._viewportHeight / this._rowHeight); 1500 return Math.ceil(this._viewportHeight / this._rowHeight);
2936 }, 1501 },
2937
2938 get _physicalRows() { 1502 get _physicalRows() {
2939 return Math.ceil(this._physicalCount / this._itemsPerRow); 1503 return Math.ceil(this._physicalCount / this._itemsPerRow);
2940 }, 1504 },
2941
2942 ready: function() { 1505 ready: function() {
2943 this.addEventListener('focus', this._didFocus.bind(this), true); 1506 this.addEventListener('focus', this._didFocus.bind(this), true);
2944 }, 1507 },
2945
2946 attached: function() { 1508 attached: function() {
2947 this.updateViewportBoundaries(); 1509 this.updateViewportBoundaries();
2948 this._render(); 1510 this._render();
2949 // `iron-resize` is fired when the list is attached if the event is added
2950 // before attached causing unnecessary work.
2951 this.listen(this, 'iron-resize', '_resizeHandler'); 1511 this.listen(this, 'iron-resize', '_resizeHandler');
2952 }, 1512 },
2953
2954 detached: function() { 1513 detached: function() {
2955 this._itemsRendered = false; 1514 this._itemsRendered = false;
2956 this.unlisten(this, 'iron-resize', '_resizeHandler'); 1515 this.unlisten(this, 'iron-resize', '_resizeHandler');
2957 }, 1516 },
2958
2959 /**
2960 * Set the overflow property if this element has its own scrolling region
2961 */
2962 _setOverflow: function(scrollTarget) { 1517 _setOverflow: function(scrollTarget) {
2963 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; 1518 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
2964 this.style.overflow = scrollTarget === this ? 'auto' : ''; 1519 this.style.overflow = scrollTarget === this ? 'auto' : '';
2965 }, 1520 },
2966
2967 /**
2968 * Invoke this method if you dynamically update the viewport's
2969 * size or CSS padding.
2970 *
2971 * @method updateViewportBoundaries
2972 */
2973 updateViewportBoundaries: function() { 1521 updateViewportBoundaries: function() {
2974 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : 1522 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo w.getComputedStyle(this)['padding-top'], 10);
2975 parseInt(window.getComputedStyle(this)['padding-top'], 10);
2976
2977 this._viewportHeight = this._scrollTargetHeight; 1523 this._viewportHeight = this._scrollTargetHeight;
2978 if (this.grid) { 1524 if (this.grid) {
2979 this._updateGridMetrics(); 1525 this._updateGridMetrics();
2980 } 1526 }
2981 }, 1527 },
2982
2983 /**
2984 * Update the models, the position of the
2985 * items in the viewport and recycle tiles as needed.
2986 */
2987 _scrollHandler: function() { 1528 _scrollHandler: function() {
2988 // clamp the `scrollTop` value
2989 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; 1529 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
2990 var delta = scrollTop - this._scrollPosition; 1530 var delta = scrollTop - this._scrollPosition;
2991 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; 1531 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
2992 var ratio = this._ratio; 1532 var ratio = this._ratio;
2993 var recycledTiles = 0; 1533 var recycledTiles = 0;
2994 var hiddenContentSize = this._hiddenContentSize; 1534 var hiddenContentSize = this._hiddenContentSize;
2995 var currentRatio = ratio; 1535 var currentRatio = ratio;
2996 var movingUp = []; 1536 var movingUp = [];
2997
2998 // track the last `scrollTop`
2999 this._scrollPosition = scrollTop; 1537 this._scrollPosition = scrollTop;
3000
3001 // clear cached visible indexes
3002 this._firstVisibleIndexVal = null; 1538 this._firstVisibleIndexVal = null;
3003 this._lastVisibleIndexVal = null; 1539 this._lastVisibleIndexVal = null;
3004
3005 scrollBottom = this._scrollBottom; 1540 scrollBottom = this._scrollBottom;
3006 physicalBottom = this._physicalBottom; 1541 physicalBottom = this._physicalBottom;
3007
3008 // random access
3009 if (Math.abs(delta) > this._physicalSize) { 1542 if (Math.abs(delta) > this._physicalSize) {
3010 this._physicalTop += delta; 1543 this._physicalTop += delta;
3011 recycledTiles = Math.round(delta / this._physicalAverage); 1544 recycledTiles = Math.round(delta / this._physicalAverage);
3012 } 1545 } else if (delta < 0) {
3013 // scroll up
3014 else if (delta < 0) {
3015 var topSpace = scrollTop - this._physicalTop; 1546 var topSpace = scrollTop - this._physicalTop;
3016 var virtualStart = this._virtualStart; 1547 var virtualStart = this._virtualStart;
3017
3018 recycledTileSet = []; 1548 recycledTileSet = [];
3019
3020 kth = this._physicalEnd; 1549 kth = this._physicalEnd;
3021 currentRatio = topSpace / hiddenContentSize; 1550 currentRatio = topSpace / hiddenContentSize;
3022 1551 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen t(kth) > scrollBottom) {
3023 // move tiles from bottom to top
3024 while (
3025 // approximate `currentRatio` to `ratio`
3026 currentRatio < ratio &&
3027 // recycle less physical items than the total
3028 recycledTiles < this._physicalCount &&
3029 // ensure that these recycled tiles are needed
3030 virtualStart - recycledTiles > 0 &&
3031 // ensure that the tile is not visible
3032 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
3033 ) {
3034
3035 tileHeight = this._getPhysicalSizeIncrement(kth); 1552 tileHeight = this._getPhysicalSizeIncrement(kth);
3036 currentRatio += tileHeight / hiddenContentSize; 1553 currentRatio += tileHeight / hiddenContentSize;
3037 physicalBottom -= tileHeight; 1554 physicalBottom -= tileHeight;
3038 recycledTileSet.push(kth); 1555 recycledTileSet.push(kth);
3039 recycledTiles++; 1556 recycledTiles++;
3040 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; 1557 kth = kth === 0 ? this._physicalCount - 1 : kth - 1;
3041 } 1558 }
3042
3043 movingUp = recycledTileSet; 1559 movingUp = recycledTileSet;
3044 recycledTiles = -recycledTiles; 1560 recycledTiles = -recycledTiles;
3045 } 1561 } else if (delta > 0) {
3046 // scroll down
3047 else if (delta > 0) {
3048 var bottomSpace = physicalBottom - scrollBottom; 1562 var bottomSpace = physicalBottom - scrollBottom;
3049 var virtualEnd = this._virtualEnd; 1563 var virtualEnd = this._virtualEnd;
3050 var lastVirtualItemIndex = this._virtualCount-1; 1564 var lastVirtualItemIndex = this._virtualCount - 1;
3051
3052 recycledTileSet = []; 1565 recycledTileSet = [];
3053
3054 kth = this._physicalStart; 1566 kth = this._physicalStart;
3055 currentRatio = bottomSpace / hiddenContentSize; 1567 currentRatio = bottomSpace / hiddenContentSize;
3056 1568 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get PhysicalSizeIncrement(kth) < scrollTop) {
3057 // move tiles from top to bottom
3058 while (
3059 // approximate `currentRatio` to `ratio`
3060 currentRatio < ratio &&
3061 // recycle less physical items than the total
3062 recycledTiles < this._physicalCount &&
3063 // ensure that these recycled tiles are needed
3064 virtualEnd + recycledTiles < lastVirtualItemIndex &&
3065 // ensure that the tile is not visible
3066 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
3067 ) {
3068
3069 tileHeight = this._getPhysicalSizeIncrement(kth); 1569 tileHeight = this._getPhysicalSizeIncrement(kth);
3070 currentRatio += tileHeight / hiddenContentSize; 1570 currentRatio += tileHeight / hiddenContentSize;
3071
3072 this._physicalTop += tileHeight; 1571 this._physicalTop += tileHeight;
3073 recycledTileSet.push(kth); 1572 recycledTileSet.push(kth);
3074 recycledTiles++; 1573 recycledTiles++;
3075 kth = (kth + 1) % this._physicalCount; 1574 kth = (kth + 1) % this._physicalCount;
3076 } 1575 }
3077 } 1576 }
3078
3079 if (recycledTiles === 0) { 1577 if (recycledTiles === 0) {
3080 // Try to increase the pool if the list's client height isn't filled up with physical items
3081 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { 1578 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
3082 this._increasePoolIfNeeded(); 1579 this._increasePoolIfNeeded();
3083 } 1580 }
3084 } else { 1581 } else {
3085 this._virtualStart = this._virtualStart + recycledTiles; 1582 this._virtualStart = this._virtualStart + recycledTiles;
3086 this._physicalStart = this._physicalStart + recycledTiles; 1583 this._physicalStart = this._physicalStart + recycledTiles;
3087 this._update(recycledTileSet, movingUp); 1584 this._update(recycledTileSet, movingUp);
3088 } 1585 }
3089 }, 1586 },
3090
3091 /**
3092 * Update the list of items, starting from the `_virtualStart` item.
3093 * @param {!Array<number>=} itemSet
3094 * @param {!Array<number>=} movingUp
3095 */
3096 _update: function(itemSet, movingUp) { 1587 _update: function(itemSet, movingUp) {
3097 // manage focus
3098 this._manageFocus(); 1588 this._manageFocus();
3099 // update models
3100 this._assignModels(itemSet); 1589 this._assignModels(itemSet);
3101 // measure heights
3102 this._updateMetrics(itemSet); 1590 this._updateMetrics(itemSet);
3103 // adjust offset after measuring
3104 if (movingUp) { 1591 if (movingUp) {
3105 while (movingUp.length) { 1592 while (movingUp.length) {
3106 var idx = movingUp.pop(); 1593 var idx = movingUp.pop();
3107 this._physicalTop -= this._getPhysicalSizeIncrement(idx); 1594 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
3108 } 1595 }
3109 } 1596 }
3110 // update the position of the items
3111 this._positionItems(); 1597 this._positionItems();
3112 // set the scroller size
3113 this._updateScrollerSize(); 1598 this._updateScrollerSize();
3114 // increase the pool of physical items
3115 this._increasePoolIfNeeded(); 1599 this._increasePoolIfNeeded();
3116 }, 1600 },
3117
3118 /**
3119 * Creates a pool of DOM elements and attaches them to the local dom.
3120 */
3121 _createPool: function(size) { 1601 _createPool: function(size) {
3122 var physicalItems = new Array(size); 1602 var physicalItems = new Array(size);
3123
3124 this._ensureTemplatized(); 1603 this._ensureTemplatized();
3125
3126 for (var i = 0; i < size; i++) { 1604 for (var i = 0; i < size; i++) {
3127 var inst = this.stamp(null); 1605 var inst = this.stamp(null);
3128 // First element child is item; Safari doesn't support children[0]
3129 // on a doc fragment
3130 physicalItems[i] = inst.root.querySelector('*'); 1606 physicalItems[i] = inst.root.querySelector('*');
3131 Polymer.dom(this).appendChild(inst.root); 1607 Polymer.dom(this).appendChild(inst.root);
3132 } 1608 }
3133 return physicalItems; 1609 return physicalItems;
3134 }, 1610 },
3135
3136 /**
3137 * Increases the pool of physical items only if needed.
3138 *
3139 * @return {boolean} True if the pool was increased.
3140 */
3141 _increasePoolIfNeeded: function() { 1611 _increasePoolIfNeeded: function() {
3142 // Base case 1: the list has no height.
3143 if (this._viewportHeight === 0) { 1612 if (this._viewportHeight === 0) {
3144 return false; 1613 return false;
3145 } 1614 }
3146 // Base case 2: If the physical size is optimal and the list's client heig ht is full
3147 // with physical items, don't increase the pool.
3148 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition; 1615 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
3149 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { 1616 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
3150 return false; 1617 return false;
3151 } 1618 }
3152 // this value should range between [0 <= `currentPage` <= `_maxPages`]
3153 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); 1619 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
3154
3155 if (currentPage === 0) { 1620 if (currentPage === 0) {
3156 // fill the first page 1621 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * .5)));
3157 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
3158 } else if (this._lastPage !== currentPage && isClientHeightFull) { 1622 } else if (this._lastPage !== currentPage && isClientHeightFull) {
3159 // paint the page and defer the next increase
3160 // wait 16ms which is rough enough to get paint cycle.
3161 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16)); 1623 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
3162 } else { 1624 } else {
3163 // fill the rest of the pages
3164 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ; 1625 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
3165 } 1626 }
3166
3167 this._lastPage = currentPage; 1627 this._lastPage = currentPage;
3168
3169 return true; 1628 return true;
3170 }, 1629 },
3171
3172 /**
3173 * Increases the pool size.
3174 */
3175 _increasePool: function(missingItems) { 1630 _increasePool: function(missingItems) {
3176 var nextPhysicalCount = Math.min( 1631 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this. _virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS ICAL_COUNT));
3177 this._physicalCount + missingItems,
3178 this._virtualCount - this._virtualStart,
3179 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
3180 );
3181 var prevPhysicalCount = this._physicalCount; 1632 var prevPhysicalCount = this._physicalCount;
3182 var delta = nextPhysicalCount - prevPhysicalCount; 1633 var delta = nextPhysicalCount - prevPhysicalCount;
3183
3184 if (delta <= 0) { 1634 if (delta <= 0) {
3185 return; 1635 return;
3186 } 1636 }
3187
3188 [].push.apply(this._physicalItems, this._createPool(delta)); 1637 [].push.apply(this._physicalItems, this._createPool(delta));
3189 [].push.apply(this._physicalSizes, new Array(delta)); 1638 [].push.apply(this._physicalSizes, new Array(delta));
3190
3191 this._physicalCount = prevPhysicalCount + delta; 1639 this._physicalCount = prevPhysicalCount + delta;
3192 1640 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this. _focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd ) {
3193 // update the physical start if we need to preserve the model of the focus ed item.
3194 // In this situation, the focused item is currently rendered and its model would
3195 // have changed after increasing the pool if the physical start remained u nchanged.
3196 if (this._physicalStart > this._physicalEnd &&
3197 this._isIndexRendered(this._focusedIndex) &&
3198 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
3199 this._physicalStart = this._physicalStart + delta; 1641 this._physicalStart = this._physicalStart + delta;
3200 } 1642 }
3201 this._update(); 1643 this._update();
3202 }, 1644 },
3203
3204 /**
3205 * Render a new list of items. This method does exactly the same as `update` ,
3206 * but it also ensures that only one `update` cycle is created.
3207 */
3208 _render: function() { 1645 _render: function() {
3209 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; 1646 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
3210
3211 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { 1647 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
3212 this._lastPage = 0; 1648 this._lastPage = 0;
3213 this._update(); 1649 this._update();
3214 this._itemsRendered = true; 1650 this._itemsRendered = true;
3215 } 1651 }
3216 }, 1652 },
3217
3218 /**
3219 * Templetizes the user template.
3220 */
3221 _ensureTemplatized: function() { 1653 _ensureTemplatized: function() {
3222 if (!this.ctor) { 1654 if (!this.ctor) {
3223 // Template instance props that should be excluded from forwarding
3224 var props = {}; 1655 var props = {};
3225 props.__key__ = true; 1656 props.__key__ = true;
3226 props[this.as] = true; 1657 props[this.as] = true;
3227 props[this.indexAs] = true; 1658 props[this.indexAs] = true;
3228 props[this.selectedAs] = true; 1659 props[this.selectedAs] = true;
3229 props.tabIndex = true; 1660 props.tabIndex = true;
3230
3231 this._instanceProps = props; 1661 this._instanceProps = props;
3232 this._userTemplate = Polymer.dom(this).querySelector('template'); 1662 this._userTemplate = Polymer.dom(this).querySelector('template');
3233
3234 if (this._userTemplate) { 1663 if (this._userTemplate) {
3235 this.templatize(this._userTemplate); 1664 this.templatize(this._userTemplate);
3236 } else { 1665 } else {
3237 console.warn('iron-list requires a template to be provided in light-do m'); 1666 console.warn('iron-list requires a template to be provided in light-do m');
3238 } 1667 }
3239 } 1668 }
3240 }, 1669 },
3241
3242 /**
3243 * Implements extension point from Templatizer mixin.
3244 */
3245 _getStampedChildren: function() { 1670 _getStampedChildren: function() {
3246 return this._physicalItems; 1671 return this._physicalItems;
3247 }, 1672 },
3248
3249 /**
3250 * Implements extension point from Templatizer
3251 * Called as a side effect of a template instance path change, responsible
3252 * for notifying items.<key-for-instance>.<path> change up to host.
3253 */
3254 _forwardInstancePath: function(inst, path, value) { 1673 _forwardInstancePath: function(inst, path, value) {
3255 if (path.indexOf(this.as + '.') === 0) { 1674 if (path.indexOf(this.as + '.') === 0) {
3256 this.notifyPath('items.' + inst.__key__ + '.' + 1675 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt h + 1), value);
3257 path.slice(this.as.length + 1), value);
3258 } 1676 }
3259 }, 1677 },
3260
3261 /**
3262 * Implements extension point from Templatizer mixin
3263 * Called as side-effect of a host property change, responsible for
3264 * notifying parent path change on each row.
3265 */
3266 _forwardParentProp: function(prop, value) { 1678 _forwardParentProp: function(prop, value) {
3267 if (this._physicalItems) { 1679 if (this._physicalItems) {
3268 this._physicalItems.forEach(function(item) { 1680 this._physicalItems.forEach(function(item) {
3269 item._templateInstance[prop] = value; 1681 item._templateInstance[prop] = value;
3270 }, this); 1682 }, this);
3271 } 1683 }
3272 }, 1684 },
3273
3274 /**
3275 * Implements extension point from Templatizer
3276 * Called as side-effect of a host path change, responsible for
3277 * notifying parent.<path> path change on each row.
3278 */
3279 _forwardParentPath: function(path, value) { 1685 _forwardParentPath: function(path, value) {
3280 if (this._physicalItems) { 1686 if (this._physicalItems) {
3281 this._physicalItems.forEach(function(item) { 1687 this._physicalItems.forEach(function(item) {
3282 item._templateInstance.notifyPath(path, value, true); 1688 item._templateInstance.notifyPath(path, value, true);
3283 }, this); 1689 }, this);
3284 } 1690 }
3285 }, 1691 },
3286
3287 /**
3288 * Called as a side effect of a host items.<key>.<path> path change,
3289 * responsible for notifying item.<path> changes.
3290 */
3291 _forwardItemPath: function(path, value) { 1692 _forwardItemPath: function(path, value) {
3292 if (!this._physicalIndexForKey) { 1693 if (!this._physicalIndexForKey) {
3293 return; 1694 return;
3294 } 1695 }
3295 var dot = path.indexOf('.'); 1696 var dot = path.indexOf('.');
3296 var key = path.substring(0, dot < 0 ? path.length : dot); 1697 var key = path.substring(0, dot < 0 ? path.length : dot);
3297 var idx = this._physicalIndexForKey[key]; 1698 var idx = this._physicalIndexForKey[key];
3298 var offscreenItem = this._offscreenFocusedItem; 1699 var offscreenItem = this._offscreenFocusedItem;
3299 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? 1700 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? offscreenItem : this._physicalItems[idx];
3300 offscreenItem : this._physicalItems[idx];
3301
3302 if (!el || el._templateInstance.__key__ !== key) { 1701 if (!el || el._templateInstance.__key__ !== key) {
3303 return; 1702 return;
3304 } 1703 }
3305 if (dot >= 0) { 1704 if (dot >= 0) {
3306 path = this.as + '.' + path.substring(dot+1); 1705 path = this.as + '.' + path.substring(dot + 1);
3307 el._templateInstance.notifyPath(path, value, true); 1706 el._templateInstance.notifyPath(path, value, true);
3308 } else { 1707 } else {
3309 // Update selection if needed
3310 var currentItem = el._templateInstance[this.as]; 1708 var currentItem = el._templateInstance[this.as];
3311 if (Array.isArray(this.selectedItems)) { 1709 if (Array.isArray(this.selectedItems)) {
3312 for (var i = 0; i < this.selectedItems.length; i++) { 1710 for (var i = 0; i < this.selectedItems.length; i++) {
3313 if (this.selectedItems[i] === currentItem) { 1711 if (this.selectedItems[i] === currentItem) {
3314 this.set('selectedItems.' + i, value); 1712 this.set('selectedItems.' + i, value);
3315 break; 1713 break;
3316 } 1714 }
3317 } 1715 }
3318 } else if (this.selectedItem === currentItem) { 1716 } else if (this.selectedItem === currentItem) {
3319 this.set('selectedItem', value); 1717 this.set('selectedItem', value);
3320 } 1718 }
3321 el._templateInstance[this.as] = value; 1719 el._templateInstance[this.as] = value;
3322 } 1720 }
3323 }, 1721 },
3324
3325 /**
3326 * Called when the items have changed. That is, ressignments
3327 * to `items`, splices or updates to a single item.
3328 */
3329 _itemsChanged: function(change) { 1722 _itemsChanged: function(change) {
3330 if (change.path === 'items') { 1723 if (change.path === 'items') {
3331 // reset items
3332 this._virtualStart = 0; 1724 this._virtualStart = 0;
3333 this._physicalTop = 0; 1725 this._physicalTop = 0;
3334 this._virtualCount = this.items ? this.items.length : 0; 1726 this._virtualCount = this.items ? this.items.length : 0;
3335 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; 1727 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
3336 this._physicalIndexForKey = {}; 1728 this._physicalIndexForKey = {};
3337 this._firstVisibleIndexVal = null; 1729 this._firstVisibleIndexVal = null;
3338 this._lastVisibleIndexVal = null; 1730 this._lastVisibleIndexVal = null;
3339
3340 this._resetScrollPosition(0); 1731 this._resetScrollPosition(0);
3341 this._removeFocusedItem(); 1732 this._removeFocusedItem();
3342 // create the initial physical items
3343 if (!this._physicalItems) { 1733 if (!this._physicalItems) {
3344 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); 1734 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
3345 this._physicalItems = this._createPool(this._physicalCount); 1735 this._physicalItems = this._createPool(this._physicalCount);
3346 this._physicalSizes = new Array(this._physicalCount); 1736 this._physicalSizes = new Array(this._physicalCount);
3347 } 1737 }
3348
3349 this._physicalStart = 0; 1738 this._physicalStart = 0;
3350
3351 } else if (change.path === 'items.splices') { 1739 } else if (change.path === 'items.splices') {
3352
3353 this._adjustVirtualIndex(change.value.indexSplices); 1740 this._adjustVirtualIndex(change.value.indexSplices);
3354 this._virtualCount = this.items ? this.items.length : 0; 1741 this._virtualCount = this.items ? this.items.length : 0;
3355
3356 } else { 1742 } else {
3357 // update a single item
3358 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); 1743 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
3359 return; 1744 return;
3360 } 1745 }
3361
3362 this._itemsRendered = false; 1746 this._itemsRendered = false;
3363 this._debounceTemplate(this._render); 1747 this._debounceTemplate(this._render);
3364 }, 1748 },
3365
3366 /**
3367 * @param {!Array<!PolymerSplice>} splices
3368 */
3369 _adjustVirtualIndex: function(splices) { 1749 _adjustVirtualIndex: function(splices) {
3370 splices.forEach(function(splice) { 1750 splices.forEach(function(splice) {
3371 // deselect removed items
3372 splice.removed.forEach(this._removeItem, this); 1751 splice.removed.forEach(this._removeItem, this);
3373 // We only need to care about changes happening above the current positi on
3374 if (splice.index < this._virtualStart) { 1752 if (splice.index < this._virtualStart) {
3375 var delta = Math.max( 1753 var delta = Math.max(splice.addedCount - splice.removed.length, splice .index - this._virtualStart);
3376 splice.addedCount - splice.removed.length,
3377 splice.index - this._virtualStart);
3378
3379 this._virtualStart = this._virtualStart + delta; 1754 this._virtualStart = this._virtualStart + delta;
3380
3381 if (this._focusedIndex >= 0) { 1755 if (this._focusedIndex >= 0) {
3382 this._focusedIndex = this._focusedIndex + delta; 1756 this._focusedIndex = this._focusedIndex + delta;
3383 } 1757 }
3384 } 1758 }
3385 }, this); 1759 }, this);
3386 }, 1760 },
3387
3388 _removeItem: function(item) { 1761 _removeItem: function(item) {
3389 this.$.selector.deselect(item); 1762 this.$.selector.deselect(item);
3390 // remove the current focused item
3391 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) { 1763 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
3392 this._removeFocusedItem(); 1764 this._removeFocusedItem();
3393 } 1765 }
3394 }, 1766 },
3395
3396 /**
3397 * Executes a provided function per every physical index in `itemSet`
3398 * `itemSet` default value is equivalent to the entire set of physical index es.
3399 *
3400 * @param {!function(number, number)} fn
3401 * @param {!Array<number>=} itemSet
3402 */
3403 _iterateItems: function(fn, itemSet) { 1767 _iterateItems: function(fn, itemSet) {
3404 var pidx, vidx, rtn, i; 1768 var pidx, vidx, rtn, i;
3405
3406 if (arguments.length === 2 && itemSet) { 1769 if (arguments.length === 2 && itemSet) {
3407 for (i = 0; i < itemSet.length; i++) { 1770 for (i = 0; i < itemSet.length; i++) {
3408 pidx = itemSet[i]; 1771 pidx = itemSet[i];
3409 vidx = this._computeVidx(pidx); 1772 vidx = this._computeVidx(pidx);
3410 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1773 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3411 return rtn; 1774 return rtn;
3412 } 1775 }
3413 } 1776 }
3414 } else { 1777 } else {
3415 pidx = this._physicalStart; 1778 pidx = this._physicalStart;
3416 vidx = this._virtualStart; 1779 vidx = this._virtualStart;
3417 1780 for (;pidx < this._physicalCount; pidx++, vidx++) {
3418 for (; pidx < this._physicalCount; pidx++, vidx++) {
3419 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1781 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3420 return rtn; 1782 return rtn;
3421 } 1783 }
3422 } 1784 }
3423 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { 1785 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
3424 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1786 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3425 return rtn; 1787 return rtn;
3426 } 1788 }
3427 } 1789 }
3428 } 1790 }
3429 }, 1791 },
3430
3431 /**
3432 * Returns the virtual index for a given physical index
3433 *
3434 * @param {number} pidx Physical index
3435 * @return {number}
3436 */
3437 _computeVidx: function(pidx) { 1792 _computeVidx: function(pidx) {
3438 if (pidx >= this._physicalStart) { 1793 if (pidx >= this._physicalStart) {
3439 return this._virtualStart + (pidx - this._physicalStart); 1794 return this._virtualStart + (pidx - this._physicalStart);
3440 } 1795 }
3441 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; 1796 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
3442 }, 1797 },
3443
3444 /**
3445 * Assigns the data models to a given set of items.
3446 * @param {!Array<number>=} itemSet
3447 */
3448 _assignModels: function(itemSet) { 1798 _assignModels: function(itemSet) {
3449 this._iterateItems(function(pidx, vidx) { 1799 this._iterateItems(function(pidx, vidx) {
3450 var el = this._physicalItems[pidx]; 1800 var el = this._physicalItems[pidx];
3451 var inst = el._templateInstance; 1801 var inst = el._templateInstance;
3452 var item = this.items && this.items[vidx]; 1802 var item = this.items && this.items[vidx];
3453
3454 if (item != null) { 1803 if (item != null) {
3455 inst[this.as] = item; 1804 inst[this.as] = item;
3456 inst.__key__ = this._collection.getKey(item); 1805 inst.__key__ = this._collection.getKey(item);
3457 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item); 1806 inst[this.selectedAs] = this.$.selector.isSelected(item);
3458 inst[this.indexAs] = vidx; 1807 inst[this.indexAs] = vidx;
3459 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; 1808 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
3460 this._physicalIndexForKey[inst.__key__] = pidx; 1809 this._physicalIndexForKey[inst.__key__] = pidx;
3461 el.removeAttribute('hidden'); 1810 el.removeAttribute('hidden');
3462 } else { 1811 } else {
3463 inst.__key__ = null; 1812 inst.__key__ = null;
3464 el.setAttribute('hidden', ''); 1813 el.setAttribute('hidden', '');
3465 } 1814 }
3466 }, itemSet); 1815 }, itemSet);
3467 }, 1816 },
3468 1817 _updateMetrics: function(itemSet) {
3469 /**
3470 * Updates the height for a given set of items.
3471 *
3472 * @param {!Array<number>=} itemSet
3473 */
3474 _updateMetrics: function(itemSet) {
3475 // Make sure we distributed all the physical items
3476 // so we can measure them
3477 Polymer.dom.flush(); 1818 Polymer.dom.flush();
3478
3479 var newPhysicalSize = 0; 1819 var newPhysicalSize = 0;
3480 var oldPhysicalSize = 0; 1820 var oldPhysicalSize = 0;
3481 var prevAvgCount = this._physicalAverageCount; 1821 var prevAvgCount = this._physicalAverageCount;
3482 var prevPhysicalAvg = this._physicalAverage; 1822 var prevPhysicalAvg = this._physicalAverage;
3483
3484 this._iterateItems(function(pidx, vidx) { 1823 this._iterateItems(function(pidx, vidx) {
3485
3486 oldPhysicalSize += this._physicalSizes[pidx] || 0; 1824 oldPhysicalSize += this._physicalSizes[pidx] || 0;
3487 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; 1825 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
3488 newPhysicalSize += this._physicalSizes[pidx]; 1826 newPhysicalSize += this._physicalSizes[pidx];
3489 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; 1827 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
3490
3491 }, itemSet); 1828 }, itemSet);
3492
3493 this._viewportHeight = this._scrollTargetHeight; 1829 this._viewportHeight = this._scrollTargetHeight;
3494 if (this.grid) { 1830 if (this.grid) {
3495 this._updateGridMetrics(); 1831 this._updateGridMetrics();
3496 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight; 1832 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
3497 } else { 1833 } else {
3498 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize; 1834 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
3499 } 1835 }
3500
3501 // update the average if we measured something
3502 if (this._physicalAverageCount !== prevAvgCount) { 1836 if (this._physicalAverageCount !== prevAvgCount) {
3503 this._physicalAverage = Math.round( 1837 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new PhysicalSize) / this._physicalAverageCount);
3504 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
3505 this._physicalAverageCount);
3506 } 1838 }
3507 }, 1839 },
3508
3509 _updateGridMetrics: function() { 1840 _updateGridMetrics: function() {
3510 this._viewportWidth = this.$.items.offsetWidth; 1841 this._viewportWidth = this.$.items.offsetWidth;
3511 // Set item width to the value of the _physicalItems offsetWidth
3512 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE; 1842 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
3513 // Set row height to the value of the _physicalItems offsetHeight
3514 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE; 1843 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
3515 // If in grid mode compute how many items with exist in each row
3516 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow; 1844 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
3517 }, 1845 },
3518
3519 /**
3520 * Updates the position of the physical items.
3521 */
3522 _positionItems: function() { 1846 _positionItems: function() {
3523 this._adjustScrollPosition(); 1847 this._adjustScrollPosition();
3524
3525 var y = this._physicalTop; 1848 var y = this._physicalTop;
3526
3527 if (this.grid) { 1849 if (this.grid) {
3528 var totalItemWidth = this._itemsPerRow * this._itemWidth; 1850 var totalItemWidth = this._itemsPerRow * this._itemWidth;
3529 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; 1851 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
3530
3531 this._iterateItems(function(pidx, vidx) { 1852 this._iterateItems(function(pidx, vidx) {
3532
3533 var modulus = vidx % this._itemsPerRow; 1853 var modulus = vidx % this._itemsPerRow;
3534 var x = Math.floor((modulus * this._itemWidth) + rowOffset); 1854 var x = Math.floor(modulus * this._itemWidth + rowOffset);
3535
3536 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); 1855 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
3537
3538 if (this._shouldRenderNextRow(vidx)) { 1856 if (this._shouldRenderNextRow(vidx)) {
3539 y += this._rowHeight; 1857 y += this._rowHeight;
3540 } 1858 }
3541
3542 }); 1859 });
3543 } else { 1860 } else {
3544 this._iterateItems(function(pidx, vidx) { 1861 this._iterateItems(function(pidx, vidx) {
3545
3546 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); 1862 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
3547 y += this._physicalSizes[pidx]; 1863 y += this._physicalSizes[pidx];
3548
3549 }); 1864 });
3550 } 1865 }
3551 }, 1866 },
3552
3553 _getPhysicalSizeIncrement: function(pidx) { 1867 _getPhysicalSizeIncrement: function(pidx) {
3554 if (!this.grid) { 1868 if (!this.grid) {
3555 return this._physicalSizes[pidx]; 1869 return this._physicalSizes[pidx];
3556 } 1870 }
3557 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) { 1871 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
3558 return 0; 1872 return 0;
3559 } 1873 }
3560 return this._rowHeight; 1874 return this._rowHeight;
3561 }, 1875 },
3562
3563 /**
3564 * Returns, based on the current index,
3565 * whether or not the next index will need
3566 * to be rendered on a new row.
3567 *
3568 * @param {number} vidx Virtual index
3569 * @return {boolean}
3570 */
3571 _shouldRenderNextRow: function(vidx) { 1876 _shouldRenderNextRow: function(vidx) {
3572 return vidx % this._itemsPerRow === this._itemsPerRow - 1; 1877 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
3573 }, 1878 },
3574
3575 /**
3576 * Adjusts the scroll position when it was overestimated.
3577 */
3578 _adjustScrollPosition: function() { 1879 _adjustScrollPosition: function() {
3579 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : 1880 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min( this._scrollPosition + this._physicalTop, 0);
3580 Math.min(this._scrollPosition + this._physicalTop, 0);
3581
3582 if (deltaHeight) { 1881 if (deltaHeight) {
3583 this._physicalTop = this._physicalTop - deltaHeight; 1882 this._physicalTop = this._physicalTop - deltaHeight;
3584 // juking scroll position during interial scrolling on iOS is no bueno
3585 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { 1883 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
3586 this._resetScrollPosition(this._scrollTop - deltaHeight); 1884 this._resetScrollPosition(this._scrollTop - deltaHeight);
3587 } 1885 }
3588 } 1886 }
3589 }, 1887 },
3590
3591 /**
3592 * Sets the position of the scroll.
3593 */
3594 _resetScrollPosition: function(pos) { 1888 _resetScrollPosition: function(pos) {
3595 if (this.scrollTarget) { 1889 if (this.scrollTarget) {
3596 this._scrollTop = pos; 1890 this._scrollTop = pos;
3597 this._scrollPosition = this._scrollTop; 1891 this._scrollPosition = this._scrollTop;
3598 } 1892 }
3599 }, 1893 },
3600
3601 /**
3602 * Sets the scroll height, that's the height of the content,
3603 *
3604 * @param {boolean=} forceUpdate If true, updates the height no matter what.
3605 */
3606 _updateScrollerSize: function(forceUpdate) { 1894 _updateScrollerSize: function(forceUpdate) {
3607 if (this.grid) { 1895 if (this.grid) {
3608 this._estScrollHeight = this._virtualRowCount * this._rowHeight; 1896 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
3609 } else { 1897 } else {
3610 this._estScrollHeight = (this._physicalBottom + 1898 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
3611 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
3612 } 1899 }
3613
3614 forceUpdate = forceUpdate || this._scrollHeight === 0; 1900 forceUpdate = forceUpdate || this._scrollHeight === 0;
3615 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; 1901 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
3616 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight; 1902 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
3617
3618 // amortize height adjustment, so it won't trigger repaints very often
3619 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { 1903 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
3620 this.$.items.style.height = this._estScrollHeight + 'px'; 1904 this.$.items.style.height = this._estScrollHeight + 'px';
3621 this._scrollHeight = this._estScrollHeight; 1905 this._scrollHeight = this._estScrollHeight;
3622 } 1906 }
3623 }, 1907 },
3624 1908 scrollToItem: function(item) {
3625 /**
3626 * Scroll to a specific item in the virtual list regardless
3627 * of the physical items in the DOM tree.
3628 *
3629 * @method scrollToItem
3630 * @param {(Object)} item The item to be scrolled to
3631 */
3632 scrollToItem: function(item){
3633 return this.scrollToIndex(this.items.indexOf(item)); 1909 return this.scrollToIndex(this.items.indexOf(item));
3634 }, 1910 },
3635
3636 /**
3637 * Scroll to a specific index in the virtual list regardless
3638 * of the physical items in the DOM tree.
3639 *
3640 * @method scrollToIndex
3641 * @param {number} idx The index of the item
3642 */
3643 scrollToIndex: function(idx) { 1911 scrollToIndex: function(idx) {
3644 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { 1912 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
3645 return; 1913 return;
3646 } 1914 }
3647
3648 Polymer.dom.flush(); 1915 Polymer.dom.flush();
3649 1916 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1);
3650 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
3651 // update the virtual start only when needed
3652 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { 1917 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
3653 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1); 1918 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1;
3654 } 1919 }
3655 // manage focus
3656 this._manageFocus(); 1920 this._manageFocus();
3657 // assign new models
3658 this._assignModels(); 1921 this._assignModels();
3659 // measure the new sizes
3660 this._updateMetrics(); 1922 this._updateMetrics();
3661 1923 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
3662 // estimate new physical offset
3663 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
3664 this._physicalTop = estPhysicalTop; 1924 this._physicalTop = estPhysicalTop;
3665
3666 var currentTopItem = this._physicalStart; 1925 var currentTopItem = this._physicalStart;
3667 var currentVirtualItem = this._virtualStart; 1926 var currentVirtualItem = this._virtualStart;
3668 var targetOffsetTop = 0; 1927 var targetOffsetTop = 0;
3669 var hiddenContentSize = this._hiddenContentSize; 1928 var hiddenContentSize = this._hiddenContentSize;
3670
3671 // scroll to the item as much as we can
3672 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { 1929 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
3673 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem); 1930 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
3674 currentTopItem = (currentTopItem + 1) % this._physicalCount; 1931 currentTopItem = (currentTopItem + 1) % this._physicalCount;
3675 currentVirtualItem++; 1932 currentVirtualItem++;
3676 } 1933 }
3677 // update the scroller size
3678 this._updateScrollerSize(true); 1934 this._updateScrollerSize(true);
3679 // update the position of the items
3680 this._positionItems(); 1935 this._positionItems();
3681 // set the new scroll position
3682 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop); 1936 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
3683 // increase the pool of physical items if needed
3684 this._increasePoolIfNeeded(); 1937 this._increasePoolIfNeeded();
3685 // clear cached visible index
3686 this._firstVisibleIndexVal = null; 1938 this._firstVisibleIndexVal = null;
3687 this._lastVisibleIndexVal = null; 1939 this._lastVisibleIndexVal = null;
3688 }, 1940 },
3689
3690 /**
3691 * Reset the physical average and the average count.
3692 */
3693 _resetAverage: function() { 1941 _resetAverage: function() {
3694 this._physicalAverage = 0; 1942 this._physicalAverage = 0;
3695 this._physicalAverageCount = 0; 1943 this._physicalAverageCount = 0;
3696 }, 1944 },
3697
3698 /**
3699 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
3700 * when the element is resized.
3701 */
3702 _resizeHandler: function() { 1945 _resizeHandler: function() {
3703 // iOS fires the resize event when the address bar slides up
3704 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) { 1946 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
3705 return; 1947 return;
3706 } 1948 }
3707 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
3708 // changing the scroll position from a resize handler would result in
3709 // the scroll position being reset. Waiting 1ms fixes the issue.
3710 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { 1949 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
3711 this.updateViewportBoundaries(); 1950 this.updateViewportBoundaries();
3712 this._render(); 1951 this._render();
3713
3714 if (this._itemsRendered && this._physicalItems && this._isVisible) { 1952 if (this._itemsRendered && this._physicalItems && this._isVisible) {
3715 this._resetAverage(); 1953 this._resetAverage();
3716 this.scrollToIndex(this.firstVisibleIndex); 1954 this.scrollToIndex(this.firstVisibleIndex);
3717 } 1955 }
3718 }.bind(this), 1)); 1956 }.bind(this), 1));
3719 }, 1957 },
3720
3721 _getModelFromItem: function(item) { 1958 _getModelFromItem: function(item) {
3722 var key = this._collection.getKey(item); 1959 var key = this._collection.getKey(item);
3723 var pidx = this._physicalIndexForKey[key]; 1960 var pidx = this._physicalIndexForKey[key];
3724
3725 if (pidx != null) { 1961 if (pidx != null) {
3726 return this._physicalItems[pidx]._templateInstance; 1962 return this._physicalItems[pidx]._templateInstance;
3727 } 1963 }
3728 return null; 1964 return null;
3729 }, 1965 },
3730
3731 /**
3732 * Gets a valid item instance from its index or the object value.
3733 *
3734 * @param {(Object|number)} item The item object or its index
3735 */
3736 _getNormalizedItem: function(item) { 1966 _getNormalizedItem: function(item) {
3737 if (this._collection.getKey(item) === undefined) { 1967 if (this._collection.getKey(item) === undefined) {
3738 if (typeof item === 'number') { 1968 if (typeof item === 'number') {
3739 item = this.items[item]; 1969 item = this.items[item];
3740 if (!item) { 1970 if (!item) {
3741 throw new RangeError('<item> not found'); 1971 throw new RangeError('<item> not found');
3742 } 1972 }
3743 return item; 1973 return item;
3744 } 1974 }
3745 throw new TypeError('<item> should be a valid item'); 1975 throw new TypeError('<item> should be a valid item');
3746 } 1976 }
3747 return item; 1977 return item;
3748 }, 1978 },
3749
3750 /**
3751 * Select the list item at the given index.
3752 *
3753 * @method selectItem
3754 * @param {(Object|number)} item The item object or its index
3755 */
3756 selectItem: function(item) { 1979 selectItem: function(item) {
3757 item = this._getNormalizedItem(item); 1980 item = this._getNormalizedItem(item);
3758 var model = this._getModelFromItem(item); 1981 var model = this._getModelFromItem(item);
3759
3760 if (!this.multiSelection && this.selectedItem) { 1982 if (!this.multiSelection && this.selectedItem) {
3761 this.deselectItem(this.selectedItem); 1983 this.deselectItem(this.selectedItem);
3762 } 1984 }
3763 if (model) { 1985 if (model) {
3764 model[this.selectedAs] = true; 1986 model[this.selectedAs] = true;
3765 } 1987 }
3766 this.$.selector.select(item); 1988 this.$.selector.select(item);
3767 this.updateSizeForItem(item); 1989 this.updateSizeForItem(item);
3768 }, 1990 },
3769
3770 /**
3771 * Deselects the given item list if it is already selected.
3772 *
3773
3774 * @method deselect
3775 * @param {(Object|number)} item The item object or its index
3776 */
3777 deselectItem: function(item) { 1991 deselectItem: function(item) {
3778 item = this._getNormalizedItem(item); 1992 item = this._getNormalizedItem(item);
3779 var model = this._getModelFromItem(item); 1993 var model = this._getModelFromItem(item);
3780
3781 if (model) { 1994 if (model) {
3782 model[this.selectedAs] = false; 1995 model[this.selectedAs] = false;
3783 } 1996 }
3784 this.$.selector.deselect(item); 1997 this.$.selector.deselect(item);
3785 this.updateSizeForItem(item); 1998 this.updateSizeForItem(item);
3786 }, 1999 },
3787
3788 /**
3789 * Select or deselect a given item depending on whether the item
3790 * has already been selected.
3791 *
3792 * @method toggleSelectionForItem
3793 * @param {(Object|number)} item The item object or its index
3794 */
3795 toggleSelectionForItem: function(item) { 2000 toggleSelectionForItem: function(item) {
3796 item = this._getNormalizedItem(item); 2001 item = this._getNormalizedItem(item);
3797 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) { 2002 if (this.$.selector.isSelected(item)) {
3798 this.deselectItem(item); 2003 this.deselectItem(item);
3799 } else { 2004 } else {
3800 this.selectItem(item); 2005 this.selectItem(item);
3801 } 2006 }
3802 }, 2007 },
3803
3804 /**
3805 * Clears the current selection state of the list.
3806 *
3807 * @method clearSelection
3808 */
3809 clearSelection: function() { 2008 clearSelection: function() {
3810 function unselect(item) { 2009 function unselect(item) {
3811 var model = this._getModelFromItem(item); 2010 var model = this._getModelFromItem(item);
3812 if (model) { 2011 if (model) {
3813 model[this.selectedAs] = false; 2012 model[this.selectedAs] = false;
3814 } 2013 }
3815 } 2014 }
3816
3817 if (Array.isArray(this.selectedItems)) { 2015 if (Array.isArray(this.selectedItems)) {
3818 this.selectedItems.forEach(unselect, this); 2016 this.selectedItems.forEach(unselect, this);
3819 } else if (this.selectedItem) { 2017 } else if (this.selectedItem) {
3820 unselect.call(this, this.selectedItem); 2018 unselect.call(this, this.selectedItem);
3821 } 2019 }
3822 2020 this.$.selector.clearSelection();
3823 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
3824 }, 2021 },
3825
3826 /**
3827 * Add an event listener to `tap` if `selectionEnabled` is true,
3828 * it will remove the listener otherwise.
3829 */
3830 _selectionEnabledChanged: function(selectionEnabled) { 2022 _selectionEnabledChanged: function(selectionEnabled) {
3831 var handler = selectionEnabled ? this.listen : this.unlisten; 2023 var handler = selectionEnabled ? this.listen : this.unlisten;
3832 handler.call(this, this, 'tap', '_selectionHandler'); 2024 handler.call(this, this, 'tap', '_selectionHandler');
3833 }, 2025 },
3834
3835 /**
3836 * Select an item from an event object.
3837 */
3838 _selectionHandler: function(e) { 2026 _selectionHandler: function(e) {
3839 var model = this.modelForElement(e.target); 2027 var model = this.modelForElement(e.target);
3840 if (!model) { 2028 if (!model) {
3841 return; 2029 return;
3842 } 2030 }
3843 var modelTabIndex, activeElTabIndex; 2031 var modelTabIndex, activeElTabIndex;
3844 var target = Polymer.dom(e).path[0]; 2032 var target = Polymer.dom(e).path[0];
3845 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement; 2033 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
3846 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])]; 2034 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
3847 // Safari does not focus certain form controls via mouse 2035 if (target.localName === 'input' || target.localName === 'button' || targe t.localName === 'select') {
3848 // https://bugs.webkit.org/show_bug.cgi?id=118043
3849 if (target.localName === 'input' ||
3850 target.localName === 'button' ||
3851 target.localName === 'select') {
3852 return; 2036 return;
3853 } 2037 }
3854 // Set a temporary tabindex
3855 modelTabIndex = model.tabIndex; 2038 modelTabIndex = model.tabIndex;
3856 model.tabIndex = SECRET_TABINDEX; 2039 model.tabIndex = SECRET_TABINDEX;
3857 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; 2040 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
3858 model.tabIndex = modelTabIndex; 2041 model.tabIndex = modelTabIndex;
3859 // Only select the item if the tap wasn't on a focusable child
3860 // or the element bound to `tabIndex`
3861 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) { 2042 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
3862 return; 2043 return;
3863 } 2044 }
3864 this.toggleSelectionForItem(model[this.as]); 2045 this.toggleSelectionForItem(model[this.as]);
3865 }, 2046 },
3866
3867 _multiSelectionChanged: function(multiSelection) { 2047 _multiSelectionChanged: function(multiSelection) {
3868 this.clearSelection(); 2048 this.clearSelection();
3869 this.$.selector.multi = multiSelection; 2049 this.$.selector.multi = multiSelection;
3870 }, 2050 },
3871
3872 /**
3873 * Updates the size of an item.
3874 *
3875 * @method updateSizeForItem
3876 * @param {(Object|number)} item The item object or its index
3877 */
3878 updateSizeForItem: function(item) { 2051 updateSizeForItem: function(item) {
3879 item = this._getNormalizedItem(item); 2052 item = this._getNormalizedItem(item);
3880 var key = this._collection.getKey(item); 2053 var key = this._collection.getKey(item);
3881 var pidx = this._physicalIndexForKey[key]; 2054 var pidx = this._physicalIndexForKey[key];
3882
3883 if (pidx != null) { 2055 if (pidx != null) {
3884 this._updateMetrics([pidx]); 2056 this._updateMetrics([ pidx ]);
3885 this._positionItems(); 2057 this._positionItems();
3886 } 2058 }
3887 }, 2059 },
3888
3889 /**
3890 * Creates a temporary backfill item in the rendered pool of physical items
3891 * to replace the main focused item. The focused item has tabIndex = 0
3892 * and might be currently focused by the user.
3893 *
3894 * This dynamic replacement helps to preserve the focus state.
3895 */
3896 _manageFocus: function() { 2060 _manageFocus: function() {
3897 var fidx = this._focusedIndex; 2061 var fidx = this._focusedIndex;
3898
3899 if (fidx >= 0 && fidx < this._virtualCount) { 2062 if (fidx >= 0 && fidx < this._virtualCount) {
3900 // if it's a valid index, check if that index is rendered
3901 // in a physical item.
3902 if (this._isIndexRendered(fidx)) { 2063 if (this._isIndexRendered(fidx)) {
3903 this._restoreFocusedItem(); 2064 this._restoreFocusedItem();
3904 } else { 2065 } else {
3905 this._createFocusBackfillItem(); 2066 this._createFocusBackfillItem();
3906 } 2067 }
3907 } else if (this._virtualCount > 0 && this._physicalCount > 0) { 2068 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
3908 // otherwise, assign the initial focused index.
3909 this._focusedIndex = this._virtualStart; 2069 this._focusedIndex = this._virtualStart;
3910 this._focusedItem = this._physicalItems[this._physicalStart]; 2070 this._focusedItem = this._physicalItems[this._physicalStart];
3911 } 2071 }
3912 }, 2072 },
3913
3914 _isIndexRendered: function(idx) { 2073 _isIndexRendered: function(idx) {
3915 return idx >= this._virtualStart && idx <= this._virtualEnd; 2074 return idx >= this._virtualStart && idx <= this._virtualEnd;
3916 }, 2075 },
3917
3918 _isIndexVisible: function(idx) { 2076 _isIndexVisible: function(idx) {
3919 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; 2077 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
3920 }, 2078 },
3921
3922 _getPhysicalIndex: function(idx) { 2079 _getPhysicalIndex: function(idx) {
3923 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))]; 2080 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
3924 }, 2081 },
3925
3926 _focusPhysicalItem: function(idx) { 2082 _focusPhysicalItem: function(idx) {
3927 if (idx < 0 || idx >= this._virtualCount) { 2083 if (idx < 0 || idx >= this._virtualCount) {
3928 return; 2084 return;
3929 } 2085 }
3930 this._restoreFocusedItem(); 2086 this._restoreFocusedItem();
3931 // scroll to index to make sure it's rendered
3932 if (!this._isIndexRendered(idx)) { 2087 if (!this._isIndexRendered(idx)) {
3933 this.scrollToIndex(idx); 2088 this.scrollToIndex(idx);
3934 } 2089 }
3935
3936 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; 2090 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
3937 var model = physicalItem._templateInstance; 2091 var model = physicalItem._templateInstance;
3938 var focusable; 2092 var focusable;
3939
3940 // set a secret tab index
3941 model.tabIndex = SECRET_TABINDEX; 2093 model.tabIndex = SECRET_TABINDEX;
3942 // check if focusable element is the physical item
3943 if (physicalItem.tabIndex === SECRET_TABINDEX) { 2094 if (physicalItem.tabIndex === SECRET_TABINDEX) {
3944 focusable = physicalItem; 2095 focusable = physicalItem;
3945 } 2096 }
3946 // search for the element which tabindex is bound to the secret tab index
3947 if (!focusable) { 2097 if (!focusable) {
3948 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]'); 2098 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
3949 } 2099 }
3950 // restore the tab index
3951 model.tabIndex = 0; 2100 model.tabIndex = 0;
3952 // focus the focusable element
3953 this._focusedIndex = idx; 2101 this._focusedIndex = idx;
3954 focusable && focusable.focus(); 2102 focusable && focusable.focus();
3955 }, 2103 },
3956
3957 _removeFocusedItem: function() { 2104 _removeFocusedItem: function() {
3958 if (this._offscreenFocusedItem) { 2105 if (this._offscreenFocusedItem) {
3959 Polymer.dom(this).removeChild(this._offscreenFocusedItem); 2106 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
3960 } 2107 }
3961 this._offscreenFocusedItem = null; 2108 this._offscreenFocusedItem = null;
3962 this._focusBackfillItem = null; 2109 this._focusBackfillItem = null;
3963 this._focusedItem = null; 2110 this._focusedItem = null;
3964 this._focusedIndex = -1; 2111 this._focusedIndex = -1;
3965 }, 2112 },
3966
3967 _createFocusBackfillItem: function() { 2113 _createFocusBackfillItem: function() {
3968 var pidx, fidx = this._focusedIndex; 2114 var pidx, fidx = this._focusedIndex;
3969 if (this._offscreenFocusedItem || fidx < 0) { 2115 if (this._offscreenFocusedItem || fidx < 0) {
3970 return; 2116 return;
3971 } 2117 }
3972 if (!this._focusBackfillItem) { 2118 if (!this._focusBackfillItem) {
3973 // create a physical item, so that it backfills the focused item.
3974 var stampedTemplate = this.stamp(null); 2119 var stampedTemplate = this.stamp(null);
3975 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); 2120 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
3976 Polymer.dom(this).appendChild(stampedTemplate.root); 2121 Polymer.dom(this).appendChild(stampedTemplate.root);
3977 } 2122 }
3978 // get the physical index for the focused index
3979 pidx = this._getPhysicalIndex(fidx); 2123 pidx = this._getPhysicalIndex(fidx);
3980
3981 if (pidx != null) { 2124 if (pidx != null) {
3982 // set the offcreen focused physical item
3983 this._offscreenFocusedItem = this._physicalItems[pidx]; 2125 this._offscreenFocusedItem = this._physicalItems[pidx];
3984 // backfill the focused physical item
3985 this._physicalItems[pidx] = this._focusBackfillItem; 2126 this._physicalItems[pidx] = this._focusBackfillItem;
3986 // hide the focused physical
3987 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); 2127 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
3988 } 2128 }
3989 }, 2129 },
3990
3991 _restoreFocusedItem: function() { 2130 _restoreFocusedItem: function() {
3992 var pidx, fidx = this._focusedIndex; 2131 var pidx, fidx = this._focusedIndex;
3993
3994 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { 2132 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
3995 return; 2133 return;
3996 } 2134 }
3997 // assign models to the focused index
3998 this._assignModels(); 2135 this._assignModels();
3999 // get the new physical index for the focused index
4000 pidx = this._getPhysicalIndex(fidx); 2136 pidx = this._getPhysicalIndex(fidx);
4001
4002 if (pidx != null) { 2137 if (pidx != null) {
4003 // flip the focus backfill
4004 this._focusBackfillItem = this._physicalItems[pidx]; 2138 this._focusBackfillItem = this._physicalItems[pidx];
4005 // restore the focused physical item
4006 this._physicalItems[pidx] = this._offscreenFocusedItem; 2139 this._physicalItems[pidx] = this._offscreenFocusedItem;
4007 // reset the offscreen focused item
4008 this._offscreenFocusedItem = null; 2140 this._offscreenFocusedItem = null;
4009 // hide the physical item that backfills
4010 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); 2141 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
4011 } 2142 }
4012 }, 2143 },
4013
4014 _didFocus: function(e) { 2144 _didFocus: function(e) {
4015 var targetModel = this.modelForElement(e.target); 2145 var targetModel = this.modelForElement(e.target);
4016 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null; 2146 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
4017 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; 2147 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
4018 var fidx = this._focusedIndex; 2148 var fidx = this._focusedIndex;
4019
4020 if (!targetModel || !focusedModel) { 2149 if (!targetModel || !focusedModel) {
4021 return; 2150 return;
4022 } 2151 }
4023 if (focusedModel === targetModel) { 2152 if (focusedModel === targetModel) {
4024 // if the user focused the same item, then bring it into view if it's no t visible
4025 if (!this._isIndexVisible(fidx)) { 2153 if (!this._isIndexVisible(fidx)) {
4026 this.scrollToIndex(fidx); 2154 this.scrollToIndex(fidx);
4027 } 2155 }
4028 } else { 2156 } else {
4029 this._restoreFocusedItem(); 2157 this._restoreFocusedItem();
4030 // restore tabIndex for the currently focused item
4031 focusedModel.tabIndex = -1; 2158 focusedModel.tabIndex = -1;
4032 // set the tabIndex for the next focused item
4033 targetModel.tabIndex = 0; 2159 targetModel.tabIndex = 0;
4034 fidx = targetModel[this.indexAs]; 2160 fidx = targetModel[this.indexAs];
4035 this._focusedIndex = fidx; 2161 this._focusedIndex = fidx;
4036 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; 2162 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
4037
4038 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { 2163 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
4039 this._update(); 2164 this._update();
4040 } 2165 }
4041 } 2166 }
4042 }, 2167 },
4043
4044 _didMoveUp: function() { 2168 _didMoveUp: function() {
4045 this._focusPhysicalItem(this._focusedIndex - 1); 2169 this._focusPhysicalItem(this._focusedIndex - 1);
4046 }, 2170 },
4047
4048 _didMoveDown: function(e) { 2171 _didMoveDown: function(e) {
4049 // disable scroll when pressing the down key
4050 e.detail.keyboardEvent.preventDefault(); 2172 e.detail.keyboardEvent.preventDefault();
4051 this._focusPhysicalItem(this._focusedIndex + 1); 2173 this._focusPhysicalItem(this._focusedIndex + 1);
4052 }, 2174 },
4053
4054 _didEnter: function(e) { 2175 _didEnter: function(e) {
4055 this._focusPhysicalItem(this._focusedIndex); 2176 this._focusPhysicalItem(this._focusedIndex);
4056 this._selectionHandler(e.detail.keyboardEvent); 2177 this._selectionHandler(e.detail.keyboardEvent);
4057 } 2178 }
4058 }); 2179 });
2180 })();
4059 2181
4060 })();
4061 // Copyright 2015 The Chromium Authors. All rights reserved. 2182 // Copyright 2015 The Chromium Authors. All rights reserved.
4062 // Use of this source code is governed by a BSD-style license that can be 2183 // Use of this source code is governed by a BSD-style license that can be
4063 // found in the LICENSE file. 2184 // found in the LICENSE file.
4064
4065 cr.define('downloads', function() { 2185 cr.define('downloads', function() {
4066 /**
4067 * @param {string} chromeSendName
4068 * @return {function(string):void} A chrome.send() callback with curried name.
4069 */
4070 function chromeSendWithId(chromeSendName) { 2186 function chromeSendWithId(chromeSendName) {
4071 return function(id) { chrome.send(chromeSendName, [id]); }; 2187 return function(id) {
2188 chrome.send(chromeSendName, [ id ]);
2189 };
4072 } 2190 }
4073
4074 /** @constructor */
4075 function ActionService() { 2191 function ActionService() {
4076 /** @private {Array<string>} */
4077 this.searchTerms_ = []; 2192 this.searchTerms_ = [];
4078 } 2193 }
4079 2194 function trim(s) {
4080 /** 2195 return s.trim();
4081 * @param {string} s 2196 }
4082 * @return {string} |s| without whitespace at the beginning or end. 2197 function truthy(value) {
4083 */ 2198 return !!value;
4084 function trim(s) { return s.trim(); } 2199 }
4085
4086 /**
4087 * @param {string|undefined} value
4088 * @return {boolean} Whether |value| is truthy.
4089 */
4090 function truthy(value) { return !!value; }
4091
4092 /**
4093 * @param {string} searchText Input typed by the user into a search box.
4094 * @return {Array<string>} A list of terms extracted from |searchText|.
4095 */
4096 ActionService.splitTerms = function(searchText) { 2200 ActionService.splitTerms = function(searchText) {
4097 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
4098 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); 2201 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
4099 }; 2202 };
4100
4101 ActionService.prototype = { 2203 ActionService.prototype = {
4102 /** @param {string} id ID of the download to cancel. */
4103 cancel: chromeSendWithId('cancel'), 2204 cancel: chromeSendWithId('cancel'),
4104
4105 /** Instructs the browser to clear all finished downloads. */
4106 clearAll: function() { 2205 clearAll: function() {
4107 if (loadTimeData.getBoolean('allowDeletingHistory')) { 2206 if (loadTimeData.getBoolean('allowDeletingHistory')) {
4108 chrome.send('clearAll'); 2207 chrome.send('clearAll');
4109 this.search(''); 2208 this.search('');
4110 } 2209 }
4111 }, 2210 },
4112
4113 /** @param {string} id ID of the dangerous download to discard. */
4114 discardDangerous: chromeSendWithId('discardDangerous'), 2211 discardDangerous: chromeSendWithId('discardDangerous'),
4115
4116 /** @param {string} url URL of a file to download. */
4117 download: function(url) { 2212 download: function(url) {
4118 var a = document.createElement('a'); 2213 var a = document.createElement('a');
4119 a.href = url; 2214 a.href = url;
4120 a.setAttribute('download', ''); 2215 a.setAttribute('download', '');
4121 a.click(); 2216 a.click();
4122 }, 2217 },
4123
4124 /** @param {string} id ID of the download that the user started dragging. */
4125 drag: chromeSendWithId('drag'), 2218 drag: chromeSendWithId('drag'),
4126
4127 /** Loads more downloads with the current search terms. */
4128 loadMore: function() { 2219 loadMore: function() {
4129 chrome.send('getDownloads', this.searchTerms_); 2220 chrome.send('getDownloads', this.searchTerms_);
4130 }, 2221 },
4131
4132 /**
4133 * @return {boolean} Whether the user is currently searching for downloads
4134 * (i.e. has a non-empty search term).
4135 */
4136 isSearching: function() { 2222 isSearching: function() {
4137 return this.searchTerms_.length > 0; 2223 return this.searchTerms_.length > 0;
4138 }, 2224 },
4139
4140 /** Opens the current local destination for downloads. */
4141 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), 2225 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
4142
4143 /**
4144 * @param {string} id ID of the download to run locally on the user's box.
4145 */
4146 openFile: chromeSendWithId('openFile'), 2226 openFile: chromeSendWithId('openFile'),
4147
4148 /** @param {string} id ID the of the progressing download to pause. */
4149 pause: chromeSendWithId('pause'), 2227 pause: chromeSendWithId('pause'),
4150
4151 /** @param {string} id ID of the finished download to remove. */
4152 remove: chromeSendWithId('remove'), 2228 remove: chromeSendWithId('remove'),
4153
4154 /** @param {string} id ID of the paused download to resume. */
4155 resume: chromeSendWithId('resume'), 2229 resume: chromeSendWithId('resume'),
4156
4157 /**
4158 * @param {string} id ID of the dangerous download to save despite
4159 * warnings.
4160 */
4161 saveDangerous: chromeSendWithId('saveDangerous'), 2230 saveDangerous: chromeSendWithId('saveDangerous'),
4162
4163 /** @param {string} searchText What to search for. */
4164 search: function(searchText) { 2231 search: function(searchText) {
4165 var searchTerms = ActionService.splitTerms(searchText); 2232 var searchTerms = ActionService.splitTerms(searchText);
4166 var sameTerms = searchTerms.length == this.searchTerms_.length; 2233 var sameTerms = searchTerms.length == this.searchTerms_.length;
4167
4168 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { 2234 for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
4169 if (searchTerms[i] != this.searchTerms_[i]) 2235 if (searchTerms[i] != this.searchTerms_[i]) sameTerms = false;
4170 sameTerms = false;
4171 } 2236 }
4172 2237 if (sameTerms) return;
4173 if (sameTerms)
4174 return;
4175
4176 this.searchTerms_ = searchTerms; 2238 this.searchTerms_ = searchTerms;
4177 this.loadMore(); 2239 this.loadMore();
4178 }, 2240 },
2241 show: chromeSendWithId('show'),
2242 undo: chrome.send.bind(chrome, 'undo')
2243 };
2244 cr.addSingletonGetter(ActionService);
2245 return {
2246 ActionService: ActionService
2247 };
2248 });
4179 2249
4180 /**
4181 * Shows the local folder a finished download resides in.
4182 * @param {string} id ID of the download to show.
4183 */
4184 show: chromeSendWithId('show'),
4185
4186 /** Undo download removal. */
4187 undo: chrome.send.bind(chrome, 'undo'),
4188 };
4189
4190 cr.addSingletonGetter(ActionService);
4191
4192 return {ActionService: ActionService};
4193 });
4194 // Copyright 2015 The Chromium Authors. All rights reserved. 2250 // Copyright 2015 The Chromium Authors. All rights reserved.
4195 // Use of this source code is governed by a BSD-style license that can be 2251 // Use of this source code is governed by a BSD-style license that can be
4196 // found in the LICENSE file. 2252 // found in the LICENSE file.
4197
4198 cr.define('downloads', function() { 2253 cr.define('downloads', function() {
4199 /**
4200 * Explains why a download is in DANGEROUS state.
4201 * @enum {string}
4202 */
4203 var DangerType = { 2254 var DangerType = {
4204 NOT_DANGEROUS: 'NOT_DANGEROUS', 2255 NOT_DANGEROUS: 'NOT_DANGEROUS',
4205 DANGEROUS_FILE: 'DANGEROUS_FILE', 2256 DANGEROUS_FILE: 'DANGEROUS_FILE',
4206 DANGEROUS_URL: 'DANGEROUS_URL', 2257 DANGEROUS_URL: 'DANGEROUS_URL',
4207 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', 2258 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
4208 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', 2259 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
4209 DANGEROUS_HOST: 'DANGEROUS_HOST', 2260 DANGEROUS_HOST: 'DANGEROUS_HOST',
4210 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', 2261 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED'
4211 }; 2262 };
4212
4213 /**
4214 * The states a download can be in. These correspond to states defined in
4215 * DownloadsDOMHandler::CreateDownloadItemValue
4216 * @enum {string}
4217 */
4218 var States = { 2263 var States = {
4219 IN_PROGRESS: 'IN_PROGRESS', 2264 IN_PROGRESS: 'IN_PROGRESS',
4220 CANCELLED: 'CANCELLED', 2265 CANCELLED: 'CANCELLED',
4221 COMPLETE: 'COMPLETE', 2266 COMPLETE: 'COMPLETE',
4222 PAUSED: 'PAUSED', 2267 PAUSED: 'PAUSED',
4223 DANGEROUS: 'DANGEROUS', 2268 DANGEROUS: 'DANGEROUS',
4224 INTERRUPTED: 'INTERRUPTED', 2269 INTERRUPTED: 'INTERRUPTED'
4225 }; 2270 };
4226
4227 return { 2271 return {
4228 DangerType: DangerType, 2272 DangerType: DangerType,
4229 States: States, 2273 States: States
4230 }; 2274 };
4231 }); 2275 });
2276
4232 // Copyright 2014 The Chromium Authors. All rights reserved. 2277 // Copyright 2014 The Chromium Authors. All rights reserved.
4233 // Use of this source code is governed by a BSD-style license that can be 2278 // Use of this source code is governed by a BSD-style license that can be
4234 // found in the LICENSE file. 2279 // found in the LICENSE file.
4235
4236 // Action links are elements that are used to perform an in-page navigation or
4237 // action (e.g. showing a dialog).
4238 //
4239 // They look like normal anchor (<a>) tags as their text color is blue. However,
4240 // they're subtly different as they're not initially underlined (giving users a
4241 // clue that underlined links navigate while action links don't).
4242 //
4243 // Action links look very similar to normal links when hovered (hand cursor,
4244 // underlined). This gives the user an idea that clicking this link will do
4245 // something similar to navigation but in the same page.
4246 //
4247 // They can be created in JavaScript like this:
4248 //
4249 // var link = document.createElement('a', 'action-link'); // Note second arg.
4250 //
4251 // or with a constructor like this:
4252 //
4253 // var link = new ActionLink();
4254 //
4255 // They can be used easily from HTML as well, like so:
4256 //
4257 // <a is="action-link">Click me!</a>
4258 //
4259 // NOTE: <action-link> and document.createElement('action-link') don't work.
4260
4261 /**
4262 * @constructor
4263 * @extends {HTMLAnchorElement}
4264 */
4265 var ActionLink = document.registerElement('action-link', { 2280 var ActionLink = document.registerElement('action-link', {
4266 prototype: { 2281 prototype: {
4267 __proto__: HTMLAnchorElement.prototype, 2282 __proto__: HTMLAnchorElement.prototype,
4268
4269 /** @this {ActionLink} */
4270 createdCallback: function() { 2283 createdCallback: function() {
4271 // Action links can start disabled (e.g. <a is="action-link" disabled>).
4272 this.tabIndex = this.disabled ? -1 : 0; 2284 this.tabIndex = this.disabled ? -1 : 0;
4273 2285 if (!this.hasAttribute('role')) this.setAttribute('role', 'link');
4274 if (!this.hasAttribute('role'))
4275 this.setAttribute('role', 'link');
4276
4277 this.addEventListener('keydown', function(e) { 2286 this.addEventListener('keydown', function(e) {
4278 if (!this.disabled && e.key == 'Enter' && !this.href) { 2287 if (!this.disabled && e.key == 'Enter' && !this.href) {
4279 // Schedule a click asynchronously because other 'keydown' handlers
4280 // may still run later (e.g. document.addEventListener('keydown')).
4281 // Specifically options dialogs break when this timeout isn't here.
4282 // NOTE: this affects the "trusted" state of the ensuing click. I
4283 // haven't found anything that breaks because of this (yet).
4284 window.setTimeout(this.click.bind(this), 0); 2288 window.setTimeout(this.click.bind(this), 0);
4285 } 2289 }
4286 }); 2290 });
4287
4288 function preventDefault(e) { 2291 function preventDefault(e) {
4289 e.preventDefault(); 2292 e.preventDefault();
4290 } 2293 }
4291
4292 function removePreventDefault() { 2294 function removePreventDefault() {
4293 document.removeEventListener('selectstart', preventDefault); 2295 document.removeEventListener('selectstart', preventDefault);
4294 document.removeEventListener('mouseup', removePreventDefault); 2296 document.removeEventListener('mouseup', removePreventDefault);
4295 } 2297 }
4296
4297 this.addEventListener('mousedown', function() { 2298 this.addEventListener('mousedown', function() {
4298 // This handlers strives to match the behavior of <a href="...">.
4299
4300 // While the mouse is down, prevent text selection from dragging.
4301 document.addEventListener('selectstart', preventDefault); 2299 document.addEventListener('selectstart', preventDefault);
4302 document.addEventListener('mouseup', removePreventDefault); 2300 document.addEventListener('mouseup', removePreventDefault);
4303 2301 if (document.activeElement != this) this.classList.add('no-outline');
4304 // If focus started via mouse press, don't show an outline.
4305 if (document.activeElement != this)
4306 this.classList.add('no-outline');
4307 }); 2302 });
4308
4309 this.addEventListener('blur', function() { 2303 this.addEventListener('blur', function() {
4310 this.classList.remove('no-outline'); 2304 this.classList.remove('no-outline');
4311 }); 2305 });
4312 }, 2306 },
4313
4314 /** @type {boolean} */
4315 set disabled(disabled) { 2307 set disabled(disabled) {
4316 if (disabled) 2308 if (disabled) HTMLAnchorElement.prototype.setAttribute.call(this, 'disable d', ''); else HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled') ;
4317 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
4318 else
4319 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
4320 this.tabIndex = disabled ? -1 : 0; 2309 this.tabIndex = disabled ? -1 : 0;
4321 }, 2310 },
4322 get disabled() { 2311 get disabled() {
4323 return this.hasAttribute('disabled'); 2312 return this.hasAttribute('disabled');
4324 }, 2313 },
4325
4326 /** @override */
4327 setAttribute: function(attr, val) { 2314 setAttribute: function(attr, val) {
4328 if (attr.toLowerCase() == 'disabled') 2315 if (attr.toLowerCase() == 'disabled') this.disabled = true; else HTMLAncho rElement.prototype.setAttribute.apply(this, arguments);
4329 this.disabled = true; 2316 },
4330 else
4331 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
4332 },
4333
4334 /** @override */
4335 removeAttribute: function(attr) { 2317 removeAttribute: function(attr) {
4336 if (attr.toLowerCase() == 'disabled') 2318 if (attr.toLowerCase() == 'disabled') this.disabled = false; else HTMLAnch orElement.prototype.removeAttribute.apply(this, arguments);
4337 this.disabled = false; 2319 }
4338 else 2320 },
4339 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); 2321 "extends": 'a'
4340 },
4341 },
4342
4343 extends: 'a',
4344 }); 2322 });
2323
4345 (function() { 2324 (function() {
4346 2325 var metaDatas = {};
4347 // monostate data 2326 var metaArrays = {};
4348 var metaDatas = {}; 2327 var singleton = null;
4349 var metaArrays = {}; 2328 Polymer.IronMeta = Polymer({
4350 var singleton = null; 2329 is: 'iron-meta',
4351 2330 properties: {
4352 Polymer.IronMeta = Polymer({ 2331 type: {
4353 2332 type: String,
4354 is: 'iron-meta', 2333 value: 'default',
4355 2334 observer: '_typeChanged'
4356 properties: { 2335 },
4357 2336 key: {
4358 /** 2337 type: String,
4359 * The type of meta-data. All meta-data of the same type is stored 2338 observer: '_keyChanged'
4360 * together. 2339 },
4361 */ 2340 value: {
4362 type: { 2341 type: Object,
4363 type: String, 2342 notify: true,
4364 value: 'default', 2343 observer: '_valueChanged'
4365 observer: '_typeChanged' 2344 },
4366 }, 2345 self: {
4367 2346 type: Boolean,
4368 /** 2347 observer: '_selfChanged'
4369 * The key used to store `value` under the `type` namespace. 2348 },
4370 */ 2349 list: {
4371 key: { 2350 type: Array,
4372 type: String, 2351 notify: true
4373 observer: '_keyChanged' 2352 }
4374 }, 2353 },
4375 2354 hostAttributes: {
4376 /** 2355 hidden: true
4377 * The meta-data to store or retrieve. 2356 },
4378 */ 2357 factoryImpl: function(config) {
4379 value: { 2358 if (config) {
4380 type: Object, 2359 for (var n in config) {
4381 notify: true, 2360 switch (n) {
4382 observer: '_valueChanged' 2361 case 'type':
4383 }, 2362 case 'key':
4384 2363 case 'value':
4385 /** 2364 this[n] = config[n];
4386 * If true, `value` is set to the iron-meta instance itself. 2365 break;
4387 */
4388 self: {
4389 type: Boolean,
4390 observer: '_selfChanged'
4391 },
4392
4393 /**
4394 * Array of all meta-data values for the given type.
4395 */
4396 list: {
4397 type: Array,
4398 notify: true
4399 }
4400
4401 },
4402
4403 hostAttributes: {
4404 hidden: true
4405 },
4406
4407 /**
4408 * Only runs if someone invokes the factory/constructor directly
4409 * e.g. `new Polymer.IronMeta()`
4410 *
4411 * @param {{type: (string|undefined), key: (string|undefined), value}=} co nfig
4412 */
4413 factoryImpl: function(config) {
4414 if (config) {
4415 for (var n in config) {
4416 switch(n) {
4417 case 'type':
4418 case 'key':
4419 case 'value':
4420 this[n] = config[n];
4421 break;
4422 }
4423 } 2366 }
4424 } 2367 }
4425 }, 2368 }
4426 2369 },
4427 created: function() { 2370 created: function() {
4428 // TODO(sjmiles): good for debugging? 2371 this._metaDatas = metaDatas;
4429 this._metaDatas = metaDatas; 2372 this._metaArrays = metaArrays;
4430 this._metaArrays = metaArrays; 2373 },
4431 }, 2374 _keyChanged: function(key, old) {
4432 2375 this._resetRegistration(old);
4433 _keyChanged: function(key, old) { 2376 },
4434 this._resetRegistration(old); 2377 _valueChanged: function(value) {
4435 }, 2378 this._resetRegistration(this.key);
4436 2379 },
4437 _valueChanged: function(value) { 2380 _selfChanged: function(self) {
4438 this._resetRegistration(this.key); 2381 if (self) {
4439 }, 2382 this.value = this;
4440 2383 }
4441 _selfChanged: function(self) { 2384 },
4442 if (self) { 2385 _typeChanged: function(type) {
4443 this.value = this; 2386 this._unregisterKey(this.key);
2387 if (!metaDatas[type]) {
2388 metaDatas[type] = {};
2389 }
2390 this._metaData = metaDatas[type];
2391 if (!metaArrays[type]) {
2392 metaArrays[type] = [];
2393 }
2394 this.list = metaArrays[type];
2395 this._registerKeyValue(this.key, this.value);
2396 },
2397 byKey: function(key) {
2398 return this._metaData && this._metaData[key];
2399 },
2400 _resetRegistration: function(oldKey) {
2401 this._unregisterKey(oldKey);
2402 this._registerKeyValue(this.key, this.value);
2403 },
2404 _unregisterKey: function(key) {
2405 this._unregister(key, this._metaData, this.list);
2406 },
2407 _registerKeyValue: function(key, value) {
2408 this._register(key, value, this._metaData, this.list);
2409 },
2410 _register: function(key, value, data, list) {
2411 if (key && data && value !== undefined) {
2412 data[key] = value;
2413 list.push(value);
2414 }
2415 },
2416 _unregister: function(key, data, list) {
2417 if (key && data) {
2418 if (key in data) {
2419 var value = data[key];
2420 delete data[key];
2421 this.arrayDelete(list, value);
4444 } 2422 }
4445 }, 2423 }
4446 2424 }
4447 _typeChanged: function(type) { 2425 });
4448 this._unregisterKey(this.key); 2426 Polymer.IronMeta.getIronMeta = function getIronMeta() {
4449 if (!metaDatas[type]) { 2427 if (singleton === null) {
4450 metaDatas[type] = {}; 2428 singleton = new Polymer.IronMeta();
4451 } 2429 }
4452 this._metaData = metaDatas[type]; 2430 return singleton;
4453 if (!metaArrays[type]) { 2431 };
4454 metaArrays[type] = []; 2432 Polymer.IronMetaQuery = Polymer({
4455 } 2433 is: 'iron-meta-query',
4456 this.list = metaArrays[type]; 2434 properties: {
4457 this._registerKeyValue(this.key, this.value); 2435 type: {
4458 }, 2436 type: String,
4459 2437 value: 'default',
4460 /** 2438 observer: '_typeChanged'
4461 * Retrieves meta data value by key. 2439 },
4462 * 2440 key: {
4463 * @method byKey 2441 type: String,
4464 * @param {string} key The key of the meta-data to be returned. 2442 observer: '_keyChanged'
4465 * @return {*} 2443 },
4466 */ 2444 value: {
4467 byKey: function(key) { 2445 type: Object,
4468 return this._metaData && this._metaData[key]; 2446 notify: true,
4469 }, 2447 readOnly: true
4470 2448 },
4471 _resetRegistration: function(oldKey) { 2449 list: {
4472 this._unregisterKey(oldKey); 2450 type: Array,
4473 this._registerKeyValue(this.key, this.value); 2451 notify: true
4474 }, 2452 }
4475 2453 },
4476 _unregisterKey: function(key) { 2454 factoryImpl: function(config) {
4477 this._unregister(key, this._metaData, this.list); 2455 if (config) {
4478 }, 2456 for (var n in config) {
4479 2457 switch (n) {
4480 _registerKeyValue: function(key, value) { 2458 case 'type':
4481 this._register(key, value, this._metaData, this.list); 2459 case 'key':
4482 }, 2460 this[n] = config[n];
4483 2461 break;
4484 _register: function(key, value, data, list) {
4485 if (key && data && value !== undefined) {
4486 data[key] = value;
4487 list.push(value);
4488 }
4489 },
4490
4491 _unregister: function(key, data, list) {
4492 if (key && data) {
4493 if (key in data) {
4494 var value = data[key];
4495 delete data[key];
4496 this.arrayDelete(list, value);
4497 } 2462 }
4498 } 2463 }
4499 } 2464 }
4500 2465 },
4501 }); 2466 created: function() {
4502 2467 this._metaDatas = metaDatas;
4503 Polymer.IronMeta.getIronMeta = function getIronMeta() { 2468 this._metaArrays = metaArrays;
4504 if (singleton === null) { 2469 },
4505 singleton = new Polymer.IronMeta(); 2470 _keyChanged: function(key) {
4506 } 2471 this._setValue(this._metaData && this._metaData[key]);
4507 return singleton; 2472 },
4508 }; 2473 _typeChanged: function(type) {
4509 2474 this._metaData = metaDatas[type];
4510 /** 2475 this.list = metaArrays[type];
4511 `iron-meta-query` can be used to access infomation stored in `iron-meta`. 2476 if (this.key) {
4512 2477 this._keyChanged(this.key);
4513 Examples: 2478 }
4514 2479 },
4515 If I create an instance like this: 2480 byKey: function(key) {
4516 2481 return this._metaData && this._metaData[key];
4517 <iron-meta key="info" value="foo/bar"></iron-meta> 2482 }
4518 2483 });
4519 Note that value="foo/bar" is the metadata I've defined. I could define more 2484 })();
4520 attributes or use child nodes to define additional metadata. 2485
4521 2486 Polymer({
4522 Now I can access that element (and it's metadata) from any `iron-meta-query` instance: 2487 is: 'iron-icon',
4523 2488 properties: {
4524 var value = new Polymer.IronMetaQuery({key: 'info'}).value; 2489 icon: {
4525 2490 type: String,
4526 @group Polymer Iron Elements 2491 observer: '_iconChanged'
4527 @element iron-meta-query 2492 },
4528 */ 2493 theme: {
4529 Polymer.IronMetaQuery = Polymer({ 2494 type: String,
4530 2495 observer: '_updateIcon'
4531 is: 'iron-meta-query', 2496 },
4532 2497 src: {
4533 properties: { 2498 type: String,
4534 2499 observer: '_srcChanged'
4535 /** 2500 },
4536 * The type of meta-data. All meta-data of the same type is stored 2501 _meta: {
4537 * together. 2502 value: Polymer.Base.create('iron-meta', {
4538 */ 2503 type: 'iconset'
4539 type: { 2504 }),
4540 type: String, 2505 observer: '_updateIcon'
4541 value: 'default', 2506 }
4542 observer: '_typeChanged' 2507 },
4543 }, 2508 _DEFAULT_ICONSET: 'icons',
4544 2509 _iconChanged: function(icon) {
4545 /** 2510 var parts = (icon || '').split(':');
4546 * Specifies a key to use for retrieving `value` from the `type` 2511 this._iconName = parts.pop();
4547 * namespace. 2512 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
4548 */ 2513 this._updateIcon();
4549 key: { 2514 },
4550 type: String, 2515 _srcChanged: function(src) {
4551 observer: '_keyChanged' 2516 this._updateIcon();
4552 }, 2517 },
4553 2518 _usesIconset: function() {
4554 /** 2519 return this.icon || !this.src;
4555 * The meta-data to store or retrieve. 2520 },
4556 */ 2521 _updateIcon: function() {
4557 value: { 2522 if (this._usesIconset()) {
4558 type: Object, 2523 if (this._img && this._img.parentNode) {
4559 notify: true, 2524 Polymer.dom(this.root).removeChild(this._img);
4560 readOnly: true 2525 }
4561 }, 2526 if (this._iconName === "") {
4562 2527 if (this._iconset) {
4563 /** 2528 this._iconset.removeIcon(this);
4564 * Array of all meta-data values for the given type.
4565 */
4566 list: {
4567 type: Array,
4568 notify: true
4569 } 2529 }
4570 2530 } else if (this._iconsetName && this._meta) {
4571 }, 2531 this._iconset = this._meta.byKey(this._iconsetName);
4572 2532 if (this._iconset) {
4573 /** 2533 this._iconset.applyIcon(this, this._iconName, this.theme);
4574 * Actually a factory method, not a true constructor. Only runs if 2534 this.unlisten(window, 'iron-iconset-added', '_updateIcon');
4575 * someone invokes it directly (via `new Polymer.IronMeta()`); 2535 } else {
4576 * 2536 this.listen(window, 'iron-iconset-added', '_updateIcon');
4577 * @param {{type: (string|undefined), key: (string|undefined)}=} config
4578 */
4579 factoryImpl: function(config) {
4580 if (config) {
4581 for (var n in config) {
4582 switch(n) {
4583 case 'type':
4584 case 'key':
4585 this[n] = config[n];
4586 break;
4587 }
4588 }
4589 } 2537 }
4590 }, 2538 }
4591 2539 } else {
4592 created: function() { 2540 if (this._iconset) {
4593 // TODO(sjmiles): good for debugging? 2541 this._iconset.removeIcon(this);
4594 this._metaDatas = metaDatas; 2542 }
4595 this._metaArrays = metaArrays; 2543 if (!this._img) {
4596 }, 2544 this._img = document.createElement('img');
4597 2545 this._img.style.width = '100%';
4598 _keyChanged: function(key) { 2546 this._img.style.height = '100%';
4599 this._setValue(this._metaData && this._metaData[key]); 2547 this._img.draggable = false;
4600 }, 2548 }
4601 2549 this._img.src = this.src;
4602 _typeChanged: function(type) { 2550 Polymer.dom(this.root).appendChild(this._img);
4603 this._metaData = metaDatas[type]; 2551 }
4604 this.list = metaArrays[type]; 2552 }
4605 if (this.key) { 2553 });
4606 this._keyChanged(this.key); 2554
2555 Polymer.IronControlState = {
2556 properties: {
2557 focused: {
2558 type: Boolean,
2559 value: false,
2560 notify: true,
2561 readOnly: true,
2562 reflectToAttribute: true
2563 },
2564 disabled: {
2565 type: Boolean,
2566 value: false,
2567 notify: true,
2568 observer: '_disabledChanged',
2569 reflectToAttribute: true
2570 },
2571 _oldTabIndex: {
2572 type: Number
2573 },
2574 _boundFocusBlurHandler: {
2575 type: Function,
2576 value: function() {
2577 return this._focusBlurHandler.bind(this);
2578 }
2579 }
2580 },
2581 observers: [ '_changedControlState(focused, disabled)' ],
2582 ready: function() {
2583 this.addEventListener('focus', this._boundFocusBlurHandler, true);
2584 this.addEventListener('blur', this._boundFocusBlurHandler, true);
2585 },
2586 _focusBlurHandler: function(event) {
2587 if (event.target === this) {
2588 this._setFocused(event.type === 'focus');
2589 } else if (!this.shadowRoot) {
2590 var target = Polymer.dom(event).localTarget;
2591 if (!this.isLightDescendant(target)) {
2592 this.fire(event.type, {
2593 sourceEvent: event
2594 }, {
2595 node: this,
2596 bubbles: event.bubbles,
2597 cancelable: event.cancelable
2598 });
2599 }
2600 }
2601 },
2602 _disabledChanged: function(disabled, old) {
2603 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
2604 this.style.pointerEvents = disabled ? 'none' : '';
2605 if (disabled) {
2606 this._oldTabIndex = this.tabIndex;
2607 this._setFocused(false);
2608 this.tabIndex = -1;
2609 this.blur();
2610 } else if (this._oldTabIndex !== undefined) {
2611 this.tabIndex = this._oldTabIndex;
2612 }
2613 },
2614 _changedControlState: function() {
2615 if (this._controlStateChanged) {
2616 this._controlStateChanged();
2617 }
2618 }
2619 };
2620
2621 Polymer.IronButtonStateImpl = {
2622 properties: {
2623 pressed: {
2624 type: Boolean,
2625 readOnly: true,
2626 value: false,
2627 reflectToAttribute: true,
2628 observer: '_pressedChanged'
2629 },
2630 toggles: {
2631 type: Boolean,
2632 value: false,
2633 reflectToAttribute: true
2634 },
2635 active: {
2636 type: Boolean,
2637 value: false,
2638 notify: true,
2639 reflectToAttribute: true
2640 },
2641 pointerDown: {
2642 type: Boolean,
2643 readOnly: true,
2644 value: false
2645 },
2646 receivedFocusFromKeyboard: {
2647 type: Boolean,
2648 readOnly: true
2649 },
2650 ariaActiveAttribute: {
2651 type: String,
2652 value: 'aria-pressed',
2653 observer: '_ariaActiveAttributeChanged'
2654 }
2655 },
2656 listeners: {
2657 down: '_downHandler',
2658 up: '_upHandler',
2659 tap: '_tapHandler'
2660 },
2661 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi veAttribute)' ],
2662 keyBindings: {
2663 'enter:keydown': '_asyncClick',
2664 'space:keydown': '_spaceKeyDownHandler',
2665 'space:keyup': '_spaceKeyUpHandler'
2666 },
2667 _mouseEventRe: /^mouse/,
2668 _tapHandler: function() {
2669 if (this.toggles) {
2670 this._userActivate(!this.active);
2671 } else {
2672 this.active = false;
2673 }
2674 },
2675 _detectKeyboardFocus: function(focused) {
2676 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
2677 },
2678 _userActivate: function(active) {
2679 if (this.active !== active) {
2680 this.active = active;
2681 this.fire('change');
2682 }
2683 },
2684 _downHandler: function(event) {
2685 this._setPointerDown(true);
2686 this._setPressed(true);
2687 this._setReceivedFocusFromKeyboard(false);
2688 },
2689 _upHandler: function() {
2690 this._setPointerDown(false);
2691 this._setPressed(false);
2692 },
2693 _spaceKeyDownHandler: function(event) {
2694 var keyboardEvent = event.detail.keyboardEvent;
2695 var target = Polymer.dom(keyboardEvent).localTarget;
2696 if (this.isLightDescendant(target)) return;
2697 keyboardEvent.preventDefault();
2698 keyboardEvent.stopImmediatePropagation();
2699 this._setPressed(true);
2700 },
2701 _spaceKeyUpHandler: function(event) {
2702 var keyboardEvent = event.detail.keyboardEvent;
2703 var target = Polymer.dom(keyboardEvent).localTarget;
2704 if (this.isLightDescendant(target)) return;
2705 if (this.pressed) {
2706 this._asyncClick();
2707 }
2708 this._setPressed(false);
2709 },
2710 _asyncClick: function() {
2711 this.async(function() {
2712 this.click();
2713 }, 1);
2714 },
2715 _pressedChanged: function(pressed) {
2716 this._changedButtonState();
2717 },
2718 _ariaActiveAttributeChanged: function(value, oldValue) {
2719 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
2720 this.removeAttribute(oldValue);
2721 }
2722 },
2723 _activeChanged: function(active, ariaActiveAttribute) {
2724 if (this.toggles) {
2725 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false');
2726 } else {
2727 this.removeAttribute(this.ariaActiveAttribute);
2728 }
2729 this._changedButtonState();
2730 },
2731 _controlStateChanged: function() {
2732 if (this.disabled) {
2733 this._setPressed(false);
2734 } else {
2735 this._changedButtonState();
2736 }
2737 },
2738 _changedButtonState: function() {
2739 if (this._buttonStateChanged) {
2740 this._buttonStateChanged();
2741 }
2742 }
2743 };
2744
2745 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat eImpl ];
2746
2747 (function() {
2748 var Utility = {
2749 distance: function(x1, y1, x2, y2) {
2750 var xDelta = x1 - x2;
2751 var yDelta = y1 - y2;
2752 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
2753 },
2754 now: window.performance && window.performance.now ? window.performance.now.b ind(window.performance) : Date.now
2755 };
2756 function ElementMetrics(element) {
2757 this.element = element;
2758 this.width = this.boundingRect.width;
2759 this.height = this.boundingRect.height;
2760 this.size = Math.max(this.width, this.height);
2761 }
2762 ElementMetrics.prototype = {
2763 get boundingRect() {
2764 return this.element.getBoundingClientRect();
2765 },
2766 furthestCornerDistanceFrom: function(x, y) {
2767 var topLeft = Utility.distance(x, y, 0, 0);
2768 var topRight = Utility.distance(x, y, this.width, 0);
2769 var bottomLeft = Utility.distance(x, y, 0, this.height);
2770 var bottomRight = Utility.distance(x, y, this.width, this.height);
2771 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
2772 }
2773 };
2774 function Ripple(element) {
2775 this.element = element;
2776 this.color = window.getComputedStyle(element).color;
2777 this.wave = document.createElement('div');
2778 this.waveContainer = document.createElement('div');
2779 this.wave.style.backgroundColor = this.color;
2780 this.wave.classList.add('wave');
2781 this.waveContainer.classList.add('wave-container');
2782 Polymer.dom(this.waveContainer).appendChild(this.wave);
2783 this.resetInteractionState();
2784 }
2785 Ripple.MAX_RADIUS = 300;
2786 Ripple.prototype = {
2787 get recenters() {
2788 return this.element.recenters;
2789 },
2790 get center() {
2791 return this.element.center;
2792 },
2793 get mouseDownElapsed() {
2794 var elapsed;
2795 if (!this.mouseDownStart) {
2796 return 0;
2797 }
2798 elapsed = Utility.now() - this.mouseDownStart;
2799 if (this.mouseUpStart) {
2800 elapsed -= this.mouseUpElapsed;
2801 }
2802 return elapsed;
2803 },
2804 get mouseUpElapsed() {
2805 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0;
2806 },
2807 get mouseDownElapsedSeconds() {
2808 return this.mouseDownElapsed / 1e3;
2809 },
2810 get mouseUpElapsedSeconds() {
2811 return this.mouseUpElapsed / 1e3;
2812 },
2813 get mouseInteractionSeconds() {
2814 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
2815 },
2816 get initialOpacity() {
2817 return this.element.initialOpacity;
2818 },
2819 get opacityDecayVelocity() {
2820 return this.element.opacityDecayVelocity;
2821 },
2822 get radius() {
2823 var width2 = this.containerMetrics.width * this.containerMetrics.width;
2824 var height2 = this.containerMetrics.height * this.containerMetrics.height;
2825 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS) * 1.1 + 5;
2826 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS);
2827 var timeNow = this.mouseInteractionSeconds / duration;
2828 var size = waveRadius * (1 - Math.pow(80, -timeNow));
2829 return Math.abs(size);
2830 },
2831 get opacity() {
2832 if (!this.mouseUpStart) {
2833 return this.initialOpacity;
2834 }
2835 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this .opacityDecayVelocity);
2836 },
2837 get outerOpacity() {
2838 var outerOpacity = this.mouseUpElapsedSeconds * .3;
2839 var waveOpacity = this.opacity;
2840 return Math.max(0, Math.min(outerOpacity, waveOpacity));
2841 },
2842 get isOpacityFullyDecayed() {
2843 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl e.MAX_RADIUS);
2844 },
2845 get isRestingAtMaxRadius() {
2846 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this .maxRadius, Ripple.MAX_RADIUS);
2847 },
2848 get isAnimationComplete() {
2849 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa xRadius;
2850 },
2851 get translationFraction() {
2852 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr t(2));
2853 },
2854 get xNow() {
2855 if (this.xEnd) {
2856 return this.xStart + this.translationFraction * (this.xEnd - this.xStart );
2857 }
2858 return this.xStart;
2859 },
2860 get yNow() {
2861 if (this.yEnd) {
2862 return this.yStart + this.translationFraction * (this.yEnd - this.yStart );
2863 }
2864 return this.yStart;
2865 },
2866 get isMouseDown() {
2867 return this.mouseDownStart && !this.mouseUpStart;
2868 },
2869 resetInteractionState: function() {
2870 this.maxRadius = 0;
2871 this.mouseDownStart = 0;
2872 this.mouseUpStart = 0;
2873 this.xStart = 0;
2874 this.yStart = 0;
2875 this.xEnd = 0;
2876 this.yEnd = 0;
2877 this.slideDistance = 0;
2878 this.containerMetrics = new ElementMetrics(this.element);
2879 },
2880 draw: function() {
2881 var scale;
2882 var translateString;
2883 var dx;
2884 var dy;
2885 this.wave.style.opacity = this.opacity;
2886 scale = this.radius / (this.containerMetrics.size / 2);
2887 dx = this.xNow - this.containerMetrics.width / 2;
2888 dy = this.yNow - this.containerMetrics.height / 2;
2889 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
2890 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + ' px, 0)';
2891 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
2892 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
2893 },
2894 downAction: function(event) {
2895 var xCenter = this.containerMetrics.width / 2;
2896 var yCenter = this.containerMetrics.height / 2;
2897 this.resetInteractionState();
2898 this.mouseDownStart = Utility.now();
2899 if (this.center) {
2900 this.xStart = xCenter;
2901 this.yStart = yCenter;
2902 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
2903 } else {
2904 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec t.left : this.containerMetrics.width / 2;
2905 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec t.top : this.containerMetrics.height / 2;
2906 }
2907 if (this.recenters) {
2908 this.xEnd = xCenter;
2909 this.yEnd = yCenter;
2910 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
2911 }
2912 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt art, this.yStart);
2913 this.waveContainer.style.top = (this.containerMetrics.height - this.contai nerMetrics.size) / 2 + 'px';
2914 this.waveContainer.style.left = (this.containerMetrics.width - this.contai nerMetrics.size) / 2 + 'px';
2915 this.waveContainer.style.width = this.containerMetrics.size + 'px';
2916 this.waveContainer.style.height = this.containerMetrics.size + 'px';
2917 },
2918 upAction: function(event) {
2919 if (!this.isMouseDown) {
2920 return;
2921 }
2922 this.mouseUpStart = Utility.now();
2923 },
2924 remove: function() {
2925 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer) ;
2926 }
2927 };
2928 Polymer({
2929 is: 'paper-ripple',
2930 behaviors: [ Polymer.IronA11yKeysBehavior ],
2931 properties: {
2932 initialOpacity: {
2933 type: Number,
2934 value: .25
2935 },
2936 opacityDecayVelocity: {
2937 type: Number,
2938 value: .8
2939 },
2940 recenters: {
2941 type: Boolean,
2942 value: false
2943 },
2944 center: {
2945 type: Boolean,
2946 value: false
2947 },
2948 ripples: {
2949 type: Array,
2950 value: function() {
2951 return [];
4607 } 2952 }
4608 }, 2953 },
4609 2954 animating: {
4610 /** 2955 type: Boolean,
4611 * Retrieves meta data value by key. 2956 readOnly: true,
4612 * @param {string} key The key of the meta-data to be returned. 2957 reflectToAttribute: true,
4613 * @return {*} 2958 value: false
4614 */ 2959 },
4615 byKey: function(key) { 2960 holdDown: {
4616 return this._metaData && this._metaData[key];
4617 }
4618
4619 });
4620
4621 })();
4622 Polymer({
4623
4624 is: 'iron-icon',
4625
4626 properties: {
4627
4628 /**
4629 * The name of the icon to use. The name should be of the form:
4630 * `iconset_name:icon_name`.
4631 */
4632 icon: {
4633 type: String,
4634 observer: '_iconChanged'
4635 },
4636
4637 /**
4638 * The name of the theme to used, if one is specified by the
4639 * iconset.
4640 */
4641 theme: {
4642 type: String,
4643 observer: '_updateIcon'
4644 },
4645
4646 /**
4647 * If using iron-icon without an iconset, you can set the src to be
4648 * the URL of an individual icon image file. Note that this will take
4649 * precedence over a given icon attribute.
4650 */
4651 src: {
4652 type: String,
4653 observer: '_srcChanged'
4654 },
4655
4656 /**
4657 * @type {!Polymer.IronMeta}
4658 */
4659 _meta: {
4660 value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
4661 observer: '_updateIcon'
4662 }
4663
4664 },
4665
4666 _DEFAULT_ICONSET: 'icons',
4667
4668 _iconChanged: function(icon) {
4669 var parts = (icon || '').split(':');
4670 this._iconName = parts.pop();
4671 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
4672 this._updateIcon();
4673 },
4674
4675 _srcChanged: function(src) {
4676 this._updateIcon();
4677 },
4678
4679 _usesIconset: function() {
4680 return this.icon || !this.src;
4681 },
4682
4683 /** @suppress {visibility} */
4684 _updateIcon: function() {
4685 if (this._usesIconset()) {
4686 if (this._img && this._img.parentNode) {
4687 Polymer.dom(this.root).removeChild(this._img);
4688 }
4689 if (this._iconName === "") {
4690 if (this._iconset) {
4691 this._iconset.removeIcon(this);
4692 }
4693 } else if (this._iconsetName && this._meta) {
4694 this._iconset = /** @type {?Polymer.Iconset} */ (
4695 this._meta.byKey(this._iconsetName));
4696 if (this._iconset) {
4697 this._iconset.applyIcon(this, this._iconName, this.theme);
4698 this.unlisten(window, 'iron-iconset-added', '_updateIcon');
4699 } else {
4700 this.listen(window, 'iron-iconset-added', '_updateIcon');
4701 }
4702 }
4703 } else {
4704 if (this._iconset) {
4705 this._iconset.removeIcon(this);
4706 }
4707 if (!this._img) {
4708 this._img = document.createElement('img');
4709 this._img.style.width = '100%';
4710 this._img.style.height = '100%';
4711 this._img.draggable = false;
4712 }
4713 this._img.src = this.src;
4714 Polymer.dom(this.root).appendChild(this._img);
4715 }
4716 }
4717
4718 });
4719 /**
4720 * @demo demo/index.html
4721 * @polymerBehavior
4722 */
4723 Polymer.IronControlState = {
4724
4725 properties: {
4726
4727 /**
4728 * If true, the element currently has focus.
4729 */
4730 focused: {
4731 type: Boolean, 2961 type: Boolean,
4732 value: false, 2962 value: false,
4733 notify: true, 2963 observer: '_holdDownChanged'
4734 readOnly: true, 2964 },
4735 reflectToAttribute: true 2965 noink: {
4736 },
4737
4738 /**
4739 * If true, the user cannot interact with this element.
4740 */
4741 disabled: {
4742 type: Boolean, 2966 type: Boolean,
4743 value: false, 2967 value: false
4744 notify: true, 2968 },
4745 observer: '_disabledChanged', 2969 _animating: {
4746 reflectToAttribute: true 2970 type: Boolean
4747 }, 2971 },
4748 2972 _boundAnimate: {
4749 _oldTabIndex: {
4750 type: Number
4751 },
4752
4753 _boundFocusBlurHandler: {
4754 type: Function, 2973 type: Function,
4755 value: function() { 2974 value: function() {
4756 return this._focusBlurHandler.bind(this); 2975 return this.animate.bind(this);
4757 } 2976 }
4758 } 2977 }
4759 2978 },
4760 }, 2979 get target() {
4761 2980 return this.keyEventTarget;
4762 observers: [ 2981 },
4763 '_changedControlState(focused, disabled)' 2982 keyBindings: {
4764 ], 2983 'enter:keydown': '_onEnterKeydown',
4765 2984 'space:keydown': '_onSpaceKeydown',
4766 ready: function() { 2985 'space:keyup': '_onSpaceKeyup'
4767 this.addEventListener('focus', this._boundFocusBlurHandler, true); 2986 },
4768 this.addEventListener('blur', this._boundFocusBlurHandler, true); 2987 attached: function() {
4769 }, 2988 if (this.parentNode.nodeType == 11) {
4770 2989 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
4771 _focusBlurHandler: function(event) { 2990 } else {
4772 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will 2991 this.keyEventTarget = this.parentNode;
4773 // eventually become `this` due to retargeting; if we are not in 2992 }
4774 // ShadowDOM land, `event.target` will eventually become `this` due 2993 var keyEventTarget = this.keyEventTarget;
4775 // to the second conditional which fires a synthetic event (that is also 2994 this.listen(keyEventTarget, 'up', 'uiUpAction');
4776 // handled). In either case, we can disregard `event.path`. 2995 this.listen(keyEventTarget, 'down', 'uiDownAction');
4777 2996 },
4778 if (event.target === this) { 2997 detached: function() {
4779 this._setFocused(event.type === 'focus'); 2998 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
4780 } else if (!this.shadowRoot) { 2999 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
4781 var target = /** @type {Node} */(Polymer.dom(event).localTarget); 3000 this.keyEventTarget = null;
4782 if (!this.isLightDescendant(target)) { 3001 },
4783 this.fire(event.type, {sourceEvent: event}, { 3002 get shouldKeepAnimating() {
4784 node: this, 3003 for (var index = 0; index < this.ripples.length; ++index) {
4785 bubbles: event.bubbles, 3004 if (!this.ripples[index].isAnimationComplete) {
4786 cancelable: event.cancelable 3005 return true;
4787 });
4788 } 3006 }
4789 } 3007 }
4790 }, 3008 return false;
4791 3009 },
4792 _disabledChanged: function(disabled, old) { 3010 simulatedRipple: function() {
4793 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 3011 this.downAction(null);
4794 this.style.pointerEvents = disabled ? 'none' : ''; 3012 this.async(function() {
4795 if (disabled) { 3013 this.upAction();
4796 this._oldTabIndex = this.tabIndex; 3014 }, 1);
4797 this._setFocused(false); 3015 },
4798 this.tabIndex = -1; 3016 uiDownAction: function(event) {
4799 this.blur(); 3017 if (!this.noink) {
4800 } else if (this._oldTabIndex !== undefined) { 3018 this.downAction(event);
4801 this.tabIndex = this._oldTabIndex; 3019 }
4802 } 3020 },
4803 }, 3021 downAction: function(event) {
4804 3022 if (this.holdDown && this.ripples.length > 0) {
4805 _changedControlState: function() {
4806 // _controlStateChanged is abstract, follow-on behaviors may implement it
4807 if (this._controlStateChanged) {
4808 this._controlStateChanged();
4809 }
4810 }
4811
4812 };
4813 /**
4814 * @demo demo/index.html
4815 * @polymerBehavior Polymer.IronButtonState
4816 */
4817 Polymer.IronButtonStateImpl = {
4818
4819 properties: {
4820
4821 /**
4822 * If true, the user is currently holding down the button.
4823 */
4824 pressed: {
4825 type: Boolean,
4826 readOnly: true,
4827 value: false,
4828 reflectToAttribute: true,
4829 observer: '_pressedChanged'
4830 },
4831
4832 /**
4833 * If true, the button toggles the active state with each tap or press
4834 * of the spacebar.
4835 */
4836 toggles: {
4837 type: Boolean,
4838 value: false,
4839 reflectToAttribute: true
4840 },
4841
4842 /**
4843 * If true, the button is a toggle and is currently in the active state.
4844 */
4845 active: {
4846 type: Boolean,
4847 value: false,
4848 notify: true,
4849 reflectToAttribute: true
4850 },
4851
4852 /**
4853 * True if the element is currently being pressed by a "pointer," which
4854 * is loosely defined as mouse or touch input (but specifically excluding
4855 * keyboard input).
4856 */
4857 pointerDown: {
4858 type: Boolean,
4859 readOnly: true,
4860 value: false
4861 },
4862
4863 /**
4864 * True if the input device that caused the element to receive focus
4865 * was a keyboard.
4866 */
4867 receivedFocusFromKeyboard: {
4868 type: Boolean,
4869 readOnly: true
4870 },
4871
4872 /**
4873 * The aria attribute to be set if the button is a toggle and in the
4874 * active state.
4875 */
4876 ariaActiveAttribute: {
4877 type: String,
4878 value: 'aria-pressed',
4879 observer: '_ariaActiveAttributeChanged'
4880 }
4881 },
4882
4883 listeners: {
4884 down: '_downHandler',
4885 up: '_upHandler',
4886 tap: '_tapHandler'
4887 },
4888
4889 observers: [
4890 '_detectKeyboardFocus(focused)',
4891 '_activeChanged(active, ariaActiveAttribute)'
4892 ],
4893
4894 keyBindings: {
4895 'enter:keydown': '_asyncClick',
4896 'space:keydown': '_spaceKeyDownHandler',
4897 'space:keyup': '_spaceKeyUpHandler',
4898 },
4899
4900 _mouseEventRe: /^mouse/,
4901
4902 _tapHandler: function() {
4903 if (this.toggles) {
4904 // a tap is needed to toggle the active state
4905 this._userActivate(!this.active);
4906 } else {
4907 this.active = false;
4908 }
4909 },
4910
4911 _detectKeyboardFocus: function(focused) {
4912 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
4913 },
4914
4915 // to emulate native checkbox, (de-)activations from a user interaction fire
4916 // 'change' events
4917 _userActivate: function(active) {
4918 if (this.active !== active) {
4919 this.active = active;
4920 this.fire('change');
4921 }
4922 },
4923
4924 _downHandler: function(event) {
4925 this._setPointerDown(true);
4926 this._setPressed(true);
4927 this._setReceivedFocusFromKeyboard(false);
4928 },
4929
4930 _upHandler: function() {
4931 this._setPointerDown(false);
4932 this._setPressed(false);
4933 },
4934
4935 /**
4936 * @param {!KeyboardEvent} event .
4937 */
4938 _spaceKeyDownHandler: function(event) {
4939 var keyboardEvent = event.detail.keyboardEvent;
4940 var target = Polymer.dom(keyboardEvent).localTarget;
4941
4942 // Ignore the event if this is coming from a focused light child, since th at
4943 // element will deal with it.
4944 if (this.isLightDescendant(/** @type {Node} */(target)))
4945 return; 3023 return;
4946 3024 }
4947 keyboardEvent.preventDefault(); 3025 var ripple = this.addRipple();
4948 keyboardEvent.stopImmediatePropagation(); 3026 ripple.downAction(event);
4949 this._setPressed(true); 3027 if (!this._animating) {
4950 },
4951
4952 /**
4953 * @param {!KeyboardEvent} event .
4954 */
4955 _spaceKeyUpHandler: function(event) {
4956 var keyboardEvent = event.detail.keyboardEvent;
4957 var target = Polymer.dom(keyboardEvent).localTarget;
4958
4959 // Ignore the event if this is coming from a focused light child, since th at
4960 // element will deal with it.
4961 if (this.isLightDescendant(/** @type {Node} */(target)))
4962 return;
4963
4964 if (this.pressed) {
4965 this._asyncClick();
4966 }
4967 this._setPressed(false);
4968 },
4969
4970 // trigger click asynchronously, the asynchrony is useful to allow one
4971 // event handler to unwind before triggering another event
4972 _asyncClick: function() {
4973 this.async(function() {
4974 this.click();
4975 }, 1);
4976 },
4977
4978 // any of these changes are considered a change to button state
4979
4980 _pressedChanged: function(pressed) {
4981 this._changedButtonState();
4982 },
4983
4984 _ariaActiveAttributeChanged: function(value, oldValue) {
4985 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
4986 this.removeAttribute(oldValue);
4987 }
4988 },
4989
4990 _activeChanged: function(active, ariaActiveAttribute) {
4991 if (this.toggles) {
4992 this.setAttribute(this.ariaActiveAttribute,
4993 active ? 'true' : 'false');
4994 } else {
4995 this.removeAttribute(this.ariaActiveAttribute);
4996 }
4997 this._changedButtonState();
4998 },
4999
5000 _controlStateChanged: function() {
5001 if (this.disabled) {
5002 this._setPressed(false);
5003 } else {
5004 this._changedButtonState();
5005 }
5006 },
5007
5008 // provide hook for follow-on behaviors to react to button-state
5009
5010 _changedButtonState: function() {
5011 if (this._buttonStateChanged) {
5012 this._buttonStateChanged(); // abstract
5013 }
5014 }
5015
5016 };
5017
5018 /** @polymerBehavior */
5019 Polymer.IronButtonState = [
5020 Polymer.IronA11yKeysBehavior,
5021 Polymer.IronButtonStateImpl
5022 ];
5023 (function() {
5024 var Utility = {
5025 distance: function(x1, y1, x2, y2) {
5026 var xDelta = (x1 - x2);
5027 var yDelta = (y1 - y2);
5028
5029 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
5030 },
5031
5032 now: window.performance && window.performance.now ?
5033 window.performance.now.bind(window.performance) : Date.now
5034 };
5035
5036 /**
5037 * @param {HTMLElement} element
5038 * @constructor
5039 */
5040 function ElementMetrics(element) {
5041 this.element = element;
5042 this.width = this.boundingRect.width;
5043 this.height = this.boundingRect.height;
5044
5045 this.size = Math.max(this.width, this.height);
5046 }
5047
5048 ElementMetrics.prototype = {
5049 get boundingRect () {
5050 return this.element.getBoundingClientRect();
5051 },
5052
5053 furthestCornerDistanceFrom: function(x, y) {
5054 var topLeft = Utility.distance(x, y, 0, 0);
5055 var topRight = Utility.distance(x, y, this.width, 0);
5056 var bottomLeft = Utility.distance(x, y, 0, this.height);
5057 var bottomRight = Utility.distance(x, y, this.width, this.height);
5058
5059 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
5060 }
5061 };
5062
5063 /**
5064 * @param {HTMLElement} element
5065 * @constructor
5066 */
5067 function Ripple(element) {
5068 this.element = element;
5069 this.color = window.getComputedStyle(element).color;
5070
5071 this.wave = document.createElement('div');
5072 this.waveContainer = document.createElement('div');
5073 this.wave.style.backgroundColor = this.color;
5074 this.wave.classList.add('wave');
5075 this.waveContainer.classList.add('wave-container');
5076 Polymer.dom(this.waveContainer).appendChild(this.wave);
5077
5078 this.resetInteractionState();
5079 }
5080
5081 Ripple.MAX_RADIUS = 300;
5082
5083 Ripple.prototype = {
5084 get recenters() {
5085 return this.element.recenters;
5086 },
5087
5088 get center() {
5089 return this.element.center;
5090 },
5091
5092 get mouseDownElapsed() {
5093 var elapsed;
5094
5095 if (!this.mouseDownStart) {
5096 return 0;
5097 }
5098
5099 elapsed = Utility.now() - this.mouseDownStart;
5100
5101 if (this.mouseUpStart) {
5102 elapsed -= this.mouseUpElapsed;
5103 }
5104
5105 return elapsed;
5106 },
5107
5108 get mouseUpElapsed() {
5109 return this.mouseUpStart ?
5110 Utility.now () - this.mouseUpStart : 0;
5111 },
5112
5113 get mouseDownElapsedSeconds() {
5114 return this.mouseDownElapsed / 1000;
5115 },
5116
5117 get mouseUpElapsedSeconds() {
5118 return this.mouseUpElapsed / 1000;
5119 },
5120
5121 get mouseInteractionSeconds() {
5122 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
5123 },
5124
5125 get initialOpacity() {
5126 return this.element.initialOpacity;
5127 },
5128
5129 get opacityDecayVelocity() {
5130 return this.element.opacityDecayVelocity;
5131 },
5132
5133 get radius() {
5134 var width2 = this.containerMetrics.width * this.containerMetrics.width;
5135 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
5136 var waveRadius = Math.min(
5137 Math.sqrt(width2 + height2),
5138 Ripple.MAX_RADIUS
5139 ) * 1.1 + 5;
5140
5141 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
5142 var timeNow = this.mouseInteractionSeconds / duration;
5143 var size = waveRadius * (1 - Math.pow(80, -timeNow));
5144
5145 return Math.abs(size);
5146 },
5147
5148 get opacity() {
5149 if (!this.mouseUpStart) {
5150 return this.initialOpacity;
5151 }
5152
5153 return Math.max(
5154 0,
5155 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
5156 );
5157 },
5158
5159 get outerOpacity() {
5160 // Linear increase in background opacity, capped at the opacity
5161 // of the wavefront (waveOpacity).
5162 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
5163 var waveOpacity = this.opacity;
5164
5165 return Math.max(
5166 0,
5167 Math.min(outerOpacity, waveOpacity)
5168 );
5169 },
5170
5171 get isOpacityFullyDecayed() {
5172 return this.opacity < 0.01 &&
5173 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5174 },
5175
5176 get isRestingAtMaxRadius() {
5177 return this.opacity >= this.initialOpacity &&
5178 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5179 },
5180
5181 get isAnimationComplete() {
5182 return this.mouseUpStart ?
5183 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
5184 },
5185
5186 get translationFraction() {
5187 return Math.min(
5188 1,
5189 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
5190 );
5191 },
5192
5193 get xNow() {
5194 if (this.xEnd) {
5195 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
5196 }
5197
5198 return this.xStart;
5199 },
5200
5201 get yNow() {
5202 if (this.yEnd) {
5203 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
5204 }
5205
5206 return this.yStart;
5207 },
5208
5209 get isMouseDown() {
5210 return this.mouseDownStart && !this.mouseUpStart;
5211 },
5212
5213 resetInteractionState: function() {
5214 this.maxRadius = 0;
5215 this.mouseDownStart = 0;
5216 this.mouseUpStart = 0;
5217
5218 this.xStart = 0;
5219 this.yStart = 0;
5220 this.xEnd = 0;
5221 this.yEnd = 0;
5222 this.slideDistance = 0;
5223
5224 this.containerMetrics = new ElementMetrics(this.element);
5225 },
5226
5227 draw: function() {
5228 var scale;
5229 var translateString;
5230 var dx;
5231 var dy;
5232
5233 this.wave.style.opacity = this.opacity;
5234
5235 scale = this.radius / (this.containerMetrics.size / 2);
5236 dx = this.xNow - (this.containerMetrics.width / 2);
5237 dy = this.yNow - (this.containerMetrics.height / 2);
5238
5239
5240 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
5241 // https://bugs.webkit.org/show_bug.cgi?id=98538
5242 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
5243 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
5244 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
5245 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
5246 },
5247
5248 /** @param {Event=} event */
5249 downAction: function(event) {
5250 var xCenter = this.containerMetrics.width / 2;
5251 var yCenter = this.containerMetrics.height / 2;
5252
5253 this.resetInteractionState();
5254 this.mouseDownStart = Utility.now();
5255
5256 if (this.center) {
5257 this.xStart = xCenter;
5258 this.yStart = yCenter;
5259 this.slideDistance = Utility.distance(
5260 this.xStart, this.yStart, this.xEnd, this.yEnd
5261 );
5262 } else {
5263 this.xStart = event ?
5264 event.detail.x - this.containerMetrics.boundingRect.left :
5265 this.containerMetrics.width / 2;
5266 this.yStart = event ?
5267 event.detail.y - this.containerMetrics.boundingRect.top :
5268 this.containerMetrics.height / 2;
5269 }
5270
5271 if (this.recenters) {
5272 this.xEnd = xCenter;
5273 this.yEnd = yCenter;
5274 this.slideDistance = Utility.distance(
5275 this.xStart, this.yStart, this.xEnd, this.yEnd
5276 );
5277 }
5278
5279 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
5280 this.xStart,
5281 this.yStart
5282 );
5283
5284 this.waveContainer.style.top =
5285 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
5286 this.waveContainer.style.left =
5287 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
5288
5289 this.waveContainer.style.width = this.containerMetrics.size + 'px';
5290 this.waveContainer.style.height = this.containerMetrics.size + 'px';
5291 },
5292
5293 /** @param {Event=} event */
5294 upAction: function(event) {
5295 if (!this.isMouseDown) {
5296 return;
5297 }
5298
5299 this.mouseUpStart = Utility.now();
5300 },
5301
5302 remove: function() {
5303 Polymer.dom(this.waveContainer.parentNode).removeChild(
5304 this.waveContainer
5305 );
5306 }
5307 };
5308
5309 Polymer({
5310 is: 'paper-ripple',
5311
5312 behaviors: [
5313 Polymer.IronA11yKeysBehavior
5314 ],
5315
5316 properties: {
5317 /**
5318 * The initial opacity set on the wave.
5319 *
5320 * @attribute initialOpacity
5321 * @type number
5322 * @default 0.25
5323 */
5324 initialOpacity: {
5325 type: Number,
5326 value: 0.25
5327 },
5328
5329 /**
5330 * How fast (opacity per second) the wave fades out.
5331 *
5332 * @attribute opacityDecayVelocity
5333 * @type number
5334 * @default 0.8
5335 */
5336 opacityDecayVelocity: {
5337 type: Number,
5338 value: 0.8
5339 },
5340
5341 /**
5342 * If true, ripples will exhibit a gravitational pull towards
5343 * the center of their container as they fade away.
5344 *
5345 * @attribute recenters
5346 * @type boolean
5347 * @default false
5348 */
5349 recenters: {
5350 type: Boolean,
5351 value: false
5352 },
5353
5354 /**
5355 * If true, ripples will center inside its container
5356 *
5357 * @attribute recenters
5358 * @type boolean
5359 * @default false
5360 */
5361 center: {
5362 type: Boolean,
5363 value: false
5364 },
5365
5366 /**
5367 * A list of the visual ripples.
5368 *
5369 * @attribute ripples
5370 * @type Array
5371 * @default []
5372 */
5373 ripples: {
5374 type: Array,
5375 value: function() {
5376 return [];
5377 }
5378 },
5379
5380 /**
5381 * True when there are visible ripples animating within the
5382 * element.
5383 */
5384 animating: {
5385 type: Boolean,
5386 readOnly: true,
5387 reflectToAttribute: true,
5388 value: false
5389 },
5390
5391 /**
5392 * If true, the ripple will remain in the "down" state until `holdDown`
5393 * is set to false again.
5394 */
5395 holdDown: {
5396 type: Boolean,
5397 value: false,
5398 observer: '_holdDownChanged'
5399 },
5400
5401 /**
5402 * If true, the ripple will not generate a ripple effect
5403 * via pointer interaction.
5404 * Calling ripple's imperative api like `simulatedRipple` will
5405 * still generate the ripple effect.
5406 */
5407 noink: {
5408 type: Boolean,
5409 value: false
5410 },
5411
5412 _animating: {
5413 type: Boolean
5414 },
5415
5416 _boundAnimate: {
5417 type: Function,
5418 value: function() {
5419 return this.animate.bind(this);
5420 }
5421 }
5422 },
5423
5424 get target () {
5425 return this.keyEventTarget;
5426 },
5427
5428 keyBindings: {
5429 'enter:keydown': '_onEnterKeydown',
5430 'space:keydown': '_onSpaceKeydown',
5431 'space:keyup': '_onSpaceKeyup'
5432 },
5433
5434 attached: function() {
5435 // Set up a11yKeysBehavior to listen to key events on the target,
5436 // so that space and enter activate the ripple even if the target doesn' t
5437 // handle key events. The key handlers deal with `noink` themselves.
5438 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
5439 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
5440 } else {
5441 this.keyEventTarget = this.parentNode;
5442 }
5443 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
5444 this.listen(keyEventTarget, 'up', 'uiUpAction');
5445 this.listen(keyEventTarget, 'down', 'uiDownAction');
5446 },
5447
5448 detached: function() {
5449 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
5450 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
5451 this.keyEventTarget = null;
5452 },
5453
5454 get shouldKeepAnimating () {
5455 for (var index = 0; index < this.ripples.length; ++index) {
5456 if (!this.ripples[index].isAnimationComplete) {
5457 return true;
5458 }
5459 }
5460
5461 return false;
5462 },
5463
5464 simulatedRipple: function() {
5465 this.downAction(null);
5466
5467 // Please see polymer/polymer#1305
5468 this.async(function() {
5469 this.upAction();
5470 }, 1);
5471 },
5472
5473 /**
5474 * Provokes a ripple down effect via a UI event,
5475 * respecting the `noink` property.
5476 * @param {Event=} event
5477 */
5478 uiDownAction: function(event) {
5479 if (!this.noink) {
5480 this.downAction(event);
5481 }
5482 },
5483
5484 /**
5485 * Provokes a ripple down effect via a UI event,
5486 * *not* respecting the `noink` property.
5487 * @param {Event=} event
5488 */
5489 downAction: function(event) {
5490 if (this.holdDown && this.ripples.length > 0) {
5491 return;
5492 }
5493
5494 var ripple = this.addRipple();
5495
5496 ripple.downAction(event);
5497
5498 if (!this._animating) {
5499 this._animating = true;
5500 this.animate();
5501 }
5502 },
5503
5504 /**
5505 * Provokes a ripple up effect via a UI event,
5506 * respecting the `noink` property.
5507 * @param {Event=} event
5508 */
5509 uiUpAction: function(event) {
5510 if (!this.noink) {
5511 this.upAction(event);
5512 }
5513 },
5514
5515 /**
5516 * Provokes a ripple up effect via a UI event,
5517 * *not* respecting the `noink` property.
5518 * @param {Event=} event
5519 */
5520 upAction: function(event) {
5521 if (this.holdDown) {
5522 return;
5523 }
5524
5525 this.ripples.forEach(function(ripple) {
5526 ripple.upAction(event);
5527 });
5528
5529 this._animating = true; 3028 this._animating = true;
5530 this.animate(); 3029 this.animate();
5531 }, 3030 }
5532 3031 },
5533 onAnimationComplete: function() { 3032 uiUpAction: function(event) {
5534 this._animating = false; 3033 if (!this.noink) {
5535 this.$.background.style.backgroundColor = null; 3034 this.upAction(event);
5536 this.fire('transitionend'); 3035 }
5537 }, 3036 },
5538 3037 upAction: function(event) {
5539 addRipple: function() { 3038 if (this.holdDown) {
5540 var ripple = new Ripple(this); 3039 return;
5541 3040 }
5542 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); 3041 this.ripples.forEach(function(ripple) {
5543 this.$.background.style.backgroundColor = ripple.color; 3042 ripple.upAction(event);
5544 this.ripples.push(ripple); 3043 });
5545 3044 this._animating = true;
5546 this._setAnimating(true); 3045 this.animate();
5547 3046 },
5548 return ripple; 3047 onAnimationComplete: function() {
5549 }, 3048 this._animating = false;
5550 3049 this.$.background.style.backgroundColor = null;
5551 removeRipple: function(ripple) { 3050 this.fire('transitionend');
5552 var rippleIndex = this.ripples.indexOf(ripple); 3051 },
5553 3052 addRipple: function() {
5554 if (rippleIndex < 0) { 3053 var ripple = new Ripple(this);
5555 return; 3054 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
3055 this.$.background.style.backgroundColor = ripple.color;
3056 this.ripples.push(ripple);
3057 this._setAnimating(true);
3058 return ripple;
3059 },
3060 removeRipple: function(ripple) {
3061 var rippleIndex = this.ripples.indexOf(ripple);
3062 if (rippleIndex < 0) {
3063 return;
3064 }
3065 this.ripples.splice(rippleIndex, 1);
3066 ripple.remove();
3067 if (!this.ripples.length) {
3068 this._setAnimating(false);
3069 }
3070 },
3071 animate: function() {
3072 if (!this._animating) {
3073 return;
3074 }
3075 var index;
3076 var ripple;
3077 for (index = 0; index < this.ripples.length; ++index) {
3078 ripple = this.ripples[index];
3079 ripple.draw();
3080 this.$.background.style.opacity = ripple.outerOpacity;
3081 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
3082 this.removeRipple(ripple);
5556 } 3083 }
5557 3084 }
5558 this.ripples.splice(rippleIndex, 1); 3085 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
5559 3086 this.onAnimationComplete();
5560 ripple.remove(); 3087 } else {
5561 3088 window.requestAnimationFrame(this._boundAnimate);
5562 if (!this.ripples.length) { 3089 }
5563 this._setAnimating(false); 3090 },
3091 _onEnterKeydown: function() {
3092 this.uiDownAction();
3093 this.async(this.uiUpAction, 1);
3094 },
3095 _onSpaceKeydown: function() {
3096 this.uiDownAction();
3097 },
3098 _onSpaceKeyup: function() {
3099 this.uiUpAction();
3100 },
3101 _holdDownChanged: function(newVal, oldVal) {
3102 if (oldVal === undefined) {
3103 return;
3104 }
3105 if (newVal) {
3106 this.downAction();
3107 } else {
3108 this.upAction();
3109 }
3110 }
3111 });
3112 })();
3113
3114 Polymer.PaperRippleBehavior = {
3115 properties: {
3116 noink: {
3117 type: Boolean,
3118 observer: '_noinkChanged'
3119 },
3120 _rippleContainer: {
3121 type: Object
3122 }
3123 },
3124 _buttonStateChanged: function() {
3125 if (this.focused) {
3126 this.ensureRipple();
3127 }
3128 },
3129 _downHandler: function(event) {
3130 Polymer.IronButtonStateImpl._downHandler.call(this, event);
3131 if (this.pressed) {
3132 this.ensureRipple(event);
3133 }
3134 },
3135 ensureRipple: function(optTriggeringEvent) {
3136 if (!this.hasRipple()) {
3137 this._ripple = this._createRipple();
3138 this._ripple.noink = this.noink;
3139 var rippleContainer = this._rippleContainer || this.root;
3140 if (rippleContainer) {
3141 Polymer.dom(rippleContainer).appendChild(this._ripple);
3142 }
3143 if (optTriggeringEvent) {
3144 var domContainer = Polymer.dom(this._rippleContainer || this);
3145 var target = Polymer.dom(optTriggeringEvent).rootTarget;
3146 if (domContainer.deepContains(target)) {
3147 this._ripple.uiDownAction(optTriggeringEvent);
5564 } 3148 }
5565 }, 3149 }
5566 3150 }
5567 animate: function() { 3151 },
5568 if (!this._animating) { 3152 getRipple: function() {
5569 return; 3153 this.ensureRipple();
5570 } 3154 return this._ripple;
5571 var index; 3155 },
5572 var ripple; 3156 hasRipple: function() {
5573 3157 return Boolean(this._ripple);
5574 for (index = 0; index < this.ripples.length; ++index) { 3158 },
5575 ripple = this.ripples[index]; 3159 _createRipple: function() {
5576 3160 return document.createElement('paper-ripple');
5577 ripple.draw(); 3161 },
5578 3162 _noinkChanged: function(noink) {
5579 this.$.background.style.opacity = ripple.outerOpacity; 3163 if (this.hasRipple()) {
5580 3164 this._ripple.noink = noink;
5581 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { 3165 }
5582 this.removeRipple(ripple); 3166 }
5583 } 3167 };
5584 } 3168
5585 3169 Polymer.PaperButtonBehaviorImpl = {
5586 if (!this.shouldKeepAnimating && this.ripples.length === 0) { 3170 properties: {
5587 this.onAnimationComplete(); 3171 elevation: {
5588 } else { 3172 type: Number,
5589 window.requestAnimationFrame(this._boundAnimate); 3173 reflectToAttribute: true,
5590 } 3174 readOnly: true
5591 }, 3175 }
5592 3176 },
5593 _onEnterKeydown: function() { 3177 observers: [ '_calculateElevation(focused, disabled, active, pressed, received FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ],
5594 this.uiDownAction(); 3178 hostAttributes: {
5595 this.async(this.uiUpAction, 1); 3179 role: 'button',
5596 }, 3180 tabindex: '0',
5597 3181 animated: true
5598 _onSpaceKeydown: function() { 3182 },
5599 this.uiDownAction(); 3183 _calculateElevation: function() {
5600 }, 3184 var e = 1;
5601 3185 if (this.disabled) {
5602 _onSpaceKeyup: function() { 3186 e = 0;
5603 this.uiUpAction(); 3187 } else if (this.active || this.pressed) {
5604 }, 3188 e = 4;
5605 3189 } else if (this.receivedFocusFromKeyboard) {
5606 // note: holdDown does not respect noink since it can be a focus based 3190 e = 3;
5607 // effect. 3191 }
5608 _holdDownChanged: function(newVal, oldVal) { 3192 this._setElevation(e);
5609 if (oldVal === undefined) { 3193 },
5610 return; 3194 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5611 } 3195 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5612 if (newVal) { 3196 },
5613 this.downAction(); 3197 _spaceKeyDownHandler: function(event) {
5614 } else { 3198 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5615 this.upAction(); 3199 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5616 } 3200 this._ripple.uiDownAction();
5617 } 3201 }
5618 3202 },
5619 /** 3203 _spaceKeyUpHandler: function(event) {
5620 Fired when the animation finishes. 3204 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5621 This is useful if you want to wait until 3205 if (this.hasRipple()) {
5622 the ripple animation finishes to perform some action. 3206 this._ripple.uiUpAction();
5623 3207 }
5624 @event transitionend 3208 }
5625 @param {{node: Object}} detail Contains the animated node. 3209 };
5626 */ 3210
5627 }); 3211 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ];
5628 })(); 3212
5629 /**
5630 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
5631 * when the element has focus via pointer or keyboard.
5632 *
5633 * NOTE: This behavior is intended to be used in conjunction with and after
5634 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
5635 *
5636 * @polymerBehavior Polymer.PaperRippleBehavior
5637 */
5638 Polymer.PaperRippleBehavior = {
5639 properties: {
5640 /**
5641 * If true, the element will not produce a ripple effect when interacted
5642 * with via the pointer.
5643 */
5644 noink: {
5645 type: Boolean,
5646 observer: '_noinkChanged'
5647 },
5648
5649 /**
5650 * @type {Element|undefined}
5651 */
5652 _rippleContainer: {
5653 type: Object,
5654 }
5655 },
5656
5657 /**
5658 * Ensures a `<paper-ripple>` element is available when the element is
5659 * focused.
5660 */
5661 _buttonStateChanged: function() {
5662 if (this.focused) {
5663 this.ensureRipple();
5664 }
5665 },
5666
5667 /**
5668 * In addition to the functionality provided in `IronButtonState`, ensures
5669 * a ripple effect is created when the element is in a `pressed` state.
5670 */
5671 _downHandler: function(event) {
5672 Polymer.IronButtonStateImpl._downHandler.call(this, event);
5673 if (this.pressed) {
5674 this.ensureRipple(event);
5675 }
5676 },
5677
5678 /**
5679 * Ensures this element contains a ripple effect. For startup efficiency
5680 * the ripple effect is dynamically on demand when needed.
5681 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
5682 * ripple.
5683 */
5684 ensureRipple: function(optTriggeringEvent) {
5685 if (!this.hasRipple()) {
5686 this._ripple = this._createRipple();
5687 this._ripple.noink = this.noink;
5688 var rippleContainer = this._rippleContainer || this.root;
5689 if (rippleContainer) {
5690 Polymer.dom(rippleContainer).appendChild(this._ripple);
5691 }
5692 if (optTriggeringEvent) {
5693 // Check if the event happened inside of the ripple container
5694 // Fall back to host instead of the root because distributed text
5695 // nodes are not valid event targets
5696 var domContainer = Polymer.dom(this._rippleContainer || this);
5697 var target = Polymer.dom(optTriggeringEvent).rootTarget;
5698 if (domContainer.deepContains( /** @type {Node} */(target))) {
5699 this._ripple.uiDownAction(optTriggeringEvent);
5700 }
5701 }
5702 }
5703 },
5704
5705 /**
5706 * Returns the `<paper-ripple>` element used by this element to create
5707 * ripple effects. The element's ripple is created on demand, when
5708 * necessary, and calling this method will force the
5709 * ripple to be created.
5710 */
5711 getRipple: function() {
5712 this.ensureRipple();
5713 return this._ripple;
5714 },
5715
5716 /**
5717 * Returns true if this element currently contains a ripple effect.
5718 * @return {boolean}
5719 */
5720 hasRipple: function() {
5721 return Boolean(this._ripple);
5722 },
5723
5724 /**
5725 * Create the element's ripple effect via creating a `<paper-ripple>`.
5726 * Override this method to customize the ripple element.
5727 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
5728 */
5729 _createRipple: function() {
5730 return /** @type {!PaperRippleElement} */ (
5731 document.createElement('paper-ripple'));
5732 },
5733
5734 _noinkChanged: function(noink) {
5735 if (this.hasRipple()) {
5736 this._ripple.noink = noink;
5737 }
5738 }
5739 };
5740 /** @polymerBehavior Polymer.PaperButtonBehavior */
5741 Polymer.PaperButtonBehaviorImpl = {
5742 properties: {
5743 /**
5744 * The z-depth of this element, from 0-5. Setting to 0 will remove the
5745 * shadow, and each increasing number greater than 0 will be "deeper"
5746 * than the last.
5747 *
5748 * @attribute elevation
5749 * @type number
5750 * @default 1
5751 */
5752 elevation: {
5753 type: Number,
5754 reflectToAttribute: true,
5755 readOnly: true
5756 }
5757 },
5758
5759 observers: [
5760 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
5761 '_computeKeyboardClass(receivedFocusFromKeyboard)'
5762 ],
5763
5764 hostAttributes: {
5765 role: 'button',
5766 tabindex: '0',
5767 animated: true
5768 },
5769
5770 _calculateElevation: function() {
5771 var e = 1;
5772 if (this.disabled) {
5773 e = 0;
5774 } else if (this.active || this.pressed) {
5775 e = 4;
5776 } else if (this.receivedFocusFromKeyboard) {
5777 e = 3;
5778 }
5779 this._setElevation(e);
5780 },
5781
5782 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5783 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5784 },
5785
5786 /**
5787 * In addition to `IronButtonState` behavior, when space key goes down,
5788 * create a ripple down effect.
5789 *
5790 * @param {!KeyboardEvent} event .
5791 */
5792 _spaceKeyDownHandler: function(event) {
5793 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5794 // Ensure that there is at most one ripple when the space key is held down .
5795 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5796 this._ripple.uiDownAction();
5797 }
5798 },
5799
5800 /**
5801 * In addition to `IronButtonState` behavior, when space key goes up,
5802 * create a ripple up effect.
5803 *
5804 * @param {!KeyboardEvent} event .
5805 */
5806 _spaceKeyUpHandler: function(event) {
5807 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5808 if (this.hasRipple()) {
5809 this._ripple.uiUpAction();
5810 }
5811 }
5812 };
5813
5814 /** @polymerBehavior */
5815 Polymer.PaperButtonBehavior = [
5816 Polymer.IronButtonState,
5817 Polymer.IronControlState,
5818 Polymer.PaperRippleBehavior,
5819 Polymer.PaperButtonBehaviorImpl
5820 ];
5821 Polymer({ 3213 Polymer({
5822 is: 'paper-button', 3214 is: 'paper-button',
5823 3215 behaviors: [ Polymer.PaperButtonBehavior ],
5824 behaviors: [ 3216 properties: {
5825 Polymer.PaperButtonBehavior 3217 raised: {
5826 ], 3218 type: Boolean,
5827 3219 reflectToAttribute: true,
5828 properties: { 3220 value: false,
5829 /** 3221 observer: '_calculateElevation'
5830 * If true, the button should be styled with a shadow. 3222 }
5831 */ 3223 },
5832 raised: { 3224 _calculateElevation: function() {
5833 type: Boolean, 3225 if (!this.raised) {
5834 reflectToAttribute: true, 3226 this._setElevation(0);
5835 value: false, 3227 } else {
5836 observer: '_calculateElevation' 3228 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5837 } 3229 }
5838 }, 3230 }
5839 3231 });
5840 _calculateElevation: function() { 3232
5841 if (!this.raised) {
5842 this._setElevation(0);
5843 } else {
5844 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5845 }
5846 }
5847
5848 /**
5849 Fired when the animation finishes.
5850 This is useful if you want to wait until
5851 the ripple animation finishes to perform some action.
5852
5853 @event transitionend
5854 Event param: {{node: Object}} detail Contains the animated node.
5855 */
5856 });
5857 Polymer({ 3233 Polymer({
5858 is: 'paper-icon-button-light', 3234 is: 'paper-icon-button-light',
5859 extends: 'button', 3235 "extends": 'button',
5860 3236 behaviors: [ Polymer.PaperRippleBehavior ],
5861 behaviors: [ 3237 listeners: {
5862 Polymer.PaperRippleBehavior 3238 down: '_rippleDown',
5863 ], 3239 up: '_rippleUp',
5864 3240 focus: '_rippleDown',
5865 listeners: { 3241 blur: '_rippleUp'
5866 'down': '_rippleDown', 3242 },
5867 'up': '_rippleUp', 3243 _rippleDown: function() {
5868 'focus': '_rippleDown', 3244 this.getRipple().downAction();
5869 'blur': '_rippleUp', 3245 },
5870 }, 3246 _rippleUp: function() {
5871 3247 this.getRipple().upAction();
5872 _rippleDown: function() { 3248 },
5873 this.getRipple().downAction(); 3249 ensureRipple: function(var_args) {
5874 }, 3250 var lastRipple = this._ripple;
5875 3251 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
5876 _rippleUp: function() { 3252 if (this._ripple && this._ripple !== lastRipple) {
5877 this.getRipple().upAction(); 3253 this._ripple.center = true;
5878 }, 3254 this._ripple.classList.add('circle');
5879 3255 }
5880 /** 3256 }
5881 * @param {...*} var_args 3257 });
5882 */ 3258
5883 ensureRipple: function(var_args) { 3259 Polymer.IronRangeBehavior = {
5884 var lastRipple = this._ripple;
5885 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
5886 if (this._ripple && this._ripple !== lastRipple) {
5887 this._ripple.center = true;
5888 this._ripple.classList.add('circle');
5889 }
5890 }
5891 });
5892 /**
5893 * `iron-range-behavior` provides the behavior for something with a minimum to m aximum range.
5894 *
5895 * @demo demo/index.html
5896 * @polymerBehavior
5897 */
5898 Polymer.IronRangeBehavior = {
5899
5900 properties: { 3260 properties: {
5901
5902 /**
5903 * The number that represents the current value.
5904 */
5905 value: { 3261 value: {
5906 type: Number, 3262 type: Number,
5907 value: 0, 3263 value: 0,
5908 notify: true, 3264 notify: true,
5909 reflectToAttribute: true 3265 reflectToAttribute: true
5910 }, 3266 },
5911
5912 /**
5913 * The number that indicates the minimum value of the range.
5914 */
5915 min: { 3267 min: {
5916 type: Number, 3268 type: Number,
5917 value: 0, 3269 value: 0,
5918 notify: true 3270 notify: true
5919 }, 3271 },
5920
5921 /**
5922 * The number that indicates the maximum value of the range.
5923 */
5924 max: { 3272 max: {
5925 type: Number, 3273 type: Number,
5926 value: 100, 3274 value: 100,
5927 notify: true 3275 notify: true
5928 }, 3276 },
5929
5930 /**
5931 * Specifies the value granularity of the range's value.
5932 */
5933 step: { 3277 step: {
5934 type: Number, 3278 type: Number,
5935 value: 1, 3279 value: 1,
5936 notify: true 3280 notify: true
5937 }, 3281 },
5938
5939 /**
5940 * Returns the ratio of the value.
5941 */
5942 ratio: { 3282 ratio: {
5943 type: Number, 3283 type: Number,
5944 value: 0, 3284 value: 0,
5945 readOnly: true, 3285 readOnly: true,
5946 notify: true 3286 notify: true
5947 }, 3287 }
5948 }, 3288 },
5949 3289 observers: [ '_update(value, min, max, step)' ],
5950 observers: [
5951 '_update(value, min, max, step)'
5952 ],
5953
5954 _calcRatio: function(value) { 3290 _calcRatio: function(value) {
5955 return (this._clampValue(value) - this.min) / (this.max - this.min); 3291 return (this._clampValue(value) - this.min) / (this.max - this.min);
5956 }, 3292 },
5957
5958 _clampValue: function(value) { 3293 _clampValue: function(value) {
5959 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); 3294 return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
5960 }, 3295 },
5961
5962 _calcStep: function(value) { 3296 _calcStep: function(value) {
5963 // polymer/issues/2493
5964 value = parseFloat(value); 3297 value = parseFloat(value);
5965
5966 if (!this.step) { 3298 if (!this.step) {
5967 return value; 3299 return value;
5968 } 3300 }
5969
5970 var numSteps = Math.round((value - this.min) / this.step); 3301 var numSteps = Math.round((value - this.min) / this.step);
5971 if (this.step < 1) { 3302 if (this.step < 1) {
5972 /**
5973 * For small values of this.step, if we calculate the step using
5974 * `Math.round(value / step) * step` we may hit a precision point issue
5975 * eg. 0.1 * 0.2 = 0.020000000000000004
5976 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
5977 *
5978 * as a work around we can divide by the reciprocal of `step`
5979 */
5980 return numSteps / (1 / this.step) + this.min; 3303 return numSteps / (1 / this.step) + this.min;
5981 } else { 3304 } else {
5982 return numSteps * this.step + this.min; 3305 return numSteps * this.step + this.min;
5983 } 3306 }
5984 }, 3307 },
5985
5986 _validateValue: function() { 3308 _validateValue: function() {
5987 var v = this._clampValue(this.value); 3309 var v = this._clampValue(this.value);
5988 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; 3310 this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
5989 return this.value !== v; 3311 return this.value !== v;
5990 }, 3312 },
5991
5992 _update: function() { 3313 _update: function() {
5993 this._validateValue(); 3314 this._validateValue();
5994 this._setRatio(this._calcRatio(this.value) * 100); 3315 this._setRatio(this._calcRatio(this.value) * 100);
5995 } 3316 }
5996
5997 }; 3317 };
3318
5998 Polymer({ 3319 Polymer({
5999 is: 'paper-progress', 3320 is: 'paper-progress',
6000 3321 behaviors: [ Polymer.IronRangeBehavior ],
6001 behaviors: [ 3322 properties: {
6002 Polymer.IronRangeBehavior 3323 secondaryProgress: {
6003 ], 3324 type: Number,
6004 3325 value: 0
6005 properties: { 3326 },
6006 /** 3327 secondaryRatio: {
6007 * The number that represents the current secondary progress. 3328 type: Number,
6008 */ 3329 value: 0,
6009 secondaryProgress: { 3330 readOnly: true
6010 type: Number, 3331 },
6011 value: 0 3332 indeterminate: {
6012 }, 3333 type: Boolean,
6013 3334 value: false,
6014 /** 3335 observer: '_toggleIndeterminate'
6015 * The secondary ratio 3336 },
6016 */ 3337 disabled: {
6017 secondaryRatio: { 3338 type: Boolean,
6018 type: Number, 3339 value: false,
6019 value: 0, 3340 reflectToAttribute: true,
6020 readOnly: true 3341 observer: '_disabledChanged'
6021 }, 3342 }
6022 3343 },
6023 /** 3344 observers: [ '_progressChanged(secondaryProgress, value, min, max)' ],
6024 * Use an indeterminate progress indicator. 3345 hostAttributes: {
6025 */ 3346 role: 'progressbar'
6026 indeterminate: { 3347 },
6027 type: Boolean, 3348 _toggleIndeterminate: function(indeterminate) {
6028 value: false, 3349 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
6029 observer: '_toggleIndeterminate' 3350 },
6030 }, 3351 _transformProgress: function(progress, ratio) {
6031 3352 var transform = 'scaleX(' + ratio / 100 + ')';
6032 /** 3353 progress.style.transform = progress.style.webkitTransform = transform;
6033 * True if the progress is disabled. 3354 },
6034 */ 3355 _mainRatioChanged: function(ratio) {
6035 disabled: { 3356 this._transformProgress(this.$.primaryProgress, ratio);
6036 type: Boolean, 3357 },
6037 value: false, 3358 _progressChanged: function(secondaryProgress, value, min, max) {
6038 reflectToAttribute: true, 3359 secondaryProgress = this._clampValue(secondaryProgress);
6039 observer: '_disabledChanged' 3360 value = this._clampValue(value);
6040 } 3361 var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
6041 }, 3362 var mainRatio = this._calcRatio(value) * 100;
6042 3363 this._setSecondaryRatio(secondaryRatio);
6043 observers: [ 3364 this._transformProgress(this.$.secondaryProgress, secondaryRatio);
6044 '_progressChanged(secondaryProgress, value, min, max)' 3365 this._transformProgress(this.$.primaryProgress, mainRatio);
6045 ], 3366 this.secondaryProgress = secondaryProgress;
6046 3367 this.setAttribute('aria-valuenow', value);
6047 hostAttributes: { 3368 this.setAttribute('aria-valuemin', min);
6048 role: 'progressbar' 3369 this.setAttribute('aria-valuemax', max);
6049 }, 3370 },
6050 3371 _disabledChanged: function(disabled) {
6051 _toggleIndeterminate: function(indeterminate) { 3372 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
6052 // If we use attribute/class binding, the animation sometimes doesn't tran slate properly 3373 },
6053 // on Safari 7.1. So instead, we toggle the class here in the update metho d. 3374 _hideSecondaryProgress: function(secondaryRatio) {
6054 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); 3375 return secondaryRatio === 0;
6055 }, 3376 }
6056 3377 });
6057 _transformProgress: function(progress, ratio) { 3378
6058 var transform = 'scaleX(' + (ratio / 100) + ')'; 3379 Polymer({
6059 progress.style.transform = progress.style.webkitTransform = transform; 3380 is: 'iron-iconset-svg',
6060 }, 3381 properties: {
6061 3382 name: {
6062 _mainRatioChanged: function(ratio) { 3383 type: String,
6063 this._transformProgress(this.$.primaryProgress, ratio); 3384 observer: '_nameChanged'
6064 }, 3385 },
6065 3386 size: {
6066 _progressChanged: function(secondaryProgress, value, min, max) { 3387 type: Number,
6067 secondaryProgress = this._clampValue(secondaryProgress); 3388 value: 24
6068 value = this._clampValue(value); 3389 }
6069 3390 },
6070 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; 3391 attached: function() {
6071 var mainRatio = this._calcRatio(value) * 100; 3392 this.style.display = 'none';
6072 3393 },
6073 this._setSecondaryRatio(secondaryRatio); 3394 getIconNames: function() {
6074 this._transformProgress(this.$.secondaryProgress, secondaryRatio); 3395 this._icons = this._createIconMap();
6075 this._transformProgress(this.$.primaryProgress, mainRatio); 3396 return Object.keys(this._icons).map(function(n) {
6076 3397 return this.name + ':' + n;
6077 this.secondaryProgress = secondaryProgress; 3398 }, this);
6078 3399 },
6079 this.setAttribute('aria-valuenow', value); 3400 applyIcon: function(element, iconName) {
6080 this.setAttribute('aria-valuemin', min); 3401 element = element.root || element;
6081 this.setAttribute('aria-valuemax', max); 3402 this.removeIcon(element);
6082 }, 3403 var svg = this._cloneIcon(iconName);
6083 3404 if (svg) {
6084 _disabledChanged: function(disabled) { 3405 var pde = Polymer.dom(element);
6085 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 3406 pde.insertBefore(svg, pde.childNodes[0]);
6086 }, 3407 return element._svgIcon = svg;
6087 3408 }
6088 _hideSecondaryProgress: function(secondaryRatio) { 3409 return null;
6089 return secondaryRatio === 0; 3410 },
6090 } 3411 removeIcon: function(element) {
6091 }); 3412 if (element._svgIcon) {
6092 /** 3413 Polymer.dom(element).removeChild(element._svgIcon);
6093 * The `iron-iconset-svg` element allows users to define their own icon sets 3414 element._svgIcon = null;
6094 * that contain svg icons. The svg icon elements should be children of the 3415 }
6095 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. 3416 },
6096 * 3417 _nameChanged: function() {
6097 * Using svg elements to create icons has a few advantages over traditional 3418 new Polymer.IronMeta({
6098 * bitmap graphics like jpg or png. Icons that use svg are vector based so 3419 type: 'iconset',
6099 * they are resolution independent and should look good on any device. They 3420 key: this.name,
6100 * are stylable via css. Icons can be themed, colorized, and even animated. 3421 value: this
6101 * 3422 });
6102 * Example: 3423 this.async(function() {
6103 * 3424 this.fire('iron-iconset-added', this, {
6104 * <iron-iconset-svg name="my-svg-icons" size="24"> 3425 node: window
6105 * <svg>
6106 * <defs>
6107 * <g id="shape">
6108 * <rect x="12" y="0" width="12" height="24" />
6109 * <circle cx="12" cy="12" r="12" />
6110 * </g>
6111 * </defs>
6112 * </svg>
6113 * </iron-iconset-svg>
6114 *
6115 * This will automatically register the icon set "my-svg-icons" to the iconset
6116 * database. To use these icons from within another element, make a
6117 * `iron-iconset` element and call the `byId` method
6118 * to retrieve a given iconset. To apply a particular icon inside an
6119 * element use the `applyIcon` method. For example:
6120 *
6121 * iconset.applyIcon(iconNode, 'car');
6122 *
6123 * @element iron-iconset-svg
6124 * @demo demo/index.html
6125 * @implements {Polymer.Iconset}
6126 */
6127 Polymer({
6128 is: 'iron-iconset-svg',
6129
6130 properties: {
6131
6132 /**
6133 * The name of the iconset.
6134 */
6135 name: {
6136 type: String,
6137 observer: '_nameChanged'
6138 },
6139
6140 /**
6141 * The size of an individual icon. Note that icons must be square.
6142 */
6143 size: {
6144 type: Number,
6145 value: 24
6146 }
6147
6148 },
6149
6150 attached: function() {
6151 this.style.display = 'none';
6152 },
6153
6154 /**
6155 * Construct an array of all icon names in this iconset.
6156 *
6157 * @return {!Array} Array of icon names.
6158 */
6159 getIconNames: function() {
6160 this._icons = this._createIconMap();
6161 return Object.keys(this._icons).map(function(n) {
6162 return this.name + ':' + n;
6163 }, this);
6164 },
6165
6166 /**
6167 * Applies an icon to the given element.
6168 *
6169 * An svg icon is prepended to the element's shadowRoot if it exists,
6170 * otherwise to the element itself.
6171 *
6172 * @method applyIcon
6173 * @param {Element} element Element to which the icon is applied.
6174 * @param {string} iconName Name of the icon to apply.
6175 * @return {?Element} The svg element which renders the icon.
6176 */
6177 applyIcon: function(element, iconName) {
6178 // insert svg element into shadow root, if it exists
6179 element = element.root || element;
6180 // Remove old svg element
6181 this.removeIcon(element);
6182 // install new svg element
6183 var svg = this._cloneIcon(iconName);
6184 if (svg) {
6185 var pde = Polymer.dom(element);
6186 pde.insertBefore(svg, pde.childNodes[0]);
6187 return element._svgIcon = svg;
6188 }
6189 return null;
6190 },
6191
6192 /**
6193 * Remove an icon from the given element by undoing the changes effected
6194 * by `applyIcon`.
6195 *
6196 * @param {Element} element The element from which the icon is removed.
6197 */
6198 removeIcon: function(element) {
6199 // Remove old svg element
6200 if (element._svgIcon) {
6201 Polymer.dom(element).removeChild(element._svgIcon);
6202 element._svgIcon = null;
6203 }
6204 },
6205
6206 /**
6207 *
6208 * When name is changed, register iconset metadata
6209 *
6210 */
6211 _nameChanged: function() {
6212 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
6213 this.async(function() {
6214 this.fire('iron-iconset-added', this, {node: window});
6215 }); 3426 });
6216 }, 3427 });
6217 3428 },
6218 /** 3429 _createIconMap: function() {
6219 * Create a map of child SVG elements by id. 3430 var icons = Object.create(null);
6220 * 3431 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) {
6221 * @return {!Object} Map of id's to SVG elements. 3432 icons[icon.id] = icon;
6222 */ 3433 });
6223 _createIconMap: function() { 3434 return icons;
6224 // Objects chained to Object.prototype (`{}`) have members. Specifically, 3435 },
6225 // on FF there is a `watch` method that confuses the icon map, so we 3436 _cloneIcon: function(id) {
6226 // need to use a null-based object here. 3437 this._icons = this._icons || this._createIconMap();
6227 var icons = Object.create(null); 3438 return this._prepareSvgClone(this._icons[id], this.size);
6228 Polymer.dom(this).querySelectorAll('[id]') 3439 },
6229 .forEach(function(icon) { 3440 _prepareSvgClone: function(sourceSvg, size) {
6230 icons[icon.id] = icon; 3441 if (sourceSvg) {
6231 }); 3442 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
6232 return icons; 3443 svg.setAttribute('viewBox', viewBox);
6233 }, 3444 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6234 3445 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he ight: 100%;';
6235 /** 3446 svg.appendChild(content).removeAttribute('id');
6236 * Produce installable clone of the SVG element matching `id` in this 3447 return svg;
6237 * iconset, or `undefined` if there is no matching element. 3448 }
6238 * 3449 return null;
6239 * @return {Element} Returns an installable clone of the SVG element 3450 }
6240 * matching `id`. 3451 });
6241 */ 3452
6242 _cloneIcon: function(id) {
6243 // create the icon map on-demand, since the iconset itself has no discrete
6244 // signal to know when it's children are fully parsed
6245 this._icons = this._icons || this._createIconMap();
6246 return this._prepareSvgClone(this._icons[id], this.size);
6247 },
6248
6249 /**
6250 * @param {Element} sourceSvg
6251 * @param {number} size
6252 * @return {Element}
6253 */
6254 _prepareSvgClone: function(sourceSvg, size) {
6255 if (sourceSvg) {
6256 var content = sourceSvg.cloneNode(true),
6257 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
6258 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s ize;
6259 svg.setAttribute('viewBox', viewBox);
6260 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6261 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136
6262 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
6263 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6264 svg.appendChild(content).removeAttribute('id');
6265 return svg;
6266 }
6267 return null;
6268 }
6269
6270 });
6271 // Copyright 2015 The Chromium Authors. All rights reserved. 3453 // Copyright 2015 The Chromium Authors. All rights reserved.
6272 // Use of this source code is governed by a BSD-style license that can be 3454 // Use of this source code is governed by a BSD-style license that can be
6273 // found in the LICENSE file. 3455 // found in the LICENSE file.
6274
6275 cr.define('downloads', function() { 3456 cr.define('downloads', function() {
6276 var Item = Polymer({ 3457 var Item = Polymer({
6277 is: 'downloads-item', 3458 is: 'downloads-item',
6278
6279 properties: { 3459 properties: {
6280 data: { 3460 data: {
6281 type: Object, 3461 type: Object
6282 }, 3462 },
6283
6284 completelyOnDisk_: { 3463 completelyOnDisk_: {
6285 computed: 'computeCompletelyOnDisk_(' + 3464 computed: 'computeCompletelyOnDisk_(' + 'data.state, data.file_externall y_removed)',
6286 'data.state, data.file_externally_removed)',
6287 type: Boolean, 3465 type: Boolean,
6288 value: true, 3466 value: true
6289 }, 3467 },
6290
6291 controlledBy_: { 3468 controlledBy_: {
6292 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', 3469 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
6293 type: String, 3470 type: String,
6294 value: '', 3471 value: ''
6295 }, 3472 },
6296
6297 isActive_: { 3473 isActive_: {
6298 computed: 'computeIsActive_(' + 3474 computed: 'computeIsActive_(' + 'data.state, data.file_externally_remove d)',
6299 'data.state, data.file_externally_removed)',
6300 type: Boolean, 3475 type: Boolean,
6301 value: true, 3476 value: true
6302 }, 3477 },
6303
6304 isDangerous_: { 3478 isDangerous_: {
6305 computed: 'computeIsDangerous_(data.state)', 3479 computed: 'computeIsDangerous_(data.state)',
6306 type: Boolean, 3480 type: Boolean,
6307 value: false, 3481 value: false
6308 }, 3482 },
6309
6310 isMalware_: { 3483 isMalware_: {
6311 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', 3484 computed: 'computeIsMalware_(isDangerous_, data.danger_type)',
6312 type: Boolean, 3485 type: Boolean,
6313 value: false, 3486 value: false
6314 }, 3487 },
6315
6316 isInProgress_: { 3488 isInProgress_: {
6317 computed: 'computeIsInProgress_(data.state)', 3489 computed: 'computeIsInProgress_(data.state)',
6318 type: Boolean, 3490 type: Boolean,
6319 value: false, 3491 value: false
6320 }, 3492 },
6321
6322 pauseOrResumeText_: { 3493 pauseOrResumeText_: {
6323 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', 3494 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)',
6324 type: String, 3495 type: String
6325 }, 3496 },
6326
6327 showCancel_: { 3497 showCancel_: {
6328 computed: 'computeShowCancel_(data.state)', 3498 computed: 'computeShowCancel_(data.state)',
6329 type: Boolean, 3499 type: Boolean,
6330 value: false, 3500 value: false
6331 }, 3501 },
6332
6333 showProgress_: { 3502 showProgress_: {
6334 computed: 'computeShowProgress_(showCancel_, data.percent)', 3503 computed: 'computeShowProgress_(showCancel_, data.percent)',
6335 type: Boolean, 3504 type: Boolean,
6336 value: false, 3505 value: false
6337 }, 3506 }
6338 }, 3507 },
6339 3508 observers: [ 'observeControlledBy_(controlledBy_)', 'observeIsDangerous_(isD angerous_, data)' ],
6340 observers: [
6341 // TODO(dbeam): this gets called way more when I observe data.by_ext_id
6342 // and data.by_ext_name directly. Why?
6343 'observeControlledBy_(controlledBy_)',
6344 'observeIsDangerous_(isDangerous_, data)',
6345 ],
6346
6347 ready: function() { 3509 ready: function() {
6348 this.content = this.$.content; 3510 this.content = this.$.content;
6349 }, 3511 },
6350
6351 /** @private */
6352 computeClass_: function() { 3512 computeClass_: function() {
6353 var classes = []; 3513 var classes = [];
6354 3514 if (this.isActive_) classes.push('is-active');
6355 if (this.isActive_) 3515 if (this.isDangerous_) classes.push('dangerous');
6356 classes.push('is-active'); 3516 if (this.showProgress_) classes.push('show-progress');
6357
6358 if (this.isDangerous_)
6359 classes.push('dangerous');
6360
6361 if (this.showProgress_)
6362 classes.push('show-progress');
6363
6364 return classes.join(' '); 3517 return classes.join(' ');
6365 }, 3518 },
6366
6367 /** @private */
6368 computeCompletelyOnDisk_: function() { 3519 computeCompletelyOnDisk_: function() {
6369 return this.data.state == downloads.States.COMPLETE && 3520 return this.data.state == downloads.States.COMPLETE && !this.data.file_ext ernally_removed;
6370 !this.data.file_externally_removed; 3521 },
6371 },
6372
6373 /** @private */
6374 computeControlledBy_: function() { 3522 computeControlledBy_: function() {
6375 if (!this.data.by_ext_id || !this.data.by_ext_name) 3523 if (!this.data.by_ext_id || !this.data.by_ext_name) return '';
6376 return '';
6377
6378 var url = 'chrome://extensions#' + this.data.by_ext_id; 3524 var url = 'chrome://extensions#' + this.data.by_ext_id;
6379 var name = this.data.by_ext_name; 3525 var name = this.data.by_ext_name;
6380 return loadTimeData.getStringF('controlledByUrl', url, name); 3526 return loadTimeData.getStringF('controlledByUrl', url, name);
6381 }, 3527 },
6382
6383 /** @private */
6384 computeDangerIcon_: function() { 3528 computeDangerIcon_: function() {
6385 if (!this.isDangerous_) 3529 if (!this.isDangerous_) return '';
6386 return '';
6387
6388 switch (this.data.danger_type) { 3530 switch (this.data.danger_type) {
6389 case downloads.DangerType.DANGEROUS_CONTENT: 3531 case downloads.DangerType.DANGEROUS_CONTENT:
6390 case downloads.DangerType.DANGEROUS_HOST: 3532 case downloads.DangerType.DANGEROUS_HOST:
6391 case downloads.DangerType.DANGEROUS_URL: 3533 case downloads.DangerType.DANGEROUS_URL:
6392 case downloads.DangerType.POTENTIALLY_UNWANTED: 3534 case downloads.DangerType.POTENTIALLY_UNWANTED:
6393 case downloads.DangerType.UNCOMMON_CONTENT: 3535 case downloads.DangerType.UNCOMMON_CONTENT:
6394 return 'downloads:remove-circle'; 3536 return 'downloads:remove-circle';
6395 default: 3537
6396 return 'cr:warning'; 3538 default:
6397 } 3539 return 'cr:warning';
6398 }, 3540 }
6399 3541 },
6400 /** @private */
6401 computeDate_: function() { 3542 computeDate_: function() {
6402 assert(typeof this.data.hideDate == 'boolean'); 3543 assert(typeof this.data.hideDate == 'boolean');
6403 if (this.data.hideDate) 3544 if (this.data.hideDate) return '';
6404 return '';
6405 return assert(this.data.since_string || this.data.date_string); 3545 return assert(this.data.since_string || this.data.date_string);
6406 }, 3546 },
6407
6408 /** @private */
6409 computeDescription_: function() { 3547 computeDescription_: function() {
6410 var data = this.data; 3548 var data = this.data;
6411
6412 switch (data.state) { 3549 switch (data.state) {
6413 case downloads.States.DANGEROUS: 3550 case downloads.States.DANGEROUS:
6414 var fileName = data.file_name; 3551 var fileName = data.file_name;
6415 switch (data.danger_type) { 3552 switch (data.danger_type) {
6416 case downloads.DangerType.DANGEROUS_FILE: 3553 case downloads.DangerType.DANGEROUS_FILE:
6417 return loadTimeData.getStringF('dangerFileDesc', fileName); 3554 return loadTimeData.getStringF('dangerFileDesc', fileName);
6418 case downloads.DangerType.DANGEROUS_URL: 3555
6419 return loadTimeData.getString('dangerUrlDesc'); 3556 case downloads.DangerType.DANGEROUS_URL:
6420 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. 3557 return loadTimeData.getString('dangerUrlDesc');
6421 case downloads.DangerType.DANGEROUS_HOST: 3558
6422 return loadTimeData.getStringF('dangerContentDesc', fileName); 3559 case downloads.DangerType.DANGEROUS_CONTENT:
6423 case downloads.DangerType.UNCOMMON_CONTENT: 3560 case downloads.DangerType.DANGEROUS_HOST:
6424 return loadTimeData.getStringF('dangerUncommonDesc', fileName); 3561 return loadTimeData.getStringF('dangerContentDesc', fileName);
6425 case downloads.DangerType.POTENTIALLY_UNWANTED: 3562
6426 return loadTimeData.getStringF('dangerSettingsDesc', fileName); 3563 case downloads.DangerType.UNCOMMON_CONTENT:
6427 } 3564 return loadTimeData.getStringF('dangerUncommonDesc', fileName);
6428 break; 3565
6429 3566 case downloads.DangerType.POTENTIALLY_UNWANTED:
6430 case downloads.States.IN_PROGRESS: 3567 return loadTimeData.getStringF('dangerSettingsDesc', fileName);
6431 case downloads.States.PAUSED: // Fallthrough. 3568 }
6432 return data.progress_status_text; 3569 break;
6433 } 3570
6434 3571 case downloads.States.IN_PROGRESS:
3572 case downloads.States.PAUSED:
3573 return data.progress_status_text;
3574 }
6435 return ''; 3575 return '';
6436 }, 3576 },
6437
6438 /** @private */
6439 computeIsActive_: function() { 3577 computeIsActive_: function() {
6440 return this.data.state != downloads.States.CANCELLED && 3578 return this.data.state != downloads.States.CANCELLED && this.data.state != downloads.States.INTERRUPTED && !this.data.file_externally_removed;
6441 this.data.state != downloads.States.INTERRUPTED && 3579 },
6442 !this.data.file_externally_removed;
6443 },
6444
6445 /** @private */
6446 computeIsDangerous_: function() { 3580 computeIsDangerous_: function() {
6447 return this.data.state == downloads.States.DANGEROUS; 3581 return this.data.state == downloads.States.DANGEROUS;
6448 }, 3582 },
6449
6450 /** @private */
6451 computeIsInProgress_: function() { 3583 computeIsInProgress_: function() {
6452 return this.data.state == downloads.States.IN_PROGRESS; 3584 return this.data.state == downloads.States.IN_PROGRESS;
6453 }, 3585 },
6454
6455 /** @private */
6456 computeIsMalware_: function() { 3586 computeIsMalware_: function() {
6457 return this.isDangerous_ && 3587 return this.isDangerous_ && (this.data.danger_type == downloads.DangerType .DANGEROUS_CONTENT || this.data.danger_type == downloads.DangerType.DANGEROUS_HO ST || this.data.danger_type == downloads.DangerType.DANGEROUS_URL || this.data.d anger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
6458 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || 3588 },
6459 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
6460 this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
6461 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
6462 },
6463
6464 /** @private */
6465 computePauseOrResumeText_: function() { 3589 computePauseOrResumeText_: function() {
6466 if (this.isInProgress_) 3590 if (this.isInProgress_) return loadTimeData.getString('controlPause');
6467 return loadTimeData.getString('controlPause'); 3591 if (this.data.resume) return loadTimeData.getString('controlResume');
6468 if (this.data.resume)
6469 return loadTimeData.getString('controlResume');
6470 return ''; 3592 return '';
6471 }, 3593 },
6472
6473 /** @private */
6474 computeRemoveStyle_: function() { 3594 computeRemoveStyle_: function() {
6475 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); 3595 var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
6476 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; 3596 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
6477 return hideRemove ? 'visibility: hidden' : ''; 3597 return hideRemove ? 'visibility: hidden' : '';
6478 }, 3598 },
6479
6480 /** @private */
6481 computeShowCancel_: function() { 3599 computeShowCancel_: function() {
6482 return this.data.state == downloads.States.IN_PROGRESS || 3600 return this.data.state == downloads.States.IN_PROGRESS || this.data.state == downloads.States.PAUSED;
6483 this.data.state == downloads.States.PAUSED; 3601 },
6484 },
6485
6486 /** @private */
6487 computeShowProgress_: function() { 3602 computeShowProgress_: function() {
6488 return this.showCancel_ && this.data.percent >= -1; 3603 return this.showCancel_ && this.data.percent >= -1;
6489 }, 3604 },
6490
6491 /** @private */
6492 computeTag_: function() { 3605 computeTag_: function() {
6493 switch (this.data.state) { 3606 switch (this.data.state) {
6494 case downloads.States.CANCELLED: 3607 case downloads.States.CANCELLED:
6495 return loadTimeData.getString('statusCancelled'); 3608 return loadTimeData.getString('statusCancelled');
6496 3609
6497 case downloads.States.INTERRUPTED: 3610 case downloads.States.INTERRUPTED:
6498 return this.data.last_reason_text; 3611 return this.data.last_reason_text;
6499 3612
6500 case downloads.States.COMPLETE: 3613 case downloads.States.COMPLETE:
6501 return this.data.file_externally_removed ? 3614 return this.data.file_externally_removed ? loadTimeData.getString('statu sRemoved') : '';
6502 loadTimeData.getString('statusRemoved') : ''; 3615 }
6503 }
6504
6505 return ''; 3616 return '';
6506 }, 3617 },
6507
6508 /** @private */
6509 isIndeterminate_: function() { 3618 isIndeterminate_: function() {
6510 return this.data.percent == -1; 3619 return this.data.percent == -1;
6511 }, 3620 },
6512
6513 /** @private */
6514 observeControlledBy_: function() { 3621 observeControlledBy_: function() {
6515 this.$['controlled-by'].innerHTML = this.controlledBy_; 3622 this.$['controlled-by'].innerHTML = this.controlledBy_;
6516 }, 3623 },
6517
6518 /** @private */
6519 observeIsDangerous_: function() { 3624 observeIsDangerous_: function() {
6520 if (!this.data) 3625 if (!this.data) return;
6521 return;
6522
6523 if (this.isDangerous_) { 3626 if (this.isDangerous_) {
6524 this.$.url.removeAttribute('href'); 3627 this.$.url.removeAttribute('href');
6525 } else { 3628 } else {
6526 this.$.url.href = assert(this.data.url); 3629 this.$.url.href = assert(this.data.url);
6527 var filePath = encodeURIComponent(this.data.file_path); 3630 var filePath = encodeURIComponent(this.data.file_path);
6528 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; 3631 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x';
6529 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; 3632 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor;
6530 } 3633 }
6531 }, 3634 },
6532
6533 /** @private */
6534 onCancelTap_: function() { 3635 onCancelTap_: function() {
6535 downloads.ActionService.getInstance().cancel(this.data.id); 3636 downloads.ActionService.getInstance().cancel(this.data.id);
6536 }, 3637 },
6537
6538 /** @private */
6539 onDiscardDangerousTap_: function() { 3638 onDiscardDangerousTap_: function() {
6540 downloads.ActionService.getInstance().discardDangerous(this.data.id); 3639 downloads.ActionService.getInstance().discardDangerous(this.data.id);
6541 }, 3640 },
6542
6543 /**
6544 * @private
6545 * @param {Event} e
6546 */
6547 onDragStart_: function(e) { 3641 onDragStart_: function(e) {
6548 e.preventDefault(); 3642 e.preventDefault();
6549 downloads.ActionService.getInstance().drag(this.data.id); 3643 downloads.ActionService.getInstance().drag(this.data.id);
6550 }, 3644 },
6551
6552 /**
6553 * @param {Event} e
6554 * @private
6555 */
6556 onFileLinkTap_: function(e) { 3645 onFileLinkTap_: function(e) {
6557 e.preventDefault(); 3646 e.preventDefault();
6558 downloads.ActionService.getInstance().openFile(this.data.id); 3647 downloads.ActionService.getInstance().openFile(this.data.id);
6559 }, 3648 },
6560
6561 /** @private */
6562 onPauseOrResumeTap_: function() { 3649 onPauseOrResumeTap_: function() {
6563 if (this.isInProgress_) 3650 if (this.isInProgress_) downloads.ActionService.getInstance().pause(this.d ata.id); else downloads.ActionService.getInstance().resume(this.data.id);
6564 downloads.ActionService.getInstance().pause(this.data.id); 3651 },
6565 else
6566 downloads.ActionService.getInstance().resume(this.data.id);
6567 },
6568
6569 /** @private */
6570 onRemoveTap_: function() { 3652 onRemoveTap_: function() {
6571 downloads.ActionService.getInstance().remove(this.data.id); 3653 downloads.ActionService.getInstance().remove(this.data.id);
6572 }, 3654 },
6573
6574 /** @private */
6575 onRetryTap_: function() { 3655 onRetryTap_: function() {
6576 downloads.ActionService.getInstance().download(this.data.url); 3656 downloads.ActionService.getInstance().download(this.data.url);
6577 }, 3657 },
6578
6579 /** @private */
6580 onSaveDangerousTap_: function() { 3658 onSaveDangerousTap_: function() {
6581 downloads.ActionService.getInstance().saveDangerous(this.data.id); 3659 downloads.ActionService.getInstance().saveDangerous(this.data.id);
6582 }, 3660 },
6583
6584 /** @private */
6585 onShowTap_: function() { 3661 onShowTap_: function() {
6586 downloads.ActionService.getInstance().show(this.data.id); 3662 downloads.ActionService.getInstance().show(this.data.id);
6587 }, 3663 }
6588 }); 3664 });
6589 3665 return {
6590 return {Item: Item}; 3666 Item: Item
3667 };
6591 }); 3668 });
6592 /** @polymerBehavior Polymer.PaperItemBehavior */ 3669
6593 Polymer.PaperItemBehaviorImpl = { 3670 Polymer.PaperItemBehaviorImpl = {
6594 hostAttributes: { 3671 hostAttributes: {
6595 role: 'option', 3672 role: 'option',
6596 tabindex: '0' 3673 tabindex: '0'
6597 } 3674 }
6598 }; 3675 };
6599 3676
6600 /** @polymerBehavior */ 3677 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState, Polymer.PaperItemBehaviorImpl ];
6601 Polymer.PaperItemBehavior = [ 3678
6602 Polymer.IronButtonState,
6603 Polymer.IronControlState,
6604 Polymer.PaperItemBehaviorImpl
6605 ];
6606 Polymer({ 3679 Polymer({
6607 is: 'paper-item', 3680 is: 'paper-item',
6608 3681 behaviors: [ Polymer.PaperItemBehavior ]
6609 behaviors: [ 3682 });
6610 Polymer.PaperItemBehavior 3683
6611 ] 3684 Polymer.IronSelection = function(selectCallback) {
6612 }); 3685 this.selection = [];
6613 /** 3686 this.selectCallback = selectCallback;
6614 * @param {!Function} selectCallback 3687 };
6615 * @constructor 3688
6616 */ 3689 Polymer.IronSelection.prototype = {
6617 Polymer.IronSelection = function(selectCallback) { 3690 get: function() {
6618 this.selection = []; 3691 return this.multi ? this.selection.slice() : this.selection[0];
6619 this.selectCallback = selectCallback; 3692 },
6620 }; 3693 clear: function(excludes) {
6621 3694 this.selection.slice().forEach(function(item) {
6622 Polymer.IronSelection.prototype = { 3695 if (!excludes || excludes.indexOf(item) < 0) {
6623 3696 this.setItemSelected(item, false);
6624 /** 3697 }
6625 * Retrieves the selected item(s). 3698 }, this);
6626 * 3699 },
6627 * @method get 3700 isSelected: function(item) {
6628 * @returns Returns the selected item(s). If the multi property is true, 3701 return this.selection.indexOf(item) >= 0;
6629 * `get` will return an array, otherwise it will return 3702 },
6630 * the selected item or undefined if there is no selection. 3703 setItemSelected: function(item, isSelected) {
6631 */ 3704 if (item != null) {
6632 get: function() { 3705 if (isSelected !== this.isSelected(item)) {
6633 return this.multi ? this.selection.slice() : this.selection[0]; 3706 if (isSelected) {
6634 }, 3707 this.selection.push(item);
6635 3708 } else {
6636 /** 3709 var i = this.selection.indexOf(item);
6637 * Clears all the selection except the ones indicated. 3710 if (i >= 0) {
6638 * 3711 this.selection.splice(i, 1);
6639 * @method clear
6640 * @param {Array} excludes items to be excluded.
6641 */
6642 clear: function(excludes) {
6643 this.selection.slice().forEach(function(item) {
6644 if (!excludes || excludes.indexOf(item) < 0) {
6645 this.setItemSelected(item, false);
6646 }
6647 }, this);
6648 },
6649
6650 /**
6651 * Indicates if a given item is selected.
6652 *
6653 * @method isSelected
6654 * @param {*} item The item whose selection state should be checked.
6655 * @returns Returns true if `item` is selected.
6656 */
6657 isSelected: function(item) {
6658 return this.selection.indexOf(item) >= 0;
6659 },
6660
6661 /**
6662 * Sets the selection state for a given item to either selected or deselecte d.
6663 *
6664 * @method setItemSelected
6665 * @param {*} item The item to select.
6666 * @param {boolean} isSelected True for selected, false for deselected.
6667 */
6668 setItemSelected: function(item, isSelected) {
6669 if (item != null) {
6670 if (isSelected !== this.isSelected(item)) {
6671 // proceed to update selection only if requested state differs from cu rrent
6672 if (isSelected) {
6673 this.selection.push(item);
6674 } else {
6675 var i = this.selection.indexOf(item);
6676 if (i >= 0) {
6677 this.selection.splice(i, 1);
6678 }
6679 }
6680 if (this.selectCallback) {
6681 this.selectCallback(item, isSelected);
6682 } 3712 }
6683 } 3713 }
6684 } 3714 if (this.selectCallback) {
6685 }, 3715 this.selectCallback(item, isSelected);
6686
6687 /**
6688 * Sets the selection state for a given item. If the `multi` property
6689 * is true, then the selected state of `item` will be toggled; otherwise
6690 * the `item` will be selected.
6691 *
6692 * @method select
6693 * @param {*} item The item to select.
6694 */
6695 select: function(item) {
6696 if (this.multi) {
6697 this.toggle(item);
6698 } else if (this.get() !== item) {
6699 this.setItemSelected(this.get(), false);
6700 this.setItemSelected(item, true);
6701 }
6702 },
6703
6704 /**
6705 * Toggles the selection state for `item`.
6706 *
6707 * @method toggle
6708 * @param {*} item The item to toggle.
6709 */
6710 toggle: function(item) {
6711 this.setItemSelected(item, !this.isSelected(item));
6712 }
6713
6714 };
6715 /** @polymerBehavior */
6716 Polymer.IronSelectableBehavior = {
6717
6718 /**
6719 * Fired when iron-selector is activated (selected or deselected).
6720 * It is fired before the selected items are changed.
6721 * Cancel the event to abort selection.
6722 *
6723 * @event iron-activate
6724 */
6725
6726 /**
6727 * Fired when an item is selected
6728 *
6729 * @event iron-select
6730 */
6731
6732 /**
6733 * Fired when an item is deselected
6734 *
6735 * @event iron-deselect
6736 */
6737
6738 /**
6739 * Fired when the list of selectable items changes (e.g., items are
6740 * added or removed). The detail of the event is a mutation record that
6741 * describes what changed.
6742 *
6743 * @event iron-items-changed
6744 */
6745
6746 properties: {
6747
6748 /**
6749 * If you want to use an attribute value or property of an element for
6750 * `selected` instead of the index, set this to the name of the attribute
6751 * or property. Hyphenated values are converted to camel case when used to
6752 * look up the property of a selectable element. Camel cased values are
6753 * *not* converted to hyphenated values for attribute lookup. It's
6754 * recommended that you provide the hyphenated form of the name so that
6755 * selection works in both cases. (Use `attr-or-property-name` instead of
6756 * `attrOrPropertyName`.)
6757 */
6758 attrForSelected: {
6759 type: String,
6760 value: null
6761 },
6762
6763 /**
6764 * Gets or sets the selected element. The default is to use the index of t he item.
6765 * @type {string|number}
6766 */
6767 selected: {
6768 type: String,
6769 notify: true
6770 },
6771
6772 /**
6773 * Returns the currently selected item.
6774 *
6775 * @type {?Object}
6776 */
6777 selectedItem: {
6778 type: Object,
6779 readOnly: true,
6780 notify: true
6781 },
6782
6783 /**
6784 * The event that fires from items when they are selected. Selectable
6785 * will listen for this event from items and update the selection state.
6786 * Set to empty string to listen to no events.
6787 */
6788 activateEvent: {
6789 type: String,
6790 value: 'tap',
6791 observer: '_activateEventChanged'
6792 },
6793
6794 /**
6795 * This is a CSS selector string. If this is set, only items that match t he CSS selector
6796 * are selectable.
6797 */
6798 selectable: String,
6799
6800 /**
6801 * The class to set on elements when selected.
6802 */
6803 selectedClass: {
6804 type: String,
6805 value: 'iron-selected'
6806 },
6807
6808 /**
6809 * The attribute to set on elements when selected.
6810 */
6811 selectedAttribute: {
6812 type: String,
6813 value: null
6814 },
6815
6816 /**
6817 * Default fallback if the selection based on selected with `attrForSelect ed`
6818 * is not found.
6819 */
6820 fallbackSelection: {
6821 type: String,
6822 value: null
6823 },
6824
6825 /**
6826 * The list of items from which a selection can be made.
6827 */
6828 items: {
6829 type: Array,
6830 readOnly: true,
6831 notify: true,
6832 value: function() {
6833 return [];
6834 } 3716 }
6835 }, 3717 }
6836 3718 }
6837 /** 3719 },
6838 * The set of excluded elements where the key is the `localName` 3720 select: function(item) {
6839 * of the element that will be ignored from the item list. 3721 if (this.multi) {
6840 * 3722 this.toggle(item);
6841 * @default {template: 1} 3723 } else if (this.get() !== item) {
6842 */ 3724 this.setItemSelected(this.get(), false);
6843 _excludedLocalNames: { 3725 this.setItemSelected(item, true);
6844 type: Object, 3726 }
6845 value: function() { 3727 },
6846 return { 3728 toggle: function(item) {
6847 'template': 1 3729 this.setItemSelected(item, !this.isSelected(item));
6848 }; 3730 }
3731 };
3732
3733 Polymer.IronSelectableBehavior = {
3734 properties: {
3735 attrForSelected: {
3736 type: String,
3737 value: null
3738 },
3739 selected: {
3740 type: String,
3741 notify: true
3742 },
3743 selectedItem: {
3744 type: Object,
3745 readOnly: true,
3746 notify: true
3747 },
3748 activateEvent: {
3749 type: String,
3750 value: 'tap',
3751 observer: '_activateEventChanged'
3752 },
3753 selectable: String,
3754 selectedClass: {
3755 type: String,
3756 value: 'iron-selected'
3757 },
3758 selectedAttribute: {
3759 type: String,
3760 value: null
3761 },
3762 fallbackSelection: {
3763 type: String,
3764 value: null
3765 },
3766 items: {
3767 type: Array,
3768 readOnly: true,
3769 notify: true,
3770 value: function() {
3771 return [];
3772 }
3773 },
3774 _excludedLocalNames: {
3775 type: Object,
3776 value: function() {
3777 return {
3778 template: 1
3779 };
3780 }
3781 }
3782 },
3783 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec ted)', '_checkFallback(fallbackSelection)' ],
3784 created: function() {
3785 this._bindFilterItem = this._filterItem.bind(this);
3786 this._selection = new Polymer.IronSelection(this._applySelection.bind(this)) ;
3787 },
3788 attached: function() {
3789 this._observer = this._observeItems(this);
3790 this._updateItems();
3791 if (!this._shouldUpdateSelection) {
3792 this._updateSelected();
3793 }
3794 this._addListener(this.activateEvent);
3795 },
3796 detached: function() {
3797 if (this._observer) {
3798 Polymer.dom(this).unobserveNodes(this._observer);
3799 }
3800 this._removeListener(this.activateEvent);
3801 },
3802 indexOf: function(item) {
3803 return this.items.indexOf(item);
3804 },
3805 select: function(value) {
3806 this.selected = value;
3807 },
3808 selectPrevious: function() {
3809 var length = this.items.length;
3810 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt h;
3811 this.selected = this._indexToValue(index);
3812 },
3813 selectNext: function() {
3814 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len gth;
3815 this.selected = this._indexToValue(index);
3816 },
3817 selectIndex: function(index) {
3818 this.select(this._indexToValue(index));
3819 },
3820 forceSynchronousItemUpdate: function() {
3821 this._updateItems();
3822 },
3823 get _shouldUpdateSelection() {
3824 return this.selected != null;
3825 },
3826 _checkFallback: function() {
3827 if (this._shouldUpdateSelection) {
3828 this._updateSelected();
3829 }
3830 },
3831 _addListener: function(eventName) {
3832 this.listen(this, eventName, '_activateHandler');
3833 },
3834 _removeListener: function(eventName) {
3835 this.unlisten(this, eventName, '_activateHandler');
3836 },
3837 _activateEventChanged: function(eventName, old) {
3838 this._removeListener(old);
3839 this._addListener(eventName);
3840 },
3841 _updateItems: function() {
3842 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '* ');
3843 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
3844 this._setItems(nodes);
3845 },
3846 _updateAttrForSelected: function() {
3847 if (this._shouldUpdateSelection) {
3848 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3849 }
3850 },
3851 _updateSelected: function() {
3852 this._selectSelected(this.selected);
3853 },
3854 _selectSelected: function(selected) {
3855 this._selection.select(this._valueToItem(this.selected));
3856 if (this.fallbackSelection && this.items.length && this._selection.get() === undefined) {
3857 this.selected = this.fallbackSelection;
3858 }
3859 },
3860 _filterItem: function(node) {
3861 return !this._excludedLocalNames[node.localName];
3862 },
3863 _valueToItem: function(value) {
3864 return value == null ? null : this.items[this._valueToIndex(value)];
3865 },
3866 _valueToIndex: function(value) {
3867 if (this.attrForSelected) {
3868 for (var i = 0, item; item = this.items[i]; i++) {
3869 if (this._valueForItem(item) == value) {
3870 return i;
6849 } 3871 }
6850 } 3872 }
6851 }, 3873 } else {
6852 3874 return Number(value);
6853 observers: [ 3875 }
6854 '_updateAttrForSelected(attrForSelected)', 3876 },
6855 '_updateSelected(selected)', 3877 _indexToValue: function(index) {
6856 '_checkFallback(fallbackSelection)' 3878 if (this.attrForSelected) {
6857 ], 3879 var item = this.items[index];
6858 3880 if (item) {
6859 created: function() { 3881 return this._valueForItem(item);
6860 this._bindFilterItem = this._filterItem.bind(this); 3882 }
6861 this._selection = new Polymer.IronSelection(this._applySelection.bind(this )); 3883 } else {
6862 }, 3884 return index;
6863 3885 }
6864 attached: function() { 3886 },
6865 this._observer = this._observeItems(this); 3887 _valueForItem: function(item) {
3888 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
3889 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe lected);
3890 },
3891 _applySelection: function(item, isSelected) {
3892 if (this.selectedClass) {
3893 this.toggleClass(this.selectedClass, isSelected, item);
3894 }
3895 if (this.selectedAttribute) {
3896 this.toggleAttribute(this.selectedAttribute, isSelected, item);
3897 }
3898 this._selectionChange();
3899 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {
3900 item: item
3901 });
3902 },
3903 _selectionChange: function() {
3904 this._setSelectedItem(this._selection.get());
3905 },
3906 _observeItems: function(node) {
3907 return Polymer.dom(node).observeNodes(function(mutation) {
6866 this._updateItems(); 3908 this._updateItems();
6867 if (!this._shouldUpdateSelection) {
6868 this._updateSelected();
6869 }
6870 this._addListener(this.activateEvent);
6871 },
6872
6873 detached: function() {
6874 if (this._observer) {
6875 Polymer.dom(this).unobserveNodes(this._observer);
6876 }
6877 this._removeListener(this.activateEvent);
6878 },
6879
6880 /**
6881 * Returns the index of the given item.
6882 *
6883 * @method indexOf
6884 * @param {Object} item
6885 * @returns Returns the index of the item
6886 */
6887 indexOf: function(item) {
6888 return this.items.indexOf(item);
6889 },
6890
6891 /**
6892 * Selects the given value.
6893 *
6894 * @method select
6895 * @param {string|number} value the value to select.
6896 */
6897 select: function(value) {
6898 this.selected = value;
6899 },
6900
6901 /**
6902 * Selects the previous item.
6903 *
6904 * @method selectPrevious
6905 */
6906 selectPrevious: function() {
6907 var length = this.items.length;
6908 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
6909 this.selected = this._indexToValue(index);
6910 },
6911
6912 /**
6913 * Selects the next item.
6914 *
6915 * @method selectNext
6916 */
6917 selectNext: function() {
6918 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
6919 this.selected = this._indexToValue(index);
6920 },
6921
6922 /**
6923 * Selects the item at the given index.
6924 *
6925 * @method selectIndex
6926 */
6927 selectIndex: function(index) {
6928 this.select(this._indexToValue(index));
6929 },
6930
6931 /**
6932 * Force a synchronous update of the `items` property.
6933 *
6934 * NOTE: Consider listening for the `iron-items-changed` event to respond to
6935 * updates to the set of selectable items after updates to the DOM list and
6936 * selection state have been made.
6937 *
6938 * WARNING: If you are using this method, you should probably consider an
6939 * alternate approach. Synchronously querying for items is potentially
6940 * slow for many use cases. The `items` property will update asynchronously
6941 * on its own to reflect selectable items in the DOM.
6942 */
6943 forceSynchronousItemUpdate: function() {
6944 this._updateItems();
6945 },
6946
6947 get _shouldUpdateSelection() {
6948 return this.selected != null;
6949 },
6950
6951 _checkFallback: function() {
6952 if (this._shouldUpdateSelection) { 3909 if (this._shouldUpdateSelection) {
6953 this._updateSelected(); 3910 this._updateSelected();
6954 } 3911 }
6955 }, 3912 this.fire('iron-items-changed', mutation, {
6956 3913 bubbles: false,
6957 _addListener: function(eventName) { 3914 cancelable: false
6958 this.listen(this, eventName, '_activateHandler'); 3915 });
6959 }, 3916 });
6960 3917 },
6961 _removeListener: function(eventName) { 3918 _activateHandler: function(e) {
6962 this.unlisten(this, eventName, '_activateHandler'); 3919 var t = e.target;
6963 }, 3920 var items = this.items;
6964 3921 while (t && t != this) {
6965 _activateEventChanged: function(eventName, old) { 3922 var i = items.indexOf(t);
6966 this._removeListener(old); 3923 if (i >= 0) {
6967 this._addListener(eventName); 3924 var value = this._indexToValue(i);
6968 }, 3925 this._itemActivate(value, t);
6969 3926 return;
6970 _updateItems: function() { 3927 }
6971 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*'); 3928 t = t.parentNode;
6972 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); 3929 }
6973 this._setItems(nodes); 3930 },
6974 }, 3931 _itemActivate: function(value, item) {
6975 3932 if (!this.fire('iron-activate', {
6976 _updateAttrForSelected: function() { 3933 selected: value,
6977 if (this._shouldUpdateSelection) { 3934 item: item
6978 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); 3935 }, {
6979 } 3936 cancelable: true
6980 }, 3937 }).defaultPrevented) {
6981 3938 this.select(value);
6982 _updateSelected: function() { 3939 }
3940 }
3941 };
3942
3943 Polymer.IronMultiSelectableBehaviorImpl = {
3944 properties: {
3945 multi: {
3946 type: Boolean,
3947 value: false,
3948 observer: 'multiChanged'
3949 },
3950 selectedValues: {
3951 type: Array,
3952 notify: true
3953 },
3954 selectedItems: {
3955 type: Array,
3956 readOnly: true,
3957 notify: true
3958 }
3959 },
3960 observers: [ '_updateSelected(selectedValues.splices)' ],
3961 select: function(value) {
3962 if (this.multi) {
3963 if (this.selectedValues) {
3964 this._toggleSelected(value);
3965 } else {
3966 this.selectedValues = [ value ];
3967 }
3968 } else {
3969 this.selected = value;
3970 }
3971 },
3972 multiChanged: function(multi) {
3973 this._selection.multi = multi;
3974 },
3975 get _shouldUpdateSelection() {
3976 return this.selected != null || this.selectedValues != null && this.selected Values.length;
3977 },
3978 _updateAttrForSelected: function() {
3979 if (!this.multi) {
3980 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
3981 } else if (this._shouldUpdateSelection) {
3982 this.selectedValues = this.selectedItems.map(function(selectedItem) {
3983 return this._indexToValue(this.indexOf(selectedItem));
3984 }, this).filter(function(unfilteredValue) {
3985 return unfilteredValue != null;
3986 }, this);
3987 }
3988 },
3989 _updateSelected: function() {
3990 if (this.multi) {
3991 this._selectMulti(this.selectedValues);
3992 } else {
6983 this._selectSelected(this.selected); 3993 this._selectSelected(this.selected);
6984 }, 3994 }
6985 3995 },
6986 _selectSelected: function(selected) { 3996 _selectMulti: function(values) {
6987 this._selection.select(this._valueToItem(this.selected)); 3997 if (values) {
6988 // Check for items, since this array is populated only when attached 3998 var selectedItems = this._valuesToItems(values);
6989 // Since Number(0) is falsy, explicitly check for undefined 3999 this._selection.clear(selectedItems);
6990 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) { 4000 for (var i = 0; i < selectedItems.length; i++) {
6991 this.selected = this.fallbackSelection; 4001 this._selection.setItemSelected(selectedItems[i], true);
6992 } 4002 }
6993 }, 4003 if (this.fallbackSelection && this.items.length && !this._selection.get(). length) {
6994 4004 var fallback = this._valueToItem(this.fallbackSelection);
6995 _filterItem: function(node) { 4005 if (fallback) {
6996 return !this._excludedLocalNames[node.localName]; 4006 this.selectedValues = [ this.fallbackSelection ];
6997 },
6998
6999 _valueToItem: function(value) {
7000 return (value == null) ? null : this.items[this._valueToIndex(value)];
7001 },
7002
7003 _valueToIndex: function(value) {
7004 if (this.attrForSelected) {
7005 for (var i = 0, item; item = this.items[i]; i++) {
7006 if (this._valueForItem(item) == value) {
7007 return i;
7008 }
7009 } 4007 }
4008 }
4009 } else {
4010 this._selection.clear();
4011 }
4012 },
4013 _selectionChange: function() {
4014 var s = this._selection.get();
4015 if (this.multi) {
4016 this._setSelectedItems(s);
4017 } else {
4018 this._setSelectedItems([ s ]);
4019 this._setSelectedItem(s);
4020 }
4021 },
4022 _toggleSelected: function(value) {
4023 var i = this.selectedValues.indexOf(value);
4024 var unselected = i < 0;
4025 if (unselected) {
4026 this.push('selectedValues', value);
4027 } else {
4028 this.splice('selectedValues', i, 1);
4029 }
4030 },
4031 _valuesToItems: function(values) {
4032 return values == null ? null : values.map(function(value) {
4033 return this._valueToItem(value);
4034 }, this);
4035 }
4036 };
4037
4038 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer. IronMultiSelectableBehaviorImpl ];
4039
4040 Polymer.IronMenuBehaviorImpl = {
4041 properties: {
4042 focusedItem: {
4043 observer: '_focusedItemChanged',
4044 readOnly: true,
4045 type: Object
4046 },
4047 attrForItemTitle: {
4048 type: String
4049 }
4050 },
4051 hostAttributes: {
4052 role: 'menu',
4053 tabindex: '0'
4054 },
4055 observers: [ '_updateMultiselectable(multi)' ],
4056 listeners: {
4057 focus: '_onFocus',
4058 keydown: '_onKeydown',
4059 'iron-items-changed': '_onIronItemsChanged'
4060 },
4061 keyBindings: {
4062 up: '_onUpKey',
4063 down: '_onDownKey',
4064 esc: '_onEscKey',
4065 'shift+tab:keydown': '_onShiftTabDown'
4066 },
4067 attached: function() {
4068 this._resetTabindices();
4069 },
4070 select: function(value) {
4071 if (this._defaultFocusAsync) {
4072 this.cancelAsync(this._defaultFocusAsync);
4073 this._defaultFocusAsync = null;
4074 }
4075 var item = this._valueToItem(value);
4076 if (item && item.hasAttribute('disabled')) return;
4077 this._setFocusedItem(item);
4078 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
4079 },
4080 _resetTabindices: function() {
4081 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0] : this.selectedItem;
4082 this.items.forEach(function(item) {
4083 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
4084 }, this);
4085 },
4086 _updateMultiselectable: function(multi) {
4087 if (multi) {
4088 this.setAttribute('aria-multiselectable', 'true');
4089 } else {
4090 this.removeAttribute('aria-multiselectable');
4091 }
4092 },
4093 _focusWithKeyboardEvent: function(event) {
4094 for (var i = 0, item; item = this.items[i]; i++) {
4095 var attr = this.attrForItemTitle || 'textContent';
4096 var title = item[attr] || item.getAttribute(attr);
4097 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo werCase() === String.fromCharCode(event.keyCode).toLowerCase()) {
4098 this._setFocusedItem(item);
4099 break;
4100 }
4101 }
4102 },
4103 _focusPrevious: function() {
4104 var length = this.items.length;
4105 var curFocusIndex = Number(this.indexOf(this.focusedItem));
4106 for (var i = 1; i < length + 1; i++) {
4107 var item = this.items[(curFocusIndex - i + length) % length];
4108 if (!item.hasAttribute('disabled')) {
4109 this._setFocusedItem(item);
4110 return;
4111 }
4112 }
4113 },
4114 _focusNext: function() {
4115 var length = this.items.length;
4116 var curFocusIndex = Number(this.indexOf(this.focusedItem));
4117 for (var i = 1; i < length + 1; i++) {
4118 var item = this.items[(curFocusIndex + i) % length];
4119 if (!item.hasAttribute('disabled')) {
4120 this._setFocusedItem(item);
4121 return;
4122 }
4123 }
4124 },
4125 _applySelection: function(item, isSelected) {
4126 if (isSelected) {
4127 item.setAttribute('aria-selected', 'true');
4128 } else {
4129 item.removeAttribute('aria-selected');
4130 }
4131 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
4132 },
4133 _focusedItemChanged: function(focusedItem, old) {
4134 old && old.setAttribute('tabindex', '-1');
4135 if (focusedItem) {
4136 focusedItem.setAttribute('tabindex', '0');
4137 focusedItem.focus();
4138 }
4139 },
4140 _onIronItemsChanged: function(event) {
4141 if (event.detail.addedNodes.length) {
4142 this._resetTabindices();
4143 }
4144 },
4145 _onShiftTabDown: function(event) {
4146 var oldTabIndex = this.getAttribute('tabindex');
4147 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
4148 this._setFocusedItem(null);
4149 this.setAttribute('tabindex', '-1');
4150 this.async(function() {
4151 this.setAttribute('tabindex', oldTabIndex);
4152 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
4153 }, 1);
4154 },
4155 _onFocus: function(event) {
4156 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
4157 return;
4158 }
4159 var rootTarget = Polymer.dom(event).rootTarget;
4160 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th is.isLightDescendant(rootTarget)) {
4161 return;
4162 }
4163 this._defaultFocusAsync = this.async(function() {
4164 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0 ] : this.selectedItem;
4165 this._setFocusedItem(null);
4166 if (selectedItem) {
4167 this._setFocusedItem(selectedItem);
4168 } else if (this.items[0]) {
4169 this._focusNext();
4170 }
4171 });
4172 },
4173 _onUpKey: function(event) {
4174 this._focusPrevious();
4175 event.detail.keyboardEvent.preventDefault();
4176 },
4177 _onDownKey: function(event) {
4178 this._focusNext();
4179 event.detail.keyboardEvent.preventDefault();
4180 },
4181 _onEscKey: function(event) {
4182 this.focusedItem.blur();
4183 },
4184 _onKeydown: function(event) {
4185 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
4186 this._focusWithKeyboardEvent(event);
4187 }
4188 event.stopPropagation();
4189 },
4190 _activateHandler: function(event) {
4191 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
4192 event.stopPropagation();
4193 }
4194 };
4195
4196 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
4197
4198 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1 1yKeysBehavior, Polymer.IronMenuBehaviorImpl ];
4199
4200 (function() {
4201 Polymer({
4202 is: 'paper-menu',
4203 behaviors: [ Polymer.IronMenuBehavior ]
4204 });
4205 })();
4206
4207 Polymer.IronFitBehavior = {
4208 properties: {
4209 sizingTarget: {
4210 type: Object,
4211 value: function() {
4212 return this;
4213 }
4214 },
4215 fitInto: {
4216 type: Object,
4217 value: window
4218 },
4219 noOverlap: {
4220 type: Boolean
4221 },
4222 positionTarget: {
4223 type: Element
4224 },
4225 horizontalAlign: {
4226 type: String
4227 },
4228 verticalAlign: {
4229 type: String
4230 },
4231 dynamicAlign: {
4232 type: Boolean
4233 },
4234 horizontalOffset: {
4235 type: Number,
4236 value: 0,
4237 notify: true
4238 },
4239 verticalOffset: {
4240 type: Number,
4241 value: 0,
4242 notify: true
4243 },
4244 autoFitOnAttach: {
4245 type: Boolean,
4246 value: false
4247 },
4248 _fitInfo: {
4249 type: Object
4250 }
4251 },
4252 get _fitWidth() {
4253 var fitWidth;
4254 if (this.fitInto === window) {
4255 fitWidth = this.fitInto.innerWidth;
4256 } else {
4257 fitWidth = this.fitInto.getBoundingClientRect().width;
4258 }
4259 return fitWidth;
4260 },
4261 get _fitHeight() {
4262 var fitHeight;
4263 if (this.fitInto === window) {
4264 fitHeight = this.fitInto.innerHeight;
4265 } else {
4266 fitHeight = this.fitInto.getBoundingClientRect().height;
4267 }
4268 return fitHeight;
4269 },
4270 get _fitLeft() {
4271 var fitLeft;
4272 if (this.fitInto === window) {
4273 fitLeft = 0;
4274 } else {
4275 fitLeft = this.fitInto.getBoundingClientRect().left;
4276 }
4277 return fitLeft;
4278 },
4279 get _fitTop() {
4280 var fitTop;
4281 if (this.fitInto === window) {
4282 fitTop = 0;
4283 } else {
4284 fitTop = this.fitInto.getBoundingClientRect().top;
4285 }
4286 return fitTop;
4287 },
4288 get _defaultPositionTarget() {
4289 var parent = Polymer.dom(this).parentNode;
4290 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
4291 parent = parent.host;
4292 }
4293 return parent;
4294 },
4295 get _localeHorizontalAlign() {
4296 if (this._isRTL) {
4297 if (this.horizontalAlign === 'right') {
4298 return 'left';
4299 }
4300 if (this.horizontalAlign === 'left') {
4301 return 'right';
4302 }
4303 }
4304 return this.horizontalAlign;
4305 },
4306 attached: function() {
4307 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
4308 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
4309 if (this.autoFitOnAttach) {
4310 if (window.getComputedStyle(this).display === 'none') {
4311 setTimeout(function() {
4312 this.fit();
4313 }.bind(this));
7010 } else { 4314 } else {
7011 return Number(value); 4315 this.fit();
7012 } 4316 }
7013 }, 4317 }
7014 4318 },
7015 _indexToValue: function(index) { 4319 fit: function() {
7016 if (this.attrForSelected) { 4320 this.position();
7017 var item = this.items[index]; 4321 this.constrain();
7018 if (item) { 4322 this.center();
7019 return this._valueForItem(item); 4323 },
4324 _discoverInfo: function() {
4325 if (this._fitInfo) {
4326 return;
4327 }
4328 var target = window.getComputedStyle(this);
4329 var sizer = window.getComputedStyle(this.sizingTarget);
4330 this._fitInfo = {
4331 inlineStyle: {
4332 top: this.style.top || '',
4333 left: this.style.left || '',
4334 position: this.style.position || ''
4335 },
4336 sizerInlineStyle: {
4337 maxWidth: this.sizingTarget.style.maxWidth || '',
4338 maxHeight: this.sizingTarget.style.maxHeight || '',
4339 boxSizing: this.sizingTarget.style.boxSizing || ''
4340 },
4341 positionedBy: {
4342 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? ' bottom' : null,
4343 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto' ? 'right' : null
4344 },
4345 sizedBy: {
4346 height: sizer.maxHeight !== 'none',
4347 width: sizer.maxWidth !== 'none',
4348 minWidth: parseInt(sizer.minWidth, 10) || 0,
4349 minHeight: parseInt(sizer.minHeight, 10) || 0
4350 },
4351 margin: {
4352 top: parseInt(target.marginTop, 10) || 0,
4353 right: parseInt(target.marginRight, 10) || 0,
4354 bottom: parseInt(target.marginBottom, 10) || 0,
4355 left: parseInt(target.marginLeft, 10) || 0
4356 }
4357 };
4358 if (this.verticalOffset) {
4359 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs et;
4360 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
4361 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
4362 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px ';
4363 }
4364 if (this.horizontalOffset) {
4365 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf fset;
4366 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
4367 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
4368 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + ' px';
4369 }
4370 },
4371 resetFit: function() {
4372 var info = this._fitInfo || {};
4373 for (var property in info.sizerInlineStyle) {
4374 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
4375 }
4376 for (var property in info.inlineStyle) {
4377 this.style[property] = info.inlineStyle[property];
4378 }
4379 this._fitInfo = null;
4380 },
4381 refit: function() {
4382 var scrollLeft = this.sizingTarget.scrollLeft;
4383 var scrollTop = this.sizingTarget.scrollTop;
4384 this.resetFit();
4385 this.fit();
4386 this.sizingTarget.scrollLeft = scrollLeft;
4387 this.sizingTarget.scrollTop = scrollTop;
4388 },
4389 position: function() {
4390 if (!this.horizontalAlign && !this.verticalAlign) {
4391 return;
4392 }
4393 this._discoverInfo();
4394 this.style.position = 'fixed';
4395 this.sizingTarget.style.boxSizing = 'border-box';
4396 this.style.left = '0px';
4397 this.style.top = '0px';
4398 var rect = this.getBoundingClientRect();
4399 var positionRect = this.__getNormalizedRect(this.positionTarget);
4400 var fitRect = this.__getNormalizedRect(this.fitInto);
4401 var margin = this._fitInfo.margin;
4402 var size = {
4403 width: rect.width + margin.left + margin.right,
4404 height: rect.height + margin.top + margin.bottom
4405 };
4406 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical Align, size, positionRect, fitRect);
4407 var left = position.left + margin.left;
4408 var top = position.top + margin.top;
4409 var right = Math.min(fitRect.right - margin.right, left + rect.width);
4410 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
4411 var minWidth = this._fitInfo.sizedBy.minWidth;
4412 var minHeight = this._fitInfo.sizedBy.minHeight;
4413 if (left < margin.left) {
4414 left = margin.left;
4415 if (right - left < minWidth) {
4416 left = right - minWidth;
4417 }
4418 }
4419 if (top < margin.top) {
4420 top = margin.top;
4421 if (bottom - top < minHeight) {
4422 top = bottom - minHeight;
4423 }
4424 }
4425 this.sizingTarget.style.maxWidth = right - left + 'px';
4426 this.sizingTarget.style.maxHeight = bottom - top + 'px';
4427 this.style.left = left - rect.left + 'px';
4428 this.style.top = top - rect.top + 'px';
4429 },
4430 constrain: function() {
4431 if (this.horizontalAlign || this.verticalAlign) {
4432 return;
4433 }
4434 this._discoverInfo();
4435 var info = this._fitInfo;
4436 if (!info.positionedBy.vertically) {
4437 this.style.position = 'fixed';
4438 this.style.top = '0px';
4439 }
4440 if (!info.positionedBy.horizontally) {
4441 this.style.position = 'fixed';
4442 this.style.left = '0px';
4443 }
4444 this.sizingTarget.style.boxSizing = 'border-box';
4445 var rect = this.getBoundingClientRect();
4446 if (!info.sizedBy.height) {
4447 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
4448 }
4449 if (!info.sizedBy.width) {
4450 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right' , 'Width');
4451 }
4452 },
4453 _sizeDimension: function(rect, positionedBy, start, end, extent) {
4454 this.__sizeDimension(rect, positionedBy, start, end, extent);
4455 },
4456 __sizeDimension: function(rect, positionedBy, start, end, extent) {
4457 var info = this._fitInfo;
4458 var fitRect = this.__getNormalizedRect(this.fitInto);
4459 var max = extent === 'Width' ? fitRect.width : fitRect.height;
4460 var flip = positionedBy === end;
4461 var offset = flip ? max - rect[end] : rect[start];
4462 var margin = info.margin[flip ? start : end];
4463 var offsetExtent = 'offset' + extent;
4464 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
4465 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs et + 'px';
4466 },
4467 center: function() {
4468 if (this.horizontalAlign || this.verticalAlign) {
4469 return;
4470 }
4471 this._discoverInfo();
4472 var positionedBy = this._fitInfo.positionedBy;
4473 if (positionedBy.vertically && positionedBy.horizontally) {
4474 return;
4475 }
4476 this.style.position = 'fixed';
4477 if (!positionedBy.vertically) {
4478 this.style.top = '0px';
4479 }
4480 if (!positionedBy.horizontally) {
4481 this.style.left = '0px';
4482 }
4483 var rect = this.getBoundingClientRect();
4484 var fitRect = this.__getNormalizedRect(this.fitInto);
4485 if (!positionedBy.vertically) {
4486 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
4487 this.style.top = top + 'px';
4488 }
4489 if (!positionedBy.horizontally) {
4490 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
4491 this.style.left = left + 'px';
4492 }
4493 },
4494 __getNormalizedRect: function(target) {
4495 if (target === document.documentElement || target === window) {
4496 return {
4497 top: 0,
4498 left: 0,
4499 width: window.innerWidth,
4500 height: window.innerHeight,
4501 right: window.innerWidth,
4502 bottom: window.innerHeight
4503 };
4504 }
4505 return target.getBoundingClientRect();
4506 },
4507 __getCroppedArea: function(position, size, fitRect) {
4508 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
4509 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
4510 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size .height;
4511 },
4512 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
4513 var positions = [ {
4514 verticalAlign: 'top',
4515 horizontalAlign: 'left',
4516 top: positionRect.top,
4517 left: positionRect.left
4518 }, {
4519 verticalAlign: 'top',
4520 horizontalAlign: 'right',
4521 top: positionRect.top,
4522 left: positionRect.right - size.width
4523 }, {
4524 verticalAlign: 'bottom',
4525 horizontalAlign: 'left',
4526 top: positionRect.bottom - size.height,
4527 left: positionRect.left
4528 }, {
4529 verticalAlign: 'bottom',
4530 horizontalAlign: 'right',
4531 top: positionRect.bottom - size.height,
4532 left: positionRect.right - size.width
4533 } ];
4534 if (this.noOverlap) {
4535 for (var i = 0, l = positions.length; i < l; i++) {
4536 var copy = {};
4537 for (var key in positions[i]) {
4538 copy[key] = positions[i][key];
7020 } 4539 }
7021 } else { 4540 positions.push(copy);
7022 return index; 4541 }
7023 } 4542 positions[0].top = positions[1].top += positionRect.height;
7024 }, 4543 positions[2].top = positions[3].top -= positionRect.height;
7025 4544 positions[4].left = positions[6].left += positionRect.width;
7026 _valueForItem: function(item) { 4545 positions[5].left = positions[7].left -= positionRect.width;
7027 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ]; 4546 }
7028 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected); 4547 vAlign = vAlign === 'auto' ? null : vAlign;
7029 }, 4548 hAlign = hAlign === 'auto' ? null : hAlign;
7030 4549 var position;
7031 _applySelection: function(item, isSelected) { 4550 for (var i = 0; i < positions.length; i++) {
7032 if (this.selectedClass) { 4551 var pos = positions[i];
7033 this.toggleClass(this.selectedClass, isSelected, item); 4552 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
7034 } 4553 position = pos;
7035 if (this.selectedAttribute) { 4554 break;
7036 this.toggleAttribute(this.selectedAttribute, isSelected, item); 4555 }
7037 } 4556 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos .horizontalAlign === hAlign);
7038 this._selectionChange(); 4557 if (!this.dynamicAlign && !alignOk) {
7039 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); 4558 continue;
7040 }, 4559 }
7041 4560 position = position || pos;
7042 _selectionChange: function() { 4561 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
7043 this._setSelectedItem(this._selection.get()); 4562 var diff = pos.croppedArea - position.croppedArea;
7044 }, 4563 if (diff < 0 || diff === 0 && alignOk) {
7045 4564 position = pos;
7046 // observe items change under the given node. 4565 }
7047 _observeItems: function(node) { 4566 if (position.croppedArea === 0 && alignOk) {
7048 return Polymer.dom(node).observeNodes(function(mutation) { 4567 break;
7049 this._updateItems(); 4568 }
7050 4569 }
7051 if (this._shouldUpdateSelection) { 4570 return position;
7052 this._updateSelected(); 4571 }
7053 } 4572 };
7054 4573
7055 // Let other interested parties know about the change so that 4574 (function() {
7056 // we don't have to recreate mutation observers everywhere. 4575 'use strict';
7057 this.fire('iron-items-changed', mutation, { 4576 Polymer({
7058 bubbles: false, 4577 is: 'iron-overlay-backdrop',
7059 cancelable: false
7060 });
7061 });
7062 },
7063
7064 _activateHandler: function(e) {
7065 var t = e.target;
7066 var items = this.items;
7067 while (t && t != this) {
7068 var i = items.indexOf(t);
7069 if (i >= 0) {
7070 var value = this._indexToValue(i);
7071 this._itemActivate(value, t);
7072 return;
7073 }
7074 t = t.parentNode;
7075 }
7076 },
7077
7078 _itemActivate: function(value, item) {
7079 if (!this.fire('iron-activate',
7080 {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
7081 this.select(value);
7082 }
7083 }
7084
7085 };
7086 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
7087 Polymer.IronMultiSelectableBehaviorImpl = {
7088 properties: { 4578 properties: {
7089
7090 /**
7091 * If true, multiple selections are allowed.
7092 */
7093 multi: {
7094 type: Boolean,
7095 value: false,
7096 observer: 'multiChanged'
7097 },
7098
7099 /**
7100 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
7101 * is true.
7102 */
7103 selectedValues: {
7104 type: Array,
7105 notify: true
7106 },
7107
7108 /**
7109 * Returns an array of currently selected items.
7110 */
7111 selectedItems: {
7112 type: Array,
7113 readOnly: true,
7114 notify: true
7115 },
7116
7117 },
7118
7119 observers: [
7120 '_updateSelected(selectedValues.splices)'
7121 ],
7122
7123 /**
7124 * Selects the given value. If the `multi` property is true, then the select ed state of the
7125 * `value` will be toggled; otherwise the `value` will be selected.
7126 *
7127 * @method select
7128 * @param {string|number} value the value to select.
7129 */
7130 select: function(value) {
7131 if (this.multi) {
7132 if (this.selectedValues) {
7133 this._toggleSelected(value);
7134 } else {
7135 this.selectedValues = [value];
7136 }
7137 } else {
7138 this.selected = value;
7139 }
7140 },
7141
7142 multiChanged: function(multi) {
7143 this._selection.multi = multi;
7144 },
7145
7146 get _shouldUpdateSelection() {
7147 return this.selected != null ||
7148 (this.selectedValues != null && this.selectedValues.length);
7149 },
7150
7151 _updateAttrForSelected: function() {
7152 if (!this.multi) {
7153 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
7154 } else if (this._shouldUpdateSelection) {
7155 this.selectedValues = this.selectedItems.map(function(selectedItem) {
7156 return this._indexToValue(this.indexOf(selectedItem));
7157 }, this).filter(function(unfilteredValue) {
7158 return unfilteredValue != null;
7159 }, this);
7160 }
7161 },
7162
7163 _updateSelected: function() {
7164 if (this.multi) {
7165 this._selectMulti(this.selectedValues);
7166 } else {
7167 this._selectSelected(this.selected);
7168 }
7169 },
7170
7171 _selectMulti: function(values) {
7172 if (values) {
7173 var selectedItems = this._valuesToItems(values);
7174 // clear all but the current selected items
7175 this._selection.clear(selectedItems);
7176 // select only those not selected yet
7177 for (var i = 0; i < selectedItems.length; i++) {
7178 this._selection.setItemSelected(selectedItems[i], true);
7179 }
7180 // Check for items, since this array is populated only when attached
7181 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
7182 var fallback = this._valueToItem(this.fallbackSelection);
7183 if (fallback) {
7184 this.selectedValues = [this.fallbackSelection];
7185 }
7186 }
7187 } else {
7188 this._selection.clear();
7189 }
7190 },
7191
7192 _selectionChange: function() {
7193 var s = this._selection.get();
7194 if (this.multi) {
7195 this._setSelectedItems(s);
7196 } else {
7197 this._setSelectedItems([s]);
7198 this._setSelectedItem(s);
7199 }
7200 },
7201
7202 _toggleSelected: function(value) {
7203 var i = this.selectedValues.indexOf(value);
7204 var unselected = i < 0;
7205 if (unselected) {
7206 this.push('selectedValues',value);
7207 } else {
7208 this.splice('selectedValues',i,1);
7209 }
7210 },
7211
7212 _valuesToItems: function(values) {
7213 return (values == null) ? null : values.map(function(value) {
7214 return this._valueToItem(value);
7215 }, this);
7216 }
7217 };
7218
7219 /** @polymerBehavior */
7220 Polymer.IronMultiSelectableBehavior = [
7221 Polymer.IronSelectableBehavior,
7222 Polymer.IronMultiSelectableBehaviorImpl
7223 ];
7224 /**
7225 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
7226 *
7227 * @demo demo/index.html
7228 * @polymerBehavior Polymer.IronMenuBehavior
7229 */
7230 Polymer.IronMenuBehaviorImpl = {
7231
7232 properties: {
7233
7234 /**
7235 * Returns the currently focused item.
7236 * @type {?Object}
7237 */
7238 focusedItem: {
7239 observer: '_focusedItemChanged',
7240 readOnly: true,
7241 type: Object
7242 },
7243
7244 /**
7245 * The attribute to use on menu items to look up the item title. Typing th e first
7246 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
7247 * will be used.
7248 */
7249 attrForItemTitle: {
7250 type: String
7251 }
7252 },
7253
7254 hostAttributes: {
7255 'role': 'menu',
7256 'tabindex': '0'
7257 },
7258
7259 observers: [
7260 '_updateMultiselectable(multi)'
7261 ],
7262
7263 listeners: {
7264 'focus': '_onFocus',
7265 'keydown': '_onKeydown',
7266 'iron-items-changed': '_onIronItemsChanged'
7267 },
7268
7269 keyBindings: {
7270 'up': '_onUpKey',
7271 'down': '_onDownKey',
7272 'esc': '_onEscKey',
7273 'shift+tab:keydown': '_onShiftTabDown'
7274 },
7275
7276 attached: function() {
7277 this._resetTabindices();
7278 },
7279
7280 /**
7281 * Selects the given value. If the `multi` property is true, then the select ed state of the
7282 * `value` will be toggled; otherwise the `value` will be selected.
7283 *
7284 * @param {string|number} value the value to select.
7285 */
7286 select: function(value) {
7287 // Cancel automatically focusing a default item if the menu received focus
7288 // through a user action selecting a particular item.
7289 if (this._defaultFocusAsync) {
7290 this.cancelAsync(this._defaultFocusAsync);
7291 this._defaultFocusAsync = null;
7292 }
7293 var item = this._valueToItem(value);
7294 if (item && item.hasAttribute('disabled')) return;
7295 this._setFocusedItem(item);
7296 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
7297 },
7298
7299 /**
7300 * Resets all tabindex attributes to the appropriate value based on the
7301 * current selection state. The appropriate value is `0` (focusable) for
7302 * the default selected item, and `-1` (not keyboard focusable) for all
7303 * other items.
7304 */
7305 _resetTabindices: function() {
7306 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem;
7307
7308 this.items.forEach(function(item) {
7309 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
7310 }, this);
7311 },
7312
7313 /**
7314 * Sets appropriate ARIA based on whether or not the menu is meant to be
7315 * multi-selectable.
7316 *
7317 * @param {boolean} multi True if the menu should be multi-selectable.
7318 */
7319 _updateMultiselectable: function(multi) {
7320 if (multi) {
7321 this.setAttribute('aria-multiselectable', 'true');
7322 } else {
7323 this.removeAttribute('aria-multiselectable');
7324 }
7325 },
7326
7327 /**
7328 * Given a KeyboardEvent, this method will focus the appropriate item in the
7329 * menu (if there is a relevant item, and it is possible to focus it).
7330 *
7331 * @param {KeyboardEvent} event A KeyboardEvent.
7332 */
7333 _focusWithKeyboardEvent: function(event) {
7334 for (var i = 0, item; item = this.items[i]; i++) {
7335 var attr = this.attrForItemTitle || 'textContent';
7336 var title = item[attr] || item.getAttribute(attr);
7337
7338 if (!item.hasAttribute('disabled') && title &&
7339 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) {
7340 this._setFocusedItem(item);
7341 break;
7342 }
7343 }
7344 },
7345
7346 /**
7347 * Focuses the previous item (relative to the currently focused item) in the
7348 * menu, disabled items will be skipped.
7349 * Loop until length + 1 to handle case of single item in menu.
7350 */
7351 _focusPrevious: function() {
7352 var length = this.items.length;
7353 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7354 for (var i = 1; i < length + 1; i++) {
7355 var item = this.items[(curFocusIndex - i + length) % length];
7356 if (!item.hasAttribute('disabled')) {
7357 this._setFocusedItem(item);
7358 return;
7359 }
7360 }
7361 },
7362
7363 /**
7364 * Focuses the next item (relative to the currently focused item) in the
7365 * menu, disabled items will be skipped.
7366 * Loop until length + 1 to handle case of single item in menu.
7367 */
7368 _focusNext: function() {
7369 var length = this.items.length;
7370 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7371 for (var i = 1; i < length + 1; i++) {
7372 var item = this.items[(curFocusIndex + i) % length];
7373 if (!item.hasAttribute('disabled')) {
7374 this._setFocusedItem(item);
7375 return;
7376 }
7377 }
7378 },
7379
7380 /**
7381 * Mutates items in the menu based on provided selection details, so that
7382 * all items correctly reflect selection state.
7383 *
7384 * @param {Element} item An item in the menu.
7385 * @param {boolean} isSelected True if the item should be shown in a
7386 * selected state, otherwise false.
7387 */
7388 _applySelection: function(item, isSelected) {
7389 if (isSelected) {
7390 item.setAttribute('aria-selected', 'true');
7391 } else {
7392 item.removeAttribute('aria-selected');
7393 }
7394 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
7395 },
7396
7397 /**
7398 * Discretely updates tabindex values among menu items as the focused item
7399 * changes.
7400 *
7401 * @param {Element} focusedItem The element that is currently focused.
7402 * @param {?Element} old The last element that was considered focused, if
7403 * applicable.
7404 */
7405 _focusedItemChanged: function(focusedItem, old) {
7406 old && old.setAttribute('tabindex', '-1');
7407 if (focusedItem) {
7408 focusedItem.setAttribute('tabindex', '0');
7409 focusedItem.focus();
7410 }
7411 },
7412
7413 /**
7414 * A handler that responds to mutation changes related to the list of items
7415 * in the menu.
7416 *
7417 * @param {CustomEvent} event An event containing mutation records as its
7418 * detail.
7419 */
7420 _onIronItemsChanged: function(event) {
7421 if (event.detail.addedNodes.length) {
7422 this._resetTabindices();
7423 }
7424 },
7425
7426 /**
7427 * Handler that is called when a shift+tab keypress is detected by the menu.
7428 *
7429 * @param {CustomEvent} event A key combination event.
7430 */
7431 _onShiftTabDown: function(event) {
7432 var oldTabIndex = this.getAttribute('tabindex');
7433
7434 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
7435
7436 this._setFocusedItem(null);
7437
7438 this.setAttribute('tabindex', '-1');
7439
7440 this.async(function() {
7441 this.setAttribute('tabindex', oldTabIndex);
7442 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7443 // NOTE(cdata): polymer/polymer#1305
7444 }, 1);
7445 },
7446
7447 /**
7448 * Handler that is called when the menu receives focus.
7449 *
7450 * @param {FocusEvent} event A focus event.
7451 */
7452 _onFocus: function(event) {
7453 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
7454 // do not focus the menu itself
7455 return;
7456 }
7457
7458 // Do not focus the selected tab if the deepest target is part of the
7459 // menu element's local DOM and is focusable.
7460 var rootTarget = /** @type {?HTMLElement} */(
7461 Polymer.dom(event).rootTarget);
7462 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
7463 return;
7464 }
7465
7466 // clear the cached focus item
7467 this._defaultFocusAsync = this.async(function() {
7468 // focus the selected item when the menu receives focus, or the first it em
7469 // if no item is selected
7470 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
7471
7472 this._setFocusedItem(null);
7473
7474 if (selectedItem) {
7475 this._setFocusedItem(selectedItem);
7476 } else if (this.items[0]) {
7477 // We find the first none-disabled item (if one exists)
7478 this._focusNext();
7479 }
7480 });
7481 },
7482
7483 /**
7484 * Handler that is called when the up key is pressed.
7485 *
7486 * @param {CustomEvent} event A key combination event.
7487 */
7488 _onUpKey: function(event) {
7489 // up and down arrows moves the focus
7490 this._focusPrevious();
7491 event.detail.keyboardEvent.preventDefault();
7492 },
7493
7494 /**
7495 * Handler that is called when the down key is pressed.
7496 *
7497 * @param {CustomEvent} event A key combination event.
7498 */
7499 _onDownKey: function(event) {
7500 this._focusNext();
7501 event.detail.keyboardEvent.preventDefault();
7502 },
7503
7504 /**
7505 * Handler that is called when the esc key is pressed.
7506 *
7507 * @param {CustomEvent} event A key combination event.
7508 */
7509 _onEscKey: function(event) {
7510 // esc blurs the control
7511 this.focusedItem.blur();
7512 },
7513
7514 /**
7515 * Handler that is called when a keydown event is detected.
7516 *
7517 * @param {KeyboardEvent} event A keyboard event.
7518 */
7519 _onKeydown: function(event) {
7520 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
7521 // all other keys focus the menu item starting with that character
7522 this._focusWithKeyboardEvent(event);
7523 }
7524 event.stopPropagation();
7525 },
7526
7527 // override _activateHandler
7528 _activateHandler: function(event) {
7529 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
7530 event.stopPropagation();
7531 }
7532 };
7533
7534 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7535
7536 /** @polymerBehavior Polymer.IronMenuBehavior */
7537 Polymer.IronMenuBehavior = [
7538 Polymer.IronMultiSelectableBehavior,
7539 Polymer.IronA11yKeysBehavior,
7540 Polymer.IronMenuBehaviorImpl
7541 ];
7542 (function() {
7543 Polymer({
7544 is: 'paper-menu',
7545
7546 behaviors: [
7547 Polymer.IronMenuBehavior
7548 ]
7549 });
7550 })();
7551 /**
7552 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
7553 optionally centers it in the window or another element.
7554
7555 The element will only be sized and/or positioned if it has not already been size d and/or positioned
7556 by CSS.
7557
7558 CSS properties | Action
7559 -----------------------------|-------------------------------------------
7560 `position` set | Element is not centered horizontally or verticall y
7561 `top` or `bottom` set | Element is not vertically centered
7562 `left` or `right` set | Element is not horizontally centered
7563 `max-height` set | Element respects `max-height`
7564 `max-width` set | Element respects `max-width`
7565
7566 `Polymer.IronFitBehavior` can position an element into another element using
7567 `verticalAlign` and `horizontalAlign`. This will override the element's css posi tion.
7568
7569 <div class="container">
7570 <iron-fit-impl vertical-align="top" horizontal-align="auto">
7571 Positioned into the container
7572 </iron-fit-impl>
7573 </div>
7574
7575 Use `noOverlap` to position the element around another element without overlappi ng it.
7576
7577 <div class="container">
7578 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
7579 Positioned around the container
7580 </iron-fit-impl>
7581 </div>
7582
7583 @demo demo/index.html
7584 @polymerBehavior
7585 */
7586
7587 Polymer.IronFitBehavior = {
7588
7589 properties: {
7590
7591 /**
7592 * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
7593 * but it can be set to a child element. This is useful, for example, for implementing a
7594 * scrolling region inside the element.
7595 * @type {!Element}
7596 */
7597 sizingTarget: {
7598 type: Object,
7599 value: function() {
7600 return this;
7601 }
7602 },
7603
7604 /**
7605 * The element to fit `this` into.
7606 */
7607 fitInto: {
7608 type: Object,
7609 value: window
7610 },
7611
7612 /**
7613 * Will position the element around the positionTarget without overlapping it.
7614 */
7615 noOverlap: {
7616 type: Boolean
7617 },
7618
7619 /**
7620 * The element that should be used to position the element. If not set, it will
7621 * default to the parent node.
7622 * @type {!Element}
7623 */
7624 positionTarget: {
7625 type: Element
7626 },
7627
7628 /**
7629 * The orientation against which to align the element horizontally
7630 * relative to the `positionTarget`. Possible values are "left", "right", "auto".
7631 */
7632 horizontalAlign: {
7633 type: String
7634 },
7635
7636 /**
7637 * The orientation against which to align the element vertically
7638 * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
7639 */
7640 verticalAlign: {
7641 type: String
7642 },
7643
7644 /**
7645 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr eferred alignment
7646 * and if there's not enough space, it will pick the values which minimize the cropping.
7647 */
7648 dynamicAlign: {
7649 type: Boolean
7650 },
7651
7652 /**
7653 * The same as setting margin-left and margin-right css properties.
7654 * @deprecated
7655 */
7656 horizontalOffset: {
7657 type: Number,
7658 value: 0,
7659 notify: true
7660 },
7661
7662 /**
7663 * The same as setting margin-top and margin-bottom css properties.
7664 * @deprecated
7665 */
7666 verticalOffset: {
7667 type: Number,
7668 value: 0,
7669 notify: true
7670 },
7671
7672 /**
7673 * Set to true to auto-fit on attach.
7674 */
7675 autoFitOnAttach: {
7676 type: Boolean,
7677 value: false
7678 },
7679
7680 /** @type {?Object} */
7681 _fitInfo: {
7682 type: Object
7683 }
7684 },
7685
7686 get _fitWidth() {
7687 var fitWidth;
7688 if (this.fitInto === window) {
7689 fitWidth = this.fitInto.innerWidth;
7690 } else {
7691 fitWidth = this.fitInto.getBoundingClientRect().width;
7692 }
7693 return fitWidth;
7694 },
7695
7696 get _fitHeight() {
7697 var fitHeight;
7698 if (this.fitInto === window) {
7699 fitHeight = this.fitInto.innerHeight;
7700 } else {
7701 fitHeight = this.fitInto.getBoundingClientRect().height;
7702 }
7703 return fitHeight;
7704 },
7705
7706 get _fitLeft() {
7707 var fitLeft;
7708 if (this.fitInto === window) {
7709 fitLeft = 0;
7710 } else {
7711 fitLeft = this.fitInto.getBoundingClientRect().left;
7712 }
7713 return fitLeft;
7714 },
7715
7716 get _fitTop() {
7717 var fitTop;
7718 if (this.fitInto === window) {
7719 fitTop = 0;
7720 } else {
7721 fitTop = this.fitInto.getBoundingClientRect().top;
7722 }
7723 return fitTop;
7724 },
7725
7726 /**
7727 * The element that should be used to position the element,
7728 * if no position target is configured.
7729 */
7730 get _defaultPositionTarget() {
7731 var parent = Polymer.dom(this).parentNode;
7732
7733 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
7734 parent = parent.host;
7735 }
7736
7737 return parent;
7738 },
7739
7740 /**
7741 * The horizontal align value, accounting for the RTL/LTR text direction.
7742 */
7743 get _localeHorizontalAlign() {
7744 if (this._isRTL) {
7745 // In RTL, "left" becomes "right".
7746 if (this.horizontalAlign === 'right') {
7747 return 'left';
7748 }
7749 if (this.horizontalAlign === 'left') {
7750 return 'right';
7751 }
7752 }
7753 return this.horizontalAlign;
7754 },
7755
7756 attached: function() {
7757 // Memoize this to avoid expensive calculations & relayouts.
7758 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
7759 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
7760 if (this.autoFitOnAttach) {
7761 if (window.getComputedStyle(this).display === 'none') {
7762 setTimeout(function() {
7763 this.fit();
7764 }.bind(this));
7765 } else {
7766 this.fit();
7767 }
7768 }
7769 },
7770
7771 /**
7772 * Positions and fits the element into the `fitInto` element.
7773 */
7774 fit: function() {
7775 this.position();
7776 this.constrain();
7777 this.center();
7778 },
7779
7780 /**
7781 * Memoize information needed to position and size the target element.
7782 * @suppress {deprecated}
7783 */
7784 _discoverInfo: function() {
7785 if (this._fitInfo) {
7786 return;
7787 }
7788 var target = window.getComputedStyle(this);
7789 var sizer = window.getComputedStyle(this.sizingTarget);
7790
7791 this._fitInfo = {
7792 inlineStyle: {
7793 top: this.style.top || '',
7794 left: this.style.left || '',
7795 position: this.style.position || ''
7796 },
7797 sizerInlineStyle: {
7798 maxWidth: this.sizingTarget.style.maxWidth || '',
7799 maxHeight: this.sizingTarget.style.maxHeight || '',
7800 boxSizing: this.sizingTarget.style.boxSizing || ''
7801 },
7802 positionedBy: {
7803 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
7804 'bottom' : null),
7805 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut o' ?
7806 'right' : null)
7807 },
7808 sizedBy: {
7809 height: sizer.maxHeight !== 'none',
7810 width: sizer.maxWidth !== 'none',
7811 minWidth: parseInt(sizer.minWidth, 10) || 0,
7812 minHeight: parseInt(sizer.minHeight, 10) || 0
7813 },
7814 margin: {
7815 top: parseInt(target.marginTop, 10) || 0,
7816 right: parseInt(target.marginRight, 10) || 0,
7817 bottom: parseInt(target.marginBottom, 10) || 0,
7818 left: parseInt(target.marginLeft, 10) || 0
7819 }
7820 };
7821
7822 // Support these properties until they are removed.
7823 if (this.verticalOffset) {
7824 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf fset;
7825 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
7826 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
7827 this.style.marginTop = this.style.marginBottom = this.verticalOffset + ' px';
7828 }
7829 if (this.horizontalOffset) {
7830 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal Offset;
7831 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
7832 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
7833 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
7834 }
7835 },
7836
7837 /**
7838 * Resets the target element's position and size constraints, and clear
7839 * the memoized data.
7840 */
7841 resetFit: function() {
7842 var info = this._fitInfo || {};
7843 for (var property in info.sizerInlineStyle) {
7844 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
7845 }
7846 for (var property in info.inlineStyle) {
7847 this.style[property] = info.inlineStyle[property];
7848 }
7849
7850 this._fitInfo = null;
7851 },
7852
7853 /**
7854 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
7855 * the element or the `fitInto` element has been resized, or if any of the
7856 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated .
7857 * It preserves the scroll position of the sizingTarget.
7858 */
7859 refit: function() {
7860 var scrollLeft = this.sizingTarget.scrollLeft;
7861 var scrollTop = this.sizingTarget.scrollTop;
7862 this.resetFit();
7863 this.fit();
7864 this.sizingTarget.scrollLeft = scrollLeft;
7865 this.sizingTarget.scrollTop = scrollTop;
7866 },
7867
7868 /**
7869 * Positions the element according to `horizontalAlign, verticalAlign`.
7870 */
7871 position: function() {
7872 if (!this.horizontalAlign && !this.verticalAlign) {
7873 // needs to be centered, and it is done after constrain.
7874 return;
7875 }
7876 this._discoverInfo();
7877
7878 this.style.position = 'fixed';
7879 // Need border-box for margin/padding.
7880 this.sizingTarget.style.boxSizing = 'border-box';
7881 // Set to 0, 0 in order to discover any offset caused by parent stacking c ontexts.
7882 this.style.left = '0px';
7883 this.style.top = '0px';
7884
7885 var rect = this.getBoundingClientRect();
7886 var positionRect = this.__getNormalizedRect(this.positionTarget);
7887 var fitRect = this.__getNormalizedRect(this.fitInto);
7888
7889 var margin = this._fitInfo.margin;
7890
7891 // Consider the margin as part of the size for position calculations.
7892 var size = {
7893 width: rect.width + margin.left + margin.right,
7894 height: rect.height + margin.top + margin.bottom
7895 };
7896
7897 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic alAlign, size, positionRect, fitRect);
7898
7899 var left = position.left + margin.left;
7900 var top = position.top + margin.top;
7901
7902 // Use original size (without margin).
7903 var right = Math.min(fitRect.right - margin.right, left + rect.width);
7904 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
7905
7906 var minWidth = this._fitInfo.sizedBy.minWidth;
7907 var minHeight = this._fitInfo.sizedBy.minHeight;
7908 if (left < margin.left) {
7909 left = margin.left;
7910 if (right - left < minWidth) {
7911 left = right - minWidth;
7912 }
7913 }
7914 if (top < margin.top) {
7915 top = margin.top;
7916 if (bottom - top < minHeight) {
7917 top = bottom - minHeight;
7918 }
7919 }
7920
7921 this.sizingTarget.style.maxWidth = (right - left) + 'px';
7922 this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
7923
7924 // Remove the offset caused by any stacking context.
7925 this.style.left = (left - rect.left) + 'px';
7926 this.style.top = (top - rect.top) + 'px';
7927 },
7928
7929 /**
7930 * Constrains the size of the element to `fitInto` by setting `max-height`
7931 * and/or `max-width`.
7932 */
7933 constrain: function() {
7934 if (this.horizontalAlign || this.verticalAlign) {
7935 return;
7936 }
7937 this._discoverInfo();
7938
7939 var info = this._fitInfo;
7940 // position at (0px, 0px) if not already positioned, so we can measure the natural size.
7941 if (!info.positionedBy.vertically) {
7942 this.style.position = 'fixed';
7943 this.style.top = '0px';
7944 }
7945 if (!info.positionedBy.horizontally) {
7946 this.style.position = 'fixed';
7947 this.style.left = '0px';
7948 }
7949
7950 // need border-box for margin/padding
7951 this.sizingTarget.style.boxSizing = 'border-box';
7952 // constrain the width and height if not already set
7953 var rect = this.getBoundingClientRect();
7954 if (!info.sizedBy.height) {
7955 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom' , 'Height');
7956 }
7957 if (!info.sizedBy.width) {
7958 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ t', 'Width');
7959 }
7960 },
7961
7962 /**
7963 * @protected
7964 * @deprecated
7965 */
7966 _sizeDimension: function(rect, positionedBy, start, end, extent) {
7967 this.__sizeDimension(rect, positionedBy, start, end, extent);
7968 },
7969
7970 /**
7971 * @private
7972 */
7973 __sizeDimension: function(rect, positionedBy, start, end, extent) {
7974 var info = this._fitInfo;
7975 var fitRect = this.__getNormalizedRect(this.fitInto);
7976 var max = extent === 'Width' ? fitRect.width : fitRect.height;
7977 var flip = (positionedBy === end);
7978 var offset = flip ? max - rect[end] : rect[start];
7979 var margin = info.margin[flip ? start : end];
7980 var offsetExtent = 'offset' + extent;
7981 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
7982 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO ffset) + 'px';
7983 },
7984
7985 /**
7986 * Centers horizontally and vertically if not already positioned. This also sets
7987 * `position:fixed`.
7988 */
7989 center: function() {
7990 if (this.horizontalAlign || this.verticalAlign) {
7991 return;
7992 }
7993 this._discoverInfo();
7994
7995 var positionedBy = this._fitInfo.positionedBy;
7996 if (positionedBy.vertically && positionedBy.horizontally) {
7997 // Already positioned.
7998 return;
7999 }
8000 // Need position:fixed to center
8001 this.style.position = 'fixed';
8002 // Take into account the offset caused by parents that create stacking
8003 // contexts (e.g. with transform: translate3d). Translate to 0,0 and
8004 // measure the bounding rect.
8005 if (!positionedBy.vertically) {
8006 this.style.top = '0px';
8007 }
8008 if (!positionedBy.horizontally) {
8009 this.style.left = '0px';
8010 }
8011 // It will take in consideration margins and transforms
8012 var rect = this.getBoundingClientRect();
8013 var fitRect = this.__getNormalizedRect(this.fitInto);
8014 if (!positionedBy.vertically) {
8015 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
8016 this.style.top = top + 'px';
8017 }
8018 if (!positionedBy.horizontally) {
8019 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
8020 this.style.left = left + 'px';
8021 }
8022 },
8023
8024 __getNormalizedRect: function(target) {
8025 if (target === document.documentElement || target === window) {
8026 return {
8027 top: 0,
8028 left: 0,
8029 width: window.innerWidth,
8030 height: window.innerHeight,
8031 right: window.innerWidth,
8032 bottom: window.innerHeight
8033 };
8034 }
8035 return target.getBoundingClientRect();
8036 },
8037
8038 __getCroppedArea: function(position, size, fitRect) {
8039 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
8040 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ t - (position.left + size.width));
8041 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si ze.height;
8042 },
8043
8044
8045 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
8046 // All the possible configurations.
8047 // Ordered as top-left, top-right, bottom-left, bottom-right.
8048 var positions = [{
8049 verticalAlign: 'top',
8050 horizontalAlign: 'left',
8051 top: positionRect.top,
8052 left: positionRect.left
8053 }, {
8054 verticalAlign: 'top',
8055 horizontalAlign: 'right',
8056 top: positionRect.top,
8057 left: positionRect.right - size.width
8058 }, {
8059 verticalAlign: 'bottom',
8060 horizontalAlign: 'left',
8061 top: positionRect.bottom - size.height,
8062 left: positionRect.left
8063 }, {
8064 verticalAlign: 'bottom',
8065 horizontalAlign: 'right',
8066 top: positionRect.bottom - size.height,
8067 left: positionRect.right - size.width
8068 }];
8069
8070 if (this.noOverlap) {
8071 // Duplicate.
8072 for (var i = 0, l = positions.length; i < l; i++) {
8073 var copy = {};
8074 for (var key in positions[i]) {
8075 copy[key] = positions[i][key];
8076 }
8077 positions.push(copy);
8078 }
8079 // Horizontal overlap only.
8080 positions[0].top = positions[1].top += positionRect.height;
8081 positions[2].top = positions[3].top -= positionRect.height;
8082 // Vertical overlap only.
8083 positions[4].left = positions[6].left += positionRect.width;
8084 positions[5].left = positions[7].left -= positionRect.width;
8085 }
8086
8087 // Consider auto as null for coding convenience.
8088 vAlign = vAlign === 'auto' ? null : vAlign;
8089 hAlign = hAlign === 'auto' ? null : hAlign;
8090
8091 var position;
8092 for (var i = 0; i < positions.length; i++) {
8093 var pos = positions[i];
8094
8095 // If both vAlign and hAlign are defined, return exact match.
8096 // For dynamicAlign and noOverlap we'll have more than one candidate, so
8097 // we'll have to check the croppedArea to make the best choice.
8098 if (!this.dynamicAlign && !this.noOverlap &&
8099 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
8100 position = pos;
8101 break;
8102 }
8103
8104 // Align is ok if alignment preferences are respected. If no preferences ,
8105 // it is considered ok.
8106 var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
8107 (!hAlign || pos.horizontalAlign === hAlign);
8108
8109 // Filter out elements that don't match the alignment (if defined).
8110 // With dynamicAlign, we need to consider all the positions to find the
8111 // one that minimizes the cropped area.
8112 if (!this.dynamicAlign && !alignOk) {
8113 continue;
8114 }
8115
8116 position = position || pos;
8117 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
8118 var diff = pos.croppedArea - position.croppedArea;
8119 // Check which crops less. If it crops equally, check if align is ok.
8120 if (diff < 0 || (diff === 0 && alignOk)) {
8121 position = pos;
8122 }
8123 // If not cropped and respects the align requirements, keep it.
8124 // This allows to prefer positions overlapping horizontally over the
8125 // ones overlapping vertically.
8126 if (position.croppedArea === 0 && alignOk) {
8127 break;
8128 }
8129 }
8130
8131 return position;
8132 }
8133
8134 };
8135 (function() {
8136 'use strict';
8137
8138 Polymer({
8139
8140 is: 'iron-overlay-backdrop',
8141
8142 properties: {
8143
8144 /**
8145 * Returns true if the backdrop is opened.
8146 */
8147 opened: { 4579 opened: {
8148 reflectToAttribute: true, 4580 reflectToAttribute: true,
8149 type: Boolean, 4581 type: Boolean,
8150 value: false, 4582 value: false,
8151 observer: '_openedChanged' 4583 observer: '_openedChanged'
8152 } 4584 }
8153 4585 },
8154 },
8155
8156 listeners: { 4586 listeners: {
8157 'transitionend': '_onTransitionend' 4587 transitionend: '_onTransitionend'
8158 }, 4588 },
8159
8160 created: function() { 4589 created: function() {
8161 // Used to cancel previous requestAnimationFrame calls when opened changes .
8162 this.__openedRaf = null; 4590 this.__openedRaf = null;
8163 }, 4591 },
8164
8165 attached: function() { 4592 attached: function() {
8166 this.opened && this._openedChanged(this.opened); 4593 this.opened && this._openedChanged(this.opened);
8167 }, 4594 },
8168
8169 /**
8170 * Appends the backdrop to document body if needed.
8171 */
8172 prepare: function() { 4595 prepare: function() {
8173 if (this.opened && !this.parentNode) { 4596 if (this.opened && !this.parentNode) {
8174 Polymer.dom(document.body).appendChild(this); 4597 Polymer.dom(document.body).appendChild(this);
8175 } 4598 }
8176 }, 4599 },
8177
8178 /**
8179 * Shows the backdrop.
8180 */
8181 open: function() { 4600 open: function() {
8182 this.opened = true; 4601 this.opened = true;
8183 }, 4602 },
8184
8185 /**
8186 * Hides the backdrop.
8187 */
8188 close: function() { 4603 close: function() {
8189 this.opened = false; 4604 this.opened = false;
8190 }, 4605 },
8191
8192 /**
8193 * Removes the backdrop from document body if needed.
8194 */
8195 complete: function() { 4606 complete: function() {
8196 if (!this.opened && this.parentNode === document.body) { 4607 if (!this.opened && this.parentNode === document.body) {
8197 Polymer.dom(this.parentNode).removeChild(this); 4608 Polymer.dom(this.parentNode).removeChild(this);
8198 } 4609 }
8199 }, 4610 },
8200
8201 _onTransitionend: function(event) { 4611 _onTransitionend: function(event) {
8202 if (event && event.target === this) { 4612 if (event && event.target === this) {
8203 this.complete(); 4613 this.complete();
8204 } 4614 }
8205 }, 4615 },
8206
8207 /**
8208 * @param {boolean} opened
8209 * @private
8210 */
8211 _openedChanged: function(opened) { 4616 _openedChanged: function(opened) {
8212 if (opened) { 4617 if (opened) {
8213 // Auto-attach.
8214 this.prepare(); 4618 this.prepare();
8215 } else { 4619 } else {
8216 // Animation might be disabled via the mixin or opacity custom property.
8217 // If it is disabled in other ways, it's up to the user to call complete .
8218 var cs = window.getComputedStyle(this); 4620 var cs = window.getComputedStyle(this);
8219 if (cs.transitionDuration === '0s' || cs.opacity == 0) { 4621 if (cs.transitionDuration === '0s' || cs.opacity == 0) {
8220 this.complete(); 4622 this.complete();
8221 } 4623 }
8222 } 4624 }
8223
8224 if (!this.isAttached) { 4625 if (!this.isAttached) {
8225 return; 4626 return;
8226 } 4627 }
8227
8228 // Always cancel previous requestAnimationFrame.
8229 if (this.__openedRaf) { 4628 if (this.__openedRaf) {
8230 window.cancelAnimationFrame(this.__openedRaf); 4629 window.cancelAnimationFrame(this.__openedRaf);
8231 this.__openedRaf = null; 4630 this.__openedRaf = null;
8232 } 4631 }
8233 // Force relayout to ensure proper transitions.
8234 this.scrollTop = this.scrollTop; 4632 this.scrollTop = this.scrollTop;
8235 this.__openedRaf = window.requestAnimationFrame(function() { 4633 this.__openedRaf = window.requestAnimationFrame(function() {
8236 this.__openedRaf = null; 4634 this.__openedRaf = null;
8237 this.toggleClass('opened', this.opened); 4635 this.toggleClass('opened', this.opened);
8238 }.bind(this)); 4636 }.bind(this));
8239 } 4637 }
8240 }); 4638 });
8241
8242 })(); 4639 })();
8243 /** 4640
8244 * @struct 4641 Polymer.IronOverlayManagerClass = function() {
8245 * @constructor 4642 this._overlays = [];
8246 * @private 4643 this._minimumZ = 101;
8247 */ 4644 this._backdropElement = null;
8248 Polymer.IronOverlayManagerClass = function() { 4645 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
8249 /** 4646 document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
8250 * Used to keep track of the opened overlays. 4647 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
8251 * @private {Array<Element>} 4648 };
8252 */ 4649
8253 this._overlays = []; 4650 Polymer.IronOverlayManagerClass.prototype = {
8254 4651 constructor: Polymer.IronOverlayManagerClass,
8255 /** 4652 get backdropElement() {
8256 * iframes have a default z-index of 100, 4653 if (!this._backdropElement) {
8257 * so this default should be at least that. 4654 this._backdropElement = document.createElement('iron-overlay-backdrop');
8258 * @private {number} 4655 }
8259 */ 4656 return this._backdropElement;
8260 this._minimumZ = 101; 4657 },
8261 4658 get deepActiveElement() {
8262 /** 4659 var active = document.activeElement || document.body;
8263 * Memoized backdrop element. 4660 while (active.root && Polymer.dom(active.root).activeElement) {
8264 * @private {Element|null} 4661 active = Polymer.dom(active.root).activeElement;
8265 */ 4662 }
8266 this._backdropElement = null; 4663 return active;
8267 4664 },
8268 // Enable document-wide tap recognizer. 4665 _bringOverlayAtIndexToFront: function(i) {
8269 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); 4666 var overlay = this._overlays[i];
8270 4667 if (!overlay) {
8271 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); 4668 return;
8272 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true ); 4669 }
8273 }; 4670 var lastI = this._overlays.length - 1;
8274 4671 var currentOverlay = this._overlays[lastI];
8275 Polymer.IronOverlayManagerClass.prototype = { 4672 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8276 4673 lastI--;
8277 constructor: Polymer.IronOverlayManagerClass, 4674 }
8278 4675 if (i >= lastI) {
8279 /** 4676 return;
8280 * The shared backdrop element. 4677 }
8281 * @type {!Element} backdropElement 4678 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8282 */ 4679 if (this._getZ(overlay) <= minimumZ) {
8283 get backdropElement() { 4680 this._applyOverlayZ(overlay, minimumZ);
8284 if (!this._backdropElement) { 4681 }
8285 this._backdropElement = document.createElement('iron-overlay-backdrop'); 4682 while (i < lastI) {
8286 } 4683 this._overlays[i] = this._overlays[i + 1];
8287 return this._backdropElement; 4684 i++;
8288 }, 4685 }
8289 4686 this._overlays[lastI] = overlay;
8290 /** 4687 },
8291 * The deepest active element. 4688 addOrRemoveOverlay: function(overlay) {
8292 * @type {!Element} activeElement the active element 4689 if (overlay.opened) {
8293 */ 4690 this.addOverlay(overlay);
8294 get deepActiveElement() { 4691 } else {
8295 // document.activeElement can be null 4692 this.removeOverlay(overlay);
8296 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement 4693 }
8297 // In case of null, default it to document.body. 4694 },
8298 var active = document.activeElement || document.body; 4695 addOverlay: function(overlay) {
8299 while (active.root && Polymer.dom(active.root).activeElement) { 4696 var i = this._overlays.indexOf(overlay);
8300 active = Polymer.dom(active.root).activeElement; 4697 if (i >= 0) {
8301 } 4698 this._bringOverlayAtIndexToFront(i);
8302 return active;
8303 },
8304
8305 /**
8306 * Brings the overlay at the specified index to the front.
8307 * @param {number} i
8308 * @private
8309 */
8310 _bringOverlayAtIndexToFront: function(i) {
8311 var overlay = this._overlays[i];
8312 if (!overlay) {
8313 return;
8314 }
8315 var lastI = this._overlays.length - 1;
8316 var currentOverlay = this._overlays[lastI];
8317 // Ensure always-on-top overlay stays on top.
8318 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8319 lastI--;
8320 }
8321 // If already the top element, return.
8322 if (i >= lastI) {
8323 return;
8324 }
8325 // Update z-index to be on top.
8326 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8327 if (this._getZ(overlay) <= minimumZ) {
8328 this._applyOverlayZ(overlay, minimumZ);
8329 }
8330
8331 // Shift other overlays behind the new on top.
8332 while (i < lastI) {
8333 this._overlays[i] = this._overlays[i + 1];
8334 i++;
8335 }
8336 this._overlays[lastI] = overlay;
8337 },
8338
8339 /**
8340 * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
8341 * Also updates the backdrop z-index.
8342 * @param {!Element} overlay
8343 */
8344 addOrRemoveOverlay: function(overlay) {
8345 if (overlay.opened) {
8346 this.addOverlay(overlay);
8347 } else {
8348 this.removeOverlay(overlay);
8349 }
8350 },
8351
8352 /**
8353 * Tracks overlays for z-index and focus management.
8354 * Ensures the last added overlay with always-on-top remains on top.
8355 * @param {!Element} overlay
8356 */
8357 addOverlay: function(overlay) {
8358 var i = this._overlays.indexOf(overlay);
8359 if (i >= 0) {
8360 this._bringOverlayAtIndexToFront(i);
8361 this.trackBackdrop();
8362 return;
8363 }
8364 var insertionIndex = this._overlays.length;
8365 var currentOverlay = this._overlays[insertionIndex - 1];
8366 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8367 var newZ = this._getZ(overlay);
8368
8369 // Ensure always-on-top overlay stays on top.
8370 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8371 // This bumps the z-index of +2.
8372 this._applyOverlayZ(currentOverlay, minimumZ);
8373 insertionIndex--;
8374 // Update minimumZ to match previous overlay's z-index.
8375 var previousOverlay = this._overlays[insertionIndex - 1];
8376 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8377 }
8378
8379 // Update z-index and insert overlay.
8380 if (newZ <= minimumZ) {
8381 this._applyOverlayZ(overlay, minimumZ);
8382 }
8383 this._overlays.splice(insertionIndex, 0, overlay);
8384
8385 this.trackBackdrop(); 4699 this.trackBackdrop();
8386 }, 4700 return;
8387 4701 }
8388 /** 4702 var insertionIndex = this._overlays.length;
8389 * @param {!Element} overlay 4703 var currentOverlay = this._overlays[insertionIndex - 1];
8390 */ 4704 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8391 removeOverlay: function(overlay) { 4705 var newZ = this._getZ(overlay);
8392 var i = this._overlays.indexOf(overlay); 4706 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8393 if (i === -1) { 4707 this._applyOverlayZ(currentOverlay, minimumZ);
8394 return; 4708 insertionIndex--;
8395 } 4709 var previousOverlay = this._overlays[insertionIndex - 1];
8396 this._overlays.splice(i, 1); 4710 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8397 4711 }
8398 this.trackBackdrop(); 4712 if (newZ <= minimumZ) {
8399 }, 4713 this._applyOverlayZ(overlay, minimumZ);
8400 4714 }
8401 /** 4715 this._overlays.splice(insertionIndex, 0, overlay);
8402 * Returns the current overlay. 4716 this.trackBackdrop();
8403 * @return {Element|undefined} 4717 },
8404 */ 4718 removeOverlay: function(overlay) {
8405 currentOverlay: function() { 4719 var i = this._overlays.indexOf(overlay);
8406 var i = this._overlays.length - 1; 4720 if (i === -1) {
8407 return this._overlays[i]; 4721 return;
8408 }, 4722 }
8409 4723 this._overlays.splice(i, 1);
8410 /** 4724 this.trackBackdrop();
8411 * Returns the current overlay z-index. 4725 },
8412 * @return {number} 4726 currentOverlay: function() {
8413 */ 4727 var i = this._overlays.length - 1;
8414 currentOverlayZ: function() { 4728 return this._overlays[i];
8415 return this._getZ(this.currentOverlay()); 4729 },
8416 }, 4730 currentOverlayZ: function() {
8417 4731 return this._getZ(this.currentOverlay());
8418 /** 4732 },
8419 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. 4733 ensureMinimumZ: function(minimumZ) {
8420 * This does not effect the z-index of any existing overlays. 4734 this._minimumZ = Math.max(this._minimumZ, minimumZ);
8421 * @param {number} minimumZ 4735 },
8422 */ 4736 focusOverlay: function() {
8423 ensureMinimumZ: function(minimumZ) { 4737 var current = this.currentOverlay();
8424 this._minimumZ = Math.max(this._minimumZ, minimumZ); 4738 if (current) {
8425 }, 4739 current._applyFocus();
8426 4740 }
8427 focusOverlay: function() { 4741 },
8428 var current = /** @type {?} */ (this.currentOverlay()); 4742 trackBackdrop: function() {
8429 if (current) { 4743 var overlay = this._overlayWithBackdrop();
8430 current._applyFocus(); 4744 if (!overlay && !this._backdropElement) {
8431 } 4745 return;
8432 }, 4746 }
8433 4747 this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
8434 /** 4748 this.backdropElement.opened = !!overlay;
8435 * Updates the backdrop z-index. 4749 },
8436 */ 4750 getBackdrops: function() {
8437 trackBackdrop: function() { 4751 var backdrops = [];
8438 var overlay = this._overlayWithBackdrop(); 4752 for (var i = 0; i < this._overlays.length; i++) {
8439 // Avoid creating the backdrop if there is no overlay with backdrop. 4753 if (this._overlays[i].withBackdrop) {
8440 if (!overlay && !this._backdropElement) { 4754 backdrops.push(this._overlays[i]);
8441 return; 4755 }
8442 } 4756 }
8443 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; 4757 return backdrops;
8444 this.backdropElement.opened = !!overlay; 4758 },
8445 }, 4759 backdropZ: function() {
8446 4760 return this._getZ(this._overlayWithBackdrop()) - 1;
8447 /** 4761 },
8448 * @return {Array<Element>} 4762 _overlayWithBackdrop: function() {
8449 */ 4763 for (var i = 0; i < this._overlays.length; i++) {
8450 getBackdrops: function() { 4764 if (this._overlays[i].withBackdrop) {
8451 var backdrops = []; 4765 return this._overlays[i];
8452 for (var i = 0; i < this._overlays.length; i++) { 4766 }
8453 if (this._overlays[i].withBackdrop) { 4767 }
8454 backdrops.push(this._overlays[i]); 4768 },
8455 } 4769 _getZ: function(overlay) {
8456 } 4770 var z = this._minimumZ;
8457 return backdrops; 4771 if (overlay) {
8458 }, 4772 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z Index);
8459 4773 if (z1 === z1) {
8460 /** 4774 z = z1;
8461 * Returns the z-index for the backdrop. 4775 }
8462 * @return {number} 4776 }
8463 */ 4777 return z;
8464 backdropZ: function() { 4778 },
8465 return this._getZ(this._overlayWithBackdrop()) - 1; 4779 _setZ: function(element, z) {
8466 }, 4780 element.style.zIndex = z;
8467 4781 },
8468 /** 4782 _applyOverlayZ: function(overlay, aboveZ) {
8469 * Returns the first opened overlay that has a backdrop. 4783 this._setZ(overlay, aboveZ + 2);
8470 * @return {Element|undefined} 4784 },
8471 * @private 4785 _overlayInPath: function(path) {
8472 */ 4786 path = path || [];
8473 _overlayWithBackdrop: function() { 4787 for (var i = 0; i < path.length; i++) {
8474 for (var i = 0; i < this._overlays.length; i++) { 4788 if (path[i]._manager === this) {
8475 if (this._overlays[i].withBackdrop) { 4789 return path[i];
8476 return this._overlays[i]; 4790 }
8477 } 4791 }
8478 } 4792 },
8479 }, 4793 _onCaptureClick: function(event) {
8480 4794 var overlay = this.currentOverlay();
8481 /** 4795 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8482 * Calculates the minimum z-index for the overlay. 4796 overlay._onCaptureClick(event);
8483 * @param {Element=} overlay 4797 }
8484 * @private 4798 },
8485 */ 4799 _onCaptureFocus: function(event) {
8486 _getZ: function(overlay) { 4800 var overlay = this.currentOverlay();
8487 var z = this._minimumZ; 4801 if (overlay) {
8488 if (overlay) { 4802 overlay._onCaptureFocus(event);
8489 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay) .zIndex); 4803 }
8490 // Check if is a number 4804 },
8491 // Number.isNaN not supported in IE 10+ 4805 _onCaptureKeyDown: function(event) {
8492 if (z1 === z1) { 4806 var overlay = this.currentOverlay();
8493 z = z1; 4807 if (overlay) {
8494 } 4808 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8495 } 4809 overlay._onCaptureEsc(event);
8496 return z; 4810 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't ab')) {
8497 }, 4811 overlay._onCaptureTab(event);
8498 4812 }
8499 /** 4813 }
8500 * @param {!Element} element 4814 },
8501 * @param {number|string} z 4815 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8502 * @private 4816 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8503 */ 4817 }
8504 _setZ: function(element, z) { 4818 };
8505 element.style.zIndex = z; 4819
8506 }, 4820 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8507 4821
8508 /**
8509 * @param {!Element} overlay
8510 * @param {number} aboveZ
8511 * @private
8512 */
8513 _applyOverlayZ: function(overlay, aboveZ) {
8514 this._setZ(overlay, aboveZ + 2);
8515 },
8516
8517 /**
8518 * Returns the deepest overlay in the path.
8519 * @param {Array<Element>=} path
8520 * @return {Element|undefined}
8521 * @suppress {missingProperties}
8522 * @private
8523 */
8524 _overlayInPath: function(path) {
8525 path = path || [];
8526 for (var i = 0; i < path.length; i++) {
8527 if (path[i]._manager === this) {
8528 return path[i];
8529 }
8530 }
8531 },
8532
8533 /**
8534 * Ensures the click event is delegated to the right overlay.
8535 * @param {!Event} event
8536 * @private
8537 */
8538 _onCaptureClick: function(event) {
8539 var overlay = /** @type {?} */ (this.currentOverlay());
8540 // Check if clicked outside of top overlay.
8541 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8542 overlay._onCaptureClick(event);
8543 }
8544 },
8545
8546 /**
8547 * Ensures the focus event is delegated to the right overlay.
8548 * @param {!Event} event
8549 * @private
8550 */
8551 _onCaptureFocus: function(event) {
8552 var overlay = /** @type {?} */ (this.currentOverlay());
8553 if (overlay) {
8554 overlay._onCaptureFocus(event);
8555 }
8556 },
8557
8558 /**
8559 * Ensures TAB and ESC keyboard events are delegated to the right overlay.
8560 * @param {!Event} event
8561 * @private
8562 */
8563 _onCaptureKeyDown: function(event) {
8564 var overlay = /** @type {?} */ (this.currentOverlay());
8565 if (overlay) {
8566 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8567 overlay._onCaptureEsc(event);
8568 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
8569 overlay._onCaptureTab(event);
8570 }
8571 }
8572 },
8573
8574 /**
8575 * Returns if the overlay1 should be behind overlay2.
8576 * @param {!Element} overlay1
8577 * @param {!Element} overlay2
8578 * @return {boolean}
8579 * @suppress {missingProperties}
8580 * @private
8581 */
8582 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8583 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8584 }
8585 };
8586
8587 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8588 (function() { 4822 (function() {
8589 'use strict'; 4823 'use strict';
8590
8591 /**
8592 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
8593 on top of other content. It includes an optional backdrop, and can be used to im plement a variety
8594 of UI controls including dialogs and drop downs. Multiple overlays may be displa yed at once.
8595
8596 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav ior/blob/master/demo/simple-overlay.html)
8597 for an example.
8598
8599 ### Closing and canceling
8600
8601 An overlay may be hidden by closing or canceling. The difference between close a nd cancel is user
8602 intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
8603 it will cancel whenever the user taps outside it or presses the escape key. This behavior is
8604 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click ` properties.
8605 `close()` should be called explicitly by the implementer when the user interacts with a control
8606 in the overlay element. When the dialog is canceled, the overlay fires an 'iron- overlay-canceled'
8607 event. Call `preventDefault` on this event to prevent the overlay from closing.
8608
8609 ### Positioning
8610
8611 By default the element is sized and positioned to fit and centered inside the wi ndow. You can
8612 position and size it manually using CSS. See `Polymer.IronFitBehavior`.
8613
8614 ### Backdrop
8615
8616 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
8617 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
8618 options.
8619
8620 In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
8621 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f ocusableNodes)
8622 to achieve a different behavior.
8623
8624 ### Limitations
8625
8626 The element is styled to appear on top of other content by setting its `z-index` property. You
8627 must ensure no element has a stacking context with a higher `z-index` than its p arent stacking
8628 context. You should place this element as a child of `<body>` whenever possible.
8629
8630 @demo demo/index.html
8631 @polymerBehavior Polymer.IronOverlayBehavior
8632 */
8633
8634 Polymer.IronOverlayBehaviorImpl = { 4824 Polymer.IronOverlayBehaviorImpl = {
8635
8636 properties: { 4825 properties: {
8637
8638 /**
8639 * True if the overlay is currently displayed.
8640 */
8641 opened: { 4826 opened: {
8642 observer: '_openedChanged', 4827 observer: '_openedChanged',
8643 type: Boolean, 4828 type: Boolean,
8644 value: false, 4829 value: false,
8645 notify: true 4830 notify: true
8646 }, 4831 },
8647
8648 /**
8649 * True if the overlay was canceled when it was last closed.
8650 */
8651 canceled: { 4832 canceled: {
8652 observer: '_canceledChanged', 4833 observer: '_canceledChanged',
8653 readOnly: true, 4834 readOnly: true,
8654 type: Boolean, 4835 type: Boolean,
8655 value: false 4836 value: false
8656 }, 4837 },
8657
8658 /**
8659 * Set to true to display a backdrop behind the overlay. It traps the focu s
8660 * within the light DOM of the overlay.
8661 */
8662 withBackdrop: { 4838 withBackdrop: {
8663 observer: '_withBackdropChanged', 4839 observer: '_withBackdropChanged',
8664 type: Boolean 4840 type: Boolean
8665 }, 4841 },
8666
8667 /**
8668 * Set to true to disable auto-focusing the overlay or child nodes with
8669 * the `autofocus` attribute` when the overlay is opened.
8670 */
8671 noAutoFocus: { 4842 noAutoFocus: {
8672 type: Boolean, 4843 type: Boolean,
8673 value: false 4844 value: false
8674 }, 4845 },
8675
8676 /**
8677 * Set to true to disable canceling the overlay with the ESC key.
8678 */
8679 noCancelOnEscKey: { 4846 noCancelOnEscKey: {
8680 type: Boolean, 4847 type: Boolean,
8681 value: false 4848 value: false
8682 }, 4849 },
8683
8684 /**
8685 * Set to true to disable canceling the overlay by clicking outside it.
8686 */
8687 noCancelOnOutsideClick: { 4850 noCancelOnOutsideClick: {
8688 type: Boolean, 4851 type: Boolean,
8689 value: false 4852 value: false
8690 }, 4853 },
8691
8692 /**
8693 * Contains the reason(s) this overlay was last closed (see `iron-overlay- closed`).
8694 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t he
8695 * behavior can provide other reasons in addition to `canceled`.
8696 */
8697 closingReason: { 4854 closingReason: {
8698 // was a getter before, but needs to be a property so other
8699 // behaviors can override this.
8700 type: Object 4855 type: Object
8701 }, 4856 },
8702
8703 /**
8704 * Set to true to enable restoring of focus when overlay is closed.
8705 */
8706 restoreFocusOnClose: { 4857 restoreFocusOnClose: {
8707 type: Boolean, 4858 type: Boolean,
8708 value: false 4859 value: false
8709 }, 4860 },
8710
8711 /**
8712 * Set to true to keep overlay always on top.
8713 */
8714 alwaysOnTop: { 4861 alwaysOnTop: {
8715 type: Boolean 4862 type: Boolean
8716 }, 4863 },
8717
8718 /**
8719 * Shortcut to access to the overlay manager.
8720 * @private
8721 * @type {Polymer.IronOverlayManagerClass}
8722 */
8723 _manager: { 4864 _manager: {
8724 type: Object, 4865 type: Object,
8725 value: Polymer.IronOverlayManager 4866 value: Polymer.IronOverlayManager
8726 }, 4867 },
8727
8728 /**
8729 * The node being focused.
8730 * @type {?Node}
8731 */
8732 _focusedChild: { 4868 _focusedChild: {
8733 type: Object 4869 type: Object
8734 } 4870 }
8735 4871 },
8736 },
8737
8738 listeners: { 4872 listeners: {
8739 'iron-resize': '_onIronResize' 4873 'iron-resize': '_onIronResize'
8740 }, 4874 },
8741
8742 /**
8743 * The backdrop element.
8744 * @type {Element}
8745 */
8746 get backdropElement() { 4875 get backdropElement() {
8747 return this._manager.backdropElement; 4876 return this._manager.backdropElement;
8748 }, 4877 },
8749
8750 /**
8751 * Returns the node to give focus to.
8752 * @type {Node}
8753 */
8754 get _focusNode() { 4878 get _focusNode() {
8755 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this; 4879 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this;
8756 }, 4880 },
8757
8758 /**
8759 * Array of nodes that can receive focus (overlay included), ordered by `tab index`.
8760 * This is used to retrieve which is the first and last focusable nodes in o rder
8761 * to wrap the focus for overlays `with-backdrop`.
8762 *
8763 * If you know what is your content (specifically the first and last focusab le children),
8764 * you can override this method to return only `[firstFocusable, lastFocusab le];`
8765 * @type {Array<Node>}
8766 * @protected
8767 */
8768 get _focusableNodes() { 4881 get _focusableNodes() {
8769 // Elements that can be focused even if they have [disabled] attribute. 4882 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin dex]', '[contentEditable=true]' ];
8770 var FOCUSABLE_WITH_DISABLED = [ 4883 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button' ];
8771 'a[href]', 4884 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"])';
8772 'area[href]',
8773 'iframe',
8774 '[tabindex]',
8775 '[contentEditable=true]'
8776 ];
8777
8778 // Elements that cannot be focused if they have [disabled] attribute.
8779 var FOCUSABLE_WITHOUT_DISABLED = [
8780 'input',
8781 'select',
8782 'textarea',
8783 'button'
8784 ];
8785
8786 // Discard elements with tabindex=-1 (makes them not focusable).
8787 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
8788 ':not([tabindex="-1"]),' +
8789 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),' ) +
8790 ':not([disabled]):not([tabindex="-1"])';
8791
8792 var focusables = Polymer.dom(this).querySelectorAll(selector); 4885 var focusables = Polymer.dom(this).querySelectorAll(selector);
8793 if (this.tabIndex >= 0) { 4886 if (this.tabIndex >= 0) {
8794 // Insert at the beginning because we might have all elements with tabIn dex = 0,
8795 // and the overlay should be the first of the list.
8796 focusables.splice(0, 0, this); 4887 focusables.splice(0, 0, this);
8797 } 4888 }
8798 // Sort by tabindex. 4889 return focusables.sort(function(a, b) {
8799 return focusables.sort(function (a, b) {
8800 if (a.tabIndex === b.tabIndex) { 4890 if (a.tabIndex === b.tabIndex) {
8801 return 0; 4891 return 0;
8802 } 4892 }
8803 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { 4893 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
8804 return 1; 4894 return 1;
8805 } 4895 }
8806 return -1; 4896 return -1;
8807 }); 4897 });
8808 }, 4898 },
8809
8810 ready: function() { 4899 ready: function() {
8811 // Used to skip calls to notifyResize and refit while the overlay is anima ting.
8812 this.__isAnimating = false; 4900 this.__isAnimating = false;
8813 // with-backdrop needs tabindex to be set in order to trap the focus.
8814 // If it is not set, IronOverlayBehavior will set it, and remove it if wit h-backdrop = false.
8815 this.__shouldRemoveTabIndex = false; 4901 this.__shouldRemoveTabIndex = false;
8816 // Used for wrapping the focus on TAB / Shift+TAB.
8817 this.__firstFocusableNode = this.__lastFocusableNode = null; 4902 this.__firstFocusableNode = this.__lastFocusableNode = null;
8818 // Used by __onNextAnimationFrame to cancel any previous callback.
8819 this.__raf = null; 4903 this.__raf = null;
8820 // Focused node before overlay gets opened. Can be restored on close.
8821 this.__restoreFocusNode = null; 4904 this.__restoreFocusNode = null;
8822 this._ensureSetup(); 4905 this._ensureSetup();
8823 }, 4906 },
8824
8825 attached: function() { 4907 attached: function() {
8826 // Call _openedChanged here so that position can be computed correctly.
8827 if (this.opened) { 4908 if (this.opened) {
8828 this._openedChanged(this.opened); 4909 this._openedChanged(this.opened);
8829 } 4910 }
8830 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); 4911 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
8831 }, 4912 },
8832
8833 detached: function() { 4913 detached: function() {
8834 Polymer.dom(this).unobserveNodes(this._observer); 4914 Polymer.dom(this).unobserveNodes(this._observer);
8835 this._observer = null; 4915 this._observer = null;
8836 if (this.__raf) { 4916 if (this.__raf) {
8837 window.cancelAnimationFrame(this.__raf); 4917 window.cancelAnimationFrame(this.__raf);
8838 this.__raf = null; 4918 this.__raf = null;
8839 } 4919 }
8840 this._manager.removeOverlay(this); 4920 this._manager.removeOverlay(this);
8841 }, 4921 },
8842
8843 /**
8844 * Toggle the opened state of the overlay.
8845 */
8846 toggle: function() { 4922 toggle: function() {
8847 this._setCanceled(false); 4923 this._setCanceled(false);
8848 this.opened = !this.opened; 4924 this.opened = !this.opened;
8849 }, 4925 },
8850
8851 /**
8852 * Open the overlay.
8853 */
8854 open: function() { 4926 open: function() {
8855 this._setCanceled(false); 4927 this._setCanceled(false);
8856 this.opened = true; 4928 this.opened = true;
8857 }, 4929 },
8858
8859 /**
8860 * Close the overlay.
8861 */
8862 close: function() { 4930 close: function() {
8863 this._setCanceled(false); 4931 this._setCanceled(false);
8864 this.opened = false; 4932 this.opened = false;
8865 }, 4933 },
8866
8867 /**
8868 * Cancels the overlay.
8869 * @param {Event=} event The original event
8870 */
8871 cancel: function(event) { 4934 cancel: function(event) {
8872 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t rue}); 4935 var cancelEvent = this.fire('iron-overlay-canceled', event, {
4936 cancelable: true
4937 });
8873 if (cancelEvent.defaultPrevented) { 4938 if (cancelEvent.defaultPrevented) {
8874 return; 4939 return;
8875 } 4940 }
8876
8877 this._setCanceled(true); 4941 this._setCanceled(true);
8878 this.opened = false; 4942 this.opened = false;
8879 }, 4943 },
8880
8881 _ensureSetup: function() { 4944 _ensureSetup: function() {
8882 if (this._overlaySetup) { 4945 if (this._overlaySetup) {
8883 return; 4946 return;
8884 } 4947 }
8885 this._overlaySetup = true; 4948 this._overlaySetup = true;
8886 this.style.outline = 'none'; 4949 this.style.outline = 'none';
8887 this.style.display = 'none'; 4950 this.style.display = 'none';
8888 }, 4951 },
8889
8890 /**
8891 * Called when `opened` changes.
8892 * @param {boolean=} opened
8893 * @protected
8894 */
8895 _openedChanged: function(opened) { 4952 _openedChanged: function(opened) {
8896 if (opened) { 4953 if (opened) {
8897 this.removeAttribute('aria-hidden'); 4954 this.removeAttribute('aria-hidden');
8898 } else { 4955 } else {
8899 this.setAttribute('aria-hidden', 'true'); 4956 this.setAttribute('aria-hidden', 'true');
8900 } 4957 }
8901
8902 // Defer any animation-related code on attached
8903 // (_openedChanged gets called again on attached).
8904 if (!this.isAttached) { 4958 if (!this.isAttached) {
8905 return; 4959 return;
8906 } 4960 }
8907
8908 this.__isAnimating = true; 4961 this.__isAnimating = true;
8909
8910 // Use requestAnimationFrame for non-blocking rendering.
8911 this.__onNextAnimationFrame(this.__openedChanged); 4962 this.__onNextAnimationFrame(this.__openedChanged);
8912 }, 4963 },
8913
8914 _canceledChanged: function() { 4964 _canceledChanged: function() {
8915 this.closingReason = this.closingReason || {}; 4965 this.closingReason = this.closingReason || {};
8916 this.closingReason.canceled = this.canceled; 4966 this.closingReason.canceled = this.canceled;
8917 }, 4967 },
8918
8919 _withBackdropChanged: function() { 4968 _withBackdropChanged: function() {
8920 // If tabindex is already set, no need to override it.
8921 if (this.withBackdrop && !this.hasAttribute('tabindex')) { 4969 if (this.withBackdrop && !this.hasAttribute('tabindex')) {
8922 this.setAttribute('tabindex', '-1'); 4970 this.setAttribute('tabindex', '-1');
8923 this.__shouldRemoveTabIndex = true; 4971 this.__shouldRemoveTabIndex = true;
8924 } else if (this.__shouldRemoveTabIndex) { 4972 } else if (this.__shouldRemoveTabIndex) {
8925 this.removeAttribute('tabindex'); 4973 this.removeAttribute('tabindex');
8926 this.__shouldRemoveTabIndex = false; 4974 this.__shouldRemoveTabIndex = false;
8927 } 4975 }
8928 if (this.opened && this.isAttached) { 4976 if (this.opened && this.isAttached) {
8929 this._manager.trackBackdrop(); 4977 this._manager.trackBackdrop();
8930 } 4978 }
8931 }, 4979 },
8932
8933 /**
8934 * tasks which must occur before opening; e.g. making the element visible.
8935 * @protected
8936 */
8937 _prepareRenderOpened: function() { 4980 _prepareRenderOpened: function() {
8938 // Store focused node.
8939 this.__restoreFocusNode = this._manager.deepActiveElement; 4981 this.__restoreFocusNode = this._manager.deepActiveElement;
8940
8941 // Needed to calculate the size of the overlay so that transitions on its size
8942 // will have the correct starting points.
8943 this._preparePositioning(); 4982 this._preparePositioning();
8944 this.refit(); 4983 this.refit();
8945 this._finishPositioning(); 4984 this._finishPositioning();
8946
8947 // Safari will apply the focus to the autofocus element when displayed
8948 // for the first time, so we make sure to return the focus where it was.
8949 if (this.noAutoFocus && document.activeElement === this._focusNode) { 4985 if (this.noAutoFocus && document.activeElement === this._focusNode) {
8950 this._focusNode.blur(); 4986 this._focusNode.blur();
8951 this.__restoreFocusNode.focus(); 4987 this.__restoreFocusNode.focus();
8952 } 4988 }
8953 }, 4989 },
8954
8955 /**
8956 * Tasks which cause the overlay to actually open; typically play an animati on.
8957 * @protected
8958 */
8959 _renderOpened: function() { 4990 _renderOpened: function() {
8960 this._finishRenderOpened(); 4991 this._finishRenderOpened();
8961 }, 4992 },
8962
8963 /**
8964 * Tasks which cause the overlay to actually close; typically play an animat ion.
8965 * @protected
8966 */
8967 _renderClosed: function() { 4993 _renderClosed: function() {
8968 this._finishRenderClosed(); 4994 this._finishRenderClosed();
8969 }, 4995 },
8970
8971 /**
8972 * Tasks to be performed at the end of open action. Will fire `iron-overlay- opened`.
8973 * @protected
8974 */
8975 _finishRenderOpened: function() { 4996 _finishRenderOpened: function() {
8976 this.notifyResize(); 4997 this.notifyResize();
8977 this.__isAnimating = false; 4998 this.__isAnimating = false;
8978
8979 // Store it so we don't query too much.
8980 var focusableNodes = this._focusableNodes; 4999 var focusableNodes = this._focusableNodes;
8981 this.__firstFocusableNode = focusableNodes[0]; 5000 this.__firstFocusableNode = focusableNodes[0];
8982 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; 5001 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
8983
8984 this.fire('iron-overlay-opened'); 5002 this.fire('iron-overlay-opened');
8985 }, 5003 },
8986
8987 /**
8988 * Tasks to be performed at the end of close action. Will fire `iron-overlay -closed`.
8989 * @protected
8990 */
8991 _finishRenderClosed: function() { 5004 _finishRenderClosed: function() {
8992 // Hide the overlay.
8993 this.style.display = 'none'; 5005 this.style.display = 'none';
8994 // Reset z-index only at the end of the animation.
8995 this.style.zIndex = ''; 5006 this.style.zIndex = '';
8996 this.notifyResize(); 5007 this.notifyResize();
8997 this.__isAnimating = false; 5008 this.__isAnimating = false;
8998 this.fire('iron-overlay-closed', this.closingReason); 5009 this.fire('iron-overlay-closed', this.closingReason);
8999 }, 5010 },
9000
9001 _preparePositioning: function() { 5011 _preparePositioning: function() {
9002 this.style.transition = this.style.webkitTransition = 'none'; 5012 this.style.transition = this.style.webkitTransition = 'none';
9003 this.style.transform = this.style.webkitTransform = 'none'; 5013 this.style.transform = this.style.webkitTransform = 'none';
9004 this.style.display = ''; 5014 this.style.display = '';
9005 }, 5015 },
9006
9007 _finishPositioning: function() { 5016 _finishPositioning: function() {
9008 // First, make it invisible & reactivate animations.
9009 this.style.display = 'none'; 5017 this.style.display = 'none';
9010 // Force reflow before re-enabling animations so that they don't start.
9011 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9012 this.scrollTop = this.scrollTop; 5018 this.scrollTop = this.scrollTop;
9013 this.style.transition = this.style.webkitTransition = ''; 5019 this.style.transition = this.style.webkitTransition = '';
9014 this.style.transform = this.style.webkitTransform = ''; 5020 this.style.transform = this.style.webkitTransform = '';
9015 // Now that animations are enabled, make it visible again
9016 this.style.display = ''; 5021 this.style.display = '';
9017 // Force reflow, so that following animations are properly started.
9018 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9019 this.scrollTop = this.scrollTop; 5022 this.scrollTop = this.scrollTop;
9020 }, 5023 },
9021
9022 /**
9023 * Applies focus according to the opened state.
9024 * @protected
9025 */
9026 _applyFocus: function() { 5024 _applyFocus: function() {
9027 if (this.opened) { 5025 if (this.opened) {
9028 if (!this.noAutoFocus) { 5026 if (!this.noAutoFocus) {
9029 this._focusNode.focus(); 5027 this._focusNode.focus();
9030 } 5028 }
9031 } 5029 } else {
9032 else {
9033 this._focusNode.blur(); 5030 this._focusNode.blur();
9034 this._focusedChild = null; 5031 this._focusedChild = null;
9035 // Restore focus.
9036 if (this.restoreFocusOnClose && this.__restoreFocusNode) { 5032 if (this.restoreFocusOnClose && this.__restoreFocusNode) {
9037 this.__restoreFocusNode.focus(); 5033 this.__restoreFocusNode.focus();
9038 } 5034 }
9039 this.__restoreFocusNode = null; 5035 this.__restoreFocusNode = null;
9040 // If many overlays get closed at the same time, one of them would still
9041 // be the currentOverlay even if already closed, and would call _applyFo cus
9042 // infinitely, so we check for this not to be the current overlay.
9043 var currentOverlay = this._manager.currentOverlay(); 5036 var currentOverlay = this._manager.currentOverlay();
9044 if (currentOverlay && this !== currentOverlay) { 5037 if (currentOverlay && this !== currentOverlay) {
9045 currentOverlay._applyFocus(); 5038 currentOverlay._applyFocus();
9046 } 5039 }
9047 } 5040 }
9048 }, 5041 },
9049
9050 /**
9051 * Cancels (closes) the overlay. Call when click happens outside the overlay .
9052 * @param {!Event} event
9053 * @protected
9054 */
9055 _onCaptureClick: function(event) { 5042 _onCaptureClick: function(event) {
9056 if (!this.noCancelOnOutsideClick) { 5043 if (!this.noCancelOnOutsideClick) {
9057 this.cancel(event); 5044 this.cancel(event);
9058 } 5045 }
9059 }, 5046 },
9060 5047 _onCaptureFocus: function(event) {
9061 /**
9062 * Keeps track of the focused child. If withBackdrop, traps focus within ove rlay.
9063 * @param {!Event} event
9064 * @protected
9065 */
9066 _onCaptureFocus: function (event) {
9067 if (!this.withBackdrop) { 5048 if (!this.withBackdrop) {
9068 return; 5049 return;
9069 } 5050 }
9070 var path = Polymer.dom(event).path; 5051 var path = Polymer.dom(event).path;
9071 if (path.indexOf(this) === -1) { 5052 if (path.indexOf(this) === -1) {
9072 event.stopPropagation(); 5053 event.stopPropagation();
9073 this._applyFocus(); 5054 this._applyFocus();
9074 } else { 5055 } else {
9075 this._focusedChild = path[0]; 5056 this._focusedChild = path[0];
9076 } 5057 }
9077 }, 5058 },
9078
9079 /**
9080 * Handles the ESC key event and cancels (closes) the overlay.
9081 * @param {!Event} event
9082 * @protected
9083 */
9084 _onCaptureEsc: function(event) { 5059 _onCaptureEsc: function(event) {
9085 if (!this.noCancelOnEscKey) { 5060 if (!this.noCancelOnEscKey) {
9086 this.cancel(event); 5061 this.cancel(event);
9087 } 5062 }
9088 }, 5063 },
9089
9090 /**
9091 * Handles TAB key events to track focus changes.
9092 * Will wrap focus for overlays withBackdrop.
9093 * @param {!Event} event
9094 * @protected
9095 */
9096 _onCaptureTab: function(event) { 5064 _onCaptureTab: function(event) {
9097 if (!this.withBackdrop) { 5065 if (!this.withBackdrop) {
9098 return; 5066 return;
9099 } 5067 }
9100 // TAB wraps from last to first focusable.
9101 // Shift + TAB wraps from first to last focusable.
9102 var shift = event.shiftKey; 5068 var shift = event.shiftKey;
9103 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node; 5069 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node;
9104 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de; 5070 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de;
9105 var shouldWrap = false; 5071 var shouldWrap = false;
9106 if (nodeToCheck === nodeToSet) { 5072 if (nodeToCheck === nodeToSet) {
9107 // If nodeToCheck is the same as nodeToSet, it means we have an overlay
9108 // with 0 or 1 focusables; in either case we still need to trap the
9109 // focus within the overlay.
9110 shouldWrap = true; 5073 shouldWrap = true;
9111 } else { 5074 } else {
9112 // In dom=shadow, the manager will receive focus changes on the main
9113 // root but not the ones within other shadow roots, so we can't rely on
9114 // _focusedChild, but we should check the deepest active element.
9115 var focusedNode = this._manager.deepActiveElement; 5075 var focusedNode = this._manager.deepActiveElement;
9116 // If the active element is not the nodeToCheck but the overlay itself, 5076 shouldWrap = focusedNode === nodeToCheck || focusedNode === this;
9117 // it means the focus is about to go outside the overlay, hence we
9118 // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
9119 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
9120 } 5077 }
9121
9122 if (shouldWrap) { 5078 if (shouldWrap) {
9123 // When the overlay contains the last focusable element of the document
9124 // and it's already focused, pressing TAB would move the focus outside
9125 // the document (e.g. to the browser search bar). Similarly, when the
9126 // overlay contains the first focusable element of the document and it's
9127 // already focused, pressing Shift+TAB would move the focus outside the
9128 // document (e.g. to the browser search bar).
9129 // In both cases, we would not receive a focus event, but only a blur.
9130 // In order to achieve focus wrapping, we prevent this TAB event and
9131 // force the focus. This will also prevent the focus to temporarily move
9132 // outside the overlay, which might cause scrolling.
9133 event.preventDefault(); 5079 event.preventDefault();
9134 this._focusedChild = nodeToSet; 5080 this._focusedChild = nodeToSet;
9135 this._applyFocus(); 5081 this._applyFocus();
9136 } 5082 }
9137 }, 5083 },
9138
9139 /**
9140 * Refits if the overlay is opened and not animating.
9141 * @protected
9142 */
9143 _onIronResize: function() { 5084 _onIronResize: function() {
9144 if (this.opened && !this.__isAnimating) { 5085 if (this.opened && !this.__isAnimating) {
9145 this.__onNextAnimationFrame(this.refit); 5086 this.__onNextAnimationFrame(this.refit);
9146 } 5087 }
9147 }, 5088 },
9148
9149 /**
9150 * Will call notifyResize if overlay is opened.
9151 * Can be overridden in order to avoid multiple observers on the same node.
9152 * @protected
9153 */
9154 _onNodesChange: function() { 5089 _onNodesChange: function() {
9155 if (this.opened && !this.__isAnimating) { 5090 if (this.opened && !this.__isAnimating) {
9156 this.notifyResize(); 5091 this.notifyResize();
9157 } 5092 }
9158 }, 5093 },
9159
9160 /**
9161 * Tasks executed when opened changes: prepare for the opening, move the
9162 * focus, update the manager, render opened/closed.
9163 * @private
9164 */
9165 __openedChanged: function() { 5094 __openedChanged: function() {
9166 if (this.opened) { 5095 if (this.opened) {
9167 // Make overlay visible, then add it to the manager.
9168 this._prepareRenderOpened(); 5096 this._prepareRenderOpened();
9169 this._manager.addOverlay(this); 5097 this._manager.addOverlay(this);
9170 // Move the focus to the child node with [autofocus].
9171 this._applyFocus(); 5098 this._applyFocus();
9172
9173 this._renderOpened(); 5099 this._renderOpened();
9174 } else { 5100 } else {
9175 // Remove overlay, then restore the focus before actually closing.
9176 this._manager.removeOverlay(this); 5101 this._manager.removeOverlay(this);
9177 this._applyFocus(); 5102 this._applyFocus();
9178
9179 this._renderClosed(); 5103 this._renderClosed();
9180 } 5104 }
9181 }, 5105 },
9182
9183 /**
9184 * Executes a callback on the next animation frame, overriding any previous
9185 * callback awaiting for the next animation frame. e.g.
9186 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
9187 * `callback1` will never be invoked.
9188 * @param {!Function} callback Its `this` parameter is the overlay itself.
9189 * @private
9190 */
9191 __onNextAnimationFrame: function(callback) { 5106 __onNextAnimationFrame: function(callback) {
9192 if (this.__raf) { 5107 if (this.__raf) {
9193 window.cancelAnimationFrame(this.__raf); 5108 window.cancelAnimationFrame(this.__raf);
9194 } 5109 }
9195 var self = this; 5110 var self = this;
9196 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { 5111 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
9197 self.__raf = null; 5112 self.__raf = null;
9198 callback.call(self); 5113 callback.call(self);
9199 }); 5114 });
9200 } 5115 }
9201
9202 }; 5116 };
9203 5117 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable Behavior, Polymer.IronOverlayBehaviorImpl ];
9204 /** @polymerBehavior */
9205 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB ehavior, Polymer.IronOverlayBehaviorImpl];
9206
9207 /**
9208 * Fired after the overlay opens.
9209 * @event iron-overlay-opened
9210 */
9211
9212 /**
9213 * Fired when the overlay is canceled, but before it is closed.
9214 * @event iron-overlay-canceled
9215 * @param {Event} event The closing of the overlay can be prevented
9216 * by calling `event.preventDefault()`. The `event.detail` is the original eve nt that
9217 * originated the canceling (e.g. ESC keyboard event or click event outside th e overlay).
9218 */
9219
9220 /**
9221 * Fired after the overlay closes.
9222 * @event iron-overlay-closed
9223 * @param {Event} event The `event.detail` is the `closingReason` property
9224 * (contains `canceled`, whether the overlay was canceled).
9225 */
9226
9227 })(); 5118 })();
9228 /** 5119
9229 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim ations for use with 5120 Polymer.NeonAnimatableBehavior = {
9230 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. 5121 properties: {
9231 * @polymerBehavior 5122 animationConfig: {
9232 */ 5123 type: Object
9233 Polymer.NeonAnimatableBehavior = { 5124 },
9234 5125 entryAnimation: {
9235 properties: { 5126 observer: '_entryAnimationChanged',
9236 5127 type: String
9237 /** 5128 },
9238 * Animation configuration. See README for more info. 5129 exitAnimation: {
9239 */ 5130 observer: '_exitAnimationChanged',
9240 animationConfig: { 5131 type: String
9241 type: Object 5132 }
9242 }, 5133 },
9243 5134 _entryAnimationChanged: function() {
9244 /** 5135 this.animationConfig = this.animationConfig || {};
9245 * Convenience property for setting an 'entry' animation. Do not set `anim ationConfig.entry` 5136 this.animationConfig['entry'] = [ {
9246 * manually if using this. The animated node is set to `this` if using thi s property. 5137 name: this.entryAnimation,
9247 */ 5138 node: this
9248 entryAnimation: { 5139 } ];
9249 observer: '_entryAnimationChanged', 5140 },
9250 type: String 5141 _exitAnimationChanged: function() {
9251 }, 5142 this.animationConfig = this.animationConfig || {};
9252 5143 this.animationConfig['exit'] = [ {
9253 /** 5144 name: this.exitAnimation,
9254 * Convenience property for setting an 'exit' animation. Do not set `anima tionConfig.exit` 5145 node: this
9255 * manually if using this. The animated node is set to `this` if using thi s property. 5146 } ];
9256 */ 5147 },
9257 exitAnimation: { 5148 _copyProperties: function(config1, config2) {
9258 observer: '_exitAnimationChanged', 5149 for (var property in config2) {
9259 type: String 5150 config1[property] = config2[property];
9260 } 5151 }
9261 5152 },
9262 }, 5153 _cloneConfig: function(config) {
9263 5154 var clone = {
9264 _entryAnimationChanged: function() { 5155 isClone: true
9265 this.animationConfig = this.animationConfig || {}; 5156 };
9266 this.animationConfig['entry'] = [{ 5157 this._copyProperties(clone, config);
9267 name: this.entryAnimation, 5158 return clone;
9268 node: this 5159 },
9269 }]; 5160 _getAnimationConfigRecursive: function(type, map, allConfigs) {
9270 }, 5161 if (!this.animationConfig) {
9271 5162 return;
9272 _exitAnimationChanged: function() { 5163 }
9273 this.animationConfig = this.animationConfig || {}; 5164 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun ction') {
9274 this.animationConfig['exit'] = [{ 5165 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid e of your components 'properties' object instead of outside of it."));
9275 name: this.exitAnimation, 5166 return;
9276 node: this 5167 }
9277 }]; 5168 var thisConfig;
9278 }, 5169 if (type) {
9279 5170 thisConfig = this.animationConfig[type];
9280 _copyProperties: function(config1, config2) { 5171 } else {
9281 // shallowly copy properties from config2 to config1 5172 thisConfig = this.animationConfig;
9282 for (var property in config2) { 5173 }
9283 config1[property] = config2[property]; 5174 if (!Array.isArray(thisConfig)) {
9284 } 5175 thisConfig = [ thisConfig ];
9285 }, 5176 }
9286 5177 if (thisConfig) {
9287 _cloneConfig: function(config) { 5178 for (var config, index = 0; config = thisConfig[index]; index++) {
9288 var clone = { 5179 if (config.animatable) {
9289 isClone: true 5180 config.animatable._getAnimationConfigRecursive(config.type || type, ma p, allConfigs);
9290 }; 5181 } else {
9291 this._copyProperties(clone, config); 5182 if (config.id) {
9292 return clone; 5183 var cachedConfig = map[config.id];
9293 }, 5184 if (cachedConfig) {
9294 5185 if (!cachedConfig.isClone) {
9295 _getAnimationConfigRecursive: function(type, map, allConfigs) { 5186 map[config.id] = this._cloneConfig(cachedConfig);
9296 if (!this.animationConfig) { 5187 cachedConfig = map[config.id];
9297 return;
9298 }
9299
9300 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu nction') {
9301 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins ide of your components 'properties' object instead of outside of it."));
9302 » return;
9303 }
9304
9305 // type is optional
9306 var thisConfig;
9307 if (type) {
9308 thisConfig = this.animationConfig[type];
9309 } else {
9310 thisConfig = this.animationConfig;
9311 }
9312
9313 if (!Array.isArray(thisConfig)) {
9314 thisConfig = [thisConfig];
9315 }
9316
9317 // iterate animations and recurse to process configurations from child nod es
9318 if (thisConfig) {
9319 for (var config, index = 0; config = thisConfig[index]; index++) {
9320 if (config.animatable) {
9321 config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
9322 } else {
9323 if (config.id) {
9324 var cachedConfig = map[config.id];
9325 if (cachedConfig) {
9326 // merge configurations with the same id, making a clone lazily
9327 if (!cachedConfig.isClone) {
9328 map[config.id] = this._cloneConfig(cachedConfig)
9329 cachedConfig = map[config.id];
9330 }
9331 this._copyProperties(cachedConfig, config);
9332 } else {
9333 // put any configs with an id into a map
9334 map[config.id] = config;
9335 } 5188 }
5189 this._copyProperties(cachedConfig, config);
9336 } else { 5190 } else {
9337 allConfigs.push(config); 5191 map[config.id] = config;
9338 }
9339 }
9340 }
9341 }
9342 },
9343
9344 /**
9345 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
9346 * an animation with an optional type. Elements implementing `Polymer.NeonAn imatableBehavior`
9347 * should define the property `animationConfig`, which is either a configura tion object
9348 * or a map of animation type to array of configuration objects.
9349 */
9350 getAnimationConfig: function(type) {
9351 var map = {};
9352 var allConfigs = [];
9353 this._getAnimationConfigRecursive(type, map, allConfigs);
9354 // append the configurations saved in the map to the array
9355 for (var key in map) {
9356 allConfigs.push(map[key]);
9357 }
9358 return allConfigs;
9359 }
9360
9361 };
9362 /**
9363 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
9364 *
9365 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
9366 */
9367 Polymer.NeonAnimationRunnerBehaviorImpl = {
9368
9369 _configureAnimations: function(configs) {
9370 var results = [];
9371 if (configs.length > 0) {
9372 for (var config, index = 0; config = configs[index]; index++) {
9373 var neonAnimation = document.createElement(config.name);
9374 // is this element actually a neon animation?
9375 if (neonAnimation.isNeonAnimation) {
9376 var result = null;
9377 // configuration or play could fail if polyfills aren't loaded
9378 try {
9379 result = neonAnimation.configure(config);
9380 // Check if we have an Effect rather than an Animation
9381 if (typeof result.cancel != 'function') {
9382 result = document.timeline.play(result);
9383 }
9384 } catch (e) {
9385 result = null;
9386 console.warn('Couldnt play', '(', config.name, ').', e);
9387 }
9388 if (result) {
9389 results.push({
9390 neonAnimation: neonAnimation,
9391 config: config,
9392 animation: result,
9393 });
9394 } 5192 }
9395 } else { 5193 } else {
9396 console.warn(this.is + ':', config.name, 'not found!'); 5194 allConfigs.push(config);
9397 } 5195 }
9398 } 5196 }
9399 } 5197 }
9400 return results; 5198 }
9401 }, 5199 },
9402 5200 getAnimationConfig: function(type) {
9403 _shouldComplete: function(activeEntries) { 5201 var map = {};
9404 var finished = true; 5202 var allConfigs = [];
9405 for (var i = 0; i < activeEntries.length; i++) { 5203 this._getAnimationConfigRecursive(type, map, allConfigs);
9406 if (activeEntries[i].animation.playState != 'finished') { 5204 for (var key in map) {
9407 finished = false; 5205 allConfigs.push(map[key]);
9408 break; 5206 }
9409 } 5207 return allConfigs;
9410 } 5208 }
9411 return finished; 5209 };
9412 }, 5210
9413 5211 Polymer.NeonAnimationRunnerBehaviorImpl = {
9414 _complete: function(activeEntries) { 5212 _configureAnimations: function(configs) {
9415 for (var i = 0; i < activeEntries.length; i++) { 5213 var results = [];
9416 activeEntries[i].neonAnimation.complete(activeEntries[i].config); 5214 if (configs.length > 0) {
9417 } 5215 for (var config, index = 0; config = configs[index]; index++) {
9418 for (var i = 0; i < activeEntries.length; i++) { 5216 var neonAnimation = document.createElement(config.name);
9419 activeEntries[i].animation.cancel(); 5217 if (neonAnimation.isNeonAnimation) {
9420 } 5218 var result = null;
9421 }, 5219 try {
9422 5220 result = neonAnimation.configure(config);
9423 /** 5221 if (typeof result.cancel != 'function') {
9424 * Plays an animation with an optional `type`. 5222 result = document.timeline.play(result);
9425 * @param {string=} type 5223 }
9426 * @param {!Object=} cookie 5224 } catch (e) {
9427 */ 5225 result = null;
9428 playAnimation: function(type, cookie) { 5226 console.warn('Couldnt play', '(', config.name, ').', e);
9429 var configs = this.getAnimationConfig(type); 5227 }
9430 if (!configs) { 5228 if (result) {
5229 results.push({
5230 neonAnimation: neonAnimation,
5231 config: config,
5232 animation: result
5233 });
5234 }
5235 } else {
5236 console.warn(this.is + ':', config.name, 'not found!');
5237 }
5238 }
5239 }
5240 return results;
5241 },
5242 _shouldComplete: function(activeEntries) {
5243 var finished = true;
5244 for (var i = 0; i < activeEntries.length; i++) {
5245 if (activeEntries[i].animation.playState != 'finished') {
5246 finished = false;
5247 break;
5248 }
5249 }
5250 return finished;
5251 },
5252 _complete: function(activeEntries) {
5253 for (var i = 0; i < activeEntries.length; i++) {
5254 activeEntries[i].neonAnimation.complete(activeEntries[i].config);
5255 }
5256 for (var i = 0; i < activeEntries.length; i++) {
5257 activeEntries[i].animation.cancel();
5258 }
5259 },
5260 playAnimation: function(type, cookie) {
5261 var configs = this.getAnimationConfig(type);
5262 if (!configs) {
5263 return;
5264 }
5265 this._active = this._active || {};
5266 if (this._active[type]) {
5267 this._complete(this._active[type]);
5268 delete this._active[type];
5269 }
5270 var activeEntries = this._configureAnimations(configs);
5271 if (activeEntries.length == 0) {
5272 this.fire('neon-animation-finish', cookie, {
5273 bubbles: false
5274 });
5275 return;
5276 }
5277 this._active[type] = activeEntries;
5278 for (var i = 0; i < activeEntries.length; i++) {
5279 activeEntries[i].animation.onfinish = function() {
5280 if (this._shouldComplete(activeEntries)) {
5281 this._complete(activeEntries);
5282 delete this._active[type];
5283 this.fire('neon-animation-finish', cookie, {
5284 bubbles: false
5285 });
5286 }
5287 }.bind(this);
5288 }
5289 },
5290 cancelAnimation: function() {
5291 for (var k in this._animations) {
5292 this._animations[k].cancel();
5293 }
5294 this._animations = {};
5295 }
5296 };
5297
5298 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer. NeonAnimationRunnerBehaviorImpl ];
5299
5300 Polymer.NeonAnimationBehavior = {
5301 properties: {
5302 animationTiming: {
5303 type: Object,
5304 value: function() {
5305 return {
5306 duration: 500,
5307 easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
5308 fill: 'both'
5309 };
5310 }
5311 }
5312 },
5313 isNeonAnimation: true,
5314 timingFromConfig: function(config) {
5315 if (config.timing) {
5316 for (var property in config.timing) {
5317 this.animationTiming[property] = config.timing[property];
5318 }
5319 }
5320 return this.animationTiming;
5321 },
5322 setPrefixedProperty: function(node, property, value) {
5323 var map = {
5324 transform: [ 'webkitTransform' ],
5325 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ]
5326 };
5327 var prefixes = map[property];
5328 for (var prefix, index = 0; prefix = prefixes[index]; index++) {
5329 node.style[prefix] = value;
5330 }
5331 node.style[property] = value;
5332 },
5333 complete: function() {}
5334 };
5335
5336 Polymer({
5337 is: 'opaque-animation',
5338 behaviors: [ Polymer.NeonAnimationBehavior ],
5339 configure: function(config) {
5340 var node = config.node;
5341 this._effect = new KeyframeEffect(node, [ {
5342 opacity: '1'
5343 }, {
5344 opacity: '1'
5345 } ], this.timingFromConfig(config));
5346 node.style.opacity = '0';
5347 return this._effect;
5348 },
5349 complete: function(config) {
5350 config.node.style.opacity = '';
5351 }
5352 });
5353
5354 (function() {
5355 'use strict';
5356 var LAST_TOUCH_POSITION = {
5357 pageX: 0,
5358 pageY: 0
5359 };
5360 var ROOT_TARGET = null;
5361 var SCROLLABLE_NODES = [];
5362 Polymer.IronDropdownScrollManager = {
5363 get currentLockingElement() {
5364 return this._lockingElements[this._lockingElements.length - 1];
5365 },
5366 elementIsScrollLocked: function(element) {
5367 var currentLockingElement = this.currentLockingElement;
5368 if (currentLockingElement === undefined) return false;
5369 var scrollLocked;
5370 if (this._hasCachedLockedElement(element)) {
5371 return true;
5372 }
5373 if (this._hasCachedUnlockedElement(element)) {
5374 return false;
5375 }
5376 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen t && !this._composedTreeContains(currentLockingElement, element);
5377 if (scrollLocked) {
5378 this._lockedElementCache.push(element);
5379 } else {
5380 this._unlockedElementCache.push(element);
5381 }
5382 return scrollLocked;
5383 },
5384 pushScrollLock: function(element) {
5385 if (this._lockingElements.indexOf(element) >= 0) {
9431 return; 5386 return;
9432 } 5387 }
9433 this._active = this._active || {}; 5388 if (this._lockingElements.length === 0) {
9434 if (this._active[type]) { 5389 this._lockScrollInteractions();
9435 this._complete(this._active[type]); 5390 }
9436 delete this._active[type]; 5391 this._lockingElements.push(element);
9437 } 5392 this._lockedElementCache = [];
9438 5393 this._unlockedElementCache = [];
9439 var activeEntries = this._configureAnimations(configs); 5394 },
9440 5395 removeScrollLock: function(element) {
9441 if (activeEntries.length == 0) { 5396 var index = this._lockingElements.indexOf(element);
9442 this.fire('neon-animation-finish', cookie, {bubbles: false}); 5397 if (index === -1) {
9443 return; 5398 return;
9444 } 5399 }
9445 5400 this._lockingElements.splice(index, 1);
9446 this._active[type] = activeEntries; 5401 this._lockedElementCache = [];
9447 5402 this._unlockedElementCache = [];
9448 for (var i = 0; i < activeEntries.length; i++) { 5403 if (this._lockingElements.length === 0) {
9449 activeEntries[i].animation.onfinish = function() { 5404 this._unlockScrollInteractions();
9450 if (this._shouldComplete(activeEntries)) { 5405 }
9451 this._complete(activeEntries); 5406 },
9452 delete this._active[type]; 5407 _lockingElements: [],
9453 this.fire('neon-animation-finish', cookie, {bubbles: false}); 5408 _lockedElementCache: null,
5409 _unlockedElementCache: null,
5410 _hasCachedLockedElement: function(element) {
5411 return this._lockedElementCache.indexOf(element) > -1;
5412 },
5413 _hasCachedUnlockedElement: function(element) {
5414 return this._unlockedElementCache.indexOf(element) > -1;
5415 },
5416 _composedTreeContains: function(element, child) {
5417 var contentElements;
5418 var distributedNodes;
5419 var contentIndex;
5420 var nodeIndex;
5421 if (element.contains(child)) {
5422 return true;
5423 }
5424 contentElements = Polymer.dom(element).querySelectorAll('content');
5425 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd ex) {
5426 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib utedNodes();
5427 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
5428 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
5429 return true;
9454 } 5430 }
9455 }.bind(this); 5431 }
9456 } 5432 }
9457 }, 5433 return false;
9458 5434 },
9459 /** 5435 _scrollInteractionHandler: function(event) {
9460 * Cancels the currently running animations. 5436 if (event.cancelable && this._shouldPreventScrolling(event)) {
9461 */ 5437 event.preventDefault();
9462 cancelAnimation: function() { 5438 }
9463 for (var k in this._animations) { 5439 if (event.targetTouches) {
9464 this._animations[k].cancel(); 5440 var touch = event.targetTouches[0];
9465 } 5441 LAST_TOUCH_POSITION.pageX = touch.pageX;
9466 this._animations = {}; 5442 LAST_TOUCH_POSITION.pageY = touch.pageY;
5443 }
5444 },
5445 _lockScrollInteractions: function() {
5446 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac tionHandler.bind(this);
5447 document.addEventListener('wheel', this._boundScrollHandler, true);
5448 document.addEventListener('mousewheel', this._boundScrollHandler, true);
5449 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true );
5450 document.addEventListener('touchstart', this._boundScrollHandler, true);
5451 document.addEventListener('touchmove', this._boundScrollHandler, true);
5452 },
5453 _unlockScrollInteractions: function() {
5454 document.removeEventListener('wheel', this._boundScrollHandler, true);
5455 document.removeEventListener('mousewheel', this._boundScrollHandler, true) ;
5456 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t rue);
5457 document.removeEventListener('touchstart', this._boundScrollHandler, true) ;
5458 document.removeEventListener('touchmove', this._boundScrollHandler, true);
5459 },
5460 _shouldPreventScrolling: function(event) {
5461 var target = Polymer.dom(event).rootTarget;
5462 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
5463 ROOT_TARGET = target;
5464 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
5465 }
5466 if (!SCROLLABLE_NODES.length) {
5467 return true;
5468 }
5469 if (event.type === 'touchstart') {
5470 return false;
5471 }
5472 var info = this._getScrollInfo(event);
5473 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY) ;
5474 },
5475 _getScrollableNodes: function(nodes) {
5476 var scrollables = [];
5477 var lockingIndex = nodes.indexOf(this.currentLockingElement);
5478 for (var i = 0; i <= lockingIndex; i++) {
5479 var node = nodes[i];
5480 if (node.nodeType === 11) {
5481 continue;
5482 }
5483 var style = node.style;
5484 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
5485 style = window.getComputedStyle(node);
5486 }
5487 if (style.overflow === 'scroll' || style.overflow === 'auto') {
5488 scrollables.push(node);
5489 }
5490 }
5491 return scrollables;
5492 },
5493 _getScrollingNode: function(nodes, deltaX, deltaY) {
5494 if (!deltaX && !deltaY) {
5495 return;
5496 }
5497 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
5498 for (var i = 0; i < nodes.length; i++) {
5499 var node = nodes[i];
5500 var canScroll = false;
5501 if (verticalScroll) {
5502 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc rollHeight - node.clientHeight;
5503 } else {
5504 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node. scrollWidth - node.clientWidth;
5505 }
5506 if (canScroll) {
5507 return node;
5508 }
5509 }
5510 },
5511 _getScrollInfo: function(event) {
5512 var info = {
5513 deltaX: event.deltaX,
5514 deltaY: event.deltaY
5515 };
5516 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) {
5517 info.deltaX = -event.wheelDeltaX;
5518 info.deltaY = -event.wheelDeltaY;
5519 } else if ('axis' in event) {
5520 info.deltaX = event.axis === 1 ? event.detail : 0;
5521 info.deltaY = event.axis === 2 ? event.detail : 0;
5522 } else if (event.targetTouches) {
5523 var touch = event.targetTouches[0];
5524 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
5525 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
5526 }
5527 return info;
9467 } 5528 }
9468 }; 5529 };
9469 5530 })();
9470 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ 5531
9471 Polymer.NeonAnimationRunnerBehavior = [ 5532 (function() {
9472 Polymer.NeonAnimatableBehavior, 5533 'use strict';
9473 Polymer.NeonAnimationRunnerBehaviorImpl 5534 Polymer({
9474 ]; 5535 is: 'iron-dropdown',
9475 /** 5536 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer .IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ],
9476 * Use `Polymer.NeonAnimationBehavior` to implement an animation.
9477 * @polymerBehavior
9478 */
9479 Polymer.NeonAnimationBehavior = {
9480
9481 properties: { 5537 properties: {
9482 5538 horizontalAlign: {
9483 /** 5539 type: String,
9484 * Defines the animation timing. 5540 value: 'left',
9485 */ 5541 reflectToAttribute: true
9486 animationTiming: { 5542 },
5543 verticalAlign: {
5544 type: String,
5545 value: 'top',
5546 reflectToAttribute: true
5547 },
5548 openAnimationConfig: {
5549 type: Object
5550 },
5551 closeAnimationConfig: {
5552 type: Object
5553 },
5554 focusTarget: {
5555 type: Object
5556 },
5557 noAnimations: {
5558 type: Boolean,
5559 value: false
5560 },
5561 allowOutsideScroll: {
5562 type: Boolean,
5563 value: false
5564 },
5565 _boundOnCaptureScroll: {
5566 type: Function,
5567 value: function() {
5568 return this._onCaptureScroll.bind(this);
5569 }
5570 }
5571 },
5572 listeners: {
5573 'neon-animation-finish': '_onNeonAnimationFinish'
5574 },
5575 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont alAlign, verticalOffset, horizontalOffset)' ],
5576 get containedElement() {
5577 return Polymer.dom(this.$.content).getDistributedNodes()[0];
5578 },
5579 get _focusTarget() {
5580 return this.focusTarget || this.containedElement;
5581 },
5582 ready: function() {
5583 this._scrollTop = 0;
5584 this._scrollLeft = 0;
5585 this._refitOnScrollRAF = null;
5586 },
5587 detached: function() {
5588 this.cancelAnimation();
5589 Polymer.IronDropdownScrollManager.removeScrollLock(this);
5590 },
5591 _openedChanged: function() {
5592 if (this.opened && this.disabled) {
5593 this.cancel();
5594 } else {
5595 this.cancelAnimation();
5596 this.sizingTarget = this.containedElement || this.sizingTarget;
5597 this._updateAnimationConfig();
5598 this._saveScrollPosition();
5599 if (this.opened) {
5600 document.addEventListener('scroll', this._boundOnCaptureScroll);
5601 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro llLock(this);
5602 } else {
5603 document.removeEventListener('scroll', this._boundOnCaptureScroll);
5604 Polymer.IronDropdownScrollManager.removeScrollLock(this);
5605 }
5606 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
5607 }
5608 },
5609 _renderOpened: function() {
5610 if (!this.noAnimations && this.animationConfig.open) {
5611 this.$.contentWrapper.classList.add('animating');
5612 this.playAnimation('open');
5613 } else {
5614 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
5615 }
5616 },
5617 _renderClosed: function() {
5618 if (!this.noAnimations && this.animationConfig.close) {
5619 this.$.contentWrapper.classList.add('animating');
5620 this.playAnimation('close');
5621 } else {
5622 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
5623 }
5624 },
5625 _onNeonAnimationFinish: function() {
5626 this.$.contentWrapper.classList.remove('animating');
5627 if (this.opened) {
5628 this._finishRenderOpened();
5629 } else {
5630 this._finishRenderClosed();
5631 }
5632 },
5633 _onCaptureScroll: function() {
5634 if (!this.allowOutsideScroll) {
5635 this._restoreScrollPosition();
5636 } else {
5637 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol lRAF);
5638 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th is));
5639 }
5640 },
5641 _saveScrollPosition: function() {
5642 if (document.scrollingElement) {
5643 this._scrollTop = document.scrollingElement.scrollTop;
5644 this._scrollLeft = document.scrollingElement.scrollLeft;
5645 } else {
5646 this._scrollTop = Math.max(document.documentElement.scrollTop, document. body.scrollTop);
5647 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen t.body.scrollLeft);
5648 }
5649 },
5650 _restoreScrollPosition: function() {
5651 if (document.scrollingElement) {
5652 document.scrollingElement.scrollTop = this._scrollTop;
5653 document.scrollingElement.scrollLeft = this._scrollLeft;
5654 } else {
5655 document.documentElement.scrollTop = this._scrollTop;
5656 document.documentElement.scrollLeft = this._scrollLeft;
5657 document.body.scrollTop = this._scrollTop;
5658 document.body.scrollLeft = this._scrollLeft;
5659 }
5660 },
5661 _updateAnimationConfig: function() {
5662 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati onConfig || []);
5663 for (var i = 0; i < animations.length; i++) {
5664 animations[i].node = this.containedElement;
5665 }
5666 this.animationConfig = {
5667 open: this.openAnimationConfig,
5668 close: this.closeAnimationConfig
5669 };
5670 },
5671 _updateOverlayPosition: function() {
5672 if (this.isAttached) {
5673 this.notifyResize();
5674 }
5675 },
5676 _applyFocus: function() {
5677 var focusTarget = this.focusTarget || this.containedElement;
5678 if (focusTarget && this.opened && !this.noAutoFocus) {
5679 focusTarget.focus();
5680 } else {
5681 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
5682 }
5683 }
5684 });
5685 })();
5686
5687 Polymer({
5688 is: 'fade-in-animation',
5689 behaviors: [ Polymer.NeonAnimationBehavior ],
5690 configure: function(config) {
5691 var node = config.node;
5692 this._effect = new KeyframeEffect(node, [ {
5693 opacity: '0'
5694 }, {
5695 opacity: '1'
5696 } ], this.timingFromConfig(config));
5697 return this._effect;
5698 }
5699 });
5700
5701 Polymer({
5702 is: 'fade-out-animation',
5703 behaviors: [ Polymer.NeonAnimationBehavior ],
5704 configure: function(config) {
5705 var node = config.node;
5706 this._effect = new KeyframeEffect(node, [ {
5707 opacity: '1'
5708 }, {
5709 opacity: '0'
5710 } ], this.timingFromConfig(config));
5711 return this._effect;
5712 }
5713 });
5714
5715 Polymer({
5716 is: 'paper-menu-grow-height-animation',
5717 behaviors: [ Polymer.NeonAnimationBehavior ],
5718 configure: function(config) {
5719 var node = config.node;
5720 var rect = node.getBoundingClientRect();
5721 var height = rect.height;
5722 this._effect = new KeyframeEffect(node, [ {
5723 height: height / 2 + 'px'
5724 }, {
5725 height: height + 'px'
5726 } ], this.timingFromConfig(config));
5727 return this._effect;
5728 }
5729 });
5730
5731 Polymer({
5732 is: 'paper-menu-grow-width-animation',
5733 behaviors: [ Polymer.NeonAnimationBehavior ],
5734 configure: function(config) {
5735 var node = config.node;
5736 var rect = node.getBoundingClientRect();
5737 var width = rect.width;
5738 this._effect = new KeyframeEffect(node, [ {
5739 width: width / 2 + 'px'
5740 }, {
5741 width: width + 'px'
5742 } ], this.timingFromConfig(config));
5743 return this._effect;
5744 }
5745 });
5746
5747 Polymer({
5748 is: 'paper-menu-shrink-width-animation',
5749 behaviors: [ Polymer.NeonAnimationBehavior ],
5750 configure: function(config) {
5751 var node = config.node;
5752 var rect = node.getBoundingClientRect();
5753 var width = rect.width;
5754 this._effect = new KeyframeEffect(node, [ {
5755 width: width + 'px'
5756 }, {
5757 width: width - width / 20 + 'px'
5758 } ], this.timingFromConfig(config));
5759 return this._effect;
5760 }
5761 });
5762
5763 Polymer({
5764 is: 'paper-menu-shrink-height-animation',
5765 behaviors: [ Polymer.NeonAnimationBehavior ],
5766 configure: function(config) {
5767 var node = config.node;
5768 var rect = node.getBoundingClientRect();
5769 var height = rect.height;
5770 var top = rect.top;
5771 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
5772 this._effect = new KeyframeEffect(node, [ {
5773 height: height + 'px',
5774 transform: 'translateY(0)'
5775 }, {
5776 height: height / 2 + 'px',
5777 transform: 'translateY(-20px)'
5778 } ], this.timingFromConfig(config));
5779 return this._effect;
5780 }
5781 });
5782
5783 (function() {
5784 'use strict';
5785 var config = {
5786 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
5787 MAX_ANIMATION_TIME_MS: 400
5788 };
5789 var PaperMenuButton = Polymer({
5790 is: 'paper-menu-button',
5791 behaviors: [ Polymer.IronA11yKeysBehavior, Polymer.IronControlState ],
5792 properties: {
5793 opened: {
5794 type: Boolean,
5795 value: false,
5796 notify: true,
5797 observer: '_openedChanged'
5798 },
5799 horizontalAlign: {
5800 type: String,
5801 value: 'left',
5802 reflectToAttribute: true
5803 },
5804 verticalAlign: {
5805 type: String,
5806 value: 'top',
5807 reflectToAttribute: true
5808 },
5809 dynamicAlign: {
5810 type: Boolean
5811 },
5812 horizontalOffset: {
5813 type: Number,
5814 value: 0,
5815 notify: true
5816 },
5817 verticalOffset: {
5818 type: Number,
5819 value: 0,
5820 notify: true
5821 },
5822 noOverlap: {
5823 type: Boolean
5824 },
5825 noAnimations: {
5826 type: Boolean,
5827 value: false
5828 },
5829 ignoreSelect: {
5830 type: Boolean,
5831 value: false
5832 },
5833 closeOnActivate: {
5834 type: Boolean,
5835 value: false
5836 },
5837 openAnimationConfig: {
9487 type: Object, 5838 type: Object,
9488 value: function() { 5839 value: function() {
9489 return { 5840 return [ {
9490 duration: 500, 5841 name: 'fade-in-animation',
9491 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', 5842 timing: {
9492 fill: 'both' 5843 delay: 100,
9493 } 5844 duration: 200
9494 } 5845 }
9495 } 5846 }, {
9496 5847 name: 'paper-menu-grow-width-animation',
9497 }, 5848 timing: {
9498 5849 delay: 100,
9499 /** 5850 duration: 150,
9500 * Can be used to determine that elements implement this behavior. 5851 easing: config.ANIMATION_CUBIC_BEZIER
9501 */ 5852 }
9502 isNeonAnimation: true, 5853 }, {
9503 5854 name: 'paper-menu-grow-height-animation',
9504 /** 5855 timing: {
9505 * Do any animation configuration here. 5856 delay: 100,
9506 */ 5857 duration: 275,
9507 // configure: function(config) { 5858 easing: config.ANIMATION_CUBIC_BEZIER
9508 // }, 5859 }
9509 5860 } ];
9510 /** 5861 }
9511 * Returns the animation timing by mixing in properties from `config` to the defaults defined 5862 },
9512 * by the animation. 5863 closeAnimationConfig: {
9513 */ 5864 type: Object,
9514 timingFromConfig: function(config) { 5865 value: function() {
9515 if (config.timing) { 5866 return [ {
9516 for (var property in config.timing) { 5867 name: 'fade-out-animation',
9517 this.animationTiming[property] = config.timing[property]; 5868 timing: {
9518 } 5869 duration: 150
9519 } 5870 }
9520 return this.animationTiming; 5871 }, {
9521 }, 5872 name: 'paper-menu-shrink-width-animation',
9522 5873 timing: {
9523 /** 5874 delay: 100,
9524 * Sets `transform` and `transformOrigin` properties along with the prefixed versions. 5875 duration: 50,
9525 */ 5876 easing: config.ANIMATION_CUBIC_BEZIER
9526 setPrefixedProperty: function(node, property, value) { 5877 }
9527 var map = { 5878 }, {
9528 'transform': ['webkitTransform'], 5879 name: 'paper-menu-shrink-height-animation',
9529 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] 5880 timing: {
9530 }; 5881 duration: 200,
9531 var prefixes = map[property]; 5882 easing: 'ease-in'
9532 for (var prefix, index = 0; prefix = prefixes[index]; index++) { 5883 }
9533 node.style[prefix] = value; 5884 } ];
9534 } 5885 }
9535 node.style[property] = value; 5886 },
9536 }, 5887 allowOutsideScroll: {
9537 5888 type: Boolean,
9538 /** 5889 value: false
9539 * Called when the animation finishes. 5890 },
9540 */ 5891 restoreFocusOnClose: {
9541 complete: function() {} 5892 type: Boolean,
9542 5893 value: true
9543 }; 5894 },
5895 _dropdownContent: {
5896 type: Object
5897 }
5898 },
5899 hostAttributes: {
5900 role: 'group',
5901 'aria-haspopup': 'true'
5902 },
5903 listeners: {
5904 'iron-activate': '_onIronActivate',
5905 'iron-select': '_onIronSelect'
5906 },
5907 get contentElement() {
5908 return Polymer.dom(this.$.content).getDistributedNodes()[0];
5909 },
5910 toggle: function() {
5911 if (this.opened) {
5912 this.close();
5913 } else {
5914 this.open();
5915 }
5916 },
5917 open: function() {
5918 if (this.disabled) {
5919 return;
5920 }
5921 this.$.dropdown.open();
5922 },
5923 close: function() {
5924 this.$.dropdown.close();
5925 },
5926 _onIronSelect: function(event) {
5927 if (!this.ignoreSelect) {
5928 this.close();
5929 }
5930 },
5931 _onIronActivate: function(event) {
5932 if (this.closeOnActivate) {
5933 this.close();
5934 }
5935 },
5936 _openedChanged: function(opened, oldOpened) {
5937 if (opened) {
5938 this._dropdownContent = this.contentElement;
5939 this.fire('paper-dropdown-open');
5940 } else if (oldOpened != null) {
5941 this.fire('paper-dropdown-close');
5942 }
5943 },
5944 _disabledChanged: function(disabled) {
5945 Polymer.IronControlState._disabledChanged.apply(this, arguments);
5946 if (disabled && this.opened) {
5947 this.close();
5948 }
5949 },
5950 __onIronOverlayCanceled: function(event) {
5951 var uiEvent = event.detail;
5952 var target = Polymer.dom(uiEvent).rootTarget;
5953 var trigger = this.$.trigger;
5954 var path = Polymer.dom(uiEvent).path;
5955 if (path.indexOf(trigger) > -1) {
5956 event.preventDefault();
5957 }
5958 }
5959 });
5960 Object.keys(config).forEach(function(key) {
5961 PaperMenuButton[key] = config[key];
5962 });
5963 Polymer.PaperMenuButton = PaperMenuButton;
5964 })();
5965
5966 Polymer.PaperInkyFocusBehaviorImpl = {
5967 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ],
5968 _focusedChanged: function(receivedFocusFromKeyboard) {
5969 if (receivedFocusFromKeyboard) {
5970 this.ensureRipple();
5971 }
5972 if (this.hasRipple()) {
5973 this._ripple.holdDown = receivedFocusFromKeyboard;
5974 }
5975 },
5976 _createRipple: function() {
5977 var ripple = Polymer.PaperRippleBehavior._createRipple();
5978 ripple.id = 'ink';
5979 ripple.setAttribute('center', '');
5980 ripple.classList.add('circle');
5981 return ripple;
5982 }
5983 };
5984
5985 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ];
5986
9544 Polymer({ 5987 Polymer({
9545 5988 is: 'paper-icon-button',
9546 is: 'opaque-animation', 5989 hostAttributes: {
9547 5990 role: 'button',
9548 behaviors: [ 5991 tabindex: '0'
9549 Polymer.NeonAnimationBehavior 5992 },
9550 ], 5993 behaviors: [ Polymer.PaperInkyFocusBehavior ],
9551 5994 properties: {
9552 configure: function(config) { 5995 src: {
9553 var node = config.node; 5996 type: String
9554 this._effect = new KeyframeEffect(node, [ 5997 },
9555 {'opacity': '1'}, 5998 icon: {
9556 {'opacity': '1'} 5999 type: String
9557 ], this.timingFromConfig(config)); 6000 },
9558 node.style.opacity = '0'; 6001 alt: {
9559 return this._effect; 6002 type: String,
9560 }, 6003 observer: "_altChanged"
9561 6004 }
9562 complete: function(config) { 6005 },
9563 config.node.style.opacity = ''; 6006 _altChanged: function(newValue, oldValue) {
9564 } 6007 var label = this.getAttribute('aria-label');
9565 6008 if (!label || oldValue == label) {
9566 }); 6009 this.setAttribute('aria-label', newValue);
9567 (function() { 6010 }
9568 'use strict'; 6011 }
9569 // Used to calculate the scroll direction during touch events. 6012 });
9570 var LAST_TOUCH_POSITION = { 6013
9571 pageX: 0,
9572 pageY: 0
9573 };
9574 // Used to avoid computing event.path and filter scrollable nodes (better pe rf).
9575 var ROOT_TARGET = null;
9576 var SCROLLABLE_NODES = [];
9577
9578 /**
9579 * The IronDropdownScrollManager is intended to provide a central source
9580 * of authority and control over which elements in a document are currently
9581 * allowed to scroll.
9582 */
9583
9584 Polymer.IronDropdownScrollManager = {
9585
9586 /**
9587 * The current element that defines the DOM boundaries of the
9588 * scroll lock. This is always the most recently locking element.
9589 */
9590 get currentLockingElement() {
9591 return this._lockingElements[this._lockingElements.length - 1];
9592 },
9593
9594 /**
9595 * Returns true if the provided element is "scroll locked", which is to
9596 * say that it cannot be scrolled via pointer or keyboard interactions.
9597 *
9598 * @param {HTMLElement} element An HTML element instance which may or may
9599 * not be scroll locked.
9600 */
9601 elementIsScrollLocked: function(element) {
9602 var currentLockingElement = this.currentLockingElement;
9603
9604 if (currentLockingElement === undefined)
9605 return false;
9606
9607 var scrollLocked;
9608
9609 if (this._hasCachedLockedElement(element)) {
9610 return true;
9611 }
9612
9613 if (this._hasCachedUnlockedElement(element)) {
9614 return false;
9615 }
9616
9617 scrollLocked = !!currentLockingElement &&
9618 currentLockingElement !== element &&
9619 !this._composedTreeContains(currentLockingElement, element);
9620
9621 if (scrollLocked) {
9622 this._lockedElementCache.push(element);
9623 } else {
9624 this._unlockedElementCache.push(element);
9625 }
9626
9627 return scrollLocked;
9628 },
9629
9630 /**
9631 * Push an element onto the current scroll lock stack. The most recently
9632 * pushed element and its children will be considered scrollable. All
9633 * other elements will not be scrollable.
9634 *
9635 * Scroll locking is implemented as a stack so that cases such as
9636 * dropdowns within dropdowns are handled well.
9637 *
9638 * @param {HTMLElement} element The element that should lock scroll.
9639 */
9640 pushScrollLock: function(element) {
9641 // Prevent pushing the same element twice
9642 if (this._lockingElements.indexOf(element) >= 0) {
9643 return;
9644 }
9645
9646 if (this._lockingElements.length === 0) {
9647 this._lockScrollInteractions();
9648 }
9649
9650 this._lockingElements.push(element);
9651
9652 this._lockedElementCache = [];
9653 this._unlockedElementCache = [];
9654 },
9655
9656 /**
9657 * Remove an element from the scroll lock stack. The element being
9658 * removed does not need to be the most recently pushed element. However,
9659 * the scroll lock constraints only change when the most recently pushed
9660 * element is removed.
9661 *
9662 * @param {HTMLElement} element The element to remove from the scroll
9663 * lock stack.
9664 */
9665 removeScrollLock: function(element) {
9666 var index = this._lockingElements.indexOf(element);
9667
9668 if (index === -1) {
9669 return;
9670 }
9671
9672 this._lockingElements.splice(index, 1);
9673
9674 this._lockedElementCache = [];
9675 this._unlockedElementCache = [];
9676
9677 if (this._lockingElements.length === 0) {
9678 this._unlockScrollInteractions();
9679 }
9680 },
9681
9682 _lockingElements: [],
9683
9684 _lockedElementCache: null,
9685
9686 _unlockedElementCache: null,
9687
9688 _hasCachedLockedElement: function(element) {
9689 return this._lockedElementCache.indexOf(element) > -1;
9690 },
9691
9692 _hasCachedUnlockedElement: function(element) {
9693 return this._unlockedElementCache.indexOf(element) > -1;
9694 },
9695
9696 _composedTreeContains: function(element, child) {
9697 // NOTE(cdata): This method iterates over content elements and their
9698 // corresponding distributed nodes to implement a contains-like method
9699 // that pierces through the composed tree of the ShadowDOM. Results of
9700 // this operation are cached (elsewhere) on a per-scroll-lock basis, to
9701 // guard against potentially expensive lookups happening repeatedly as
9702 // a user scrolls / touchmoves.
9703 var contentElements;
9704 var distributedNodes;
9705 var contentIndex;
9706 var nodeIndex;
9707
9708 if (element.contains(child)) {
9709 return true;
9710 }
9711
9712 contentElements = Polymer.dom(element).querySelectorAll('content');
9713
9714 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI ndex) {
9715
9716 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr ibutedNodes();
9717
9718 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
9719
9720 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
9721 return true;
9722 }
9723 }
9724 }
9725
9726 return false;
9727 },
9728
9729 _scrollInteractionHandler: function(event) {
9730 // Avoid canceling an event with cancelable=false, e.g. scrolling is in
9731 // progress and cannot be interrupted.
9732 if (event.cancelable && this._shouldPreventScrolling(event)) {
9733 event.preventDefault();
9734 }
9735 // If event has targetTouches (touch event), update last touch position.
9736 if (event.targetTouches) {
9737 var touch = event.targetTouches[0];
9738 LAST_TOUCH_POSITION.pageX = touch.pageX;
9739 LAST_TOUCH_POSITION.pageY = touch.pageY;
9740 }
9741 },
9742
9743 _lockScrollInteractions: function() {
9744 this._boundScrollHandler = this._boundScrollHandler ||
9745 this._scrollInteractionHandler.bind(this);
9746 // Modern `wheel` event for mouse wheel scrolling:
9747 document.addEventListener('wheel', this._boundScrollHandler, true);
9748 // Older, non-standard `mousewheel` event for some FF:
9749 document.addEventListener('mousewheel', this._boundScrollHandler, true);
9750 // IE:
9751 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr ue);
9752 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
9753 document.addEventListener('touchstart', this._boundScrollHandler, true);
9754 // Mobile devices can scroll on touch move:
9755 document.addEventListener('touchmove', this._boundScrollHandler, true);
9756 },
9757
9758 _unlockScrollInteractions: function() {
9759 document.removeEventListener('wheel', this._boundScrollHandler, true);
9760 document.removeEventListener('mousewheel', this._boundScrollHandler, tru e);
9761 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
9762 document.removeEventListener('touchstart', this._boundScrollHandler, tru e);
9763 document.removeEventListener('touchmove', this._boundScrollHandler, true );
9764 },
9765
9766 /**
9767 * Returns true if the event causes scroll outside the current locking
9768 * element, e.g. pointer/keyboard interactions, or scroll "leaking"
9769 * outside the locking element when it is already at its scroll boundaries .
9770 * @param {!Event} event
9771 * @return {boolean}
9772 * @private
9773 */
9774 _shouldPreventScrolling: function(event) {
9775
9776 // Update if root target changed. For touch events, ensure we don't
9777 // update during touchmove.
9778 var target = Polymer.dom(event).rootTarget;
9779 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
9780 ROOT_TARGET = target;
9781 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
9782 }
9783
9784 // Prevent event if no scrollable nodes.
9785 if (!SCROLLABLE_NODES.length) {
9786 return true;
9787 }
9788 // Don't prevent touchstart event inside the locking element when it has
9789 // scrollable nodes.
9790 if (event.type === 'touchstart') {
9791 return false;
9792 }
9793 // Get deltaX/Y.
9794 var info = this._getScrollInfo(event);
9795 // Prevent if there is no child that can scroll.
9796 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta Y);
9797 },
9798
9799 /**
9800 * Returns an array of scrollable nodes up to the current locking element,
9801 * which is included too if scrollable.
9802 * @param {!Array<Node>} nodes
9803 * @return {Array<Node>} scrollables
9804 * @private
9805 */
9806 _getScrollableNodes: function(nodes) {
9807 var scrollables = [];
9808 var lockingIndex = nodes.indexOf(this.currentLockingElement);
9809 // Loop from root target to locking element (included).
9810 for (var i = 0; i <= lockingIndex; i++) {
9811 var node = nodes[i];
9812 // Skip document fragments.
9813 if (node.nodeType === 11) {
9814 continue;
9815 }
9816 // Check inline style before checking computed style.
9817 var style = node.style;
9818 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
9819 style = window.getComputedStyle(node);
9820 }
9821 if (style.overflow === 'scroll' || style.overflow === 'auto') {
9822 scrollables.push(node);
9823 }
9824 }
9825 return scrollables;
9826 },
9827
9828 /**
9829 * Returns the node that is scrolling. If there is no scrolling,
9830 * returns undefined.
9831 * @param {!Array<Node>} nodes
9832 * @param {number} deltaX Scroll delta on the x-axis
9833 * @param {number} deltaY Scroll delta on the y-axis
9834 * @return {Node|undefined}
9835 * @private
9836 */
9837 _getScrollingNode: function(nodes, deltaX, deltaY) {
9838 // No scroll.
9839 if (!deltaX && !deltaY) {
9840 return;
9841 }
9842 // Check only one axis according to where there is more scroll.
9843 // Prefer vertical to horizontal.
9844 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
9845 for (var i = 0; i < nodes.length; i++) {
9846 var node = nodes[i];
9847 var canScroll = false;
9848 if (verticalScroll) {
9849 // delta < 0 is scroll up, delta > 0 is scroll down.
9850 canScroll = deltaY < 0 ? node.scrollTop > 0 :
9851 node.scrollTop < node.scrollHeight - node.clientHeight;
9852 } else {
9853 // delta < 0 is scroll left, delta > 0 is scroll right.
9854 canScroll = deltaX < 0 ? node.scrollLeft > 0 :
9855 node.scrollLeft < node.scrollWidth - node.clientWidth;
9856 }
9857 if (canScroll) {
9858 return node;
9859 }
9860 }
9861 },
9862
9863 /**
9864 * Returns scroll `deltaX` and `deltaY`.
9865 * @param {!Event} event The scroll event
9866 * @return {{
9867 * deltaX: number The x-axis scroll delta (positive: scroll right,
9868 * negative: scroll left, 0: no scroll),
9869 * deltaY: number The y-axis scroll delta (positive: scroll down,
9870 * negative: scroll up, 0: no scroll)
9871 * }} info
9872 * @private
9873 */
9874 _getScrollInfo: function(event) {
9875 var info = {
9876 deltaX: event.deltaX,
9877 deltaY: event.deltaY
9878 };
9879 // Already available.
9880 if ('deltaX' in event) {
9881 // do nothing, values are already good.
9882 }
9883 // Safari has scroll info in `wheelDeltaX/Y`.
9884 else if ('wheelDeltaX' in event) {
9885 info.deltaX = -event.wheelDeltaX;
9886 info.deltaY = -event.wheelDeltaY;
9887 }
9888 // Firefox has scroll info in `detail` and `axis`.
9889 else if ('axis' in event) {
9890 info.deltaX = event.axis === 1 ? event.detail : 0;
9891 info.deltaY = event.axis === 2 ? event.detail : 0;
9892 }
9893 // On mobile devices, calculate scroll direction.
9894 else if (event.targetTouches) {
9895 var touch = event.targetTouches[0];
9896 // Touch moves from right to left => scrolling goes right.
9897 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
9898 // Touch moves from down to up => scrolling goes down.
9899 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
9900 }
9901 return info;
9902 }
9903 };
9904 })();
9905 (function() {
9906 'use strict';
9907
9908 Polymer({
9909 is: 'iron-dropdown',
9910
9911 behaviors: [
9912 Polymer.IronControlState,
9913 Polymer.IronA11yKeysBehavior,
9914 Polymer.IronOverlayBehavior,
9915 Polymer.NeonAnimationRunnerBehavior
9916 ],
9917
9918 properties: {
9919 /**
9920 * The orientation against which to align the dropdown content
9921 * horizontally relative to the dropdown trigger.
9922 * Overridden from `Polymer.IronFitBehavior`.
9923 */
9924 horizontalAlign: {
9925 type: String,
9926 value: 'left',
9927 reflectToAttribute: true
9928 },
9929
9930 /**
9931 * The orientation against which to align the dropdown content
9932 * vertically relative to the dropdown trigger.
9933 * Overridden from `Polymer.IronFitBehavior`.
9934 */
9935 verticalAlign: {
9936 type: String,
9937 value: 'top',
9938 reflectToAttribute: true
9939 },
9940
9941 /**
9942 * An animation config. If provided, this will be used to animate the
9943 * opening of the dropdown.
9944 */
9945 openAnimationConfig: {
9946 type: Object
9947 },
9948
9949 /**
9950 * An animation config. If provided, this will be used to animate the
9951 * closing of the dropdown.
9952 */
9953 closeAnimationConfig: {
9954 type: Object
9955 },
9956
9957 /**
9958 * If provided, this will be the element that will be focused when
9959 * the dropdown opens.
9960 */
9961 focusTarget: {
9962 type: Object
9963 },
9964
9965 /**
9966 * Set to true to disable animations when opening and closing the
9967 * dropdown.
9968 */
9969 noAnimations: {
9970 type: Boolean,
9971 value: false
9972 },
9973
9974 /**
9975 * By default, the dropdown will constrain scrolling on the page
9976 * to itself when opened.
9977 * Set to true in order to prevent scroll from being constrained
9978 * to the dropdown when it opens.
9979 */
9980 allowOutsideScroll: {
9981 type: Boolean,
9982 value: false
9983 },
9984
9985 /**
9986 * Callback for scroll events.
9987 * @type {Function}
9988 * @private
9989 */
9990 _boundOnCaptureScroll: {
9991 type: Function,
9992 value: function() {
9993 return this._onCaptureScroll.bind(this);
9994 }
9995 }
9996 },
9997
9998 listeners: {
9999 'neon-animation-finish': '_onNeonAnimationFinish'
10000 },
10001
10002 observers: [
10003 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign , verticalOffset, horizontalOffset)'
10004 ],
10005
10006 /**
10007 * The element that is contained by the dropdown, if any.
10008 */
10009 get containedElement() {
10010 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10011 },
10012
10013 /**
10014 * The element that should be focused when the dropdown opens.
10015 * @deprecated
10016 */
10017 get _focusTarget() {
10018 return this.focusTarget || this.containedElement;
10019 },
10020
10021 ready: function() {
10022 // Memoized scrolling position, used to block scrolling outside.
10023 this._scrollTop = 0;
10024 this._scrollLeft = 0;
10025 // Used to perform a non-blocking refit on scroll.
10026 this._refitOnScrollRAF = null;
10027 },
10028
10029 detached: function() {
10030 this.cancelAnimation();
10031 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10032 },
10033
10034 /**
10035 * Called when the value of `opened` changes.
10036 * Overridden from `IronOverlayBehavior`
10037 */
10038 _openedChanged: function() {
10039 if (this.opened && this.disabled) {
10040 this.cancel();
10041 } else {
10042 this.cancelAnimation();
10043 this.sizingTarget = this.containedElement || this.sizingTarget;
10044 this._updateAnimationConfig();
10045 this._saveScrollPosition();
10046 if (this.opened) {
10047 document.addEventListener('scroll', this._boundOnCaptureScroll);
10048 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push ScrollLock(this);
10049 } else {
10050 document.removeEventListener('scroll', this._boundOnCaptureScroll) ;
10051 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10052 }
10053 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments );
10054 }
10055 },
10056
10057 /**
10058 * Overridden from `IronOverlayBehavior`.
10059 */
10060 _renderOpened: function() {
10061 if (!this.noAnimations && this.animationConfig.open) {
10062 this.$.contentWrapper.classList.add('animating');
10063 this.playAnimation('open');
10064 } else {
10065 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments) ;
10066 }
10067 },
10068
10069 /**
10070 * Overridden from `IronOverlayBehavior`.
10071 */
10072 _renderClosed: function() {
10073
10074 if (!this.noAnimations && this.animationConfig.close) {
10075 this.$.contentWrapper.classList.add('animating');
10076 this.playAnimation('close');
10077 } else {
10078 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments) ;
10079 }
10080 },
10081
10082 /**
10083 * Called when animation finishes on the dropdown (when opening or
10084 * closing). Responsible for "completing" the process of opening or
10085 * closing the dropdown by positioning it or setting its display to
10086 * none.
10087 */
10088 _onNeonAnimationFinish: function() {
10089 this.$.contentWrapper.classList.remove('animating');
10090 if (this.opened) {
10091 this._finishRenderOpened();
10092 } else {
10093 this._finishRenderClosed();
10094 }
10095 },
10096
10097 _onCaptureScroll: function() {
10098 if (!this.allowOutsideScroll) {
10099 this._restoreScrollPosition();
10100 } else {
10101 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS crollRAF);
10102 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin d(this));
10103 }
10104 },
10105
10106 /**
10107 * Memoizes the scroll position of the outside scrolling element.
10108 * @private
10109 */
10110 _saveScrollPosition: function() {
10111 if (document.scrollingElement) {
10112 this._scrollTop = document.scrollingElement.scrollTop;
10113 this._scrollLeft = document.scrollingElement.scrollLeft;
10114 } else {
10115 // Since we don't know if is the body or html, get max.
10116 this._scrollTop = Math.max(document.documentElement.scrollTop, docum ent.body.scrollTop);
10117 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc ument.body.scrollLeft);
10118 }
10119 },
10120
10121 /**
10122 * Resets the scroll position of the outside scrolling element.
10123 * @private
10124 */
10125 _restoreScrollPosition: function() {
10126 if (document.scrollingElement) {
10127 document.scrollingElement.scrollTop = this._scrollTop;
10128 document.scrollingElement.scrollLeft = this._scrollLeft;
10129 } else {
10130 // Since we don't know if is the body or html, set both.
10131 document.documentElement.scrollTop = this._scrollTop;
10132 document.documentElement.scrollLeft = this._scrollLeft;
10133 document.body.scrollTop = this._scrollTop;
10134 document.body.scrollLeft = this._scrollLeft;
10135 }
10136 },
10137
10138 /**
10139 * Constructs the final animation config from different properties used
10140 * to configure specific parts of the opening and closing animations.
10141 */
10142 _updateAnimationConfig: function() {
10143 var animations = (this.openAnimationConfig || []).concat(this.closeAni mationConfig || []);
10144 for (var i = 0; i < animations.length; i++) {
10145 animations[i].node = this.containedElement;
10146 }
10147 this.animationConfig = {
10148 open: this.openAnimationConfig,
10149 close: this.closeAnimationConfig
10150 };
10151 },
10152
10153 /**
10154 * Updates the overlay position based on configured horizontal
10155 * and vertical alignment.
10156 */
10157 _updateOverlayPosition: function() {
10158 if (this.isAttached) {
10159 // This triggers iron-resize, and iron-overlay-behavior will call re fit if needed.
10160 this.notifyResize();
10161 }
10162 },
10163
10164 /**
10165 * Apply focus to focusTarget or containedElement
10166 */
10167 _applyFocus: function () {
10168 var focusTarget = this.focusTarget || this.containedElement;
10169 if (focusTarget && this.opened && !this.noAutoFocus) {
10170 focusTarget.focus();
10171 } else {
10172 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
10173 }
10174 }
10175 });
10176 })();
10177 Polymer({
10178
10179 is: 'fade-in-animation',
10180
10181 behaviors: [
10182 Polymer.NeonAnimationBehavior
10183 ],
10184
10185 configure: function(config) {
10186 var node = config.node;
10187 this._effect = new KeyframeEffect(node, [
10188 {'opacity': '0'},
10189 {'opacity': '1'}
10190 ], this.timingFromConfig(config));
10191 return this._effect;
10192 }
10193
10194 });
10195 Polymer({
10196
10197 is: 'fade-out-animation',
10198
10199 behaviors: [
10200 Polymer.NeonAnimationBehavior
10201 ],
10202
10203 configure: function(config) {
10204 var node = config.node;
10205 this._effect = new KeyframeEffect(node, [
10206 {'opacity': '1'},
10207 {'opacity': '0'}
10208 ], this.timingFromConfig(config));
10209 return this._effect;
10210 }
10211
10212 });
10213 Polymer({
10214 is: 'paper-menu-grow-height-animation',
10215
10216 behaviors: [
10217 Polymer.NeonAnimationBehavior
10218 ],
10219
10220 configure: function(config) {
10221 var node = config.node;
10222 var rect = node.getBoundingClientRect();
10223 var height = rect.height;
10224
10225 this._effect = new KeyframeEffect(node, [{
10226 height: (height / 2) + 'px'
10227 }, {
10228 height: height + 'px'
10229 }], this.timingFromConfig(config));
10230
10231 return this._effect;
10232 }
10233 });
10234
10235 Polymer({
10236 is: 'paper-menu-grow-width-animation',
10237
10238 behaviors: [
10239 Polymer.NeonAnimationBehavior
10240 ],
10241
10242 configure: function(config) {
10243 var node = config.node;
10244 var rect = node.getBoundingClientRect();
10245 var width = rect.width;
10246
10247 this._effect = new KeyframeEffect(node, [{
10248 width: (width / 2) + 'px'
10249 }, {
10250 width: width + 'px'
10251 }], this.timingFromConfig(config));
10252
10253 return this._effect;
10254 }
10255 });
10256
10257 Polymer({
10258 is: 'paper-menu-shrink-width-animation',
10259
10260 behaviors: [
10261 Polymer.NeonAnimationBehavior
10262 ],
10263
10264 configure: function(config) {
10265 var node = config.node;
10266 var rect = node.getBoundingClientRect();
10267 var width = rect.width;
10268
10269 this._effect = new KeyframeEffect(node, [{
10270 width: width + 'px'
10271 }, {
10272 width: width - (width / 20) + 'px'
10273 }], this.timingFromConfig(config));
10274
10275 return this._effect;
10276 }
10277 });
10278
10279 Polymer({
10280 is: 'paper-menu-shrink-height-animation',
10281
10282 behaviors: [
10283 Polymer.NeonAnimationBehavior
10284 ],
10285
10286 configure: function(config) {
10287 var node = config.node;
10288 var rect = node.getBoundingClientRect();
10289 var height = rect.height;
10290 var top = rect.top;
10291
10292 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
10293
10294 this._effect = new KeyframeEffect(node, [{
10295 height: height + 'px',
10296 transform: 'translateY(0)'
10297 }, {
10298 height: height / 2 + 'px',
10299 transform: 'translateY(-20px)'
10300 }], this.timingFromConfig(config));
10301
10302 return this._effect;
10303 }
10304 });
10305 (function() {
10306 'use strict';
10307
10308 var config = {
10309 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
10310 MAX_ANIMATION_TIME_MS: 400
10311 };
10312
10313 var PaperMenuButton = Polymer({
10314 is: 'paper-menu-button',
10315
10316 /**
10317 * Fired when the dropdown opens.
10318 *
10319 * @event paper-dropdown-open
10320 */
10321
10322 /**
10323 * Fired when the dropdown closes.
10324 *
10325 * @event paper-dropdown-close
10326 */
10327
10328 behaviors: [
10329 Polymer.IronA11yKeysBehavior,
10330 Polymer.IronControlState
10331 ],
10332
10333 properties: {
10334 /**
10335 * True if the content is currently displayed.
10336 */
10337 opened: {
10338 type: Boolean,
10339 value: false,
10340 notify: true,
10341 observer: '_openedChanged'
10342 },
10343
10344 /**
10345 * The orientation against which to align the menu dropdown
10346 * horizontally relative to the dropdown trigger.
10347 */
10348 horizontalAlign: {
10349 type: String,
10350 value: 'left',
10351 reflectToAttribute: true
10352 },
10353
10354 /**
10355 * The orientation against which to align the menu dropdown
10356 * vertically relative to the dropdown trigger.
10357 */
10358 verticalAlign: {
10359 type: String,
10360 value: 'top',
10361 reflectToAttribute: true
10362 },
10363
10364 /**
10365 * If true, the `horizontalAlign` and `verticalAlign` properties will
10366 * be considered preferences instead of strict requirements when
10367 * positioning the dropdown and may be changed if doing so reduces
10368 * the area of the dropdown falling outside of `fitInto`.
10369 */
10370 dynamicAlign: {
10371 type: Boolean
10372 },
10373
10374 /**
10375 * A pixel value that will be added to the position calculated for the
10376 * given `horizontalAlign`. Use a negative value to offset to the
10377 * left, or a positive value to offset to the right.
10378 */
10379 horizontalOffset: {
10380 type: Number,
10381 value: 0,
10382 notify: true
10383 },
10384
10385 /**
10386 * A pixel value that will be added to the position calculated for the
10387 * given `verticalAlign`. Use a negative value to offset towards the
10388 * top, or a positive value to offset towards the bottom.
10389 */
10390 verticalOffset: {
10391 type: Number,
10392 value: 0,
10393 notify: true
10394 },
10395
10396 /**
10397 * If true, the dropdown will be positioned so that it doesn't overlap
10398 * the button.
10399 */
10400 noOverlap: {
10401 type: Boolean
10402 },
10403
10404 /**
10405 * Set to true to disable animations when opening and closing the
10406 * dropdown.
10407 */
10408 noAnimations: {
10409 type: Boolean,
10410 value: false
10411 },
10412
10413 /**
10414 * Set to true to disable automatically closing the dropdown after
10415 * a selection has been made.
10416 */
10417 ignoreSelect: {
10418 type: Boolean,
10419 value: false
10420 },
10421
10422 /**
10423 * Set to true to enable automatically closing the dropdown after an
10424 * item has been activated, even if the selection did not change.
10425 */
10426 closeOnActivate: {
10427 type: Boolean,
10428 value: false
10429 },
10430
10431 /**
10432 * An animation config. If provided, this will be used to animate the
10433 * opening of the dropdown.
10434 */
10435 openAnimationConfig: {
10436 type: Object,
10437 value: function() {
10438 return [{
10439 name: 'fade-in-animation',
10440 timing: {
10441 delay: 100,
10442 duration: 200
10443 }
10444 }, {
10445 name: 'paper-menu-grow-width-animation',
10446 timing: {
10447 delay: 100,
10448 duration: 150,
10449 easing: config.ANIMATION_CUBIC_BEZIER
10450 }
10451 }, {
10452 name: 'paper-menu-grow-height-animation',
10453 timing: {
10454 delay: 100,
10455 duration: 275,
10456 easing: config.ANIMATION_CUBIC_BEZIER
10457 }
10458 }];
10459 }
10460 },
10461
10462 /**
10463 * An animation config. If provided, this will be used to animate the
10464 * closing of the dropdown.
10465 */
10466 closeAnimationConfig: {
10467 type: Object,
10468 value: function() {
10469 return [{
10470 name: 'fade-out-animation',
10471 timing: {
10472 duration: 150
10473 }
10474 }, {
10475 name: 'paper-menu-shrink-width-animation',
10476 timing: {
10477 delay: 100,
10478 duration: 50,
10479 easing: config.ANIMATION_CUBIC_BEZIER
10480 }
10481 }, {
10482 name: 'paper-menu-shrink-height-animation',
10483 timing: {
10484 duration: 200,
10485 easing: 'ease-in'
10486 }
10487 }];
10488 }
10489 },
10490
10491 /**
10492 * By default, the dropdown will constrain scrolling on the page
10493 * to itself when opened.
10494 * Set to true in order to prevent scroll from being constrained
10495 * to the dropdown when it opens.
10496 */
10497 allowOutsideScroll: {
10498 type: Boolean,
10499 value: false
10500 },
10501
10502 /**
10503 * Whether focus should be restored to the button when the menu closes .
10504 */
10505 restoreFocusOnClose: {
10506 type: Boolean,
10507 value: true
10508 },
10509
10510 /**
10511 * This is the element intended to be bound as the focus target
10512 * for the `iron-dropdown` contained by `paper-menu-button`.
10513 */
10514 _dropdownContent: {
10515 type: Object
10516 }
10517 },
10518
10519 hostAttributes: {
10520 role: 'group',
10521 'aria-haspopup': 'true'
10522 },
10523
10524 listeners: {
10525 'iron-activate': '_onIronActivate',
10526 'iron-select': '_onIronSelect'
10527 },
10528
10529 /**
10530 * The content element that is contained by the menu button, if any.
10531 */
10532 get contentElement() {
10533 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10534 },
10535
10536 /**
10537 * Toggles the drowpdown content between opened and closed.
10538 */
10539 toggle: function() {
10540 if (this.opened) {
10541 this.close();
10542 } else {
10543 this.open();
10544 }
10545 },
10546
10547 /**
10548 * Make the dropdown content appear as an overlay positioned relative
10549 * to the dropdown trigger.
10550 */
10551 open: function() {
10552 if (this.disabled) {
10553 return;
10554 }
10555
10556 this.$.dropdown.open();
10557 },
10558
10559 /**
10560 * Hide the dropdown content.
10561 */
10562 close: function() {
10563 this.$.dropdown.close();
10564 },
10565
10566 /**
10567 * When an `iron-select` event is received, the dropdown should
10568 * automatically close on the assumption that a value has been chosen.
10569 *
10570 * @param {CustomEvent} event A CustomEvent instance with type
10571 * set to `"iron-select"`.
10572 */
10573 _onIronSelect: function(event) {
10574 if (!this.ignoreSelect) {
10575 this.close();
10576 }
10577 },
10578
10579 /**
10580 * Closes the dropdown when an `iron-activate` event is received if
10581 * `closeOnActivate` is true.
10582 *
10583 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'.
10584 */
10585 _onIronActivate: function(event) {
10586 if (this.closeOnActivate) {
10587 this.close();
10588 }
10589 },
10590
10591 /**
10592 * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
10593 * When the dropdown closes, the `paper-menu-button` fires `paper-close` .
10594 *
10595 * @param {boolean} opened True if the dropdown is opened, otherwise fal se.
10596 * @param {boolean} oldOpened The previous value of `opened`.
10597 */
10598 _openedChanged: function(opened, oldOpened) {
10599 if (opened) {
10600 // TODO(cdata): Update this when we can measure changes in distribut ed
10601 // children in an idiomatic way.
10602 // We poke this property in case the element has changed. This will
10603 // cause the focus target for the `iron-dropdown` to be updated as
10604 // necessary:
10605 this._dropdownContent = this.contentElement;
10606 this.fire('paper-dropdown-open');
10607 } else if (oldOpened != null) {
10608 this.fire('paper-dropdown-close');
10609 }
10610 },
10611
10612 /**
10613 * If the dropdown is open when disabled becomes true, close the
10614 * dropdown.
10615 *
10616 * @param {boolean} disabled True if disabled, otherwise false.
10617 */
10618 _disabledChanged: function(disabled) {
10619 Polymer.IronControlState._disabledChanged.apply(this, arguments);
10620 if (disabled && this.opened) {
10621 this.close();
10622 }
10623 },
10624
10625 __onIronOverlayCanceled: function(event) {
10626 var uiEvent = event.detail;
10627 var target = Polymer.dom(uiEvent).rootTarget;
10628 var trigger = this.$.trigger;
10629 var path = Polymer.dom(uiEvent).path;
10630
10631 if (path.indexOf(trigger) > -1) {
10632 event.preventDefault();
10633 }
10634 }
10635 });
10636
10637 Object.keys(config).forEach(function (key) {
10638 PaperMenuButton[key] = config[key];
10639 });
10640
10641 Polymer.PaperMenuButton = PaperMenuButton;
10642 })();
10643 /**
10644 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus.
10645 *
10646 * @polymerBehavior Polymer.PaperInkyFocusBehavior
10647 */
10648 Polymer.PaperInkyFocusBehaviorImpl = {
10649 observers: [
10650 '_focusedChanged(receivedFocusFromKeyboard)'
10651 ],
10652
10653 _focusedChanged: function(receivedFocusFromKeyboard) {
10654 if (receivedFocusFromKeyboard) {
10655 this.ensureRipple();
10656 }
10657 if (this.hasRipple()) {
10658 this._ripple.holdDown = receivedFocusFromKeyboard;
10659 }
10660 },
10661
10662 _createRipple: function() {
10663 var ripple = Polymer.PaperRippleBehavior._createRipple();
10664 ripple.id = 'ink';
10665 ripple.setAttribute('center', '');
10666 ripple.classList.add('circle');
10667 return ripple;
10668 }
10669 };
10670
10671 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
10672 Polymer.PaperInkyFocusBehavior = [
10673 Polymer.IronButtonState,
10674 Polymer.IronControlState,
10675 Polymer.PaperRippleBehavior,
10676 Polymer.PaperInkyFocusBehaviorImpl
10677 ];
10678 Polymer({
10679 is: 'paper-icon-button',
10680
10681 hostAttributes: {
10682 role: 'button',
10683 tabindex: '0'
10684 },
10685
10686 behaviors: [
10687 Polymer.PaperInkyFocusBehavior
10688 ],
10689
10690 properties: {
10691 /**
10692 * The URL of an image for the icon. If the src property is specified,
10693 * the icon property should not be.
10694 */
10695 src: {
10696 type: String
10697 },
10698
10699 /**
10700 * Specifies the icon name or index in the set of icons available in
10701 * the icon's icon set. If the icon property is specified,
10702 * the src property should not be.
10703 */
10704 icon: {
10705 type: String
10706 },
10707
10708 /**
10709 * Specifies the alternate text for the button, for accessibility.
10710 */
10711 alt: {
10712 type: String,
10713 observer: "_altChanged"
10714 }
10715 },
10716
10717 _altChanged: function(newValue, oldValue) {
10718 var label = this.getAttribute('aria-label');
10719
10720 // Don't stomp over a user-set aria-label.
10721 if (!label || oldValue == label) {
10722 this.setAttribute('aria-label', newValue);
10723 }
10724 }
10725 });
10726 // Copyright 2016 The Chromium Authors. All rights reserved. 6014 // Copyright 2016 The Chromium Authors. All rights reserved.
10727 // Use of this source code is governed by a BSD-style license that can be 6015 // Use of this source code is governed by a BSD-style license that can be
10728 // found in the LICENSE file. 6016 // found in the LICENSE file.
10729
10730 /**
10731 * Implements an incremental search field which can be shown and hidden.
10732 * Canonical implementation is <cr-search-field>.
10733 * @polymerBehavior
10734 */
10735 var CrSearchFieldBehavior = { 6017 var CrSearchFieldBehavior = {
10736 properties: { 6018 properties: {
10737 label: { 6019 label: {
10738 type: String, 6020 type: String,
10739 value: '', 6021 value: ''
10740 }, 6022 },
10741
10742 clearLabel: { 6023 clearLabel: {
10743 type: String, 6024 type: String,
10744 value: '', 6025 value: ''
10745 }, 6026 },
10746
10747 showingSearch: { 6027 showingSearch: {
10748 type: Boolean, 6028 type: Boolean,
10749 value: false, 6029 value: false,
10750 notify: true, 6030 notify: true,
10751 observer: 'showingSearchChanged_', 6031 observer: 'showingSearchChanged_',
10752 reflectToAttribute: true 6032 reflectToAttribute: true
10753 }, 6033 },
10754
10755 /** @private */
10756 lastValue_: { 6034 lastValue_: {
10757 type: String, 6035 type: String,
10758 value: '', 6036 value: ''
10759 }, 6037 }
10760 }, 6038 },
10761
10762 /**
10763 * @abstract
10764 * @return {!HTMLInputElement} The input field element the behavior should
10765 * use.
10766 */
10767 getSearchInput: function() {}, 6039 getSearchInput: function() {},
10768
10769 /**
10770 * @return {string} The value of the search field.
10771 */
10772 getValue: function() { 6040 getValue: function() {
10773 return this.getSearchInput().value; 6041 return this.getSearchInput().value;
10774 }, 6042 },
10775
10776 /**
10777 * Sets the value of the search field.
10778 * @param {string} value
10779 */
10780 setValue: function(value) { 6043 setValue: function(value) {
10781 // Use bindValue when setting the input value so that changes propagate
10782 // correctly.
10783 this.getSearchInput().bindValue = value; 6044 this.getSearchInput().bindValue = value;
10784 this.onValueChanged_(value); 6045 this.onValueChanged_(value);
10785 }, 6046 },
10786
10787 showAndFocus: function() { 6047 showAndFocus: function() {
10788 this.showingSearch = true; 6048 this.showingSearch = true;
10789 this.focus_(); 6049 this.focus_();
10790 }, 6050 },
10791
10792 /** @private */
10793 focus_: function() { 6051 focus_: function() {
10794 this.getSearchInput().focus(); 6052 this.getSearchInput().focus();
10795 }, 6053 },
10796
10797 onSearchTermSearch: function() { 6054 onSearchTermSearch: function() {
10798 this.onValueChanged_(this.getValue()); 6055 this.onValueChanged_(this.getValue());
10799 }, 6056 },
10800
10801 /**
10802 * Updates the internal state of the search field based on a change that has
10803 * already happened.
10804 * @param {string} newValue
10805 * @private
10806 */
10807 onValueChanged_: function(newValue) { 6057 onValueChanged_: function(newValue) {
10808 if (newValue == this.lastValue_) 6058 if (newValue == this.lastValue_) return;
10809 return;
10810
10811 this.fire('search-changed', newValue); 6059 this.fire('search-changed', newValue);
10812 this.lastValue_ = newValue; 6060 this.lastValue_ = newValue;
10813 }, 6061 },
10814
10815 onSearchTermKeydown: function(e) { 6062 onSearchTermKeydown: function(e) {
10816 if (e.key == 'Escape') 6063 if (e.key == 'Escape') this.showingSearch = false;
10817 this.showingSearch = false; 6064 },
10818 },
10819
10820 /** @private */
10821 showingSearchChanged_: function() { 6065 showingSearchChanged_: function() {
10822 if (this.showingSearch) { 6066 if (this.showingSearch) {
10823 this.focus_(); 6067 this.focus_();
10824 return; 6068 return;
10825 } 6069 }
10826
10827 this.setValue(''); 6070 this.setValue('');
10828 this.getSearchInput().blur(); 6071 this.getSearchInput().blur();
10829 } 6072 }
10830 }; 6073 };
6074
10831 (function() { 6075 (function() {
10832 'use strict'; 6076 'use strict';
10833 6077 Polymer.IronA11yAnnouncer = Polymer({
10834 Polymer.IronA11yAnnouncer = Polymer({ 6078 is: 'iron-a11y-announcer',
10835 is: 'iron-a11y-announcer',
10836
10837 properties: {
10838
10839 /**
10840 * The value of mode is used to set the `aria-live` attribute
10841 * for the element that will be announced. Valid values are: `off`,
10842 * `polite` and `assertive`.
10843 */
10844 mode: {
10845 type: String,
10846 value: 'polite'
10847 },
10848
10849 _text: {
10850 type: String,
10851 value: ''
10852 }
10853 },
10854
10855 created: function() {
10856 if (!Polymer.IronA11yAnnouncer.instance) {
10857 Polymer.IronA11yAnnouncer.instance = this;
10858 }
10859
10860 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
10861 },
10862
10863 /**
10864 * Cause a text string to be announced by screen readers.
10865 *
10866 * @param {string} text The text that should be announced.
10867 */
10868 announce: function(text) {
10869 this._text = '';
10870 this.async(function() {
10871 this._text = text;
10872 }, 100);
10873 },
10874
10875 _onIronAnnounce: function(event) {
10876 if (event.detail && event.detail.text) {
10877 this.announce(event.detail.text);
10878 }
10879 }
10880 });
10881
10882 Polymer.IronA11yAnnouncer.instance = null;
10883
10884 Polymer.IronA11yAnnouncer.requestAvailability = function() {
10885 if (!Polymer.IronA11yAnnouncer.instance) {
10886 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
10887 }
10888
10889 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
10890 };
10891 })();
10892 /**
10893 * Singleton IronMeta instance.
10894 */
10895 Polymer.IronValidatableBehaviorMeta = null;
10896
10897 /**
10898 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
10899 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
10900 *
10901 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
10902 * To validate a form imperatively, call the form's `validate()` method, which in turn will
10903 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
10904 * custom element will get a public `validate()`, which
10905 * will return the validity of the element, and a corresponding `invalid` attr ibute,
10906 * which can be used for styling.
10907 *
10908 * To implement the custom validation logic of your element, you must override
10909 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
10910 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
10911 * for an example.
10912 *
10913 * ### Accessibility
10914 *
10915 * Changing the `invalid` property, either manually or by calling `validate()` will update the
10916 * `aria-invalid` attribute.
10917 *
10918 * @demo demo/index.html
10919 * @polymerBehavior
10920 */
10921 Polymer.IronValidatableBehavior = {
10922
10923 properties: { 6079 properties: {
10924 6080 mode: {
10925 /** 6081 type: String,
10926 * Name of the validator to use. 6082 value: 'polite'
10927 */
10928 validator: {
10929 type: String
10930 }, 6083 },
10931 6084 _text: {
10932 /**
10933 * True if the last call to `validate` is invalid.
10934 */
10935 invalid: {
10936 notify: true,
10937 reflectToAttribute: true,
10938 type: Boolean,
10939 value: false
10940 },
10941
10942 /**
10943 * This property is deprecated and should not be used. Use the global
10944 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
10945 */
10946 _validatorMeta: {
10947 type: Object
10948 },
10949
10950 /**
10951 * Namespace for this validator. This property is deprecated and should
10952 * not be used. For all intents and purposes, please consider it a
10953 * read-only, config-time property.
10954 */
10955 validatorType: {
10956 type: String,
10957 value: 'validator'
10958 },
10959
10960 _validator: {
10961 type: Object,
10962 computed: '__computeValidator(validator)'
10963 }
10964 },
10965
10966 observers: [
10967 '_invalidChanged(invalid)'
10968 ],
10969
10970 registered: function() {
10971 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
10972 },
10973
10974 _invalidChanged: function() {
10975 if (this.invalid) {
10976 this.setAttribute('aria-invalid', 'true');
10977 } else {
10978 this.removeAttribute('aria-invalid');
10979 }
10980 },
10981
10982 /**
10983 * @return {boolean} True if the validator `validator` exists.
10984 */
10985 hasValidator: function() {
10986 return this._validator != null;
10987 },
10988
10989 /**
10990 * Returns true if the `value` is valid, and updates `invalid`. If you want
10991 * your element to have custom validation logic, do not override this method ;
10992 * override `_getValidity(value)` instead.
10993
10994 * @param {Object} value The value to be validated. By default, it is passed
10995 * to the validator's `validate()` function, if a validator is set.
10996 * @return {boolean} True if `value` is valid.
10997 */
10998 validate: function(value) {
10999 this.invalid = !this._getValidity(value);
11000 return !this.invalid;
11001 },
11002
11003 /**
11004 * Returns true if `value` is valid. By default, it is passed
11005 * to the validator's `validate()` function, if a validator is set. You
11006 * should override this method if you want to implement custom validity
11007 * logic for your element.
11008 *
11009 * @param {Object} value The value to be validated.
11010 * @return {boolean} True if `value` is valid.
11011 */
11012
11013 _getValidity: function(value) {
11014 if (this.hasValidator()) {
11015 return this._validator.validate(value);
11016 }
11017 return true;
11018 },
11019
11020 __computeValidator: function() {
11021 return Polymer.IronValidatableBehaviorMeta &&
11022 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
11023 }
11024 };
11025 /*
11026 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
11027 to `<input>`.
11028
11029 ### Two-way binding
11030
11031 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
11032
11033 <input value="{{myValue::input}}">
11034
11035 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
11036 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
11037
11038 <input is="iron-input" bind-value="{{myValue}}">
11039
11040 ### Custom validators
11041
11042 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
11043
11044 <input is="iron-input" validator="my-custom-validator">
11045
11046 ### Stopping invalid input
11047
11048 It may be desirable to only allow users to enter certain characters. You can use the
11049 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
11050 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
11051
11052 \x3c!-- only allow characters that match [0-9] --\x3e
11053 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
11054
11055 @hero hero.svg
11056 @demo demo/index.html
11057 */
11058
11059 Polymer({
11060
11061 is: 'iron-input',
11062
11063 extends: 'input',
11064
11065 behaviors: [
11066 Polymer.IronValidatableBehavior
11067 ],
11068
11069 properties: {
11070
11071 /**
11072 * Use this property instead of `value` for two-way data binding.
11073 */
11074 bindValue: {
11075 observer: '_bindValueChanged',
11076 type: String
11077 },
11078
11079 /**
11080 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
11081 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
11082 * Pasted input will have each character checked individually; if any char acter
11083 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
11084 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
11085 */
11086 preventInvalidInput: {
11087 type: Boolean
11088 },
11089
11090 /**
11091 * Regular expression that list the characters allowed as input.
11092 * This pattern represents the allowed characters for the field; as the us er inputs text,
11093 * each individual character will be checked against the pattern (rather t han checking
11094 * the entire value as a whole). The recommended format should be a list o f allowed characters;
11095 * for example, `[a-zA-Z0-9.+-!;:]`
11096 */
11097 allowedPattern: {
11098 type: String,
11099 observer: "_allowedPatternChanged"
11100 },
11101
11102 _previousValidInput: {
11103 type: String, 6085 type: String,
11104 value: '' 6086 value: ''
11105 }, 6087 }
11106 6088 },
11107 _patternAlreadyChecked: {
11108 type: Boolean,
11109 value: false
11110 }
11111
11112 },
11113
11114 listeners: {
11115 'input': '_onInput',
11116 'keypress': '_onKeypress'
11117 },
11118
11119 /** @suppress {checkTypes} */
11120 registered: function() {
11121 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
11122 if (!this._canDispatchEventOnDisabled()) {
11123 this._origDispatchEvent = this.dispatchEvent;
11124 this.dispatchEvent = this._dispatchEventFirefoxIE;
11125 }
11126 },
11127
11128 created: function() { 6089 created: function() {
11129 Polymer.IronA11yAnnouncer.requestAvailability(); 6090 if (!Polymer.IronA11yAnnouncer.instance) {
11130 }, 6091 Polymer.IronA11yAnnouncer.instance = this;
11131 6092 }
11132 _canDispatchEventOnDisabled: function() { 6093 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind( this));
11133 var input = document.createElement('input'); 6094 },
11134 var canDispatch = false; 6095 announce: function(text) {
11135 input.disabled = true; 6096 this._text = '';
11136 6097 this.async(function() {
11137 input.addEventListener('feature-check-dispatch-event', function() { 6098 this._text = text;
11138 canDispatch = true; 6099 }, 100);
11139 }); 6100 },
11140 6101 _onIronAnnounce: function(event) {
11141 try { 6102 if (event.detail && event.detail.text) {
11142 input.dispatchEvent(new Event('feature-check-dispatch-event')); 6103 this.announce(event.detail.text);
11143 } catch(e) {} 6104 }
11144 6105 }
11145 return canDispatch; 6106 });
11146 }, 6107 Polymer.IronA11yAnnouncer.instance = null;
11147 6108 Polymer.IronA11yAnnouncer.requestAvailability = function() {
11148 _dispatchEventFirefoxIE: function() { 6109 if (!Polymer.IronA11yAnnouncer.instance) {
11149 // Due to Firefox bug, events fired on disabled form controls can throw 6110 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann ouncer');
11150 // errors; furthermore, neither IE nor Firefox will actually dispatch 6111 }
11151 // events from disabled form controls; as such, we toggle disable around 6112 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
11152 // the dispatch to allow notifying properties to notify 6113 };
11153 // See issue #47 for details 6114 })();
11154 var disabled = this.disabled; 6115
11155 this.disabled = false; 6116 Polymer.IronValidatableBehaviorMeta = null;
11156 this._origDispatchEvent.apply(this, arguments); 6117
11157 this.disabled = disabled; 6118 Polymer.IronValidatableBehavior = {
11158 }, 6119 properties: {
11159 6120 validator: {
11160 get _patternRegExp() { 6121 type: String
11161 var pattern; 6122 },
11162 if (this.allowedPattern) { 6123 invalid: {
11163 pattern = new RegExp(this.allowedPattern); 6124 notify: true,
6125 reflectToAttribute: true,
6126 type: Boolean,
6127 value: false
6128 },
6129 _validatorMeta: {
6130 type: Object
6131 },
6132 validatorType: {
6133 type: String,
6134 value: 'validator'
6135 },
6136 _validator: {
6137 type: Object,
6138 computed: '__computeValidator(validator)'
6139 }
6140 },
6141 observers: [ '_invalidChanged(invalid)' ],
6142 registered: function() {
6143 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({
6144 type: 'validator'
6145 });
6146 },
6147 _invalidChanged: function() {
6148 if (this.invalid) {
6149 this.setAttribute('aria-invalid', 'true');
6150 } else {
6151 this.removeAttribute('aria-invalid');
6152 }
6153 },
6154 hasValidator: function() {
6155 return this._validator != null;
6156 },
6157 validate: function(value) {
6158 this.invalid = !this._getValidity(value);
6159 return !this.invalid;
6160 },
6161 _getValidity: function(value) {
6162 if (this.hasValidator()) {
6163 return this._validator.validate(value);
6164 }
6165 return true;
6166 },
6167 __computeValidator: function() {
6168 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio rMeta.byKey(this.validator);
6169 }
6170 };
6171
6172 Polymer({
6173 is: 'iron-input',
6174 "extends": 'input',
6175 behaviors: [ Polymer.IronValidatableBehavior ],
6176 properties: {
6177 bindValue: {
6178 observer: '_bindValueChanged',
6179 type: String
6180 },
6181 preventInvalidInput: {
6182 type: Boolean
6183 },
6184 allowedPattern: {
6185 type: String,
6186 observer: "_allowedPatternChanged"
6187 },
6188 _previousValidInput: {
6189 type: String,
6190 value: ''
6191 },
6192 _patternAlreadyChecked: {
6193 type: Boolean,
6194 value: false
6195 }
6196 },
6197 listeners: {
6198 input: '_onInput',
6199 keypress: '_onKeypress'
6200 },
6201 registered: function() {
6202 if (!this._canDispatchEventOnDisabled()) {
6203 this._origDispatchEvent = this.dispatchEvent;
6204 this.dispatchEvent = this._dispatchEventFirefoxIE;
6205 }
6206 },
6207 created: function() {
6208 Polymer.IronA11yAnnouncer.requestAvailability();
6209 },
6210 _canDispatchEventOnDisabled: function() {
6211 var input = document.createElement('input');
6212 var canDispatch = false;
6213 input.disabled = true;
6214 input.addEventListener('feature-check-dispatch-event', function() {
6215 canDispatch = true;
6216 });
6217 try {
6218 input.dispatchEvent(new Event('feature-check-dispatch-event'));
6219 } catch (e) {}
6220 return canDispatch;
6221 },
6222 _dispatchEventFirefoxIE: function() {
6223 var disabled = this.disabled;
6224 this.disabled = false;
6225 this._origDispatchEvent.apply(this, arguments);
6226 this.disabled = disabled;
6227 },
6228 get _patternRegExp() {
6229 var pattern;
6230 if (this.allowedPattern) {
6231 pattern = new RegExp(this.allowedPattern);
6232 } else {
6233 switch (this.type) {
6234 case 'number':
6235 pattern = /[0-9.,e-]/;
6236 break;
6237 }
6238 }
6239 return pattern;
6240 },
6241 ready: function() {
6242 this.bindValue = this.value;
6243 },
6244 _bindValueChanged: function() {
6245 if (this.value !== this.bindValue) {
6246 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue == = false) ? '' : this.bindValue;
6247 }
6248 this.fire('bind-value-changed', {
6249 value: this.bindValue
6250 });
6251 },
6252 _allowedPatternChanged: function() {
6253 this.preventInvalidInput = this.allowedPattern ? true : false;
6254 },
6255 _onInput: function() {
6256 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
6257 var valid = this._checkPatternValidity();
6258 if (!valid) {
6259 this._announceInvalidCharacter('Invalid string of characters not entered .');
6260 this.value = this._previousValidInput;
6261 }
6262 }
6263 this.bindValue = this.value;
6264 this._previousValidInput = this.value;
6265 this._patternAlreadyChecked = false;
6266 },
6267 _isPrintable: function(event) {
6268 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC ode == 13 || event.keyCode == 27;
6269 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;
6270 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
6271 },
6272 _onKeypress: function(event) {
6273 if (!this.preventInvalidInput && this.type !== 'number') {
6274 return;
6275 }
6276 var regexp = this._patternRegExp;
6277 if (!regexp) {
6278 return;
6279 }
6280 if (event.metaKey || event.ctrlKey || event.altKey) return;
6281 this._patternAlreadyChecked = true;
6282 var thisChar = String.fromCharCode(event.charCode);
6283 if (this._isPrintable(event) && !regexp.test(thisChar)) {
6284 event.preventDefault();
6285 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent ered.');
6286 }
6287 },
6288 _checkPatternValidity: function() {
6289 var regexp = this._patternRegExp;
6290 if (!regexp) {
6291 return true;
6292 }
6293 for (var i = 0; i < this.value.length; i++) {
6294 if (!regexp.test(this.value[i])) {
6295 return false;
6296 }
6297 }
6298 return true;
6299 },
6300 validate: function() {
6301 var valid = this.checkValidity();
6302 if (valid) {
6303 if (this.required && this.value === '') {
6304 valid = false;
6305 } else if (this.hasValidator()) {
6306 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
6307 }
6308 }
6309 this.invalid = !valid;
6310 this.fire('iron-input-validate');
6311 return valid;
6312 },
6313 _announceInvalidCharacter: function(message) {
6314 this.fire('iron-announce', {
6315 text: message
6316 });
6317 }
6318 });
6319
6320 Polymer({
6321 is: 'paper-input-container',
6322 properties: {
6323 noLabelFloat: {
6324 type: Boolean,
6325 value: false
6326 },
6327 alwaysFloatLabel: {
6328 type: Boolean,
6329 value: false
6330 },
6331 attrForValue: {
6332 type: String,
6333 value: 'bind-value'
6334 },
6335 autoValidate: {
6336 type: Boolean,
6337 value: false
6338 },
6339 invalid: {
6340 observer: '_invalidChanged',
6341 type: Boolean,
6342 value: false
6343 },
6344 focused: {
6345 readOnly: true,
6346 type: Boolean,
6347 value: false,
6348 notify: true
6349 },
6350 _addons: {
6351 type: Array
6352 },
6353 _inputHasContent: {
6354 type: Boolean,
6355 value: false
6356 },
6357 _inputSelector: {
6358 type: String,
6359 value: 'input,textarea,.paper-input-input'
6360 },
6361 _boundOnFocus: {
6362 type: Function,
6363 value: function() {
6364 return this._onFocus.bind(this);
6365 }
6366 },
6367 _boundOnBlur: {
6368 type: Function,
6369 value: function() {
6370 return this._onBlur.bind(this);
6371 }
6372 },
6373 _boundOnInput: {
6374 type: Function,
6375 value: function() {
6376 return this._onInput.bind(this);
6377 }
6378 },
6379 _boundValueChanged: {
6380 type: Function,
6381 value: function() {
6382 return this._onValueChanged.bind(this);
6383 }
6384 }
6385 },
6386 listeners: {
6387 'addon-attached': '_onAddonAttached',
6388 'iron-input-validate': '_onIronInputValidate'
6389 },
6390 get _valueChangedEvent() {
6391 return this.attrForValue + '-changed';
6392 },
6393 get _propertyForValue() {
6394 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
6395 },
6396 get _inputElement() {
6397 return Polymer.dom(this).querySelector(this._inputSelector);
6398 },
6399 get _inputElementValue() {
6400 return this._inputElement[this._propertyForValue] || this._inputElement.valu e;
6401 },
6402 ready: function() {
6403 if (!this._addons) {
6404 this._addons = [];
6405 }
6406 this.addEventListener('focus', this._boundOnFocus, true);
6407 this.addEventListener('blur', this._boundOnBlur, true);
6408 },
6409 attached: function() {
6410 if (this.attrForValue) {
6411 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa lueChanged);
6412 } else {
6413 this.addEventListener('input', this._onInput);
6414 }
6415 if (this._inputElementValue != '') {
6416 this._handleValueAndAutoValidate(this._inputElement);
6417 } else {
6418 this._handleValue(this._inputElement);
6419 }
6420 },
6421 _onAddonAttached: function(event) {
6422 if (!this._addons) {
6423 this._addons = [];
6424 }
6425 var target = event.target;
6426 if (this._addons.indexOf(target) === -1) {
6427 this._addons.push(target);
6428 if (this.isAttached) {
6429 this._handleValue(this._inputElement);
6430 }
6431 }
6432 },
6433 _onFocus: function() {
6434 this._setFocused(true);
6435 },
6436 _onBlur: function() {
6437 this._setFocused(false);
6438 this._handleValueAndAutoValidate(this._inputElement);
6439 },
6440 _onInput: function(event) {
6441 this._handleValueAndAutoValidate(event.target);
6442 },
6443 _onValueChanged: function(event) {
6444 this._handleValueAndAutoValidate(event.target);
6445 },
6446 _handleValue: function(inputElement) {
6447 var value = this._inputElementValue;
6448 if (value || value === 0 || inputElement.type === 'number' && !inputElement. checkValidity()) {
6449 this._inputHasContent = true;
6450 } else {
6451 this._inputHasContent = false;
6452 }
6453 this.updateAddons({
6454 inputElement: inputElement,
6455 value: value,
6456 invalid: this.invalid
6457 });
6458 },
6459 _handleValueAndAutoValidate: function(inputElement) {
6460 if (this.autoValidate) {
6461 var valid;
6462 if (inputElement.validate) {
6463 valid = inputElement.validate(this._inputElementValue);
11164 } else { 6464 } else {
11165 switch (this.type) { 6465 valid = inputElement.checkValidity();
11166 case 'number': 6466 }
11167 pattern = /[0-9.,e-]/;
11168 break;
11169 }
11170 }
11171 return pattern;
11172 },
11173
11174 ready: function() {
11175 this.bindValue = this.value;
11176 },
11177
11178 /**
11179 * @suppress {checkTypes}
11180 */
11181 _bindValueChanged: function() {
11182 if (this.value !== this.bindValue) {
11183 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
11184 }
11185 // manually notify because we don't want to notify until after setting val ue
11186 this.fire('bind-value-changed', {value: this.bindValue});
11187 },
11188
11189 _allowedPatternChanged: function() {
11190 // Force to prevent invalid input when an `allowed-pattern` is set
11191 this.preventInvalidInput = this.allowedPattern ? true : false;
11192 },
11193
11194 _onInput: function() {
11195 // Need to validate each of the characters pasted if they haven't
11196 // been validated inside `_onKeypress` already.
11197 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
11198 var valid = this._checkPatternValidity();
11199 if (!valid) {
11200 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
11201 this.value = this._previousValidInput;
11202 }
11203 }
11204
11205 this.bindValue = this.value;
11206 this._previousValidInput = this.value;
11207 this._patternAlreadyChecked = false;
11208 },
11209
11210 _isPrintable: function(event) {
11211 // What a control/printable character is varies wildly based on the browse r.
11212 // - most control characters (arrows, backspace) do not send a `keypress` event
11213 // in Chrome, but the *do* on Firefox
11214 // - in Firefox, when they do send a `keypress` event, control chars have
11215 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
11216 // - printable characters always send a keypress event.
11217 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
11218 // always matches the charCode.
11219 // None of this makes any sense.
11220
11221 // For these keys, ASCII code == browser keycode.
11222 var anyNonPrintable =
11223 (event.keyCode == 8) || // backspace
11224 (event.keyCode == 9) || // tab
11225 (event.keyCode == 13) || // enter
11226 (event.keyCode == 27); // escape
11227
11228 // For these keys, make sure it's a browser keycode and not an ASCII code.
11229 var mozNonPrintable =
11230 (event.keyCode == 19) || // pause
11231 (event.keyCode == 20) || // caps lock
11232 (event.keyCode == 45) || // insert
11233 (event.keyCode == 46) || // delete
11234 (event.keyCode == 144) || // num lock
11235 (event.keyCode == 145) || // scroll lock
11236 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
11237 (event.keyCode > 111 && event.keyCode < 124); // fn keys
11238
11239 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
11240 },
11241
11242 _onKeypress: function(event) {
11243 if (!this.preventInvalidInput && this.type !== 'number') {
11244 return;
11245 }
11246 var regexp = this._patternRegExp;
11247 if (!regexp) {
11248 return;
11249 }
11250
11251 // Handle special keys and backspace
11252 if (event.metaKey || event.ctrlKey || event.altKey)
11253 return;
11254
11255 // Check the pattern either here or in `_onInput`, but not in both.
11256 this._patternAlreadyChecked = true;
11257
11258 var thisChar = String.fromCharCode(event.charCode);
11259 if (this._isPrintable(event) && !regexp.test(thisChar)) {
11260 event.preventDefault();
11261 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
11262 }
11263 },
11264
11265 _checkPatternValidity: function() {
11266 var regexp = this._patternRegExp;
11267 if (!regexp) {
11268 return true;
11269 }
11270 for (var i = 0; i < this.value.length; i++) {
11271 if (!regexp.test(this.value[i])) {
11272 return false;
11273 }
11274 }
11275 return true;
11276 },
11277
11278 /**
11279 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
11280 * then any constraints.
11281 * @return {boolean} True if the value is valid.
11282 */
11283 validate: function() {
11284 // First, check what the browser thinks. Some inputs (like type=number)
11285 // behave weirdly and will set the value to "" if something invalid is
11286 // entered, but will set the validity correctly.
11287 var valid = this.checkValidity();
11288
11289 // Only do extra checking if the browser thought this was valid.
11290 if (valid) {
11291 // Empty, required input is invalid
11292 if (this.required && this.value === '') {
11293 valid = false;
11294 } else if (this.hasValidator()) {
11295 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
11296 }
11297 }
11298
11299 this.invalid = !valid; 6467 this.invalid = !valid;
11300 this.fire('iron-input-validate'); 6468 }
11301 return valid; 6469 this._handleValue(inputElement);
11302 }, 6470 },
11303 6471 _onIronInputValidate: function(event) {
11304 _announceInvalidCharacter: function(message) { 6472 this.invalid = this._inputElement.invalid;
11305 this.fire('iron-announce', { text: message }); 6473 },
11306 } 6474 _invalidChanged: function() {
11307 }); 6475 if (this._addons) {
11308
11309 /*
11310 The `iron-input-validate` event is fired whenever `validate()` is called.
11311 @event iron-input-validate
11312 */
11313 Polymer({
11314 is: 'paper-input-container',
11315
11316 properties: {
11317 /**
11318 * Set to true to disable the floating label. The label disappears when th e input value is
11319 * not null.
11320 */
11321 noLabelFloat: {
11322 type: Boolean,
11323 value: false
11324 },
11325
11326 /**
11327 * Set to true to always float the floating label.
11328 */
11329 alwaysFloatLabel: {
11330 type: Boolean,
11331 value: false
11332 },
11333
11334 /**
11335 * The attribute to listen for value changes on.
11336 */
11337 attrForValue: {
11338 type: String,
11339 value: 'bind-value'
11340 },
11341
11342 /**
11343 * Set to true to auto-validate the input value when it changes.
11344 */
11345 autoValidate: {
11346 type: Boolean,
11347 value: false
11348 },
11349
11350 /**
11351 * True if the input is invalid. This property is set automatically when t he input value
11352 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
11353 */
11354 invalid: {
11355 observer: '_invalidChanged',
11356 type: Boolean,
11357 value: false
11358 },
11359
11360 /**
11361 * True if the input has focus.
11362 */
11363 focused: {
11364 readOnly: true,
11365 type: Boolean,
11366 value: false,
11367 notify: true
11368 },
11369
11370 _addons: {
11371 type: Array
11372 // do not set a default value here intentionally - it will be initialize d lazily when a
11373 // distributed child is attached, which may occur before configuration f or this element
11374 // in polyfill.
11375 },
11376
11377 _inputHasContent: {
11378 type: Boolean,
11379 value: false
11380 },
11381
11382 _inputSelector: {
11383 type: String,
11384 value: 'input,textarea,.paper-input-input'
11385 },
11386
11387 _boundOnFocus: {
11388 type: Function,
11389 value: function() {
11390 return this._onFocus.bind(this);
11391 }
11392 },
11393
11394 _boundOnBlur: {
11395 type: Function,
11396 value: function() {
11397 return this._onBlur.bind(this);
11398 }
11399 },
11400
11401 _boundOnInput: {
11402 type: Function,
11403 value: function() {
11404 return this._onInput.bind(this);
11405 }
11406 },
11407
11408 _boundValueChanged: {
11409 type: Function,
11410 value: function() {
11411 return this._onValueChanged.bind(this);
11412 }
11413 }
11414 },
11415
11416 listeners: {
11417 'addon-attached': '_onAddonAttached',
11418 'iron-input-validate': '_onIronInputValidate'
11419 },
11420
11421 get _valueChangedEvent() {
11422 return this.attrForValue + '-changed';
11423 },
11424
11425 get _propertyForValue() {
11426 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
11427 },
11428
11429 get _inputElement() {
11430 return Polymer.dom(this).querySelector(this._inputSelector);
11431 },
11432
11433 get _inputElementValue() {
11434 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
11435 },
11436
11437 ready: function() {
11438 if (!this._addons) {
11439 this._addons = [];
11440 }
11441 this.addEventListener('focus', this._boundOnFocus, true);
11442 this.addEventListener('blur', this._boundOnBlur, true);
11443 },
11444
11445 attached: function() {
11446 if (this.attrForValue) {
11447 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
11448 } else {
11449 this.addEventListener('input', this._onInput);
11450 }
11451
11452 // Only validate when attached if the input already has a value.
11453 if (this._inputElementValue != '') {
11454 this._handleValueAndAutoValidate(this._inputElement);
11455 } else {
11456 this._handleValue(this._inputElement);
11457 }
11458 },
11459
11460 _onAddonAttached: function(event) {
11461 if (!this._addons) {
11462 this._addons = [];
11463 }
11464 var target = event.target;
11465 if (this._addons.indexOf(target) === -1) {
11466 this._addons.push(target);
11467 if (this.isAttached) {
11468 this._handleValue(this._inputElement);
11469 }
11470 }
11471 },
11472
11473 _onFocus: function() {
11474 this._setFocused(true);
11475 },
11476
11477 _onBlur: function() {
11478 this._setFocused(false);
11479 this._handleValueAndAutoValidate(this._inputElement);
11480 },
11481
11482 _onInput: function(event) {
11483 this._handleValueAndAutoValidate(event.target);
11484 },
11485
11486 _onValueChanged: function(event) {
11487 this._handleValueAndAutoValidate(event.target);
11488 },
11489
11490 _handleValue: function(inputElement) {
11491 var value = this._inputElementValue;
11492
11493 // type="number" hack needed because this.value is empty until it's valid
11494 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
11495 this._inputHasContent = true;
11496 } else {
11497 this._inputHasContent = false;
11498 }
11499
11500 this.updateAddons({ 6476 this.updateAddons({
11501 inputElement: inputElement,
11502 value: value,
11503 invalid: this.invalid 6477 invalid: this.invalid
11504 }); 6478 });
11505 }, 6479 }
11506 6480 },
11507 _handleValueAndAutoValidate: function(inputElement) { 6481 updateAddons: function(state) {
11508 if (this.autoValidate) { 6482 for (var addon, index = 0; addon = this._addons[index]; index++) {
11509 var valid; 6483 addon.update(state);
11510 if (inputElement.validate) { 6484 }
11511 valid = inputElement.validate(this._inputElementValue); 6485 },
11512 } else { 6486 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i nvalid, _inputHasContent) {
11513 valid = inputElement.checkValidity(); 6487 var cls = 'input-content';
11514 } 6488 if (!noLabelFloat) {
11515 this.invalid = !valid; 6489 var label = this.querySelector('label');
11516 } 6490 if (alwaysFloatLabel || _inputHasContent) {
11517 6491 cls += ' label-is-floating';
11518 // Call this last to notify the add-ons. 6492 this.$.labelAndInputContainer.style.position = 'static';
11519 this._handleValue(inputElement); 6493 if (invalid) {
11520 }, 6494 cls += ' is-invalid';
11521 6495 } else if (focused) {
11522 _onIronInputValidate: function(event) { 6496 cls += " label-is-highlighted";
11523 this.invalid = this._inputElement.invalid;
11524 },
11525
11526 _invalidChanged: function() {
11527 if (this._addons) {
11528 this.updateAddons({invalid: this.invalid});
11529 }
11530 },
11531
11532 /**
11533 * Call this to update the state of add-ons.
11534 * @param {Object} state Add-on state.
11535 */
11536 updateAddons: function(state) {
11537 for (var addon, index = 0; addon = this._addons[index]; index++) {
11538 addon.update(state);
11539 }
11540 },
11541
11542 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
11543 var cls = 'input-content';
11544 if (!noLabelFloat) {
11545 var label = this.querySelector('label');
11546
11547 if (alwaysFloatLabel || _inputHasContent) {
11548 cls += ' label-is-floating';
11549 // If the label is floating, ignore any offsets that may have been
11550 // applied from a prefix element.
11551 this.$.labelAndInputContainer.style.position = 'static';
11552
11553 if (invalid) {
11554 cls += ' is-invalid';
11555 } else if (focused) {
11556 cls += " label-is-highlighted";
11557 }
11558 } else {
11559 // When the label is not floating, it should overlap the input element .
11560 if (label) {
11561 this.$.labelAndInputContainer.style.position = 'relative';
11562 }
11563 } 6497 }
11564 } else { 6498 } else {
11565 if (_inputHasContent) { 6499 if (label) {
11566 cls += ' label-is-hidden'; 6500 this.$.labelAndInputContainer.style.position = 'relative';
11567 } 6501 }
11568 } 6502 }
11569 return cls; 6503 } else {
11570 }, 6504 if (_inputHasContent) {
11571 6505 cls += ' label-is-hidden';
11572 _computeUnderlineClass: function(focused, invalid) { 6506 }
11573 var cls = 'underline'; 6507 }
11574 if (invalid) { 6508 return cls;
11575 cls += ' is-invalid'; 6509 },
11576 } else if (focused) { 6510 _computeUnderlineClass: function(focused, invalid) {
11577 cls += ' is-highlighted' 6511 var cls = 'underline';
11578 } 6512 if (invalid) {
11579 return cls; 6513 cls += ' is-invalid';
11580 }, 6514 } else if (focused) {
11581 6515 cls += ' is-highlighted';
11582 _computeAddOnContentClass: function(focused, invalid) { 6516 }
11583 var cls = 'add-on-content'; 6517 return cls;
11584 if (invalid) { 6518 },
11585 cls += ' is-invalid'; 6519 _computeAddOnContentClass: function(focused, invalid) {
11586 } else if (focused) { 6520 var cls = 'add-on-content';
11587 cls += ' is-highlighted' 6521 if (invalid) {
11588 } 6522 cls += ' is-invalid';
11589 return cls; 6523 } else if (focused) {
11590 } 6524 cls += ' is-highlighted';
11591 }); 6525 }
6526 return cls;
6527 }
6528 });
6529
11592 // Copyright 2015 The Chromium Authors. All rights reserved. 6530 // Copyright 2015 The Chromium Authors. All rights reserved.
11593 // Use of this source code is governed by a BSD-style license that can be 6531 // Use of this source code is governed by a BSD-style license that can be
11594 // found in the LICENSE file. 6532 // found in the LICENSE file.
11595
11596 var SearchField = Polymer({ 6533 var SearchField = Polymer({
11597 is: 'cr-search-field', 6534 is: 'cr-search-field',
11598 6535 behaviors: [ CrSearchFieldBehavior ],
11599 behaviors: [CrSearchFieldBehavior],
11600
11601 properties: { 6536 properties: {
11602 value_: String, 6537 value_: String
11603 }, 6538 },
11604
11605 /** @return {!HTMLInputElement} */
11606 getSearchInput: function() { 6539 getSearchInput: function() {
11607 return this.$.searchInput; 6540 return this.$.searchInput;
11608 }, 6541 },
11609
11610 /** @private */
11611 clearSearch_: function() { 6542 clearSearch_: function() {
11612 this.setValue(''); 6543 this.setValue('');
11613 this.getSearchInput().focus(); 6544 this.getSearchInput().focus();
11614 }, 6545 },
11615
11616 /** @private */
11617 toggleShowingSearch_: function() { 6546 toggleShowingSearch_: function() {
11618 this.showingSearch = !this.showingSearch; 6547 this.showingSearch = !this.showingSearch;
11619 }, 6548 }
11620 }); 6549 });
6550
11621 // Copyright 2015 The Chromium Authors. All rights reserved. 6551 // Copyright 2015 The Chromium Authors. All rights reserved.
11622 // Use of this source code is governed by a BSD-style license that can be 6552 // Use of this source code is governed by a BSD-style license that can be
11623 // found in the LICENSE file. 6553 // found in the LICENSE file.
11624
11625 cr.define('downloads', function() { 6554 cr.define('downloads', function() {
11626 var Toolbar = Polymer({ 6555 var Toolbar = Polymer({
11627 is: 'downloads-toolbar', 6556 is: 'downloads-toolbar',
11628
11629 attached: function() { 6557 attached: function() {
11630 // isRTL() only works after i18n_template.js runs to set <html dir>.
11631 this.overflowAlign_ = isRTL() ? 'left' : 'right'; 6558 this.overflowAlign_ = isRTL() ? 'left' : 'right';
11632 }, 6559 },
11633
11634 properties: { 6560 properties: {
11635 downloadsShowing: { 6561 downloadsShowing: {
11636 reflectToAttribute: true, 6562 reflectToAttribute: true,
11637 type: Boolean, 6563 type: Boolean,
11638 value: false, 6564 value: false,
11639 observer: 'downloadsShowingChanged_', 6565 observer: 'downloadsShowingChanged_'
11640 }, 6566 },
11641
11642 overflowAlign_: { 6567 overflowAlign_: {
11643 type: String, 6568 type: String,
11644 value: 'right', 6569 value: 'right'
11645 }, 6570 }
11646 }, 6571 },
11647
11648 listeners: { 6572 listeners: {
11649 'paper-dropdown-close': 'onPaperDropdownClose_', 6573 'paper-dropdown-close': 'onPaperDropdownClose_',
11650 'paper-dropdown-open': 'onPaperDropdownOpen_', 6574 'paper-dropdown-open': 'onPaperDropdownOpen_'
11651 }, 6575 },
11652
11653 /** @return {boolean} Whether removal can be undone. */
11654 canUndo: function() { 6576 canUndo: function() {
11655 return this.$['search-input'] != this.shadowRoot.activeElement; 6577 return this.$['search-input'] != this.shadowRoot.activeElement;
11656 }, 6578 },
11657
11658 /** @return {boolean} Whether "Clear all" should be allowed. */
11659 canClearAll: function() { 6579 canClearAll: function() {
11660 return !this.$['search-input'].getValue() && this.downloadsShowing; 6580 return !this.$['search-input'].getValue() && this.downloadsShowing;
11661 }, 6581 },
11662
11663 onFindCommand: function() { 6582 onFindCommand: function() {
11664 this.$['search-input'].showAndFocus(); 6583 this.$['search-input'].showAndFocus();
11665 }, 6584 },
11666
11667 /** @private */
11668 closeMoreActions_: function() { 6585 closeMoreActions_: function() {
11669 this.$.more.close(); 6586 this.$.more.close();
11670 }, 6587 },
11671
11672 /** @private */
11673 downloadsShowingChanged_: function() { 6588 downloadsShowingChanged_: function() {
11674 this.updateClearAll_(); 6589 this.updateClearAll_();
11675 }, 6590 },
11676
11677 /** @private */
11678 onClearAllTap_: function() { 6591 onClearAllTap_: function() {
11679 assert(this.canClearAll()); 6592 assert(this.canClearAll());
11680 downloads.ActionService.getInstance().clearAll(); 6593 downloads.ActionService.getInstance().clearAll();
11681 }, 6594 },
11682
11683 /** @private */
11684 onPaperDropdownClose_: function() { 6595 onPaperDropdownClose_: function() {
11685 window.removeEventListener('resize', assert(this.boundClose_)); 6596 window.removeEventListener('resize', assert(this.boundClose_));
11686 }, 6597 },
11687
11688 /**
11689 * @param {!Event} e
11690 * @private
11691 */
11692 onItemBlur_: function(e) { 6598 onItemBlur_: function(e) {
11693 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); 6599 var menu = this.$$('paper-menu');
11694 if (menu.items.indexOf(e.relatedTarget) >= 0) 6600 if (menu.items.indexOf(e.relatedTarget) >= 0) return;
11695 return;
11696
11697 this.$.more.restoreFocusOnClose = false; 6601 this.$.more.restoreFocusOnClose = false;
11698 this.closeMoreActions_(); 6602 this.closeMoreActions_();
11699 this.$.more.restoreFocusOnClose = true; 6603 this.$.more.restoreFocusOnClose = true;
11700 }, 6604 },
11701
11702 /** @private */
11703 onPaperDropdownOpen_: function() { 6605 onPaperDropdownOpen_: function() {
11704 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); 6606 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this);
11705 window.addEventListener('resize', this.boundClose_); 6607 window.addEventListener('resize', this.boundClose_);
11706 }, 6608 },
11707
11708 /**
11709 * @param {!CustomEvent} event
11710 * @private
11711 */
11712 onSearchChanged_: function(event) { 6609 onSearchChanged_: function(event) {
11713 downloads.ActionService.getInstance().search( 6610 downloads.ActionService.getInstance().search(event.detail);
11714 /** @type {string} */ (event.detail));
11715 this.updateClearAll_(); 6611 this.updateClearAll_();
11716 }, 6612 },
11717
11718 /** @private */
11719 onOpenDownloadsFolderTap_: function() { 6613 onOpenDownloadsFolderTap_: function() {
11720 downloads.ActionService.getInstance().openDownloadsFolder(); 6614 downloads.ActionService.getInstance().openDownloadsFolder();
11721 }, 6615 },
11722
11723 /** @private */
11724 updateClearAll_: function() { 6616 updateClearAll_: function() {
11725 this.$$('#actions .clear-all').hidden = !this.canClearAll(); 6617 this.$$('#actions .clear-all').hidden = !this.canClearAll();
11726 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); 6618 this.$$('paper-menu .clear-all').hidden = !this.canClearAll();
11727 }, 6619 }
11728 }); 6620 });
11729 6621 return {
11730 return {Toolbar: Toolbar}; 6622 Toolbar: Toolbar
6623 };
11731 }); 6624 });
6625
11732 // Copyright 2015 The Chromium Authors. All rights reserved. 6626 // Copyright 2015 The Chromium Authors. All rights reserved.
11733 // Use of this source code is governed by a BSD-style license that can be 6627 // Use of this source code is governed by a BSD-style license that can be
11734 // found in the LICENSE file. 6628 // found in the LICENSE file.
11735
11736 cr.define('downloads', function() { 6629 cr.define('downloads', function() {
11737 var Manager = Polymer({ 6630 var Manager = Polymer({
11738 is: 'downloads-manager', 6631 is: 'downloads-manager',
11739
11740 properties: { 6632 properties: {
11741 hasDownloads_: { 6633 hasDownloads_: {
11742 observer: 'hasDownloadsChanged_', 6634 observer: 'hasDownloadsChanged_',
11743 type: Boolean, 6635 type: Boolean
11744 }, 6636 },
11745
11746 items_: { 6637 items_: {
11747 type: Array, 6638 type: Array,
11748 value: function() { return []; }, 6639 value: function() {
11749 }, 6640 return [];
11750 }, 6641 }
11751 6642 }
6643 },
11752 hostAttributes: { 6644 hostAttributes: {
11753 loading: true, 6645 loading: true
11754 }, 6646 },
11755
11756 listeners: { 6647 listeners: {
11757 'downloads-list.scroll': 'onListScroll_', 6648 'downloads-list.scroll': 'onListScroll_'
11758 }, 6649 },
11759 6650 observers: [ 'itemsChanged_(items_.*)' ],
11760 observers: [
11761 'itemsChanged_(items_.*)',
11762 ],
11763
11764 /** @private */
11765 clearAll_: function() { 6651 clearAll_: function() {
11766 this.set('items_', []); 6652 this.set('items_', []);
11767 }, 6653 },
11768
11769 /** @private */
11770 hasDownloadsChanged_: function() { 6654 hasDownloadsChanged_: function() {
11771 if (loadTimeData.getBoolean('allowDeletingHistory')) 6655 if (loadTimeData.getBoolean('allowDeletingHistory')) this.$.toolbar.downlo adsShowing = this.hasDownloads_;
11772 this.$.toolbar.downloadsShowing = this.hasDownloads_;
11773
11774 if (this.hasDownloads_) { 6656 if (this.hasDownloads_) {
11775 this.$['downloads-list'].fire('iron-resize'); 6657 this.$['downloads-list'].fire('iron-resize');
11776 } else { 6658 } else {
11777 var isSearching = downloads.ActionService.getInstance().isSearching(); 6659 var isSearching = downloads.ActionService.getInstance().isSearching();
11778 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; 6660 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads';
11779 this.$['no-downloads'].querySelector('span').textContent = 6661 this.$['no-downloads'].querySelector('span').textContent = loadTimeData. getString(messageToShow);
11780 loadTimeData.getString(messageToShow); 6662 }
11781 } 6663 },
11782 },
11783
11784 /**
11785 * @param {number} index
11786 * @param {!Array<!downloads.Data>} list
11787 * @private
11788 */
11789 insertItems_: function(index, list) { 6664 insertItems_: function(index, list) {
11790 this.splice.apply(this, ['items_', index, 0].concat(list)); 6665 this.splice.apply(this, [ 'items_', index, 0 ].concat(list));
11791 this.updateHideDates_(index, index + list.length); 6666 this.updateHideDates_(index, index + list.length);
11792 this.removeAttribute('loading'); 6667 this.removeAttribute('loading');
11793 }, 6668 },
11794
11795 /** @private */
11796 itemsChanged_: function() { 6669 itemsChanged_: function() {
11797 this.hasDownloads_ = this.items_.length > 0; 6670 this.hasDownloads_ = this.items_.length > 0;
11798 }, 6671 },
11799
11800 /**
11801 * @param {Event} e
11802 * @private
11803 */
11804 onCanExecute_: function(e) { 6672 onCanExecute_: function(e) {
11805 e = /** @type {cr.ui.CanExecuteEvent} */(e); 6673 e = e;
11806 switch (e.command.id) { 6674 switch (e.command.id) {
11807 case 'undo-command': 6675 case 'undo-command':
11808 e.canExecute = this.$.toolbar.canUndo(); 6676 e.canExecute = this.$.toolbar.canUndo();
11809 break; 6677 break;
11810 case 'clear-all-command': 6678
11811 e.canExecute = this.$.toolbar.canClearAll(); 6679 case 'clear-all-command':
11812 break; 6680 e.canExecute = this.$.toolbar.canClearAll();
11813 case 'find-command': 6681 break;
11814 e.canExecute = true; 6682
11815 break; 6683 case 'find-command':
11816 } 6684 e.canExecute = true;
11817 }, 6685 break;
11818 6686 }
11819 /** 6687 },
11820 * @param {Event} e
11821 * @private
11822 */
11823 onCommand_: function(e) { 6688 onCommand_: function(e) {
11824 if (e.command.id == 'clear-all-command') 6689 if (e.command.id == 'clear-all-command') downloads.ActionService.getInstan ce().clearAll(); else if (e.command.id == 'undo-command') downloads.ActionServic e.getInstance().undo(); else if (e.command.id == 'find-command') this.$.toolbar. onFindCommand();
11825 downloads.ActionService.getInstance().clearAll(); 6690 },
11826 else if (e.command.id == 'undo-command')
11827 downloads.ActionService.getInstance().undo();
11828 else if (e.command.id == 'find-command')
11829 this.$.toolbar.onFindCommand();
11830 },
11831
11832 /** @private */
11833 onListScroll_: function() { 6691 onListScroll_: function() {
11834 var list = this.$['downloads-list']; 6692 var list = this.$['downloads-list'];
11835 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { 6693 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) {
11836 // Approaching the end of the scrollback. Attempt to load more items.
11837 downloads.ActionService.getInstance().loadMore(); 6694 downloads.ActionService.getInstance().loadMore();
11838 } 6695 }
11839 }, 6696 },
11840
11841 /** @private */
11842 onLoad_: function() { 6697 onLoad_: function() {
11843 cr.ui.decorate('command', cr.ui.Command); 6698 cr.ui.decorate('command', cr.ui.Command);
11844 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); 6699 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
11845 document.addEventListener('command', this.onCommand_.bind(this)); 6700 document.addEventListener('command', this.onCommand_.bind(this));
11846
11847 downloads.ActionService.getInstance().loadMore(); 6701 downloads.ActionService.getInstance().loadMore();
11848 }, 6702 },
11849
11850 /**
11851 * @param {number} index
11852 * @private
11853 */
11854 removeItem_: function(index) { 6703 removeItem_: function(index) {
11855 this.splice('items_', index, 1); 6704 this.splice('items_', index, 1);
11856 this.updateHideDates_(index, index); 6705 this.updateHideDates_(index, index);
11857 this.onListScroll_(); 6706 this.onListScroll_();
11858 }, 6707 },
11859
11860 /**
11861 * @param {number} start
11862 * @param {number} end
11863 * @private
11864 */
11865 updateHideDates_: function(start, end) { 6708 updateHideDates_: function(start, end) {
11866 for (var i = start; i <= end; ++i) { 6709 for (var i = start; i <= end; ++i) {
11867 var current = this.items_[i]; 6710 var current = this.items_[i];
11868 if (!current) 6711 if (!current) continue;
11869 continue;
11870 var prev = this.items_[i - 1]; 6712 var prev = this.items_[i - 1];
11871 current.hideDate = !!prev && prev.date_string == current.date_string; 6713 current.hideDate = !!prev && prev.date_string == current.date_string;
11872 } 6714 }
11873 }, 6715 },
11874
11875 /**
11876 * @param {number} index
11877 * @param {!downloads.Data} data
11878 * @private
11879 */
11880 updateItem_: function(index, data) { 6716 updateItem_: function(index, data) {
11881 this.set('items_.' + index, data); 6717 this.set('items_.' + index, data);
11882 this.updateHideDates_(index, index); 6718 this.updateHideDates_(index, index);
11883 var list = /** @type {!IronListElement} */(this.$['downloads-list']); 6719 var list = this.$['downloads-list'];
11884 list.updateSizeForItem(index); 6720 list.updateSizeForItem(index);
11885 }, 6721 }
11886 }); 6722 });
11887
11888 Manager.clearAll = function() { 6723 Manager.clearAll = function() {
11889 Manager.get().clearAll_(); 6724 Manager.get().clearAll_();
11890 }; 6725 };
11891
11892 /** @return {!downloads.Manager} */
11893 Manager.get = function() { 6726 Manager.get = function() {
11894 return /** @type {!downloads.Manager} */( 6727 return queryRequiredElement('downloads-manager');
11895 queryRequiredElement('downloads-manager')); 6728 };
11896 };
11897
11898 Manager.insertItems = function(index, list) { 6729 Manager.insertItems = function(index, list) {
11899 Manager.get().insertItems_(index, list); 6730 Manager.get().insertItems_(index, list);
11900 }; 6731 };
11901
11902 Manager.onLoad = function() { 6732 Manager.onLoad = function() {
11903 Manager.get().onLoad_(); 6733 Manager.get().onLoad_();
11904 }; 6734 };
11905
11906 Manager.removeItem = function(index) { 6735 Manager.removeItem = function(index) {
11907 Manager.get().removeItem_(index); 6736 Manager.get().removeItem_(index);
11908 }; 6737 };
11909
11910 Manager.updateItem = function(index, data) { 6738 Manager.updateItem = function(index, data) {
11911 Manager.get().updateItem_(index, data); 6739 Manager.get().updateItem_(index, data);
11912 }; 6740 };
11913 6741 return {
11914 return {Manager: Manager}; 6742 Manager: Manager
6743 };
11915 }); 6744 });
6745
11916 // Copyright 2015 The Chromium Authors. All rights reserved. 6746 // Copyright 2015 The Chromium Authors. All rights reserved.
11917 // Use of this source code is governed by a BSD-style license that can be 6747 // Use of this source code is governed by a BSD-style license that can be
11918 // found in the LICENSE file. 6748 // found in the LICENSE file.
11919
11920 window.addEventListener('load', downloads.Manager.onLoad); 6749 window.addEventListener('load', downloads.Manager.onLoad);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698