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

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

Issue 2257723002: MD WebUI: Uglify vulcanized javascript bundles to remove comments/whitespace (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Reparent and rebase Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | chrome/browser/resources/md_history/app.crisper.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (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-disabled="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);
1344 } 642 }
1345 643
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);
1359 } 647 }
1360 648
1361 // Handle click on a link. If the link points to a chrome: or file: url, then 649 [ 'click', 'auxclick' ].forEach(function(eventName) {
1362 // call into the browser to do the navigation.
1363 ['click', 'auxclick'].forEach(function(eventName) {
1364 document.addEventListener(eventName, function(e) { 650 document.addEventListener(eventName, function(e) {
1365 if (e.defaultPrevented) 651 if (e.defaultPrevented) return;
1366 return;
1367
1368 var eventPath = e.path; 652 var eventPath = e.path;
1369 var anchor = null; 653 var anchor = null;
1370 if (eventPath) { 654 if (eventPath) {
1371 for (var i = 0; i < eventPath.length; i++) { 655 for (var i = 0; i < eventPath.length; i++) {
1372 var element = eventPath[i]; 656 var element = eventPath[i];
1373 if (element.tagName === 'A' && element.href) { 657 if (element.tagName === 'A' && element.href) {
1374 anchor = element; 658 anchor = element;
1375 break; 659 break;
1376 } 660 }
1377 } 661 }
1378 } 662 }
1379
1380 // Fallback if Event.path is not available.
1381 var el = e.target; 663 var el = e.target;
1382 if (!anchor && el.nodeType == Node.ELEMENT_NODE && 664 if (!anchor && el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector( 'A, A *')) {
1383 el.webkitMatchesSelector('A, A *')) {
1384 while (el.tagName != 'A') { 665 while (el.tagName != 'A') {
1385 el = el.parentElement; 666 el = el.parentElement;
1386 } 667 }
1387 anchor = el; 668 anchor = el;
1388 } 669 }
1389 670 if (!anchor) return;
1390 if (!anchor) 671 anchor = anchor;
1391 return; 672 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && (e.button == 0 || e.button == 1)) {
1392 673 chrome.send('navigateToUrl', [ anchor.href, anchor.target, e.button, e.alt Key, e.ctrlKey, e.metaKey, e.shiftKey ]);
1393 anchor = /** @type {!HTMLAnchorElement} */(anchor);
1394 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') &&
1395 (e.button == 0 || e.button == 1)) {
1396 chrome.send('navigateToUrl', [
1397 anchor.href,
1398 anchor.target,
1399 e.button,
1400 e.altKey,
1401 e.ctrlKey,
1402 e.metaKey,
1403 e.shiftKey
1404 ]);
1405 e.preventDefault(); 674 e.preventDefault();
1406 } 675 }
1407 }); 676 });
1408 }); 677 });
1409 678
1410 /**
1411 * Creates a new URL which is the old URL with a GET param of key=value.
1412 * @param {string} url The base URL. There is not sanity checking on the URL so
1413 * it must be passed in a proper format.
1414 * @param {string} key The key of the param.
1415 * @param {string} value The value of the param.
1416 * @return {string} The new URL.
1417 */
1418 function appendParam(url, key, value) { 679 function appendParam(url, key, value) {
1419 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); 680 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
1420 681 if (url.indexOf('?') == -1) return url + '?' + param;
1421 if (url.indexOf('?') == -1)
1422 return url + '?' + param;
1423 return url + '&' + param; 682 return url + '&' + param;
1424 } 683 }
1425 684
1426 /**
1427 * Creates an element of a specified type with a specified class name.
1428 * @param {string} type The node type.
1429 * @param {string} className The class name to use.
1430 * @return {Element} The created element.
1431 */
1432 function createElementWithClassName(type, className) { 685 function createElementWithClassName(type, className) {
1433 var elm = document.createElement(type); 686 var elm = document.createElement(type);
1434 elm.className = className; 687 elm.className = className;
1435 return elm; 688 return elm;
1436 } 689 }
1437 690
1438 /**
1439 * webkitTransitionEnd does not always fire (e.g. when animation is aborted
1440 * or when no paint happens during the animation). This function sets up
1441 * a timer and emulate the event if it is not fired when the timer expires.
1442 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
1443 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
1444 * webkitTransitionEnd to happen. If not specified, it is fetched from |el|
1445 * using the transitionDuration style value.
1446 */
1447 function ensureTransitionEndEvent(el, opt_timeOut) { 691 function ensureTransitionEndEvent(el, opt_timeOut) {
1448 if (opt_timeOut === undefined) { 692 if (opt_timeOut === undefined) {
1449 var style = getComputedStyle(el); 693 var style = getComputedStyle(el);
1450 opt_timeOut = parseFloat(style.transitionDuration) * 1000; 694 opt_timeOut = parseFloat(style.transitionDuration) * 1e3;
1451
1452 // Give an additional 50ms buffer for the animation to complete.
1453 opt_timeOut += 50; 695 opt_timeOut += 50;
1454 } 696 }
1455
1456 var fired = false; 697 var fired = false;
1457 el.addEventListener('webkitTransitionEnd', function f(e) { 698 el.addEventListener('webkitTransitionEnd', function f(e) {
1458 el.removeEventListener('webkitTransitionEnd', f); 699 el.removeEventListener('webkitTransitionEnd', f);
1459 fired = true; 700 fired = true;
1460 }); 701 });
1461 window.setTimeout(function() { 702 window.setTimeout(function() {
1462 if (!fired) 703 if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
1463 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
1464 }, opt_timeOut); 704 }, opt_timeOut);
1465 } 705 }
1466 706
1467 /**
1468 * Alias for document.scrollTop getter.
1469 * @param {!HTMLDocument} doc The document node where information will be
1470 * queried from.
1471 * @return {number} The Y document scroll offset.
1472 */
1473 function scrollTopForDocument(doc) { 707 function scrollTopForDocument(doc) {
1474 return doc.documentElement.scrollTop || doc.body.scrollTop; 708 return doc.documentElement.scrollTop || doc.body.scrollTop;
1475 } 709 }
1476 710
1477 /**
1478 * Alias for document.scrollTop setter.
1479 * @param {!HTMLDocument} doc The document node where information will be
1480 * queried from.
1481 * @param {number} value The target Y scroll offset.
1482 */
1483 function setScrollTopForDocument(doc, value) { 711 function setScrollTopForDocument(doc, value) {
1484 doc.documentElement.scrollTop = doc.body.scrollTop = value; 712 doc.documentElement.scrollTop = doc.body.scrollTop = value;
1485 } 713 }
1486 714
1487 /**
1488 * Alias for document.scrollLeft getter.
1489 * @param {!HTMLDocument} doc The document node where information will be
1490 * queried from.
1491 * @return {number} The X document scroll offset.
1492 */
1493 function scrollLeftForDocument(doc) { 715 function scrollLeftForDocument(doc) {
1494 return doc.documentElement.scrollLeft || doc.body.scrollLeft; 716 return doc.documentElement.scrollLeft || doc.body.scrollLeft;
1495 } 717 }
1496 718
1497 /**
1498 * Alias for document.scrollLeft setter.
1499 * @param {!HTMLDocument} doc The document node where information will be
1500 * queried from.
1501 * @param {number} value The target X scroll offset.
1502 */
1503 function setScrollLeftForDocument(doc, value) { 719 function setScrollLeftForDocument(doc, value) {
1504 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; 720 doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
1505 } 721 }
1506 722
1507 /**
1508 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
1509 * @param {string} original The original string.
1510 * @return {string} The string with all the characters mentioned above replaced.
1511 */
1512 function HTMLEscape(original) { 723 function HTMLEscape(original) {
1513 return original.replace(/&/g, '&amp;') 724 return original.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&g t;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
1514 .replace(/</g, '&lt;') 725 }
1515 .replace(/>/g, '&gt;') 726
1516 .replace(/"/g, '&quot;')
1517 .replace(/'/g, '&#39;');
1518 }
1519
1520 /**
1521 * Shortens the provided string (if necessary) to a string of length at most
1522 * |maxLength|.
1523 * @param {string} original The original string.
1524 * @param {number} maxLength The maximum length allowed for the string.
1525 * @return {string} The original string if its length does not exceed
1526 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
1527 * appended.
1528 */
1529 function elide(original, maxLength) { 727 function elide(original, maxLength) {
1530 if (original.length <= maxLength) 728 if (original.length <= maxLength) return original;
1531 return original; 729 return original.substring(0, maxLength - 1) + '…';
1532 return original.substring(0, maxLength - 1) + '\u2026'; 730 }
1533 } 731
1534
1535 /**
1536 * Quote a string so it can be used in a regular expression.
1537 * @param {string} str The source string.
1538 * @return {string} The escaped string.
1539 */
1540 function quoteString(str) { 732 function quoteString(str) {
1541 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); 733 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
1542 } 734 }
1543 735
1544 // <if expr="is_ios"> 736 // <if expr="is_ios">
1545 // Polyfill 'key' in KeyboardEvent for iOS.
1546 // This function is not intended to be complete but should
1547 // be sufficient enough to have iOS work correctly while
1548 // it does not support key yet.
1549 if (!('key' in KeyboardEvent.prototype)) { 737 if (!('key' in KeyboardEvent.prototype)) {
1550 Object.defineProperty(KeyboardEvent.prototype, 'key', { 738 Object.defineProperty(KeyboardEvent.prototype, 'key', {
1551 /** @this {KeyboardEvent} */ 739 get: function() {
1552 get: function () { 740 if (this.keyCode >= 48 && this.keyCode <= 57) return String.fromCharCode(t his.keyCode);
1553 // 0-9 741 if (this.keyCode >= 65 && this.keyCode <= 90) {
1554 if (this.keyCode >= 0x30 && this.keyCode <= 0x39)
1555 return String.fromCharCode(this.keyCode);
1556
1557 // A-Z
1558 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) {
1559 var result = String.fromCharCode(this.keyCode).toLowerCase(); 742 var result = String.fromCharCode(this.keyCode).toLowerCase();
1560 if (this.shiftKey) 743 if (this.shiftKey) result = result.toUpperCase();
1561 result = result.toUpperCase();
1562 return result; 744 return result;
1563 } 745 }
1564 746 switch (this.keyCode) {
1565 // Special characters 747 case 8:
1566 switch(this.keyCode) { 748 return 'Backspace';
1567 case 0x08: return 'Backspace'; 749
1568 case 0x09: return 'Tab'; 750 case 9:
1569 case 0x0d: return 'Enter'; 751 return 'Tab';
1570 case 0x10: return 'Shift'; 752
1571 case 0x11: return 'Control'; 753 case 13:
1572 case 0x12: return 'Alt'; 754 return 'Enter';
1573 case 0x1b: return 'Escape'; 755
1574 case 0x20: return ' '; 756 case 16:
1575 case 0x21: return 'PageUp'; 757 return 'Shift';
1576 case 0x22: return 'PageDown'; 758
1577 case 0x23: return 'End'; 759 case 17:
1578 case 0x24: return 'Home'; 760 return 'Control';
1579 case 0x25: return 'ArrowLeft'; 761
1580 case 0x26: return 'ArrowUp'; 762 case 18:
1581 case 0x27: return 'ArrowRight'; 763 return 'Alt';
1582 case 0x28: return 'ArrowDown'; 764
1583 case 0x2d: return 'Insert'; 765 case 27:
1584 case 0x2e: return 'Delete'; 766 return 'Escape';
1585 case 0x5b: return 'Meta'; 767
1586 case 0x70: return 'F1'; 768 case 32:
1587 case 0x71: return 'F2'; 769 return ' ';
1588 case 0x72: return 'F3'; 770
1589 case 0x73: return 'F4'; 771 case 33:
1590 case 0x74: return 'F5'; 772 return 'PageUp';
1591 case 0x75: return 'F6'; 773
1592 case 0x76: return 'F7'; 774 case 34:
1593 case 0x77: return 'F8'; 775 return 'PageDown';
1594 case 0x78: return 'F9'; 776
1595 case 0x79: return 'F10'; 777 case 35:
1596 case 0x7a: return 'F11'; 778 return 'End';
1597 case 0x7b: return 'F12'; 779
1598 case 0xbb: return '='; 780 case 36:
1599 case 0xbd: return '-'; 781 return 'Home';
1600 case 0xdb: return '['; 782
1601 case 0xdd: return ']'; 783 case 37:
784 return 'ArrowLeft';
785
786 case 38:
787 return 'ArrowUp';
788
789 case 39:
790 return 'ArrowRight';
791
792 case 40:
793 return 'ArrowDown';
794
795 case 45:
796 return 'Insert';
797
798 case 46:
799 return 'Delete';
800
801 case 91:
802 return 'Meta';
803
804 case 112:
805 return 'F1';
806
807 case 113:
808 return 'F2';
809
810 case 114:
811 return 'F3';
812
813 case 115:
814 return 'F4';
815
816 case 116:
817 return 'F5';
818
819 case 117:
820 return 'F6';
821
822 case 118:
823 return 'F7';
824
825 case 119:
826 return 'F8';
827
828 case 120:
829 return 'F9';
830
831 case 121:
832 return 'F10';
833
834 case 122:
835 return 'F11';
836
837 case 123:
838 return 'F12';
839
840 case 187:
841 return '=';
842
843 case 189:
844 return '-';
845
846 case 219:
847 return '[';
848
849 case 221:
850 return ']';
1602 } 851 }
1603 return 'Unidentified'; 852 return 'Unidentified';
1604 } 853 }
1605 }); 854 });
1606 } else { 855 } else {
1607 window.console.log("KeyboardEvent.Key polyfill not required"); 856 window.console.log("KeyboardEvent.Key polyfill not required");
1608 } 857 }
858
1609 // </if> /* is_ios */ 859 // </if> /* is_ios */
1610 /** 860 Polymer.IronResizableBehavior = {
1611 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to 861 properties: {
1612 * coordinate the flow of resize events between "resizers" (elements that cont rol the 862 _parentResizable: {
1613 * size or hidden state of their children) and "resizables" (elements that nee d to be 863 type: Object,
1614 * notified when they are resized or un-hidden by their parents in order to ta ke 864 observer: '_parentResizableChanged'
1615 * action on their new measurements). 865 },
1616 * 866 _notifyingDescendant: {
1617 * Elements that perform measurement should add the `IronResizableBehavior` be havior to 867 type: Boolean,
1618 * their element definition and listen for the `iron-resize` event on themselv es. 868 value: false
1619 * This event will be fired when they become showing after having been hidden, 869 }
1620 * when they are resized explicitly by another resizable, or when the window h as been 870 },
1621 * resized. 871 listeners: {
1622 * 872 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
1623 * Note, the `iron-resize` event is non-bubbling. 873 },
1624 * 874 created: function() {
1625 * @polymerBehavior Polymer.IronResizableBehavior 875 this._interestedResizables = [];
1626 * @demo demo/index.html 876 this._boundNotifyResize = this.notifyResize.bind(this);
1627 **/ 877 },
1628 Polymer.IronResizableBehavior = { 878 attached: function() {
879 this.fire('iron-request-resize-notifications', null, {
880 node: this,
881 bubbles: true,
882 cancelable: true
883 });
884 if (!this._parentResizable) {
885 window.addEventListener('resize', this._boundNotifyResize);
886 this.notifyResize();
887 }
888 },
889 detached: function() {
890 if (this._parentResizable) {
891 this._parentResizable.stopResizeNotificationsFor(this);
892 } else {
893 window.removeEventListener('resize', this._boundNotifyResize);
894 }
895 this._parentResizable = null;
896 },
897 notifyResize: function() {
898 if (!this.isAttached) {
899 return;
900 }
901 this._interestedResizables.forEach(function(resizable) {
902 if (this.resizerShouldNotify(resizable)) {
903 this._notifyDescendant(resizable);
904 }
905 }, this);
906 this._fireResize();
907 },
908 assignParentResizable: function(parentResizable) {
909 this._parentResizable = parentResizable;
910 },
911 stopResizeNotificationsFor: function(target) {
912 var index = this._interestedResizables.indexOf(target);
913 if (index > -1) {
914 this._interestedResizables.splice(index, 1);
915 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
916 }
917 },
918 resizerShouldNotify: function(element) {
919 return true;
920 },
921 _onDescendantIronResize: function(event) {
922 if (this._notifyingDescendant) {
923 event.stopPropagation();
924 return;
925 }
926 if (!Polymer.Settings.useShadow) {
927 this._fireResize();
928 }
929 },
930 _fireResize: function() {
931 this.fire('iron-resize', null, {
932 node: this,
933 bubbles: false
934 });
935 },
936 _onIronRequestResizeNotifications: function(event) {
937 var target = event.path ? event.path[0] : event.target;
938 if (target === this) {
939 return;
940 }
941 if (this._interestedResizables.indexOf(target) === -1) {
942 this._interestedResizables.push(target);
943 this.listen(target, 'iron-resize', '_onDescendantIronResize');
944 }
945 target.assignParentResizable(this);
946 this._notifyDescendant(target);
947 event.stopPropagation();
948 },
949 _parentResizableChanged: function(parentResizable) {
950 if (parentResizable) {
951 window.removeEventListener('resize', this._boundNotifyResize);
952 }
953 },
954 _notifyDescendant: function(descendant) {
955 if (!this.isAttached) {
956 return;
957 }
958 this._notifyingDescendant = true;
959 descendant.notifyResize();
960 this._notifyingDescendant = false;
961 }
962 };
963
964 (function() {
965 'use strict';
966 var KEY_IDENTIFIER = {
967 'U+0008': 'backspace',
968 'U+0009': 'tab',
969 'U+001B': 'esc',
970 'U+0020': 'space',
971 'U+007F': 'del'
972 };
973 var KEY_CODE = {
974 8: 'backspace',
975 9: 'tab',
976 13: 'enter',
977 27: 'esc',
978 33: 'pageup',
979 34: 'pagedown',
980 35: 'end',
981 36: 'home',
982 32: 'space',
983 37: 'left',
984 38: 'up',
985 39: 'right',
986 40: 'down',
987 46: 'del',
988 106: '*'
989 };
990 var MODIFIER_KEYS = {
991 shift: 'shiftKey',
992 ctrl: 'ctrlKey',
993 alt: 'altKey',
994 meta: 'metaKey'
995 };
996 var KEY_CHAR = /[a-z0-9*]/;
997 var IDENT_CHAR = /U\+/;
998 var ARROW_KEY = /^arrow/;
999 var SPACE_KEY = /^space(bar)?/;
1000 var ESC_KEY = /^escape$/;
1001 function transformKey(key, noSpecialChars) {
1002 var validKey = '';
1003 if (key) {
1004 var lKey = key.toLowerCase();
1005 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
1006 validKey = 'space';
1007 } else if (ESC_KEY.test(lKey)) {
1008 validKey = 'esc';
1009 } else if (lKey.length == 1) {
1010 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
1011 validKey = lKey;
1012 }
1013 } else if (ARROW_KEY.test(lKey)) {
1014 validKey = lKey.replace('arrow', '');
1015 } else if (lKey == 'multiply') {
1016 validKey = '*';
1017 } else {
1018 validKey = lKey;
1019 }
1020 }
1021 return validKey;
1022 }
1023 function transformKeyIdentifier(keyIdent) {
1024 var validKey = '';
1025 if (keyIdent) {
1026 if (keyIdent in KEY_IDENTIFIER) {
1027 validKey = KEY_IDENTIFIER[keyIdent];
1028 } else if (IDENT_CHAR.test(keyIdent)) {
1029 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1030 validKey = String.fromCharCode(keyIdent).toLowerCase();
1031 } else {
1032 validKey = keyIdent.toLowerCase();
1033 }
1034 }
1035 return validKey;
1036 }
1037 function transformKeyCode(keyCode) {
1038 var validKey = '';
1039 if (Number(keyCode)) {
1040 if (keyCode >= 65 && keyCode <= 90) {
1041 validKey = String.fromCharCode(32 + keyCode);
1042 } else if (keyCode >= 112 && keyCode <= 123) {
1043 validKey = 'f' + (keyCode - 112);
1044 } else if (keyCode >= 48 && keyCode <= 57) {
1045 validKey = String(keyCode - 48);
1046 } else if (keyCode >= 96 && keyCode <= 105) {
1047 validKey = String(keyCode - 96);
1048 } else {
1049 validKey = KEY_CODE[keyCode];
1050 }
1051 }
1052 return validKey;
1053 }
1054 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1055 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier( keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
1056 }
1057 function keyComboMatchesEvent(keyCombo, event) {
1058 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1059 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event .altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey);
1060 }
1061 function parseKeyComboString(keyComboString) {
1062 if (keyComboString.length === 1) {
1063 return {
1064 combo: keyComboString,
1065 key: keyComboString,
1066 event: 'keydown'
1067 };
1068 }
1069 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar t) {
1070 var eventParts = keyComboPart.split(':');
1071 var keyName = eventParts[0];
1072 var event = eventParts[1];
1073 if (keyName in MODIFIER_KEYS) {
1074 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1075 parsedKeyCombo.hasModifiers = true;
1076 } else {
1077 parsedKeyCombo.key = keyName;
1078 parsedKeyCombo.event = event || 'keydown';
1079 }
1080 return parsedKeyCombo;
1081 }, {
1082 combo: keyComboString.split(':').shift()
1083 });
1084 }
1085 function parseEventString(eventString) {
1086 return eventString.trim().split(' ').map(function(keyComboString) {
1087 return parseKeyComboString(keyComboString);
1088 });
1089 }
1090 Polymer.IronA11yKeysBehavior = {
1629 properties: { 1091 properties: {
1630 /** 1092 keyEventTarget: {
1631 * The closest ancestor element that implements `IronResizableBehavior`.
1632 */
1633 _parentResizable: {
1634 type: Object, 1093 type: Object,
1635 observer: '_parentResizableChanged' 1094 value: function() {
1636 }, 1095 return this;
1637 1096 }
1638 /** 1097 },
1639 * True if this element is currently notifying its descedant elements of 1098 stopKeyboardEventPropagation: {
1640 * resize.
1641 */
1642 _notifyingDescendant: {
1643 type: Boolean, 1099 type: Boolean,
1644 value: false 1100 value: false
1645 } 1101 },
1646 }, 1102 _boundKeyHandlers: {
1647 1103 type: Array,
1648 listeners: { 1104 value: function() {
1649 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' 1105 return [];
1650 }, 1106 }
1651 1107 },
1652 created: function() { 1108 _imperativeKeyBindings: {
1653 // We don't really need property effects on these, and also we want them 1109 type: Object,
1654 // to be created before the `_parentResizable` observer fires: 1110 value: function() {
1655 this._interestedResizables = []; 1111 return {};
1656 this._boundNotifyResize = this.notifyResize.bind(this); 1112 }
1657 }, 1113 }
1658 1114 },
1115 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ],
1116 keyBindings: {},
1117 registered: function() {
1118 this._prepKeyBindings();
1119 },
1659 attached: function() { 1120 attached: function() {
1660 this.fire('iron-request-resize-notifications', null, { 1121 this._listenKeyEventListeners();
1661 node: this, 1122 },
1662 bubbles: true, 1123 detached: function() {
1124 this._unlistenKeyEventListeners();
1125 },
1126 addOwnKeyBinding: function(eventString, handlerName) {
1127 this._imperativeKeyBindings[eventString] = handlerName;
1128 this._prepKeyBindings();
1129 this._resetKeyEventListeners();
1130 },
1131 removeOwnKeyBindings: function() {
1132 this._imperativeKeyBindings = {};
1133 this._prepKeyBindings();
1134 this._resetKeyEventListeners();
1135 },
1136 keyboardEventMatchesKeys: function(event, eventString) {
1137 var keyCombos = parseEventString(eventString);
1138 for (var i = 0; i < keyCombos.length; ++i) {
1139 if (keyComboMatchesEvent(keyCombos[i], event)) {
1140 return true;
1141 }
1142 }
1143 return false;
1144 },
1145 _collectKeyBindings: function() {
1146 var keyBindings = this.behaviors.map(function(behavior) {
1147 return behavior.keyBindings;
1148 });
1149 if (keyBindings.indexOf(this.keyBindings) === -1) {
1150 keyBindings.push(this.keyBindings);
1151 }
1152 return keyBindings;
1153 },
1154 _prepKeyBindings: function() {
1155 this._keyBindings = {};
1156 this._collectKeyBindings().forEach(function(keyBindings) {
1157 for (var eventString in keyBindings) {
1158 this._addKeyBinding(eventString, keyBindings[eventString]);
1159 }
1160 }, this);
1161 for (var eventString in this._imperativeKeyBindings) {
1162 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString ]);
1163 }
1164 for (var eventName in this._keyBindings) {
1165 this._keyBindings[eventName].sort(function(kb1, kb2) {
1166 var b1 = kb1[0].hasModifiers;
1167 var b2 = kb2[0].hasModifiers;
1168 return b1 === b2 ? 0 : b1 ? -1 : 1;
1169 });
1170 }
1171 },
1172 _addKeyBinding: function(eventString, handlerName) {
1173 parseEventString(eventString).forEach(function(keyCombo) {
1174 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] || [];
1175 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]);
1176 }, this);
1177 },
1178 _resetKeyEventListeners: function() {
1179 this._unlistenKeyEventListeners();
1180 if (this.isAttached) {
1181 this._listenKeyEventListeners();
1182 }
1183 },
1184 _listenKeyEventListeners: function() {
1185 if (!this.keyEventTarget) {
1186 return;
1187 }
1188 Object.keys(this._keyBindings).forEach(function(eventName) {
1189 var keyBindings = this._keyBindings[eventName];
1190 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
1191 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa ndler ]);
1192 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
1193 }, this);
1194 },
1195 _unlistenKeyEventListeners: function() {
1196 var keyHandlerTuple;
1197 var keyEventTarget;
1198 var eventName;
1199 var boundKeyHandler;
1200 while (this._boundKeyHandlers.length) {
1201 keyHandlerTuple = this._boundKeyHandlers.pop();
1202 keyEventTarget = keyHandlerTuple[0];
1203 eventName = keyHandlerTuple[1];
1204 boundKeyHandler = keyHandlerTuple[2];
1205 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
1206 }
1207 },
1208 _onKeyBindingEvent: function(keyBindings, event) {
1209 if (this.stopKeyboardEventPropagation) {
1210 event.stopPropagation();
1211 }
1212 if (event.defaultPrevented) {
1213 return;
1214 }
1215 for (var i = 0; i < keyBindings.length; i++) {
1216 var keyCombo = keyBindings[i][0];
1217 var handlerName = keyBindings[i][1];
1218 if (keyComboMatchesEvent(keyCombo, event)) {
1219 this._triggerKeyHandler(keyCombo, handlerName, event);
1220 if (event.defaultPrevented) {
1221 return;
1222 }
1223 }
1224 }
1225 },
1226 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
1227 var detail = Object.create(keyCombo);
1228 detail.keyboardEvent = keyboardEvent;
1229 var event = new CustomEvent(keyCombo.event, {
1230 detail: detail,
1663 cancelable: true 1231 cancelable: true
1664 }); 1232 });
1665 1233 this[handlerName].call(this, event);
1666 if (!this._parentResizable) { 1234 if (event.defaultPrevented) {
1667 window.addEventListener('resize', this._boundNotifyResize); 1235 keyboardEvent.preventDefault();
1668 this.notifyResize(); 1236 }
1669 }
1670 },
1671
1672 detached: function() {
1673 if (this._parentResizable) {
1674 this._parentResizable.stopResizeNotificationsFor(this);
1675 } else {
1676 window.removeEventListener('resize', this._boundNotifyResize);
1677 }
1678
1679 this._parentResizable = null;
1680 },
1681
1682 /**
1683 * Can be called to manually notify a resizable and its descendant
1684 * resizables of a resize change.
1685 */
1686 notifyResize: function() {
1687 if (!this.isAttached) {
1688 return;
1689 }
1690
1691 this._interestedResizables.forEach(function(resizable) {
1692 if (this.resizerShouldNotify(resizable)) {
1693 this._notifyDescendant(resizable);
1694 }
1695 }, this);
1696
1697 this._fireResize();
1698 },
1699
1700 /**
1701 * Used to assign the closest resizable ancestor to this resizable
1702 * if the ancestor detects a request for notifications.
1703 */
1704 assignParentResizable: function(parentResizable) {
1705 this._parentResizable = parentResizable;
1706 },
1707
1708 /**
1709 * Used to remove a resizable descendant from the list of descendants
1710 * that should be notified of a resize change.
1711 */
1712 stopResizeNotificationsFor: function(target) {
1713 var index = this._interestedResizables.indexOf(target);
1714
1715 if (index > -1) {
1716 this._interestedResizables.splice(index, 1);
1717 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
1718 }
1719 },
1720
1721 /**
1722 * This method can be overridden to filter nested elements that should or
1723 * should not be notified by the current element. Return true if an element
1724 * should be notified, or false if it should not be notified.
1725 *
1726 * @param {HTMLElement} element A candidate descendant element that
1727 * implements `IronResizableBehavior`.
1728 * @return {boolean} True if the `element` should be notified of resize.
1729 */
1730 resizerShouldNotify: function(element) { return true; },
1731
1732 _onDescendantIronResize: function(event) {
1733 if (this._notifyingDescendant) {
1734 event.stopPropagation();
1735 return;
1736 }
1737
1738 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
1739 // otherwise non-bubbling event "just work." We do it manually here for
1740 // the case where Polymer is not using shadow roots for whatever reason:
1741 if (!Polymer.Settings.useShadow) {
1742 this._fireResize();
1743 }
1744 },
1745
1746 _fireResize: function() {
1747 this.fire('iron-resize', null, {
1748 node: this,
1749 bubbles: false
1750 });
1751 },
1752
1753 _onIronRequestResizeNotifications: function(event) {
1754 var target = event.path ? event.path[0] : event.target;
1755
1756 if (target === this) {
1757 return;
1758 }
1759
1760 if (this._interestedResizables.indexOf(target) === -1) {
1761 this._interestedResizables.push(target);
1762 this.listen(target, 'iron-resize', '_onDescendantIronResize');
1763 }
1764
1765 target.assignParentResizable(this);
1766 this._notifyDescendant(target);
1767
1768 event.stopPropagation();
1769 },
1770
1771 _parentResizableChanged: function(parentResizable) {
1772 if (parentResizable) {
1773 window.removeEventListener('resize', this._boundNotifyResize);
1774 }
1775 },
1776
1777 _notifyDescendant: function(descendant) {
1778 // NOTE(cdata): In IE10, attached is fired on children first, so it's
1779 // important not to notify them if the parent is not attached yet (or
1780 // else they will get redundantly notified when the parent attaches).
1781 if (!this.isAttached) {
1782 return;
1783 }
1784
1785 this._notifyingDescendant = true;
1786 descendant.notifyResize();
1787 this._notifyingDescendant = false;
1788 } 1237 }
1789 }; 1238 };
1239 })();
1240
1241 Polymer.IronScrollTargetBehavior = {
1242 properties: {
1243 scrollTarget: {
1244 type: HTMLElement,
1245 value: function() {
1246 return this._defaultScrollTarget;
1247 }
1248 }
1249 },
1250 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ],
1251 _scrollTargetChanged: function(scrollTarget, isAttached) {
1252 var eventTarget;
1253 if (this._oldScrollTarget) {
1254 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro llTarget;
1255 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
1256 this._oldScrollTarget = null;
1257 }
1258 if (!isAttached) {
1259 return;
1260 }
1261 if (scrollTarget === 'document') {
1262 this.scrollTarget = this._doc;
1263 } else if (typeof scrollTarget === 'string') {
1264 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer. dom(this.ownerDocument).querySelector('#' + scrollTarget);
1265 } else if (this._isValidScrollTarget()) {
1266 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
1267 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler .bind(this);
1268 this._oldScrollTarget = scrollTarget;
1269 eventTarget.addEventListener('scroll', this._boundScrollHandler);
1270 }
1271 },
1272 _scrollHandler: function scrollHandler() {},
1273 get _defaultScrollTarget() {
1274 return this._doc;
1275 },
1276 get _doc() {
1277 return this.ownerDocument.documentElement;
1278 },
1279 get _scrollTop() {
1280 if (this._isValidScrollTarget()) {
1281 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT arget.scrollTop;
1282 }
1283 return 0;
1284 },
1285 get _scrollLeft() {
1286 if (this._isValidScrollTarget()) {
1287 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT arget.scrollLeft;
1288 }
1289 return 0;
1290 },
1291 set _scrollTop(top) {
1292 if (this.scrollTarget === this._doc) {
1293 window.scrollTo(window.pageXOffset, top);
1294 } else if (this._isValidScrollTarget()) {
1295 this.scrollTarget.scrollTop = top;
1296 }
1297 },
1298 set _scrollLeft(left) {
1299 if (this.scrollTarget === this._doc) {
1300 window.scrollTo(left, window.pageYOffset);
1301 } else if (this._isValidScrollTarget()) {
1302 this.scrollTarget.scrollLeft = left;
1303 }
1304 },
1305 scroll: function(left, top) {
1306 if (this.scrollTarget === this._doc) {
1307 window.scrollTo(left, top);
1308 } else if (this._isValidScrollTarget()) {
1309 this.scrollTarget.scrollLeft = left;
1310 this.scrollTarget.scrollTop = top;
1311 }
1312 },
1313 get _scrollTargetWidth() {
1314 if (this._isValidScrollTarget()) {
1315 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa rget.offsetWidth;
1316 }
1317 return 0;
1318 },
1319 get _scrollTargetHeight() {
1320 if (this._isValidScrollTarget()) {
1321 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT arget.offsetHeight;
1322 }
1323 return 0;
1324 },
1325 _isValidScrollTarget: function() {
1326 return this.scrollTarget instanceof HTMLElement;
1327 }
1328 };
1329
1790 (function() { 1330 (function() {
1791 'use strict';
1792
1793 /**
1794 * Chrome uses an older version of DOM Level 3 Keyboard Events
1795 *
1796 * Most keys are labeled as text, but some are Unicode codepoints.
1797 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set
1798 */
1799 var KEY_IDENTIFIER = {
1800 'U+0008': 'backspace',
1801 'U+0009': 'tab',
1802 'U+001B': 'esc',
1803 'U+0020': 'space',
1804 'U+007F': 'del'
1805 };
1806
1807 /**
1808 * Special table for KeyboardEvent.keyCode.
1809 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er
1810 * than that.
1811 *
1812 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode
1813 */
1814 var KEY_CODE = {
1815 8: 'backspace',
1816 9: 'tab',
1817 13: 'enter',
1818 27: 'esc',
1819 33: 'pageup',
1820 34: 'pagedown',
1821 35: 'end',
1822 36: 'home',
1823 32: 'space',
1824 37: 'left',
1825 38: 'up',
1826 39: 'right',
1827 40: 'down',
1828 46: 'del',
1829 106: '*'
1830 };
1831
1832 /**
1833 * MODIFIER_KEYS maps the short name for modifier keys used in a key
1834 * combo string to the property name that references those same keys
1835 * in a KeyboardEvent instance.
1836 */
1837 var MODIFIER_KEYS = {
1838 'shift': 'shiftKey',
1839 'ctrl': 'ctrlKey',
1840 'alt': 'altKey',
1841 'meta': 'metaKey'
1842 };
1843
1844 /**
1845 * KeyboardEvent.key is mostly represented by printable character made by
1846 * the keyboard, with unprintable keys labeled nicely.
1847 *
1848 * However, on OS X, Alt+char can make a Unicode character that follows an
1849 * Apple-specific mapping. In this case, we fall back to .keyCode.
1850 */
1851 var KEY_CHAR = /[a-z0-9*]/;
1852
1853 /**
1854 * Matches a keyIdentifier string.
1855 */
1856 var IDENT_CHAR = /U\+/;
1857
1858 /**
1859 * Matches arrow keys in Gecko 27.0+
1860 */
1861 var ARROW_KEY = /^arrow/;
1862
1863 /**
1864 * Matches space keys everywhere (notably including IE10's exceptional name
1865 * `spacebar`).
1866 */
1867 var SPACE_KEY = /^space(bar)?/;
1868
1869 /**
1870 * Matches ESC key.
1871 *
1872 * Value from: http://w3c.github.io/uievents-key/#key-Escape
1873 */
1874 var ESC_KEY = /^escape$/;
1875
1876 /**
1877 * Transforms the key.
1878 * @param {string} key The KeyBoardEvent.key
1879 * @param {Boolean} [noSpecialChars] Limits the transformation to
1880 * alpha-numeric characters.
1881 */
1882 function transformKey(key, noSpecialChars) {
1883 var validKey = '';
1884 if (key) {
1885 var lKey = key.toLowerCase();
1886 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
1887 validKey = 'space';
1888 } else if (ESC_KEY.test(lKey)) {
1889 validKey = 'esc';
1890 } else if (lKey.length == 1) {
1891 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
1892 validKey = lKey;
1893 }
1894 } else if (ARROW_KEY.test(lKey)) {
1895 validKey = lKey.replace('arrow', '');
1896 } else if (lKey == 'multiply') {
1897 // numpad '*' can map to Multiply on IE/Windows
1898 validKey = '*';
1899 } else {
1900 validKey = lKey;
1901 }
1902 }
1903 return validKey;
1904 }
1905
1906 function transformKeyIdentifier(keyIdent) {
1907 var validKey = '';
1908 if (keyIdent) {
1909 if (keyIdent in KEY_IDENTIFIER) {
1910 validKey = KEY_IDENTIFIER[keyIdent];
1911 } else if (IDENT_CHAR.test(keyIdent)) {
1912 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1913 validKey = String.fromCharCode(keyIdent).toLowerCase();
1914 } else {
1915 validKey = keyIdent.toLowerCase();
1916 }
1917 }
1918 return validKey;
1919 }
1920
1921 function transformKeyCode(keyCode) {
1922 var validKey = '';
1923 if (Number(keyCode)) {
1924 if (keyCode >= 65 && keyCode <= 90) {
1925 // ascii a-z
1926 // lowercase is 32 offset from uppercase
1927 validKey = String.fromCharCode(32 + keyCode);
1928 } else if (keyCode >= 112 && keyCode <= 123) {
1929 // function keys f1-f12
1930 validKey = 'f' + (keyCode - 112);
1931 } else if (keyCode >= 48 && keyCode <= 57) {
1932 // top 0-9 keys
1933 validKey = String(keyCode - 48);
1934 } else if (keyCode >= 96 && keyCode <= 105) {
1935 // num pad 0-9
1936 validKey = String(keyCode - 96);
1937 } else {
1938 validKey = KEY_CODE[keyCode];
1939 }
1940 }
1941 return validKey;
1942 }
1943
1944 /**
1945 * Calculates the normalized key for a KeyboardEvent.
1946 * @param {KeyboardEvent} keyEvent
1947 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
1948 * transformation to alpha-numeric chars. This is useful with key
1949 * combinations like shift + 2, which on FF for MacOS produces
1950 * keyEvent.key = @
1951 * To get 2 returned, set noSpecialChars = true
1952 * To get @ returned, set noSpecialChars = false
1953 */
1954 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1955 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
1956 // .detail.key to support artificial keyboard events.
1957 return transformKey(keyEvent.key, noSpecialChars) ||
1958 transformKeyIdentifier(keyEvent.keyIdentifier) ||
1959 transformKeyCode(keyEvent.keyCode) ||
1960 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
1961 }
1962
1963 function keyComboMatchesEvent(keyCombo, event) {
1964 // For combos with modifiers we support only alpha-numeric keys
1965 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1966 return keyEvent === keyCombo.key &&
1967 (!keyCombo.hasModifiers || (
1968 !!event.shiftKey === !!keyCombo.shiftKey &&
1969 !!event.ctrlKey === !!keyCombo.ctrlKey &&
1970 !!event.altKey === !!keyCombo.altKey &&
1971 !!event.metaKey === !!keyCombo.metaKey)
1972 );
1973 }
1974
1975 function parseKeyComboString(keyComboString) {
1976 if (keyComboString.length === 1) {
1977 return {
1978 combo: keyComboString,
1979 key: keyComboString,
1980 event: 'keydown'
1981 };
1982 }
1983 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP art) {
1984 var eventParts = keyComboPart.split(':');
1985 var keyName = eventParts[0];
1986 var event = eventParts[1];
1987
1988 if (keyName in MODIFIER_KEYS) {
1989 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1990 parsedKeyCombo.hasModifiers = true;
1991 } else {
1992 parsedKeyCombo.key = keyName;
1993 parsedKeyCombo.event = event || 'keydown';
1994 }
1995
1996 return parsedKeyCombo;
1997 }, {
1998 combo: keyComboString.split(':').shift()
1999 });
2000 }
2001
2002 function parseEventString(eventString) {
2003 return eventString.trim().split(' ').map(function(keyComboString) {
2004 return parseKeyComboString(keyComboString);
2005 });
2006 }
2007
2008 /**
2009 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
2010 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
2011 * The element takes care of browser differences with respect to Keyboard ev ents
2012 * and uses an expressive syntax to filter key presses.
2013 *
2014 * Use the `keyBindings` prototype property to express what combination of k eys
2015 * will trigger the callback. A key binding has the format
2016 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
2017 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
2018 *
2019 * keyBindings: {
2020 * 'space': '_onKeydown', // same as 'space:keydown'
2021 * 'shift+tab': '_onKeydown',
2022 * 'enter:keypress': '_onKeypress',
2023 * 'esc:keyup': '_onKeyup'
2024 * }
2025 *
2026 * The callback will receive with an event containing the following informat ion in `event.detail`:
2027 *
2028 * _onKeydown: function(event) {
2029 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
2030 * console.log(event.detail.key); // KEY only, e.g. "tab"
2031 * console.log(event.detail.event); // EVENT, e.g. "keydown"
2032 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
2033 * }
2034 *
2035 * Use the `keyEventTarget` attribute to set up event handlers on a specific
2036 * node.
2037 *
2038 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
2039 * for an example.
2040 *
2041 * @demo demo/index.html
2042 * @polymerBehavior
2043 */
2044 Polymer.IronA11yKeysBehavior = {
2045 properties: {
2046 /**
2047 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
2048 * `null` to disable the listeners.
2049 * @type {?EventTarget}
2050 */
2051 keyEventTarget: {
2052 type: Object,
2053 value: function() {
2054 return this;
2055 }
2056 },
2057
2058 /**
2059 * If true, this property will cause the implementing element to
2060 * automatically stop propagation on any handled KeyboardEvents.
2061 */
2062 stopKeyboardEventPropagation: {
2063 type: Boolean,
2064 value: false
2065 },
2066
2067 _boundKeyHandlers: {
2068 type: Array,
2069 value: function() {
2070 return [];
2071 }
2072 },
2073
2074 // We use this due to a limitation in IE10 where instances will have
2075 // own properties of everything on the "prototype".
2076 _imperativeKeyBindings: {
2077 type: Object,
2078 value: function() {
2079 return {};
2080 }
2081 }
2082 },
2083
2084 observers: [
2085 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
2086 ],
2087
2088
2089 /**
2090 * To be used to express what combination of keys will trigger the relati ve
2091 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
2092 * @type {Object}
2093 */
2094 keyBindings: {},
2095
2096 registered: function() {
2097 this._prepKeyBindings();
2098 },
2099
2100 attached: function() {
2101 this._listenKeyEventListeners();
2102 },
2103
2104 detached: function() {
2105 this._unlistenKeyEventListeners();
2106 },
2107
2108 /**
2109 * Can be used to imperatively add a key binding to the implementing
2110 * element. This is the imperative equivalent of declaring a keybinding
2111 * in the `keyBindings` prototype property.
2112 */
2113 addOwnKeyBinding: function(eventString, handlerName) {
2114 this._imperativeKeyBindings[eventString] = handlerName;
2115 this._prepKeyBindings();
2116 this._resetKeyEventListeners();
2117 },
2118
2119 /**
2120 * When called, will remove all imperatively-added key bindings.
2121 */
2122 removeOwnKeyBindings: function() {
2123 this._imperativeKeyBindings = {};
2124 this._prepKeyBindings();
2125 this._resetKeyEventListeners();
2126 },
2127
2128 /**
2129 * Returns true if a keyboard event matches `eventString`.
2130 *
2131 * @param {KeyboardEvent} event
2132 * @param {string} eventString
2133 * @return {boolean}
2134 */
2135 keyboardEventMatchesKeys: function(event, eventString) {
2136 var keyCombos = parseEventString(eventString);
2137 for (var i = 0; i < keyCombos.length; ++i) {
2138 if (keyComboMatchesEvent(keyCombos[i], event)) {
2139 return true;
2140 }
2141 }
2142 return false;
2143 },
2144
2145 _collectKeyBindings: function() {
2146 var keyBindings = this.behaviors.map(function(behavior) {
2147 return behavior.keyBindings;
2148 });
2149
2150 if (keyBindings.indexOf(this.keyBindings) === -1) {
2151 keyBindings.push(this.keyBindings);
2152 }
2153
2154 return keyBindings;
2155 },
2156
2157 _prepKeyBindings: function() {
2158 this._keyBindings = {};
2159
2160 this._collectKeyBindings().forEach(function(keyBindings) {
2161 for (var eventString in keyBindings) {
2162 this._addKeyBinding(eventString, keyBindings[eventString]);
2163 }
2164 }, this);
2165
2166 for (var eventString in this._imperativeKeyBindings) {
2167 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
2168 }
2169
2170 // Give precedence to combos with modifiers to be checked first.
2171 for (var eventName in this._keyBindings) {
2172 this._keyBindings[eventName].sort(function (kb1, kb2) {
2173 var b1 = kb1[0].hasModifiers;
2174 var b2 = kb2[0].hasModifiers;
2175 return (b1 === b2) ? 0 : b1 ? -1 : 1;
2176 })
2177 }
2178 },
2179
2180 _addKeyBinding: function(eventString, handlerName) {
2181 parseEventString(eventString).forEach(function(keyCombo) {
2182 this._keyBindings[keyCombo.event] =
2183 this._keyBindings[keyCombo.event] || [];
2184
2185 this._keyBindings[keyCombo.event].push([
2186 keyCombo,
2187 handlerName
2188 ]);
2189 }, this);
2190 },
2191
2192 _resetKeyEventListeners: function() {
2193 this._unlistenKeyEventListeners();
2194
2195 if (this.isAttached) {
2196 this._listenKeyEventListeners();
2197 }
2198 },
2199
2200 _listenKeyEventListeners: function() {
2201 if (!this.keyEventTarget) {
2202 return;
2203 }
2204 Object.keys(this._keyBindings).forEach(function(eventName) {
2205 var keyBindings = this._keyBindings[eventName];
2206 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
2207
2208 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH andler]);
2209
2210 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
2211 }, this);
2212 },
2213
2214 _unlistenKeyEventListeners: function() {
2215 var keyHandlerTuple;
2216 var keyEventTarget;
2217 var eventName;
2218 var boundKeyHandler;
2219
2220 while (this._boundKeyHandlers.length) {
2221 // My kingdom for block-scope binding and destructuring assignment..
2222 keyHandlerTuple = this._boundKeyHandlers.pop();
2223 keyEventTarget = keyHandlerTuple[0];
2224 eventName = keyHandlerTuple[1];
2225 boundKeyHandler = keyHandlerTuple[2];
2226
2227 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
2228 }
2229 },
2230
2231 _onKeyBindingEvent: function(keyBindings, event) {
2232 if (this.stopKeyboardEventPropagation) {
2233 event.stopPropagation();
2234 }
2235
2236 // if event has been already prevented, don't do anything
2237 if (event.defaultPrevented) {
2238 return;
2239 }
2240
2241 for (var i = 0; i < keyBindings.length; i++) {
2242 var keyCombo = keyBindings[i][0];
2243 var handlerName = keyBindings[i][1];
2244 if (keyComboMatchesEvent(keyCombo, event)) {
2245 this._triggerKeyHandler(keyCombo, handlerName, event);
2246 // exit the loop if eventDefault was prevented
2247 if (event.defaultPrevented) {
2248 return;
2249 }
2250 }
2251 }
2252 },
2253
2254 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
2255 var detail = Object.create(keyCombo);
2256 detail.keyboardEvent = keyboardEvent;
2257 var event = new CustomEvent(keyCombo.event, {
2258 detail: detail,
2259 cancelable: true
2260 });
2261 this[handlerName].call(this, event);
2262 if (event.defaultPrevented) {
2263 keyboardEvent.preventDefault();
2264 }
2265 }
2266 };
2267 })();
2268 /**
2269 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
2270 * designated scroll target.
2271 *
2272 * Elements that consume this behavior can override the `_scrollHandler`
2273 * method to add logic on the scroll event.
2274 *
2275 * @demo demo/scrolling-region.html Scrolling Region
2276 * @demo demo/document.html Document Element
2277 * @polymerBehavior
2278 */
2279 Polymer.IronScrollTargetBehavior = {
2280
2281 properties: {
2282
2283 /**
2284 * Specifies the element that will handle the scroll event
2285 * on the behalf of the current element. This is typically a reference to an element,
2286 * but there are a few more posibilities:
2287 *
2288 * ### Elements id
2289 *
2290 *```html
2291 * <div id="scrollable-element" style="overflow: auto;">
2292 * <x-element scroll-target="scrollable-element">
2293 * \x3c!-- Content--\x3e
2294 * </x-element>
2295 * </div>
2296 *```
2297 * In this case, the `scrollTarget` will point to the outer div element.
2298 *
2299 * ### Document scrolling
2300 *
2301 * For document scrolling, you can use the reserved word `document`:
2302 *
2303 *```html
2304 * <x-element scroll-target="document">
2305 * \x3c!-- Content --\x3e
2306 * </x-element>
2307 *```
2308 *
2309 * ### Elements reference
2310 *
2311 *```js
2312 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
2313 *```
2314 *
2315 * @type {HTMLElement}
2316 */
2317 scrollTarget: {
2318 type: HTMLElement,
2319 value: function() {
2320 return this._defaultScrollTarget;
2321 }
2322 }
2323 },
2324
2325 observers: [
2326 '_scrollTargetChanged(scrollTarget, isAttached)'
2327 ],
2328
2329 _scrollTargetChanged: function(scrollTarget, isAttached) {
2330 var eventTarget;
2331
2332 if (this._oldScrollTarget) {
2333 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
2334 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
2335 this._oldScrollTarget = null;
2336 }
2337
2338 if (!isAttached) {
2339 return;
2340 }
2341 // Support element id references
2342 if (scrollTarget === 'document') {
2343
2344 this.scrollTarget = this._doc;
2345
2346 } else if (typeof scrollTarget === 'string') {
2347
2348 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
2349 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
2350
2351 } else if (this._isValidScrollTarget()) {
2352
2353 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
2354 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
2355 this._oldScrollTarget = scrollTarget;
2356
2357 eventTarget.addEventListener('scroll', this._boundScrollHandler);
2358 }
2359 },
2360
2361 /**
2362 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
2363 *
2364 * @protected
2365 */
2366 _scrollHandler: function scrollHandler() {},
2367
2368 /**
2369 * The default scroll target. Consumers of this behavior may want to customi ze
2370 * the default scroll target.
2371 *
2372 * @type {Element}
2373 */
2374 get _defaultScrollTarget() {
2375 return this._doc;
2376 },
2377
2378 /**
2379 * Shortcut for the document element
2380 *
2381 * @type {Element}
2382 */
2383 get _doc() {
2384 return this.ownerDocument.documentElement;
2385 },
2386
2387 /**
2388 * Gets the number of pixels that the content of an element is scrolled upwa rd.
2389 *
2390 * @type {number}
2391 */
2392 get _scrollTop() {
2393 if (this._isValidScrollTarget()) {
2394 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
2395 }
2396 return 0;
2397 },
2398
2399 /**
2400 * Gets the number of pixels that the content of an element is scrolled to t he left.
2401 *
2402 * @type {number}
2403 */
2404 get _scrollLeft() {
2405 if (this._isValidScrollTarget()) {
2406 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
2407 }
2408 return 0;
2409 },
2410
2411 /**
2412 * Sets the number of pixels that the content of an element is scrolled upwa rd.
2413 *
2414 * @type {number}
2415 */
2416 set _scrollTop(top) {
2417 if (this.scrollTarget === this._doc) {
2418 window.scrollTo(window.pageXOffset, top);
2419 } else if (this._isValidScrollTarget()) {
2420 this.scrollTarget.scrollTop = top;
2421 }
2422 },
2423
2424 /**
2425 * Sets the number of pixels that the content of an element is scrolled to t he left.
2426 *
2427 * @type {number}
2428 */
2429 set _scrollLeft(left) {
2430 if (this.scrollTarget === this._doc) {
2431 window.scrollTo(left, window.pageYOffset);
2432 } else if (this._isValidScrollTarget()) {
2433 this.scrollTarget.scrollLeft = left;
2434 }
2435 },
2436
2437 /**
2438 * Scrolls the content to a particular place.
2439 *
2440 * @method scroll
2441 * @param {number} left The left position
2442 * @param {number} top The top position
2443 */
2444 scroll: function(left, top) {
2445 if (this.scrollTarget === this._doc) {
2446 window.scrollTo(left, top);
2447 } else if (this._isValidScrollTarget()) {
2448 this.scrollTarget.scrollLeft = left;
2449 this.scrollTarget.scrollTop = top;
2450 }
2451 },
2452
2453 /**
2454 * Gets the width of the scroll target.
2455 *
2456 * @type {number}
2457 */
2458 get _scrollTargetWidth() {
2459 if (this._isValidScrollTarget()) {
2460 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
2461 }
2462 return 0;
2463 },
2464
2465 /**
2466 * Gets the height of the scroll target.
2467 *
2468 * @type {number}
2469 */
2470 get _scrollTargetHeight() {
2471 if (this._isValidScrollTarget()) {
2472 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
2473 }
2474 return 0;
2475 },
2476
2477 /**
2478 * Returns true if the scroll target is a valid HTMLElement.
2479 *
2480 * @return {boolean}
2481 */
2482 _isValidScrollTarget: function() {
2483 return this.scrollTarget instanceof HTMLElement;
2484 }
2485 };
2486 (function() {
2487
2488 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); 1331 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
2489 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; 1332 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
2490 var DEFAULT_PHYSICAL_COUNT = 3; 1333 var DEFAULT_PHYSICAL_COUNT = 3;
2491 var HIDDEN_Y = '-10000px'; 1334 var HIDDEN_Y = '-10000px';
2492 var DEFAULT_GRID_SIZE = 200; 1335 var DEFAULT_GRID_SIZE = 200;
2493 var SECRET_TABINDEX = -100; 1336 var SECRET_TABINDEX = -100;
2494
2495 Polymer({ 1337 Polymer({
2496
2497 is: 'iron-list', 1338 is: 'iron-list',
2498
2499 properties: { 1339 properties: {
2500
2501 /**
2502 * An array containing items determining how many instances of the templat e
2503 * to stamp and that that each template instance should bind to.
2504 */
2505 items: { 1340 items: {
2506 type: Array 1341 type: Array
2507 }, 1342 },
2508
2509 /**
2510 * The max count of physical items the pool can extend to.
2511 */
2512 maxPhysicalCount: { 1343 maxPhysicalCount: {
2513 type: Number, 1344 type: Number,
2514 value: 500 1345 value: 500
2515 }, 1346 },
2516
2517 /**
2518 * The name of the variable to add to the binding scope for the array
2519 * element associated with a given template instance.
2520 */
2521 as: { 1347 as: {
2522 type: String, 1348 type: String,
2523 value: 'item' 1349 value: 'item'
2524 }, 1350 },
2525
2526 /**
2527 * The name of the variable to add to the binding scope with the index
2528 * for the row.
2529 */
2530 indexAs: { 1351 indexAs: {
2531 type: String, 1352 type: String,
2532 value: 'index' 1353 value: 'index'
2533 }, 1354 },
2534
2535 /**
2536 * The name of the variable to add to the binding scope to indicate
2537 * if the row is selected.
2538 */
2539 selectedAs: { 1355 selectedAs: {
2540 type: String, 1356 type: String,
2541 value: 'selected' 1357 value: 'selected'
2542 }, 1358 },
2543
2544 /**
2545 * When true, the list is rendered as a grid. Grid items must have
2546 * fixed width and height set via CSS. e.g.
2547 *
2548 * ```html
2549 * <iron-list grid>
2550 * <template>
2551 * <div style="width: 100px; height: 100px;"> 100x100 </div>
2552 * </template>
2553 * </iron-list>
2554 * ```
2555 */
2556 grid: { 1359 grid: {
2557 type: Boolean, 1360 type: Boolean,
2558 value: false, 1361 value: false,
2559 reflectToAttribute: true 1362 reflectToAttribute: true
2560 }, 1363 },
2561
2562 /**
2563 * When true, tapping a row will select the item, placing its data model
2564 * in the set of selected items retrievable via the selection property.
2565 *
2566 * Note that tapping focusable elements within the list item will not
2567 * result in selection, since they are presumed to have their * own action .
2568 */
2569 selectionEnabled: { 1364 selectionEnabled: {
2570 type: Boolean, 1365 type: Boolean,
2571 value: false 1366 value: false
2572 }, 1367 },
2573
2574 /**
2575 * When `multiSelection` is false, this is the currently selected item, or `null`
2576 * if no item is selected.
2577 */
2578 selectedItem: { 1368 selectedItem: {
2579 type: Object, 1369 type: Object,
2580 notify: true 1370 notify: true
2581 }, 1371 },
2582
2583 /**
2584 * When `multiSelection` is true, this is an array that contains the selec ted items.
2585 */
2586 selectedItems: { 1372 selectedItems: {
2587 type: Object, 1373 type: Object,
2588 notify: true 1374 notify: true
2589 }, 1375 },
2590
2591 /**
2592 * When `true`, multiple items may be selected at once (in this case,
2593 * `selected` is an array of currently selected items). When `false`,
2594 * only one item may be selected at a time.
2595 */
2596 multiSelection: { 1376 multiSelection: {
2597 type: Boolean, 1377 type: Boolean,
2598 value: false 1378 value: false
2599 } 1379 }
2600 }, 1380 },
2601 1381 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)' ],
2602 observers: [ 1382 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ],
2603 '_itemsChanged(items.*)',
2604 '_selectionEnabledChanged(selectionEnabled)',
2605 '_multiSelectionChanged(multiSelection)',
2606 '_setOverflow(scrollTarget)'
2607 ],
2608
2609 behaviors: [
2610 Polymer.Templatizer,
2611 Polymer.IronResizableBehavior,
2612 Polymer.IronA11yKeysBehavior,
2613 Polymer.IronScrollTargetBehavior
2614 ],
2615
2616 keyBindings: { 1383 keyBindings: {
2617 'up': '_didMoveUp', 1384 up: '_didMoveUp',
2618 'down': '_didMoveDown', 1385 down: '_didMoveDown',
2619 'enter': '_didEnter' 1386 enter: '_didEnter'
2620 }, 1387 },
2621 1388 _ratio: .5,
2622 /**
2623 * The ratio of hidden tiles that should remain in the scroll direction.
2624 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
2625 */
2626 _ratio: 0.5,
2627
2628 /**
2629 * The padding-top value for the list.
2630 */
2631 _scrollerPaddingTop: 0, 1389 _scrollerPaddingTop: 0,
2632
2633 /**
2634 * This value is the same as `scrollTop`.
2635 */
2636 _scrollPosition: 0, 1390 _scrollPosition: 0,
2637
2638 /**
2639 * The sum of the heights of all the tiles in the DOM.
2640 */
2641 _physicalSize: 0, 1391 _physicalSize: 0,
2642
2643 /**
2644 * The average `offsetHeight` of the tiles observed till now.
2645 */
2646 _physicalAverage: 0, 1392 _physicalAverage: 0,
2647
2648 /**
2649 * The number of tiles which `offsetHeight` > 0 observed until now.
2650 */
2651 _physicalAverageCount: 0, 1393 _physicalAverageCount: 0,
2652
2653 /**
2654 * The Y position of the item rendered in the `_physicalStart`
2655 * tile relative to the scrolling list.
2656 */
2657 _physicalTop: 0, 1394 _physicalTop: 0,
2658
2659 /**
2660 * The number of items in the list.
2661 */
2662 _virtualCount: 0, 1395 _virtualCount: 0,
2663
2664 /**
2665 * A map between an item key and its physical item index
2666 */
2667 _physicalIndexForKey: null, 1396 _physicalIndexForKey: null,
2668
2669 /**
2670 * The estimated scroll height based on `_physicalAverage`
2671 */
2672 _estScrollHeight: 0, 1397 _estScrollHeight: 0,
2673
2674 /**
2675 * The scroll height of the dom node
2676 */
2677 _scrollHeight: 0, 1398 _scrollHeight: 0,
2678
2679 /**
2680 * The height of the list. This is referred as the viewport in the context o f list.
2681 */
2682 _viewportHeight: 0, 1399 _viewportHeight: 0,
2683
2684 /**
2685 * The width of the list. This is referred as the viewport in the context of list.
2686 */
2687 _viewportWidth: 0, 1400 _viewportWidth: 0,
2688
2689 /**
2690 * An array of DOM nodes that are currently in the tree
2691 * @type {?Array<!TemplatizerNode>}
2692 */
2693 _physicalItems: null, 1401 _physicalItems: null,
2694
2695 /**
2696 * An array of heights for each item in `_physicalItems`
2697 * @type {?Array<number>}
2698 */
2699 _physicalSizes: null, 1402 _physicalSizes: null,
2700
2701 /**
2702 * A cached value for the first visible index.
2703 * See `firstVisibleIndex`
2704 * @type {?number}
2705 */
2706 _firstVisibleIndexVal: null, 1403 _firstVisibleIndexVal: null,
2707
2708 /**
2709 * A cached value for the last visible index.
2710 * See `lastVisibleIndex`
2711 * @type {?number}
2712 */
2713 _lastVisibleIndexVal: null, 1404 _lastVisibleIndexVal: null,
2714
2715 /**
2716 * A Polymer collection for the items.
2717 * @type {?Polymer.Collection}
2718 */
2719 _collection: null, 1405 _collection: null,
2720
2721 /**
2722 * True if the current item list was rendered for the first time
2723 * after attached.
2724 */
2725 _itemsRendered: false, 1406 _itemsRendered: false,
2726
2727 /**
2728 * The page that is currently rendered.
2729 */
2730 _lastPage: null, 1407 _lastPage: null,
2731
2732 /**
2733 * The max number of pages to render. One page is equivalent to the height o f the list.
2734 */
2735 _maxPages: 3, 1408 _maxPages: 3,
2736
2737 /**
2738 * The currently focused physical item.
2739 */
2740 _focusedItem: null, 1409 _focusedItem: null,
2741
2742 /**
2743 * The index of the `_focusedItem`.
2744 */
2745 _focusedIndex: -1, 1410 _focusedIndex: -1,
2746
2747 /**
2748 * The the item that is focused if it is moved offscreen.
2749 * @private {?TemplatizerNode}
2750 */
2751 _offscreenFocusedItem: null, 1411 _offscreenFocusedItem: null,
2752
2753 /**
2754 * The item that backfills the `_offscreenFocusedItem` in the physical items
2755 * list when that item is moved offscreen.
2756 */
2757 _focusBackfillItem: null, 1412 _focusBackfillItem: null,
2758
2759 /**
2760 * The maximum items per row
2761 */
2762 _itemsPerRow: 1, 1413 _itemsPerRow: 1,
2763
2764 /**
2765 * The width of each grid item
2766 */
2767 _itemWidth: 0, 1414 _itemWidth: 0,
2768
2769 /**
2770 * The height of the row in grid layout.
2771 */
2772 _rowHeight: 0, 1415 _rowHeight: 0,
2773
2774 /**
2775 * The bottom of the physical content.
2776 */
2777 get _physicalBottom() { 1416 get _physicalBottom() {
2778 return this._physicalTop + this._physicalSize; 1417 return this._physicalTop + this._physicalSize;
2779 }, 1418 },
2780
2781 /**
2782 * The bottom of the scroll.
2783 */
2784 get _scrollBottom() { 1419 get _scrollBottom() {
2785 return this._scrollPosition + this._viewportHeight; 1420 return this._scrollPosition + this._viewportHeight;
2786 }, 1421 },
2787
2788 /**
2789 * The n-th item rendered in the last physical item.
2790 */
2791 get _virtualEnd() { 1422 get _virtualEnd() {
2792 return this._virtualStart + this._physicalCount - 1; 1423 return this._virtualStart + this._physicalCount - 1;
2793 }, 1424 },
2794
2795 /**
2796 * The height of the physical content that isn't on the screen.
2797 */
2798 get _hiddenContentSize() { 1425 get _hiddenContentSize() {
2799 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize; 1426 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
2800 return size - this._viewportHeight; 1427 return size - this._viewportHeight;
2801 }, 1428 },
2802
2803 /**
2804 * The maximum scroll top value.
2805 */
2806 get _maxScrollTop() { 1429 get _maxScrollTop() {
2807 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop; 1430 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
2808 }, 1431 },
2809
2810 /**
2811 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
2812 */
2813 _minVirtualStart: 0, 1432 _minVirtualStart: 0,
2814
2815 /**
2816 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
2817 */
2818 get _maxVirtualStart() { 1433 get _maxVirtualStart() {
2819 return Math.max(0, this._virtualCount - this._physicalCount); 1434 return Math.max(0, this._virtualCount - this._physicalCount);
2820 }, 1435 },
2821
2822 /**
2823 * The n-th item rendered in the `_physicalStart` tile.
2824 */
2825 _virtualStartVal: 0, 1436 _virtualStartVal: 0,
2826
2827 set _virtualStart(val) { 1437 set _virtualStart(val) {
2828 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); 1438 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
2829 }, 1439 },
2830
2831 get _virtualStart() { 1440 get _virtualStart() {
2832 return this._virtualStartVal || 0; 1441 return this._virtualStartVal || 0;
2833 }, 1442 },
2834
2835 /**
2836 * The k-th tile that is at the top of the scrolling list.
2837 */
2838 _physicalStartVal: 0, 1443 _physicalStartVal: 0,
2839
2840 set _physicalStart(val) { 1444 set _physicalStart(val) {
2841 this._physicalStartVal = val % this._physicalCount; 1445 this._physicalStartVal = val % this._physicalCount;
2842 if (this._physicalStartVal < 0) { 1446 if (this._physicalStartVal < 0) {
2843 this._physicalStartVal = this._physicalCount + this._physicalStartVal; 1447 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
2844 } 1448 }
2845 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 1449 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2846 }, 1450 },
2847
2848 get _physicalStart() { 1451 get _physicalStart() {
2849 return this._physicalStartVal || 0; 1452 return this._physicalStartVal || 0;
2850 }, 1453 },
2851
2852 /**
2853 * The number of tiles in the DOM.
2854 */
2855 _physicalCountVal: 0, 1454 _physicalCountVal: 0,
2856
2857 set _physicalCount(val) { 1455 set _physicalCount(val) {
2858 this._physicalCountVal = val; 1456 this._physicalCountVal = val;
2859 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 1457 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2860 }, 1458 },
2861
2862 get _physicalCount() { 1459 get _physicalCount() {
2863 return this._physicalCountVal; 1460 return this._physicalCountVal;
2864 }, 1461 },
2865
2866 /**
2867 * The k-th tile that is at the bottom of the scrolling list.
2868 */
2869 _physicalEnd: 0, 1462 _physicalEnd: 0,
2870
2871 /**
2872 * An optimal physical size such that we will have enough physical items
2873 * to fill up the viewport and recycle when the user scrolls.
2874 *
2875 * This default value assumes that we will at least have the equivalent
2876 * to a viewport of physical items above and below the user's viewport.
2877 */
2878 get _optPhysicalSize() { 1463 get _optPhysicalSize() {
2879 if (this.grid) { 1464 if (this.grid) {
2880 return this._estRowsInView * this._rowHeight * this._maxPages; 1465 return this._estRowsInView * this._rowHeight * this._maxPages;
2881 } 1466 }
2882 return this._viewportHeight * this._maxPages; 1467 return this._viewportHeight * this._maxPages;
2883 }, 1468 },
2884
2885 get _optPhysicalCount() { 1469 get _optPhysicalCount() {
2886 return this._estRowsInView * this._itemsPerRow * this._maxPages; 1470 return this._estRowsInView * this._itemsPerRow * this._maxPages;
2887 }, 1471 },
2888
2889 /**
2890 * True if the current list is visible.
2891 */
2892 get _isVisible() { 1472 get _isVisible() {
2893 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight); 1473 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
2894 }, 1474 },
2895
2896 /**
2897 * Gets the index of the first visible item in the viewport.
2898 *
2899 * @type {number}
2900 */
2901 get firstVisibleIndex() { 1475 get firstVisibleIndex() {
2902 if (this._firstVisibleIndexVal === null) { 1476 if (this._firstVisibleIndexVal === null) {
2903 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 1477 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
2904 1478 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) {
2905 this._firstVisibleIndexVal = this._iterateItems( 1479 physicalOffset += this._getPhysicalSizeIncrement(pidx);
2906 function(pidx, vidx) { 1480 if (physicalOffset > this._scrollPosition) {
2907 physicalOffset += this._getPhysicalSizeIncrement(pidx); 1481 return this.grid ? vidx - vidx % this._itemsPerRow : vidx;
2908 1482 }
2909 if (physicalOffset > this._scrollPosition) { 1483 if (this.grid && this._virtualCount - 1 === vidx) {
2910 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; 1484 return vidx - vidx % this._itemsPerRow;
2911 } 1485 }
2912 // Handle a partially rendered final row in grid mode 1486 }) || 0;
2913 if (this.grid && this._virtualCount - 1 === vidx) {
2914 return vidx - (vidx % this._itemsPerRow);
2915 }
2916 }) || 0;
2917 } 1487 }
2918 return this._firstVisibleIndexVal; 1488 return this._firstVisibleIndexVal;
2919 }, 1489 },
2920
2921 /**
2922 * Gets the index of the last visible item in the viewport.
2923 *
2924 * @type {number}
2925 */
2926 get lastVisibleIndex() { 1490 get lastVisibleIndex() {
2927 if (this._lastVisibleIndexVal === null) { 1491 if (this._lastVisibleIndexVal === null) {
2928 if (this.grid) { 1492 if (this.grid) {
2929 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 1493 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
2930 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 1494 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
2931 } else { 1495 } else {
2932 var physicalOffset = this._physicalTop; 1496 var physicalOffset = this._physicalTop;
2933 this._iterateItems(function(pidx, vidx) { 1497 this._iterateItems(function(pidx, vidx) {
2934 if (physicalOffset < this._scrollBottom) { 1498 if (physicalOffset < this._scrollBottom) {
2935 this._lastVisibleIndexVal = vidx; 1499 this._lastVisibleIndexVal = vidx;
2936 } else { 1500 } else {
2937 // Break _iterateItems
2938 return true; 1501 return true;
2939 } 1502 }
2940 physicalOffset += this._getPhysicalSizeIncrement(pidx); 1503 physicalOffset += this._getPhysicalSizeIncrement(pidx);
2941 }); 1504 });
2942 } 1505 }
2943 } 1506 }
2944 return this._lastVisibleIndexVal; 1507 return this._lastVisibleIndexVal;
2945 }, 1508 },
2946
2947 get _defaultScrollTarget() { 1509 get _defaultScrollTarget() {
2948 return this; 1510 return this;
2949 }, 1511 },
2950 get _virtualRowCount() { 1512 get _virtualRowCount() {
2951 return Math.ceil(this._virtualCount / this._itemsPerRow); 1513 return Math.ceil(this._virtualCount / this._itemsPerRow);
2952 }, 1514 },
2953
2954 get _estRowsInView() { 1515 get _estRowsInView() {
2955 return Math.ceil(this._viewportHeight / this._rowHeight); 1516 return Math.ceil(this._viewportHeight / this._rowHeight);
2956 }, 1517 },
2957
2958 get _physicalRows() { 1518 get _physicalRows() {
2959 return Math.ceil(this._physicalCount / this._itemsPerRow); 1519 return Math.ceil(this._physicalCount / this._itemsPerRow);
2960 }, 1520 },
2961
2962 ready: function() { 1521 ready: function() {
2963 this.addEventListener('focus', this._didFocus.bind(this), true); 1522 this.addEventListener('focus', this._didFocus.bind(this), true);
2964 }, 1523 },
2965
2966 attached: function() { 1524 attached: function() {
2967 this.updateViewportBoundaries(); 1525 this.updateViewportBoundaries();
2968 this._render(); 1526 this._render();
2969 // `iron-resize` is fired when the list is attached if the event is added
2970 // before attached causing unnecessary work.
2971 this.listen(this, 'iron-resize', '_resizeHandler'); 1527 this.listen(this, 'iron-resize', '_resizeHandler');
2972 }, 1528 },
2973
2974 detached: function() { 1529 detached: function() {
2975 this._itemsRendered = false; 1530 this._itemsRendered = false;
2976 this.unlisten(this, 'iron-resize', '_resizeHandler'); 1531 this.unlisten(this, 'iron-resize', '_resizeHandler');
2977 }, 1532 },
2978
2979 /**
2980 * Set the overflow property if this element has its own scrolling region
2981 */
2982 _setOverflow: function(scrollTarget) { 1533 _setOverflow: function(scrollTarget) {
2983 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; 1534 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
2984 this.style.overflow = scrollTarget === this ? 'auto' : ''; 1535 this.style.overflow = scrollTarget === this ? 'auto' : '';
2985 }, 1536 },
2986
2987 /**
2988 * Invoke this method if you dynamically update the viewport's
2989 * size or CSS padding.
2990 *
2991 * @method updateViewportBoundaries
2992 */
2993 updateViewportBoundaries: function() { 1537 updateViewportBoundaries: function() {
2994 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : 1538 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo w.getComputedStyle(this)['padding-top'], 10);
2995 parseInt(window.getComputedStyle(this)['padding-top'], 10);
2996
2997 this._viewportHeight = this._scrollTargetHeight; 1539 this._viewportHeight = this._scrollTargetHeight;
2998 if (this.grid) { 1540 if (this.grid) {
2999 this._updateGridMetrics(); 1541 this._updateGridMetrics();
3000 } 1542 }
3001 }, 1543 },
3002
3003 /**
3004 * Update the models, the position of the
3005 * items in the viewport and recycle tiles as needed.
3006 */
3007 _scrollHandler: function() { 1544 _scrollHandler: function() {
3008 // clamp the `scrollTop` value
3009 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; 1545 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
3010 var delta = scrollTop - this._scrollPosition; 1546 var delta = scrollTop - this._scrollPosition;
3011 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; 1547 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
3012 var ratio = this._ratio; 1548 var ratio = this._ratio;
3013 var recycledTiles = 0; 1549 var recycledTiles = 0;
3014 var hiddenContentSize = this._hiddenContentSize; 1550 var hiddenContentSize = this._hiddenContentSize;
3015 var currentRatio = ratio; 1551 var currentRatio = ratio;
3016 var movingUp = []; 1552 var movingUp = [];
3017
3018 // track the last `scrollTop`
3019 this._scrollPosition = scrollTop; 1553 this._scrollPosition = scrollTop;
3020
3021 // clear cached visible indexes
3022 this._firstVisibleIndexVal = null; 1554 this._firstVisibleIndexVal = null;
3023 this._lastVisibleIndexVal = null; 1555 this._lastVisibleIndexVal = null;
3024
3025 scrollBottom = this._scrollBottom; 1556 scrollBottom = this._scrollBottom;
3026 physicalBottom = this._physicalBottom; 1557 physicalBottom = this._physicalBottom;
3027
3028 // random access
3029 if (Math.abs(delta) > this._physicalSize) { 1558 if (Math.abs(delta) > this._physicalSize) {
3030 this._physicalTop += delta; 1559 this._physicalTop += delta;
3031 recycledTiles = Math.round(delta / this._physicalAverage); 1560 recycledTiles = Math.round(delta / this._physicalAverage);
3032 } 1561 } else if (delta < 0) {
3033 // scroll up
3034 else if (delta < 0) {
3035 var topSpace = scrollTop - this._physicalTop; 1562 var topSpace = scrollTop - this._physicalTop;
3036 var virtualStart = this._virtualStart; 1563 var virtualStart = this._virtualStart;
3037
3038 recycledTileSet = []; 1564 recycledTileSet = [];
3039
3040 kth = this._physicalEnd; 1565 kth = this._physicalEnd;
3041 currentRatio = topSpace / hiddenContentSize; 1566 currentRatio = topSpace / hiddenContentSize;
3042 1567 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen t(kth) > scrollBottom) {
3043 // move tiles from bottom to top
3044 while (
3045 // approximate `currentRatio` to `ratio`
3046 currentRatio < ratio &&
3047 // recycle less physical items than the total
3048 recycledTiles < this._physicalCount &&
3049 // ensure that these recycled tiles are needed
3050 virtualStart - recycledTiles > 0 &&
3051 // ensure that the tile is not visible
3052 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
3053 ) {
3054
3055 tileHeight = this._getPhysicalSizeIncrement(kth); 1568 tileHeight = this._getPhysicalSizeIncrement(kth);
3056 currentRatio += tileHeight / hiddenContentSize; 1569 currentRatio += tileHeight / hiddenContentSize;
3057 physicalBottom -= tileHeight; 1570 physicalBottom -= tileHeight;
3058 recycledTileSet.push(kth); 1571 recycledTileSet.push(kth);
3059 recycledTiles++; 1572 recycledTiles++;
3060 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; 1573 kth = kth === 0 ? this._physicalCount - 1 : kth - 1;
3061 } 1574 }
3062
3063 movingUp = recycledTileSet; 1575 movingUp = recycledTileSet;
3064 recycledTiles = -recycledTiles; 1576 recycledTiles = -recycledTiles;
3065 } 1577 } else if (delta > 0) {
3066 // scroll down
3067 else if (delta > 0) {
3068 var bottomSpace = physicalBottom - scrollBottom; 1578 var bottomSpace = physicalBottom - scrollBottom;
3069 var virtualEnd = this._virtualEnd; 1579 var virtualEnd = this._virtualEnd;
3070 var lastVirtualItemIndex = this._virtualCount-1; 1580 var lastVirtualItemIndex = this._virtualCount - 1;
3071
3072 recycledTileSet = []; 1581 recycledTileSet = [];
3073
3074 kth = this._physicalStart; 1582 kth = this._physicalStart;
3075 currentRatio = bottomSpace / hiddenContentSize; 1583 currentRatio = bottomSpace / hiddenContentSize;
3076 1584 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get PhysicalSizeIncrement(kth) < scrollTop) {
3077 // move tiles from top to bottom
3078 while (
3079 // approximate `currentRatio` to `ratio`
3080 currentRatio < ratio &&
3081 // recycle less physical items than the total
3082 recycledTiles < this._physicalCount &&
3083 // ensure that these recycled tiles are needed
3084 virtualEnd + recycledTiles < lastVirtualItemIndex &&
3085 // ensure that the tile is not visible
3086 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
3087 ) {
3088
3089 tileHeight = this._getPhysicalSizeIncrement(kth); 1585 tileHeight = this._getPhysicalSizeIncrement(kth);
3090 currentRatio += tileHeight / hiddenContentSize; 1586 currentRatio += tileHeight / hiddenContentSize;
3091
3092 this._physicalTop += tileHeight; 1587 this._physicalTop += tileHeight;
3093 recycledTileSet.push(kth); 1588 recycledTileSet.push(kth);
3094 recycledTiles++; 1589 recycledTiles++;
3095 kth = (kth + 1) % this._physicalCount; 1590 kth = (kth + 1) % this._physicalCount;
3096 } 1591 }
3097 } 1592 }
3098
3099 if (recycledTiles === 0) { 1593 if (recycledTiles === 0) {
3100 // Try to increase the pool if the list's client height isn't filled up with physical items
3101 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { 1594 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
3102 this._increasePoolIfNeeded(); 1595 this._increasePoolIfNeeded();
3103 } 1596 }
3104 } else { 1597 } else {
3105 this._virtualStart = this._virtualStart + recycledTiles; 1598 this._virtualStart = this._virtualStart + recycledTiles;
3106 this._physicalStart = this._physicalStart + recycledTiles; 1599 this._physicalStart = this._physicalStart + recycledTiles;
3107 this._update(recycledTileSet, movingUp); 1600 this._update(recycledTileSet, movingUp);
3108 } 1601 }
3109 }, 1602 },
3110
3111 /**
3112 * Update the list of items, starting from the `_virtualStart` item.
3113 * @param {!Array<number>=} itemSet
3114 * @param {!Array<number>=} movingUp
3115 */
3116 _update: function(itemSet, movingUp) { 1603 _update: function(itemSet, movingUp) {
3117 // manage focus
3118 this._manageFocus(); 1604 this._manageFocus();
3119 // update models
3120 this._assignModels(itemSet); 1605 this._assignModels(itemSet);
3121 // measure heights
3122 this._updateMetrics(itemSet); 1606 this._updateMetrics(itemSet);
3123 // adjust offset after measuring
3124 if (movingUp) { 1607 if (movingUp) {
3125 while (movingUp.length) { 1608 while (movingUp.length) {
3126 var idx = movingUp.pop(); 1609 var idx = movingUp.pop();
3127 this._physicalTop -= this._getPhysicalSizeIncrement(idx); 1610 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
3128 } 1611 }
3129 } 1612 }
3130 // update the position of the items
3131 this._positionItems(); 1613 this._positionItems();
3132 // set the scroller size
3133 this._updateScrollerSize(); 1614 this._updateScrollerSize();
3134 // increase the pool of physical items
3135 this._increasePoolIfNeeded(); 1615 this._increasePoolIfNeeded();
3136 }, 1616 },
3137
3138 /**
3139 * Creates a pool of DOM elements and attaches them to the local dom.
3140 */
3141 _createPool: function(size) { 1617 _createPool: function(size) {
3142 var physicalItems = new Array(size); 1618 var physicalItems = new Array(size);
3143
3144 this._ensureTemplatized(); 1619 this._ensureTemplatized();
3145
3146 for (var i = 0; i < size; i++) { 1620 for (var i = 0; i < size; i++) {
3147 var inst = this.stamp(null); 1621 var inst = this.stamp(null);
3148 // First element child is item; Safari doesn't support children[0]
3149 // on a doc fragment
3150 physicalItems[i] = inst.root.querySelector('*'); 1622 physicalItems[i] = inst.root.querySelector('*');
3151 Polymer.dom(this).appendChild(inst.root); 1623 Polymer.dom(this).appendChild(inst.root);
3152 } 1624 }
3153 return physicalItems; 1625 return physicalItems;
3154 }, 1626 },
3155
3156 /**
3157 * Increases the pool of physical items only if needed.
3158 *
3159 * @return {boolean} True if the pool was increased.
3160 */
3161 _increasePoolIfNeeded: function() { 1627 _increasePoolIfNeeded: function() {
3162 // Base case 1: the list has no height.
3163 if (this._viewportHeight === 0) { 1628 if (this._viewportHeight === 0) {
3164 return false; 1629 return false;
3165 } 1630 }
3166 // Base case 2: If the physical size is optimal and the list's client heig ht is full
3167 // with physical items, don't increase the pool.
3168 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition; 1631 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
3169 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { 1632 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
3170 return false; 1633 return false;
3171 } 1634 }
3172 // this value should range between [0 <= `currentPage` <= `_maxPages`]
3173 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); 1635 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
3174
3175 if (currentPage === 0) { 1636 if (currentPage === 0) {
3176 // fill the first page 1637 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * .5)));
3177 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
3178 } else if (this._lastPage !== currentPage && isClientHeightFull) { 1638 } else if (this._lastPage !== currentPage && isClientHeightFull) {
3179 // paint the page and defer the next increase
3180 // wait 16ms which is rough enough to get paint cycle.
3181 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16)); 1639 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
3182 } else { 1640 } else {
3183 // fill the rest of the pages
3184 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ; 1641 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
3185 } 1642 }
3186
3187 this._lastPage = currentPage; 1643 this._lastPage = currentPage;
3188
3189 return true; 1644 return true;
3190 }, 1645 },
3191
3192 /**
3193 * Increases the pool size.
3194 */
3195 _increasePool: function(missingItems) { 1646 _increasePool: function(missingItems) {
3196 var nextPhysicalCount = Math.min( 1647 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this. _virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS ICAL_COUNT));
3197 this._physicalCount + missingItems,
3198 this._virtualCount - this._virtualStart,
3199 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
3200 );
3201 var prevPhysicalCount = this._physicalCount; 1648 var prevPhysicalCount = this._physicalCount;
3202 var delta = nextPhysicalCount - prevPhysicalCount; 1649 var delta = nextPhysicalCount - prevPhysicalCount;
3203
3204 if (delta <= 0) { 1650 if (delta <= 0) {
3205 return; 1651 return;
3206 } 1652 }
3207
3208 [].push.apply(this._physicalItems, this._createPool(delta)); 1653 [].push.apply(this._physicalItems, this._createPool(delta));
3209 [].push.apply(this._physicalSizes, new Array(delta)); 1654 [].push.apply(this._physicalSizes, new Array(delta));
3210
3211 this._physicalCount = prevPhysicalCount + delta; 1655 this._physicalCount = prevPhysicalCount + delta;
3212 1656 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this. _focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd ) {
3213 // update the physical start if we need to preserve the model of the focus ed item.
3214 // In this situation, the focused item is currently rendered and its model would
3215 // have changed after increasing the pool if the physical start remained u nchanged.
3216 if (this._physicalStart > this._physicalEnd &&
3217 this._isIndexRendered(this._focusedIndex) &&
3218 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
3219 this._physicalStart = this._physicalStart + delta; 1657 this._physicalStart = this._physicalStart + delta;
3220 } 1658 }
3221 this._update(); 1659 this._update();
3222 }, 1660 },
3223
3224 /**
3225 * Render a new list of items. This method does exactly the same as `update` ,
3226 * but it also ensures that only one `update` cycle is created.
3227 */
3228 _render: function() { 1661 _render: function() {
3229 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; 1662 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
3230
3231 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { 1663 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
3232 this._lastPage = 0; 1664 this._lastPage = 0;
3233 this._update(); 1665 this._update();
3234 this._itemsRendered = true; 1666 this._itemsRendered = true;
3235 } 1667 }
3236 }, 1668 },
3237
3238 /**
3239 * Templetizes the user template.
3240 */
3241 _ensureTemplatized: function() { 1669 _ensureTemplatized: function() {
3242 if (!this.ctor) { 1670 if (!this.ctor) {
3243 // Template instance props that should be excluded from forwarding
3244 var props = {}; 1671 var props = {};
3245 props.__key__ = true; 1672 props.__key__ = true;
3246 props[this.as] = true; 1673 props[this.as] = true;
3247 props[this.indexAs] = true; 1674 props[this.indexAs] = true;
3248 props[this.selectedAs] = true; 1675 props[this.selectedAs] = true;
3249 props.tabIndex = true; 1676 props.tabIndex = true;
3250
3251 this._instanceProps = props; 1677 this._instanceProps = props;
3252 this._userTemplate = Polymer.dom(this).querySelector('template'); 1678 this._userTemplate = Polymer.dom(this).querySelector('template');
3253
3254 if (this._userTemplate) { 1679 if (this._userTemplate) {
3255 this.templatize(this._userTemplate); 1680 this.templatize(this._userTemplate);
3256 } else { 1681 } else {
3257 console.warn('iron-list requires a template to be provided in light-do m'); 1682 console.warn('iron-list requires a template to be provided in light-do m');
3258 } 1683 }
3259 } 1684 }
3260 }, 1685 },
3261
3262 /**
3263 * Implements extension point from Templatizer mixin.
3264 */
3265 _getStampedChildren: function() { 1686 _getStampedChildren: function() {
3266 return this._physicalItems; 1687 return this._physicalItems;
3267 }, 1688 },
3268
3269 /**
3270 * Implements extension point from Templatizer
3271 * Called as a side effect of a template instance path change, responsible
3272 * for notifying items.<key-for-instance>.<path> change up to host.
3273 */
3274 _forwardInstancePath: function(inst, path, value) { 1689 _forwardInstancePath: function(inst, path, value) {
3275 if (path.indexOf(this.as + '.') === 0) { 1690 if (path.indexOf(this.as + '.') === 0) {
3276 this.notifyPath('items.' + inst.__key__ + '.' + 1691 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt h + 1), value);
3277 path.slice(this.as.length + 1), value);
3278 } 1692 }
3279 }, 1693 },
3280
3281 /**
3282 * Implements extension point from Templatizer mixin
3283 * Called as side-effect of a host property change, responsible for
3284 * notifying parent path change on each row.
3285 */
3286 _forwardParentProp: function(prop, value) { 1694 _forwardParentProp: function(prop, value) {
3287 if (this._physicalItems) { 1695 if (this._physicalItems) {
3288 this._physicalItems.forEach(function(item) { 1696 this._physicalItems.forEach(function(item) {
3289 item._templateInstance[prop] = value; 1697 item._templateInstance[prop] = value;
3290 }, this); 1698 }, this);
3291 } 1699 }
3292 }, 1700 },
3293
3294 /**
3295 * Implements extension point from Templatizer
3296 * Called as side-effect of a host path change, responsible for
3297 * notifying parent.<path> path change on each row.
3298 */
3299 _forwardParentPath: function(path, value) { 1701 _forwardParentPath: function(path, value) {
3300 if (this._physicalItems) { 1702 if (this._physicalItems) {
3301 this._physicalItems.forEach(function(item) { 1703 this._physicalItems.forEach(function(item) {
3302 item._templateInstance.notifyPath(path, value, true); 1704 item._templateInstance.notifyPath(path, value, true);
3303 }, this); 1705 }, this);
3304 } 1706 }
3305 }, 1707 },
3306
3307 /**
3308 * Called as a side effect of a host items.<key>.<path> path change,
3309 * responsible for notifying item.<path> changes.
3310 */
3311 _forwardItemPath: function(path, value) { 1708 _forwardItemPath: function(path, value) {
3312 if (!this._physicalIndexForKey) { 1709 if (!this._physicalIndexForKey) {
3313 return; 1710 return;
3314 } 1711 }
3315 var dot = path.indexOf('.'); 1712 var dot = path.indexOf('.');
3316 var key = path.substring(0, dot < 0 ? path.length : dot); 1713 var key = path.substring(0, dot < 0 ? path.length : dot);
3317 var idx = this._physicalIndexForKey[key]; 1714 var idx = this._physicalIndexForKey[key];
3318 var offscreenItem = this._offscreenFocusedItem; 1715 var offscreenItem = this._offscreenFocusedItem;
3319 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? 1716 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? offscreenItem : this._physicalItems[idx];
3320 offscreenItem : this._physicalItems[idx];
3321
3322 if (!el || el._templateInstance.__key__ !== key) { 1717 if (!el || el._templateInstance.__key__ !== key) {
3323 return; 1718 return;
3324 } 1719 }
3325 if (dot >= 0) { 1720 if (dot >= 0) {
3326 path = this.as + '.' + path.substring(dot+1); 1721 path = this.as + '.' + path.substring(dot + 1);
3327 el._templateInstance.notifyPath(path, value, true); 1722 el._templateInstance.notifyPath(path, value, true);
3328 } else { 1723 } else {
3329 // Update selection if needed
3330 var currentItem = el._templateInstance[this.as]; 1724 var currentItem = el._templateInstance[this.as];
3331 if (Array.isArray(this.selectedItems)) { 1725 if (Array.isArray(this.selectedItems)) {
3332 for (var i = 0; i < this.selectedItems.length; i++) { 1726 for (var i = 0; i < this.selectedItems.length; i++) {
3333 if (this.selectedItems[i] === currentItem) { 1727 if (this.selectedItems[i] === currentItem) {
3334 this.set('selectedItems.' + i, value); 1728 this.set('selectedItems.' + i, value);
3335 break; 1729 break;
3336 } 1730 }
3337 } 1731 }
3338 } else if (this.selectedItem === currentItem) { 1732 } else if (this.selectedItem === currentItem) {
3339 this.set('selectedItem', value); 1733 this.set('selectedItem', value);
3340 } 1734 }
3341 el._templateInstance[this.as] = value; 1735 el._templateInstance[this.as] = value;
3342 } 1736 }
3343 }, 1737 },
3344
3345 /**
3346 * Called when the items have changed. That is, ressignments
3347 * to `items`, splices or updates to a single item.
3348 */
3349 _itemsChanged: function(change) { 1738 _itemsChanged: function(change) {
3350 if (change.path === 'items') { 1739 if (change.path === 'items') {
3351 // reset items
3352 this._virtualStart = 0; 1740 this._virtualStart = 0;
3353 this._physicalTop = 0; 1741 this._physicalTop = 0;
3354 this._virtualCount = this.items ? this.items.length : 0; 1742 this._virtualCount = this.items ? this.items.length : 0;
3355 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; 1743 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
3356 this._physicalIndexForKey = {}; 1744 this._physicalIndexForKey = {};
3357 this._firstVisibleIndexVal = null; 1745 this._firstVisibleIndexVal = null;
3358 this._lastVisibleIndexVal = null; 1746 this._lastVisibleIndexVal = null;
3359
3360 this._resetScrollPosition(0); 1747 this._resetScrollPosition(0);
3361 this._removeFocusedItem(); 1748 this._removeFocusedItem();
3362 // create the initial physical items
3363 if (!this._physicalItems) { 1749 if (!this._physicalItems) {
3364 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); 1750 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
3365 this._physicalItems = this._createPool(this._physicalCount); 1751 this._physicalItems = this._createPool(this._physicalCount);
3366 this._physicalSizes = new Array(this._physicalCount); 1752 this._physicalSizes = new Array(this._physicalCount);
3367 } 1753 }
3368
3369 this._physicalStart = 0; 1754 this._physicalStart = 0;
3370
3371 } else if (change.path === 'items.splices') { 1755 } else if (change.path === 'items.splices') {
3372
3373 this._adjustVirtualIndex(change.value.indexSplices); 1756 this._adjustVirtualIndex(change.value.indexSplices);
3374 this._virtualCount = this.items ? this.items.length : 0; 1757 this._virtualCount = this.items ? this.items.length : 0;
3375
3376 } else { 1758 } else {
3377 // update a single item
3378 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); 1759 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
3379 return; 1760 return;
3380 } 1761 }
3381
3382 this._itemsRendered = false; 1762 this._itemsRendered = false;
3383 this._debounceTemplate(this._render); 1763 this._debounceTemplate(this._render);
3384 }, 1764 },
3385
3386 /**
3387 * @param {!Array<!PolymerSplice>} splices
3388 */
3389 _adjustVirtualIndex: function(splices) { 1765 _adjustVirtualIndex: function(splices) {
3390 splices.forEach(function(splice) { 1766 splices.forEach(function(splice) {
3391 // deselect removed items
3392 splice.removed.forEach(this._removeItem, this); 1767 splice.removed.forEach(this._removeItem, this);
3393 // We only need to care about changes happening above the current positi on
3394 if (splice.index < this._virtualStart) { 1768 if (splice.index < this._virtualStart) {
3395 var delta = Math.max( 1769 var delta = Math.max(splice.addedCount - splice.removed.length, splice .index - this._virtualStart);
3396 splice.addedCount - splice.removed.length,
3397 splice.index - this._virtualStart);
3398
3399 this._virtualStart = this._virtualStart + delta; 1770 this._virtualStart = this._virtualStart + delta;
3400
3401 if (this._focusedIndex >= 0) { 1771 if (this._focusedIndex >= 0) {
3402 this._focusedIndex = this._focusedIndex + delta; 1772 this._focusedIndex = this._focusedIndex + delta;
3403 } 1773 }
3404 } 1774 }
3405 }, this); 1775 }, this);
3406 }, 1776 },
3407
3408 _removeItem: function(item) { 1777 _removeItem: function(item) {
3409 this.$.selector.deselect(item); 1778 this.$.selector.deselect(item);
3410 // remove the current focused item
3411 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) { 1779 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
3412 this._removeFocusedItem(); 1780 this._removeFocusedItem();
3413 } 1781 }
3414 }, 1782 },
3415
3416 /**
3417 * Executes a provided function per every physical index in `itemSet`
3418 * `itemSet` default value is equivalent to the entire set of physical index es.
3419 *
3420 * @param {!function(number, number)} fn
3421 * @param {!Array<number>=} itemSet
3422 */
3423 _iterateItems: function(fn, itemSet) { 1783 _iterateItems: function(fn, itemSet) {
3424 var pidx, vidx, rtn, i; 1784 var pidx, vidx, rtn, i;
3425
3426 if (arguments.length === 2 && itemSet) { 1785 if (arguments.length === 2 && itemSet) {
3427 for (i = 0; i < itemSet.length; i++) { 1786 for (i = 0; i < itemSet.length; i++) {
3428 pidx = itemSet[i]; 1787 pidx = itemSet[i];
3429 vidx = this._computeVidx(pidx); 1788 vidx = this._computeVidx(pidx);
3430 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1789 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3431 return rtn; 1790 return rtn;
3432 } 1791 }
3433 } 1792 }
3434 } else { 1793 } else {
3435 pidx = this._physicalStart; 1794 pidx = this._physicalStart;
3436 vidx = this._virtualStart; 1795 vidx = this._virtualStart;
3437 1796 for (;pidx < this._physicalCount; pidx++, vidx++) {
3438 for (; pidx < this._physicalCount; pidx++, vidx++) {
3439 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1797 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3440 return rtn; 1798 return rtn;
3441 } 1799 }
3442 } 1800 }
3443 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { 1801 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
3444 if ((rtn = fn.call(this, pidx, vidx)) != null) { 1802 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3445 return rtn; 1803 return rtn;
3446 } 1804 }
3447 } 1805 }
3448 } 1806 }
3449 }, 1807 },
3450
3451 /**
3452 * Returns the virtual index for a given physical index
3453 *
3454 * @param {number} pidx Physical index
3455 * @return {number}
3456 */
3457 _computeVidx: function(pidx) { 1808 _computeVidx: function(pidx) {
3458 if (pidx >= this._physicalStart) { 1809 if (pidx >= this._physicalStart) {
3459 return this._virtualStart + (pidx - this._physicalStart); 1810 return this._virtualStart + (pidx - this._physicalStart);
3460 } 1811 }
3461 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; 1812 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
3462 }, 1813 },
3463
3464 /**
3465 * Assigns the data models to a given set of items.
3466 * @param {!Array<number>=} itemSet
3467 */
3468 _assignModels: function(itemSet) { 1814 _assignModels: function(itemSet) {
3469 this._iterateItems(function(pidx, vidx) { 1815 this._iterateItems(function(pidx, vidx) {
3470 var el = this._physicalItems[pidx]; 1816 var el = this._physicalItems[pidx];
3471 var inst = el._templateInstance; 1817 var inst = el._templateInstance;
3472 var item = this.items && this.items[vidx]; 1818 var item = this.items && this.items[vidx];
3473
3474 if (item != null) { 1819 if (item != null) {
3475 inst[this.as] = item; 1820 inst[this.as] = item;
3476 inst.__key__ = this._collection.getKey(item); 1821 inst.__key__ = this._collection.getKey(item);
3477 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item); 1822 inst[this.selectedAs] = this.$.selector.isSelected(item);
3478 inst[this.indexAs] = vidx; 1823 inst[this.indexAs] = vidx;
3479 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; 1824 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
3480 this._physicalIndexForKey[inst.__key__] = pidx; 1825 this._physicalIndexForKey[inst.__key__] = pidx;
3481 el.removeAttribute('hidden'); 1826 el.removeAttribute('hidden');
3482 } else { 1827 } else {
3483 inst.__key__ = null; 1828 inst.__key__ = null;
3484 el.setAttribute('hidden', ''); 1829 el.setAttribute('hidden', '');
3485 } 1830 }
3486 }, itemSet); 1831 }, itemSet);
3487 }, 1832 },
3488 1833 _updateMetrics: function(itemSet) {
3489 /**
3490 * Updates the height for a given set of items.
3491 *
3492 * @param {!Array<number>=} itemSet
3493 */
3494 _updateMetrics: function(itemSet) {
3495 // Make sure we distributed all the physical items
3496 // so we can measure them
3497 Polymer.dom.flush(); 1834 Polymer.dom.flush();
3498
3499 var newPhysicalSize = 0; 1835 var newPhysicalSize = 0;
3500 var oldPhysicalSize = 0; 1836 var oldPhysicalSize = 0;
3501 var prevAvgCount = this._physicalAverageCount; 1837 var prevAvgCount = this._physicalAverageCount;
3502 var prevPhysicalAvg = this._physicalAverage; 1838 var prevPhysicalAvg = this._physicalAverage;
3503
3504 this._iterateItems(function(pidx, vidx) { 1839 this._iterateItems(function(pidx, vidx) {
3505
3506 oldPhysicalSize += this._physicalSizes[pidx] || 0; 1840 oldPhysicalSize += this._physicalSizes[pidx] || 0;
3507 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; 1841 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
3508 newPhysicalSize += this._physicalSizes[pidx]; 1842 newPhysicalSize += this._physicalSizes[pidx];
3509 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; 1843 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
3510
3511 }, itemSet); 1844 }, itemSet);
3512
3513 this._viewportHeight = this._scrollTargetHeight; 1845 this._viewportHeight = this._scrollTargetHeight;
3514 if (this.grid) { 1846 if (this.grid) {
3515 this._updateGridMetrics(); 1847 this._updateGridMetrics();
3516 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight; 1848 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
3517 } else { 1849 } else {
3518 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize; 1850 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
3519 } 1851 }
3520
3521 // update the average if we measured something
3522 if (this._physicalAverageCount !== prevAvgCount) { 1852 if (this._physicalAverageCount !== prevAvgCount) {
3523 this._physicalAverage = Math.round( 1853 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new PhysicalSize) / this._physicalAverageCount);
3524 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
3525 this._physicalAverageCount);
3526 } 1854 }
3527 }, 1855 },
3528
3529 _updateGridMetrics: function() { 1856 _updateGridMetrics: function() {
3530 this._viewportWidth = this.$.items.offsetWidth; 1857 this._viewportWidth = this.$.items.offsetWidth;
3531 // Set item width to the value of the _physicalItems offsetWidth
3532 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE; 1858 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
3533 // Set row height to the value of the _physicalItems offsetHeight
3534 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE; 1859 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
3535 // If in grid mode compute how many items with exist in each row
3536 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow; 1860 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
3537 }, 1861 },
3538
3539 /**
3540 * Updates the position of the physical items.
3541 */
3542 _positionItems: function() { 1862 _positionItems: function() {
3543 this._adjustScrollPosition(); 1863 this._adjustScrollPosition();
3544
3545 var y = this._physicalTop; 1864 var y = this._physicalTop;
3546
3547 if (this.grid) { 1865 if (this.grid) {
3548 var totalItemWidth = this._itemsPerRow * this._itemWidth; 1866 var totalItemWidth = this._itemsPerRow * this._itemWidth;
3549 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; 1867 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
3550
3551 this._iterateItems(function(pidx, vidx) { 1868 this._iterateItems(function(pidx, vidx) {
3552
3553 var modulus = vidx % this._itemsPerRow; 1869 var modulus = vidx % this._itemsPerRow;
3554 var x = Math.floor((modulus * this._itemWidth) + rowOffset); 1870 var x = Math.floor(modulus * this._itemWidth + rowOffset);
3555
3556 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); 1871 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
3557
3558 if (this._shouldRenderNextRow(vidx)) { 1872 if (this._shouldRenderNextRow(vidx)) {
3559 y += this._rowHeight; 1873 y += this._rowHeight;
3560 } 1874 }
3561
3562 }); 1875 });
3563 } else { 1876 } else {
3564 this._iterateItems(function(pidx, vidx) { 1877 this._iterateItems(function(pidx, vidx) {
3565
3566 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); 1878 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
3567 y += this._physicalSizes[pidx]; 1879 y += this._physicalSizes[pidx];
3568
3569 }); 1880 });
3570 } 1881 }
3571 }, 1882 },
3572
3573 _getPhysicalSizeIncrement: function(pidx) { 1883 _getPhysicalSizeIncrement: function(pidx) {
3574 if (!this.grid) { 1884 if (!this.grid) {
3575 return this._physicalSizes[pidx]; 1885 return this._physicalSizes[pidx];
3576 } 1886 }
3577 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) { 1887 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
3578 return 0; 1888 return 0;
3579 } 1889 }
3580 return this._rowHeight; 1890 return this._rowHeight;
3581 }, 1891 },
3582
3583 /**
3584 * Returns, based on the current index,
3585 * whether or not the next index will need
3586 * to be rendered on a new row.
3587 *
3588 * @param {number} vidx Virtual index
3589 * @return {boolean}
3590 */
3591 _shouldRenderNextRow: function(vidx) { 1892 _shouldRenderNextRow: function(vidx) {
3592 return vidx % this._itemsPerRow === this._itemsPerRow - 1; 1893 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
3593 }, 1894 },
3594
3595 /**
3596 * Adjusts the scroll position when it was overestimated.
3597 */
3598 _adjustScrollPosition: function() { 1895 _adjustScrollPosition: function() {
3599 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : 1896 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min( this._scrollPosition + this._physicalTop, 0);
3600 Math.min(this._scrollPosition + this._physicalTop, 0);
3601
3602 if (deltaHeight) { 1897 if (deltaHeight) {
3603 this._physicalTop = this._physicalTop - deltaHeight; 1898 this._physicalTop = this._physicalTop - deltaHeight;
3604 // juking scroll position during interial scrolling on iOS is no bueno
3605 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { 1899 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
3606 this._resetScrollPosition(this._scrollTop - deltaHeight); 1900 this._resetScrollPosition(this._scrollTop - deltaHeight);
3607 } 1901 }
3608 } 1902 }
3609 }, 1903 },
3610
3611 /**
3612 * Sets the position of the scroll.
3613 */
3614 _resetScrollPosition: function(pos) { 1904 _resetScrollPosition: function(pos) {
3615 if (this.scrollTarget) { 1905 if (this.scrollTarget) {
3616 this._scrollTop = pos; 1906 this._scrollTop = pos;
3617 this._scrollPosition = this._scrollTop; 1907 this._scrollPosition = this._scrollTop;
3618 } 1908 }
3619 }, 1909 },
3620
3621 /**
3622 * Sets the scroll height, that's the height of the content,
3623 *
3624 * @param {boolean=} forceUpdate If true, updates the height no matter what.
3625 */
3626 _updateScrollerSize: function(forceUpdate) { 1910 _updateScrollerSize: function(forceUpdate) {
3627 if (this.grid) { 1911 if (this.grid) {
3628 this._estScrollHeight = this._virtualRowCount * this._rowHeight; 1912 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
3629 } else { 1913 } else {
3630 this._estScrollHeight = (this._physicalBottom + 1914 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
3631 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
3632 } 1915 }
3633
3634 forceUpdate = forceUpdate || this._scrollHeight === 0; 1916 forceUpdate = forceUpdate || this._scrollHeight === 0;
3635 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; 1917 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
3636 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight; 1918 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
3637
3638 // amortize height adjustment, so it won't trigger repaints very often
3639 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { 1919 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
3640 this.$.items.style.height = this._estScrollHeight + 'px'; 1920 this.$.items.style.height = this._estScrollHeight + 'px';
3641 this._scrollHeight = this._estScrollHeight; 1921 this._scrollHeight = this._estScrollHeight;
3642 } 1922 }
3643 }, 1923 },
3644 1924 scrollToItem: function(item) {
3645 /**
3646 * Scroll to a specific item in the virtual list regardless
3647 * of the physical items in the DOM tree.
3648 *
3649 * @method scrollToItem
3650 * @param {(Object)} item The item to be scrolled to
3651 */
3652 scrollToItem: function(item){
3653 return this.scrollToIndex(this.items.indexOf(item)); 1925 return this.scrollToIndex(this.items.indexOf(item));
3654 }, 1926 },
3655
3656 /**
3657 * Scroll to a specific index in the virtual list regardless
3658 * of the physical items in the DOM tree.
3659 *
3660 * @method scrollToIndex
3661 * @param {number} idx The index of the item
3662 */
3663 scrollToIndex: function(idx) { 1927 scrollToIndex: function(idx) {
3664 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { 1928 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
3665 return; 1929 return;
3666 } 1930 }
3667
3668 Polymer.dom.flush(); 1931 Polymer.dom.flush();
3669 1932 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1);
3670 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
3671 // update the virtual start only when needed
3672 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { 1933 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
3673 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1); 1934 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1;
3674 } 1935 }
3675 // manage focus
3676 this._manageFocus(); 1936 this._manageFocus();
3677 // assign new models
3678 this._assignModels(); 1937 this._assignModels();
3679 // measure the new sizes
3680 this._updateMetrics(); 1938 this._updateMetrics();
3681 1939 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
3682 // estimate new physical offset
3683 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
3684 this._physicalTop = estPhysicalTop; 1940 this._physicalTop = estPhysicalTop;
3685
3686 var currentTopItem = this._physicalStart; 1941 var currentTopItem = this._physicalStart;
3687 var currentVirtualItem = this._virtualStart; 1942 var currentVirtualItem = this._virtualStart;
3688 var targetOffsetTop = 0; 1943 var targetOffsetTop = 0;
3689 var hiddenContentSize = this._hiddenContentSize; 1944 var hiddenContentSize = this._hiddenContentSize;
3690
3691 // scroll to the item as much as we can
3692 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { 1945 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
3693 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem); 1946 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
3694 currentTopItem = (currentTopItem + 1) % this._physicalCount; 1947 currentTopItem = (currentTopItem + 1) % this._physicalCount;
3695 currentVirtualItem++; 1948 currentVirtualItem++;
3696 } 1949 }
3697 // update the scroller size
3698 this._updateScrollerSize(true); 1950 this._updateScrollerSize(true);
3699 // update the position of the items
3700 this._positionItems(); 1951 this._positionItems();
3701 // set the new scroll position
3702 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop); 1952 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
3703 // increase the pool of physical items if needed
3704 this._increasePoolIfNeeded(); 1953 this._increasePoolIfNeeded();
3705 // clear cached visible index
3706 this._firstVisibleIndexVal = null; 1954 this._firstVisibleIndexVal = null;
3707 this._lastVisibleIndexVal = null; 1955 this._lastVisibleIndexVal = null;
3708 }, 1956 },
3709
3710 /**
3711 * Reset the physical average and the average count.
3712 */
3713 _resetAverage: function() { 1957 _resetAverage: function() {
3714 this._physicalAverage = 0; 1958 this._physicalAverage = 0;
3715 this._physicalAverageCount = 0; 1959 this._physicalAverageCount = 0;
3716 }, 1960 },
3717
3718 /**
3719 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
3720 * when the element is resized.
3721 */
3722 _resizeHandler: function() { 1961 _resizeHandler: function() {
3723 // iOS fires the resize event when the address bar slides up
3724 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) { 1962 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
3725 return; 1963 return;
3726 } 1964 }
3727 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
3728 // changing the scroll position from a resize handler would result in
3729 // the scroll position being reset. Waiting 1ms fixes the issue.
3730 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { 1965 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
3731 this.updateViewportBoundaries(); 1966 this.updateViewportBoundaries();
3732 this._render(); 1967 this._render();
3733
3734 if (this._itemsRendered && this._physicalItems && this._isVisible) { 1968 if (this._itemsRendered && this._physicalItems && this._isVisible) {
3735 this._resetAverage(); 1969 this._resetAverage();
3736 this.scrollToIndex(this.firstVisibleIndex); 1970 this.scrollToIndex(this.firstVisibleIndex);
3737 } 1971 }
3738 }.bind(this), 1)); 1972 }.bind(this), 1));
3739 }, 1973 },
3740
3741 _getModelFromItem: function(item) { 1974 _getModelFromItem: function(item) {
3742 var key = this._collection.getKey(item); 1975 var key = this._collection.getKey(item);
3743 var pidx = this._physicalIndexForKey[key]; 1976 var pidx = this._physicalIndexForKey[key];
3744
3745 if (pidx != null) { 1977 if (pidx != null) {
3746 return this._physicalItems[pidx]._templateInstance; 1978 return this._physicalItems[pidx]._templateInstance;
3747 } 1979 }
3748 return null; 1980 return null;
3749 }, 1981 },
3750
3751 /**
3752 * Gets a valid item instance from its index or the object value.
3753 *
3754 * @param {(Object|number)} item The item object or its index
3755 */
3756 _getNormalizedItem: function(item) { 1982 _getNormalizedItem: function(item) {
3757 if (this._collection.getKey(item) === undefined) { 1983 if (this._collection.getKey(item) === undefined) {
3758 if (typeof item === 'number') { 1984 if (typeof item === 'number') {
3759 item = this.items[item]; 1985 item = this.items[item];
3760 if (!item) { 1986 if (!item) {
3761 throw new RangeError('<item> not found'); 1987 throw new RangeError('<item> not found');
3762 } 1988 }
3763 return item; 1989 return item;
3764 } 1990 }
3765 throw new TypeError('<item> should be a valid item'); 1991 throw new TypeError('<item> should be a valid item');
3766 } 1992 }
3767 return item; 1993 return item;
3768 }, 1994 },
3769
3770 /**
3771 * Select the list item at the given index.
3772 *
3773 * @method selectItem
3774 * @param {(Object|number)} item The item object or its index
3775 */
3776 selectItem: function(item) { 1995 selectItem: function(item) {
3777 item = this._getNormalizedItem(item); 1996 item = this._getNormalizedItem(item);
3778 var model = this._getModelFromItem(item); 1997 var model = this._getModelFromItem(item);
3779
3780 if (!this.multiSelection && this.selectedItem) { 1998 if (!this.multiSelection && this.selectedItem) {
3781 this.deselectItem(this.selectedItem); 1999 this.deselectItem(this.selectedItem);
3782 } 2000 }
3783 if (model) { 2001 if (model) {
3784 model[this.selectedAs] = true; 2002 model[this.selectedAs] = true;
3785 } 2003 }
3786 this.$.selector.select(item); 2004 this.$.selector.select(item);
3787 this.updateSizeForItem(item); 2005 this.updateSizeForItem(item);
3788 }, 2006 },
3789
3790 /**
3791 * Deselects the given item list if it is already selected.
3792 *
3793
3794 * @method deselect
3795 * @param {(Object|number)} item The item object or its index
3796 */
3797 deselectItem: function(item) { 2007 deselectItem: function(item) {
3798 item = this._getNormalizedItem(item); 2008 item = this._getNormalizedItem(item);
3799 var model = this._getModelFromItem(item); 2009 var model = this._getModelFromItem(item);
3800
3801 if (model) { 2010 if (model) {
3802 model[this.selectedAs] = false; 2011 model[this.selectedAs] = false;
3803 } 2012 }
3804 this.$.selector.deselect(item); 2013 this.$.selector.deselect(item);
3805 this.updateSizeForItem(item); 2014 this.updateSizeForItem(item);
3806 }, 2015 },
3807
3808 /**
3809 * Select or deselect a given item depending on whether the item
3810 * has already been selected.
3811 *
3812 * @method toggleSelectionForItem
3813 * @param {(Object|number)} item The item object or its index
3814 */
3815 toggleSelectionForItem: function(item) { 2016 toggleSelectionForItem: function(item) {
3816 item = this._getNormalizedItem(item); 2017 item = this._getNormalizedItem(item);
3817 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) { 2018 if (this.$.selector.isSelected(item)) {
3818 this.deselectItem(item); 2019 this.deselectItem(item);
3819 } else { 2020 } else {
3820 this.selectItem(item); 2021 this.selectItem(item);
3821 } 2022 }
3822 }, 2023 },
3823
3824 /**
3825 * Clears the current selection state of the list.
3826 *
3827 * @method clearSelection
3828 */
3829 clearSelection: function() { 2024 clearSelection: function() {
3830 function unselect(item) { 2025 function unselect(item) {
3831 var model = this._getModelFromItem(item); 2026 var model = this._getModelFromItem(item);
3832 if (model) { 2027 if (model) {
3833 model[this.selectedAs] = false; 2028 model[this.selectedAs] = false;
3834 } 2029 }
3835 } 2030 }
3836
3837 if (Array.isArray(this.selectedItems)) { 2031 if (Array.isArray(this.selectedItems)) {
3838 this.selectedItems.forEach(unselect, this); 2032 this.selectedItems.forEach(unselect, this);
3839 } else if (this.selectedItem) { 2033 } else if (this.selectedItem) {
3840 unselect.call(this, this.selectedItem); 2034 unselect.call(this, this.selectedItem);
3841 } 2035 }
3842 2036 this.$.selector.clearSelection();
3843 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
3844 }, 2037 },
3845
3846 /**
3847 * Add an event listener to `tap` if `selectionEnabled` is true,
3848 * it will remove the listener otherwise.
3849 */
3850 _selectionEnabledChanged: function(selectionEnabled) { 2038 _selectionEnabledChanged: function(selectionEnabled) {
3851 var handler = selectionEnabled ? this.listen : this.unlisten; 2039 var handler = selectionEnabled ? this.listen : this.unlisten;
3852 handler.call(this, this, 'tap', '_selectionHandler'); 2040 handler.call(this, this, 'tap', '_selectionHandler');
3853 }, 2041 },
3854
3855 /**
3856 * Select an item from an event object.
3857 */
3858 _selectionHandler: function(e) { 2042 _selectionHandler: function(e) {
3859 var model = this.modelForElement(e.target); 2043 var model = this.modelForElement(e.target);
3860 if (!model) { 2044 if (!model) {
3861 return; 2045 return;
3862 } 2046 }
3863 var modelTabIndex, activeElTabIndex; 2047 var modelTabIndex, activeElTabIndex;
3864 var target = Polymer.dom(e).path[0]; 2048 var target = Polymer.dom(e).path[0];
3865 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement; 2049 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
3866 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])]; 2050 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
3867 // Safari does not focus certain form controls via mouse 2051 if (target.localName === 'input' || target.localName === 'button' || targe t.localName === 'select') {
3868 // https://bugs.webkit.org/show_bug.cgi?id=118043
3869 if (target.localName === 'input' ||
3870 target.localName === 'button' ||
3871 target.localName === 'select') {
3872 return; 2052 return;
3873 } 2053 }
3874 // Set a temporary tabindex
3875 modelTabIndex = model.tabIndex; 2054 modelTabIndex = model.tabIndex;
3876 model.tabIndex = SECRET_TABINDEX; 2055 model.tabIndex = SECRET_TABINDEX;
3877 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; 2056 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
3878 model.tabIndex = modelTabIndex; 2057 model.tabIndex = modelTabIndex;
3879 // Only select the item if the tap wasn't on a focusable child
3880 // or the element bound to `tabIndex`
3881 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) { 2058 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
3882 return; 2059 return;
3883 } 2060 }
3884 this.toggleSelectionForItem(model[this.as]); 2061 this.toggleSelectionForItem(model[this.as]);
3885 }, 2062 },
3886
3887 _multiSelectionChanged: function(multiSelection) { 2063 _multiSelectionChanged: function(multiSelection) {
3888 this.clearSelection(); 2064 this.clearSelection();
3889 this.$.selector.multi = multiSelection; 2065 this.$.selector.multi = multiSelection;
3890 }, 2066 },
3891
3892 /**
3893 * Updates the size of an item.
3894 *
3895 * @method updateSizeForItem
3896 * @param {(Object|number)} item The item object or its index
3897 */
3898 updateSizeForItem: function(item) { 2067 updateSizeForItem: function(item) {
3899 item = this._getNormalizedItem(item); 2068 item = this._getNormalizedItem(item);
3900 var key = this._collection.getKey(item); 2069 var key = this._collection.getKey(item);
3901 var pidx = this._physicalIndexForKey[key]; 2070 var pidx = this._physicalIndexForKey[key];
3902
3903 if (pidx != null) { 2071 if (pidx != null) {
3904 this._updateMetrics([pidx]); 2072 this._updateMetrics([ pidx ]);
3905 this._positionItems(); 2073 this._positionItems();
3906 } 2074 }
3907 }, 2075 },
3908
3909 /**
3910 * Creates a temporary backfill item in the rendered pool of physical items
3911 * to replace the main focused item. The focused item has tabIndex = 0
3912 * and might be currently focused by the user.
3913 *
3914 * This dynamic replacement helps to preserve the focus state.
3915 */
3916 _manageFocus: function() { 2076 _manageFocus: function() {
3917 var fidx = this._focusedIndex; 2077 var fidx = this._focusedIndex;
3918
3919 if (fidx >= 0 && fidx < this._virtualCount) { 2078 if (fidx >= 0 && fidx < this._virtualCount) {
3920 // if it's a valid index, check if that index is rendered
3921 // in a physical item.
3922 if (this._isIndexRendered(fidx)) { 2079 if (this._isIndexRendered(fidx)) {
3923 this._restoreFocusedItem(); 2080 this._restoreFocusedItem();
3924 } else { 2081 } else {
3925 this._createFocusBackfillItem(); 2082 this._createFocusBackfillItem();
3926 } 2083 }
3927 } else if (this._virtualCount > 0 && this._physicalCount > 0) { 2084 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
3928 // otherwise, assign the initial focused index.
3929 this._focusedIndex = this._virtualStart; 2085 this._focusedIndex = this._virtualStart;
3930 this._focusedItem = this._physicalItems[this._physicalStart]; 2086 this._focusedItem = this._physicalItems[this._physicalStart];
3931 } 2087 }
3932 }, 2088 },
3933
3934 _isIndexRendered: function(idx) { 2089 _isIndexRendered: function(idx) {
3935 return idx >= this._virtualStart && idx <= this._virtualEnd; 2090 return idx >= this._virtualStart && idx <= this._virtualEnd;
3936 }, 2091 },
3937
3938 _isIndexVisible: function(idx) { 2092 _isIndexVisible: function(idx) {
3939 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; 2093 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
3940 }, 2094 },
3941
3942 _getPhysicalIndex: function(idx) { 2095 _getPhysicalIndex: function(idx) {
3943 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))]; 2096 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
3944 }, 2097 },
3945
3946 _focusPhysicalItem: function(idx) { 2098 _focusPhysicalItem: function(idx) {
3947 if (idx < 0 || idx >= this._virtualCount) { 2099 if (idx < 0 || idx >= this._virtualCount) {
3948 return; 2100 return;
3949 } 2101 }
3950 this._restoreFocusedItem(); 2102 this._restoreFocusedItem();
3951 // scroll to index to make sure it's rendered
3952 if (!this._isIndexRendered(idx)) { 2103 if (!this._isIndexRendered(idx)) {
3953 this.scrollToIndex(idx); 2104 this.scrollToIndex(idx);
3954 } 2105 }
3955
3956 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; 2106 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
3957 var model = physicalItem._templateInstance; 2107 var model = physicalItem._templateInstance;
3958 var focusable; 2108 var focusable;
3959
3960 // set a secret tab index
3961 model.tabIndex = SECRET_TABINDEX; 2109 model.tabIndex = SECRET_TABINDEX;
3962 // check if focusable element is the physical item
3963 if (physicalItem.tabIndex === SECRET_TABINDEX) { 2110 if (physicalItem.tabIndex === SECRET_TABINDEX) {
3964 focusable = physicalItem; 2111 focusable = physicalItem;
3965 } 2112 }
3966 // search for the element which tabindex is bound to the secret tab index
3967 if (!focusable) { 2113 if (!focusable) {
3968 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]'); 2114 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
3969 } 2115 }
3970 // restore the tab index
3971 model.tabIndex = 0; 2116 model.tabIndex = 0;
3972 // focus the focusable element
3973 this._focusedIndex = idx; 2117 this._focusedIndex = idx;
3974 focusable && focusable.focus(); 2118 focusable && focusable.focus();
3975 }, 2119 },
3976
3977 _removeFocusedItem: function() { 2120 _removeFocusedItem: function() {
3978 if (this._offscreenFocusedItem) { 2121 if (this._offscreenFocusedItem) {
3979 Polymer.dom(this).removeChild(this._offscreenFocusedItem); 2122 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
3980 } 2123 }
3981 this._offscreenFocusedItem = null; 2124 this._offscreenFocusedItem = null;
3982 this._focusBackfillItem = null; 2125 this._focusBackfillItem = null;
3983 this._focusedItem = null; 2126 this._focusedItem = null;
3984 this._focusedIndex = -1; 2127 this._focusedIndex = -1;
3985 }, 2128 },
3986
3987 _createFocusBackfillItem: function() { 2129 _createFocusBackfillItem: function() {
3988 var pidx, fidx = this._focusedIndex; 2130 var pidx, fidx = this._focusedIndex;
3989 if (this._offscreenFocusedItem || fidx < 0) { 2131 if (this._offscreenFocusedItem || fidx < 0) {
3990 return; 2132 return;
3991 } 2133 }
3992 if (!this._focusBackfillItem) { 2134 if (!this._focusBackfillItem) {
3993 // create a physical item, so that it backfills the focused item.
3994 var stampedTemplate = this.stamp(null); 2135 var stampedTemplate = this.stamp(null);
3995 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); 2136 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
3996 Polymer.dom(this).appendChild(stampedTemplate.root); 2137 Polymer.dom(this).appendChild(stampedTemplate.root);
3997 } 2138 }
3998 // get the physical index for the focused index
3999 pidx = this._getPhysicalIndex(fidx); 2139 pidx = this._getPhysicalIndex(fidx);
4000
4001 if (pidx != null) { 2140 if (pidx != null) {
4002 // set the offcreen focused physical item
4003 this._offscreenFocusedItem = this._physicalItems[pidx]; 2141 this._offscreenFocusedItem = this._physicalItems[pidx];
4004 // backfill the focused physical item
4005 this._physicalItems[pidx] = this._focusBackfillItem; 2142 this._physicalItems[pidx] = this._focusBackfillItem;
4006 // hide the focused physical
4007 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); 2143 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
4008 } 2144 }
4009 }, 2145 },
4010
4011 _restoreFocusedItem: function() { 2146 _restoreFocusedItem: function() {
4012 var pidx, fidx = this._focusedIndex; 2147 var pidx, fidx = this._focusedIndex;
4013
4014 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { 2148 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
4015 return; 2149 return;
4016 } 2150 }
4017 // assign models to the focused index
4018 this._assignModels(); 2151 this._assignModels();
4019 // get the new physical index for the focused index
4020 pidx = this._getPhysicalIndex(fidx); 2152 pidx = this._getPhysicalIndex(fidx);
4021
4022 if (pidx != null) { 2153 if (pidx != null) {
4023 // flip the focus backfill
4024 this._focusBackfillItem = this._physicalItems[pidx]; 2154 this._focusBackfillItem = this._physicalItems[pidx];
4025 // restore the focused physical item
4026 this._physicalItems[pidx] = this._offscreenFocusedItem; 2155 this._physicalItems[pidx] = this._offscreenFocusedItem;
4027 // reset the offscreen focused item
4028 this._offscreenFocusedItem = null; 2156 this._offscreenFocusedItem = null;
4029 // hide the physical item that backfills
4030 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); 2157 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
4031 } 2158 }
4032 }, 2159 },
4033
4034 _didFocus: function(e) { 2160 _didFocus: function(e) {
4035 var targetModel = this.modelForElement(e.target); 2161 var targetModel = this.modelForElement(e.target);
4036 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null; 2162 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
4037 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; 2163 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
4038 var fidx = this._focusedIndex; 2164 var fidx = this._focusedIndex;
4039
4040 if (!targetModel || !focusedModel) { 2165 if (!targetModel || !focusedModel) {
4041 return; 2166 return;
4042 } 2167 }
4043 if (focusedModel === targetModel) { 2168 if (focusedModel === targetModel) {
4044 // if the user focused the same item, then bring it into view if it's no t visible
4045 if (!this._isIndexVisible(fidx)) { 2169 if (!this._isIndexVisible(fidx)) {
4046 this.scrollToIndex(fidx); 2170 this.scrollToIndex(fidx);
4047 } 2171 }
4048 } else { 2172 } else {
4049 this._restoreFocusedItem(); 2173 this._restoreFocusedItem();
4050 // restore tabIndex for the currently focused item
4051 focusedModel.tabIndex = -1; 2174 focusedModel.tabIndex = -1;
4052 // set the tabIndex for the next focused item
4053 targetModel.tabIndex = 0; 2175 targetModel.tabIndex = 0;
4054 fidx = targetModel[this.indexAs]; 2176 fidx = targetModel[this.indexAs];
4055 this._focusedIndex = fidx; 2177 this._focusedIndex = fidx;
4056 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; 2178 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
4057
4058 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { 2179 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
4059 this._update(); 2180 this._update();
4060 } 2181 }
4061 } 2182 }
4062 }, 2183 },
4063
4064 _didMoveUp: function() { 2184 _didMoveUp: function() {
4065 this._focusPhysicalItem(this._focusedIndex - 1); 2185 this._focusPhysicalItem(this._focusedIndex - 1);
4066 }, 2186 },
4067
4068 _didMoveDown: function(e) { 2187 _didMoveDown: function(e) {
4069 // disable scroll when pressing the down key
4070 e.detail.keyboardEvent.preventDefault(); 2188 e.detail.keyboardEvent.preventDefault();
4071 this._focusPhysicalItem(this._focusedIndex + 1); 2189 this._focusPhysicalItem(this._focusedIndex + 1);
4072 }, 2190 },
4073
4074 _didEnter: function(e) { 2191 _didEnter: function(e) {
4075 this._focusPhysicalItem(this._focusedIndex); 2192 this._focusPhysicalItem(this._focusedIndex);
4076 this._selectionHandler(e.detail.keyboardEvent); 2193 this._selectionHandler(e.detail.keyboardEvent);
4077 } 2194 }
4078 }); 2195 });
2196 })();
4079 2197
4080 })();
4081 // Copyright 2015 The Chromium Authors. All rights reserved. 2198 // Copyright 2015 The Chromium Authors. All rights reserved.
4082 // Use of this source code is governed by a BSD-style license that can be 2199 // Use of this source code is governed by a BSD-style license that can be
4083 // found in the LICENSE file. 2200 // found in the LICENSE file.
4084
4085 cr.define('downloads', function() { 2201 cr.define('downloads', function() {
4086 /**
4087 * @param {string} chromeSendName
4088 * @return {function(string):void} A chrome.send() callback with curried name.
4089 */
4090 function chromeSendWithId(chromeSendName) { 2202 function chromeSendWithId(chromeSendName) {
4091 return function(id) { chrome.send(chromeSendName, [id]); }; 2203 return function(id) {
2204 chrome.send(chromeSendName, [ id ]);
2205 };
4092 } 2206 }
4093
4094 /** @constructor */
4095 function ActionService() { 2207 function ActionService() {
4096 /** @private {Array<string>} */
4097 this.searchTerms_ = []; 2208 this.searchTerms_ = [];
4098 } 2209 }
4099 2210 function trim(s) {
4100 /** 2211 return s.trim();
4101 * @param {string} s 2212 }
4102 * @return {string} |s| without whitespace at the beginning or end. 2213 function truthy(value) {
4103 */ 2214 return !!value;
4104 function trim(s) { return s.trim(); } 2215 }
4105
4106 /**
4107 * @param {string|undefined} value
4108 * @return {boolean} Whether |value| is truthy.
4109 */
4110 function truthy(value) { return !!value; }
4111
4112 /**
4113 * @param {string} searchText Input typed by the user into a search box.
4114 * @return {Array<string>} A list of terms extracted from |searchText|.
4115 */
4116 ActionService.splitTerms = function(searchText) { 2216 ActionService.splitTerms = function(searchText) {
4117 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
4118 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); 2217 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
4119 }; 2218 };
4120
4121 ActionService.prototype = { 2219 ActionService.prototype = {
4122 /** @param {string} id ID of the download to cancel. */
4123 cancel: chromeSendWithId('cancel'), 2220 cancel: chromeSendWithId('cancel'),
4124
4125 /** Instructs the browser to clear all finished downloads. */
4126 clearAll: function() { 2221 clearAll: function() {
4127 if (loadTimeData.getBoolean('allowDeletingHistory')) { 2222 if (loadTimeData.getBoolean('allowDeletingHistory')) {
4128 chrome.send('clearAll'); 2223 chrome.send('clearAll');
4129 this.search(''); 2224 this.search('');
4130 } 2225 }
4131 }, 2226 },
4132
4133 /** @param {string} id ID of the dangerous download to discard. */
4134 discardDangerous: chromeSendWithId('discardDangerous'), 2227 discardDangerous: chromeSendWithId('discardDangerous'),
4135
4136 /** @param {string} url URL of a file to download. */
4137 download: function(url) { 2228 download: function(url) {
4138 var a = document.createElement('a'); 2229 var a = document.createElement('a');
4139 a.href = url; 2230 a.href = url;
4140 a.setAttribute('download', ''); 2231 a.setAttribute('download', '');
4141 a.click(); 2232 a.click();
4142 }, 2233 },
4143
4144 /** @param {string} id ID of the download that the user started dragging. */
4145 drag: chromeSendWithId('drag'), 2234 drag: chromeSendWithId('drag'),
4146
4147 /** Loads more downloads with the current search terms. */
4148 loadMore: function() { 2235 loadMore: function() {
4149 chrome.send('getDownloads', this.searchTerms_); 2236 chrome.send('getDownloads', this.searchTerms_);
4150 }, 2237 },
4151
4152 /**
4153 * @return {boolean} Whether the user is currently searching for downloads
4154 * (i.e. has a non-empty search term).
4155 */
4156 isSearching: function() { 2238 isSearching: function() {
4157 return this.searchTerms_.length > 0; 2239 return this.searchTerms_.length > 0;
4158 }, 2240 },
4159
4160 /** Opens the current local destination for downloads. */
4161 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), 2241 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
4162
4163 /**
4164 * @param {string} id ID of the download to run locally on the user's box.
4165 */
4166 openFile: chromeSendWithId('openFile'), 2242 openFile: chromeSendWithId('openFile'),
4167
4168 /** @param {string} id ID the of the progressing download to pause. */
4169 pause: chromeSendWithId('pause'), 2243 pause: chromeSendWithId('pause'),
4170
4171 /** @param {string} id ID of the finished download to remove. */
4172 remove: chromeSendWithId('remove'), 2244 remove: chromeSendWithId('remove'),
4173
4174 /** @param {string} id ID of the paused download to resume. */
4175 resume: chromeSendWithId('resume'), 2245 resume: chromeSendWithId('resume'),
4176
4177 /**
4178 * @param {string} id ID of the dangerous download to save despite
4179 * warnings.
4180 */
4181 saveDangerous: chromeSendWithId('saveDangerous'), 2246 saveDangerous: chromeSendWithId('saveDangerous'),
4182
4183 /** @param {string} searchText What to search for. */
4184 search: function(searchText) { 2247 search: function(searchText) {
4185 var searchTerms = ActionService.splitTerms(searchText); 2248 var searchTerms = ActionService.splitTerms(searchText);
4186 var sameTerms = searchTerms.length == this.searchTerms_.length; 2249 var sameTerms = searchTerms.length == this.searchTerms_.length;
4187
4188 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { 2250 for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
4189 if (searchTerms[i] != this.searchTerms_[i]) 2251 if (searchTerms[i] != this.searchTerms_[i]) sameTerms = false;
4190 sameTerms = false;
4191 } 2252 }
4192 2253 if (sameTerms) return;
4193 if (sameTerms)
4194 return;
4195
4196 this.searchTerms_ = searchTerms; 2254 this.searchTerms_ = searchTerms;
4197 this.loadMore(); 2255 this.loadMore();
4198 }, 2256 },
2257 show: chromeSendWithId('show'),
2258 undo: chrome.send.bind(chrome, 'undo')
2259 };
2260 cr.addSingletonGetter(ActionService);
2261 return {
2262 ActionService: ActionService
2263 };
2264 });
4199 2265
4200 /**
4201 * Shows the local folder a finished download resides in.
4202 * @param {string} id ID of the download to show.
4203 */
4204 show: chromeSendWithId('show'),
4205
4206 /** Undo download removal. */
4207 undo: chrome.send.bind(chrome, 'undo'),
4208 };
4209
4210 cr.addSingletonGetter(ActionService);
4211
4212 return {ActionService: ActionService};
4213 });
4214 // Copyright 2015 The Chromium Authors. All rights reserved. 2266 // Copyright 2015 The Chromium Authors. All rights reserved.
4215 // Use of this source code is governed by a BSD-style license that can be 2267 // Use of this source code is governed by a BSD-style license that can be
4216 // found in the LICENSE file. 2268 // found in the LICENSE file.
4217
4218 cr.define('downloads', function() { 2269 cr.define('downloads', function() {
4219 /**
4220 * Explains why a download is in DANGEROUS state.
4221 * @enum {string}
4222 */
4223 var DangerType = { 2270 var DangerType = {
4224 NOT_DANGEROUS: 'NOT_DANGEROUS', 2271 NOT_DANGEROUS: 'NOT_DANGEROUS',
4225 DANGEROUS_FILE: 'DANGEROUS_FILE', 2272 DANGEROUS_FILE: 'DANGEROUS_FILE',
4226 DANGEROUS_URL: 'DANGEROUS_URL', 2273 DANGEROUS_URL: 'DANGEROUS_URL',
4227 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', 2274 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
4228 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', 2275 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
4229 DANGEROUS_HOST: 'DANGEROUS_HOST', 2276 DANGEROUS_HOST: 'DANGEROUS_HOST',
4230 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', 2277 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED'
4231 }; 2278 };
4232
4233 /**
4234 * The states a download can be in. These correspond to states defined in
4235 * DownloadsDOMHandler::CreateDownloadItemValue
4236 * @enum {string}
4237 */
4238 var States = { 2279 var States = {
4239 IN_PROGRESS: 'IN_PROGRESS', 2280 IN_PROGRESS: 'IN_PROGRESS',
4240 CANCELLED: 'CANCELLED', 2281 CANCELLED: 'CANCELLED',
4241 COMPLETE: 'COMPLETE', 2282 COMPLETE: 'COMPLETE',
4242 PAUSED: 'PAUSED', 2283 PAUSED: 'PAUSED',
4243 DANGEROUS: 'DANGEROUS', 2284 DANGEROUS: 'DANGEROUS',
4244 INTERRUPTED: 'INTERRUPTED', 2285 INTERRUPTED: 'INTERRUPTED'
4245 }; 2286 };
4246
4247 return { 2287 return {
4248 DangerType: DangerType, 2288 DangerType: DangerType,
4249 States: States, 2289 States: States
4250 }; 2290 };
4251 }); 2291 });
2292
4252 // Copyright 2014 The Chromium Authors. All rights reserved. 2293 // Copyright 2014 The Chromium Authors. All rights reserved.
4253 // Use of this source code is governed by a BSD-style license that can be 2294 // Use of this source code is governed by a BSD-style license that can be
4254 // found in the LICENSE file. 2295 // found in the LICENSE file.
4255
4256 // Action links are elements that are used to perform an in-page navigation or
4257 // action (e.g. showing a dialog).
4258 //
4259 // They look like normal anchor (<a>) tags as their text color is blue. However,
4260 // they're subtly different as they're not initially underlined (giving users a
4261 // clue that underlined links navigate while action links don't).
4262 //
4263 // Action links look very similar to normal links when hovered (hand cursor,
4264 // underlined). This gives the user an idea that clicking this link will do
4265 // something similar to navigation but in the same page.
4266 //
4267 // They can be created in JavaScript like this:
4268 //
4269 // var link = document.createElement('a', 'action-link'); // Note second arg.
4270 //
4271 // or with a constructor like this:
4272 //
4273 // var link = new ActionLink();
4274 //
4275 // They can be used easily from HTML as well, like so:
4276 //
4277 // <a is="action-link">Click me!</a>
4278 //
4279 // NOTE: <action-link> and document.createElement('action-link') don't work.
4280
4281 /**
4282 * @constructor
4283 * @extends {HTMLAnchorElement}
4284 */
4285 var ActionLink = document.registerElement('action-link', { 2296 var ActionLink = document.registerElement('action-link', {
4286 prototype: { 2297 prototype: {
4287 __proto__: HTMLAnchorElement.prototype, 2298 __proto__: HTMLAnchorElement.prototype,
4288
4289 /** @this {ActionLink} */
4290 createdCallback: function() { 2299 createdCallback: function() {
4291 // Action links can start disabled (e.g. <a is="action-link" disabled>).
4292 this.tabIndex = this.disabled ? -1 : 0; 2300 this.tabIndex = this.disabled ? -1 : 0;
4293 2301 if (!this.hasAttribute('role')) this.setAttribute('role', 'link');
4294 if (!this.hasAttribute('role'))
4295 this.setAttribute('role', 'link');
4296
4297 this.addEventListener('keydown', function(e) { 2302 this.addEventListener('keydown', function(e) {
4298 if (!this.disabled && e.key == 'Enter' && !this.href) { 2303 if (!this.disabled && e.key == 'Enter' && !this.href) {
4299 // Schedule a click asynchronously because other 'keydown' handlers
4300 // may still run later (e.g. document.addEventListener('keydown')).
4301 // Specifically options dialogs break when this timeout isn't here.
4302 // NOTE: this affects the "trusted" state of the ensuing click. I
4303 // haven't found anything that breaks because of this (yet).
4304 window.setTimeout(this.click.bind(this), 0); 2304 window.setTimeout(this.click.bind(this), 0);
4305 } 2305 }
4306 }); 2306 });
4307
4308 function preventDefault(e) { 2307 function preventDefault(e) {
4309 e.preventDefault(); 2308 e.preventDefault();
4310 } 2309 }
4311
4312 function removePreventDefault() { 2310 function removePreventDefault() {
4313 document.removeEventListener('selectstart', preventDefault); 2311 document.removeEventListener('selectstart', preventDefault);
4314 document.removeEventListener('mouseup', removePreventDefault); 2312 document.removeEventListener('mouseup', removePreventDefault);
4315 } 2313 }
4316
4317 this.addEventListener('mousedown', function() { 2314 this.addEventListener('mousedown', function() {
4318 // This handlers strives to match the behavior of <a href="...">.
4319
4320 // While the mouse is down, prevent text selection from dragging.
4321 document.addEventListener('selectstart', preventDefault); 2315 document.addEventListener('selectstart', preventDefault);
4322 document.addEventListener('mouseup', removePreventDefault); 2316 document.addEventListener('mouseup', removePreventDefault);
4323 2317 if (document.activeElement != this) this.classList.add('no-outline');
4324 // If focus started via mouse press, don't show an outline.
4325 if (document.activeElement != this)
4326 this.classList.add('no-outline');
4327 }); 2318 });
4328
4329 this.addEventListener('blur', function() { 2319 this.addEventListener('blur', function() {
4330 this.classList.remove('no-outline'); 2320 this.classList.remove('no-outline');
4331 }); 2321 });
4332 }, 2322 },
4333
4334 /** @type {boolean} */
4335 set disabled(disabled) { 2323 set disabled(disabled) {
4336 if (disabled) 2324 if (disabled) HTMLAnchorElement.prototype.setAttribute.call(this, 'disable d', ''); else HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled') ;
4337 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
4338 else
4339 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
4340 this.tabIndex = disabled ? -1 : 0; 2325 this.tabIndex = disabled ? -1 : 0;
4341 }, 2326 },
4342 get disabled() { 2327 get disabled() {
4343 return this.hasAttribute('disabled'); 2328 return this.hasAttribute('disabled');
4344 }, 2329 },
4345
4346 /** @override */
4347 setAttribute: function(attr, val) { 2330 setAttribute: function(attr, val) {
4348 if (attr.toLowerCase() == 'disabled') 2331 if (attr.toLowerCase() == 'disabled') this.disabled = true; else HTMLAncho rElement.prototype.setAttribute.apply(this, arguments);
4349 this.disabled = true; 2332 },
4350 else
4351 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
4352 },
4353
4354 /** @override */
4355 removeAttribute: function(attr) { 2333 removeAttribute: function(attr) {
4356 if (attr.toLowerCase() == 'disabled') 2334 if (attr.toLowerCase() == 'disabled') this.disabled = false; else HTMLAnch orElement.prototype.removeAttribute.apply(this, arguments);
4357 this.disabled = false; 2335 }
4358 else 2336 },
4359 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); 2337 "extends": 'a'
4360 },
4361 },
4362
4363 extends: 'a',
4364 }); 2338 });
2339
4365 (function() { 2340 (function() {
4366 2341 var metaDatas = {};
4367 // monostate data 2342 var metaArrays = {};
4368 var metaDatas = {}; 2343 var singleton = null;
4369 var metaArrays = {}; 2344 Polymer.IronMeta = Polymer({
4370 var singleton = null; 2345 is: 'iron-meta',
4371 2346 properties: {
4372 Polymer.IronMeta = Polymer({ 2347 type: {
4373 2348 type: String,
4374 is: 'iron-meta', 2349 value: 'default',
4375 2350 observer: '_typeChanged'
4376 properties: { 2351 },
4377 2352 key: {
4378 /** 2353 type: String,
4379 * The type of meta-data. All meta-data of the same type is stored 2354 observer: '_keyChanged'
4380 * together. 2355 },
4381 */ 2356 value: {
4382 type: { 2357 type: Object,
4383 type: String, 2358 notify: true,
4384 value: 'default', 2359 observer: '_valueChanged'
4385 observer: '_typeChanged' 2360 },
4386 }, 2361 self: {
4387 2362 type: Boolean,
4388 /** 2363 observer: '_selfChanged'
4389 * The key used to store `value` under the `type` namespace. 2364 },
4390 */ 2365 list: {
4391 key: { 2366 type: Array,
4392 type: String, 2367 notify: true
4393 observer: '_keyChanged' 2368 }
4394 }, 2369 },
4395 2370 hostAttributes: {
4396 /** 2371 hidden: true
4397 * The meta-data to store or retrieve. 2372 },
4398 */ 2373 factoryImpl: function(config) {
4399 value: { 2374 if (config) {
4400 type: Object, 2375 for (var n in config) {
4401 notify: true, 2376 switch (n) {
4402 observer: '_valueChanged' 2377 case 'type':
4403 }, 2378 case 'key':
4404 2379 case 'value':
4405 /** 2380 this[n] = config[n];
4406 * If true, `value` is set to the iron-meta instance itself. 2381 break;
4407 */
4408 self: {
4409 type: Boolean,
4410 observer: '_selfChanged'
4411 },
4412
4413 /**
4414 * Array of all meta-data values for the given type.
4415 */
4416 list: {
4417 type: Array,
4418 notify: true
4419 }
4420
4421 },
4422
4423 hostAttributes: {
4424 hidden: true
4425 },
4426
4427 /**
4428 * Only runs if someone invokes the factory/constructor directly
4429 * e.g. `new Polymer.IronMeta()`
4430 *
4431 * @param {{type: (string|undefined), key: (string|undefined), value}=} co nfig
4432 */
4433 factoryImpl: function(config) {
4434 if (config) {
4435 for (var n in config) {
4436 switch(n) {
4437 case 'type':
4438 case 'key':
4439 case 'value':
4440 this[n] = config[n];
4441 break;
4442 }
4443 } 2382 }
4444 } 2383 }
4445 }, 2384 }
4446 2385 },
4447 created: function() { 2386 created: function() {
4448 // TODO(sjmiles): good for debugging? 2387 this._metaDatas = metaDatas;
4449 this._metaDatas = metaDatas; 2388 this._metaArrays = metaArrays;
4450 this._metaArrays = metaArrays; 2389 },
4451 }, 2390 _keyChanged: function(key, old) {
4452 2391 this._resetRegistration(old);
4453 _keyChanged: function(key, old) { 2392 },
4454 this._resetRegistration(old); 2393 _valueChanged: function(value) {
4455 }, 2394 this._resetRegistration(this.key);
4456 2395 },
4457 _valueChanged: function(value) { 2396 _selfChanged: function(self) {
4458 this._resetRegistration(this.key); 2397 if (self) {
4459 }, 2398 this.value = this;
4460 2399 }
4461 _selfChanged: function(self) { 2400 },
4462 if (self) { 2401 _typeChanged: function(type) {
4463 this.value = this; 2402 this._unregisterKey(this.key);
2403 if (!metaDatas[type]) {
2404 metaDatas[type] = {};
2405 }
2406 this._metaData = metaDatas[type];
2407 if (!metaArrays[type]) {
2408 metaArrays[type] = [];
2409 }
2410 this.list = metaArrays[type];
2411 this._registerKeyValue(this.key, this.value);
2412 },
2413 byKey: function(key) {
2414 return this._metaData && this._metaData[key];
2415 },
2416 _resetRegistration: function(oldKey) {
2417 this._unregisterKey(oldKey);
2418 this._registerKeyValue(this.key, this.value);
2419 },
2420 _unregisterKey: function(key) {
2421 this._unregister(key, this._metaData, this.list);
2422 },
2423 _registerKeyValue: function(key, value) {
2424 this._register(key, value, this._metaData, this.list);
2425 },
2426 _register: function(key, value, data, list) {
2427 if (key && data && value !== undefined) {
2428 data[key] = value;
2429 list.push(value);
2430 }
2431 },
2432 _unregister: function(key, data, list) {
2433 if (key && data) {
2434 if (key in data) {
2435 var value = data[key];
2436 delete data[key];
2437 this.arrayDelete(list, value);
4464 } 2438 }
4465 }, 2439 }
4466 2440 }
4467 _typeChanged: function(type) { 2441 });
4468 this._unregisterKey(this.key); 2442 Polymer.IronMeta.getIronMeta = function getIronMeta() {
4469 if (!metaDatas[type]) { 2443 if (singleton === null) {
4470 metaDatas[type] = {}; 2444 singleton = new Polymer.IronMeta();
4471 } 2445 }
4472 this._metaData = metaDatas[type]; 2446 return singleton;
4473 if (!metaArrays[type]) { 2447 };
4474 metaArrays[type] = []; 2448 Polymer.IronMetaQuery = Polymer({
4475 } 2449 is: 'iron-meta-query',
4476 this.list = metaArrays[type]; 2450 properties: {
4477 this._registerKeyValue(this.key, this.value); 2451 type: {
4478 }, 2452 type: String,
4479 2453 value: 'default',
4480 /** 2454 observer: '_typeChanged'
4481 * Retrieves meta data value by key. 2455 },
4482 * 2456 key: {
4483 * @method byKey 2457 type: String,
4484 * @param {string} key The key of the meta-data to be returned. 2458 observer: '_keyChanged'
4485 * @return {*} 2459 },
4486 */ 2460 value: {
4487 byKey: function(key) { 2461 type: Object,
4488 return this._metaData && this._metaData[key]; 2462 notify: true,
4489 }, 2463 readOnly: true
4490 2464 },
4491 _resetRegistration: function(oldKey) { 2465 list: {
4492 this._unregisterKey(oldKey); 2466 type: Array,
4493 this._registerKeyValue(this.key, this.value); 2467 notify: true
4494 }, 2468 }
4495 2469 },
4496 _unregisterKey: function(key) { 2470 factoryImpl: function(config) {
4497 this._unregister(key, this._metaData, this.list); 2471 if (config) {
4498 }, 2472 for (var n in config) {
4499 2473 switch (n) {
4500 _registerKeyValue: function(key, value) { 2474 case 'type':
4501 this._register(key, value, this._metaData, this.list); 2475 case 'key':
4502 }, 2476 this[n] = config[n];
4503 2477 break;
4504 _register: function(key, value, data, list) {
4505 if (key && data && value !== undefined) {
4506 data[key] = value;
4507 list.push(value);
4508 }
4509 },
4510
4511 _unregister: function(key, data, list) {
4512 if (key && data) {
4513 if (key in data) {
4514 var value = data[key];
4515 delete data[key];
4516 this.arrayDelete(list, value);
4517 } 2478 }
4518 } 2479 }
4519 } 2480 }
4520 2481 },
4521 }); 2482 created: function() {
4522 2483 this._metaDatas = metaDatas;
4523 Polymer.IronMeta.getIronMeta = function getIronMeta() { 2484 this._metaArrays = metaArrays;
4524 if (singleton === null) { 2485 },
4525 singleton = new Polymer.IronMeta(); 2486 _keyChanged: function(key) {
4526 } 2487 this._setValue(this._metaData && this._metaData[key]);
4527 return singleton; 2488 },
4528 }; 2489 _typeChanged: function(type) {
4529 2490 this._metaData = metaDatas[type];
4530 /** 2491 this.list = metaArrays[type];
4531 `iron-meta-query` can be used to access infomation stored in `iron-meta`. 2492 if (this.key) {
4532 2493 this._keyChanged(this.key);
4533 Examples: 2494 }
4534 2495 },
4535 If I create an instance like this: 2496 byKey: function(key) {
4536 2497 return this._metaData && this._metaData[key];
4537 <iron-meta key="info" value="foo/bar"></iron-meta> 2498 }
4538 2499 });
4539 Note that value="foo/bar" is the metadata I've defined. I could define more 2500 })();
4540 attributes or use child nodes to define additional metadata. 2501
4541 2502 Polymer({
4542 Now I can access that element (and it's metadata) from any `iron-meta-query` instance: 2503 is: 'iron-icon',
4543 2504 properties: {
4544 var value = new Polymer.IronMetaQuery({key: 'info'}).value; 2505 icon: {
4545 2506 type: String,
4546 @group Polymer Iron Elements 2507 observer: '_iconChanged'
4547 @element iron-meta-query 2508 },
4548 */ 2509 theme: {
4549 Polymer.IronMetaQuery = Polymer({ 2510 type: String,
4550 2511 observer: '_updateIcon'
4551 is: 'iron-meta-query', 2512 },
4552 2513 src: {
4553 properties: { 2514 type: String,
4554 2515 observer: '_srcChanged'
4555 /** 2516 },
4556 * The type of meta-data. All meta-data of the same type is stored 2517 _meta: {
4557 * together. 2518 value: Polymer.Base.create('iron-meta', {
4558 */ 2519 type: 'iconset'
4559 type: { 2520 }),
4560 type: String, 2521 observer: '_updateIcon'
4561 value: 'default', 2522 }
4562 observer: '_typeChanged' 2523 },
4563 }, 2524 _DEFAULT_ICONSET: 'icons',
4564 2525 _iconChanged: function(icon) {
4565 /** 2526 var parts = (icon || '').split(':');
4566 * Specifies a key to use for retrieving `value` from the `type` 2527 this._iconName = parts.pop();
4567 * namespace. 2528 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
4568 */ 2529 this._updateIcon();
4569 key: { 2530 },
4570 type: String, 2531 _srcChanged: function(src) {
4571 observer: '_keyChanged' 2532 this._updateIcon();
4572 }, 2533 },
4573 2534 _usesIconset: function() {
4574 /** 2535 return this.icon || !this.src;
4575 * The meta-data to store or retrieve. 2536 },
4576 */ 2537 _updateIcon: function() {
4577 value: { 2538 if (this._usesIconset()) {
4578 type: Object, 2539 if (this._img && this._img.parentNode) {
4579 notify: true, 2540 Polymer.dom(this.root).removeChild(this._img);
4580 readOnly: true 2541 }
4581 }, 2542 if (this._iconName === "") {
4582 2543 if (this._iconset) {
4583 /** 2544 this._iconset.removeIcon(this);
4584 * Array of all meta-data values for the given type.
4585 */
4586 list: {
4587 type: Array,
4588 notify: true
4589 } 2545 }
4590 2546 } else if (this._iconsetName && this._meta) {
4591 }, 2547 this._iconset = this._meta.byKey(this._iconsetName);
4592 2548 if (this._iconset) {
4593 /** 2549 this._iconset.applyIcon(this, this._iconName, this.theme);
4594 * Actually a factory method, not a true constructor. Only runs if 2550 this.unlisten(window, 'iron-iconset-added', '_updateIcon');
4595 * someone invokes it directly (via `new Polymer.IronMeta()`); 2551 } else {
4596 * 2552 this.listen(window, 'iron-iconset-added', '_updateIcon');
4597 * @param {{type: (string|undefined), key: (string|undefined)}=} config
4598 */
4599 factoryImpl: function(config) {
4600 if (config) {
4601 for (var n in config) {
4602 switch(n) {
4603 case 'type':
4604 case 'key':
4605 this[n] = config[n];
4606 break;
4607 }
4608 }
4609 } 2553 }
4610 }, 2554 }
4611 2555 } else {
4612 created: function() { 2556 if (this._iconset) {
4613 // TODO(sjmiles): good for debugging? 2557 this._iconset.removeIcon(this);
4614 this._metaDatas = metaDatas; 2558 }
4615 this._metaArrays = metaArrays; 2559 if (!this._img) {
4616 }, 2560 this._img = document.createElement('img');
4617 2561 this._img.style.width = '100%';
4618 _keyChanged: function(key) { 2562 this._img.style.height = '100%';
4619 this._setValue(this._metaData && this._metaData[key]); 2563 this._img.draggable = false;
4620 }, 2564 }
4621 2565 this._img.src = this.src;
4622 _typeChanged: function(type) { 2566 Polymer.dom(this.root).appendChild(this._img);
4623 this._metaData = metaDatas[type]; 2567 }
4624 this.list = metaArrays[type]; 2568 }
4625 if (this.key) { 2569 });
4626 this._keyChanged(this.key); 2570
2571 Polymer.IronControlState = {
2572 properties: {
2573 focused: {
2574 type: Boolean,
2575 value: false,
2576 notify: true,
2577 readOnly: true,
2578 reflectToAttribute: true
2579 },
2580 disabled: {
2581 type: Boolean,
2582 value: false,
2583 notify: true,
2584 observer: '_disabledChanged',
2585 reflectToAttribute: true
2586 },
2587 _oldTabIndex: {
2588 type: Number
2589 },
2590 _boundFocusBlurHandler: {
2591 type: Function,
2592 value: function() {
2593 return this._focusBlurHandler.bind(this);
2594 }
2595 }
2596 },
2597 observers: [ '_changedControlState(focused, disabled)' ],
2598 ready: function() {
2599 this.addEventListener('focus', this._boundFocusBlurHandler, true);
2600 this.addEventListener('blur', this._boundFocusBlurHandler, true);
2601 },
2602 _focusBlurHandler: function(event) {
2603 if (event.target === this) {
2604 this._setFocused(event.type === 'focus');
2605 } else if (!this.shadowRoot) {
2606 var target = Polymer.dom(event).localTarget;
2607 if (!this.isLightDescendant(target)) {
2608 this.fire(event.type, {
2609 sourceEvent: event
2610 }, {
2611 node: this,
2612 bubbles: event.bubbles,
2613 cancelable: event.cancelable
2614 });
2615 }
2616 }
2617 },
2618 _disabledChanged: function(disabled, old) {
2619 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
2620 this.style.pointerEvents = disabled ? 'none' : '';
2621 if (disabled) {
2622 this._oldTabIndex = this.tabIndex;
2623 this._setFocused(false);
2624 this.tabIndex = -1;
2625 this.blur();
2626 } else if (this._oldTabIndex !== undefined) {
2627 this.tabIndex = this._oldTabIndex;
2628 }
2629 },
2630 _changedControlState: function() {
2631 if (this._controlStateChanged) {
2632 this._controlStateChanged();
2633 }
2634 }
2635 };
2636
2637 Polymer.IronButtonStateImpl = {
2638 properties: {
2639 pressed: {
2640 type: Boolean,
2641 readOnly: true,
2642 value: false,
2643 reflectToAttribute: true,
2644 observer: '_pressedChanged'
2645 },
2646 toggles: {
2647 type: Boolean,
2648 value: false,
2649 reflectToAttribute: true
2650 },
2651 active: {
2652 type: Boolean,
2653 value: false,
2654 notify: true,
2655 reflectToAttribute: true
2656 },
2657 pointerDown: {
2658 type: Boolean,
2659 readOnly: true,
2660 value: false
2661 },
2662 receivedFocusFromKeyboard: {
2663 type: Boolean,
2664 readOnly: true
2665 },
2666 ariaActiveAttribute: {
2667 type: String,
2668 value: 'aria-pressed',
2669 observer: '_ariaActiveAttributeChanged'
2670 }
2671 },
2672 listeners: {
2673 down: '_downHandler',
2674 up: '_upHandler',
2675 tap: '_tapHandler'
2676 },
2677 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi veAttribute)' ],
2678 keyBindings: {
2679 'enter:keydown': '_asyncClick',
2680 'space:keydown': '_spaceKeyDownHandler',
2681 'space:keyup': '_spaceKeyUpHandler'
2682 },
2683 _mouseEventRe: /^mouse/,
2684 _tapHandler: function() {
2685 if (this.toggles) {
2686 this._userActivate(!this.active);
2687 } else {
2688 this.active = false;
2689 }
2690 },
2691 _detectKeyboardFocus: function(focused) {
2692 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
2693 },
2694 _userActivate: function(active) {
2695 if (this.active !== active) {
2696 this.active = active;
2697 this.fire('change');
2698 }
2699 },
2700 _downHandler: function(event) {
2701 this._setPointerDown(true);
2702 this._setPressed(true);
2703 this._setReceivedFocusFromKeyboard(false);
2704 },
2705 _upHandler: function() {
2706 this._setPointerDown(false);
2707 this._setPressed(false);
2708 },
2709 _spaceKeyDownHandler: function(event) {
2710 var keyboardEvent = event.detail.keyboardEvent;
2711 var target = Polymer.dom(keyboardEvent).localTarget;
2712 if (this.isLightDescendant(target)) return;
2713 keyboardEvent.preventDefault();
2714 keyboardEvent.stopImmediatePropagation();
2715 this._setPressed(true);
2716 },
2717 _spaceKeyUpHandler: function(event) {
2718 var keyboardEvent = event.detail.keyboardEvent;
2719 var target = Polymer.dom(keyboardEvent).localTarget;
2720 if (this.isLightDescendant(target)) return;
2721 if (this.pressed) {
2722 this._asyncClick();
2723 }
2724 this._setPressed(false);
2725 },
2726 _asyncClick: function() {
2727 this.async(function() {
2728 this.click();
2729 }, 1);
2730 },
2731 _pressedChanged: function(pressed) {
2732 this._changedButtonState();
2733 },
2734 _ariaActiveAttributeChanged: function(value, oldValue) {
2735 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
2736 this.removeAttribute(oldValue);
2737 }
2738 },
2739 _activeChanged: function(active, ariaActiveAttribute) {
2740 if (this.toggles) {
2741 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false');
2742 } else {
2743 this.removeAttribute(this.ariaActiveAttribute);
2744 }
2745 this._changedButtonState();
2746 },
2747 _controlStateChanged: function() {
2748 if (this.disabled) {
2749 this._setPressed(false);
2750 } else {
2751 this._changedButtonState();
2752 }
2753 },
2754 _changedButtonState: function() {
2755 if (this._buttonStateChanged) {
2756 this._buttonStateChanged();
2757 }
2758 }
2759 };
2760
2761 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat eImpl ];
2762
2763 (function() {
2764 var Utility = {
2765 distance: function(x1, y1, x2, y2) {
2766 var xDelta = x1 - x2;
2767 var yDelta = y1 - y2;
2768 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
2769 },
2770 now: window.performance && window.performance.now ? window.performance.now.b ind(window.performance) : Date.now
2771 };
2772 function ElementMetrics(element) {
2773 this.element = element;
2774 this.width = this.boundingRect.width;
2775 this.height = this.boundingRect.height;
2776 this.size = Math.max(this.width, this.height);
2777 }
2778 ElementMetrics.prototype = {
2779 get boundingRect() {
2780 return this.element.getBoundingClientRect();
2781 },
2782 furthestCornerDistanceFrom: function(x, y) {
2783 var topLeft = Utility.distance(x, y, 0, 0);
2784 var topRight = Utility.distance(x, y, this.width, 0);
2785 var bottomLeft = Utility.distance(x, y, 0, this.height);
2786 var bottomRight = Utility.distance(x, y, this.width, this.height);
2787 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
2788 }
2789 };
2790 function Ripple(element) {
2791 this.element = element;
2792 this.color = window.getComputedStyle(element).color;
2793 this.wave = document.createElement('div');
2794 this.waveContainer = document.createElement('div');
2795 this.wave.style.backgroundColor = this.color;
2796 this.wave.classList.add('wave');
2797 this.waveContainer.classList.add('wave-container');
2798 Polymer.dom(this.waveContainer).appendChild(this.wave);
2799 this.resetInteractionState();
2800 }
2801 Ripple.MAX_RADIUS = 300;
2802 Ripple.prototype = {
2803 get recenters() {
2804 return this.element.recenters;
2805 },
2806 get center() {
2807 return this.element.center;
2808 },
2809 get mouseDownElapsed() {
2810 var elapsed;
2811 if (!this.mouseDownStart) {
2812 return 0;
2813 }
2814 elapsed = Utility.now() - this.mouseDownStart;
2815 if (this.mouseUpStart) {
2816 elapsed -= this.mouseUpElapsed;
2817 }
2818 return elapsed;
2819 },
2820 get mouseUpElapsed() {
2821 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0;
2822 },
2823 get mouseDownElapsedSeconds() {
2824 return this.mouseDownElapsed / 1e3;
2825 },
2826 get mouseUpElapsedSeconds() {
2827 return this.mouseUpElapsed / 1e3;
2828 },
2829 get mouseInteractionSeconds() {
2830 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
2831 },
2832 get initialOpacity() {
2833 return this.element.initialOpacity;
2834 },
2835 get opacityDecayVelocity() {
2836 return this.element.opacityDecayVelocity;
2837 },
2838 get radius() {
2839 var width2 = this.containerMetrics.width * this.containerMetrics.width;
2840 var height2 = this.containerMetrics.height * this.containerMetrics.height;
2841 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS) * 1.1 + 5;
2842 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS);
2843 var timeNow = this.mouseInteractionSeconds / duration;
2844 var size = waveRadius * (1 - Math.pow(80, -timeNow));
2845 return Math.abs(size);
2846 },
2847 get opacity() {
2848 if (!this.mouseUpStart) {
2849 return this.initialOpacity;
2850 }
2851 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this .opacityDecayVelocity);
2852 },
2853 get outerOpacity() {
2854 var outerOpacity = this.mouseUpElapsedSeconds * .3;
2855 var waveOpacity = this.opacity;
2856 return Math.max(0, Math.min(outerOpacity, waveOpacity));
2857 },
2858 get isOpacityFullyDecayed() {
2859 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl e.MAX_RADIUS);
2860 },
2861 get isRestingAtMaxRadius() {
2862 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this .maxRadius, Ripple.MAX_RADIUS);
2863 },
2864 get isAnimationComplete() {
2865 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa xRadius;
2866 },
2867 get translationFraction() {
2868 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr t(2));
2869 },
2870 get xNow() {
2871 if (this.xEnd) {
2872 return this.xStart + this.translationFraction * (this.xEnd - this.xStart );
2873 }
2874 return this.xStart;
2875 },
2876 get yNow() {
2877 if (this.yEnd) {
2878 return this.yStart + this.translationFraction * (this.yEnd - this.yStart );
2879 }
2880 return this.yStart;
2881 },
2882 get isMouseDown() {
2883 return this.mouseDownStart && !this.mouseUpStart;
2884 },
2885 resetInteractionState: function() {
2886 this.maxRadius = 0;
2887 this.mouseDownStart = 0;
2888 this.mouseUpStart = 0;
2889 this.xStart = 0;
2890 this.yStart = 0;
2891 this.xEnd = 0;
2892 this.yEnd = 0;
2893 this.slideDistance = 0;
2894 this.containerMetrics = new ElementMetrics(this.element);
2895 },
2896 draw: function() {
2897 var scale;
2898 var translateString;
2899 var dx;
2900 var dy;
2901 this.wave.style.opacity = this.opacity;
2902 scale = this.radius / (this.containerMetrics.size / 2);
2903 dx = this.xNow - this.containerMetrics.width / 2;
2904 dy = this.yNow - this.containerMetrics.height / 2;
2905 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
2906 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + ' px, 0)';
2907 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
2908 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
2909 },
2910 downAction: function(event) {
2911 var xCenter = this.containerMetrics.width / 2;
2912 var yCenter = this.containerMetrics.height / 2;
2913 this.resetInteractionState();
2914 this.mouseDownStart = Utility.now();
2915 if (this.center) {
2916 this.xStart = xCenter;
2917 this.yStart = yCenter;
2918 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
2919 } else {
2920 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec t.left : this.containerMetrics.width / 2;
2921 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec t.top : this.containerMetrics.height / 2;
2922 }
2923 if (this.recenters) {
2924 this.xEnd = xCenter;
2925 this.yEnd = yCenter;
2926 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn d, this.yEnd);
2927 }
2928 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt art, this.yStart);
2929 this.waveContainer.style.top = (this.containerMetrics.height - this.contai nerMetrics.size) / 2 + 'px';
2930 this.waveContainer.style.left = (this.containerMetrics.width - this.contai nerMetrics.size) / 2 + 'px';
2931 this.waveContainer.style.width = this.containerMetrics.size + 'px';
2932 this.waveContainer.style.height = this.containerMetrics.size + 'px';
2933 },
2934 upAction: function(event) {
2935 if (!this.isMouseDown) {
2936 return;
2937 }
2938 this.mouseUpStart = Utility.now();
2939 },
2940 remove: function() {
2941 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer) ;
2942 }
2943 };
2944 Polymer({
2945 is: 'paper-ripple',
2946 behaviors: [ Polymer.IronA11yKeysBehavior ],
2947 properties: {
2948 initialOpacity: {
2949 type: Number,
2950 value: .25
2951 },
2952 opacityDecayVelocity: {
2953 type: Number,
2954 value: .8
2955 },
2956 recenters: {
2957 type: Boolean,
2958 value: false
2959 },
2960 center: {
2961 type: Boolean,
2962 value: false
2963 },
2964 ripples: {
2965 type: Array,
2966 value: function() {
2967 return [];
4627 } 2968 }
4628 }, 2969 },
4629 2970 animating: {
4630 /** 2971 type: Boolean,
4631 * Retrieves meta data value by key. 2972 readOnly: true,
4632 * @param {string} key The key of the meta-data to be returned. 2973 reflectToAttribute: true,
4633 * @return {*} 2974 value: false
4634 */ 2975 },
4635 byKey: function(key) { 2976 holdDown: {
4636 return this._metaData && this._metaData[key];
4637 }
4638
4639 });
4640
4641 })();
4642 Polymer({
4643
4644 is: 'iron-icon',
4645
4646 properties: {
4647
4648 /**
4649 * The name of the icon to use. The name should be of the form:
4650 * `iconset_name:icon_name`.
4651 */
4652 icon: {
4653 type: String,
4654 observer: '_iconChanged'
4655 },
4656
4657 /**
4658 * The name of the theme to used, if one is specified by the
4659 * iconset.
4660 */
4661 theme: {
4662 type: String,
4663 observer: '_updateIcon'
4664 },
4665
4666 /**
4667 * If using iron-icon without an iconset, you can set the src to be
4668 * the URL of an individual icon image file. Note that this will take
4669 * precedence over a given icon attribute.
4670 */
4671 src: {
4672 type: String,
4673 observer: '_srcChanged'
4674 },
4675
4676 /**
4677 * @type {!Polymer.IronMeta}
4678 */
4679 _meta: {
4680 value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
4681 observer: '_updateIcon'
4682 }
4683
4684 },
4685
4686 _DEFAULT_ICONSET: 'icons',
4687
4688 _iconChanged: function(icon) {
4689 var parts = (icon || '').split(':');
4690 this._iconName = parts.pop();
4691 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
4692 this._updateIcon();
4693 },
4694
4695 _srcChanged: function(src) {
4696 this._updateIcon();
4697 },
4698
4699 _usesIconset: function() {
4700 return this.icon || !this.src;
4701 },
4702
4703 /** @suppress {visibility} */
4704 _updateIcon: function() {
4705 if (this._usesIconset()) {
4706 if (this._img && this._img.parentNode) {
4707 Polymer.dom(this.root).removeChild(this._img);
4708 }
4709 if (this._iconName === "") {
4710 if (this._iconset) {
4711 this._iconset.removeIcon(this);
4712 }
4713 } else if (this._iconsetName && this._meta) {
4714 this._iconset = /** @type {?Polymer.Iconset} */ (
4715 this._meta.byKey(this._iconsetName));
4716 if (this._iconset) {
4717 this._iconset.applyIcon(this, this._iconName, this.theme);
4718 this.unlisten(window, 'iron-iconset-added', '_updateIcon');
4719 } else {
4720 this.listen(window, 'iron-iconset-added', '_updateIcon');
4721 }
4722 }
4723 } else {
4724 if (this._iconset) {
4725 this._iconset.removeIcon(this);
4726 }
4727 if (!this._img) {
4728 this._img = document.createElement('img');
4729 this._img.style.width = '100%';
4730 this._img.style.height = '100%';
4731 this._img.draggable = false;
4732 }
4733 this._img.src = this.src;
4734 Polymer.dom(this.root).appendChild(this._img);
4735 }
4736 }
4737
4738 });
4739 /**
4740 * @demo demo/index.html
4741 * @polymerBehavior
4742 */
4743 Polymer.IronControlState = {
4744
4745 properties: {
4746
4747 /**
4748 * If true, the element currently has focus.
4749 */
4750 focused: {
4751 type: Boolean, 2977 type: Boolean,
4752 value: false, 2978 value: false,
4753 notify: true, 2979 observer: '_holdDownChanged'
4754 readOnly: true, 2980 },
4755 reflectToAttribute: true 2981 noink: {
4756 },
4757
4758 /**
4759 * If true, the user cannot interact with this element.
4760 */
4761 disabled: {
4762 type: Boolean, 2982 type: Boolean,
4763 value: false, 2983 value: false
4764 notify: true, 2984 },
4765 observer: '_disabledChanged', 2985 _animating: {
4766 reflectToAttribute: true 2986 type: Boolean
4767 }, 2987 },
4768 2988 _boundAnimate: {
4769 _oldTabIndex: {
4770 type: Number
4771 },
4772
4773 _boundFocusBlurHandler: {
4774 type: Function, 2989 type: Function,
4775 value: function() { 2990 value: function() {
4776 return this._focusBlurHandler.bind(this); 2991 return this.animate.bind(this);
4777 } 2992 }
4778 } 2993 }
4779 2994 },
4780 }, 2995 get target() {
4781 2996 return this.keyEventTarget;
4782 observers: [ 2997 },
4783 '_changedControlState(focused, disabled)' 2998 keyBindings: {
4784 ], 2999 'enter:keydown': '_onEnterKeydown',
4785 3000 'space:keydown': '_onSpaceKeydown',
4786 ready: function() { 3001 'space:keyup': '_onSpaceKeyup'
4787 this.addEventListener('focus', this._boundFocusBlurHandler, true); 3002 },
4788 this.addEventListener('blur', this._boundFocusBlurHandler, true); 3003 attached: function() {
4789 }, 3004 if (this.parentNode.nodeType == 11) {
4790 3005 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
4791 _focusBlurHandler: function(event) { 3006 } else {
4792 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will 3007 this.keyEventTarget = this.parentNode;
4793 // eventually become `this` due to retargeting; if we are not in 3008 }
4794 // ShadowDOM land, `event.target` will eventually become `this` due 3009 var keyEventTarget = this.keyEventTarget;
4795 // to the second conditional which fires a synthetic event (that is also 3010 this.listen(keyEventTarget, 'up', 'uiUpAction');
4796 // handled). In either case, we can disregard `event.path`. 3011 this.listen(keyEventTarget, 'down', 'uiDownAction');
4797 3012 },
4798 if (event.target === this) { 3013 detached: function() {
4799 this._setFocused(event.type === 'focus'); 3014 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
4800 } else if (!this.shadowRoot) { 3015 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
4801 var target = /** @type {Node} */(Polymer.dom(event).localTarget); 3016 this.keyEventTarget = null;
4802 if (!this.isLightDescendant(target)) { 3017 },
4803 this.fire(event.type, {sourceEvent: event}, { 3018 get shouldKeepAnimating() {
4804 node: this, 3019 for (var index = 0; index < this.ripples.length; ++index) {
4805 bubbles: event.bubbles, 3020 if (!this.ripples[index].isAnimationComplete) {
4806 cancelable: event.cancelable 3021 return true;
4807 });
4808 } 3022 }
4809 } 3023 }
4810 }, 3024 return false;
4811 3025 },
4812 _disabledChanged: function(disabled, old) { 3026 simulatedRipple: function() {
4813 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 3027 this.downAction(null);
4814 this.style.pointerEvents = disabled ? 'none' : ''; 3028 this.async(function() {
4815 if (disabled) { 3029 this.upAction();
4816 this._oldTabIndex = this.tabIndex; 3030 }, 1);
4817 this._setFocused(false); 3031 },
4818 this.tabIndex = -1; 3032 uiDownAction: function(event) {
4819 this.blur(); 3033 if (!this.noink) {
4820 } else if (this._oldTabIndex !== undefined) { 3034 this.downAction(event);
4821 this.tabIndex = this._oldTabIndex; 3035 }
4822 } 3036 },
4823 }, 3037 downAction: function(event) {
4824 3038 if (this.holdDown && this.ripples.length > 0) {
4825 _changedControlState: function() {
4826 // _controlStateChanged is abstract, follow-on behaviors may implement it
4827 if (this._controlStateChanged) {
4828 this._controlStateChanged();
4829 }
4830 }
4831
4832 };
4833 /**
4834 * @demo demo/index.html
4835 * @polymerBehavior Polymer.IronButtonState
4836 */
4837 Polymer.IronButtonStateImpl = {
4838
4839 properties: {
4840
4841 /**
4842 * If true, the user is currently holding down the button.
4843 */
4844 pressed: {
4845 type: Boolean,
4846 readOnly: true,
4847 value: false,
4848 reflectToAttribute: true,
4849 observer: '_pressedChanged'
4850 },
4851
4852 /**
4853 * If true, the button toggles the active state with each tap or press
4854 * of the spacebar.
4855 */
4856 toggles: {
4857 type: Boolean,
4858 value: false,
4859 reflectToAttribute: true
4860 },
4861
4862 /**
4863 * If true, the button is a toggle and is currently in the active state.
4864 */
4865 active: {
4866 type: Boolean,
4867 value: false,
4868 notify: true,
4869 reflectToAttribute: true
4870 },
4871
4872 /**
4873 * True if the element is currently being pressed by a "pointer," which
4874 * is loosely defined as mouse or touch input (but specifically excluding
4875 * keyboard input).
4876 */
4877 pointerDown: {
4878 type: Boolean,
4879 readOnly: true,
4880 value: false
4881 },
4882
4883 /**
4884 * True if the input device that caused the element to receive focus
4885 * was a keyboard.
4886 */
4887 receivedFocusFromKeyboard: {
4888 type: Boolean,
4889 readOnly: true
4890 },
4891
4892 /**
4893 * The aria attribute to be set if the button is a toggle and in the
4894 * active state.
4895 */
4896 ariaActiveAttribute: {
4897 type: String,
4898 value: 'aria-pressed',
4899 observer: '_ariaActiveAttributeChanged'
4900 }
4901 },
4902
4903 listeners: {
4904 down: '_downHandler',
4905 up: '_upHandler',
4906 tap: '_tapHandler'
4907 },
4908
4909 observers: [
4910 '_detectKeyboardFocus(focused)',
4911 '_activeChanged(active, ariaActiveAttribute)'
4912 ],
4913
4914 keyBindings: {
4915 'enter:keydown': '_asyncClick',
4916 'space:keydown': '_spaceKeyDownHandler',
4917 'space:keyup': '_spaceKeyUpHandler',
4918 },
4919
4920 _mouseEventRe: /^mouse/,
4921
4922 _tapHandler: function() {
4923 if (this.toggles) {
4924 // a tap is needed to toggle the active state
4925 this._userActivate(!this.active);
4926 } else {
4927 this.active = false;
4928 }
4929 },
4930
4931 _detectKeyboardFocus: function(focused) {
4932 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
4933 },
4934
4935 // to emulate native checkbox, (de-)activations from a user interaction fire
4936 // 'change' events
4937 _userActivate: function(active) {
4938 if (this.active !== active) {
4939 this.active = active;
4940 this.fire('change');
4941 }
4942 },
4943
4944 _downHandler: function(event) {
4945 this._setPointerDown(true);
4946 this._setPressed(true);
4947 this._setReceivedFocusFromKeyboard(false);
4948 },
4949
4950 _upHandler: function() {
4951 this._setPointerDown(false);
4952 this._setPressed(false);
4953 },
4954
4955 /**
4956 * @param {!KeyboardEvent} event .
4957 */
4958 _spaceKeyDownHandler: function(event) {
4959 var keyboardEvent = event.detail.keyboardEvent;
4960 var target = Polymer.dom(keyboardEvent).localTarget;
4961
4962 // Ignore the event if this is coming from a focused light child, since th at
4963 // element will deal with it.
4964 if (this.isLightDescendant(/** @type {Node} */(target)))
4965 return; 3039 return;
4966 3040 }
4967 keyboardEvent.preventDefault(); 3041 var ripple = this.addRipple();
4968 keyboardEvent.stopImmediatePropagation(); 3042 ripple.downAction(event);
4969 this._setPressed(true); 3043 if (!this._animating) {
4970 },
4971
4972 /**
4973 * @param {!KeyboardEvent} event .
4974 */
4975 _spaceKeyUpHandler: function(event) {
4976 var keyboardEvent = event.detail.keyboardEvent;
4977 var target = Polymer.dom(keyboardEvent).localTarget;
4978
4979 // Ignore the event if this is coming from a focused light child, since th at
4980 // element will deal with it.
4981 if (this.isLightDescendant(/** @type {Node} */(target)))
4982 return;
4983
4984 if (this.pressed) {
4985 this._asyncClick();
4986 }
4987 this._setPressed(false);
4988 },
4989
4990 // trigger click asynchronously, the asynchrony is useful to allow one
4991 // event handler to unwind before triggering another event
4992 _asyncClick: function() {
4993 this.async(function() {
4994 this.click();
4995 }, 1);
4996 },
4997
4998 // any of these changes are considered a change to button state
4999
5000 _pressedChanged: function(pressed) {
5001 this._changedButtonState();
5002 },
5003
5004 _ariaActiveAttributeChanged: function(value, oldValue) {
5005 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
5006 this.removeAttribute(oldValue);
5007 }
5008 },
5009
5010 _activeChanged: function(active, ariaActiveAttribute) {
5011 if (this.toggles) {
5012 this.setAttribute(this.ariaActiveAttribute,
5013 active ? 'true' : 'false');
5014 } else {
5015 this.removeAttribute(this.ariaActiveAttribute);
5016 }
5017 this._changedButtonState();
5018 },
5019
5020 _controlStateChanged: function() {
5021 if (this.disabled) {
5022 this._setPressed(false);
5023 } else {
5024 this._changedButtonState();
5025 }
5026 },
5027
5028 // provide hook for follow-on behaviors to react to button-state
5029
5030 _changedButtonState: function() {
5031 if (this._buttonStateChanged) {
5032 this._buttonStateChanged(); // abstract
5033 }
5034 }
5035
5036 };
5037
5038 /** @polymerBehavior */
5039 Polymer.IronButtonState = [
5040 Polymer.IronA11yKeysBehavior,
5041 Polymer.IronButtonStateImpl
5042 ];
5043 (function() {
5044 var Utility = {
5045 distance: function(x1, y1, x2, y2) {
5046 var xDelta = (x1 - x2);
5047 var yDelta = (y1 - y2);
5048
5049 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
5050 },
5051
5052 now: window.performance && window.performance.now ?
5053 window.performance.now.bind(window.performance) : Date.now
5054 };
5055
5056 /**
5057 * @param {HTMLElement} element
5058 * @constructor
5059 */
5060 function ElementMetrics(element) {
5061 this.element = element;
5062 this.width = this.boundingRect.width;
5063 this.height = this.boundingRect.height;
5064
5065 this.size = Math.max(this.width, this.height);
5066 }
5067
5068 ElementMetrics.prototype = {
5069 get boundingRect () {
5070 return this.element.getBoundingClientRect();
5071 },
5072
5073 furthestCornerDistanceFrom: function(x, y) {
5074 var topLeft = Utility.distance(x, y, 0, 0);
5075 var topRight = Utility.distance(x, y, this.width, 0);
5076 var bottomLeft = Utility.distance(x, y, 0, this.height);
5077 var bottomRight = Utility.distance(x, y, this.width, this.height);
5078
5079 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
5080 }
5081 };
5082
5083 /**
5084 * @param {HTMLElement} element
5085 * @constructor
5086 */
5087 function Ripple(element) {
5088 this.element = element;
5089 this.color = window.getComputedStyle(element).color;
5090
5091 this.wave = document.createElement('div');
5092 this.waveContainer = document.createElement('div');
5093 this.wave.style.backgroundColor = this.color;
5094 this.wave.classList.add('wave');
5095 this.waveContainer.classList.add('wave-container');
5096 Polymer.dom(this.waveContainer).appendChild(this.wave);
5097
5098 this.resetInteractionState();
5099 }
5100
5101 Ripple.MAX_RADIUS = 300;
5102
5103 Ripple.prototype = {
5104 get recenters() {
5105 return this.element.recenters;
5106 },
5107
5108 get center() {
5109 return this.element.center;
5110 },
5111
5112 get mouseDownElapsed() {
5113 var elapsed;
5114
5115 if (!this.mouseDownStart) {
5116 return 0;
5117 }
5118
5119 elapsed = Utility.now() - this.mouseDownStart;
5120
5121 if (this.mouseUpStart) {
5122 elapsed -= this.mouseUpElapsed;
5123 }
5124
5125 return elapsed;
5126 },
5127
5128 get mouseUpElapsed() {
5129 return this.mouseUpStart ?
5130 Utility.now () - this.mouseUpStart : 0;
5131 },
5132
5133 get mouseDownElapsedSeconds() {
5134 return this.mouseDownElapsed / 1000;
5135 },
5136
5137 get mouseUpElapsedSeconds() {
5138 return this.mouseUpElapsed / 1000;
5139 },
5140
5141 get mouseInteractionSeconds() {
5142 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
5143 },
5144
5145 get initialOpacity() {
5146 return this.element.initialOpacity;
5147 },
5148
5149 get opacityDecayVelocity() {
5150 return this.element.opacityDecayVelocity;
5151 },
5152
5153 get radius() {
5154 var width2 = this.containerMetrics.width * this.containerMetrics.width;
5155 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
5156 var waveRadius = Math.min(
5157 Math.sqrt(width2 + height2),
5158 Ripple.MAX_RADIUS
5159 ) * 1.1 + 5;
5160
5161 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
5162 var timeNow = this.mouseInteractionSeconds / duration;
5163 var size = waveRadius * (1 - Math.pow(80, -timeNow));
5164
5165 return Math.abs(size);
5166 },
5167
5168 get opacity() {
5169 if (!this.mouseUpStart) {
5170 return this.initialOpacity;
5171 }
5172
5173 return Math.max(
5174 0,
5175 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
5176 );
5177 },
5178
5179 get outerOpacity() {
5180 // Linear increase in background opacity, capped at the opacity
5181 // of the wavefront (waveOpacity).
5182 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
5183 var waveOpacity = this.opacity;
5184
5185 return Math.max(
5186 0,
5187 Math.min(outerOpacity, waveOpacity)
5188 );
5189 },
5190
5191 get isOpacityFullyDecayed() {
5192 return this.opacity < 0.01 &&
5193 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5194 },
5195
5196 get isRestingAtMaxRadius() {
5197 return this.opacity >= this.initialOpacity &&
5198 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5199 },
5200
5201 get isAnimationComplete() {
5202 return this.mouseUpStart ?
5203 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
5204 },
5205
5206 get translationFraction() {
5207 return Math.min(
5208 1,
5209 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
5210 );
5211 },
5212
5213 get xNow() {
5214 if (this.xEnd) {
5215 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
5216 }
5217
5218 return this.xStart;
5219 },
5220
5221 get yNow() {
5222 if (this.yEnd) {
5223 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
5224 }
5225
5226 return this.yStart;
5227 },
5228
5229 get isMouseDown() {
5230 return this.mouseDownStart && !this.mouseUpStart;
5231 },
5232
5233 resetInteractionState: function() {
5234 this.maxRadius = 0;
5235 this.mouseDownStart = 0;
5236 this.mouseUpStart = 0;
5237
5238 this.xStart = 0;
5239 this.yStart = 0;
5240 this.xEnd = 0;
5241 this.yEnd = 0;
5242 this.slideDistance = 0;
5243
5244 this.containerMetrics = new ElementMetrics(this.element);
5245 },
5246
5247 draw: function() {
5248 var scale;
5249 var translateString;
5250 var dx;
5251 var dy;
5252
5253 this.wave.style.opacity = this.opacity;
5254
5255 scale = this.radius / (this.containerMetrics.size / 2);
5256 dx = this.xNow - (this.containerMetrics.width / 2);
5257 dy = this.yNow - (this.containerMetrics.height / 2);
5258
5259
5260 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
5261 // https://bugs.webkit.org/show_bug.cgi?id=98538
5262 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
5263 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
5264 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
5265 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
5266 },
5267
5268 /** @param {Event=} event */
5269 downAction: function(event) {
5270 var xCenter = this.containerMetrics.width / 2;
5271 var yCenter = this.containerMetrics.height / 2;
5272
5273 this.resetInteractionState();
5274 this.mouseDownStart = Utility.now();
5275
5276 if (this.center) {
5277 this.xStart = xCenter;
5278 this.yStart = yCenter;
5279 this.slideDistance = Utility.distance(
5280 this.xStart, this.yStart, this.xEnd, this.yEnd
5281 );
5282 } else {
5283 this.xStart = event ?
5284 event.detail.x - this.containerMetrics.boundingRect.left :
5285 this.containerMetrics.width / 2;
5286 this.yStart = event ?
5287 event.detail.y - this.containerMetrics.boundingRect.top :
5288 this.containerMetrics.height / 2;
5289 }
5290
5291 if (this.recenters) {
5292 this.xEnd = xCenter;
5293 this.yEnd = yCenter;
5294 this.slideDistance = Utility.distance(
5295 this.xStart, this.yStart, this.xEnd, this.yEnd
5296 );
5297 }
5298
5299 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
5300 this.xStart,
5301 this.yStart
5302 );
5303
5304 this.waveContainer.style.top =
5305 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
5306 this.waveContainer.style.left =
5307 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
5308
5309 this.waveContainer.style.width = this.containerMetrics.size + 'px';
5310 this.waveContainer.style.height = this.containerMetrics.size + 'px';
5311 },
5312
5313 /** @param {Event=} event */
5314 upAction: function(event) {
5315 if (!this.isMouseDown) {
5316 return;
5317 }
5318
5319 this.mouseUpStart = Utility.now();
5320 },
5321
5322 remove: function() {
5323 Polymer.dom(this.waveContainer.parentNode).removeChild(
5324 this.waveContainer
5325 );
5326 }
5327 };
5328
5329 Polymer({
5330 is: 'paper-ripple',
5331
5332 behaviors: [
5333 Polymer.IronA11yKeysBehavior
5334 ],
5335
5336 properties: {
5337 /**
5338 * The initial opacity set on the wave.
5339 *
5340 * @attribute initialOpacity
5341 * @type number
5342 * @default 0.25
5343 */
5344 initialOpacity: {
5345 type: Number,
5346 value: 0.25
5347 },
5348
5349 /**
5350 * How fast (opacity per second) the wave fades out.
5351 *
5352 * @attribute opacityDecayVelocity
5353 * @type number
5354 * @default 0.8
5355 */
5356 opacityDecayVelocity: {
5357 type: Number,
5358 value: 0.8
5359 },
5360
5361 /**
5362 * If true, ripples will exhibit a gravitational pull towards
5363 * the center of their container as they fade away.
5364 *
5365 * @attribute recenters
5366 * @type boolean
5367 * @default false
5368 */
5369 recenters: {
5370 type: Boolean,
5371 value: false
5372 },
5373
5374 /**
5375 * If true, ripples will center inside its container
5376 *
5377 * @attribute recenters
5378 * @type boolean
5379 * @default false
5380 */
5381 center: {
5382 type: Boolean,
5383 value: false
5384 },
5385
5386 /**
5387 * A list of the visual ripples.
5388 *
5389 * @attribute ripples
5390 * @type Array
5391 * @default []
5392 */
5393 ripples: {
5394 type: Array,
5395 value: function() {
5396 return [];
5397 }
5398 },
5399
5400 /**
5401 * True when there are visible ripples animating within the
5402 * element.
5403 */
5404 animating: {
5405 type: Boolean,
5406 readOnly: true,
5407 reflectToAttribute: true,
5408 value: false
5409 },
5410
5411 /**
5412 * If true, the ripple will remain in the "down" state until `holdDown`
5413 * is set to false again.
5414 */
5415 holdDown: {
5416 type: Boolean,
5417 value: false,
5418 observer: '_holdDownChanged'
5419 },
5420
5421 /**
5422 * If true, the ripple will not generate a ripple effect
5423 * via pointer interaction.
5424 * Calling ripple's imperative api like `simulatedRipple` will
5425 * still generate the ripple effect.
5426 */
5427 noink: {
5428 type: Boolean,
5429 value: false
5430 },
5431
5432 _animating: {
5433 type: Boolean
5434 },
5435
5436 _boundAnimate: {
5437 type: Function,
5438 value: function() {
5439 return this.animate.bind(this);
5440 }
5441 }
5442 },
5443
5444 get target () {
5445 return this.keyEventTarget;
5446 },
5447
5448 keyBindings: {
5449 'enter:keydown': '_onEnterKeydown',
5450 'space:keydown': '_onSpaceKeydown',
5451 'space:keyup': '_onSpaceKeyup'
5452 },
5453
5454 attached: function() {
5455 // Set up a11yKeysBehavior to listen to key events on the target,
5456 // so that space and enter activate the ripple even if the target doesn' t
5457 // handle key events. The key handlers deal with `noink` themselves.
5458 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
5459 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
5460 } else {
5461 this.keyEventTarget = this.parentNode;
5462 }
5463 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
5464 this.listen(keyEventTarget, 'up', 'uiUpAction');
5465 this.listen(keyEventTarget, 'down', 'uiDownAction');
5466 },
5467
5468 detached: function() {
5469 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
5470 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
5471 this.keyEventTarget = null;
5472 },
5473
5474 get shouldKeepAnimating () {
5475 for (var index = 0; index < this.ripples.length; ++index) {
5476 if (!this.ripples[index].isAnimationComplete) {
5477 return true;
5478 }
5479 }
5480
5481 return false;
5482 },
5483
5484 simulatedRipple: function() {
5485 this.downAction(null);
5486
5487 // Please see polymer/polymer#1305
5488 this.async(function() {
5489 this.upAction();
5490 }, 1);
5491 },
5492
5493 /**
5494 * Provokes a ripple down effect via a UI event,
5495 * respecting the `noink` property.
5496 * @param {Event=} event
5497 */
5498 uiDownAction: function(event) {
5499 if (!this.noink) {
5500 this.downAction(event);
5501 }
5502 },
5503
5504 /**
5505 * Provokes a ripple down effect via a UI event,
5506 * *not* respecting the `noink` property.
5507 * @param {Event=} event
5508 */
5509 downAction: function(event) {
5510 if (this.holdDown && this.ripples.length > 0) {
5511 return;
5512 }
5513
5514 var ripple = this.addRipple();
5515
5516 ripple.downAction(event);
5517
5518 if (!this._animating) {
5519 this._animating = true;
5520 this.animate();
5521 }
5522 },
5523
5524 /**
5525 * Provokes a ripple up effect via a UI event,
5526 * respecting the `noink` property.
5527 * @param {Event=} event
5528 */
5529 uiUpAction: function(event) {
5530 if (!this.noink) {
5531 this.upAction(event);
5532 }
5533 },
5534
5535 /**
5536 * Provokes a ripple up effect via a UI event,
5537 * *not* respecting the `noink` property.
5538 * @param {Event=} event
5539 */
5540 upAction: function(event) {
5541 if (this.holdDown) {
5542 return;
5543 }
5544
5545 this.ripples.forEach(function(ripple) {
5546 ripple.upAction(event);
5547 });
5548
5549 this._animating = true; 3044 this._animating = true;
5550 this.animate(); 3045 this.animate();
5551 }, 3046 }
5552 3047 },
5553 onAnimationComplete: function() { 3048 uiUpAction: function(event) {
5554 this._animating = false; 3049 if (!this.noink) {
5555 this.$.background.style.backgroundColor = null; 3050 this.upAction(event);
5556 this.fire('transitionend'); 3051 }
5557 }, 3052 },
5558 3053 upAction: function(event) {
5559 addRipple: function() { 3054 if (this.holdDown) {
5560 var ripple = new Ripple(this); 3055 return;
5561 3056 }
5562 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); 3057 this.ripples.forEach(function(ripple) {
5563 this.$.background.style.backgroundColor = ripple.color; 3058 ripple.upAction(event);
5564 this.ripples.push(ripple); 3059 });
5565 3060 this._animating = true;
5566 this._setAnimating(true); 3061 this.animate();
5567 3062 },
5568 return ripple; 3063 onAnimationComplete: function() {
5569 }, 3064 this._animating = false;
5570 3065 this.$.background.style.backgroundColor = null;
5571 removeRipple: function(ripple) { 3066 this.fire('transitionend');
5572 var rippleIndex = this.ripples.indexOf(ripple); 3067 },
5573 3068 addRipple: function() {
5574 if (rippleIndex < 0) { 3069 var ripple = new Ripple(this);
5575 return; 3070 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
3071 this.$.background.style.backgroundColor = ripple.color;
3072 this.ripples.push(ripple);
3073 this._setAnimating(true);
3074 return ripple;
3075 },
3076 removeRipple: function(ripple) {
3077 var rippleIndex = this.ripples.indexOf(ripple);
3078 if (rippleIndex < 0) {
3079 return;
3080 }
3081 this.ripples.splice(rippleIndex, 1);
3082 ripple.remove();
3083 if (!this.ripples.length) {
3084 this._setAnimating(false);
3085 }
3086 },
3087 animate: function() {
3088 if (!this._animating) {
3089 return;
3090 }
3091 var index;
3092 var ripple;
3093 for (index = 0; index < this.ripples.length; ++index) {
3094 ripple = this.ripples[index];
3095 ripple.draw();
3096 this.$.background.style.opacity = ripple.outerOpacity;
3097 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
3098 this.removeRipple(ripple);
5576 } 3099 }
5577 3100 }
5578 this.ripples.splice(rippleIndex, 1); 3101 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
5579 3102 this.onAnimationComplete();
5580 ripple.remove(); 3103 } else {
5581 3104 window.requestAnimationFrame(this._boundAnimate);
5582 if (!this.ripples.length) { 3105 }
5583 this._setAnimating(false); 3106 },
3107 _onEnterKeydown: function() {
3108 this.uiDownAction();
3109 this.async(this.uiUpAction, 1);
3110 },
3111 _onSpaceKeydown: function() {
3112 this.uiDownAction();
3113 },
3114 _onSpaceKeyup: function() {
3115 this.uiUpAction();
3116 },
3117 _holdDownChanged: function(newVal, oldVal) {
3118 if (oldVal === undefined) {
3119 return;
3120 }
3121 if (newVal) {
3122 this.downAction();
3123 } else {
3124 this.upAction();
3125 }
3126 }
3127 });
3128 })();
3129
3130 Polymer.PaperRippleBehavior = {
3131 properties: {
3132 noink: {
3133 type: Boolean,
3134 observer: '_noinkChanged'
3135 },
3136 _rippleContainer: {
3137 type: Object
3138 }
3139 },
3140 _buttonStateChanged: function() {
3141 if (this.focused) {
3142 this.ensureRipple();
3143 }
3144 },
3145 _downHandler: function(event) {
3146 Polymer.IronButtonStateImpl._downHandler.call(this, event);
3147 if (this.pressed) {
3148 this.ensureRipple(event);
3149 }
3150 },
3151 ensureRipple: function(optTriggeringEvent) {
3152 if (!this.hasRipple()) {
3153 this._ripple = this._createRipple();
3154 this._ripple.noink = this.noink;
3155 var rippleContainer = this._rippleContainer || this.root;
3156 if (rippleContainer) {
3157 Polymer.dom(rippleContainer).appendChild(this._ripple);
3158 }
3159 if (optTriggeringEvent) {
3160 var domContainer = Polymer.dom(this._rippleContainer || this);
3161 var target = Polymer.dom(optTriggeringEvent).rootTarget;
3162 if (domContainer.deepContains(target)) {
3163 this._ripple.uiDownAction(optTriggeringEvent);
5584 } 3164 }
5585 }, 3165 }
5586 3166 }
5587 animate: function() { 3167 },
5588 if (!this._animating) { 3168 getRipple: function() {
5589 return; 3169 this.ensureRipple();
5590 } 3170 return this._ripple;
5591 var index; 3171 },
5592 var ripple; 3172 hasRipple: function() {
5593 3173 return Boolean(this._ripple);
5594 for (index = 0; index < this.ripples.length; ++index) { 3174 },
5595 ripple = this.ripples[index]; 3175 _createRipple: function() {
5596 3176 return document.createElement('paper-ripple');
5597 ripple.draw(); 3177 },
5598 3178 _noinkChanged: function(noink) {
5599 this.$.background.style.opacity = ripple.outerOpacity; 3179 if (this.hasRipple()) {
5600 3180 this._ripple.noink = noink;
5601 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { 3181 }
5602 this.removeRipple(ripple); 3182 }
5603 } 3183 };
5604 } 3184
5605 3185 Polymer.PaperButtonBehaviorImpl = {
5606 if (!this.shouldKeepAnimating && this.ripples.length === 0) { 3186 properties: {
5607 this.onAnimationComplete(); 3187 elevation: {
5608 } else { 3188 type: Number,
5609 window.requestAnimationFrame(this._boundAnimate); 3189 reflectToAttribute: true,
5610 } 3190 readOnly: true
5611 }, 3191 }
5612 3192 },
5613 _onEnterKeydown: function() { 3193 observers: [ '_calculateElevation(focused, disabled, active, pressed, received FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ],
5614 this.uiDownAction(); 3194 hostAttributes: {
5615 this.async(this.uiUpAction, 1); 3195 role: 'button',
5616 }, 3196 tabindex: '0',
5617 3197 animated: true
5618 _onSpaceKeydown: function() { 3198 },
5619 this.uiDownAction(); 3199 _calculateElevation: function() {
5620 }, 3200 var e = 1;
5621 3201 if (this.disabled) {
5622 _onSpaceKeyup: function() { 3202 e = 0;
5623 this.uiUpAction(); 3203 } else if (this.active || this.pressed) {
5624 }, 3204 e = 4;
5625 3205 } else if (this.receivedFocusFromKeyboard) {
5626 // note: holdDown does not respect noink since it can be a focus based 3206 e = 3;
5627 // effect. 3207 }
5628 _holdDownChanged: function(newVal, oldVal) { 3208 this._setElevation(e);
5629 if (oldVal === undefined) { 3209 },
5630 return; 3210 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5631 } 3211 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5632 if (newVal) { 3212 },
5633 this.downAction(); 3213 _spaceKeyDownHandler: function(event) {
5634 } else { 3214 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5635 this.upAction(); 3215 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5636 } 3216 this._ripple.uiDownAction();
5637 } 3217 }
5638 3218 },
5639 /** 3219 _spaceKeyUpHandler: function(event) {
5640 Fired when the animation finishes. 3220 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5641 This is useful if you want to wait until 3221 if (this.hasRipple()) {
5642 the ripple animation finishes to perform some action. 3222 this._ripple.uiUpAction();
5643 3223 }
5644 @event transitionend 3224 }
5645 @param {{node: Object}} detail Contains the animated node. 3225 };
5646 */ 3226
5647 }); 3227 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ];
5648 })(); 3228
5649 /**
5650 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
5651 * when the element has focus via pointer or keyboard.
5652 *
5653 * NOTE: This behavior is intended to be used in conjunction with and after
5654 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
5655 *
5656 * @polymerBehavior Polymer.PaperRippleBehavior
5657 */
5658 Polymer.PaperRippleBehavior = {
5659 properties: {
5660 /**
5661 * If true, the element will not produce a ripple effect when interacted
5662 * with via the pointer.
5663 */
5664 noink: {
5665 type: Boolean,
5666 observer: '_noinkChanged'
5667 },
5668
5669 /**
5670 * @type {Element|undefined}
5671 */
5672 _rippleContainer: {
5673 type: Object,
5674 }
5675 },
5676
5677 /**
5678 * Ensures a `<paper-ripple>` element is available when the element is
5679 * focused.
5680 */
5681 _buttonStateChanged: function() {
5682 if (this.focused) {
5683 this.ensureRipple();
5684 }
5685 },
5686
5687 /**
5688 * In addition to the functionality provided in `IronButtonState`, ensures
5689 * a ripple effect is created when the element is in a `pressed` state.
5690 */
5691 _downHandler: function(event) {
5692 Polymer.IronButtonStateImpl._downHandler.call(this, event);
5693 if (this.pressed) {
5694 this.ensureRipple(event);
5695 }
5696 },
5697
5698 /**
5699 * Ensures this element contains a ripple effect. For startup efficiency
5700 * the ripple effect is dynamically on demand when needed.
5701 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
5702 * ripple.
5703 */
5704 ensureRipple: function(optTriggeringEvent) {
5705 if (!this.hasRipple()) {
5706 this._ripple = this._createRipple();
5707 this._ripple.noink = this.noink;
5708 var rippleContainer = this._rippleContainer || this.root;
5709 if (rippleContainer) {
5710 Polymer.dom(rippleContainer).appendChild(this._ripple);
5711 }
5712 if (optTriggeringEvent) {
5713 // Check if the event happened inside of the ripple container
5714 // Fall back to host instead of the root because distributed text
5715 // nodes are not valid event targets
5716 var domContainer = Polymer.dom(this._rippleContainer || this);
5717 var target = Polymer.dom(optTriggeringEvent).rootTarget;
5718 if (domContainer.deepContains( /** @type {Node} */(target))) {
5719 this._ripple.uiDownAction(optTriggeringEvent);
5720 }
5721 }
5722 }
5723 },
5724
5725 /**
5726 * Returns the `<paper-ripple>` element used by this element to create
5727 * ripple effects. The element's ripple is created on demand, when
5728 * necessary, and calling this method will force the
5729 * ripple to be created.
5730 */
5731 getRipple: function() {
5732 this.ensureRipple();
5733 return this._ripple;
5734 },
5735
5736 /**
5737 * Returns true if this element currently contains a ripple effect.
5738 * @return {boolean}
5739 */
5740 hasRipple: function() {
5741 return Boolean(this._ripple);
5742 },
5743
5744 /**
5745 * Create the element's ripple effect via creating a `<paper-ripple>`.
5746 * Override this method to customize the ripple element.
5747 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
5748 */
5749 _createRipple: function() {
5750 return /** @type {!PaperRippleElement} */ (
5751 document.createElement('paper-ripple'));
5752 },
5753
5754 _noinkChanged: function(noink) {
5755 if (this.hasRipple()) {
5756 this._ripple.noink = noink;
5757 }
5758 }
5759 };
5760 /** @polymerBehavior Polymer.PaperButtonBehavior */
5761 Polymer.PaperButtonBehaviorImpl = {
5762 properties: {
5763 /**
5764 * The z-depth of this element, from 0-5. Setting to 0 will remove the
5765 * shadow, and each increasing number greater than 0 will be "deeper"
5766 * than the last.
5767 *
5768 * @attribute elevation
5769 * @type number
5770 * @default 1
5771 */
5772 elevation: {
5773 type: Number,
5774 reflectToAttribute: true,
5775 readOnly: true
5776 }
5777 },
5778
5779 observers: [
5780 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
5781 '_computeKeyboardClass(receivedFocusFromKeyboard)'
5782 ],
5783
5784 hostAttributes: {
5785 role: 'button',
5786 tabindex: '0',
5787 animated: true
5788 },
5789
5790 _calculateElevation: function() {
5791 var e = 1;
5792 if (this.disabled) {
5793 e = 0;
5794 } else if (this.active || this.pressed) {
5795 e = 4;
5796 } else if (this.receivedFocusFromKeyboard) {
5797 e = 3;
5798 }
5799 this._setElevation(e);
5800 },
5801
5802 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5803 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5804 },
5805
5806 /**
5807 * In addition to `IronButtonState` behavior, when space key goes down,
5808 * create a ripple down effect.
5809 *
5810 * @param {!KeyboardEvent} event .
5811 */
5812 _spaceKeyDownHandler: function(event) {
5813 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5814 // Ensure that there is at most one ripple when the space key is held down .
5815 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5816 this._ripple.uiDownAction();
5817 }
5818 },
5819
5820 /**
5821 * In addition to `IronButtonState` behavior, when space key goes up,
5822 * create a ripple up effect.
5823 *
5824 * @param {!KeyboardEvent} event .
5825 */
5826 _spaceKeyUpHandler: function(event) {
5827 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5828 if (this.hasRipple()) {
5829 this._ripple.uiUpAction();
5830 }
5831 }
5832 };
5833
5834 /** @polymerBehavior */
5835 Polymer.PaperButtonBehavior = [
5836 Polymer.IronButtonState,
5837 Polymer.IronControlState,
5838 Polymer.PaperRippleBehavior,
5839 Polymer.PaperButtonBehaviorImpl
5840 ];
5841 Polymer({ 3229 Polymer({
5842 is: 'paper-button', 3230 is: 'paper-button',
5843 3231 behaviors: [ Polymer.PaperButtonBehavior ],
5844 behaviors: [ 3232 properties: {
5845 Polymer.PaperButtonBehavior 3233 raised: {
5846 ], 3234 type: Boolean,
5847 3235 reflectToAttribute: true,
5848 properties: { 3236 value: false,
5849 /** 3237 observer: '_calculateElevation'
5850 * If true, the button should be styled with a shadow. 3238 }
5851 */ 3239 },
5852 raised: { 3240 _calculateElevation: function() {
5853 type: Boolean, 3241 if (!this.raised) {
5854 reflectToAttribute: true, 3242 this._setElevation(0);
5855 value: false, 3243 } else {
5856 observer: '_calculateElevation' 3244 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5857 } 3245 }
5858 }, 3246 }
5859 3247 });
5860 _calculateElevation: function() { 3248
5861 if (!this.raised) {
5862 this._setElevation(0);
5863 } else {
5864 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5865 }
5866 }
5867
5868 /**
5869 Fired when the animation finishes.
5870 This is useful if you want to wait until
5871 the ripple animation finishes to perform some action.
5872
5873 @event transitionend
5874 Event param: {{node: Object}} detail Contains the animated node.
5875 */
5876 });
5877 Polymer({ 3249 Polymer({
5878 is: 'paper-icon-button-light', 3250 is: 'paper-icon-button-light',
5879 extends: 'button', 3251 "extends": 'button',
5880 3252 behaviors: [ Polymer.PaperRippleBehavior ],
5881 behaviors: [ 3253 listeners: {
5882 Polymer.PaperRippleBehavior 3254 down: '_rippleDown',
5883 ], 3255 up: '_rippleUp',
5884 3256 focus: '_rippleDown',
5885 listeners: { 3257 blur: '_rippleUp'
5886 'down': '_rippleDown', 3258 },
5887 'up': '_rippleUp', 3259 _rippleDown: function() {
5888 'focus': '_rippleDown', 3260 this.getRipple().downAction();
5889 'blur': '_rippleUp', 3261 },
5890 }, 3262 _rippleUp: function() {
5891 3263 this.getRipple().upAction();
5892 _rippleDown: function() { 3264 },
5893 this.getRipple().downAction(); 3265 ensureRipple: function(var_args) {
5894 }, 3266 var lastRipple = this._ripple;
5895 3267 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
5896 _rippleUp: function() { 3268 if (this._ripple && this._ripple !== lastRipple) {
5897 this.getRipple().upAction(); 3269 this._ripple.center = true;
5898 }, 3270 this._ripple.classList.add('circle');
5899 3271 }
5900 /** 3272 }
5901 * @param {...*} var_args 3273 });
5902 */ 3274
5903 ensureRipple: function(var_args) { 3275 Polymer.IronRangeBehavior = {
5904 var lastRipple = this._ripple;
5905 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
5906 if (this._ripple && this._ripple !== lastRipple) {
5907 this._ripple.center = true;
5908 this._ripple.classList.add('circle');
5909 }
5910 }
5911 });
5912 /**
5913 * `iron-range-behavior` provides the behavior for something with a minimum to m aximum range.
5914 *
5915 * @demo demo/index.html
5916 * @polymerBehavior
5917 */
5918 Polymer.IronRangeBehavior = {
5919
5920 properties: { 3276 properties: {
5921
5922 /**
5923 * The number that represents the current value.
5924 */
5925 value: { 3277 value: {
5926 type: Number, 3278 type: Number,
5927 value: 0, 3279 value: 0,
5928 notify: true, 3280 notify: true,
5929 reflectToAttribute: true 3281 reflectToAttribute: true
5930 }, 3282 },
5931
5932 /**
5933 * The number that indicates the minimum value of the range.
5934 */
5935 min: { 3283 min: {
5936 type: Number, 3284 type: Number,
5937 value: 0, 3285 value: 0,
5938 notify: true 3286 notify: true
5939 }, 3287 },
5940
5941 /**
5942 * The number that indicates the maximum value of the range.
5943 */
5944 max: { 3288 max: {
5945 type: Number, 3289 type: Number,
5946 value: 100, 3290 value: 100,
5947 notify: true 3291 notify: true
5948 }, 3292 },
5949
5950 /**
5951 * Specifies the value granularity of the range's value.
5952 */
5953 step: { 3293 step: {
5954 type: Number, 3294 type: Number,
5955 value: 1, 3295 value: 1,
5956 notify: true 3296 notify: true
5957 }, 3297 },
5958
5959 /**
5960 * Returns the ratio of the value.
5961 */
5962 ratio: { 3298 ratio: {
5963 type: Number, 3299 type: Number,
5964 value: 0, 3300 value: 0,
5965 readOnly: true, 3301 readOnly: true,
5966 notify: true 3302 notify: true
5967 }, 3303 }
5968 }, 3304 },
5969 3305 observers: [ '_update(value, min, max, step)' ],
5970 observers: [
5971 '_update(value, min, max, step)'
5972 ],
5973
5974 _calcRatio: function(value) { 3306 _calcRatio: function(value) {
5975 return (this._clampValue(value) - this.min) / (this.max - this.min); 3307 return (this._clampValue(value) - this.min) / (this.max - this.min);
5976 }, 3308 },
5977
5978 _clampValue: function(value) { 3309 _clampValue: function(value) {
5979 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); 3310 return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
5980 }, 3311 },
5981
5982 _calcStep: function(value) { 3312 _calcStep: function(value) {
5983 // polymer/issues/2493
5984 value = parseFloat(value); 3313 value = parseFloat(value);
5985
5986 if (!this.step) { 3314 if (!this.step) {
5987 return value; 3315 return value;
5988 } 3316 }
5989
5990 var numSteps = Math.round((value - this.min) / this.step); 3317 var numSteps = Math.round((value - this.min) / this.step);
5991 if (this.step < 1) { 3318 if (this.step < 1) {
5992 /**
5993 * For small values of this.step, if we calculate the step using
5994 * `Math.round(value / step) * step` we may hit a precision point issue
5995 * eg. 0.1 * 0.2 = 0.020000000000000004
5996 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
5997 *
5998 * as a work around we can divide by the reciprocal of `step`
5999 */
6000 return numSteps / (1 / this.step) + this.min; 3319 return numSteps / (1 / this.step) + this.min;
6001 } else { 3320 } else {
6002 return numSteps * this.step + this.min; 3321 return numSteps * this.step + this.min;
6003 } 3322 }
6004 }, 3323 },
6005
6006 _validateValue: function() { 3324 _validateValue: function() {
6007 var v = this._clampValue(this.value); 3325 var v = this._clampValue(this.value);
6008 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; 3326 this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
6009 return this.value !== v; 3327 return this.value !== v;
6010 }, 3328 },
6011
6012 _update: function() { 3329 _update: function() {
6013 this._validateValue(); 3330 this._validateValue();
6014 this._setRatio(this._calcRatio(this.value) * 100); 3331 this._setRatio(this._calcRatio(this.value) * 100);
6015 } 3332 }
6016
6017 }; 3333 };
3334
6018 Polymer({ 3335 Polymer({
6019 is: 'paper-progress', 3336 is: 'paper-progress',
6020 3337 behaviors: [ Polymer.IronRangeBehavior ],
6021 behaviors: [ 3338 properties: {
6022 Polymer.IronRangeBehavior 3339 secondaryProgress: {
6023 ], 3340 type: Number,
6024 3341 value: 0
6025 properties: { 3342 },
6026 /** 3343 secondaryRatio: {
6027 * The number that represents the current secondary progress. 3344 type: Number,
6028 */ 3345 value: 0,
6029 secondaryProgress: { 3346 readOnly: true
6030 type: Number, 3347 },
6031 value: 0 3348 indeterminate: {
6032 }, 3349 type: Boolean,
6033 3350 value: false,
6034 /** 3351 observer: '_toggleIndeterminate'
6035 * The secondary ratio 3352 },
6036 */ 3353 disabled: {
6037 secondaryRatio: { 3354 type: Boolean,
6038 type: Number, 3355 value: false,
6039 value: 0, 3356 reflectToAttribute: true,
6040 readOnly: true 3357 observer: '_disabledChanged'
6041 }, 3358 }
6042 3359 },
6043 /** 3360 observers: [ '_progressChanged(secondaryProgress, value, min, max)' ],
6044 * Use an indeterminate progress indicator. 3361 hostAttributes: {
6045 */ 3362 role: 'progressbar'
6046 indeterminate: { 3363 },
6047 type: Boolean, 3364 _toggleIndeterminate: function(indeterminate) {
6048 value: false, 3365 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
6049 observer: '_toggleIndeterminate' 3366 },
6050 }, 3367 _transformProgress: function(progress, ratio) {
6051 3368 var transform = 'scaleX(' + ratio / 100 + ')';
6052 /** 3369 progress.style.transform = progress.style.webkitTransform = transform;
6053 * True if the progress is disabled. 3370 },
6054 */ 3371 _mainRatioChanged: function(ratio) {
6055 disabled: { 3372 this._transformProgress(this.$.primaryProgress, ratio);
6056 type: Boolean, 3373 },
6057 value: false, 3374 _progressChanged: function(secondaryProgress, value, min, max) {
6058 reflectToAttribute: true, 3375 secondaryProgress = this._clampValue(secondaryProgress);
6059 observer: '_disabledChanged' 3376 value = this._clampValue(value);
6060 } 3377 var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
6061 }, 3378 var mainRatio = this._calcRatio(value) * 100;
6062 3379 this._setSecondaryRatio(secondaryRatio);
6063 observers: [ 3380 this._transformProgress(this.$.secondaryProgress, secondaryRatio);
6064 '_progressChanged(secondaryProgress, value, min, max)' 3381 this._transformProgress(this.$.primaryProgress, mainRatio);
6065 ], 3382 this.secondaryProgress = secondaryProgress;
6066 3383 this.setAttribute('aria-valuenow', value);
6067 hostAttributes: { 3384 this.setAttribute('aria-valuemin', min);
6068 role: 'progressbar' 3385 this.setAttribute('aria-valuemax', max);
6069 }, 3386 },
6070 3387 _disabledChanged: function(disabled) {
6071 _toggleIndeterminate: function(indeterminate) { 3388 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
6072 // If we use attribute/class binding, the animation sometimes doesn't tran slate properly 3389 },
6073 // on Safari 7.1. So instead, we toggle the class here in the update metho d. 3390 _hideSecondaryProgress: function(secondaryRatio) {
6074 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); 3391 return secondaryRatio === 0;
6075 }, 3392 }
6076 3393 });
6077 _transformProgress: function(progress, ratio) { 3394
6078 var transform = 'scaleX(' + (ratio / 100) + ')'; 3395 Polymer({
6079 progress.style.transform = progress.style.webkitTransform = transform; 3396 is: 'iron-iconset-svg',
6080 }, 3397 properties: {
6081 3398 name: {
6082 _mainRatioChanged: function(ratio) { 3399 type: String,
6083 this._transformProgress(this.$.primaryProgress, ratio); 3400 observer: '_nameChanged'
6084 }, 3401 },
6085 3402 size: {
6086 _progressChanged: function(secondaryProgress, value, min, max) { 3403 type: Number,
6087 secondaryProgress = this._clampValue(secondaryProgress); 3404 value: 24
6088 value = this._clampValue(value); 3405 }
6089 3406 },
6090 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; 3407 attached: function() {
6091 var mainRatio = this._calcRatio(value) * 100; 3408 this.style.display = 'none';
6092 3409 },
6093 this._setSecondaryRatio(secondaryRatio); 3410 getIconNames: function() {
6094 this._transformProgress(this.$.secondaryProgress, secondaryRatio); 3411 this._icons = this._createIconMap();
6095 this._transformProgress(this.$.primaryProgress, mainRatio); 3412 return Object.keys(this._icons).map(function(n) {
6096 3413 return this.name + ':' + n;
6097 this.secondaryProgress = secondaryProgress; 3414 }, this);
6098 3415 },
6099 this.setAttribute('aria-valuenow', value); 3416 applyIcon: function(element, iconName) {
6100 this.setAttribute('aria-valuemin', min); 3417 element = element.root || element;
6101 this.setAttribute('aria-valuemax', max); 3418 this.removeIcon(element);
6102 }, 3419 var svg = this._cloneIcon(iconName);
6103 3420 if (svg) {
6104 _disabledChanged: function(disabled) { 3421 var pde = Polymer.dom(element);
6105 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 3422 pde.insertBefore(svg, pde.childNodes[0]);
6106 }, 3423 return element._svgIcon = svg;
6107 3424 }
6108 _hideSecondaryProgress: function(secondaryRatio) { 3425 return null;
6109 return secondaryRatio === 0; 3426 },
6110 } 3427 removeIcon: function(element) {
6111 }); 3428 if (element._svgIcon) {
6112 /** 3429 Polymer.dom(element).removeChild(element._svgIcon);
6113 * The `iron-iconset-svg` element allows users to define their own icon sets 3430 element._svgIcon = null;
6114 * that contain svg icons. The svg icon elements should be children of the 3431 }
6115 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. 3432 },
6116 * 3433 _nameChanged: function() {
6117 * Using svg elements to create icons has a few advantages over traditional 3434 new Polymer.IronMeta({
6118 * bitmap graphics like jpg or png. Icons that use svg are vector based so 3435 type: 'iconset',
6119 * they are resolution independent and should look good on any device. They 3436 key: this.name,
6120 * are stylable via css. Icons can be themed, colorized, and even animated. 3437 value: this
6121 * 3438 });
6122 * Example: 3439 this.async(function() {
6123 * 3440 this.fire('iron-iconset-added', this, {
6124 * <iron-iconset-svg name="my-svg-icons" size="24"> 3441 node: window
6125 * <svg>
6126 * <defs>
6127 * <g id="shape">
6128 * <rect x="12" y="0" width="12" height="24" />
6129 * <circle cx="12" cy="12" r="12" />
6130 * </g>
6131 * </defs>
6132 * </svg>
6133 * </iron-iconset-svg>
6134 *
6135 * This will automatically register the icon set "my-svg-icons" to the iconset
6136 * database. To use these icons from within another element, make a
6137 * `iron-iconset` element and call the `byId` method
6138 * to retrieve a given iconset. To apply a particular icon inside an
6139 * element use the `applyIcon` method. For example:
6140 *
6141 * iconset.applyIcon(iconNode, 'car');
6142 *
6143 * @element iron-iconset-svg
6144 * @demo demo/index.html
6145 * @implements {Polymer.Iconset}
6146 */
6147 Polymer({
6148 is: 'iron-iconset-svg',
6149
6150 properties: {
6151
6152 /**
6153 * The name of the iconset.
6154 */
6155 name: {
6156 type: String,
6157 observer: '_nameChanged'
6158 },
6159
6160 /**
6161 * The size of an individual icon. Note that icons must be square.
6162 */
6163 size: {
6164 type: Number,
6165 value: 24
6166 }
6167
6168 },
6169
6170 attached: function() {
6171 this.style.display = 'none';
6172 },
6173
6174 /**
6175 * Construct an array of all icon names in this iconset.
6176 *
6177 * @return {!Array} Array of icon names.
6178 */
6179 getIconNames: function() {
6180 this._icons = this._createIconMap();
6181 return Object.keys(this._icons).map(function(n) {
6182 return this.name + ':' + n;
6183 }, this);
6184 },
6185
6186 /**
6187 * Applies an icon to the given element.
6188 *
6189 * An svg icon is prepended to the element's shadowRoot if it exists,
6190 * otherwise to the element itself.
6191 *
6192 * @method applyIcon
6193 * @param {Element} element Element to which the icon is applied.
6194 * @param {string} iconName Name of the icon to apply.
6195 * @return {?Element} The svg element which renders the icon.
6196 */
6197 applyIcon: function(element, iconName) {
6198 // insert svg element into shadow root, if it exists
6199 element = element.root || element;
6200 // Remove old svg element
6201 this.removeIcon(element);
6202 // install new svg element
6203 var svg = this._cloneIcon(iconName);
6204 if (svg) {
6205 var pde = Polymer.dom(element);
6206 pde.insertBefore(svg, pde.childNodes[0]);
6207 return element._svgIcon = svg;
6208 }
6209 return null;
6210 },
6211
6212 /**
6213 * Remove an icon from the given element by undoing the changes effected
6214 * by `applyIcon`.
6215 *
6216 * @param {Element} element The element from which the icon is removed.
6217 */
6218 removeIcon: function(element) {
6219 // Remove old svg element
6220 if (element._svgIcon) {
6221 Polymer.dom(element).removeChild(element._svgIcon);
6222 element._svgIcon = null;
6223 }
6224 },
6225
6226 /**
6227 *
6228 * When name is changed, register iconset metadata
6229 *
6230 */
6231 _nameChanged: function() {
6232 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
6233 this.async(function() {
6234 this.fire('iron-iconset-added', this, {node: window});
6235 }); 3442 });
6236 }, 3443 });
6237 3444 },
6238 /** 3445 _createIconMap: function() {
6239 * Create a map of child SVG elements by id. 3446 var icons = Object.create(null);
6240 * 3447 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) {
6241 * @return {!Object} Map of id's to SVG elements. 3448 icons[icon.id] = icon;
6242 */ 3449 });
6243 _createIconMap: function() { 3450 return icons;
6244 // Objects chained to Object.prototype (`{}`) have members. Specifically, 3451 },
6245 // on FF there is a `watch` method that confuses the icon map, so we 3452 _cloneIcon: function(id) {
6246 // need to use a null-based object here. 3453 this._icons = this._icons || this._createIconMap();
6247 var icons = Object.create(null); 3454 return this._prepareSvgClone(this._icons[id], this.size);
6248 Polymer.dom(this).querySelectorAll('[id]') 3455 },
6249 .forEach(function(icon) { 3456 _prepareSvgClone: function(sourceSvg, size) {
6250 icons[icon.id] = icon; 3457 if (sourceSvg) {
6251 }); 3458 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
6252 return icons; 3459 svg.setAttribute('viewBox', viewBox);
6253 }, 3460 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6254 3461 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he ight: 100%;';
6255 /** 3462 svg.appendChild(content).removeAttribute('id');
6256 * Produce installable clone of the SVG element matching `id` in this 3463 return svg;
6257 * iconset, or `undefined` if there is no matching element. 3464 }
6258 * 3465 return null;
6259 * @return {Element} Returns an installable clone of the SVG element 3466 }
6260 * matching `id`. 3467 });
6261 */ 3468
6262 _cloneIcon: function(id) {
6263 // create the icon map on-demand, since the iconset itself has no discrete
6264 // signal to know when it's children are fully parsed
6265 this._icons = this._icons || this._createIconMap();
6266 return this._prepareSvgClone(this._icons[id], this.size);
6267 },
6268
6269 /**
6270 * @param {Element} sourceSvg
6271 * @param {number} size
6272 * @return {Element}
6273 */
6274 _prepareSvgClone: function(sourceSvg, size) {
6275 if (sourceSvg) {
6276 var content = sourceSvg.cloneNode(true),
6277 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
6278 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s ize;
6279 svg.setAttribute('viewBox', viewBox);
6280 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6281 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136
6282 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
6283 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6284 svg.appendChild(content).removeAttribute('id');
6285 return svg;
6286 }
6287 return null;
6288 }
6289
6290 });
6291 // Copyright 2015 The Chromium Authors. All rights reserved. 3469 // Copyright 2015 The Chromium Authors. All rights reserved.
6292 // Use of this source code is governed by a BSD-style license that can be 3470 // Use of this source code is governed by a BSD-style license that can be
6293 // found in the LICENSE file. 3471 // found in the LICENSE file.
6294
6295 cr.define('downloads', function() { 3472 cr.define('downloads', function() {
6296 var Item = Polymer({ 3473 var Item = Polymer({
6297 is: 'downloads-item', 3474 is: 'downloads-item',
6298
6299 properties: { 3475 properties: {
6300 data: { 3476 data: {
6301 type: Object, 3477 type: Object
6302 }, 3478 },
6303
6304 completelyOnDisk_: { 3479 completelyOnDisk_: {
6305 computed: 'computeCompletelyOnDisk_(' + 3480 computed: 'computeCompletelyOnDisk_(' + 'data.state, data.file_externall y_removed)',
6306 'data.state, data.file_externally_removed)',
6307 type: Boolean, 3481 type: Boolean,
6308 value: true, 3482 value: true
6309 }, 3483 },
6310
6311 controlledBy_: { 3484 controlledBy_: {
6312 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', 3485 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
6313 type: String, 3486 type: String,
6314 value: '', 3487 value: ''
6315 }, 3488 },
6316
6317 isActive_: { 3489 isActive_: {
6318 computed: 'computeIsActive_(' + 3490 computed: 'computeIsActive_(' + 'data.state, data.file_externally_remove d)',
6319 'data.state, data.file_externally_removed)',
6320 type: Boolean, 3491 type: Boolean,
6321 value: true, 3492 value: true
6322 }, 3493 },
6323
6324 isDangerous_: { 3494 isDangerous_: {
6325 computed: 'computeIsDangerous_(data.state)', 3495 computed: 'computeIsDangerous_(data.state)',
6326 type: Boolean, 3496 type: Boolean,
6327 value: false, 3497 value: false
6328 }, 3498 },
6329
6330 isMalware_: { 3499 isMalware_: {
6331 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', 3500 computed: 'computeIsMalware_(isDangerous_, data.danger_type)',
6332 type: Boolean, 3501 type: Boolean,
6333 value: false, 3502 value: false
6334 }, 3503 },
6335
6336 isInProgress_: { 3504 isInProgress_: {
6337 computed: 'computeIsInProgress_(data.state)', 3505 computed: 'computeIsInProgress_(data.state)',
6338 type: Boolean, 3506 type: Boolean,
6339 value: false, 3507 value: false
6340 }, 3508 },
6341
6342 pauseOrResumeText_: { 3509 pauseOrResumeText_: {
6343 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', 3510 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)',
6344 type: String, 3511 type: String
6345 }, 3512 },
6346
6347 showCancel_: { 3513 showCancel_: {
6348 computed: 'computeShowCancel_(data.state)', 3514 computed: 'computeShowCancel_(data.state)',
6349 type: Boolean, 3515 type: Boolean,
6350 value: false, 3516 value: false
6351 }, 3517 },
6352
6353 showProgress_: { 3518 showProgress_: {
6354 computed: 'computeShowProgress_(showCancel_, data.percent)', 3519 computed: 'computeShowProgress_(showCancel_, data.percent)',
6355 type: Boolean, 3520 type: Boolean,
6356 value: false, 3521 value: false
6357 }, 3522 }
6358 }, 3523 },
6359 3524 observers: [ 'observeControlledBy_(controlledBy_)', 'observeIsDangerous_(isD angerous_, data)' ],
6360 observers: [
6361 // TODO(dbeam): this gets called way more when I observe data.by_ext_id
6362 // and data.by_ext_name directly. Why?
6363 'observeControlledBy_(controlledBy_)',
6364 'observeIsDangerous_(isDangerous_, data)',
6365 ],
6366
6367 ready: function() { 3525 ready: function() {
6368 this.content = this.$.content; 3526 this.content = this.$.content;
6369 }, 3527 },
6370
6371 /** @private */
6372 computeClass_: function() { 3528 computeClass_: function() {
6373 var classes = []; 3529 var classes = [];
6374 3530 if (this.isActive_) classes.push('is-active');
6375 if (this.isActive_) 3531 if (this.isDangerous_) classes.push('dangerous');
6376 classes.push('is-active'); 3532 if (this.showProgress_) classes.push('show-progress');
6377
6378 if (this.isDangerous_)
6379 classes.push('dangerous');
6380
6381 if (this.showProgress_)
6382 classes.push('show-progress');
6383
6384 return classes.join(' '); 3533 return classes.join(' ');
6385 }, 3534 },
6386
6387 /** @private */
6388 computeCompletelyOnDisk_: function() { 3535 computeCompletelyOnDisk_: function() {
6389 return this.data.state == downloads.States.COMPLETE && 3536 return this.data.state == downloads.States.COMPLETE && !this.data.file_ext ernally_removed;
6390 !this.data.file_externally_removed; 3537 },
6391 },
6392
6393 /** @private */
6394 computeControlledBy_: function() { 3538 computeControlledBy_: function() {
6395 if (!this.data.by_ext_id || !this.data.by_ext_name) 3539 if (!this.data.by_ext_id || !this.data.by_ext_name) return '';
6396 return '';
6397
6398 var url = 'chrome://extensions#' + this.data.by_ext_id; 3540 var url = 'chrome://extensions#' + this.data.by_ext_id;
6399 var name = this.data.by_ext_name; 3541 var name = this.data.by_ext_name;
6400 return loadTimeData.getStringF('controlledByUrl', url, name); 3542 return loadTimeData.getStringF('controlledByUrl', url, name);
6401 }, 3543 },
6402
6403 /** @private */
6404 computeDangerIcon_: function() { 3544 computeDangerIcon_: function() {
6405 if (!this.isDangerous_) 3545 if (!this.isDangerous_) return '';
6406 return '';
6407
6408 switch (this.data.danger_type) { 3546 switch (this.data.danger_type) {
6409 case downloads.DangerType.DANGEROUS_CONTENT: 3547 case downloads.DangerType.DANGEROUS_CONTENT:
6410 case downloads.DangerType.DANGEROUS_HOST: 3548 case downloads.DangerType.DANGEROUS_HOST:
6411 case downloads.DangerType.DANGEROUS_URL: 3549 case downloads.DangerType.DANGEROUS_URL:
6412 case downloads.DangerType.POTENTIALLY_UNWANTED: 3550 case downloads.DangerType.POTENTIALLY_UNWANTED:
6413 case downloads.DangerType.UNCOMMON_CONTENT: 3551 case downloads.DangerType.UNCOMMON_CONTENT:
6414 return 'downloads:remove-circle'; 3552 return 'downloads:remove-circle';
6415 default: 3553
6416 return 'cr:warning'; 3554 default:
6417 } 3555 return 'cr:warning';
6418 }, 3556 }
6419 3557 },
6420 /** @private */
6421 computeDate_: function() { 3558 computeDate_: function() {
6422 assert(typeof this.data.hideDate == 'boolean'); 3559 assert(typeof this.data.hideDate == 'boolean');
6423 if (this.data.hideDate) 3560 if (this.data.hideDate) return '';
6424 return '';
6425 return assert(this.data.since_string || this.data.date_string); 3561 return assert(this.data.since_string || this.data.date_string);
6426 }, 3562 },
6427
6428 /** @private */
6429 computeDescription_: function() { 3563 computeDescription_: function() {
6430 var data = this.data; 3564 var data = this.data;
6431
6432 switch (data.state) { 3565 switch (data.state) {
6433 case downloads.States.DANGEROUS: 3566 case downloads.States.DANGEROUS:
6434 var fileName = data.file_name; 3567 var fileName = data.file_name;
6435 switch (data.danger_type) { 3568 switch (data.danger_type) {
6436 case downloads.DangerType.DANGEROUS_FILE: 3569 case downloads.DangerType.DANGEROUS_FILE:
6437 return loadTimeData.getStringF('dangerFileDesc', fileName); 3570 return loadTimeData.getStringF('dangerFileDesc', fileName);
6438 case downloads.DangerType.DANGEROUS_URL: 3571
6439 return loadTimeData.getString('dangerUrlDesc'); 3572 case downloads.DangerType.DANGEROUS_URL:
6440 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. 3573 return loadTimeData.getString('dangerUrlDesc');
6441 case downloads.DangerType.DANGEROUS_HOST: 3574
6442 return loadTimeData.getStringF('dangerContentDesc', fileName); 3575 case downloads.DangerType.DANGEROUS_CONTENT:
6443 case downloads.DangerType.UNCOMMON_CONTENT: 3576 case downloads.DangerType.DANGEROUS_HOST:
6444 return loadTimeData.getStringF('dangerUncommonDesc', fileName); 3577 return loadTimeData.getStringF('dangerContentDesc', fileName);
6445 case downloads.DangerType.POTENTIALLY_UNWANTED: 3578
6446 return loadTimeData.getStringF('dangerSettingsDesc', fileName); 3579 case downloads.DangerType.UNCOMMON_CONTENT:
6447 } 3580 return loadTimeData.getStringF('dangerUncommonDesc', fileName);
6448 break; 3581
6449 3582 case downloads.DangerType.POTENTIALLY_UNWANTED:
6450 case downloads.States.IN_PROGRESS: 3583 return loadTimeData.getStringF('dangerSettingsDesc', fileName);
6451 case downloads.States.PAUSED: // Fallthrough. 3584 }
6452 return data.progress_status_text; 3585 break;
6453 } 3586
6454 3587 case downloads.States.IN_PROGRESS:
3588 case downloads.States.PAUSED:
3589 return data.progress_status_text;
3590 }
6455 return ''; 3591 return '';
6456 }, 3592 },
6457
6458 /** @private */
6459 computeIsActive_: function() { 3593 computeIsActive_: function() {
6460 return this.data.state != downloads.States.CANCELLED && 3594 return this.data.state != downloads.States.CANCELLED && this.data.state != downloads.States.INTERRUPTED && !this.data.file_externally_removed;
6461 this.data.state != downloads.States.INTERRUPTED && 3595 },
6462 !this.data.file_externally_removed;
6463 },
6464
6465 /** @private */
6466 computeIsDangerous_: function() { 3596 computeIsDangerous_: function() {
6467 return this.data.state == downloads.States.DANGEROUS; 3597 return this.data.state == downloads.States.DANGEROUS;
6468 }, 3598 },
6469
6470 /** @private */
6471 computeIsInProgress_: function() { 3599 computeIsInProgress_: function() {
6472 return this.data.state == downloads.States.IN_PROGRESS; 3600 return this.data.state == downloads.States.IN_PROGRESS;
6473 }, 3601 },
6474
6475 /** @private */
6476 computeIsMalware_: function() { 3602 computeIsMalware_: function() {
6477 return this.isDangerous_ && 3603 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);
6478 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || 3604 },
6479 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
6480 this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
6481 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
6482 },
6483
6484 /** @private */
6485 computePauseOrResumeText_: function() { 3605 computePauseOrResumeText_: function() {
6486 if (this.isInProgress_) 3606 if (this.isInProgress_) return loadTimeData.getString('controlPause');
6487 return loadTimeData.getString('controlPause'); 3607 if (this.data.resume) return loadTimeData.getString('controlResume');
6488 if (this.data.resume)
6489 return loadTimeData.getString('controlResume');
6490 return ''; 3608 return '';
6491 }, 3609 },
6492
6493 /** @private */
6494 computeRemoveStyle_: function() { 3610 computeRemoveStyle_: function() {
6495 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); 3611 var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
6496 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; 3612 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
6497 return hideRemove ? 'visibility: hidden' : ''; 3613 return hideRemove ? 'visibility: hidden' : '';
6498 }, 3614 },
6499
6500 /** @private */
6501 computeShowCancel_: function() { 3615 computeShowCancel_: function() {
6502 return this.data.state == downloads.States.IN_PROGRESS || 3616 return this.data.state == downloads.States.IN_PROGRESS || this.data.state == downloads.States.PAUSED;
6503 this.data.state == downloads.States.PAUSED; 3617 },
6504 },
6505
6506 /** @private */
6507 computeShowProgress_: function() { 3618 computeShowProgress_: function() {
6508 return this.showCancel_ && this.data.percent >= -1; 3619 return this.showCancel_ && this.data.percent >= -1;
6509 }, 3620 },
6510
6511 /** @private */
6512 computeTag_: function() { 3621 computeTag_: function() {
6513 switch (this.data.state) { 3622 switch (this.data.state) {
6514 case downloads.States.CANCELLED: 3623 case downloads.States.CANCELLED:
6515 return loadTimeData.getString('statusCancelled'); 3624 return loadTimeData.getString('statusCancelled');
6516 3625
6517 case downloads.States.INTERRUPTED: 3626 case downloads.States.INTERRUPTED:
6518 return this.data.last_reason_text; 3627 return this.data.last_reason_text;
6519 3628
6520 case downloads.States.COMPLETE: 3629 case downloads.States.COMPLETE:
6521 return this.data.file_externally_removed ? 3630 return this.data.file_externally_removed ? loadTimeData.getString('statu sRemoved') : '';
6522 loadTimeData.getString('statusRemoved') : ''; 3631 }
6523 }
6524
6525 return ''; 3632 return '';
6526 }, 3633 },
6527
6528 /** @private */
6529 isIndeterminate_: function() { 3634 isIndeterminate_: function() {
6530 return this.data.percent == -1; 3635 return this.data.percent == -1;
6531 }, 3636 },
6532
6533 /** @private */
6534 observeControlledBy_: function() { 3637 observeControlledBy_: function() {
6535 this.$['controlled-by'].innerHTML = this.controlledBy_; 3638 this.$['controlled-by'].innerHTML = this.controlledBy_;
6536 }, 3639 },
6537
6538 /** @private */
6539 observeIsDangerous_: function() { 3640 observeIsDangerous_: function() {
6540 if (!this.data) 3641 if (!this.data) return;
6541 return;
6542
6543 if (this.isDangerous_) { 3642 if (this.isDangerous_) {
6544 this.$.url.removeAttribute('href'); 3643 this.$.url.removeAttribute('href');
6545 } else { 3644 } else {
6546 this.$.url.href = assert(this.data.url); 3645 this.$.url.href = assert(this.data.url);
6547 var filePath = encodeURIComponent(this.data.file_path); 3646 var filePath = encodeURIComponent(this.data.file_path);
6548 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; 3647 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x';
6549 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; 3648 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor;
6550 } 3649 }
6551 }, 3650 },
6552
6553 /** @private */
6554 onCancelTap_: function() { 3651 onCancelTap_: function() {
6555 downloads.ActionService.getInstance().cancel(this.data.id); 3652 downloads.ActionService.getInstance().cancel(this.data.id);
6556 }, 3653 },
6557
6558 /** @private */
6559 onDiscardDangerousTap_: function() { 3654 onDiscardDangerousTap_: function() {
6560 downloads.ActionService.getInstance().discardDangerous(this.data.id); 3655 downloads.ActionService.getInstance().discardDangerous(this.data.id);
6561 }, 3656 },
6562
6563 /**
6564 * @private
6565 * @param {Event} e
6566 */
6567 onDragStart_: function(e) { 3657 onDragStart_: function(e) {
6568 e.preventDefault(); 3658 e.preventDefault();
6569 downloads.ActionService.getInstance().drag(this.data.id); 3659 downloads.ActionService.getInstance().drag(this.data.id);
6570 }, 3660 },
6571
6572 /**
6573 * @param {Event} e
6574 * @private
6575 */
6576 onFileLinkTap_: function(e) { 3661 onFileLinkTap_: function(e) {
6577 e.preventDefault(); 3662 e.preventDefault();
6578 downloads.ActionService.getInstance().openFile(this.data.id); 3663 downloads.ActionService.getInstance().openFile(this.data.id);
6579 }, 3664 },
6580
6581 /** @private */
6582 onPauseOrResumeTap_: function() { 3665 onPauseOrResumeTap_: function() {
6583 if (this.isInProgress_) 3666 if (this.isInProgress_) downloads.ActionService.getInstance().pause(this.d ata.id); else downloads.ActionService.getInstance().resume(this.data.id);
6584 downloads.ActionService.getInstance().pause(this.data.id); 3667 },
6585 else
6586 downloads.ActionService.getInstance().resume(this.data.id);
6587 },
6588
6589 /** @private */
6590 onRemoveTap_: function() { 3668 onRemoveTap_: function() {
6591 downloads.ActionService.getInstance().remove(this.data.id); 3669 downloads.ActionService.getInstance().remove(this.data.id);
6592 }, 3670 },
6593
6594 /** @private */
6595 onRetryTap_: function() { 3671 onRetryTap_: function() {
6596 downloads.ActionService.getInstance().download(this.data.url); 3672 downloads.ActionService.getInstance().download(this.data.url);
6597 }, 3673 },
6598
6599 /** @private */
6600 onSaveDangerousTap_: function() { 3674 onSaveDangerousTap_: function() {
6601 downloads.ActionService.getInstance().saveDangerous(this.data.id); 3675 downloads.ActionService.getInstance().saveDangerous(this.data.id);
6602 }, 3676 },
6603
6604 /** @private */
6605 onShowTap_: function() { 3677 onShowTap_: function() {
6606 downloads.ActionService.getInstance().show(this.data.id); 3678 downloads.ActionService.getInstance().show(this.data.id);
6607 }, 3679 }
6608 }); 3680 });
6609 3681 return {
6610 return {Item: Item}; 3682 Item: Item
3683 };
6611 }); 3684 });
6612 /** @polymerBehavior Polymer.PaperItemBehavior */ 3685
6613 Polymer.PaperItemBehaviorImpl = { 3686 Polymer.PaperItemBehaviorImpl = {
6614 hostAttributes: { 3687 hostAttributes: {
6615 role: 'option', 3688 role: 'option',
6616 tabindex: '0' 3689 tabindex: '0'
6617 } 3690 }
6618 }; 3691 };
6619 3692
6620 /** @polymerBehavior */ 3693 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState, Polymer.PaperItemBehaviorImpl ];
6621 Polymer.PaperItemBehavior = [ 3694
6622 Polymer.IronButtonState,
6623 Polymer.IronControlState,
6624 Polymer.PaperItemBehaviorImpl
6625 ];
6626 Polymer({ 3695 Polymer({
6627 is: 'paper-item', 3696 is: 'paper-item',
6628 3697 behaviors: [ Polymer.PaperItemBehavior ]
6629 behaviors: [ 3698 });
6630 Polymer.PaperItemBehavior 3699
6631 ] 3700 Polymer.IronSelection = function(selectCallback) {
6632 }); 3701 this.selection = [];
6633 /** 3702 this.selectCallback = selectCallback;
6634 * @param {!Function} selectCallback 3703 };
6635 * @constructor 3704
6636 */ 3705 Polymer.IronSelection.prototype = {
6637 Polymer.IronSelection = function(selectCallback) { 3706 get: function() {
6638 this.selection = []; 3707 return this.multi ? this.selection.slice() : this.selection[0];
6639 this.selectCallback = selectCallback; 3708 },
6640 }; 3709 clear: function(excludes) {
6641 3710 this.selection.slice().forEach(function(item) {
6642 Polymer.IronSelection.prototype = { 3711 if (!excludes || excludes.indexOf(item) < 0) {
6643 3712 this.setItemSelected(item, false);
6644 /** 3713 }
6645 * Retrieves the selected item(s). 3714 }, this);
6646 * 3715 },
6647 * @method get 3716 isSelected: function(item) {
6648 * @returns Returns the selected item(s). If the multi property is true, 3717 return this.selection.indexOf(item) >= 0;
6649 * `get` will return an array, otherwise it will return 3718 },
6650 * the selected item or undefined if there is no selection. 3719 setItemSelected: function(item, isSelected) {
6651 */ 3720 if (item != null) {
6652 get: function() { 3721 if (isSelected !== this.isSelected(item)) {
6653 return this.multi ? this.selection.slice() : this.selection[0]; 3722 if (isSelected) {
6654 }, 3723 this.selection.push(item);
6655 3724 } else {
6656 /** 3725 var i = this.selection.indexOf(item);
6657 * Clears all the selection except the ones indicated. 3726 if (i >= 0) {
6658 * 3727 this.selection.splice(i, 1);
6659 * @method clear
6660 * @param {Array} excludes items to be excluded.
6661 */
6662 clear: function(excludes) {
6663 this.selection.slice().forEach(function(item) {
6664 if (!excludes || excludes.indexOf(item) < 0) {
6665 this.setItemSelected(item, false);
6666 }
6667 }, this);
6668 },
6669
6670 /**
6671 * Indicates if a given item is selected.
6672 *
6673 * @method isSelected
6674 * @param {*} item The item whose selection state should be checked.
6675 * @returns Returns true if `item` is selected.
6676 */
6677 isSelected: function(item) {
6678 return this.selection.indexOf(item) >= 0;
6679 },
6680
6681 /**
6682 * Sets the selection state for a given item to either selected or deselecte d.
6683 *
6684 * @method setItemSelected
6685 * @param {*} item The item to select.
6686 * @param {boolean} isSelected True for selected, false for deselected.
6687 */
6688 setItemSelected: function(item, isSelected) {
6689 if (item != null) {
6690 if (isSelected !== this.isSelected(item)) {
6691 // proceed to update selection only if requested state differs from cu rrent
6692 if (isSelected) {
6693 this.selection.push(item);
6694 } else {
6695 var i = this.selection.indexOf(item);
6696 if (i >= 0) {
6697 this.selection.splice(i, 1);
6698 }
6699 }
6700 if (this.selectCallback) {
6701 this.selectCallback(item, isSelected);
6702 } 3728 }
6703 } 3729 }
6704 } 3730 if (this.selectCallback) {
6705 }, 3731 this.selectCallback(item, isSelected);
6706
6707 /**
6708 * Sets the selection state for a given item. If the `multi` property
6709 * is true, then the selected state of `item` will be toggled; otherwise
6710 * the `item` will be selected.
6711 *
6712 * @method select
6713 * @param {*} item The item to select.
6714 */
6715 select: function(item) {
6716 if (this.multi) {
6717 this.toggle(item);
6718 } else if (this.get() !== item) {
6719 this.setItemSelected(this.get(), false);
6720 this.setItemSelected(item, true);
6721 }
6722 },
6723
6724 /**
6725 * Toggles the selection state for `item`.
6726 *
6727 * @method toggle
6728 * @param {*} item The item to toggle.
6729 */
6730 toggle: function(item) {
6731 this.setItemSelected(item, !this.isSelected(item));
6732 }
6733
6734 };
6735 /** @polymerBehavior */
6736 Polymer.IronSelectableBehavior = {
6737
6738 /**
6739 * Fired when iron-selector is activated (selected or deselected).
6740 * It is fired before the selected items are changed.
6741 * Cancel the event to abort selection.
6742 *
6743 * @event iron-activate
6744 */
6745
6746 /**
6747 * Fired when an item is selected
6748 *
6749 * @event iron-select
6750 */
6751
6752 /**
6753 * Fired when an item is deselected
6754 *
6755 * @event iron-deselect
6756 */
6757
6758 /**
6759 * Fired when the list of selectable items changes (e.g., items are
6760 * added or removed). The detail of the event is a mutation record that
6761 * describes what changed.
6762 *
6763 * @event iron-items-changed
6764 */
6765
6766 properties: {
6767
6768 /**
6769 * If you want to use an attribute value or property of an element for
6770 * `selected` instead of the index, set this to the name of the attribute
6771 * or property. Hyphenated values are converted to camel case when used to
6772 * look up the property of a selectable element. Camel cased values are
6773 * *not* converted to hyphenated values for attribute lookup. It's
6774 * recommended that you provide the hyphenated form of the name so that
6775 * selection works in both cases. (Use `attr-or-property-name` instead of
6776 * `attrOrPropertyName`.)
6777 */
6778 attrForSelected: {
6779 type: String,
6780 value: null
6781 },
6782
6783 /**
6784 * Gets or sets the selected element. The default is to use the index of t he item.
6785 * @type {string|number}
6786 */
6787 selected: {
6788 type: String,
6789 notify: true
6790 },
6791
6792 /**
6793 * Returns the currently selected item.
6794 *
6795 * @type {?Object}
6796 */
6797 selectedItem: {
6798 type: Object,
6799 readOnly: true,
6800 notify: true
6801 },
6802
6803 /**
6804 * The event that fires from items when they are selected. Selectable
6805 * will listen for this event from items and update the selection state.
6806 * Set to empty string to listen to no events.
6807 */
6808 activateEvent: {
6809 type: String,
6810 value: 'tap',
6811 observer: '_activateEventChanged'
6812 },
6813
6814 /**
6815 * This is a CSS selector string. If this is set, only items that match t he CSS selector
6816 * are selectable.
6817 */
6818 selectable: String,
6819
6820 /**
6821 * The class to set on elements when selected.
6822 */
6823 selectedClass: {
6824 type: String,
6825 value: 'iron-selected'
6826 },
6827
6828 /**
6829 * The attribute to set on elements when selected.
6830 */
6831 selectedAttribute: {
6832 type: String,
6833 value: null
6834 },
6835
6836 /**
6837 * Default fallback if the selection based on selected with `attrForSelect ed`
6838 * is not found.
6839 */
6840 fallbackSelection: {
6841 type: String,
6842 value: null
6843 },
6844
6845 /**
6846 * The list of items from which a selection can be made.
6847 */
6848 items: {
6849 type: Array,
6850 readOnly: true,
6851 notify: true,
6852 value: function() {
6853 return [];
6854 } 3732 }
6855 }, 3733 }
6856 3734 }
6857 /** 3735 },
6858 * The set of excluded elements where the key is the `localName` 3736 select: function(item) {
6859 * of the element that will be ignored from the item list. 3737 if (this.multi) {
6860 * 3738 this.toggle(item);
6861 * @default {template: 1} 3739 } else if (this.get() !== item) {
6862 */ 3740 this.setItemSelected(this.get(), false);
6863 _excludedLocalNames: { 3741 this.setItemSelected(item, true);
6864 type: Object, 3742 }
6865 value: function() { 3743 },
6866 return { 3744 toggle: function(item) {
6867 'template': 1 3745 this.setItemSelected(item, !this.isSelected(item));
6868 }; 3746 }
3747 };
3748
3749 Polymer.IronSelectableBehavior = {
3750 properties: {
3751 attrForSelected: {
3752 type: String,
3753 value: null
3754 },
3755 selected: {
3756 type: String,
3757 notify: true
3758 },
3759 selectedItem: {
3760 type: Object,
3761 readOnly: true,
3762 notify: true
3763 },
3764 activateEvent: {
3765 type: String,
3766 value: 'tap',
3767 observer: '_activateEventChanged'
3768 },
3769 selectable: String,
3770 selectedClass: {
3771 type: String,
3772 value: 'iron-selected'
3773 },
3774 selectedAttribute: {
3775 type: String,
3776 value: null
3777 },
3778 fallbackSelection: {
3779 type: String,
3780 value: null
3781 },
3782 items: {
3783 type: Array,
3784 readOnly: true,
3785 notify: true,
3786 value: function() {
3787 return [];
3788 }
3789 },
3790 _excludedLocalNames: {
3791 type: Object,
3792 value: function() {
3793 return {
3794 template: 1
3795 };
3796 }
3797 }
3798 },
3799 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec ted)', '_checkFallback(fallbackSelection)' ],
3800 created: function() {
3801 this._bindFilterItem = this._filterItem.bind(this);
3802 this._selection = new Polymer.IronSelection(this._applySelection.bind(this)) ;
3803 },
3804 attached: function() {
3805 this._observer = this._observeItems(this);
3806 this._updateItems();
3807 if (!this._shouldUpdateSelection) {
3808 this._updateSelected();
3809 }
3810 this._addListener(this.activateEvent);
3811 },
3812 detached: function() {
3813 if (this._observer) {
3814 Polymer.dom(this).unobserveNodes(this._observer);
3815 }
3816 this._removeListener(this.activateEvent);
3817 },
3818 indexOf: function(item) {
3819 return this.items.indexOf(item);
3820 },
3821 select: function(value) {
3822 this.selected = value;
3823 },
3824 selectPrevious: function() {
3825 var length = this.items.length;
3826 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt h;
3827 this.selected = this._indexToValue(index);
3828 },
3829 selectNext: function() {
3830 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len gth;
3831 this.selected = this._indexToValue(index);
3832 },
3833 selectIndex: function(index) {
3834 this.select(this._indexToValue(index));
3835 },
3836 forceSynchronousItemUpdate: function() {
3837 this._updateItems();
3838 },
3839 get _shouldUpdateSelection() {
3840 return this.selected != null;
3841 },
3842 _checkFallback: function() {
3843 if (this._shouldUpdateSelection) {
3844 this._updateSelected();
3845 }
3846 },
3847 _addListener: function(eventName) {
3848 this.listen(this, eventName, '_activateHandler');
3849 },
3850 _removeListener: function(eventName) {
3851 this.unlisten(this, eventName, '_activateHandler');
3852 },
3853 _activateEventChanged: function(eventName, old) {
3854 this._removeListener(old);
3855 this._addListener(eventName);
3856 },
3857 _updateItems: function() {
3858 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '* ');
3859 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
3860 this._setItems(nodes);
3861 },
3862 _updateAttrForSelected: function() {
3863 if (this._shouldUpdateSelection) {
3864 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3865 }
3866 },
3867 _updateSelected: function() {
3868 this._selectSelected(this.selected);
3869 },
3870 _selectSelected: function(selected) {
3871 this._selection.select(this._valueToItem(this.selected));
3872 if (this.fallbackSelection && this.items.length && this._selection.get() === undefined) {
3873 this.selected = this.fallbackSelection;
3874 }
3875 },
3876 _filterItem: function(node) {
3877 return !this._excludedLocalNames[node.localName];
3878 },
3879 _valueToItem: function(value) {
3880 return value == null ? null : this.items[this._valueToIndex(value)];
3881 },
3882 _valueToIndex: function(value) {
3883 if (this.attrForSelected) {
3884 for (var i = 0, item; item = this.items[i]; i++) {
3885 if (this._valueForItem(item) == value) {
3886 return i;
6869 } 3887 }
6870 } 3888 }
6871 }, 3889 } else {
6872 3890 return Number(value);
6873 observers: [ 3891 }
6874 '_updateAttrForSelected(attrForSelected)', 3892 },
6875 '_updateSelected(selected)', 3893 _indexToValue: function(index) {
6876 '_checkFallback(fallbackSelection)' 3894 if (this.attrForSelected) {
6877 ], 3895 var item = this.items[index];
6878 3896 if (item) {
6879 created: function() { 3897 return this._valueForItem(item);
6880 this._bindFilterItem = this._filterItem.bind(this); 3898 }
6881 this._selection = new Polymer.IronSelection(this._applySelection.bind(this )); 3899 } else {
6882 }, 3900 return index;
6883 3901 }
6884 attached: function() { 3902 },
6885 this._observer = this._observeItems(this); 3903 _valueForItem: function(item) {
3904 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
3905 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe lected);
3906 },
3907 _applySelection: function(item, isSelected) {
3908 if (this.selectedClass) {
3909 this.toggleClass(this.selectedClass, isSelected, item);
3910 }
3911 if (this.selectedAttribute) {
3912 this.toggleAttribute(this.selectedAttribute, isSelected, item);
3913 }
3914 this._selectionChange();
3915 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {
3916 item: item
3917 });
3918 },
3919 _selectionChange: function() {
3920 this._setSelectedItem(this._selection.get());
3921 },
3922 _observeItems: function(node) {
3923 return Polymer.dom(node).observeNodes(function(mutation) {
6886 this._updateItems(); 3924 this._updateItems();
6887 if (!this._shouldUpdateSelection) {
6888 this._updateSelected();
6889 }
6890 this._addListener(this.activateEvent);
6891 },
6892
6893 detached: function() {
6894 if (this._observer) {
6895 Polymer.dom(this).unobserveNodes(this._observer);
6896 }
6897 this._removeListener(this.activateEvent);
6898 },
6899
6900 /**
6901 * Returns the index of the given item.
6902 *
6903 * @method indexOf
6904 * @param {Object} item
6905 * @returns Returns the index of the item
6906 */
6907 indexOf: function(item) {
6908 return this.items.indexOf(item);
6909 },
6910
6911 /**
6912 * Selects the given value.
6913 *
6914 * @method select
6915 * @param {string|number} value the value to select.
6916 */
6917 select: function(value) {
6918 this.selected = value;
6919 },
6920
6921 /**
6922 * Selects the previous item.
6923 *
6924 * @method selectPrevious
6925 */
6926 selectPrevious: function() {
6927 var length = this.items.length;
6928 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
6929 this.selected = this._indexToValue(index);
6930 },
6931
6932 /**
6933 * Selects the next item.
6934 *
6935 * @method selectNext
6936 */
6937 selectNext: function() {
6938 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
6939 this.selected = this._indexToValue(index);
6940 },
6941
6942 /**
6943 * Selects the item at the given index.
6944 *
6945 * @method selectIndex
6946 */
6947 selectIndex: function(index) {
6948 this.select(this._indexToValue(index));
6949 },
6950
6951 /**
6952 * Force a synchronous update of the `items` property.
6953 *
6954 * NOTE: Consider listening for the `iron-items-changed` event to respond to
6955 * updates to the set of selectable items after updates to the DOM list and
6956 * selection state have been made.
6957 *
6958 * WARNING: If you are using this method, you should probably consider an
6959 * alternate approach. Synchronously querying for items is potentially
6960 * slow for many use cases. The `items` property will update asynchronously
6961 * on its own to reflect selectable items in the DOM.
6962 */
6963 forceSynchronousItemUpdate: function() {
6964 this._updateItems();
6965 },
6966
6967 get _shouldUpdateSelection() {
6968 return this.selected != null;
6969 },
6970
6971 _checkFallback: function() {
6972 if (this._shouldUpdateSelection) { 3925 if (this._shouldUpdateSelection) {
6973 this._updateSelected(); 3926 this._updateSelected();
6974 } 3927 }
6975 }, 3928 this.fire('iron-items-changed', mutation, {
6976 3929 bubbles: false,
6977 _addListener: function(eventName) { 3930 cancelable: false
6978 this.listen(this, eventName, '_activateHandler'); 3931 });
6979 }, 3932 });
6980 3933 },
6981 _removeListener: function(eventName) { 3934 _activateHandler: function(e) {
6982 this.unlisten(this, eventName, '_activateHandler'); 3935 var t = e.target;
6983 }, 3936 var items = this.items;
6984 3937 while (t && t != this) {
6985 _activateEventChanged: function(eventName, old) { 3938 var i = items.indexOf(t);
6986 this._removeListener(old); 3939 if (i >= 0) {
6987 this._addListener(eventName); 3940 var value = this._indexToValue(i);
6988 }, 3941 this._itemActivate(value, t);
6989 3942 return;
6990 _updateItems: function() { 3943 }
6991 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*'); 3944 t = t.parentNode;
6992 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); 3945 }
6993 this._setItems(nodes); 3946 },
6994 }, 3947 _itemActivate: function(value, item) {
6995 3948 if (!this.fire('iron-activate', {
6996 _updateAttrForSelected: function() { 3949 selected: value,
6997 if (this._shouldUpdateSelection) { 3950 item: item
6998 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); 3951 }, {
6999 } 3952 cancelable: true
7000 }, 3953 }).defaultPrevented) {
7001 3954 this.select(value);
7002 _updateSelected: function() { 3955 }
3956 }
3957 };
3958
3959 Polymer.IronMultiSelectableBehaviorImpl = {
3960 properties: {
3961 multi: {
3962 type: Boolean,
3963 value: false,
3964 observer: 'multiChanged'
3965 },
3966 selectedValues: {
3967 type: Array,
3968 notify: true
3969 },
3970 selectedItems: {
3971 type: Array,
3972 readOnly: true,
3973 notify: true
3974 }
3975 },
3976 observers: [ '_updateSelected(selectedValues.splices)' ],
3977 select: function(value) {
3978 if (this.multi) {
3979 if (this.selectedValues) {
3980 this._toggleSelected(value);
3981 } else {
3982 this.selectedValues = [ value ];
3983 }
3984 } else {
3985 this.selected = value;
3986 }
3987 },
3988 multiChanged: function(multi) {
3989 this._selection.multi = multi;
3990 },
3991 get _shouldUpdateSelection() {
3992 return this.selected != null || this.selectedValues != null && this.selected Values.length;
3993 },
3994 _updateAttrForSelected: function() {
3995 if (!this.multi) {
3996 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
3997 } else if (this._shouldUpdateSelection) {
3998 this.selectedValues = this.selectedItems.map(function(selectedItem) {
3999 return this._indexToValue(this.indexOf(selectedItem));
4000 }, this).filter(function(unfilteredValue) {
4001 return unfilteredValue != null;
4002 }, this);
4003 }
4004 },
4005 _updateSelected: function() {
4006 if (this.multi) {
4007 this._selectMulti(this.selectedValues);
4008 } else {
7003 this._selectSelected(this.selected); 4009 this._selectSelected(this.selected);
7004 }, 4010 }
7005 4011 },
7006 _selectSelected: function(selected) { 4012 _selectMulti: function(values) {
7007 this._selection.select(this._valueToItem(this.selected)); 4013 if (values) {
7008 // Check for items, since this array is populated only when attached 4014 var selectedItems = this._valuesToItems(values);
7009 // Since Number(0) is falsy, explicitly check for undefined 4015 this._selection.clear(selectedItems);
7010 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) { 4016 for (var i = 0; i < selectedItems.length; i++) {
7011 this.selected = this.fallbackSelection; 4017 this._selection.setItemSelected(selectedItems[i], true);
7012 } 4018 }
7013 }, 4019 if (this.fallbackSelection && this.items.length && !this._selection.get(). length) {
7014 4020 var fallback = this._valueToItem(this.fallbackSelection);
7015 _filterItem: function(node) { 4021 if (fallback) {
7016 return !this._excludedLocalNames[node.localName]; 4022 this.selectedValues = [ this.fallbackSelection ];
7017 },
7018
7019 _valueToItem: function(value) {
7020 return (value == null) ? null : this.items[this._valueToIndex(value)];
7021 },
7022
7023 _valueToIndex: function(value) {
7024 if (this.attrForSelected) {
7025 for (var i = 0, item; item = this.items[i]; i++) {
7026 if (this._valueForItem(item) == value) {
7027 return i;
7028 }
7029 } 4023 }
4024 }
4025 } else {
4026 this._selection.clear();
4027 }
4028 },
4029 _selectionChange: function() {
4030 var s = this._selection.get();
4031 if (this.multi) {
4032 this._setSelectedItems(s);
4033 } else {
4034 this._setSelectedItems([ s ]);
4035 this._setSelectedItem(s);
4036 }
4037 },
4038 _toggleSelected: function(value) {
4039 var i = this.selectedValues.indexOf(value);
4040 var unselected = i < 0;
4041 if (unselected) {
4042 this.push('selectedValues', value);
4043 } else {
4044 this.splice('selectedValues', i, 1);
4045 }
4046 },
4047 _valuesToItems: function(values) {
4048 return values == null ? null : values.map(function(value) {
4049 return this._valueToItem(value);
4050 }, this);
4051 }
4052 };
4053
4054 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer. IronMultiSelectableBehaviorImpl ];
4055
4056 Polymer.IronMenuBehaviorImpl = {
4057 properties: {
4058 focusedItem: {
4059 observer: '_focusedItemChanged',
4060 readOnly: true,
4061 type: Object
4062 },
4063 attrForItemTitle: {
4064 type: String
4065 }
4066 },
4067 hostAttributes: {
4068 role: 'menu',
4069 tabindex: '0'
4070 },
4071 observers: [ '_updateMultiselectable(multi)' ],
4072 listeners: {
4073 focus: '_onFocus',
4074 keydown: '_onKeydown',
4075 'iron-items-changed': '_onIronItemsChanged'
4076 },
4077 keyBindings: {
4078 up: '_onUpKey',
4079 down: '_onDownKey',
4080 esc: '_onEscKey',
4081 'shift+tab:keydown': '_onShiftTabDown'
4082 },
4083 attached: function() {
4084 this._resetTabindices();
4085 },
4086 select: function(value) {
4087 if (this._defaultFocusAsync) {
4088 this.cancelAsync(this._defaultFocusAsync);
4089 this._defaultFocusAsync = null;
4090 }
4091 var item = this._valueToItem(value);
4092 if (item && item.hasAttribute('disabled')) return;
4093 this._setFocusedItem(item);
4094 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
4095 },
4096 _resetTabindices: function() {
4097 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0] : this.selectedItem;
4098 this.items.forEach(function(item) {
4099 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
4100 }, this);
4101 },
4102 _updateMultiselectable: function(multi) {
4103 if (multi) {
4104 this.setAttribute('aria-multiselectable', 'true');
4105 } else {
4106 this.removeAttribute('aria-multiselectable');
4107 }
4108 },
4109 _focusWithKeyboardEvent: function(event) {
4110 for (var i = 0, item; item = this.items[i]; i++) {
4111 var attr = this.attrForItemTitle || 'textContent';
4112 var title = item[attr] || item.getAttribute(attr);
4113 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo werCase() === String.fromCharCode(event.keyCode).toLowerCase()) {
4114 this._setFocusedItem(item);
4115 break;
4116 }
4117 }
4118 },
4119 _focusPrevious: function() {
4120 var length = this.items.length;
4121 var curFocusIndex = Number(this.indexOf(this.focusedItem));
4122 for (var i = 1; i < length + 1; i++) {
4123 var item = this.items[(curFocusIndex - i + length) % length];
4124 if (!item.hasAttribute('disabled')) {
4125 this._setFocusedItem(item);
4126 return;
4127 }
4128 }
4129 },
4130 _focusNext: function() {
4131 var length = this.items.length;
4132 var curFocusIndex = Number(this.indexOf(this.focusedItem));
4133 for (var i = 1; i < length + 1; i++) {
4134 var item = this.items[(curFocusIndex + i) % length];
4135 if (!item.hasAttribute('disabled')) {
4136 this._setFocusedItem(item);
4137 return;
4138 }
4139 }
4140 },
4141 _applySelection: function(item, isSelected) {
4142 if (isSelected) {
4143 item.setAttribute('aria-selected', 'true');
4144 } else {
4145 item.removeAttribute('aria-selected');
4146 }
4147 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
4148 },
4149 _focusedItemChanged: function(focusedItem, old) {
4150 old && old.setAttribute('tabindex', '-1');
4151 if (focusedItem) {
4152 focusedItem.setAttribute('tabindex', '0');
4153 focusedItem.focus();
4154 }
4155 },
4156 _onIronItemsChanged: function(event) {
4157 if (event.detail.addedNodes.length) {
4158 this._resetTabindices();
4159 }
4160 },
4161 _onShiftTabDown: function(event) {
4162 var oldTabIndex = this.getAttribute('tabindex');
4163 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
4164 this._setFocusedItem(null);
4165 this.setAttribute('tabindex', '-1');
4166 this.async(function() {
4167 this.setAttribute('tabindex', oldTabIndex);
4168 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
4169 }, 1);
4170 },
4171 _onFocus: function(event) {
4172 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
4173 return;
4174 }
4175 var rootTarget = Polymer.dom(event).rootTarget;
4176 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th is.isLightDescendant(rootTarget)) {
4177 return;
4178 }
4179 this._defaultFocusAsync = this.async(function() {
4180 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0 ] : this.selectedItem;
4181 this._setFocusedItem(null);
4182 if (selectedItem) {
4183 this._setFocusedItem(selectedItem);
4184 } else if (this.items[0]) {
4185 this._focusNext();
4186 }
4187 });
4188 },
4189 _onUpKey: function(event) {
4190 this._focusPrevious();
4191 event.detail.keyboardEvent.preventDefault();
4192 },
4193 _onDownKey: function(event) {
4194 this._focusNext();
4195 event.detail.keyboardEvent.preventDefault();
4196 },
4197 _onEscKey: function(event) {
4198 this.focusedItem.blur();
4199 },
4200 _onKeydown: function(event) {
4201 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
4202 this._focusWithKeyboardEvent(event);
4203 }
4204 event.stopPropagation();
4205 },
4206 _activateHandler: function(event) {
4207 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
4208 event.stopPropagation();
4209 }
4210 };
4211
4212 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
4213
4214 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1 1yKeysBehavior, Polymer.IronMenuBehaviorImpl ];
4215
4216 (function() {
4217 Polymer({
4218 is: 'paper-menu',
4219 behaviors: [ Polymer.IronMenuBehavior ]
4220 });
4221 })();
4222
4223 Polymer.IronFitBehavior = {
4224 properties: {
4225 sizingTarget: {
4226 type: Object,
4227 value: function() {
4228 return this;
4229 }
4230 },
4231 fitInto: {
4232 type: Object,
4233 value: window
4234 },
4235 noOverlap: {
4236 type: Boolean
4237 },
4238 positionTarget: {
4239 type: Element
4240 },
4241 horizontalAlign: {
4242 type: String
4243 },
4244 verticalAlign: {
4245 type: String
4246 },
4247 dynamicAlign: {
4248 type: Boolean
4249 },
4250 horizontalOffset: {
4251 type: Number,
4252 value: 0,
4253 notify: true
4254 },
4255 verticalOffset: {
4256 type: Number,
4257 value: 0,
4258 notify: true
4259 },
4260 autoFitOnAttach: {
4261 type: Boolean,
4262 value: false
4263 },
4264 _fitInfo: {
4265 type: Object
4266 }
4267 },
4268 get _fitWidth() {
4269 var fitWidth;
4270 if (this.fitInto === window) {
4271 fitWidth = this.fitInto.innerWidth;
4272 } else {
4273 fitWidth = this.fitInto.getBoundingClientRect().width;
4274 }
4275 return fitWidth;
4276 },
4277 get _fitHeight() {
4278 var fitHeight;
4279 if (this.fitInto === window) {
4280 fitHeight = this.fitInto.innerHeight;
4281 } else {
4282 fitHeight = this.fitInto.getBoundingClientRect().height;
4283 }
4284 return fitHeight;
4285 },
4286 get _fitLeft() {
4287 var fitLeft;
4288 if (this.fitInto === window) {
4289 fitLeft = 0;
4290 } else {
4291 fitLeft = this.fitInto.getBoundingClientRect().left;
4292 }
4293 return fitLeft;
4294 },
4295 get _fitTop() {
4296 var fitTop;
4297 if (this.fitInto === window) {
4298 fitTop = 0;
4299 } else {
4300 fitTop = this.fitInto.getBoundingClientRect().top;
4301 }
4302 return fitTop;
4303 },
4304 get _defaultPositionTarget() {
4305 var parent = Polymer.dom(this).parentNode;
4306 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
4307 parent = parent.host;
4308 }
4309 return parent;
4310 },
4311 get _localeHorizontalAlign() {
4312 if (this._isRTL) {
4313 if (this.horizontalAlign === 'right') {
4314 return 'left';
4315 }
4316 if (this.horizontalAlign === 'left') {
4317 return 'right';
4318 }
4319 }
4320 return this.horizontalAlign;
4321 },
4322 attached: function() {
4323 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
4324 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
4325 if (this.autoFitOnAttach) {
4326 if (window.getComputedStyle(this).display === 'none') {
4327 setTimeout(function() {
4328 this.fit();
4329 }.bind(this));
7030 } else { 4330 } else {
7031 return Number(value); 4331 this.fit();
7032 } 4332 }
7033 }, 4333 }
7034 4334 },
7035 _indexToValue: function(index) { 4335 fit: function() {
7036 if (this.attrForSelected) { 4336 this.position();
7037 var item = this.items[index]; 4337 this.constrain();
7038 if (item) { 4338 this.center();
7039 return this._valueForItem(item); 4339 },
4340 _discoverInfo: function() {
4341 if (this._fitInfo) {
4342 return;
4343 }
4344 var target = window.getComputedStyle(this);
4345 var sizer = window.getComputedStyle(this.sizingTarget);
4346 this._fitInfo = {
4347 inlineStyle: {
4348 top: this.style.top || '',
4349 left: this.style.left || '',
4350 position: this.style.position || ''
4351 },
4352 sizerInlineStyle: {
4353 maxWidth: this.sizingTarget.style.maxWidth || '',
4354 maxHeight: this.sizingTarget.style.maxHeight || '',
4355 boxSizing: this.sizingTarget.style.boxSizing || ''
4356 },
4357 positionedBy: {
4358 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? ' bottom' : null,
4359 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto' ? 'right' : null
4360 },
4361 sizedBy: {
4362 height: sizer.maxHeight !== 'none',
4363 width: sizer.maxWidth !== 'none',
4364 minWidth: parseInt(sizer.minWidth, 10) || 0,
4365 minHeight: parseInt(sizer.minHeight, 10) || 0
4366 },
4367 margin: {
4368 top: parseInt(target.marginTop, 10) || 0,
4369 right: parseInt(target.marginRight, 10) || 0,
4370 bottom: parseInt(target.marginBottom, 10) || 0,
4371 left: parseInt(target.marginLeft, 10) || 0
4372 }
4373 };
4374 if (this.verticalOffset) {
4375 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs et;
4376 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
4377 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
4378 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px ';
4379 }
4380 if (this.horizontalOffset) {
4381 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf fset;
4382 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
4383 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
4384 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + ' px';
4385 }
4386 },
4387 resetFit: function() {
4388 var info = this._fitInfo || {};
4389 for (var property in info.sizerInlineStyle) {
4390 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
4391 }
4392 for (var property in info.inlineStyle) {
4393 this.style[property] = info.inlineStyle[property];
4394 }
4395 this._fitInfo = null;
4396 },
4397 refit: function() {
4398 var scrollLeft = this.sizingTarget.scrollLeft;
4399 var scrollTop = this.sizingTarget.scrollTop;
4400 this.resetFit();
4401 this.fit();
4402 this.sizingTarget.scrollLeft = scrollLeft;
4403 this.sizingTarget.scrollTop = scrollTop;
4404 },
4405 position: function() {
4406 if (!this.horizontalAlign && !this.verticalAlign) {
4407 return;
4408 }
4409 this._discoverInfo();
4410 this.style.position = 'fixed';
4411 this.sizingTarget.style.boxSizing = 'border-box';
4412 this.style.left = '0px';
4413 this.style.top = '0px';
4414 var rect = this.getBoundingClientRect();
4415 var positionRect = this.__getNormalizedRect(this.positionTarget);
4416 var fitRect = this.__getNormalizedRect(this.fitInto);
4417 var margin = this._fitInfo.margin;
4418 var size = {
4419 width: rect.width + margin.left + margin.right,
4420 height: rect.height + margin.top + margin.bottom
4421 };
4422 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical Align, size, positionRect, fitRect);
4423 var left = position.left + margin.left;
4424 var top = position.top + margin.top;
4425 var right = Math.min(fitRect.right - margin.right, left + rect.width);
4426 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
4427 var minWidth = this._fitInfo.sizedBy.minWidth;
4428 var minHeight = this._fitInfo.sizedBy.minHeight;
4429 if (left < margin.left) {
4430 left = margin.left;
4431 if (right - left < minWidth) {
4432 left = right - minWidth;
4433 }
4434 }
4435 if (top < margin.top) {
4436 top = margin.top;
4437 if (bottom - top < minHeight) {
4438 top = bottom - minHeight;
4439 }
4440 }
4441 this.sizingTarget.style.maxWidth = right - left + 'px';
4442 this.sizingTarget.style.maxHeight = bottom - top + 'px';
4443 this.style.left = left - rect.left + 'px';
4444 this.style.top = top - rect.top + 'px';
4445 },
4446 constrain: function() {
4447 if (this.horizontalAlign || this.verticalAlign) {
4448 return;
4449 }
4450 this._discoverInfo();
4451 var info = this._fitInfo;
4452 if (!info.positionedBy.vertically) {
4453 this.style.position = 'fixed';
4454 this.style.top = '0px';
4455 }
4456 if (!info.positionedBy.horizontally) {
4457 this.style.position = 'fixed';
4458 this.style.left = '0px';
4459 }
4460 this.sizingTarget.style.boxSizing = 'border-box';
4461 var rect = this.getBoundingClientRect();
4462 if (!info.sizedBy.height) {
4463 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
4464 }
4465 if (!info.sizedBy.width) {
4466 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right' , 'Width');
4467 }
4468 },
4469 _sizeDimension: function(rect, positionedBy, start, end, extent) {
4470 this.__sizeDimension(rect, positionedBy, start, end, extent);
4471 },
4472 __sizeDimension: function(rect, positionedBy, start, end, extent) {
4473 var info = this._fitInfo;
4474 var fitRect = this.__getNormalizedRect(this.fitInto);
4475 var max = extent === 'Width' ? fitRect.width : fitRect.height;
4476 var flip = positionedBy === end;
4477 var offset = flip ? max - rect[end] : rect[start];
4478 var margin = info.margin[flip ? start : end];
4479 var offsetExtent = 'offset' + extent;
4480 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
4481 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs et + 'px';
4482 },
4483 center: function() {
4484 if (this.horizontalAlign || this.verticalAlign) {
4485 return;
4486 }
4487 this._discoverInfo();
4488 var positionedBy = this._fitInfo.positionedBy;
4489 if (positionedBy.vertically && positionedBy.horizontally) {
4490 return;
4491 }
4492 this.style.position = 'fixed';
4493 if (!positionedBy.vertically) {
4494 this.style.top = '0px';
4495 }
4496 if (!positionedBy.horizontally) {
4497 this.style.left = '0px';
4498 }
4499 var rect = this.getBoundingClientRect();
4500 var fitRect = this.__getNormalizedRect(this.fitInto);
4501 if (!positionedBy.vertically) {
4502 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
4503 this.style.top = top + 'px';
4504 }
4505 if (!positionedBy.horizontally) {
4506 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
4507 this.style.left = left + 'px';
4508 }
4509 },
4510 __getNormalizedRect: function(target) {
4511 if (target === document.documentElement || target === window) {
4512 return {
4513 top: 0,
4514 left: 0,
4515 width: window.innerWidth,
4516 height: window.innerHeight,
4517 right: window.innerWidth,
4518 bottom: window.innerHeight
4519 };
4520 }
4521 return target.getBoundingClientRect();
4522 },
4523 __getCroppedArea: function(position, size, fitRect) {
4524 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
4525 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
4526 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size .height;
4527 },
4528 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
4529 var positions = [ {
4530 verticalAlign: 'top',
4531 horizontalAlign: 'left',
4532 top: positionRect.top,
4533 left: positionRect.left
4534 }, {
4535 verticalAlign: 'top',
4536 horizontalAlign: 'right',
4537 top: positionRect.top,
4538 left: positionRect.right - size.width
4539 }, {
4540 verticalAlign: 'bottom',
4541 horizontalAlign: 'left',
4542 top: positionRect.bottom - size.height,
4543 left: positionRect.left
4544 }, {
4545 verticalAlign: 'bottom',
4546 horizontalAlign: 'right',
4547 top: positionRect.bottom - size.height,
4548 left: positionRect.right - size.width
4549 } ];
4550 if (this.noOverlap) {
4551 for (var i = 0, l = positions.length; i < l; i++) {
4552 var copy = {};
4553 for (var key in positions[i]) {
4554 copy[key] = positions[i][key];
7040 } 4555 }
7041 } else { 4556 positions.push(copy);
7042 return index; 4557 }
7043 } 4558 positions[0].top = positions[1].top += positionRect.height;
7044 }, 4559 positions[2].top = positions[3].top -= positionRect.height;
7045 4560 positions[4].left = positions[6].left += positionRect.width;
7046 _valueForItem: function(item) { 4561 positions[5].left = positions[7].left -= positionRect.width;
7047 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ]; 4562 }
7048 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected); 4563 vAlign = vAlign === 'auto' ? null : vAlign;
7049 }, 4564 hAlign = hAlign === 'auto' ? null : hAlign;
7050 4565 var position;
7051 _applySelection: function(item, isSelected) { 4566 for (var i = 0; i < positions.length; i++) {
7052 if (this.selectedClass) { 4567 var pos = positions[i];
7053 this.toggleClass(this.selectedClass, isSelected, item); 4568 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
7054 } 4569 position = pos;
7055 if (this.selectedAttribute) { 4570 break;
7056 this.toggleAttribute(this.selectedAttribute, isSelected, item); 4571 }
7057 } 4572 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos .horizontalAlign === hAlign);
7058 this._selectionChange(); 4573 if (!this.dynamicAlign && !alignOk) {
7059 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); 4574 continue;
7060 }, 4575 }
7061 4576 position = position || pos;
7062 _selectionChange: function() { 4577 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
7063 this._setSelectedItem(this._selection.get()); 4578 var diff = pos.croppedArea - position.croppedArea;
7064 }, 4579 if (diff < 0 || diff === 0 && alignOk) {
7065 4580 position = pos;
7066 // observe items change under the given node. 4581 }
7067 _observeItems: function(node) { 4582 if (position.croppedArea === 0 && alignOk) {
7068 return Polymer.dom(node).observeNodes(function(mutation) { 4583 break;
7069 this._updateItems(); 4584 }
7070 4585 }
7071 if (this._shouldUpdateSelection) { 4586 return position;
7072 this._updateSelected(); 4587 }
7073 } 4588 };
7074 4589
7075 // Let other interested parties know about the change so that 4590 (function() {
7076 // we don't have to recreate mutation observers everywhere. 4591 'use strict';
7077 this.fire('iron-items-changed', mutation, { 4592 Polymer({
7078 bubbles: false, 4593 is: 'iron-overlay-backdrop',
7079 cancelable: false
7080 });
7081 });
7082 },
7083
7084 _activateHandler: function(e) {
7085 var t = e.target;
7086 var items = this.items;
7087 while (t && t != this) {
7088 var i = items.indexOf(t);
7089 if (i >= 0) {
7090 var value = this._indexToValue(i);
7091 this._itemActivate(value, t);
7092 return;
7093 }
7094 t = t.parentNode;
7095 }
7096 },
7097
7098 _itemActivate: function(value, item) {
7099 if (!this.fire('iron-activate',
7100 {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
7101 this.select(value);
7102 }
7103 }
7104
7105 };
7106 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
7107 Polymer.IronMultiSelectableBehaviorImpl = {
7108 properties: { 4594 properties: {
7109
7110 /**
7111 * If true, multiple selections are allowed.
7112 */
7113 multi: {
7114 type: Boolean,
7115 value: false,
7116 observer: 'multiChanged'
7117 },
7118
7119 /**
7120 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
7121 * is true.
7122 */
7123 selectedValues: {
7124 type: Array,
7125 notify: true
7126 },
7127
7128 /**
7129 * Returns an array of currently selected items.
7130 */
7131 selectedItems: {
7132 type: Array,
7133 readOnly: true,
7134 notify: true
7135 },
7136
7137 },
7138
7139 observers: [
7140 '_updateSelected(selectedValues.splices)'
7141 ],
7142
7143 /**
7144 * Selects the given value. If the `multi` property is true, then the select ed state of the
7145 * `value` will be toggled; otherwise the `value` will be selected.
7146 *
7147 * @method select
7148 * @param {string|number} value the value to select.
7149 */
7150 select: function(value) {
7151 if (this.multi) {
7152 if (this.selectedValues) {
7153 this._toggleSelected(value);
7154 } else {
7155 this.selectedValues = [value];
7156 }
7157 } else {
7158 this.selected = value;
7159 }
7160 },
7161
7162 multiChanged: function(multi) {
7163 this._selection.multi = multi;
7164 },
7165
7166 get _shouldUpdateSelection() {
7167 return this.selected != null ||
7168 (this.selectedValues != null && this.selectedValues.length);
7169 },
7170
7171 _updateAttrForSelected: function() {
7172 if (!this.multi) {
7173 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
7174 } else if (this._shouldUpdateSelection) {
7175 this.selectedValues = this.selectedItems.map(function(selectedItem) {
7176 return this._indexToValue(this.indexOf(selectedItem));
7177 }, this).filter(function(unfilteredValue) {
7178 return unfilteredValue != null;
7179 }, this);
7180 }
7181 },
7182
7183 _updateSelected: function() {
7184 if (this.multi) {
7185 this._selectMulti(this.selectedValues);
7186 } else {
7187 this._selectSelected(this.selected);
7188 }
7189 },
7190
7191 _selectMulti: function(values) {
7192 if (values) {
7193 var selectedItems = this._valuesToItems(values);
7194 // clear all but the current selected items
7195 this._selection.clear(selectedItems);
7196 // select only those not selected yet
7197 for (var i = 0; i < selectedItems.length; i++) {
7198 this._selection.setItemSelected(selectedItems[i], true);
7199 }
7200 // Check for items, since this array is populated only when attached
7201 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
7202 var fallback = this._valueToItem(this.fallbackSelection);
7203 if (fallback) {
7204 this.selectedValues = [this.fallbackSelection];
7205 }
7206 }
7207 } else {
7208 this._selection.clear();
7209 }
7210 },
7211
7212 _selectionChange: function() {
7213 var s = this._selection.get();
7214 if (this.multi) {
7215 this._setSelectedItems(s);
7216 } else {
7217 this._setSelectedItems([s]);
7218 this._setSelectedItem(s);
7219 }
7220 },
7221
7222 _toggleSelected: function(value) {
7223 var i = this.selectedValues.indexOf(value);
7224 var unselected = i < 0;
7225 if (unselected) {
7226 this.push('selectedValues',value);
7227 } else {
7228 this.splice('selectedValues',i,1);
7229 }
7230 },
7231
7232 _valuesToItems: function(values) {
7233 return (values == null) ? null : values.map(function(value) {
7234 return this._valueToItem(value);
7235 }, this);
7236 }
7237 };
7238
7239 /** @polymerBehavior */
7240 Polymer.IronMultiSelectableBehavior = [
7241 Polymer.IronSelectableBehavior,
7242 Polymer.IronMultiSelectableBehaviorImpl
7243 ];
7244 /**
7245 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
7246 *
7247 * @demo demo/index.html
7248 * @polymerBehavior Polymer.IronMenuBehavior
7249 */
7250 Polymer.IronMenuBehaviorImpl = {
7251
7252 properties: {
7253
7254 /**
7255 * Returns the currently focused item.
7256 * @type {?Object}
7257 */
7258 focusedItem: {
7259 observer: '_focusedItemChanged',
7260 readOnly: true,
7261 type: Object
7262 },
7263
7264 /**
7265 * The attribute to use on menu items to look up the item title. Typing th e first
7266 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
7267 * will be used.
7268 */
7269 attrForItemTitle: {
7270 type: String
7271 }
7272 },
7273
7274 hostAttributes: {
7275 'role': 'menu',
7276 'tabindex': '0'
7277 },
7278
7279 observers: [
7280 '_updateMultiselectable(multi)'
7281 ],
7282
7283 listeners: {
7284 'focus': '_onFocus',
7285 'keydown': '_onKeydown',
7286 'iron-items-changed': '_onIronItemsChanged'
7287 },
7288
7289 keyBindings: {
7290 'up': '_onUpKey',
7291 'down': '_onDownKey',
7292 'esc': '_onEscKey',
7293 'shift+tab:keydown': '_onShiftTabDown'
7294 },
7295
7296 attached: function() {
7297 this._resetTabindices();
7298 },
7299
7300 /**
7301 * Selects the given value. If the `multi` property is true, then the select ed state of the
7302 * `value` will be toggled; otherwise the `value` will be selected.
7303 *
7304 * @param {string|number} value the value to select.
7305 */
7306 select: function(value) {
7307 // Cancel automatically focusing a default item if the menu received focus
7308 // through a user action selecting a particular item.
7309 if (this._defaultFocusAsync) {
7310 this.cancelAsync(this._defaultFocusAsync);
7311 this._defaultFocusAsync = null;
7312 }
7313 var item = this._valueToItem(value);
7314 if (item && item.hasAttribute('disabled')) return;
7315 this._setFocusedItem(item);
7316 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
7317 },
7318
7319 /**
7320 * Resets all tabindex attributes to the appropriate value based on the
7321 * current selection state. The appropriate value is `0` (focusable) for
7322 * the default selected item, and `-1` (not keyboard focusable) for all
7323 * other items.
7324 */
7325 _resetTabindices: function() {
7326 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem;
7327
7328 this.items.forEach(function(item) {
7329 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
7330 }, this);
7331 },
7332
7333 /**
7334 * Sets appropriate ARIA based on whether or not the menu is meant to be
7335 * multi-selectable.
7336 *
7337 * @param {boolean} multi True if the menu should be multi-selectable.
7338 */
7339 _updateMultiselectable: function(multi) {
7340 if (multi) {
7341 this.setAttribute('aria-multiselectable', 'true');
7342 } else {
7343 this.removeAttribute('aria-multiselectable');
7344 }
7345 },
7346
7347 /**
7348 * Given a KeyboardEvent, this method will focus the appropriate item in the
7349 * menu (if there is a relevant item, and it is possible to focus it).
7350 *
7351 * @param {KeyboardEvent} event A KeyboardEvent.
7352 */
7353 _focusWithKeyboardEvent: function(event) {
7354 for (var i = 0, item; item = this.items[i]; i++) {
7355 var attr = this.attrForItemTitle || 'textContent';
7356 var title = item[attr] || item.getAttribute(attr);
7357
7358 if (!item.hasAttribute('disabled') && title &&
7359 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) {
7360 this._setFocusedItem(item);
7361 break;
7362 }
7363 }
7364 },
7365
7366 /**
7367 * Focuses the previous item (relative to the currently focused item) in the
7368 * menu, disabled items will be skipped.
7369 * Loop until length + 1 to handle case of single item in menu.
7370 */
7371 _focusPrevious: function() {
7372 var length = this.items.length;
7373 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7374 for (var i = 1; i < length + 1; i++) {
7375 var item = this.items[(curFocusIndex - i + length) % length];
7376 if (!item.hasAttribute('disabled')) {
7377 this._setFocusedItem(item);
7378 return;
7379 }
7380 }
7381 },
7382
7383 /**
7384 * Focuses the next item (relative to the currently focused item) in the
7385 * menu, disabled items will be skipped.
7386 * Loop until length + 1 to handle case of single item in menu.
7387 */
7388 _focusNext: function() {
7389 var length = this.items.length;
7390 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7391 for (var i = 1; i < length + 1; i++) {
7392 var item = this.items[(curFocusIndex + i) % length];
7393 if (!item.hasAttribute('disabled')) {
7394 this._setFocusedItem(item);
7395 return;
7396 }
7397 }
7398 },
7399
7400 /**
7401 * Mutates items in the menu based on provided selection details, so that
7402 * all items correctly reflect selection state.
7403 *
7404 * @param {Element} item An item in the menu.
7405 * @param {boolean} isSelected True if the item should be shown in a
7406 * selected state, otherwise false.
7407 */
7408 _applySelection: function(item, isSelected) {
7409 if (isSelected) {
7410 item.setAttribute('aria-selected', 'true');
7411 } else {
7412 item.removeAttribute('aria-selected');
7413 }
7414 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
7415 },
7416
7417 /**
7418 * Discretely updates tabindex values among menu items as the focused item
7419 * changes.
7420 *
7421 * @param {Element} focusedItem The element that is currently focused.
7422 * @param {?Element} old The last element that was considered focused, if
7423 * applicable.
7424 */
7425 _focusedItemChanged: function(focusedItem, old) {
7426 old && old.setAttribute('tabindex', '-1');
7427 if (focusedItem) {
7428 focusedItem.setAttribute('tabindex', '0');
7429 focusedItem.focus();
7430 }
7431 },
7432
7433 /**
7434 * A handler that responds to mutation changes related to the list of items
7435 * in the menu.
7436 *
7437 * @param {CustomEvent} event An event containing mutation records as its
7438 * detail.
7439 */
7440 _onIronItemsChanged: function(event) {
7441 if (event.detail.addedNodes.length) {
7442 this._resetTabindices();
7443 }
7444 },
7445
7446 /**
7447 * Handler that is called when a shift+tab keypress is detected by the menu.
7448 *
7449 * @param {CustomEvent} event A key combination event.
7450 */
7451 _onShiftTabDown: function(event) {
7452 var oldTabIndex = this.getAttribute('tabindex');
7453
7454 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
7455
7456 this._setFocusedItem(null);
7457
7458 this.setAttribute('tabindex', '-1');
7459
7460 this.async(function() {
7461 this.setAttribute('tabindex', oldTabIndex);
7462 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7463 // NOTE(cdata): polymer/polymer#1305
7464 }, 1);
7465 },
7466
7467 /**
7468 * Handler that is called when the menu receives focus.
7469 *
7470 * @param {FocusEvent} event A focus event.
7471 */
7472 _onFocus: function(event) {
7473 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
7474 // do not focus the menu itself
7475 return;
7476 }
7477
7478 // Do not focus the selected tab if the deepest target is part of the
7479 // menu element's local DOM and is focusable.
7480 var rootTarget = /** @type {?HTMLElement} */(
7481 Polymer.dom(event).rootTarget);
7482 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
7483 return;
7484 }
7485
7486 // clear the cached focus item
7487 this._defaultFocusAsync = this.async(function() {
7488 // focus the selected item when the menu receives focus, or the first it em
7489 // if no item is selected
7490 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
7491
7492 this._setFocusedItem(null);
7493
7494 if (selectedItem) {
7495 this._setFocusedItem(selectedItem);
7496 } else if (this.items[0]) {
7497 // We find the first none-disabled item (if one exists)
7498 this._focusNext();
7499 }
7500 });
7501 },
7502
7503 /**
7504 * Handler that is called when the up key is pressed.
7505 *
7506 * @param {CustomEvent} event A key combination event.
7507 */
7508 _onUpKey: function(event) {
7509 // up and down arrows moves the focus
7510 this._focusPrevious();
7511 event.detail.keyboardEvent.preventDefault();
7512 },
7513
7514 /**
7515 * Handler that is called when the down key is pressed.
7516 *
7517 * @param {CustomEvent} event A key combination event.
7518 */
7519 _onDownKey: function(event) {
7520 this._focusNext();
7521 event.detail.keyboardEvent.preventDefault();
7522 },
7523
7524 /**
7525 * Handler that is called when the esc key is pressed.
7526 *
7527 * @param {CustomEvent} event A key combination event.
7528 */
7529 _onEscKey: function(event) {
7530 // esc blurs the control
7531 this.focusedItem.blur();
7532 },
7533
7534 /**
7535 * Handler that is called when a keydown event is detected.
7536 *
7537 * @param {KeyboardEvent} event A keyboard event.
7538 */
7539 _onKeydown: function(event) {
7540 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
7541 // all other keys focus the menu item starting with that character
7542 this._focusWithKeyboardEvent(event);
7543 }
7544 event.stopPropagation();
7545 },
7546
7547 // override _activateHandler
7548 _activateHandler: function(event) {
7549 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
7550 event.stopPropagation();
7551 }
7552 };
7553
7554 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7555
7556 /** @polymerBehavior Polymer.IronMenuBehavior */
7557 Polymer.IronMenuBehavior = [
7558 Polymer.IronMultiSelectableBehavior,
7559 Polymer.IronA11yKeysBehavior,
7560 Polymer.IronMenuBehaviorImpl
7561 ];
7562 (function() {
7563 Polymer({
7564 is: 'paper-menu',
7565
7566 behaviors: [
7567 Polymer.IronMenuBehavior
7568 ]
7569 });
7570 })();
7571 /**
7572 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
7573 optionally centers it in the window or another element.
7574
7575 The element will only be sized and/or positioned if it has not already been size d and/or positioned
7576 by CSS.
7577
7578 CSS properties | Action
7579 -----------------------------|-------------------------------------------
7580 `position` set | Element is not centered horizontally or verticall y
7581 `top` or `bottom` set | Element is not vertically centered
7582 `left` or `right` set | Element is not horizontally centered
7583 `max-height` set | Element respects `max-height`
7584 `max-width` set | Element respects `max-width`
7585
7586 `Polymer.IronFitBehavior` can position an element into another element using
7587 `verticalAlign` and `horizontalAlign`. This will override the element's css posi tion.
7588
7589 <div class="container">
7590 <iron-fit-impl vertical-align="top" horizontal-align="auto">
7591 Positioned into the container
7592 </iron-fit-impl>
7593 </div>
7594
7595 Use `noOverlap` to position the element around another element without overlappi ng it.
7596
7597 <div class="container">
7598 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
7599 Positioned around the container
7600 </iron-fit-impl>
7601 </div>
7602
7603 @demo demo/index.html
7604 @polymerBehavior
7605 */
7606
7607 Polymer.IronFitBehavior = {
7608
7609 properties: {
7610
7611 /**
7612 * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
7613 * but it can be set to a child element. This is useful, for example, for implementing a
7614 * scrolling region inside the element.
7615 * @type {!Element}
7616 */
7617 sizingTarget: {
7618 type: Object,
7619 value: function() {
7620 return this;
7621 }
7622 },
7623
7624 /**
7625 * The element to fit `this` into.
7626 */
7627 fitInto: {
7628 type: Object,
7629 value: window
7630 },
7631
7632 /**
7633 * Will position the element around the positionTarget without overlapping it.
7634 */
7635 noOverlap: {
7636 type: Boolean
7637 },
7638
7639 /**
7640 * The element that should be used to position the element. If not set, it will
7641 * default to the parent node.
7642 * @type {!Element}
7643 */
7644 positionTarget: {
7645 type: Element
7646 },
7647
7648 /**
7649 * The orientation against which to align the element horizontally
7650 * relative to the `positionTarget`. Possible values are "left", "right", "auto".
7651 */
7652 horizontalAlign: {
7653 type: String
7654 },
7655
7656 /**
7657 * The orientation against which to align the element vertically
7658 * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
7659 */
7660 verticalAlign: {
7661 type: String
7662 },
7663
7664 /**
7665 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr eferred alignment
7666 * and if there's not enough space, it will pick the values which minimize the cropping.
7667 */
7668 dynamicAlign: {
7669 type: Boolean
7670 },
7671
7672 /**
7673 * The same as setting margin-left and margin-right css properties.
7674 * @deprecated
7675 */
7676 horizontalOffset: {
7677 type: Number,
7678 value: 0,
7679 notify: true
7680 },
7681
7682 /**
7683 * The same as setting margin-top and margin-bottom css properties.
7684 * @deprecated
7685 */
7686 verticalOffset: {
7687 type: Number,
7688 value: 0,
7689 notify: true
7690 },
7691
7692 /**
7693 * Set to true to auto-fit on attach.
7694 */
7695 autoFitOnAttach: {
7696 type: Boolean,
7697 value: false
7698 },
7699
7700 /** @type {?Object} */
7701 _fitInfo: {
7702 type: Object
7703 }
7704 },
7705
7706 get _fitWidth() {
7707 var fitWidth;
7708 if (this.fitInto === window) {
7709 fitWidth = this.fitInto.innerWidth;
7710 } else {
7711 fitWidth = this.fitInto.getBoundingClientRect().width;
7712 }
7713 return fitWidth;
7714 },
7715
7716 get _fitHeight() {
7717 var fitHeight;
7718 if (this.fitInto === window) {
7719 fitHeight = this.fitInto.innerHeight;
7720 } else {
7721 fitHeight = this.fitInto.getBoundingClientRect().height;
7722 }
7723 return fitHeight;
7724 },
7725
7726 get _fitLeft() {
7727 var fitLeft;
7728 if (this.fitInto === window) {
7729 fitLeft = 0;
7730 } else {
7731 fitLeft = this.fitInto.getBoundingClientRect().left;
7732 }
7733 return fitLeft;
7734 },
7735
7736 get _fitTop() {
7737 var fitTop;
7738 if (this.fitInto === window) {
7739 fitTop = 0;
7740 } else {
7741 fitTop = this.fitInto.getBoundingClientRect().top;
7742 }
7743 return fitTop;
7744 },
7745
7746 /**
7747 * The element that should be used to position the element,
7748 * if no position target is configured.
7749 */
7750 get _defaultPositionTarget() {
7751 var parent = Polymer.dom(this).parentNode;
7752
7753 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
7754 parent = parent.host;
7755 }
7756
7757 return parent;
7758 },
7759
7760 /**
7761 * The horizontal align value, accounting for the RTL/LTR text direction.
7762 */
7763 get _localeHorizontalAlign() {
7764 if (this._isRTL) {
7765 // In RTL, "left" becomes "right".
7766 if (this.horizontalAlign === 'right') {
7767 return 'left';
7768 }
7769 if (this.horizontalAlign === 'left') {
7770 return 'right';
7771 }
7772 }
7773 return this.horizontalAlign;
7774 },
7775
7776 attached: function() {
7777 // Memoize this to avoid expensive calculations & relayouts.
7778 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
7779 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
7780 if (this.autoFitOnAttach) {
7781 if (window.getComputedStyle(this).display === 'none') {
7782 setTimeout(function() {
7783 this.fit();
7784 }.bind(this));
7785 } else {
7786 this.fit();
7787 }
7788 }
7789 },
7790
7791 /**
7792 * Positions and fits the element into the `fitInto` element.
7793 */
7794 fit: function() {
7795 this.position();
7796 this.constrain();
7797 this.center();
7798 },
7799
7800 /**
7801 * Memoize information needed to position and size the target element.
7802 * @suppress {deprecated}
7803 */
7804 _discoverInfo: function() {
7805 if (this._fitInfo) {
7806 return;
7807 }
7808 var target = window.getComputedStyle(this);
7809 var sizer = window.getComputedStyle(this.sizingTarget);
7810
7811 this._fitInfo = {
7812 inlineStyle: {
7813 top: this.style.top || '',
7814 left: this.style.left || '',
7815 position: this.style.position || ''
7816 },
7817 sizerInlineStyle: {
7818 maxWidth: this.sizingTarget.style.maxWidth || '',
7819 maxHeight: this.sizingTarget.style.maxHeight || '',
7820 boxSizing: this.sizingTarget.style.boxSizing || ''
7821 },
7822 positionedBy: {
7823 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
7824 'bottom' : null),
7825 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut o' ?
7826 'right' : null)
7827 },
7828 sizedBy: {
7829 height: sizer.maxHeight !== 'none',
7830 width: sizer.maxWidth !== 'none',
7831 minWidth: parseInt(sizer.minWidth, 10) || 0,
7832 minHeight: parseInt(sizer.minHeight, 10) || 0
7833 },
7834 margin: {
7835 top: parseInt(target.marginTop, 10) || 0,
7836 right: parseInt(target.marginRight, 10) || 0,
7837 bottom: parseInt(target.marginBottom, 10) || 0,
7838 left: parseInt(target.marginLeft, 10) || 0
7839 }
7840 };
7841
7842 // Support these properties until they are removed.
7843 if (this.verticalOffset) {
7844 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf fset;
7845 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
7846 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
7847 this.style.marginTop = this.style.marginBottom = this.verticalOffset + ' px';
7848 }
7849 if (this.horizontalOffset) {
7850 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal Offset;
7851 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
7852 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
7853 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
7854 }
7855 },
7856
7857 /**
7858 * Resets the target element's position and size constraints, and clear
7859 * the memoized data.
7860 */
7861 resetFit: function() {
7862 var info = this._fitInfo || {};
7863 for (var property in info.sizerInlineStyle) {
7864 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
7865 }
7866 for (var property in info.inlineStyle) {
7867 this.style[property] = info.inlineStyle[property];
7868 }
7869
7870 this._fitInfo = null;
7871 },
7872
7873 /**
7874 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
7875 * the element or the `fitInto` element has been resized, or if any of the
7876 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated .
7877 * It preserves the scroll position of the sizingTarget.
7878 */
7879 refit: function() {
7880 var scrollLeft = this.sizingTarget.scrollLeft;
7881 var scrollTop = this.sizingTarget.scrollTop;
7882 this.resetFit();
7883 this.fit();
7884 this.sizingTarget.scrollLeft = scrollLeft;
7885 this.sizingTarget.scrollTop = scrollTop;
7886 },
7887
7888 /**
7889 * Positions the element according to `horizontalAlign, verticalAlign`.
7890 */
7891 position: function() {
7892 if (!this.horizontalAlign && !this.verticalAlign) {
7893 // needs to be centered, and it is done after constrain.
7894 return;
7895 }
7896 this._discoverInfo();
7897
7898 this.style.position = 'fixed';
7899 // Need border-box for margin/padding.
7900 this.sizingTarget.style.boxSizing = 'border-box';
7901 // Set to 0, 0 in order to discover any offset caused by parent stacking c ontexts.
7902 this.style.left = '0px';
7903 this.style.top = '0px';
7904
7905 var rect = this.getBoundingClientRect();
7906 var positionRect = this.__getNormalizedRect(this.positionTarget);
7907 var fitRect = this.__getNormalizedRect(this.fitInto);
7908
7909 var margin = this._fitInfo.margin;
7910
7911 // Consider the margin as part of the size for position calculations.
7912 var size = {
7913 width: rect.width + margin.left + margin.right,
7914 height: rect.height + margin.top + margin.bottom
7915 };
7916
7917 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic alAlign, size, positionRect, fitRect);
7918
7919 var left = position.left + margin.left;
7920 var top = position.top + margin.top;
7921
7922 // Use original size (without margin).
7923 var right = Math.min(fitRect.right - margin.right, left + rect.width);
7924 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
7925
7926 var minWidth = this._fitInfo.sizedBy.minWidth;
7927 var minHeight = this._fitInfo.sizedBy.minHeight;
7928 if (left < margin.left) {
7929 left = margin.left;
7930 if (right - left < minWidth) {
7931 left = right - minWidth;
7932 }
7933 }
7934 if (top < margin.top) {
7935 top = margin.top;
7936 if (bottom - top < minHeight) {
7937 top = bottom - minHeight;
7938 }
7939 }
7940
7941 this.sizingTarget.style.maxWidth = (right - left) + 'px';
7942 this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
7943
7944 // Remove the offset caused by any stacking context.
7945 this.style.left = (left - rect.left) + 'px';
7946 this.style.top = (top - rect.top) + 'px';
7947 },
7948
7949 /**
7950 * Constrains the size of the element to `fitInto` by setting `max-height`
7951 * and/or `max-width`.
7952 */
7953 constrain: function() {
7954 if (this.horizontalAlign || this.verticalAlign) {
7955 return;
7956 }
7957 this._discoverInfo();
7958
7959 var info = this._fitInfo;
7960 // position at (0px, 0px) if not already positioned, so we can measure the natural size.
7961 if (!info.positionedBy.vertically) {
7962 this.style.position = 'fixed';
7963 this.style.top = '0px';
7964 }
7965 if (!info.positionedBy.horizontally) {
7966 this.style.position = 'fixed';
7967 this.style.left = '0px';
7968 }
7969
7970 // need border-box for margin/padding
7971 this.sizingTarget.style.boxSizing = 'border-box';
7972 // constrain the width and height if not already set
7973 var rect = this.getBoundingClientRect();
7974 if (!info.sizedBy.height) {
7975 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom' , 'Height');
7976 }
7977 if (!info.sizedBy.width) {
7978 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ t', 'Width');
7979 }
7980 },
7981
7982 /**
7983 * @protected
7984 * @deprecated
7985 */
7986 _sizeDimension: function(rect, positionedBy, start, end, extent) {
7987 this.__sizeDimension(rect, positionedBy, start, end, extent);
7988 },
7989
7990 /**
7991 * @private
7992 */
7993 __sizeDimension: function(rect, positionedBy, start, end, extent) {
7994 var info = this._fitInfo;
7995 var fitRect = this.__getNormalizedRect(this.fitInto);
7996 var max = extent === 'Width' ? fitRect.width : fitRect.height;
7997 var flip = (positionedBy === end);
7998 var offset = flip ? max - rect[end] : rect[start];
7999 var margin = info.margin[flip ? start : end];
8000 var offsetExtent = 'offset' + extent;
8001 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
8002 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO ffset) + 'px';
8003 },
8004
8005 /**
8006 * Centers horizontally and vertically if not already positioned. This also sets
8007 * `position:fixed`.
8008 */
8009 center: function() {
8010 if (this.horizontalAlign || this.verticalAlign) {
8011 return;
8012 }
8013 this._discoverInfo();
8014
8015 var positionedBy = this._fitInfo.positionedBy;
8016 if (positionedBy.vertically && positionedBy.horizontally) {
8017 // Already positioned.
8018 return;
8019 }
8020 // Need position:fixed to center
8021 this.style.position = 'fixed';
8022 // Take into account the offset caused by parents that create stacking
8023 // contexts (e.g. with transform: translate3d). Translate to 0,0 and
8024 // measure the bounding rect.
8025 if (!positionedBy.vertically) {
8026 this.style.top = '0px';
8027 }
8028 if (!positionedBy.horizontally) {
8029 this.style.left = '0px';
8030 }
8031 // It will take in consideration margins and transforms
8032 var rect = this.getBoundingClientRect();
8033 var fitRect = this.__getNormalizedRect(this.fitInto);
8034 if (!positionedBy.vertically) {
8035 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
8036 this.style.top = top + 'px';
8037 }
8038 if (!positionedBy.horizontally) {
8039 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
8040 this.style.left = left + 'px';
8041 }
8042 },
8043
8044 __getNormalizedRect: function(target) {
8045 if (target === document.documentElement || target === window) {
8046 return {
8047 top: 0,
8048 left: 0,
8049 width: window.innerWidth,
8050 height: window.innerHeight,
8051 right: window.innerWidth,
8052 bottom: window.innerHeight
8053 };
8054 }
8055 return target.getBoundingClientRect();
8056 },
8057
8058 __getCroppedArea: function(position, size, fitRect) {
8059 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
8060 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ t - (position.left + size.width));
8061 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si ze.height;
8062 },
8063
8064
8065 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
8066 // All the possible configurations.
8067 // Ordered as top-left, top-right, bottom-left, bottom-right.
8068 var positions = [{
8069 verticalAlign: 'top',
8070 horizontalAlign: 'left',
8071 top: positionRect.top,
8072 left: positionRect.left
8073 }, {
8074 verticalAlign: 'top',
8075 horizontalAlign: 'right',
8076 top: positionRect.top,
8077 left: positionRect.right - size.width
8078 }, {
8079 verticalAlign: 'bottom',
8080 horizontalAlign: 'left',
8081 top: positionRect.bottom - size.height,
8082 left: positionRect.left
8083 }, {
8084 verticalAlign: 'bottom',
8085 horizontalAlign: 'right',
8086 top: positionRect.bottom - size.height,
8087 left: positionRect.right - size.width
8088 }];
8089
8090 if (this.noOverlap) {
8091 // Duplicate.
8092 for (var i = 0, l = positions.length; i < l; i++) {
8093 var copy = {};
8094 for (var key in positions[i]) {
8095 copy[key] = positions[i][key];
8096 }
8097 positions.push(copy);
8098 }
8099 // Horizontal overlap only.
8100 positions[0].top = positions[1].top += positionRect.height;
8101 positions[2].top = positions[3].top -= positionRect.height;
8102 // Vertical overlap only.
8103 positions[4].left = positions[6].left += positionRect.width;
8104 positions[5].left = positions[7].left -= positionRect.width;
8105 }
8106
8107 // Consider auto as null for coding convenience.
8108 vAlign = vAlign === 'auto' ? null : vAlign;
8109 hAlign = hAlign === 'auto' ? null : hAlign;
8110
8111 var position;
8112 for (var i = 0; i < positions.length; i++) {
8113 var pos = positions[i];
8114
8115 // If both vAlign and hAlign are defined, return exact match.
8116 // For dynamicAlign and noOverlap we'll have more than one candidate, so
8117 // we'll have to check the croppedArea to make the best choice.
8118 if (!this.dynamicAlign && !this.noOverlap &&
8119 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
8120 position = pos;
8121 break;
8122 }
8123
8124 // Align is ok if alignment preferences are respected. If no preferences ,
8125 // it is considered ok.
8126 var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
8127 (!hAlign || pos.horizontalAlign === hAlign);
8128
8129 // Filter out elements that don't match the alignment (if defined).
8130 // With dynamicAlign, we need to consider all the positions to find the
8131 // one that minimizes the cropped area.
8132 if (!this.dynamicAlign && !alignOk) {
8133 continue;
8134 }
8135
8136 position = position || pos;
8137 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
8138 var diff = pos.croppedArea - position.croppedArea;
8139 // Check which crops less. If it crops equally, check if align is ok.
8140 if (diff < 0 || (diff === 0 && alignOk)) {
8141 position = pos;
8142 }
8143 // If not cropped and respects the align requirements, keep it.
8144 // This allows to prefer positions overlapping horizontally over the
8145 // ones overlapping vertically.
8146 if (position.croppedArea === 0 && alignOk) {
8147 break;
8148 }
8149 }
8150
8151 return position;
8152 }
8153
8154 };
8155 (function() {
8156 'use strict';
8157
8158 Polymer({
8159
8160 is: 'iron-overlay-backdrop',
8161
8162 properties: {
8163
8164 /**
8165 * Returns true if the backdrop is opened.
8166 */
8167 opened: { 4595 opened: {
8168 reflectToAttribute: true, 4596 reflectToAttribute: true,
8169 type: Boolean, 4597 type: Boolean,
8170 value: false, 4598 value: false,
8171 observer: '_openedChanged' 4599 observer: '_openedChanged'
8172 } 4600 }
8173 4601 },
8174 },
8175
8176 listeners: { 4602 listeners: {
8177 'transitionend': '_onTransitionend' 4603 transitionend: '_onTransitionend'
8178 }, 4604 },
8179
8180 created: function() { 4605 created: function() {
8181 // Used to cancel previous requestAnimationFrame calls when opened changes .
8182 this.__openedRaf = null; 4606 this.__openedRaf = null;
8183 }, 4607 },
8184
8185 attached: function() { 4608 attached: function() {
8186 this.opened && this._openedChanged(this.opened); 4609 this.opened && this._openedChanged(this.opened);
8187 }, 4610 },
8188
8189 /**
8190 * Appends the backdrop to document body if needed.
8191 */
8192 prepare: function() { 4611 prepare: function() {
8193 if (this.opened && !this.parentNode) { 4612 if (this.opened && !this.parentNode) {
8194 Polymer.dom(document.body).appendChild(this); 4613 Polymer.dom(document.body).appendChild(this);
8195 } 4614 }
8196 }, 4615 },
8197
8198 /**
8199 * Shows the backdrop.
8200 */
8201 open: function() { 4616 open: function() {
8202 this.opened = true; 4617 this.opened = true;
8203 }, 4618 },
8204
8205 /**
8206 * Hides the backdrop.
8207 */
8208 close: function() { 4619 close: function() {
8209 this.opened = false; 4620 this.opened = false;
8210 }, 4621 },
8211
8212 /**
8213 * Removes the backdrop from document body if needed.
8214 */
8215 complete: function() { 4622 complete: function() {
8216 if (!this.opened && this.parentNode === document.body) { 4623 if (!this.opened && this.parentNode === document.body) {
8217 Polymer.dom(this.parentNode).removeChild(this); 4624 Polymer.dom(this.parentNode).removeChild(this);
8218 } 4625 }
8219 }, 4626 },
8220
8221 _onTransitionend: function(event) { 4627 _onTransitionend: function(event) {
8222 if (event && event.target === this) { 4628 if (event && event.target === this) {
8223 this.complete(); 4629 this.complete();
8224 } 4630 }
8225 }, 4631 },
8226
8227 /**
8228 * @param {boolean} opened
8229 * @private
8230 */
8231 _openedChanged: function(opened) { 4632 _openedChanged: function(opened) {
8232 if (opened) { 4633 if (opened) {
8233 // Auto-attach.
8234 this.prepare(); 4634 this.prepare();
8235 } else { 4635 } else {
8236 // Animation might be disabled via the mixin or opacity custom property.
8237 // If it is disabled in other ways, it's up to the user to call complete .
8238 var cs = window.getComputedStyle(this); 4636 var cs = window.getComputedStyle(this);
8239 if (cs.transitionDuration === '0s' || cs.opacity == 0) { 4637 if (cs.transitionDuration === '0s' || cs.opacity == 0) {
8240 this.complete(); 4638 this.complete();
8241 } 4639 }
8242 } 4640 }
8243
8244 if (!this.isAttached) { 4641 if (!this.isAttached) {
8245 return; 4642 return;
8246 } 4643 }
8247
8248 // Always cancel previous requestAnimationFrame.
8249 if (this.__openedRaf) { 4644 if (this.__openedRaf) {
8250 window.cancelAnimationFrame(this.__openedRaf); 4645 window.cancelAnimationFrame(this.__openedRaf);
8251 this.__openedRaf = null; 4646 this.__openedRaf = null;
8252 } 4647 }
8253 // Force relayout to ensure proper transitions.
8254 this.scrollTop = this.scrollTop; 4648 this.scrollTop = this.scrollTop;
8255 this.__openedRaf = window.requestAnimationFrame(function() { 4649 this.__openedRaf = window.requestAnimationFrame(function() {
8256 this.__openedRaf = null; 4650 this.__openedRaf = null;
8257 this.toggleClass('opened', this.opened); 4651 this.toggleClass('opened', this.opened);
8258 }.bind(this)); 4652 }.bind(this));
8259 } 4653 }
8260 }); 4654 });
8261
8262 })(); 4655 })();
8263 /** 4656
8264 * @struct 4657 Polymer.IronOverlayManagerClass = function() {
8265 * @constructor 4658 this._overlays = [];
8266 * @private 4659 this._minimumZ = 101;
8267 */ 4660 this._backdropElement = null;
8268 Polymer.IronOverlayManagerClass = function() { 4661 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
8269 /** 4662 document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
8270 * Used to keep track of the opened overlays. 4663 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
8271 * @private {Array<Element>} 4664 };
8272 */ 4665
8273 this._overlays = []; 4666 Polymer.IronOverlayManagerClass.prototype = {
8274 4667 constructor: Polymer.IronOverlayManagerClass,
8275 /** 4668 get backdropElement() {
8276 * iframes have a default z-index of 100, 4669 if (!this._backdropElement) {
8277 * so this default should be at least that. 4670 this._backdropElement = document.createElement('iron-overlay-backdrop');
8278 * @private {number} 4671 }
8279 */ 4672 return this._backdropElement;
8280 this._minimumZ = 101; 4673 },
8281 4674 get deepActiveElement() {
8282 /** 4675 var active = document.activeElement || document.body;
8283 * Memoized backdrop element. 4676 while (active.root && Polymer.dom(active.root).activeElement) {
8284 * @private {Element|null} 4677 active = Polymer.dom(active.root).activeElement;
8285 */ 4678 }
8286 this._backdropElement = null; 4679 return active;
8287 4680 },
8288 // Enable document-wide tap recognizer. 4681 _bringOverlayAtIndexToFront: function(i) {
8289 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); 4682 var overlay = this._overlays[i];
8290 4683 if (!overlay) {
8291 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); 4684 return;
8292 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true ); 4685 }
8293 }; 4686 var lastI = this._overlays.length - 1;
8294 4687 var currentOverlay = this._overlays[lastI];
8295 Polymer.IronOverlayManagerClass.prototype = { 4688 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8296 4689 lastI--;
8297 constructor: Polymer.IronOverlayManagerClass, 4690 }
8298 4691 if (i >= lastI) {
8299 /** 4692 return;
8300 * The shared backdrop element. 4693 }
8301 * @type {!Element} backdropElement 4694 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8302 */ 4695 if (this._getZ(overlay) <= minimumZ) {
8303 get backdropElement() { 4696 this._applyOverlayZ(overlay, minimumZ);
8304 if (!this._backdropElement) { 4697 }
8305 this._backdropElement = document.createElement('iron-overlay-backdrop'); 4698 while (i < lastI) {
8306 } 4699 this._overlays[i] = this._overlays[i + 1];
8307 return this._backdropElement; 4700 i++;
8308 }, 4701 }
8309 4702 this._overlays[lastI] = overlay;
8310 /** 4703 },
8311 * The deepest active element. 4704 addOrRemoveOverlay: function(overlay) {
8312 * @type {!Element} activeElement the active element 4705 if (overlay.opened) {
8313 */ 4706 this.addOverlay(overlay);
8314 get deepActiveElement() { 4707 } else {
8315 // document.activeElement can be null 4708 this.removeOverlay(overlay);
8316 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement 4709 }
8317 // In case of null, default it to document.body. 4710 },
8318 var active = document.activeElement || document.body; 4711 addOverlay: function(overlay) {
8319 while (active.root && Polymer.dom(active.root).activeElement) { 4712 var i = this._overlays.indexOf(overlay);
8320 active = Polymer.dom(active.root).activeElement; 4713 if (i >= 0) {
8321 } 4714 this._bringOverlayAtIndexToFront(i);
8322 return active;
8323 },
8324
8325 /**
8326 * Brings the overlay at the specified index to the front.
8327 * @param {number} i
8328 * @private
8329 */
8330 _bringOverlayAtIndexToFront: function(i) {
8331 var overlay = this._overlays[i];
8332 if (!overlay) {
8333 return;
8334 }
8335 var lastI = this._overlays.length - 1;
8336 var currentOverlay = this._overlays[lastI];
8337 // Ensure always-on-top overlay stays on top.
8338 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8339 lastI--;
8340 }
8341 // If already the top element, return.
8342 if (i >= lastI) {
8343 return;
8344 }
8345 // Update z-index to be on top.
8346 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8347 if (this._getZ(overlay) <= minimumZ) {
8348 this._applyOverlayZ(overlay, minimumZ);
8349 }
8350
8351 // Shift other overlays behind the new on top.
8352 while (i < lastI) {
8353 this._overlays[i] = this._overlays[i + 1];
8354 i++;
8355 }
8356 this._overlays[lastI] = overlay;
8357 },
8358
8359 /**
8360 * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
8361 * Also updates the backdrop z-index.
8362 * @param {!Element} overlay
8363 */
8364 addOrRemoveOverlay: function(overlay) {
8365 if (overlay.opened) {
8366 this.addOverlay(overlay);
8367 } else {
8368 this.removeOverlay(overlay);
8369 }
8370 },
8371
8372 /**
8373 * Tracks overlays for z-index and focus management.
8374 * Ensures the last added overlay with always-on-top remains on top.
8375 * @param {!Element} overlay
8376 */
8377 addOverlay: function(overlay) {
8378 var i = this._overlays.indexOf(overlay);
8379 if (i >= 0) {
8380 this._bringOverlayAtIndexToFront(i);
8381 this.trackBackdrop();
8382 return;
8383 }
8384 var insertionIndex = this._overlays.length;
8385 var currentOverlay = this._overlays[insertionIndex - 1];
8386 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8387 var newZ = this._getZ(overlay);
8388
8389 // Ensure always-on-top overlay stays on top.
8390 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8391 // This bumps the z-index of +2.
8392 this._applyOverlayZ(currentOverlay, minimumZ);
8393 insertionIndex--;
8394 // Update minimumZ to match previous overlay's z-index.
8395 var previousOverlay = this._overlays[insertionIndex - 1];
8396 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8397 }
8398
8399 // Update z-index and insert overlay.
8400 if (newZ <= minimumZ) {
8401 this._applyOverlayZ(overlay, minimumZ);
8402 }
8403 this._overlays.splice(insertionIndex, 0, overlay);
8404
8405 this.trackBackdrop(); 4715 this.trackBackdrop();
8406 }, 4716 return;
8407 4717 }
8408 /** 4718 var insertionIndex = this._overlays.length;
8409 * @param {!Element} overlay 4719 var currentOverlay = this._overlays[insertionIndex - 1];
8410 */ 4720 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8411 removeOverlay: function(overlay) { 4721 var newZ = this._getZ(overlay);
8412 var i = this._overlays.indexOf(overlay); 4722 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
8413 if (i === -1) { 4723 this._applyOverlayZ(currentOverlay, minimumZ);
8414 return; 4724 insertionIndex--;
8415 } 4725 var previousOverlay = this._overlays[insertionIndex - 1];
8416 this._overlays.splice(i, 1); 4726 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8417 4727 }
8418 this.trackBackdrop(); 4728 if (newZ <= minimumZ) {
8419 }, 4729 this._applyOverlayZ(overlay, minimumZ);
8420 4730 }
8421 /** 4731 this._overlays.splice(insertionIndex, 0, overlay);
8422 * Returns the current overlay. 4732 this.trackBackdrop();
8423 * @return {Element|undefined} 4733 },
8424 */ 4734 removeOverlay: function(overlay) {
8425 currentOverlay: function() { 4735 var i = this._overlays.indexOf(overlay);
8426 var i = this._overlays.length - 1; 4736 if (i === -1) {
8427 return this._overlays[i]; 4737 return;
8428 }, 4738 }
8429 4739 this._overlays.splice(i, 1);
8430 /** 4740 this.trackBackdrop();
8431 * Returns the current overlay z-index. 4741 },
8432 * @return {number} 4742 currentOverlay: function() {
8433 */ 4743 var i = this._overlays.length - 1;
8434 currentOverlayZ: function() { 4744 return this._overlays[i];
8435 return this._getZ(this.currentOverlay()); 4745 },
8436 }, 4746 currentOverlayZ: function() {
8437 4747 return this._getZ(this.currentOverlay());
8438 /** 4748 },
8439 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. 4749 ensureMinimumZ: function(minimumZ) {
8440 * This does not effect the z-index of any existing overlays. 4750 this._minimumZ = Math.max(this._minimumZ, minimumZ);
8441 * @param {number} minimumZ 4751 },
8442 */ 4752 focusOverlay: function() {
8443 ensureMinimumZ: function(minimumZ) { 4753 var current = this.currentOverlay();
8444 this._minimumZ = Math.max(this._minimumZ, minimumZ); 4754 if (current) {
8445 }, 4755 current._applyFocus();
8446 4756 }
8447 focusOverlay: function() { 4757 },
8448 var current = /** @type {?} */ (this.currentOverlay()); 4758 trackBackdrop: function() {
8449 if (current) { 4759 var overlay = this._overlayWithBackdrop();
8450 current._applyFocus(); 4760 if (!overlay && !this._backdropElement) {
8451 } 4761 return;
8452 }, 4762 }
8453 4763 this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
8454 /** 4764 this.backdropElement.opened = !!overlay;
8455 * Updates the backdrop z-index. 4765 },
8456 */ 4766 getBackdrops: function() {
8457 trackBackdrop: function() { 4767 var backdrops = [];
8458 var overlay = this._overlayWithBackdrop(); 4768 for (var i = 0; i < this._overlays.length; i++) {
8459 // Avoid creating the backdrop if there is no overlay with backdrop. 4769 if (this._overlays[i].withBackdrop) {
8460 if (!overlay && !this._backdropElement) { 4770 backdrops.push(this._overlays[i]);
8461 return; 4771 }
8462 } 4772 }
8463 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; 4773 return backdrops;
8464 this.backdropElement.opened = !!overlay; 4774 },
8465 }, 4775 backdropZ: function() {
8466 4776 return this._getZ(this._overlayWithBackdrop()) - 1;
8467 /** 4777 },
8468 * @return {Array<Element>} 4778 _overlayWithBackdrop: function() {
8469 */ 4779 for (var i = 0; i < this._overlays.length; i++) {
8470 getBackdrops: function() { 4780 if (this._overlays[i].withBackdrop) {
8471 var backdrops = []; 4781 return this._overlays[i];
8472 for (var i = 0; i < this._overlays.length; i++) { 4782 }
8473 if (this._overlays[i].withBackdrop) { 4783 }
8474 backdrops.push(this._overlays[i]); 4784 },
8475 } 4785 _getZ: function(overlay) {
8476 } 4786 var z = this._minimumZ;
8477 return backdrops; 4787 if (overlay) {
8478 }, 4788 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z Index);
8479 4789 if (z1 === z1) {
8480 /** 4790 z = z1;
8481 * Returns the z-index for the backdrop. 4791 }
8482 * @return {number} 4792 }
8483 */ 4793 return z;
8484 backdropZ: function() { 4794 },
8485 return this._getZ(this._overlayWithBackdrop()) - 1; 4795 _setZ: function(element, z) {
8486 }, 4796 element.style.zIndex = z;
8487 4797 },
8488 /** 4798 _applyOverlayZ: function(overlay, aboveZ) {
8489 * Returns the first opened overlay that has a backdrop. 4799 this._setZ(overlay, aboveZ + 2);
8490 * @return {Element|undefined} 4800 },
8491 * @private 4801 _overlayInPath: function(path) {
8492 */ 4802 path = path || [];
8493 _overlayWithBackdrop: function() { 4803 for (var i = 0; i < path.length; i++) {
8494 for (var i = 0; i < this._overlays.length; i++) { 4804 if (path[i]._manager === this) {
8495 if (this._overlays[i].withBackdrop) { 4805 return path[i];
8496 return this._overlays[i]; 4806 }
8497 } 4807 }
8498 } 4808 },
8499 }, 4809 _onCaptureClick: function(event) {
8500 4810 var overlay = this.currentOverlay();
8501 /** 4811 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8502 * Calculates the minimum z-index for the overlay. 4812 overlay._onCaptureClick(event);
8503 * @param {Element=} overlay 4813 }
8504 * @private 4814 },
8505 */ 4815 _onCaptureFocus: function(event) {
8506 _getZ: function(overlay) { 4816 var overlay = this.currentOverlay();
8507 var z = this._minimumZ; 4817 if (overlay) {
8508 if (overlay) { 4818 overlay._onCaptureFocus(event);
8509 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay) .zIndex); 4819 }
8510 // Check if is a number 4820 },
8511 // Number.isNaN not supported in IE 10+ 4821 _onCaptureKeyDown: function(event) {
8512 if (z1 === z1) { 4822 var overlay = this.currentOverlay();
8513 z = z1; 4823 if (overlay) {
8514 } 4824 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8515 } 4825 overlay._onCaptureEsc(event);
8516 return z; 4826 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't ab')) {
8517 }, 4827 overlay._onCaptureTab(event);
8518 4828 }
8519 /** 4829 }
8520 * @param {!Element} element 4830 },
8521 * @param {number|string} z 4831 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8522 * @private 4832 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8523 */ 4833 }
8524 _setZ: function(element, z) { 4834 };
8525 element.style.zIndex = z; 4835
8526 }, 4836 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8527 4837
8528 /**
8529 * @param {!Element} overlay
8530 * @param {number} aboveZ
8531 * @private
8532 */
8533 _applyOverlayZ: function(overlay, aboveZ) {
8534 this._setZ(overlay, aboveZ + 2);
8535 },
8536
8537 /**
8538 * Returns the deepest overlay in the path.
8539 * @param {Array<Element>=} path
8540 * @return {Element|undefined}
8541 * @suppress {missingProperties}
8542 * @private
8543 */
8544 _overlayInPath: function(path) {
8545 path = path || [];
8546 for (var i = 0; i < path.length; i++) {
8547 if (path[i]._manager === this) {
8548 return path[i];
8549 }
8550 }
8551 },
8552
8553 /**
8554 * Ensures the click event is delegated to the right overlay.
8555 * @param {!Event} event
8556 * @private
8557 */
8558 _onCaptureClick: function(event) {
8559 var overlay = /** @type {?} */ (this.currentOverlay());
8560 // Check if clicked outside of top overlay.
8561 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8562 overlay._onCaptureClick(event);
8563 }
8564 },
8565
8566 /**
8567 * Ensures the focus event is delegated to the right overlay.
8568 * @param {!Event} event
8569 * @private
8570 */
8571 _onCaptureFocus: function(event) {
8572 var overlay = /** @type {?} */ (this.currentOverlay());
8573 if (overlay) {
8574 overlay._onCaptureFocus(event);
8575 }
8576 },
8577
8578 /**
8579 * Ensures TAB and ESC keyboard events are delegated to the right overlay.
8580 * @param {!Event} event
8581 * @private
8582 */
8583 _onCaptureKeyDown: function(event) {
8584 var overlay = /** @type {?} */ (this.currentOverlay());
8585 if (overlay) {
8586 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8587 overlay._onCaptureEsc(event);
8588 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
8589 overlay._onCaptureTab(event);
8590 }
8591 }
8592 },
8593
8594 /**
8595 * Returns if the overlay1 should be behind overlay2.
8596 * @param {!Element} overlay1
8597 * @param {!Element} overlay2
8598 * @return {boolean}
8599 * @suppress {missingProperties}
8600 * @private
8601 */
8602 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8603 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8604 }
8605 };
8606
8607 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8608 (function() { 4838 (function() {
8609 'use strict'; 4839 'use strict';
8610
8611 /**
8612 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
8613 on top of other content. It includes an optional backdrop, and can be used to im plement a variety
8614 of UI controls including dialogs and drop downs. Multiple overlays may be displa yed at once.
8615
8616 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav ior/blob/master/demo/simple-overlay.html)
8617 for an example.
8618
8619 ### Closing and canceling
8620
8621 An overlay may be hidden by closing or canceling. The difference between close a nd cancel is user
8622 intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
8623 it will cancel whenever the user taps outside it or presses the escape key. This behavior is
8624 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click ` properties.
8625 `close()` should be called explicitly by the implementer when the user interacts with a control
8626 in the overlay element. When the dialog is canceled, the overlay fires an 'iron- overlay-canceled'
8627 event. Call `preventDefault` on this event to prevent the overlay from closing.
8628
8629 ### Positioning
8630
8631 By default the element is sized and positioned to fit and centered inside the wi ndow. You can
8632 position and size it manually using CSS. See `Polymer.IronFitBehavior`.
8633
8634 ### Backdrop
8635
8636 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
8637 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
8638 options.
8639
8640 In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
8641 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f ocusableNodes)
8642 to achieve a different behavior.
8643
8644 ### Limitations
8645
8646 The element is styled to appear on top of other content by setting its `z-index` property. You
8647 must ensure no element has a stacking context with a higher `z-index` than its p arent stacking
8648 context. You should place this element as a child of `<body>` whenever possible.
8649
8650 @demo demo/index.html
8651 @polymerBehavior Polymer.IronOverlayBehavior
8652 */
8653
8654 Polymer.IronOverlayBehaviorImpl = { 4840 Polymer.IronOverlayBehaviorImpl = {
8655
8656 properties: { 4841 properties: {
8657
8658 /**
8659 * True if the overlay is currently displayed.
8660 */
8661 opened: { 4842 opened: {
8662 observer: '_openedChanged', 4843 observer: '_openedChanged',
8663 type: Boolean, 4844 type: Boolean,
8664 value: false, 4845 value: false,
8665 notify: true 4846 notify: true
8666 }, 4847 },
8667
8668 /**
8669 * True if the overlay was canceled when it was last closed.
8670 */
8671 canceled: { 4848 canceled: {
8672 observer: '_canceledChanged', 4849 observer: '_canceledChanged',
8673 readOnly: true, 4850 readOnly: true,
8674 type: Boolean, 4851 type: Boolean,
8675 value: false 4852 value: false
8676 }, 4853 },
8677
8678 /**
8679 * Set to true to display a backdrop behind the overlay. It traps the focu s
8680 * within the light DOM of the overlay.
8681 */
8682 withBackdrop: { 4854 withBackdrop: {
8683 observer: '_withBackdropChanged', 4855 observer: '_withBackdropChanged',
8684 type: Boolean 4856 type: Boolean
8685 }, 4857 },
8686
8687 /**
8688 * Set to true to disable auto-focusing the overlay or child nodes with
8689 * the `autofocus` attribute` when the overlay is opened.
8690 */
8691 noAutoFocus: { 4858 noAutoFocus: {
8692 type: Boolean, 4859 type: Boolean,
8693 value: false 4860 value: false
8694 }, 4861 },
8695
8696 /**
8697 * Set to true to disable canceling the overlay with the ESC key.
8698 */
8699 noCancelOnEscKey: { 4862 noCancelOnEscKey: {
8700 type: Boolean, 4863 type: Boolean,
8701 value: false 4864 value: false
8702 }, 4865 },
8703
8704 /**
8705 * Set to true to disable canceling the overlay by clicking outside it.
8706 */
8707 noCancelOnOutsideClick: { 4866 noCancelOnOutsideClick: {
8708 type: Boolean, 4867 type: Boolean,
8709 value: false 4868 value: false
8710 }, 4869 },
8711
8712 /**
8713 * Contains the reason(s) this overlay was last closed (see `iron-overlay- closed`).
8714 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t he
8715 * behavior can provide other reasons in addition to `canceled`.
8716 */
8717 closingReason: { 4870 closingReason: {
8718 // was a getter before, but needs to be a property so other
8719 // behaviors can override this.
8720 type: Object 4871 type: Object
8721 }, 4872 },
8722
8723 /**
8724 * Set to true to enable restoring of focus when overlay is closed.
8725 */
8726 restoreFocusOnClose: { 4873 restoreFocusOnClose: {
8727 type: Boolean, 4874 type: Boolean,
8728 value: false 4875 value: false
8729 }, 4876 },
8730
8731 /**
8732 * Set to true to keep overlay always on top.
8733 */
8734 alwaysOnTop: { 4877 alwaysOnTop: {
8735 type: Boolean 4878 type: Boolean
8736 }, 4879 },
8737
8738 /**
8739 * Shortcut to access to the overlay manager.
8740 * @private
8741 * @type {Polymer.IronOverlayManagerClass}
8742 */
8743 _manager: { 4880 _manager: {
8744 type: Object, 4881 type: Object,
8745 value: Polymer.IronOverlayManager 4882 value: Polymer.IronOverlayManager
8746 }, 4883 },
8747
8748 /**
8749 * The node being focused.
8750 * @type {?Node}
8751 */
8752 _focusedChild: { 4884 _focusedChild: {
8753 type: Object 4885 type: Object
8754 } 4886 }
8755 4887 },
8756 },
8757
8758 listeners: { 4888 listeners: {
8759 'iron-resize': '_onIronResize' 4889 'iron-resize': '_onIronResize'
8760 }, 4890 },
8761
8762 /**
8763 * The backdrop element.
8764 * @type {Element}
8765 */
8766 get backdropElement() { 4891 get backdropElement() {
8767 return this._manager.backdropElement; 4892 return this._manager.backdropElement;
8768 }, 4893 },
8769
8770 /**
8771 * Returns the node to give focus to.
8772 * @type {Node}
8773 */
8774 get _focusNode() { 4894 get _focusNode() {
8775 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this; 4895 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this;
8776 }, 4896 },
8777
8778 /**
8779 * Array of nodes that can receive focus (overlay included), ordered by `tab index`.
8780 * This is used to retrieve which is the first and last focusable nodes in o rder
8781 * to wrap the focus for overlays `with-backdrop`.
8782 *
8783 * If you know what is your content (specifically the first and last focusab le children),
8784 * you can override this method to return only `[firstFocusable, lastFocusab le];`
8785 * @type {Array<Node>}
8786 * @protected
8787 */
8788 get _focusableNodes() { 4897 get _focusableNodes() {
8789 // Elements that can be focused even if they have [disabled] attribute. 4898 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin dex]', '[contentEditable=true]' ];
8790 var FOCUSABLE_WITH_DISABLED = [ 4899 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button' ];
8791 'a[href]', 4900 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"])';
8792 'area[href]',
8793 'iframe',
8794 '[tabindex]',
8795 '[contentEditable=true]'
8796 ];
8797
8798 // Elements that cannot be focused if they have [disabled] attribute.
8799 var FOCUSABLE_WITHOUT_DISABLED = [
8800 'input',
8801 'select',
8802 'textarea',
8803 'button'
8804 ];
8805
8806 // Discard elements with tabindex=-1 (makes them not focusable).
8807 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
8808 ':not([tabindex="-1"]),' +
8809 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),' ) +
8810 ':not([disabled]):not([tabindex="-1"])';
8811
8812 var focusables = Polymer.dom(this).querySelectorAll(selector); 4901 var focusables = Polymer.dom(this).querySelectorAll(selector);
8813 if (this.tabIndex >= 0) { 4902 if (this.tabIndex >= 0) {
8814 // Insert at the beginning because we might have all elements with tabIn dex = 0,
8815 // and the overlay should be the first of the list.
8816 focusables.splice(0, 0, this); 4903 focusables.splice(0, 0, this);
8817 } 4904 }
8818 // Sort by tabindex. 4905 return focusables.sort(function(a, b) {
8819 return focusables.sort(function (a, b) {
8820 if (a.tabIndex === b.tabIndex) { 4906 if (a.tabIndex === b.tabIndex) {
8821 return 0; 4907 return 0;
8822 } 4908 }
8823 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { 4909 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
8824 return 1; 4910 return 1;
8825 } 4911 }
8826 return -1; 4912 return -1;
8827 }); 4913 });
8828 }, 4914 },
8829
8830 ready: function() { 4915 ready: function() {
8831 // Used to skip calls to notifyResize and refit while the overlay is anima ting.
8832 this.__isAnimating = false; 4916 this.__isAnimating = false;
8833 // with-backdrop needs tabindex to be set in order to trap the focus.
8834 // If it is not set, IronOverlayBehavior will set it, and remove it if wit h-backdrop = false.
8835 this.__shouldRemoveTabIndex = false; 4917 this.__shouldRemoveTabIndex = false;
8836 // Used for wrapping the focus on TAB / Shift+TAB.
8837 this.__firstFocusableNode = this.__lastFocusableNode = null; 4918 this.__firstFocusableNode = this.__lastFocusableNode = null;
8838 // Used by __onNextAnimationFrame to cancel any previous callback.
8839 this.__raf = null; 4919 this.__raf = null;
8840 // Focused node before overlay gets opened. Can be restored on close.
8841 this.__restoreFocusNode = null; 4920 this.__restoreFocusNode = null;
8842 this._ensureSetup(); 4921 this._ensureSetup();
8843 }, 4922 },
8844
8845 attached: function() { 4923 attached: function() {
8846 // Call _openedChanged here so that position can be computed correctly.
8847 if (this.opened) { 4924 if (this.opened) {
8848 this._openedChanged(this.opened); 4925 this._openedChanged(this.opened);
8849 } 4926 }
8850 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); 4927 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
8851 }, 4928 },
8852
8853 detached: function() { 4929 detached: function() {
8854 Polymer.dom(this).unobserveNodes(this._observer); 4930 Polymer.dom(this).unobserveNodes(this._observer);
8855 this._observer = null; 4931 this._observer = null;
8856 if (this.__raf) { 4932 if (this.__raf) {
8857 window.cancelAnimationFrame(this.__raf); 4933 window.cancelAnimationFrame(this.__raf);
8858 this.__raf = null; 4934 this.__raf = null;
8859 } 4935 }
8860 this._manager.removeOverlay(this); 4936 this._manager.removeOverlay(this);
8861 }, 4937 },
8862
8863 /**
8864 * Toggle the opened state of the overlay.
8865 */
8866 toggle: function() { 4938 toggle: function() {
8867 this._setCanceled(false); 4939 this._setCanceled(false);
8868 this.opened = !this.opened; 4940 this.opened = !this.opened;
8869 }, 4941 },
8870
8871 /**
8872 * Open the overlay.
8873 */
8874 open: function() { 4942 open: function() {
8875 this._setCanceled(false); 4943 this._setCanceled(false);
8876 this.opened = true; 4944 this.opened = true;
8877 }, 4945 },
8878
8879 /**
8880 * Close the overlay.
8881 */
8882 close: function() { 4946 close: function() {
8883 this._setCanceled(false); 4947 this._setCanceled(false);
8884 this.opened = false; 4948 this.opened = false;
8885 }, 4949 },
8886
8887 /**
8888 * Cancels the overlay.
8889 * @param {Event=} event The original event
8890 */
8891 cancel: function(event) { 4950 cancel: function(event) {
8892 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t rue}); 4951 var cancelEvent = this.fire('iron-overlay-canceled', event, {
4952 cancelable: true
4953 });
8893 if (cancelEvent.defaultPrevented) { 4954 if (cancelEvent.defaultPrevented) {
8894 return; 4955 return;
8895 } 4956 }
8896
8897 this._setCanceled(true); 4957 this._setCanceled(true);
8898 this.opened = false; 4958 this.opened = false;
8899 }, 4959 },
8900
8901 _ensureSetup: function() { 4960 _ensureSetup: function() {
8902 if (this._overlaySetup) { 4961 if (this._overlaySetup) {
8903 return; 4962 return;
8904 } 4963 }
8905 this._overlaySetup = true; 4964 this._overlaySetup = true;
8906 this.style.outline = 'none'; 4965 this.style.outline = 'none';
8907 this.style.display = 'none'; 4966 this.style.display = 'none';
8908 }, 4967 },
8909
8910 /**
8911 * Called when `opened` changes.
8912 * @param {boolean=} opened
8913 * @protected
8914 */
8915 _openedChanged: function(opened) { 4968 _openedChanged: function(opened) {
8916 if (opened) { 4969 if (opened) {
8917 this.removeAttribute('aria-hidden'); 4970 this.removeAttribute('aria-hidden');
8918 } else { 4971 } else {
8919 this.setAttribute('aria-hidden', 'true'); 4972 this.setAttribute('aria-hidden', 'true');
8920 } 4973 }
8921
8922 // Defer any animation-related code on attached
8923 // (_openedChanged gets called again on attached).
8924 if (!this.isAttached) { 4974 if (!this.isAttached) {
8925 return; 4975 return;
8926 } 4976 }
8927
8928 this.__isAnimating = true; 4977 this.__isAnimating = true;
8929
8930 // Use requestAnimationFrame for non-blocking rendering.
8931 this.__onNextAnimationFrame(this.__openedChanged); 4978 this.__onNextAnimationFrame(this.__openedChanged);
8932 }, 4979 },
8933
8934 _canceledChanged: function() { 4980 _canceledChanged: function() {
8935 this.closingReason = this.closingReason || {}; 4981 this.closingReason = this.closingReason || {};
8936 this.closingReason.canceled = this.canceled; 4982 this.closingReason.canceled = this.canceled;
8937 }, 4983 },
8938
8939 _withBackdropChanged: function() { 4984 _withBackdropChanged: function() {
8940 // If tabindex is already set, no need to override it.
8941 if (this.withBackdrop && !this.hasAttribute('tabindex')) { 4985 if (this.withBackdrop && !this.hasAttribute('tabindex')) {
8942 this.setAttribute('tabindex', '-1'); 4986 this.setAttribute('tabindex', '-1');
8943 this.__shouldRemoveTabIndex = true; 4987 this.__shouldRemoveTabIndex = true;
8944 } else if (this.__shouldRemoveTabIndex) { 4988 } else if (this.__shouldRemoveTabIndex) {
8945 this.removeAttribute('tabindex'); 4989 this.removeAttribute('tabindex');
8946 this.__shouldRemoveTabIndex = false; 4990 this.__shouldRemoveTabIndex = false;
8947 } 4991 }
8948 if (this.opened && this.isAttached) { 4992 if (this.opened && this.isAttached) {
8949 this._manager.trackBackdrop(); 4993 this._manager.trackBackdrop();
8950 } 4994 }
8951 }, 4995 },
8952
8953 /**
8954 * tasks which must occur before opening; e.g. making the element visible.
8955 * @protected
8956 */
8957 _prepareRenderOpened: function() { 4996 _prepareRenderOpened: function() {
8958 // Store focused node.
8959 this.__restoreFocusNode = this._manager.deepActiveElement; 4997 this.__restoreFocusNode = this._manager.deepActiveElement;
8960
8961 // Needed to calculate the size of the overlay so that transitions on its size
8962 // will have the correct starting points.
8963 this._preparePositioning(); 4998 this._preparePositioning();
8964 this.refit(); 4999 this.refit();
8965 this._finishPositioning(); 5000 this._finishPositioning();
8966
8967 // Safari will apply the focus to the autofocus element when displayed
8968 // for the first time, so we make sure to return the focus where it was.
8969 if (this.noAutoFocus && document.activeElement === this._focusNode) { 5001 if (this.noAutoFocus && document.activeElement === this._focusNode) {
8970 this._focusNode.blur(); 5002 this._focusNode.blur();
8971 this.__restoreFocusNode.focus(); 5003 this.__restoreFocusNode.focus();
8972 } 5004 }
8973 }, 5005 },
8974
8975 /**
8976 * Tasks which cause the overlay to actually open; typically play an animati on.
8977 * @protected
8978 */
8979 _renderOpened: function() { 5006 _renderOpened: function() {
8980 this._finishRenderOpened(); 5007 this._finishRenderOpened();
8981 }, 5008 },
8982
8983 /**
8984 * Tasks which cause the overlay to actually close; typically play an animat ion.
8985 * @protected
8986 */
8987 _renderClosed: function() { 5009 _renderClosed: function() {
8988 this._finishRenderClosed(); 5010 this._finishRenderClosed();
8989 }, 5011 },
8990
8991 /**
8992 * Tasks to be performed at the end of open action. Will fire `iron-overlay- opened`.
8993 * @protected
8994 */
8995 _finishRenderOpened: function() { 5012 _finishRenderOpened: function() {
8996 this.notifyResize(); 5013 this.notifyResize();
8997 this.__isAnimating = false; 5014 this.__isAnimating = false;
8998
8999 // Store it so we don't query too much.
9000 var focusableNodes = this._focusableNodes; 5015 var focusableNodes = this._focusableNodes;
9001 this.__firstFocusableNode = focusableNodes[0]; 5016 this.__firstFocusableNode = focusableNodes[0];
9002 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; 5017 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
9003
9004 this.fire('iron-overlay-opened'); 5018 this.fire('iron-overlay-opened');
9005 }, 5019 },
9006
9007 /**
9008 * Tasks to be performed at the end of close action. Will fire `iron-overlay -closed`.
9009 * @protected
9010 */
9011 _finishRenderClosed: function() { 5020 _finishRenderClosed: function() {
9012 // Hide the overlay.
9013 this.style.display = 'none'; 5021 this.style.display = 'none';
9014 // Reset z-index only at the end of the animation.
9015 this.style.zIndex = ''; 5022 this.style.zIndex = '';
9016 this.notifyResize(); 5023 this.notifyResize();
9017 this.__isAnimating = false; 5024 this.__isAnimating = false;
9018 this.fire('iron-overlay-closed', this.closingReason); 5025 this.fire('iron-overlay-closed', this.closingReason);
9019 }, 5026 },
9020
9021 _preparePositioning: function() { 5027 _preparePositioning: function() {
9022 this.style.transition = this.style.webkitTransition = 'none'; 5028 this.style.transition = this.style.webkitTransition = 'none';
9023 this.style.transform = this.style.webkitTransform = 'none'; 5029 this.style.transform = this.style.webkitTransform = 'none';
9024 this.style.display = ''; 5030 this.style.display = '';
9025 }, 5031 },
9026
9027 _finishPositioning: function() { 5032 _finishPositioning: function() {
9028 // First, make it invisible & reactivate animations.
9029 this.style.display = 'none'; 5033 this.style.display = 'none';
9030 // Force reflow before re-enabling animations so that they don't start.
9031 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9032 this.scrollTop = this.scrollTop; 5034 this.scrollTop = this.scrollTop;
9033 this.style.transition = this.style.webkitTransition = ''; 5035 this.style.transition = this.style.webkitTransition = '';
9034 this.style.transform = this.style.webkitTransform = ''; 5036 this.style.transform = this.style.webkitTransform = '';
9035 // Now that animations are enabled, make it visible again
9036 this.style.display = ''; 5037 this.style.display = '';
9037 // Force reflow, so that following animations are properly started.
9038 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9039 this.scrollTop = this.scrollTop; 5038 this.scrollTop = this.scrollTop;
9040 }, 5039 },
9041
9042 /**
9043 * Applies focus according to the opened state.
9044 * @protected
9045 */
9046 _applyFocus: function() { 5040 _applyFocus: function() {
9047 if (this.opened) { 5041 if (this.opened) {
9048 if (!this.noAutoFocus) { 5042 if (!this.noAutoFocus) {
9049 this._focusNode.focus(); 5043 this._focusNode.focus();
9050 } 5044 }
9051 } 5045 } else {
9052 else {
9053 this._focusNode.blur(); 5046 this._focusNode.blur();
9054 this._focusedChild = null; 5047 this._focusedChild = null;
9055 // Restore focus.
9056 if (this.restoreFocusOnClose && this.__restoreFocusNode) { 5048 if (this.restoreFocusOnClose && this.__restoreFocusNode) {
9057 this.__restoreFocusNode.focus(); 5049 this.__restoreFocusNode.focus();
9058 } 5050 }
9059 this.__restoreFocusNode = null; 5051 this.__restoreFocusNode = null;
9060 // If many overlays get closed at the same time, one of them would still
9061 // be the currentOverlay even if already closed, and would call _applyFo cus
9062 // infinitely, so we check for this not to be the current overlay.
9063 var currentOverlay = this._manager.currentOverlay(); 5052 var currentOverlay = this._manager.currentOverlay();
9064 if (currentOverlay && this !== currentOverlay) { 5053 if (currentOverlay && this !== currentOverlay) {
9065 currentOverlay._applyFocus(); 5054 currentOverlay._applyFocus();
9066 } 5055 }
9067 } 5056 }
9068 }, 5057 },
9069
9070 /**
9071 * Cancels (closes) the overlay. Call when click happens outside the overlay .
9072 * @param {!Event} event
9073 * @protected
9074 */
9075 _onCaptureClick: function(event) { 5058 _onCaptureClick: function(event) {
9076 if (!this.noCancelOnOutsideClick) { 5059 if (!this.noCancelOnOutsideClick) {
9077 this.cancel(event); 5060 this.cancel(event);
9078 } 5061 }
9079 }, 5062 },
9080 5063 _onCaptureFocus: function(event) {
9081 /**
9082 * Keeps track of the focused child. If withBackdrop, traps focus within ove rlay.
9083 * @param {!Event} event
9084 * @protected
9085 */
9086 _onCaptureFocus: function (event) {
9087 if (!this.withBackdrop) { 5064 if (!this.withBackdrop) {
9088 return; 5065 return;
9089 } 5066 }
9090 var path = Polymer.dom(event).path; 5067 var path = Polymer.dom(event).path;
9091 if (path.indexOf(this) === -1) { 5068 if (path.indexOf(this) === -1) {
9092 event.stopPropagation(); 5069 event.stopPropagation();
9093 this._applyFocus(); 5070 this._applyFocus();
9094 } else { 5071 } else {
9095 this._focusedChild = path[0]; 5072 this._focusedChild = path[0];
9096 } 5073 }
9097 }, 5074 },
9098
9099 /**
9100 * Handles the ESC key event and cancels (closes) the overlay.
9101 * @param {!Event} event
9102 * @protected
9103 */
9104 _onCaptureEsc: function(event) { 5075 _onCaptureEsc: function(event) {
9105 if (!this.noCancelOnEscKey) { 5076 if (!this.noCancelOnEscKey) {
9106 this.cancel(event); 5077 this.cancel(event);
9107 } 5078 }
9108 }, 5079 },
9109
9110 /**
9111 * Handles TAB key events to track focus changes.
9112 * Will wrap focus for overlays withBackdrop.
9113 * @param {!Event} event
9114 * @protected
9115 */
9116 _onCaptureTab: function(event) { 5080 _onCaptureTab: function(event) {
9117 if (!this.withBackdrop) { 5081 if (!this.withBackdrop) {
9118 return; 5082 return;
9119 } 5083 }
9120 // TAB wraps from last to first focusable.
9121 // Shift + TAB wraps from first to last focusable.
9122 var shift = event.shiftKey; 5084 var shift = event.shiftKey;
9123 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node; 5085 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node;
9124 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de; 5086 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de;
9125 var shouldWrap = false; 5087 var shouldWrap = false;
9126 if (nodeToCheck === nodeToSet) { 5088 if (nodeToCheck === nodeToSet) {
9127 // If nodeToCheck is the same as nodeToSet, it means we have an overlay
9128 // with 0 or 1 focusables; in either case we still need to trap the
9129 // focus within the overlay.
9130 shouldWrap = true; 5089 shouldWrap = true;
9131 } else { 5090 } else {
9132 // In dom=shadow, the manager will receive focus changes on the main
9133 // root but not the ones within other shadow roots, so we can't rely on
9134 // _focusedChild, but we should check the deepest active element.
9135 var focusedNode = this._manager.deepActiveElement; 5091 var focusedNode = this._manager.deepActiveElement;
9136 // If the active element is not the nodeToCheck but the overlay itself, 5092 shouldWrap = focusedNode === nodeToCheck || focusedNode === this;
9137 // it means the focus is about to go outside the overlay, hence we
9138 // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
9139 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
9140 } 5093 }
9141
9142 if (shouldWrap) { 5094 if (shouldWrap) {
9143 // When the overlay contains the last focusable element of the document
9144 // and it's already focused, pressing TAB would move the focus outside
9145 // the document (e.g. to the browser search bar). Similarly, when the
9146 // overlay contains the first focusable element of the document and it's
9147 // already focused, pressing Shift+TAB would move the focus outside the
9148 // document (e.g. to the browser search bar).
9149 // In both cases, we would not receive a focus event, but only a blur.
9150 // In order to achieve focus wrapping, we prevent this TAB event and
9151 // force the focus. This will also prevent the focus to temporarily move
9152 // outside the overlay, which might cause scrolling.
9153 event.preventDefault(); 5095 event.preventDefault();
9154 this._focusedChild = nodeToSet; 5096 this._focusedChild = nodeToSet;
9155 this._applyFocus(); 5097 this._applyFocus();
9156 } 5098 }
9157 }, 5099 },
9158
9159 /**
9160 * Refits if the overlay is opened and not animating.
9161 * @protected
9162 */
9163 _onIronResize: function() { 5100 _onIronResize: function() {
9164 if (this.opened && !this.__isAnimating) { 5101 if (this.opened && !this.__isAnimating) {
9165 this.__onNextAnimationFrame(this.refit); 5102 this.__onNextAnimationFrame(this.refit);
9166 } 5103 }
9167 }, 5104 },
9168
9169 /**
9170 * Will call notifyResize if overlay is opened.
9171 * Can be overridden in order to avoid multiple observers on the same node.
9172 * @protected
9173 */
9174 _onNodesChange: function() { 5105 _onNodesChange: function() {
9175 if (this.opened && !this.__isAnimating) { 5106 if (this.opened && !this.__isAnimating) {
9176 this.notifyResize(); 5107 this.notifyResize();
9177 } 5108 }
9178 }, 5109 },
9179
9180 /**
9181 * Tasks executed when opened changes: prepare for the opening, move the
9182 * focus, update the manager, render opened/closed.
9183 * @private
9184 */
9185 __openedChanged: function() { 5110 __openedChanged: function() {
9186 if (this.opened) { 5111 if (this.opened) {
9187 // Make overlay visible, then add it to the manager.
9188 this._prepareRenderOpened(); 5112 this._prepareRenderOpened();
9189 this._manager.addOverlay(this); 5113 this._manager.addOverlay(this);
9190 // Move the focus to the child node with [autofocus].
9191 this._applyFocus(); 5114 this._applyFocus();
9192
9193 this._renderOpened(); 5115 this._renderOpened();
9194 } else { 5116 } else {
9195 // Remove overlay, then restore the focus before actually closing.
9196 this._manager.removeOverlay(this); 5117 this._manager.removeOverlay(this);
9197 this._applyFocus(); 5118 this._applyFocus();
9198
9199 this._renderClosed(); 5119 this._renderClosed();
9200 } 5120 }
9201 }, 5121 },
9202
9203 /**
9204 * Executes a callback on the next animation frame, overriding any previous
9205 * callback awaiting for the next animation frame. e.g.
9206 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
9207 * `callback1` will never be invoked.
9208 * @param {!Function} callback Its `this` parameter is the overlay itself.
9209 * @private
9210 */
9211 __onNextAnimationFrame: function(callback) { 5122 __onNextAnimationFrame: function(callback) {
9212 if (this.__raf) { 5123 if (this.__raf) {
9213 window.cancelAnimationFrame(this.__raf); 5124 window.cancelAnimationFrame(this.__raf);
9214 } 5125 }
9215 var self = this; 5126 var self = this;
9216 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { 5127 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
9217 self.__raf = null; 5128 self.__raf = null;
9218 callback.call(self); 5129 callback.call(self);
9219 }); 5130 });
9220 } 5131 }
9221
9222 }; 5132 };
9223 5133 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable Behavior, Polymer.IronOverlayBehaviorImpl ];
9224 /** @polymerBehavior */
9225 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB ehavior, Polymer.IronOverlayBehaviorImpl];
9226
9227 /**
9228 * Fired after the overlay opens.
9229 * @event iron-overlay-opened
9230 */
9231
9232 /**
9233 * Fired when the overlay is canceled, but before it is closed.
9234 * @event iron-overlay-canceled
9235 * @param {Event} event The closing of the overlay can be prevented
9236 * by calling `event.preventDefault()`. The `event.detail` is the original eve nt that
9237 * originated the canceling (e.g. ESC keyboard event or click event outside th e overlay).
9238 */
9239
9240 /**
9241 * Fired after the overlay closes.
9242 * @event iron-overlay-closed
9243 * @param {Event} event The `event.detail` is the `closingReason` property
9244 * (contains `canceled`, whether the overlay was canceled).
9245 */
9246
9247 })(); 5134 })();
9248 /** 5135
9249 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim ations for use with 5136 Polymer.NeonAnimatableBehavior = {
9250 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. 5137 properties: {
9251 * @polymerBehavior 5138 animationConfig: {
9252 */ 5139 type: Object
9253 Polymer.NeonAnimatableBehavior = { 5140 },
9254 5141 entryAnimation: {
9255 properties: { 5142 observer: '_entryAnimationChanged',
9256 5143 type: String
9257 /** 5144 },
9258 * Animation configuration. See README for more info. 5145 exitAnimation: {
9259 */ 5146 observer: '_exitAnimationChanged',
9260 animationConfig: { 5147 type: String
9261 type: Object 5148 }
9262 }, 5149 },
9263 5150 _entryAnimationChanged: function() {
9264 /** 5151 this.animationConfig = this.animationConfig || {};
9265 * Convenience property for setting an 'entry' animation. Do not set `anim ationConfig.entry` 5152 this.animationConfig['entry'] = [ {
9266 * manually if using this. The animated node is set to `this` if using thi s property. 5153 name: this.entryAnimation,
9267 */ 5154 node: this
9268 entryAnimation: { 5155 } ];
9269 observer: '_entryAnimationChanged', 5156 },
9270 type: String 5157 _exitAnimationChanged: function() {
9271 }, 5158 this.animationConfig = this.animationConfig || {};
9272 5159 this.animationConfig['exit'] = [ {
9273 /** 5160 name: this.exitAnimation,
9274 * Convenience property for setting an 'exit' animation. Do not set `anima tionConfig.exit` 5161 node: this
9275 * manually if using this. The animated node is set to `this` if using thi s property. 5162 } ];
9276 */ 5163 },
9277 exitAnimation: { 5164 _copyProperties: function(config1, config2) {
9278 observer: '_exitAnimationChanged', 5165 for (var property in config2) {
9279 type: String 5166 config1[property] = config2[property];
9280 } 5167 }
9281 5168 },
9282 }, 5169 _cloneConfig: function(config) {
9283 5170 var clone = {
9284 _entryAnimationChanged: function() { 5171 isClone: true
9285 this.animationConfig = this.animationConfig || {}; 5172 };
9286 this.animationConfig['entry'] = [{ 5173 this._copyProperties(clone, config);
9287 name: this.entryAnimation, 5174 return clone;
9288 node: this 5175 },
9289 }]; 5176 _getAnimationConfigRecursive: function(type, map, allConfigs) {
9290 }, 5177 if (!this.animationConfig) {
9291 5178 return;
9292 _exitAnimationChanged: function() { 5179 }
9293 this.animationConfig = this.animationConfig || {}; 5180 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun ction') {
9294 this.animationConfig['exit'] = [{ 5181 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid e of your components 'properties' object instead of outside of it."));
9295 name: this.exitAnimation, 5182 return;
9296 node: this 5183 }
9297 }]; 5184 var thisConfig;
9298 }, 5185 if (type) {
9299 5186 thisConfig = this.animationConfig[type];
9300 _copyProperties: function(config1, config2) { 5187 } else {
9301 // shallowly copy properties from config2 to config1 5188 thisConfig = this.animationConfig;
9302 for (var property in config2) { 5189 }
9303 config1[property] = config2[property]; 5190 if (!Array.isArray(thisConfig)) {
9304 } 5191 thisConfig = [ thisConfig ];
9305 }, 5192 }
9306 5193 if (thisConfig) {
9307 _cloneConfig: function(config) { 5194 for (var config, index = 0; config = thisConfig[index]; index++) {
9308 var clone = { 5195 if (config.animatable) {
9309 isClone: true 5196 config.animatable._getAnimationConfigRecursive(config.type || type, ma p, allConfigs);
9310 }; 5197 } else {
9311 this._copyProperties(clone, config); 5198 if (config.id) {
9312 return clone; 5199 var cachedConfig = map[config.id];
9313 }, 5200 if (cachedConfig) {
9314 5201 if (!cachedConfig.isClone) {
9315 _getAnimationConfigRecursive: function(type, map, allConfigs) { 5202 map[config.id] = this._cloneConfig(cachedConfig);
9316 if (!this.animationConfig) { 5203 cachedConfig = map[config.id];
9317 return;
9318 }
9319
9320 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu nction') {
9321 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins ide of your components 'properties' object instead of outside of it."));
9322 » return;
9323 }
9324
9325 // type is optional
9326 var thisConfig;
9327 if (type) {
9328 thisConfig = this.animationConfig[type];
9329 } else {
9330 thisConfig = this.animationConfig;
9331 }
9332
9333 if (!Array.isArray(thisConfig)) {
9334 thisConfig = [thisConfig];
9335 }
9336
9337 // iterate animations and recurse to process configurations from child nod es
9338 if (thisConfig) {
9339 for (var config, index = 0; config = thisConfig[index]; index++) {
9340 if (config.animatable) {
9341 config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
9342 } else {
9343 if (config.id) {
9344 var cachedConfig = map[config.id];
9345 if (cachedConfig) {
9346 // merge configurations with the same id, making a clone lazily
9347 if (!cachedConfig.isClone) {
9348 map[config.id] = this._cloneConfig(cachedConfig)
9349 cachedConfig = map[config.id];
9350 }
9351 this._copyProperties(cachedConfig, config);
9352 } else {
9353 // put any configs with an id into a map
9354 map[config.id] = config;
9355 } 5204 }
5205 this._copyProperties(cachedConfig, config);
9356 } else { 5206 } else {
9357 allConfigs.push(config); 5207 map[config.id] = config;
9358 }
9359 }
9360 }
9361 }
9362 },
9363
9364 /**
9365 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
9366 * an animation with an optional type. Elements implementing `Polymer.NeonAn imatableBehavior`
9367 * should define the property `animationConfig`, which is either a configura tion object
9368 * or a map of animation type to array of configuration objects.
9369 */
9370 getAnimationConfig: function(type) {
9371 var map = {};
9372 var allConfigs = [];
9373 this._getAnimationConfigRecursive(type, map, allConfigs);
9374 // append the configurations saved in the map to the array
9375 for (var key in map) {
9376 allConfigs.push(map[key]);
9377 }
9378 return allConfigs;
9379 }
9380
9381 };
9382 /**
9383 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
9384 *
9385 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
9386 */
9387 Polymer.NeonAnimationRunnerBehaviorImpl = {
9388
9389 _configureAnimations: function(configs) {
9390 var results = [];
9391 if (configs.length > 0) {
9392 for (var config, index = 0; config = configs[index]; index++) {
9393 var neonAnimation = document.createElement(config.name);
9394 // is this element actually a neon animation?
9395 if (neonAnimation.isNeonAnimation) {
9396 var result = null;
9397 // configuration or play could fail if polyfills aren't loaded
9398 try {
9399 result = neonAnimation.configure(config);
9400 // Check if we have an Effect rather than an Animation
9401 if (typeof result.cancel != 'function') {
9402 result = document.timeline.play(result);
9403 }
9404 } catch (e) {
9405 result = null;
9406 console.warn('Couldnt play', '(', config.name, ').', e);
9407 }
9408 if (result) {
9409 results.push({
9410 neonAnimation: neonAnimation,
9411 config: config,
9412 animation: result,
9413 });
9414 } 5208 }
9415 } else { 5209 } else {
9416 console.warn(this.is + ':', config.name, 'not found!'); 5210 allConfigs.push(config);
9417 } 5211 }
9418 } 5212 }
9419 } 5213 }
9420 return results; 5214 }
9421 }, 5215 },
9422 5216 getAnimationConfig: function(type) {
9423 _shouldComplete: function(activeEntries) { 5217 var map = {};
9424 var finished = true; 5218 var allConfigs = [];
9425 for (var i = 0; i < activeEntries.length; i++) { 5219 this._getAnimationConfigRecursive(type, map, allConfigs);
9426 if (activeEntries[i].animation.playState != 'finished') { 5220 for (var key in map) {
9427 finished = false; 5221 allConfigs.push(map[key]);
9428 break; 5222 }
9429 } 5223 return allConfigs;
9430 } 5224 }
9431 return finished; 5225 };
9432 }, 5226
9433 5227 Polymer.NeonAnimationRunnerBehaviorImpl = {
9434 _complete: function(activeEntries) { 5228 _configureAnimations: function(configs) {
9435 for (var i = 0; i < activeEntries.length; i++) { 5229 var results = [];
9436 activeEntries[i].neonAnimation.complete(activeEntries[i].config); 5230 if (configs.length > 0) {
9437 } 5231 for (var config, index = 0; config = configs[index]; index++) {
9438 for (var i = 0; i < activeEntries.length; i++) { 5232 var neonAnimation = document.createElement(config.name);
9439 activeEntries[i].animation.cancel(); 5233 if (neonAnimation.isNeonAnimation) {
9440 } 5234 var result = null;
9441 }, 5235 try {
9442 5236 result = neonAnimation.configure(config);
9443 /** 5237 if (typeof result.cancel != 'function') {
9444 * Plays an animation with an optional `type`. 5238 result = document.timeline.play(result);
9445 * @param {string=} type 5239 }
9446 * @param {!Object=} cookie 5240 } catch (e) {
9447 */ 5241 result = null;
9448 playAnimation: function(type, cookie) { 5242 console.warn('Couldnt play', '(', config.name, ').', e);
9449 var configs = this.getAnimationConfig(type); 5243 }
9450 if (!configs) { 5244 if (result) {
5245 results.push({
5246 neonAnimation: neonAnimation,
5247 config: config,
5248 animation: result
5249 });
5250 }
5251 } else {
5252 console.warn(this.is + ':', config.name, 'not found!');
5253 }
5254 }
5255 }
5256 return results;
5257 },
5258 _shouldComplete: function(activeEntries) {
5259 var finished = true;
5260 for (var i = 0; i < activeEntries.length; i++) {
5261 if (activeEntries[i].animation.playState != 'finished') {
5262 finished = false;
5263 break;
5264 }
5265 }
5266 return finished;
5267 },
5268 _complete: function(activeEntries) {
5269 for (var i = 0; i < activeEntries.length; i++) {
5270 activeEntries[i].neonAnimation.complete(activeEntries[i].config);
5271 }
5272 for (var i = 0; i < activeEntries.length; i++) {
5273 activeEntries[i].animation.cancel();
5274 }
5275 },
5276 playAnimation: function(type, cookie) {
5277 var configs = this.getAnimationConfig(type);
5278 if (!configs) {
5279 return;
5280 }
5281 this._active = this._active || {};
5282 if (this._active[type]) {
5283 this._complete(this._active[type]);
5284 delete this._active[type];
5285 }
5286 var activeEntries = this._configureAnimations(configs);
5287 if (activeEntries.length == 0) {
5288 this.fire('neon-animation-finish', cookie, {
5289 bubbles: false
5290 });
5291 return;
5292 }
5293 this._active[type] = activeEntries;
5294 for (var i = 0; i < activeEntries.length; i++) {
5295 activeEntries[i].animation.onfinish = function() {
5296 if (this._shouldComplete(activeEntries)) {
5297 this._complete(activeEntries);
5298 delete this._active[type];
5299 this.fire('neon-animation-finish', cookie, {
5300 bubbles: false
5301 });
5302 }
5303 }.bind(this);
5304 }
5305 },
5306 cancelAnimation: function() {
5307 for (var k in this._animations) {
5308 this._animations[k].cancel();
5309 }
5310 this._animations = {};
5311 }
5312 };
5313
5314 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer. NeonAnimationRunnerBehaviorImpl ];
5315
5316 Polymer.NeonAnimationBehavior = {
5317 properties: {
5318 animationTiming: {
5319 type: Object,
5320 value: function() {
5321 return {
5322 duration: 500,
5323 easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
5324 fill: 'both'
5325 };
5326 }
5327 }
5328 },
5329 isNeonAnimation: true,
5330 timingFromConfig: function(config) {
5331 if (config.timing) {
5332 for (var property in config.timing) {
5333 this.animationTiming[property] = config.timing[property];
5334 }
5335 }
5336 return this.animationTiming;
5337 },
5338 setPrefixedProperty: function(node, property, value) {
5339 var map = {
5340 transform: [ 'webkitTransform' ],
5341 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ]
5342 };
5343 var prefixes = map[property];
5344 for (var prefix, index = 0; prefix = prefixes[index]; index++) {
5345 node.style[prefix] = value;
5346 }
5347 node.style[property] = value;
5348 },
5349 complete: function() {}
5350 };
5351
5352 Polymer({
5353 is: 'opaque-animation',
5354 behaviors: [ Polymer.NeonAnimationBehavior ],
5355 configure: function(config) {
5356 var node = config.node;
5357 this._effect = new KeyframeEffect(node, [ {
5358 opacity: '1'
5359 }, {
5360 opacity: '1'
5361 } ], this.timingFromConfig(config));
5362 node.style.opacity = '0';
5363 return this._effect;
5364 },
5365 complete: function(config) {
5366 config.node.style.opacity = '';
5367 }
5368 });
5369
5370 (function() {
5371 'use strict';
5372 var LAST_TOUCH_POSITION = {
5373 pageX: 0,
5374 pageY: 0
5375 };
5376 var ROOT_TARGET = null;
5377 var SCROLLABLE_NODES = [];
5378 Polymer.IronDropdownScrollManager = {
5379 get currentLockingElement() {
5380 return this._lockingElements[this._lockingElements.length - 1];
5381 },
5382 elementIsScrollLocked: function(element) {
5383 var currentLockingElement = this.currentLockingElement;
5384 if (currentLockingElement === undefined) return false;
5385 var scrollLocked;
5386 if (this._hasCachedLockedElement(element)) {
5387 return true;
5388 }
5389 if (this._hasCachedUnlockedElement(element)) {
5390 return false;
5391 }
5392 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen t && !this._composedTreeContains(currentLockingElement, element);
5393 if (scrollLocked) {
5394 this._lockedElementCache.push(element);
5395 } else {
5396 this._unlockedElementCache.push(element);
5397 }
5398 return scrollLocked;
5399 },
5400 pushScrollLock: function(element) {
5401 if (this._lockingElements.indexOf(element) >= 0) {
9451 return; 5402 return;
9452 } 5403 }
9453 this._active = this._active || {}; 5404 if (this._lockingElements.length === 0) {
9454 if (this._active[type]) { 5405 this._lockScrollInteractions();
9455 this._complete(this._active[type]); 5406 }
9456 delete this._active[type]; 5407 this._lockingElements.push(element);
9457 } 5408 this._lockedElementCache = [];
9458 5409 this._unlockedElementCache = [];
9459 var activeEntries = this._configureAnimations(configs); 5410 },
9460 5411 removeScrollLock: function(element) {
9461 if (activeEntries.length == 0) { 5412 var index = this._lockingElements.indexOf(element);
9462 this.fire('neon-animation-finish', cookie, {bubbles: false}); 5413 if (index === -1) {
9463 return; 5414 return;
9464 } 5415 }
9465 5416 this._lockingElements.splice(index, 1);
9466 this._active[type] = activeEntries; 5417 this._lockedElementCache = [];
9467 5418 this._unlockedElementCache = [];
9468 for (var i = 0; i < activeEntries.length; i++) { 5419 if (this._lockingElements.length === 0) {
9469 activeEntries[i].animation.onfinish = function() { 5420 this._unlockScrollInteractions();
9470 if (this._shouldComplete(activeEntries)) { 5421 }
9471 this._complete(activeEntries); 5422 },
9472 delete this._active[type]; 5423 _lockingElements: [],
9473 this.fire('neon-animation-finish', cookie, {bubbles: false}); 5424 _lockedElementCache: null,
5425 _unlockedElementCache: null,
5426 _hasCachedLockedElement: function(element) {
5427 return this._lockedElementCache.indexOf(element) > -1;
5428 },
5429 _hasCachedUnlockedElement: function(element) {
5430 return this._unlockedElementCache.indexOf(element) > -1;
5431 },
5432 _composedTreeContains: function(element, child) {
5433 var contentElements;
5434 var distributedNodes;
5435 var contentIndex;
5436 var nodeIndex;
5437 if (element.contains(child)) {
5438 return true;
5439 }
5440 contentElements = Polymer.dom(element).querySelectorAll('content');
5441 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd ex) {
5442 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib utedNodes();
5443 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
5444 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
5445 return true;
9474 } 5446 }
9475 }.bind(this); 5447 }
9476 } 5448 }
9477 }, 5449 return false;
9478 5450 },
9479 /** 5451 _scrollInteractionHandler: function(event) {
9480 * Cancels the currently running animations. 5452 if (event.cancelable && this._shouldPreventScrolling(event)) {
9481 */ 5453 event.preventDefault();
9482 cancelAnimation: function() { 5454 }
9483 for (var k in this._animations) { 5455 if (event.targetTouches) {
9484 this._animations[k].cancel(); 5456 var touch = event.targetTouches[0];
9485 } 5457 LAST_TOUCH_POSITION.pageX = touch.pageX;
9486 this._animations = {}; 5458 LAST_TOUCH_POSITION.pageY = touch.pageY;
5459 }
5460 },
5461 _lockScrollInteractions: function() {
5462 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac tionHandler.bind(this);
5463 document.addEventListener('wheel', this._boundScrollHandler, true);
5464 document.addEventListener('mousewheel', this._boundScrollHandler, true);
5465 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true );
5466 document.addEventListener('touchstart', this._boundScrollHandler, true);
5467 document.addEventListener('touchmove', this._boundScrollHandler, true);
5468 },
5469 _unlockScrollInteractions: function() {
5470 document.removeEventListener('wheel', this._boundScrollHandler, true);
5471 document.removeEventListener('mousewheel', this._boundScrollHandler, true) ;
5472 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t rue);
5473 document.removeEventListener('touchstart', this._boundScrollHandler, true) ;
5474 document.removeEventListener('touchmove', this._boundScrollHandler, true);
5475 },
5476 _shouldPreventScrolling: function(event) {
5477 var target = Polymer.dom(event).rootTarget;
5478 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
5479 ROOT_TARGET = target;
5480 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
5481 }
5482 if (!SCROLLABLE_NODES.length) {
5483 return true;
5484 }
5485 if (event.type === 'touchstart') {
5486 return false;
5487 }
5488 var info = this._getScrollInfo(event);
5489 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY) ;
5490 },
5491 _getScrollableNodes: function(nodes) {
5492 var scrollables = [];
5493 var lockingIndex = nodes.indexOf(this.currentLockingElement);
5494 for (var i = 0; i <= lockingIndex; i++) {
5495 var node = nodes[i];
5496 if (node.nodeType === 11) {
5497 continue;
5498 }
5499 var style = node.style;
5500 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
5501 style = window.getComputedStyle(node);
5502 }
5503 if (style.overflow === 'scroll' || style.overflow === 'auto') {
5504 scrollables.push(node);
5505 }
5506 }
5507 return scrollables;
5508 },
5509 _getScrollingNode: function(nodes, deltaX, deltaY) {
5510 if (!deltaX && !deltaY) {
5511 return;
5512 }
5513 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
5514 for (var i = 0; i < nodes.length; i++) {
5515 var node = nodes[i];
5516 var canScroll = false;
5517 if (verticalScroll) {
5518 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc rollHeight - node.clientHeight;
5519 } else {
5520 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node. scrollWidth - node.clientWidth;
5521 }
5522 if (canScroll) {
5523 return node;
5524 }
5525 }
5526 },
5527 _getScrollInfo: function(event) {
5528 var info = {
5529 deltaX: event.deltaX,
5530 deltaY: event.deltaY
5531 };
5532 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) {
5533 info.deltaX = -event.wheelDeltaX;
5534 info.deltaY = -event.wheelDeltaY;
5535 } else if ('axis' in event) {
5536 info.deltaX = event.axis === 1 ? event.detail : 0;
5537 info.deltaY = event.axis === 2 ? event.detail : 0;
5538 } else if (event.targetTouches) {
5539 var touch = event.targetTouches[0];
5540 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
5541 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
5542 }
5543 return info;
9487 } 5544 }
9488 }; 5545 };
9489 5546 })();
9490 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ 5547
9491 Polymer.NeonAnimationRunnerBehavior = [ 5548 (function() {
9492 Polymer.NeonAnimatableBehavior, 5549 'use strict';
9493 Polymer.NeonAnimationRunnerBehaviorImpl 5550 Polymer({
9494 ]; 5551 is: 'iron-dropdown',
9495 /** 5552 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer .IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ],
9496 * Use `Polymer.NeonAnimationBehavior` to implement an animation.
9497 * @polymerBehavior
9498 */
9499 Polymer.NeonAnimationBehavior = {
9500
9501 properties: { 5553 properties: {
9502 5554 horizontalAlign: {
9503 /** 5555 type: String,
9504 * Defines the animation timing. 5556 value: 'left',
9505 */ 5557 reflectToAttribute: true
9506 animationTiming: { 5558 },
5559 verticalAlign: {
5560 type: String,
5561 value: 'top',
5562 reflectToAttribute: true
5563 },
5564 openAnimationConfig: {
5565 type: Object
5566 },
5567 closeAnimationConfig: {
5568 type: Object
5569 },
5570 focusTarget: {
5571 type: Object
5572 },
5573 noAnimations: {
5574 type: Boolean,
5575 value: false
5576 },
5577 allowOutsideScroll: {
5578 type: Boolean,
5579 value: false
5580 },
5581 _boundOnCaptureScroll: {
5582 type: Function,
5583 value: function() {
5584 return this._onCaptureScroll.bind(this);
5585 }
5586 }
5587 },
5588 listeners: {
5589 'neon-animation-finish': '_onNeonAnimationFinish'
5590 },
5591 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont alAlign, verticalOffset, horizontalOffset)' ],
5592 get containedElement() {
5593 return Polymer.dom(this.$.content).getDistributedNodes()[0];
5594 },
5595 get _focusTarget() {
5596 return this.focusTarget || this.containedElement;
5597 },
5598 ready: function() {
5599 this._scrollTop = 0;
5600 this._scrollLeft = 0;
5601 this._refitOnScrollRAF = null;
5602 },
5603 detached: function() {
5604 this.cancelAnimation();
5605 Polymer.IronDropdownScrollManager.removeScrollLock(this);
5606 },
5607 _openedChanged: function() {
5608 if (this.opened && this.disabled) {
5609 this.cancel();
5610 } else {
5611 this.cancelAnimation();
5612 this.sizingTarget = this.containedElement || this.sizingTarget;
5613 this._updateAnimationConfig();
5614 this._saveScrollPosition();
5615 if (this.opened) {
5616 document.addEventListener('scroll', this._boundOnCaptureScroll);
5617 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro llLock(this);
5618 } else {
5619 document.removeEventListener('scroll', this._boundOnCaptureScroll);
5620 Polymer.IronDropdownScrollManager.removeScrollLock(this);
5621 }
5622 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
5623 }
5624 },
5625 _renderOpened: function() {
5626 if (!this.noAnimations && this.animationConfig.open) {
5627 this.$.contentWrapper.classList.add('animating');
5628 this.playAnimation('open');
5629 } else {
5630 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
5631 }
5632 },
5633 _renderClosed: function() {
5634 if (!this.noAnimations && this.animationConfig.close) {
5635 this.$.contentWrapper.classList.add('animating');
5636 this.playAnimation('close');
5637 } else {
5638 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
5639 }
5640 },
5641 _onNeonAnimationFinish: function() {
5642 this.$.contentWrapper.classList.remove('animating');
5643 if (this.opened) {
5644 this._finishRenderOpened();
5645 } else {
5646 this._finishRenderClosed();
5647 }
5648 },
5649 _onCaptureScroll: function() {
5650 if (!this.allowOutsideScroll) {
5651 this._restoreScrollPosition();
5652 } else {
5653 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol lRAF);
5654 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th is));
5655 }
5656 },
5657 _saveScrollPosition: function() {
5658 if (document.scrollingElement) {
5659 this._scrollTop = document.scrollingElement.scrollTop;
5660 this._scrollLeft = document.scrollingElement.scrollLeft;
5661 } else {
5662 this._scrollTop = Math.max(document.documentElement.scrollTop, document. body.scrollTop);
5663 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen t.body.scrollLeft);
5664 }
5665 },
5666 _restoreScrollPosition: function() {
5667 if (document.scrollingElement) {
5668 document.scrollingElement.scrollTop = this._scrollTop;
5669 document.scrollingElement.scrollLeft = this._scrollLeft;
5670 } else {
5671 document.documentElement.scrollTop = this._scrollTop;
5672 document.documentElement.scrollLeft = this._scrollLeft;
5673 document.body.scrollTop = this._scrollTop;
5674 document.body.scrollLeft = this._scrollLeft;
5675 }
5676 },
5677 _updateAnimationConfig: function() {
5678 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati onConfig || []);
5679 for (var i = 0; i < animations.length; i++) {
5680 animations[i].node = this.containedElement;
5681 }
5682 this.animationConfig = {
5683 open: this.openAnimationConfig,
5684 close: this.closeAnimationConfig
5685 };
5686 },
5687 _updateOverlayPosition: function() {
5688 if (this.isAttached) {
5689 this.notifyResize();
5690 }
5691 },
5692 _applyFocus: function() {
5693 var focusTarget = this.focusTarget || this.containedElement;
5694 if (focusTarget && this.opened && !this.noAutoFocus) {
5695 focusTarget.focus();
5696 } else {
5697 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
5698 }
5699 }
5700 });
5701 })();
5702
5703 Polymer({
5704 is: 'fade-in-animation',
5705 behaviors: [ Polymer.NeonAnimationBehavior ],
5706 configure: function(config) {
5707 var node = config.node;
5708 this._effect = new KeyframeEffect(node, [ {
5709 opacity: '0'
5710 }, {
5711 opacity: '1'
5712 } ], this.timingFromConfig(config));
5713 return this._effect;
5714 }
5715 });
5716
5717 Polymer({
5718 is: 'fade-out-animation',
5719 behaviors: [ Polymer.NeonAnimationBehavior ],
5720 configure: function(config) {
5721 var node = config.node;
5722 this._effect = new KeyframeEffect(node, [ {
5723 opacity: '1'
5724 }, {
5725 opacity: '0'
5726 } ], this.timingFromConfig(config));
5727 return this._effect;
5728 }
5729 });
5730
5731 Polymer({
5732 is: 'paper-menu-grow-height-animation',
5733 behaviors: [ Polymer.NeonAnimationBehavior ],
5734 configure: function(config) {
5735 var node = config.node;
5736 var rect = node.getBoundingClientRect();
5737 var height = rect.height;
5738 this._effect = new KeyframeEffect(node, [ {
5739 height: height / 2 + 'px'
5740 }, {
5741 height: height + 'px'
5742 } ], this.timingFromConfig(config));
5743 return this._effect;
5744 }
5745 });
5746
5747 Polymer({
5748 is: 'paper-menu-grow-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 / 2 + 'px'
5756 }, {
5757 width: width + 'px'
5758 } ], this.timingFromConfig(config));
5759 return this._effect;
5760 }
5761 });
5762
5763 Polymer({
5764 is: 'paper-menu-shrink-width-animation',
5765 behaviors: [ Polymer.NeonAnimationBehavior ],
5766 configure: function(config) {
5767 var node = config.node;
5768 var rect = node.getBoundingClientRect();
5769 var width = rect.width;
5770 this._effect = new KeyframeEffect(node, [ {
5771 width: width + 'px'
5772 }, {
5773 width: width - width / 20 + 'px'
5774 } ], this.timingFromConfig(config));
5775 return this._effect;
5776 }
5777 });
5778
5779 Polymer({
5780 is: 'paper-menu-shrink-height-animation',
5781 behaviors: [ Polymer.NeonAnimationBehavior ],
5782 configure: function(config) {
5783 var node = config.node;
5784 var rect = node.getBoundingClientRect();
5785 var height = rect.height;
5786 var top = rect.top;
5787 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
5788 this._effect = new KeyframeEffect(node, [ {
5789 height: height + 'px',
5790 transform: 'translateY(0)'
5791 }, {
5792 height: height / 2 + 'px',
5793 transform: 'translateY(-20px)'
5794 } ], this.timingFromConfig(config));
5795 return this._effect;
5796 }
5797 });
5798
5799 (function() {
5800 'use strict';
5801 var config = {
5802 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
5803 MAX_ANIMATION_TIME_MS: 400
5804 };
5805 var PaperMenuButton = Polymer({
5806 is: 'paper-menu-button',
5807 behaviors: [ Polymer.IronA11yKeysBehavior, Polymer.IronControlState ],
5808 properties: {
5809 opened: {
5810 type: Boolean,
5811 value: false,
5812 notify: true,
5813 observer: '_openedChanged'
5814 },
5815 horizontalAlign: {
5816 type: String,
5817 value: 'left',
5818 reflectToAttribute: true
5819 },
5820 verticalAlign: {
5821 type: String,
5822 value: 'top',
5823 reflectToAttribute: true
5824 },
5825 dynamicAlign: {
5826 type: Boolean
5827 },
5828 horizontalOffset: {
5829 type: Number,
5830 value: 0,
5831 notify: true
5832 },
5833 verticalOffset: {
5834 type: Number,
5835 value: 0,
5836 notify: true
5837 },
5838 noOverlap: {
5839 type: Boolean
5840 },
5841 noAnimations: {
5842 type: Boolean,
5843 value: false
5844 },
5845 ignoreSelect: {
5846 type: Boolean,
5847 value: false
5848 },
5849 closeOnActivate: {
5850 type: Boolean,
5851 value: false
5852 },
5853 openAnimationConfig: {
9507 type: Object, 5854 type: Object,
9508 value: function() { 5855 value: function() {
9509 return { 5856 return [ {
9510 duration: 500, 5857 name: 'fade-in-animation',
9511 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', 5858 timing: {
9512 fill: 'both' 5859 delay: 100,
9513 } 5860 duration: 200
9514 } 5861 }
9515 } 5862 }, {
9516 5863 name: 'paper-menu-grow-width-animation',
9517 }, 5864 timing: {
9518 5865 delay: 100,
9519 /** 5866 duration: 150,
9520 * Can be used to determine that elements implement this behavior. 5867 easing: config.ANIMATION_CUBIC_BEZIER
9521 */ 5868 }
9522 isNeonAnimation: true, 5869 }, {
9523 5870 name: 'paper-menu-grow-height-animation',
9524 /** 5871 timing: {
9525 * Do any animation configuration here. 5872 delay: 100,
9526 */ 5873 duration: 275,
9527 // configure: function(config) { 5874 easing: config.ANIMATION_CUBIC_BEZIER
9528 // }, 5875 }
9529 5876 } ];
9530 /** 5877 }
9531 * Returns the animation timing by mixing in properties from `config` to the defaults defined 5878 },
9532 * by the animation. 5879 closeAnimationConfig: {
9533 */ 5880 type: Object,
9534 timingFromConfig: function(config) { 5881 value: function() {
9535 if (config.timing) { 5882 return [ {
9536 for (var property in config.timing) { 5883 name: 'fade-out-animation',
9537 this.animationTiming[property] = config.timing[property]; 5884 timing: {
9538 } 5885 duration: 150
9539 } 5886 }
9540 return this.animationTiming; 5887 }, {
9541 }, 5888 name: 'paper-menu-shrink-width-animation',
9542 5889 timing: {
9543 /** 5890 delay: 100,
9544 * Sets `transform` and `transformOrigin` properties along with the prefixed versions. 5891 duration: 50,
9545 */ 5892 easing: config.ANIMATION_CUBIC_BEZIER
9546 setPrefixedProperty: function(node, property, value) { 5893 }
9547 var map = { 5894 }, {
9548 'transform': ['webkitTransform'], 5895 name: 'paper-menu-shrink-height-animation',
9549 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] 5896 timing: {
9550 }; 5897 duration: 200,
9551 var prefixes = map[property]; 5898 easing: 'ease-in'
9552 for (var prefix, index = 0; prefix = prefixes[index]; index++) { 5899 }
9553 node.style[prefix] = value; 5900 } ];
9554 } 5901 }
9555 node.style[property] = value; 5902 },
9556 }, 5903 allowOutsideScroll: {
9557 5904 type: Boolean,
9558 /** 5905 value: false
9559 * Called when the animation finishes. 5906 },
9560 */ 5907 restoreFocusOnClose: {
9561 complete: function() {} 5908 type: Boolean,
9562 5909 value: true
9563 }; 5910 },
5911 _dropdownContent: {
5912 type: Object
5913 }
5914 },
5915 hostAttributes: {
5916 role: 'group',
5917 'aria-haspopup': 'true'
5918 },
5919 listeners: {
5920 'iron-activate': '_onIronActivate',
5921 'iron-select': '_onIronSelect'
5922 },
5923 get contentElement() {
5924 return Polymer.dom(this.$.content).getDistributedNodes()[0];
5925 },
5926 toggle: function() {
5927 if (this.opened) {
5928 this.close();
5929 } else {
5930 this.open();
5931 }
5932 },
5933 open: function() {
5934 if (this.disabled) {
5935 return;
5936 }
5937 this.$.dropdown.open();
5938 },
5939 close: function() {
5940 this.$.dropdown.close();
5941 },
5942 _onIronSelect: function(event) {
5943 if (!this.ignoreSelect) {
5944 this.close();
5945 }
5946 },
5947 _onIronActivate: function(event) {
5948 if (this.closeOnActivate) {
5949 this.close();
5950 }
5951 },
5952 _openedChanged: function(opened, oldOpened) {
5953 if (opened) {
5954 this._dropdownContent = this.contentElement;
5955 this.fire('paper-dropdown-open');
5956 } else if (oldOpened != null) {
5957 this.fire('paper-dropdown-close');
5958 }
5959 },
5960 _disabledChanged: function(disabled) {
5961 Polymer.IronControlState._disabledChanged.apply(this, arguments);
5962 if (disabled && this.opened) {
5963 this.close();
5964 }
5965 },
5966 __onIronOverlayCanceled: function(event) {
5967 var uiEvent = event.detail;
5968 var target = Polymer.dom(uiEvent).rootTarget;
5969 var trigger = this.$.trigger;
5970 var path = Polymer.dom(uiEvent).path;
5971 if (path.indexOf(trigger) > -1) {
5972 event.preventDefault();
5973 }
5974 }
5975 });
5976 Object.keys(config).forEach(function(key) {
5977 PaperMenuButton[key] = config[key];
5978 });
5979 Polymer.PaperMenuButton = PaperMenuButton;
5980 })();
5981
5982 Polymer.PaperInkyFocusBehaviorImpl = {
5983 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ],
5984 _focusedChanged: function(receivedFocusFromKeyboard) {
5985 if (receivedFocusFromKeyboard) {
5986 this.ensureRipple();
5987 }
5988 if (this.hasRipple()) {
5989 this._ripple.holdDown = receivedFocusFromKeyboard;
5990 }
5991 },
5992 _createRipple: function() {
5993 var ripple = Polymer.PaperRippleBehavior._createRipple();
5994 ripple.id = 'ink';
5995 ripple.setAttribute('center', '');
5996 ripple.classList.add('circle');
5997 return ripple;
5998 }
5999 };
6000
6001 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ];
6002
9564 Polymer({ 6003 Polymer({
9565 6004 is: 'paper-icon-button',
9566 is: 'opaque-animation', 6005 hostAttributes: {
9567 6006 role: 'button',
9568 behaviors: [ 6007 tabindex: '0'
9569 Polymer.NeonAnimationBehavior 6008 },
9570 ], 6009 behaviors: [ Polymer.PaperInkyFocusBehavior ],
9571 6010 properties: {
9572 configure: function(config) { 6011 src: {
9573 var node = config.node; 6012 type: String
9574 this._effect = new KeyframeEffect(node, [ 6013 },
9575 {'opacity': '1'}, 6014 icon: {
9576 {'opacity': '1'} 6015 type: String
9577 ], this.timingFromConfig(config)); 6016 },
9578 node.style.opacity = '0'; 6017 alt: {
9579 return this._effect; 6018 type: String,
9580 }, 6019 observer: "_altChanged"
9581 6020 }
9582 complete: function(config) { 6021 },
9583 config.node.style.opacity = ''; 6022 _altChanged: function(newValue, oldValue) {
9584 } 6023 var label = this.getAttribute('aria-label');
9585 6024 if (!label || oldValue == label) {
9586 }); 6025 this.setAttribute('aria-label', newValue);
9587 (function() { 6026 }
9588 'use strict'; 6027 }
9589 // Used to calculate the scroll direction during touch events. 6028 });
9590 var LAST_TOUCH_POSITION = { 6029
9591 pageX: 0,
9592 pageY: 0
9593 };
9594 // Used to avoid computing event.path and filter scrollable nodes (better pe rf).
9595 var ROOT_TARGET = null;
9596 var SCROLLABLE_NODES = [];
9597
9598 /**
9599 * The IronDropdownScrollManager is intended to provide a central source
9600 * of authority and control over which elements in a document are currently
9601 * allowed to scroll.
9602 */
9603
9604 Polymer.IronDropdownScrollManager = {
9605
9606 /**
9607 * The current element that defines the DOM boundaries of the
9608 * scroll lock. This is always the most recently locking element.
9609 */
9610 get currentLockingElement() {
9611 return this._lockingElements[this._lockingElements.length - 1];
9612 },
9613
9614 /**
9615 * Returns true if the provided element is "scroll locked", which is to
9616 * say that it cannot be scrolled via pointer or keyboard interactions.
9617 *
9618 * @param {HTMLElement} element An HTML element instance which may or may
9619 * not be scroll locked.
9620 */
9621 elementIsScrollLocked: function(element) {
9622 var currentLockingElement = this.currentLockingElement;
9623
9624 if (currentLockingElement === undefined)
9625 return false;
9626
9627 var scrollLocked;
9628
9629 if (this._hasCachedLockedElement(element)) {
9630 return true;
9631 }
9632
9633 if (this._hasCachedUnlockedElement(element)) {
9634 return false;
9635 }
9636
9637 scrollLocked = !!currentLockingElement &&
9638 currentLockingElement !== element &&
9639 !this._composedTreeContains(currentLockingElement, element);
9640
9641 if (scrollLocked) {
9642 this._lockedElementCache.push(element);
9643 } else {
9644 this._unlockedElementCache.push(element);
9645 }
9646
9647 return scrollLocked;
9648 },
9649
9650 /**
9651 * Push an element onto the current scroll lock stack. The most recently
9652 * pushed element and its children will be considered scrollable. All
9653 * other elements will not be scrollable.
9654 *
9655 * Scroll locking is implemented as a stack so that cases such as
9656 * dropdowns within dropdowns are handled well.
9657 *
9658 * @param {HTMLElement} element The element that should lock scroll.
9659 */
9660 pushScrollLock: function(element) {
9661 // Prevent pushing the same element twice
9662 if (this._lockingElements.indexOf(element) >= 0) {
9663 return;
9664 }
9665
9666 if (this._lockingElements.length === 0) {
9667 this._lockScrollInteractions();
9668 }
9669
9670 this._lockingElements.push(element);
9671
9672 this._lockedElementCache = [];
9673 this._unlockedElementCache = [];
9674 },
9675
9676 /**
9677 * Remove an element from the scroll lock stack. The element being
9678 * removed does not need to be the most recently pushed element. However,
9679 * the scroll lock constraints only change when the most recently pushed
9680 * element is removed.
9681 *
9682 * @param {HTMLElement} element The element to remove from the scroll
9683 * lock stack.
9684 */
9685 removeScrollLock: function(element) {
9686 var index = this._lockingElements.indexOf(element);
9687
9688 if (index === -1) {
9689 return;
9690 }
9691
9692 this._lockingElements.splice(index, 1);
9693
9694 this._lockedElementCache = [];
9695 this._unlockedElementCache = [];
9696
9697 if (this._lockingElements.length === 0) {
9698 this._unlockScrollInteractions();
9699 }
9700 },
9701
9702 _lockingElements: [],
9703
9704 _lockedElementCache: null,
9705
9706 _unlockedElementCache: null,
9707
9708 _hasCachedLockedElement: function(element) {
9709 return this._lockedElementCache.indexOf(element) > -1;
9710 },
9711
9712 _hasCachedUnlockedElement: function(element) {
9713 return this._unlockedElementCache.indexOf(element) > -1;
9714 },
9715
9716 _composedTreeContains: function(element, child) {
9717 // NOTE(cdata): This method iterates over content elements and their
9718 // corresponding distributed nodes to implement a contains-like method
9719 // that pierces through the composed tree of the ShadowDOM. Results of
9720 // this operation are cached (elsewhere) on a per-scroll-lock basis, to
9721 // guard against potentially expensive lookups happening repeatedly as
9722 // a user scrolls / touchmoves.
9723 var contentElements;
9724 var distributedNodes;
9725 var contentIndex;
9726 var nodeIndex;
9727
9728 if (element.contains(child)) {
9729 return true;
9730 }
9731
9732 contentElements = Polymer.dom(element).querySelectorAll('content');
9733
9734 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI ndex) {
9735
9736 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr ibutedNodes();
9737
9738 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
9739
9740 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
9741 return true;
9742 }
9743 }
9744 }
9745
9746 return false;
9747 },
9748
9749 _scrollInteractionHandler: function(event) {
9750 // Avoid canceling an event with cancelable=false, e.g. scrolling is in
9751 // progress and cannot be interrupted.
9752 if (event.cancelable && this._shouldPreventScrolling(event)) {
9753 event.preventDefault();
9754 }
9755 // If event has targetTouches (touch event), update last touch position.
9756 if (event.targetTouches) {
9757 var touch = event.targetTouches[0];
9758 LAST_TOUCH_POSITION.pageX = touch.pageX;
9759 LAST_TOUCH_POSITION.pageY = touch.pageY;
9760 }
9761 },
9762
9763 _lockScrollInteractions: function() {
9764 this._boundScrollHandler = this._boundScrollHandler ||
9765 this._scrollInteractionHandler.bind(this);
9766 // Modern `wheel` event for mouse wheel scrolling:
9767 document.addEventListener('wheel', this._boundScrollHandler, true);
9768 // Older, non-standard `mousewheel` event for some FF:
9769 document.addEventListener('mousewheel', this._boundScrollHandler, true);
9770 // IE:
9771 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr ue);
9772 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
9773 document.addEventListener('touchstart', this._boundScrollHandler, true);
9774 // Mobile devices can scroll on touch move:
9775 document.addEventListener('touchmove', this._boundScrollHandler, true);
9776 },
9777
9778 _unlockScrollInteractions: function() {
9779 document.removeEventListener('wheel', this._boundScrollHandler, true);
9780 document.removeEventListener('mousewheel', this._boundScrollHandler, tru e);
9781 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
9782 document.removeEventListener('touchstart', this._boundScrollHandler, tru e);
9783 document.removeEventListener('touchmove', this._boundScrollHandler, true );
9784 },
9785
9786 /**
9787 * Returns true if the event causes scroll outside the current locking
9788 * element, e.g. pointer/keyboard interactions, or scroll "leaking"
9789 * outside the locking element when it is already at its scroll boundaries .
9790 * @param {!Event} event
9791 * @return {boolean}
9792 * @private
9793 */
9794 _shouldPreventScrolling: function(event) {
9795
9796 // Update if root target changed. For touch events, ensure we don't
9797 // update during touchmove.
9798 var target = Polymer.dom(event).rootTarget;
9799 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
9800 ROOT_TARGET = target;
9801 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
9802 }
9803
9804 // Prevent event if no scrollable nodes.
9805 if (!SCROLLABLE_NODES.length) {
9806 return true;
9807 }
9808 // Don't prevent touchstart event inside the locking element when it has
9809 // scrollable nodes.
9810 if (event.type === 'touchstart') {
9811 return false;
9812 }
9813 // Get deltaX/Y.
9814 var info = this._getScrollInfo(event);
9815 // Prevent if there is no child that can scroll.
9816 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta Y);
9817 },
9818
9819 /**
9820 * Returns an array of scrollable nodes up to the current locking element,
9821 * which is included too if scrollable.
9822 * @param {!Array<Node>} nodes
9823 * @return {Array<Node>} scrollables
9824 * @private
9825 */
9826 _getScrollableNodes: function(nodes) {
9827 var scrollables = [];
9828 var lockingIndex = nodes.indexOf(this.currentLockingElement);
9829 // Loop from root target to locking element (included).
9830 for (var i = 0; i <= lockingIndex; i++) {
9831 var node = nodes[i];
9832 // Skip document fragments.
9833 if (node.nodeType === 11) {
9834 continue;
9835 }
9836 // Check inline style before checking computed style.
9837 var style = node.style;
9838 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
9839 style = window.getComputedStyle(node);
9840 }
9841 if (style.overflow === 'scroll' || style.overflow === 'auto') {
9842 scrollables.push(node);
9843 }
9844 }
9845 return scrollables;
9846 },
9847
9848 /**
9849 * Returns the node that is scrolling. If there is no scrolling,
9850 * returns undefined.
9851 * @param {!Array<Node>} nodes
9852 * @param {number} deltaX Scroll delta on the x-axis
9853 * @param {number} deltaY Scroll delta on the y-axis
9854 * @return {Node|undefined}
9855 * @private
9856 */
9857 _getScrollingNode: function(nodes, deltaX, deltaY) {
9858 // No scroll.
9859 if (!deltaX && !deltaY) {
9860 return;
9861 }
9862 // Check only one axis according to where there is more scroll.
9863 // Prefer vertical to horizontal.
9864 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
9865 for (var i = 0; i < nodes.length; i++) {
9866 var node = nodes[i];
9867 var canScroll = false;
9868 if (verticalScroll) {
9869 // delta < 0 is scroll up, delta > 0 is scroll down.
9870 canScroll = deltaY < 0 ? node.scrollTop > 0 :
9871 node.scrollTop < node.scrollHeight - node.clientHeight;
9872 } else {
9873 // delta < 0 is scroll left, delta > 0 is scroll right.
9874 canScroll = deltaX < 0 ? node.scrollLeft > 0 :
9875 node.scrollLeft < node.scrollWidth - node.clientWidth;
9876 }
9877 if (canScroll) {
9878 return node;
9879 }
9880 }
9881 },
9882
9883 /**
9884 * Returns scroll `deltaX` and `deltaY`.
9885 * @param {!Event} event The scroll event
9886 * @return {{
9887 * deltaX: number The x-axis scroll delta (positive: scroll right,
9888 * negative: scroll left, 0: no scroll),
9889 * deltaY: number The y-axis scroll delta (positive: scroll down,
9890 * negative: scroll up, 0: no scroll)
9891 * }} info
9892 * @private
9893 */
9894 _getScrollInfo: function(event) {
9895 var info = {
9896 deltaX: event.deltaX,
9897 deltaY: event.deltaY
9898 };
9899 // Already available.
9900 if ('deltaX' in event) {
9901 // do nothing, values are already good.
9902 }
9903 // Safari has scroll info in `wheelDeltaX/Y`.
9904 else if ('wheelDeltaX' in event) {
9905 info.deltaX = -event.wheelDeltaX;
9906 info.deltaY = -event.wheelDeltaY;
9907 }
9908 // Firefox has scroll info in `detail` and `axis`.
9909 else if ('axis' in event) {
9910 info.deltaX = event.axis === 1 ? event.detail : 0;
9911 info.deltaY = event.axis === 2 ? event.detail : 0;
9912 }
9913 // On mobile devices, calculate scroll direction.
9914 else if (event.targetTouches) {
9915 var touch = event.targetTouches[0];
9916 // Touch moves from right to left => scrolling goes right.
9917 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
9918 // Touch moves from down to up => scrolling goes down.
9919 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
9920 }
9921 return info;
9922 }
9923 };
9924 })();
9925 (function() {
9926 'use strict';
9927
9928 Polymer({
9929 is: 'iron-dropdown',
9930
9931 behaviors: [
9932 Polymer.IronControlState,
9933 Polymer.IronA11yKeysBehavior,
9934 Polymer.IronOverlayBehavior,
9935 Polymer.NeonAnimationRunnerBehavior
9936 ],
9937
9938 properties: {
9939 /**
9940 * The orientation against which to align the dropdown content
9941 * horizontally relative to the dropdown trigger.
9942 * Overridden from `Polymer.IronFitBehavior`.
9943 */
9944 horizontalAlign: {
9945 type: String,
9946 value: 'left',
9947 reflectToAttribute: true
9948 },
9949
9950 /**
9951 * The orientation against which to align the dropdown content
9952 * vertically relative to the dropdown trigger.
9953 * Overridden from `Polymer.IronFitBehavior`.
9954 */
9955 verticalAlign: {
9956 type: String,
9957 value: 'top',
9958 reflectToAttribute: true
9959 },
9960
9961 /**
9962 * An animation config. If provided, this will be used to animate the
9963 * opening of the dropdown.
9964 */
9965 openAnimationConfig: {
9966 type: Object
9967 },
9968
9969 /**
9970 * An animation config. If provided, this will be used to animate the
9971 * closing of the dropdown.
9972 */
9973 closeAnimationConfig: {
9974 type: Object
9975 },
9976
9977 /**
9978 * If provided, this will be the element that will be focused when
9979 * the dropdown opens.
9980 */
9981 focusTarget: {
9982 type: Object
9983 },
9984
9985 /**
9986 * Set to true to disable animations when opening and closing the
9987 * dropdown.
9988 */
9989 noAnimations: {
9990 type: Boolean,
9991 value: false
9992 },
9993
9994 /**
9995 * By default, the dropdown will constrain scrolling on the page
9996 * to itself when opened.
9997 * Set to true in order to prevent scroll from being constrained
9998 * to the dropdown when it opens.
9999 */
10000 allowOutsideScroll: {
10001 type: Boolean,
10002 value: false
10003 },
10004
10005 /**
10006 * Callback for scroll events.
10007 * @type {Function}
10008 * @private
10009 */
10010 _boundOnCaptureScroll: {
10011 type: Function,
10012 value: function() {
10013 return this._onCaptureScroll.bind(this);
10014 }
10015 }
10016 },
10017
10018 listeners: {
10019 'neon-animation-finish': '_onNeonAnimationFinish'
10020 },
10021
10022 observers: [
10023 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign , verticalOffset, horizontalOffset)'
10024 ],
10025
10026 /**
10027 * The element that is contained by the dropdown, if any.
10028 */
10029 get containedElement() {
10030 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10031 },
10032
10033 /**
10034 * The element that should be focused when the dropdown opens.
10035 * @deprecated
10036 */
10037 get _focusTarget() {
10038 return this.focusTarget || this.containedElement;
10039 },
10040
10041 ready: function() {
10042 // Memoized scrolling position, used to block scrolling outside.
10043 this._scrollTop = 0;
10044 this._scrollLeft = 0;
10045 // Used to perform a non-blocking refit on scroll.
10046 this._refitOnScrollRAF = null;
10047 },
10048
10049 detached: function() {
10050 this.cancelAnimation();
10051 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10052 },
10053
10054 /**
10055 * Called when the value of `opened` changes.
10056 * Overridden from `IronOverlayBehavior`
10057 */
10058 _openedChanged: function() {
10059 if (this.opened && this.disabled) {
10060 this.cancel();
10061 } else {
10062 this.cancelAnimation();
10063 this.sizingTarget = this.containedElement || this.sizingTarget;
10064 this._updateAnimationConfig();
10065 this._saveScrollPosition();
10066 if (this.opened) {
10067 document.addEventListener('scroll', this._boundOnCaptureScroll);
10068 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push ScrollLock(this);
10069 } else {
10070 document.removeEventListener('scroll', this._boundOnCaptureScroll) ;
10071 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10072 }
10073 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments );
10074 }
10075 },
10076
10077 /**
10078 * Overridden from `IronOverlayBehavior`.
10079 */
10080 _renderOpened: function() {
10081 if (!this.noAnimations && this.animationConfig.open) {
10082 this.$.contentWrapper.classList.add('animating');
10083 this.playAnimation('open');
10084 } else {
10085 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments) ;
10086 }
10087 },
10088
10089 /**
10090 * Overridden from `IronOverlayBehavior`.
10091 */
10092 _renderClosed: function() {
10093
10094 if (!this.noAnimations && this.animationConfig.close) {
10095 this.$.contentWrapper.classList.add('animating');
10096 this.playAnimation('close');
10097 } else {
10098 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments) ;
10099 }
10100 },
10101
10102 /**
10103 * Called when animation finishes on the dropdown (when opening or
10104 * closing). Responsible for "completing" the process of opening or
10105 * closing the dropdown by positioning it or setting its display to
10106 * none.
10107 */
10108 _onNeonAnimationFinish: function() {
10109 this.$.contentWrapper.classList.remove('animating');
10110 if (this.opened) {
10111 this._finishRenderOpened();
10112 } else {
10113 this._finishRenderClosed();
10114 }
10115 },
10116
10117 _onCaptureScroll: function() {
10118 if (!this.allowOutsideScroll) {
10119 this._restoreScrollPosition();
10120 } else {
10121 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS crollRAF);
10122 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin d(this));
10123 }
10124 },
10125
10126 /**
10127 * Memoizes the scroll position of the outside scrolling element.
10128 * @private
10129 */
10130 _saveScrollPosition: function() {
10131 if (document.scrollingElement) {
10132 this._scrollTop = document.scrollingElement.scrollTop;
10133 this._scrollLeft = document.scrollingElement.scrollLeft;
10134 } else {
10135 // Since we don't know if is the body or html, get max.
10136 this._scrollTop = Math.max(document.documentElement.scrollTop, docum ent.body.scrollTop);
10137 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc ument.body.scrollLeft);
10138 }
10139 },
10140
10141 /**
10142 * Resets the scroll position of the outside scrolling element.
10143 * @private
10144 */
10145 _restoreScrollPosition: function() {
10146 if (document.scrollingElement) {
10147 document.scrollingElement.scrollTop = this._scrollTop;
10148 document.scrollingElement.scrollLeft = this._scrollLeft;
10149 } else {
10150 // Since we don't know if is the body or html, set both.
10151 document.documentElement.scrollTop = this._scrollTop;
10152 document.documentElement.scrollLeft = this._scrollLeft;
10153 document.body.scrollTop = this._scrollTop;
10154 document.body.scrollLeft = this._scrollLeft;
10155 }
10156 },
10157
10158 /**
10159 * Constructs the final animation config from different properties used
10160 * to configure specific parts of the opening and closing animations.
10161 */
10162 _updateAnimationConfig: function() {
10163 var animations = (this.openAnimationConfig || []).concat(this.closeAni mationConfig || []);
10164 for (var i = 0; i < animations.length; i++) {
10165 animations[i].node = this.containedElement;
10166 }
10167 this.animationConfig = {
10168 open: this.openAnimationConfig,
10169 close: this.closeAnimationConfig
10170 };
10171 },
10172
10173 /**
10174 * Updates the overlay position based on configured horizontal
10175 * and vertical alignment.
10176 */
10177 _updateOverlayPosition: function() {
10178 if (this.isAttached) {
10179 // This triggers iron-resize, and iron-overlay-behavior will call re fit if needed.
10180 this.notifyResize();
10181 }
10182 },
10183
10184 /**
10185 * Apply focus to focusTarget or containedElement
10186 */
10187 _applyFocus: function () {
10188 var focusTarget = this.focusTarget || this.containedElement;
10189 if (focusTarget && this.opened && !this.noAutoFocus) {
10190 focusTarget.focus();
10191 } else {
10192 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
10193 }
10194 }
10195 });
10196 })();
10197 Polymer({
10198
10199 is: 'fade-in-animation',
10200
10201 behaviors: [
10202 Polymer.NeonAnimationBehavior
10203 ],
10204
10205 configure: function(config) {
10206 var node = config.node;
10207 this._effect = new KeyframeEffect(node, [
10208 {'opacity': '0'},
10209 {'opacity': '1'}
10210 ], this.timingFromConfig(config));
10211 return this._effect;
10212 }
10213
10214 });
10215 Polymer({
10216
10217 is: 'fade-out-animation',
10218
10219 behaviors: [
10220 Polymer.NeonAnimationBehavior
10221 ],
10222
10223 configure: function(config) {
10224 var node = config.node;
10225 this._effect = new KeyframeEffect(node, [
10226 {'opacity': '1'},
10227 {'opacity': '0'}
10228 ], this.timingFromConfig(config));
10229 return this._effect;
10230 }
10231
10232 });
10233 Polymer({
10234 is: 'paper-menu-grow-height-animation',
10235
10236 behaviors: [
10237 Polymer.NeonAnimationBehavior
10238 ],
10239
10240 configure: function(config) {
10241 var node = config.node;
10242 var rect = node.getBoundingClientRect();
10243 var height = rect.height;
10244
10245 this._effect = new KeyframeEffect(node, [{
10246 height: (height / 2) + 'px'
10247 }, {
10248 height: height + 'px'
10249 }], this.timingFromConfig(config));
10250
10251 return this._effect;
10252 }
10253 });
10254
10255 Polymer({
10256 is: 'paper-menu-grow-width-animation',
10257
10258 behaviors: [
10259 Polymer.NeonAnimationBehavior
10260 ],
10261
10262 configure: function(config) {
10263 var node = config.node;
10264 var rect = node.getBoundingClientRect();
10265 var width = rect.width;
10266
10267 this._effect = new KeyframeEffect(node, [{
10268 width: (width / 2) + 'px'
10269 }, {
10270 width: width + 'px'
10271 }], this.timingFromConfig(config));
10272
10273 return this._effect;
10274 }
10275 });
10276
10277 Polymer({
10278 is: 'paper-menu-shrink-width-animation',
10279
10280 behaviors: [
10281 Polymer.NeonAnimationBehavior
10282 ],
10283
10284 configure: function(config) {
10285 var node = config.node;
10286 var rect = node.getBoundingClientRect();
10287 var width = rect.width;
10288
10289 this._effect = new KeyframeEffect(node, [{
10290 width: width + 'px'
10291 }, {
10292 width: width - (width / 20) + 'px'
10293 }], this.timingFromConfig(config));
10294
10295 return this._effect;
10296 }
10297 });
10298
10299 Polymer({
10300 is: 'paper-menu-shrink-height-animation',
10301
10302 behaviors: [
10303 Polymer.NeonAnimationBehavior
10304 ],
10305
10306 configure: function(config) {
10307 var node = config.node;
10308 var rect = node.getBoundingClientRect();
10309 var height = rect.height;
10310 var top = rect.top;
10311
10312 this.setPrefixedProperty(node, 'transformOrigin', '0 0');
10313
10314 this._effect = new KeyframeEffect(node, [{
10315 height: height + 'px',
10316 transform: 'translateY(0)'
10317 }, {
10318 height: height / 2 + 'px',
10319 transform: 'translateY(-20px)'
10320 }], this.timingFromConfig(config));
10321
10322 return this._effect;
10323 }
10324 });
10325 (function() {
10326 'use strict';
10327
10328 var config = {
10329 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
10330 MAX_ANIMATION_TIME_MS: 400
10331 };
10332
10333 var PaperMenuButton = Polymer({
10334 is: 'paper-menu-button',
10335
10336 /**
10337 * Fired when the dropdown opens.
10338 *
10339 * @event paper-dropdown-open
10340 */
10341
10342 /**
10343 * Fired when the dropdown closes.
10344 *
10345 * @event paper-dropdown-close
10346 */
10347
10348 behaviors: [
10349 Polymer.IronA11yKeysBehavior,
10350 Polymer.IronControlState
10351 ],
10352
10353 properties: {
10354 /**
10355 * True if the content is currently displayed.
10356 */
10357 opened: {
10358 type: Boolean,
10359 value: false,
10360 notify: true,
10361 observer: '_openedChanged'
10362 },
10363
10364 /**
10365 * The orientation against which to align the menu dropdown
10366 * horizontally relative to the dropdown trigger.
10367 */
10368 horizontalAlign: {
10369 type: String,
10370 value: 'left',
10371 reflectToAttribute: true
10372 },
10373
10374 /**
10375 * The orientation against which to align the menu dropdown
10376 * vertically relative to the dropdown trigger.
10377 */
10378 verticalAlign: {
10379 type: String,
10380 value: 'top',
10381 reflectToAttribute: true
10382 },
10383
10384 /**
10385 * If true, the `horizontalAlign` and `verticalAlign` properties will
10386 * be considered preferences instead of strict requirements when
10387 * positioning the dropdown and may be changed if doing so reduces
10388 * the area of the dropdown falling outside of `fitInto`.
10389 */
10390 dynamicAlign: {
10391 type: Boolean
10392 },
10393
10394 /**
10395 * A pixel value that will be added to the position calculated for the
10396 * given `horizontalAlign`. Use a negative value to offset to the
10397 * left, or a positive value to offset to the right.
10398 */
10399 horizontalOffset: {
10400 type: Number,
10401 value: 0,
10402 notify: true
10403 },
10404
10405 /**
10406 * A pixel value that will be added to the position calculated for the
10407 * given `verticalAlign`. Use a negative value to offset towards the
10408 * top, or a positive value to offset towards the bottom.
10409 */
10410 verticalOffset: {
10411 type: Number,
10412 value: 0,
10413 notify: true
10414 },
10415
10416 /**
10417 * If true, the dropdown will be positioned so that it doesn't overlap
10418 * the button.
10419 */
10420 noOverlap: {
10421 type: Boolean
10422 },
10423
10424 /**
10425 * Set to true to disable animations when opening and closing the
10426 * dropdown.
10427 */
10428 noAnimations: {
10429 type: Boolean,
10430 value: false
10431 },
10432
10433 /**
10434 * Set to true to disable automatically closing the dropdown after
10435 * a selection has been made.
10436 */
10437 ignoreSelect: {
10438 type: Boolean,
10439 value: false
10440 },
10441
10442 /**
10443 * Set to true to enable automatically closing the dropdown after an
10444 * item has been activated, even if the selection did not change.
10445 */
10446 closeOnActivate: {
10447 type: Boolean,
10448 value: false
10449 },
10450
10451 /**
10452 * An animation config. If provided, this will be used to animate the
10453 * opening of the dropdown.
10454 */
10455 openAnimationConfig: {
10456 type: Object,
10457 value: function() {
10458 return [{
10459 name: 'fade-in-animation',
10460 timing: {
10461 delay: 100,
10462 duration: 200
10463 }
10464 }, {
10465 name: 'paper-menu-grow-width-animation',
10466 timing: {
10467 delay: 100,
10468 duration: 150,
10469 easing: config.ANIMATION_CUBIC_BEZIER
10470 }
10471 }, {
10472 name: 'paper-menu-grow-height-animation',
10473 timing: {
10474 delay: 100,
10475 duration: 275,
10476 easing: config.ANIMATION_CUBIC_BEZIER
10477 }
10478 }];
10479 }
10480 },
10481
10482 /**
10483 * An animation config. If provided, this will be used to animate the
10484 * closing of the dropdown.
10485 */
10486 closeAnimationConfig: {
10487 type: Object,
10488 value: function() {
10489 return [{
10490 name: 'fade-out-animation',
10491 timing: {
10492 duration: 150
10493 }
10494 }, {
10495 name: 'paper-menu-shrink-width-animation',
10496 timing: {
10497 delay: 100,
10498 duration: 50,
10499 easing: config.ANIMATION_CUBIC_BEZIER
10500 }
10501 }, {
10502 name: 'paper-menu-shrink-height-animation',
10503 timing: {
10504 duration: 200,
10505 easing: 'ease-in'
10506 }
10507 }];
10508 }
10509 },
10510
10511 /**
10512 * By default, the dropdown will constrain scrolling on the page
10513 * to itself when opened.
10514 * Set to true in order to prevent scroll from being constrained
10515 * to the dropdown when it opens.
10516 */
10517 allowOutsideScroll: {
10518 type: Boolean,
10519 value: false
10520 },
10521
10522 /**
10523 * Whether focus should be restored to the button when the menu closes .
10524 */
10525 restoreFocusOnClose: {
10526 type: Boolean,
10527 value: true
10528 },
10529
10530 /**
10531 * This is the element intended to be bound as the focus target
10532 * for the `iron-dropdown` contained by `paper-menu-button`.
10533 */
10534 _dropdownContent: {
10535 type: Object
10536 }
10537 },
10538
10539 hostAttributes: {
10540 role: 'group',
10541 'aria-haspopup': 'true'
10542 },
10543
10544 listeners: {
10545 'iron-activate': '_onIronActivate',
10546 'iron-select': '_onIronSelect'
10547 },
10548
10549 /**
10550 * The content element that is contained by the menu button, if any.
10551 */
10552 get contentElement() {
10553 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10554 },
10555
10556 /**
10557 * Toggles the drowpdown content between opened and closed.
10558 */
10559 toggle: function() {
10560 if (this.opened) {
10561 this.close();
10562 } else {
10563 this.open();
10564 }
10565 },
10566
10567 /**
10568 * Make the dropdown content appear as an overlay positioned relative
10569 * to the dropdown trigger.
10570 */
10571 open: function() {
10572 if (this.disabled) {
10573 return;
10574 }
10575
10576 this.$.dropdown.open();
10577 },
10578
10579 /**
10580 * Hide the dropdown content.
10581 */
10582 close: function() {
10583 this.$.dropdown.close();
10584 },
10585
10586 /**
10587 * When an `iron-select` event is received, the dropdown should
10588 * automatically close on the assumption that a value has been chosen.
10589 *
10590 * @param {CustomEvent} event A CustomEvent instance with type
10591 * set to `"iron-select"`.
10592 */
10593 _onIronSelect: function(event) {
10594 if (!this.ignoreSelect) {
10595 this.close();
10596 }
10597 },
10598
10599 /**
10600 * Closes the dropdown when an `iron-activate` event is received if
10601 * `closeOnActivate` is true.
10602 *
10603 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'.
10604 */
10605 _onIronActivate: function(event) {
10606 if (this.closeOnActivate) {
10607 this.close();
10608 }
10609 },
10610
10611 /**
10612 * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
10613 * When the dropdown closes, the `paper-menu-button` fires `paper-close` .
10614 *
10615 * @param {boolean} opened True if the dropdown is opened, otherwise fal se.
10616 * @param {boolean} oldOpened The previous value of `opened`.
10617 */
10618 _openedChanged: function(opened, oldOpened) {
10619 if (opened) {
10620 // TODO(cdata): Update this when we can measure changes in distribut ed
10621 // children in an idiomatic way.
10622 // We poke this property in case the element has changed. This will
10623 // cause the focus target for the `iron-dropdown` to be updated as
10624 // necessary:
10625 this._dropdownContent = this.contentElement;
10626 this.fire('paper-dropdown-open');
10627 } else if (oldOpened != null) {
10628 this.fire('paper-dropdown-close');
10629 }
10630 },
10631
10632 /**
10633 * If the dropdown is open when disabled becomes true, close the
10634 * dropdown.
10635 *
10636 * @param {boolean} disabled True if disabled, otherwise false.
10637 */
10638 _disabledChanged: function(disabled) {
10639 Polymer.IronControlState._disabledChanged.apply(this, arguments);
10640 if (disabled && this.opened) {
10641 this.close();
10642 }
10643 },
10644
10645 __onIronOverlayCanceled: function(event) {
10646 var uiEvent = event.detail;
10647 var target = Polymer.dom(uiEvent).rootTarget;
10648 var trigger = this.$.trigger;
10649 var path = Polymer.dom(uiEvent).path;
10650
10651 if (path.indexOf(trigger) > -1) {
10652 event.preventDefault();
10653 }
10654 }
10655 });
10656
10657 Object.keys(config).forEach(function (key) {
10658 PaperMenuButton[key] = config[key];
10659 });
10660
10661 Polymer.PaperMenuButton = PaperMenuButton;
10662 })();
10663 /**
10664 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus.
10665 *
10666 * @polymerBehavior Polymer.PaperInkyFocusBehavior
10667 */
10668 Polymer.PaperInkyFocusBehaviorImpl = {
10669 observers: [
10670 '_focusedChanged(receivedFocusFromKeyboard)'
10671 ],
10672
10673 _focusedChanged: function(receivedFocusFromKeyboard) {
10674 if (receivedFocusFromKeyboard) {
10675 this.ensureRipple();
10676 }
10677 if (this.hasRipple()) {
10678 this._ripple.holdDown = receivedFocusFromKeyboard;
10679 }
10680 },
10681
10682 _createRipple: function() {
10683 var ripple = Polymer.PaperRippleBehavior._createRipple();
10684 ripple.id = 'ink';
10685 ripple.setAttribute('center', '');
10686 ripple.classList.add('circle');
10687 return ripple;
10688 }
10689 };
10690
10691 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
10692 Polymer.PaperInkyFocusBehavior = [
10693 Polymer.IronButtonState,
10694 Polymer.IronControlState,
10695 Polymer.PaperRippleBehavior,
10696 Polymer.PaperInkyFocusBehaviorImpl
10697 ];
10698 Polymer({
10699 is: 'paper-icon-button',
10700
10701 hostAttributes: {
10702 role: 'button',
10703 tabindex: '0'
10704 },
10705
10706 behaviors: [
10707 Polymer.PaperInkyFocusBehavior
10708 ],
10709
10710 properties: {
10711 /**
10712 * The URL of an image for the icon. If the src property is specified,
10713 * the icon property should not be.
10714 */
10715 src: {
10716 type: String
10717 },
10718
10719 /**
10720 * Specifies the icon name or index in the set of icons available in
10721 * the icon's icon set. If the icon property is specified,
10722 * the src property should not be.
10723 */
10724 icon: {
10725 type: String
10726 },
10727
10728 /**
10729 * Specifies the alternate text for the button, for accessibility.
10730 */
10731 alt: {
10732 type: String,
10733 observer: "_altChanged"
10734 }
10735 },
10736
10737 _altChanged: function(newValue, oldValue) {
10738 var label = this.getAttribute('aria-label');
10739
10740 // Don't stomp over a user-set aria-label.
10741 if (!label || oldValue == label) {
10742 this.setAttribute('aria-label', newValue);
10743 }
10744 }
10745 });
10746 // Copyright 2016 The Chromium Authors. All rights reserved. 6030 // Copyright 2016 The Chromium Authors. All rights reserved.
10747 // Use of this source code is governed by a BSD-style license that can be 6031 // Use of this source code is governed by a BSD-style license that can be
10748 // found in the LICENSE file. 6032 // found in the LICENSE file.
10749
10750 /**
10751 * Implements an incremental search field which can be shown and hidden.
10752 * Canonical implementation is <cr-search-field>.
10753 * @polymerBehavior
10754 */
10755 var CrSearchFieldBehavior = { 6033 var CrSearchFieldBehavior = {
10756 properties: { 6034 properties: {
10757 label: { 6035 label: {
10758 type: String, 6036 type: String,
10759 value: '', 6037 value: ''
10760 }, 6038 },
10761
10762 clearLabel: { 6039 clearLabel: {
10763 type: String, 6040 type: String,
10764 value: '', 6041 value: ''
10765 }, 6042 },
10766
10767 showingSearch: { 6043 showingSearch: {
10768 type: Boolean, 6044 type: Boolean,
10769 value: false, 6045 value: false,
10770 notify: true, 6046 notify: true,
10771 observer: 'showingSearchChanged_', 6047 observer: 'showingSearchChanged_',
10772 reflectToAttribute: true 6048 reflectToAttribute: true
10773 }, 6049 },
10774
10775 /** @private */
10776 lastValue_: { 6050 lastValue_: {
10777 type: String, 6051 type: String,
10778 value: '', 6052 value: ''
10779 }, 6053 }
10780 }, 6054 },
10781
10782 /**
10783 * @abstract
10784 * @return {!HTMLInputElement} The input field element the behavior should
10785 * use.
10786 */
10787 getSearchInput: function() {}, 6055 getSearchInput: function() {},
10788
10789 /**
10790 * @return {string} The value of the search field.
10791 */
10792 getValue: function() { 6056 getValue: function() {
10793 return this.getSearchInput().value; 6057 return this.getSearchInput().value;
10794 }, 6058 },
10795
10796 /**
10797 * Sets the value of the search field.
10798 * @param {string} value
10799 */
10800 setValue: function(value) { 6059 setValue: function(value) {
10801 // Use bindValue when setting the input value so that changes propagate
10802 // correctly.
10803 this.getSearchInput().bindValue = value; 6060 this.getSearchInput().bindValue = value;
10804 this.onValueChanged_(value); 6061 this.onValueChanged_(value);
10805 }, 6062 },
10806
10807 showAndFocus: function() { 6063 showAndFocus: function() {
10808 this.showingSearch = true; 6064 this.showingSearch = true;
10809 this.focus_(); 6065 this.focus_();
10810 }, 6066 },
10811
10812 /** @private */
10813 focus_: function() { 6067 focus_: function() {
10814 this.getSearchInput().focus(); 6068 this.getSearchInput().focus();
10815 }, 6069 },
10816
10817 onSearchTermSearch: function() { 6070 onSearchTermSearch: function() {
10818 this.onValueChanged_(this.getValue()); 6071 this.onValueChanged_(this.getValue());
10819 }, 6072 },
10820
10821 /**
10822 * Updates the internal state of the search field based on a change that has
10823 * already happened.
10824 * @param {string} newValue
10825 * @private
10826 */
10827 onValueChanged_: function(newValue) { 6073 onValueChanged_: function(newValue) {
10828 if (newValue == this.lastValue_) 6074 if (newValue == this.lastValue_) return;
10829 return;
10830
10831 this.fire('search-changed', newValue); 6075 this.fire('search-changed', newValue);
10832 this.lastValue_ = newValue; 6076 this.lastValue_ = newValue;
10833 }, 6077 },
10834
10835 onSearchTermKeydown: function(e) { 6078 onSearchTermKeydown: function(e) {
10836 if (e.key == 'Escape') 6079 if (e.key == 'Escape') this.showingSearch = false;
10837 this.showingSearch = false; 6080 },
10838 },
10839
10840 /** @private */
10841 showingSearchChanged_: function() { 6081 showingSearchChanged_: function() {
10842 if (this.showingSearch) { 6082 if (this.showingSearch) {
10843 this.focus_(); 6083 this.focus_();
10844 return; 6084 return;
10845 } 6085 }
10846
10847 this.setValue(''); 6086 this.setValue('');
10848 this.getSearchInput().blur(); 6087 this.getSearchInput().blur();
10849 } 6088 }
10850 }; 6089 };
6090
10851 (function() { 6091 (function() {
10852 'use strict'; 6092 'use strict';
10853 6093 Polymer.IronA11yAnnouncer = Polymer({
10854 Polymer.IronA11yAnnouncer = Polymer({ 6094 is: 'iron-a11y-announcer',
10855 is: 'iron-a11y-announcer',
10856
10857 properties: {
10858
10859 /**
10860 * The value of mode is used to set the `aria-live` attribute
10861 * for the element that will be announced. Valid values are: `off`,
10862 * `polite` and `assertive`.
10863 */
10864 mode: {
10865 type: String,
10866 value: 'polite'
10867 },
10868
10869 _text: {
10870 type: String,
10871 value: ''
10872 }
10873 },
10874
10875 created: function() {
10876 if (!Polymer.IronA11yAnnouncer.instance) {
10877 Polymer.IronA11yAnnouncer.instance = this;
10878 }
10879
10880 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
10881 },
10882
10883 /**
10884 * Cause a text string to be announced by screen readers.
10885 *
10886 * @param {string} text The text that should be announced.
10887 */
10888 announce: function(text) {
10889 this._text = '';
10890 this.async(function() {
10891 this._text = text;
10892 }, 100);
10893 },
10894
10895 _onIronAnnounce: function(event) {
10896 if (event.detail && event.detail.text) {
10897 this.announce(event.detail.text);
10898 }
10899 }
10900 });
10901
10902 Polymer.IronA11yAnnouncer.instance = null;
10903
10904 Polymer.IronA11yAnnouncer.requestAvailability = function() {
10905 if (!Polymer.IronA11yAnnouncer.instance) {
10906 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
10907 }
10908
10909 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
10910 };
10911 })();
10912 /**
10913 * Singleton IronMeta instance.
10914 */
10915 Polymer.IronValidatableBehaviorMeta = null;
10916
10917 /**
10918 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
10919 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
10920 *
10921 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
10922 * To validate a form imperatively, call the form's `validate()` method, which in turn will
10923 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
10924 * custom element will get a public `validate()`, which
10925 * will return the validity of the element, and a corresponding `invalid` attr ibute,
10926 * which can be used for styling.
10927 *
10928 * To implement the custom validation logic of your element, you must override
10929 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
10930 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
10931 * for an example.
10932 *
10933 * ### Accessibility
10934 *
10935 * Changing the `invalid` property, either manually or by calling `validate()` will update the
10936 * `aria-invalid` attribute.
10937 *
10938 * @demo demo/index.html
10939 * @polymerBehavior
10940 */
10941 Polymer.IronValidatableBehavior = {
10942
10943 properties: { 6095 properties: {
10944 6096 mode: {
10945 /** 6097 type: String,
10946 * Name of the validator to use. 6098 value: 'polite'
10947 */
10948 validator: {
10949 type: String
10950 }, 6099 },
10951 6100 _text: {
10952 /**
10953 * True if the last call to `validate` is invalid.
10954 */
10955 invalid: {
10956 notify: true,
10957 reflectToAttribute: true,
10958 type: Boolean,
10959 value: false
10960 },
10961
10962 /**
10963 * This property is deprecated and should not be used. Use the global
10964 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
10965 */
10966 _validatorMeta: {
10967 type: Object
10968 },
10969
10970 /**
10971 * Namespace for this validator. This property is deprecated and should
10972 * not be used. For all intents and purposes, please consider it a
10973 * read-only, config-time property.
10974 */
10975 validatorType: {
10976 type: String,
10977 value: 'validator'
10978 },
10979
10980 _validator: {
10981 type: Object,
10982 computed: '__computeValidator(validator)'
10983 }
10984 },
10985
10986 observers: [
10987 '_invalidChanged(invalid)'
10988 ],
10989
10990 registered: function() {
10991 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
10992 },
10993
10994 _invalidChanged: function() {
10995 if (this.invalid) {
10996 this.setAttribute('aria-invalid', 'true');
10997 } else {
10998 this.removeAttribute('aria-invalid');
10999 }
11000 },
11001
11002 /**
11003 * @return {boolean} True if the validator `validator` exists.
11004 */
11005 hasValidator: function() {
11006 return this._validator != null;
11007 },
11008
11009 /**
11010 * Returns true if the `value` is valid, and updates `invalid`. If you want
11011 * your element to have custom validation logic, do not override this method ;
11012 * override `_getValidity(value)` instead.
11013
11014 * @param {Object} value The value to be validated. By default, it is passed
11015 * to the validator's `validate()` function, if a validator is set.
11016 * @return {boolean} True if `value` is valid.
11017 */
11018 validate: function(value) {
11019 this.invalid = !this._getValidity(value);
11020 return !this.invalid;
11021 },
11022
11023 /**
11024 * Returns true if `value` is valid. By default, it is passed
11025 * to the validator's `validate()` function, if a validator is set. You
11026 * should override this method if you want to implement custom validity
11027 * logic for your element.
11028 *
11029 * @param {Object} value The value to be validated.
11030 * @return {boolean} True if `value` is valid.
11031 */
11032
11033 _getValidity: function(value) {
11034 if (this.hasValidator()) {
11035 return this._validator.validate(value);
11036 }
11037 return true;
11038 },
11039
11040 __computeValidator: function() {
11041 return Polymer.IronValidatableBehaviorMeta &&
11042 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
11043 }
11044 };
11045 /*
11046 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
11047 to `<input>`.
11048
11049 ### Two-way binding
11050
11051 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
11052
11053 <input value="{{myValue::input}}">
11054
11055 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
11056 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
11057
11058 <input is="iron-input" bind-value="{{myValue}}">
11059
11060 ### Custom validators
11061
11062 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
11063
11064 <input is="iron-input" validator="my-custom-validator">
11065
11066 ### Stopping invalid input
11067
11068 It may be desirable to only allow users to enter certain characters. You can use the
11069 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
11070 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
11071
11072 \x3c!-- only allow characters that match [0-9] --\x3e
11073 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
11074
11075 @hero hero.svg
11076 @demo demo/index.html
11077 */
11078
11079 Polymer({
11080
11081 is: 'iron-input',
11082
11083 extends: 'input',
11084
11085 behaviors: [
11086 Polymer.IronValidatableBehavior
11087 ],
11088
11089 properties: {
11090
11091 /**
11092 * Use this property instead of `value` for two-way data binding.
11093 */
11094 bindValue: {
11095 observer: '_bindValueChanged',
11096 type: String
11097 },
11098
11099 /**
11100 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
11101 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
11102 * Pasted input will have each character checked individually; if any char acter
11103 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
11104 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
11105 */
11106 preventInvalidInput: {
11107 type: Boolean
11108 },
11109
11110 /**
11111 * Regular expression that list the characters allowed as input.
11112 * This pattern represents the allowed characters for the field; as the us er inputs text,
11113 * each individual character will be checked against the pattern (rather t han checking
11114 * the entire value as a whole). The recommended format should be a list o f allowed characters;
11115 * for example, `[a-zA-Z0-9.+-!;:]`
11116 */
11117 allowedPattern: {
11118 type: String,
11119 observer: "_allowedPatternChanged"
11120 },
11121
11122 _previousValidInput: {
11123 type: String, 6101 type: String,
11124 value: '' 6102 value: ''
11125 }, 6103 }
11126 6104 },
11127 _patternAlreadyChecked: {
11128 type: Boolean,
11129 value: false
11130 }
11131
11132 },
11133
11134 listeners: {
11135 'input': '_onInput',
11136 'keypress': '_onKeypress'
11137 },
11138
11139 /** @suppress {checkTypes} */
11140 registered: function() {
11141 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
11142 if (!this._canDispatchEventOnDisabled()) {
11143 this._origDispatchEvent = this.dispatchEvent;
11144 this.dispatchEvent = this._dispatchEventFirefoxIE;
11145 }
11146 },
11147
11148 created: function() { 6105 created: function() {
11149 Polymer.IronA11yAnnouncer.requestAvailability(); 6106 if (!Polymer.IronA11yAnnouncer.instance) {
11150 }, 6107 Polymer.IronA11yAnnouncer.instance = this;
11151 6108 }
11152 _canDispatchEventOnDisabled: function() { 6109 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind( this));
11153 var input = document.createElement('input'); 6110 },
11154 var canDispatch = false; 6111 announce: function(text) {
11155 input.disabled = true; 6112 this._text = '';
11156 6113 this.async(function() {
11157 input.addEventListener('feature-check-dispatch-event', function() { 6114 this._text = text;
11158 canDispatch = true; 6115 }, 100);
11159 }); 6116 },
11160 6117 _onIronAnnounce: function(event) {
11161 try { 6118 if (event.detail && event.detail.text) {
11162 input.dispatchEvent(new Event('feature-check-dispatch-event')); 6119 this.announce(event.detail.text);
11163 } catch(e) {} 6120 }
11164 6121 }
11165 return canDispatch; 6122 });
11166 }, 6123 Polymer.IronA11yAnnouncer.instance = null;
11167 6124 Polymer.IronA11yAnnouncer.requestAvailability = function() {
11168 _dispatchEventFirefoxIE: function() { 6125 if (!Polymer.IronA11yAnnouncer.instance) {
11169 // Due to Firefox bug, events fired on disabled form controls can throw 6126 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann ouncer');
11170 // errors; furthermore, neither IE nor Firefox will actually dispatch 6127 }
11171 // events from disabled form controls; as such, we toggle disable around 6128 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
11172 // the dispatch to allow notifying properties to notify 6129 };
11173 // See issue #47 for details 6130 })();
11174 var disabled = this.disabled; 6131
11175 this.disabled = false; 6132 Polymer.IronValidatableBehaviorMeta = null;
11176 this._origDispatchEvent.apply(this, arguments); 6133
11177 this.disabled = disabled; 6134 Polymer.IronValidatableBehavior = {
11178 }, 6135 properties: {
11179 6136 validator: {
11180 get _patternRegExp() { 6137 type: String
11181 var pattern; 6138 },
11182 if (this.allowedPattern) { 6139 invalid: {
11183 pattern = new RegExp(this.allowedPattern); 6140 notify: true,
6141 reflectToAttribute: true,
6142 type: Boolean,
6143 value: false
6144 },
6145 _validatorMeta: {
6146 type: Object
6147 },
6148 validatorType: {
6149 type: String,
6150 value: 'validator'
6151 },
6152 _validator: {
6153 type: Object,
6154 computed: '__computeValidator(validator)'
6155 }
6156 },
6157 observers: [ '_invalidChanged(invalid)' ],
6158 registered: function() {
6159 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({
6160 type: 'validator'
6161 });
6162 },
6163 _invalidChanged: function() {
6164 if (this.invalid) {
6165 this.setAttribute('aria-invalid', 'true');
6166 } else {
6167 this.removeAttribute('aria-invalid');
6168 }
6169 },
6170 hasValidator: function() {
6171 return this._validator != null;
6172 },
6173 validate: function(value) {
6174 this.invalid = !this._getValidity(value);
6175 return !this.invalid;
6176 },
6177 _getValidity: function(value) {
6178 if (this.hasValidator()) {
6179 return this._validator.validate(value);
6180 }
6181 return true;
6182 },
6183 __computeValidator: function() {
6184 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio rMeta.byKey(this.validator);
6185 }
6186 };
6187
6188 Polymer({
6189 is: 'iron-input',
6190 "extends": 'input',
6191 behaviors: [ Polymer.IronValidatableBehavior ],
6192 properties: {
6193 bindValue: {
6194 observer: '_bindValueChanged',
6195 type: String
6196 },
6197 preventInvalidInput: {
6198 type: Boolean
6199 },
6200 allowedPattern: {
6201 type: String,
6202 observer: "_allowedPatternChanged"
6203 },
6204 _previousValidInput: {
6205 type: String,
6206 value: ''
6207 },
6208 _patternAlreadyChecked: {
6209 type: Boolean,
6210 value: false
6211 }
6212 },
6213 listeners: {
6214 input: '_onInput',
6215 keypress: '_onKeypress'
6216 },
6217 registered: function() {
6218 if (!this._canDispatchEventOnDisabled()) {
6219 this._origDispatchEvent = this.dispatchEvent;
6220 this.dispatchEvent = this._dispatchEventFirefoxIE;
6221 }
6222 },
6223 created: function() {
6224 Polymer.IronA11yAnnouncer.requestAvailability();
6225 },
6226 _canDispatchEventOnDisabled: function() {
6227 var input = document.createElement('input');
6228 var canDispatch = false;
6229 input.disabled = true;
6230 input.addEventListener('feature-check-dispatch-event', function() {
6231 canDispatch = true;
6232 });
6233 try {
6234 input.dispatchEvent(new Event('feature-check-dispatch-event'));
6235 } catch (e) {}
6236 return canDispatch;
6237 },
6238 _dispatchEventFirefoxIE: function() {
6239 var disabled = this.disabled;
6240 this.disabled = false;
6241 this._origDispatchEvent.apply(this, arguments);
6242 this.disabled = disabled;
6243 },
6244 get _patternRegExp() {
6245 var pattern;
6246 if (this.allowedPattern) {
6247 pattern = new RegExp(this.allowedPattern);
6248 } else {
6249 switch (this.type) {
6250 case 'number':
6251 pattern = /[0-9.,e-]/;
6252 break;
6253 }
6254 }
6255 return pattern;
6256 },
6257 ready: function() {
6258 this.bindValue = this.value;
6259 },
6260 _bindValueChanged: function() {
6261 if (this.value !== this.bindValue) {
6262 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue == = false) ? '' : this.bindValue;
6263 }
6264 this.fire('bind-value-changed', {
6265 value: this.bindValue
6266 });
6267 },
6268 _allowedPatternChanged: function() {
6269 this.preventInvalidInput = this.allowedPattern ? true : false;
6270 },
6271 _onInput: function() {
6272 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
6273 var valid = this._checkPatternValidity();
6274 if (!valid) {
6275 this._announceInvalidCharacter('Invalid string of characters not entered .');
6276 this.value = this._previousValidInput;
6277 }
6278 }
6279 this.bindValue = this.value;
6280 this._previousValidInput = this.value;
6281 this._patternAlreadyChecked = false;
6282 },
6283 _isPrintable: function(event) {
6284 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC ode == 13 || event.keyCode == 27;
6285 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;
6286 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
6287 },
6288 _onKeypress: function(event) {
6289 if (!this.preventInvalidInput && this.type !== 'number') {
6290 return;
6291 }
6292 var regexp = this._patternRegExp;
6293 if (!regexp) {
6294 return;
6295 }
6296 if (event.metaKey || event.ctrlKey || event.altKey) return;
6297 this._patternAlreadyChecked = true;
6298 var thisChar = String.fromCharCode(event.charCode);
6299 if (this._isPrintable(event) && !regexp.test(thisChar)) {
6300 event.preventDefault();
6301 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent ered.');
6302 }
6303 },
6304 _checkPatternValidity: function() {
6305 var regexp = this._patternRegExp;
6306 if (!regexp) {
6307 return true;
6308 }
6309 for (var i = 0; i < this.value.length; i++) {
6310 if (!regexp.test(this.value[i])) {
6311 return false;
6312 }
6313 }
6314 return true;
6315 },
6316 validate: function() {
6317 var valid = this.checkValidity();
6318 if (valid) {
6319 if (this.required && this.value === '') {
6320 valid = false;
6321 } else if (this.hasValidator()) {
6322 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
6323 }
6324 }
6325 this.invalid = !valid;
6326 this.fire('iron-input-validate');
6327 return valid;
6328 },
6329 _announceInvalidCharacter: function(message) {
6330 this.fire('iron-announce', {
6331 text: message
6332 });
6333 }
6334 });
6335
6336 Polymer({
6337 is: 'paper-input-container',
6338 properties: {
6339 noLabelFloat: {
6340 type: Boolean,
6341 value: false
6342 },
6343 alwaysFloatLabel: {
6344 type: Boolean,
6345 value: false
6346 },
6347 attrForValue: {
6348 type: String,
6349 value: 'bind-value'
6350 },
6351 autoValidate: {
6352 type: Boolean,
6353 value: false
6354 },
6355 invalid: {
6356 observer: '_invalidChanged',
6357 type: Boolean,
6358 value: false
6359 },
6360 focused: {
6361 readOnly: true,
6362 type: Boolean,
6363 value: false,
6364 notify: true
6365 },
6366 _addons: {
6367 type: Array
6368 },
6369 _inputHasContent: {
6370 type: Boolean,
6371 value: false
6372 },
6373 _inputSelector: {
6374 type: String,
6375 value: 'input,textarea,.paper-input-input'
6376 },
6377 _boundOnFocus: {
6378 type: Function,
6379 value: function() {
6380 return this._onFocus.bind(this);
6381 }
6382 },
6383 _boundOnBlur: {
6384 type: Function,
6385 value: function() {
6386 return this._onBlur.bind(this);
6387 }
6388 },
6389 _boundOnInput: {
6390 type: Function,
6391 value: function() {
6392 return this._onInput.bind(this);
6393 }
6394 },
6395 _boundValueChanged: {
6396 type: Function,
6397 value: function() {
6398 return this._onValueChanged.bind(this);
6399 }
6400 }
6401 },
6402 listeners: {
6403 'addon-attached': '_onAddonAttached',
6404 'iron-input-validate': '_onIronInputValidate'
6405 },
6406 get _valueChangedEvent() {
6407 return this.attrForValue + '-changed';
6408 },
6409 get _propertyForValue() {
6410 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
6411 },
6412 get _inputElement() {
6413 return Polymer.dom(this).querySelector(this._inputSelector);
6414 },
6415 get _inputElementValue() {
6416 return this._inputElement[this._propertyForValue] || this._inputElement.valu e;
6417 },
6418 ready: function() {
6419 if (!this._addons) {
6420 this._addons = [];
6421 }
6422 this.addEventListener('focus', this._boundOnFocus, true);
6423 this.addEventListener('blur', this._boundOnBlur, true);
6424 },
6425 attached: function() {
6426 if (this.attrForValue) {
6427 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa lueChanged);
6428 } else {
6429 this.addEventListener('input', this._onInput);
6430 }
6431 if (this._inputElementValue != '') {
6432 this._handleValueAndAutoValidate(this._inputElement);
6433 } else {
6434 this._handleValue(this._inputElement);
6435 }
6436 },
6437 _onAddonAttached: function(event) {
6438 if (!this._addons) {
6439 this._addons = [];
6440 }
6441 var target = event.target;
6442 if (this._addons.indexOf(target) === -1) {
6443 this._addons.push(target);
6444 if (this.isAttached) {
6445 this._handleValue(this._inputElement);
6446 }
6447 }
6448 },
6449 _onFocus: function() {
6450 this._setFocused(true);
6451 },
6452 _onBlur: function() {
6453 this._setFocused(false);
6454 this._handleValueAndAutoValidate(this._inputElement);
6455 },
6456 _onInput: function(event) {
6457 this._handleValueAndAutoValidate(event.target);
6458 },
6459 _onValueChanged: function(event) {
6460 this._handleValueAndAutoValidate(event.target);
6461 },
6462 _handleValue: function(inputElement) {
6463 var value = this._inputElementValue;
6464 if (value || value === 0 || inputElement.type === 'number' && !inputElement. checkValidity()) {
6465 this._inputHasContent = true;
6466 } else {
6467 this._inputHasContent = false;
6468 }
6469 this.updateAddons({
6470 inputElement: inputElement,
6471 value: value,
6472 invalid: this.invalid
6473 });
6474 },
6475 _handleValueAndAutoValidate: function(inputElement) {
6476 if (this.autoValidate) {
6477 var valid;
6478 if (inputElement.validate) {
6479 valid = inputElement.validate(this._inputElementValue);
11184 } else { 6480 } else {
11185 switch (this.type) { 6481 valid = inputElement.checkValidity();
11186 case 'number': 6482 }
11187 pattern = /[0-9.,e-]/;
11188 break;
11189 }
11190 }
11191 return pattern;
11192 },
11193
11194 ready: function() {
11195 this.bindValue = this.value;
11196 },
11197
11198 /**
11199 * @suppress {checkTypes}
11200 */
11201 _bindValueChanged: function() {
11202 if (this.value !== this.bindValue) {
11203 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
11204 }
11205 // manually notify because we don't want to notify until after setting val ue
11206 this.fire('bind-value-changed', {value: this.bindValue});
11207 },
11208
11209 _allowedPatternChanged: function() {
11210 // Force to prevent invalid input when an `allowed-pattern` is set
11211 this.preventInvalidInput = this.allowedPattern ? true : false;
11212 },
11213
11214 _onInput: function() {
11215 // Need to validate each of the characters pasted if they haven't
11216 // been validated inside `_onKeypress` already.
11217 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
11218 var valid = this._checkPatternValidity();
11219 if (!valid) {
11220 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
11221 this.value = this._previousValidInput;
11222 }
11223 }
11224
11225 this.bindValue = this.value;
11226 this._previousValidInput = this.value;
11227 this._patternAlreadyChecked = false;
11228 },
11229
11230 _isPrintable: function(event) {
11231 // What a control/printable character is varies wildly based on the browse r.
11232 // - most control characters (arrows, backspace) do not send a `keypress` event
11233 // in Chrome, but the *do* on Firefox
11234 // - in Firefox, when they do send a `keypress` event, control chars have
11235 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
11236 // - printable characters always send a keypress event.
11237 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
11238 // always matches the charCode.
11239 // None of this makes any sense.
11240
11241 // For these keys, ASCII code == browser keycode.
11242 var anyNonPrintable =
11243 (event.keyCode == 8) || // backspace
11244 (event.keyCode == 9) || // tab
11245 (event.keyCode == 13) || // enter
11246 (event.keyCode == 27); // escape
11247
11248 // For these keys, make sure it's a browser keycode and not an ASCII code.
11249 var mozNonPrintable =
11250 (event.keyCode == 19) || // pause
11251 (event.keyCode == 20) || // caps lock
11252 (event.keyCode == 45) || // insert
11253 (event.keyCode == 46) || // delete
11254 (event.keyCode == 144) || // num lock
11255 (event.keyCode == 145) || // scroll lock
11256 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
11257 (event.keyCode > 111 && event.keyCode < 124); // fn keys
11258
11259 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
11260 },
11261
11262 _onKeypress: function(event) {
11263 if (!this.preventInvalidInput && this.type !== 'number') {
11264 return;
11265 }
11266 var regexp = this._patternRegExp;
11267 if (!regexp) {
11268 return;
11269 }
11270
11271 // Handle special keys and backspace
11272 if (event.metaKey || event.ctrlKey || event.altKey)
11273 return;
11274
11275 // Check the pattern either here or in `_onInput`, but not in both.
11276 this._patternAlreadyChecked = true;
11277
11278 var thisChar = String.fromCharCode(event.charCode);
11279 if (this._isPrintable(event) && !regexp.test(thisChar)) {
11280 event.preventDefault();
11281 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
11282 }
11283 },
11284
11285 _checkPatternValidity: function() {
11286 var regexp = this._patternRegExp;
11287 if (!regexp) {
11288 return true;
11289 }
11290 for (var i = 0; i < this.value.length; i++) {
11291 if (!regexp.test(this.value[i])) {
11292 return false;
11293 }
11294 }
11295 return true;
11296 },
11297
11298 /**
11299 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
11300 * then any constraints.
11301 * @return {boolean} True if the value is valid.
11302 */
11303 validate: function() {
11304 // First, check what the browser thinks. Some inputs (like type=number)
11305 // behave weirdly and will set the value to "" if something invalid is
11306 // entered, but will set the validity correctly.
11307 var valid = this.checkValidity();
11308
11309 // Only do extra checking if the browser thought this was valid.
11310 if (valid) {
11311 // Empty, required input is invalid
11312 if (this.required && this.value === '') {
11313 valid = false;
11314 } else if (this.hasValidator()) {
11315 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
11316 }
11317 }
11318
11319 this.invalid = !valid; 6483 this.invalid = !valid;
11320 this.fire('iron-input-validate'); 6484 }
11321 return valid; 6485 this._handleValue(inputElement);
11322 }, 6486 },
11323 6487 _onIronInputValidate: function(event) {
11324 _announceInvalidCharacter: function(message) { 6488 this.invalid = this._inputElement.invalid;
11325 this.fire('iron-announce', { text: message }); 6489 },
11326 } 6490 _invalidChanged: function() {
11327 }); 6491 if (this._addons) {
11328
11329 /*
11330 The `iron-input-validate` event is fired whenever `validate()` is called.
11331 @event iron-input-validate
11332 */
11333 Polymer({
11334 is: 'paper-input-container',
11335
11336 properties: {
11337 /**
11338 * Set to true to disable the floating label. The label disappears when th e input value is
11339 * not null.
11340 */
11341 noLabelFloat: {
11342 type: Boolean,
11343 value: false
11344 },
11345
11346 /**
11347 * Set to true to always float the floating label.
11348 */
11349 alwaysFloatLabel: {
11350 type: Boolean,
11351 value: false
11352 },
11353
11354 /**
11355 * The attribute to listen for value changes on.
11356 */
11357 attrForValue: {
11358 type: String,
11359 value: 'bind-value'
11360 },
11361
11362 /**
11363 * Set to true to auto-validate the input value when it changes.
11364 */
11365 autoValidate: {
11366 type: Boolean,
11367 value: false
11368 },
11369
11370 /**
11371 * True if the input is invalid. This property is set automatically when t he input value
11372 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
11373 */
11374 invalid: {
11375 observer: '_invalidChanged',
11376 type: Boolean,
11377 value: false
11378 },
11379
11380 /**
11381 * True if the input has focus.
11382 */
11383 focused: {
11384 readOnly: true,
11385 type: Boolean,
11386 value: false,
11387 notify: true
11388 },
11389
11390 _addons: {
11391 type: Array
11392 // do not set a default value here intentionally - it will be initialize d lazily when a
11393 // distributed child is attached, which may occur before configuration f or this element
11394 // in polyfill.
11395 },
11396
11397 _inputHasContent: {
11398 type: Boolean,
11399 value: false
11400 },
11401
11402 _inputSelector: {
11403 type: String,
11404 value: 'input,textarea,.paper-input-input'
11405 },
11406
11407 _boundOnFocus: {
11408 type: Function,
11409 value: function() {
11410 return this._onFocus.bind(this);
11411 }
11412 },
11413
11414 _boundOnBlur: {
11415 type: Function,
11416 value: function() {
11417 return this._onBlur.bind(this);
11418 }
11419 },
11420
11421 _boundOnInput: {
11422 type: Function,
11423 value: function() {
11424 return this._onInput.bind(this);
11425 }
11426 },
11427
11428 _boundValueChanged: {
11429 type: Function,
11430 value: function() {
11431 return this._onValueChanged.bind(this);
11432 }
11433 }
11434 },
11435
11436 listeners: {
11437 'addon-attached': '_onAddonAttached',
11438 'iron-input-validate': '_onIronInputValidate'
11439 },
11440
11441 get _valueChangedEvent() {
11442 return this.attrForValue + '-changed';
11443 },
11444
11445 get _propertyForValue() {
11446 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
11447 },
11448
11449 get _inputElement() {
11450 return Polymer.dom(this).querySelector(this._inputSelector);
11451 },
11452
11453 get _inputElementValue() {
11454 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
11455 },
11456
11457 ready: function() {
11458 if (!this._addons) {
11459 this._addons = [];
11460 }
11461 this.addEventListener('focus', this._boundOnFocus, true);
11462 this.addEventListener('blur', this._boundOnBlur, true);
11463 },
11464
11465 attached: function() {
11466 if (this.attrForValue) {
11467 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
11468 } else {
11469 this.addEventListener('input', this._onInput);
11470 }
11471
11472 // Only validate when attached if the input already has a value.
11473 if (this._inputElementValue != '') {
11474 this._handleValueAndAutoValidate(this._inputElement);
11475 } else {
11476 this._handleValue(this._inputElement);
11477 }
11478 },
11479
11480 _onAddonAttached: function(event) {
11481 if (!this._addons) {
11482 this._addons = [];
11483 }
11484 var target = event.target;
11485 if (this._addons.indexOf(target) === -1) {
11486 this._addons.push(target);
11487 if (this.isAttached) {
11488 this._handleValue(this._inputElement);
11489 }
11490 }
11491 },
11492
11493 _onFocus: function() {
11494 this._setFocused(true);
11495 },
11496
11497 _onBlur: function() {
11498 this._setFocused(false);
11499 this._handleValueAndAutoValidate(this._inputElement);
11500 },
11501
11502 _onInput: function(event) {
11503 this._handleValueAndAutoValidate(event.target);
11504 },
11505
11506 _onValueChanged: function(event) {
11507 this._handleValueAndAutoValidate(event.target);
11508 },
11509
11510 _handleValue: function(inputElement) {
11511 var value = this._inputElementValue;
11512
11513 // type="number" hack needed because this.value is empty until it's valid
11514 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
11515 this._inputHasContent = true;
11516 } else {
11517 this._inputHasContent = false;
11518 }
11519
11520 this.updateAddons({ 6492 this.updateAddons({
11521 inputElement: inputElement,
11522 value: value,
11523 invalid: this.invalid 6493 invalid: this.invalid
11524 }); 6494 });
11525 }, 6495 }
11526 6496 },
11527 _handleValueAndAutoValidate: function(inputElement) { 6497 updateAddons: function(state) {
11528 if (this.autoValidate) { 6498 for (var addon, index = 0; addon = this._addons[index]; index++) {
11529 var valid; 6499 addon.update(state);
11530 if (inputElement.validate) { 6500 }
11531 valid = inputElement.validate(this._inputElementValue); 6501 },
11532 } else { 6502 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i nvalid, _inputHasContent) {
11533 valid = inputElement.checkValidity(); 6503 var cls = 'input-content';
11534 } 6504 if (!noLabelFloat) {
11535 this.invalid = !valid; 6505 var label = this.querySelector('label');
11536 } 6506 if (alwaysFloatLabel || _inputHasContent) {
11537 6507 cls += ' label-is-floating';
11538 // Call this last to notify the add-ons. 6508 this.$.labelAndInputContainer.style.position = 'static';
11539 this._handleValue(inputElement); 6509 if (invalid) {
11540 }, 6510 cls += ' is-invalid';
11541 6511 } else if (focused) {
11542 _onIronInputValidate: function(event) { 6512 cls += " label-is-highlighted";
11543 this.invalid = this._inputElement.invalid;
11544 },
11545
11546 _invalidChanged: function() {
11547 if (this._addons) {
11548 this.updateAddons({invalid: this.invalid});
11549 }
11550 },
11551
11552 /**
11553 * Call this to update the state of add-ons.
11554 * @param {Object} state Add-on state.
11555 */
11556 updateAddons: function(state) {
11557 for (var addon, index = 0; addon = this._addons[index]; index++) {
11558 addon.update(state);
11559 }
11560 },
11561
11562 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
11563 var cls = 'input-content';
11564 if (!noLabelFloat) {
11565 var label = this.querySelector('label');
11566
11567 if (alwaysFloatLabel || _inputHasContent) {
11568 cls += ' label-is-floating';
11569 // If the label is floating, ignore any offsets that may have been
11570 // applied from a prefix element.
11571 this.$.labelAndInputContainer.style.position = 'static';
11572
11573 if (invalid) {
11574 cls += ' is-invalid';
11575 } else if (focused) {
11576 cls += " label-is-highlighted";
11577 }
11578 } else {
11579 // When the label is not floating, it should overlap the input element .
11580 if (label) {
11581 this.$.labelAndInputContainer.style.position = 'relative';
11582 }
11583 } 6513 }
11584 } else { 6514 } else {
11585 if (_inputHasContent) { 6515 if (label) {
11586 cls += ' label-is-hidden'; 6516 this.$.labelAndInputContainer.style.position = 'relative';
11587 } 6517 }
11588 } 6518 }
11589 return cls; 6519 } else {
11590 }, 6520 if (_inputHasContent) {
11591 6521 cls += ' label-is-hidden';
11592 _computeUnderlineClass: function(focused, invalid) { 6522 }
11593 var cls = 'underline'; 6523 }
11594 if (invalid) { 6524 return cls;
11595 cls += ' is-invalid'; 6525 },
11596 } else if (focused) { 6526 _computeUnderlineClass: function(focused, invalid) {
11597 cls += ' is-highlighted' 6527 var cls = 'underline';
11598 } 6528 if (invalid) {
11599 return cls; 6529 cls += ' is-invalid';
11600 }, 6530 } else if (focused) {
11601 6531 cls += ' is-highlighted';
11602 _computeAddOnContentClass: function(focused, invalid) { 6532 }
11603 var cls = 'add-on-content'; 6533 return cls;
11604 if (invalid) { 6534 },
11605 cls += ' is-invalid'; 6535 _computeAddOnContentClass: function(focused, invalid) {
11606 } else if (focused) { 6536 var cls = 'add-on-content';
11607 cls += ' is-highlighted' 6537 if (invalid) {
11608 } 6538 cls += ' is-invalid';
11609 return cls; 6539 } else if (focused) {
11610 } 6540 cls += ' is-highlighted';
11611 }); 6541 }
6542 return cls;
6543 }
6544 });
6545
11612 // Copyright 2015 The Chromium Authors. All rights reserved. 6546 // Copyright 2015 The Chromium Authors. All rights reserved.
11613 // Use of this source code is governed by a BSD-style license that can be 6547 // Use of this source code is governed by a BSD-style license that can be
11614 // found in the LICENSE file. 6548 // found in the LICENSE file.
11615
11616 var SearchField = Polymer({ 6549 var SearchField = Polymer({
11617 is: 'cr-search-field', 6550 is: 'cr-search-field',
11618 6551 behaviors: [ CrSearchFieldBehavior ],
11619 behaviors: [CrSearchFieldBehavior],
11620
11621 properties: { 6552 properties: {
11622 value_: String, 6553 value_: String
11623 }, 6554 },
11624
11625 /** @return {!HTMLInputElement} */
11626 getSearchInput: function() { 6555 getSearchInput: function() {
11627 return this.$.searchInput; 6556 return this.$.searchInput;
11628 }, 6557 },
11629
11630 /** @private */
11631 clearSearch_: function() { 6558 clearSearch_: function() {
11632 this.setValue(''); 6559 this.setValue('');
11633 this.getSearchInput().focus(); 6560 this.getSearchInput().focus();
11634 }, 6561 },
11635
11636 /** @private */
11637 toggleShowingSearch_: function() { 6562 toggleShowingSearch_: function() {
11638 this.showingSearch = !this.showingSearch; 6563 this.showingSearch = !this.showingSearch;
11639 }, 6564 }
11640 }); 6565 });
6566
11641 // Copyright 2015 The Chromium Authors. All rights reserved. 6567 // Copyright 2015 The Chromium Authors. All rights reserved.
11642 // Use of this source code is governed by a BSD-style license that can be 6568 // Use of this source code is governed by a BSD-style license that can be
11643 // found in the LICENSE file. 6569 // found in the LICENSE file.
11644
11645 cr.define('downloads', function() { 6570 cr.define('downloads', function() {
11646 var Toolbar = Polymer({ 6571 var Toolbar = Polymer({
11647 is: 'downloads-toolbar', 6572 is: 'downloads-toolbar',
11648
11649 attached: function() { 6573 attached: function() {
11650 // isRTL() only works after i18n_template.js runs to set <html dir>.
11651 this.overflowAlign_ = isRTL() ? 'left' : 'right'; 6574 this.overflowAlign_ = isRTL() ? 'left' : 'right';
11652 }, 6575 },
11653
11654 properties: { 6576 properties: {
11655 downloadsShowing: { 6577 downloadsShowing: {
11656 reflectToAttribute: true, 6578 reflectToAttribute: true,
11657 type: Boolean, 6579 type: Boolean,
11658 value: false, 6580 value: false,
11659 observer: 'downloadsShowingChanged_', 6581 observer: 'downloadsShowingChanged_'
11660 }, 6582 },
11661
11662 overflowAlign_: { 6583 overflowAlign_: {
11663 type: String, 6584 type: String,
11664 value: 'right', 6585 value: 'right'
11665 }, 6586 }
11666 }, 6587 },
11667
11668 listeners: { 6588 listeners: {
11669 'paper-dropdown-close': 'onPaperDropdownClose_', 6589 'paper-dropdown-close': 'onPaperDropdownClose_',
11670 'paper-dropdown-open': 'onPaperDropdownOpen_', 6590 'paper-dropdown-open': 'onPaperDropdownOpen_'
11671 }, 6591 },
11672
11673 /** @return {boolean} Whether removal can be undone. */
11674 canUndo: function() { 6592 canUndo: function() {
11675 return this.$['search-input'] != this.shadowRoot.activeElement; 6593 return this.$['search-input'] != this.shadowRoot.activeElement;
11676 }, 6594 },
11677
11678 /** @return {boolean} Whether "Clear all" should be allowed. */
11679 canClearAll: function() { 6595 canClearAll: function() {
11680 return !this.$['search-input'].getValue() && this.downloadsShowing; 6596 return !this.$['search-input'].getValue() && this.downloadsShowing;
11681 }, 6597 },
11682
11683 onFindCommand: function() { 6598 onFindCommand: function() {
11684 this.$['search-input'].showAndFocus(); 6599 this.$['search-input'].showAndFocus();
11685 }, 6600 },
11686
11687 /** @private */
11688 closeMoreActions_: function() { 6601 closeMoreActions_: function() {
11689 this.$.more.close(); 6602 this.$.more.close();
11690 }, 6603 },
11691
11692 /** @private */
11693 downloadsShowingChanged_: function() { 6604 downloadsShowingChanged_: function() {
11694 this.updateClearAll_(); 6605 this.updateClearAll_();
11695 }, 6606 },
11696
11697 /** @private */
11698 onClearAllTap_: function() { 6607 onClearAllTap_: function() {
11699 assert(this.canClearAll()); 6608 assert(this.canClearAll());
11700 downloads.ActionService.getInstance().clearAll(); 6609 downloads.ActionService.getInstance().clearAll();
11701 }, 6610 },
11702
11703 /** @private */
11704 onPaperDropdownClose_: function() { 6611 onPaperDropdownClose_: function() {
11705 window.removeEventListener('resize', assert(this.boundClose_)); 6612 window.removeEventListener('resize', assert(this.boundClose_));
11706 }, 6613 },
11707
11708 /**
11709 * @param {!Event} e
11710 * @private
11711 */
11712 onItemBlur_: function(e) { 6614 onItemBlur_: function(e) {
11713 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); 6615 var menu = this.$$('paper-menu');
11714 if (menu.items.indexOf(e.relatedTarget) >= 0) 6616 if (menu.items.indexOf(e.relatedTarget) >= 0) return;
11715 return;
11716
11717 this.$.more.restoreFocusOnClose = false; 6617 this.$.more.restoreFocusOnClose = false;
11718 this.closeMoreActions_(); 6618 this.closeMoreActions_();
11719 this.$.more.restoreFocusOnClose = true; 6619 this.$.more.restoreFocusOnClose = true;
11720 }, 6620 },
11721
11722 /** @private */
11723 onPaperDropdownOpen_: function() { 6621 onPaperDropdownOpen_: function() {
11724 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); 6622 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this);
11725 window.addEventListener('resize', this.boundClose_); 6623 window.addEventListener('resize', this.boundClose_);
11726 }, 6624 },
11727
11728 /**
11729 * @param {!CustomEvent} event
11730 * @private
11731 */
11732 onSearchChanged_: function(event) { 6625 onSearchChanged_: function(event) {
11733 downloads.ActionService.getInstance().search( 6626 downloads.ActionService.getInstance().search(event.detail);
11734 /** @type {string} */ (event.detail));
11735 this.updateClearAll_(); 6627 this.updateClearAll_();
11736 }, 6628 },
11737
11738 /** @private */
11739 onOpenDownloadsFolderTap_: function() { 6629 onOpenDownloadsFolderTap_: function() {
11740 downloads.ActionService.getInstance().openDownloadsFolder(); 6630 downloads.ActionService.getInstance().openDownloadsFolder();
11741 }, 6631 },
11742
11743 /** @private */
11744 updateClearAll_: function() { 6632 updateClearAll_: function() {
11745 this.$$('#actions .clear-all').hidden = !this.canClearAll(); 6633 this.$$('#actions .clear-all').hidden = !this.canClearAll();
11746 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); 6634 this.$$('paper-menu .clear-all').hidden = !this.canClearAll();
11747 }, 6635 }
11748 }); 6636 });
11749 6637 return {
11750 return {Toolbar: Toolbar}; 6638 Toolbar: Toolbar
6639 };
11751 }); 6640 });
6641
11752 // Copyright 2015 The Chromium Authors. All rights reserved. 6642 // Copyright 2015 The Chromium Authors. All rights reserved.
11753 // Use of this source code is governed by a BSD-style license that can be 6643 // Use of this source code is governed by a BSD-style license that can be
11754 // found in the LICENSE file. 6644 // found in the LICENSE file.
11755
11756 cr.define('downloads', function() { 6645 cr.define('downloads', function() {
11757 var Manager = Polymer({ 6646 var Manager = Polymer({
11758 is: 'downloads-manager', 6647 is: 'downloads-manager',
11759
11760 properties: { 6648 properties: {
11761 hasDownloads_: { 6649 hasDownloads_: {
11762 observer: 'hasDownloadsChanged_', 6650 observer: 'hasDownloadsChanged_',
11763 type: Boolean, 6651 type: Boolean
11764 }, 6652 },
11765
11766 items_: { 6653 items_: {
11767 type: Array, 6654 type: Array,
11768 value: function() { return []; }, 6655 value: function() {
11769 }, 6656 return [];
11770 }, 6657 }
11771 6658 }
6659 },
11772 hostAttributes: { 6660 hostAttributes: {
11773 loading: true, 6661 loading: true
11774 }, 6662 },
11775
11776 listeners: { 6663 listeners: {
11777 'downloads-list.scroll': 'onListScroll_', 6664 'downloads-list.scroll': 'onListScroll_'
11778 }, 6665 },
11779 6666 observers: [ 'itemsChanged_(items_.*)' ],
11780 observers: [
11781 'itemsChanged_(items_.*)',
11782 ],
11783
11784 /** @private */
11785 clearAll_: function() { 6667 clearAll_: function() {
11786 this.set('items_', []); 6668 this.set('items_', []);
11787 }, 6669 },
11788
11789 /** @private */
11790 hasDownloadsChanged_: function() { 6670 hasDownloadsChanged_: function() {
11791 if (loadTimeData.getBoolean('allowDeletingHistory')) 6671 if (loadTimeData.getBoolean('allowDeletingHistory')) this.$.toolbar.downlo adsShowing = this.hasDownloads_;
11792 this.$.toolbar.downloadsShowing = this.hasDownloads_;
11793
11794 if (this.hasDownloads_) { 6672 if (this.hasDownloads_) {
11795 this.$['downloads-list'].fire('iron-resize'); 6673 this.$['downloads-list'].fire('iron-resize');
11796 } else { 6674 } else {
11797 var isSearching = downloads.ActionService.getInstance().isSearching(); 6675 var isSearching = downloads.ActionService.getInstance().isSearching();
11798 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; 6676 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads';
11799 this.$['no-downloads'].querySelector('span').textContent = 6677 this.$['no-downloads'].querySelector('span').textContent = loadTimeData. getString(messageToShow);
11800 loadTimeData.getString(messageToShow); 6678 }
11801 } 6679 },
11802 },
11803
11804 /**
11805 * @param {number} index
11806 * @param {!Array<!downloads.Data>} list
11807 * @private
11808 */
11809 insertItems_: function(index, list) { 6680 insertItems_: function(index, list) {
11810 this.splice.apply(this, ['items_', index, 0].concat(list)); 6681 this.splice.apply(this, [ 'items_', index, 0 ].concat(list));
11811 this.updateHideDates_(index, index + list.length); 6682 this.updateHideDates_(index, index + list.length);
11812 this.removeAttribute('loading'); 6683 this.removeAttribute('loading');
11813 }, 6684 },
11814
11815 /** @private */
11816 itemsChanged_: function() { 6685 itemsChanged_: function() {
11817 this.hasDownloads_ = this.items_.length > 0; 6686 this.hasDownloads_ = this.items_.length > 0;
11818 }, 6687 },
11819
11820 /**
11821 * @param {Event} e
11822 * @private
11823 */
11824 onCanExecute_: function(e) { 6688 onCanExecute_: function(e) {
11825 e = /** @type {cr.ui.CanExecuteEvent} */(e); 6689 e = e;
11826 switch (e.command.id) { 6690 switch (e.command.id) {
11827 case 'undo-command': 6691 case 'undo-command':
11828 e.canExecute = this.$.toolbar.canUndo(); 6692 e.canExecute = this.$.toolbar.canUndo();
11829 break; 6693 break;
11830 case 'clear-all-command': 6694
11831 e.canExecute = this.$.toolbar.canClearAll(); 6695 case 'clear-all-command':
11832 break; 6696 e.canExecute = this.$.toolbar.canClearAll();
11833 case 'find-command': 6697 break;
11834 e.canExecute = true; 6698
11835 break; 6699 case 'find-command':
11836 } 6700 e.canExecute = true;
11837 }, 6701 break;
11838 6702 }
11839 /** 6703 },
11840 * @param {Event} e
11841 * @private
11842 */
11843 onCommand_: function(e) { 6704 onCommand_: function(e) {
11844 if (e.command.id == 'clear-all-command') 6705 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();
11845 downloads.ActionService.getInstance().clearAll(); 6706 },
11846 else if (e.command.id == 'undo-command')
11847 downloads.ActionService.getInstance().undo();
11848 else if (e.command.id == 'find-command')
11849 this.$.toolbar.onFindCommand();
11850 },
11851
11852 /** @private */
11853 onListScroll_: function() { 6707 onListScroll_: function() {
11854 var list = this.$['downloads-list']; 6708 var list = this.$['downloads-list'];
11855 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { 6709 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) {
11856 // Approaching the end of the scrollback. Attempt to load more items.
11857 downloads.ActionService.getInstance().loadMore(); 6710 downloads.ActionService.getInstance().loadMore();
11858 } 6711 }
11859 }, 6712 },
11860
11861 /** @private */
11862 onLoad_: function() { 6713 onLoad_: function() {
11863 cr.ui.decorate('command', cr.ui.Command); 6714 cr.ui.decorate('command', cr.ui.Command);
11864 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); 6715 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
11865 document.addEventListener('command', this.onCommand_.bind(this)); 6716 document.addEventListener('command', this.onCommand_.bind(this));
11866
11867 downloads.ActionService.getInstance().loadMore(); 6717 downloads.ActionService.getInstance().loadMore();
11868 }, 6718 },
11869
11870 /**
11871 * @param {number} index
11872 * @private
11873 */
11874 removeItem_: function(index) { 6719 removeItem_: function(index) {
11875 this.splice('items_', index, 1); 6720 this.splice('items_', index, 1);
11876 this.updateHideDates_(index, index); 6721 this.updateHideDates_(index, index);
11877 this.onListScroll_(); 6722 this.onListScroll_();
11878 }, 6723 },
11879
11880 /**
11881 * @param {number} start
11882 * @param {number} end
11883 * @private
11884 */
11885 updateHideDates_: function(start, end) { 6724 updateHideDates_: function(start, end) {
11886 for (var i = start; i <= end; ++i) { 6725 for (var i = start; i <= end; ++i) {
11887 var current = this.items_[i]; 6726 var current = this.items_[i];
11888 if (!current) 6727 if (!current) continue;
11889 continue;
11890 var prev = this.items_[i - 1]; 6728 var prev = this.items_[i - 1];
11891 current.hideDate = !!prev && prev.date_string == current.date_string; 6729 current.hideDate = !!prev && prev.date_string == current.date_string;
11892 } 6730 }
11893 }, 6731 },
11894
11895 /**
11896 * @param {number} index
11897 * @param {!downloads.Data} data
11898 * @private
11899 */
11900 updateItem_: function(index, data) { 6732 updateItem_: function(index, data) {
11901 this.set('items_.' + index, data); 6733 this.set('items_.' + index, data);
11902 this.updateHideDates_(index, index); 6734 this.updateHideDates_(index, index);
11903 var list = /** @type {!IronListElement} */(this.$['downloads-list']); 6735 var list = this.$['downloads-list'];
11904 list.updateSizeForItem(index); 6736 list.updateSizeForItem(index);
11905 }, 6737 }
11906 }); 6738 });
11907
11908 Manager.clearAll = function() { 6739 Manager.clearAll = function() {
11909 Manager.get().clearAll_(); 6740 Manager.get().clearAll_();
11910 }; 6741 };
11911
11912 /** @return {!downloads.Manager} */
11913 Manager.get = function() { 6742 Manager.get = function() {
11914 return /** @type {!downloads.Manager} */( 6743 return queryRequiredElement('downloads-manager');
11915 queryRequiredElement('downloads-manager')); 6744 };
11916 };
11917
11918 Manager.insertItems = function(index, list) { 6745 Manager.insertItems = function(index, list) {
11919 Manager.get().insertItems_(index, list); 6746 Manager.get().insertItems_(index, list);
11920 }; 6747 };
11921
11922 Manager.onLoad = function() { 6748 Manager.onLoad = function() {
11923 Manager.get().onLoad_(); 6749 Manager.get().onLoad_();
11924 }; 6750 };
11925
11926 Manager.removeItem = function(index) { 6751 Manager.removeItem = function(index) {
11927 Manager.get().removeItem_(index); 6752 Manager.get().removeItem_(index);
11928 }; 6753 };
11929
11930 Manager.updateItem = function(index, data) { 6754 Manager.updateItem = function(index, data) {
11931 Manager.get().updateItem_(index, data); 6755 Manager.get().updateItem_(index, data);
11932 }; 6756 };
11933 6757 return {
11934 return {Manager: Manager}; 6758 Manager: Manager
6759 };
11935 }); 6760 });
6761
11936 // Copyright 2015 The Chromium Authors. All rights reserved. 6762 // Copyright 2015 The Chromium Authors. All rights reserved.
11937 // Use of this source code is governed by a BSD-style license that can be 6763 // Use of this source code is governed by a BSD-style license that can be
11938 // found in the LICENSE file. 6764 // found in the LICENSE file.
11939
11940 window.addEventListener('load', downloads.Manager.onLoad); 6765 window.addEventListener('load', downloads.Manager.onLoad);
OLDNEW
« no previous file with comments | « no previous file | chrome/browser/resources/md_history/app.crisper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698