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

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

Issue 2252963002: MD WebUI: Strip comments from vulcanized JS files (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@history_vulcanize
Patch Set: Fix comments ending in **/ Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /**
6 * @fileoverview PromiseResolver is a helper class that allows creating a
7 * Promise that will be fulfilled (resolved or rejected) some time later.
8 *
9 * Example:
10 * var resolver = new PromiseResolver();
11 * resolver.promise.then(function(result) {
12 * console.log('resolved with', result);
13 * });
14 * ...
15 * ...
16 * resolver.resolve({hello: 'world'});
17 */
18 5
19 /**
20 * @constructor @struct
21 * @template T
22 */
23 function PromiseResolver() { 6 function PromiseResolver() {
24 /** @private {function(T=): void} */ 7 /** @private {function(T=): void} */
25 this.resolve_; 8 this.resolve_;
26 9
27 /** @private {function(*=): void} */ 10 /** @private {function(*=): void} */
28 this.reject_; 11 this.reject_;
29 12
30 /** @private {!Promise<T>} */ 13 /** @private {!Promise<T>} */
31 this.promise_ = new Promise(function(resolve, reject) { 14 this.promise_ = new Promise(function(resolve, reject) {
32 this.resolve_ = resolve; 15 this.resolve_ = resolve;
(...skipping 11 matching lines...) Expand all
44 set resolve(r) { assertNotReached(); }, 27 set resolve(r) { assertNotReached(); },
45 28
46 /** @return {function(*=): void} */ 29 /** @return {function(*=): void} */
47 get reject() { return this.reject_; }, 30 get reject() { return this.reject_; },
48 set reject(s) { assertNotReached(); }, 31 set reject(s) { assertNotReached(); },
49 }; 32 };
50 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 33 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
51 // Use of this source code is governed by a BSD-style license that can be 34 // Use of this source code is governed by a BSD-style license that can be
52 // found in the LICENSE file. 35 // found in the LICENSE file.
53 36
54 /**
55 * The global object.
56 * @type {!Object}
57 * @const
58 */
59 var global = this; 37 var global = this;
60 38
61 /** @typedef {{eventName: string, uid: number}} */ 39 /** @typedef {{eventName: string, uid: number}} */
62 var WebUIListener; 40 var WebUIListener;
63 41
64 /** Platform, package, object property, and Event support. **/ 42 /** Platform, package, object property, and Event support. **/
65 var cr = cr || function() { 43 var cr = cr || function() {
66 'use strict'; 44 'use strict';
67 45
68 /**
69 * Builds an object structure for the provided namespace path,
70 * ensuring that names that already exist are not overwritten. For
71 * example:
72 * "a.b.c" -> a = {};a.b={};a.b.c={};
73 * @param {string} name Name of the object that this file defines.
74 * @param {*=} opt_object The object to expose at the end of the path.
75 * @param {Object=} opt_objectToExportTo The object to add the path to;
76 * default is {@code global}.
77 * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
78 * returns a reference to the ui property of window.cr).
79 * @private
80 */
81 function exportPath(name, opt_object, opt_objectToExportTo) { 46 function exportPath(name, opt_object, opt_objectToExportTo) {
82 var parts = name.split('.'); 47 var parts = name.split('.');
83 var cur = opt_objectToExportTo || global; 48 var cur = opt_objectToExportTo || global;
84 49
85 for (var part; parts.length && (part = parts.shift());) { 50 for (var part; parts.length && (part = parts.shift());) {
86 if (!parts.length && opt_object !== undefined) { 51 if (!parts.length && opt_object !== undefined) {
87 // last part and we have an object; use it
88 cur[part] = opt_object; 52 cur[part] = opt_object;
89 } else if (part in cur) { 53 } else if (part in cur) {
90 cur = cur[part]; 54 cur = cur[part];
91 } else { 55 } else {
92 cur = cur[part] = {}; 56 cur = cur[part] = {};
93 } 57 }
94 } 58 }
95 return cur; 59 return cur;
96 } 60 }
97 61
98 /**
99 * Fires a property change event on the target.
100 * @param {EventTarget} target The target to dispatch the event on.
101 * @param {string} propertyName The name of the property that changed.
102 * @param {*} newValue The new value for the property.
103 * @param {*} oldValue The old value for the property.
104 */
105 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { 62 function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
106 var e = new Event(propertyName + 'Change'); 63 var e = new Event(propertyName + 'Change');
107 e.propertyName = propertyName; 64 e.propertyName = propertyName;
108 e.newValue = newValue; 65 e.newValue = newValue;
109 e.oldValue = oldValue; 66 e.oldValue = oldValue;
110 target.dispatchEvent(e); 67 target.dispatchEvent(e);
111 } 68 }
112 69
113 /**
114 * Converts a camelCase javascript property name to a hyphenated-lower-case
115 * attribute name.
116 * @param {string} jsName The javascript camelCase property name.
117 * @return {string} The equivalent hyphenated-lower-case attribute name.
118 */
119 function getAttributeName(jsName) { 70 function getAttributeName(jsName) {
120 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); 71 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
121 } 72 }
122 73
123 /**
124 * The kind of property to define in {@code defineProperty}.
125 * @enum {string}
126 * @const
127 */
128 var PropertyKind = { 74 var PropertyKind = {
129 /**
130 * Plain old JS property where the backing data is stored as a "private"
131 * field on the object.
132 * Use for properties of any type. Type will not be checked.
133 */
134 JS: 'js', 75 JS: 'js',
135 76
136 /**
137 * The property backing data is stored as an attribute on an element.
138 * Use only for properties of type {string}.
139 */
140 ATTR: 'attr', 77 ATTR: 'attr',
141 78
142 /**
143 * The property backing data is stored as an attribute on an element. If the
144 * element has the attribute then the value is true.
145 * Use only for properties of type {boolean}.
146 */
147 BOOL_ATTR: 'boolAttr' 79 BOOL_ATTR: 'boolAttr'
148 }; 80 };
149 81
150 /**
151 * Helper function for defineProperty that returns the getter to use for the
152 * property.
153 * @param {string} name The name of the property.
154 * @param {PropertyKind} kind The kind of the property.
155 * @return {function():*} The getter for the property.
156 */
157 function getGetter(name, kind) { 82 function getGetter(name, kind) {
158 switch (kind) { 83 switch (kind) {
159 case PropertyKind.JS: 84 case PropertyKind.JS:
160 var privateName = name + '_'; 85 var privateName = name + '_';
161 return function() { 86 return function() {
162 return this[privateName]; 87 return this[privateName];
163 }; 88 };
164 case PropertyKind.ATTR: 89 case PropertyKind.ATTR:
165 var attributeName = getAttributeName(name); 90 var attributeName = getAttributeName(name);
166 return function() { 91 return function() {
167 return this.getAttribute(attributeName); 92 return this.getAttribute(attributeName);
168 }; 93 };
169 case PropertyKind.BOOL_ATTR: 94 case PropertyKind.BOOL_ATTR:
170 var attributeName = getAttributeName(name); 95 var attributeName = getAttributeName(name);
171 return function() { 96 return function() {
172 return this.hasAttribute(attributeName); 97 return this.hasAttribute(attributeName);
173 }; 98 };
174 } 99 }
175 100
176 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
177 // the browser/unit tests to preprocess this file through grit.
178 throw 'not reached'; 101 throw 'not reached';
179 } 102 }
180 103
181 /**
182 * Helper function for defineProperty that returns the setter of the right
183 * kind.
184 * @param {string} name The name of the property we are defining the setter
185 * for.
186 * @param {PropertyKind} kind The kind of property we are getting the
187 * setter for.
188 * @param {function(*, *):void=} opt_setHook A function to run after the
189 * property is set, but before the propertyChange event is fired.
190 * @return {function(*):void} The function to use as a setter.
191 */
192 function getSetter(name, kind, opt_setHook) { 104 function getSetter(name, kind, opt_setHook) {
193 switch (kind) { 105 switch (kind) {
194 case PropertyKind.JS: 106 case PropertyKind.JS:
195 var privateName = name + '_'; 107 var privateName = name + '_';
196 return function(value) { 108 return function(value) {
197 var oldValue = this[name]; 109 var oldValue = this[name];
198 if (value !== oldValue) { 110 if (value !== oldValue) {
199 this[privateName] = value; 111 this[privateName] = value;
200 if (opt_setHook) 112 if (opt_setHook)
201 opt_setHook.call(this, value, oldValue); 113 opt_setHook.call(this, value, oldValue);
(...skipping 25 matching lines...) Expand all
227 this.setAttribute(attributeName, name); 139 this.setAttribute(attributeName, name);
228 else 140 else
229 this.removeAttribute(attributeName); 141 this.removeAttribute(attributeName);
230 if (opt_setHook) 142 if (opt_setHook)
231 opt_setHook.call(this, value, oldValue); 143 opt_setHook.call(this, value, oldValue);
232 dispatchPropertyChange(this, name, value, oldValue); 144 dispatchPropertyChange(this, name, value, oldValue);
233 } 145 }
234 }; 146 };
235 } 147 }
236 148
237 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax
238 // the browser/unit tests to preprocess this file through grit.
239 throw 'not reached'; 149 throw 'not reached';
240 } 150 }
241 151
242 /**
243 * Defines a property on an object. When the setter changes the value a
244 * property change event with the type {@code name + 'Change'} is fired.
245 * @param {!Object} obj The object to define the property for.
246 * @param {string} name The name of the property.
247 * @param {PropertyKind=} opt_kind What kind of underlying storage to use.
248 * @param {function(*, *):void=} opt_setHook A function to run after the
249 * property is set, but before the propertyChange event is fired.
250 */
251 function defineProperty(obj, name, opt_kind, opt_setHook) { 152 function defineProperty(obj, name, opt_kind, opt_setHook) {
252 if (typeof obj == 'function') 153 if (typeof obj == 'function')
253 obj = obj.prototype; 154 obj = obj.prototype;
254 155
255 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); 156 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS);
256 157
257 if (!obj.__lookupGetter__(name)) 158 if (!obj.__lookupGetter__(name))
258 obj.__defineGetter__(name, getGetter(name, kind)); 159 obj.__defineGetter__(name, getGetter(name, kind));
259 160
260 if (!obj.__lookupSetter__(name)) 161 if (!obj.__lookupSetter__(name))
261 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); 162 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
262 } 163 }
263 164
264 /**
265 * Counter for use with createUid
266 */
267 var uidCounter = 1; 165 var uidCounter = 1;
268 166
269 /**
270 * @return {number} A new unique ID.
271 */
272 function createUid() { 167 function createUid() {
273 return uidCounter++; 168 return uidCounter++;
274 } 169 }
275 170
276 /**
277 * Returns a unique ID for the item. This mutates the item so it needs to be
278 * an object
279 * @param {!Object} item The item to get the unique ID for.
280 * @return {number} The unique ID for the item.
281 */
282 function getUid(item) { 171 function getUid(item) {
283 if (item.hasOwnProperty('uid')) 172 if (item.hasOwnProperty('uid'))
284 return item.uid; 173 return item.uid;
285 return item.uid = createUid(); 174 return item.uid = createUid();
286 } 175 }
287 176
288 /**
289 * Dispatches a simple event on an event target.
290 * @param {!EventTarget} target The event target to dispatch the event on.
291 * @param {string} type The type of the event.
292 * @param {boolean=} opt_bubbles Whether the event bubbles or not.
293 * @param {boolean=} opt_cancelable Whether the default action of the event
294 * can be prevented. Default is true.
295 * @return {boolean} If any of the listeners called {@code preventDefault}
296 * during the dispatch this will return false.
297 */
298 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { 177 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
299 var e = new Event(type, { 178 var e = new Event(type, {
300 bubbles: opt_bubbles, 179 bubbles: opt_bubbles,
301 cancelable: opt_cancelable === undefined || opt_cancelable 180 cancelable: opt_cancelable === undefined || opt_cancelable
302 }); 181 });
303 return target.dispatchEvent(e); 182 return target.dispatchEvent(e);
304 } 183 }
305 184
306 /**
307 * Calls |fun| and adds all the fields of the returned object to the object
308 * named by |name|. For example, cr.define('cr.ui', function() {
309 * function List() {
310 * ...
311 * }
312 * function ListItem() {
313 * ...
314 * }
315 * return {
316 * List: List,
317 * ListItem: ListItem,
318 * };
319 * });
320 * defines the functions cr.ui.List and cr.ui.ListItem.
321 * @param {string} name The name of the object that we are adding fields to.
322 * @param {!Function} fun The function that will return an object containing
323 * the names and values of the new fields.
324 */
325 function define(name, fun) { 185 function define(name, fun) {
326 var obj = exportPath(name); 186 var obj = exportPath(name);
327 var exports = fun(); 187 var exports = fun();
328 for (var propertyName in exports) { 188 for (var propertyName in exports) {
329 // Maybe we should check the prototype chain here? The current usage
330 // pattern is always using an object literal so we only care about own
331 // properties.
332 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, 189 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
333 propertyName); 190 propertyName);
334 if (propertyDescriptor) 191 if (propertyDescriptor)
335 Object.defineProperty(obj, propertyName, propertyDescriptor); 192 Object.defineProperty(obj, propertyName, propertyDescriptor);
336 } 193 }
337 } 194 }
338 195
339 /**
340 * Adds a {@code getInstance} static method that always return the same
341 * instance object.
342 * @param {!Function} ctor The constructor for the class to add the static
343 * method to.
344 */
345 function addSingletonGetter(ctor) { 196 function addSingletonGetter(ctor) {
346 ctor.getInstance = function() { 197 ctor.getInstance = function() {
347 return ctor.instance_ || (ctor.instance_ = new ctor()); 198 return ctor.instance_ || (ctor.instance_ = new ctor());
348 }; 199 };
349 } 200 }
350 201
351 /**
352 * Forwards public APIs to private implementations.
353 * @param {Function} ctor Constructor that have private implementations in its
354 * prototype.
355 * @param {Array<string>} methods List of public method names that have their
356 * underscored counterparts in constructor's prototype.
357 * @param {string=} opt_target Selector for target node.
358 */
359 function makePublic(ctor, methods, opt_target) { 202 function makePublic(ctor, methods, opt_target) {
360 methods.forEach(function(method) { 203 methods.forEach(function(method) {
361 ctor[method] = function() { 204 ctor[method] = function() {
362 var target = opt_target ? document.getElementById(opt_target) : 205 var target = opt_target ? document.getElementById(opt_target) :
363 ctor.getInstance(); 206 ctor.getInstance();
364 return target[method + '_'].apply(target, arguments); 207 return target[method + '_'].apply(target, arguments);
365 }; 208 };
366 }); 209 });
367 } 210 }
368 211
369 /**
370 * The mapping used by the sendWithPromise mechanism to tie the Promise
371 * returned to callers with the corresponding WebUI response. The mapping is
372 * from ID to the PromiseResolver helper; the ID is generated by
373 * sendWithPromise and is unique across all invocations of said method.
374 * @type {!Object<!PromiseResolver>}
375 */
376 var chromeSendResolverMap = {}; 212 var chromeSendResolverMap = {};
377 213
378 /**
379 * The named method the WebUI handler calls directly in response to a
380 * chrome.send call that expects a response. The handler requires no knowledge
381 * of the specific name of this method, as the name is passed to the handler
382 * as the first argument in the arguments list of chrome.send. The handler
383 * must pass the ID, also sent via the chrome.send arguments list, as the
384 * first argument of the JS invocation; additionally, the handler may
385 * supply any number of other arguments that will be included in the response.
386 * @param {string} id The unique ID identifying the Promise this response is
387 * tied to.
388 * @param {boolean} isSuccess Whether the request was successful.
389 * @param {*} response The response as sent from C++.
390 */
391 function webUIResponse(id, isSuccess, response) { 214 function webUIResponse(id, isSuccess, response) {
392 var resolver = chromeSendResolverMap[id]; 215 var resolver = chromeSendResolverMap[id];
393 delete chromeSendResolverMap[id]; 216 delete chromeSendResolverMap[id];
394 217
395 if (isSuccess) 218 if (isSuccess)
396 resolver.resolve(response); 219 resolver.resolve(response);
397 else 220 else
398 resolver.reject(response); 221 resolver.reject(response);
399 } 222 }
400 223
401 /**
402 * A variation of chrome.send, suitable for messages that expect a single
403 * response from C++.
404 * @param {string} methodName The name of the WebUI handler API.
405 * @param {...*} var_args Varibale number of arguments to be forwarded to the
406 * C++ call.
407 * @return {!Promise}
408 */
409 function sendWithPromise(methodName, var_args) { 224 function sendWithPromise(methodName, var_args) {
410 var args = Array.prototype.slice.call(arguments, 1); 225 var args = Array.prototype.slice.call(arguments, 1);
411 var promiseResolver = new PromiseResolver(); 226 var promiseResolver = new PromiseResolver();
412 var id = methodName + '_' + createUid(); 227 var id = methodName + '_' + createUid();
413 chromeSendResolverMap[id] = promiseResolver; 228 chromeSendResolverMap[id] = promiseResolver;
414 chrome.send(methodName, [id].concat(args)); 229 chrome.send(methodName, [id].concat(args));
415 return promiseResolver.promise; 230 return promiseResolver.promise;
416 } 231 }
417 232
418 /**
419 * A map of maps associating event names with listeners. The 2nd level map
420 * associates a listener ID with the callback function, such that individual
421 * listeners can be removed from an event without affecting other listeners of
422 * the same event.
423 * @type {!Object<!Object<!Function>>}
424 */
425 var webUIListenerMap = {}; 233 var webUIListenerMap = {};
426 234
427 /**
428 * The named method the WebUI handler calls directly when an event occurs.
429 * The WebUI handler must supply the name of the event as the first argument
430 * of the JS invocation; additionally, the handler may supply any number of
431 * other arguments that will be forwarded to the listener callbacks.
432 * @param {string} event The name of the event that has occurred.
433 * @param {...*} var_args Additional arguments passed from C++.
434 */
435 function webUIListenerCallback(event, var_args) { 235 function webUIListenerCallback(event, var_args) {
436 var eventListenersMap = webUIListenerMap[event]; 236 var eventListenersMap = webUIListenerMap[event];
437 if (!eventListenersMap) { 237 if (!eventListenersMap) {
438 // C++ event sent for an event that has no listeners.
439 // TODO(dpapad): Should a warning be displayed here?
440 return; 238 return;
441 } 239 }
442 240
443 var args = Array.prototype.slice.call(arguments, 1); 241 var args = Array.prototype.slice.call(arguments, 1);
444 for (var listenerId in eventListenersMap) { 242 for (var listenerId in eventListenersMap) {
445 eventListenersMap[listenerId].apply(null, args); 243 eventListenersMap[listenerId].apply(null, args);
446 } 244 }
447 } 245 }
448 246
449 /**
450 * Registers a listener for an event fired from WebUI handlers. Any number of
451 * listeners may register for a single event.
452 * @param {string} eventName The event to listen to.
453 * @param {!Function} callback The callback run when the event is fired.
454 * @return {!WebUIListener} An object to be used for removing a listener via
455 * cr.removeWebUIListener. Should be treated as read-only.
456 */
457 function addWebUIListener(eventName, callback) { 247 function addWebUIListener(eventName, callback) {
458 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; 248 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
459 var uid = createUid(); 249 var uid = createUid();
460 webUIListenerMap[eventName][uid] = callback; 250 webUIListenerMap[eventName][uid] = callback;
461 return {eventName: eventName, uid: uid}; 251 return {eventName: eventName, uid: uid};
462 } 252 }
463 253
464 /**
465 * Removes a listener. Does nothing if the specified listener is not found.
466 * @param {!WebUIListener} listener The listener to be removed (as returned by
467 * addWebUIListener).
468 * @return {boolean} Whether the given listener was found and actually
469 * removed.
470 */
471 function removeWebUIListener(listener) { 254 function removeWebUIListener(listener) {
472 var listenerExists = webUIListenerMap[listener.eventName] && 255 var listenerExists = webUIListenerMap[listener.eventName] &&
473 webUIListenerMap[listener.eventName][listener.uid]; 256 webUIListenerMap[listener.eventName][listener.uid];
474 if (listenerExists) { 257 if (listenerExists) {
475 delete webUIListenerMap[listener.eventName][listener.uid]; 258 delete webUIListenerMap[listener.eventName][listener.uid];
476 return true; 259 return true;
477 } 260 }
478 return false; 261 return false;
479 } 262 }
480 263
481 return { 264 return {
482 addSingletonGetter: addSingletonGetter, 265 addSingletonGetter: addSingletonGetter,
483 createUid: createUid, 266 createUid: createUid,
484 define: define, 267 define: define,
485 defineProperty: defineProperty, 268 defineProperty: defineProperty,
486 dispatchPropertyChange: dispatchPropertyChange, 269 dispatchPropertyChange: dispatchPropertyChange,
487 dispatchSimpleEvent: dispatchSimpleEvent, 270 dispatchSimpleEvent: dispatchSimpleEvent,
488 exportPath: exportPath, 271 exportPath: exportPath,
489 getUid: getUid, 272 getUid: getUid,
490 makePublic: makePublic, 273 makePublic: makePublic,
491 PropertyKind: PropertyKind, 274 PropertyKind: PropertyKind,
492 275
493 // C++ <-> JS communication related methods.
494 addWebUIListener: addWebUIListener, 276 addWebUIListener: addWebUIListener,
495 removeWebUIListener: removeWebUIListener, 277 removeWebUIListener: removeWebUIListener,
496 sendWithPromise: sendWithPromise, 278 sendWithPromise: sendWithPromise,
497 webUIListenerCallback: webUIListenerCallback, 279 webUIListenerCallback: webUIListenerCallback,
498 webUIResponse: webUIResponse, 280 webUIResponse: webUIResponse,
499 281
500 get doc() { 282 get doc() {
501 return document; 283 return document;
502 }, 284 },
503 285
(...skipping 27 matching lines...) Expand all
531 return /iPad|iPhone|iPod/.test(navigator.platform); 313 return /iPad|iPhone|iPod/.test(navigator.platform);
532 } 314 }
533 }; 315 };
534 }(); 316 }();
535 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 317 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
536 // Use of this source code is governed by a BSD-style license that can be 318 // Use of this source code is governed by a BSD-style license that can be
537 // found in the LICENSE file. 319 // found in the LICENSE file.
538 320
539 cr.define('cr.ui', function() { 321 cr.define('cr.ui', function() {
540 322
541 /**
542 * Decorates elements as an instance of a class.
543 * @param {string|!Element} source The way to find the element(s) to decorate.
544 * If this is a string then {@code querySeletorAll} is used to find the
545 * elements to decorate.
546 * @param {!Function} constr The constructor to decorate with. The constr
547 * needs to have a {@code decorate} function.
548 */
549 function decorate(source, constr) { 323 function decorate(source, constr) {
550 var elements; 324 var elements;
551 if (typeof source == 'string') 325 if (typeof source == 'string')
552 elements = cr.doc.querySelectorAll(source); 326 elements = cr.doc.querySelectorAll(source);
553 else 327 else
554 elements = [source]; 328 elements = [source];
555 329
556 for (var i = 0, el; el = elements[i]; i++) { 330 for (var i = 0, el; el = elements[i]; i++) {
557 if (!(el instanceof constr)) 331 if (!(el instanceof constr))
558 constr.decorate(el); 332 constr.decorate(el);
559 } 333 }
560 } 334 }
561 335
562 /**
563 * Helper function for creating new element for define.
564 */
565 function createElementHelper(tagName, opt_bag) { 336 function createElementHelper(tagName, opt_bag) {
566 // Allow passing in ownerDocument to create in a different document.
567 var doc; 337 var doc;
568 if (opt_bag && opt_bag.ownerDocument) 338 if (opt_bag && opt_bag.ownerDocument)
569 doc = opt_bag.ownerDocument; 339 doc = opt_bag.ownerDocument;
570 else 340 else
571 doc = cr.doc; 341 doc = cr.doc;
572 return doc.createElement(tagName); 342 return doc.createElement(tagName);
573 } 343 }
574 344
575 /**
576 * Creates the constructor for a UI element class.
577 *
578 * Usage:
579 * <pre>
580 * var List = cr.ui.define('list');
581 * List.prototype = {
582 * __proto__: HTMLUListElement.prototype,
583 * decorate: function() {
584 * ...
585 * },
586 * ...
587 * };
588 * </pre>
589 *
590 * @param {string|Function} tagNameOrFunction The tagName or
591 * function to use for newly created elements. If this is a function it
592 * needs to return a new element when called.
593 * @return {function(Object=):Element} The constructor function which takes
594 * an optional property bag. The function also has a static
595 * {@code decorate} method added to it.
596 */
597 function define(tagNameOrFunction) { 345 function define(tagNameOrFunction) {
598 var createFunction, tagName; 346 var createFunction, tagName;
599 if (typeof tagNameOrFunction == 'function') { 347 if (typeof tagNameOrFunction == 'function') {
600 createFunction = tagNameOrFunction; 348 createFunction = tagNameOrFunction;
601 tagName = ''; 349 tagName = '';
602 } else { 350 } else {
603 createFunction = createElementHelper; 351 createFunction = createElementHelper;
604 tagName = tagNameOrFunction; 352 tagName = tagNameOrFunction;
605 } 353 }
606 354
607 /**
608 * Creates a new UI element constructor.
609 * @param {Object=} opt_propertyBag Optional bag of properties to set on the
610 * object after created. The property {@code ownerDocument} is special
611 * cased and it allows you to create the element in a different
612 * document than the default.
613 * @constructor
614 */
615 function f(opt_propertyBag) { 355 function f(opt_propertyBag) {
616 var el = createFunction(tagName, opt_propertyBag); 356 var el = createFunction(tagName, opt_propertyBag);
617 f.decorate(el); 357 f.decorate(el);
618 for (var propertyName in opt_propertyBag) { 358 for (var propertyName in opt_propertyBag) {
619 el[propertyName] = opt_propertyBag[propertyName]; 359 el[propertyName] = opt_propertyBag[propertyName];
620 } 360 }
621 return el; 361 return el;
622 } 362 }
623 363
624 /**
625 * Decorates an element as a UI element class.
626 * @param {!Element} el The element to decorate.
627 */
628 f.decorate = function(el) { 364 f.decorate = function(el) {
629 el.__proto__ = f.prototype; 365 el.__proto__ = f.prototype;
630 el.decorate(); 366 el.decorate();
631 }; 367 };
632 368
633 return f; 369 return f;
634 } 370 }
635 371
636 /**
637 * Input elements do not grow and shrink with their content. This is a simple
638 * (and not very efficient) way of handling shrinking to content with support
639 * for min width and limited by the width of the parent element.
640 * @param {!HTMLElement} el The element to limit the width for.
641 * @param {!HTMLElement} parentEl The parent element that should limit the
642 * size.
643 * @param {number} min The minimum width.
644 * @param {number=} opt_scale Optional scale factor to apply to the width.
645 */
646 function limitInputWidth(el, parentEl, min, opt_scale) { 372 function limitInputWidth(el, parentEl, min, opt_scale) {
647 // Needs a size larger than borders
648 el.style.width = '10px'; 373 el.style.width = '10px';
649 var doc = el.ownerDocument; 374 var doc = el.ownerDocument;
650 var win = doc.defaultView; 375 var win = doc.defaultView;
651 var computedStyle = win.getComputedStyle(el); 376 var computedStyle = win.getComputedStyle(el);
652 var parentComputedStyle = win.getComputedStyle(parentEl); 377 var parentComputedStyle = win.getComputedStyle(parentEl);
653 var rtl = computedStyle.direction == 'rtl'; 378 var rtl = computedStyle.direction == 'rtl';
654 379
655 // To get the max width we get the width of the treeItem minus the position
656 // of the input.
657 var inputRect = el.getBoundingClientRect(); // box-sizing 380 var inputRect = el.getBoundingClientRect(); // box-sizing
658 var parentRect = parentEl.getBoundingClientRect(); 381 var parentRect = parentEl.getBoundingClientRect();
659 var startPos = rtl ? parentRect.right - inputRect.right : 382 var startPos = rtl ? parentRect.right - inputRect.right :
660 inputRect.left - parentRect.left; 383 inputRect.left - parentRect.left;
661 384
662 // Add up border and padding of the input.
663 var inner = parseInt(computedStyle.borderLeftWidth, 10) + 385 var inner = parseInt(computedStyle.borderLeftWidth, 10) +
664 parseInt(computedStyle.paddingLeft, 10) + 386 parseInt(computedStyle.paddingLeft, 10) +
665 parseInt(computedStyle.paddingRight, 10) + 387 parseInt(computedStyle.paddingRight, 10) +
666 parseInt(computedStyle.borderRightWidth, 10); 388 parseInt(computedStyle.borderRightWidth, 10);
667 389
668 // We also need to subtract the padding of parent to prevent it to overflow.
669 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : 390 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
670 parseInt(parentComputedStyle.paddingRight, 10); 391 parseInt(parentComputedStyle.paddingRight, 10);
671 392
672 var max = parentEl.clientWidth - startPos - inner - parentPadding; 393 var max = parentEl.clientWidth - startPos - inner - parentPadding;
673 if (opt_scale) 394 if (opt_scale)
674 max *= opt_scale; 395 max *= opt_scale;
675 396
676 function limit() { 397 function limit() {
677 if (el.scrollWidth > max) { 398 if (el.scrollWidth > max) {
678 el.style.width = max + 'px'; 399 el.style.width = max + 'px';
679 } else { 400 } else {
680 el.style.width = 0; 401 el.style.width = 0;
681 var sw = el.scrollWidth; 402 var sw = el.scrollWidth;
682 if (sw < min) { 403 if (sw < min) {
683 el.style.width = min + 'px'; 404 el.style.width = min + 'px';
684 } else { 405 } else {
685 el.style.width = sw + 'px'; 406 el.style.width = sw + 'px';
686 } 407 }
687 } 408 }
688 } 409 }
689 410
690 el.addEventListener('input', limit); 411 el.addEventListener('input', limit);
691 limit(); 412 limit();
692 } 413 }
693 414
694 /**
695 * Takes a number and spits out a value CSS will be happy with. To avoid
696 * subpixel layout issues, the value is rounded to the nearest integral value.
697 * @param {number} pixels The number of pixels.
698 * @return {string} e.g. '16px'.
699 */
700 function toCssPx(pixels) { 415 function toCssPx(pixels) {
701 if (!window.isFinite(pixels)) 416 if (!window.isFinite(pixels))
702 console.error('Pixel value is not a number: ' + pixels); 417 console.error('Pixel value is not a number: ' + pixels);
703 return Math.round(pixels) + 'px'; 418 return Math.round(pixels) + 'px';
704 } 419 }
705 420
706 /**
707 * Users complain they occasionaly use doubleclicks instead of clicks
708 * (http://crbug.com/140364). To fix it we freeze click handling for
709 * the doubleclick time interval.
710 * @param {MouseEvent} e Initial click event.
711 */
712 function swallowDoubleClick(e) { 421 function swallowDoubleClick(e) {
713 var doc = e.target.ownerDocument; 422 var doc = e.target.ownerDocument;
714 var counter = Math.min(1, e.detail); 423 var counter = Math.min(1, e.detail);
715 function swallow(e) { 424 function swallow(e) {
716 e.stopPropagation(); 425 e.stopPropagation();
717 e.preventDefault(); 426 e.preventDefault();
718 } 427 }
719 function onclick(e) { 428 function onclick(e) {
720 if (e.detail > counter) { 429 if (e.detail > counter) {
721 counter = e.detail; 430 counter = e.detail;
722 // Swallow the click since it's a click inside the doubleclick timeout.
723 swallow(e); 431 swallow(e);
724 } else { 432 } else {
725 // Stop tracking clicks and let regular handling.
726 doc.removeEventListener('dblclick', swallow, true); 433 doc.removeEventListener('dblclick', swallow, true);
727 doc.removeEventListener('click', onclick, true); 434 doc.removeEventListener('click', onclick, true);
728 } 435 }
729 } 436 }
730 // The following 'click' event (if e.type == 'mouseup') mustn't be taken
731 // into account (it mustn't stop tracking clicks). Start event listening
732 // after zero timeout.
733 setTimeout(function() { 437 setTimeout(function() {
734 doc.addEventListener('click', onclick, true); 438 doc.addEventListener('click', onclick, true);
735 doc.addEventListener('dblclick', swallow, true); 439 doc.addEventListener('dblclick', swallow, true);
736 }, 0); 440 }, 0);
737 } 441 }
738 442
739 return { 443 return {
740 decorate: decorate, 444 decorate: decorate,
741 define: define, 445 define: define,
742 limitInputWidth: limitInputWidth, 446 limitInputWidth: limitInputWidth,
743 toCssPx: toCssPx, 447 toCssPx: toCssPx,
744 swallowDoubleClick: swallowDoubleClick 448 swallowDoubleClick: swallowDoubleClick
745 }; 449 };
746 }); 450 });
747 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 451 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
748 // Use of this source code is governed by a BSD-style license that can be 452 // Use of this source code is governed by a BSD-style license that can be
749 // found in the LICENSE file. 453 // found in the LICENSE file.
750 454
751 /**
752 * @fileoverview A command is an abstraction of an action a user can do in the
753 * UI.
754 *
755 * When the focus changes in the document for each command a canExecute event
756 * is dispatched on the active element. By listening to this event you can
757 * enable and disable the command by setting the event.canExecute property.
758 *
759 * When a command is executed a command event is dispatched on the active
760 * element. Note that you should stop the propagation after you have handled the
761 * command if there might be other command listeners higher up in the DOM tree.
762 */
763 455
764 cr.define('cr.ui', function() { 456 cr.define('cr.ui', function() {
765 457
766 /**
767 * This is used to identify keyboard shortcuts.
768 * @param {string} shortcut The text used to describe the keys for this
769 * keyboard shortcut.
770 * @constructor
771 */
772 function KeyboardShortcut(shortcut) { 458 function KeyboardShortcut(shortcut) {
773 var mods = {}; 459 var mods = {};
774 var ident = ''; 460 var ident = '';
775 shortcut.split('|').forEach(function(part) { 461 shortcut.split('|').forEach(function(part) {
776 var partLc = part.toLowerCase(); 462 var partLc = part.toLowerCase();
777 switch (partLc) { 463 switch (partLc) {
778 case 'alt': 464 case 'alt':
779 case 'ctrl': 465 case 'ctrl':
780 case 'meta': 466 case 'meta':
781 case 'shift': 467 case 'shift':
782 mods[partLc + 'Key'] = true; 468 mods[partLc + 'Key'] = true;
783 break; 469 break;
784 default: 470 default:
785 if (ident) 471 if (ident)
786 throw Error('Invalid shortcut'); 472 throw Error('Invalid shortcut');
787 ident = part; 473 ident = part;
788 } 474 }
789 }); 475 });
790 476
791 this.ident_ = ident; 477 this.ident_ = ident;
792 this.mods_ = mods; 478 this.mods_ = mods;
793 } 479 }
794 480
795 KeyboardShortcut.prototype = { 481 KeyboardShortcut.prototype = {
796 /**
797 * Whether the keyboard shortcut object matches a keyboard event.
798 * @param {!Event} e The keyboard event object.
799 * @return {boolean} Whether we found a match or not.
800 */
801 matchesEvent: function(e) { 482 matchesEvent: function(e) {
802 if (e.key == this.ident_) { 483 if (e.key == this.ident_) {
803 // All keyboard modifiers needs to match.
804 var mods = this.mods_; 484 var mods = this.mods_;
805 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { 485 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
806 return e[k] == !!mods[k]; 486 return e[k] == !!mods[k];
807 }); 487 });
808 } 488 }
809 return false; 489 return false;
810 } 490 }
811 }; 491 };
812 492
813 /**
814 * Creates a new command element.
815 * @constructor
816 * @extends {HTMLElement}
817 */
818 var Command = cr.ui.define('command'); 493 var Command = cr.ui.define('command');
819 494
820 Command.prototype = { 495 Command.prototype = {
821 __proto__: HTMLElement.prototype, 496 __proto__: HTMLElement.prototype,
822 497
823 /**
824 * Initializes the command.
825 */
826 decorate: function() { 498 decorate: function() {
827 CommandManager.init(assert(this.ownerDocument)); 499 CommandManager.init(assert(this.ownerDocument));
828 500
829 if (this.hasAttribute('shortcut')) 501 if (this.hasAttribute('shortcut'))
830 this.shortcut = this.getAttribute('shortcut'); 502 this.shortcut = this.getAttribute('shortcut');
831 }, 503 },
832 504
833 /**
834 * Executes the command by dispatching a command event on the given element.
835 * If |element| isn't given, the active element is used instead.
836 * If the command is {@code disabled} this does nothing.
837 * @param {HTMLElement=} opt_element Optional element to dispatch event on.
838 */
839 execute: function(opt_element) { 505 execute: function(opt_element) {
840 if (this.disabled) 506 if (this.disabled)
841 return; 507 return;
842 var doc = this.ownerDocument; 508 var doc = this.ownerDocument;
843 if (doc.activeElement) { 509 if (doc.activeElement) {
844 var e = new Event('command', {bubbles: true}); 510 var e = new Event('command', {bubbles: true});
845 e.command = this; 511 e.command = this;
846 512
847 (opt_element || doc.activeElement).dispatchEvent(e); 513 (opt_element || doc.activeElement).dispatchEvent(e);
848 } 514 }
849 }, 515 },
850 516
851 /**
852 * Call this when there have been changes that might change whether the
853 * command can be executed or not.
854 * @param {Node=} opt_node Node for which to actuate command state.
855 */
856 canExecuteChange: function(opt_node) { 517 canExecuteChange: function(opt_node) {
857 dispatchCanExecuteEvent(this, 518 dispatchCanExecuteEvent(this,
858 opt_node || this.ownerDocument.activeElement); 519 opt_node || this.ownerDocument.activeElement);
859 }, 520 },
860 521
861 /**
862 * The keyboard shortcut that triggers the command. This is a string
863 * consisting of a key (as reported by WebKit in keydown) as
864 * well as optional key modifiers joinded with a '|'.
865 *
866 * Multiple keyboard shortcuts can be provided by separating them by
867 * whitespace.
868 *
869 * For example:
870 * "F1"
871 * "Backspace|Meta" for Apple command backspace.
872 * "a|Ctrl" for Control A
873 * "Delete Backspace|Meta" for Delete and Command Backspace
874 *
875 * @type {string}
876 */
877 shortcut_: '', 522 shortcut_: '',
878 get shortcut() { 523 get shortcut() {
879 return this.shortcut_; 524 return this.shortcut_;
880 }, 525 },
881 set shortcut(shortcut) { 526 set shortcut(shortcut) {
882 var oldShortcut = this.shortcut_; 527 var oldShortcut = this.shortcut_;
883 if (shortcut !== oldShortcut) { 528 if (shortcut !== oldShortcut) {
884 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { 529 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
885 return new KeyboardShortcut(shortcut); 530 return new KeyboardShortcut(shortcut);
886 }); 531 });
887 532
888 // Set this after the keyboardShortcuts_ since that might throw.
889 this.shortcut_ = shortcut; 533 this.shortcut_ = shortcut;
890 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, 534 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
891 oldShortcut); 535 oldShortcut);
892 } 536 }
893 }, 537 },
894 538
895 /**
896 * Whether the event object matches the shortcut for this command.
897 * @param {!Event} e The key event object.
898 * @return {boolean} Whether it matched or not.
899 */
900 matchesEvent: function(e) { 539 matchesEvent: function(e) {
901 if (!this.keyboardShortcuts_) 540 if (!this.keyboardShortcuts_)
902 return false; 541 return false;
903 542
904 return this.keyboardShortcuts_.some(function(keyboardShortcut) { 543 return this.keyboardShortcuts_.some(function(keyboardShortcut) {
905 return keyboardShortcut.matchesEvent(e); 544 return keyboardShortcut.matchesEvent(e);
906 }); 545 });
907 }, 546 },
908 }; 547 };
909 548
910 /**
911 * The label of the command.
912 */
913 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); 549 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
914 550
915 /**
916 * Whether the command is disabled or not.
917 */
918 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); 551 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
919 552
920 /**
921 * Whether the command is hidden or not.
922 */
923 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); 553 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
924 554
925 /**
926 * Whether the command is checked or not.
927 */
928 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); 555 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
929 556
930 /**
931 * The flag that prevents the shortcut text from being displayed on menu.
932 *
933 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
934 * is displayed in menu when the command is assosiated with a menu item.
935 * Otherwise, no text is displayed.
936 */
937 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); 557 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
938 558
939 /**
940 * Dispatches a canExecute event on the target.
941 * @param {!cr.ui.Command} command The command that we are testing for.
942 * @param {EventTarget} target The target element to dispatch the event on.
943 */
944 function dispatchCanExecuteEvent(command, target) { 559 function dispatchCanExecuteEvent(command, target) {
945 var e = new CanExecuteEvent(command); 560 var e = new CanExecuteEvent(command);
946 target.dispatchEvent(e); 561 target.dispatchEvent(e);
947 command.disabled = !e.canExecute; 562 command.disabled = !e.canExecute;
948 } 563 }
949 564
950 /**
951 * The command managers for different documents.
952 */
953 var commandManagers = {}; 565 var commandManagers = {};
954 566
955 /**
956 * Keeps track of the focused element and updates the commands when the focus
957 * changes.
958 * @param {!Document} doc The document that we are managing the commands for.
959 * @constructor
960 */
961 function CommandManager(doc) { 567 function CommandManager(doc) {
962 doc.addEventListener('focus', this.handleFocus_.bind(this), true); 568 doc.addEventListener('focus', this.handleFocus_.bind(this), true);
963 // Make sure we add the listener to the bubbling phase so that elements can
964 // prevent the command.
965 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); 569 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
966 } 570 }
967 571
968 /**
969 * Initializes a command manager for the document as needed.
970 * @param {!Document} doc The document to manage the commands for.
971 */
972 CommandManager.init = function(doc) { 572 CommandManager.init = function(doc) {
973 var uid = cr.getUid(doc); 573 var uid = cr.getUid(doc);
974 if (!(uid in commandManagers)) { 574 if (!(uid in commandManagers)) {
975 commandManagers[uid] = new CommandManager(doc); 575 commandManagers[uid] = new CommandManager(doc);
976 } 576 }
977 }; 577 };
978 578
979 CommandManager.prototype = { 579 CommandManager.prototype = {
980 580
981 /**
982 * Handles focus changes on the document.
983 * @param {Event} e The focus event object.
984 * @private
985 * @suppress {checkTypes}
986 * TODO(vitalyp): remove the suppression.
987 */
988 handleFocus_: function(e) { 581 handleFocus_: function(e) {
989 var target = e.target; 582 var target = e.target;
990 583
991 // Ignore focus on a menu button or command item.
992 if (target.menu || target.command) 584 if (target.menu || target.command)
993 return; 585 return;
994 586
995 var commands = Array.prototype.slice.call( 587 var commands = Array.prototype.slice.call(
996 target.ownerDocument.querySelectorAll('command')); 588 target.ownerDocument.querySelectorAll('command'));
997 589
998 commands.forEach(function(command) { 590 commands.forEach(function(command) {
999 dispatchCanExecuteEvent(command, target); 591 dispatchCanExecuteEvent(command, target);
1000 }); 592 });
1001 }, 593 },
1002 594
1003 /**
1004 * Handles the keydown event and routes it to the right command.
1005 * @param {!Event} e The keydown event.
1006 */
1007 handleKeyDown_: function(e) { 595 handleKeyDown_: function(e) {
1008 var target = e.target; 596 var target = e.target;
1009 var commands = Array.prototype.slice.call( 597 var commands = Array.prototype.slice.call(
1010 target.ownerDocument.querySelectorAll('command')); 598 target.ownerDocument.querySelectorAll('command'));
1011 599
1012 for (var i = 0, command; command = commands[i]; i++) { 600 for (var i = 0, command; command = commands[i]; i++) {
1013 if (command.matchesEvent(e)) { 601 if (command.matchesEvent(e)) {
1014 // When invoking a command via a shortcut, we have to manually check
1015 // if it can be executed, since focus might not have been changed
1016 // what would have updated the command's state.
1017 command.canExecuteChange(); 602 command.canExecuteChange();
1018 603
1019 if (!command.disabled) { 604 if (!command.disabled) {
1020 e.preventDefault(); 605 e.preventDefault();
1021 // We do not want any other element to handle this.
1022 e.stopPropagation(); 606 e.stopPropagation();
1023 command.execute(); 607 command.execute();
1024 return; 608 return;
1025 } 609 }
1026 } 610 }
1027 } 611 }
1028 } 612 }
1029 }; 613 };
1030 614
1031 /**
1032 * The event type used for canExecute events.
1033 * @param {!cr.ui.Command} command The command that we are evaluating.
1034 * @extends {Event}
1035 * @constructor
1036 * @class
1037 */
1038 function CanExecuteEvent(command) { 615 function CanExecuteEvent(command) {
1039 var e = new Event('canExecute', {bubbles: true, cancelable: true}); 616 var e = new Event('canExecute', {bubbles: true, cancelable: true});
1040 e.__proto__ = CanExecuteEvent.prototype; 617 e.__proto__ = CanExecuteEvent.prototype;
1041 e.command = command; 618 e.command = command;
1042 return e; 619 return e;
1043 } 620 }
1044 621
1045 CanExecuteEvent.prototype = { 622 CanExecuteEvent.prototype = {
1046 __proto__: Event.prototype, 623 __proto__: Event.prototype,
1047 624
1048 /**
1049 * The current command
1050 * @type {cr.ui.Command}
1051 */
1052 command: null, 625 command: null,
1053 626
1054 /**
1055 * Whether the target can execute the command. Setting this also stops the
1056 * propagation and prevents the default. Callers can tell if an event has
1057 * been handled via |this.defaultPrevented|.
1058 * @type {boolean}
1059 */
1060 canExecute_: false, 627 canExecute_: false,
1061 get canExecute() { 628 get canExecute() {
1062 return this.canExecute_; 629 return this.canExecute_;
1063 }, 630 },
1064 set canExecute(canExecute) { 631 set canExecute(canExecute) {
1065 this.canExecute_ = !!canExecute; 632 this.canExecute_ = !!canExecute;
1066 this.stopPropagation(); 633 this.stopPropagation();
1067 this.preventDefault(); 634 this.preventDefault();
1068 } 635 }
1069 }; 636 };
1070 637
1071 // Export
1072 return { 638 return {
1073 Command: Command, 639 Command: Command,
1074 CanExecuteEvent: CanExecuteEvent 640 CanExecuteEvent: CanExecuteEvent
1075 }; 641 };
1076 }); 642 });
1077 Polymer({ 643 Polymer({
1078 is: 'app-drawer', 644 is: 'app-drawer',
1079 645
1080 properties: { 646 properties: {
1081 /**
1082 * The opened state of the drawer.
1083 */
1084 opened: { 647 opened: {
1085 type: Boolean, 648 type: Boolean,
1086 value: false, 649 value: false,
1087 notify: true, 650 notify: true,
1088 reflectToAttribute: true 651 reflectToAttribute: true
1089 }, 652 },
1090 653
1091 /**
1092 * The drawer does not have a scrim and cannot be swiped close.
1093 */
1094 persistent: { 654 persistent: {
1095 type: Boolean, 655 type: Boolean,
1096 value: false, 656 value: false,
1097 reflectToAttribute: true 657 reflectToAttribute: true
1098 }, 658 },
1099 659
1100 /**
1101 * The alignment of the drawer on the screen ('left', 'right', 'start' o r 'end').
1102 * 'start' computes to left and 'end' to right in LTR layout and vice ve rsa in RTL
1103 * layout.
1104 */
1105 align: { 660 align: {
1106 type: String, 661 type: String,
1107 value: 'left' 662 value: 'left'
1108 }, 663 },
1109 664
1110 /**
1111 * The computed, read-only position of the drawer on the screen ('left' or 'right').
1112 */
1113 position: { 665 position: {
1114 type: String, 666 type: String,
1115 readOnly: true, 667 readOnly: true,
1116 value: 'left', 668 value: 'left',
1117 reflectToAttribute: true 669 reflectToAttribute: true
1118 }, 670 },
1119 671
1120 /**
1121 * Create an area at the edge of the screen to swipe open the drawer.
1122 */
1123 swipeOpen: { 672 swipeOpen: {
1124 type: Boolean, 673 type: Boolean,
1125 value: false, 674 value: false,
1126 reflectToAttribute: true 675 reflectToAttribute: true
1127 }, 676 },
1128 677
1129 /**
1130 * Trap keyboard focus when the drawer is opened and not persistent.
1131 */
1132 noFocusTrap: { 678 noFocusTrap: {
1133 type: Boolean, 679 type: Boolean,
1134 value: false 680 value: false
1135 } 681 }
1136 }, 682 },
1137 683
1138 observers: [ 684 observers: [
1139 'resetLayout(position)', 685 'resetLayout(position)',
1140 '_resetPosition(align, isAttached)' 686 '_resetPosition(align, isAttached)'
1141 ], 687 ],
1142 688
1143 _translateOffset: 0, 689 _translateOffset: 0,
1144 690
1145 _trackDetails: null, 691 _trackDetails: null,
1146 692
1147 _drawerState: 0, 693 _drawerState: 0,
1148 694
1149 _boundEscKeydownHandler: null, 695 _boundEscKeydownHandler: null,
1150 696
1151 _firstTabStop: null, 697 _firstTabStop: null,
1152 698
1153 _lastTabStop: null, 699 _lastTabStop: null,
1154 700
1155 ready: function() { 701 ready: function() {
1156 // Set the scroll direction so you can vertically scroll inside the draw er.
1157 this.setScrollDirection('y'); 702 this.setScrollDirection('y');
1158 703
1159 // Only transition the drawer after its first render (e.g. app-drawer-la yout
1160 // may need to set the initial opened state which should not be transiti oned).
1161 this._setTransitionDuration('0s'); 704 this._setTransitionDuration('0s');
1162 }, 705 },
1163 706
1164 attached: function() { 707 attached: function() {
1165 // Only transition the drawer after its first render (e.g. app-drawer-la yout
1166 // may need to set the initial opened state which should not be transiti oned).
1167 Polymer.RenderStatus.afterNextRender(this, function() { 708 Polymer.RenderStatus.afterNextRender(this, function() {
1168 this._setTransitionDuration(''); 709 this._setTransitionDuration('');
1169 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); 710 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
1170 this._resetDrawerState(); 711 this._resetDrawerState();
1171 712
1172 this.listen(this, 'track', '_track'); 713 this.listen(this, 'track', '_track');
1173 this.addEventListener('transitionend', this._transitionend.bind(this)) ; 714 this.addEventListener('transitionend', this._transitionend.bind(this)) ;
1174 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) 715 this.addEventListener('keydown', this._tabKeydownHandler.bind(this))
1175 }); 716 });
1176 }, 717 },
1177 718
1178 detached: function() { 719 detached: function() {
1179 document.removeEventListener('keydown', this._boundEscKeydownHandler); 720 document.removeEventListener('keydown', this._boundEscKeydownHandler);
1180 }, 721 },
1181 722
1182 /**
1183 * Opens the drawer.
1184 */
1185 open: function() { 723 open: function() {
1186 this.opened = true; 724 this.opened = true;
1187 }, 725 },
1188 726
1189 /**
1190 * Closes the drawer.
1191 */
1192 close: function() { 727 close: function() {
1193 this.opened = false; 728 this.opened = false;
1194 }, 729 },
1195 730
1196 /**
1197 * Toggles the drawer open and close.
1198 */
1199 toggle: function() { 731 toggle: function() {
1200 this.opened = !this.opened; 732 this.opened = !this.opened;
1201 }, 733 },
1202 734
1203 /**
1204 * Gets the width of the drawer.
1205 *
1206 * @return {number} The width of the drawer in pixels.
1207 */
1208 getWidth: function() { 735 getWidth: function() {
1209 return this.$.contentContainer.offsetWidth; 736 return this.$.contentContainer.offsetWidth;
1210 }, 737 },
1211 738
1212 /**
1213 * Resets the layout. If you changed the size of app-header via CSS
1214 * you can notify the changes by either firing the `iron-resize` event
1215 * or calling `resetLayout` directly.
1216 *
1217 * @method resetLayout
1218 */
1219 resetLayout: function() { 739 resetLayout: function() {
1220 this.debounce('_resetLayout', function() { 740 this.debounce('_resetLayout', function() {
1221 this.fire('app-drawer-reset-layout'); 741 this.fire('app-drawer-reset-layout');
1222 }, 1); 742 }, 1);
1223 }, 743 },
1224 744
1225 _isRTL: function() { 745 _isRTL: function() {
1226 return window.getComputedStyle(this).direction === 'rtl'; 746 return window.getComputedStyle(this).direction === 'rtl';
1227 }, 747 },
1228 748
1229 _resetPosition: function() { 749 _resetPosition: function() {
1230 switch (this.align) { 750 switch (this.align) {
1231 case 'start': 751 case 'start':
1232 this._setPosition(this._isRTL() ? 'right' : 'left'); 752 this._setPosition(this._isRTL() ? 'right' : 'left');
1233 return; 753 return;
1234 case 'end': 754 case 'end':
1235 this._setPosition(this._isRTL() ? 'left' : 'right'); 755 this._setPosition(this._isRTL() ? 'left' : 'right');
1236 return; 756 return;
1237 } 757 }
1238 this._setPosition(this.align); 758 this._setPosition(this.align);
1239 }, 759 },
1240 760
1241 _escKeydownHandler: function(event) { 761 _escKeydownHandler: function(event) {
1242 var ESC_KEYCODE = 27; 762 var ESC_KEYCODE = 27;
1243 if (event.keyCode === ESC_KEYCODE) { 763 if (event.keyCode === ESC_KEYCODE) {
1244 // Prevent any side effects if app-drawer closes.
1245 event.preventDefault(); 764 event.preventDefault();
1246 this.close(); 765 this.close();
1247 } 766 }
1248 }, 767 },
1249 768
1250 _track: function(event) { 769 _track: function(event) {
1251 if (this.persistent) { 770 if (this.persistent) {
1252 return; 771 return;
1253 } 772 }
1254 773
1255 // Disable user selection on desktop.
1256 event.preventDefault(); 774 event.preventDefault();
1257 775
1258 switch (event.detail.state) { 776 switch (event.detail.state) {
1259 case 'start': 777 case 'start':
1260 this._trackStart(event); 778 this._trackStart(event);
1261 break; 779 break;
1262 case 'track': 780 case 'track':
1263 this._trackMove(event); 781 this._trackMove(event);
1264 break; 782 break;
1265 case 'end': 783 case 'end':
1266 this._trackEnd(event); 784 this._trackEnd(event);
1267 break; 785 break;
1268 } 786 }
1269 }, 787 },
1270 788
1271 _trackStart: function(event) { 789 _trackStart: function(event) {
1272 this._drawerState = this._DRAWER_STATE.TRACKING; 790 this._drawerState = this._DRAWER_STATE.TRACKING;
1273 791
1274 // Disable transitions since style attributes will reflect user track ev ents.
1275 this._setTransitionDuration('0s'); 792 this._setTransitionDuration('0s');
1276 this.style.visibility = 'visible'; 793 this.style.visibility = 'visible';
1277 794
1278 var rect = this.$.contentContainer.getBoundingClientRect(); 795 var rect = this.$.contentContainer.getBoundingClientRect();
1279 if (this.position === 'left') { 796 if (this.position === 'left') {
1280 this._translateOffset = rect.left; 797 this._translateOffset = rect.left;
1281 } else { 798 } else {
1282 this._translateOffset = rect.right - window.innerWidth; 799 this._translateOffset = rect.right - window.innerWidth;
1283 } 800 }
1284 801
1285 this._trackDetails = []; 802 this._trackDetails = [];
1286 }, 803 },
1287 804
1288 _trackMove: function(event) { 805 _trackMove: function(event) {
1289 this._translateDrawer(event.detail.dx + this._translateOffset); 806 this._translateDrawer(event.detail.dx + this._translateOffset);
1290 807
1291 // Use Date.now() since event.timeStamp is inconsistent across browsers (e.g. most
1292 // browsers use milliseconds but FF 44 uses microseconds).
1293 this._trackDetails.push({ 808 this._trackDetails.push({
1294 dx: event.detail.dx, 809 dx: event.detail.dx,
1295 timeStamp: Date.now() 810 timeStamp: Date.now()
1296 }); 811 });
1297 }, 812 },
1298 813
1299 _trackEnd: function(event) { 814 _trackEnd: function(event) {
1300 var x = event.detail.dx + this._translateOffset; 815 var x = event.detail.dx + this._translateOffset;
1301 var drawerWidth = this.getWidth(); 816 var drawerWidth = this.getWidth();
1302 var isPositionLeft = this.position === 'left'; 817 var isPositionLeft = this.position === 'left';
1303 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : 818 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
1304 (x <= 0 || x >= drawerWidth); 819 (x <= 0 || x >= drawerWidth);
1305 820
1306 if (!isInEndState) { 821 if (!isInEndState) {
1307 // No longer need the track events after this method returns - allow t hem to be GC'd.
1308 var trackDetails = this._trackDetails; 822 var trackDetails = this._trackDetails;
1309 this._trackDetails = null; 823 this._trackDetails = null;
1310 824
1311 this._flingDrawer(event, trackDetails); 825 this._flingDrawer(event, trackDetails);
1312 if (this._drawerState === this._DRAWER_STATE.FLINGING) { 826 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1313 return; 827 return;
1314 } 828 }
1315 } 829 }
1316 830
1317 // If the drawer is not flinging, toggle the opened state based on the p osition of
1318 // the drawer.
1319 var halfWidth = drawerWidth / 2; 831 var halfWidth = drawerWidth / 2;
1320 if (event.detail.dx < -halfWidth) { 832 if (event.detail.dx < -halfWidth) {
1321 this.opened = this.position === 'right'; 833 this.opened = this.position === 'right';
1322 } else if (event.detail.dx > halfWidth) { 834 } else if (event.detail.dx > halfWidth) {
1323 this.opened = this.position === 'left'; 835 this.opened = this.position === 'left';
1324 } 836 }
1325 837
1326 // Trigger app-drawer-transitioned now since there will be no transition end event.
1327 if (isInEndState) { 838 if (isInEndState) {
1328 this._resetDrawerState(); 839 this._resetDrawerState();
1329 } 840 }
1330 841
1331 this._setTransitionDuration(''); 842 this._setTransitionDuration('');
1332 this._resetDrawerTranslate(); 843 this._resetDrawerTranslate();
1333 this.style.visibility = ''; 844 this.style.visibility = '';
1334 }, 845 },
1335 846
1336 _calculateVelocity: function(event, trackDetails) { 847 _calculateVelocity: function(event, trackDetails) {
1337 // Find the oldest track event that is within 100ms using binary search.
1338 var now = Date.now(); 848 var now = Date.now();
1339 var timeLowerBound = now - 100; 849 var timeLowerBound = now - 100;
1340 var trackDetail; 850 var trackDetail;
1341 var min = 0; 851 var min = 0;
1342 var max = trackDetails.length - 1; 852 var max = trackDetails.length - 1;
1343 853
1344 while (min <= max) { 854 while (min <= max) {
1345 // Floor of average of min and max.
1346 var mid = (min + max) >> 1; 855 var mid = (min + max) >> 1;
1347 var d = trackDetails[mid]; 856 var d = trackDetails[mid];
1348 if (d.timeStamp >= timeLowerBound) { 857 if (d.timeStamp >= timeLowerBound) {
1349 trackDetail = d; 858 trackDetail = d;
1350 max = mid - 1; 859 max = mid - 1;
1351 } else { 860 } else {
1352 min = mid + 1; 861 min = mid + 1;
1353 } 862 }
1354 } 863 }
1355 864
1356 if (trackDetail) { 865 if (trackDetail) {
1357 var dx = event.detail.dx - trackDetail.dx; 866 var dx = event.detail.dx - trackDetail.dx;
1358 var dt = (now - trackDetail.timeStamp) || 1; 867 var dt = (now - trackDetail.timeStamp) || 1;
1359 return dx / dt; 868 return dx / dt;
1360 } 869 }
1361 return 0; 870 return 0;
1362 }, 871 },
1363 872
1364 _flingDrawer: function(event, trackDetails) { 873 _flingDrawer: function(event, trackDetails) {
1365 var velocity = this._calculateVelocity(event, trackDetails); 874 var velocity = this._calculateVelocity(event, trackDetails);
1366 875
1367 // Do not fling if velocity is not above a threshold.
1368 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { 876 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
1369 return; 877 return;
1370 } 878 }
1371 879
1372 this._drawerState = this._DRAWER_STATE.FLINGING; 880 this._drawerState = this._DRAWER_STATE.FLINGING;
1373 881
1374 var x = event.detail.dx + this._translateOffset; 882 var x = event.detail.dx + this._translateOffset;
1375 var drawerWidth = this.getWidth(); 883 var drawerWidth = this.getWidth();
1376 var isPositionLeft = this.position === 'left'; 884 var isPositionLeft = this.position === 'left';
1377 var isVelocityPositive = velocity > 0; 885 var isVelocityPositive = velocity > 0;
1378 var isClosingLeft = !isVelocityPositive && isPositionLeft; 886 var isClosingLeft = !isVelocityPositive && isPositionLeft;
1379 var isClosingRight = isVelocityPositive && !isPositionLeft; 887 var isClosingRight = isVelocityPositive && !isPositionLeft;
1380 var dx; 888 var dx;
1381 if (isClosingLeft) { 889 if (isClosingLeft) {
1382 dx = -(x + drawerWidth); 890 dx = -(x + drawerWidth);
1383 } else if (isClosingRight) { 891 } else if (isClosingRight) {
1384 dx = (drawerWidth - x); 892 dx = (drawerWidth - x);
1385 } else { 893 } else {
1386 dx = -x; 894 dx = -x;
1387 } 895 }
1388 896
1389 // Enforce a minimum transition velocity to make the drawer feel snappy.
1390 if (isVelocityPositive) { 897 if (isVelocityPositive) {
1391 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); 898 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
1392 this.opened = this.position === 'left'; 899 this.opened = this.position === 'left';
1393 } else { 900 } else {
1394 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); 901 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
1395 this.opened = this.position === 'right'; 902 this.opened = this.position === 'right';
1396 } 903 }
1397 904
1398 // Calculate the amount of time needed to finish the transition based on the
1399 // initial slope of the timing function.
1400 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms'); 905 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms');
1401 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); 906 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);
1402 907
1403 this._resetDrawerTranslate(); 908 this._resetDrawerTranslate();
1404 }, 909 },
1405 910
1406 _transitionend: function(event) { 911 _transitionend: function(event) {
1407 // contentContainer will transition on opened state changed, and scrim w ill
1408 // transition on persistent state changed when opened - these are the
1409 // transitions we are interested in.
1410 var target = Polymer.dom(event).rootTarget; 912 var target = Polymer.dom(event).rootTarget;
1411 if (target === this.$.contentContainer || target === this.$.scrim) { 913 if (target === this.$.contentContainer || target === this.$.scrim) {
1412 914
1413 // If the drawer was flinging, we need to reset the style attributes.
1414 if (this._drawerState === this._DRAWER_STATE.FLINGING) { 915 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1415 this._setTransitionDuration(''); 916 this._setTransitionDuration('');
1416 this._setTransitionTimingFunction(''); 917 this._setTransitionTimingFunction('');
1417 this.style.visibility = ''; 918 this.style.visibility = '';
1418 } 919 }
1419 920
1420 this._resetDrawerState(); 921 this._resetDrawerState();
1421 } 922 }
1422 }, 923 },
1423 924
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
1462 if (oldState !== this._drawerState) { 963 if (oldState !== this._drawerState) {
1463 if (this._drawerState === this._DRAWER_STATE.OPENED) { 964 if (this._drawerState === this._DRAWER_STATE.OPENED) {
1464 this._setKeyboardFocusTrap(); 965 this._setKeyboardFocusTrap();
1465 document.addEventListener('keydown', this._boundEscKeydownHandler); 966 document.addEventListener('keydown', this._boundEscKeydownHandler);
1466 document.body.style.overflow = 'hidden'; 967 document.body.style.overflow = 'hidden';
1467 } else { 968 } else {
1468 document.removeEventListener('keydown', this._boundEscKeydownHandler ); 969 document.removeEventListener('keydown', this._boundEscKeydownHandler );
1469 document.body.style.overflow = ''; 970 document.body.style.overflow = '';
1470 } 971 }
1471 972
1472 // Don't fire the event on initial load.
1473 if (oldState !== this._DRAWER_STATE.INIT) { 973 if (oldState !== this._DRAWER_STATE.INIT) {
1474 this.fire('app-drawer-transitioned'); 974 this.fire('app-drawer-transitioned');
1475 } 975 }
1476 } 976 }
1477 }, 977 },
1478 978
1479 _setKeyboardFocusTrap: function() { 979 _setKeyboardFocusTrap: function() {
1480 if (this.noFocusTrap) { 980 if (this.noFocusTrap) {
1481 return; 981 return;
1482 } 982 }
1483 983
1484 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated) , this will
1485 // not select focusable elements inside shadow roots.
1486 var focusableElementsSelector = [ 984 var focusableElementsSelector = [
1487 'a[href]:not([tabindex="-1"])', 985 'a[href]:not([tabindex="-1"])',
1488 'area[href]:not([tabindex="-1"])', 986 'area[href]:not([tabindex="-1"])',
1489 'input:not([disabled]):not([tabindex="-1"])', 987 'input:not([disabled]):not([tabindex="-1"])',
1490 'select:not([disabled]):not([tabindex="-1"])', 988 'select:not([disabled]):not([tabindex="-1"])',
1491 'textarea:not([disabled]):not([tabindex="-1"])', 989 'textarea:not([disabled]):not([tabindex="-1"])',
1492 'button:not([disabled]):not([tabindex="-1"])', 990 'button:not([disabled]):not([tabindex="-1"])',
1493 'iframe:not([tabindex="-1"])', 991 'iframe:not([tabindex="-1"])',
1494 '[tabindex]:not([tabindex="-1"])', 992 '[tabindex]:not([tabindex="-1"])',
1495 '[contentEditable=true]:not([tabindex="-1"])' 993 '[contentEditable=true]:not([tabindex="-1"])'
1496 ].join(','); 994 ].join(',');
1497 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem entsSelector); 995 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem entsSelector);
1498 996
1499 if (focusableElements.length > 0) { 997 if (focusableElements.length > 0) {
1500 this._firstTabStop = focusableElements[0]; 998 this._firstTabStop = focusableElements[0];
1501 this._lastTabStop = focusableElements[focusableElements.length - 1]; 999 this._lastTabStop = focusableElements[focusableElements.length - 1];
1502 } else { 1000 } else {
1503 // Reset saved tab stops when there are no focusable elements in the d rawer.
1504 this._firstTabStop = null; 1001 this._firstTabStop = null;
1505 this._lastTabStop = null; 1002 this._lastTabStop = null;
1506 } 1003 }
1507 1004
1508 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the first focusable
1509 // element in the drawer, if it exists. Use the tabindex attribute since the this.tabIndex
1510 // property in IE/Edge returns 0 (instead of -1) when the attribute is n ot set.
1511 var tabindex = this.getAttribute('tabindex'); 1005 var tabindex = this.getAttribute('tabindex');
1512 if (tabindex && parseInt(tabindex, 10) > -1) { 1006 if (tabindex && parseInt(tabindex, 10) > -1) {
1513 this.focus(); 1007 this.focus();
1514 } else if (this._firstTabStop) { 1008 } else if (this._firstTabStop) {
1515 this._firstTabStop.focus(); 1009 this._firstTabStop.focus();
1516 } 1010 }
1517 }, 1011 },
1518 1012
1519 _tabKeydownHandler: function(event) { 1013 _tabKeydownHandler: function(event) {
1520 if (this.noFocusTrap) { 1014 if (this.noFocusTrap) {
(...skipping 26 matching lines...) Expand all
1547 1041
1548 _DRAWER_STATE: { 1042 _DRAWER_STATE: {
1549 INIT: 0, 1043 INIT: 0,
1550 OPENED: 1, 1044 OPENED: 1,
1551 OPENED_PERSISTENT: 2, 1045 OPENED_PERSISTENT: 2,
1552 CLOSED: 3, 1046 CLOSED: 3,
1553 TRACKING: 4, 1047 TRACKING: 4,
1554 FLINGING: 5 1048 FLINGING: 5
1555 } 1049 }
1556 1050
1557 /**
1558 * Fired when the layout of app-drawer has changed.
1559 *
1560 * @event app-drawer-reset-layout
1561 */
1562 1051
1563 /**
1564 * Fired when app-drawer has finished transitioning.
1565 *
1566 * @event app-drawer-transitioned
1567 */
1568 }); 1052 });
1569 (function() { 1053 (function() {
1570 'use strict'; 1054 'use strict';
1571 1055
1572 Polymer({ 1056 Polymer({
1573 is: 'iron-location', 1057 is: 'iron-location',
1574 properties: { 1058 properties: {
1575 /**
1576 * The pathname component of the URL.
1577 */
1578 path: { 1059 path: {
1579 type: String, 1060 type: String,
1580 notify: true, 1061 notify: true,
1581 value: function() { 1062 value: function() {
1582 return window.decodeURIComponent(window.location.pathname); 1063 return window.decodeURIComponent(window.location.pathname);
1583 } 1064 }
1584 }, 1065 },
1585 /**
1586 * The query string portion of the URL.
1587 */
1588 query: { 1066 query: {
1589 type: String, 1067 type: String,
1590 notify: true, 1068 notify: true,
1591 value: function() { 1069 value: function() {
1592 return window.decodeURIComponent(window.location.search.slice(1)); 1070 return window.decodeURIComponent(window.location.search.slice(1));
1593 } 1071 }
1594 }, 1072 },
1595 /**
1596 * The hash component of the URL.
1597 */
1598 hash: { 1073 hash: {
1599 type: String, 1074 type: String,
1600 notify: true, 1075 notify: true,
1601 value: function() { 1076 value: function() {
1602 return window.decodeURIComponent(window.location.hash.slice(1)); 1077 return window.decodeURIComponent(window.location.hash.slice(1));
1603 } 1078 }
1604 }, 1079 },
1605 /**
1606 * If the user was on a URL for less than `dwellTime` milliseconds, it
1607 * won't be added to the browser's history, but instead will be replaced
1608 * by the next entry.
1609 *
1610 * This is to prevent large numbers of entries from clogging up the user 's
1611 * browser history. Disable by setting to a negative number.
1612 */
1613 dwellTime: { 1080 dwellTime: {
1614 type: Number, 1081 type: Number,
1615 value: 2000 1082 value: 2000
1616 }, 1083 },
1617 1084
1618 /**
1619 * A regexp that defines the set of URLs that should be considered part
1620 * of this web app.
1621 *
1622 * Clicking on a link that matches this regex won't result in a full pag e
1623 * navigation, but will instead just update the URL state in place.
1624 *
1625 * This regexp is given everything after the origin in an absolute
1626 * URL. So to match just URLs that start with /search/ do:
1627 * url-space-regex="^/search/"
1628 *
1629 * @type {string|RegExp}
1630 */
1631 urlSpaceRegex: { 1085 urlSpaceRegex: {
1632 type: String, 1086 type: String,
1633 value: '' 1087 value: ''
1634 }, 1088 },
1635 1089
1636 /**
1637 * urlSpaceRegex, but coerced into a regexp.
1638 *
1639 * @type {RegExp}
1640 */
1641 _urlSpaceRegExp: { 1090 _urlSpaceRegExp: {
1642 computed: '_makeRegExp(urlSpaceRegex)' 1091 computed: '_makeRegExp(urlSpaceRegex)'
1643 }, 1092 },
1644 1093
1645 _lastChangedAt: { 1094 _lastChangedAt: {
1646 type: Number 1095 type: Number
1647 }, 1096 },
1648 1097
1649 _initialized: { 1098 _initialized: {
1650 type: Boolean, 1099 type: Boolean,
1651 value: false 1100 value: false
1652 } 1101 }
1653 }, 1102 },
1654 hostAttributes: { 1103 hostAttributes: {
1655 hidden: true 1104 hidden: true
1656 }, 1105 },
1657 observers: [ 1106 observers: [
1658 '_updateUrl(path, query, hash)' 1107 '_updateUrl(path, query, hash)'
1659 ], 1108 ],
1660 attached: function() { 1109 attached: function() {
1661 this.listen(window, 'hashchange', '_hashChanged'); 1110 this.listen(window, 'hashchange', '_hashChanged');
1662 this.listen(window, 'location-changed', '_urlChanged'); 1111 this.listen(window, 'location-changed', '_urlChanged');
1663 this.listen(window, 'popstate', '_urlChanged'); 1112 this.listen(window, 'popstate', '_urlChanged');
1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g lobalOnClick'); 1113 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g lobalOnClick');
1665 // Give a 200ms grace period to make initial redirects without any
1666 // additions to the user's history.
1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); 1114 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
1668 1115
1669 this._initialized = true; 1116 this._initialized = true;
1670 this._urlChanged(); 1117 this._urlChanged();
1671 }, 1118 },
1672 detached: function() { 1119 detached: function() {
1673 this.unlisten(window, 'hashchange', '_hashChanged'); 1120 this.unlisten(window, 'hashchange', '_hashChanged');
1674 this.unlisten(window, 'location-changed', '_urlChanged'); 1121 this.unlisten(window, 'location-changed', '_urlChanged');
1675 this.unlisten(window, 'popstate', '_urlChanged'); 1122 this.unlisten(window, 'popstate', '_urlChanged');
1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', ' _globalOnClick'); 1123 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', ' _globalOnClick');
1677 this._initialized = false; 1124 this._initialized = false;
1678 }, 1125 },
1679 _hashChanged: function() { 1126 _hashChanged: function() {
1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1)) ; 1127 this.hash = window.decodeURIComponent(window.location.hash.substring(1)) ;
1681 }, 1128 },
1682 _urlChanged: function() { 1129 _urlChanged: function() {
1683 // We want to extract all info out of the updated URL before we
1684 // try to write anything back into it.
1685 //
1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
1687 // one when we set this.hash. Likewise for query.
1688 this._dontUpdateUrl = true; 1130 this._dontUpdateUrl = true;
1689 this._hashChanged(); 1131 this._hashChanged();
1690 this.path = window.decodeURIComponent(window.location.pathname); 1132 this.path = window.decodeURIComponent(window.location.pathname);
1691 this.query = window.decodeURIComponent( 1133 this.query = window.decodeURIComponent(
1692 window.location.search.substring(1)); 1134 window.location.search.substring(1));
1693 this._dontUpdateUrl = false; 1135 this._dontUpdateUrl = false;
1694 this._updateUrl(); 1136 this._updateUrl();
1695 }, 1137 },
1696 _getUrl: function() { 1138 _getUrl: function() {
1697 var partiallyEncodedPath = window.encodeURI( 1139 var partiallyEncodedPath = window.encodeURI(
(...skipping 12 matching lines...) Expand all
1710 }, 1152 },
1711 _updateUrl: function() { 1153 _updateUrl: function() {
1712 if (this._dontUpdateUrl || !this._initialized) { 1154 if (this._dontUpdateUrl || !this._initialized) {
1713 return; 1155 return;
1714 } 1156 }
1715 if (this.path === window.decodeURIComponent(window.location.pathname) && 1157 if (this.path === window.decodeURIComponent(window.location.pathname) &&
1716 this.query === window.decodeURIComponent( 1158 this.query === window.decodeURIComponent(
1717 window.location.search.substring(1)) && 1159 window.location.search.substring(1)) &&
1718 this.hash === window.decodeURIComponent( 1160 this.hash === window.decodeURIComponent(
1719 window.location.hash.substring(1))) { 1161 window.location.hash.substring(1))) {
1720 // Nothing to do, the current URL is a representation of our propertie s.
1721 return; 1162 return;
1722 } 1163 }
1723 var newUrl = this._getUrl(); 1164 var newUrl = this._getUrl();
1724 // Need to use a full URL in case the containing page has a base URI.
1725 var fullNewUrl = new URL( 1165 var fullNewUrl = new URL(
1726 newUrl, window.location.protocol + '//' + window.location.host).href ; 1166 newUrl, window.location.protocol + '//' + window.location.host).href ;
1727 var now = window.performance.now(); 1167 var now = window.performance.now();
1728 var shouldReplace = 1168 var shouldReplace =
1729 this._lastChangedAt + this.dwellTime > now; 1169 this._lastChangedAt + this.dwellTime > now;
1730 this._lastChangedAt = now; 1170 this._lastChangedAt = now;
1731 if (shouldReplace) { 1171 if (shouldReplace) {
1732 window.history.replaceState({}, '', fullNewUrl); 1172 window.history.replaceState({}, '', fullNewUrl);
1733 } else { 1173 } else {
1734 window.history.pushState({}, '', fullNewUrl); 1174 window.history.pushState({}, '', fullNewUrl);
1735 } 1175 }
1736 this.fire('location-changed', {}, {node: window}); 1176 this.fire('location-changed', {}, {node: window});
1737 }, 1177 },
1738 /**
1739 * A necessary evil so that links work as expected. Does its best to
1740 * bail out early if possible.
1741 *
1742 * @param {MouseEvent} event .
1743 */
1744 _globalOnClick: function(event) { 1178 _globalOnClick: function(event) {
1745 // If another event handler has stopped this event then there's nothing
1746 // for us to do. This can happen e.g. when there are multiple
1747 // iron-location elements in a page.
1748 if (event.defaultPrevented) { 1179 if (event.defaultPrevented) {
1749 return; 1180 return;
1750 } 1181 }
1751 var href = this._getSameOriginLinkHref(event); 1182 var href = this._getSameOriginLinkHref(event);
1752 if (!href) { 1183 if (!href) {
1753 return; 1184 return;
1754 } 1185 }
1755 event.preventDefault(); 1186 event.preventDefault();
1756 // If the navigation is to the current page we shouldn't add a history
1757 // entry or fire a change event.
1758 if (href === window.location.href) { 1187 if (href === window.location.href) {
1759 return; 1188 return;
1760 } 1189 }
1761 window.history.pushState({}, '', href); 1190 window.history.pushState({}, '', href);
1762 this.fire('location-changed', {}, {node: window}); 1191 this.fire('location-changed', {}, {node: window});
1763 }, 1192 },
1764 /**
1765 * Returns the absolute URL of the link (if any) that this click event
1766 * is clicking on, if we can and should override the resulting full
1767 * page navigation. Returns null otherwise.
1768 *
1769 * @param {MouseEvent} event .
1770 * @return {string?} .
1771 */
1772 _getSameOriginLinkHref: function(event) { 1193 _getSameOriginLinkHref: function(event) {
1773 // We only care about left-clicks.
1774 if (event.button !== 0) { 1194 if (event.button !== 0) {
1775 return null; 1195 return null;
1776 } 1196 }
1777 // We don't want modified clicks, where the intent is to open the page
1778 // in a new tab.
1779 if (event.metaKey || event.ctrlKey) { 1197 if (event.metaKey || event.ctrlKey) {
1780 return null; 1198 return null;
1781 } 1199 }
1782 var eventPath = Polymer.dom(event).path; 1200 var eventPath = Polymer.dom(event).path;
1783 var anchor = null; 1201 var anchor = null;
1784 for (var i = 0; i < eventPath.length; i++) { 1202 for (var i = 0; i < eventPath.length; i++) {
1785 var element = eventPath[i]; 1203 var element = eventPath[i];
1786 if (element.tagName === 'A' && element.href) { 1204 if (element.tagName === 'A' && element.href) {
1787 anchor = element; 1205 anchor = element;
1788 break; 1206 break;
1789 } 1207 }
1790 } 1208 }
1791 1209
1792 // If there's no link there's nothing to do.
1793 if (!anchor) { 1210 if (!anchor) {
1794 return null; 1211 return null;
1795 } 1212 }
1796 1213
1797 // Target blank is a new tab, don't intercept.
1798 if (anchor.target === '_blank') { 1214 if (anchor.target === '_blank') {
1799 return null; 1215 return null;
1800 } 1216 }
1801 // If the link is for an existing parent frame, don't intercept.
1802 if ((anchor.target === '_top' || 1217 if ((anchor.target === '_top' ||
1803 anchor.target === '_parent') && 1218 anchor.target === '_parent') &&
1804 window.top !== window) { 1219 window.top !== window) {
1805 return null; 1220 return null;
1806 } 1221 }
1807 1222
1808 var href = anchor.href; 1223 var href = anchor.href;
1809 1224
1810 // It only makes sense for us to intercept same-origin navigations.
1811 // pushState/replaceState don't work with cross-origin links.
1812 var url; 1225 var url;
1813 if (document.baseURI != null) { 1226 if (document.baseURI != null) {
1814 url = new URL(href, /** @type {string} */(document.baseURI)); 1227 url = new URL(href, /** @type {string} */(document.baseURI));
1815 } else { 1228 } else {
1816 url = new URL(href); 1229 url = new URL(href);
1817 } 1230 }
1818 1231
1819 var origin; 1232 var origin;
1820 1233
1821 // IE Polyfill
1822 if (window.location.origin) { 1234 if (window.location.origin) {
1823 origin = window.location.origin; 1235 origin = window.location.origin;
1824 } else { 1236 } else {
1825 origin = window.location.protocol + '//' + window.location.hostname; 1237 origin = window.location.protocol + '//' + window.location.hostname;
1826 1238
1827 if (window.location.port) { 1239 if (window.location.port) {
1828 origin += ':' + window.location.port; 1240 origin += ':' + window.location.port;
1829 } 1241 }
1830 } 1242 }
1831 1243
1832 if (url.origin !== origin) { 1244 if (url.origin !== origin) {
1833 return null; 1245 return null;
1834 } 1246 }
1835 var normalizedHref = url.pathname + url.search + url.hash; 1247 var normalizedHref = url.pathname + url.search + url.hash;
1836 1248
1837 // If we've been configured not to handle this url... don't handle it!
1838 if (this._urlSpaceRegExp && 1249 if (this._urlSpaceRegExp &&
1839 !this._urlSpaceRegExp.test(normalizedHref)) { 1250 !this._urlSpaceRegExp.test(normalizedHref)) {
1840 return null; 1251 return null;
1841 } 1252 }
1842 // Need to use a full URL in case the containing page has a base URI.
1843 var fullNormalizedHref = new URL( 1253 var fullNormalizedHref = new URL(
1844 normalizedHref, window.location.href).href; 1254 normalizedHref, window.location.href).href;
1845 return fullNormalizedHref; 1255 return fullNormalizedHref;
1846 }, 1256 },
1847 _makeRegExp: function(urlSpaceRegex) { 1257 _makeRegExp: function(urlSpaceRegex) {
1848 return RegExp(urlSpaceRegex); 1258 return RegExp(urlSpaceRegex);
1849 } 1259 }
1850 }); 1260 });
1851 })(); 1261 })();
1852 'use strict'; 1262 'use strict';
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
1900 '=' + 1310 '=' +
1901 encodeURIComponent(value.toString()) 1311 encodeURIComponent(value.toString())
1902 ); 1312 );
1903 } 1313 }
1904 } 1314 }
1905 return encodedParams.join('&'); 1315 return encodedParams.join('&');
1906 }, 1316 },
1907 _decodeParams: function(paramString) { 1317 _decodeParams: function(paramString) {
1908 var params = {}; 1318 var params = {};
1909 1319
1910 // Work around a bug in decodeURIComponent where + is not
1911 // converted to spaces:
1912 paramString = (paramString || '').replace(/\+/g, '%20'); 1320 paramString = (paramString || '').replace(/\+/g, '%20');
1913 1321
1914 var paramList = paramString.split('&'); 1322 var paramList = paramString.split('&');
1915 for (var i = 0; i < paramList.length; i++) { 1323 for (var i = 0; i < paramList.length; i++) {
1916 var param = paramList[i].split('='); 1324 var param = paramList[i].split('=');
1917 if (param[0]) { 1325 if (param[0]) {
1918 params[decodeURIComponent(param[0])] = 1326 params[decodeURIComponent(param[0])] =
1919 decodeURIComponent(param[1] || ''); 1327 decodeURIComponent(param[1] || '');
1920 } 1328 }
1921 } 1329 }
1922 return params; 1330 return params;
1923 } 1331 }
1924 }); 1332 });
1925 'use strict'; 1333 'use strict';
1926 1334
1927 /**
1928 * Provides bidirectional mapping between `path` and `queryParams` and a
1929 * app-route compatible `route` object.
1930 *
1931 * For more information, see the docs for `app-route-converter`.
1932 *
1933 * @polymerBehavior
1934 */
1935 Polymer.AppRouteConverterBehavior = { 1335 Polymer.AppRouteConverterBehavior = {
1936 properties: { 1336 properties: {
1937 /**
1938 * A model representing the deserialized path through the route tree, as
1939 * well as the current queryParams.
1940 *
1941 * A route object is the kernel of the routing system. It is intended to
1942 * be fed into consuming elements such as `app-route`.
1943 *
1944 * @type {?Object}
1945 */
1946 route: { 1337 route: {
1947 type: Object, 1338 type: Object,
1948 notify: true 1339 notify: true
1949 }, 1340 },
1950 1341
1951 /**
1952 * A set of key/value pairs that are universally accessible to branches of
1953 * the route tree.
1954 *
1955 * @type {?Object}
1956 */
1957 queryParams: { 1342 queryParams: {
1958 type: Object, 1343 type: Object,
1959 notify: true 1344 notify: true
1960 }, 1345 },
1961 1346
1962 /**
1963 * The serialized path through the route tree. This corresponds to the
1964 * `window.location.pathname` value, and will update to reflect changes
1965 * to that value.
1966 */
1967 path: { 1347 path: {
1968 type: String, 1348 type: String,
1969 notify: true, 1349 notify: true,
1970 } 1350 }
1971 }, 1351 },
1972 1352
1973 observers: [ 1353 observers: [
1974 '_locationChanged(path, queryParams)', 1354 '_locationChanged(path, queryParams)',
1975 '_routeChanged(route.prefix, route.path)', 1355 '_routeChanged(route.prefix, route.path)',
1976 '_routeQueryParamsChanged(route.__queryParams)' 1356 '_routeQueryParamsChanged(route.__queryParams)'
1977 ], 1357 ],
1978 1358
1979 created: function() { 1359 created: function() {
1980 this.linkPaths('route.__queryParams', 'queryParams'); 1360 this.linkPaths('route.__queryParams', 'queryParams');
1981 this.linkPaths('queryParams', 'route.__queryParams'); 1361 this.linkPaths('queryParams', 'route.__queryParams');
1982 }, 1362 },
1983 1363
1984 /**
1985 * Handler called when the path or queryParams change.
1986 */
1987 _locationChanged: function() { 1364 _locationChanged: function() {
1988 if (this.route && 1365 if (this.route &&
1989 this.route.path === this.path && 1366 this.route.path === this.path &&
1990 this.queryParams === this.route.__queryParams) { 1367 this.queryParams === this.route.__queryParams) {
1991 return; 1368 return;
1992 } 1369 }
1993 this.route = { 1370 this.route = {
1994 prefix: '', 1371 prefix: '',
1995 path: this.path, 1372 path: this.path,
1996 __queryParams: this.queryParams 1373 __queryParams: this.queryParams
1997 }; 1374 };
1998 }, 1375 },
1999 1376
2000 /**
2001 * Handler called when the route prefix and route path change.
2002 */
2003 _routeChanged: function() { 1377 _routeChanged: function() {
2004 if (!this.route) { 1378 if (!this.route) {
2005 return; 1379 return;
2006 } 1380 }
2007 1381
2008 this.path = this.route.prefix + this.route.path; 1382 this.path = this.route.prefix + this.route.path;
2009 }, 1383 },
2010 1384
2011 /**
2012 * Handler called when the route queryParams change.
2013 *
2014 * @param {Object} queryParams A set of key/value pairs that are
2015 * universally accessible to branches of the route tree.
2016 */
2017 _routeQueryParamsChanged: function(queryParams) { 1385 _routeQueryParamsChanged: function(queryParams) {
2018 if (!this.route) { 1386 if (!this.route) {
2019 return; 1387 return;
2020 } 1388 }
2021 this.queryParams = queryParams; 1389 this.queryParams = queryParams;
2022 } 1390 }
2023 }; 1391 };
2024 'use strict'; 1392 'use strict';
2025 1393
2026 Polymer({ 1394 Polymer({
2027 is: 'app-location', 1395 is: 'app-location',
2028 1396
2029 properties: { 1397 properties: {
2030 /**
2031 * A model representing the deserialized path through the route tree, as
2032 * well as the current queryParams.
2033 */
2034 route: { 1398 route: {
2035 type: Object, 1399 type: Object,
2036 notify: true 1400 notify: true
2037 }, 1401 },
2038 1402
2039 /**
2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in
2041 * alternative to the `path`. For example, if deploying an app to a stat ic
2042 * web server (e.g., Github Pages) - where one does not have control ove r
2043 * server-side routing - it is usually a better experience to use the ha sh
2044 * to represent paths through one's app.
2045 *
2046 * When this property is set to true, the `hash` will be used in place o f
2047
2048 * the `path` for generating a `route`.
2049 */
2050 useHashAsPath: { 1403 useHashAsPath: {
2051 type: Boolean, 1404 type: Boolean,
2052 value: false 1405 value: false
2053 }, 1406 },
2054 1407
2055 /**
2056 * A regexp that defines the set of URLs that should be considered part
2057 * of this web app.
2058 *
2059 * Clicking on a link that matches this regex won't result in a full pag e
2060 * navigation, but will instead just update the URL state in place.
2061 *
2062 * This regexp is given everything after the origin in an absolute
2063 * URL. So to match just URLs that start with /search/ do:
2064 * url-space-regex="^/search/"
2065 *
2066 * @type {string|RegExp}
2067 */
2068 urlSpaceRegex: { 1408 urlSpaceRegex: {
2069 type: String, 1409 type: String,
2070 notify: true 1410 notify: true
2071 }, 1411 },
2072 1412
2073 /**
2074 * A set of key/value pairs that are universally accessible to branches
2075 * of the route tree.
2076 */
2077 __queryParams: { 1413 __queryParams: {
2078 type: Object 1414 type: Object
2079 }, 1415 },
2080 1416
2081 /**
2082 * The pathname component of the current URL.
2083 */
2084 __path: { 1417 __path: {
2085 type: String 1418 type: String
2086 }, 1419 },
2087 1420
2088 /**
2089 * The query string portion of the current URL.
2090 */
2091 __query: { 1421 __query: {
2092 type: String 1422 type: String
2093 }, 1423 },
2094 1424
2095 /**
2096 * The hash portion of the current URL.
2097 */
2098 __hash: { 1425 __hash: {
2099 type: String 1426 type: String
2100 }, 1427 },
2101 1428
2102 /**
2103 * The route path, which will be either the hash or the path, depending
2104 * on useHashAsPath.
2105 */
2106 path: { 1429 path: {
2107 type: String, 1430 type: String,
2108 observer: '__onPathChanged' 1431 observer: '__onPathChanged'
2109 } 1432 }
2110 }, 1433 },
2111 1434
2112 behaviors: [Polymer.AppRouteConverterBehavior], 1435 behaviors: [Polymer.AppRouteConverterBehavior],
2113 1436
2114 observers: [ 1437 observers: [
2115 '__computeRoutePath(useHashAsPath, __hash, __path)' 1438 '__computeRoutePath(useHashAsPath, __hash, __path)'
(...skipping 14 matching lines...) Expand all
2130 this.__path = this.path; 1453 this.__path = this.path;
2131 } 1454 }
2132 } 1455 }
2133 }); 1456 });
2134 'use strict'; 1457 'use strict';
2135 1458
2136 Polymer({ 1459 Polymer({
2137 is: 'app-route', 1460 is: 'app-route',
2138 1461
2139 properties: { 1462 properties: {
2140 /**
2141 * The URL component managed by this element.
2142 */
2143 route: { 1463 route: {
2144 type: Object, 1464 type: Object,
2145 notify: true 1465 notify: true
2146 }, 1466 },
2147 1467
2148 /**
2149 * The pattern of slash-separated segments to match `path` against.
2150 *
2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar"
2152 * but not "/foobar".
2153 *
2154 * Path segments like `/:named` are mapped to properties on the `data` obj ect.
2155 */
2156 pattern: { 1468 pattern: {
2157 type: String 1469 type: String
2158 }, 1470 },
2159 1471
2160 /**
2161 * The parameterized values that are extracted from the route as
2162 * described by `pattern`.
2163 */
2164 data: { 1472 data: {
2165 type: Object, 1473 type: Object,
2166 value: function() {return {};}, 1474 value: function() {return {};},
2167 notify: true 1475 notify: true
2168 }, 1476 },
2169 1477
2170 /**
2171 * @type {?Object}
2172 */
2173 queryParams: { 1478 queryParams: {
2174 type: Object, 1479 type: Object,
2175 value: function() { 1480 value: function() {
2176 return {}; 1481 return {};
2177 }, 1482 },
2178 notify: true 1483 notify: true
2179 }, 1484 },
2180 1485
2181 /**
2182 * The part of `path` NOT consumed by `pattern`.
2183 */
2184 tail: { 1486 tail: {
2185 type: Object, 1487 type: Object,
2186 value: function() {return {path: null, prefix: null, __queryParams: null };}, 1488 value: function() {return {path: null, prefix: null, __queryParams: null };},
2187 notify: true 1489 notify: true
2188 }, 1490 },
2189 1491
2190 active: { 1492 active: {
2191 type: Boolean, 1493 type: Boolean,
2192 notify: true, 1494 notify: true,
2193 readOnly: true 1495 readOnly: true
2194 }, 1496 },
2195 1497
2196 _queryParamsUpdating: { 1498 _queryParamsUpdating: {
2197 type: Boolean, 1499 type: Boolean,
2198 value: false 1500 value: false
2199 }, 1501 },
2200 /**
2201 * @type {?string}
2202 */
2203 _matched: { 1502 _matched: {
2204 type: String, 1503 type: String,
2205 value: '' 1504 value: ''
2206 } 1505 }
2207 }, 1506 },
2208 1507
2209 observers: [ 1508 observers: [
2210 '__tryToMatch(route.path, pattern)', 1509 '__tryToMatch(route.path, pattern)',
2211 '__updatePathOnDataChange(data.*)', 1510 '__updatePathOnDataChange(data.*)',
2212 '__tailPathChanged(tail.path)', 1511 '__tailPathChanged(tail.path)',
2213 '__routeQueryParamsChanged(route.__queryParams)', 1512 '__routeQueryParamsChanged(route.__queryParams)',
2214 '__tailQueryParamsChanged(tail.__queryParams)', 1513 '__tailQueryParamsChanged(tail.__queryParams)',
2215 '__queryParamsChanged(queryParams.*)' 1514 '__queryParamsChanged(queryParams.*)'
2216 ], 1515 ],
2217 1516
2218 created: function() { 1517 created: function() {
2219 this.linkPaths('route.__queryParams', 'tail.__queryParams'); 1518 this.linkPaths('route.__queryParams', 'tail.__queryParams');
2220 this.linkPaths('tail.__queryParams', 'route.__queryParams'); 1519 this.linkPaths('tail.__queryParams', 'route.__queryParams');
2221 }, 1520 },
2222 1521
2223 /**
2224 * Deal with the query params object being assigned to wholesale.
2225 * @export
2226 */
2227 __routeQueryParamsChanged: function(queryParams) { 1522 __routeQueryParamsChanged: function(queryParams) {
2228 if (queryParams && this.tail) { 1523 if (queryParams && this.tail) {
2229 this.set('tail.__queryParams', queryParams); 1524 this.set('tail.__queryParams', queryParams);
2230 1525
2231 if (!this.active || this._queryParamsUpdating) { 1526 if (!this.active || this._queryParamsUpdating) {
2232 return; 1527 return;
2233 } 1528 }
2234 1529
2235 // Copy queryParams and track whether there are any differences compared
2236 // to the existing query params.
2237 var copyOfQueryParams = {}; 1530 var copyOfQueryParams = {};
2238 var anythingChanged = false; 1531 var anythingChanged = false;
2239 for (var key in queryParams) { 1532 for (var key in queryParams) {
2240 copyOfQueryParams[key] = queryParams[key]; 1533 copyOfQueryParams[key] = queryParams[key];
2241 if (anythingChanged || 1534 if (anythingChanged ||
2242 !this.queryParams || 1535 !this.queryParams ||
2243 queryParams[key] !== this.queryParams[key]) { 1536 queryParams[key] !== this.queryParams[key]) {
2244 anythingChanged = true; 1537 anythingChanged = true;
2245 } 1538 }
2246 } 1539 }
2247 // Need to check whether any keys were deleted
2248 for (var key in this.queryParams) { 1540 for (var key in this.queryParams) {
2249 if (anythingChanged || !(key in queryParams)) { 1541 if (anythingChanged || !(key in queryParams)) {
2250 anythingChanged = true; 1542 anythingChanged = true;
2251 break; 1543 break;
2252 } 1544 }
2253 } 1545 }
2254 1546
2255 if (!anythingChanged) { 1547 if (!anythingChanged) {
2256 return; 1548 return;
2257 } 1549 }
2258 this._queryParamsUpdating = true; 1550 this._queryParamsUpdating = true;
2259 this.set('queryParams', copyOfQueryParams); 1551 this.set('queryParams', copyOfQueryParams);
2260 this._queryParamsUpdating = false; 1552 this._queryParamsUpdating = false;
2261 } 1553 }
2262 }, 1554 },
2263 1555
2264 /**
2265 * @export
2266 */
2267 __tailQueryParamsChanged: function(queryParams) { 1556 __tailQueryParamsChanged: function(queryParams) {
2268 if (queryParams && this.route) { 1557 if (queryParams && this.route) {
2269 this.set('route.__queryParams', queryParams); 1558 this.set('route.__queryParams', queryParams);
2270 } 1559 }
2271 }, 1560 },
2272 1561
2273 /**
2274 * @export
2275 */
2276 __queryParamsChanged: function(changes) { 1562 __queryParamsChanged: function(changes) {
2277 if (!this.active || this._queryParamsUpdating) { 1563 if (!this.active || this._queryParamsUpdating) {
2278 return; 1564 return;
2279 } 1565 }
2280 1566
2281 this.set('route.__' + changes.path, changes.value); 1567 this.set('route.__' + changes.path, changes.value);
2282 }, 1568 },
2283 1569
2284 __resetProperties: function() { 1570 __resetProperties: function() {
2285 this._setActive(false); 1571 this._setActive(false);
2286 this._matched = null; 1572 this._matched = null;
2287 //this.tail = { path: null, prefix: null, queryParams: null };
2288 //this.data = {};
2289 }, 1573 },
2290 1574
2291 /**
2292 * @export
2293 */
2294 __tryToMatch: function() { 1575 __tryToMatch: function() {
2295 if (!this.route) { 1576 if (!this.route) {
2296 return; 1577 return;
2297 } 1578 }
2298 var path = this.route.path; 1579 var path = this.route.path;
2299 var pattern = this.pattern; 1580 var pattern = this.pattern;
2300 if (!pattern) { 1581 if (!pattern) {
2301 return; 1582 return;
2302 } 1583 }
2303 1584
2304 if (!path) { 1585 if (!path) {
2305 this.__resetProperties(); 1586 this.__resetProperties();
2306 return; 1587 return;
2307 } 1588 }
2308 1589
2309 var remainingPieces = path.split('/'); 1590 var remainingPieces = path.split('/');
2310 var patternPieces = pattern.split('/'); 1591 var patternPieces = pattern.split('/');
2311 1592
2312 var matched = []; 1593 var matched = [];
2313 var namedMatches = {}; 1594 var namedMatches = {};
2314 1595
2315 for (var i=0; i < patternPieces.length; i++) { 1596 for (var i=0; i < patternPieces.length; i++) {
2316 var patternPiece = patternPieces[i]; 1597 var patternPiece = patternPieces[i];
2317 if (!patternPiece && patternPiece !== '') { 1598 if (!patternPiece && patternPiece !== '') {
2318 break; 1599 break;
2319 } 1600 }
2320 var pathPiece = remainingPieces.shift(); 1601 var pathPiece = remainingPieces.shift();
2321 1602
2322 // We don't match this path.
2323 if (!pathPiece && pathPiece !== '') { 1603 if (!pathPiece && pathPiece !== '') {
2324 this.__resetProperties(); 1604 this.__resetProperties();
2325 return; 1605 return;
2326 } 1606 }
2327 matched.push(pathPiece); 1607 matched.push(pathPiece);
2328 1608
2329 if (patternPiece.charAt(0) == ':') { 1609 if (patternPiece.charAt(0) == ':') {
2330 namedMatches[patternPiece.slice(1)] = pathPiece; 1610 namedMatches[patternPiece.slice(1)] = pathPiece;
2331 } else if (patternPiece !== pathPiece) { 1611 } else if (patternPiece !== pathPiece) {
2332 this.__resetProperties(); 1612 this.__resetProperties();
2333 return; 1613 return;
2334 } 1614 }
2335 } 1615 }
2336 1616
2337 this._matched = matched.join('/'); 1617 this._matched = matched.join('/');
2338 1618
2339 // Properties that must be updated atomically.
2340 var propertyUpdates = {}; 1619 var propertyUpdates = {};
2341 1620
2342 //this.active
2343 if (!this.active) { 1621 if (!this.active) {
2344 propertyUpdates.active = true; 1622 propertyUpdates.active = true;
2345 } 1623 }
2346 1624
2347 // this.tail
2348 var tailPrefix = this.route.prefix + this._matched; 1625 var tailPrefix = this.route.prefix + this._matched;
2349 var tailPath = remainingPieces.join('/'); 1626 var tailPath = remainingPieces.join('/');
2350 if (remainingPieces.length > 0) { 1627 if (remainingPieces.length > 0) {
2351 tailPath = '/' + tailPath; 1628 tailPath = '/' + tailPath;
2352 } 1629 }
2353 if (!this.tail || 1630 if (!this.tail ||
2354 this.tail.prefix !== tailPrefix || 1631 this.tail.prefix !== tailPrefix ||
2355 this.tail.path !== tailPath) { 1632 this.tail.path !== tailPath) {
2356 propertyUpdates.tail = { 1633 propertyUpdates.tail = {
2357 prefix: tailPrefix, 1634 prefix: tailPrefix,
2358 path: tailPath, 1635 path: tailPath,
2359 __queryParams: this.route.__queryParams 1636 __queryParams: this.route.__queryParams
2360 }; 1637 };
2361 } 1638 }
2362 1639
2363 // this.data
2364 propertyUpdates.data = namedMatches; 1640 propertyUpdates.data = namedMatches;
2365 this._dataInUrl = {}; 1641 this._dataInUrl = {};
2366 for (var key in namedMatches) { 1642 for (var key in namedMatches) {
2367 this._dataInUrl[key] = namedMatches[key]; 1643 this._dataInUrl[key] = namedMatches[key];
2368 } 1644 }
2369 1645
2370 this.__setMulti(propertyUpdates); 1646 this.__setMulti(propertyUpdates);
2371 }, 1647 },
2372 1648
2373 /**
2374 * @export
2375 */
2376 __tailPathChanged: function() { 1649 __tailPathChanged: function() {
2377 if (!this.active) { 1650 if (!this.active) {
2378 return; 1651 return;
2379 } 1652 }
2380 var tailPath = this.tail.path; 1653 var tailPath = this.tail.path;
2381 var newPath = this._matched; 1654 var newPath = this._matched;
2382 if (tailPath) { 1655 if (tailPath) {
2383 if (tailPath.charAt(0) !== '/') { 1656 if (tailPath.charAt(0) !== '/') {
2384 tailPath = '/' + tailPath; 1657 tailPath = '/' + tailPath;
2385 } 1658 }
2386 newPath += tailPath; 1659 newPath += tailPath;
2387 } 1660 }
2388 this.set('route.path', newPath); 1661 this.set('route.path', newPath);
2389 }, 1662 },
2390 1663
2391 /**
2392 * @export
2393 */
2394 __updatePathOnDataChange: function() { 1664 __updatePathOnDataChange: function() {
2395 if (!this.route || !this.active) { 1665 if (!this.route || !this.active) {
2396 return; 1666 return;
2397 } 1667 }
2398 var newPath = this.__getLink({}); 1668 var newPath = this.__getLink({});
2399 var oldPath = this.__getLink(this._dataInUrl); 1669 var oldPath = this.__getLink(this._dataInUrl);
2400 if (newPath === oldPath) { 1670 if (newPath === oldPath) {
2401 return; 1671 return;
2402 } 1672 }
2403 this.set('route.path', newPath); 1673 this.set('route.path', newPath);
(...skipping 18 matching lines...) Expand all
2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { 1692 if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
2423 interp.push(values.tail.path.slice(1)); 1693 interp.push(values.tail.path.slice(1));
2424 } else { 1694 } else {
2425 interp.push(values.tail.path); 1695 interp.push(values.tail.path);
2426 } 1696 }
2427 } 1697 }
2428 return interp.join('/'); 1698 return interp.join('/');
2429 }, 1699 },
2430 1700
2431 __setMulti: function(setObj) { 1701 __setMulti: function(setObj) {
2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at
2433 // internal data structures. I would not advise that you copy this
2434 // example.
2435 //
2436 // In the future this will be a feature of Polymer itself.
2437 // See: https://github.com/Polymer/polymer/issues/3640
2438 //
2439 // Hacking around with private methods like this is juggling footguns,
2440 // and is likely to have unexpected and unsupported rough edges.
2441 //
2442 // Be ye so warned.
2443 for (var property in setObj) { 1702 for (var property in setObj) {
2444 this._propertySetter(property, setObj[property]); 1703 this._propertySetter(property, setObj[property]);
2445 } 1704 }
2446 1705
2447 for (var property in setObj) { 1706 for (var property in setObj) {
2448 this._pathEffector(property, this[property]); 1707 this._pathEffector(property, this[property]);
2449 this._notifyPathUp(property, this[property]); 1708 this._notifyPathUp(property, this[property]);
2450 } 1709 }
2451 } 1710 }
2452 }); 1711 });
2453 Polymer({ 1712 Polymer({
2454 1713
2455 is: 'iron-media-query', 1714 is: 'iron-media-query',
2456 1715
2457 properties: { 1716 properties: {
2458 1717
2459 /**
2460 * The Boolean return value of the media query.
2461 */
2462 queryMatches: { 1718 queryMatches: {
2463 type: Boolean, 1719 type: Boolean,
2464 value: false, 1720 value: false,
2465 readOnly: true, 1721 readOnly: true,
2466 notify: true 1722 notify: true
2467 }, 1723 },
2468 1724
2469 /**
2470 * The CSS media query to evaluate.
2471 */
2472 query: { 1725 query: {
2473 type: String, 1726 type: String,
2474 observer: 'queryChanged' 1727 observer: 'queryChanged'
2475 }, 1728 },
2476 1729
2477 /**
2478 * If true, the query attribute is assumed to be a complete media query
2479 * string rather than a single media feature.
2480 */
2481 full: { 1730 full: {
2482 type: Boolean, 1731 type: Boolean,
2483 value: false 1732 value: false
2484 }, 1733 },
2485 1734
2486 /**
2487 * @type {function(MediaQueryList)}
2488 */
2489 _boundMQHandler: { 1735 _boundMQHandler: {
2490 value: function() { 1736 value: function() {
2491 return this.queryHandler.bind(this); 1737 return this.queryHandler.bind(this);
2492 } 1738 }
2493 }, 1739 },
2494 1740
2495 /**
2496 * @type {MediaQueryList}
2497 */
2498 _mq: { 1741 _mq: {
2499 value: null 1742 value: null
2500 } 1743 }
2501 }, 1744 },
2502 1745
2503 attached: function() { 1746 attached: function() {
2504 this.style.display = 'none'; 1747 this.style.display = 'none';
2505 this.queryChanged(); 1748 this.queryChanged();
2506 }, 1749 },
2507 1750
(...skipping 26 matching lines...) Expand all
2534 this._mq = window.matchMedia(query); 1777 this._mq = window.matchMedia(query);
2535 this._add(); 1778 this._add();
2536 this.queryHandler(this._mq); 1779 this.queryHandler(this._mq);
2537 }, 1780 },
2538 1781
2539 queryHandler: function(mq) { 1782 queryHandler: function(mq) {
2540 this._setQueryMatches(mq.matches); 1783 this._setQueryMatches(mq.matches);
2541 } 1784 }
2542 1785
2543 }); 1786 });
2544 /**
2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
2546 * coordinate the flow of resize events between "resizers" (elements that cont rol the
2547 * size or hidden state of their children) and "resizables" (elements that nee d to be
2548 * notified when they are resized or un-hidden by their parents in order to ta ke
2549 * action on their new measurements).
2550 *
2551 * Elements that perform measurement should add the `IronResizableBehavior` be havior to
2552 * their element definition and listen for the `iron-resize` event on themselv es.
2553 * This event will be fired when they become showing after having been hidden,
2554 * when they are resized explicitly by another resizable, or when the window h as been
2555 * resized.
2556 *
2557 * Note, the `iron-resize` event is non-bubbling.
2558 *
2559 * @polymerBehavior Polymer.IronResizableBehavior
2560 * @demo demo/index.html
2561 **/
2562 Polymer.IronResizableBehavior = { 1787 Polymer.IronResizableBehavior = {
2563 properties: { 1788 properties: {
2564 /**
2565 * The closest ancestor element that implements `IronResizableBehavior`.
2566 */
2567 _parentResizable: { 1789 _parentResizable: {
2568 type: Object, 1790 type: Object,
2569 observer: '_parentResizableChanged' 1791 observer: '_parentResizableChanged'
2570 }, 1792 },
2571 1793
2572 /**
2573 * True if this element is currently notifying its descedant elements of
2574 * resize.
2575 */
2576 _notifyingDescendant: { 1794 _notifyingDescendant: {
2577 type: Boolean, 1795 type: Boolean,
2578 value: false 1796 value: false
2579 } 1797 }
2580 }, 1798 },
2581 1799
2582 listeners: { 1800 listeners: {
2583 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' 1801 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
2584 }, 1802 },
2585 1803
2586 created: function() { 1804 created: function() {
2587 // We don't really need property effects on these, and also we want them
2588 // to be created before the `_parentResizable` observer fires:
2589 this._interestedResizables = []; 1805 this._interestedResizables = [];
2590 this._boundNotifyResize = this.notifyResize.bind(this); 1806 this._boundNotifyResize = this.notifyResize.bind(this);
2591 }, 1807 },
2592 1808
2593 attached: function() { 1809 attached: function() {
2594 this.fire('iron-request-resize-notifications', null, { 1810 this.fire('iron-request-resize-notifications', null, {
2595 node: this, 1811 node: this,
2596 bubbles: true, 1812 bubbles: true,
2597 cancelable: true 1813 cancelable: true
2598 }); 1814 });
2599 1815
2600 if (!this._parentResizable) { 1816 if (!this._parentResizable) {
2601 window.addEventListener('resize', this._boundNotifyResize); 1817 window.addEventListener('resize', this._boundNotifyResize);
2602 this.notifyResize(); 1818 this.notifyResize();
2603 } 1819 }
2604 }, 1820 },
2605 1821
2606 detached: function() { 1822 detached: function() {
2607 if (this._parentResizable) { 1823 if (this._parentResizable) {
2608 this._parentResizable.stopResizeNotificationsFor(this); 1824 this._parentResizable.stopResizeNotificationsFor(this);
2609 } else { 1825 } else {
2610 window.removeEventListener('resize', this._boundNotifyResize); 1826 window.removeEventListener('resize', this._boundNotifyResize);
2611 } 1827 }
2612 1828
2613 this._parentResizable = null; 1829 this._parentResizable = null;
2614 }, 1830 },
2615 1831
2616 /**
2617 * Can be called to manually notify a resizable and its descendant
2618 * resizables of a resize change.
2619 */
2620 notifyResize: function() { 1832 notifyResize: function() {
2621 if (!this.isAttached) { 1833 if (!this.isAttached) {
2622 return; 1834 return;
2623 } 1835 }
2624 1836
2625 this._interestedResizables.forEach(function(resizable) { 1837 this._interestedResizables.forEach(function(resizable) {
2626 if (this.resizerShouldNotify(resizable)) { 1838 if (this.resizerShouldNotify(resizable)) {
2627 this._notifyDescendant(resizable); 1839 this._notifyDescendant(resizable);
2628 } 1840 }
2629 }, this); 1841 }, this);
2630 1842
2631 this._fireResize(); 1843 this._fireResize();
2632 }, 1844 },
2633 1845
2634 /**
2635 * Used to assign the closest resizable ancestor to this resizable
2636 * if the ancestor detects a request for notifications.
2637 */
2638 assignParentResizable: function(parentResizable) { 1846 assignParentResizable: function(parentResizable) {
2639 this._parentResizable = parentResizable; 1847 this._parentResizable = parentResizable;
2640 }, 1848 },
2641 1849
2642 /**
2643 * Used to remove a resizable descendant from the list of descendants
2644 * that should be notified of a resize change.
2645 */
2646 stopResizeNotificationsFor: function(target) { 1850 stopResizeNotificationsFor: function(target) {
2647 var index = this._interestedResizables.indexOf(target); 1851 var index = this._interestedResizables.indexOf(target);
2648 1852
2649 if (index > -1) { 1853 if (index > -1) {
2650 this._interestedResizables.splice(index, 1); 1854 this._interestedResizables.splice(index, 1);
2651 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); 1855 this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
2652 } 1856 }
2653 }, 1857 },
2654 1858
2655 /**
2656 * This method can be overridden to filter nested elements that should or
2657 * should not be notified by the current element. Return true if an element
2658 * should be notified, or false if it should not be notified.
2659 *
2660 * @param {HTMLElement} element A candidate descendant element that
2661 * implements `IronResizableBehavior`.
2662 * @return {boolean} True if the `element` should be notified of resize.
2663 */
2664 resizerShouldNotify: function(element) { return true; }, 1859 resizerShouldNotify: function(element) { return true; },
2665 1860
2666 _onDescendantIronResize: function(event) { 1861 _onDescendantIronResize: function(event) {
2667 if (this._notifyingDescendant) { 1862 if (this._notifyingDescendant) {
2668 event.stopPropagation(); 1863 event.stopPropagation();
2669 return; 1864 return;
2670 } 1865 }
2671 1866
2672 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
2673 // otherwise non-bubbling event "just work." We do it manually here for
2674 // the case where Polymer is not using shadow roots for whatever reason:
2675 if (!Polymer.Settings.useShadow) { 1867 if (!Polymer.Settings.useShadow) {
2676 this._fireResize(); 1868 this._fireResize();
2677 } 1869 }
2678 }, 1870 },
2679 1871
2680 _fireResize: function() { 1872 _fireResize: function() {
2681 this.fire('iron-resize', null, { 1873 this.fire('iron-resize', null, {
2682 node: this, 1874 node: this,
2683 bubbles: false 1875 bubbles: false
2684 }); 1876 });
(...skipping 17 matching lines...) Expand all
2702 event.stopPropagation(); 1894 event.stopPropagation();
2703 }, 1895 },
2704 1896
2705 _parentResizableChanged: function(parentResizable) { 1897 _parentResizableChanged: function(parentResizable) {
2706 if (parentResizable) { 1898 if (parentResizable) {
2707 window.removeEventListener('resize', this._boundNotifyResize); 1899 window.removeEventListener('resize', this._boundNotifyResize);
2708 } 1900 }
2709 }, 1901 },
2710 1902
2711 _notifyDescendant: function(descendant) { 1903 _notifyDescendant: function(descendant) {
2712 // NOTE(cdata): In IE10, attached is fired on children first, so it's
2713 // important not to notify them if the parent is not attached yet (or
2714 // else they will get redundantly notified when the parent attaches).
2715 if (!this.isAttached) { 1904 if (!this.isAttached) {
2716 return; 1905 return;
2717 } 1906 }
2718 1907
2719 this._notifyingDescendant = true; 1908 this._notifyingDescendant = true;
2720 descendant.notifyResize(); 1909 descendant.notifyResize();
2721 this._notifyingDescendant = false; 1910 this._notifyingDescendant = false;
2722 } 1911 }
2723 }; 1912 };
2724 /**
2725 * @param {!Function} selectCallback
2726 * @constructor
2727 */
2728 Polymer.IronSelection = function(selectCallback) { 1913 Polymer.IronSelection = function(selectCallback) {
2729 this.selection = []; 1914 this.selection = [];
2730 this.selectCallback = selectCallback; 1915 this.selectCallback = selectCallback;
2731 }; 1916 };
2732 1917
2733 Polymer.IronSelection.prototype = { 1918 Polymer.IronSelection.prototype = {
2734 1919
2735 /**
2736 * Retrieves the selected item(s).
2737 *
2738 * @method get
2739 * @returns Returns the selected item(s). If the multi property is true,
2740 * `get` will return an array, otherwise it will return
2741 * the selected item or undefined if there is no selection.
2742 */
2743 get: function() { 1920 get: function() {
2744 return this.multi ? this.selection.slice() : this.selection[0]; 1921 return this.multi ? this.selection.slice() : this.selection[0];
2745 }, 1922 },
2746 1923
2747 /**
2748 * Clears all the selection except the ones indicated.
2749 *
2750 * @method clear
2751 * @param {Array} excludes items to be excluded.
2752 */
2753 clear: function(excludes) { 1924 clear: function(excludes) {
2754 this.selection.slice().forEach(function(item) { 1925 this.selection.slice().forEach(function(item) {
2755 if (!excludes || excludes.indexOf(item) < 0) { 1926 if (!excludes || excludes.indexOf(item) < 0) {
2756 this.setItemSelected(item, false); 1927 this.setItemSelected(item, false);
2757 } 1928 }
2758 }, this); 1929 }, this);
2759 }, 1930 },
2760 1931
2761 /**
2762 * Indicates if a given item is selected.
2763 *
2764 * @method isSelected
2765 * @param {*} item The item whose selection state should be checked.
2766 * @returns Returns true if `item` is selected.
2767 */
2768 isSelected: function(item) { 1932 isSelected: function(item) {
2769 return this.selection.indexOf(item) >= 0; 1933 return this.selection.indexOf(item) >= 0;
2770 }, 1934 },
2771 1935
2772 /**
2773 * Sets the selection state for a given item to either selected or deselecte d.
2774 *
2775 * @method setItemSelected
2776 * @param {*} item The item to select.
2777 * @param {boolean} isSelected True for selected, false for deselected.
2778 */
2779 setItemSelected: function(item, isSelected) { 1936 setItemSelected: function(item, isSelected) {
2780 if (item != null) { 1937 if (item != null) {
2781 if (isSelected !== this.isSelected(item)) { 1938 if (isSelected !== this.isSelected(item)) {
2782 // proceed to update selection only if requested state differs from cu rrent
2783 if (isSelected) { 1939 if (isSelected) {
2784 this.selection.push(item); 1940 this.selection.push(item);
2785 } else { 1941 } else {
2786 var i = this.selection.indexOf(item); 1942 var i = this.selection.indexOf(item);
2787 if (i >= 0) { 1943 if (i >= 0) {
2788 this.selection.splice(i, 1); 1944 this.selection.splice(i, 1);
2789 } 1945 }
2790 } 1946 }
2791 if (this.selectCallback) { 1947 if (this.selectCallback) {
2792 this.selectCallback(item, isSelected); 1948 this.selectCallback(item, isSelected);
2793 } 1949 }
2794 } 1950 }
2795 } 1951 }
2796 }, 1952 },
2797 1953
2798 /**
2799 * Sets the selection state for a given item. If the `multi` property
2800 * is true, then the selected state of `item` will be toggled; otherwise
2801 * the `item` will be selected.
2802 *
2803 * @method select
2804 * @param {*} item The item to select.
2805 */
2806 select: function(item) { 1954 select: function(item) {
2807 if (this.multi) { 1955 if (this.multi) {
2808 this.toggle(item); 1956 this.toggle(item);
2809 } else if (this.get() !== item) { 1957 } else if (this.get() !== item) {
2810 this.setItemSelected(this.get(), false); 1958 this.setItemSelected(this.get(), false);
2811 this.setItemSelected(item, true); 1959 this.setItemSelected(item, true);
2812 } 1960 }
2813 }, 1961 },
2814 1962
2815 /**
2816 * Toggles the selection state for `item`.
2817 *
2818 * @method toggle
2819 * @param {*} item The item to toggle.
2820 */
2821 toggle: function(item) { 1963 toggle: function(item) {
2822 this.setItemSelected(item, !this.isSelected(item)); 1964 this.setItemSelected(item, !this.isSelected(item));
2823 } 1965 }
2824 1966
2825 }; 1967 };
2826 /** @polymerBehavior */ 1968 /** @polymerBehavior */
2827 Polymer.IronSelectableBehavior = { 1969 Polymer.IronSelectableBehavior = {
2828 1970
2829 /**
2830 * Fired when iron-selector is activated (selected or deselected).
2831 * It is fired before the selected items are changed.
2832 * Cancel the event to abort selection.
2833 *
2834 * @event iron-activate
2835 */
2836 1971
2837 /**
2838 * Fired when an item is selected
2839 *
2840 * @event iron-select
2841 */
2842 1972
2843 /**
2844 * Fired when an item is deselected
2845 *
2846 * @event iron-deselect
2847 */
2848 1973
2849 /**
2850 * Fired when the list of selectable items changes (e.g., items are
2851 * added or removed). The detail of the event is a mutation record that
2852 * describes what changed.
2853 *
2854 * @event iron-items-changed
2855 */
2856 1974
2857 properties: { 1975 properties: {
2858 1976
2859 /**
2860 * If you want to use an attribute value or property of an element for
2861 * `selected` instead of the index, set this to the name of the attribute
2862 * or property. Hyphenated values are converted to camel case when used to
2863 * look up the property of a selectable element. Camel cased values are
2864 * *not* converted to hyphenated values for attribute lookup. It's
2865 * recommended that you provide the hyphenated form of the name so that
2866 * selection works in both cases. (Use `attr-or-property-name` instead of
2867 * `attrOrPropertyName`.)
2868 */
2869 attrForSelected: { 1977 attrForSelected: {
2870 type: String, 1978 type: String,
2871 value: null 1979 value: null
2872 }, 1980 },
2873 1981
2874 /**
2875 * Gets or sets the selected element. The default is to use the index of t he item.
2876 * @type {string|number}
2877 */
2878 selected: { 1982 selected: {
2879 type: String, 1983 type: String,
2880 notify: true 1984 notify: true
2881 }, 1985 },
2882 1986
2883 /**
2884 * Returns the currently selected item.
2885 *
2886 * @type {?Object}
2887 */
2888 selectedItem: { 1987 selectedItem: {
2889 type: Object, 1988 type: Object,
2890 readOnly: true, 1989 readOnly: true,
2891 notify: true 1990 notify: true
2892 }, 1991 },
2893 1992
2894 /**
2895 * The event that fires from items when they are selected. Selectable
2896 * will listen for this event from items and update the selection state.
2897 * Set to empty string to listen to no events.
2898 */
2899 activateEvent: { 1993 activateEvent: {
2900 type: String, 1994 type: String,
2901 value: 'tap', 1995 value: 'tap',
2902 observer: '_activateEventChanged' 1996 observer: '_activateEventChanged'
2903 }, 1997 },
2904 1998
2905 /**
2906 * This is a CSS selector string. If this is set, only items that match t he CSS selector
2907 * are selectable.
2908 */
2909 selectable: String, 1999 selectable: String,
2910 2000
2911 /**
2912 * The class to set on elements when selected.
2913 */
2914 selectedClass: { 2001 selectedClass: {
2915 type: String, 2002 type: String,
2916 value: 'iron-selected' 2003 value: 'iron-selected'
2917 }, 2004 },
2918 2005
2919 /**
2920 * The attribute to set on elements when selected.
2921 */
2922 selectedAttribute: { 2006 selectedAttribute: {
2923 type: String, 2007 type: String,
2924 value: null 2008 value: null
2925 }, 2009 },
2926 2010
2927 /**
2928 * Default fallback if the selection based on selected with `attrForSelect ed`
2929 * is not found.
2930 */
2931 fallbackSelection: { 2011 fallbackSelection: {
2932 type: String, 2012 type: String,
2933 value: null 2013 value: null
2934 }, 2014 },
2935 2015
2936 /**
2937 * The list of items from which a selection can be made.
2938 */
2939 items: { 2016 items: {
2940 type: Array, 2017 type: Array,
2941 readOnly: true, 2018 readOnly: true,
2942 notify: true, 2019 notify: true,
2943 value: function() { 2020 value: function() {
2944 return []; 2021 return [];
2945 } 2022 }
2946 }, 2023 },
2947 2024
2948 /**
2949 * The set of excluded elements where the key is the `localName`
2950 * of the element that will be ignored from the item list.
2951 *
2952 * @default {template: 1}
2953 */
2954 _excludedLocalNames: { 2025 _excludedLocalNames: {
2955 type: Object, 2026 type: Object,
2956 value: function() { 2027 value: function() {
2957 return { 2028 return {
2958 'template': 1 2029 'template': 1
2959 }; 2030 };
2960 } 2031 }
2961 } 2032 }
2962 }, 2033 },
2963 2034
(...skipping 17 matching lines...) Expand all
2981 this._addListener(this.activateEvent); 2052 this._addListener(this.activateEvent);
2982 }, 2053 },
2983 2054
2984 detached: function() { 2055 detached: function() {
2985 if (this._observer) { 2056 if (this._observer) {
2986 Polymer.dom(this).unobserveNodes(this._observer); 2057 Polymer.dom(this).unobserveNodes(this._observer);
2987 } 2058 }
2988 this._removeListener(this.activateEvent); 2059 this._removeListener(this.activateEvent);
2989 }, 2060 },
2990 2061
2991 /**
2992 * Returns the index of the given item.
2993 *
2994 * @method indexOf
2995 * @param {Object} item
2996 * @returns Returns the index of the item
2997 */
2998 indexOf: function(item) { 2062 indexOf: function(item) {
2999 return this.items.indexOf(item); 2063 return this.items.indexOf(item);
3000 }, 2064 },
3001 2065
3002 /**
3003 * Selects the given value.
3004 *
3005 * @method select
3006 * @param {string|number} value the value to select.
3007 */
3008 select: function(value) { 2066 select: function(value) {
3009 this.selected = value; 2067 this.selected = value;
3010 }, 2068 },
3011 2069
3012 /**
3013 * Selects the previous item.
3014 *
3015 * @method selectPrevious
3016 */
3017 selectPrevious: function() { 2070 selectPrevious: function() {
3018 var length = this.items.length; 2071 var length = this.items.length;
3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth; 2072 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
3020 this.selected = this._indexToValue(index); 2073 this.selected = this._indexToValue(index);
3021 }, 2074 },
3022 2075
3023 /**
3024 * Selects the next item.
3025 *
3026 * @method selectNext
3027 */
3028 selectNext: function() { 2076 selectNext: function() {
3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength; 2077 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
3030 this.selected = this._indexToValue(index); 2078 this.selected = this._indexToValue(index);
3031 }, 2079 },
3032 2080
3033 /**
3034 * Selects the item at the given index.
3035 *
3036 * @method selectIndex
3037 */
3038 selectIndex: function(index) { 2081 selectIndex: function(index) {
3039 this.select(this._indexToValue(index)); 2082 this.select(this._indexToValue(index));
3040 }, 2083 },
3041 2084
3042 /**
3043 * Force a synchronous update of the `items` property.
3044 *
3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to
3046 * updates to the set of selectable items after updates to the DOM list and
3047 * selection state have been made.
3048 *
3049 * WARNING: If you are using this method, you should probably consider an
3050 * alternate approach. Synchronously querying for items is potentially
3051 * slow for many use cases. The `items` property will update asynchronously
3052 * on its own to reflect selectable items in the DOM.
3053 */
3054 forceSynchronousItemUpdate: function() { 2085 forceSynchronousItemUpdate: function() {
3055 this._updateItems(); 2086 this._updateItems();
3056 }, 2087 },
3057 2088
3058 get _shouldUpdateSelection() { 2089 get _shouldUpdateSelection() {
3059 return this.selected != null; 2090 return this.selected != null;
3060 }, 2091 },
3061 2092
3062 _checkFallback: function() { 2093 _checkFallback: function() {
3063 if (this._shouldUpdateSelection) { 2094 if (this._shouldUpdateSelection) {
(...skipping 25 matching lines...) Expand all
3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); 2120 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3090 } 2121 }
3091 }, 2122 },
3092 2123
3093 _updateSelected: function() { 2124 _updateSelected: function() {
3094 this._selectSelected(this.selected); 2125 this._selectSelected(this.selected);
3095 }, 2126 },
3096 2127
3097 _selectSelected: function(selected) { 2128 _selectSelected: function(selected) {
3098 this._selection.select(this._valueToItem(this.selected)); 2129 this._selection.select(this._valueToItem(this.selected));
3099 // Check for items, since this array is populated only when attached
3100 // Since Number(0) is falsy, explicitly check for undefined
3101 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) { 2130 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
3102 this.selected = this.fallbackSelection; 2131 this.selected = this.fallbackSelection;
3103 } 2132 }
3104 }, 2133 },
3105 2134
3106 _filterItem: function(node) { 2135 _filterItem: function(node) {
3107 return !this._excludedLocalNames[node.localName]; 2136 return !this._excludedLocalNames[node.localName];
3108 }, 2137 },
3109 2138
3110 _valueToItem: function(value) { 2139 _valueToItem: function(value) {
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
3147 this.toggleAttribute(this.selectedAttribute, isSelected, item); 2176 this.toggleAttribute(this.selectedAttribute, isSelected, item);
3148 } 2177 }
3149 this._selectionChange(); 2178 this._selectionChange();
3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); 2179 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
3151 }, 2180 },
3152 2181
3153 _selectionChange: function() { 2182 _selectionChange: function() {
3154 this._setSelectedItem(this._selection.get()); 2183 this._setSelectedItem(this._selection.get());
3155 }, 2184 },
3156 2185
3157 // observe items change under the given node.
3158 _observeItems: function(node) { 2186 _observeItems: function(node) {
3159 return Polymer.dom(node).observeNodes(function(mutation) { 2187 return Polymer.dom(node).observeNodes(function(mutation) {
3160 this._updateItems(); 2188 this._updateItems();
3161 2189
3162 if (this._shouldUpdateSelection) { 2190 if (this._shouldUpdateSelection) {
3163 this._updateSelected(); 2191 this._updateSelected();
3164 } 2192 }
3165 2193
3166 // Let other interested parties know about the change so that
3167 // we don't have to recreate mutation observers everywhere.
3168 this.fire('iron-items-changed', mutation, { 2194 this.fire('iron-items-changed', mutation, {
3169 bubbles: false, 2195 bubbles: false,
3170 cancelable: false 2196 cancelable: false
3171 }); 2197 });
3172 }); 2198 });
3173 }, 2199 },
3174 2200
3175 _activateHandler: function(e) { 2201 _activateHandler: function(e) {
3176 var t = e.target; 2202 var t = e.target;
3177 var items = this.items; 2203 var items = this.items;
(...skipping 20 matching lines...) Expand all
3198 2224
3199 is: 'iron-pages', 2225 is: 'iron-pages',
3200 2226
3201 behaviors: [ 2227 behaviors: [
3202 Polymer.IronResizableBehavior, 2228 Polymer.IronResizableBehavior,
3203 Polymer.IronSelectableBehavior 2229 Polymer.IronSelectableBehavior
3204 ], 2230 ],
3205 2231
3206 properties: { 2232 properties: {
3207 2233
3208 // as the selected page is the only one visible, activateEvent
3209 // is both non-sensical and problematic; e.g. in cases where a user
3210 // handler attempts to change the page and the activateEvent
3211 // handler immediately changes it back
3212 activateEvent: { 2234 activateEvent: {
3213 type: String, 2235 type: String,
3214 value: null 2236 value: null
3215 } 2237 }
3216 2238
3217 }, 2239 },
3218 2240
3219 observers: [ 2241 observers: [
3220 '_selectedPageChanged(selected)' 2242 '_selectedPageChanged(selected)'
3221 ], 2243 ],
3222 2244
3223 _selectedPageChanged: function(selected, old) { 2245 _selectedPageChanged: function(selected, old) {
3224 this.async(this.notifyResize); 2246 this.async(this.notifyResize);
3225 } 2247 }
3226 }); 2248 });
3227 (function() { 2249 (function() {
3228 'use strict'; 2250 'use strict';
3229 2251
3230 /**
3231 * Chrome uses an older version of DOM Level 3 Keyboard Events
3232 *
3233 * Most keys are labeled as text, but some are Unicode codepoints.
3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set
3235 */
3236 var KEY_IDENTIFIER = { 2252 var KEY_IDENTIFIER = {
3237 'U+0008': 'backspace', 2253 'U+0008': 'backspace',
3238 'U+0009': 'tab', 2254 'U+0009': 'tab',
3239 'U+001B': 'esc', 2255 'U+001B': 'esc',
3240 'U+0020': 'space', 2256 'U+0020': 'space',
3241 'U+007F': 'del' 2257 'U+007F': 'del'
3242 }; 2258 };
3243 2259
3244 /**
3245 * Special table for KeyboardEvent.keyCode.
3246 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er
3247 * than that.
3248 *
3249 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode
3250 */
3251 var KEY_CODE = { 2260 var KEY_CODE = {
3252 8: 'backspace', 2261 8: 'backspace',
3253 9: 'tab', 2262 9: 'tab',
3254 13: 'enter', 2263 13: 'enter',
3255 27: 'esc', 2264 27: 'esc',
3256 33: 'pageup', 2265 33: 'pageup',
3257 34: 'pagedown', 2266 34: 'pagedown',
3258 35: 'end', 2267 35: 'end',
3259 36: 'home', 2268 36: 'home',
3260 32: 'space', 2269 32: 'space',
3261 37: 'left', 2270 37: 'left',
3262 38: 'up', 2271 38: 'up',
3263 39: 'right', 2272 39: 'right',
3264 40: 'down', 2273 40: 'down',
3265 46: 'del', 2274 46: 'del',
3266 106: '*' 2275 106: '*'
3267 }; 2276 };
3268 2277
3269 /**
3270 * MODIFIER_KEYS maps the short name for modifier keys used in a key
3271 * combo string to the property name that references those same keys
3272 * in a KeyboardEvent instance.
3273 */
3274 var MODIFIER_KEYS = { 2278 var MODIFIER_KEYS = {
3275 'shift': 'shiftKey', 2279 'shift': 'shiftKey',
3276 'ctrl': 'ctrlKey', 2280 'ctrl': 'ctrlKey',
3277 'alt': 'altKey', 2281 'alt': 'altKey',
3278 'meta': 'metaKey' 2282 'meta': 'metaKey'
3279 }; 2283 };
3280 2284
3281 /**
3282 * KeyboardEvent.key is mostly represented by printable character made by
3283 * the keyboard, with unprintable keys labeled nicely.
3284 *
3285 * However, on OS X, Alt+char can make a Unicode character that follows an
3286 * Apple-specific mapping. In this case, we fall back to .keyCode.
3287 */
3288 var KEY_CHAR = /[a-z0-9*]/; 2285 var KEY_CHAR = /[a-z0-9*]/;
3289 2286
3290 /**
3291 * Matches a keyIdentifier string.
3292 */
3293 var IDENT_CHAR = /U\+/; 2287 var IDENT_CHAR = /U\+/;
3294 2288
3295 /**
3296 * Matches arrow keys in Gecko 27.0+
3297 */
3298 var ARROW_KEY = /^arrow/; 2289 var ARROW_KEY = /^arrow/;
3299 2290
3300 /**
3301 * Matches space keys everywhere (notably including IE10's exceptional name
3302 * `spacebar`).
3303 */
3304 var SPACE_KEY = /^space(bar)?/; 2291 var SPACE_KEY = /^space(bar)?/;
3305 2292
3306 /**
3307 * Matches ESC key.
3308 *
3309 * Value from: http://w3c.github.io/uievents-key/#key-Escape
3310 */
3311 var ESC_KEY = /^escape$/; 2293 var ESC_KEY = /^escape$/;
3312 2294
3313 /**
3314 * Transforms the key.
3315 * @param {string} key The KeyBoardEvent.key
3316 * @param {Boolean} [noSpecialChars] Limits the transformation to
3317 * alpha-numeric characters.
3318 */
3319 function transformKey(key, noSpecialChars) { 2295 function transformKey(key, noSpecialChars) {
3320 var validKey = ''; 2296 var validKey = '';
3321 if (key) { 2297 if (key) {
3322 var lKey = key.toLowerCase(); 2298 var lKey = key.toLowerCase();
3323 if (lKey === ' ' || SPACE_KEY.test(lKey)) { 2299 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
3324 validKey = 'space'; 2300 validKey = 'space';
3325 } else if (ESC_KEY.test(lKey)) { 2301 } else if (ESC_KEY.test(lKey)) {
3326 validKey = 'esc'; 2302 validKey = 'esc';
3327 } else if (lKey.length == 1) { 2303 } else if (lKey.length == 1) {
3328 if (!noSpecialChars || KEY_CHAR.test(lKey)) { 2304 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
3329 validKey = lKey; 2305 validKey = lKey;
3330 } 2306 }
3331 } else if (ARROW_KEY.test(lKey)) { 2307 } else if (ARROW_KEY.test(lKey)) {
3332 validKey = lKey.replace('arrow', ''); 2308 validKey = lKey.replace('arrow', '');
3333 } else if (lKey == 'multiply') { 2309 } else if (lKey == 'multiply') {
3334 // numpad '*' can map to Multiply on IE/Windows
3335 validKey = '*'; 2310 validKey = '*';
3336 } else { 2311 } else {
3337 validKey = lKey; 2312 validKey = lKey;
3338 } 2313 }
3339 } 2314 }
3340 return validKey; 2315 return validKey;
3341 } 2316 }
3342 2317
3343 function transformKeyIdentifier(keyIdent) { 2318 function transformKeyIdentifier(keyIdent) {
3344 var validKey = ''; 2319 var validKey = '';
3345 if (keyIdent) { 2320 if (keyIdent) {
3346 if (keyIdent in KEY_IDENTIFIER) { 2321 if (keyIdent in KEY_IDENTIFIER) {
3347 validKey = KEY_IDENTIFIER[keyIdent]; 2322 validKey = KEY_IDENTIFIER[keyIdent];
3348 } else if (IDENT_CHAR.test(keyIdent)) { 2323 } else if (IDENT_CHAR.test(keyIdent)) {
3349 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); 2324 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
3350 validKey = String.fromCharCode(keyIdent).toLowerCase(); 2325 validKey = String.fromCharCode(keyIdent).toLowerCase();
3351 } else { 2326 } else {
3352 validKey = keyIdent.toLowerCase(); 2327 validKey = keyIdent.toLowerCase();
3353 } 2328 }
3354 } 2329 }
3355 return validKey; 2330 return validKey;
3356 } 2331 }
3357 2332
3358 function transformKeyCode(keyCode) { 2333 function transformKeyCode(keyCode) {
3359 var validKey = ''; 2334 var validKey = '';
3360 if (Number(keyCode)) { 2335 if (Number(keyCode)) {
3361 if (keyCode >= 65 && keyCode <= 90) { 2336 if (keyCode >= 65 && keyCode <= 90) {
3362 // ascii a-z
3363 // lowercase is 32 offset from uppercase
3364 validKey = String.fromCharCode(32 + keyCode); 2337 validKey = String.fromCharCode(32 + keyCode);
3365 } else if (keyCode >= 112 && keyCode <= 123) { 2338 } else if (keyCode >= 112 && keyCode <= 123) {
3366 // function keys f1-f12
3367 validKey = 'f' + (keyCode - 112); 2339 validKey = 'f' + (keyCode - 112);
3368 } else if (keyCode >= 48 && keyCode <= 57) { 2340 } else if (keyCode >= 48 && keyCode <= 57) {
3369 // top 0-9 keys
3370 validKey = String(keyCode - 48); 2341 validKey = String(keyCode - 48);
3371 } else if (keyCode >= 96 && keyCode <= 105) { 2342 } else if (keyCode >= 96 && keyCode <= 105) {
3372 // num pad 0-9
3373 validKey = String(keyCode - 96); 2343 validKey = String(keyCode - 96);
3374 } else { 2344 } else {
3375 validKey = KEY_CODE[keyCode]; 2345 validKey = KEY_CODE[keyCode];
3376 } 2346 }
3377 } 2347 }
3378 return validKey; 2348 return validKey;
3379 } 2349 }
3380 2350
3381 /**
3382 * Calculates the normalized key for a KeyboardEvent.
3383 * @param {KeyboardEvent} keyEvent
3384 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
3385 * transformation to alpha-numeric chars. This is useful with key
3386 * combinations like shift + 2, which on FF for MacOS produces
3387 * keyEvent.key = @
3388 * To get 2 returned, set noSpecialChars = true
3389 * To get @ returned, set noSpecialChars = false
3390 */
3391 function normalizedKeyForEvent(keyEvent, noSpecialChars) { 2351 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
3392 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
3393 // .detail.key to support artificial keyboard events.
3394 return transformKey(keyEvent.key, noSpecialChars) || 2352 return transformKey(keyEvent.key, noSpecialChars) ||
3395 transformKeyIdentifier(keyEvent.keyIdentifier) || 2353 transformKeyIdentifier(keyEvent.keyIdentifier) ||
3396 transformKeyCode(keyEvent.keyCode) || 2354 transformKeyCode(keyEvent.keyCode) ||
3397 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || ''; 2355 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
3398 } 2356 }
3399 2357
3400 function keyComboMatchesEvent(keyCombo, event) { 2358 function keyComboMatchesEvent(keyCombo, event) {
3401 // For combos with modifiers we support only alpha-numeric keys
3402 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); 2359 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
3403 return keyEvent === keyCombo.key && 2360 return keyEvent === keyCombo.key &&
3404 (!keyCombo.hasModifiers || ( 2361 (!keyCombo.hasModifiers || (
3405 !!event.shiftKey === !!keyCombo.shiftKey && 2362 !!event.shiftKey === !!keyCombo.shiftKey &&
3406 !!event.ctrlKey === !!keyCombo.ctrlKey && 2363 !!event.ctrlKey === !!keyCombo.ctrlKey &&
3407 !!event.altKey === !!keyCombo.altKey && 2364 !!event.altKey === !!keyCombo.altKey &&
3408 !!event.metaKey === !!keyCombo.metaKey) 2365 !!event.metaKey === !!keyCombo.metaKey)
3409 ); 2366 );
3410 } 2367 }
3411 2368
(...skipping 23 matching lines...) Expand all
3435 combo: keyComboString.split(':').shift() 2392 combo: keyComboString.split(':').shift()
3436 }); 2393 });
3437 } 2394 }
3438 2395
3439 function parseEventString(eventString) { 2396 function parseEventString(eventString) {
3440 return eventString.trim().split(' ').map(function(keyComboString) { 2397 return eventString.trim().split(' ').map(function(keyComboString) {
3441 return parseKeyComboString(keyComboString); 2398 return parseKeyComboString(keyComboString);
3442 }); 2399 });
3443 } 2400 }
3444 2401
3445 /**
3446 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
3447 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
3448 * The element takes care of browser differences with respect to Keyboard ev ents
3449 * and uses an expressive syntax to filter key presses.
3450 *
3451 * Use the `keyBindings` prototype property to express what combination of k eys
3452 * will trigger the callback. A key binding has the format
3453 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
3454 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
3455 *
3456 * keyBindings: {
3457 * 'space': '_onKeydown', // same as 'space:keydown'
3458 * 'shift+tab': '_onKeydown',
3459 * 'enter:keypress': '_onKeypress',
3460 * 'esc:keyup': '_onKeyup'
3461 * }
3462 *
3463 * The callback will receive with an event containing the following informat ion in `event.detail`:
3464 *
3465 * _onKeydown: function(event) {
3466 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
3467 * console.log(event.detail.key); // KEY only, e.g. "tab"
3468 * console.log(event.detail.event); // EVENT, e.g. "keydown"
3469 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
3470 * }
3471 *
3472 * Use the `keyEventTarget` attribute to set up event handlers on a specific
3473 * node.
3474 *
3475 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
3476 * for an example.
3477 *
3478 * @demo demo/index.html
3479 * @polymerBehavior
3480 */
3481 Polymer.IronA11yKeysBehavior = { 2402 Polymer.IronA11yKeysBehavior = {
3482 properties: { 2403 properties: {
3483 /**
3484 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
3485 * `null` to disable the listeners.
3486 * @type {?EventTarget}
3487 */
3488 keyEventTarget: { 2404 keyEventTarget: {
3489 type: Object, 2405 type: Object,
3490 value: function() { 2406 value: function() {
3491 return this; 2407 return this;
3492 } 2408 }
3493 }, 2409 },
3494 2410
3495 /**
3496 * If true, this property will cause the implementing element to
3497 * automatically stop propagation on any handled KeyboardEvents.
3498 */
3499 stopKeyboardEventPropagation: { 2411 stopKeyboardEventPropagation: {
3500 type: Boolean, 2412 type: Boolean,
3501 value: false 2413 value: false
3502 }, 2414 },
3503 2415
3504 _boundKeyHandlers: { 2416 _boundKeyHandlers: {
3505 type: Array, 2417 type: Array,
3506 value: function() { 2418 value: function() {
3507 return []; 2419 return [];
3508 } 2420 }
3509 }, 2421 },
3510 2422
3511 // We use this due to a limitation in IE10 where instances will have
3512 // own properties of everything on the "prototype".
3513 _imperativeKeyBindings: { 2423 _imperativeKeyBindings: {
3514 type: Object, 2424 type: Object,
3515 value: function() { 2425 value: function() {
3516 return {}; 2426 return {};
3517 } 2427 }
3518 } 2428 }
3519 }, 2429 },
3520 2430
3521 observers: [ 2431 observers: [
3522 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' 2432 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
3523 ], 2433 ],
3524 2434
3525 2435
3526 /**
3527 * To be used to express what combination of keys will trigger the relati ve
3528 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
3529 * @type {Object}
3530 */
3531 keyBindings: {}, 2436 keyBindings: {},
3532 2437
3533 registered: function() { 2438 registered: function() {
3534 this._prepKeyBindings(); 2439 this._prepKeyBindings();
3535 }, 2440 },
3536 2441
3537 attached: function() { 2442 attached: function() {
3538 this._listenKeyEventListeners(); 2443 this._listenKeyEventListeners();
3539 }, 2444 },
3540 2445
3541 detached: function() { 2446 detached: function() {
3542 this._unlistenKeyEventListeners(); 2447 this._unlistenKeyEventListeners();
3543 }, 2448 },
3544 2449
3545 /**
3546 * Can be used to imperatively add a key binding to the implementing
3547 * element. This is the imperative equivalent of declaring a keybinding
3548 * in the `keyBindings` prototype property.
3549 */
3550 addOwnKeyBinding: function(eventString, handlerName) { 2450 addOwnKeyBinding: function(eventString, handlerName) {
3551 this._imperativeKeyBindings[eventString] = handlerName; 2451 this._imperativeKeyBindings[eventString] = handlerName;
3552 this._prepKeyBindings(); 2452 this._prepKeyBindings();
3553 this._resetKeyEventListeners(); 2453 this._resetKeyEventListeners();
3554 }, 2454 },
3555 2455
3556 /**
3557 * When called, will remove all imperatively-added key bindings.
3558 */
3559 removeOwnKeyBindings: function() { 2456 removeOwnKeyBindings: function() {
3560 this._imperativeKeyBindings = {}; 2457 this._imperativeKeyBindings = {};
3561 this._prepKeyBindings(); 2458 this._prepKeyBindings();
3562 this._resetKeyEventListeners(); 2459 this._resetKeyEventListeners();
3563 }, 2460 },
3564 2461
3565 /**
3566 * Returns true if a keyboard event matches `eventString`.
3567 *
3568 * @param {KeyboardEvent} event
3569 * @param {string} eventString
3570 * @return {boolean}
3571 */
3572 keyboardEventMatchesKeys: function(event, eventString) { 2462 keyboardEventMatchesKeys: function(event, eventString) {
3573 var keyCombos = parseEventString(eventString); 2463 var keyCombos = parseEventString(eventString);
3574 for (var i = 0; i < keyCombos.length; ++i) { 2464 for (var i = 0; i < keyCombos.length; ++i) {
3575 if (keyComboMatchesEvent(keyCombos[i], event)) { 2465 if (keyComboMatchesEvent(keyCombos[i], event)) {
3576 return true; 2466 return true;
3577 } 2467 }
3578 } 2468 }
3579 return false; 2469 return false;
3580 }, 2470 },
3581 2471
(...skipping 15 matching lines...) Expand all
3597 this._collectKeyBindings().forEach(function(keyBindings) { 2487 this._collectKeyBindings().forEach(function(keyBindings) {
3598 for (var eventString in keyBindings) { 2488 for (var eventString in keyBindings) {
3599 this._addKeyBinding(eventString, keyBindings[eventString]); 2489 this._addKeyBinding(eventString, keyBindings[eventString]);
3600 } 2490 }
3601 }, this); 2491 }, this);
3602 2492
3603 for (var eventString in this._imperativeKeyBindings) { 2493 for (var eventString in this._imperativeKeyBindings) {
3604 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]); 2494 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
3605 } 2495 }
3606 2496
3607 // Give precedence to combos with modifiers to be checked first.
3608 for (var eventName in this._keyBindings) { 2497 for (var eventName in this._keyBindings) {
3609 this._keyBindings[eventName].sort(function (kb1, kb2) { 2498 this._keyBindings[eventName].sort(function (kb1, kb2) {
3610 var b1 = kb1[0].hasModifiers; 2499 var b1 = kb1[0].hasModifiers;
3611 var b2 = kb2[0].hasModifiers; 2500 var b2 = kb2[0].hasModifiers;
3612 return (b1 === b2) ? 0 : b1 ? -1 : 1; 2501 return (b1 === b2) ? 0 : b1 ? -1 : 1;
3613 }) 2502 })
3614 } 2503 }
3615 }, 2504 },
3616 2505
3617 _addKeyBinding: function(eventString, handlerName) { 2506 _addKeyBinding: function(eventString, handlerName) {
(...skipping 30 matching lines...) Expand all
3648 }, this); 2537 }, this);
3649 }, 2538 },
3650 2539
3651 _unlistenKeyEventListeners: function() { 2540 _unlistenKeyEventListeners: function() {
3652 var keyHandlerTuple; 2541 var keyHandlerTuple;
3653 var keyEventTarget; 2542 var keyEventTarget;
3654 var eventName; 2543 var eventName;
3655 var boundKeyHandler; 2544 var boundKeyHandler;
3656 2545
3657 while (this._boundKeyHandlers.length) { 2546 while (this._boundKeyHandlers.length) {
3658 // My kingdom for block-scope binding and destructuring assignment..
3659 keyHandlerTuple = this._boundKeyHandlers.pop(); 2547 keyHandlerTuple = this._boundKeyHandlers.pop();
3660 keyEventTarget = keyHandlerTuple[0]; 2548 keyEventTarget = keyHandlerTuple[0];
3661 eventName = keyHandlerTuple[1]; 2549 eventName = keyHandlerTuple[1];
3662 boundKeyHandler = keyHandlerTuple[2]; 2550 boundKeyHandler = keyHandlerTuple[2];
3663 2551
3664 keyEventTarget.removeEventListener(eventName, boundKeyHandler); 2552 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
3665 } 2553 }
3666 }, 2554 },
3667 2555
3668 _onKeyBindingEvent: function(keyBindings, event) { 2556 _onKeyBindingEvent: function(keyBindings, event) {
3669 if (this.stopKeyboardEventPropagation) { 2557 if (this.stopKeyboardEventPropagation) {
3670 event.stopPropagation(); 2558 event.stopPropagation();
3671 } 2559 }
3672 2560
3673 // if event has been already prevented, don't do anything
3674 if (event.defaultPrevented) { 2561 if (event.defaultPrevented) {
3675 return; 2562 return;
3676 } 2563 }
3677 2564
3678 for (var i = 0; i < keyBindings.length; i++) { 2565 for (var i = 0; i < keyBindings.length; i++) {
3679 var keyCombo = keyBindings[i][0]; 2566 var keyCombo = keyBindings[i][0];
3680 var handlerName = keyBindings[i][1]; 2567 var handlerName = keyBindings[i][1];
3681 if (keyComboMatchesEvent(keyCombo, event)) { 2568 if (keyComboMatchesEvent(keyCombo, event)) {
3682 this._triggerKeyHandler(keyCombo, handlerName, event); 2569 this._triggerKeyHandler(keyCombo, handlerName, event);
3683 // exit the loop if eventDefault was prevented
3684 if (event.defaultPrevented) { 2570 if (event.defaultPrevented) {
3685 return; 2571 return;
3686 } 2572 }
3687 } 2573 }
3688 } 2574 }
3689 }, 2575 },
3690 2576
3691 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { 2577 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
3692 var detail = Object.create(keyCombo); 2578 var detail = Object.create(keyCombo);
3693 detail.keyboardEvent = keyboardEvent; 2579 detail.keyboardEvent = keyboardEvent;
3694 var event = new CustomEvent(keyCombo.event, { 2580 var event = new CustomEvent(keyCombo.event, {
3695 detail: detail, 2581 detail: detail,
3696 cancelable: true 2582 cancelable: true
3697 }); 2583 });
3698 this[handlerName].call(this, event); 2584 this[handlerName].call(this, event);
3699 if (event.defaultPrevented) { 2585 if (event.defaultPrevented) {
3700 keyboardEvent.preventDefault(); 2586 keyboardEvent.preventDefault();
3701 } 2587 }
3702 } 2588 }
3703 }; 2589 };
3704 })(); 2590 })();
3705 /**
3706 * @demo demo/index.html
3707 * @polymerBehavior
3708 */
3709 Polymer.IronControlState = { 2591 Polymer.IronControlState = {
3710 2592
3711 properties: { 2593 properties: {
3712 2594
3713 /**
3714 * If true, the element currently has focus.
3715 */
3716 focused: { 2595 focused: {
3717 type: Boolean, 2596 type: Boolean,
3718 value: false, 2597 value: false,
3719 notify: true, 2598 notify: true,
3720 readOnly: true, 2599 readOnly: true,
3721 reflectToAttribute: true 2600 reflectToAttribute: true
3722 }, 2601 },
3723 2602
3724 /**
3725 * If true, the user cannot interact with this element.
3726 */
3727 disabled: { 2603 disabled: {
3728 type: Boolean, 2604 type: Boolean,
3729 value: false, 2605 value: false,
3730 notify: true, 2606 notify: true,
3731 observer: '_disabledChanged', 2607 observer: '_disabledChanged',
3732 reflectToAttribute: true 2608 reflectToAttribute: true
3733 }, 2609 },
3734 2610
3735 _oldTabIndex: { 2611 _oldTabIndex: {
3736 type: Number 2612 type: Number
(...skipping 11 matching lines...) Expand all
3748 observers: [ 2624 observers: [
3749 '_changedControlState(focused, disabled)' 2625 '_changedControlState(focused, disabled)'
3750 ], 2626 ],
3751 2627
3752 ready: function() { 2628 ready: function() {
3753 this.addEventListener('focus', this._boundFocusBlurHandler, true); 2629 this.addEventListener('focus', this._boundFocusBlurHandler, true);
3754 this.addEventListener('blur', this._boundFocusBlurHandler, true); 2630 this.addEventListener('blur', this._boundFocusBlurHandler, true);
3755 }, 2631 },
3756 2632
3757 _focusBlurHandler: function(event) { 2633 _focusBlurHandler: function(event) {
3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
3759 // eventually become `this` due to retargeting; if we are not in
3760 // ShadowDOM land, `event.target` will eventually become `this` due
3761 // to the second conditional which fires a synthetic event (that is also
3762 // handled). In either case, we can disregard `event.path`.
3763 2634
3764 if (event.target === this) { 2635 if (event.target === this) {
3765 this._setFocused(event.type === 'focus'); 2636 this._setFocused(event.type === 'focus');
3766 } else if (!this.shadowRoot) { 2637 } else if (!this.shadowRoot) {
3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget); 2638 var target = /** @type {Node} */(Polymer.dom(event).localTarget);
3768 if (!this.isLightDescendant(target)) { 2639 if (!this.isLightDescendant(target)) {
3769 this.fire(event.type, {sourceEvent: event}, { 2640 this.fire(event.type, {sourceEvent: event}, {
3770 node: this, 2641 node: this,
3771 bubbles: event.bubbles, 2642 bubbles: event.bubbles,
3772 cancelable: event.cancelable 2643 cancelable: event.cancelable
3773 }); 2644 });
3774 } 2645 }
3775 } 2646 }
3776 }, 2647 },
3777 2648
3778 _disabledChanged: function(disabled, old) { 2649 _disabledChanged: function(disabled, old) {
3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 2650 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
3780 this.style.pointerEvents = disabled ? 'none' : ''; 2651 this.style.pointerEvents = disabled ? 'none' : '';
3781 if (disabled) { 2652 if (disabled) {
3782 this._oldTabIndex = this.tabIndex; 2653 this._oldTabIndex = this.tabIndex;
3783 this._setFocused(false); 2654 this._setFocused(false);
3784 this.tabIndex = -1; 2655 this.tabIndex = -1;
3785 this.blur(); 2656 this.blur();
3786 } else if (this._oldTabIndex !== undefined) { 2657 } else if (this._oldTabIndex !== undefined) {
3787 this.tabIndex = this._oldTabIndex; 2658 this.tabIndex = this._oldTabIndex;
3788 } 2659 }
3789 }, 2660 },
3790 2661
3791 _changedControlState: function() { 2662 _changedControlState: function() {
3792 // _controlStateChanged is abstract, follow-on behaviors may implement it
3793 if (this._controlStateChanged) { 2663 if (this._controlStateChanged) {
3794 this._controlStateChanged(); 2664 this._controlStateChanged();
3795 } 2665 }
3796 } 2666 }
3797 2667
3798 }; 2668 };
3799 /**
3800 * @demo demo/index.html
3801 * @polymerBehavior Polymer.IronButtonState
3802 */
3803 Polymer.IronButtonStateImpl = { 2669 Polymer.IronButtonStateImpl = {
3804 2670
3805 properties: { 2671 properties: {
3806 2672
3807 /**
3808 * If true, the user is currently holding down the button.
3809 */
3810 pressed: { 2673 pressed: {
3811 type: Boolean, 2674 type: Boolean,
3812 readOnly: true, 2675 readOnly: true,
3813 value: false, 2676 value: false,
3814 reflectToAttribute: true, 2677 reflectToAttribute: true,
3815 observer: '_pressedChanged' 2678 observer: '_pressedChanged'
3816 }, 2679 },
3817 2680
3818 /**
3819 * If true, the button toggles the active state with each tap or press
3820 * of the spacebar.
3821 */
3822 toggles: { 2681 toggles: {
3823 type: Boolean, 2682 type: Boolean,
3824 value: false, 2683 value: false,
3825 reflectToAttribute: true 2684 reflectToAttribute: true
3826 }, 2685 },
3827 2686
3828 /**
3829 * If true, the button is a toggle and is currently in the active state.
3830 */
3831 active: { 2687 active: {
3832 type: Boolean, 2688 type: Boolean,
3833 value: false, 2689 value: false,
3834 notify: true, 2690 notify: true,
3835 reflectToAttribute: true 2691 reflectToAttribute: true
3836 }, 2692 },
3837 2693
3838 /**
3839 * True if the element is currently being pressed by a "pointer," which
3840 * is loosely defined as mouse or touch input (but specifically excluding
3841 * keyboard input).
3842 */
3843 pointerDown: { 2694 pointerDown: {
3844 type: Boolean, 2695 type: Boolean,
3845 readOnly: true, 2696 readOnly: true,
3846 value: false 2697 value: false
3847 }, 2698 },
3848 2699
3849 /**
3850 * True if the input device that caused the element to receive focus
3851 * was a keyboard.
3852 */
3853 receivedFocusFromKeyboard: { 2700 receivedFocusFromKeyboard: {
3854 type: Boolean, 2701 type: Boolean,
3855 readOnly: true 2702 readOnly: true
3856 }, 2703 },
3857 2704
3858 /**
3859 * The aria attribute to be set if the button is a toggle and in the
3860 * active state.
3861 */
3862 ariaActiveAttribute: { 2705 ariaActiveAttribute: {
3863 type: String, 2706 type: String,
3864 value: 'aria-pressed', 2707 value: 'aria-pressed',
3865 observer: '_ariaActiveAttributeChanged' 2708 observer: '_ariaActiveAttributeChanged'
3866 } 2709 }
3867 }, 2710 },
3868 2711
3869 listeners: { 2712 listeners: {
3870 down: '_downHandler', 2713 down: '_downHandler',
3871 up: '_upHandler', 2714 up: '_upHandler',
3872 tap: '_tapHandler' 2715 tap: '_tapHandler'
3873 }, 2716 },
3874 2717
3875 observers: [ 2718 observers: [
3876 '_detectKeyboardFocus(focused)', 2719 '_detectKeyboardFocus(focused)',
3877 '_activeChanged(active, ariaActiveAttribute)' 2720 '_activeChanged(active, ariaActiveAttribute)'
3878 ], 2721 ],
3879 2722
3880 keyBindings: { 2723 keyBindings: {
3881 'enter:keydown': '_asyncClick', 2724 'enter:keydown': '_asyncClick',
3882 'space:keydown': '_spaceKeyDownHandler', 2725 'space:keydown': '_spaceKeyDownHandler',
3883 'space:keyup': '_spaceKeyUpHandler', 2726 'space:keyup': '_spaceKeyUpHandler',
3884 }, 2727 },
3885 2728
3886 _mouseEventRe: /^mouse/, 2729 _mouseEventRe: /^mouse/,
3887 2730
3888 _tapHandler: function() { 2731 _tapHandler: function() {
3889 if (this.toggles) { 2732 if (this.toggles) {
3890 // a tap is needed to toggle the active state
3891 this._userActivate(!this.active); 2733 this._userActivate(!this.active);
3892 } else { 2734 } else {
3893 this.active = false; 2735 this.active = false;
3894 } 2736 }
3895 }, 2737 },
3896 2738
3897 _detectKeyboardFocus: function(focused) { 2739 _detectKeyboardFocus: function(focused) {
3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); 2740 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
3899 }, 2741 },
3900 2742
3901 // to emulate native checkbox, (de-)activations from a user interaction fire
3902 // 'change' events
3903 _userActivate: function(active) { 2743 _userActivate: function(active) {
3904 if (this.active !== active) { 2744 if (this.active !== active) {
3905 this.active = active; 2745 this.active = active;
3906 this.fire('change'); 2746 this.fire('change');
3907 } 2747 }
3908 }, 2748 },
3909 2749
3910 _downHandler: function(event) { 2750 _downHandler: function(event) {
3911 this._setPointerDown(true); 2751 this._setPointerDown(true);
3912 this._setPressed(true); 2752 this._setPressed(true);
3913 this._setReceivedFocusFromKeyboard(false); 2753 this._setReceivedFocusFromKeyboard(false);
3914 }, 2754 },
3915 2755
3916 _upHandler: function() { 2756 _upHandler: function() {
3917 this._setPointerDown(false); 2757 this._setPointerDown(false);
3918 this._setPressed(false); 2758 this._setPressed(false);
3919 }, 2759 },
3920 2760
3921 /**
3922 * @param {!KeyboardEvent} event .
3923 */
3924 _spaceKeyDownHandler: function(event) { 2761 _spaceKeyDownHandler: function(event) {
3925 var keyboardEvent = event.detail.keyboardEvent; 2762 var keyboardEvent = event.detail.keyboardEvent;
3926 var target = Polymer.dom(keyboardEvent).localTarget; 2763 var target = Polymer.dom(keyboardEvent).localTarget;
3927 2764
3928 // Ignore the event if this is coming from a focused light child, since th at
3929 // element will deal with it.
3930 if (this.isLightDescendant(/** @type {Node} */(target))) 2765 if (this.isLightDescendant(/** @type {Node} */(target)))
3931 return; 2766 return;
3932 2767
3933 keyboardEvent.preventDefault(); 2768 keyboardEvent.preventDefault();
3934 keyboardEvent.stopImmediatePropagation(); 2769 keyboardEvent.stopImmediatePropagation();
3935 this._setPressed(true); 2770 this._setPressed(true);
3936 }, 2771 },
3937 2772
3938 /**
3939 * @param {!KeyboardEvent} event .
3940 */
3941 _spaceKeyUpHandler: function(event) { 2773 _spaceKeyUpHandler: function(event) {
3942 var keyboardEvent = event.detail.keyboardEvent; 2774 var keyboardEvent = event.detail.keyboardEvent;
3943 var target = Polymer.dom(keyboardEvent).localTarget; 2775 var target = Polymer.dom(keyboardEvent).localTarget;
3944 2776
3945 // Ignore the event if this is coming from a focused light child, since th at
3946 // element will deal with it.
3947 if (this.isLightDescendant(/** @type {Node} */(target))) 2777 if (this.isLightDescendant(/** @type {Node} */(target)))
3948 return; 2778 return;
3949 2779
3950 if (this.pressed) { 2780 if (this.pressed) {
3951 this._asyncClick(); 2781 this._asyncClick();
3952 } 2782 }
3953 this._setPressed(false); 2783 this._setPressed(false);
3954 }, 2784 },
3955 2785
3956 // trigger click asynchronously, the asynchrony is useful to allow one
3957 // event handler to unwind before triggering another event
3958 _asyncClick: function() { 2786 _asyncClick: function() {
3959 this.async(function() { 2787 this.async(function() {
3960 this.click(); 2788 this.click();
3961 }, 1); 2789 }, 1);
3962 }, 2790 },
3963 2791
3964 // any of these changes are considered a change to button state
3965 2792
3966 _pressedChanged: function(pressed) { 2793 _pressedChanged: function(pressed) {
3967 this._changedButtonState(); 2794 this._changedButtonState();
3968 }, 2795 },
3969 2796
3970 _ariaActiveAttributeChanged: function(value, oldValue) { 2797 _ariaActiveAttributeChanged: function(value, oldValue) {
3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { 2798 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
3972 this.removeAttribute(oldValue); 2799 this.removeAttribute(oldValue);
3973 } 2800 }
3974 }, 2801 },
3975 2802
3976 _activeChanged: function(active, ariaActiveAttribute) { 2803 _activeChanged: function(active, ariaActiveAttribute) {
3977 if (this.toggles) { 2804 if (this.toggles) {
3978 this.setAttribute(this.ariaActiveAttribute, 2805 this.setAttribute(this.ariaActiveAttribute,
3979 active ? 'true' : 'false'); 2806 active ? 'true' : 'false');
3980 } else { 2807 } else {
3981 this.removeAttribute(this.ariaActiveAttribute); 2808 this.removeAttribute(this.ariaActiveAttribute);
3982 } 2809 }
3983 this._changedButtonState(); 2810 this._changedButtonState();
3984 }, 2811 },
3985 2812
3986 _controlStateChanged: function() { 2813 _controlStateChanged: function() {
3987 if (this.disabled) { 2814 if (this.disabled) {
3988 this._setPressed(false); 2815 this._setPressed(false);
3989 } else { 2816 } else {
3990 this._changedButtonState(); 2817 this._changedButtonState();
3991 } 2818 }
3992 }, 2819 },
3993 2820
3994 // provide hook for follow-on behaviors to react to button-state
3995 2821
3996 _changedButtonState: function() { 2822 _changedButtonState: function() {
3997 if (this._buttonStateChanged) { 2823 if (this._buttonStateChanged) {
3998 this._buttonStateChanged(); // abstract 2824 this._buttonStateChanged(); // abstract
3999 } 2825 }
4000 } 2826 }
4001 2827
4002 }; 2828 };
4003 2829
4004 /** @polymerBehavior */ 2830 /** @polymerBehavior */
4005 Polymer.IronButtonState = [ 2831 Polymer.IronButtonState = [
4006 Polymer.IronA11yKeysBehavior, 2832 Polymer.IronA11yKeysBehavior,
4007 Polymer.IronButtonStateImpl 2833 Polymer.IronButtonStateImpl
4008 ]; 2834 ];
4009 (function() { 2835 (function() {
4010 var Utility = { 2836 var Utility = {
4011 distance: function(x1, y1, x2, y2) { 2837 distance: function(x1, y1, x2, y2) {
4012 var xDelta = (x1 - x2); 2838 var xDelta = (x1 - x2);
4013 var yDelta = (y1 - y2); 2839 var yDelta = (y1 - y2);
4014 2840
4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); 2841 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
4016 }, 2842 },
4017 2843
4018 now: window.performance && window.performance.now ? 2844 now: window.performance && window.performance.now ?
4019 window.performance.now.bind(window.performance) : Date.now 2845 window.performance.now.bind(window.performance) : Date.now
4020 }; 2846 };
4021 2847
4022 /**
4023 * @param {HTMLElement} element
4024 * @constructor
4025 */
4026 function ElementMetrics(element) { 2848 function ElementMetrics(element) {
4027 this.element = element; 2849 this.element = element;
4028 this.width = this.boundingRect.width; 2850 this.width = this.boundingRect.width;
4029 this.height = this.boundingRect.height; 2851 this.height = this.boundingRect.height;
4030 2852
4031 this.size = Math.max(this.width, this.height); 2853 this.size = Math.max(this.width, this.height);
4032 } 2854 }
4033 2855
4034 ElementMetrics.prototype = { 2856 ElementMetrics.prototype = {
4035 get boundingRect () { 2857 get boundingRect () {
4036 return this.element.getBoundingClientRect(); 2858 return this.element.getBoundingClientRect();
4037 }, 2859 },
4038 2860
4039 furthestCornerDistanceFrom: function(x, y) { 2861 furthestCornerDistanceFrom: function(x, y) {
4040 var topLeft = Utility.distance(x, y, 0, 0); 2862 var topLeft = Utility.distance(x, y, 0, 0);
4041 var topRight = Utility.distance(x, y, this.width, 0); 2863 var topRight = Utility.distance(x, y, this.width, 0);
4042 var bottomLeft = Utility.distance(x, y, 0, this.height); 2864 var bottomLeft = Utility.distance(x, y, 0, this.height);
4043 var bottomRight = Utility.distance(x, y, this.width, this.height); 2865 var bottomRight = Utility.distance(x, y, this.width, this.height);
4044 2866
4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight); 2867 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
4046 } 2868 }
4047 }; 2869 };
4048 2870
4049 /**
4050 * @param {HTMLElement} element
4051 * @constructor
4052 */
4053 function Ripple(element) { 2871 function Ripple(element) {
4054 this.element = element; 2872 this.element = element;
4055 this.color = window.getComputedStyle(element).color; 2873 this.color = window.getComputedStyle(element).color;
4056 2874
4057 this.wave = document.createElement('div'); 2875 this.wave = document.createElement('div');
4058 this.waveContainer = document.createElement('div'); 2876 this.waveContainer = document.createElement('div');
4059 this.wave.style.backgroundColor = this.color; 2877 this.wave.style.backgroundColor = this.color;
4060 this.wave.classList.add('wave'); 2878 this.wave.classList.add('wave');
4061 this.waveContainer.classList.add('wave-container'); 2879 this.waveContainer.classList.add('wave-container');
4062 Polymer.dom(this.waveContainer).appendChild(this.wave); 2880 Polymer.dom(this.waveContainer).appendChild(this.wave);
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
4136 return this.initialOpacity; 2954 return this.initialOpacity;
4137 } 2955 }
4138 2956
4139 return Math.max( 2957 return Math.max(
4140 0, 2958 0,
4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity 2959 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
4142 ); 2960 );
4143 }, 2961 },
4144 2962
4145 get outerOpacity() { 2963 get outerOpacity() {
4146 // Linear increase in background opacity, capped at the opacity
4147 // of the wavefront (waveOpacity).
4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; 2964 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
4149 var waveOpacity = this.opacity; 2965 var waveOpacity = this.opacity;
4150 2966
4151 return Math.max( 2967 return Math.max(
4152 0, 2968 0,
4153 Math.min(outerOpacity, waveOpacity) 2969 Math.min(outerOpacity, waveOpacity)
4154 ); 2970 );
4155 }, 2971 },
4156 2972
4157 get isOpacityFullyDecayed() { 2973 get isOpacityFullyDecayed() {
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
4216 var dx; 3032 var dx;
4217 var dy; 3033 var dy;
4218 3034
4219 this.wave.style.opacity = this.opacity; 3035 this.wave.style.opacity = this.opacity;
4220 3036
4221 scale = this.radius / (this.containerMetrics.size / 2); 3037 scale = this.radius / (this.containerMetrics.size / 2);
4222 dx = this.xNow - (this.containerMetrics.width / 2); 3038 dx = this.xNow - (this.containerMetrics.width / 2);
4223 dy = this.yNow - (this.containerMetrics.height / 2); 3039 dy = this.yNow - (this.containerMetrics.height / 2);
4224 3040
4225 3041
4226 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
4227 // https://bugs.webkit.org/show_bug.cgi?id=98538
4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)'; 3042 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)'; 3043 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; 3044 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; 3045 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
4232 }, 3046 },
4233 3047
4234 /** @param {Event=} event */ 3048 /** @param {Event=} event */
4235 downAction: function(event) { 3049 downAction: function(event) {
4236 var xCenter = this.containerMetrics.width / 2; 3050 var xCenter = this.containerMetrics.width / 2;
4237 var yCenter = this.containerMetrics.height / 2; 3051 var yCenter = this.containerMetrics.height / 2;
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
4293 }; 3107 };
4294 3108
4295 Polymer({ 3109 Polymer({
4296 is: 'paper-ripple', 3110 is: 'paper-ripple',
4297 3111
4298 behaviors: [ 3112 behaviors: [
4299 Polymer.IronA11yKeysBehavior 3113 Polymer.IronA11yKeysBehavior
4300 ], 3114 ],
4301 3115
4302 properties: { 3116 properties: {
4303 /**
4304 * The initial opacity set on the wave.
4305 *
4306 * @attribute initialOpacity
4307 * @type number
4308 * @default 0.25
4309 */
4310 initialOpacity: { 3117 initialOpacity: {
4311 type: Number, 3118 type: Number,
4312 value: 0.25 3119 value: 0.25
4313 }, 3120 },
4314 3121
4315 /**
4316 * How fast (opacity per second) the wave fades out.
4317 *
4318 * @attribute opacityDecayVelocity
4319 * @type number
4320 * @default 0.8
4321 */
4322 opacityDecayVelocity: { 3122 opacityDecayVelocity: {
4323 type: Number, 3123 type: Number,
4324 value: 0.8 3124 value: 0.8
4325 }, 3125 },
4326 3126
4327 /**
4328 * If true, ripples will exhibit a gravitational pull towards
4329 * the center of their container as they fade away.
4330 *
4331 * @attribute recenters
4332 * @type boolean
4333 * @default false
4334 */
4335 recenters: { 3127 recenters: {
4336 type: Boolean, 3128 type: Boolean,
4337 value: false 3129 value: false
4338 }, 3130 },
4339 3131
4340 /**
4341 * If true, ripples will center inside its container
4342 *
4343 * @attribute recenters
4344 * @type boolean
4345 * @default false
4346 */
4347 center: { 3132 center: {
4348 type: Boolean, 3133 type: Boolean,
4349 value: false 3134 value: false
4350 }, 3135 },
4351 3136
4352 /**
4353 * A list of the visual ripples.
4354 *
4355 * @attribute ripples
4356 * @type Array
4357 * @default []
4358 */
4359 ripples: { 3137 ripples: {
4360 type: Array, 3138 type: Array,
4361 value: function() { 3139 value: function() {
4362 return []; 3140 return [];
4363 } 3141 }
4364 }, 3142 },
4365 3143
4366 /**
4367 * True when there are visible ripples animating within the
4368 * element.
4369 */
4370 animating: { 3144 animating: {
4371 type: Boolean, 3145 type: Boolean,
4372 readOnly: true, 3146 readOnly: true,
4373 reflectToAttribute: true, 3147 reflectToAttribute: true,
4374 value: false 3148 value: false
4375 }, 3149 },
4376 3150
4377 /**
4378 * If true, the ripple will remain in the "down" state until `holdDown`
4379 * is set to false again.
4380 */
4381 holdDown: { 3151 holdDown: {
4382 type: Boolean, 3152 type: Boolean,
4383 value: false, 3153 value: false,
4384 observer: '_holdDownChanged' 3154 observer: '_holdDownChanged'
4385 }, 3155 },
4386 3156
4387 /**
4388 * If true, the ripple will not generate a ripple effect
4389 * via pointer interaction.
4390 * Calling ripple's imperative api like `simulatedRipple` will
4391 * still generate the ripple effect.
4392 */
4393 noink: { 3157 noink: {
4394 type: Boolean, 3158 type: Boolean,
4395 value: false 3159 value: false
4396 }, 3160 },
4397 3161
4398 _animating: { 3162 _animating: {
4399 type: Boolean 3163 type: Boolean
4400 }, 3164 },
4401 3165
4402 _boundAnimate: { 3166 _boundAnimate: {
4403 type: Function, 3167 type: Function,
4404 value: function() { 3168 value: function() {
4405 return this.animate.bind(this); 3169 return this.animate.bind(this);
4406 } 3170 }
4407 } 3171 }
4408 }, 3172 },
4409 3173
4410 get target () { 3174 get target () {
4411 return this.keyEventTarget; 3175 return this.keyEventTarget;
4412 }, 3176 },
4413 3177
4414 keyBindings: { 3178 keyBindings: {
4415 'enter:keydown': '_onEnterKeydown', 3179 'enter:keydown': '_onEnterKeydown',
4416 'space:keydown': '_onSpaceKeydown', 3180 'space:keydown': '_onSpaceKeydown',
4417 'space:keyup': '_onSpaceKeyup' 3181 'space:keyup': '_onSpaceKeyup'
4418 }, 3182 },
4419 3183
4420 attached: function() { 3184 attached: function() {
4421 // Set up a11yKeysBehavior to listen to key events on the target,
4422 // so that space and enter activate the ripple even if the target doesn' t
4423 // handle key events. The key handlers deal with `noink` themselves.
4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE 3185 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; 3186 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
4426 } else { 3187 } else {
4427 this.keyEventTarget = this.parentNode; 3188 this.keyEventTarget = this.parentNode;
4428 } 3189 }
4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); 3190 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
4430 this.listen(keyEventTarget, 'up', 'uiUpAction'); 3191 this.listen(keyEventTarget, 'up', 'uiUpAction');
4431 this.listen(keyEventTarget, 'down', 'uiDownAction'); 3192 this.listen(keyEventTarget, 'down', 'uiDownAction');
4432 }, 3193 },
4433 3194
4434 detached: function() { 3195 detached: function() {
4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); 3196 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); 3197 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
4437 this.keyEventTarget = null; 3198 this.keyEventTarget = null;
4438 }, 3199 },
4439 3200
4440 get shouldKeepAnimating () { 3201 get shouldKeepAnimating () {
4441 for (var index = 0; index < this.ripples.length; ++index) { 3202 for (var index = 0; index < this.ripples.length; ++index) {
4442 if (!this.ripples[index].isAnimationComplete) { 3203 if (!this.ripples[index].isAnimationComplete) {
4443 return true; 3204 return true;
4444 } 3205 }
4445 } 3206 }
4446 3207
4447 return false; 3208 return false;
4448 }, 3209 },
4449 3210
4450 simulatedRipple: function() { 3211 simulatedRipple: function() {
4451 this.downAction(null); 3212 this.downAction(null);
4452 3213
4453 // Please see polymer/polymer#1305
4454 this.async(function() { 3214 this.async(function() {
4455 this.upAction(); 3215 this.upAction();
4456 }, 1); 3216 }, 1);
4457 }, 3217 },
4458 3218
4459 /**
4460 * Provokes a ripple down effect via a UI event,
4461 * respecting the `noink` property.
4462 * @param {Event=} event
4463 */
4464 uiDownAction: function(event) { 3219 uiDownAction: function(event) {
4465 if (!this.noink) { 3220 if (!this.noink) {
4466 this.downAction(event); 3221 this.downAction(event);
4467 } 3222 }
4468 }, 3223 },
4469 3224
4470 /**
4471 * Provokes a ripple down effect via a UI event,
4472 * *not* respecting the `noink` property.
4473 * @param {Event=} event
4474 */
4475 downAction: function(event) { 3225 downAction: function(event) {
4476 if (this.holdDown && this.ripples.length > 0) { 3226 if (this.holdDown && this.ripples.length > 0) {
4477 return; 3227 return;
4478 } 3228 }
4479 3229
4480 var ripple = this.addRipple(); 3230 var ripple = this.addRipple();
4481 3231
4482 ripple.downAction(event); 3232 ripple.downAction(event);
4483 3233
4484 if (!this._animating) { 3234 if (!this._animating) {
4485 this._animating = true; 3235 this._animating = true;
4486 this.animate(); 3236 this.animate();
4487 } 3237 }
4488 }, 3238 },
4489 3239
4490 /**
4491 * Provokes a ripple up effect via a UI event,
4492 * respecting the `noink` property.
4493 * @param {Event=} event
4494 */
4495 uiUpAction: function(event) { 3240 uiUpAction: function(event) {
4496 if (!this.noink) { 3241 if (!this.noink) {
4497 this.upAction(event); 3242 this.upAction(event);
4498 } 3243 }
4499 }, 3244 },
4500 3245
4501 /**
4502 * Provokes a ripple up effect via a UI event,
4503 * *not* respecting the `noink` property.
4504 * @param {Event=} event
4505 */
4506 upAction: function(event) { 3246 upAction: function(event) {
4507 if (this.holdDown) { 3247 if (this.holdDown) {
4508 return; 3248 return;
4509 } 3249 }
4510 3250
4511 this.ripples.forEach(function(ripple) { 3251 this.ripples.forEach(function(ripple) {
4512 ripple.upAction(event); 3252 ripple.upAction(event);
4513 }); 3253 });
4514 3254
4515 this._animating = true; 3255 this._animating = true;
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
4582 }, 3322 },
4583 3323
4584 _onSpaceKeydown: function() { 3324 _onSpaceKeydown: function() {
4585 this.uiDownAction(); 3325 this.uiDownAction();
4586 }, 3326 },
4587 3327
4588 _onSpaceKeyup: function() { 3328 _onSpaceKeyup: function() {
4589 this.uiUpAction(); 3329 this.uiUpAction();
4590 }, 3330 },
4591 3331
4592 // note: holdDown does not respect noink since it can be a focus based
4593 // effect.
4594 _holdDownChanged: function(newVal, oldVal) { 3332 _holdDownChanged: function(newVal, oldVal) {
4595 if (oldVal === undefined) { 3333 if (oldVal === undefined) {
4596 return; 3334 return;
4597 } 3335 }
4598 if (newVal) { 3336 if (newVal) {
4599 this.downAction(); 3337 this.downAction();
4600 } else { 3338 } else {
4601 this.upAction(); 3339 this.upAction();
4602 } 3340 }
4603 } 3341 }
4604 3342
4605 /**
4606 Fired when the animation finishes.
4607 This is useful if you want to wait until
4608 the ripple animation finishes to perform some action.
4609
4610 @event transitionend
4611 @param {{node: Object}} detail Contains the animated node.
4612 */
4613 }); 3343 });
4614 })(); 3344 })();
4615 /**
4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
4617 * when the element has focus via pointer or keyboard.
4618 *
4619 * NOTE: This behavior is intended to be used in conjunction with and after
4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
4621 *
4622 * @polymerBehavior Polymer.PaperRippleBehavior
4623 */
4624 Polymer.PaperRippleBehavior = { 3345 Polymer.PaperRippleBehavior = {
4625 properties: { 3346 properties: {
4626 /**
4627 * If true, the element will not produce a ripple effect when interacted
4628 * with via the pointer.
4629 */
4630 noink: { 3347 noink: {
4631 type: Boolean, 3348 type: Boolean,
4632 observer: '_noinkChanged' 3349 observer: '_noinkChanged'
4633 }, 3350 },
4634 3351
4635 /**
4636 * @type {Element|undefined}
4637 */
4638 _rippleContainer: { 3352 _rippleContainer: {
4639 type: Object, 3353 type: Object,
4640 } 3354 }
4641 }, 3355 },
4642 3356
4643 /**
4644 * Ensures a `<paper-ripple>` element is available when the element is
4645 * focused.
4646 */
4647 _buttonStateChanged: function() { 3357 _buttonStateChanged: function() {
4648 if (this.focused) { 3358 if (this.focused) {
4649 this.ensureRipple(); 3359 this.ensureRipple();
4650 } 3360 }
4651 }, 3361 },
4652 3362
4653 /**
4654 * In addition to the functionality provided in `IronButtonState`, ensures
4655 * a ripple effect is created when the element is in a `pressed` state.
4656 */
4657 _downHandler: function(event) { 3363 _downHandler: function(event) {
4658 Polymer.IronButtonStateImpl._downHandler.call(this, event); 3364 Polymer.IronButtonStateImpl._downHandler.call(this, event);
4659 if (this.pressed) { 3365 if (this.pressed) {
4660 this.ensureRipple(event); 3366 this.ensureRipple(event);
4661 } 3367 }
4662 }, 3368 },
4663 3369
4664 /**
4665 * Ensures this element contains a ripple effect. For startup efficiency
4666 * the ripple effect is dynamically on demand when needed.
4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
4668 * ripple.
4669 */
4670 ensureRipple: function(optTriggeringEvent) { 3370 ensureRipple: function(optTriggeringEvent) {
4671 if (!this.hasRipple()) { 3371 if (!this.hasRipple()) {
4672 this._ripple = this._createRipple(); 3372 this._ripple = this._createRipple();
4673 this._ripple.noink = this.noink; 3373 this._ripple.noink = this.noink;
4674 var rippleContainer = this._rippleContainer || this.root; 3374 var rippleContainer = this._rippleContainer || this.root;
4675 if (rippleContainer) { 3375 if (rippleContainer) {
4676 Polymer.dom(rippleContainer).appendChild(this._ripple); 3376 Polymer.dom(rippleContainer).appendChild(this._ripple);
4677 } 3377 }
4678 if (optTriggeringEvent) { 3378 if (optTriggeringEvent) {
4679 // Check if the event happened inside of the ripple container
4680 // Fall back to host instead of the root because distributed text
4681 // nodes are not valid event targets
4682 var domContainer = Polymer.dom(this._rippleContainer || this); 3379 var domContainer = Polymer.dom(this._rippleContainer || this);
4683 var target = Polymer.dom(optTriggeringEvent).rootTarget; 3380 var target = Polymer.dom(optTriggeringEvent).rootTarget;
4684 if (domContainer.deepContains( /** @type {Node} */(target))) { 3381 if (domContainer.deepContains( /** @type {Node} */(target))) {
4685 this._ripple.uiDownAction(optTriggeringEvent); 3382 this._ripple.uiDownAction(optTriggeringEvent);
4686 } 3383 }
4687 } 3384 }
4688 } 3385 }
4689 }, 3386 },
4690 3387
4691 /**
4692 * Returns the `<paper-ripple>` element used by this element to create
4693 * ripple effects. The element's ripple is created on demand, when
4694 * necessary, and calling this method will force the
4695 * ripple to be created.
4696 */
4697 getRipple: function() { 3388 getRipple: function() {
4698 this.ensureRipple(); 3389 this.ensureRipple();
4699 return this._ripple; 3390 return this._ripple;
4700 }, 3391 },
4701 3392
4702 /**
4703 * Returns true if this element currently contains a ripple effect.
4704 * @return {boolean}
4705 */
4706 hasRipple: function() { 3393 hasRipple: function() {
4707 return Boolean(this._ripple); 3394 return Boolean(this._ripple);
4708 }, 3395 },
4709 3396
4710 /**
4711 * Create the element's ripple effect via creating a `<paper-ripple>`.
4712 * Override this method to customize the ripple element.
4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
4714 */
4715 _createRipple: function() { 3397 _createRipple: function() {
4716 return /** @type {!PaperRippleElement} */ ( 3398 return /** @type {!PaperRippleElement} */ (
4717 document.createElement('paper-ripple')); 3399 document.createElement('paper-ripple'));
4718 }, 3400 },
4719 3401
4720 _noinkChanged: function(noink) { 3402 _noinkChanged: function(noink) {
4721 if (this.hasRipple()) { 3403 if (this.hasRipple()) {
4722 this._ripple.noink = noink; 3404 this._ripple.noink = noink;
4723 } 3405 }
4724 } 3406 }
4725 }; 3407 };
4726 /** @polymerBehavior Polymer.PaperButtonBehavior */ 3408 /** @polymerBehavior Polymer.PaperButtonBehavior */
4727 Polymer.PaperButtonBehaviorImpl = { 3409 Polymer.PaperButtonBehaviorImpl = {
4728 properties: { 3410 properties: {
4729 /**
4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the
4731 * shadow, and each increasing number greater than 0 will be "deeper"
4732 * than the last.
4733 *
4734 * @attribute elevation
4735 * @type number
4736 * @default 1
4737 */
4738 elevation: { 3411 elevation: {
4739 type: Number, 3412 type: Number,
4740 reflectToAttribute: true, 3413 reflectToAttribute: true,
4741 readOnly: true 3414 readOnly: true
4742 } 3415 }
4743 }, 3416 },
4744 3417
4745 observers: [ 3418 observers: [
4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)', 3419 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
4747 '_computeKeyboardClass(receivedFocusFromKeyboard)' 3420 '_computeKeyboardClass(receivedFocusFromKeyboard)'
(...skipping 14 matching lines...) Expand all
4762 } else if (this.receivedFocusFromKeyboard) { 3435 } else if (this.receivedFocusFromKeyboard) {
4763 e = 3; 3436 e = 3;
4764 } 3437 }
4765 this._setElevation(e); 3438 this._setElevation(e);
4766 }, 3439 },
4767 3440
4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) { 3441 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); 3442 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
4770 }, 3443 },
4771 3444
4772 /**
4773 * In addition to `IronButtonState` behavior, when space key goes down,
4774 * create a ripple down effect.
4775 *
4776 * @param {!KeyboardEvent} event .
4777 */
4778 _spaceKeyDownHandler: function(event) { 3445 _spaceKeyDownHandler: function(event) {
4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); 3446 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
4780 // Ensure that there is at most one ripple when the space key is held down .
4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) { 3447 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
4782 this._ripple.uiDownAction(); 3448 this._ripple.uiDownAction();
4783 } 3449 }
4784 }, 3450 },
4785 3451
4786 /**
4787 * In addition to `IronButtonState` behavior, when space key goes up,
4788 * create a ripple up effect.
4789 *
4790 * @param {!KeyboardEvent} event .
4791 */
4792 _spaceKeyUpHandler: function(event) { 3452 _spaceKeyUpHandler: function(event) {
4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); 3453 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
4794 if (this.hasRipple()) { 3454 if (this.hasRipple()) {
4795 this._ripple.uiUpAction(); 3455 this._ripple.uiUpAction();
4796 } 3456 }
4797 } 3457 }
4798 }; 3458 };
4799 3459
4800 /** @polymerBehavior */ 3460 /** @polymerBehavior */
4801 Polymer.PaperButtonBehavior = [ 3461 Polymer.PaperButtonBehavior = [
4802 Polymer.IronButtonState, 3462 Polymer.IronButtonState,
4803 Polymer.IronControlState, 3463 Polymer.IronControlState,
4804 Polymer.PaperRippleBehavior, 3464 Polymer.PaperRippleBehavior,
4805 Polymer.PaperButtonBehaviorImpl 3465 Polymer.PaperButtonBehaviorImpl
4806 ]; 3466 ];
4807 Polymer({ 3467 Polymer({
4808 is: 'paper-button', 3468 is: 'paper-button',
4809 3469
4810 behaviors: [ 3470 behaviors: [
4811 Polymer.PaperButtonBehavior 3471 Polymer.PaperButtonBehavior
4812 ], 3472 ],
4813 3473
4814 properties: { 3474 properties: {
4815 /**
4816 * If true, the button should be styled with a shadow.
4817 */
4818 raised: { 3475 raised: {
4819 type: Boolean, 3476 type: Boolean,
4820 reflectToAttribute: true, 3477 reflectToAttribute: true,
4821 value: false, 3478 value: false,
4822 observer: '_calculateElevation' 3479 observer: '_calculateElevation'
4823 } 3480 }
4824 }, 3481 },
4825 3482
4826 _calculateElevation: function() { 3483 _calculateElevation: function() {
4827 if (!this.raised) { 3484 if (!this.raised) {
4828 this._setElevation(0); 3485 this._setElevation(0);
4829 } else { 3486 } else {
4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); 3487 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
4831 } 3488 }
4832 } 3489 }
4833 3490
4834 /**
4835 Fired when the animation finishes.
4836 This is useful if you want to wait until
4837 the ripple animation finishes to perform some action.
4838
4839 @event transitionend
4840 Event param: {{node: Object}} detail Contains the animated node.
4841 */
4842 }); 3491 });
4843 (function() { 3492 (function() {
4844 3493
4845 // monostate data
4846 var metaDatas = {}; 3494 var metaDatas = {};
4847 var metaArrays = {}; 3495 var metaArrays = {};
4848 var singleton = null; 3496 var singleton = null;
4849 3497
4850 Polymer.IronMeta = Polymer({ 3498 Polymer.IronMeta = Polymer({
4851 3499
4852 is: 'iron-meta', 3500 is: 'iron-meta',
4853 3501
4854 properties: { 3502 properties: {
4855 3503
4856 /**
4857 * The type of meta-data. All meta-data of the same type is stored
4858 * together.
4859 */
4860 type: { 3504 type: {
4861 type: String, 3505 type: String,
4862 value: 'default', 3506 value: 'default',
4863 observer: '_typeChanged' 3507 observer: '_typeChanged'
4864 }, 3508 },
4865 3509
4866 /**
4867 * The key used to store `value` under the `type` namespace.
4868 */
4869 key: { 3510 key: {
4870 type: String, 3511 type: String,
4871 observer: '_keyChanged' 3512 observer: '_keyChanged'
4872 }, 3513 },
4873 3514
4874 /**
4875 * The meta-data to store or retrieve.
4876 */
4877 value: { 3515 value: {
4878 type: Object, 3516 type: Object,
4879 notify: true, 3517 notify: true,
4880 observer: '_valueChanged' 3518 observer: '_valueChanged'
4881 }, 3519 },
4882 3520
4883 /**
4884 * If true, `value` is set to the iron-meta instance itself.
4885 */
4886 self: { 3521 self: {
4887 type: Boolean, 3522 type: Boolean,
4888 observer: '_selfChanged' 3523 observer: '_selfChanged'
4889 }, 3524 },
4890 3525
4891 /**
4892 * Array of all meta-data values for the given type.
4893 */
4894 list: { 3526 list: {
4895 type: Array, 3527 type: Array,
4896 notify: true 3528 notify: true
4897 } 3529 }
4898 3530
4899 }, 3531 },
4900 3532
4901 hostAttributes: { 3533 hostAttributes: {
4902 hidden: true 3534 hidden: true
4903 }, 3535 },
4904 3536
4905 /**
4906 * Only runs if someone invokes the factory/constructor directly
4907 * e.g. `new Polymer.IronMeta()`
4908 *
4909 * @param {{type: (string|undefined), key: (string|undefined), value}=} co nfig
4910 */
4911 factoryImpl: function(config) { 3537 factoryImpl: function(config) {
4912 if (config) { 3538 if (config) {
4913 for (var n in config) { 3539 for (var n in config) {
4914 switch(n) { 3540 switch(n) {
4915 case 'type': 3541 case 'type':
4916 case 'key': 3542 case 'key':
4917 case 'value': 3543 case 'value':
4918 this[n] = config[n]; 3544 this[n] = config[n];
4919 break; 3545 break;
4920 } 3546 }
4921 } 3547 }
4922 } 3548 }
4923 }, 3549 },
4924 3550
4925 created: function() { 3551 created: function() {
4926 // TODO(sjmiles): good for debugging?
4927 this._metaDatas = metaDatas; 3552 this._metaDatas = metaDatas;
4928 this._metaArrays = metaArrays; 3553 this._metaArrays = metaArrays;
4929 }, 3554 },
4930 3555
4931 _keyChanged: function(key, old) { 3556 _keyChanged: function(key, old) {
4932 this._resetRegistration(old); 3557 this._resetRegistration(old);
4933 }, 3558 },
4934 3559
4935 _valueChanged: function(value) { 3560 _valueChanged: function(value) {
4936 this._resetRegistration(this.key); 3561 this._resetRegistration(this.key);
(...skipping 11 matching lines...) Expand all
4948 metaDatas[type] = {}; 3573 metaDatas[type] = {};
4949 } 3574 }
4950 this._metaData = metaDatas[type]; 3575 this._metaData = metaDatas[type];
4951 if (!metaArrays[type]) { 3576 if (!metaArrays[type]) {
4952 metaArrays[type] = []; 3577 metaArrays[type] = [];
4953 } 3578 }
4954 this.list = metaArrays[type]; 3579 this.list = metaArrays[type];
4955 this._registerKeyValue(this.key, this.value); 3580 this._registerKeyValue(this.key, this.value);
4956 }, 3581 },
4957 3582
4958 /**
4959 * Retrieves meta data value by key.
4960 *
4961 * @method byKey
4962 * @param {string} key The key of the meta-data to be returned.
4963 * @return {*}
4964 */
4965 byKey: function(key) { 3583 byKey: function(key) {
4966 return this._metaData && this._metaData[key]; 3584 return this._metaData && this._metaData[key];
4967 }, 3585 },
4968 3586
4969 _resetRegistration: function(oldKey) { 3587 _resetRegistration: function(oldKey) {
4970 this._unregisterKey(oldKey); 3588 this._unregisterKey(oldKey);
4971 this._registerKeyValue(this.key, this.value); 3589 this._registerKeyValue(this.key, this.value);
4972 }, 3590 },
4973 3591
4974 _unregisterKey: function(key) { 3592 _unregisterKey: function(key) {
(...skipping 23 matching lines...) Expand all
4998 3616
4999 }); 3617 });
5000 3618
5001 Polymer.IronMeta.getIronMeta = function getIronMeta() { 3619 Polymer.IronMeta.getIronMeta = function getIronMeta() {
5002 if (singleton === null) { 3620 if (singleton === null) {
5003 singleton = new Polymer.IronMeta(); 3621 singleton = new Polymer.IronMeta();
5004 } 3622 }
5005 return singleton; 3623 return singleton;
5006 }; 3624 };
5007 3625
5008 /**
5009 `iron-meta-query` can be used to access infomation stored in `iron-meta`.
5010
5011 Examples:
5012
5013 If I create an instance like this:
5014
5015 <iron-meta key="info" value="foo/bar"></iron-meta>
5016
5017 Note that value="foo/bar" is the metadata I've defined. I could define more
5018 attributes or use child nodes to define additional metadata.
5019
5020 Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
5021
5022 var value = new Polymer.IronMetaQuery({key: 'info'}).value;
5023
5024 @group Polymer Iron Elements
5025 @element iron-meta-query
5026 */
5027 Polymer.IronMetaQuery = Polymer({ 3626 Polymer.IronMetaQuery = Polymer({
5028 3627
5029 is: 'iron-meta-query', 3628 is: 'iron-meta-query',
5030 3629
5031 properties: { 3630 properties: {
5032 3631
5033 /**
5034 * The type of meta-data. All meta-data of the same type is stored
5035 * together.
5036 */
5037 type: { 3632 type: {
5038 type: String, 3633 type: String,
5039 value: 'default', 3634 value: 'default',
5040 observer: '_typeChanged' 3635 observer: '_typeChanged'
5041 }, 3636 },
5042 3637
5043 /**
5044 * Specifies a key to use for retrieving `value` from the `type`
5045 * namespace.
5046 */
5047 key: { 3638 key: {
5048 type: String, 3639 type: String,
5049 observer: '_keyChanged' 3640 observer: '_keyChanged'
5050 }, 3641 },
5051 3642
5052 /**
5053 * The meta-data to store or retrieve.
5054 */
5055 value: { 3643 value: {
5056 type: Object, 3644 type: Object,
5057 notify: true, 3645 notify: true,
5058 readOnly: true 3646 readOnly: true
5059 }, 3647 },
5060 3648
5061 /**
5062 * Array of all meta-data values for the given type.
5063 */
5064 list: { 3649 list: {
5065 type: Array, 3650 type: Array,
5066 notify: true 3651 notify: true
5067 } 3652 }
5068 3653
5069 }, 3654 },
5070 3655
5071 /**
5072 * Actually a factory method, not a true constructor. Only runs if
5073 * someone invokes it directly (via `new Polymer.IronMeta()`);
5074 *
5075 * @param {{type: (string|undefined), key: (string|undefined)}=} config
5076 */
5077 factoryImpl: function(config) { 3656 factoryImpl: function(config) {
5078 if (config) { 3657 if (config) {
5079 for (var n in config) { 3658 for (var n in config) {
5080 switch(n) { 3659 switch(n) {
5081 case 'type': 3660 case 'type':
5082 case 'key': 3661 case 'key':
5083 this[n] = config[n]; 3662 this[n] = config[n];
5084 break; 3663 break;
5085 } 3664 }
5086 } 3665 }
5087 } 3666 }
5088 }, 3667 },
5089 3668
5090 created: function() { 3669 created: function() {
5091 // TODO(sjmiles): good for debugging?
5092 this._metaDatas = metaDatas; 3670 this._metaDatas = metaDatas;
5093 this._metaArrays = metaArrays; 3671 this._metaArrays = metaArrays;
5094 }, 3672 },
5095 3673
5096 _keyChanged: function(key) { 3674 _keyChanged: function(key) {
5097 this._setValue(this._metaData && this._metaData[key]); 3675 this._setValue(this._metaData && this._metaData[key]);
5098 }, 3676 },
5099 3677
5100 _typeChanged: function(type) { 3678 _typeChanged: function(type) {
5101 this._metaData = metaDatas[type]; 3679 this._metaData = metaDatas[type];
5102 this.list = metaArrays[type]; 3680 this.list = metaArrays[type];
5103 if (this.key) { 3681 if (this.key) {
5104 this._keyChanged(this.key); 3682 this._keyChanged(this.key);
5105 } 3683 }
5106 }, 3684 },
5107 3685
5108 /**
5109 * Retrieves meta data value by key.
5110 * @param {string} key The key of the meta-data to be returned.
5111 * @return {*}
5112 */
5113 byKey: function(key) { 3686 byKey: function(key) {
5114 return this._metaData && this._metaData[key]; 3687 return this._metaData && this._metaData[key];
5115 } 3688 }
5116 3689
5117 }); 3690 });
5118 3691
5119 })(); 3692 })();
5120 Polymer({ 3693 Polymer({
5121 3694
5122 is: 'iron-icon', 3695 is: 'iron-icon',
5123 3696
5124 properties: { 3697 properties: {
5125 3698
5126 /**
5127 * The name of the icon to use. The name should be of the form:
5128 * `iconset_name:icon_name`.
5129 */
5130 icon: { 3699 icon: {
5131 type: String, 3700 type: String,
5132 observer: '_iconChanged' 3701 observer: '_iconChanged'
5133 }, 3702 },
5134 3703
5135 /**
5136 * The name of the theme to used, if one is specified by the
5137 * iconset.
5138 */
5139 theme: { 3704 theme: {
5140 type: String, 3705 type: String,
5141 observer: '_updateIcon' 3706 observer: '_updateIcon'
5142 }, 3707 },
5143 3708
5144 /**
5145 * If using iron-icon without an iconset, you can set the src to be
5146 * the URL of an individual icon image file. Note that this will take
5147 * precedence over a given icon attribute.
5148 */
5149 src: { 3709 src: {
5150 type: String, 3710 type: String,
5151 observer: '_srcChanged' 3711 observer: '_srcChanged'
5152 }, 3712 },
5153 3713
5154 /**
5155 * @type {!Polymer.IronMeta}
5156 */
5157 _meta: { 3714 _meta: {
5158 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), 3715 value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
5159 observer: '_updateIcon' 3716 observer: '_updateIcon'
5160 } 3717 }
5161 3718
5162 }, 3719 },
5163 3720
5164 _DEFAULT_ICONSET: 'icons', 3721 _DEFAULT_ICONSET: 'icons',
5165 3722
5166 _iconChanged: function(icon) { 3723 _iconChanged: function(icon) {
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
5207 this._img.style.width = '100%'; 3764 this._img.style.width = '100%';
5208 this._img.style.height = '100%'; 3765 this._img.style.height = '100%';
5209 this._img.draggable = false; 3766 this._img.draggable = false;
5210 } 3767 }
5211 this._img.src = this.src; 3768 this._img.src = this.src;
5212 Polymer.dom(this.root).appendChild(this._img); 3769 Polymer.dom(this.root).appendChild(this._img);
5213 } 3770 }
5214 } 3771 }
5215 3772
5216 }); 3773 });
5217 /**
5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus.
5219 *
5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior
5221 */
5222 Polymer.PaperInkyFocusBehaviorImpl = { 3774 Polymer.PaperInkyFocusBehaviorImpl = {
5223 observers: [ 3775 observers: [
5224 '_focusedChanged(receivedFocusFromKeyboard)' 3776 '_focusedChanged(receivedFocusFromKeyboard)'
5225 ], 3777 ],
5226 3778
5227 _focusedChanged: function(receivedFocusFromKeyboard) { 3779 _focusedChanged: function(receivedFocusFromKeyboard) {
5228 if (receivedFocusFromKeyboard) { 3780 if (receivedFocusFromKeyboard) {
5229 this.ensureRipple(); 3781 this.ensureRipple();
5230 } 3782 }
5231 if (this.hasRipple()) { 3783 if (this.hasRipple()) {
(...skipping 23 matching lines...) Expand all
5255 hostAttributes: { 3807 hostAttributes: {
5256 role: 'button', 3808 role: 'button',
5257 tabindex: '0' 3809 tabindex: '0'
5258 }, 3810 },
5259 3811
5260 behaviors: [ 3812 behaviors: [
5261 Polymer.PaperInkyFocusBehavior 3813 Polymer.PaperInkyFocusBehavior
5262 ], 3814 ],
5263 3815
5264 properties: { 3816 properties: {
5265 /**
5266 * The URL of an image for the icon. If the src property is specified,
5267 * the icon property should not be.
5268 */
5269 src: { 3817 src: {
5270 type: String 3818 type: String
5271 }, 3819 },
5272 3820
5273 /**
5274 * Specifies the icon name or index in the set of icons available in
5275 * the icon's icon set. If the icon property is specified,
5276 * the src property should not be.
5277 */
5278 icon: { 3821 icon: {
5279 type: String 3822 type: String
5280 }, 3823 },
5281 3824
5282 /**
5283 * Specifies the alternate text for the button, for accessibility.
5284 */
5285 alt: { 3825 alt: {
5286 type: String, 3826 type: String,
5287 observer: "_altChanged" 3827 observer: "_altChanged"
5288 } 3828 }
5289 }, 3829 },
5290 3830
5291 _altChanged: function(newValue, oldValue) { 3831 _altChanged: function(newValue, oldValue) {
5292 var label = this.getAttribute('aria-label'); 3832 var label = this.getAttribute('aria-label');
5293 3833
5294 // Don't stomp over a user-set aria-label.
5295 if (!label || oldValue == label) { 3834 if (!label || oldValue == label) {
5296 this.setAttribute('aria-label', newValue); 3835 this.setAttribute('aria-label', newValue);
5297 } 3836 }
5298 } 3837 }
5299 }); 3838 });
5300 Polymer({ 3839 Polymer({
5301 is: 'paper-tab', 3840 is: 'paper-tab',
5302 3841
5303 behaviors: [ 3842 behaviors: [
5304 Polymer.IronControlState, 3843 Polymer.IronControlState,
5305 Polymer.IronButtonState, 3844 Polymer.IronButtonState,
5306 Polymer.PaperRippleBehavior 3845 Polymer.PaperRippleBehavior
5307 ], 3846 ],
5308 3847
5309 properties: { 3848 properties: {
5310 3849
5311 /**
5312 * If true, the tab will forward keyboard clicks (enter/space) to
5313 * the first anchor element found in its descendants
5314 */
5315 link: { 3850 link: {
5316 type: Boolean, 3851 type: Boolean,
5317 value: false, 3852 value: false,
5318 reflectToAttribute: true 3853 reflectToAttribute: true
5319 } 3854 }
5320 3855
5321 }, 3856 },
5322 3857
5323 hostAttributes: { 3858 hostAttributes: {
5324 role: 'tab' 3859 role: 'tab'
(...skipping 18 matching lines...) Expand all
5343 }, 3878 },
5344 3879
5345 _onTap: function(event) { 3880 _onTap: function(event) {
5346 if (this.link) { 3881 if (this.link) {
5347 var anchor = this.queryEffectiveChildren('a'); 3882 var anchor = this.queryEffectiveChildren('a');
5348 3883
5349 if (!anchor) { 3884 if (!anchor) {
5350 return; 3885 return;
5351 } 3886 }
5352 3887
5353 // Don't get stuck in a loop delegating
5354 // the listener from the child anchor
5355 if (event.target === anchor) { 3888 if (event.target === anchor) {
5356 return; 3889 return;
5357 } 3890 }
5358 3891
5359 anchor.click(); 3892 anchor.click();
5360 } 3893 }
5361 } 3894 }
5362 3895
5363 }); 3896 });
5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ 3897 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
5365 Polymer.IronMultiSelectableBehaviorImpl = { 3898 Polymer.IronMultiSelectableBehaviorImpl = {
5366 properties: { 3899 properties: {
5367 3900
5368 /**
5369 * If true, multiple selections are allowed.
5370 */
5371 multi: { 3901 multi: {
5372 type: Boolean, 3902 type: Boolean,
5373 value: false, 3903 value: false,
5374 observer: 'multiChanged' 3904 observer: 'multiChanged'
5375 }, 3905 },
5376 3906
5377 /**
5378 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
5379 * is true.
5380 */
5381 selectedValues: { 3907 selectedValues: {
5382 type: Array, 3908 type: Array,
5383 notify: true 3909 notify: true
5384 }, 3910 },
5385 3911
5386 /**
5387 * Returns an array of currently selected items.
5388 */
5389 selectedItems: { 3912 selectedItems: {
5390 type: Array, 3913 type: Array,
5391 readOnly: true, 3914 readOnly: true,
5392 notify: true 3915 notify: true
5393 }, 3916 },
5394 3917
5395 }, 3918 },
5396 3919
5397 observers: [ 3920 observers: [
5398 '_updateSelected(selectedValues.splices)' 3921 '_updateSelected(selectedValues.splices)'
5399 ], 3922 ],
5400 3923
5401 /**
5402 * Selects the given value. If the `multi` property is true, then the select ed state of the
5403 * `value` will be toggled; otherwise the `value` will be selected.
5404 *
5405 * @method select
5406 * @param {string|number} value the value to select.
5407 */
5408 select: function(value) { 3924 select: function(value) {
5409 if (this.multi) { 3925 if (this.multi) {
5410 if (this.selectedValues) { 3926 if (this.selectedValues) {
5411 this._toggleSelected(value); 3927 this._toggleSelected(value);
5412 } else { 3928 } else {
5413 this.selectedValues = [value]; 3929 this.selectedValues = [value];
5414 } 3930 }
5415 } else { 3931 } else {
5416 this.selected = value; 3932 this.selected = value;
5417 } 3933 }
(...skipping 24 matching lines...) Expand all
5442 if (this.multi) { 3958 if (this.multi) {
5443 this._selectMulti(this.selectedValues); 3959 this._selectMulti(this.selectedValues);
5444 } else { 3960 } else {
5445 this._selectSelected(this.selected); 3961 this._selectSelected(this.selected);
5446 } 3962 }
5447 }, 3963 },
5448 3964
5449 _selectMulti: function(values) { 3965 _selectMulti: function(values) {
5450 if (values) { 3966 if (values) {
5451 var selectedItems = this._valuesToItems(values); 3967 var selectedItems = this._valuesToItems(values);
5452 // clear all but the current selected items
5453 this._selection.clear(selectedItems); 3968 this._selection.clear(selectedItems);
5454 // select only those not selected yet
5455 for (var i = 0; i < selectedItems.length; i++) { 3969 for (var i = 0; i < selectedItems.length; i++) {
5456 this._selection.setItemSelected(selectedItems[i], true); 3970 this._selection.setItemSelected(selectedItems[i], true);
5457 } 3971 }
5458 // Check for items, since this array is populated only when attached
5459 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) { 3972 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
5460 var fallback = this._valueToItem(this.fallbackSelection); 3973 var fallback = this._valueToItem(this.fallbackSelection);
5461 if (fallback) { 3974 if (fallback) {
5462 this.selectedValues = [this.fallbackSelection]; 3975 this.selectedValues = [this.fallbackSelection];
5463 } 3976 }
5464 } 3977 }
5465 } else { 3978 } else {
5466 this._selection.clear(); 3979 this._selection.clear();
5467 } 3980 }
5468 }, 3981 },
(...skipping 23 matching lines...) Expand all
5492 return this._valueToItem(value); 4005 return this._valueToItem(value);
5493 }, this); 4006 }, this);
5494 } 4007 }
5495 }; 4008 };
5496 4009
5497 /** @polymerBehavior */ 4010 /** @polymerBehavior */
5498 Polymer.IronMultiSelectableBehavior = [ 4011 Polymer.IronMultiSelectableBehavior = [
5499 Polymer.IronSelectableBehavior, 4012 Polymer.IronSelectableBehavior,
5500 Polymer.IronMultiSelectableBehaviorImpl 4013 Polymer.IronMultiSelectableBehaviorImpl
5501 ]; 4014 ];
5502 /**
5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
5504 *
5505 * @demo demo/index.html
5506 * @polymerBehavior Polymer.IronMenuBehavior
5507 */
5508 Polymer.IronMenuBehaviorImpl = { 4015 Polymer.IronMenuBehaviorImpl = {
5509 4016
5510 properties: { 4017 properties: {
5511 4018
5512 /**
5513 * Returns the currently focused item.
5514 * @type {?Object}
5515 */
5516 focusedItem: { 4019 focusedItem: {
5517 observer: '_focusedItemChanged', 4020 observer: '_focusedItemChanged',
5518 readOnly: true, 4021 readOnly: true,
5519 type: Object 4022 type: Object
5520 }, 4023 },
5521 4024
5522 /**
5523 * The attribute to use on menu items to look up the item title. Typing th e first
5524 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
5525 * will be used.
5526 */
5527 attrForItemTitle: { 4025 attrForItemTitle: {
5528 type: String 4026 type: String
5529 } 4027 }
5530 }, 4028 },
5531 4029
5532 hostAttributes: { 4030 hostAttributes: {
5533 'role': 'menu', 4031 'role': 'menu',
5534 'tabindex': '0' 4032 'tabindex': '0'
5535 }, 4033 },
5536 4034
(...skipping 11 matching lines...) Expand all
5548 'up': '_onUpKey', 4046 'up': '_onUpKey',
5549 'down': '_onDownKey', 4047 'down': '_onDownKey',
5550 'esc': '_onEscKey', 4048 'esc': '_onEscKey',
5551 'shift+tab:keydown': '_onShiftTabDown' 4049 'shift+tab:keydown': '_onShiftTabDown'
5552 }, 4050 },
5553 4051
5554 attached: function() { 4052 attached: function() {
5555 this._resetTabindices(); 4053 this._resetTabindices();
5556 }, 4054 },
5557 4055
5558 /**
5559 * Selects the given value. If the `multi` property is true, then the select ed state of the
5560 * `value` will be toggled; otherwise the `value` will be selected.
5561 *
5562 * @param {string|number} value the value to select.
5563 */
5564 select: function(value) { 4056 select: function(value) {
5565 // Cancel automatically focusing a default item if the menu received focus
5566 // through a user action selecting a particular item.
5567 if (this._defaultFocusAsync) { 4057 if (this._defaultFocusAsync) {
5568 this.cancelAsync(this._defaultFocusAsync); 4058 this.cancelAsync(this._defaultFocusAsync);
5569 this._defaultFocusAsync = null; 4059 this._defaultFocusAsync = null;
5570 } 4060 }
5571 var item = this._valueToItem(value); 4061 var item = this._valueToItem(value);
5572 if (item && item.hasAttribute('disabled')) return; 4062 if (item && item.hasAttribute('disabled')) return;
5573 this._setFocusedItem(item); 4063 this._setFocusedItem(item);
5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); 4064 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
5575 }, 4065 },
5576 4066
5577 /**
5578 * Resets all tabindex attributes to the appropriate value based on the
5579 * current selection state. The appropriate value is `0` (focusable) for
5580 * the default selected item, and `-1` (not keyboard focusable) for all
5581 * other items.
5582 */
5583 _resetTabindices: function() { 4067 _resetTabindices: function() {
5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem; 4068 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem;
5585 4069
5586 this.items.forEach(function(item) { 4070 this.items.forEach(function(item) {
5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); 4071 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
5588 }, this); 4072 }, this);
5589 }, 4073 },
5590 4074
5591 /**
5592 * Sets appropriate ARIA based on whether or not the menu is meant to be
5593 * multi-selectable.
5594 *
5595 * @param {boolean} multi True if the menu should be multi-selectable.
5596 */
5597 _updateMultiselectable: function(multi) { 4075 _updateMultiselectable: function(multi) {
5598 if (multi) { 4076 if (multi) {
5599 this.setAttribute('aria-multiselectable', 'true'); 4077 this.setAttribute('aria-multiselectable', 'true');
5600 } else { 4078 } else {
5601 this.removeAttribute('aria-multiselectable'); 4079 this.removeAttribute('aria-multiselectable');
5602 } 4080 }
5603 }, 4081 },
5604 4082
5605 /**
5606 * Given a KeyboardEvent, this method will focus the appropriate item in the
5607 * menu (if there is a relevant item, and it is possible to focus it).
5608 *
5609 * @param {KeyboardEvent} event A KeyboardEvent.
5610 */
5611 _focusWithKeyboardEvent: function(event) { 4083 _focusWithKeyboardEvent: function(event) {
5612 for (var i = 0, item; item = this.items[i]; i++) { 4084 for (var i = 0, item; item = this.items[i]; i++) {
5613 var attr = this.attrForItemTitle || 'textContent'; 4085 var attr = this.attrForItemTitle || 'textContent';
5614 var title = item[attr] || item.getAttribute(attr); 4086 var title = item[attr] || item.getAttribute(attr);
5615 4087
5616 if (!item.hasAttribute('disabled') && title && 4088 if (!item.hasAttribute('disabled') && title &&
5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) { 4089 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) {
5618 this._setFocusedItem(item); 4090 this._setFocusedItem(item);
5619 break; 4091 break;
5620 } 4092 }
5621 } 4093 }
5622 }, 4094 },
5623 4095
5624 /**
5625 * Focuses the previous item (relative to the currently focused item) in the
5626 * menu, disabled items will be skipped.
5627 * Loop until length + 1 to handle case of single item in menu.
5628 */
5629 _focusPrevious: function() { 4096 _focusPrevious: function() {
5630 var length = this.items.length; 4097 var length = this.items.length;
5631 var curFocusIndex = Number(this.indexOf(this.focusedItem)); 4098 var curFocusIndex = Number(this.indexOf(this.focusedItem));
5632 for (var i = 1; i < length + 1; i++) { 4099 for (var i = 1; i < length + 1; i++) {
5633 var item = this.items[(curFocusIndex - i + length) % length]; 4100 var item = this.items[(curFocusIndex - i + length) % length];
5634 if (!item.hasAttribute('disabled')) { 4101 if (!item.hasAttribute('disabled')) {
5635 this._setFocusedItem(item); 4102 this._setFocusedItem(item);
5636 return; 4103 return;
5637 } 4104 }
5638 } 4105 }
5639 }, 4106 },
5640 4107
5641 /**
5642 * Focuses the next item (relative to the currently focused item) in the
5643 * menu, disabled items will be skipped.
5644 * Loop until length + 1 to handle case of single item in menu.
5645 */
5646 _focusNext: function() { 4108 _focusNext: function() {
5647 var length = this.items.length; 4109 var length = this.items.length;
5648 var curFocusIndex = Number(this.indexOf(this.focusedItem)); 4110 var curFocusIndex = Number(this.indexOf(this.focusedItem));
5649 for (var i = 1; i < length + 1; i++) { 4111 for (var i = 1; i < length + 1; i++) {
5650 var item = this.items[(curFocusIndex + i) % length]; 4112 var item = this.items[(curFocusIndex + i) % length];
5651 if (!item.hasAttribute('disabled')) { 4113 if (!item.hasAttribute('disabled')) {
5652 this._setFocusedItem(item); 4114 this._setFocusedItem(item);
5653 return; 4115 return;
5654 } 4116 }
5655 } 4117 }
5656 }, 4118 },
5657 4119
5658 /**
5659 * Mutates items in the menu based on provided selection details, so that
5660 * all items correctly reflect selection state.
5661 *
5662 * @param {Element} item An item in the menu.
5663 * @param {boolean} isSelected True if the item should be shown in a
5664 * selected state, otherwise false.
5665 */
5666 _applySelection: function(item, isSelected) { 4120 _applySelection: function(item, isSelected) {
5667 if (isSelected) { 4121 if (isSelected) {
5668 item.setAttribute('aria-selected', 'true'); 4122 item.setAttribute('aria-selected', 'true');
5669 } else { 4123 } else {
5670 item.removeAttribute('aria-selected'); 4124 item.removeAttribute('aria-selected');
5671 } 4125 }
5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); 4126 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
5673 }, 4127 },
5674 4128
5675 /**
5676 * Discretely updates tabindex values among menu items as the focused item
5677 * changes.
5678 *
5679 * @param {Element} focusedItem The element that is currently focused.
5680 * @param {?Element} old The last element that was considered focused, if
5681 * applicable.
5682 */
5683 _focusedItemChanged: function(focusedItem, old) { 4129 _focusedItemChanged: function(focusedItem, old) {
5684 old && old.setAttribute('tabindex', '-1'); 4130 old && old.setAttribute('tabindex', '-1');
5685 if (focusedItem) { 4131 if (focusedItem) {
5686 focusedItem.setAttribute('tabindex', '0'); 4132 focusedItem.setAttribute('tabindex', '0');
5687 focusedItem.focus(); 4133 focusedItem.focus();
5688 } 4134 }
5689 }, 4135 },
5690 4136
5691 /**
5692 * A handler that responds to mutation changes related to the list of items
5693 * in the menu.
5694 *
5695 * @param {CustomEvent} event An event containing mutation records as its
5696 * detail.
5697 */
5698 _onIronItemsChanged: function(event) { 4137 _onIronItemsChanged: function(event) {
5699 if (event.detail.addedNodes.length) { 4138 if (event.detail.addedNodes.length) {
5700 this._resetTabindices(); 4139 this._resetTabindices();
5701 } 4140 }
5702 }, 4141 },
5703 4142
5704 /**
5705 * Handler that is called when a shift+tab keypress is detected by the menu.
5706 *
5707 * @param {CustomEvent} event A key combination event.
5708 */
5709 _onShiftTabDown: function(event) { 4143 _onShiftTabDown: function(event) {
5710 var oldTabIndex = this.getAttribute('tabindex'); 4144 var oldTabIndex = this.getAttribute('tabindex');
5711 4145
5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; 4146 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
5713 4147
5714 this._setFocusedItem(null); 4148 this._setFocusedItem(null);
5715 4149
5716 this.setAttribute('tabindex', '-1'); 4150 this.setAttribute('tabindex', '-1');
5717 4151
5718 this.async(function() { 4152 this.async(function() {
5719 this.setAttribute('tabindex', oldTabIndex); 4153 this.setAttribute('tabindex', oldTabIndex);
5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; 4154 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5721 // NOTE(cdata): polymer/polymer#1305
5722 }, 1); 4155 }, 1);
5723 }, 4156 },
5724 4157
5725 /**
5726 * Handler that is called when the menu receives focus.
5727 *
5728 * @param {FocusEvent} event A focus event.
5729 */
5730 _onFocus: function(event) { 4158 _onFocus: function(event) {
5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { 4159 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
5732 // do not focus the menu itself
5733 return; 4160 return;
5734 } 4161 }
5735 4162
5736 // Do not focus the selected tab if the deepest target is part of the
5737 // menu element's local DOM and is focusable.
5738 var rootTarget = /** @type {?HTMLElement} */( 4163 var rootTarget = /** @type {?HTMLElement} */(
5739 Polymer.dom(event).rootTarget); 4164 Polymer.dom(event).rootTarget);
5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) { 4165 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
5741 return; 4166 return;
5742 } 4167 }
5743 4168
5744 // clear the cached focus item
5745 this._defaultFocusAsync = this.async(function() { 4169 this._defaultFocusAsync = this.async(function() {
5746 // focus the selected item when the menu receives focus, or the first it em
5747 // if no item is selected
5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem; 4170 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
5749 4171
5750 this._setFocusedItem(null); 4172 this._setFocusedItem(null);
5751 4173
5752 if (selectedItem) { 4174 if (selectedItem) {
5753 this._setFocusedItem(selectedItem); 4175 this._setFocusedItem(selectedItem);
5754 } else if (this.items[0]) { 4176 } else if (this.items[0]) {
5755 // We find the first none-disabled item (if one exists)
5756 this._focusNext(); 4177 this._focusNext();
5757 } 4178 }
5758 }); 4179 });
5759 }, 4180 },
5760 4181
5761 /**
5762 * Handler that is called when the up key is pressed.
5763 *
5764 * @param {CustomEvent} event A key combination event.
5765 */
5766 _onUpKey: function(event) { 4182 _onUpKey: function(event) {
5767 // up and down arrows moves the focus
5768 this._focusPrevious(); 4183 this._focusPrevious();
5769 event.detail.keyboardEvent.preventDefault(); 4184 event.detail.keyboardEvent.preventDefault();
5770 }, 4185 },
5771 4186
5772 /**
5773 * Handler that is called when the down key is pressed.
5774 *
5775 * @param {CustomEvent} event A key combination event.
5776 */
5777 _onDownKey: function(event) { 4187 _onDownKey: function(event) {
5778 this._focusNext(); 4188 this._focusNext();
5779 event.detail.keyboardEvent.preventDefault(); 4189 event.detail.keyboardEvent.preventDefault();
5780 }, 4190 },
5781 4191
5782 /**
5783 * Handler that is called when the esc key is pressed.
5784 *
5785 * @param {CustomEvent} event A key combination event.
5786 */
5787 _onEscKey: function(event) { 4192 _onEscKey: function(event) {
5788 // esc blurs the control
5789 this.focusedItem.blur(); 4193 this.focusedItem.blur();
5790 }, 4194 },
5791 4195
5792 /**
5793 * Handler that is called when a keydown event is detected.
5794 *
5795 * @param {KeyboardEvent} event A keyboard event.
5796 */
5797 _onKeydown: function(event) { 4196 _onKeydown: function(event) {
5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { 4197 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
5799 // all other keys focus the menu item starting with that character
5800 this._focusWithKeyboardEvent(event); 4198 this._focusWithKeyboardEvent(event);
5801 } 4199 }
5802 event.stopPropagation(); 4200 event.stopPropagation();
5803 }, 4201 },
5804 4202
5805 // override _activateHandler
5806 _activateHandler: function(event) { 4203 _activateHandler: function(event) {
5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event); 4204 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
5808 event.stopPropagation(); 4205 event.stopPropagation();
5809 } 4206 }
5810 }; 4207 };
5811 4208
5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; 4209 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5813 4210
5814 /** @polymerBehavior Polymer.IronMenuBehavior */ 4211 /** @polymerBehavior Polymer.IronMenuBehavior */
5815 Polymer.IronMenuBehavior = [ 4212 Polymer.IronMenuBehavior = [
5816 Polymer.IronMultiSelectableBehavior, 4213 Polymer.IronMultiSelectableBehavior,
5817 Polymer.IronA11yKeysBehavior, 4214 Polymer.IronA11yKeysBehavior,
5818 Polymer.IronMenuBehaviorImpl 4215 Polymer.IronMenuBehaviorImpl
5819 ]; 4216 ];
5820 /**
5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior.
5822 *
5823 * @polymerBehavior Polymer.IronMenubarBehavior
5824 */
5825 Polymer.IronMenubarBehaviorImpl = { 4217 Polymer.IronMenubarBehaviorImpl = {
5826 4218
5827 hostAttributes: { 4219 hostAttributes: {
5828 'role': 'menubar' 4220 'role': 'menubar'
5829 }, 4221 },
5830 4222
5831 keyBindings: { 4223 keyBindings: {
5832 'left': '_onLeftKey', 4224 'left': '_onLeftKey',
5833 'right': '_onRightKey' 4225 'right': '_onRightKey'
5834 }, 4226 },
(...skipping 28 matching lines...) Expand all
5863 this._focusNext(); 4255 this._focusNext();
5864 } 4256 }
5865 event.detail.keyboardEvent.preventDefault(); 4257 event.detail.keyboardEvent.preventDefault();
5866 }, 4258 },
5867 4259
5868 _onKeydown: function(event) { 4260 _onKeydown: function(event) {
5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { 4261 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
5870 return; 4262 return;
5871 } 4263 }
5872 4264
5873 // all other keys focus the menu item starting with that character
5874 this._focusWithKeyboardEvent(event); 4265 this._focusWithKeyboardEvent(event);
5875 } 4266 }
5876 4267
5877 }; 4268 };
5878 4269
5879 /** @polymerBehavior Polymer.IronMenubarBehavior */ 4270 /** @polymerBehavior Polymer.IronMenubarBehavior */
5880 Polymer.IronMenubarBehavior = [ 4271 Polymer.IronMenubarBehavior = [
5881 Polymer.IronMenuBehavior, 4272 Polymer.IronMenuBehavior,
5882 Polymer.IronMenubarBehaviorImpl 4273 Polymer.IronMenubarBehaviorImpl
5883 ]; 4274 ];
5884 /**
5885 * The `iron-iconset-svg` element allows users to define their own icon sets
5886 * that contain svg icons. The svg icon elements should be children of the
5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
5888 *
5889 * Using svg elements to create icons has a few advantages over traditional
5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so
5891 * they are resolution independent and should look good on any device. They
5892 * are stylable via css. Icons can be themed, colorized, and even animated.
5893 *
5894 * Example:
5895 *
5896 * <iron-iconset-svg name="my-svg-icons" size="24">
5897 * <svg>
5898 * <defs>
5899 * <g id="shape">
5900 * <rect x="12" y="0" width="12" height="24" />
5901 * <circle cx="12" cy="12" r="12" />
5902 * </g>
5903 * </defs>
5904 * </svg>
5905 * </iron-iconset-svg>
5906 *
5907 * This will automatically register the icon set "my-svg-icons" to the iconset
5908 * database. To use these icons from within another element, make a
5909 * `iron-iconset` element and call the `byId` method
5910 * to retrieve a given iconset. To apply a particular icon inside an
5911 * element use the `applyIcon` method. For example:
5912 *
5913 * iconset.applyIcon(iconNode, 'car');
5914 *
5915 * @element iron-iconset-svg
5916 * @demo demo/index.html
5917 * @implements {Polymer.Iconset}
5918 */
5919 Polymer({ 4275 Polymer({
5920 is: 'iron-iconset-svg', 4276 is: 'iron-iconset-svg',
5921 4277
5922 properties: { 4278 properties: {
5923 4279
5924 /**
5925 * The name of the iconset.
5926 */
5927 name: { 4280 name: {
5928 type: String, 4281 type: String,
5929 observer: '_nameChanged' 4282 observer: '_nameChanged'
5930 }, 4283 },
5931 4284
5932 /**
5933 * The size of an individual icon. Note that icons must be square.
5934 */
5935 size: { 4285 size: {
5936 type: Number, 4286 type: Number,
5937 value: 24 4287 value: 24
5938 } 4288 }
5939 4289
5940 }, 4290 },
5941 4291
5942 attached: function() { 4292 attached: function() {
5943 this.style.display = 'none'; 4293 this.style.display = 'none';
5944 }, 4294 },
5945 4295
5946 /**
5947 * Construct an array of all icon names in this iconset.
5948 *
5949 * @return {!Array} Array of icon names.
5950 */
5951 getIconNames: function() { 4296 getIconNames: function() {
5952 this._icons = this._createIconMap(); 4297 this._icons = this._createIconMap();
5953 return Object.keys(this._icons).map(function(n) { 4298 return Object.keys(this._icons).map(function(n) {
5954 return this.name + ':' + n; 4299 return this.name + ':' + n;
5955 }, this); 4300 }, this);
5956 }, 4301 },
5957 4302
5958 /**
5959 * Applies an icon to the given element.
5960 *
5961 * An svg icon is prepended to the element's shadowRoot if it exists,
5962 * otherwise to the element itself.
5963 *
5964 * @method applyIcon
5965 * @param {Element} element Element to which the icon is applied.
5966 * @param {string} iconName Name of the icon to apply.
5967 * @return {?Element} The svg element which renders the icon.
5968 */
5969 applyIcon: function(element, iconName) { 4303 applyIcon: function(element, iconName) {
5970 // insert svg element into shadow root, if it exists
5971 element = element.root || element; 4304 element = element.root || element;
5972 // Remove old svg element
5973 this.removeIcon(element); 4305 this.removeIcon(element);
5974 // install new svg element
5975 var svg = this._cloneIcon(iconName); 4306 var svg = this._cloneIcon(iconName);
5976 if (svg) { 4307 if (svg) {
5977 var pde = Polymer.dom(element); 4308 var pde = Polymer.dom(element);
5978 pde.insertBefore(svg, pde.childNodes[0]); 4309 pde.insertBefore(svg, pde.childNodes[0]);
5979 return element._svgIcon = svg; 4310 return element._svgIcon = svg;
5980 } 4311 }
5981 return null; 4312 return null;
5982 }, 4313 },
5983 4314
5984 /**
5985 * Remove an icon from the given element by undoing the changes effected
5986 * by `applyIcon`.
5987 *
5988 * @param {Element} element The element from which the icon is removed.
5989 */
5990 removeIcon: function(element) { 4315 removeIcon: function(element) {
5991 // Remove old svg element
5992 if (element._svgIcon) { 4316 if (element._svgIcon) {
5993 Polymer.dom(element).removeChild(element._svgIcon); 4317 Polymer.dom(element).removeChild(element._svgIcon);
5994 element._svgIcon = null; 4318 element._svgIcon = null;
5995 } 4319 }
5996 }, 4320 },
5997 4321
5998 /**
5999 *
6000 * When name is changed, register iconset metadata
6001 *
6002 */
6003 _nameChanged: function() { 4322 _nameChanged: function() {
6004 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); 4323 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
6005 this.async(function() { 4324 this.async(function() {
6006 this.fire('iron-iconset-added', this, {node: window}); 4325 this.fire('iron-iconset-added', this, {node: window});
6007 }); 4326 });
6008 }, 4327 },
6009 4328
6010 /**
6011 * Create a map of child SVG elements by id.
6012 *
6013 * @return {!Object} Map of id's to SVG elements.
6014 */
6015 _createIconMap: function() { 4329 _createIconMap: function() {
6016 // Objects chained to Object.prototype (`{}`) have members. Specifically,
6017 // on FF there is a `watch` method that confuses the icon map, so we
6018 // need to use a null-based object here.
6019 var icons = Object.create(null); 4330 var icons = Object.create(null);
6020 Polymer.dom(this).querySelectorAll('[id]') 4331 Polymer.dom(this).querySelectorAll('[id]')
6021 .forEach(function(icon) { 4332 .forEach(function(icon) {
6022 icons[icon.id] = icon; 4333 icons[icon.id] = icon;
6023 }); 4334 });
6024 return icons; 4335 return icons;
6025 }, 4336 },
6026 4337
6027 /**
6028 * Produce installable clone of the SVG element matching `id` in this
6029 * iconset, or `undefined` if there is no matching element.
6030 *
6031 * @return {Element} Returns an installable clone of the SVG element
6032 * matching `id`.
6033 */
6034 _cloneIcon: function(id) { 4338 _cloneIcon: function(id) {
6035 // create the icon map on-demand, since the iconset itself has no discrete
6036 // signal to know when it's children are fully parsed
6037 this._icons = this._icons || this._createIconMap(); 4339 this._icons = this._icons || this._createIconMap();
6038 return this._prepareSvgClone(this._icons[id], this.size); 4340 return this._prepareSvgClone(this._icons[id], this.size);
6039 }, 4341 },
6040 4342
6041 /**
6042 * @param {Element} sourceSvg
6043 * @param {number} size
6044 * @return {Element}
6045 */
6046 _prepareSvgClone: function(sourceSvg, size) { 4343 _prepareSvgClone: function(sourceSvg, size) {
6047 if (sourceSvg) { 4344 if (sourceSvg) {
6048 var content = sourceSvg.cloneNode(true), 4345 var content = sourceSvg.cloneNode(true),
6049 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), 4346 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
6050 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s ize; 4347 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s ize;
6051 svg.setAttribute('viewBox', viewBox); 4348 svg.setAttribute('viewBox', viewBox);
6052 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); 4349 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136
6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;'; 4350 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6056 svg.appendChild(content).removeAttribute('id'); 4351 svg.appendChild(content).removeAttribute('id');
6057 return svg; 4352 return svg;
6058 } 4353 }
6059 return null; 4354 return null;
6060 } 4355 }
6061 4356
6062 }); 4357 });
6063 Polymer({ 4358 Polymer({
6064 is: 'paper-tabs', 4359 is: 'paper-tabs',
6065 4360
6066 behaviors: [ 4361 behaviors: [
6067 Polymer.IronResizableBehavior, 4362 Polymer.IronResizableBehavior,
6068 Polymer.IronMenubarBehavior 4363 Polymer.IronMenubarBehavior
6069 ], 4364 ],
6070 4365
6071 properties: { 4366 properties: {
6072 /**
6073 * If true, ink ripple effect is disabled. When this property is changed ,
6074 * all descendant `<paper-tab>` elements have their `noink` property
6075 * changed to the new value as well.
6076 */
6077 noink: { 4367 noink: {
6078 type: Boolean, 4368 type: Boolean,
6079 value: false, 4369 value: false,
6080 observer: '_noinkChanged' 4370 observer: '_noinkChanged'
6081 }, 4371 },
6082 4372
6083 /**
6084 * If true, the bottom bar to indicate the selected tab will not be show n.
6085 */
6086 noBar: { 4373 noBar: {
6087 type: Boolean, 4374 type: Boolean,
6088 value: false 4375 value: false
6089 }, 4376 },
6090 4377
6091 /**
6092 * If true, the slide effect for the bottom bar is disabled.
6093 */
6094 noSlide: { 4378 noSlide: {
6095 type: Boolean, 4379 type: Boolean,
6096 value: false 4380 value: false
6097 }, 4381 },
6098 4382
6099 /**
6100 * If true, tabs are scrollable and the tab width is based on the label width.
6101 */
6102 scrollable: { 4383 scrollable: {
6103 type: Boolean, 4384 type: Boolean,
6104 value: false 4385 value: false
6105 }, 4386 },
6106 4387
6107 /**
6108 * If true, tabs expand to fit their container. This currently only appl ies when
6109 * scrollable is true.
6110 */
6111 fitContainer: { 4388 fitContainer: {
6112 type: Boolean, 4389 type: Boolean,
6113 value: false 4390 value: false
6114 }, 4391 },
6115 4392
6116 /**
6117 * If true, dragging on the tabs to scroll is disabled.
6118 */
6119 disableDrag: { 4393 disableDrag: {
6120 type: Boolean, 4394 type: Boolean,
6121 value: false 4395 value: false
6122 }, 4396 },
6123 4397
6124 /**
6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla ble tabs.
6126 */
6127 hideScrollButtons: { 4398 hideScrollButtons: {
6128 type: Boolean, 4399 type: Boolean,
6129 value: false 4400 value: false
6130 }, 4401 },
6131 4402
6132 /**
6133 * If true, the tabs are aligned to bottom (the selection bar appears at the top).
6134 */
6135 alignBottom: { 4403 alignBottom: {
6136 type: Boolean, 4404 type: Boolean,
6137 value: false 4405 value: false
6138 }, 4406 },
6139 4407
6140 selectable: { 4408 selectable: {
6141 type: String, 4409 type: String,
6142 value: 'paper-tab' 4410 value: 'paper-tab'
6143 }, 4411 },
6144 4412
6145 /**
6146 * If true, tabs are automatically selected when focused using the
6147 * keyboard.
6148 */
6149 autoselect: { 4413 autoselect: {
6150 type: Boolean, 4414 type: Boolean,
6151 value: false 4415 value: false
6152 }, 4416 },
6153 4417
6154 /**
6155 * The delay (in milliseconds) between when the user stops interacting
6156 * with the tabs through the keyboard and when the focused item is
6157 * automatically selected (if `autoselect` is true).
6158 */
6159 autoselectDelay: { 4418 autoselectDelay: {
6160 type: Number, 4419 type: Number,
6161 value: 0 4420 value: 0
6162 }, 4421 },
6163 4422
6164 _step: { 4423 _step: {
6165 type: Number, 4424 type: Number,
6166 value: 10 4425 value: 10
6167 }, 4426 },
6168 4427
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
6249 _computeSelectionBarClass: function(noBar, alignBottom) { 4508 _computeSelectionBarClass: function(noBar, alignBottom) {
6250 if (noBar) { 4509 if (noBar) {
6251 return 'hidden'; 4510 return 'hidden';
6252 } else if (alignBottom) { 4511 } else if (alignBottom) {
6253 return 'align-bottom'; 4512 return 'align-bottom';
6254 } 4513 }
6255 4514
6256 return ''; 4515 return '';
6257 }, 4516 },
6258 4517
6259 // TODO(cdata): Add `track` response back in when gesture lands.
6260 4518
6261 _onTabSizingChanged: function() { 4519 _onTabSizingChanged: function() {
6262 this.debounce('_onTabSizingChanged', function() { 4520 this.debounce('_onTabSizingChanged', function() {
6263 this._scroll(); 4521 this._scroll();
6264 this._tabChanged(this.selectedItem); 4522 this._tabChanged(this.selectedItem);
6265 }, 10); 4523 }, 10);
6266 }, 4524 },
6267 4525
6268 _onIronSelect: function(event) { 4526 _onIronSelect: function(event) {
6269 this._tabChanged(event.detail.item, this._previousTab); 4527 this._tabChanged(event.detail.item, this._previousTab);
6270 this._previousTab = event.detail.item; 4528 this._previousTab = event.detail.item;
6271 this.cancelDebouncer('tab-changed'); 4529 this.cancelDebouncer('tab-changed');
6272 }, 4530 },
6273 4531
6274 _onIronDeselect: function(event) { 4532 _onIronDeselect: function(event) {
6275 this.debounce('tab-changed', function() { 4533 this.debounce('tab-changed', function() {
6276 this._tabChanged(null, this._previousTab); 4534 this._tabChanged(null, this._previousTab);
6277 this._previousTab = null; 4535 this._previousTab = null;
6278 // See polymer/polymer#1305
6279 }, 1); 4536 }, 1);
6280 }, 4537 },
6281 4538
6282 _activateHandler: function() { 4539 _activateHandler: function() {
6283 // Cancel item activations scheduled by keyboard events when any other
6284 // action causes an item to be activated (e.g. clicks).
6285 this._cancelPendingActivation(); 4540 this._cancelPendingActivation();
6286 4541
6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); 4542 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
6288 }, 4543 },
6289 4544
6290 /**
6291 * Activates an item after a delay (in milliseconds).
6292 */
6293 _scheduleActivation: function(item, delay) { 4545 _scheduleActivation: function(item, delay) {
6294 this._pendingActivationItem = item; 4546 this._pendingActivationItem = item;
6295 this._pendingActivationTimeout = this.async( 4547 this._pendingActivationTimeout = this.async(
6296 this._bindDelayedActivationHandler, delay); 4548 this._bindDelayedActivationHandler, delay);
6297 }, 4549 },
6298 4550
6299 /**
6300 * Activates the last item given to `_scheduleActivation`.
6301 */
6302 _delayedActivationHandler: function() { 4551 _delayedActivationHandler: function() {
6303 var item = this._pendingActivationItem; 4552 var item = this._pendingActivationItem;
6304 this._pendingActivationItem = undefined; 4553 this._pendingActivationItem = undefined;
6305 this._pendingActivationTimeout = undefined; 4554 this._pendingActivationTimeout = undefined;
6306 item.fire(this.activateEvent, null, { 4555 item.fire(this.activateEvent, null, {
6307 bubbles: true, 4556 bubbles: true,
6308 cancelable: true 4557 cancelable: true
6309 }); 4558 });
6310 }, 4559 },
6311 4560
6312 /**
6313 * Cancels a previously scheduled item activation made with
6314 * `_scheduleActivation`.
6315 */
6316 _cancelPendingActivation: function() { 4561 _cancelPendingActivation: function() {
6317 if (this._pendingActivationTimeout !== undefined) { 4562 if (this._pendingActivationTimeout !== undefined) {
6318 this.cancelAsync(this._pendingActivationTimeout); 4563 this.cancelAsync(this._pendingActivationTimeout);
6319 this._pendingActivationItem = undefined; 4564 this._pendingActivationItem = undefined;
6320 this._pendingActivationTimeout = undefined; 4565 this._pendingActivationTimeout = undefined;
6321 } 4566 }
6322 }, 4567 },
6323 4568
6324 _onArrowKeyup: function(event) { 4569 _onArrowKeyup: function(event) {
6325 if (this.autoselect) { 4570 if (this.autoselect) {
6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay); 4571 this._scheduleActivation(this.focusedItem, this.autoselectDelay);
6327 } 4572 }
6328 }, 4573 },
6329 4574
6330 _onBlurCapture: function(event) { 4575 _onBlurCapture: function(event) {
6331 // Cancel a scheduled item activation (if any) when that item is
6332 // blurred.
6333 if (event.target === this._pendingActivationItem) { 4576 if (event.target === this._pendingActivationItem) {
6334 this._cancelPendingActivation(); 4577 this._cancelPendingActivation();
6335 } 4578 }
6336 }, 4579 },
6337 4580
6338 get _tabContainerScrollSize () { 4581 get _tabContainerScrollSize () {
6339 return Math.max( 4582 return Math.max(
6340 0, 4583 0,
6341 this.$.tabsContainer.scrollWidth - 4584 this.$.tabsContainer.scrollWidth -
6342 this.$.tabsContainer.offsetWidth 4585 this.$.tabsContainer.offsetWidth
6343 ); 4586 );
6344 }, 4587 },
6345 4588
6346 _scroll: function(e, detail) { 4589 _scroll: function(e, detail) {
6347 if (!this.scrollable) { 4590 if (!this.scrollable) {
6348 return; 4591 return;
6349 } 4592 }
6350 4593
6351 var ddx = (detail && -detail.ddx) || 0; 4594 var ddx = (detail && -detail.ddx) || 0;
6352 this._affectScroll(ddx); 4595 this._affectScroll(ddx);
6353 }, 4596 },
6354 4597
6355 _down: function(e) { 4598 _down: function(e) {
6356 // go one beat async to defeat IronMenuBehavior
6357 // autorefocus-on-no-selection timeout
6358 this.async(function() { 4599 this.async(function() {
6359 if (this._defaultFocusAsync) { 4600 if (this._defaultFocusAsync) {
6360 this.cancelAsync(this._defaultFocusAsync); 4601 this.cancelAsync(this._defaultFocusAsync);
6361 this._defaultFocusAsync = null; 4602 this._defaultFocusAsync = null;
6362 } 4603 }
6363 }, 1); 4604 }, 1);
6364 }, 4605 },
6365 4606
6366 _affectScroll: function(dx) { 4607 _affectScroll: function(dx) {
6367 this.$.tabsContainer.scrollLeft += dx; 4608 this.$.tabsContainer.scrollLeft += dx;
(...skipping 22 matching lines...) Expand all
6390 _scrollToLeft: function() { 4631 _scrollToLeft: function() {
6391 this._affectScroll(-this._step); 4632 this._affectScroll(-this._step);
6392 }, 4633 },
6393 4634
6394 _scrollToRight: function() { 4635 _scrollToRight: function() {
6395 this._affectScroll(this._step); 4636 this._affectScroll(this._step);
6396 }, 4637 },
6397 4638
6398 _tabChanged: function(tab, old) { 4639 _tabChanged: function(tab, old) {
6399 if (!tab) { 4640 if (!tab) {
6400 // Remove the bar without animation.
6401 this.$.selectionBar.classList.remove('expand'); 4641 this.$.selectionBar.classList.remove('expand');
6402 this.$.selectionBar.classList.remove('contract'); 4642 this.$.selectionBar.classList.remove('contract');
6403 this._positionBar(0, 0); 4643 this._positionBar(0, 0);
6404 return; 4644 return;
6405 } 4645 }
6406 4646
6407 var r = this.$.tabsContent.getBoundingClientRect(); 4647 var r = this.$.tabsContent.getBoundingClientRect();
6408 var w = r.width; 4648 var w = r.width;
6409 var tabRect = tab.getBoundingClientRect(); 4649 var tabRect = tab.getBoundingClientRect();
6410 var tabOffsetLeft = tabRect.left - r.left; 4650 var tabOffsetLeft = tabRect.left - r.left;
6411 4651
6412 this._pos = { 4652 this._pos = {
6413 width: this._calcPercent(tabRect.width, w), 4653 width: this._calcPercent(tabRect.width, w),
6414 left: this._calcPercent(tabOffsetLeft, w) 4654 left: this._calcPercent(tabOffsetLeft, w)
6415 }; 4655 };
6416 4656
6417 if (this.noSlide || old == null) { 4657 if (this.noSlide || old == null) {
6418 // Position the bar without animation.
6419 this.$.selectionBar.classList.remove('expand'); 4658 this.$.selectionBar.classList.remove('expand');
6420 this.$.selectionBar.classList.remove('contract'); 4659 this.$.selectionBar.classList.remove('contract');
6421 this._positionBar(this._pos.width, this._pos.left); 4660 this._positionBar(this._pos.width, this._pos.left);
6422 return; 4661 return;
6423 } 4662 }
6424 4663
6425 var oldRect = old.getBoundingClientRect(); 4664 var oldRect = old.getBoundingClientRect();
6426 var oldIndex = this.items.indexOf(old); 4665 var oldIndex = this.items.indexOf(old);
6427 var index = this.items.indexOf(tab); 4666 var index = this.items.indexOf(tab);
6428 var m = 5; 4667 var m = 5;
6429 4668
6430 // bar animation: expand
6431 this.$.selectionBar.classList.add('expand'); 4669 this.$.selectionBar.classList.add('expand');
6432 4670
6433 var moveRight = oldIndex < index; 4671 var moveRight = oldIndex < index;
6434 var isRTL = this._isRTL; 4672 var isRTL = this._isRTL;
6435 if (isRTL) { 4673 if (isRTL) {
6436 moveRight = !moveRight; 4674 moveRight = !moveRight;
6437 } 4675 }
6438 4676
6439 if (moveRight) { 4677 if (moveRight) {
6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old Rect.left, w) - m, 4678 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old Rect.left, w) - m,
(...skipping 30 matching lines...) Expand all
6471 4709
6472 this._width = width; 4710 this._width = width;
6473 this._left = left; 4711 this._left = left;
6474 this.transform( 4712 this.transform(
6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')', 4713 'translateX(' + left + '%) scaleX(' + (width / 100) + ')',
6476 this.$.selectionBar); 4714 this.$.selectionBar);
6477 }, 4715 },
6478 4716
6479 _onBarTransitionEnd: function(e) { 4717 _onBarTransitionEnd: function(e) {
6480 var cl = this.$.selectionBar.classList; 4718 var cl = this.$.selectionBar.classList;
6481 // bar animation: expand -> contract
6482 if (cl.contains('expand')) { 4719 if (cl.contains('expand')) {
6483 cl.remove('expand'); 4720 cl.remove('expand');
6484 cl.add('contract'); 4721 cl.add('contract');
6485 this._positionBar(this._pos.width, this._pos.left); 4722 this._positionBar(this._pos.width, this._pos.left);
6486 // bar animation done
6487 } else if (cl.contains('contract')) { 4723 } else if (cl.contains('contract')) {
6488 cl.remove('contract'); 4724 cl.remove('contract');
6489 } 4725 }
6490 } 4726 }
6491 }); 4727 });
6492 (function() { 4728 (function() {
6493 'use strict'; 4729 'use strict';
6494 4730
6495 Polymer.IronA11yAnnouncer = Polymer({ 4731 Polymer.IronA11yAnnouncer = Polymer({
6496 is: 'iron-a11y-announcer', 4732 is: 'iron-a11y-announcer',
6497 4733
6498 properties: { 4734 properties: {
6499 4735
6500 /**
6501 * The value of mode is used to set the `aria-live` attribute
6502 * for the element that will be announced. Valid values are: `off`,
6503 * `polite` and `assertive`.
6504 */
6505 mode: { 4736 mode: {
6506 type: String, 4737 type: String,
6507 value: 'polite' 4738 value: 'polite'
6508 }, 4739 },
6509 4740
6510 _text: { 4741 _text: {
6511 type: String, 4742 type: String,
6512 value: '' 4743 value: ''
6513 } 4744 }
6514 }, 4745 },
6515 4746
6516 created: function() { 4747 created: function() {
6517 if (!Polymer.IronA11yAnnouncer.instance) { 4748 if (!Polymer.IronA11yAnnouncer.instance) {
6518 Polymer.IronA11yAnnouncer.instance = this; 4749 Polymer.IronA11yAnnouncer.instance = this;
6519 } 4750 }
6520 4751
6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this)); 4752 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
6522 }, 4753 },
6523 4754
6524 /**
6525 * Cause a text string to be announced by screen readers.
6526 *
6527 * @param {string} text The text that should be announced.
6528 */
6529 announce: function(text) { 4755 announce: function(text) {
6530 this._text = ''; 4756 this._text = '';
6531 this.async(function() { 4757 this.async(function() {
6532 this._text = text; 4758 this._text = text;
6533 }, 100); 4759 }, 100);
6534 }, 4760 },
6535 4761
6536 _onIronAnnounce: function(event) { 4762 _onIronAnnounce: function(event) {
6537 if (event.detail && event.detail.text) { 4763 if (event.detail && event.detail.text) {
6538 this.announce(event.detail.text); 4764 this.announce(event.detail.text);
6539 } 4765 }
6540 } 4766 }
6541 }); 4767 });
6542 4768
6543 Polymer.IronA11yAnnouncer.instance = null; 4769 Polymer.IronA11yAnnouncer.instance = null;
6544 4770
6545 Polymer.IronA11yAnnouncer.requestAvailability = function() { 4771 Polymer.IronA11yAnnouncer.requestAvailability = function() {
6546 if (!Polymer.IronA11yAnnouncer.instance) { 4772 if (!Polymer.IronA11yAnnouncer.instance) {
6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer'); 4773 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
6548 } 4774 }
6549 4775
6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); 4776 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
6551 }; 4777 };
6552 })(); 4778 })();
6553 /**
6554 * Singleton IronMeta instance.
6555 */
6556 Polymer.IronValidatableBehaviorMeta = null; 4779 Polymer.IronValidatableBehaviorMeta = null;
6557 4780
6558 /**
6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
6561 *
6562 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
6563 * To validate a form imperatively, call the form's `validate()` method, which in turn will
6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
6565 * custom element will get a public `validate()`, which
6566 * will return the validity of the element, and a corresponding `invalid` attr ibute,
6567 * which can be used for styling.
6568 *
6569 * To implement the custom validation logic of your element, you must override
6570 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
6572 * for an example.
6573 *
6574 * ### Accessibility
6575 *
6576 * Changing the `invalid` property, either manually or by calling `validate()` will update the
6577 * `aria-invalid` attribute.
6578 *
6579 * @demo demo/index.html
6580 * @polymerBehavior
6581 */
6582 Polymer.IronValidatableBehavior = { 4781 Polymer.IronValidatableBehavior = {
6583 4782
6584 properties: { 4783 properties: {
6585 4784
6586 /**
6587 * Name of the validator to use.
6588 */
6589 validator: { 4785 validator: {
6590 type: String 4786 type: String
6591 }, 4787 },
6592 4788
6593 /**
6594 * True if the last call to `validate` is invalid.
6595 */
6596 invalid: { 4789 invalid: {
6597 notify: true, 4790 notify: true,
6598 reflectToAttribute: true, 4791 reflectToAttribute: true,
6599 type: Boolean, 4792 type: Boolean,
6600 value: false 4793 value: false
6601 }, 4794 },
6602 4795
6603 /**
6604 * This property is deprecated and should not be used. Use the global
6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
6606 */
6607 _validatorMeta: { 4796 _validatorMeta: {
6608 type: Object 4797 type: Object
6609 }, 4798 },
6610 4799
6611 /**
6612 * Namespace for this validator. This property is deprecated and should
6613 * not be used. For all intents and purposes, please consider it a
6614 * read-only, config-time property.
6615 */
6616 validatorType: { 4800 validatorType: {
6617 type: String, 4801 type: String,
6618 value: 'validator' 4802 value: 'validator'
6619 }, 4803 },
6620 4804
6621 _validator: { 4805 _validator: {
6622 type: Object, 4806 type: Object,
6623 computed: '__computeValidator(validator)' 4807 computed: '__computeValidator(validator)'
6624 } 4808 }
6625 }, 4809 },
6626 4810
6627 observers: [ 4811 observers: [
6628 '_invalidChanged(invalid)' 4812 '_invalidChanged(invalid)'
6629 ], 4813 ],
6630 4814
6631 registered: function() { 4815 registered: function() {
6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'}); 4816 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
6633 }, 4817 },
6634 4818
6635 _invalidChanged: function() { 4819 _invalidChanged: function() {
6636 if (this.invalid) { 4820 if (this.invalid) {
6637 this.setAttribute('aria-invalid', 'true'); 4821 this.setAttribute('aria-invalid', 'true');
6638 } else { 4822 } else {
6639 this.removeAttribute('aria-invalid'); 4823 this.removeAttribute('aria-invalid');
6640 } 4824 }
6641 }, 4825 },
6642 4826
6643 /**
6644 * @return {boolean} True if the validator `validator` exists.
6645 */
6646 hasValidator: function() { 4827 hasValidator: function() {
6647 return this._validator != null; 4828 return this._validator != null;
6648 }, 4829 },
6649 4830
6650 /**
6651 * Returns true if the `value` is valid, and updates `invalid`. If you want
6652 * your element to have custom validation logic, do not override this method ;
6653 * override `_getValidity(value)` instead.
6654
6655 * @param {Object} value The value to be validated. By default, it is passed
6656 * to the validator's `validate()` function, if a validator is set.
6657 * @return {boolean} True if `value` is valid.
6658 */
6659 validate: function(value) { 4831 validate: function(value) {
6660 this.invalid = !this._getValidity(value); 4832 this.invalid = !this._getValidity(value);
6661 return !this.invalid; 4833 return !this.invalid;
6662 }, 4834 },
6663 4835
6664 /**
6665 * Returns true if `value` is valid. By default, it is passed
6666 * to the validator's `validate()` function, if a validator is set. You
6667 * should override this method if you want to implement custom validity
6668 * logic for your element.
6669 *
6670 * @param {Object} value The value to be validated.
6671 * @return {boolean} True if `value` is valid.
6672 */
6673 4836
6674 _getValidity: function(value) { 4837 _getValidity: function(value) {
6675 if (this.hasValidator()) { 4838 if (this.hasValidator()) {
6676 return this._validator.validate(value); 4839 return this._validator.validate(value);
6677 } 4840 }
6678 return true; 4841 return true;
6679 }, 4842 },
6680 4843
6681 __computeValidator: function() { 4844 __computeValidator: function() {
6682 return Polymer.IronValidatableBehaviorMeta && 4845 return Polymer.IronValidatableBehaviorMeta &&
6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); 4846 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
6684 } 4847 }
6685 }; 4848 };
6686 /*
6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
6688 to `<input>`.
6689
6690 ### Two-way binding
6691
6692 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
6693
6694 <input value="{{myValue::input}}">
6695
6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
6697 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
6698
6699 <input is="iron-input" bind-value="{{myValue}}">
6700
6701 ### Custom validators
6702
6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
6704
6705 <input is="iron-input" validator="my-custom-validator">
6706
6707 ### Stopping invalid input
6708
6709 It may be desirable to only allow users to enter certain characters. You can use the
6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
6711 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
6712
6713 \x3c!-- only allow characters that match [0-9] --\x3e
6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
6715
6716 @hero hero.svg
6717 @demo demo/index.html
6718 */
6719 4849
6720 Polymer({ 4850 Polymer({
6721 4851
6722 is: 'iron-input', 4852 is: 'iron-input',
6723 4853
6724 extends: 'input', 4854 extends: 'input',
6725 4855
6726 behaviors: [ 4856 behaviors: [
6727 Polymer.IronValidatableBehavior 4857 Polymer.IronValidatableBehavior
6728 ], 4858 ],
6729 4859
6730 properties: { 4860 properties: {
6731 4861
6732 /**
6733 * Use this property instead of `value` for two-way data binding.
6734 */
6735 bindValue: { 4862 bindValue: {
6736 observer: '_bindValueChanged', 4863 observer: '_bindValueChanged',
6737 type: String 4864 type: String
6738 }, 4865 },
6739 4866
6740 /**
6741 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
6742 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
6743 * Pasted input will have each character checked individually; if any char acter
6744 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
6745 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
6746 */
6747 preventInvalidInput: { 4867 preventInvalidInput: {
6748 type: Boolean 4868 type: Boolean
6749 }, 4869 },
6750 4870
6751 /**
6752 * Regular expression that list the characters allowed as input.
6753 * This pattern represents the allowed characters for the field; as the us er inputs text,
6754 * each individual character will be checked against the pattern (rather t han checking
6755 * the entire value as a whole). The recommended format should be a list o f allowed characters;
6756 * for example, `[a-zA-Z0-9.+-!;:]`
6757 */
6758 allowedPattern: { 4871 allowedPattern: {
6759 type: String, 4872 type: String,
6760 observer: "_allowedPatternChanged" 4873 observer: "_allowedPatternChanged"
6761 }, 4874 },
6762 4875
6763 _previousValidInput: { 4876 _previousValidInput: {
6764 type: String, 4877 type: String,
6765 value: '' 4878 value: ''
6766 }, 4879 },
6767 4880
6768 _patternAlreadyChecked: { 4881 _patternAlreadyChecked: {
6769 type: Boolean, 4882 type: Boolean,
6770 value: false 4883 value: false
6771 } 4884 }
6772 4885
6773 }, 4886 },
6774 4887
6775 listeners: { 4888 listeners: {
6776 'input': '_onInput', 4889 'input': '_onInput',
6777 'keypress': '_onKeypress' 4890 'keypress': '_onKeypress'
6778 }, 4891 },
6779 4892
6780 /** @suppress {checkTypes} */ 4893 /** @suppress {checkTypes} */
6781 registered: function() { 4894 registered: function() {
6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
6783 if (!this._canDispatchEventOnDisabled()) { 4895 if (!this._canDispatchEventOnDisabled()) {
6784 this._origDispatchEvent = this.dispatchEvent; 4896 this._origDispatchEvent = this.dispatchEvent;
6785 this.dispatchEvent = this._dispatchEventFirefoxIE; 4897 this.dispatchEvent = this._dispatchEventFirefoxIE;
6786 } 4898 }
6787 }, 4899 },
6788 4900
6789 created: function() { 4901 created: function() {
6790 Polymer.IronA11yAnnouncer.requestAvailability(); 4902 Polymer.IronA11yAnnouncer.requestAvailability();
6791 }, 4903 },
6792 4904
6793 _canDispatchEventOnDisabled: function() { 4905 _canDispatchEventOnDisabled: function() {
6794 var input = document.createElement('input'); 4906 var input = document.createElement('input');
6795 var canDispatch = false; 4907 var canDispatch = false;
6796 input.disabled = true; 4908 input.disabled = true;
6797 4909
6798 input.addEventListener('feature-check-dispatch-event', function() { 4910 input.addEventListener('feature-check-dispatch-event', function() {
6799 canDispatch = true; 4911 canDispatch = true;
6800 }); 4912 });
6801 4913
6802 try { 4914 try {
6803 input.dispatchEvent(new Event('feature-check-dispatch-event')); 4915 input.dispatchEvent(new Event('feature-check-dispatch-event'));
6804 } catch(e) {} 4916 } catch(e) {}
6805 4917
6806 return canDispatch; 4918 return canDispatch;
6807 }, 4919 },
6808 4920
6809 _dispatchEventFirefoxIE: function() { 4921 _dispatchEventFirefoxIE: function() {
6810 // Due to Firefox bug, events fired on disabled form controls can throw
6811 // errors; furthermore, neither IE nor Firefox will actually dispatch
6812 // events from disabled form controls; as such, we toggle disable around
6813 // the dispatch to allow notifying properties to notify
6814 // See issue #47 for details
6815 var disabled = this.disabled; 4922 var disabled = this.disabled;
6816 this.disabled = false; 4923 this.disabled = false;
6817 this._origDispatchEvent.apply(this, arguments); 4924 this._origDispatchEvent.apply(this, arguments);
6818 this.disabled = disabled; 4925 this.disabled = disabled;
6819 }, 4926 },
6820 4927
6821 get _patternRegExp() { 4928 get _patternRegExp() {
6822 var pattern; 4929 var pattern;
6823 if (this.allowedPattern) { 4930 if (this.allowedPattern) {
6824 pattern = new RegExp(this.allowedPattern); 4931 pattern = new RegExp(this.allowedPattern);
6825 } else { 4932 } else {
6826 switch (this.type) { 4933 switch (this.type) {
6827 case 'number': 4934 case 'number':
6828 pattern = /[0-9.,e-]/; 4935 pattern = /[0-9.,e-]/;
6829 break; 4936 break;
6830 } 4937 }
6831 } 4938 }
6832 return pattern; 4939 return pattern;
6833 }, 4940 },
6834 4941
6835 ready: function() { 4942 ready: function() {
6836 this.bindValue = this.value; 4943 this.bindValue = this.value;
6837 }, 4944 },
6838 4945
6839 /**
6840 * @suppress {checkTypes}
6841 */
6842 _bindValueChanged: function() { 4946 _bindValueChanged: function() {
6843 if (this.value !== this.bindValue) { 4947 if (this.value !== this.bindValue) {
6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue; 4948 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
6845 } 4949 }
6846 // manually notify because we don't want to notify until after setting val ue
6847 this.fire('bind-value-changed', {value: this.bindValue}); 4950 this.fire('bind-value-changed', {value: this.bindValue});
6848 }, 4951 },
6849 4952
6850 _allowedPatternChanged: function() { 4953 _allowedPatternChanged: function() {
6851 // Force to prevent invalid input when an `allowed-pattern` is set
6852 this.preventInvalidInput = this.allowedPattern ? true : false; 4954 this.preventInvalidInput = this.allowedPattern ? true : false;
6853 }, 4955 },
6854 4956
6855 _onInput: function() { 4957 _onInput: function() {
6856 // Need to validate each of the characters pasted if they haven't
6857 // been validated inside `_onKeypress` already.
6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) { 4958 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
6859 var valid = this._checkPatternValidity(); 4959 var valid = this._checkPatternValidity();
6860 if (!valid) { 4960 if (!valid) {
6861 this._announceInvalidCharacter('Invalid string of characters not enter ed.'); 4961 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
6862 this.value = this._previousValidInput; 4962 this.value = this._previousValidInput;
6863 } 4963 }
6864 } 4964 }
6865 4965
6866 this.bindValue = this.value; 4966 this.bindValue = this.value;
6867 this._previousValidInput = this.value; 4967 this._previousValidInput = this.value;
6868 this._patternAlreadyChecked = false; 4968 this._patternAlreadyChecked = false;
6869 }, 4969 },
6870 4970
6871 _isPrintable: function(event) { 4971 _isPrintable: function(event) {
6872 // What a control/printable character is varies wildly based on the browse r.
6873 // - most control characters (arrows, backspace) do not send a `keypress` event
6874 // in Chrome, but the *do* on Firefox
6875 // - in Firefox, when they do send a `keypress` event, control chars have
6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
6877 // - printable characters always send a keypress event.
6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
6879 // always matches the charCode.
6880 // None of this makes any sense.
6881 4972
6882 // For these keys, ASCII code == browser keycode.
6883 var anyNonPrintable = 4973 var anyNonPrintable =
6884 (event.keyCode == 8) || // backspace 4974 (event.keyCode == 8) || // backspace
6885 (event.keyCode == 9) || // tab 4975 (event.keyCode == 9) || // tab
6886 (event.keyCode == 13) || // enter 4976 (event.keyCode == 13) || // enter
6887 (event.keyCode == 27); // escape 4977 (event.keyCode == 27); // escape
6888 4978
6889 // For these keys, make sure it's a browser keycode and not an ASCII code.
6890 var mozNonPrintable = 4979 var mozNonPrintable =
6891 (event.keyCode == 19) || // pause 4980 (event.keyCode == 19) || // pause
6892 (event.keyCode == 20) || // caps lock 4981 (event.keyCode == 20) || // caps lock
6893 (event.keyCode == 45) || // insert 4982 (event.keyCode == 45) || // insert
6894 (event.keyCode == 46) || // delete 4983 (event.keyCode == 46) || // delete
6895 (event.keyCode == 144) || // num lock 4984 (event.keyCode == 144) || // num lock
6896 (event.keyCode == 145) || // scroll lock 4985 (event.keyCode == 145) || // scroll lock
6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows 4986 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys 4987 (event.keyCode > 111 && event.keyCode < 124); // fn keys
6899 4988
6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); 4989 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
6901 }, 4990 },
6902 4991
6903 _onKeypress: function(event) { 4992 _onKeypress: function(event) {
6904 if (!this.preventInvalidInput && this.type !== 'number') { 4993 if (!this.preventInvalidInput && this.type !== 'number') {
6905 return; 4994 return;
6906 } 4995 }
6907 var regexp = this._patternRegExp; 4996 var regexp = this._patternRegExp;
6908 if (!regexp) { 4997 if (!regexp) {
6909 return; 4998 return;
6910 } 4999 }
6911 5000
6912 // Handle special keys and backspace
6913 if (event.metaKey || event.ctrlKey || event.altKey) 5001 if (event.metaKey || event.ctrlKey || event.altKey)
6914 return; 5002 return;
6915 5003
6916 // Check the pattern either here or in `_onInput`, but not in both.
6917 this._patternAlreadyChecked = true; 5004 this._patternAlreadyChecked = true;
6918 5005
6919 var thisChar = String.fromCharCode(event.charCode); 5006 var thisChar = String.fromCharCode(event.charCode);
6920 if (this._isPrintable(event) && !regexp.test(thisChar)) { 5007 if (this._isPrintable(event) && !regexp.test(thisChar)) {
6921 event.preventDefault(); 5008 event.preventDefault();
6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.'); 5009 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
6923 } 5010 }
6924 }, 5011 },
6925 5012
6926 _checkPatternValidity: function() { 5013 _checkPatternValidity: function() {
6927 var regexp = this._patternRegExp; 5014 var regexp = this._patternRegExp;
6928 if (!regexp) { 5015 if (!regexp) {
6929 return true; 5016 return true;
6930 } 5017 }
6931 for (var i = 0; i < this.value.length; i++) { 5018 for (var i = 0; i < this.value.length; i++) {
6932 if (!regexp.test(this.value[i])) { 5019 if (!regexp.test(this.value[i])) {
6933 return false; 5020 return false;
6934 } 5021 }
6935 } 5022 }
6936 return true; 5023 return true;
6937 }, 5024 },
6938 5025
6939 /**
6940 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
6941 * then any constraints.
6942 * @return {boolean} True if the value is valid.
6943 */
6944 validate: function() { 5026 validate: function() {
6945 // First, check what the browser thinks. Some inputs (like type=number)
6946 // behave weirdly and will set the value to "" if something invalid is
6947 // entered, but will set the validity correctly.
6948 var valid = this.checkValidity(); 5027 var valid = this.checkValidity();
6949 5028
6950 // Only do extra checking if the browser thought this was valid.
6951 if (valid) { 5029 if (valid) {
6952 // Empty, required input is invalid
6953 if (this.required && this.value === '') { 5030 if (this.required && this.value === '') {
6954 valid = false; 5031 valid = false;
6955 } else if (this.hasValidator()) { 5032 } else if (this.hasValidator()) {
6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value ); 5033 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
6957 } 5034 }
6958 } 5035 }
6959 5036
6960 this.invalid = !valid; 5037 this.invalid = !valid;
6961 this.fire('iron-input-validate'); 5038 this.fire('iron-input-validate');
6962 return valid; 5039 return valid;
6963 }, 5040 },
6964 5041
6965 _announceInvalidCharacter: function(message) { 5042 _announceInvalidCharacter: function(message) {
6966 this.fire('iron-announce', { text: message }); 5043 this.fire('iron-announce', { text: message });
6967 } 5044 }
6968 }); 5045 });
6969 5046
6970 /*
6971 The `iron-input-validate` event is fired whenever `validate()` is called.
6972 @event iron-input-validate
6973 */
6974 Polymer({ 5047 Polymer({
6975 is: 'paper-input-container', 5048 is: 'paper-input-container',
6976 5049
6977 properties: { 5050 properties: {
6978 /**
6979 * Set to true to disable the floating label. The label disappears when th e input value is
6980 * not null.
6981 */
6982 noLabelFloat: { 5051 noLabelFloat: {
6983 type: Boolean, 5052 type: Boolean,
6984 value: false 5053 value: false
6985 }, 5054 },
6986 5055
6987 /**
6988 * Set to true to always float the floating label.
6989 */
6990 alwaysFloatLabel: { 5056 alwaysFloatLabel: {
6991 type: Boolean, 5057 type: Boolean,
6992 value: false 5058 value: false
6993 }, 5059 },
6994 5060
6995 /**
6996 * The attribute to listen for value changes on.
6997 */
6998 attrForValue: { 5061 attrForValue: {
6999 type: String, 5062 type: String,
7000 value: 'bind-value' 5063 value: 'bind-value'
7001 }, 5064 },
7002 5065
7003 /**
7004 * Set to true to auto-validate the input value when it changes.
7005 */
7006 autoValidate: { 5066 autoValidate: {
7007 type: Boolean, 5067 type: Boolean,
7008 value: false 5068 value: false
7009 }, 5069 },
7010 5070
7011 /**
7012 * True if the input is invalid. This property is set automatically when t he input value
7013 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
7014 */
7015 invalid: { 5071 invalid: {
7016 observer: '_invalidChanged', 5072 observer: '_invalidChanged',
7017 type: Boolean, 5073 type: Boolean,
7018 value: false 5074 value: false
7019 }, 5075 },
7020 5076
7021 /**
7022 * True if the input has focus.
7023 */
7024 focused: { 5077 focused: {
7025 readOnly: true, 5078 readOnly: true,
7026 type: Boolean, 5079 type: Boolean,
7027 value: false, 5080 value: false,
7028 notify: true 5081 notify: true
7029 }, 5082 },
7030 5083
7031 _addons: { 5084 _addons: {
7032 type: Array 5085 type: Array
7033 // do not set a default value here intentionally - it will be initialize d lazily when a
7034 // distributed child is attached, which may occur before configuration f or this element
7035 // in polyfill.
7036 }, 5086 },
7037 5087
7038 _inputHasContent: { 5088 _inputHasContent: {
7039 type: Boolean, 5089 type: Boolean,
7040 value: false 5090 value: false
7041 }, 5091 },
7042 5092
7043 _inputSelector: { 5093 _inputSelector: {
7044 type: String, 5094 type: String,
7045 value: 'input,textarea,.paper-input-input' 5095 value: 'input,textarea,.paper-input-input'
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
7103 this.addEventListener('blur', this._boundOnBlur, true); 5153 this.addEventListener('blur', this._boundOnBlur, true);
7104 }, 5154 },
7105 5155
7106 attached: function() { 5156 attached: function() {
7107 if (this.attrForValue) { 5157 if (this.attrForValue) {
7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged); 5158 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
7109 } else { 5159 } else {
7110 this.addEventListener('input', this._onInput); 5160 this.addEventListener('input', this._onInput);
7111 } 5161 }
7112 5162
7113 // Only validate when attached if the input already has a value.
7114 if (this._inputElementValue != '') { 5163 if (this._inputElementValue != '') {
7115 this._handleValueAndAutoValidate(this._inputElement); 5164 this._handleValueAndAutoValidate(this._inputElement);
7116 } else { 5165 } else {
7117 this._handleValue(this._inputElement); 5166 this._handleValue(this._inputElement);
7118 } 5167 }
7119 }, 5168 },
7120 5169
7121 _onAddonAttached: function(event) { 5170 _onAddonAttached: function(event) {
7122 if (!this._addons) { 5171 if (!this._addons) {
7123 this._addons = []; 5172 this._addons = [];
(...skipping 20 matching lines...) Expand all
7144 this._handleValueAndAutoValidate(event.target); 5193 this._handleValueAndAutoValidate(event.target);
7145 }, 5194 },
7146 5195
7147 _onValueChanged: function(event) { 5196 _onValueChanged: function(event) {
7148 this._handleValueAndAutoValidate(event.target); 5197 this._handleValueAndAutoValidate(event.target);
7149 }, 5198 },
7150 5199
7151 _handleValue: function(inputElement) { 5200 _handleValue: function(inputElement) {
7152 var value = this._inputElementValue; 5201 var value = this._inputElementValue;
7153 5202
7154 // type="number" hack needed because this.value is empty until it's valid
7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) { 5203 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
7156 this._inputHasContent = true; 5204 this._inputHasContent = true;
7157 } else { 5205 } else {
7158 this._inputHasContent = false; 5206 this._inputHasContent = false;
7159 } 5207 }
7160 5208
7161 this.updateAddons({ 5209 this.updateAddons({
7162 inputElement: inputElement, 5210 inputElement: inputElement,
7163 value: value, 5211 value: value,
7164 invalid: this.invalid 5212 invalid: this.invalid
7165 }); 5213 });
7166 }, 5214 },
7167 5215
7168 _handleValueAndAutoValidate: function(inputElement) { 5216 _handleValueAndAutoValidate: function(inputElement) {
7169 if (this.autoValidate) { 5217 if (this.autoValidate) {
7170 var valid; 5218 var valid;
7171 if (inputElement.validate) { 5219 if (inputElement.validate) {
7172 valid = inputElement.validate(this._inputElementValue); 5220 valid = inputElement.validate(this._inputElementValue);
7173 } else { 5221 } else {
7174 valid = inputElement.checkValidity(); 5222 valid = inputElement.checkValidity();
7175 } 5223 }
7176 this.invalid = !valid; 5224 this.invalid = !valid;
7177 } 5225 }
7178 5226
7179 // Call this last to notify the add-ons.
7180 this._handleValue(inputElement); 5227 this._handleValue(inputElement);
7181 }, 5228 },
7182 5229
7183 _onIronInputValidate: function(event) { 5230 _onIronInputValidate: function(event) {
7184 this.invalid = this._inputElement.invalid; 5231 this.invalid = this._inputElement.invalid;
7185 }, 5232 },
7186 5233
7187 _invalidChanged: function() { 5234 _invalidChanged: function() {
7188 if (this._addons) { 5235 if (this._addons) {
7189 this.updateAddons({invalid: this.invalid}); 5236 this.updateAddons({invalid: this.invalid});
7190 } 5237 }
7191 }, 5238 },
7192 5239
7193 /**
7194 * Call this to update the state of add-ons.
7195 * @param {Object} state Add-on state.
7196 */
7197 updateAddons: function(state) { 5240 updateAddons: function(state) {
7198 for (var addon, index = 0; addon = this._addons[index]; index++) { 5241 for (var addon, index = 0; addon = this._addons[index]; index++) {
7199 addon.update(state); 5242 addon.update(state);
7200 } 5243 }
7201 }, 5244 },
7202 5245
7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) { 5246 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
7204 var cls = 'input-content'; 5247 var cls = 'input-content';
7205 if (!noLabelFloat) { 5248 if (!noLabelFloat) {
7206 var label = this.querySelector('label'); 5249 var label = this.querySelector('label');
7207 5250
7208 if (alwaysFloatLabel || _inputHasContent) { 5251 if (alwaysFloatLabel || _inputHasContent) {
7209 cls += ' label-is-floating'; 5252 cls += ' label-is-floating';
7210 // If the label is floating, ignore any offsets that may have been
7211 // applied from a prefix element.
7212 this.$.labelAndInputContainer.style.position = 'static'; 5253 this.$.labelAndInputContainer.style.position = 'static';
7213 5254
7214 if (invalid) { 5255 if (invalid) {
7215 cls += ' is-invalid'; 5256 cls += ' is-invalid';
7216 } else if (focused) { 5257 } else if (focused) {
7217 cls += " label-is-highlighted"; 5258 cls += " label-is-highlighted";
7218 } 5259 }
7219 } else { 5260 } else {
7220 // When the label is not floating, it should overlap the input element .
7221 if (label) { 5261 if (label) {
7222 this.$.labelAndInputContainer.style.position = 'relative'; 5262 this.$.labelAndInputContainer.style.position = 'relative';
7223 } 5263 }
7224 } 5264 }
7225 } else { 5265 } else {
7226 if (_inputHasContent) { 5266 if (_inputHasContent) {
7227 cls += ' label-is-hidden'; 5267 cls += ' label-is-hidden';
7228 } 5268 }
7229 } 5269 }
7230 return cls; 5270 return cls;
(...skipping 21 matching lines...) Expand all
7252 }); 5292 });
7253 /** @polymerBehavior */ 5293 /** @polymerBehavior */
7254 Polymer.PaperSpinnerBehavior = { 5294 Polymer.PaperSpinnerBehavior = {
7255 5295
7256 listeners: { 5296 listeners: {
7257 'animationend': '__reset', 5297 'animationend': '__reset',
7258 'webkitAnimationEnd': '__reset' 5298 'webkitAnimationEnd': '__reset'
7259 }, 5299 },
7260 5300
7261 properties: { 5301 properties: {
7262 /**
7263 * Displays the spinner.
7264 */
7265 active: { 5302 active: {
7266 type: Boolean, 5303 type: Boolean,
7267 value: false, 5304 value: false,
7268 reflectToAttribute: true, 5305 reflectToAttribute: true,
7269 observer: '__activeChanged' 5306 observer: '__activeChanged'
7270 }, 5307 },
7271 5308
7272 /**
7273 * Alternative text content for accessibility support.
7274 * If alt is present, it will add an aria-label whose content matches alt when active.
7275 * If alt is not present, it will default to 'loading' as the alt value.
7276 */
7277 alt: { 5309 alt: {
7278 type: String, 5310 type: String,
7279 value: 'loading', 5311 value: 'loading',
7280 observer: '__altChanged' 5312 observer: '__altChanged'
7281 }, 5313 },
7282 5314
7283 __coolingDown: { 5315 __coolingDown: {
7284 type: Boolean, 5316 type: Boolean,
7285 value: false 5317 value: false
7286 } 5318 }
7287 }, 5319 },
7288 5320
7289 __computeContainerClasses: function(active, coolingDown) { 5321 __computeContainerClasses: function(active, coolingDown) {
7290 return [ 5322 return [
7291 active || coolingDown ? 'active' : '', 5323 active || coolingDown ? 'active' : '',
7292 coolingDown ? 'cooldown' : '' 5324 coolingDown ? 'cooldown' : ''
7293 ].join(' '); 5325 ].join(' ');
7294 }, 5326 },
7295 5327
7296 __activeChanged: function(active, old) { 5328 __activeChanged: function(active, old) {
7297 this.__setAriaHidden(!active); 5329 this.__setAriaHidden(!active);
7298 this.__coolingDown = !active && old; 5330 this.__coolingDown = !active && old;
7299 }, 5331 },
7300 5332
7301 __altChanged: function(alt) { 5333 __altChanged: function(alt) {
7302 // user-provided `aria-label` takes precedence over prototype default
7303 if (alt === this.getPropertyInfo('alt').value) { 5334 if (alt === this.getPropertyInfo('alt').value) {
7304 this.alt = this.getAttribute('aria-label') || alt; 5335 this.alt = this.getAttribute('aria-label') || alt;
7305 } else { 5336 } else {
7306 this.__setAriaHidden(alt===''); 5337 this.__setAriaHidden(alt==='');
7307 this.setAttribute('aria-label', alt); 5338 this.setAttribute('aria-label', alt);
7308 } 5339 }
7309 }, 5340 },
7310 5341
7311 __setAriaHidden: function(hidden) { 5342 __setAriaHidden: function(hidden) {
7312 var attr = 'aria-hidden'; 5343 var attr = 'aria-hidden';
(...skipping 13 matching lines...) Expand all
7326 is: 'paper-spinner-lite', 5357 is: 'paper-spinner-lite',
7327 5358
7328 behaviors: [ 5359 behaviors: [
7329 Polymer.PaperSpinnerBehavior 5360 Polymer.PaperSpinnerBehavior
7330 ] 5361 ]
7331 }); 5362 });
7332 // Copyright 2016 The Chromium Authors. All rights reserved. 5363 // Copyright 2016 The Chromium Authors. All rights reserved.
7333 // Use of this source code is governed by a BSD-style license that can be 5364 // Use of this source code is governed by a BSD-style license that can be
7334 // found in the LICENSE file. 5365 // found in the LICENSE file.
7335 5366
7336 /**
7337 * Implements an incremental search field which can be shown and hidden.
7338 * Canonical implementation is <cr-search-field>.
7339 * @polymerBehavior
7340 */
7341 var CrSearchFieldBehavior = { 5367 var CrSearchFieldBehavior = {
7342 properties: { 5368 properties: {
7343 label: { 5369 label: {
7344 type: String, 5370 type: String,
7345 value: '', 5371 value: '',
7346 }, 5372 },
7347 5373
7348 clearLabel: { 5374 clearLabel: {
7349 type: String, 5375 type: String,
7350 value: '', 5376 value: '',
7351 }, 5377 },
7352 5378
7353 showingSearch: { 5379 showingSearch: {
7354 type: Boolean, 5380 type: Boolean,
7355 value: false, 5381 value: false,
7356 notify: true, 5382 notify: true,
7357 observer: 'showingSearchChanged_', 5383 observer: 'showingSearchChanged_',
7358 reflectToAttribute: true 5384 reflectToAttribute: true
7359 }, 5385 },
7360 5386
7361 /** @private */ 5387 /** @private */
7362 lastValue_: { 5388 lastValue_: {
7363 type: String, 5389 type: String,
7364 value: '', 5390 value: '',
7365 }, 5391 },
7366 }, 5392 },
7367 5393
7368 /**
7369 * @abstract
7370 * @return {!HTMLInputElement} The input field element the behavior should
7371 * use.
7372 */
7373 getSearchInput: function() {}, 5394 getSearchInput: function() {},
7374 5395
7375 /**
7376 * @return {string} The value of the search field.
7377 */
7378 getValue: function() { 5396 getValue: function() {
7379 return this.getSearchInput().value; 5397 return this.getSearchInput().value;
7380 }, 5398 },
7381 5399
7382 /**
7383 * Sets the value of the search field.
7384 * @param {string} value
7385 */
7386 setValue: function(value) { 5400 setValue: function(value) {
7387 // Use bindValue when setting the input value so that changes propagate
7388 // correctly.
7389 this.getSearchInput().bindValue = value; 5401 this.getSearchInput().bindValue = value;
7390 this.onValueChanged_(value); 5402 this.onValueChanged_(value);
7391 }, 5403 },
7392 5404
7393 showAndFocus: function() { 5405 showAndFocus: function() {
7394 this.showingSearch = true; 5406 this.showingSearch = true;
7395 this.focus_(); 5407 this.focus_();
7396 }, 5408 },
7397 5409
7398 /** @private */ 5410 /** @private */
7399 focus_: function() { 5411 focus_: function() {
7400 this.getSearchInput().focus(); 5412 this.getSearchInput().focus();
7401 }, 5413 },
7402 5414
7403 onSearchTermSearch: function() { 5415 onSearchTermSearch: function() {
7404 this.onValueChanged_(this.getValue()); 5416 this.onValueChanged_(this.getValue());
7405 }, 5417 },
7406 5418
7407 /**
7408 * Updates the internal state of the search field based on a change that has
7409 * already happened.
7410 * @param {string} newValue
7411 * @private
7412 */
7413 onValueChanged_: function(newValue) { 5419 onValueChanged_: function(newValue) {
7414 if (newValue == this.lastValue_) 5420 if (newValue == this.lastValue_)
7415 return; 5421 return;
7416 5422
7417 this.fire('search-changed', newValue); 5423 this.fire('search-changed', newValue);
7418 this.lastValue_ = newValue; 5424 this.lastValue_ = newValue;
7419 }, 5425 },
7420 5426
7421 onSearchTermKeydown: function(e) { 5427 onSearchTermKeydown: function(e) {
7422 if (e.key == 'Escape') 5428 if (e.key == 'Escape')
7423 this.showingSearch = false; 5429 this.showingSearch = false;
7424 }, 5430 },
7425 5431
7426 /** @private */ 5432 /** @private */
7427 showingSearchChanged_: function() { 5433 showingSearchChanged_: function() {
7428 if (this.showingSearch) { 5434 if (this.showingSearch) {
7429 this.focus_(); 5435 this.focus_();
7430 return; 5436 return;
7431 } 5437 }
7432 5438
7433 this.setValue(''); 5439 this.setValue('');
7434 this.getSearchInput().blur(); 5440 this.getSearchInput().blur();
7435 } 5441 }
7436 }; 5442 };
7437 // Copyright 2016 The Chromium Authors. All rights reserved. 5443 // Copyright 2016 The Chromium Authors. All rights reserved.
7438 // Use of this source code is governed by a BSD-style license that can be 5444 // Use of this source code is governed by a BSD-style license that can be
7439 // found in the LICENSE file. 5445 // found in the LICENSE file.
7440 5446
7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field.
7442 Polymer({ 5447 Polymer({
7443 is: 'cr-toolbar-search-field', 5448 is: 'cr-toolbar-search-field',
7444 5449
7445 behaviors: [CrSearchFieldBehavior], 5450 behaviors: [CrSearchFieldBehavior],
7446 5451
7447 properties: { 5452 properties: {
7448 narrow: { 5453 narrow: {
7449 type: Boolean, 5454 type: Boolean,
7450 reflectToAttribute: true, 5455 reflectToAttribute: true,
7451 }, 5456 },
7452 5457
7453 // Prompt text to display in the search field.
7454 label: String, 5458 label: String,
7455 5459
7456 // Tooltip to display on the clear search button.
7457 clearLabel: String, 5460 clearLabel: String,
7458 5461
7459 // When true, show a loading spinner to indicate that the backend is
7460 // processing the search. Will only show if the search field is open.
7461 spinnerActive: { 5462 spinnerActive: {
7462 type: Boolean, 5463 type: Boolean,
7463 reflectToAttribute: true 5464 reflectToAttribute: true
7464 }, 5465 },
7465 5466
7466 /** @private */ 5467 /** @private */
7467 hasSearchText_: Boolean, 5468 hasSearchText_: Boolean,
7468 }, 5469 },
7469 5470
7470 listeners: { 5471 listeners: {
7471 'tap': 'showSearch_', 5472 'tap': 'showSearch_',
7472 'searchInput.bind-value-changed': 'onBindValueChanged_', 5473 'searchInput.bind-value-changed': 'onBindValueChanged_',
7473 }, 5474 },
7474 5475
7475 /** @return {!HTMLInputElement} */ 5476 /** @return {!HTMLInputElement} */
7476 getSearchInput: function() { 5477 getSearchInput: function() {
7477 return this.$.searchInput; 5478 return this.$.searchInput;
7478 }, 5479 },
7479 5480
7480 /** @return {boolean} */ 5481 /** @return {boolean} */
7481 isSearchFocused: function() { 5482 isSearchFocused: function() {
7482 return this.$.searchTerm.focused; 5483 return this.$.searchTerm.focused;
7483 }, 5484 },
7484 5485
7485 /**
7486 * @param {boolean} narrow
7487 * @return {number}
7488 * @private
7489 */
7490 computeIconTabIndex_: function(narrow) { 5486 computeIconTabIndex_: function(narrow) {
7491 return narrow ? 0 : -1; 5487 return narrow ? 0 : -1;
7492 }, 5488 },
7493 5489
7494 /**
7495 * @param {boolean} spinnerActive
7496 * @param {boolean} showingSearch
7497 * @return {boolean}
7498 * @private
7499 */
7500 isSpinnerShown_: function(spinnerActive, showingSearch) { 5490 isSpinnerShown_: function(spinnerActive, showingSearch) {
7501 return spinnerActive && showingSearch; 5491 return spinnerActive && showingSearch;
7502 }, 5492 },
7503 5493
7504 /** @private */ 5494 /** @private */
7505 onInputBlur_: function() { 5495 onInputBlur_: function() {
7506 if (!this.hasSearchText_) 5496 if (!this.hasSearchText_)
7507 this.showingSearch = false; 5497 this.showingSearch = false;
7508 }, 5498 },
7509 5499
7510 /**
7511 * Update the state of the search field whenever the underlying input value
7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately
7513 * after any change, whether the result of user input or JS modification.
7514 * @private
7515 */
7516 onBindValueChanged_: function() { 5500 onBindValueChanged_: function() {
7517 var newValue = this.$.searchInput.bindValue; 5501 var newValue = this.$.searchInput.bindValue;
7518 this.hasSearchText_ = newValue != ''; 5502 this.hasSearchText_ = newValue != '';
7519 if (newValue != '') 5503 if (newValue != '')
7520 this.showingSearch = true; 5504 this.showingSearch = true;
7521 }, 5505 },
7522 5506
7523 /**
7524 * @param {Event} e
7525 * @private
7526 */
7527 showSearch_: function(e) { 5507 showSearch_: function(e) {
7528 if (e.target != this.$.clearSearch) 5508 if (e.target != this.$.clearSearch)
7529 this.showingSearch = true; 5509 this.showingSearch = true;
7530 }, 5510 },
7531 5511
7532 /**
7533 * @param {Event} e
7534 * @private
7535 */
7536 hideSearch_: function(e) { 5512 hideSearch_: function(e) {
7537 this.showingSearch = false; 5513 this.showingSearch = false;
7538 e.stopPropagation(); 5514 e.stopPropagation();
7539 } 5515 }
7540 }); 5516 });
7541 // Copyright 2016 The Chromium Authors. All rights reserved. 5517 // Copyright 2016 The Chromium Authors. All rights reserved.
7542 // Use of this source code is governed by a BSD-style license that can be 5518 // Use of this source code is governed by a BSD-style license that can be
7543 // found in the LICENSE file. 5519 // found in the LICENSE file.
7544 5520
7545 Polymer({ 5521 Polymer({
7546 is: 'cr-toolbar', 5522 is: 'cr-toolbar',
7547 5523
7548 properties: { 5524 properties: {
7549 // Name to display in the toolbar, in titlecase.
7550 pageName: String, 5525 pageName: String,
7551 5526
7552 // Prompt text to display in the search field.
7553 searchPrompt: String, 5527 searchPrompt: String,
7554 5528
7555 // Tooltip to display on the clear search button.
7556 clearLabel: String, 5529 clearLabel: String,
7557 5530
7558 // Value is proxied through to cr-toolbar-search-field. When true,
7559 // the search field will show a processing spinner.
7560 spinnerActive: Boolean, 5531 spinnerActive: Boolean,
7561 5532
7562 // Controls whether the menu button is shown at the start of the menu.
7563 showMenu: { 5533 showMenu: {
7564 type: Boolean, 5534 type: Boolean,
7565 reflectToAttribute: true, 5535 reflectToAttribute: true,
7566 value: true 5536 value: true
7567 }, 5537 },
7568 5538
7569 /** @private */ 5539 /** @private */
7570 narrow_: { 5540 narrow_: {
7571 type: Boolean, 5541 type: Boolean,
7572 reflectToAttribute: true 5542 reflectToAttribute: true
(...skipping 16 matching lines...) Expand all
7589 this.fire('cr-menu-tap'); 5559 this.fire('cr-menu-tap');
7590 } 5560 }
7591 }); 5561 });
7592 // Copyright 2015 The Chromium Authors. All rights reserved. 5562 // Copyright 2015 The Chromium Authors. All rights reserved.
7593 // Use of this source code is governed by a BSD-style license that can be 5563 // Use of this source code is governed by a BSD-style license that can be
7594 // found in the LICENSE file. 5564 // found in the LICENSE file.
7595 5565
7596 Polymer({ 5566 Polymer({
7597 is: 'history-toolbar', 5567 is: 'history-toolbar',
7598 properties: { 5568 properties: {
7599 // Number of history items currently selected.
7600 // TODO(calamity): bind this to
7601 // listContainer.selectedItem.selectedPaths.length.
7602 count: { 5569 count: {
7603 type: Number, 5570 type: Number,
7604 value: 0, 5571 value: 0,
7605 observer: 'changeToolbarView_' 5572 observer: 'changeToolbarView_'
7606 }, 5573 },
7607 5574
7608 // True if 1 or more history items are selected. When this value changes
7609 // the background colour changes.
7610 itemsSelected_: { 5575 itemsSelected_: {
7611 type: Boolean, 5576 type: Boolean,
7612 value: false, 5577 value: false,
7613 reflectToAttribute: true 5578 reflectToAttribute: true
7614 }, 5579 },
7615 5580
7616 // The most recent term entered in the search field. Updated incrementally
7617 // as the user types.
7618 searchTerm: { 5581 searchTerm: {
7619 type: String, 5582 type: String,
7620 notify: true, 5583 notify: true,
7621 }, 5584 },
7622 5585
7623 // True if the backend is processing and a spinner should be shown in the
7624 // toolbar.
7625 spinnerActive: { 5586 spinnerActive: {
7626 type: Boolean, 5587 type: Boolean,
7627 value: false 5588 value: false
7628 }, 5589 },
7629 5590
7630 hasDrawer: { 5591 hasDrawer: {
7631 type: Boolean, 5592 type: Boolean,
7632 observer: 'hasDrawerChanged_', 5593 observer: 'hasDrawerChanged_',
7633 reflectToAttribute: true, 5594 reflectToAttribute: true,
7634 }, 5595 },
7635 5596
7636 // Whether domain-grouped history is enabled.
7637 isGroupedMode: { 5597 isGroupedMode: {
7638 type: Boolean, 5598 type: Boolean,
7639 reflectToAttribute: true, 5599 reflectToAttribute: true,
7640 }, 5600 },
7641 5601
7642 // The period to search over. Matches BrowsingHistoryHandler::Range.
7643 groupedRange: { 5602 groupedRange: {
7644 type: Number, 5603 type: Number,
7645 value: 0, 5604 value: 0,
7646 reflectToAttribute: true, 5605 reflectToAttribute: true,
7647 notify: true 5606 notify: true
7648 }, 5607 },
7649 5608
7650 // The start time of the query range.
7651 queryStartTime: String, 5609 queryStartTime: String,
7652 5610
7653 // The end time of the query range.
7654 queryEndTime: String, 5611 queryEndTime: String,
7655 }, 5612 },
7656 5613
7657 /**
7658 * Changes the toolbar background color depending on whether any history items
7659 * are currently selected.
7660 * @private
7661 */
7662 changeToolbarView_: function() { 5614 changeToolbarView_: function() {
7663 this.itemsSelected_ = this.count > 0; 5615 this.itemsSelected_ = this.count > 0;
7664 }, 5616 },
7665 5617
7666 /**
7667 * When changing the search term externally, update the search field to
7668 * reflect the new search term.
7669 * @param {string} search
7670 */
7671 setSearchTerm: function(search) { 5618 setSearchTerm: function(search) {
7672 if (this.searchTerm == search) 5619 if (this.searchTerm == search)
7673 return; 5620 return;
7674 5621
7675 this.searchTerm = search; 5622 this.searchTerm = search;
7676 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) 5623 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
7677 .getSearchField(); 5624 .getSearchField();
7678 searchField.showAndFocus(); 5625 searchField.showAndFocus();
7679 searchField.setValue(search); 5626 searchField.setValue(search);
7680 }, 5627 },
7681 5628
7682 /**
7683 * @param {!CustomEvent} event
7684 * @private
7685 */
7686 onSearchChanged_: function(event) { 5629 onSearchChanged_: function(event) {
7687 this.searchTerm = /** @type {string} */ (event.detail); 5630 this.searchTerm = /** @type {string} */ (event.detail);
7688 }, 5631 },
7689 5632
7690 onClearSelectionTap_: function() { 5633 onClearSelectionTap_: function() {
7691 this.fire('unselect-all'); 5634 this.fire('unselect-all');
7692 }, 5635 },
7693 5636
7694 onDeleteTap_: function() { 5637 onDeleteTap_: function() {
7695 this.fire('delete-selected'); 5638 this.fire('delete-selected');
7696 }, 5639 },
7697 5640
7698 get searchBar() { 5641 get searchBar() {
7699 return this.$['main-toolbar'].getSearchField(); 5642 return this.$['main-toolbar'].getSearchField();
7700 }, 5643 },
7701 5644
7702 showSearchField: function() { 5645 showSearchField: function() {
7703 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) 5646 /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
7704 .getSearchField() 5647 .getSearchField()
7705 .showAndFocus(); 5648 .showAndFocus();
7706 }, 5649 },
7707 5650
7708 /**
7709 * If the user is a supervised user the delete button is not shown.
7710 * @private
7711 */
7712 deletingAllowed_: function() { 5651 deletingAllowed_: function() {
7713 return loadTimeData.getBoolean('allowDeletingHistory'); 5652 return loadTimeData.getBoolean('allowDeletingHistory');
7714 }, 5653 },
7715 5654
7716 numberOfItemsSelected_: function(count) { 5655 numberOfItemsSelected_: function(count) {
7717 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; 5656 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : '';
7718 }, 5657 },
7719 5658
7720 getHistoryInterval_: function(queryStartTime, queryEndTime) { 5659 getHistoryInterval_: function(queryStartTime, queryEndTime) {
7721 // TODO(calamity): Fix the format of these dates.
7722 return loadTimeData.getStringF( 5660 return loadTimeData.getStringF(
7723 'historyInterval', queryStartTime, queryEndTime); 5661 'historyInterval', queryStartTime, queryEndTime);
7724 }, 5662 },
7725 5663
7726 /** @private */ 5664 /** @private */
7727 hasDrawerChanged_: function() { 5665 hasDrawerChanged_: function() {
7728 this.updateStyles(); 5666 this.updateStyles();
7729 }, 5667 },
7730 }); 5668 });
7731 // Copyright 2016 The Chromium Authors. All rights reserved. 5669 // Copyright 2016 The Chromium Authors. All rights reserved.
7732 // Use of this source code is governed by a BSD-style license that can be 5670 // Use of this source code is governed by a BSD-style license that can be
7733 // found in the LICENSE file. 5671 // found in the LICENSE file.
7734 5672
7735 /**
7736 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the
7737 * dialog is closed via close(), a 'close' event is fired. If the dialog is
7738 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event.
7739 * Additionally clients can inspect the dialog's |returnValue| property inside
7740 * the 'close' event listener to determine whether it was canceled or just
7741 * closed, where a truthy value means success, and a falsy value means it was
7742 * canceled.
7743 */
7744 Polymer({ 5673 Polymer({
7745 is: 'cr-dialog', 5674 is: 'cr-dialog',
7746 extends: 'dialog', 5675 extends: 'dialog',
7747 5676
7748 cancel: function() { 5677 cancel: function() {
7749 this.fire('cancel'); 5678 this.fire('cancel');
7750 HTMLDialogElement.prototype.close.call(this, ''); 5679 HTMLDialogElement.prototype.close.call(this, '');
7751 }, 5680 },
7752 5681
7753 /**
7754 * @param {string=} opt_returnValue
7755 * @override
7756 */
7757 close: function(opt_returnValue) { 5682 close: function(opt_returnValue) {
7758 HTMLDialogElement.prototype.close.call(this, 'success'); 5683 HTMLDialogElement.prototype.close.call(this, 'success');
7759 }, 5684 },
7760 5685
7761 /** @return {!PaperIconButtonElement} */ 5686 /** @return {!PaperIconButtonElement} */
7762 getCloseButton: function() { 5687 getCloseButton: function() {
7763 return this.$.close; 5688 return this.$.close;
7764 }, 5689 },
7765 }); 5690 });
7766 /**
7767 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
7768 optionally centers it in the window or another element.
7769
7770 The element will only be sized and/or positioned if it has not already been size d and/or positioned
7771 by CSS.
7772
7773 CSS properties | Action
7774 -----------------------------|-------------------------------------------
7775 `position` set | Element is not centered horizontally or verticall y
7776 `top` or `bottom` set | Element is not vertically centered
7777 `left` or `right` set | Element is not horizontally centered
7778 `max-height` set | Element respects `max-height`
7779 `max-width` set | Element respects `max-width`
7780
7781 `Polymer.IronFitBehavior` can position an element into another element using
7782 `verticalAlign` and `horizontalAlign`. This will override the element's css posi tion.
7783
7784 <div class="container">
7785 <iron-fit-impl vertical-align="top" horizontal-align="auto">
7786 Positioned into the container
7787 </iron-fit-impl>
7788 </div>
7789
7790 Use `noOverlap` to position the element around another element without overlappi ng it.
7791
7792 <div class="container">
7793 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
7794 Positioned around the container
7795 </iron-fit-impl>
7796 </div>
7797
7798 @demo demo/index.html
7799 @polymerBehavior
7800 */
7801 5691
7802 Polymer.IronFitBehavior = { 5692 Polymer.IronFitBehavior = {
7803 5693
7804 properties: { 5694 properties: {
7805 5695
7806 /**
7807 * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
7808 * but it can be set to a child element. This is useful, for example, for implementing a
7809 * scrolling region inside the element.
7810 * @type {!Element}
7811 */
7812 sizingTarget: { 5696 sizingTarget: {
7813 type: Object, 5697 type: Object,
7814 value: function() { 5698 value: function() {
7815 return this; 5699 return this;
7816 } 5700 }
7817 }, 5701 },
7818 5702
7819 /**
7820 * The element to fit `this` into.
7821 */
7822 fitInto: { 5703 fitInto: {
7823 type: Object, 5704 type: Object,
7824 value: window 5705 value: window
7825 }, 5706 },
7826 5707
7827 /**
7828 * Will position the element around the positionTarget without overlapping it.
7829 */
7830 noOverlap: { 5708 noOverlap: {
7831 type: Boolean 5709 type: Boolean
7832 }, 5710 },
7833 5711
7834 /**
7835 * The element that should be used to position the element. If not set, it will
7836 * default to the parent node.
7837 * @type {!Element}
7838 */
7839 positionTarget: { 5712 positionTarget: {
7840 type: Element 5713 type: Element
7841 }, 5714 },
7842 5715
7843 /**
7844 * The orientation against which to align the element horizontally
7845 * relative to the `positionTarget`. Possible values are "left", "right", "auto".
7846 */
7847 horizontalAlign: { 5716 horizontalAlign: {
7848 type: String 5717 type: String
7849 }, 5718 },
7850 5719
7851 /**
7852 * The orientation against which to align the element vertically
7853 * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
7854 */
7855 verticalAlign: { 5720 verticalAlign: {
7856 type: String 5721 type: String
7857 }, 5722 },
7858 5723
7859 /**
7860 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr eferred alignment
7861 * and if there's not enough space, it will pick the values which minimize the cropping.
7862 */
7863 dynamicAlign: { 5724 dynamicAlign: {
7864 type: Boolean 5725 type: Boolean
7865 }, 5726 },
7866 5727
7867 /**
7868 * The same as setting margin-left and margin-right css properties.
7869 * @deprecated
7870 */
7871 horizontalOffset: { 5728 horizontalOffset: {
7872 type: Number, 5729 type: Number,
7873 value: 0, 5730 value: 0,
7874 notify: true 5731 notify: true
7875 }, 5732 },
7876 5733
7877 /**
7878 * The same as setting margin-top and margin-bottom css properties.
7879 * @deprecated
7880 */
7881 verticalOffset: { 5734 verticalOffset: {
7882 type: Number, 5735 type: Number,
7883 value: 0, 5736 value: 0,
7884 notify: true 5737 notify: true
7885 }, 5738 },
7886 5739
7887 /**
7888 * Set to true to auto-fit on attach.
7889 */
7890 autoFitOnAttach: { 5740 autoFitOnAttach: {
7891 type: Boolean, 5741 type: Boolean,
7892 value: false 5742 value: false
7893 }, 5743 },
7894 5744
7895 /** @type {?Object} */ 5745 /** @type {?Object} */
7896 _fitInfo: { 5746 _fitInfo: {
7897 type: Object 5747 type: Object
7898 } 5748 }
7899 }, 5749 },
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
7931 get _fitTop() { 5781 get _fitTop() {
7932 var fitTop; 5782 var fitTop;
7933 if (this.fitInto === window) { 5783 if (this.fitInto === window) {
7934 fitTop = 0; 5784 fitTop = 0;
7935 } else { 5785 } else {
7936 fitTop = this.fitInto.getBoundingClientRect().top; 5786 fitTop = this.fitInto.getBoundingClientRect().top;
7937 } 5787 }
7938 return fitTop; 5788 return fitTop;
7939 }, 5789 },
7940 5790
7941 /**
7942 * The element that should be used to position the element,
7943 * if no position target is configured.
7944 */
7945 get _defaultPositionTarget() { 5791 get _defaultPositionTarget() {
7946 var parent = Polymer.dom(this).parentNode; 5792 var parent = Polymer.dom(this).parentNode;
7947 5793
7948 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 5794 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
7949 parent = parent.host; 5795 parent = parent.host;
7950 } 5796 }
7951 5797
7952 return parent; 5798 return parent;
7953 }, 5799 },
7954 5800
7955 /**
7956 * The horizontal align value, accounting for the RTL/LTR text direction.
7957 */
7958 get _localeHorizontalAlign() { 5801 get _localeHorizontalAlign() {
7959 if (this._isRTL) { 5802 if (this._isRTL) {
7960 // In RTL, "left" becomes "right".
7961 if (this.horizontalAlign === 'right') { 5803 if (this.horizontalAlign === 'right') {
7962 return 'left'; 5804 return 'left';
7963 } 5805 }
7964 if (this.horizontalAlign === 'left') { 5806 if (this.horizontalAlign === 'left') {
7965 return 'right'; 5807 return 'right';
7966 } 5808 }
7967 } 5809 }
7968 return this.horizontalAlign; 5810 return this.horizontalAlign;
7969 }, 5811 },
7970 5812
7971 attached: function() { 5813 attached: function() {
7972 // Memoize this to avoid expensive calculations & relayouts.
7973 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; 5814 this._isRTL = window.getComputedStyle(this).direction == 'rtl';
7974 this.positionTarget = this.positionTarget || this._defaultPositionTarget; 5815 this.positionTarget = this.positionTarget || this._defaultPositionTarget;
7975 if (this.autoFitOnAttach) { 5816 if (this.autoFitOnAttach) {
7976 if (window.getComputedStyle(this).display === 'none') { 5817 if (window.getComputedStyle(this).display === 'none') {
7977 setTimeout(function() { 5818 setTimeout(function() {
7978 this.fit(); 5819 this.fit();
7979 }.bind(this)); 5820 }.bind(this));
7980 } else { 5821 } else {
7981 this.fit(); 5822 this.fit();
7982 } 5823 }
7983 } 5824 }
7984 }, 5825 },
7985 5826
7986 /**
7987 * Positions and fits the element into the `fitInto` element.
7988 */
7989 fit: function() { 5827 fit: function() {
7990 this.position(); 5828 this.position();
7991 this.constrain(); 5829 this.constrain();
7992 this.center(); 5830 this.center();
7993 }, 5831 },
7994 5832
7995 /**
7996 * Memoize information needed to position and size the target element.
7997 * @suppress {deprecated}
7998 */
7999 _discoverInfo: function() { 5833 _discoverInfo: function() {
8000 if (this._fitInfo) { 5834 if (this._fitInfo) {
8001 return; 5835 return;
8002 } 5836 }
8003 var target = window.getComputedStyle(this); 5837 var target = window.getComputedStyle(this);
8004 var sizer = window.getComputedStyle(this.sizingTarget); 5838 var sizer = window.getComputedStyle(this.sizingTarget);
8005 5839
8006 this._fitInfo = { 5840 this._fitInfo = {
8007 inlineStyle: { 5841 inlineStyle: {
8008 top: this.style.top || '', 5842 top: this.style.top || '',
(...skipping 18 matching lines...) Expand all
8027 minHeight: parseInt(sizer.minHeight, 10) || 0 5861 minHeight: parseInt(sizer.minHeight, 10) || 0
8028 }, 5862 },
8029 margin: { 5863 margin: {
8030 top: parseInt(target.marginTop, 10) || 0, 5864 top: parseInt(target.marginTop, 10) || 0,
8031 right: parseInt(target.marginRight, 10) || 0, 5865 right: parseInt(target.marginRight, 10) || 0,
8032 bottom: parseInt(target.marginBottom, 10) || 0, 5866 bottom: parseInt(target.marginBottom, 10) || 0,
8033 left: parseInt(target.marginLeft, 10) || 0 5867 left: parseInt(target.marginLeft, 10) || 0
8034 } 5868 }
8035 }; 5869 };
8036 5870
8037 // Support these properties until they are removed.
8038 if (this.verticalOffset) { 5871 if (this.verticalOffset) {
8039 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf fset; 5872 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf fset;
8040 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; 5873 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
8041 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; 5874 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
8042 this.style.marginTop = this.style.marginBottom = this.verticalOffset + ' px'; 5875 this.style.marginTop = this.style.marginBottom = this.verticalOffset + ' px';
8043 } 5876 }
8044 if (this.horizontalOffset) { 5877 if (this.horizontalOffset) {
8045 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal Offset; 5878 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal Offset;
8046 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; 5879 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
8047 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; 5880 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
8048 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px'; 5881 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
8049 } 5882 }
8050 }, 5883 },
8051 5884
8052 /**
8053 * Resets the target element's position and size constraints, and clear
8054 * the memoized data.
8055 */
8056 resetFit: function() { 5885 resetFit: function() {
8057 var info = this._fitInfo || {}; 5886 var info = this._fitInfo || {};
8058 for (var property in info.sizerInlineStyle) { 5887 for (var property in info.sizerInlineStyle) {
8059 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; 5888 this.sizingTarget.style[property] = info.sizerInlineStyle[property];
8060 } 5889 }
8061 for (var property in info.inlineStyle) { 5890 for (var property in info.inlineStyle) {
8062 this.style[property] = info.inlineStyle[property]; 5891 this.style[property] = info.inlineStyle[property];
8063 } 5892 }
8064 5893
8065 this._fitInfo = null; 5894 this._fitInfo = null;
8066 }, 5895 },
8067 5896
8068 /**
8069 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
8070 * the element or the `fitInto` element has been resized, or if any of the
8071 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated .
8072 * It preserves the scroll position of the sizingTarget.
8073 */
8074 refit: function() { 5897 refit: function() {
8075 var scrollLeft = this.sizingTarget.scrollLeft; 5898 var scrollLeft = this.sizingTarget.scrollLeft;
8076 var scrollTop = this.sizingTarget.scrollTop; 5899 var scrollTop = this.sizingTarget.scrollTop;
8077 this.resetFit(); 5900 this.resetFit();
8078 this.fit(); 5901 this.fit();
8079 this.sizingTarget.scrollLeft = scrollLeft; 5902 this.sizingTarget.scrollLeft = scrollLeft;
8080 this.sizingTarget.scrollTop = scrollTop; 5903 this.sizingTarget.scrollTop = scrollTop;
8081 }, 5904 },
8082 5905
8083 /**
8084 * Positions the element according to `horizontalAlign, verticalAlign`.
8085 */
8086 position: function() { 5906 position: function() {
8087 if (!this.horizontalAlign && !this.verticalAlign) { 5907 if (!this.horizontalAlign && !this.verticalAlign) {
8088 // needs to be centered, and it is done after constrain.
8089 return; 5908 return;
8090 } 5909 }
8091 this._discoverInfo(); 5910 this._discoverInfo();
8092 5911
8093 this.style.position = 'fixed'; 5912 this.style.position = 'fixed';
8094 // Need border-box for margin/padding.
8095 this.sizingTarget.style.boxSizing = 'border-box'; 5913 this.sizingTarget.style.boxSizing = 'border-box';
8096 // Set to 0, 0 in order to discover any offset caused by parent stacking c ontexts.
8097 this.style.left = '0px'; 5914 this.style.left = '0px';
8098 this.style.top = '0px'; 5915 this.style.top = '0px';
8099 5916
8100 var rect = this.getBoundingClientRect(); 5917 var rect = this.getBoundingClientRect();
8101 var positionRect = this.__getNormalizedRect(this.positionTarget); 5918 var positionRect = this.__getNormalizedRect(this.positionTarget);
8102 var fitRect = this.__getNormalizedRect(this.fitInto); 5919 var fitRect = this.__getNormalizedRect(this.fitInto);
8103 5920
8104 var margin = this._fitInfo.margin; 5921 var margin = this._fitInfo.margin;
8105 5922
8106 // Consider the margin as part of the size for position calculations.
8107 var size = { 5923 var size = {
8108 width: rect.width + margin.left + margin.right, 5924 width: rect.width + margin.left + margin.right,
8109 height: rect.height + margin.top + margin.bottom 5925 height: rect.height + margin.top + margin.bottom
8110 }; 5926 };
8111 5927
8112 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic alAlign, size, positionRect, fitRect); 5928 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic alAlign, size, positionRect, fitRect);
8113 5929
8114 var left = position.left + margin.left; 5930 var left = position.left + margin.left;
8115 var top = position.top + margin.top; 5931 var top = position.top + margin.top;
8116 5932
8117 // Use original size (without margin).
8118 var right = Math.min(fitRect.right - margin.right, left + rect.width); 5933 var right = Math.min(fitRect.right - margin.right, left + rect.width);
8119 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); 5934 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
8120 5935
8121 var minWidth = this._fitInfo.sizedBy.minWidth; 5936 var minWidth = this._fitInfo.sizedBy.minWidth;
8122 var minHeight = this._fitInfo.sizedBy.minHeight; 5937 var minHeight = this._fitInfo.sizedBy.minHeight;
8123 if (left < margin.left) { 5938 if (left < margin.left) {
8124 left = margin.left; 5939 left = margin.left;
8125 if (right - left < minWidth) { 5940 if (right - left < minWidth) {
8126 left = right - minWidth; 5941 left = right - minWidth;
8127 } 5942 }
8128 } 5943 }
8129 if (top < margin.top) { 5944 if (top < margin.top) {
8130 top = margin.top; 5945 top = margin.top;
8131 if (bottom - top < minHeight) { 5946 if (bottom - top < minHeight) {
8132 top = bottom - minHeight; 5947 top = bottom - minHeight;
8133 } 5948 }
8134 } 5949 }
8135 5950
8136 this.sizingTarget.style.maxWidth = (right - left) + 'px'; 5951 this.sizingTarget.style.maxWidth = (right - left) + 'px';
8137 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; 5952 this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
8138 5953
8139 // Remove the offset caused by any stacking context.
8140 this.style.left = (left - rect.left) + 'px'; 5954 this.style.left = (left - rect.left) + 'px';
8141 this.style.top = (top - rect.top) + 'px'; 5955 this.style.top = (top - rect.top) + 'px';
8142 }, 5956 },
8143 5957
8144 /**
8145 * Constrains the size of the element to `fitInto` by setting `max-height`
8146 * and/or `max-width`.
8147 */
8148 constrain: function() { 5958 constrain: function() {
8149 if (this.horizontalAlign || this.verticalAlign) { 5959 if (this.horizontalAlign || this.verticalAlign) {
8150 return; 5960 return;
8151 } 5961 }
8152 this._discoverInfo(); 5962 this._discoverInfo();
8153 5963
8154 var info = this._fitInfo; 5964 var info = this._fitInfo;
8155 // position at (0px, 0px) if not already positioned, so we can measure the natural size.
8156 if (!info.positionedBy.vertically) { 5965 if (!info.positionedBy.vertically) {
8157 this.style.position = 'fixed'; 5966 this.style.position = 'fixed';
8158 this.style.top = '0px'; 5967 this.style.top = '0px';
8159 } 5968 }
8160 if (!info.positionedBy.horizontally) { 5969 if (!info.positionedBy.horizontally) {
8161 this.style.position = 'fixed'; 5970 this.style.position = 'fixed';
8162 this.style.left = '0px'; 5971 this.style.left = '0px';
8163 } 5972 }
8164 5973
8165 // need border-box for margin/padding
8166 this.sizingTarget.style.boxSizing = 'border-box'; 5974 this.sizingTarget.style.boxSizing = 'border-box';
8167 // constrain the width and height if not already set
8168 var rect = this.getBoundingClientRect(); 5975 var rect = this.getBoundingClientRect();
8169 if (!info.sizedBy.height) { 5976 if (!info.sizedBy.height) {
8170 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom' , 'Height'); 5977 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom' , 'Height');
8171 } 5978 }
8172 if (!info.sizedBy.width) { 5979 if (!info.sizedBy.width) {
8173 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ t', 'Width'); 5980 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ t', 'Width');
8174 } 5981 }
8175 }, 5982 },
8176 5983
8177 /**
8178 * @protected
8179 * @deprecated
8180 */
8181 _sizeDimension: function(rect, positionedBy, start, end, extent) { 5984 _sizeDimension: function(rect, positionedBy, start, end, extent) {
8182 this.__sizeDimension(rect, positionedBy, start, end, extent); 5985 this.__sizeDimension(rect, positionedBy, start, end, extent);
8183 }, 5986 },
8184 5987
8185 /**
8186 * @private
8187 */
8188 __sizeDimension: function(rect, positionedBy, start, end, extent) { 5988 __sizeDimension: function(rect, positionedBy, start, end, extent) {
8189 var info = this._fitInfo; 5989 var info = this._fitInfo;
8190 var fitRect = this.__getNormalizedRect(this.fitInto); 5990 var fitRect = this.__getNormalizedRect(this.fitInto);
8191 var max = extent === 'Width' ? fitRect.width : fitRect.height; 5991 var max = extent === 'Width' ? fitRect.width : fitRect.height;
8192 var flip = (positionedBy === end); 5992 var flip = (positionedBy === end);
8193 var offset = flip ? max - rect[end] : rect[start]; 5993 var offset = flip ? max - rect[end] : rect[start];
8194 var margin = info.margin[flip ? start : end]; 5994 var margin = info.margin[flip ? start : end];
8195 var offsetExtent = 'offset' + extent; 5995 var offsetExtent = 'offset' + extent;
8196 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; 5996 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
8197 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO ffset) + 'px'; 5997 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO ffset) + 'px';
8198 }, 5998 },
8199 5999
8200 /**
8201 * Centers horizontally and vertically if not already positioned. This also sets
8202 * `position:fixed`.
8203 */
8204 center: function() { 6000 center: function() {
8205 if (this.horizontalAlign || this.verticalAlign) { 6001 if (this.horizontalAlign || this.verticalAlign) {
8206 return; 6002 return;
8207 } 6003 }
8208 this._discoverInfo(); 6004 this._discoverInfo();
8209 6005
8210 var positionedBy = this._fitInfo.positionedBy; 6006 var positionedBy = this._fitInfo.positionedBy;
8211 if (positionedBy.vertically && positionedBy.horizontally) { 6007 if (positionedBy.vertically && positionedBy.horizontally) {
8212 // Already positioned.
8213 return; 6008 return;
8214 } 6009 }
8215 // Need position:fixed to center
8216 this.style.position = 'fixed'; 6010 this.style.position = 'fixed';
8217 // Take into account the offset caused by parents that create stacking
8218 // contexts (e.g. with transform: translate3d). Translate to 0,0 and
8219 // measure the bounding rect.
8220 if (!positionedBy.vertically) { 6011 if (!positionedBy.vertically) {
8221 this.style.top = '0px'; 6012 this.style.top = '0px';
8222 } 6013 }
8223 if (!positionedBy.horizontally) { 6014 if (!positionedBy.horizontally) {
8224 this.style.left = '0px'; 6015 this.style.left = '0px';
8225 } 6016 }
8226 // It will take in consideration margins and transforms
8227 var rect = this.getBoundingClientRect(); 6017 var rect = this.getBoundingClientRect();
8228 var fitRect = this.__getNormalizedRect(this.fitInto); 6018 var fitRect = this.__getNormalizedRect(this.fitInto);
8229 if (!positionedBy.vertically) { 6019 if (!positionedBy.vertically) {
8230 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; 6020 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
8231 this.style.top = top + 'px'; 6021 this.style.top = top + 'px';
8232 } 6022 }
8233 if (!positionedBy.horizontally) { 6023 if (!positionedBy.horizontally) {
8234 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; 6024 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
8235 this.style.left = left + 'px'; 6025 this.style.left = left + 'px';
8236 } 6026 }
(...skipping 14 matching lines...) Expand all
8251 }, 6041 },
8252 6042
8253 __getCroppedArea: function(position, size, fitRect) { 6043 __getCroppedArea: function(position, size, fitRect) {
8254 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height)); 6044 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
8255 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ t - (position.left + size.width)); 6045 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ t - (position.left + size.width));
8256 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si ze.height; 6046 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si ze.height;
8257 }, 6047 },
8258 6048
8259 6049
8260 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { 6050 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
8261 // All the possible configurations.
8262 // Ordered as top-left, top-right, bottom-left, bottom-right.
8263 var positions = [{ 6051 var positions = [{
8264 verticalAlign: 'top', 6052 verticalAlign: 'top',
8265 horizontalAlign: 'left', 6053 horizontalAlign: 'left',
8266 top: positionRect.top, 6054 top: positionRect.top,
8267 left: positionRect.left 6055 left: positionRect.left
8268 }, { 6056 }, {
8269 verticalAlign: 'top', 6057 verticalAlign: 'top',
8270 horizontalAlign: 'right', 6058 horizontalAlign: 'right',
8271 top: positionRect.top, 6059 top: positionRect.top,
8272 left: positionRect.right - size.width 6060 left: positionRect.right - size.width
8273 }, { 6061 }, {
8274 verticalAlign: 'bottom', 6062 verticalAlign: 'bottom',
8275 horizontalAlign: 'left', 6063 horizontalAlign: 'left',
8276 top: positionRect.bottom - size.height, 6064 top: positionRect.bottom - size.height,
8277 left: positionRect.left 6065 left: positionRect.left
8278 }, { 6066 }, {
8279 verticalAlign: 'bottom', 6067 verticalAlign: 'bottom',
8280 horizontalAlign: 'right', 6068 horizontalAlign: 'right',
8281 top: positionRect.bottom - size.height, 6069 top: positionRect.bottom - size.height,
8282 left: positionRect.right - size.width 6070 left: positionRect.right - size.width
8283 }]; 6071 }];
8284 6072
8285 if (this.noOverlap) { 6073 if (this.noOverlap) {
8286 // Duplicate.
8287 for (var i = 0, l = positions.length; i < l; i++) { 6074 for (var i = 0, l = positions.length; i < l; i++) {
8288 var copy = {}; 6075 var copy = {};
8289 for (var key in positions[i]) { 6076 for (var key in positions[i]) {
8290 copy[key] = positions[i][key]; 6077 copy[key] = positions[i][key];
8291 } 6078 }
8292 positions.push(copy); 6079 positions.push(copy);
8293 } 6080 }
8294 // Horizontal overlap only.
8295 positions[0].top = positions[1].top += positionRect.height; 6081 positions[0].top = positions[1].top += positionRect.height;
8296 positions[2].top = positions[3].top -= positionRect.height; 6082 positions[2].top = positions[3].top -= positionRect.height;
8297 // Vertical overlap only.
8298 positions[4].left = positions[6].left += positionRect.width; 6083 positions[4].left = positions[6].left += positionRect.width;
8299 positions[5].left = positions[7].left -= positionRect.width; 6084 positions[5].left = positions[7].left -= positionRect.width;
8300 } 6085 }
8301 6086
8302 // Consider auto as null for coding convenience.
8303 vAlign = vAlign === 'auto' ? null : vAlign; 6087 vAlign = vAlign === 'auto' ? null : vAlign;
8304 hAlign = hAlign === 'auto' ? null : hAlign; 6088 hAlign = hAlign === 'auto' ? null : hAlign;
8305 6089
8306 var position; 6090 var position;
8307 for (var i = 0; i < positions.length; i++) { 6091 for (var i = 0; i < positions.length; i++) {
8308 var pos = positions[i]; 6092 var pos = positions[i];
8309 6093
8310 // If both vAlign and hAlign are defined, return exact match.
8311 // For dynamicAlign and noOverlap we'll have more than one candidate, so
8312 // we'll have to check the croppedArea to make the best choice.
8313 if (!this.dynamicAlign && !this.noOverlap && 6094 if (!this.dynamicAlign && !this.noOverlap &&
8314 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { 6095 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
8315 position = pos; 6096 position = pos;
8316 break; 6097 break;
8317 } 6098 }
8318 6099
8319 // Align is ok if alignment preferences are respected. If no preferences ,
8320 // it is considered ok.
8321 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && 6100 var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
8322 (!hAlign || pos.horizontalAlign === hAlign); 6101 (!hAlign || pos.horizontalAlign === hAlign);
8323 6102
8324 // Filter out elements that don't match the alignment (if defined).
8325 // With dynamicAlign, we need to consider all the positions to find the
8326 // one that minimizes the cropped area.
8327 if (!this.dynamicAlign && !alignOk) { 6103 if (!this.dynamicAlign && !alignOk) {
8328 continue; 6104 continue;
8329 } 6105 }
8330 6106
8331 position = position || pos; 6107 position = position || pos;
8332 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); 6108 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
8333 var diff = pos.croppedArea - position.croppedArea; 6109 var diff = pos.croppedArea - position.croppedArea;
8334 // Check which crops less. If it crops equally, check if align is ok.
8335 if (diff < 0 || (diff === 0 && alignOk)) { 6110 if (diff < 0 || (diff === 0 && alignOk)) {
8336 position = pos; 6111 position = pos;
8337 } 6112 }
8338 // If not cropped and respects the align requirements, keep it.
8339 // This allows to prefer positions overlapping horizontally over the
8340 // ones overlapping vertically.
8341 if (position.croppedArea === 0 && alignOk) { 6113 if (position.croppedArea === 0 && alignOk) {
8342 break; 6114 break;
8343 } 6115 }
8344 } 6116 }
8345 6117
8346 return position; 6118 return position;
8347 } 6119 }
8348 6120
8349 }; 6121 };
8350 (function() { 6122 (function() {
8351 'use strict'; 6123 'use strict';
8352 6124
8353 Polymer({ 6125 Polymer({
8354 6126
8355 is: 'iron-overlay-backdrop', 6127 is: 'iron-overlay-backdrop',
8356 6128
8357 properties: { 6129 properties: {
8358 6130
8359 /**
8360 * Returns true if the backdrop is opened.
8361 */
8362 opened: { 6131 opened: {
8363 reflectToAttribute: true, 6132 reflectToAttribute: true,
8364 type: Boolean, 6133 type: Boolean,
8365 value: false, 6134 value: false,
8366 observer: '_openedChanged' 6135 observer: '_openedChanged'
8367 } 6136 }
8368 6137
8369 }, 6138 },
8370 6139
8371 listeners: { 6140 listeners: {
8372 'transitionend': '_onTransitionend' 6141 'transitionend': '_onTransitionend'
8373 }, 6142 },
8374 6143
8375 created: function() { 6144 created: function() {
8376 // Used to cancel previous requestAnimationFrame calls when opened changes .
8377 this.__openedRaf = null; 6145 this.__openedRaf = null;
8378 }, 6146 },
8379 6147
8380 attached: function() { 6148 attached: function() {
8381 this.opened && this._openedChanged(this.opened); 6149 this.opened && this._openedChanged(this.opened);
8382 }, 6150 },
8383 6151
8384 /**
8385 * Appends the backdrop to document body if needed.
8386 */
8387 prepare: function() { 6152 prepare: function() {
8388 if (this.opened && !this.parentNode) { 6153 if (this.opened && !this.parentNode) {
8389 Polymer.dom(document.body).appendChild(this); 6154 Polymer.dom(document.body).appendChild(this);
8390 } 6155 }
8391 }, 6156 },
8392 6157
8393 /**
8394 * Shows the backdrop.
8395 */
8396 open: function() { 6158 open: function() {
8397 this.opened = true; 6159 this.opened = true;
8398 }, 6160 },
8399 6161
8400 /**
8401 * Hides the backdrop.
8402 */
8403 close: function() { 6162 close: function() {
8404 this.opened = false; 6163 this.opened = false;
8405 }, 6164 },
8406 6165
8407 /**
8408 * Removes the backdrop from document body if needed.
8409 */
8410 complete: function() { 6166 complete: function() {
8411 if (!this.opened && this.parentNode === document.body) { 6167 if (!this.opened && this.parentNode === document.body) {
8412 Polymer.dom(this.parentNode).removeChild(this); 6168 Polymer.dom(this.parentNode).removeChild(this);
8413 } 6169 }
8414 }, 6170 },
8415 6171
8416 _onTransitionend: function(event) { 6172 _onTransitionend: function(event) {
8417 if (event && event.target === this) { 6173 if (event && event.target === this) {
8418 this.complete(); 6174 this.complete();
8419 } 6175 }
8420 }, 6176 },
8421 6177
8422 /**
8423 * @param {boolean} opened
8424 * @private
8425 */
8426 _openedChanged: function(opened) { 6178 _openedChanged: function(opened) {
8427 if (opened) { 6179 if (opened) {
8428 // Auto-attach.
8429 this.prepare(); 6180 this.prepare();
8430 } else { 6181 } else {
8431 // Animation might be disabled via the mixin or opacity custom property.
8432 // If it is disabled in other ways, it's up to the user to call complete .
8433 var cs = window.getComputedStyle(this); 6182 var cs = window.getComputedStyle(this);
8434 if (cs.transitionDuration === '0s' || cs.opacity == 0) { 6183 if (cs.transitionDuration === '0s' || cs.opacity == 0) {
8435 this.complete(); 6184 this.complete();
8436 } 6185 }
8437 } 6186 }
8438 6187
8439 if (!this.isAttached) { 6188 if (!this.isAttached) {
8440 return; 6189 return;
8441 } 6190 }
8442 6191
8443 // Always cancel previous requestAnimationFrame.
8444 if (this.__openedRaf) { 6192 if (this.__openedRaf) {
8445 window.cancelAnimationFrame(this.__openedRaf); 6193 window.cancelAnimationFrame(this.__openedRaf);
8446 this.__openedRaf = null; 6194 this.__openedRaf = null;
8447 } 6195 }
8448 // Force relayout to ensure proper transitions.
8449 this.scrollTop = this.scrollTop; 6196 this.scrollTop = this.scrollTop;
8450 this.__openedRaf = window.requestAnimationFrame(function() { 6197 this.__openedRaf = window.requestAnimationFrame(function() {
8451 this.__openedRaf = null; 6198 this.__openedRaf = null;
8452 this.toggleClass('opened', this.opened); 6199 this.toggleClass('opened', this.opened);
8453 }.bind(this)); 6200 }.bind(this));
8454 } 6201 }
8455 }); 6202 });
8456 6203
8457 })(); 6204 })();
8458 /**
8459 * @struct
8460 * @constructor
8461 * @private
8462 */
8463 Polymer.IronOverlayManagerClass = function() { 6205 Polymer.IronOverlayManagerClass = function() {
8464 /**
8465 * Used to keep track of the opened overlays.
8466 * @private {Array<Element>}
8467 */
8468 this._overlays = []; 6206 this._overlays = [];
8469 6207
8470 /**
8471 * iframes have a default z-index of 100,
8472 * so this default should be at least that.
8473 * @private {number}
8474 */
8475 this._minimumZ = 101; 6208 this._minimumZ = 101;
8476 6209
8477 /**
8478 * Memoized backdrop element.
8479 * @private {Element|null}
8480 */
8481 this._backdropElement = null; 6210 this._backdropElement = null;
8482 6211
8483 // Enable document-wide tap recognizer.
8484 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); 6212 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
8485 6213
8486 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); 6214 document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
8487 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true ); 6215 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true );
8488 }; 6216 };
8489 6217
8490 Polymer.IronOverlayManagerClass.prototype = { 6218 Polymer.IronOverlayManagerClass.prototype = {
8491 6219
8492 constructor: Polymer.IronOverlayManagerClass, 6220 constructor: Polymer.IronOverlayManagerClass,
8493 6221
8494 /**
8495 * The shared backdrop element.
8496 * @type {!Element} backdropElement
8497 */
8498 get backdropElement() { 6222 get backdropElement() {
8499 if (!this._backdropElement) { 6223 if (!this._backdropElement) {
8500 this._backdropElement = document.createElement('iron-overlay-backdrop'); 6224 this._backdropElement = document.createElement('iron-overlay-backdrop');
8501 } 6225 }
8502 return this._backdropElement; 6226 return this._backdropElement;
8503 }, 6227 },
8504 6228
8505 /**
8506 * The deepest active element.
8507 * @type {!Element} activeElement the active element
8508 */
8509 get deepActiveElement() { 6229 get deepActiveElement() {
8510 // document.activeElement can be null
8511 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
8512 // In case of null, default it to document.body.
8513 var active = document.activeElement || document.body; 6230 var active = document.activeElement || document.body;
8514 while (active.root && Polymer.dom(active.root).activeElement) { 6231 while (active.root && Polymer.dom(active.root).activeElement) {
8515 active = Polymer.dom(active.root).activeElement; 6232 active = Polymer.dom(active.root).activeElement;
8516 } 6233 }
8517 return active; 6234 return active;
8518 }, 6235 },
8519 6236
8520 /**
8521 * Brings the overlay at the specified index to the front.
8522 * @param {number} i
8523 * @private
8524 */
8525 _bringOverlayAtIndexToFront: function(i) { 6237 _bringOverlayAtIndexToFront: function(i) {
8526 var overlay = this._overlays[i]; 6238 var overlay = this._overlays[i];
8527 if (!overlay) { 6239 if (!overlay) {
8528 return; 6240 return;
8529 } 6241 }
8530 var lastI = this._overlays.length - 1; 6242 var lastI = this._overlays.length - 1;
8531 var currentOverlay = this._overlays[lastI]; 6243 var currentOverlay = this._overlays[lastI];
8532 // Ensure always-on-top overlay stays on top.
8533 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) { 6244 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8534 lastI--; 6245 lastI--;
8535 } 6246 }
8536 // If already the top element, return.
8537 if (i >= lastI) { 6247 if (i >= lastI) {
8538 return; 6248 return;
8539 } 6249 }
8540 // Update z-index to be on top.
8541 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); 6250 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
8542 if (this._getZ(overlay) <= minimumZ) { 6251 if (this._getZ(overlay) <= minimumZ) {
8543 this._applyOverlayZ(overlay, minimumZ); 6252 this._applyOverlayZ(overlay, minimumZ);
8544 } 6253 }
8545 6254
8546 // Shift other overlays behind the new on top.
8547 while (i < lastI) { 6255 while (i < lastI) {
8548 this._overlays[i] = this._overlays[i + 1]; 6256 this._overlays[i] = this._overlays[i + 1];
8549 i++; 6257 i++;
8550 } 6258 }
8551 this._overlays[lastI] = overlay; 6259 this._overlays[lastI] = overlay;
8552 }, 6260 },
8553 6261
8554 /**
8555 * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
8556 * Also updates the backdrop z-index.
8557 * @param {!Element} overlay
8558 */
8559 addOrRemoveOverlay: function(overlay) { 6262 addOrRemoveOverlay: function(overlay) {
8560 if (overlay.opened) { 6263 if (overlay.opened) {
8561 this.addOverlay(overlay); 6264 this.addOverlay(overlay);
8562 } else { 6265 } else {
8563 this.removeOverlay(overlay); 6266 this.removeOverlay(overlay);
8564 } 6267 }
8565 }, 6268 },
8566 6269
8567 /**
8568 * Tracks overlays for z-index and focus management.
8569 * Ensures the last added overlay with always-on-top remains on top.
8570 * @param {!Element} overlay
8571 */
8572 addOverlay: function(overlay) { 6270 addOverlay: function(overlay) {
8573 var i = this._overlays.indexOf(overlay); 6271 var i = this._overlays.indexOf(overlay);
8574 if (i >= 0) { 6272 if (i >= 0) {
8575 this._bringOverlayAtIndexToFront(i); 6273 this._bringOverlayAtIndexToFront(i);
8576 this.trackBackdrop(); 6274 this.trackBackdrop();
8577 return; 6275 return;
8578 } 6276 }
8579 var insertionIndex = this._overlays.length; 6277 var insertionIndex = this._overlays.length;
8580 var currentOverlay = this._overlays[insertionIndex - 1]; 6278 var currentOverlay = this._overlays[insertionIndex - 1];
8581 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); 6279 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
8582 var newZ = this._getZ(overlay); 6280 var newZ = this._getZ(overlay);
8583 6281
8584 // Ensure always-on-top overlay stays on top.
8585 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) { 6282 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay) ) {
8586 // This bumps the z-index of +2.
8587 this._applyOverlayZ(currentOverlay, minimumZ); 6283 this._applyOverlayZ(currentOverlay, minimumZ);
8588 insertionIndex--; 6284 insertionIndex--;
8589 // Update minimumZ to match previous overlay's z-index.
8590 var previousOverlay = this._overlays[insertionIndex - 1]; 6285 var previousOverlay = this._overlays[insertionIndex - 1];
8591 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); 6286 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
8592 } 6287 }
8593 6288
8594 // Update z-index and insert overlay.
8595 if (newZ <= minimumZ) { 6289 if (newZ <= minimumZ) {
8596 this._applyOverlayZ(overlay, minimumZ); 6290 this._applyOverlayZ(overlay, minimumZ);
8597 } 6291 }
8598 this._overlays.splice(insertionIndex, 0, overlay); 6292 this._overlays.splice(insertionIndex, 0, overlay);
8599 6293
8600 this.trackBackdrop(); 6294 this.trackBackdrop();
8601 }, 6295 },
8602 6296
8603 /**
8604 * @param {!Element} overlay
8605 */
8606 removeOverlay: function(overlay) { 6297 removeOverlay: function(overlay) {
8607 var i = this._overlays.indexOf(overlay); 6298 var i = this._overlays.indexOf(overlay);
8608 if (i === -1) { 6299 if (i === -1) {
8609 return; 6300 return;
8610 } 6301 }
8611 this._overlays.splice(i, 1); 6302 this._overlays.splice(i, 1);
8612 6303
8613 this.trackBackdrop(); 6304 this.trackBackdrop();
8614 }, 6305 },
8615 6306
8616 /**
8617 * Returns the current overlay.
8618 * @return {Element|undefined}
8619 */
8620 currentOverlay: function() { 6307 currentOverlay: function() {
8621 var i = this._overlays.length - 1; 6308 var i = this._overlays.length - 1;
8622 return this._overlays[i]; 6309 return this._overlays[i];
8623 }, 6310 },
8624 6311
8625 /**
8626 * Returns the current overlay z-index.
8627 * @return {number}
8628 */
8629 currentOverlayZ: function() { 6312 currentOverlayZ: function() {
8630 return this._getZ(this.currentOverlay()); 6313 return this._getZ(this.currentOverlay());
8631 }, 6314 },
8632 6315
8633 /**
8634 * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
8635 * This does not effect the z-index of any existing overlays.
8636 * @param {number} minimumZ
8637 */
8638 ensureMinimumZ: function(minimumZ) { 6316 ensureMinimumZ: function(minimumZ) {
8639 this._minimumZ = Math.max(this._minimumZ, minimumZ); 6317 this._minimumZ = Math.max(this._minimumZ, minimumZ);
8640 }, 6318 },
8641 6319
8642 focusOverlay: function() { 6320 focusOverlay: function() {
8643 var current = /** @type {?} */ (this.currentOverlay()); 6321 var current = /** @type {?} */ (this.currentOverlay());
8644 if (current) { 6322 if (current) {
8645 current._applyFocus(); 6323 current._applyFocus();
8646 } 6324 }
8647 }, 6325 },
8648 6326
8649 /**
8650 * Updates the backdrop z-index.
8651 */
8652 trackBackdrop: function() { 6327 trackBackdrop: function() {
8653 var overlay = this._overlayWithBackdrop(); 6328 var overlay = this._overlayWithBackdrop();
8654 // Avoid creating the backdrop if there is no overlay with backdrop.
8655 if (!overlay && !this._backdropElement) { 6329 if (!overlay && !this._backdropElement) {
8656 return; 6330 return;
8657 } 6331 }
8658 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; 6332 this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
8659 this.backdropElement.opened = !!overlay; 6333 this.backdropElement.opened = !!overlay;
8660 }, 6334 },
8661 6335
8662 /**
8663 * @return {Array<Element>}
8664 */
8665 getBackdrops: function() { 6336 getBackdrops: function() {
8666 var backdrops = []; 6337 var backdrops = [];
8667 for (var i = 0; i < this._overlays.length; i++) { 6338 for (var i = 0; i < this._overlays.length; i++) {
8668 if (this._overlays[i].withBackdrop) { 6339 if (this._overlays[i].withBackdrop) {
8669 backdrops.push(this._overlays[i]); 6340 backdrops.push(this._overlays[i]);
8670 } 6341 }
8671 } 6342 }
8672 return backdrops; 6343 return backdrops;
8673 }, 6344 },
8674 6345
8675 /**
8676 * Returns the z-index for the backdrop.
8677 * @return {number}
8678 */
8679 backdropZ: function() { 6346 backdropZ: function() {
8680 return this._getZ(this._overlayWithBackdrop()) - 1; 6347 return this._getZ(this._overlayWithBackdrop()) - 1;
8681 }, 6348 },
8682 6349
8683 /**
8684 * Returns the first opened overlay that has a backdrop.
8685 * @return {Element|undefined}
8686 * @private
8687 */
8688 _overlayWithBackdrop: function() { 6350 _overlayWithBackdrop: function() {
8689 for (var i = 0; i < this._overlays.length; i++) { 6351 for (var i = 0; i < this._overlays.length; i++) {
8690 if (this._overlays[i].withBackdrop) { 6352 if (this._overlays[i].withBackdrop) {
8691 return this._overlays[i]; 6353 return this._overlays[i];
8692 } 6354 }
8693 } 6355 }
8694 }, 6356 },
8695 6357
8696 /**
8697 * Calculates the minimum z-index for the overlay.
8698 * @param {Element=} overlay
8699 * @private
8700 */
8701 _getZ: function(overlay) { 6358 _getZ: function(overlay) {
8702 var z = this._minimumZ; 6359 var z = this._minimumZ;
8703 if (overlay) { 6360 if (overlay) {
8704 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay) .zIndex); 6361 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay) .zIndex);
8705 // Check if is a number
8706 // Number.isNaN not supported in IE 10+
8707 if (z1 === z1) { 6362 if (z1 === z1) {
8708 z = z1; 6363 z = z1;
8709 } 6364 }
8710 } 6365 }
8711 return z; 6366 return z;
8712 }, 6367 },
8713 6368
8714 /**
8715 * @param {!Element} element
8716 * @param {number|string} z
8717 * @private
8718 */
8719 _setZ: function(element, z) { 6369 _setZ: function(element, z) {
8720 element.style.zIndex = z; 6370 element.style.zIndex = z;
8721 }, 6371 },
8722 6372
8723 /**
8724 * @param {!Element} overlay
8725 * @param {number} aboveZ
8726 * @private
8727 */
8728 _applyOverlayZ: function(overlay, aboveZ) { 6373 _applyOverlayZ: function(overlay, aboveZ) {
8729 this._setZ(overlay, aboveZ + 2); 6374 this._setZ(overlay, aboveZ + 2);
8730 }, 6375 },
8731 6376
8732 /**
8733 * Returns the deepest overlay in the path.
8734 * @param {Array<Element>=} path
8735 * @return {Element|undefined}
8736 * @suppress {missingProperties}
8737 * @private
8738 */
8739 _overlayInPath: function(path) { 6377 _overlayInPath: function(path) {
8740 path = path || []; 6378 path = path || [];
8741 for (var i = 0; i < path.length; i++) { 6379 for (var i = 0; i < path.length; i++) {
8742 if (path[i]._manager === this) { 6380 if (path[i]._manager === this) {
8743 return path[i]; 6381 return path[i];
8744 } 6382 }
8745 } 6383 }
8746 }, 6384 },
8747 6385
8748 /**
8749 * Ensures the click event is delegated to the right overlay.
8750 * @param {!Event} event
8751 * @private
8752 */
8753 _onCaptureClick: function(event) { 6386 _onCaptureClick: function(event) {
8754 var overlay = /** @type {?} */ (this.currentOverlay()); 6387 var overlay = /** @type {?} */ (this.currentOverlay());
8755 // Check if clicked outside of top overlay.
8756 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { 6388 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
8757 overlay._onCaptureClick(event); 6389 overlay._onCaptureClick(event);
8758 } 6390 }
8759 }, 6391 },
8760 6392
8761 /**
8762 * Ensures the focus event is delegated to the right overlay.
8763 * @param {!Event} event
8764 * @private
8765 */
8766 _onCaptureFocus: function(event) { 6393 _onCaptureFocus: function(event) {
8767 var overlay = /** @type {?} */ (this.currentOverlay()); 6394 var overlay = /** @type {?} */ (this.currentOverlay());
8768 if (overlay) { 6395 if (overlay) {
8769 overlay._onCaptureFocus(event); 6396 overlay._onCaptureFocus(event);
8770 } 6397 }
8771 }, 6398 },
8772 6399
8773 /**
8774 * Ensures TAB and ESC keyboard events are delegated to the right overlay.
8775 * @param {!Event} event
8776 * @private
8777 */
8778 _onCaptureKeyDown: function(event) { 6400 _onCaptureKeyDown: function(event) {
8779 var overlay = /** @type {?} */ (this.currentOverlay()); 6401 var overlay = /** @type {?} */ (this.currentOverlay());
8780 if (overlay) { 6402 if (overlay) {
8781 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { 6403 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
8782 overlay._onCaptureEsc(event); 6404 overlay._onCaptureEsc(event);
8783 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) { 6405 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
8784 overlay._onCaptureTab(event); 6406 overlay._onCaptureTab(event);
8785 } 6407 }
8786 } 6408 }
8787 }, 6409 },
8788 6410
8789 /**
8790 * Returns if the overlay1 should be behind overlay2.
8791 * @param {!Element} overlay1
8792 * @param {!Element} overlay2
8793 * @return {boolean}
8794 * @suppress {missingProperties}
8795 * @private
8796 */
8797 _shouldBeBehindOverlay: function(overlay1, overlay2) { 6411 _shouldBeBehindOverlay: function(overlay1, overlay2) {
8798 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; 6412 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
8799 } 6413 }
8800 }; 6414 };
8801 6415
8802 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); 6416 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
8803 (function() { 6417 (function() {
8804 'use strict'; 6418 'use strict';
8805 6419
8806 /**
8807 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
8808 on top of other content. It includes an optional backdrop, and can be used to im plement a variety
8809 of UI controls including dialogs and drop downs. Multiple overlays may be displa yed at once.
8810
8811 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav ior/blob/master/demo/simple-overlay.html)
8812 for an example.
8813
8814 ### Closing and canceling
8815
8816 An overlay may be hidden by closing or canceling. The difference between close a nd cancel is user
8817 intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
8818 it will cancel whenever the user taps outside it or presses the escape key. This behavior is
8819 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click ` properties.
8820 `close()` should be called explicitly by the implementer when the user interacts with a control
8821 in the overlay element. When the dialog is canceled, the overlay fires an 'iron- overlay-canceled'
8822 event. Call `preventDefault` on this event to prevent the overlay from closing.
8823
8824 ### Positioning
8825
8826 By default the element is sized and positioned to fit and centered inside the wi ndow. You can
8827 position and size it manually using CSS. See `Polymer.IronFitBehavior`.
8828
8829 ### Backdrop
8830
8831 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
8832 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
8833 options.
8834
8835 In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
8836 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f ocusableNodes)
8837 to achieve a different behavior.
8838
8839 ### Limitations
8840
8841 The element is styled to appear on top of other content by setting its `z-index` property. You
8842 must ensure no element has a stacking context with a higher `z-index` than its p arent stacking
8843 context. You should place this element as a child of `<body>` whenever possible.
8844
8845 @demo demo/index.html
8846 @polymerBehavior Polymer.IronOverlayBehavior
8847 */
8848 6420
8849 Polymer.IronOverlayBehaviorImpl = { 6421 Polymer.IronOverlayBehaviorImpl = {
8850 6422
8851 properties: { 6423 properties: {
8852 6424
8853 /**
8854 * True if the overlay is currently displayed.
8855 */
8856 opened: { 6425 opened: {
8857 observer: '_openedChanged', 6426 observer: '_openedChanged',
8858 type: Boolean, 6427 type: Boolean,
8859 value: false, 6428 value: false,
8860 notify: true 6429 notify: true
8861 }, 6430 },
8862 6431
8863 /**
8864 * True if the overlay was canceled when it was last closed.
8865 */
8866 canceled: { 6432 canceled: {
8867 observer: '_canceledChanged', 6433 observer: '_canceledChanged',
8868 readOnly: true, 6434 readOnly: true,
8869 type: Boolean, 6435 type: Boolean,
8870 value: false 6436 value: false
8871 }, 6437 },
8872 6438
8873 /**
8874 * Set to true to display a backdrop behind the overlay. It traps the focu s
8875 * within the light DOM of the overlay.
8876 */
8877 withBackdrop: { 6439 withBackdrop: {
8878 observer: '_withBackdropChanged', 6440 observer: '_withBackdropChanged',
8879 type: Boolean 6441 type: Boolean
8880 }, 6442 },
8881 6443
8882 /**
8883 * Set to true to disable auto-focusing the overlay or child nodes with
8884 * the `autofocus` attribute` when the overlay is opened.
8885 */
8886 noAutoFocus: { 6444 noAutoFocus: {
8887 type: Boolean, 6445 type: Boolean,
8888 value: false 6446 value: false
8889 }, 6447 },
8890 6448
8891 /**
8892 * Set to true to disable canceling the overlay with the ESC key.
8893 */
8894 noCancelOnEscKey: { 6449 noCancelOnEscKey: {
8895 type: Boolean, 6450 type: Boolean,
8896 value: false 6451 value: false
8897 }, 6452 },
8898 6453
8899 /**
8900 * Set to true to disable canceling the overlay by clicking outside it.
8901 */
8902 noCancelOnOutsideClick: { 6454 noCancelOnOutsideClick: {
8903 type: Boolean, 6455 type: Boolean,
8904 value: false 6456 value: false
8905 }, 6457 },
8906 6458
8907 /**
8908 * Contains the reason(s) this overlay was last closed (see `iron-overlay- closed`).
8909 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t he
8910 * behavior can provide other reasons in addition to `canceled`.
8911 */
8912 closingReason: { 6459 closingReason: {
8913 // was a getter before, but needs to be a property so other
8914 // behaviors can override this.
8915 type: Object 6460 type: Object
8916 }, 6461 },
8917 6462
8918 /**
8919 * Set to true to enable restoring of focus when overlay is closed.
8920 */
8921 restoreFocusOnClose: { 6463 restoreFocusOnClose: {
8922 type: Boolean, 6464 type: Boolean,
8923 value: false 6465 value: false
8924 }, 6466 },
8925 6467
8926 /**
8927 * Set to true to keep overlay always on top.
8928 */
8929 alwaysOnTop: { 6468 alwaysOnTop: {
8930 type: Boolean 6469 type: Boolean
8931 }, 6470 },
8932 6471
8933 /**
8934 * Shortcut to access to the overlay manager.
8935 * @private
8936 * @type {Polymer.IronOverlayManagerClass}
8937 */
8938 _manager: { 6472 _manager: {
8939 type: Object, 6473 type: Object,
8940 value: Polymer.IronOverlayManager 6474 value: Polymer.IronOverlayManager
8941 }, 6475 },
8942 6476
8943 /**
8944 * The node being focused.
8945 * @type {?Node}
8946 */
8947 _focusedChild: { 6477 _focusedChild: {
8948 type: Object 6478 type: Object
8949 } 6479 }
8950 6480
8951 }, 6481 },
8952 6482
8953 listeners: { 6483 listeners: {
8954 'iron-resize': '_onIronResize' 6484 'iron-resize': '_onIronResize'
8955 }, 6485 },
8956 6486
8957 /**
8958 * The backdrop element.
8959 * @type {Element}
8960 */
8961 get backdropElement() { 6487 get backdropElement() {
8962 return this._manager.backdropElement; 6488 return this._manager.backdropElement;
8963 }, 6489 },
8964 6490
8965 /**
8966 * Returns the node to give focus to.
8967 * @type {Node}
8968 */
8969 get _focusNode() { 6491 get _focusNode() {
8970 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this; 6492 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]' ) || this;
8971 }, 6493 },
8972 6494
8973 /**
8974 * Array of nodes that can receive focus (overlay included), ordered by `tab index`.
8975 * This is used to retrieve which is the first and last focusable nodes in o rder
8976 * to wrap the focus for overlays `with-backdrop`.
8977 *
8978 * If you know what is your content (specifically the first and last focusab le children),
8979 * you can override this method to return only `[firstFocusable, lastFocusab le];`
8980 * @type {Array<Node>}
8981 * @protected
8982 */
8983 get _focusableNodes() { 6495 get _focusableNodes() {
8984 // Elements that can be focused even if they have [disabled] attribute.
8985 var FOCUSABLE_WITH_DISABLED = [ 6496 var FOCUSABLE_WITH_DISABLED = [
8986 'a[href]', 6497 'a[href]',
8987 'area[href]', 6498 'area[href]',
8988 'iframe', 6499 'iframe',
8989 '[tabindex]', 6500 '[tabindex]',
8990 '[contentEditable=true]' 6501 '[contentEditable=true]'
8991 ]; 6502 ];
8992 6503
8993 // Elements that cannot be focused if they have [disabled] attribute.
8994 var FOCUSABLE_WITHOUT_DISABLED = [ 6504 var FOCUSABLE_WITHOUT_DISABLED = [
8995 'input', 6505 'input',
8996 'select', 6506 'select',
8997 'textarea', 6507 'textarea',
8998 'button' 6508 'button'
8999 ]; 6509 ];
9000 6510
9001 // Discard elements with tabindex=-1 (makes them not focusable).
9002 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + 6511 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
9003 ':not([tabindex="-1"]),' + 6512 ':not([tabindex="-1"]),' +
9004 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),' ) + 6513 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),' ) +
9005 ':not([disabled]):not([tabindex="-1"])'; 6514 ':not([disabled]):not([tabindex="-1"])';
9006 6515
9007 var focusables = Polymer.dom(this).querySelectorAll(selector); 6516 var focusables = Polymer.dom(this).querySelectorAll(selector);
9008 if (this.tabIndex >= 0) { 6517 if (this.tabIndex >= 0) {
9009 // Insert at the beginning because we might have all elements with tabIn dex = 0,
9010 // and the overlay should be the first of the list.
9011 focusables.splice(0, 0, this); 6518 focusables.splice(0, 0, this);
9012 } 6519 }
9013 // Sort by tabindex.
9014 return focusables.sort(function (a, b) { 6520 return focusables.sort(function (a, b) {
9015 if (a.tabIndex === b.tabIndex) { 6521 if (a.tabIndex === b.tabIndex) {
9016 return 0; 6522 return 0;
9017 } 6523 }
9018 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { 6524 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
9019 return 1; 6525 return 1;
9020 } 6526 }
9021 return -1; 6527 return -1;
9022 }); 6528 });
9023 }, 6529 },
9024 6530
9025 ready: function() { 6531 ready: function() {
9026 // Used to skip calls to notifyResize and refit while the overlay is anima ting.
9027 this.__isAnimating = false; 6532 this.__isAnimating = false;
9028 // with-backdrop needs tabindex to be set in order to trap the focus.
9029 // If it is not set, IronOverlayBehavior will set it, and remove it if wit h-backdrop = false.
9030 this.__shouldRemoveTabIndex = false; 6533 this.__shouldRemoveTabIndex = false;
9031 // Used for wrapping the focus on TAB / Shift+TAB.
9032 this.__firstFocusableNode = this.__lastFocusableNode = null; 6534 this.__firstFocusableNode = this.__lastFocusableNode = null;
9033 // Used by __onNextAnimationFrame to cancel any previous callback.
9034 this.__raf = null; 6535 this.__raf = null;
9035 // Focused node before overlay gets opened. Can be restored on close.
9036 this.__restoreFocusNode = null; 6536 this.__restoreFocusNode = null;
9037 this._ensureSetup(); 6537 this._ensureSetup();
9038 }, 6538 },
9039 6539
9040 attached: function() { 6540 attached: function() {
9041 // Call _openedChanged here so that position can be computed correctly.
9042 if (this.opened) { 6541 if (this.opened) {
9043 this._openedChanged(this.opened); 6542 this._openedChanged(this.opened);
9044 } 6543 }
9045 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); 6544 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
9046 }, 6545 },
9047 6546
9048 detached: function() { 6547 detached: function() {
9049 Polymer.dom(this).unobserveNodes(this._observer); 6548 Polymer.dom(this).unobserveNodes(this._observer);
9050 this._observer = null; 6549 this._observer = null;
9051 if (this.__raf) { 6550 if (this.__raf) {
9052 window.cancelAnimationFrame(this.__raf); 6551 window.cancelAnimationFrame(this.__raf);
9053 this.__raf = null; 6552 this.__raf = null;
9054 } 6553 }
9055 this._manager.removeOverlay(this); 6554 this._manager.removeOverlay(this);
9056 }, 6555 },
9057 6556
9058 /**
9059 * Toggle the opened state of the overlay.
9060 */
9061 toggle: function() { 6557 toggle: function() {
9062 this._setCanceled(false); 6558 this._setCanceled(false);
9063 this.opened = !this.opened; 6559 this.opened = !this.opened;
9064 }, 6560 },
9065 6561
9066 /**
9067 * Open the overlay.
9068 */
9069 open: function() { 6562 open: function() {
9070 this._setCanceled(false); 6563 this._setCanceled(false);
9071 this.opened = true; 6564 this.opened = true;
9072 }, 6565 },
9073 6566
9074 /**
9075 * Close the overlay.
9076 */
9077 close: function() { 6567 close: function() {
9078 this._setCanceled(false); 6568 this._setCanceled(false);
9079 this.opened = false; 6569 this.opened = false;
9080 }, 6570 },
9081 6571
9082 /**
9083 * Cancels the overlay.
9084 * @param {Event=} event The original event
9085 */
9086 cancel: function(event) { 6572 cancel: function(event) {
9087 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t rue}); 6573 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t rue});
9088 if (cancelEvent.defaultPrevented) { 6574 if (cancelEvent.defaultPrevented) {
9089 return; 6575 return;
9090 } 6576 }
9091 6577
9092 this._setCanceled(true); 6578 this._setCanceled(true);
9093 this.opened = false; 6579 this.opened = false;
9094 }, 6580 },
9095 6581
9096 _ensureSetup: function() { 6582 _ensureSetup: function() {
9097 if (this._overlaySetup) { 6583 if (this._overlaySetup) {
9098 return; 6584 return;
9099 } 6585 }
9100 this._overlaySetup = true; 6586 this._overlaySetup = true;
9101 this.style.outline = 'none'; 6587 this.style.outline = 'none';
9102 this.style.display = 'none'; 6588 this.style.display = 'none';
9103 }, 6589 },
9104 6590
9105 /**
9106 * Called when `opened` changes.
9107 * @param {boolean=} opened
9108 * @protected
9109 */
9110 _openedChanged: function(opened) { 6591 _openedChanged: function(opened) {
9111 if (opened) { 6592 if (opened) {
9112 this.removeAttribute('aria-hidden'); 6593 this.removeAttribute('aria-hidden');
9113 } else { 6594 } else {
9114 this.setAttribute('aria-hidden', 'true'); 6595 this.setAttribute('aria-hidden', 'true');
9115 } 6596 }
9116 6597
9117 // Defer any animation-related code on attached
9118 // (_openedChanged gets called again on attached).
9119 if (!this.isAttached) { 6598 if (!this.isAttached) {
9120 return; 6599 return;
9121 } 6600 }
9122 6601
9123 this.__isAnimating = true; 6602 this.__isAnimating = true;
9124 6603
9125 // Use requestAnimationFrame for non-blocking rendering.
9126 this.__onNextAnimationFrame(this.__openedChanged); 6604 this.__onNextAnimationFrame(this.__openedChanged);
9127 }, 6605 },
9128 6606
9129 _canceledChanged: function() { 6607 _canceledChanged: function() {
9130 this.closingReason = this.closingReason || {}; 6608 this.closingReason = this.closingReason || {};
9131 this.closingReason.canceled = this.canceled; 6609 this.closingReason.canceled = this.canceled;
9132 }, 6610 },
9133 6611
9134 _withBackdropChanged: function() { 6612 _withBackdropChanged: function() {
9135 // If tabindex is already set, no need to override it.
9136 if (this.withBackdrop && !this.hasAttribute('tabindex')) { 6613 if (this.withBackdrop && !this.hasAttribute('tabindex')) {
9137 this.setAttribute('tabindex', '-1'); 6614 this.setAttribute('tabindex', '-1');
9138 this.__shouldRemoveTabIndex = true; 6615 this.__shouldRemoveTabIndex = true;
9139 } else if (this.__shouldRemoveTabIndex) { 6616 } else if (this.__shouldRemoveTabIndex) {
9140 this.removeAttribute('tabindex'); 6617 this.removeAttribute('tabindex');
9141 this.__shouldRemoveTabIndex = false; 6618 this.__shouldRemoveTabIndex = false;
9142 } 6619 }
9143 if (this.opened && this.isAttached) { 6620 if (this.opened && this.isAttached) {
9144 this._manager.trackBackdrop(); 6621 this._manager.trackBackdrop();
9145 } 6622 }
9146 }, 6623 },
9147 6624
9148 /**
9149 * tasks which must occur before opening; e.g. making the element visible.
9150 * @protected
9151 */
9152 _prepareRenderOpened: function() { 6625 _prepareRenderOpened: function() {
9153 // Store focused node.
9154 this.__restoreFocusNode = this._manager.deepActiveElement; 6626 this.__restoreFocusNode = this._manager.deepActiveElement;
9155 6627
9156 // Needed to calculate the size of the overlay so that transitions on its size
9157 // will have the correct starting points.
9158 this._preparePositioning(); 6628 this._preparePositioning();
9159 this.refit(); 6629 this.refit();
9160 this._finishPositioning(); 6630 this._finishPositioning();
9161 6631
9162 // Safari will apply the focus to the autofocus element when displayed
9163 // for the first time, so we make sure to return the focus where it was.
9164 if (this.noAutoFocus && document.activeElement === this._focusNode) { 6632 if (this.noAutoFocus && document.activeElement === this._focusNode) {
9165 this._focusNode.blur(); 6633 this._focusNode.blur();
9166 this.__restoreFocusNode.focus(); 6634 this.__restoreFocusNode.focus();
9167 } 6635 }
9168 }, 6636 },
9169 6637
9170 /**
9171 * Tasks which cause the overlay to actually open; typically play an animati on.
9172 * @protected
9173 */
9174 _renderOpened: function() { 6638 _renderOpened: function() {
9175 this._finishRenderOpened(); 6639 this._finishRenderOpened();
9176 }, 6640 },
9177 6641
9178 /**
9179 * Tasks which cause the overlay to actually close; typically play an animat ion.
9180 * @protected
9181 */
9182 _renderClosed: function() { 6642 _renderClosed: function() {
9183 this._finishRenderClosed(); 6643 this._finishRenderClosed();
9184 }, 6644 },
9185 6645
9186 /**
9187 * Tasks to be performed at the end of open action. Will fire `iron-overlay- opened`.
9188 * @protected
9189 */
9190 _finishRenderOpened: function() { 6646 _finishRenderOpened: function() {
9191 this.notifyResize(); 6647 this.notifyResize();
9192 this.__isAnimating = false; 6648 this.__isAnimating = false;
9193 6649
9194 // Store it so we don't query too much.
9195 var focusableNodes = this._focusableNodes; 6650 var focusableNodes = this._focusableNodes;
9196 this.__firstFocusableNode = focusableNodes[0]; 6651 this.__firstFocusableNode = focusableNodes[0];
9197 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; 6652 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
9198 6653
9199 this.fire('iron-overlay-opened'); 6654 this.fire('iron-overlay-opened');
9200 }, 6655 },
9201 6656
9202 /**
9203 * Tasks to be performed at the end of close action. Will fire `iron-overlay -closed`.
9204 * @protected
9205 */
9206 _finishRenderClosed: function() { 6657 _finishRenderClosed: function() {
9207 // Hide the overlay.
9208 this.style.display = 'none'; 6658 this.style.display = 'none';
9209 // Reset z-index only at the end of the animation.
9210 this.style.zIndex = ''; 6659 this.style.zIndex = '';
9211 this.notifyResize(); 6660 this.notifyResize();
9212 this.__isAnimating = false; 6661 this.__isAnimating = false;
9213 this.fire('iron-overlay-closed', this.closingReason); 6662 this.fire('iron-overlay-closed', this.closingReason);
9214 }, 6663 },
9215 6664
9216 _preparePositioning: function() { 6665 _preparePositioning: function() {
9217 this.style.transition = this.style.webkitTransition = 'none'; 6666 this.style.transition = this.style.webkitTransition = 'none';
9218 this.style.transform = this.style.webkitTransform = 'none'; 6667 this.style.transform = this.style.webkitTransform = 'none';
9219 this.style.display = ''; 6668 this.style.display = '';
9220 }, 6669 },
9221 6670
9222 _finishPositioning: function() { 6671 _finishPositioning: function() {
9223 // First, make it invisible & reactivate animations.
9224 this.style.display = 'none'; 6672 this.style.display = 'none';
9225 // Force reflow before re-enabling animations so that they don't start.
9226 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9227 this.scrollTop = this.scrollTop; 6673 this.scrollTop = this.scrollTop;
9228 this.style.transition = this.style.webkitTransition = ''; 6674 this.style.transition = this.style.webkitTransition = '';
9229 this.style.transform = this.style.webkitTransform = ''; 6675 this.style.transform = this.style.webkitTransform = '';
9230 // Now that animations are enabled, make it visible again
9231 this.style.display = ''; 6676 this.style.display = '';
9232 // Force reflow, so that following animations are properly started.
9233 // Set scrollTop to itself so that Closure Compiler doesn't remove this.
9234 this.scrollTop = this.scrollTop; 6677 this.scrollTop = this.scrollTop;
9235 }, 6678 },
9236 6679
9237 /**
9238 * Applies focus according to the opened state.
9239 * @protected
9240 */
9241 _applyFocus: function() { 6680 _applyFocus: function() {
9242 if (this.opened) { 6681 if (this.opened) {
9243 if (!this.noAutoFocus) { 6682 if (!this.noAutoFocus) {
9244 this._focusNode.focus(); 6683 this._focusNode.focus();
9245 } 6684 }
9246 } 6685 }
9247 else { 6686 else {
9248 this._focusNode.blur(); 6687 this._focusNode.blur();
9249 this._focusedChild = null; 6688 this._focusedChild = null;
9250 // Restore focus.
9251 if (this.restoreFocusOnClose && this.__restoreFocusNode) { 6689 if (this.restoreFocusOnClose && this.__restoreFocusNode) {
9252 this.__restoreFocusNode.focus(); 6690 this.__restoreFocusNode.focus();
9253 } 6691 }
9254 this.__restoreFocusNode = null; 6692 this.__restoreFocusNode = null;
9255 // If many overlays get closed at the same time, one of them would still
9256 // be the currentOverlay even if already closed, and would call _applyFo cus
9257 // infinitely, so we check for this not to be the current overlay.
9258 var currentOverlay = this._manager.currentOverlay(); 6693 var currentOverlay = this._manager.currentOverlay();
9259 if (currentOverlay && this !== currentOverlay) { 6694 if (currentOverlay && this !== currentOverlay) {
9260 currentOverlay._applyFocus(); 6695 currentOverlay._applyFocus();
9261 } 6696 }
9262 } 6697 }
9263 }, 6698 },
9264 6699
9265 /**
9266 * Cancels (closes) the overlay. Call when click happens outside the overlay .
9267 * @param {!Event} event
9268 * @protected
9269 */
9270 _onCaptureClick: function(event) { 6700 _onCaptureClick: function(event) {
9271 if (!this.noCancelOnOutsideClick) { 6701 if (!this.noCancelOnOutsideClick) {
9272 this.cancel(event); 6702 this.cancel(event);
9273 } 6703 }
9274 }, 6704 },
9275 6705
9276 /**
9277 * Keeps track of the focused child. If withBackdrop, traps focus within ove rlay.
9278 * @param {!Event} event
9279 * @protected
9280 */
9281 _onCaptureFocus: function (event) { 6706 _onCaptureFocus: function (event) {
9282 if (!this.withBackdrop) { 6707 if (!this.withBackdrop) {
9283 return; 6708 return;
9284 } 6709 }
9285 var path = Polymer.dom(event).path; 6710 var path = Polymer.dom(event).path;
9286 if (path.indexOf(this) === -1) { 6711 if (path.indexOf(this) === -1) {
9287 event.stopPropagation(); 6712 event.stopPropagation();
9288 this._applyFocus(); 6713 this._applyFocus();
9289 } else { 6714 } else {
9290 this._focusedChild = path[0]; 6715 this._focusedChild = path[0];
9291 } 6716 }
9292 }, 6717 },
9293 6718
9294 /**
9295 * Handles the ESC key event and cancels (closes) the overlay.
9296 * @param {!Event} event
9297 * @protected
9298 */
9299 _onCaptureEsc: function(event) { 6719 _onCaptureEsc: function(event) {
9300 if (!this.noCancelOnEscKey) { 6720 if (!this.noCancelOnEscKey) {
9301 this.cancel(event); 6721 this.cancel(event);
9302 } 6722 }
9303 }, 6723 },
9304 6724
9305 /**
9306 * Handles TAB key events to track focus changes.
9307 * Will wrap focus for overlays withBackdrop.
9308 * @param {!Event} event
9309 * @protected
9310 */
9311 _onCaptureTab: function(event) { 6725 _onCaptureTab: function(event) {
9312 if (!this.withBackdrop) { 6726 if (!this.withBackdrop) {
9313 return; 6727 return;
9314 } 6728 }
9315 // TAB wraps from last to first focusable.
9316 // Shift + TAB wraps from first to last focusable.
9317 var shift = event.shiftKey; 6729 var shift = event.shiftKey;
9318 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node; 6730 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable Node;
9319 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de; 6731 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo de;
9320 var shouldWrap = false; 6732 var shouldWrap = false;
9321 if (nodeToCheck === nodeToSet) { 6733 if (nodeToCheck === nodeToSet) {
9322 // If nodeToCheck is the same as nodeToSet, it means we have an overlay
9323 // with 0 or 1 focusables; in either case we still need to trap the
9324 // focus within the overlay.
9325 shouldWrap = true; 6734 shouldWrap = true;
9326 } else { 6735 } else {
9327 // In dom=shadow, the manager will receive focus changes on the main
9328 // root but not the ones within other shadow roots, so we can't rely on
9329 // _focusedChild, but we should check the deepest active element.
9330 var focusedNode = this._manager.deepActiveElement; 6736 var focusedNode = this._manager.deepActiveElement;
9331 // If the active element is not the nodeToCheck but the overlay itself,
9332 // it means the focus is about to go outside the overlay, hence we
9333 // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
9334 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); 6737 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
9335 } 6738 }
9336 6739
9337 if (shouldWrap) { 6740 if (shouldWrap) {
9338 // When the overlay contains the last focusable element of the document
9339 // and it's already focused, pressing TAB would move the focus outside
9340 // the document (e.g. to the browser search bar). Similarly, when the
9341 // overlay contains the first focusable element of the document and it's
9342 // already focused, pressing Shift+TAB would move the focus outside the
9343 // document (e.g. to the browser search bar).
9344 // In both cases, we would not receive a focus event, but only a blur.
9345 // In order to achieve focus wrapping, we prevent this TAB event and
9346 // force the focus. This will also prevent the focus to temporarily move
9347 // outside the overlay, which might cause scrolling.
9348 event.preventDefault(); 6741 event.preventDefault();
9349 this._focusedChild = nodeToSet; 6742 this._focusedChild = nodeToSet;
9350 this._applyFocus(); 6743 this._applyFocus();
9351 } 6744 }
9352 }, 6745 },
9353 6746
9354 /**
9355 * Refits if the overlay is opened and not animating.
9356 * @protected
9357 */
9358 _onIronResize: function() { 6747 _onIronResize: function() {
9359 if (this.opened && !this.__isAnimating) { 6748 if (this.opened && !this.__isAnimating) {
9360 this.__onNextAnimationFrame(this.refit); 6749 this.__onNextAnimationFrame(this.refit);
9361 } 6750 }
9362 }, 6751 },
9363 6752
9364 /**
9365 * Will call notifyResize if overlay is opened.
9366 * Can be overridden in order to avoid multiple observers on the same node.
9367 * @protected
9368 */
9369 _onNodesChange: function() { 6753 _onNodesChange: function() {
9370 if (this.opened && !this.__isAnimating) { 6754 if (this.opened && !this.__isAnimating) {
9371 this.notifyResize(); 6755 this.notifyResize();
9372 } 6756 }
9373 }, 6757 },
9374 6758
9375 /**
9376 * Tasks executed when opened changes: prepare for the opening, move the
9377 * focus, update the manager, render opened/closed.
9378 * @private
9379 */
9380 __openedChanged: function() { 6759 __openedChanged: function() {
9381 if (this.opened) { 6760 if (this.opened) {
9382 // Make overlay visible, then add it to the manager.
9383 this._prepareRenderOpened(); 6761 this._prepareRenderOpened();
9384 this._manager.addOverlay(this); 6762 this._manager.addOverlay(this);
9385 // Move the focus to the child node with [autofocus].
9386 this._applyFocus(); 6763 this._applyFocus();
9387 6764
9388 this._renderOpened(); 6765 this._renderOpened();
9389 } else { 6766 } else {
9390 // Remove overlay, then restore the focus before actually closing.
9391 this._manager.removeOverlay(this); 6767 this._manager.removeOverlay(this);
9392 this._applyFocus(); 6768 this._applyFocus();
9393 6769
9394 this._renderClosed(); 6770 this._renderClosed();
9395 } 6771 }
9396 }, 6772 },
9397 6773
9398 /**
9399 * Executes a callback on the next animation frame, overriding any previous
9400 * callback awaiting for the next animation frame. e.g.
9401 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
9402 * `callback1` will never be invoked.
9403 * @param {!Function} callback Its `this` parameter is the overlay itself.
9404 * @private
9405 */
9406 __onNextAnimationFrame: function(callback) { 6774 __onNextAnimationFrame: function(callback) {
9407 if (this.__raf) { 6775 if (this.__raf) {
9408 window.cancelAnimationFrame(this.__raf); 6776 window.cancelAnimationFrame(this.__raf);
9409 } 6777 }
9410 var self = this; 6778 var self = this;
9411 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { 6779 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
9412 self.__raf = null; 6780 self.__raf = null;
9413 callback.call(self); 6781 callback.call(self);
9414 }); 6782 });
9415 } 6783 }
9416 6784
9417 }; 6785 };
9418 6786
9419 /** @polymerBehavior */ 6787 /** @polymerBehavior */
9420 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB ehavior, Polymer.IronOverlayBehaviorImpl]; 6788 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB ehavior, Polymer.IronOverlayBehaviorImpl];
9421 6789
9422 /**
9423 * Fired after the overlay opens.
9424 * @event iron-overlay-opened
9425 */
9426 6790
9427 /**
9428 * Fired when the overlay is canceled, but before it is closed.
9429 * @event iron-overlay-canceled
9430 * @param {Event} event The closing of the overlay can be prevented
9431 * by calling `event.preventDefault()`. The `event.detail` is the original eve nt that
9432 * originated the canceling (e.g. ESC keyboard event or click event outside th e overlay).
9433 */
9434 6791
9435 /**
9436 * Fired after the overlay closes.
9437 * @event iron-overlay-closed
9438 * @param {Event} event The `event.detail` is the `closingReason` property
9439 * (contains `canceled`, whether the overlay was canceled).
9440 */
9441 6792
9442 })(); 6793 })();
9443 /**
9444 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim ations for use with
9445 * elements implementing `Polymer.NeonAnimationRunnerBehavior`.
9446 * @polymerBehavior
9447 */
9448 Polymer.NeonAnimatableBehavior = { 6794 Polymer.NeonAnimatableBehavior = {
9449 6795
9450 properties: { 6796 properties: {
9451 6797
9452 /**
9453 * Animation configuration. See README for more info.
9454 */
9455 animationConfig: { 6798 animationConfig: {
9456 type: Object 6799 type: Object
9457 }, 6800 },
9458 6801
9459 /**
9460 * Convenience property for setting an 'entry' animation. Do not set `anim ationConfig.entry`
9461 * manually if using this. The animated node is set to `this` if using thi s property.
9462 */
9463 entryAnimation: { 6802 entryAnimation: {
9464 observer: '_entryAnimationChanged', 6803 observer: '_entryAnimationChanged',
9465 type: String 6804 type: String
9466 }, 6805 },
9467 6806
9468 /**
9469 * Convenience property for setting an 'exit' animation. Do not set `anima tionConfig.exit`
9470 * manually if using this. The animated node is set to `this` if using thi s property.
9471 */
9472 exitAnimation: { 6807 exitAnimation: {
9473 observer: '_exitAnimationChanged', 6808 observer: '_exitAnimationChanged',
9474 type: String 6809 type: String
9475 } 6810 }
9476 6811
9477 }, 6812 },
9478 6813
9479 _entryAnimationChanged: function() { 6814 _entryAnimationChanged: function() {
9480 this.animationConfig = this.animationConfig || {}; 6815 this.animationConfig = this.animationConfig || {};
9481 this.animationConfig['entry'] = [{ 6816 this.animationConfig['entry'] = [{
9482 name: this.entryAnimation, 6817 name: this.entryAnimation,
9483 node: this 6818 node: this
9484 }]; 6819 }];
9485 }, 6820 },
9486 6821
9487 _exitAnimationChanged: function() { 6822 _exitAnimationChanged: function() {
9488 this.animationConfig = this.animationConfig || {}; 6823 this.animationConfig = this.animationConfig || {};
9489 this.animationConfig['exit'] = [{ 6824 this.animationConfig['exit'] = [{
9490 name: this.exitAnimation, 6825 name: this.exitAnimation,
9491 node: this 6826 node: this
9492 }]; 6827 }];
9493 }, 6828 },
9494 6829
9495 _copyProperties: function(config1, config2) { 6830 _copyProperties: function(config1, config2) {
9496 // shallowly copy properties from config2 to config1
9497 for (var property in config2) { 6831 for (var property in config2) {
9498 config1[property] = config2[property]; 6832 config1[property] = config2[property];
9499 } 6833 }
9500 }, 6834 },
9501 6835
9502 _cloneConfig: function(config) { 6836 _cloneConfig: function(config) {
9503 var clone = { 6837 var clone = {
9504 isClone: true 6838 isClone: true
9505 }; 6839 };
9506 this._copyProperties(clone, config); 6840 this._copyProperties(clone, config);
9507 return clone; 6841 return clone;
9508 }, 6842 },
9509 6843
9510 _getAnimationConfigRecursive: function(type, map, allConfigs) { 6844 _getAnimationConfigRecursive: function(type, map, allConfigs) {
9511 if (!this.animationConfig) { 6845 if (!this.animationConfig) {
9512 return; 6846 return;
9513 } 6847 }
9514 6848
9515 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu nction') { 6849 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu nction') {
9516 this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins ide of your components 'properties' object instead of outside of it.")); 6850 this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins ide of your components 'properties' object instead of outside of it."));
9517 return; 6851 return;
9518 } 6852 }
9519 6853
9520 // type is optional
9521 var thisConfig; 6854 var thisConfig;
9522 if (type) { 6855 if (type) {
9523 thisConfig = this.animationConfig[type]; 6856 thisConfig = this.animationConfig[type];
9524 } else { 6857 } else {
9525 thisConfig = this.animationConfig; 6858 thisConfig = this.animationConfig;
9526 } 6859 }
9527 6860
9528 if (!Array.isArray(thisConfig)) { 6861 if (!Array.isArray(thisConfig)) {
9529 thisConfig = [thisConfig]; 6862 thisConfig = [thisConfig];
9530 } 6863 }
9531 6864
9532 // iterate animations and recurse to process configurations from child nod es
9533 if (thisConfig) { 6865 if (thisConfig) {
9534 for (var config, index = 0; config = thisConfig[index]; index++) { 6866 for (var config, index = 0; config = thisConfig[index]; index++) {
9535 if (config.animatable) { 6867 if (config.animatable) {
9536 config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs); 6868 config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
9537 } else { 6869 } else {
9538 if (config.id) { 6870 if (config.id) {
9539 var cachedConfig = map[config.id]; 6871 var cachedConfig = map[config.id];
9540 if (cachedConfig) { 6872 if (cachedConfig) {
9541 // merge configurations with the same id, making a clone lazily
9542 if (!cachedConfig.isClone) { 6873 if (!cachedConfig.isClone) {
9543 map[config.id] = this._cloneConfig(cachedConfig) 6874 map[config.id] = this._cloneConfig(cachedConfig)
9544 cachedConfig = map[config.id]; 6875 cachedConfig = map[config.id];
9545 } 6876 }
9546 this._copyProperties(cachedConfig, config); 6877 this._copyProperties(cachedConfig, config);
9547 } else { 6878 } else {
9548 // put any configs with an id into a map
9549 map[config.id] = config; 6879 map[config.id] = config;
9550 } 6880 }
9551 } else { 6881 } else {
9552 allConfigs.push(config); 6882 allConfigs.push(config);
9553 } 6883 }
9554 } 6884 }
9555 } 6885 }
9556 } 6886 }
9557 }, 6887 },
9558 6888
9559 /**
9560 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
9561 * an animation with an optional type. Elements implementing `Polymer.NeonAn imatableBehavior`
9562 * should define the property `animationConfig`, which is either a configura tion object
9563 * or a map of animation type to array of configuration objects.
9564 */
9565 getAnimationConfig: function(type) { 6889 getAnimationConfig: function(type) {
9566 var map = {}; 6890 var map = {};
9567 var allConfigs = []; 6891 var allConfigs = [];
9568 this._getAnimationConfigRecursive(type, map, allConfigs); 6892 this._getAnimationConfigRecursive(type, map, allConfigs);
9569 // append the configurations saved in the map to the array
9570 for (var key in map) { 6893 for (var key in map) {
9571 allConfigs.push(map[key]); 6894 allConfigs.push(map[key]);
9572 } 6895 }
9573 return allConfigs; 6896 return allConfigs;
9574 } 6897 }
9575 6898
9576 }; 6899 };
9577 /**
9578 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
9579 *
9580 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
9581 */
9582 Polymer.NeonAnimationRunnerBehaviorImpl = { 6900 Polymer.NeonAnimationRunnerBehaviorImpl = {
9583 6901
9584 _configureAnimations: function(configs) { 6902 _configureAnimations: function(configs) {
9585 var results = []; 6903 var results = [];
9586 if (configs.length > 0) { 6904 if (configs.length > 0) {
9587 for (var config, index = 0; config = configs[index]; index++) { 6905 for (var config, index = 0; config = configs[index]; index++) {
9588 var neonAnimation = document.createElement(config.name); 6906 var neonAnimation = document.createElement(config.name);
9589 // is this element actually a neon animation?
9590 if (neonAnimation.isNeonAnimation) { 6907 if (neonAnimation.isNeonAnimation) {
9591 var result = null; 6908 var result = null;
9592 // configuration or play could fail if polyfills aren't loaded
9593 try { 6909 try {
9594 result = neonAnimation.configure(config); 6910 result = neonAnimation.configure(config);
9595 // Check if we have an Effect rather than an Animation
9596 if (typeof result.cancel != 'function') { 6911 if (typeof result.cancel != 'function') {
9597 result = document.timeline.play(result); 6912 result = document.timeline.play(result);
9598 } 6913 }
9599 } catch (e) { 6914 } catch (e) {
9600 result = null; 6915 result = null;
9601 console.warn('Couldnt play', '(', config.name, ').', e); 6916 console.warn('Couldnt play', '(', config.name, ').', e);
9602 } 6917 }
9603 if (result) { 6918 if (result) {
9604 results.push({ 6919 results.push({
9605 neonAnimation: neonAnimation, 6920 neonAnimation: neonAnimation,
(...skipping 22 matching lines...) Expand all
9628 6943
9629 _complete: function(activeEntries) { 6944 _complete: function(activeEntries) {
9630 for (var i = 0; i < activeEntries.length; i++) { 6945 for (var i = 0; i < activeEntries.length; i++) {
9631 activeEntries[i].neonAnimation.complete(activeEntries[i].config); 6946 activeEntries[i].neonAnimation.complete(activeEntries[i].config);
9632 } 6947 }
9633 for (var i = 0; i < activeEntries.length; i++) { 6948 for (var i = 0; i < activeEntries.length; i++) {
9634 activeEntries[i].animation.cancel(); 6949 activeEntries[i].animation.cancel();
9635 } 6950 }
9636 }, 6951 },
9637 6952
9638 /**
9639 * Plays an animation with an optional `type`.
9640 * @param {string=} type
9641 * @param {!Object=} cookie
9642 */
9643 playAnimation: function(type, cookie) { 6953 playAnimation: function(type, cookie) {
9644 var configs = this.getAnimationConfig(type); 6954 var configs = this.getAnimationConfig(type);
9645 if (!configs) { 6955 if (!configs) {
9646 return; 6956 return;
9647 } 6957 }
9648 this._active = this._active || {}; 6958 this._active = this._active || {};
9649 if (this._active[type]) { 6959 if (this._active[type]) {
9650 this._complete(this._active[type]); 6960 this._complete(this._active[type]);
9651 delete this._active[type]; 6961 delete this._active[type];
9652 } 6962 }
(...skipping 11 matching lines...) Expand all
9664 activeEntries[i].animation.onfinish = function() { 6974 activeEntries[i].animation.onfinish = function() {
9665 if (this._shouldComplete(activeEntries)) { 6975 if (this._shouldComplete(activeEntries)) {
9666 this._complete(activeEntries); 6976 this._complete(activeEntries);
9667 delete this._active[type]; 6977 delete this._active[type];
9668 this.fire('neon-animation-finish', cookie, {bubbles: false}); 6978 this.fire('neon-animation-finish', cookie, {bubbles: false});
9669 } 6979 }
9670 }.bind(this); 6980 }.bind(this);
9671 } 6981 }
9672 }, 6982 },
9673 6983
9674 /**
9675 * Cancels the currently running animations.
9676 */
9677 cancelAnimation: function() { 6984 cancelAnimation: function() {
9678 for (var k in this._animations) { 6985 for (var k in this._animations) {
9679 this._animations[k].cancel(); 6986 this._animations[k].cancel();
9680 } 6987 }
9681 this._animations = {}; 6988 this._animations = {};
9682 } 6989 }
9683 }; 6990 };
9684 6991
9685 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ 6992 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */
9686 Polymer.NeonAnimationRunnerBehavior = [ 6993 Polymer.NeonAnimationRunnerBehavior = [
9687 Polymer.NeonAnimatableBehavior, 6994 Polymer.NeonAnimatableBehavior,
9688 Polymer.NeonAnimationRunnerBehaviorImpl 6995 Polymer.NeonAnimationRunnerBehaviorImpl
9689 ]; 6996 ];
9690 /**
9691 * Use `Polymer.NeonAnimationBehavior` to implement an animation.
9692 * @polymerBehavior
9693 */
9694 Polymer.NeonAnimationBehavior = { 6997 Polymer.NeonAnimationBehavior = {
9695 6998
9696 properties: { 6999 properties: {
9697 7000
9698 /**
9699 * Defines the animation timing.
9700 */
9701 animationTiming: { 7001 animationTiming: {
9702 type: Object, 7002 type: Object,
9703 value: function() { 7003 value: function() {
9704 return { 7004 return {
9705 duration: 500, 7005 duration: 500,
9706 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', 7006 easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
9707 fill: 'both' 7007 fill: 'both'
9708 } 7008 }
9709 } 7009 }
9710 } 7010 }
9711 7011
9712 }, 7012 },
9713 7013
9714 /**
9715 * Can be used to determine that elements implement this behavior.
9716 */
9717 isNeonAnimation: true, 7014 isNeonAnimation: true,
9718 7015
9719 /**
9720 * Do any animation configuration here.
9721 */
9722 // configure: function(config) {
9723 // },
9724 7016
9725 /**
9726 * Returns the animation timing by mixing in properties from `config` to the defaults defined
9727 * by the animation.
9728 */
9729 timingFromConfig: function(config) { 7017 timingFromConfig: function(config) {
9730 if (config.timing) { 7018 if (config.timing) {
9731 for (var property in config.timing) { 7019 for (var property in config.timing) {
9732 this.animationTiming[property] = config.timing[property]; 7020 this.animationTiming[property] = config.timing[property];
9733 } 7021 }
9734 } 7022 }
9735 return this.animationTiming; 7023 return this.animationTiming;
9736 }, 7024 },
9737 7025
9738 /**
9739 * Sets `transform` and `transformOrigin` properties along with the prefixed versions.
9740 */
9741 setPrefixedProperty: function(node, property, value) { 7026 setPrefixedProperty: function(node, property, value) {
9742 var map = { 7027 var map = {
9743 'transform': ['webkitTransform'], 7028 'transform': ['webkitTransform'],
9744 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] 7029 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin']
9745 }; 7030 };
9746 var prefixes = map[property]; 7031 var prefixes = map[property];
9747 for (var prefix, index = 0; prefix = prefixes[index]; index++) { 7032 for (var prefix, index = 0; prefix = prefixes[index]; index++) {
9748 node.style[prefix] = value; 7033 node.style[prefix] = value;
9749 } 7034 }
9750 node.style[property] = value; 7035 node.style[property] = value;
9751 }, 7036 },
9752 7037
9753 /**
9754 * Called when the animation finishes.
9755 */
9756 complete: function() {} 7038 complete: function() {}
9757 7039
9758 }; 7040 };
9759 Polymer({ 7041 Polymer({
9760 7042
9761 is: 'opaque-animation', 7043 is: 'opaque-animation',
9762 7044
9763 behaviors: [ 7045 behaviors: [
9764 Polymer.NeonAnimationBehavior 7046 Polymer.NeonAnimationBehavior
9765 ], 7047 ],
9766 7048
9767 configure: function(config) { 7049 configure: function(config) {
9768 var node = config.node; 7050 var node = config.node;
9769 this._effect = new KeyframeEffect(node, [ 7051 this._effect = new KeyframeEffect(node, [
9770 {'opacity': '1'}, 7052 {'opacity': '1'},
9771 {'opacity': '1'} 7053 {'opacity': '1'}
9772 ], this.timingFromConfig(config)); 7054 ], this.timingFromConfig(config));
9773 node.style.opacity = '0'; 7055 node.style.opacity = '0';
9774 return this._effect; 7056 return this._effect;
9775 }, 7057 },
9776 7058
9777 complete: function(config) { 7059 complete: function(config) {
9778 config.node.style.opacity = ''; 7060 config.node.style.opacity = '';
9779 } 7061 }
9780 7062
9781 }); 7063 });
9782 (function() { 7064 (function() {
9783 'use strict'; 7065 'use strict';
9784 // Used to calculate the scroll direction during touch events.
9785 var LAST_TOUCH_POSITION = { 7066 var LAST_TOUCH_POSITION = {
9786 pageX: 0, 7067 pageX: 0,
9787 pageY: 0 7068 pageY: 0
9788 }; 7069 };
9789 // Used to avoid computing event.path and filter scrollable nodes (better pe rf).
9790 var ROOT_TARGET = null; 7070 var ROOT_TARGET = null;
9791 var SCROLLABLE_NODES = []; 7071 var SCROLLABLE_NODES = [];
9792 7072
9793 /**
9794 * The IronDropdownScrollManager is intended to provide a central source
9795 * of authority and control over which elements in a document are currently
9796 * allowed to scroll.
9797 */
9798 7073
9799 Polymer.IronDropdownScrollManager = { 7074 Polymer.IronDropdownScrollManager = {
9800 7075
9801 /**
9802 * The current element that defines the DOM boundaries of the
9803 * scroll lock. This is always the most recently locking element.
9804 */
9805 get currentLockingElement() { 7076 get currentLockingElement() {
9806 return this._lockingElements[this._lockingElements.length - 1]; 7077 return this._lockingElements[this._lockingElements.length - 1];
9807 }, 7078 },
9808 7079
9809 /**
9810 * Returns true if the provided element is "scroll locked", which is to
9811 * say that it cannot be scrolled via pointer or keyboard interactions.
9812 *
9813 * @param {HTMLElement} element An HTML element instance which may or may
9814 * not be scroll locked.
9815 */
9816 elementIsScrollLocked: function(element) { 7080 elementIsScrollLocked: function(element) {
9817 var currentLockingElement = this.currentLockingElement; 7081 var currentLockingElement = this.currentLockingElement;
9818 7082
9819 if (currentLockingElement === undefined) 7083 if (currentLockingElement === undefined)
9820 return false; 7084 return false;
9821 7085
9822 var scrollLocked; 7086 var scrollLocked;
9823 7087
9824 if (this._hasCachedLockedElement(element)) { 7088 if (this._hasCachedLockedElement(element)) {
9825 return true; 7089 return true;
9826 } 7090 }
9827 7091
9828 if (this._hasCachedUnlockedElement(element)) { 7092 if (this._hasCachedUnlockedElement(element)) {
9829 return false; 7093 return false;
9830 } 7094 }
9831 7095
9832 scrollLocked = !!currentLockingElement && 7096 scrollLocked = !!currentLockingElement &&
9833 currentLockingElement !== element && 7097 currentLockingElement !== element &&
9834 !this._composedTreeContains(currentLockingElement, element); 7098 !this._composedTreeContains(currentLockingElement, element);
9835 7099
9836 if (scrollLocked) { 7100 if (scrollLocked) {
9837 this._lockedElementCache.push(element); 7101 this._lockedElementCache.push(element);
9838 } else { 7102 } else {
9839 this._unlockedElementCache.push(element); 7103 this._unlockedElementCache.push(element);
9840 } 7104 }
9841 7105
9842 return scrollLocked; 7106 return scrollLocked;
9843 }, 7107 },
9844 7108
9845 /**
9846 * Push an element onto the current scroll lock stack. The most recently
9847 * pushed element and its children will be considered scrollable. All
9848 * other elements will not be scrollable.
9849 *
9850 * Scroll locking is implemented as a stack so that cases such as
9851 * dropdowns within dropdowns are handled well.
9852 *
9853 * @param {HTMLElement} element The element that should lock scroll.
9854 */
9855 pushScrollLock: function(element) { 7109 pushScrollLock: function(element) {
9856 // Prevent pushing the same element twice
9857 if (this._lockingElements.indexOf(element) >= 0) { 7110 if (this._lockingElements.indexOf(element) >= 0) {
9858 return; 7111 return;
9859 } 7112 }
9860 7113
9861 if (this._lockingElements.length === 0) { 7114 if (this._lockingElements.length === 0) {
9862 this._lockScrollInteractions(); 7115 this._lockScrollInteractions();
9863 } 7116 }
9864 7117
9865 this._lockingElements.push(element); 7118 this._lockingElements.push(element);
9866 7119
9867 this._lockedElementCache = []; 7120 this._lockedElementCache = [];
9868 this._unlockedElementCache = []; 7121 this._unlockedElementCache = [];
9869 }, 7122 },
9870 7123
9871 /**
9872 * Remove an element from the scroll lock stack. The element being
9873 * removed does not need to be the most recently pushed element. However,
9874 * the scroll lock constraints only change when the most recently pushed
9875 * element is removed.
9876 *
9877 * @param {HTMLElement} element The element to remove from the scroll
9878 * lock stack.
9879 */
9880 removeScrollLock: function(element) { 7124 removeScrollLock: function(element) {
9881 var index = this._lockingElements.indexOf(element); 7125 var index = this._lockingElements.indexOf(element);
9882 7126
9883 if (index === -1) { 7127 if (index === -1) {
9884 return; 7128 return;
9885 } 7129 }
9886 7130
9887 this._lockingElements.splice(index, 1); 7131 this._lockingElements.splice(index, 1);
9888 7132
9889 this._lockedElementCache = []; 7133 this._lockedElementCache = [];
(...skipping 12 matching lines...) Expand all
9902 7146
9903 _hasCachedLockedElement: function(element) { 7147 _hasCachedLockedElement: function(element) {
9904 return this._lockedElementCache.indexOf(element) > -1; 7148 return this._lockedElementCache.indexOf(element) > -1;
9905 }, 7149 },
9906 7150
9907 _hasCachedUnlockedElement: function(element) { 7151 _hasCachedUnlockedElement: function(element) {
9908 return this._unlockedElementCache.indexOf(element) > -1; 7152 return this._unlockedElementCache.indexOf(element) > -1;
9909 }, 7153 },
9910 7154
9911 _composedTreeContains: function(element, child) { 7155 _composedTreeContains: function(element, child) {
9912 // NOTE(cdata): This method iterates over content elements and their
9913 // corresponding distributed nodes to implement a contains-like method
9914 // that pierces through the composed tree of the ShadowDOM. Results of
9915 // this operation are cached (elsewhere) on a per-scroll-lock basis, to
9916 // guard against potentially expensive lookups happening repeatedly as
9917 // a user scrolls / touchmoves.
9918 var contentElements; 7156 var contentElements;
9919 var distributedNodes; 7157 var distributedNodes;
9920 var contentIndex; 7158 var contentIndex;
9921 var nodeIndex; 7159 var nodeIndex;
9922 7160
9923 if (element.contains(child)) { 7161 if (element.contains(child)) {
9924 return true; 7162 return true;
9925 } 7163 }
9926 7164
9927 contentElements = Polymer.dom(element).querySelectorAll('content'); 7165 contentElements = Polymer.dom(element).querySelectorAll('content');
9928 7166
9929 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI ndex) { 7167 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI ndex) {
9930 7168
9931 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr ibutedNodes(); 7169 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr ibutedNodes();
9932 7170
9933 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { 7171 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
9934 7172
9935 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { 7173 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
9936 return true; 7174 return true;
9937 } 7175 }
9938 } 7176 }
9939 } 7177 }
9940 7178
9941 return false; 7179 return false;
9942 }, 7180 },
9943 7181
9944 _scrollInteractionHandler: function(event) { 7182 _scrollInteractionHandler: function(event) {
9945 // Avoid canceling an event with cancelable=false, e.g. scrolling is in
9946 // progress and cannot be interrupted.
9947 if (event.cancelable && this._shouldPreventScrolling(event)) { 7183 if (event.cancelable && this._shouldPreventScrolling(event)) {
9948 event.preventDefault(); 7184 event.preventDefault();
9949 } 7185 }
9950 // If event has targetTouches (touch event), update last touch position.
9951 if (event.targetTouches) { 7186 if (event.targetTouches) {
9952 var touch = event.targetTouches[0]; 7187 var touch = event.targetTouches[0];
9953 LAST_TOUCH_POSITION.pageX = touch.pageX; 7188 LAST_TOUCH_POSITION.pageX = touch.pageX;
9954 LAST_TOUCH_POSITION.pageY = touch.pageY; 7189 LAST_TOUCH_POSITION.pageY = touch.pageY;
9955 } 7190 }
9956 }, 7191 },
9957 7192
9958 _lockScrollInteractions: function() { 7193 _lockScrollInteractions: function() {
9959 this._boundScrollHandler = this._boundScrollHandler || 7194 this._boundScrollHandler = this._boundScrollHandler ||
9960 this._scrollInteractionHandler.bind(this); 7195 this._scrollInteractionHandler.bind(this);
9961 // Modern `wheel` event for mouse wheel scrolling:
9962 document.addEventListener('wheel', this._boundScrollHandler, true); 7196 document.addEventListener('wheel', this._boundScrollHandler, true);
9963 // Older, non-standard `mousewheel` event for some FF:
9964 document.addEventListener('mousewheel', this._boundScrollHandler, true); 7197 document.addEventListener('mousewheel', this._boundScrollHandler, true);
9965 // IE:
9966 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr ue); 7198 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr ue);
9967 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
9968 document.addEventListener('touchstart', this._boundScrollHandler, true); 7199 document.addEventListener('touchstart', this._boundScrollHandler, true);
9969 // Mobile devices can scroll on touch move:
9970 document.addEventListener('touchmove', this._boundScrollHandler, true); 7200 document.addEventListener('touchmove', this._boundScrollHandler, true);
9971 }, 7201 },
9972 7202
9973 _unlockScrollInteractions: function() { 7203 _unlockScrollInteractions: function() {
9974 document.removeEventListener('wheel', this._boundScrollHandler, true); 7204 document.removeEventListener('wheel', this._boundScrollHandler, true);
9975 document.removeEventListener('mousewheel', this._boundScrollHandler, tru e); 7205 document.removeEventListener('mousewheel', this._boundScrollHandler, tru e);
9976 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true); 7206 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
9977 document.removeEventListener('touchstart', this._boundScrollHandler, tru e); 7207 document.removeEventListener('touchstart', this._boundScrollHandler, tru e);
9978 document.removeEventListener('touchmove', this._boundScrollHandler, true ); 7208 document.removeEventListener('touchmove', this._boundScrollHandler, true );
9979 }, 7209 },
9980 7210
9981 /**
9982 * Returns true if the event causes scroll outside the current locking
9983 * element, e.g. pointer/keyboard interactions, or scroll "leaking"
9984 * outside the locking element when it is already at its scroll boundaries .
9985 * @param {!Event} event
9986 * @return {boolean}
9987 * @private
9988 */
9989 _shouldPreventScrolling: function(event) { 7211 _shouldPreventScrolling: function(event) {
9990 7212
9991 // Update if root target changed. For touch events, ensure we don't
9992 // update during touchmove.
9993 var target = Polymer.dom(event).rootTarget; 7213 var target = Polymer.dom(event).rootTarget;
9994 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { 7214 if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
9995 ROOT_TARGET = target; 7215 ROOT_TARGET = target;
9996 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); 7216 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
9997 } 7217 }
9998 7218
9999 // Prevent event if no scrollable nodes.
10000 if (!SCROLLABLE_NODES.length) { 7219 if (!SCROLLABLE_NODES.length) {
10001 return true; 7220 return true;
10002 } 7221 }
10003 // Don't prevent touchstart event inside the locking element when it has
10004 // scrollable nodes.
10005 if (event.type === 'touchstart') { 7222 if (event.type === 'touchstart') {
10006 return false; 7223 return false;
10007 } 7224 }
10008 // Get deltaX/Y.
10009 var info = this._getScrollInfo(event); 7225 var info = this._getScrollInfo(event);
10010 // Prevent if there is no child that can scroll.
10011 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta Y); 7226 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta Y);
10012 }, 7227 },
10013 7228
10014 /**
10015 * Returns an array of scrollable nodes up to the current locking element,
10016 * which is included too if scrollable.
10017 * @param {!Array<Node>} nodes
10018 * @return {Array<Node>} scrollables
10019 * @private
10020 */
10021 _getScrollableNodes: function(nodes) { 7229 _getScrollableNodes: function(nodes) {
10022 var scrollables = []; 7230 var scrollables = [];
10023 var lockingIndex = nodes.indexOf(this.currentLockingElement); 7231 var lockingIndex = nodes.indexOf(this.currentLockingElement);
10024 // Loop from root target to locking element (included).
10025 for (var i = 0; i <= lockingIndex; i++) { 7232 for (var i = 0; i <= lockingIndex; i++) {
10026 var node = nodes[i]; 7233 var node = nodes[i];
10027 // Skip document fragments.
10028 if (node.nodeType === 11) { 7234 if (node.nodeType === 11) {
10029 continue; 7235 continue;
10030 } 7236 }
10031 // Check inline style before checking computed style.
10032 var style = node.style; 7237 var style = node.style;
10033 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { 7238 if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
10034 style = window.getComputedStyle(node); 7239 style = window.getComputedStyle(node);
10035 } 7240 }
10036 if (style.overflow === 'scroll' || style.overflow === 'auto') { 7241 if (style.overflow === 'scroll' || style.overflow === 'auto') {
10037 scrollables.push(node); 7242 scrollables.push(node);
10038 } 7243 }
10039 } 7244 }
10040 return scrollables; 7245 return scrollables;
10041 }, 7246 },
10042 7247
10043 /**
10044 * Returns the node that is scrolling. If there is no scrolling,
10045 * returns undefined.
10046 * @param {!Array<Node>} nodes
10047 * @param {number} deltaX Scroll delta on the x-axis
10048 * @param {number} deltaY Scroll delta on the y-axis
10049 * @return {Node|undefined}
10050 * @private
10051 */
10052 _getScrollingNode: function(nodes, deltaX, deltaY) { 7248 _getScrollingNode: function(nodes, deltaX, deltaY) {
10053 // No scroll.
10054 if (!deltaX && !deltaY) { 7249 if (!deltaX && !deltaY) {
10055 return; 7250 return;
10056 } 7251 }
10057 // Check only one axis according to where there is more scroll.
10058 // Prefer vertical to horizontal.
10059 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); 7252 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
10060 for (var i = 0; i < nodes.length; i++) { 7253 for (var i = 0; i < nodes.length; i++) {
10061 var node = nodes[i]; 7254 var node = nodes[i];
10062 var canScroll = false; 7255 var canScroll = false;
10063 if (verticalScroll) { 7256 if (verticalScroll) {
10064 // delta < 0 is scroll up, delta > 0 is scroll down.
10065 canScroll = deltaY < 0 ? node.scrollTop > 0 : 7257 canScroll = deltaY < 0 ? node.scrollTop > 0 :
10066 node.scrollTop < node.scrollHeight - node.clientHeight; 7258 node.scrollTop < node.scrollHeight - node.clientHeight;
10067 } else { 7259 } else {
10068 // delta < 0 is scroll left, delta > 0 is scroll right.
10069 canScroll = deltaX < 0 ? node.scrollLeft > 0 : 7260 canScroll = deltaX < 0 ? node.scrollLeft > 0 :
10070 node.scrollLeft < node.scrollWidth - node.clientWidth; 7261 node.scrollLeft < node.scrollWidth - node.clientWidth;
10071 } 7262 }
10072 if (canScroll) { 7263 if (canScroll) {
10073 return node; 7264 return node;
10074 } 7265 }
10075 } 7266 }
10076 }, 7267 },
10077 7268
10078 /**
10079 * Returns scroll `deltaX` and `deltaY`.
10080 * @param {!Event} event The scroll event
10081 * @return {{
10082 * deltaX: number The x-axis scroll delta (positive: scroll right,
10083 * negative: scroll left, 0: no scroll),
10084 * deltaY: number The y-axis scroll delta (positive: scroll down,
10085 * negative: scroll up, 0: no scroll)
10086 * }} info
10087 * @private
10088 */
10089 _getScrollInfo: function(event) { 7269 _getScrollInfo: function(event) {
10090 var info = { 7270 var info = {
10091 deltaX: event.deltaX, 7271 deltaX: event.deltaX,
10092 deltaY: event.deltaY 7272 deltaY: event.deltaY
10093 }; 7273 };
10094 // Already available.
10095 if ('deltaX' in event) { 7274 if ('deltaX' in event) {
10096 // do nothing, values are already good.
10097 } 7275 }
10098 // Safari has scroll info in `wheelDeltaX/Y`.
10099 else if ('wheelDeltaX' in event) { 7276 else if ('wheelDeltaX' in event) {
10100 info.deltaX = -event.wheelDeltaX; 7277 info.deltaX = -event.wheelDeltaX;
10101 info.deltaY = -event.wheelDeltaY; 7278 info.deltaY = -event.wheelDeltaY;
10102 } 7279 }
10103 // Firefox has scroll info in `detail` and `axis`.
10104 else if ('axis' in event) { 7280 else if ('axis' in event) {
10105 info.deltaX = event.axis === 1 ? event.detail : 0; 7281 info.deltaX = event.axis === 1 ? event.detail : 0;
10106 info.deltaY = event.axis === 2 ? event.detail : 0; 7282 info.deltaY = event.axis === 2 ? event.detail : 0;
10107 } 7283 }
10108 // On mobile devices, calculate scroll direction.
10109 else if (event.targetTouches) { 7284 else if (event.targetTouches) {
10110 var touch = event.targetTouches[0]; 7285 var touch = event.targetTouches[0];
10111 // Touch moves from right to left => scrolling goes right.
10112 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; 7286 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
10113 // Touch moves from down to up => scrolling goes down.
10114 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; 7287 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
10115 } 7288 }
10116 return info; 7289 return info;
10117 } 7290 }
10118 }; 7291 };
10119 })(); 7292 })();
10120 (function() { 7293 (function() {
10121 'use strict'; 7294 'use strict';
10122 7295
10123 Polymer({ 7296 Polymer({
10124 is: 'iron-dropdown', 7297 is: 'iron-dropdown',
10125 7298
10126 behaviors: [ 7299 behaviors: [
10127 Polymer.IronControlState, 7300 Polymer.IronControlState,
10128 Polymer.IronA11yKeysBehavior, 7301 Polymer.IronA11yKeysBehavior,
10129 Polymer.IronOverlayBehavior, 7302 Polymer.IronOverlayBehavior,
10130 Polymer.NeonAnimationRunnerBehavior 7303 Polymer.NeonAnimationRunnerBehavior
10131 ], 7304 ],
10132 7305
10133 properties: { 7306 properties: {
10134 /**
10135 * The orientation against which to align the dropdown content
10136 * horizontally relative to the dropdown trigger.
10137 * Overridden from `Polymer.IronFitBehavior`.
10138 */
10139 horizontalAlign: { 7307 horizontalAlign: {
10140 type: String, 7308 type: String,
10141 value: 'left', 7309 value: 'left',
10142 reflectToAttribute: true 7310 reflectToAttribute: true
10143 }, 7311 },
10144 7312
10145 /**
10146 * The orientation against which to align the dropdown content
10147 * vertically relative to the dropdown trigger.
10148 * Overridden from `Polymer.IronFitBehavior`.
10149 */
10150 verticalAlign: { 7313 verticalAlign: {
10151 type: String, 7314 type: String,
10152 value: 'top', 7315 value: 'top',
10153 reflectToAttribute: true 7316 reflectToAttribute: true
10154 }, 7317 },
10155 7318
10156 /**
10157 * An animation config. If provided, this will be used to animate the
10158 * opening of the dropdown.
10159 */
10160 openAnimationConfig: { 7319 openAnimationConfig: {
10161 type: Object 7320 type: Object
10162 }, 7321 },
10163 7322
10164 /**
10165 * An animation config. If provided, this will be used to animate the
10166 * closing of the dropdown.
10167 */
10168 closeAnimationConfig: { 7323 closeAnimationConfig: {
10169 type: Object 7324 type: Object
10170 }, 7325 },
10171 7326
10172 /**
10173 * If provided, this will be the element that will be focused when
10174 * the dropdown opens.
10175 */
10176 focusTarget: { 7327 focusTarget: {
10177 type: Object 7328 type: Object
10178 }, 7329 },
10179 7330
10180 /**
10181 * Set to true to disable animations when opening and closing the
10182 * dropdown.
10183 */
10184 noAnimations: { 7331 noAnimations: {
10185 type: Boolean, 7332 type: Boolean,
10186 value: false 7333 value: false
10187 }, 7334 },
10188 7335
10189 /**
10190 * By default, the dropdown will constrain scrolling on the page
10191 * to itself when opened.
10192 * Set to true in order to prevent scroll from being constrained
10193 * to the dropdown when it opens.
10194 */
10195 allowOutsideScroll: { 7336 allowOutsideScroll: {
10196 type: Boolean, 7337 type: Boolean,
10197 value: false 7338 value: false
10198 }, 7339 },
10199 7340
10200 /**
10201 * Callback for scroll events.
10202 * @type {Function}
10203 * @private
10204 */
10205 _boundOnCaptureScroll: { 7341 _boundOnCaptureScroll: {
10206 type: Function, 7342 type: Function,
10207 value: function() { 7343 value: function() {
10208 return this._onCaptureScroll.bind(this); 7344 return this._onCaptureScroll.bind(this);
10209 } 7345 }
10210 } 7346 }
10211 }, 7347 },
10212 7348
10213 listeners: { 7349 listeners: {
10214 'neon-animation-finish': '_onNeonAnimationFinish' 7350 'neon-animation-finish': '_onNeonAnimationFinish'
10215 }, 7351 },
10216 7352
10217 observers: [ 7353 observers: [
10218 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign , verticalOffset, horizontalOffset)' 7354 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign , verticalOffset, horizontalOffset)'
10219 ], 7355 ],
10220 7356
10221 /**
10222 * The element that is contained by the dropdown, if any.
10223 */
10224 get containedElement() { 7357 get containedElement() {
10225 return Polymer.dom(this.$.content).getDistributedNodes()[0]; 7358 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10226 }, 7359 },
10227 7360
10228 /**
10229 * The element that should be focused when the dropdown opens.
10230 * @deprecated
10231 */
10232 get _focusTarget() { 7361 get _focusTarget() {
10233 return this.focusTarget || this.containedElement; 7362 return this.focusTarget || this.containedElement;
10234 }, 7363 },
10235 7364
10236 ready: function() { 7365 ready: function() {
10237 // Memoized scrolling position, used to block scrolling outside.
10238 this._scrollTop = 0; 7366 this._scrollTop = 0;
10239 this._scrollLeft = 0; 7367 this._scrollLeft = 0;
10240 // Used to perform a non-blocking refit on scroll.
10241 this._refitOnScrollRAF = null; 7368 this._refitOnScrollRAF = null;
10242 }, 7369 },
10243 7370
10244 detached: function() { 7371 detached: function() {
10245 this.cancelAnimation(); 7372 this.cancelAnimation();
10246 Polymer.IronDropdownScrollManager.removeScrollLock(this); 7373 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10247 }, 7374 },
10248 7375
10249 /**
10250 * Called when the value of `opened` changes.
10251 * Overridden from `IronOverlayBehavior`
10252 */
10253 _openedChanged: function() { 7376 _openedChanged: function() {
10254 if (this.opened && this.disabled) { 7377 if (this.opened && this.disabled) {
10255 this.cancel(); 7378 this.cancel();
10256 } else { 7379 } else {
10257 this.cancelAnimation(); 7380 this.cancelAnimation();
10258 this.sizingTarget = this.containedElement || this.sizingTarget; 7381 this.sizingTarget = this.containedElement || this.sizingTarget;
10259 this._updateAnimationConfig(); 7382 this._updateAnimationConfig();
10260 this._saveScrollPosition(); 7383 this._saveScrollPosition();
10261 if (this.opened) { 7384 if (this.opened) {
10262 document.addEventListener('scroll', this._boundOnCaptureScroll); 7385 document.addEventListener('scroll', this._boundOnCaptureScroll);
10263 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push ScrollLock(this); 7386 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push ScrollLock(this);
10264 } else { 7387 } else {
10265 document.removeEventListener('scroll', this._boundOnCaptureScroll) ; 7388 document.removeEventListener('scroll', this._boundOnCaptureScroll) ;
10266 Polymer.IronDropdownScrollManager.removeScrollLock(this); 7389 Polymer.IronDropdownScrollManager.removeScrollLock(this);
10267 } 7390 }
10268 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments ); 7391 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments );
10269 } 7392 }
10270 }, 7393 },
10271 7394
10272 /**
10273 * Overridden from `IronOverlayBehavior`.
10274 */
10275 _renderOpened: function() { 7395 _renderOpened: function() {
10276 if (!this.noAnimations && this.animationConfig.open) { 7396 if (!this.noAnimations && this.animationConfig.open) {
10277 this.$.contentWrapper.classList.add('animating'); 7397 this.$.contentWrapper.classList.add('animating');
10278 this.playAnimation('open'); 7398 this.playAnimation('open');
10279 } else { 7399 } else {
10280 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments) ; 7400 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments) ;
10281 } 7401 }
10282 }, 7402 },
10283 7403
10284 /**
10285 * Overridden from `IronOverlayBehavior`.
10286 */
10287 _renderClosed: function() { 7404 _renderClosed: function() {
10288 7405
10289 if (!this.noAnimations && this.animationConfig.close) { 7406 if (!this.noAnimations && this.animationConfig.close) {
10290 this.$.contentWrapper.classList.add('animating'); 7407 this.$.contentWrapper.classList.add('animating');
10291 this.playAnimation('close'); 7408 this.playAnimation('close');
10292 } else { 7409 } else {
10293 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments) ; 7410 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments) ;
10294 } 7411 }
10295 }, 7412 },
10296 7413
10297 /**
10298 * Called when animation finishes on the dropdown (when opening or
10299 * closing). Responsible for "completing" the process of opening or
10300 * closing the dropdown by positioning it or setting its display to
10301 * none.
10302 */
10303 _onNeonAnimationFinish: function() { 7414 _onNeonAnimationFinish: function() {
10304 this.$.contentWrapper.classList.remove('animating'); 7415 this.$.contentWrapper.classList.remove('animating');
10305 if (this.opened) { 7416 if (this.opened) {
10306 this._finishRenderOpened(); 7417 this._finishRenderOpened();
10307 } else { 7418 } else {
10308 this._finishRenderClosed(); 7419 this._finishRenderClosed();
10309 } 7420 }
10310 }, 7421 },
10311 7422
10312 _onCaptureScroll: function() { 7423 _onCaptureScroll: function() {
10313 if (!this.allowOutsideScroll) { 7424 if (!this.allowOutsideScroll) {
10314 this._restoreScrollPosition(); 7425 this._restoreScrollPosition();
10315 } else { 7426 } else {
10316 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS crollRAF); 7427 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS crollRAF);
10317 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin d(this)); 7428 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin d(this));
10318 } 7429 }
10319 }, 7430 },
10320 7431
10321 /**
10322 * Memoizes the scroll position of the outside scrolling element.
10323 * @private
10324 */
10325 _saveScrollPosition: function() { 7432 _saveScrollPosition: function() {
10326 if (document.scrollingElement) { 7433 if (document.scrollingElement) {
10327 this._scrollTop = document.scrollingElement.scrollTop; 7434 this._scrollTop = document.scrollingElement.scrollTop;
10328 this._scrollLeft = document.scrollingElement.scrollLeft; 7435 this._scrollLeft = document.scrollingElement.scrollLeft;
10329 } else { 7436 } else {
10330 // Since we don't know if is the body or html, get max.
10331 this._scrollTop = Math.max(document.documentElement.scrollTop, docum ent.body.scrollTop); 7437 this._scrollTop = Math.max(document.documentElement.scrollTop, docum ent.body.scrollTop);
10332 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc ument.body.scrollLeft); 7438 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc ument.body.scrollLeft);
10333 } 7439 }
10334 }, 7440 },
10335 7441
10336 /**
10337 * Resets the scroll position of the outside scrolling element.
10338 * @private
10339 */
10340 _restoreScrollPosition: function() { 7442 _restoreScrollPosition: function() {
10341 if (document.scrollingElement) { 7443 if (document.scrollingElement) {
10342 document.scrollingElement.scrollTop = this._scrollTop; 7444 document.scrollingElement.scrollTop = this._scrollTop;
10343 document.scrollingElement.scrollLeft = this._scrollLeft; 7445 document.scrollingElement.scrollLeft = this._scrollLeft;
10344 } else { 7446 } else {
10345 // Since we don't know if is the body or html, set both.
10346 document.documentElement.scrollTop = this._scrollTop; 7447 document.documentElement.scrollTop = this._scrollTop;
10347 document.documentElement.scrollLeft = this._scrollLeft; 7448 document.documentElement.scrollLeft = this._scrollLeft;
10348 document.body.scrollTop = this._scrollTop; 7449 document.body.scrollTop = this._scrollTop;
10349 document.body.scrollLeft = this._scrollLeft; 7450 document.body.scrollLeft = this._scrollLeft;
10350 } 7451 }
10351 }, 7452 },
10352 7453
10353 /**
10354 * Constructs the final animation config from different properties used
10355 * to configure specific parts of the opening and closing animations.
10356 */
10357 _updateAnimationConfig: function() { 7454 _updateAnimationConfig: function() {
10358 var animations = (this.openAnimationConfig || []).concat(this.closeAni mationConfig || []); 7455 var animations = (this.openAnimationConfig || []).concat(this.closeAni mationConfig || []);
10359 for (var i = 0; i < animations.length; i++) { 7456 for (var i = 0; i < animations.length; i++) {
10360 animations[i].node = this.containedElement; 7457 animations[i].node = this.containedElement;
10361 } 7458 }
10362 this.animationConfig = { 7459 this.animationConfig = {
10363 open: this.openAnimationConfig, 7460 open: this.openAnimationConfig,
10364 close: this.closeAnimationConfig 7461 close: this.closeAnimationConfig
10365 }; 7462 };
10366 }, 7463 },
10367 7464
10368 /**
10369 * Updates the overlay position based on configured horizontal
10370 * and vertical alignment.
10371 */
10372 _updateOverlayPosition: function() { 7465 _updateOverlayPosition: function() {
10373 if (this.isAttached) { 7466 if (this.isAttached) {
10374 // This triggers iron-resize, and iron-overlay-behavior will call re fit if needed.
10375 this.notifyResize(); 7467 this.notifyResize();
10376 } 7468 }
10377 }, 7469 },
10378 7470
10379 /**
10380 * Apply focus to focusTarget or containedElement
10381 */
10382 _applyFocus: function () { 7471 _applyFocus: function () {
10383 var focusTarget = this.focusTarget || this.containedElement; 7472 var focusTarget = this.focusTarget || this.containedElement;
10384 if (focusTarget && this.opened && !this.noAutoFocus) { 7473 if (focusTarget && this.opened && !this.noAutoFocus) {
10385 focusTarget.focus(); 7474 focusTarget.focus();
10386 } else { 7475 } else {
10387 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); 7476 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
10388 } 7477 }
10389 } 7478 }
10390 }); 7479 });
10391 })(); 7480 })();
(...skipping 138 matching lines...) Expand 10 before | Expand all | Expand 10 after
10530 behaviors: [Polymer.IronA11yKeysBehavior], 7619 behaviors: [Polymer.IronA11yKeysBehavior],
10531 7620
10532 properties: { 7621 properties: {
10533 menuOpen: { 7622 menuOpen: {
10534 type: Boolean, 7623 type: Boolean,
10535 observer: 'menuOpenChanged_', 7624 observer: 'menuOpenChanged_',
10536 value: false, 7625 value: false,
10537 notify: true, 7626 notify: true,
10538 }, 7627 },
10539 7628
10540 /**
10541 * The contextual item that this menu was clicked for.
10542 * e.g. the data used to render an item in an <iron-list> or <dom-repeat>
10543 * @type {?Object}
10544 */
10545 itemData: { 7629 itemData: {
10546 type: Object, 7630 type: Object,
10547 value: null, 7631 value: null,
10548 }, 7632 },
10549 7633
10550 /** @override */ 7634 /** @override */
10551 keyEventTarget: { 7635 keyEventTarget: {
10552 type: Object, 7636 type: Object,
10553 value: function() { 7637 value: function() {
10554 return this.$.menu; 7638 return this.$.menu;
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
10596 }, 7680 },
10597 7681
10598 keyBindings: { 7682 keyBindings: {
10599 'tab': 'onTabPressed_', 7683 'tab': 'onTabPressed_',
10600 }, 7684 },
10601 7685
10602 listeners: { 7686 listeners: {
10603 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', 7687 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_',
10604 }, 7688 },
10605 7689
10606 /**
10607 * The last anchor that was used to open a menu. It's necessary for toggling.
10608 * @private {?Element}
10609 */
10610 lastAnchor_: null, 7690 lastAnchor_: null,
10611 7691
10612 /**
10613 * The first focusable child in the menu's light DOM.
10614 * @private {?Element}
10615 */
10616 firstFocus_: null, 7692 firstFocus_: null,
10617 7693
10618 /**
10619 * The last focusable child in the menu's light DOM.
10620 * @private {?Element}
10621 */
10622 lastFocus_: null, 7694 lastFocus_: null,
10623 7695
10624 /** @override */ 7696 /** @override */
10625 attached: function() { 7697 attached: function() {
10626 window.addEventListener('resize', this.closeMenu.bind(this)); 7698 window.addEventListener('resize', this.closeMenu.bind(this));
10627 }, 7699 },
10628 7700
10629 /** Closes the menu. */ 7701 /** Closes the menu. */
10630 closeMenu: function() { 7702 closeMenu: function() {
10631 if (this.root.activeElement == null) { 7703 if (this.root.activeElement == null) {
10632 // Something else has taken focus away from the menu. Do not attempt to
10633 // restore focus to the button which opened the menu.
10634 this.$.dropdown.restoreFocusOnClose = false; 7704 this.$.dropdown.restoreFocusOnClose = false;
10635 } 7705 }
10636 this.menuOpen = false; 7706 this.menuOpen = false;
10637 }, 7707 },
10638 7708
10639 /**
10640 * Opens the menu at the anchor location.
10641 * @param {!Element} anchor The location to display the menu.
10642 * @param {!Object} itemData The contextual item's data.
10643 */
10644 openMenu: function(anchor, itemData) { 7709 openMenu: function(anchor, itemData) {
10645 if (this.lastAnchor_ == anchor && this.menuOpen) 7710 if (this.lastAnchor_ == anchor && this.menuOpen)
10646 return; 7711 return;
10647 7712
10648 if (this.menuOpen) 7713 if (this.menuOpen)
10649 this.closeMenu(); 7714 this.closeMenu();
10650 7715
10651 this.itemData = itemData; 7716 this.itemData = itemData;
10652 this.lastAnchor_ = anchor; 7717 this.lastAnchor_ = anchor;
10653 this.$.dropdown.restoreFocusOnClose = true; 7718 this.$.dropdown.restoreFocusOnClose = true;
10654 7719
10655 var focusableChildren = Polymer.dom(this).querySelectorAll( 7720 var focusableChildren = Polymer.dom(this).querySelectorAll(
10656 '[tabindex]:not([hidden]),button:not([hidden])'); 7721 '[tabindex]:not([hidden]),button:not([hidden])');
10657 if (focusableChildren.length > 0) { 7722 if (focusableChildren.length > 0) {
10658 this.$.dropdown.focusTarget = focusableChildren[0]; 7723 this.$.dropdown.focusTarget = focusableChildren[0];
10659 this.firstFocus_ = focusableChildren[0]; 7724 this.firstFocus_ = focusableChildren[0];
10660 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; 7725 this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
10661 } 7726 }
10662 7727
10663 // Move the menu to the anchor.
10664 this.$.dropdown.positionTarget = anchor; 7728 this.$.dropdown.positionTarget = anchor;
10665 this.menuOpen = true; 7729 this.menuOpen = true;
10666 }, 7730 },
10667 7731
10668 /**
10669 * Toggles the menu for the anchor that is passed in.
10670 * @param {!Element} anchor The location to display the menu.
10671 * @param {!Object} itemData The contextual item's data.
10672 */
10673 toggleMenu: function(anchor, itemData) { 7732 toggleMenu: function(anchor, itemData) {
10674 if (anchor == this.lastAnchor_ && this.menuOpen) 7733 if (anchor == this.lastAnchor_ && this.menuOpen)
10675 this.closeMenu(); 7734 this.closeMenu();
10676 else 7735 else
10677 this.openMenu(anchor, itemData); 7736 this.openMenu(anchor, itemData);
10678 }, 7737 },
10679 7738
10680 /**
10681 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
10682 * the first element with a nonzero tabindex to the last such element.
10683 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
10684 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
10685 * @param {CustomEvent} e
10686 */
10687 onTabPressed_: function(e) { 7739 onTabPressed_: function(e) {
10688 if (!this.firstFocus_ || !this.lastFocus_) 7740 if (!this.firstFocus_ || !this.lastFocus_)
10689 return; 7741 return;
10690 7742
10691 var toFocus; 7743 var toFocus;
10692 var keyEvent = e.detail.keyboardEvent; 7744 var keyEvent = e.detail.keyboardEvent;
10693 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) 7745 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_)
10694 toFocus = this.lastFocus_; 7746 toFocus = this.lastFocus_;
10695 else if (keyEvent.target == this.lastFocus_) 7747 else if (keyEvent.target == this.lastFocus_)
10696 toFocus = this.firstFocus_; 7748 toFocus = this.firstFocus_;
10697 7749
10698 if (!toFocus) 7750 if (!toFocus)
10699 return; 7751 return;
10700 7752
10701 e.preventDefault(); 7753 e.preventDefault();
10702 toFocus.focus(); 7754 toFocus.focus();
10703 }, 7755 },
10704 7756
10705 /**
10706 * Ensure the menu is reset properly when it is closed by the dropdown (eg,
10707 * clicking outside).
10708 * @private
10709 */
10710 menuOpenChanged_: function() { 7757 menuOpenChanged_: function() {
10711 if (!this.menuOpen) { 7758 if (!this.menuOpen) {
10712 this.itemData = null; 7759 this.itemData = null;
10713 this.lastAnchor_ = null; 7760 this.lastAnchor_ = null;
10714 } 7761 }
10715 }, 7762 },
10716 7763
10717 /**
10718 * Prevent focus restoring when tapping outside the menu. This stops the
10719 * focus moving around unexpectedly when closing the menu with the mouse.
10720 * @param {CustomEvent} e
10721 * @private
10722 */
10723 onOverlayCanceled_: function(e) { 7764 onOverlayCanceled_: function(e) {
10724 if (e.detail.type == 'tap') 7765 if (e.detail.type == 'tap')
10725 this.$.dropdown.restoreFocusOnClose = false; 7766 this.$.dropdown.restoreFocusOnClose = false;
10726 }, 7767 },
10727 }); 7768 });
10728 /** @polymerBehavior Polymer.PaperItemBehavior */ 7769 /** @polymerBehavior Polymer.PaperItemBehavior */
10729 Polymer.PaperItemBehaviorImpl = { 7770 Polymer.PaperItemBehaviorImpl = {
10730 hostAttributes: { 7771 hostAttributes: {
10731 role: 'option', 7772 role: 'option',
10732 tabindex: '0' 7773 tabindex: '0'
(...skipping 16 matching lines...) Expand all
10749 Polymer({ 7790 Polymer({
10750 7791
10751 is: 'iron-collapse', 7792 is: 'iron-collapse',
10752 7793
10753 behaviors: [ 7794 behaviors: [
10754 Polymer.IronResizableBehavior 7795 Polymer.IronResizableBehavior
10755 ], 7796 ],
10756 7797
10757 properties: { 7798 properties: {
10758 7799
10759 /**
10760 * If true, the orientation is horizontal; otherwise is vertical.
10761 *
10762 * @attribute horizontal
10763 */
10764 horizontal: { 7800 horizontal: {
10765 type: Boolean, 7801 type: Boolean,
10766 value: false, 7802 value: false,
10767 observer: '_horizontalChanged' 7803 observer: '_horizontalChanged'
10768 }, 7804 },
10769 7805
10770 /**
10771 * Set opened to true to show the collapse element and to false to hide it .
10772 *
10773 * @attribute opened
10774 */
10775 opened: { 7806 opened: {
10776 type: Boolean, 7807 type: Boolean,
10777 value: false, 7808 value: false,
10778 notify: true, 7809 notify: true,
10779 observer: '_openedChanged' 7810 observer: '_openedChanged'
10780 }, 7811 },
10781 7812
10782 /**
10783 * Set noAnimation to true to disable animations
10784 *
10785 * @attribute noAnimation
10786 */
10787 noAnimation: { 7813 noAnimation: {
10788 type: Boolean 7814 type: Boolean
10789 }, 7815 },
10790 7816
10791 }, 7817 },
10792 7818
10793 get dimension() { 7819 get dimension() {
10794 return this.horizontal ? 'width' : 'height'; 7820 return this.horizontal ? 'width' : 'height';
10795 }, 7821 },
10796 7822
10797 /**
10798 * `maxWidth` or `maxHeight`.
10799 * @private
10800 */
10801 get _dimensionMax() { 7823 get _dimensionMax() {
10802 return this.horizontal ? 'maxWidth' : 'maxHeight'; 7824 return this.horizontal ? 'maxWidth' : 'maxHeight';
10803 }, 7825 },
10804 7826
10805 /**
10806 * `max-width` or `max-height`.
10807 * @private
10808 */
10809 get _dimensionMaxCss() { 7827 get _dimensionMaxCss() {
10810 return this.horizontal ? 'max-width' : 'max-height'; 7828 return this.horizontal ? 'max-width' : 'max-height';
10811 }, 7829 },
10812 7830
10813 hostAttributes: { 7831 hostAttributes: {
10814 role: 'group', 7832 role: 'group',
10815 'aria-hidden': 'true', 7833 'aria-hidden': 'true',
10816 'aria-expanded': 'false' 7834 'aria-expanded': 'false'
10817 }, 7835 },
10818 7836
10819 listeners: { 7837 listeners: {
10820 transitionend: '_transitionEnd' 7838 transitionend: '_transitionEnd'
10821 }, 7839 },
10822 7840
10823 attached: function() { 7841 attached: function() {
10824 // It will take care of setting correct classes and styles.
10825 this._transitionEnd(); 7842 this._transitionEnd();
10826 }, 7843 },
10827 7844
10828 /**
10829 * Toggle the opened state.
10830 *
10831 * @method toggle
10832 */
10833 toggle: function() { 7845 toggle: function() {
10834 this.opened = !this.opened; 7846 this.opened = !this.opened;
10835 }, 7847 },
10836 7848
10837 show: function() { 7849 show: function() {
10838 this.opened = true; 7850 this.opened = true;
10839 }, 7851 },
10840 7852
10841 hide: function() { 7853 hide: function() {
10842 this.opened = false; 7854 this.opened = false;
10843 }, 7855 },
10844 7856
10845 /**
10846 * Updates the size of the element.
10847 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop erty value, usually `auto` or `0px`.
10848 * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
10849 */
10850 updateSize: function(size, animated) { 7857 updateSize: function(size, animated) {
10851 // No change!
10852 var curSize = this.style[this._dimensionMax]; 7858 var curSize = this.style[this._dimensionMax];
10853 if (curSize === size || (size === 'auto' && !curSize)) { 7859 if (curSize === size || (size === 'auto' && !curSize)) {
10854 return; 7860 return;
10855 } 7861 }
10856 7862
10857 this._updateTransition(false); 7863 this._updateTransition(false);
10858 // If we can animate, must do some prep work.
10859 if (animated && !this.noAnimation && this._isDisplayed) { 7864 if (animated && !this.noAnimation && this._isDisplayed) {
10860 // Animation will start at the current size.
10861 var startSize = this._calcSize(); 7865 var startSize = this._calcSize();
10862 // For `auto` we must calculate what is the final size for the animation .
10863 // After the transition is done, _transitionEnd will set the size back t o `auto`.
10864 if (size === 'auto') { 7866 if (size === 'auto') {
10865 this.style[this._dimensionMax] = ''; 7867 this.style[this._dimensionMax] = '';
10866 size = this._calcSize(); 7868 size = this._calcSize();
10867 } 7869 }
10868 // Go to startSize without animation.
10869 this.style[this._dimensionMax] = startSize; 7870 this.style[this._dimensionMax] = startSize;
10870 // Force layout to ensure transition will go. Set scrollTop to itself
10871 // so that compilers won't remove it.
10872 this.scrollTop = this.scrollTop; 7871 this.scrollTop = this.scrollTop;
10873 // Enable animation.
10874 this._updateTransition(true); 7872 this._updateTransition(true);
10875 } 7873 }
10876 // Set the final size.
10877 if (size === 'auto') { 7874 if (size === 'auto') {
10878 this.style[this._dimensionMax] = ''; 7875 this.style[this._dimensionMax] = '';
10879 } else { 7876 } else {
10880 this.style[this._dimensionMax] = size; 7877 this.style[this._dimensionMax] = size;
10881 } 7878 }
10882 }, 7879 },
10883 7880
10884 /**
10885 * enableTransition() is deprecated, but left over so it doesn't break exist ing code.
10886 * Please use `noAnimation` property instead.
10887 *
10888 * @method enableTransition
10889 * @deprecated since version 1.0.4
10890 */
10891 enableTransition: function(enabled) { 7881 enableTransition: function(enabled) {
10892 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.'); 7882 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.');
10893 this.noAnimation = !enabled; 7883 this.noAnimation = !enabled;
10894 }, 7884 },
10895 7885
10896 _updateTransition: function(enabled) { 7886 _updateTransition: function(enabled) {
10897 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s' ; 7887 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s' ;
10898 }, 7888 },
10899 7889
10900 _horizontalChanged: function() { 7890 _horizontalChanged: function() {
10901 this.style.transitionProperty = this._dimensionMaxCss; 7891 this.style.transitionProperty = this._dimensionMaxCss;
10902 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma xWidth'; 7892 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma xWidth';
10903 this.style[otherDimension] = ''; 7893 this.style[otherDimension] = '';
10904 this.updateSize(this.opened ? 'auto' : '0px', false); 7894 this.updateSize(this.opened ? 'auto' : '0px', false);
10905 }, 7895 },
10906 7896
10907 _openedChanged: function() { 7897 _openedChanged: function() {
10908 this.setAttribute('aria-expanded', this.opened); 7898 this.setAttribute('aria-expanded', this.opened);
10909 this.setAttribute('aria-hidden', !this.opened); 7899 this.setAttribute('aria-hidden', !this.opened);
10910 7900
10911 this.toggleClass('iron-collapse-closed', false); 7901 this.toggleClass('iron-collapse-closed', false);
10912 this.toggleClass('iron-collapse-opened', false); 7902 this.toggleClass('iron-collapse-opened', false);
10913 this.updateSize(this.opened ? 'auto' : '0px', true); 7903 this.updateSize(this.opened ? 'auto' : '0px', true);
10914 7904
10915 // Focus the current collapse.
10916 if (this.opened) { 7905 if (this.opened) {
10917 this.focus(); 7906 this.focus();
10918 } 7907 }
10919 if (this.noAnimation) { 7908 if (this.noAnimation) {
10920 this._transitionEnd(); 7909 this._transitionEnd();
10921 } 7910 }
10922 }, 7911 },
10923 7912
10924 _transitionEnd: function() { 7913 _transitionEnd: function() {
10925 if (this.opened) { 7914 if (this.opened) {
10926 this.style[this._dimensionMax] = ''; 7915 this.style[this._dimensionMax] = '';
10927 } 7916 }
10928 this.toggleClass('iron-collapse-closed', !this.opened); 7917 this.toggleClass('iron-collapse-closed', !this.opened);
10929 this.toggleClass('iron-collapse-opened', this.opened); 7918 this.toggleClass('iron-collapse-opened', this.opened);
10930 this._updateTransition(false); 7919 this._updateTransition(false);
10931 this.notifyResize(); 7920 this.notifyResize();
10932 }, 7921 },
10933 7922
10934 /**
10935 * Simplistic heuristic to detect if element has a parent with display: none
10936 *
10937 * @private
10938 */
10939 get _isDisplayed() { 7923 get _isDisplayed() {
10940 var rect = this.getBoundingClientRect(); 7924 var rect = this.getBoundingClientRect();
10941 for (var prop in rect) { 7925 for (var prop in rect) {
10942 if (rect[prop] !== 0) return true; 7926 if (rect[prop] !== 0) return true;
10943 } 7927 }
10944 return false; 7928 return false;
10945 }, 7929 },
10946 7930
10947 _calcSize: function() { 7931 _calcSize: function() {
10948 return this.getBoundingClientRect()[this.dimension] + 'px'; 7932 return this.getBoundingClientRect()[this.dimension] + 'px';
10949 } 7933 }
10950 7934
10951 }); 7935 });
10952 /**
10953 Polymer.IronFormElementBehavior enables a custom element to be included
10954 in an `iron-form`.
10955
10956 @demo demo/index.html
10957 @polymerBehavior
10958 */
10959 Polymer.IronFormElementBehavior = { 7936 Polymer.IronFormElementBehavior = {
10960 7937
10961 properties: { 7938 properties: {
10962 /**
10963 * Fired when the element is added to an `iron-form`.
10964 *
10965 * @event iron-form-element-register
10966 */
10967 7939
10968 /**
10969 * Fired when the element is removed from an `iron-form`.
10970 *
10971 * @event iron-form-element-unregister
10972 */
10973 7940
10974 /**
10975 * The name of this element.
10976 */
10977 name: { 7941 name: {
10978 type: String 7942 type: String
10979 }, 7943 },
10980 7944
10981 /**
10982 * The value for this element.
10983 */
10984 value: { 7945 value: {
10985 notify: true, 7946 notify: true,
10986 type: String 7947 type: String
10987 }, 7948 },
10988 7949
10989 /**
10990 * Set to true to mark the input as required. If used in a form, a
10991 * custom element that uses this behavior should also use
10992 * Polymer.IronValidatableBehavior and define a custom validation method.
10993 * Otherwise, a `required` element will always be considered valid.
10994 * It's also strongly recommended to provide a visual style for the elemen t
10995 * when its value is invalid.
10996 */
10997 required: { 7950 required: {
10998 type: Boolean, 7951 type: Boolean,
10999 value: false 7952 value: false
11000 }, 7953 },
11001 7954
11002 /**
11003 * The form that the element is registered to.
11004 */
11005 _parentForm: { 7955 _parentForm: {
11006 type: Object 7956 type: Object
11007 } 7957 }
11008 }, 7958 },
11009 7959
11010 attached: function() { 7960 attached: function() {
11011 // Note: the iron-form that this element belongs to will set this
11012 // element's _parentForm property when handling this event.
11013 this.fire('iron-form-element-register'); 7961 this.fire('iron-form-element-register');
11014 }, 7962 },
11015 7963
11016 detached: function() { 7964 detached: function() {
11017 if (this._parentForm) { 7965 if (this._parentForm) {
11018 this._parentForm.fire('iron-form-element-unregister', {target: this}); 7966 this._parentForm.fire('iron-form-element-unregister', {target: this});
11019 } 7967 }
11020 } 7968 }
11021 7969
11022 }; 7970 };
11023 /**
11024 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element
11025 * that has a `checked` property, which can be used for validation if the
11026 * element is also `required`. Element instances implementing this behavior
11027 * will also be registered for use in an `iron-form` element.
11028 *
11029 * @demo demo/index.html
11030 * @polymerBehavior Polymer.IronCheckedElementBehavior
11031 */
11032 Polymer.IronCheckedElementBehaviorImpl = { 7971 Polymer.IronCheckedElementBehaviorImpl = {
11033 7972
11034 properties: { 7973 properties: {
11035 /**
11036 * Fired when the checked state changes.
11037 *
11038 * @event iron-change
11039 */
11040 7974
11041 /**
11042 * Gets or sets the state, `true` is checked and `false` is unchecked.
11043 */
11044 checked: { 7975 checked: {
11045 type: Boolean, 7976 type: Boolean,
11046 value: false, 7977 value: false,
11047 reflectToAttribute: true, 7978 reflectToAttribute: true,
11048 notify: true, 7979 notify: true,
11049 observer: '_checkedChanged' 7980 observer: '_checkedChanged'
11050 }, 7981 },
11051 7982
11052 /**
11053 * If true, the button toggles the active state with each tap or press
11054 * of the spacebar.
11055 */
11056 toggles: { 7983 toggles: {
11057 type: Boolean, 7984 type: Boolean,
11058 value: true, 7985 value: true,
11059 reflectToAttribute: true 7986 reflectToAttribute: true
11060 }, 7987 },
11061 7988
11062 /* Overriden from Polymer.IronFormElementBehavior */ 7989 /* Overriden from Polymer.IronFormElementBehavior */
11063 value: { 7990 value: {
11064 type: String, 7991 type: String,
11065 value: 'on', 7992 value: 'on',
11066 observer: '_valueChanged' 7993 observer: '_valueChanged'
11067 } 7994 }
11068 }, 7995 },
11069 7996
11070 observers: [ 7997 observers: [
11071 '_requiredChanged(required)' 7998 '_requiredChanged(required)'
11072 ], 7999 ],
11073 8000
11074 created: function() { 8001 created: function() {
11075 // Used by `iron-form` to handle the case that an element with this behavi or
11076 // doesn't have a role of 'checkbox' or 'radio', but should still only be
11077 // included when the form is serialized if `this.checked === true`.
11078 this._hasIronCheckedElementBehavior = true; 8002 this._hasIronCheckedElementBehavior = true;
11079 }, 8003 },
11080 8004
11081 /**
11082 * Returns false if the element is required and not checked, and true otherw ise.
11083 * @param {*=} _value Ignored.
11084 * @return {boolean} true if `required` is false or if `checked` is true.
11085 */
11086 _getValidity: function(_value) { 8005 _getValidity: function(_value) {
11087 return this.disabled || !this.required || this.checked; 8006 return this.disabled || !this.required || this.checked;
11088 }, 8007 },
11089 8008
11090 /**
11091 * Update the aria-required label when `required` is changed.
11092 */
11093 _requiredChanged: function() { 8009 _requiredChanged: function() {
11094 if (this.required) { 8010 if (this.required) {
11095 this.setAttribute('aria-required', 'true'); 8011 this.setAttribute('aria-required', 'true');
11096 } else { 8012 } else {
11097 this.removeAttribute('aria-required'); 8013 this.removeAttribute('aria-required');
11098 } 8014 }
11099 }, 8015 },
11100 8016
11101 /**
11102 * Fire `iron-changed` when the checked state changes.
11103 */
11104 _checkedChanged: function() { 8017 _checkedChanged: function() {
11105 this.active = this.checked; 8018 this.active = this.checked;
11106 this.fire('iron-change'); 8019 this.fire('iron-change');
11107 }, 8020 },
11108 8021
11109 /**
11110 * Reset value to 'on' if it is set to `undefined`.
11111 */
11112 _valueChanged: function() { 8022 _valueChanged: function() {
11113 if (this.value === undefined || this.value === null) { 8023 if (this.value === undefined || this.value === null) {
11114 this.value = 'on'; 8024 this.value = 'on';
11115 } 8025 }
11116 } 8026 }
11117 }; 8027 };
11118 8028
11119 /** @polymerBehavior Polymer.IronCheckedElementBehavior */ 8029 /** @polymerBehavior Polymer.IronCheckedElementBehavior */
11120 Polymer.IronCheckedElementBehavior = [ 8030 Polymer.IronCheckedElementBehavior = [
11121 Polymer.IronFormElementBehavior, 8031 Polymer.IronFormElementBehavior,
11122 Polymer.IronValidatableBehavior, 8032 Polymer.IronValidatableBehavior,
11123 Polymer.IronCheckedElementBehaviorImpl 8033 Polymer.IronCheckedElementBehaviorImpl
11124 ]; 8034 ];
11125 /**
11126 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element
11127 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio r`
11128 * and is compatible with having a ripple effect.
11129 * @polymerBehavior Polymer.PaperCheckedElementBehavior
11130 */
11131 Polymer.PaperCheckedElementBehaviorImpl = { 8035 Polymer.PaperCheckedElementBehaviorImpl = {
11132 /**
11133 * Synchronizes the element's checked state with its ripple effect.
11134 */
11135 _checkedChanged: function() { 8036 _checkedChanged: function() {
11136 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); 8037 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
11137 if (this.hasRipple()) { 8038 if (this.hasRipple()) {
11138 if (this.checked) { 8039 if (this.checked) {
11139 this._ripple.setAttribute('checked', ''); 8040 this._ripple.setAttribute('checked', '');
11140 } else { 8041 } else {
11141 this._ripple.removeAttribute('checked'); 8042 this._ripple.removeAttribute('checked');
11142 } 8043 }
11143 } 8044 }
11144 }, 8045 },
11145 8046
11146 /**
11147 * Synchronizes the element's `active` and `checked` state.
11148 */
11149 _buttonStateChanged: function() { 8047 _buttonStateChanged: function() {
11150 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); 8048 Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
11151 if (this.disabled) { 8049 if (this.disabled) {
11152 return; 8050 return;
11153 } 8051 }
11154 if (this.isAttached) { 8052 if (this.isAttached) {
11155 this.checked = this.active; 8053 this.checked = this.active;
11156 } 8054 }
11157 } 8055 }
11158 }; 8056 };
(...skipping 11 matching lines...) Expand all
11170 Polymer.PaperCheckedElementBehavior 8068 Polymer.PaperCheckedElementBehavior
11171 ], 8069 ],
11172 8070
11173 hostAttributes: { 8071 hostAttributes: {
11174 role: 'checkbox', 8072 role: 'checkbox',
11175 'aria-checked': false, 8073 'aria-checked': false,
11176 tabindex: 0 8074 tabindex: 0
11177 }, 8075 },
11178 8076
11179 properties: { 8077 properties: {
11180 /**
11181 * Fired when the checked state changes due to user interaction.
11182 *
11183 * @event change
11184 */
11185 8078
11186 /**
11187 * Fired when the checked state changes.
11188 *
11189 * @event iron-change
11190 */
11191 ariaActiveAttribute: { 8079 ariaActiveAttribute: {
11192 type: String, 8080 type: String,
11193 value: 'aria-checked' 8081 value: 'aria-checked'
11194 } 8082 }
11195 }, 8083 },
11196 8084
11197 _computeCheckboxClass: function(checked, invalid) { 8085 _computeCheckboxClass: function(checked, invalid) {
11198 var className = ''; 8086 var className = '';
11199 if (checked) { 8087 if (checked) {
11200 className += 'checked '; 8088 className += 'checked ';
11201 } 8089 }
11202 if (invalid) { 8090 if (invalid) {
11203 className += 'invalid'; 8091 className += 'invalid';
11204 } 8092 }
11205 return className; 8093 return className;
11206 }, 8094 },
11207 8095
11208 _computeCheckmarkClass: function(checked) { 8096 _computeCheckmarkClass: function(checked) {
11209 return checked ? '' : 'hidden'; 8097 return checked ? '' : 'hidden';
11210 }, 8098 },
11211 8099
11212 // create ripple inside the checkboxContainer
11213 _createRipple: function() { 8100 _createRipple: function() {
11214 this._rippleContainer = this.$.checkboxContainer; 8101 this._rippleContainer = this.$.checkboxContainer;
11215 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); 8102 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
11216 } 8103 }
11217 8104
11218 }); 8105 });
11219 Polymer({ 8106 Polymer({
11220 is: 'paper-icon-button-light', 8107 is: 'paper-icon-button-light',
11221 extends: 'button', 8108 extends: 'button',
11222 8109
11223 behaviors: [ 8110 behaviors: [
11224 Polymer.PaperRippleBehavior 8111 Polymer.PaperRippleBehavior
11225 ], 8112 ],
11226 8113
11227 listeners: { 8114 listeners: {
11228 'down': '_rippleDown', 8115 'down': '_rippleDown',
11229 'up': '_rippleUp', 8116 'up': '_rippleUp',
11230 'focus': '_rippleDown', 8117 'focus': '_rippleDown',
11231 'blur': '_rippleUp', 8118 'blur': '_rippleUp',
11232 }, 8119 },
11233 8120
11234 _rippleDown: function() { 8121 _rippleDown: function() {
11235 this.getRipple().downAction(); 8122 this.getRipple().downAction();
11236 }, 8123 },
11237 8124
11238 _rippleUp: function() { 8125 _rippleUp: function() {
11239 this.getRipple().upAction(); 8126 this.getRipple().upAction();
11240 }, 8127 },
11241 8128
11242 /**
11243 * @param {...*} var_args
11244 */
11245 ensureRipple: function(var_args) { 8129 ensureRipple: function(var_args) {
11246 var lastRipple = this._ripple; 8130 var lastRipple = this._ripple;
11247 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); 8131 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
11248 if (this._ripple && this._ripple !== lastRipple) { 8132 if (this._ripple && this._ripple !== lastRipple) {
11249 this._ripple.center = true; 8133 this._ripple.center = true;
11250 this._ripple.classList.add('circle'); 8134 this._ripple.classList.add('circle');
11251 } 8135 }
11252 } 8136 }
11253 }); 8137 });
11254 // Copyright 2016 The Chromium Authors. All rights reserved. 8138 // Copyright 2016 The Chromium Authors. All rights reserved.
11255 // Use of this source code is governed by a BSD-style license that can be 8139 // Use of this source code is governed by a BSD-style license that can be
11256 // found in the LICENSE file. 8140 // found in the LICENSE file.
11257 8141
11258 cr.define('cr.icon', function() { 8142 cr.define('cr.icon', function() {
11259 /**
11260 * @return {!Array<number>} The scale factors supported by this platform for
11261 * webui resources.
11262 */
11263 function getSupportedScaleFactors() { 8143 function getSupportedScaleFactors() {
11264 var supportedScaleFactors = []; 8144 var supportedScaleFactors = [];
11265 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { 8145 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
11266 // All desktop platforms support zooming which also updates the
11267 // renderer's device scale factors (a.k.a devicePixelRatio), and
11268 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
11269 // image-set on these platforms so that the renderer can pick the
11270 // closest image for the current device scale factor.
11271 supportedScaleFactors.push(1); 8146 supportedScaleFactors.push(1);
11272 supportedScaleFactors.push(2); 8147 supportedScaleFactors.push(2);
11273 } else { 8148 } else {
11274 // For other platforms that use fixed device scale factor, use
11275 // the window's device pixel ratio.
11276 // TODO(oshima): Investigate if Android/iOS need to use image-set.
11277 supportedScaleFactors.push(window.devicePixelRatio); 8149 supportedScaleFactors.push(window.devicePixelRatio);
11278 } 8150 }
11279 return supportedScaleFactors; 8151 return supportedScaleFactors;
11280 } 8152 }
11281 8153
11282 /**
11283 * Returns the URL of the image, or an image set of URLs for the profile
11284 * avatar. Default avatars have resources available for multiple scalefactors,
11285 * whereas the GAIA profile image only comes in one size.
11286 *
11287 * @param {string} path The path of the image.
11288 * @return {string} The url, or an image set of URLs of the avatar image.
11289 */
11290 function getProfileAvatarIcon(path) { 8154 function getProfileAvatarIcon(path) {
11291 var chromeThemePath = 'chrome://theme'; 8155 var chromeThemePath = 'chrome://theme';
11292 var isDefaultAvatar = 8156 var isDefaultAvatar =
11293 (path.slice(0, chromeThemePath.length) == chromeThemePath); 8157 (path.slice(0, chromeThemePath.length) == chromeThemePath);
11294 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); 8158 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path);
11295 } 8159 }
11296 8160
11297 /**
11298 * Generates a CSS -webkit-image-set for a chrome:// url.
11299 * An entry in the image set is added for each of getSupportedScaleFactors().
11300 * The scale-factor-specific url is generated by replacing the first instance
11301 * of 'scalefactor' in |path| with the numeric scale factor.
11302 * @param {string} path The URL to generate an image set for.
11303 * 'scalefactor' should be a substring of |path|.
11304 * @return {string} The CSS -webkit-image-set.
11305 */
11306 function imageset(path) { 8161 function imageset(path) {
11307 var supportedScaleFactors = getSupportedScaleFactors(); 8162 var supportedScaleFactors = getSupportedScaleFactors();
11308 8163
11309 var replaceStartIndex = path.indexOf('scalefactor'); 8164 var replaceStartIndex = path.indexOf('scalefactor');
11310 if (replaceStartIndex < 0) 8165 if (replaceStartIndex < 0)
11311 return url(path); 8166 return url(path);
11312 8167
11313 var s = ''; 8168 var s = '';
11314 for (var i = 0; i < supportedScaleFactors.length; ++i) { 8169 for (var i = 0; i < supportedScaleFactors.length; ++i) {
11315 var scaleFactor = supportedScaleFactors[i]; 8170 var scaleFactor = supportedScaleFactors[i];
11316 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + 8171 var pathWithScaleFactor = path.substr(0, replaceStartIndex) +
11317 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length); 8172 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
11318 8173
11319 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; 8174 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
11320 8175
11321 if (i != supportedScaleFactors.length - 1) 8176 if (i != supportedScaleFactors.length - 1)
11322 s += ', '; 8177 s += ', ';
11323 } 8178 }
11324 return '-webkit-image-set(' + s + ')'; 8179 return '-webkit-image-set(' + s + ')';
11325 } 8180 }
11326 8181
11327 /**
11328 * A regular expression for identifying favicon URLs.
11329 * @const {!RegExp}
11330 */
11331 var FAVICON_URL_REGEX = /\.ico$/i; 8182 var FAVICON_URL_REGEX = /\.ico$/i;
11332 8183
11333 /**
11334 * Creates a CSS -webkit-image-set for a favicon request.
11335 * @param {string} url Either the URL of the original page or of the favicon
11336 * itself.
11337 * @param {number=} opt_size Optional preferred size of the favicon.
11338 * @param {string=} opt_type Optional type of favicon to request. Valid values
11339 * are 'favicon' and 'touch-icon'. Default is 'favicon'.
11340 * @return {string} -webkit-image-set for the favicon.
11341 */
11342 function getFaviconImageSet(url, opt_size, opt_type) { 8184 function getFaviconImageSet(url, opt_size, opt_type) {
11343 var size = opt_size || 16; 8185 var size = opt_size || 16;
11344 var type = opt_type || 'favicon'; 8186 var type = opt_type || 'favicon';
11345 8187
11346 return imageset( 8188 return imageset(
11347 'chrome://' + type + '/size/' + size + '@scalefactorx/' + 8189 'chrome://' + type + '/size/' + size + '@scalefactorx/' +
11348 // Note: Literal 'iconurl' must match |kIconURLParameter| in
11349 // components/favicon_base/favicon_url_parser.cc.
11350 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); 8190 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
11351 } 8191 }
11352 8192
11353 return { 8193 return {
11354 getSupportedScaleFactors: getSupportedScaleFactors, 8194 getSupportedScaleFactors: getSupportedScaleFactors,
11355 getProfileAvatarIcon: getProfileAvatarIcon, 8195 getProfileAvatarIcon: getProfileAvatarIcon,
11356 getFaviconImageSet: getFaviconImageSet, 8196 getFaviconImageSet: getFaviconImageSet,
11357 }; 8197 };
11358 }); 8198 });
11359 // Copyright 2016 The Chromium Authors. All rights reserved. 8199 // Copyright 2016 The Chromium Authors. All rights reserved.
11360 // Use of this source code is governed by a BSD-style license that can be 8200 // Use of this source code is governed by a BSD-style license that can be
11361 // found in the LICENSE file. 8201 // found in the LICENSE file.
11362 8202
11363 /**
11364 * @fileoverview Defines a singleton object, md_history.BrowserService, which
11365 * provides access to chrome.send APIs.
11366 */
11367 8203
11368 cr.define('md_history', function() { 8204 cr.define('md_history', function() {
11369 /** @constructor */ 8205 /** @constructor */
11370 function BrowserService() { 8206 function BrowserService() {
11371 /** @private {Array<!HistoryEntry>} */ 8207 /** @private {Array<!HistoryEntry>} */
11372 this.pendingDeleteItems_ = null; 8208 this.pendingDeleteItems_ = null;
11373 /** @private {PromiseResolver} */ 8209 /** @private {PromiseResolver} */
11374 this.pendingDeletePromise_ = null; 8210 this.pendingDeletePromise_ = null;
11375 } 8211 }
11376 8212
11377 BrowserService.prototype = { 8213 BrowserService.prototype = {
11378 /**
11379 * @param {!Array<!HistoryEntry>} items
11380 * @return {Promise<!Array<!HistoryEntry>>}
11381 */
11382 deleteItems: function(items) { 8214 deleteItems: function(items) {
11383 if (this.pendingDeleteItems_ != null) { 8215 if (this.pendingDeleteItems_ != null) {
11384 // There's already a deletion in progress, reject immediately.
11385 return new Promise(function(resolve, reject) { reject(items); }); 8216 return new Promise(function(resolve, reject) { reject(items); });
11386 } 8217 }
11387 8218
11388 var removalList = items.map(function(item) { 8219 var removalList = items.map(function(item) {
11389 return { 8220 return {
11390 url: item.url, 8221 url: item.url,
11391 timestamps: item.allTimestamps 8222 timestamps: item.allTimestamps
11392 }; 8223 };
11393 }); 8224 });
11394 8225
11395 this.pendingDeleteItems_ = items; 8226 this.pendingDeleteItems_ = items;
11396 this.pendingDeletePromise_ = new PromiseResolver(); 8227 this.pendingDeletePromise_ = new PromiseResolver();
11397 8228
11398 chrome.send('removeVisits', removalList); 8229 chrome.send('removeVisits', removalList);
11399 8230
11400 return this.pendingDeletePromise_.promise; 8231 return this.pendingDeletePromise_.promise;
11401 }, 8232 },
11402 8233
11403 /**
11404 * @param {!string} url
11405 */
11406 removeBookmark: function(url) { 8234 removeBookmark: function(url) {
11407 chrome.send('removeBookmark', [url]); 8235 chrome.send('removeBookmark', [url]);
11408 }, 8236 },
11409 8237
11410 /**
11411 * @param {string} sessionTag
11412 */
11413 openForeignSessionAllTabs: function(sessionTag) { 8238 openForeignSessionAllTabs: function(sessionTag) {
11414 chrome.send('openForeignSession', [sessionTag]); 8239 chrome.send('openForeignSession', [sessionTag]);
11415 }, 8240 },
11416 8241
11417 /**
11418 * @param {string} sessionTag
11419 * @param {number} windowId
11420 * @param {number} tabId
11421 * @param {MouseEvent} e
11422 */
11423 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { 8242 openForeignSessionTab: function(sessionTag, windowId, tabId, e) {
11424 chrome.send('openForeignSession', [ 8243 chrome.send('openForeignSession', [
11425 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, 8244 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey,
11426 e.ctrlKey, e.metaKey, e.shiftKey 8245 e.ctrlKey, e.metaKey, e.shiftKey
11427 ]); 8246 ]);
11428 }, 8247 },
11429 8248
11430 /**
11431 * @param {string} sessionTag
11432 */
11433 deleteForeignSession: function(sessionTag) { 8249 deleteForeignSession: function(sessionTag) {
11434 chrome.send('deleteForeignSession', [sessionTag]); 8250 chrome.send('deleteForeignSession', [sessionTag]);
11435 }, 8251 },
11436 8252
11437 openClearBrowsingData: function() { 8253 openClearBrowsingData: function() {
11438 chrome.send('clearBrowsingData'); 8254 chrome.send('clearBrowsingData');
11439 }, 8255 },
11440 8256
11441 /**
11442 * Record an action in UMA.
11443 * @param {string} actionDesc The name of the action to be logged.
11444 */
11445 recordAction: function(actionDesc) { 8257 recordAction: function(actionDesc) {
11446 chrome.send('metricsHandler:recordAction', [actionDesc]); 8258 chrome.send('metricsHandler:recordAction', [actionDesc]);
11447 }, 8259 },
11448 8260
11449 /**
11450 * @param {boolean} successful
11451 * @private
11452 */
11453 resolveDelete_: function(successful) { 8261 resolveDelete_: function(successful) {
11454 if (this.pendingDeleteItems_ == null || 8262 if (this.pendingDeleteItems_ == null ||
11455 this.pendingDeletePromise_ == null) { 8263 this.pendingDeletePromise_ == null) {
11456 return; 8264 return;
11457 } 8265 }
11458 8266
11459 if (successful) 8267 if (successful)
11460 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_); 8268 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_);
11461 else 8269 else
11462 this.pendingDeletePromise_.reject(this.pendingDeleteItems_); 8270 this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
11463 8271
11464 this.pendingDeleteItems_ = null; 8272 this.pendingDeleteItems_ = null;
11465 this.pendingDeletePromise_ = null; 8273 this.pendingDeletePromise_ = null;
11466 }, 8274 },
11467 }; 8275 };
11468 8276
11469 cr.addSingletonGetter(BrowserService); 8277 cr.addSingletonGetter(BrowserService);
11470 8278
11471 return {BrowserService: BrowserService}; 8279 return {BrowserService: BrowserService};
11472 }); 8280 });
11473 8281
11474 /**
11475 * Called by the history backend when deletion was succesful.
11476 */
11477 function deleteComplete() { 8282 function deleteComplete() {
11478 md_history.BrowserService.getInstance().resolveDelete_(true); 8283 md_history.BrowserService.getInstance().resolveDelete_(true);
11479 } 8284 }
11480 8285
11481 /**
11482 * Called by the history backend when the deletion failed.
11483 */
11484 function deleteFailed() { 8286 function deleteFailed() {
11485 md_history.BrowserService.getInstance().resolveDelete_(false); 8287 md_history.BrowserService.getInstance().resolveDelete_(false);
11486 }; 8288 };
11487 // Copyright 2016 The Chromium Authors. All rights reserved. 8289 // Copyright 2016 The Chromium Authors. All rights reserved.
11488 // Use of this source code is governed by a BSD-style license that can be 8290 // Use of this source code is governed by a BSD-style license that can be
11489 // found in the LICENSE file. 8291 // found in the LICENSE file.
11490 8292
11491 Polymer({ 8293 Polymer({
11492 is: 'history-searched-label', 8294 is: 'history-searched-label',
11493 8295
11494 properties: { 8296 properties: {
11495 // The text to show in this label.
11496 title: String, 8297 title: String,
11497 8298
11498 // The search term to bold within the title.
11499 searchTerm: String, 8299 searchTerm: String,
11500 }, 8300 },
11501 8301
11502 observers: ['setSearchedTextToBold_(title, searchTerm)'], 8302 observers: ['setSearchedTextToBold_(title, searchTerm)'],
11503 8303
11504 /**
11505 * Updates the page title. If a search term is specified, highlights any
11506 * occurrences of the search term in bold.
11507 * @private
11508 */
11509 setSearchedTextToBold_: function() { 8304 setSearchedTextToBold_: function() {
11510 var i = 0; 8305 var i = 0;
11511 var titleElem = this.$.container; 8306 var titleElem = this.$.container;
11512 var titleText = this.title; 8307 var titleText = this.title;
11513 8308
11514 if (this.searchTerm == '' || this.searchTerm == null) { 8309 if (this.searchTerm == '' || this.searchTerm == null) {
11515 titleElem.textContent = titleText; 8310 titleElem.textContent = titleText;
11516 return; 8311 return;
11517 } 8312 }
11518 8313
11519 var re = new RegExp(quoteString(this.searchTerm), 'gim'); 8314 var re = new RegExp(quoteString(this.searchTerm), 'gim');
11520 var match; 8315 var match;
11521 titleElem.textContent = ''; 8316 titleElem.textContent = '';
11522 while (match = re.exec(titleText)) { 8317 while (match = re.exec(titleText)) {
11523 if (match.index > i) 8318 if (match.index > i)
11524 titleElem.appendChild(document.createTextNode( 8319 titleElem.appendChild(document.createTextNode(
11525 titleText.slice(i, match.index))); 8320 titleText.slice(i, match.index)));
11526 i = re.lastIndex; 8321 i = re.lastIndex;
11527 // Mark the highlighted text in bold.
11528 var b = document.createElement('b'); 8322 var b = document.createElement('b');
11529 b.textContent = titleText.substring(match.index, i); 8323 b.textContent = titleText.substring(match.index, i);
11530 titleElem.appendChild(b); 8324 titleElem.appendChild(b);
11531 } 8325 }
11532 if (i < titleText.length) 8326 if (i < titleText.length)
11533 titleElem.appendChild( 8327 titleElem.appendChild(
11534 document.createTextNode(titleText.slice(i))); 8328 document.createTextNode(titleText.slice(i)));
11535 }, 8329 },
11536 }); 8330 });
11537 // Copyright 2015 The Chromium Authors. All rights reserved. 8331 // Copyright 2015 The Chromium Authors. All rights reserved.
11538 // Use of this source code is governed by a BSD-style license that can be 8332 // Use of this source code is governed by a BSD-style license that can be
11539 // found in the LICENSE file. 8333 // found in the LICENSE file.
11540 8334
11541 cr.define('md_history', function() { 8335 cr.define('md_history', function() {
11542 var HistoryItem = Polymer({ 8336 var HistoryItem = Polymer({
11543 is: 'history-item', 8337 is: 'history-item',
11544 8338
11545 properties: { 8339 properties: {
11546 // Underlying HistoryEntry data for this item. Contains read-only fields
11547 // from the history backend, as well as fields computed by history-list.
11548 item: {type: Object, observer: 'showIcon_'}, 8340 item: {type: Object, observer: 'showIcon_'},
11549 8341
11550 // Search term used to obtain this history-item.
11551 searchTerm: {type: String}, 8342 searchTerm: {type: String},
11552 8343
11553 selected: {type: Boolean, notify: true}, 8344 selected: {type: Boolean, notify: true},
11554 8345
11555 isFirstItem: {type: Boolean, reflectToAttribute: true}, 8346 isFirstItem: {type: Boolean, reflectToAttribute: true},
11556 8347
11557 isCardStart: {type: Boolean, reflectToAttribute: true}, 8348 isCardStart: {type: Boolean, reflectToAttribute: true},
11558 8349
11559 isCardEnd: {type: Boolean, reflectToAttribute: true}, 8350 isCardEnd: {type: Boolean, reflectToAttribute: true},
11560 8351
11561 // True if the item is being displayed embedded in another element and
11562 // should not manage its own borders or size.
11563 embedded: {type: Boolean, reflectToAttribute: true}, 8352 embedded: {type: Boolean, reflectToAttribute: true},
11564 8353
11565 hasTimeGap: {type: Boolean}, 8354 hasTimeGap: {type: Boolean},
11566 8355
11567 numberOfItems: {type: Number}, 8356 numberOfItems: {type: Number},
11568 8357
11569 // The path of this history item inside its parent.
11570 path: String, 8358 path: String,
11571 }, 8359 },
11572 8360
11573 /**
11574 * When a history-item is selected the toolbar is notified and increases
11575 * or decreases its count of selected items accordingly.
11576 * @param {MouseEvent} e
11577 * @private
11578 */
11579 onCheckboxSelected_: function(e) { 8361 onCheckboxSelected_: function(e) {
11580 // TODO(calamity): Fire this event whenever |selected| changes.
11581 this.fire('history-checkbox-select', { 8362 this.fire('history-checkbox-select', {
11582 element: this, 8363 element: this,
11583 shiftKey: e.shiftKey, 8364 shiftKey: e.shiftKey,
11584 }); 8365 });
11585 e.preventDefault(); 8366 e.preventDefault();
11586 }, 8367 },
11587 8368
11588 /**
11589 * @param {MouseEvent} e
11590 * @private
11591 */
11592 onCheckboxMousedown_: function(e) { 8369 onCheckboxMousedown_: function(e) {
11593 // Prevent shift clicking a checkbox from selecting text.
11594 if (e.shiftKey) 8370 if (e.shiftKey)
11595 e.preventDefault(); 8371 e.preventDefault();
11596 }, 8372 },
11597 8373
11598 /**
11599 * Remove bookmark of current item when bookmark-star is clicked.
11600 * @private
11601 */
11602 onRemoveBookmarkTap_: function() { 8374 onRemoveBookmarkTap_: function() {
11603 if (!this.item.starred) 8375 if (!this.item.starred)
11604 return; 8376 return;
11605 8377
11606 if (this.$$('#bookmark-star') == this.root.activeElement) 8378 if (this.$$('#bookmark-star') == this.root.activeElement)
11607 this.$['menu-button'].focus(); 8379 this.$['menu-button'].focus();
11608 8380
11609 md_history.BrowserService.getInstance() 8381 md_history.BrowserService.getInstance()
11610 .removeBookmark(this.item.url); 8382 .removeBookmark(this.item.url);
11611 this.fire('remove-bookmark-stars', this.item.url); 8383 this.fire('remove-bookmark-stars', this.item.url);
11612 }, 8384 },
11613 8385
11614 /**
11615 * Fires a custom event when the menu button is clicked. Sends the details
11616 * of the history item and where the menu should appear.
11617 */
11618 onMenuButtonTap_: function(e) { 8386 onMenuButtonTap_: function(e) {
11619 this.fire('toggle-menu', { 8387 this.fire('toggle-menu', {
11620 target: Polymer.dom(e).localTarget, 8388 target: Polymer.dom(e).localTarget,
11621 item: this.item, 8389 item: this.item,
11622 path: this.path, 8390 path: this.path,
11623 }); 8391 });
11624 8392
11625 // Stops the 'tap' event from closing the menu when it opens.
11626 e.stopPropagation(); 8393 e.stopPropagation();
11627 }, 8394 },
11628 8395
11629 /**
11630 * Set the favicon image, based on the URL of the history item.
11631 * @private
11632 */
11633 showIcon_: function() { 8396 showIcon_: function() {
11634 this.$.icon.style.backgroundImage = 8397 this.$.icon.style.backgroundImage =
11635 cr.icon.getFaviconImageSet(this.item.url); 8398 cr.icon.getFaviconImageSet(this.item.url);
11636 }, 8399 },
11637 8400
11638 selectionNotAllowed_: function() { 8401 selectionNotAllowed_: function() {
11639 return !loadTimeData.getBoolean('allowDeletingHistory'); 8402 return !loadTimeData.getBoolean('allowDeletingHistory');
11640 }, 8403 },
11641 8404
11642 /**
11643 * Generates the title for this history card.
11644 * @param {number} numberOfItems The number of items in the card.
11645 * @param {string} search The search term associated with these results.
11646 * @private
11647 */
11648 cardTitle_: function(numberOfItems, historyDate, search) { 8405 cardTitle_: function(numberOfItems, historyDate, search) {
11649 if (!search) 8406 if (!search)
11650 return this.item.dateRelativeDay; 8407 return this.item.dateRelativeDay;
11651 8408
11652 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; 8409 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults';
11653 return loadTimeData.getStringF('foundSearchResults', numberOfItems, 8410 return loadTimeData.getStringF('foundSearchResults', numberOfItems,
11654 loadTimeData.getString(resultId), search); 8411 loadTimeData.getString(resultId), search);
11655 }, 8412 },
11656 8413
11657 /**
11658 * Crop long item titles to reduce their effect on layout performance. See
11659 * crbug.com/621347.
11660 * @param {string} title
11661 * @return {string}
11662 */
11663 cropItemTitle_: function(title) { 8414 cropItemTitle_: function(title) {
11664 return (title.length > TITLE_MAX_LENGTH) ? 8415 return (title.length > TITLE_MAX_LENGTH) ?
11665 title.substr(0, TITLE_MAX_LENGTH) : 8416 title.substr(0, TITLE_MAX_LENGTH) :
11666 title; 8417 title;
11667 } 8418 }
11668 }); 8419 });
11669 8420
11670 /**
11671 * Check whether the time difference between the given history item and the
11672 * next one is large enough for a spacer to be required.
11673 * @param {Array<HistoryEntry>} visits
11674 * @param {number} currentIndex
11675 * @param {string} searchedTerm
11676 * @return {boolean} Whether or not time gap separator is required.
11677 * @private
11678 */
11679 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { 8421 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) {
11680 if (currentIndex >= visits.length - 1 || visits.length == 0) 8422 if (currentIndex >= visits.length - 1 || visits.length == 0)
11681 return false; 8423 return false;
11682 8424
11683 var currentItem = visits[currentIndex]; 8425 var currentItem = visits[currentIndex];
11684 var nextItem = visits[currentIndex + 1]; 8426 var nextItem = visits[currentIndex + 1];
11685 8427
11686 if (searchedTerm) 8428 if (searchedTerm)
11687 return currentItem.dateShort != nextItem.dateShort; 8429 return currentItem.dateShort != nextItem.dateShort;
11688 8430
11689 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && 8431 return currentItem.time - nextItem.time > BROWSING_GAP_TIME &&
11690 currentItem.dateRelativeDay == nextItem.dateRelativeDay; 8432 currentItem.dateRelativeDay == nextItem.dateRelativeDay;
11691 }; 8433 };
11692 8434
11693 return { HistoryItem: HistoryItem }; 8435 return { HistoryItem: HistoryItem };
11694 }); 8436 });
11695 // Copyright 2016 The Chromium Authors. All rights reserved. 8437 // Copyright 2016 The Chromium Authors. All rights reserved.
11696 // Use of this source code is governed by a BSD-style license that can be 8438 // Use of this source code is governed by a BSD-style license that can be
11697 // found in the LICENSE file. 8439 // found in the LICENSE file.
11698 8440
11699 /**
11700 * @constructor
11701 * @param {string} currentPath
11702 */
11703 var SelectionTreeNode = function(currentPath) { 8441 var SelectionTreeNode = function(currentPath) {
11704 /** @type {string} */ 8442 /** @type {string} */
11705 this.currentPath = currentPath; 8443 this.currentPath = currentPath;
11706 /** @type {boolean} */ 8444 /** @type {boolean} */
11707 this.leaf = false; 8445 this.leaf = false;
11708 /** @type {Array<number>} */ 8446 /** @type {Array<number>} */
11709 this.indexes = []; 8447 this.indexes = [];
11710 /** @type {Array<SelectionTreeNode>} */ 8448 /** @type {Array<SelectionTreeNode>} */
11711 this.children = []; 8449 this.children = [];
11712 }; 8450 };
11713 8451
11714 /**
11715 * @param {number} index
11716 * @param {string} path
11717 */
11718 SelectionTreeNode.prototype.addChild = function(index, path) { 8452 SelectionTreeNode.prototype.addChild = function(index, path) {
11719 this.indexes.push(index); 8453 this.indexes.push(index);
11720 this.children[index] = new SelectionTreeNode(path); 8454 this.children[index] = new SelectionTreeNode(path);
11721 }; 8455 };
11722 8456
11723 /** @polymerBehavior */ 8457 /** @polymerBehavior */
11724 var HistoryListBehavior = { 8458 var HistoryListBehavior = {
11725 properties: { 8459 properties: {
11726 /**
11727 * Polymer paths to the history items contained in this list.
11728 * @type {!Set<string>} selectedPaths
11729 */
11730 selectedPaths: { 8460 selectedPaths: {
11731 type: Object, 8461 type: Object,
11732 value: /** @return {!Set<string>} */ function() { return new Set(); } 8462 value: /** @return {!Set<string>} */ function() { return new Set(); }
11733 }, 8463 },
11734 8464
11735 lastSelectedPath: String, 8465 lastSelectedPath: String,
11736 }, 8466 },
11737 8467
11738 listeners: { 8468 listeners: {
11739 'history-checkbox-select': 'itemSelected_', 8469 'history-checkbox-select': 'itemSelected_',
11740 }, 8470 },
11741 8471
11742 /**
11743 * @param {number} historyDataLength
11744 * @return {boolean}
11745 * @private
11746 */
11747 hasResults: function(historyDataLength) { return historyDataLength > 0; }, 8472 hasResults: function(historyDataLength) { return historyDataLength > 0; },
11748 8473
11749 /**
11750 * @param {string} searchedTerm
11751 * @param {boolean} isLoading
11752 * @return {string}
11753 * @private
11754 */
11755 noResultsMessage: function(searchedTerm, isLoading) { 8474 noResultsMessage: function(searchedTerm, isLoading) {
11756 if (isLoading) 8475 if (isLoading)
11757 return ''; 8476 return '';
11758 8477
11759 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; 8478 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
11760 return loadTimeData.getString(messageId); 8479 return loadTimeData.getString(messageId);
11761 }, 8480 },
11762 8481
11763 /**
11764 * Deselect each item in |selectedPaths|.
11765 */
11766 unselectAllItems: function() { 8482 unselectAllItems: function() {
11767 this.selectedPaths.forEach(function(path) { 8483 this.selectedPaths.forEach(function(path) {
11768 this.set(path + '.selected', false); 8484 this.set(path + '.selected', false);
11769 }.bind(this)); 8485 }.bind(this));
11770 8486
11771 this.selectedPaths.clear(); 8487 this.selectedPaths.clear();
11772 }, 8488 },
11773 8489
11774 /**
11775 * Performs a request to the backend to delete all selected items. If
11776 * successful, removes them from the view. Does not prompt the user before
11777 * deleting -- see <history-list-container> for a version of this method which
11778 * does prompt.
11779 */
11780 deleteSelected: function() { 8490 deleteSelected: function() {
11781 var toBeRemoved = 8491 var toBeRemoved =
11782 Array.from(this.selectedPaths.values()).map(function(path) { 8492 Array.from(this.selectedPaths.values()).map(function(path) {
11783 return this.get(path); 8493 return this.get(path);
11784 }.bind(this)); 8494 }.bind(this));
11785 8495
11786 md_history.BrowserService.getInstance() 8496 md_history.BrowserService.getInstance()
11787 .deleteItems(toBeRemoved) 8497 .deleteItems(toBeRemoved)
11788 .then(function() { 8498 .then(function() {
11789 this.removeItemsByPath(Array.from(this.selectedPaths)); 8499 this.removeItemsByPath(Array.from(this.selectedPaths));
11790 this.fire('unselect-all'); 8500 this.fire('unselect-all');
11791 }.bind(this)); 8501 }.bind(this));
11792 }, 8502 },
11793 8503
11794 /**
11795 * Removes the history items in |paths|. Assumes paths are of a.0.b.0...
11796 * structure.
11797 *
11798 * We want to use notifySplices to update the arrays for performance reasons
11799 * which requires manually batching and sending the notifySplices for each
11800 * level. To do this, we build a tree where each node is an array and then
11801 * depth traverse it to remove items. Each time a node has all children
11802 * deleted, we can also remove the node.
11803 *
11804 * @param {Array<string>} paths
11805 * @private
11806 */
11807 removeItemsByPath: function(paths) { 8504 removeItemsByPath: function(paths) {
11808 if (paths.length == 0) 8505 if (paths.length == 0)
11809 return; 8506 return;
11810 8507
11811 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); 8508 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths));
11812 }, 8509 },
11813 8510
11814 /**
11815 * Creates the tree to traverse in order to remove |paths| from this list.
11816 * Assumes paths are of a.0.b.0...
11817 * structure.
11818 *
11819 * @param {Array<string>} paths
11820 * @return {SelectionTreeNode}
11821 * @private
11822 */
11823 buildRemovalTree_: function(paths) { 8511 buildRemovalTree_: function(paths) {
11824 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); 8512 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]);
11825 8513
11826 // Build a tree to each history item specified in |paths|.
11827 paths.forEach(function(path) { 8514 paths.forEach(function(path) {
11828 var components = path.split('.'); 8515 var components = path.split('.');
11829 var node = rootNode; 8516 var node = rootNode;
11830 components.shift(); 8517 components.shift();
11831 while (components.length > 1) { 8518 while (components.length > 1) {
11832 var index = Number(components.shift()); 8519 var index = Number(components.shift());
11833 var arrayName = components.shift(); 8520 var arrayName = components.shift();
11834 8521
11835 if (!node.children[index]) 8522 if (!node.children[index])
11836 node.addChild(index, [node.currentPath, index, arrayName].join('.')); 8523 node.addChild(index, [node.currentPath, index, arrayName].join('.'));
11837 8524
11838 node = node.children[index]; 8525 node = node.children[index];
11839 } 8526 }
11840 node.leaf = true; 8527 node.leaf = true;
11841 node.indexes.push(Number(components.shift())); 8528 node.indexes.push(Number(components.shift()));
11842 }); 8529 });
11843 8530
11844 return rootNode; 8531 return rootNode;
11845 }, 8532 },
11846 8533
11847 /**
11848 * Removes the history items underneath |node| and deletes container arrays as
11849 * they become empty.
11850 * @param {SelectionTreeNode} node
11851 * @return {boolean} Whether this node's array should be deleted.
11852 * @private
11853 */
11854 removeItemsBeneathNode_: function(node) { 8534 removeItemsBeneathNode_: function(node) {
11855 var array = this.get(node.currentPath); 8535 var array = this.get(node.currentPath);
11856 var splices = []; 8536 var splices = [];
11857 8537
11858 node.indexes.sort(function(a, b) { return b - a; }); 8538 node.indexes.sort(function(a, b) { return b - a; });
11859 node.indexes.forEach(function(index) { 8539 node.indexes.forEach(function(index) {
11860 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { 8540 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) {
11861 var item = array.splice(index, 1); 8541 var item = array.splice(index, 1);
11862 splices.push({ 8542 splices.push({
11863 index: index, 8543 index: index,
11864 removed: [item], 8544 removed: [item],
11865 addedCount: 0, 8545 addedCount: 0,
11866 object: array, 8546 object: array,
11867 type: 'splice' 8547 type: 'splice'
11868 }); 8548 });
11869 } 8549 }
11870 }.bind(this)); 8550 }.bind(this));
11871 8551
11872 if (array.length == 0) 8552 if (array.length == 0)
11873 return true; 8553 return true;
11874 8554
11875 // notifySplices gives better performance than individually splicing as it
11876 // batches all of the updates together.
11877 this.notifySplices(node.currentPath, splices); 8555 this.notifySplices(node.currentPath, splices);
11878 return false; 8556 return false;
11879 }, 8557 },
11880 8558
11881 /**
11882 * @param {Event} e
11883 * @private
11884 */
11885 itemSelected_: function(e) { 8559 itemSelected_: function(e) {
11886 var item = e.detail.element; 8560 var item = e.detail.element;
11887 var paths = []; 8561 var paths = [];
11888 var itemPath = item.path; 8562 var itemPath = item.path;
11889 8563
11890 // Handle shift selection. Change the selection state of all items between
11891 // |path| and |lastSelected| to the selection state of |item|.
11892 if (e.detail.shiftKey && this.lastSelectedPath) { 8564 if (e.detail.shiftKey && this.lastSelectedPath) {
11893 var itemPathComponents = itemPath.split('.'); 8565 var itemPathComponents = itemPath.split('.');
11894 var itemIndex = Number(itemPathComponents.pop()); 8566 var itemIndex = Number(itemPathComponents.pop());
11895 var itemArrayPath = itemPathComponents.join('.'); 8567 var itemArrayPath = itemPathComponents.join('.');
11896 8568
11897 var lastItemPathComponents = this.lastSelectedPath.split('.'); 8569 var lastItemPathComponents = this.lastSelectedPath.split('.');
11898 var lastItemIndex = Number(lastItemPathComponents.pop()); 8570 var lastItemIndex = Number(lastItemPathComponents.pop());
11899 if (itemArrayPath == lastItemPathComponents.join('.')) { 8571 if (itemArrayPath == lastItemPathComponents.join('.')) {
11900 for (var i = Math.min(itemIndex, lastItemIndex); 8572 for (var i = Math.min(itemIndex, lastItemIndex);
11901 i <= Math.max(itemIndex, lastItemIndex); i++) { 8573 i <= Math.max(itemIndex, lastItemIndex); i++) {
(...skipping 16 matching lines...) Expand all
11918 this.selectedPaths.delete(path); 8590 this.selectedPaths.delete(path);
11919 }.bind(this)); 8591 }.bind(this));
11920 8592
11921 this.lastSelectedPath = itemPath; 8593 this.lastSelectedPath = itemPath;
11922 }, 8594 },
11923 }; 8595 };
11924 // Copyright 2016 The Chromium Authors. All rights reserved. 8596 // Copyright 2016 The Chromium Authors. All rights reserved.
11925 // Use of this source code is governed by a BSD-style license that can be 8597 // Use of this source code is governed by a BSD-style license that can be
11926 // found in the LICENSE file. 8598 // found in the LICENSE file.
11927 8599
11928 /**
11929 * @typedef {{domain: string,
11930 * visits: !Array<HistoryEntry>,
11931 * rendered: boolean,
11932 * expanded: boolean}}
11933 */
11934 var HistoryDomain; 8600 var HistoryDomain;
11935 8601
11936 /**
11937 * @typedef {{title: string,
11938 * domains: !Array<HistoryDomain>}}
11939 */
11940 var HistoryGroup; 8602 var HistoryGroup;
11941 8603
11942 Polymer({ 8604 Polymer({
11943 is: 'history-grouped-list', 8605 is: 'history-grouped-list',
11944 8606
11945 behaviors: [HistoryListBehavior], 8607 behaviors: [HistoryListBehavior],
11946 8608
11947 properties: { 8609 properties: {
11948 // An array of history entries in reverse chronological order.
11949 historyData: { 8610 historyData: {
11950 type: Array, 8611 type: Array,
11951 }, 8612 },
11952 8613
11953 /**
11954 * @type {Array<HistoryGroup>}
11955 */
11956 groupedHistoryData_: { 8614 groupedHistoryData_: {
11957 type: Array, 8615 type: Array,
11958 }, 8616 },
11959 8617
11960 searchedTerm: { 8618 searchedTerm: {
11961 type: String, 8619 type: String,
11962 value: '' 8620 value: ''
11963 }, 8621 },
11964 8622
11965 range: { 8623 range: {
11966 type: Number, 8624 type: Number,
11967 }, 8625 },
11968 8626
11969 queryStartTime: String, 8627 queryStartTime: String,
11970 queryEndTime: String, 8628 queryEndTime: String,
11971 }, 8629 },
11972 8630
11973 observers: [ 8631 observers: [
11974 'updateGroupedHistoryData_(range, historyData)' 8632 'updateGroupedHistoryData_(range, historyData)'
11975 ], 8633 ],
11976 8634
11977 /**
11978 * Make a list of domains from visits.
11979 * @param {!Array<!HistoryEntry>} visits
11980 * @return {!Array<!HistoryDomain>}
11981 */
11982 createHistoryDomains_: function(visits) { 8635 createHistoryDomains_: function(visits) {
11983 var domainIndexes = {}; 8636 var domainIndexes = {};
11984 var domains = []; 8637 var domains = [];
11985 8638
11986 // Group the visits into a dictionary and generate a list of domains.
11987 for (var i = 0, visit; visit = visits[i]; i++) { 8639 for (var i = 0, visit; visit = visits[i]; i++) {
11988 var domain = visit.domain; 8640 var domain = visit.domain;
11989 if (domainIndexes[domain] == undefined) { 8641 if (domainIndexes[domain] == undefined) {
11990 domainIndexes[domain] = domains.length; 8642 domainIndexes[domain] = domains.length;
11991 domains.push({ 8643 domains.push({
11992 domain: domain, 8644 domain: domain,
11993 visits: [], 8645 visits: [],
11994 expanded: false, 8646 expanded: false,
11995 rendered: false, 8647 rendered: false,
11996 }); 8648 });
11997 } 8649 }
11998 domains[domainIndexes[domain]].visits.push(visit); 8650 domains[domainIndexes[domain]].visits.push(visit);
11999 } 8651 }
12000 var sortByVisits = function(a, b) { 8652 var sortByVisits = function(a, b) {
12001 return b.visits.length - a.visits.length; 8653 return b.visits.length - a.visits.length;
12002 }; 8654 };
12003 domains.sort(sortByVisits); 8655 domains.sort(sortByVisits);
12004 8656
12005 return domains; 8657 return domains;
12006 }, 8658 },
12007 8659
12008 updateGroupedHistoryData_: function() { 8660 updateGroupedHistoryData_: function() {
12009 if (this.historyData.length == 0) { 8661 if (this.historyData.length == 0) {
12010 this.groupedHistoryData_ = []; 8662 this.groupedHistoryData_ = [];
12011 return; 8663 return;
12012 } 8664 }
12013 8665
12014 if (this.range == HistoryRange.WEEK) { 8666 if (this.range == HistoryRange.WEEK) {
12015 // Group each day into a list of results.
12016 var days = []; 8667 var days = [];
12017 var currentDayVisits = [this.historyData[0]]; 8668 var currentDayVisits = [this.historyData[0]];
12018 8669
12019 var pushCurrentDay = function() { 8670 var pushCurrentDay = function() {
12020 days.push({ 8671 days.push({
12021 title: this.searchedTerm ? currentDayVisits[0].dateShort : 8672 title: this.searchedTerm ? currentDayVisits[0].dateShort :
12022 currentDayVisits[0].dateRelativeDay, 8673 currentDayVisits[0].dateRelativeDay,
12023 domains: this.createHistoryDomains_(currentDayVisits), 8674 domains: this.createHistoryDomains_(currentDayVisits),
12024 }); 8675 });
12025 }.bind(this); 8676 }.bind(this);
(...skipping 10 matching lines...) Expand all
12036 if (!visitsSameDay(visit, currentDayVisits[0])) { 8687 if (!visitsSameDay(visit, currentDayVisits[0])) {
12037 pushCurrentDay(); 8688 pushCurrentDay();
12038 currentDayVisits = []; 8689 currentDayVisits = [];
12039 } 8690 }
12040 currentDayVisits.push(visit); 8691 currentDayVisits.push(visit);
12041 } 8692 }
12042 pushCurrentDay(); 8693 pushCurrentDay();
12043 8694
12044 this.groupedHistoryData_ = days; 8695 this.groupedHistoryData_ = days;
12045 } else if (this.range == HistoryRange.MONTH) { 8696 } else if (this.range == HistoryRange.MONTH) {
12046 // Group each all visits into a single list.
12047 this.groupedHistoryData_ = [{ 8697 this.groupedHistoryData_ = [{
12048 title: this.queryStartTime + ' – ' + this.queryEndTime, 8698 title: this.queryStartTime + ' – ' + this.queryEndTime,
12049 domains: this.createHistoryDomains_(this.historyData) 8699 domains: this.createHistoryDomains_(this.historyData)
12050 }]; 8700 }];
12051 } 8701 }
12052 }, 8702 },
12053 8703
12054 /**
12055 * @param {{model:Object, currentTarget:IronCollapseElement}} e
12056 */
12057 toggleDomainExpanded_: function(e) { 8704 toggleDomainExpanded_: function(e) {
12058 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); 8705 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse');
12059 e.model.set('domain.rendered', true); 8706 e.model.set('domain.rendered', true);
12060 8707
12061 // Give the history-items time to render.
12062 setTimeout(function() { collapse.toggle() }, 0); 8708 setTimeout(function() { collapse.toggle() }, 0);
12063 }, 8709 },
12064 8710
12065 /**
12066 * Check whether the time difference between the given history item and the
12067 * next one is large enough for a spacer to be required.
12068 * @param {number} groupIndex
12069 * @param {number} domainIndex
12070 * @param {number} itemIndex
12071 * @return {boolean} Whether or not time gap separator is required.
12072 * @private
12073 */
12074 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { 8711 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) {
12075 var visits = 8712 var visits =
12076 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; 8713 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits;
12077 8714
12078 return md_history.HistoryItem.needsTimeGap( 8715 return md_history.HistoryItem.needsTimeGap(
12079 visits, itemIndex, this.searchedTerm); 8716 visits, itemIndex, this.searchedTerm);
12080 }, 8717 },
12081 8718
12082 /**
12083 * @param {number} groupIndex
12084 * @param {number} domainIndex
12085 * @param {number} itemIndex
12086 * @return {string}
12087 * @private
12088 */
12089 pathForItem_: function(groupIndex, domainIndex, itemIndex) { 8719 pathForItem_: function(groupIndex, domainIndex, itemIndex) {
12090 return [ 8720 return [
12091 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', 8721 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits',
12092 itemIndex 8722 itemIndex
12093 ].join('.'); 8723 ].join('.');
12094 }, 8724 },
12095 8725
12096 /**
12097 * @param {HistoryDomain} domain
12098 * @return {string}
12099 * @private
12100 */
12101 getWebsiteIconStyle_: function(domain) { 8726 getWebsiteIconStyle_: function(domain) {
12102 return 'background-image: ' + 8727 return 'background-image: ' +
12103 cr.icon.getFaviconImageSet(domain.visits[0].url); 8728 cr.icon.getFaviconImageSet(domain.visits[0].url);
12104 }, 8729 },
12105 8730
12106 /**
12107 * @param {boolean} expanded
12108 * @return {string}
12109 * @private
12110 */
12111 getDropdownIcon_: function(expanded) { 8731 getDropdownIcon_: function(expanded) {
12112 return expanded ? 'cr:expand-less' : 'cr:expand-more'; 8732 return expanded ? 'cr:expand-less' : 'cr:expand-more';
12113 }, 8733 },
12114 }); 8734 });
12115 /**
12116 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
12117 * designated scroll target.
12118 *
12119 * Elements that consume this behavior can override the `_scrollHandler`
12120 * method to add logic on the scroll event.
12121 *
12122 * @demo demo/scrolling-region.html Scrolling Region
12123 * @demo demo/document.html Document Element
12124 * @polymerBehavior
12125 */
12126 Polymer.IronScrollTargetBehavior = { 8735 Polymer.IronScrollTargetBehavior = {
12127 8736
12128 properties: { 8737 properties: {
12129 8738
12130 /**
12131 * Specifies the element that will handle the scroll event
12132 * on the behalf of the current element. This is typically a reference to an element,
12133 * but there are a few more posibilities:
12134 *
12135 * ### Elements id
12136 *
12137 *```html
12138 * <div id="scrollable-element" style="overflow: auto;">
12139 * <x-element scroll-target="scrollable-element">
12140 * \x3c!-- Content--\x3e
12141 * </x-element>
12142 * </div>
12143 *```
12144 * In this case, the `scrollTarget` will point to the outer div element.
12145 *
12146 * ### Document scrolling
12147 *
12148 * For document scrolling, you can use the reserved word `document`:
12149 *
12150 *```html
12151 * <x-element scroll-target="document">
12152 * \x3c!-- Content --\x3e
12153 * </x-element>
12154 *```
12155 *
12156 * ### Elements reference
12157 *
12158 *```js
12159 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
12160 *```
12161 *
12162 * @type {HTMLElement}
12163 */
12164 scrollTarget: { 8739 scrollTarget: {
12165 type: HTMLElement, 8740 type: HTMLElement,
12166 value: function() { 8741 value: function() {
12167 return this._defaultScrollTarget; 8742 return this._defaultScrollTarget;
12168 } 8743 }
12169 } 8744 }
12170 }, 8745 },
12171 8746
12172 observers: [ 8747 observers: [
12173 '_scrollTargetChanged(scrollTarget, isAttached)' 8748 '_scrollTargetChanged(scrollTarget, isAttached)'
12174 ], 8749 ],
12175 8750
12176 _scrollTargetChanged: function(scrollTarget, isAttached) { 8751 _scrollTargetChanged: function(scrollTarget, isAttached) {
12177 var eventTarget; 8752 var eventTarget;
12178 8753
12179 if (this._oldScrollTarget) { 8754 if (this._oldScrollTarget) {
12180 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget; 8755 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
12181 eventTarget.removeEventListener('scroll', this._boundScrollHandler); 8756 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
12182 this._oldScrollTarget = null; 8757 this._oldScrollTarget = null;
12183 } 8758 }
12184 8759
12185 if (!isAttached) { 8760 if (!isAttached) {
12186 return; 8761 return;
12187 } 8762 }
12188 // Support element id references
12189 if (scrollTarget === 'document') { 8763 if (scrollTarget === 'document') {
12190 8764
12191 this.scrollTarget = this._doc; 8765 this.scrollTarget = this._doc;
12192 8766
12193 } else if (typeof scrollTarget === 'string') { 8767 } else if (typeof scrollTarget === 'string') {
12194 8768
12195 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : 8769 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
12196 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); 8770 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
12197 8771
12198 } else if (this._isValidScrollTarget()) { 8772 } else if (this._isValidScrollTarget()) {
12199 8773
12200 eventTarget = scrollTarget === this._doc ? window : scrollTarget; 8774 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
12201 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this); 8775 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
12202 this._oldScrollTarget = scrollTarget; 8776 this._oldScrollTarget = scrollTarget;
12203 8777
12204 eventTarget.addEventListener('scroll', this._boundScrollHandler); 8778 eventTarget.addEventListener('scroll', this._boundScrollHandler);
12205 } 8779 }
12206 }, 8780 },
12207 8781
12208 /**
12209 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
12210 *
12211 * @protected
12212 */
12213 _scrollHandler: function scrollHandler() {}, 8782 _scrollHandler: function scrollHandler() {},
12214 8783
12215 /**
12216 * The default scroll target. Consumers of this behavior may want to customi ze
12217 * the default scroll target.
12218 *
12219 * @type {Element}
12220 */
12221 get _defaultScrollTarget() { 8784 get _defaultScrollTarget() {
12222 return this._doc; 8785 return this._doc;
12223 }, 8786 },
12224 8787
12225 /**
12226 * Shortcut for the document element
12227 *
12228 * @type {Element}
12229 */
12230 get _doc() { 8788 get _doc() {
12231 return this.ownerDocument.documentElement; 8789 return this.ownerDocument.documentElement;
12232 }, 8790 },
12233 8791
12234 /**
12235 * Gets the number of pixels that the content of an element is scrolled upwa rd.
12236 *
12237 * @type {number}
12238 */
12239 get _scrollTop() { 8792 get _scrollTop() {
12240 if (this._isValidScrollTarget()) { 8793 if (this._isValidScrollTarget()) {
12241 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop; 8794 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
12242 } 8795 }
12243 return 0; 8796 return 0;
12244 }, 8797 },
12245 8798
12246 /**
12247 * Gets the number of pixels that the content of an element is scrolled to t he left.
12248 *
12249 * @type {number}
12250 */
12251 get _scrollLeft() { 8799 get _scrollLeft() {
12252 if (this._isValidScrollTarget()) { 8800 if (this._isValidScrollTarget()) {
12253 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft; 8801 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
12254 } 8802 }
12255 return 0; 8803 return 0;
12256 }, 8804 },
12257 8805
12258 /**
12259 * Sets the number of pixels that the content of an element is scrolled upwa rd.
12260 *
12261 * @type {number}
12262 */
12263 set _scrollTop(top) { 8806 set _scrollTop(top) {
12264 if (this.scrollTarget === this._doc) { 8807 if (this.scrollTarget === this._doc) {
12265 window.scrollTo(window.pageXOffset, top); 8808 window.scrollTo(window.pageXOffset, top);
12266 } else if (this._isValidScrollTarget()) { 8809 } else if (this._isValidScrollTarget()) {
12267 this.scrollTarget.scrollTop = top; 8810 this.scrollTarget.scrollTop = top;
12268 } 8811 }
12269 }, 8812 },
12270 8813
12271 /**
12272 * Sets the number of pixels that the content of an element is scrolled to t he left.
12273 *
12274 * @type {number}
12275 */
12276 set _scrollLeft(left) { 8814 set _scrollLeft(left) {
12277 if (this.scrollTarget === this._doc) { 8815 if (this.scrollTarget === this._doc) {
12278 window.scrollTo(left, window.pageYOffset); 8816 window.scrollTo(left, window.pageYOffset);
12279 } else if (this._isValidScrollTarget()) { 8817 } else if (this._isValidScrollTarget()) {
12280 this.scrollTarget.scrollLeft = left; 8818 this.scrollTarget.scrollLeft = left;
12281 } 8819 }
12282 }, 8820 },
12283 8821
12284 /**
12285 * Scrolls the content to a particular place.
12286 *
12287 * @method scroll
12288 * @param {number} left The left position
12289 * @param {number} top The top position
12290 */
12291 scroll: function(left, top) { 8822 scroll: function(left, top) {
12292 if (this.scrollTarget === this._doc) { 8823 if (this.scrollTarget === this._doc) {
12293 window.scrollTo(left, top); 8824 window.scrollTo(left, top);
12294 } else if (this._isValidScrollTarget()) { 8825 } else if (this._isValidScrollTarget()) {
12295 this.scrollTarget.scrollLeft = left; 8826 this.scrollTarget.scrollLeft = left;
12296 this.scrollTarget.scrollTop = top; 8827 this.scrollTarget.scrollTop = top;
12297 } 8828 }
12298 }, 8829 },
12299 8830
12300 /**
12301 * Gets the width of the scroll target.
12302 *
12303 * @type {number}
12304 */
12305 get _scrollTargetWidth() { 8831 get _scrollTargetWidth() {
12306 if (this._isValidScrollTarget()) { 8832 if (this._isValidScrollTarget()) {
12307 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth; 8833 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
12308 } 8834 }
12309 return 0; 8835 return 0;
12310 }, 8836 },
12311 8837
12312 /**
12313 * Gets the height of the scroll target.
12314 *
12315 * @type {number}
12316 */
12317 get _scrollTargetHeight() { 8838 get _scrollTargetHeight() {
12318 if (this._isValidScrollTarget()) { 8839 if (this._isValidScrollTarget()) {
12319 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight; 8840 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
12320 } 8841 }
12321 return 0; 8842 return 0;
12322 }, 8843 },
12323 8844
12324 /**
12325 * Returns true if the scroll target is a valid HTMLElement.
12326 *
12327 * @return {boolean}
12328 */
12329 _isValidScrollTarget: function() { 8845 _isValidScrollTarget: function() {
12330 return this.scrollTarget instanceof HTMLElement; 8846 return this.scrollTarget instanceof HTMLElement;
12331 } 8847 }
12332 }; 8848 };
12333 (function() { 8849 (function() {
12334 8850
12335 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); 8851 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
12336 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; 8852 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
12337 var DEFAULT_PHYSICAL_COUNT = 3; 8853 var DEFAULT_PHYSICAL_COUNT = 3;
12338 var HIDDEN_Y = '-10000px'; 8854 var HIDDEN_Y = '-10000px';
12339 var DEFAULT_GRID_SIZE = 200; 8855 var DEFAULT_GRID_SIZE = 200;
12340 var SECRET_TABINDEX = -100; 8856 var SECRET_TABINDEX = -100;
12341 8857
12342 Polymer({ 8858 Polymer({
12343 8859
12344 is: 'iron-list', 8860 is: 'iron-list',
12345 8861
12346 properties: { 8862 properties: {
12347 8863
12348 /**
12349 * An array containing items determining how many instances of the templat e
12350 * to stamp and that that each template instance should bind to.
12351 */
12352 items: { 8864 items: {
12353 type: Array 8865 type: Array
12354 }, 8866 },
12355 8867
12356 /**
12357 * The max count of physical items the pool can extend to.
12358 */
12359 maxPhysicalCount: { 8868 maxPhysicalCount: {
12360 type: Number, 8869 type: Number,
12361 value: 500 8870 value: 500
12362 }, 8871 },
12363 8872
12364 /**
12365 * The name of the variable to add to the binding scope for the array
12366 * element associated with a given template instance.
12367 */
12368 as: { 8873 as: {
12369 type: String, 8874 type: String,
12370 value: 'item' 8875 value: 'item'
12371 }, 8876 },
12372 8877
12373 /**
12374 * The name of the variable to add to the binding scope with the index
12375 * for the row.
12376 */
12377 indexAs: { 8878 indexAs: {
12378 type: String, 8879 type: String,
12379 value: 'index' 8880 value: 'index'
12380 }, 8881 },
12381 8882
12382 /**
12383 * The name of the variable to add to the binding scope to indicate
12384 * if the row is selected.
12385 */
12386 selectedAs: { 8883 selectedAs: {
12387 type: String, 8884 type: String,
12388 value: 'selected' 8885 value: 'selected'
12389 }, 8886 },
12390 8887
12391 /**
12392 * When true, the list is rendered as a grid. Grid items must have
12393 * fixed width and height set via CSS. e.g.
12394 *
12395 * ```html
12396 * <iron-list grid>
12397 * <template>
12398 * <div style="width: 100px; height: 100px;"> 100x100 </div>
12399 * </template>
12400 * </iron-list>
12401 * ```
12402 */
12403 grid: { 8888 grid: {
12404 type: Boolean, 8889 type: Boolean,
12405 value: false, 8890 value: false,
12406 reflectToAttribute: true 8891 reflectToAttribute: true
12407 }, 8892 },
12408 8893
12409 /**
12410 * When true, tapping a row will select the item, placing its data model
12411 * in the set of selected items retrievable via the selection property.
12412 *
12413 * Note that tapping focusable elements within the list item will not
12414 * result in selection, since they are presumed to have their * own action .
12415 */
12416 selectionEnabled: { 8894 selectionEnabled: {
12417 type: Boolean, 8895 type: Boolean,
12418 value: false 8896 value: false
12419 }, 8897 },
12420 8898
12421 /**
12422 * When `multiSelection` is false, this is the currently selected item, or `null`
12423 * if no item is selected.
12424 */
12425 selectedItem: { 8899 selectedItem: {
12426 type: Object, 8900 type: Object,
12427 notify: true 8901 notify: true
12428 }, 8902 },
12429 8903
12430 /**
12431 * When `multiSelection` is true, this is an array that contains the selec ted items.
12432 */
12433 selectedItems: { 8904 selectedItems: {
12434 type: Object, 8905 type: Object,
12435 notify: true 8906 notify: true
12436 }, 8907 },
12437 8908
12438 /**
12439 * When `true`, multiple items may be selected at once (in this case,
12440 * `selected` is an array of currently selected items). When `false`,
12441 * only one item may be selected at a time.
12442 */
12443 multiSelection: { 8909 multiSelection: {
12444 type: Boolean, 8910 type: Boolean,
12445 value: false 8911 value: false
12446 } 8912 }
12447 }, 8913 },
12448 8914
12449 observers: [ 8915 observers: [
12450 '_itemsChanged(items.*)', 8916 '_itemsChanged(items.*)',
12451 '_selectionEnabledChanged(selectionEnabled)', 8917 '_selectionEnabledChanged(selectionEnabled)',
12452 '_multiSelectionChanged(multiSelection)', 8918 '_multiSelectionChanged(multiSelection)',
12453 '_setOverflow(scrollTarget)' 8919 '_setOverflow(scrollTarget)'
12454 ], 8920 ],
12455 8921
12456 behaviors: [ 8922 behaviors: [
12457 Polymer.Templatizer, 8923 Polymer.Templatizer,
12458 Polymer.IronResizableBehavior, 8924 Polymer.IronResizableBehavior,
12459 Polymer.IronA11yKeysBehavior, 8925 Polymer.IronA11yKeysBehavior,
12460 Polymer.IronScrollTargetBehavior 8926 Polymer.IronScrollTargetBehavior
12461 ], 8927 ],
12462 8928
12463 keyBindings: { 8929 keyBindings: {
12464 'up': '_didMoveUp', 8930 'up': '_didMoveUp',
12465 'down': '_didMoveDown', 8931 'down': '_didMoveDown',
12466 'enter': '_didEnter' 8932 'enter': '_didEnter'
12467 }, 8933 },
12468 8934
12469 /**
12470 * The ratio of hidden tiles that should remain in the scroll direction.
12471 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
12472 */
12473 _ratio: 0.5, 8935 _ratio: 0.5,
12474 8936
12475 /**
12476 * The padding-top value for the list.
12477 */
12478 _scrollerPaddingTop: 0, 8937 _scrollerPaddingTop: 0,
12479 8938
12480 /**
12481 * This value is the same as `scrollTop`.
12482 */
12483 _scrollPosition: 0, 8939 _scrollPosition: 0,
12484 8940
12485 /**
12486 * The sum of the heights of all the tiles in the DOM.
12487 */
12488 _physicalSize: 0, 8941 _physicalSize: 0,
12489 8942
12490 /**
12491 * The average `offsetHeight` of the tiles observed till now.
12492 */
12493 _physicalAverage: 0, 8943 _physicalAverage: 0,
12494 8944
12495 /**
12496 * The number of tiles which `offsetHeight` > 0 observed until now.
12497 */
12498 _physicalAverageCount: 0, 8945 _physicalAverageCount: 0,
12499 8946
12500 /**
12501 * The Y position of the item rendered in the `_physicalStart`
12502 * tile relative to the scrolling list.
12503 */
12504 _physicalTop: 0, 8947 _physicalTop: 0,
12505 8948
12506 /**
12507 * The number of items in the list.
12508 */
12509 _virtualCount: 0, 8949 _virtualCount: 0,
12510 8950
12511 /**
12512 * A map between an item key and its physical item index
12513 */
12514 _physicalIndexForKey: null, 8951 _physicalIndexForKey: null,
12515 8952
12516 /**
12517 * The estimated scroll height based on `_physicalAverage`
12518 */
12519 _estScrollHeight: 0, 8953 _estScrollHeight: 0,
12520 8954
12521 /**
12522 * The scroll height of the dom node
12523 */
12524 _scrollHeight: 0, 8955 _scrollHeight: 0,
12525 8956
12526 /**
12527 * The height of the list. This is referred as the viewport in the context o f list.
12528 */
12529 _viewportHeight: 0, 8957 _viewportHeight: 0,
12530 8958
12531 /**
12532 * The width of the list. This is referred as the viewport in the context of list.
12533 */
12534 _viewportWidth: 0, 8959 _viewportWidth: 0,
12535 8960
12536 /**
12537 * An array of DOM nodes that are currently in the tree
12538 * @type {?Array<!TemplatizerNode>}
12539 */
12540 _physicalItems: null, 8961 _physicalItems: null,
12541 8962
12542 /**
12543 * An array of heights for each item in `_physicalItems`
12544 * @type {?Array<number>}
12545 */
12546 _physicalSizes: null, 8963 _physicalSizes: null,
12547 8964
12548 /**
12549 * A cached value for the first visible index.
12550 * See `firstVisibleIndex`
12551 * @type {?number}
12552 */
12553 _firstVisibleIndexVal: null, 8965 _firstVisibleIndexVal: null,
12554 8966
12555 /**
12556 * A cached value for the last visible index.
12557 * See `lastVisibleIndex`
12558 * @type {?number}
12559 */
12560 _lastVisibleIndexVal: null, 8967 _lastVisibleIndexVal: null,
12561 8968
12562 /**
12563 * A Polymer collection for the items.
12564 * @type {?Polymer.Collection}
12565 */
12566 _collection: null, 8969 _collection: null,
12567 8970
12568 /**
12569 * True if the current item list was rendered for the first time
12570 * after attached.
12571 */
12572 _itemsRendered: false, 8971 _itemsRendered: false,
12573 8972
12574 /**
12575 * The page that is currently rendered.
12576 */
12577 _lastPage: null, 8973 _lastPage: null,
12578 8974
12579 /**
12580 * The max number of pages to render. One page is equivalent to the height o f the list.
12581 */
12582 _maxPages: 3, 8975 _maxPages: 3,
12583 8976
12584 /**
12585 * The currently focused physical item.
12586 */
12587 _focusedItem: null, 8977 _focusedItem: null,
12588 8978
12589 /**
12590 * The index of the `_focusedItem`.
12591 */
12592 _focusedIndex: -1, 8979 _focusedIndex: -1,
12593 8980
12594 /**
12595 * The the item that is focused if it is moved offscreen.
12596 * @private {?TemplatizerNode}
12597 */
12598 _offscreenFocusedItem: null, 8981 _offscreenFocusedItem: null,
12599 8982
12600 /**
12601 * The item that backfills the `_offscreenFocusedItem` in the physical items
12602 * list when that item is moved offscreen.
12603 */
12604 _focusBackfillItem: null, 8983 _focusBackfillItem: null,
12605 8984
12606 /**
12607 * The maximum items per row
12608 */
12609 _itemsPerRow: 1, 8985 _itemsPerRow: 1,
12610 8986
12611 /**
12612 * The width of each grid item
12613 */
12614 _itemWidth: 0, 8987 _itemWidth: 0,
12615 8988
12616 /**
12617 * The height of the row in grid layout.
12618 */
12619 _rowHeight: 0, 8989 _rowHeight: 0,
12620 8990
12621 /**
12622 * The bottom of the physical content.
12623 */
12624 get _physicalBottom() { 8991 get _physicalBottom() {
12625 return this._physicalTop + this._physicalSize; 8992 return this._physicalTop + this._physicalSize;
12626 }, 8993 },
12627 8994
12628 /**
12629 * The bottom of the scroll.
12630 */
12631 get _scrollBottom() { 8995 get _scrollBottom() {
12632 return this._scrollPosition + this._viewportHeight; 8996 return this._scrollPosition + this._viewportHeight;
12633 }, 8997 },
12634 8998
12635 /**
12636 * The n-th item rendered in the last physical item.
12637 */
12638 get _virtualEnd() { 8999 get _virtualEnd() {
12639 return this._virtualStart + this._physicalCount - 1; 9000 return this._virtualStart + this._physicalCount - 1;
12640 }, 9001 },
12641 9002
12642 /**
12643 * The height of the physical content that isn't on the screen.
12644 */
12645 get _hiddenContentSize() { 9003 get _hiddenContentSize() {
12646 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize; 9004 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
12647 return size - this._viewportHeight; 9005 return size - this._viewportHeight;
12648 }, 9006 },
12649 9007
12650 /**
12651 * The maximum scroll top value.
12652 */
12653 get _maxScrollTop() { 9008 get _maxScrollTop() {
12654 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop; 9009 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
12655 }, 9010 },
12656 9011
12657 /**
12658 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
12659 */
12660 _minVirtualStart: 0, 9012 _minVirtualStart: 0,
12661 9013
12662 /**
12663 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
12664 */
12665 get _maxVirtualStart() { 9014 get _maxVirtualStart() {
12666 return Math.max(0, this._virtualCount - this._physicalCount); 9015 return Math.max(0, this._virtualCount - this._physicalCount);
12667 }, 9016 },
12668 9017
12669 /**
12670 * The n-th item rendered in the `_physicalStart` tile.
12671 */
12672 _virtualStartVal: 0, 9018 _virtualStartVal: 0,
12673 9019
12674 set _virtualStart(val) { 9020 set _virtualStart(val) {
12675 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); 9021 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
12676 }, 9022 },
12677 9023
12678 get _virtualStart() { 9024 get _virtualStart() {
12679 return this._virtualStartVal || 0; 9025 return this._virtualStartVal || 0;
12680 }, 9026 },
12681 9027
12682 /**
12683 * The k-th tile that is at the top of the scrolling list.
12684 */
12685 _physicalStartVal: 0, 9028 _physicalStartVal: 0,
12686 9029
12687 set _physicalStart(val) { 9030 set _physicalStart(val) {
12688 this._physicalStartVal = val % this._physicalCount; 9031 this._physicalStartVal = val % this._physicalCount;
12689 if (this._physicalStartVal < 0) { 9032 if (this._physicalStartVal < 0) {
12690 this._physicalStartVal = this._physicalCount + this._physicalStartVal; 9033 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
12691 } 9034 }
12692 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 9035 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12693 }, 9036 },
12694 9037
12695 get _physicalStart() { 9038 get _physicalStart() {
12696 return this._physicalStartVal || 0; 9039 return this._physicalStartVal || 0;
12697 }, 9040 },
12698 9041
12699 /**
12700 * The number of tiles in the DOM.
12701 */
12702 _physicalCountVal: 0, 9042 _physicalCountVal: 0,
12703 9043
12704 set _physicalCount(val) { 9044 set _physicalCount(val) {
12705 this._physicalCountVal = val; 9045 this._physicalCountVal = val;
12706 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; 9046 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12707 }, 9047 },
12708 9048
12709 get _physicalCount() { 9049 get _physicalCount() {
12710 return this._physicalCountVal; 9050 return this._physicalCountVal;
12711 }, 9051 },
12712 9052
12713 /**
12714 * The k-th tile that is at the bottom of the scrolling list.
12715 */
12716 _physicalEnd: 0, 9053 _physicalEnd: 0,
12717 9054
12718 /**
12719 * An optimal physical size such that we will have enough physical items
12720 * to fill up the viewport and recycle when the user scrolls.
12721 *
12722 * This default value assumes that we will at least have the equivalent
12723 * to a viewport of physical items above and below the user's viewport.
12724 */
12725 get _optPhysicalSize() { 9055 get _optPhysicalSize() {
12726 if (this.grid) { 9056 if (this.grid) {
12727 return this._estRowsInView * this._rowHeight * this._maxPages; 9057 return this._estRowsInView * this._rowHeight * this._maxPages;
12728 } 9058 }
12729 return this._viewportHeight * this._maxPages; 9059 return this._viewportHeight * this._maxPages;
12730 }, 9060 },
12731 9061
12732 get _optPhysicalCount() { 9062 get _optPhysicalCount() {
12733 return this._estRowsInView * this._itemsPerRow * this._maxPages; 9063 return this._estRowsInView * this._itemsPerRow * this._maxPages;
12734 }, 9064 },
12735 9065
12736 /**
12737 * True if the current list is visible.
12738 */
12739 get _isVisible() { 9066 get _isVisible() {
12740 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight); 9067 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
12741 }, 9068 },
12742 9069
12743 /**
12744 * Gets the index of the first visible item in the viewport.
12745 *
12746 * @type {number}
12747 */
12748 get firstVisibleIndex() { 9070 get firstVisibleIndex() {
12749 if (this._firstVisibleIndexVal === null) { 9071 if (this._firstVisibleIndexVal === null) {
12750 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 9072 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
12751 9073
12752 this._firstVisibleIndexVal = this._iterateItems( 9074 this._firstVisibleIndexVal = this._iterateItems(
12753 function(pidx, vidx) { 9075 function(pidx, vidx) {
12754 physicalOffset += this._getPhysicalSizeIncrement(pidx); 9076 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12755 9077
12756 if (physicalOffset > this._scrollPosition) { 9078 if (physicalOffset > this._scrollPosition) {
12757 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; 9079 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
12758 } 9080 }
12759 // Handle a partially rendered final row in grid mode
12760 if (this.grid && this._virtualCount - 1 === vidx) { 9081 if (this.grid && this._virtualCount - 1 === vidx) {
12761 return vidx - (vidx % this._itemsPerRow); 9082 return vidx - (vidx % this._itemsPerRow);
12762 } 9083 }
12763 }) || 0; 9084 }) || 0;
12764 } 9085 }
12765 return this._firstVisibleIndexVal; 9086 return this._firstVisibleIndexVal;
12766 }, 9087 },
12767 9088
12768 /**
12769 * Gets the index of the last visible item in the viewport.
12770 *
12771 * @type {number}
12772 */
12773 get lastVisibleIndex() { 9089 get lastVisibleIndex() {
12774 if (this._lastVisibleIndexVal === null) { 9090 if (this._lastVisibleIndexVal === null) {
12775 if (this.grid) { 9091 if (this.grid) {
12776 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 9092 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
12777 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 9093 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
12778 } else { 9094 } else {
12779 var physicalOffset = this._physicalTop; 9095 var physicalOffset = this._physicalTop;
12780 this._iterateItems(function(pidx, vidx) { 9096 this._iterateItems(function(pidx, vidx) {
12781 if (physicalOffset < this._scrollBottom) { 9097 if (physicalOffset < this._scrollBottom) {
12782 this._lastVisibleIndexVal = vidx; 9098 this._lastVisibleIndexVal = vidx;
12783 } else { 9099 } else {
12784 // Break _iterateItems
12785 return true; 9100 return true;
12786 } 9101 }
12787 physicalOffset += this._getPhysicalSizeIncrement(pidx); 9102 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12788 }); 9103 });
12789 } 9104 }
12790 } 9105 }
12791 return this._lastVisibleIndexVal; 9106 return this._lastVisibleIndexVal;
12792 }, 9107 },
12793 9108
12794 get _defaultScrollTarget() { 9109 get _defaultScrollTarget() {
(...skipping 11 matching lines...) Expand all
12806 return Math.ceil(this._physicalCount / this._itemsPerRow); 9121 return Math.ceil(this._physicalCount / this._itemsPerRow);
12807 }, 9122 },
12808 9123
12809 ready: function() { 9124 ready: function() {
12810 this.addEventListener('focus', this._didFocus.bind(this), true); 9125 this.addEventListener('focus', this._didFocus.bind(this), true);
12811 }, 9126 },
12812 9127
12813 attached: function() { 9128 attached: function() {
12814 this.updateViewportBoundaries(); 9129 this.updateViewportBoundaries();
12815 this._render(); 9130 this._render();
12816 // `iron-resize` is fired when the list is attached if the event is added
12817 // before attached causing unnecessary work.
12818 this.listen(this, 'iron-resize', '_resizeHandler'); 9131 this.listen(this, 'iron-resize', '_resizeHandler');
12819 }, 9132 },
12820 9133
12821 detached: function() { 9134 detached: function() {
12822 this._itemsRendered = false; 9135 this._itemsRendered = false;
12823 this.unlisten(this, 'iron-resize', '_resizeHandler'); 9136 this.unlisten(this, 'iron-resize', '_resizeHandler');
12824 }, 9137 },
12825 9138
12826 /**
12827 * Set the overflow property if this element has its own scrolling region
12828 */
12829 _setOverflow: function(scrollTarget) { 9139 _setOverflow: function(scrollTarget) {
12830 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; 9140 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
12831 this.style.overflow = scrollTarget === this ? 'auto' : ''; 9141 this.style.overflow = scrollTarget === this ? 'auto' : '';
12832 }, 9142 },
12833 9143
12834 /**
12835 * Invoke this method if you dynamically update the viewport's
12836 * size or CSS padding.
12837 *
12838 * @method updateViewportBoundaries
12839 */
12840 updateViewportBoundaries: function() { 9144 updateViewportBoundaries: function() {
12841 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : 9145 this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
12842 parseInt(window.getComputedStyle(this)['padding-top'], 10); 9146 parseInt(window.getComputedStyle(this)['padding-top'], 10);
12843 9147
12844 this._viewportHeight = this._scrollTargetHeight; 9148 this._viewportHeight = this._scrollTargetHeight;
12845 if (this.grid) { 9149 if (this.grid) {
12846 this._updateGridMetrics(); 9150 this._updateGridMetrics();
12847 } 9151 }
12848 }, 9152 },
12849 9153
12850 /**
12851 * Update the models, the position of the
12852 * items in the viewport and recycle tiles as needed.
12853 */
12854 _scrollHandler: function() { 9154 _scrollHandler: function() {
12855 // clamp the `scrollTop` value
12856 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; 9155 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
12857 var delta = scrollTop - this._scrollPosition; 9156 var delta = scrollTop - this._scrollPosition;
12858 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; 9157 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
12859 var ratio = this._ratio; 9158 var ratio = this._ratio;
12860 var recycledTiles = 0; 9159 var recycledTiles = 0;
12861 var hiddenContentSize = this._hiddenContentSize; 9160 var hiddenContentSize = this._hiddenContentSize;
12862 var currentRatio = ratio; 9161 var currentRatio = ratio;
12863 var movingUp = []; 9162 var movingUp = [];
12864 9163
12865 // track the last `scrollTop`
12866 this._scrollPosition = scrollTop; 9164 this._scrollPosition = scrollTop;
12867 9165
12868 // clear cached visible indexes
12869 this._firstVisibleIndexVal = null; 9166 this._firstVisibleIndexVal = null;
12870 this._lastVisibleIndexVal = null; 9167 this._lastVisibleIndexVal = null;
12871 9168
12872 scrollBottom = this._scrollBottom; 9169 scrollBottom = this._scrollBottom;
12873 physicalBottom = this._physicalBottom; 9170 physicalBottom = this._physicalBottom;
12874 9171
12875 // random access
12876 if (Math.abs(delta) > this._physicalSize) { 9172 if (Math.abs(delta) > this._physicalSize) {
12877 this._physicalTop += delta; 9173 this._physicalTop += delta;
12878 recycledTiles = Math.round(delta / this._physicalAverage); 9174 recycledTiles = Math.round(delta / this._physicalAverage);
12879 } 9175 }
12880 // scroll up
12881 else if (delta < 0) { 9176 else if (delta < 0) {
12882 var topSpace = scrollTop - this._physicalTop; 9177 var topSpace = scrollTop - this._physicalTop;
12883 var virtualStart = this._virtualStart; 9178 var virtualStart = this._virtualStart;
12884 9179
12885 recycledTileSet = []; 9180 recycledTileSet = [];
12886 9181
12887 kth = this._physicalEnd; 9182 kth = this._physicalEnd;
12888 currentRatio = topSpace / hiddenContentSize; 9183 currentRatio = topSpace / hiddenContentSize;
12889 9184
12890 // move tiles from bottom to top
12891 while ( 9185 while (
12892 // approximate `currentRatio` to `ratio`
12893 currentRatio < ratio && 9186 currentRatio < ratio &&
12894 // recycle less physical items than the total
12895 recycledTiles < this._physicalCount && 9187 recycledTiles < this._physicalCount &&
12896 // ensure that these recycled tiles are needed
12897 virtualStart - recycledTiles > 0 && 9188 virtualStart - recycledTiles > 0 &&
12898 // ensure that the tile is not visible
12899 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom 9189 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
12900 ) { 9190 ) {
12901 9191
12902 tileHeight = this._getPhysicalSizeIncrement(kth); 9192 tileHeight = this._getPhysicalSizeIncrement(kth);
12903 currentRatio += tileHeight / hiddenContentSize; 9193 currentRatio += tileHeight / hiddenContentSize;
12904 physicalBottom -= tileHeight; 9194 physicalBottom -= tileHeight;
12905 recycledTileSet.push(kth); 9195 recycledTileSet.push(kth);
12906 recycledTiles++; 9196 recycledTiles++;
12907 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; 9197 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
12908 } 9198 }
12909 9199
12910 movingUp = recycledTileSet; 9200 movingUp = recycledTileSet;
12911 recycledTiles = -recycledTiles; 9201 recycledTiles = -recycledTiles;
12912 } 9202 }
12913 // scroll down
12914 else if (delta > 0) { 9203 else if (delta > 0) {
12915 var bottomSpace = physicalBottom - scrollBottom; 9204 var bottomSpace = physicalBottom - scrollBottom;
12916 var virtualEnd = this._virtualEnd; 9205 var virtualEnd = this._virtualEnd;
12917 var lastVirtualItemIndex = this._virtualCount-1; 9206 var lastVirtualItemIndex = this._virtualCount-1;
12918 9207
12919 recycledTileSet = []; 9208 recycledTileSet = [];
12920 9209
12921 kth = this._physicalStart; 9210 kth = this._physicalStart;
12922 currentRatio = bottomSpace / hiddenContentSize; 9211 currentRatio = bottomSpace / hiddenContentSize;
12923 9212
12924 // move tiles from top to bottom
12925 while ( 9213 while (
12926 // approximate `currentRatio` to `ratio`
12927 currentRatio < ratio && 9214 currentRatio < ratio &&
12928 // recycle less physical items than the total
12929 recycledTiles < this._physicalCount && 9215 recycledTiles < this._physicalCount &&
12930 // ensure that these recycled tiles are needed
12931 virtualEnd + recycledTiles < lastVirtualItemIndex && 9216 virtualEnd + recycledTiles < lastVirtualItemIndex &&
12932 // ensure that the tile is not visible
12933 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop 9217 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
12934 ) { 9218 ) {
12935 9219
12936 tileHeight = this._getPhysicalSizeIncrement(kth); 9220 tileHeight = this._getPhysicalSizeIncrement(kth);
12937 currentRatio += tileHeight / hiddenContentSize; 9221 currentRatio += tileHeight / hiddenContentSize;
12938 9222
12939 this._physicalTop += tileHeight; 9223 this._physicalTop += tileHeight;
12940 recycledTileSet.push(kth); 9224 recycledTileSet.push(kth);
12941 recycledTiles++; 9225 recycledTiles++;
12942 kth = (kth + 1) % this._physicalCount; 9226 kth = (kth + 1) % this._physicalCount;
12943 } 9227 }
12944 } 9228 }
12945 9229
12946 if (recycledTiles === 0) { 9230 if (recycledTiles === 0) {
12947 // Try to increase the pool if the list's client height isn't filled up with physical items
12948 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { 9231 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
12949 this._increasePoolIfNeeded(); 9232 this._increasePoolIfNeeded();
12950 } 9233 }
12951 } else { 9234 } else {
12952 this._virtualStart = this._virtualStart + recycledTiles; 9235 this._virtualStart = this._virtualStart + recycledTiles;
12953 this._physicalStart = this._physicalStart + recycledTiles; 9236 this._physicalStart = this._physicalStart + recycledTiles;
12954 this._update(recycledTileSet, movingUp); 9237 this._update(recycledTileSet, movingUp);
12955 } 9238 }
12956 }, 9239 },
12957 9240
12958 /**
12959 * Update the list of items, starting from the `_virtualStart` item.
12960 * @param {!Array<number>=} itemSet
12961 * @param {!Array<number>=} movingUp
12962 */
12963 _update: function(itemSet, movingUp) { 9241 _update: function(itemSet, movingUp) {
12964 // manage focus
12965 this._manageFocus(); 9242 this._manageFocus();
12966 // update models
12967 this._assignModels(itemSet); 9243 this._assignModels(itemSet);
12968 // measure heights
12969 this._updateMetrics(itemSet); 9244 this._updateMetrics(itemSet);
12970 // adjust offset after measuring
12971 if (movingUp) { 9245 if (movingUp) {
12972 while (movingUp.length) { 9246 while (movingUp.length) {
12973 var idx = movingUp.pop(); 9247 var idx = movingUp.pop();
12974 this._physicalTop -= this._getPhysicalSizeIncrement(idx); 9248 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
12975 } 9249 }
12976 } 9250 }
12977 // update the position of the items
12978 this._positionItems(); 9251 this._positionItems();
12979 // set the scroller size
12980 this._updateScrollerSize(); 9252 this._updateScrollerSize();
12981 // increase the pool of physical items
12982 this._increasePoolIfNeeded(); 9253 this._increasePoolIfNeeded();
12983 }, 9254 },
12984 9255
12985 /**
12986 * Creates a pool of DOM elements and attaches them to the local dom.
12987 */
12988 _createPool: function(size) { 9256 _createPool: function(size) {
12989 var physicalItems = new Array(size); 9257 var physicalItems = new Array(size);
12990 9258
12991 this._ensureTemplatized(); 9259 this._ensureTemplatized();
12992 9260
12993 for (var i = 0; i < size; i++) { 9261 for (var i = 0; i < size; i++) {
12994 var inst = this.stamp(null); 9262 var inst = this.stamp(null);
12995 // First element child is item; Safari doesn't support children[0]
12996 // on a doc fragment
12997 physicalItems[i] = inst.root.querySelector('*'); 9263 physicalItems[i] = inst.root.querySelector('*');
12998 Polymer.dom(this).appendChild(inst.root); 9264 Polymer.dom(this).appendChild(inst.root);
12999 } 9265 }
13000 return physicalItems; 9266 return physicalItems;
13001 }, 9267 },
13002 9268
13003 /**
13004 * Increases the pool of physical items only if needed.
13005 *
13006 * @return {boolean} True if the pool was increased.
13007 */
13008 _increasePoolIfNeeded: function() { 9269 _increasePoolIfNeeded: function() {
13009 // Base case 1: the list has no height.
13010 if (this._viewportHeight === 0) { 9270 if (this._viewportHeight === 0) {
13011 return false; 9271 return false;
13012 } 9272 }
13013 // Base case 2: If the physical size is optimal and the list's client heig ht is full
13014 // with physical items, don't increase the pool.
13015 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition; 9273 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
13016 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { 9274 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
13017 return false; 9275 return false;
13018 } 9276 }
13019 // this value should range between [0 <= `currentPage` <= `_maxPages`]
13020 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); 9277 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
13021 9278
13022 if (currentPage === 0) { 9279 if (currentPage === 0) {
13023 // fill the first page
13024 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5))); 9280 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
13025 } else if (this._lastPage !== currentPage && isClientHeightFull) { 9281 } else if (this._lastPage !== currentPage && isClientHeightFull) {
13026 // paint the page and defer the next increase
13027 // wait 16ms which is rough enough to get paint cycle.
13028 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16)); 9282 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
13029 } else { 9283 } else {
13030 // fill the rest of the pages
13031 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ; 9284 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
13032 } 9285 }
13033 9286
13034 this._lastPage = currentPage; 9287 this._lastPage = currentPage;
13035 9288
13036 return true; 9289 return true;
13037 }, 9290 },
13038 9291
13039 /**
13040 * Increases the pool size.
13041 */
13042 _increasePool: function(missingItems) { 9292 _increasePool: function(missingItems) {
13043 var nextPhysicalCount = Math.min( 9293 var nextPhysicalCount = Math.min(
13044 this._physicalCount + missingItems, 9294 this._physicalCount + missingItems,
13045 this._virtualCount - this._virtualStart, 9295 this._virtualCount - this._virtualStart,
13046 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) 9296 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
13047 ); 9297 );
13048 var prevPhysicalCount = this._physicalCount; 9298 var prevPhysicalCount = this._physicalCount;
13049 var delta = nextPhysicalCount - prevPhysicalCount; 9299 var delta = nextPhysicalCount - prevPhysicalCount;
13050 9300
13051 if (delta <= 0) { 9301 if (delta <= 0) {
13052 return; 9302 return;
13053 } 9303 }
13054 9304
13055 [].push.apply(this._physicalItems, this._createPool(delta)); 9305 [].push.apply(this._physicalItems, this._createPool(delta));
13056 [].push.apply(this._physicalSizes, new Array(delta)); 9306 [].push.apply(this._physicalSizes, new Array(delta));
13057 9307
13058 this._physicalCount = prevPhysicalCount + delta; 9308 this._physicalCount = prevPhysicalCount + delta;
13059 9309
13060 // update the physical start if we need to preserve the model of the focus ed item.
13061 // In this situation, the focused item is currently rendered and its model would
13062 // have changed after increasing the pool if the physical start remained u nchanged.
13063 if (this._physicalStart > this._physicalEnd && 9310 if (this._physicalStart > this._physicalEnd &&
13064 this._isIndexRendered(this._focusedIndex) && 9311 this._isIndexRendered(this._focusedIndex) &&
13065 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { 9312 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
13066 this._physicalStart = this._physicalStart + delta; 9313 this._physicalStart = this._physicalStart + delta;
13067 } 9314 }
13068 this._update(); 9315 this._update();
13069 }, 9316 },
13070 9317
13071 /**
13072 * Render a new list of items. This method does exactly the same as `update` ,
13073 * but it also ensures that only one `update` cycle is created.
13074 */
13075 _render: function() { 9318 _render: function() {
13076 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; 9319 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
13077 9320
13078 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { 9321 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
13079 this._lastPage = 0; 9322 this._lastPage = 0;
13080 this._update(); 9323 this._update();
13081 this._itemsRendered = true; 9324 this._itemsRendered = true;
13082 } 9325 }
13083 }, 9326 },
13084 9327
13085 /**
13086 * Templetizes the user template.
13087 */
13088 _ensureTemplatized: function() { 9328 _ensureTemplatized: function() {
13089 if (!this.ctor) { 9329 if (!this.ctor) {
13090 // Template instance props that should be excluded from forwarding
13091 var props = {}; 9330 var props = {};
13092 props.__key__ = true; 9331 props.__key__ = true;
13093 props[this.as] = true; 9332 props[this.as] = true;
13094 props[this.indexAs] = true; 9333 props[this.indexAs] = true;
13095 props[this.selectedAs] = true; 9334 props[this.selectedAs] = true;
13096 props.tabIndex = true; 9335 props.tabIndex = true;
13097 9336
13098 this._instanceProps = props; 9337 this._instanceProps = props;
13099 this._userTemplate = Polymer.dom(this).querySelector('template'); 9338 this._userTemplate = Polymer.dom(this).querySelector('template');
13100 9339
13101 if (this._userTemplate) { 9340 if (this._userTemplate) {
13102 this.templatize(this._userTemplate); 9341 this.templatize(this._userTemplate);
13103 } else { 9342 } else {
13104 console.warn('iron-list requires a template to be provided in light-do m'); 9343 console.warn('iron-list requires a template to be provided in light-do m');
13105 } 9344 }
13106 } 9345 }
13107 }, 9346 },
13108 9347
13109 /**
13110 * Implements extension point from Templatizer mixin.
13111 */
13112 _getStampedChildren: function() { 9348 _getStampedChildren: function() {
13113 return this._physicalItems; 9349 return this._physicalItems;
13114 }, 9350 },
13115 9351
13116 /**
13117 * Implements extension point from Templatizer
13118 * Called as a side effect of a template instance path change, responsible
13119 * for notifying items.<key-for-instance>.<path> change up to host.
13120 */
13121 _forwardInstancePath: function(inst, path, value) { 9352 _forwardInstancePath: function(inst, path, value) {
13122 if (path.indexOf(this.as + '.') === 0) { 9353 if (path.indexOf(this.as + '.') === 0) {
13123 this.notifyPath('items.' + inst.__key__ + '.' + 9354 this.notifyPath('items.' + inst.__key__ + '.' +
13124 path.slice(this.as.length + 1), value); 9355 path.slice(this.as.length + 1), value);
13125 } 9356 }
13126 }, 9357 },
13127 9358
13128 /**
13129 * Implements extension point from Templatizer mixin
13130 * Called as side-effect of a host property change, responsible for
13131 * notifying parent path change on each row.
13132 */
13133 _forwardParentProp: function(prop, value) { 9359 _forwardParentProp: function(prop, value) {
13134 if (this._physicalItems) { 9360 if (this._physicalItems) {
13135 this._physicalItems.forEach(function(item) { 9361 this._physicalItems.forEach(function(item) {
13136 item._templateInstance[prop] = value; 9362 item._templateInstance[prop] = value;
13137 }, this); 9363 }, this);
13138 } 9364 }
13139 }, 9365 },
13140 9366
13141 /**
13142 * Implements extension point from Templatizer
13143 * Called as side-effect of a host path change, responsible for
13144 * notifying parent.<path> path change on each row.
13145 */
13146 _forwardParentPath: function(path, value) { 9367 _forwardParentPath: function(path, value) {
13147 if (this._physicalItems) { 9368 if (this._physicalItems) {
13148 this._physicalItems.forEach(function(item) { 9369 this._physicalItems.forEach(function(item) {
13149 item._templateInstance.notifyPath(path, value, true); 9370 item._templateInstance.notifyPath(path, value, true);
13150 }, this); 9371 }, this);
13151 } 9372 }
13152 }, 9373 },
13153 9374
13154 /**
13155 * Called as a side effect of a host items.<key>.<path> path change,
13156 * responsible for notifying item.<path> changes.
13157 */
13158 _forwardItemPath: function(path, value) { 9375 _forwardItemPath: function(path, value) {
13159 if (!this._physicalIndexForKey) { 9376 if (!this._physicalIndexForKey) {
13160 return; 9377 return;
13161 } 9378 }
13162 var dot = path.indexOf('.'); 9379 var dot = path.indexOf('.');
13163 var key = path.substring(0, dot < 0 ? path.length : dot); 9380 var key = path.substring(0, dot < 0 ? path.length : dot);
13164 var idx = this._physicalIndexForKey[key]; 9381 var idx = this._physicalIndexForKey[key];
13165 var offscreenItem = this._offscreenFocusedItem; 9382 var offscreenItem = this._offscreenFocusedItem;
13166 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ? 9383 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
13167 offscreenItem : this._physicalItems[idx]; 9384 offscreenItem : this._physicalItems[idx];
13168 9385
13169 if (!el || el._templateInstance.__key__ !== key) { 9386 if (!el || el._templateInstance.__key__ !== key) {
13170 return; 9387 return;
13171 } 9388 }
13172 if (dot >= 0) { 9389 if (dot >= 0) {
13173 path = this.as + '.' + path.substring(dot+1); 9390 path = this.as + '.' + path.substring(dot+1);
13174 el._templateInstance.notifyPath(path, value, true); 9391 el._templateInstance.notifyPath(path, value, true);
13175 } else { 9392 } else {
13176 // Update selection if needed
13177 var currentItem = el._templateInstance[this.as]; 9393 var currentItem = el._templateInstance[this.as];
13178 if (Array.isArray(this.selectedItems)) { 9394 if (Array.isArray(this.selectedItems)) {
13179 for (var i = 0; i < this.selectedItems.length; i++) { 9395 for (var i = 0; i < this.selectedItems.length; i++) {
13180 if (this.selectedItems[i] === currentItem) { 9396 if (this.selectedItems[i] === currentItem) {
13181 this.set('selectedItems.' + i, value); 9397 this.set('selectedItems.' + i, value);
13182 break; 9398 break;
13183 } 9399 }
13184 } 9400 }
13185 } else if (this.selectedItem === currentItem) { 9401 } else if (this.selectedItem === currentItem) {
13186 this.set('selectedItem', value); 9402 this.set('selectedItem', value);
13187 } 9403 }
13188 el._templateInstance[this.as] = value; 9404 el._templateInstance[this.as] = value;
13189 } 9405 }
13190 }, 9406 },
13191 9407
13192 /**
13193 * Called when the items have changed. That is, ressignments
13194 * to `items`, splices or updates to a single item.
13195 */
13196 _itemsChanged: function(change) { 9408 _itemsChanged: function(change) {
13197 if (change.path === 'items') { 9409 if (change.path === 'items') {
13198 // reset items
13199 this._virtualStart = 0; 9410 this._virtualStart = 0;
13200 this._physicalTop = 0; 9411 this._physicalTop = 0;
13201 this._virtualCount = this.items ? this.items.length : 0; 9412 this._virtualCount = this.items ? this.items.length : 0;
13202 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; 9413 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
13203 this._physicalIndexForKey = {}; 9414 this._physicalIndexForKey = {};
13204 this._firstVisibleIndexVal = null; 9415 this._firstVisibleIndexVal = null;
13205 this._lastVisibleIndexVal = null; 9416 this._lastVisibleIndexVal = null;
13206 9417
13207 this._resetScrollPosition(0); 9418 this._resetScrollPosition(0);
13208 this._removeFocusedItem(); 9419 this._removeFocusedItem();
13209 // create the initial physical items
13210 if (!this._physicalItems) { 9420 if (!this._physicalItems) {
13211 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); 9421 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
13212 this._physicalItems = this._createPool(this._physicalCount); 9422 this._physicalItems = this._createPool(this._physicalCount);
13213 this._physicalSizes = new Array(this._physicalCount); 9423 this._physicalSizes = new Array(this._physicalCount);
13214 } 9424 }
13215 9425
13216 this._physicalStart = 0; 9426 this._physicalStart = 0;
13217 9427
13218 } else if (change.path === 'items.splices') { 9428 } else if (change.path === 'items.splices') {
13219 9429
13220 this._adjustVirtualIndex(change.value.indexSplices); 9430 this._adjustVirtualIndex(change.value.indexSplices);
13221 this._virtualCount = this.items ? this.items.length : 0; 9431 this._virtualCount = this.items ? this.items.length : 0;
13222 9432
13223 } else { 9433 } else {
13224 // update a single item
13225 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); 9434 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
13226 return; 9435 return;
13227 } 9436 }
13228 9437
13229 this._itemsRendered = false; 9438 this._itemsRendered = false;
13230 this._debounceTemplate(this._render); 9439 this._debounceTemplate(this._render);
13231 }, 9440 },
13232 9441
13233 /**
13234 * @param {!Array<!PolymerSplice>} splices
13235 */
13236 _adjustVirtualIndex: function(splices) { 9442 _adjustVirtualIndex: function(splices) {
13237 splices.forEach(function(splice) { 9443 splices.forEach(function(splice) {
13238 // deselect removed items
13239 splice.removed.forEach(this._removeItem, this); 9444 splice.removed.forEach(this._removeItem, this);
13240 // We only need to care about changes happening above the current positi on
13241 if (splice.index < this._virtualStart) { 9445 if (splice.index < this._virtualStart) {
13242 var delta = Math.max( 9446 var delta = Math.max(
13243 splice.addedCount - splice.removed.length, 9447 splice.addedCount - splice.removed.length,
13244 splice.index - this._virtualStart); 9448 splice.index - this._virtualStart);
13245 9449
13246 this._virtualStart = this._virtualStart + delta; 9450 this._virtualStart = this._virtualStart + delta;
13247 9451
13248 if (this._focusedIndex >= 0) { 9452 if (this._focusedIndex >= 0) {
13249 this._focusedIndex = this._focusedIndex + delta; 9453 this._focusedIndex = this._focusedIndex + delta;
13250 } 9454 }
13251 } 9455 }
13252 }, this); 9456 }, this);
13253 }, 9457 },
13254 9458
13255 _removeItem: function(item) { 9459 _removeItem: function(item) {
13256 this.$.selector.deselect(item); 9460 this.$.selector.deselect(item);
13257 // remove the current focused item
13258 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) { 9461 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
13259 this._removeFocusedItem(); 9462 this._removeFocusedItem();
13260 } 9463 }
13261 }, 9464 },
13262 9465
13263 /**
13264 * Executes a provided function per every physical index in `itemSet`
13265 * `itemSet` default value is equivalent to the entire set of physical index es.
13266 *
13267 * @param {!function(number, number)} fn
13268 * @param {!Array<number>=} itemSet
13269 */
13270 _iterateItems: function(fn, itemSet) { 9466 _iterateItems: function(fn, itemSet) {
13271 var pidx, vidx, rtn, i; 9467 var pidx, vidx, rtn, i;
13272 9468
13273 if (arguments.length === 2 && itemSet) { 9469 if (arguments.length === 2 && itemSet) {
13274 for (i = 0; i < itemSet.length; i++) { 9470 for (i = 0; i < itemSet.length; i++) {
13275 pidx = itemSet[i]; 9471 pidx = itemSet[i];
13276 vidx = this._computeVidx(pidx); 9472 vidx = this._computeVidx(pidx);
13277 if ((rtn = fn.call(this, pidx, vidx)) != null) { 9473 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13278 return rtn; 9474 return rtn;
13279 } 9475 }
13280 } 9476 }
13281 } else { 9477 } else {
13282 pidx = this._physicalStart; 9478 pidx = this._physicalStart;
13283 vidx = this._virtualStart; 9479 vidx = this._virtualStart;
13284 9480
13285 for (; pidx < this._physicalCount; pidx++, vidx++) { 9481 for (; pidx < this._physicalCount; pidx++, vidx++) {
13286 if ((rtn = fn.call(this, pidx, vidx)) != null) { 9482 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13287 return rtn; 9483 return rtn;
13288 } 9484 }
13289 } 9485 }
13290 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { 9486 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
13291 if ((rtn = fn.call(this, pidx, vidx)) != null) { 9487 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13292 return rtn; 9488 return rtn;
13293 } 9489 }
13294 } 9490 }
13295 } 9491 }
13296 }, 9492 },
13297 9493
13298 /**
13299 * Returns the virtual index for a given physical index
13300 *
13301 * @param {number} pidx Physical index
13302 * @return {number}
13303 */
13304 _computeVidx: function(pidx) { 9494 _computeVidx: function(pidx) {
13305 if (pidx >= this._physicalStart) { 9495 if (pidx >= this._physicalStart) {
13306 return this._virtualStart + (pidx - this._physicalStart); 9496 return this._virtualStart + (pidx - this._physicalStart);
13307 } 9497 }
13308 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; 9498 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
13309 }, 9499 },
13310 9500
13311 /**
13312 * Assigns the data models to a given set of items.
13313 * @param {!Array<number>=} itemSet
13314 */
13315 _assignModels: function(itemSet) { 9501 _assignModels: function(itemSet) {
13316 this._iterateItems(function(pidx, vidx) { 9502 this._iterateItems(function(pidx, vidx) {
13317 var el = this._physicalItems[pidx]; 9503 var el = this._physicalItems[pidx];
13318 var inst = el._templateInstance; 9504 var inst = el._templateInstance;
13319 var item = this.items && this.items[vidx]; 9505 var item = this.items && this.items[vidx];
13320 9506
13321 if (item != null) { 9507 if (item != null) {
13322 inst[this.as] = item; 9508 inst[this.as] = item;
13323 inst.__key__ = this._collection.getKey(item); 9509 inst.__key__ = this._collection.getKey(item);
13324 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item); 9510 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item);
13325 inst[this.indexAs] = vidx; 9511 inst[this.indexAs] = vidx;
13326 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; 9512 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
13327 this._physicalIndexForKey[inst.__key__] = pidx; 9513 this._physicalIndexForKey[inst.__key__] = pidx;
13328 el.removeAttribute('hidden'); 9514 el.removeAttribute('hidden');
13329 } else { 9515 } else {
13330 inst.__key__ = null; 9516 inst.__key__ = null;
13331 el.setAttribute('hidden', ''); 9517 el.setAttribute('hidden', '');
13332 } 9518 }
13333 }, itemSet); 9519 }, itemSet);
13334 }, 9520 },
13335 9521
13336 /**
13337 * Updates the height for a given set of items.
13338 *
13339 * @param {!Array<number>=} itemSet
13340 */
13341 _updateMetrics: function(itemSet) { 9522 _updateMetrics: function(itemSet) {
13342 // Make sure we distributed all the physical items
13343 // so we can measure them
13344 Polymer.dom.flush(); 9523 Polymer.dom.flush();
13345 9524
13346 var newPhysicalSize = 0; 9525 var newPhysicalSize = 0;
13347 var oldPhysicalSize = 0; 9526 var oldPhysicalSize = 0;
13348 var prevAvgCount = this._physicalAverageCount; 9527 var prevAvgCount = this._physicalAverageCount;
13349 var prevPhysicalAvg = this._physicalAverage; 9528 var prevPhysicalAvg = this._physicalAverage;
13350 9529
13351 this._iterateItems(function(pidx, vidx) { 9530 this._iterateItems(function(pidx, vidx) {
13352 9531
13353 oldPhysicalSize += this._physicalSizes[pidx] || 0; 9532 oldPhysicalSize += this._physicalSizes[pidx] || 0;
13354 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; 9533 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
13355 newPhysicalSize += this._physicalSizes[pidx]; 9534 newPhysicalSize += this._physicalSizes[pidx];
13356 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; 9535 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
13357 9536
13358 }, itemSet); 9537 }, itemSet);
13359 9538
13360 this._viewportHeight = this._scrollTargetHeight; 9539 this._viewportHeight = this._scrollTargetHeight;
13361 if (this.grid) { 9540 if (this.grid) {
13362 this._updateGridMetrics(); 9541 this._updateGridMetrics();
13363 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight; 9542 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
13364 } else { 9543 } else {
13365 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize; 9544 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
13366 } 9545 }
13367 9546
13368 // update the average if we measured something
13369 if (this._physicalAverageCount !== prevAvgCount) { 9547 if (this._physicalAverageCount !== prevAvgCount) {
13370 this._physicalAverage = Math.round( 9548 this._physicalAverage = Math.round(
13371 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / 9549 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
13372 this._physicalAverageCount); 9550 this._physicalAverageCount);
13373 } 9551 }
13374 }, 9552 },
13375 9553
13376 _updateGridMetrics: function() { 9554 _updateGridMetrics: function() {
13377 this._viewportWidth = this.$.items.offsetWidth; 9555 this._viewportWidth = this.$.items.offsetWidth;
13378 // Set item width to the value of the _physicalItems offsetWidth
13379 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE; 9556 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
13380 // Set row height to the value of the _physicalItems offsetHeight
13381 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE; 9557 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
13382 // If in grid mode compute how many items with exist in each row
13383 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow; 9558 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
13384 }, 9559 },
13385 9560
13386 /**
13387 * Updates the position of the physical items.
13388 */
13389 _positionItems: function() { 9561 _positionItems: function() {
13390 this._adjustScrollPosition(); 9562 this._adjustScrollPosition();
13391 9563
13392 var y = this._physicalTop; 9564 var y = this._physicalTop;
13393 9565
13394 if (this.grid) { 9566 if (this.grid) {
13395 var totalItemWidth = this._itemsPerRow * this._itemWidth; 9567 var totalItemWidth = this._itemsPerRow * this._itemWidth;
13396 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; 9568 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
13397 9569
13398 this._iterateItems(function(pidx, vidx) { 9570 this._iterateItems(function(pidx, vidx) {
(...skipping 21 matching lines...) Expand all
13420 _getPhysicalSizeIncrement: function(pidx) { 9592 _getPhysicalSizeIncrement: function(pidx) {
13421 if (!this.grid) { 9593 if (!this.grid) {
13422 return this._physicalSizes[pidx]; 9594 return this._physicalSizes[pidx];
13423 } 9595 }
13424 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) { 9596 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
13425 return 0; 9597 return 0;
13426 } 9598 }
13427 return this._rowHeight; 9599 return this._rowHeight;
13428 }, 9600 },
13429 9601
13430 /**
13431 * Returns, based on the current index,
13432 * whether or not the next index will need
13433 * to be rendered on a new row.
13434 *
13435 * @param {number} vidx Virtual index
13436 * @return {boolean}
13437 */
13438 _shouldRenderNextRow: function(vidx) { 9602 _shouldRenderNextRow: function(vidx) {
13439 return vidx % this._itemsPerRow === this._itemsPerRow - 1; 9603 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
13440 }, 9604 },
13441 9605
13442 /**
13443 * Adjusts the scroll position when it was overestimated.
13444 */
13445 _adjustScrollPosition: function() { 9606 _adjustScrollPosition: function() {
13446 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : 9607 var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
13447 Math.min(this._scrollPosition + this._physicalTop, 0); 9608 Math.min(this._scrollPosition + this._physicalTop, 0);
13448 9609
13449 if (deltaHeight) { 9610 if (deltaHeight) {
13450 this._physicalTop = this._physicalTop - deltaHeight; 9611 this._physicalTop = this._physicalTop - deltaHeight;
13451 // juking scroll position during interial scrolling on iOS is no bueno
13452 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { 9612 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
13453 this._resetScrollPosition(this._scrollTop - deltaHeight); 9613 this._resetScrollPosition(this._scrollTop - deltaHeight);
13454 } 9614 }
13455 } 9615 }
13456 }, 9616 },
13457 9617
13458 /**
13459 * Sets the position of the scroll.
13460 */
13461 _resetScrollPosition: function(pos) { 9618 _resetScrollPosition: function(pos) {
13462 if (this.scrollTarget) { 9619 if (this.scrollTarget) {
13463 this._scrollTop = pos; 9620 this._scrollTop = pos;
13464 this._scrollPosition = this._scrollTop; 9621 this._scrollPosition = this._scrollTop;
13465 } 9622 }
13466 }, 9623 },
13467 9624
13468 /**
13469 * Sets the scroll height, that's the height of the content,
13470 *
13471 * @param {boolean=} forceUpdate If true, updates the height no matter what.
13472 */
13473 _updateScrollerSize: function(forceUpdate) { 9625 _updateScrollerSize: function(forceUpdate) {
13474 if (this.grid) { 9626 if (this.grid) {
13475 this._estScrollHeight = this._virtualRowCount * this._rowHeight; 9627 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
13476 } else { 9628 } else {
13477 this._estScrollHeight = (this._physicalBottom + 9629 this._estScrollHeight = (this._physicalBottom +
13478 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage); 9630 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
13479 } 9631 }
13480 9632
13481 forceUpdate = forceUpdate || this._scrollHeight === 0; 9633 forceUpdate = forceUpdate || this._scrollHeight === 0;
13482 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; 9634 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
13483 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight; 9635 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
13484 9636
13485 // amortize height adjustment, so it won't trigger repaints very often
13486 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { 9637 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
13487 this.$.items.style.height = this._estScrollHeight + 'px'; 9638 this.$.items.style.height = this._estScrollHeight + 'px';
13488 this._scrollHeight = this._estScrollHeight; 9639 this._scrollHeight = this._estScrollHeight;
13489 } 9640 }
13490 }, 9641 },
13491 9642
13492 /**
13493 * Scroll to a specific item in the virtual list regardless
13494 * of the physical items in the DOM tree.
13495 *
13496 * @method scrollToItem
13497 * @param {(Object)} item The item to be scrolled to
13498 */
13499 scrollToItem: function(item){ 9643 scrollToItem: function(item){
13500 return this.scrollToIndex(this.items.indexOf(item)); 9644 return this.scrollToIndex(this.items.indexOf(item));
13501 }, 9645 },
13502 9646
13503 /**
13504 * Scroll to a specific index in the virtual list regardless
13505 * of the physical items in the DOM tree.
13506 *
13507 * @method scrollToIndex
13508 * @param {number} idx The index of the item
13509 */
13510 scrollToIndex: function(idx) { 9647 scrollToIndex: function(idx) {
13511 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { 9648 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
13512 return; 9649 return;
13513 } 9650 }
13514 9651
13515 Polymer.dom.flush(); 9652 Polymer.dom.flush();
13516 9653
13517 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); 9654 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
13518 // update the virtual start only when needed
13519 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { 9655 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
13520 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1); 9656 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
13521 } 9657 }
13522 // manage focus
13523 this._manageFocus(); 9658 this._manageFocus();
13524 // assign new models
13525 this._assignModels(); 9659 this._assignModels();
13526 // measure the new sizes
13527 this._updateMetrics(); 9660 this._updateMetrics();
13528 9661
13529 // estimate new physical offset
13530 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage; 9662 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
13531 this._physicalTop = estPhysicalTop; 9663 this._physicalTop = estPhysicalTop;
13532 9664
13533 var currentTopItem = this._physicalStart; 9665 var currentTopItem = this._physicalStart;
13534 var currentVirtualItem = this._virtualStart; 9666 var currentVirtualItem = this._virtualStart;
13535 var targetOffsetTop = 0; 9667 var targetOffsetTop = 0;
13536 var hiddenContentSize = this._hiddenContentSize; 9668 var hiddenContentSize = this._hiddenContentSize;
13537 9669
13538 // scroll to the item as much as we can
13539 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { 9670 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
13540 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem); 9671 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
13541 currentTopItem = (currentTopItem + 1) % this._physicalCount; 9672 currentTopItem = (currentTopItem + 1) % this._physicalCount;
13542 currentVirtualItem++; 9673 currentVirtualItem++;
13543 } 9674 }
13544 // update the scroller size
13545 this._updateScrollerSize(true); 9675 this._updateScrollerSize(true);
13546 // update the position of the items
13547 this._positionItems(); 9676 this._positionItems();
13548 // set the new scroll position
13549 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop); 9677 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
13550 // increase the pool of physical items if needed
13551 this._increasePoolIfNeeded(); 9678 this._increasePoolIfNeeded();
13552 // clear cached visible index
13553 this._firstVisibleIndexVal = null; 9679 this._firstVisibleIndexVal = null;
13554 this._lastVisibleIndexVal = null; 9680 this._lastVisibleIndexVal = null;
13555 }, 9681 },
13556 9682
13557 /**
13558 * Reset the physical average and the average count.
13559 */
13560 _resetAverage: function() { 9683 _resetAverage: function() {
13561 this._physicalAverage = 0; 9684 this._physicalAverage = 0;
13562 this._physicalAverageCount = 0; 9685 this._physicalAverageCount = 0;
13563 }, 9686 },
13564 9687
13565 /**
13566 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
13567 * when the element is resized.
13568 */
13569 _resizeHandler: function() { 9688 _resizeHandler: function() {
13570 // iOS fires the resize event when the address bar slides up
13571 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) { 9689 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
13572 return; 9690 return;
13573 } 9691 }
13574 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
13575 // changing the scroll position from a resize handler would result in
13576 // the scroll position being reset. Waiting 1ms fixes the issue.
13577 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { 9692 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
13578 this.updateViewportBoundaries(); 9693 this.updateViewportBoundaries();
13579 this._render(); 9694 this._render();
13580 9695
13581 if (this._itemsRendered && this._physicalItems && this._isVisible) { 9696 if (this._itemsRendered && this._physicalItems && this._isVisible) {
13582 this._resetAverage(); 9697 this._resetAverage();
13583 this.scrollToIndex(this.firstVisibleIndex); 9698 this.scrollToIndex(this.firstVisibleIndex);
13584 } 9699 }
13585 }.bind(this), 1)); 9700 }.bind(this), 1));
13586 }, 9701 },
13587 9702
13588 _getModelFromItem: function(item) { 9703 _getModelFromItem: function(item) {
13589 var key = this._collection.getKey(item); 9704 var key = this._collection.getKey(item);
13590 var pidx = this._physicalIndexForKey[key]; 9705 var pidx = this._physicalIndexForKey[key];
13591 9706
13592 if (pidx != null) { 9707 if (pidx != null) {
13593 return this._physicalItems[pidx]._templateInstance; 9708 return this._physicalItems[pidx]._templateInstance;
13594 } 9709 }
13595 return null; 9710 return null;
13596 }, 9711 },
13597 9712
13598 /**
13599 * Gets a valid item instance from its index or the object value.
13600 *
13601 * @param {(Object|number)} item The item object or its index
13602 */
13603 _getNormalizedItem: function(item) { 9713 _getNormalizedItem: function(item) {
13604 if (this._collection.getKey(item) === undefined) { 9714 if (this._collection.getKey(item) === undefined) {
13605 if (typeof item === 'number') { 9715 if (typeof item === 'number') {
13606 item = this.items[item]; 9716 item = this.items[item];
13607 if (!item) { 9717 if (!item) {
13608 throw new RangeError('<item> not found'); 9718 throw new RangeError('<item> not found');
13609 } 9719 }
13610 return item; 9720 return item;
13611 } 9721 }
13612 throw new TypeError('<item> should be a valid item'); 9722 throw new TypeError('<item> should be a valid item');
13613 } 9723 }
13614 return item; 9724 return item;
13615 }, 9725 },
13616 9726
13617 /**
13618 * Select the list item at the given index.
13619 *
13620 * @method selectItem
13621 * @param {(Object|number)} item The item object or its index
13622 */
13623 selectItem: function(item) { 9727 selectItem: function(item) {
13624 item = this._getNormalizedItem(item); 9728 item = this._getNormalizedItem(item);
13625 var model = this._getModelFromItem(item); 9729 var model = this._getModelFromItem(item);
13626 9730
13627 if (!this.multiSelection && this.selectedItem) { 9731 if (!this.multiSelection && this.selectedItem) {
13628 this.deselectItem(this.selectedItem); 9732 this.deselectItem(this.selectedItem);
13629 } 9733 }
13630 if (model) { 9734 if (model) {
13631 model[this.selectedAs] = true; 9735 model[this.selectedAs] = true;
13632 } 9736 }
13633 this.$.selector.select(item); 9737 this.$.selector.select(item);
13634 this.updateSizeForItem(item); 9738 this.updateSizeForItem(item);
13635 }, 9739 },
13636 9740
13637 /**
13638 * Deselects the given item list if it is already selected.
13639 *
13640
13641 * @method deselect
13642 * @param {(Object|number)} item The item object or its index
13643 */
13644 deselectItem: function(item) { 9741 deselectItem: function(item) {
13645 item = this._getNormalizedItem(item); 9742 item = this._getNormalizedItem(item);
13646 var model = this._getModelFromItem(item); 9743 var model = this._getModelFromItem(item);
13647 9744
13648 if (model) { 9745 if (model) {
13649 model[this.selectedAs] = false; 9746 model[this.selectedAs] = false;
13650 } 9747 }
13651 this.$.selector.deselect(item); 9748 this.$.selector.deselect(item);
13652 this.updateSizeForItem(item); 9749 this.updateSizeForItem(item);
13653 }, 9750 },
13654 9751
13655 /**
13656 * Select or deselect a given item depending on whether the item
13657 * has already been selected.
13658 *
13659 * @method toggleSelectionForItem
13660 * @param {(Object|number)} item The item object or its index
13661 */
13662 toggleSelectionForItem: function(item) { 9752 toggleSelectionForItem: function(item) {
13663 item = this._getNormalizedItem(item); 9753 item = this._getNormalizedItem(item);
13664 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) { 9754 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) {
13665 this.deselectItem(item); 9755 this.deselectItem(item);
13666 } else { 9756 } else {
13667 this.selectItem(item); 9757 this.selectItem(item);
13668 } 9758 }
13669 }, 9759 },
13670 9760
13671 /**
13672 * Clears the current selection state of the list.
13673 *
13674 * @method clearSelection
13675 */
13676 clearSelection: function() { 9761 clearSelection: function() {
13677 function unselect(item) { 9762 function unselect(item) {
13678 var model = this._getModelFromItem(item); 9763 var model = this._getModelFromItem(item);
13679 if (model) { 9764 if (model) {
13680 model[this.selectedAs] = false; 9765 model[this.selectedAs] = false;
13681 } 9766 }
13682 } 9767 }
13683 9768
13684 if (Array.isArray(this.selectedItems)) { 9769 if (Array.isArray(this.selectedItems)) {
13685 this.selectedItems.forEach(unselect, this); 9770 this.selectedItems.forEach(unselect, this);
13686 } else if (this.selectedItem) { 9771 } else if (this.selectedItem) {
13687 unselect.call(this, this.selectedItem); 9772 unselect.call(this, this.selectedItem);
13688 } 9773 }
13689 9774
13690 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); 9775 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
13691 }, 9776 },
13692 9777
13693 /**
13694 * Add an event listener to `tap` if `selectionEnabled` is true,
13695 * it will remove the listener otherwise.
13696 */
13697 _selectionEnabledChanged: function(selectionEnabled) { 9778 _selectionEnabledChanged: function(selectionEnabled) {
13698 var handler = selectionEnabled ? this.listen : this.unlisten; 9779 var handler = selectionEnabled ? this.listen : this.unlisten;
13699 handler.call(this, this, 'tap', '_selectionHandler'); 9780 handler.call(this, this, 'tap', '_selectionHandler');
13700 }, 9781 },
13701 9782
13702 /**
13703 * Select an item from an event object.
13704 */
13705 _selectionHandler: function(e) { 9783 _selectionHandler: function(e) {
13706 var model = this.modelForElement(e.target); 9784 var model = this.modelForElement(e.target);
13707 if (!model) { 9785 if (!model) {
13708 return; 9786 return;
13709 } 9787 }
13710 var modelTabIndex, activeElTabIndex; 9788 var modelTabIndex, activeElTabIndex;
13711 var target = Polymer.dom(e).path[0]; 9789 var target = Polymer.dom(e).path[0];
13712 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement; 9790 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
13713 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])]; 9791 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
13714 // Safari does not focus certain form controls via mouse
13715 // https://bugs.webkit.org/show_bug.cgi?id=118043
13716 if (target.localName === 'input' || 9792 if (target.localName === 'input' ||
13717 target.localName === 'button' || 9793 target.localName === 'button' ||
13718 target.localName === 'select') { 9794 target.localName === 'select') {
13719 return; 9795 return;
13720 } 9796 }
13721 // Set a temporary tabindex
13722 modelTabIndex = model.tabIndex; 9797 modelTabIndex = model.tabIndex;
13723 model.tabIndex = SECRET_TABINDEX; 9798 model.tabIndex = SECRET_TABINDEX;
13724 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; 9799 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
13725 model.tabIndex = modelTabIndex; 9800 model.tabIndex = modelTabIndex;
13726 // Only select the item if the tap wasn't on a focusable child
13727 // or the element bound to `tabIndex`
13728 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) { 9801 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
13729 return; 9802 return;
13730 } 9803 }
13731 this.toggleSelectionForItem(model[this.as]); 9804 this.toggleSelectionForItem(model[this.as]);
13732 }, 9805 },
13733 9806
13734 _multiSelectionChanged: function(multiSelection) { 9807 _multiSelectionChanged: function(multiSelection) {
13735 this.clearSelection(); 9808 this.clearSelection();
13736 this.$.selector.multi = multiSelection; 9809 this.$.selector.multi = multiSelection;
13737 }, 9810 },
13738 9811
13739 /**
13740 * Updates the size of an item.
13741 *
13742 * @method updateSizeForItem
13743 * @param {(Object|number)} item The item object or its index
13744 */
13745 updateSizeForItem: function(item) { 9812 updateSizeForItem: function(item) {
13746 item = this._getNormalizedItem(item); 9813 item = this._getNormalizedItem(item);
13747 var key = this._collection.getKey(item); 9814 var key = this._collection.getKey(item);
13748 var pidx = this._physicalIndexForKey[key]; 9815 var pidx = this._physicalIndexForKey[key];
13749 9816
13750 if (pidx != null) { 9817 if (pidx != null) {
13751 this._updateMetrics([pidx]); 9818 this._updateMetrics([pidx]);
13752 this._positionItems(); 9819 this._positionItems();
13753 } 9820 }
13754 }, 9821 },
13755 9822
13756 /**
13757 * Creates a temporary backfill item in the rendered pool of physical items
13758 * to replace the main focused item. The focused item has tabIndex = 0
13759 * and might be currently focused by the user.
13760 *
13761 * This dynamic replacement helps to preserve the focus state.
13762 */
13763 _manageFocus: function() { 9823 _manageFocus: function() {
13764 var fidx = this._focusedIndex; 9824 var fidx = this._focusedIndex;
13765 9825
13766 if (fidx >= 0 && fidx < this._virtualCount) { 9826 if (fidx >= 0 && fidx < this._virtualCount) {
13767 // if it's a valid index, check if that index is rendered
13768 // in a physical item.
13769 if (this._isIndexRendered(fidx)) { 9827 if (this._isIndexRendered(fidx)) {
13770 this._restoreFocusedItem(); 9828 this._restoreFocusedItem();
13771 } else { 9829 } else {
13772 this._createFocusBackfillItem(); 9830 this._createFocusBackfillItem();
13773 } 9831 }
13774 } else if (this._virtualCount > 0 && this._physicalCount > 0) { 9832 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
13775 // otherwise, assign the initial focused index.
13776 this._focusedIndex = this._virtualStart; 9833 this._focusedIndex = this._virtualStart;
13777 this._focusedItem = this._physicalItems[this._physicalStart]; 9834 this._focusedItem = this._physicalItems[this._physicalStart];
13778 } 9835 }
13779 }, 9836 },
13780 9837
13781 _isIndexRendered: function(idx) { 9838 _isIndexRendered: function(idx) {
13782 return idx >= this._virtualStart && idx <= this._virtualEnd; 9839 return idx >= this._virtualStart && idx <= this._virtualEnd;
13783 }, 9840 },
13784 9841
13785 _isIndexVisible: function(idx) { 9842 _isIndexVisible: function(idx) {
13786 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; 9843 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
13787 }, 9844 },
13788 9845
13789 _getPhysicalIndex: function(idx) { 9846 _getPhysicalIndex: function(idx) {
13790 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))]; 9847 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
13791 }, 9848 },
13792 9849
13793 _focusPhysicalItem: function(idx) { 9850 _focusPhysicalItem: function(idx) {
13794 if (idx < 0 || idx >= this._virtualCount) { 9851 if (idx < 0 || idx >= this._virtualCount) {
13795 return; 9852 return;
13796 } 9853 }
13797 this._restoreFocusedItem(); 9854 this._restoreFocusedItem();
13798 // scroll to index to make sure it's rendered
13799 if (!this._isIndexRendered(idx)) { 9855 if (!this._isIndexRendered(idx)) {
13800 this.scrollToIndex(idx); 9856 this.scrollToIndex(idx);
13801 } 9857 }
13802 9858
13803 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; 9859 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
13804 var model = physicalItem._templateInstance; 9860 var model = physicalItem._templateInstance;
13805 var focusable; 9861 var focusable;
13806 9862
13807 // set a secret tab index
13808 model.tabIndex = SECRET_TABINDEX; 9863 model.tabIndex = SECRET_TABINDEX;
13809 // check if focusable element is the physical item
13810 if (physicalItem.tabIndex === SECRET_TABINDEX) { 9864 if (physicalItem.tabIndex === SECRET_TABINDEX) {
13811 focusable = physicalItem; 9865 focusable = physicalItem;
13812 } 9866 }
13813 // search for the element which tabindex is bound to the secret tab index
13814 if (!focusable) { 9867 if (!focusable) {
13815 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]'); 9868 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
13816 } 9869 }
13817 // restore the tab index
13818 model.tabIndex = 0; 9870 model.tabIndex = 0;
13819 // focus the focusable element
13820 this._focusedIndex = idx; 9871 this._focusedIndex = idx;
13821 focusable && focusable.focus(); 9872 focusable && focusable.focus();
13822 }, 9873 },
13823 9874
13824 _removeFocusedItem: function() { 9875 _removeFocusedItem: function() {
13825 if (this._offscreenFocusedItem) { 9876 if (this._offscreenFocusedItem) {
13826 Polymer.dom(this).removeChild(this._offscreenFocusedItem); 9877 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
13827 } 9878 }
13828 this._offscreenFocusedItem = null; 9879 this._offscreenFocusedItem = null;
13829 this._focusBackfillItem = null; 9880 this._focusBackfillItem = null;
13830 this._focusedItem = null; 9881 this._focusedItem = null;
13831 this._focusedIndex = -1; 9882 this._focusedIndex = -1;
13832 }, 9883 },
13833 9884
13834 _createFocusBackfillItem: function() { 9885 _createFocusBackfillItem: function() {
13835 var pidx, fidx = this._focusedIndex; 9886 var pidx, fidx = this._focusedIndex;
13836 if (this._offscreenFocusedItem || fidx < 0) { 9887 if (this._offscreenFocusedItem || fidx < 0) {
13837 return; 9888 return;
13838 } 9889 }
13839 if (!this._focusBackfillItem) { 9890 if (!this._focusBackfillItem) {
13840 // create a physical item, so that it backfills the focused item.
13841 var stampedTemplate = this.stamp(null); 9891 var stampedTemplate = this.stamp(null);
13842 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); 9892 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
13843 Polymer.dom(this).appendChild(stampedTemplate.root); 9893 Polymer.dom(this).appendChild(stampedTemplate.root);
13844 } 9894 }
13845 // get the physical index for the focused index
13846 pidx = this._getPhysicalIndex(fidx); 9895 pidx = this._getPhysicalIndex(fidx);
13847 9896
13848 if (pidx != null) { 9897 if (pidx != null) {
13849 // set the offcreen focused physical item
13850 this._offscreenFocusedItem = this._physicalItems[pidx]; 9898 this._offscreenFocusedItem = this._physicalItems[pidx];
13851 // backfill the focused physical item
13852 this._physicalItems[pidx] = this._focusBackfillItem; 9899 this._physicalItems[pidx] = this._focusBackfillItem;
13853 // hide the focused physical
13854 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); 9900 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
13855 } 9901 }
13856 }, 9902 },
13857 9903
13858 _restoreFocusedItem: function() { 9904 _restoreFocusedItem: function() {
13859 var pidx, fidx = this._focusedIndex; 9905 var pidx, fidx = this._focusedIndex;
13860 9906
13861 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { 9907 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
13862 return; 9908 return;
13863 } 9909 }
13864 // assign models to the focused index
13865 this._assignModels(); 9910 this._assignModels();
13866 // get the new physical index for the focused index
13867 pidx = this._getPhysicalIndex(fidx); 9911 pidx = this._getPhysicalIndex(fidx);
13868 9912
13869 if (pidx != null) { 9913 if (pidx != null) {
13870 // flip the focus backfill
13871 this._focusBackfillItem = this._physicalItems[pidx]; 9914 this._focusBackfillItem = this._physicalItems[pidx];
13872 // restore the focused physical item
13873 this._physicalItems[pidx] = this._offscreenFocusedItem; 9915 this._physicalItems[pidx] = this._offscreenFocusedItem;
13874 // reset the offscreen focused item
13875 this._offscreenFocusedItem = null; 9916 this._offscreenFocusedItem = null;
13876 // hide the physical item that backfills
13877 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); 9917 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
13878 } 9918 }
13879 }, 9919 },
13880 9920
13881 _didFocus: function(e) { 9921 _didFocus: function(e) {
13882 var targetModel = this.modelForElement(e.target); 9922 var targetModel = this.modelForElement(e.target);
13883 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null; 9923 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
13884 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; 9924 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
13885 var fidx = this._focusedIndex; 9925 var fidx = this._focusedIndex;
13886 9926
13887 if (!targetModel || !focusedModel) { 9927 if (!targetModel || !focusedModel) {
13888 return; 9928 return;
13889 } 9929 }
13890 if (focusedModel === targetModel) { 9930 if (focusedModel === targetModel) {
13891 // if the user focused the same item, then bring it into view if it's no t visible
13892 if (!this._isIndexVisible(fidx)) { 9931 if (!this._isIndexVisible(fidx)) {
13893 this.scrollToIndex(fidx); 9932 this.scrollToIndex(fidx);
13894 } 9933 }
13895 } else { 9934 } else {
13896 this._restoreFocusedItem(); 9935 this._restoreFocusedItem();
13897 // restore tabIndex for the currently focused item
13898 focusedModel.tabIndex = -1; 9936 focusedModel.tabIndex = -1;
13899 // set the tabIndex for the next focused item
13900 targetModel.tabIndex = 0; 9937 targetModel.tabIndex = 0;
13901 fidx = targetModel[this.indexAs]; 9938 fidx = targetModel[this.indexAs];
13902 this._focusedIndex = fidx; 9939 this._focusedIndex = fidx;
13903 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; 9940 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
13904 9941
13905 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { 9942 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
13906 this._update(); 9943 this._update();
13907 } 9944 }
13908 } 9945 }
13909 }, 9946 },
13910 9947
13911 _didMoveUp: function() { 9948 _didMoveUp: function() {
13912 this._focusPhysicalItem(this._focusedIndex - 1); 9949 this._focusPhysicalItem(this._focusedIndex - 1);
13913 }, 9950 },
13914 9951
13915 _didMoveDown: function(e) { 9952 _didMoveDown: function(e) {
13916 // disable scroll when pressing the down key
13917 e.detail.keyboardEvent.preventDefault(); 9953 e.detail.keyboardEvent.preventDefault();
13918 this._focusPhysicalItem(this._focusedIndex + 1); 9954 this._focusPhysicalItem(this._focusedIndex + 1);
13919 }, 9955 },
13920 9956
13921 _didEnter: function(e) { 9957 _didEnter: function(e) {
13922 this._focusPhysicalItem(this._focusedIndex); 9958 this._focusPhysicalItem(this._focusedIndex);
13923 this._selectionHandler(e.detail.keyboardEvent); 9959 this._selectionHandler(e.detail.keyboardEvent);
13924 } 9960 }
13925 }); 9961 });
13926 9962
13927 })(); 9963 })();
13928 Polymer({ 9964 Polymer({
13929 9965
13930 is: 'iron-scroll-threshold', 9966 is: 'iron-scroll-threshold',
13931 9967
13932 properties: { 9968 properties: {
13933 9969
13934 /**
13935 * Distance from the top (or left, for horizontal) bound of the scroller
13936 * where the "upper trigger" will fire.
13937 */
13938 upperThreshold: { 9970 upperThreshold: {
13939 type: Number, 9971 type: Number,
13940 value: 100 9972 value: 100
13941 }, 9973 },
13942 9974
13943 /**
13944 * Distance from the bottom (or right, for horizontal) bound of the scroll er
13945 * where the "lower trigger" will fire.
13946 */
13947 lowerThreshold: { 9975 lowerThreshold: {
13948 type: Number, 9976 type: Number,
13949 value: 100 9977 value: 100
13950 }, 9978 },
13951 9979
13952 /**
13953 * Read-only value that tracks the triggered state of the upper threshold.
13954 */
13955 upperTriggered: { 9980 upperTriggered: {
13956 type: Boolean, 9981 type: Boolean,
13957 value: false, 9982 value: false,
13958 notify: true, 9983 notify: true,
13959 readOnly: true 9984 readOnly: true
13960 }, 9985 },
13961 9986
13962 /**
13963 * Read-only value that tracks the triggered state of the lower threshold.
13964 */
13965 lowerTriggered: { 9987 lowerTriggered: {
13966 type: Boolean, 9988 type: Boolean,
13967 value: false, 9989 value: false,
13968 notify: true, 9990 notify: true,
13969 readOnly: true 9991 readOnly: true
13970 }, 9992 },
13971 9993
13972 /**
13973 * True if the orientation of the scroller is horizontal.
13974 */
13975 horizontal: { 9994 horizontal: {
13976 type: Boolean, 9995 type: Boolean,
13977 value: false 9996 value: false
13978 } 9997 }
13979 }, 9998 },
13980 9999
13981 behaviors: [ 10000 behaviors: [
13982 Polymer.IronScrollTargetBehavior 10001 Polymer.IronScrollTargetBehavior
13983 ], 10002 ],
13984 10003
13985 observers: [ 10004 observers: [
13986 '_setOverflow(scrollTarget)', 10005 '_setOverflow(scrollTarget)',
13987 '_initCheck(horizontal, isAttached)' 10006 '_initCheck(horizontal, isAttached)'
13988 ], 10007 ],
13989 10008
13990 get _defaultScrollTarget() { 10009 get _defaultScrollTarget() {
13991 return this; 10010 return this;
13992 }, 10011 },
13993 10012
13994 _setOverflow: function(scrollTarget) { 10013 _setOverflow: function(scrollTarget) {
13995 this.style.overflow = scrollTarget === this ? 'auto' : ''; 10014 this.style.overflow = scrollTarget === this ? 'auto' : '';
13996 }, 10015 },
13997 10016
13998 _scrollHandler: function() { 10017 _scrollHandler: function() {
13999 // throttle the work on the scroll event
14000 var THROTTLE_THRESHOLD = 200; 10018 var THROTTLE_THRESHOLD = 200;
14001 if (!this.isDebouncerActive('_checkTheshold')) { 10019 if (!this.isDebouncerActive('_checkTheshold')) {
14002 this.debounce('_checkTheshold', function() { 10020 this.debounce('_checkTheshold', function() {
14003 this.checkScrollThesholds(); 10021 this.checkScrollThesholds();
14004 }, THROTTLE_THRESHOLD); 10022 }, THROTTLE_THRESHOLD);
14005 } 10023 }
14006 }, 10024 },
14007 10025
14008 _initCheck: function(horizontal, isAttached) { 10026 _initCheck: function(horizontal, isAttached) {
14009 if (isAttached) { 10027 if (isAttached) {
14010 this.debounce('_init', function() { 10028 this.debounce('_init', function() {
14011 this.clearTriggers(); 10029 this.clearTriggers();
14012 this.checkScrollThesholds(); 10030 this.checkScrollThesholds();
14013 }); 10031 });
14014 } 10032 }
14015 }, 10033 },
14016 10034
14017 /**
14018 * Checks the scroll thresholds.
14019 * This method is automatically called by iron-scroll-threshold.
14020 *
14021 * @method checkScrollThesholds
14022 */
14023 checkScrollThesholds: function() { 10035 checkScrollThesholds: function() {
14024 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) { 10036 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
14025 return; 10037 return;
14026 } 10038 }
14027 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo p; 10039 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo p;
14028 var lowerScrollValue = this.horizontal ? 10040 var lowerScrollValue = this.horizontal ?
14029 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll Left : 10041 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll Left :
14030 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._ scrollTop; 10042 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._ scrollTop;
14031 10043
14032 // Detect upper threshold
14033 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { 10044 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
14034 this._setUpperTriggered(true); 10045 this._setUpperTriggered(true);
14035 this.fire('upper-threshold'); 10046 this.fire('upper-threshold');
14036 } 10047 }
14037 // Detect lower threshold
14038 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { 10048 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
14039 this._setLowerTriggered(true); 10049 this._setLowerTriggered(true);
14040 this.fire('lower-threshold'); 10050 this.fire('lower-threshold');
14041 } 10051 }
14042 }, 10052 },
14043 10053
14044 /**
14045 * Clear the upper and lower threshold states.
14046 *
14047 * @method clearTriggers
14048 */
14049 clearTriggers: function() { 10054 clearTriggers: function() {
14050 this._setUpperTriggered(false); 10055 this._setUpperTriggered(false);
14051 this._setLowerTriggered(false); 10056 this._setLowerTriggered(false);
14052 } 10057 }
14053 10058
14054 /**
14055 * Fires when the lower threshold has been reached.
14056 *
14057 * @event lower-threshold
14058 */
14059 10059
14060 /**
14061 * Fires when the upper threshold has been reached.
14062 *
14063 * @event upper-threshold
14064 */
14065 10060
14066 }); 10061 });
14067 // Copyright 2015 The Chromium Authors. All rights reserved. 10062 // Copyright 2015 The Chromium Authors. All rights reserved.
14068 // Use of this source code is governed by a BSD-style license that can be 10063 // Use of this source code is governed by a BSD-style license that can be
14069 // found in the LICENSE file. 10064 // found in the LICENSE file.
14070 10065
14071 Polymer({ 10066 Polymer({
14072 is: 'history-list', 10067 is: 'history-list',
14073 10068
14074 behaviors: [HistoryListBehavior], 10069 behaviors: [HistoryListBehavior],
14075 10070
14076 properties: { 10071 properties: {
14077 // The search term for the current query. Set when the query returns.
14078 searchedTerm: { 10072 searchedTerm: {
14079 type: String, 10073 type: String,
14080 value: '', 10074 value: '',
14081 }, 10075 },
14082 10076
14083 lastSearchedTerm_: String, 10077 lastSearchedTerm_: String,
14084 10078
14085 querying: Boolean, 10079 querying: Boolean,
14086 10080
14087 // An array of history entries in reverse chronological order.
14088 historyData_: Array, 10081 historyData_: Array,
14089 10082
14090 resultLoadingDisabled_: { 10083 resultLoadingDisabled_: {
14091 type: Boolean, 10084 type: Boolean,
14092 value: false, 10085 value: false,
14093 }, 10086 },
14094 }, 10087 },
14095 10088
14096 listeners: { 10089 listeners: {
14097 'scroll': 'notifyListScroll_', 10090 'scroll': 'notifyListScroll_',
14098 'remove-bookmark-stars': 'removeBookmarkStars_', 10091 'remove-bookmark-stars': 'removeBookmarkStars_',
14099 }, 10092 },
14100 10093
14101 /** @override */ 10094 /** @override */
14102 attached: function() { 10095 attached: function() {
14103 // It is possible (eg, when middle clicking the reload button) for all other
14104 // resize events to fire before the list is attached and can be measured.
14105 // Adding another resize here ensures it will get sized correctly.
14106 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize(); 10096 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize();
14107 this.$['infinite-list'].scrollTarget = this; 10097 this.$['infinite-list'].scrollTarget = this;
14108 this.$['scroll-threshold'].scrollTarget = this; 10098 this.$['scroll-threshold'].scrollTarget = this;
14109 }, 10099 },
14110 10100
14111 /**
14112 * Remove bookmark star for history items with matching URLs.
14113 * @param {{detail: !string}} e
14114 * @private
14115 */
14116 removeBookmarkStars_: function(e) { 10101 removeBookmarkStars_: function(e) {
14117 var url = e.detail; 10102 var url = e.detail;
14118 10103
14119 if (this.historyData_ === undefined) 10104 if (this.historyData_ === undefined)
14120 return; 10105 return;
14121 10106
14122 for (var i = 0; i < this.historyData_.length; i++) { 10107 for (var i = 0; i < this.historyData_.length; i++) {
14123 if (this.historyData_[i].url == url) 10108 if (this.historyData_[i].url == url)
14124 this.set('historyData_.' + i + '.starred', false); 10109 this.set('historyData_.' + i + '.starred', false);
14125 } 10110 }
14126 }, 10111 },
14127 10112
14128 /**
14129 * Disables history result loading when there are no more history results.
14130 */
14131 disableResultLoading: function() { 10113 disableResultLoading: function() {
14132 this.resultLoadingDisabled_ = true; 10114 this.resultLoadingDisabled_ = true;
14133 }, 10115 },
14134 10116
14135 /**
14136 * Adds the newly updated history results into historyData_. Adds new fields
14137 * for each result.
14138 * @param {!Array<!HistoryEntry>} historyResults The new history results.
14139 */
14140 addNewResults: function(historyResults) { 10117 addNewResults: function(historyResults) {
14141 var results = historyResults.slice(); 10118 var results = historyResults.slice();
14142 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) 10119 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold'])
14143 .clearTriggers(); 10120 .clearTriggers();
14144 10121
14145 if (this.lastSearchedTerm_ != this.searchedTerm) { 10122 if (this.lastSearchedTerm_ != this.searchedTerm) {
14146 this.resultLoadingDisabled_ = false; 10123 this.resultLoadingDisabled_ = false;
14147 if (this.historyData_) 10124 if (this.historyData_)
14148 this.splice('historyData_', 0, this.historyData_.length); 10125 this.splice('historyData_', 0, this.historyData_.length);
14149 this.fire('unselect-all'); 10126 this.fire('unselect-all');
14150 this.lastSearchedTerm_ = this.searchedTerm; 10127 this.lastSearchedTerm_ = this.searchedTerm;
14151 } 10128 }
14152 10129
14153 if (this.historyData_) { 10130 if (this.historyData_) {
14154 // If we have previously received data, push the new items onto the
14155 // existing array.
14156 results.unshift('historyData_'); 10131 results.unshift('historyData_');
14157 this.push.apply(this, results); 10132 this.push.apply(this, results);
14158 } else { 10133 } else {
14159 // The first time we receive data, use set() to ensure the iron-list is
14160 // initialized correctly.
14161 this.set('historyData_', results); 10134 this.set('historyData_', results);
14162 } 10135 }
14163 }, 10136 },
14164 10137
14165 /**
14166 * Called when the page is scrolled to near the bottom of the list.
14167 * @private
14168 */
14169 loadMoreData_: function() { 10138 loadMoreData_: function() {
14170 if (this.resultLoadingDisabled_ || this.querying) 10139 if (this.resultLoadingDisabled_ || this.querying)
14171 return; 10140 return;
14172 10141
14173 this.fire('load-more-history'); 10142 this.fire('load-more-history');
14174 }, 10143 },
14175 10144
14176 /**
14177 * Check whether the time difference between the given history item and the
14178 * next one is large enough for a spacer to be required.
14179 * @param {HistoryEntry} item
14180 * @param {number} index The index of |item| in |historyData_|.
14181 * @param {number} length The length of |historyData_|.
14182 * @return {boolean} Whether or not time gap separator is required.
14183 * @private
14184 */
14185 needsTimeGap_: function(item, index, length) { 10145 needsTimeGap_: function(item, index, length) {
14186 return md_history.HistoryItem.needsTimeGap( 10146 return md_history.HistoryItem.needsTimeGap(
14187 this.historyData_, index, this.searchedTerm); 10147 this.historyData_, index, this.searchedTerm);
14188 }, 10148 },
14189 10149
14190 /**
14191 * True if the given item is the beginning of a new card.
14192 * @param {HistoryEntry} item
14193 * @param {number} i Index of |item| within |historyData_|.
14194 * @param {number} length
14195 * @return {boolean}
14196 * @private
14197 */
14198 isCardStart_: function(item, i, length) { 10150 isCardStart_: function(item, i, length) {
14199 if (length == 0 || i > length - 1) 10151 if (length == 0 || i > length - 1)
14200 return false; 10152 return false;
14201 return i == 0 || 10153 return i == 0 ||
14202 this.historyData_[i].dateRelativeDay != 10154 this.historyData_[i].dateRelativeDay !=
14203 this.historyData_[i - 1].dateRelativeDay; 10155 this.historyData_[i - 1].dateRelativeDay;
14204 }, 10156 },
14205 10157
14206 /**
14207 * True if the given item is the end of a card.
14208 * @param {HistoryEntry} item
14209 * @param {number} i Index of |item| within |historyData_|.
14210 * @param {number} length
14211 * @return {boolean}
14212 * @private
14213 */
14214 isCardEnd_: function(item, i, length) { 10158 isCardEnd_: function(item, i, length) {
14215 if (length == 0 || i > length - 1) 10159 if (length == 0 || i > length - 1)
14216 return false; 10160 return false;
14217 return i == length - 1 || 10161 return i == length - 1 ||
14218 this.historyData_[i].dateRelativeDay != 10162 this.historyData_[i].dateRelativeDay !=
14219 this.historyData_[i + 1].dateRelativeDay; 10163 this.historyData_[i + 1].dateRelativeDay;
14220 }, 10164 },
14221 10165
14222 /**
14223 * @param {number} index
14224 * @return {boolean}
14225 * @private
14226 */
14227 isFirstItem_: function(index) { 10166 isFirstItem_: function(index) {
14228 return index == 0; 10167 return index == 0;
14229 }, 10168 },
14230 10169
14231 /**
14232 * @private
14233 */
14234 notifyListScroll_: function() { 10170 notifyListScroll_: function() {
14235 this.fire('history-list-scrolled'); 10171 this.fire('history-list-scrolled');
14236 }, 10172 },
14237 10173
14238 /**
14239 * @param {number} index
14240 * @return {string}
14241 * @private
14242 */
14243 pathForItem_: function(index) { 10174 pathForItem_: function(index) {
14244 return 'historyData_.' + index; 10175 return 'historyData_.' + index;
14245 }, 10176 },
14246 }); 10177 });
14247 // Copyright 2016 The Chromium Authors. All rights reserved. 10178 // Copyright 2016 The Chromium Authors. All rights reserved.
14248 // Use of this source code is governed by a BSD-style license that can be 10179 // Use of this source code is governed by a BSD-style license that can be
14249 // found in the LICENSE file. 10180 // found in the LICENSE file.
14250 10181
14251 /**
14252 * @fileoverview
14253 * history-lazy-render is a simple variant of dom-if designed for lazy rendering
14254 * of elements that are accessed imperatively.
14255 * Usage:
14256 * <template is="history-lazy-render" id="menu">
14257 * <heavy-menu></heavy-menu>
14258 * </template>
14259 *
14260 * this.$.menu.get().then(function(menu) {
14261 * menu.show();
14262 * });
14263 */
14264 10182
14265 Polymer({ 10183 Polymer({
14266 is: 'history-lazy-render', 10184 is: 'history-lazy-render',
14267 extends: 'template', 10185 extends: 'template',
14268 10186
14269 behaviors: [ 10187 behaviors: [
14270 Polymer.Templatizer 10188 Polymer.Templatizer
14271 ], 10189 ],
14272 10190
14273 /** @private {Promise<Element>} */ 10191 /** @private {Promise<Element>} */
14274 _renderPromise: null, 10192 _renderPromise: null,
14275 10193
14276 /** @private {TemplateInstance} */ 10194 /** @private {TemplateInstance} */
14277 _instance: null, 10195 _instance: null,
14278 10196
14279 /**
14280 * Stamp the template into the DOM tree asynchronously
14281 * @return {Promise<Element>} Promise which resolves when the template has
14282 * been stamped.
14283 */
14284 get: function() { 10197 get: function() {
14285 if (!this._renderPromise) { 10198 if (!this._renderPromise) {
14286 this._renderPromise = new Promise(function(resolve) { 10199 this._renderPromise = new Promise(function(resolve) {
14287 this._debounceTemplate(function() { 10200 this._debounceTemplate(function() {
14288 this._render(); 10201 this._render();
14289 this._renderPromise = null; 10202 this._renderPromise = null;
14290 resolve(this.getIfExists()); 10203 resolve(this.getIfExists());
14291 }.bind(this)); 10204 }.bind(this));
14292 }.bind(this)); 10205 }.bind(this));
14293 } 10206 }
14294 return this._renderPromise; 10207 return this._renderPromise;
14295 }, 10208 },
14296 10209
14297 /**
14298 * @return {?Element} The element contained in the template, if it has
14299 * already been stamped.
14300 */
14301 getIfExists: function() { 10210 getIfExists: function() {
14302 if (this._instance) { 10211 if (this._instance) {
14303 var children = this._instance._children; 10212 var children = this._instance._children;
14304 10213
14305 for (var i = 0; i < children.length; i++) { 10214 for (var i = 0; i < children.length; i++) {
14306 if (children[i].nodeType == Node.ELEMENT_NODE) 10215 if (children[i].nodeType == Node.ELEMENT_NODE)
14307 return children[i]; 10216 return children[i];
14308 } 10217 }
14309 } 10218 }
14310 return null; 10219 return null;
14311 }, 10220 },
14312 10221
14313 _render: function() { 10222 _render: function() {
14314 if (!this.ctor) 10223 if (!this.ctor)
14315 this.templatize(this); 10224 this.templatize(this);
14316 var parentNode = this.parentNode; 10225 var parentNode = this.parentNode;
14317 if (parentNode && !this._instance) { 10226 if (parentNode && !this._instance) {
14318 this._instance = /** @type {TemplateInstance} */(this.stamp({})); 10227 this._instance = /** @type {TemplateInstance} */(this.stamp({}));
14319 var root = this._instance.root; 10228 var root = this._instance.root;
14320 parentNode.insertBefore(root, this); 10229 parentNode.insertBefore(root, this);
14321 } 10230 }
14322 }, 10231 },
14323 10232
14324 /**
14325 * @param {string} prop
14326 * @param {Object} value
14327 */
14328 _forwardParentProp: function(prop, value) { 10233 _forwardParentProp: function(prop, value) {
14329 if (this._instance) 10234 if (this._instance)
14330 this._instance.__setProperty(prop, value, true); 10235 this._instance.__setProperty(prop, value, true);
14331 }, 10236 },
14332 10237
14333 /**
14334 * @param {string} path
14335 * @param {Object} value
14336 */
14337 _forwardParentPath: function(path, value) { 10238 _forwardParentPath: function(path, value) {
14338 if (this._instance) 10239 if (this._instance)
14339 this._instance._notifyPath(path, value, true); 10240 this._instance._notifyPath(path, value, true);
14340 } 10241 }
14341 }); 10242 });
14342 // Copyright 2016 The Chromium Authors. All rights reserved. 10243 // Copyright 2016 The Chromium Authors. All rights reserved.
14343 // Use of this source code is governed by a BSD-style license that can be 10244 // Use of this source code is governed by a BSD-style license that can be
14344 // found in the LICENSE file. 10245 // found in the LICENSE file.
14345 10246
14346 Polymer({ 10247 Polymer({
14347 is: 'history-list-container', 10248 is: 'history-list-container',
14348 10249
14349 properties: { 10250 properties: {
14350 // The path of the currently selected page.
14351 selectedPage_: String, 10251 selectedPage_: String,
14352 10252
14353 // Whether domain-grouped history is enabled.
14354 grouped: Boolean, 10253 grouped: Boolean,
14355 10254
14356 /** @type {!QueryState} */ 10255 /** @type {!QueryState} */
14357 queryState: Object, 10256 queryState: Object,
14358 10257
14359 /** @type {!QueryResult} */ 10258 /** @type {!QueryResult} */
14360 queryResult: Object, 10259 queryResult: Object,
14361 }, 10260 },
14362 10261
14363 observers: [ 10262 observers: [
14364 'groupedRangeChanged_(queryState.range)', 10263 'groupedRangeChanged_(queryState.range)',
14365 ], 10264 ],
14366 10265
14367 listeners: { 10266 listeners: {
14368 'history-list-scrolled': 'closeMenu_', 10267 'history-list-scrolled': 'closeMenu_',
14369 'load-more-history': 'loadMoreHistory_', 10268 'load-more-history': 'loadMoreHistory_',
14370 'toggle-menu': 'toggleMenu_', 10269 'toggle-menu': 'toggleMenu_',
14371 }, 10270 },
14372 10271
14373 /**
14374 * @param {HistoryQuery} info An object containing information about the
14375 * query.
14376 * @param {!Array<HistoryEntry>} results A list of results.
14377 */
14378 historyResult: function(info, results) { 10272 historyResult: function(info, results) {
14379 this.initializeResults_(info, results); 10273 this.initializeResults_(info, results);
14380 this.closeMenu_(); 10274 this.closeMenu_();
14381 10275
14382 if (this.selectedPage_ == 'grouped-list') { 10276 if (this.selectedPage_ == 'grouped-list') {
14383 this.$$('#grouped-list').historyData = results; 10277 this.$$('#grouped-list').historyData = results;
14384 return; 10278 return;
14385 } 10279 }
14386 10280
14387 var list = /** @type {HistoryListElement} */(this.$['infinite-list']); 10281 var list = /** @type {HistoryListElement} */(this.$['infinite-list']);
14388 list.addNewResults(results); 10282 list.addNewResults(results);
14389 if (info.finished) 10283 if (info.finished)
14390 list.disableResultLoading(); 10284 list.disableResultLoading();
14391 }, 10285 },
14392 10286
14393 /**
14394 * Queries the history backend for results based on queryState.
14395 * @param {boolean} incremental Whether the new query should continue where
14396 * the previous query stopped.
14397 */
14398 queryHistory: function(incremental) { 10287 queryHistory: function(incremental) {
14399 var queryState = this.queryState; 10288 var queryState = this.queryState;
14400 // Disable querying until the first set of results have been returned. If
14401 // there is a search, query immediately to support search query params from
14402 // the URL.
14403 var noResults = !this.queryResult || this.queryResult.results == null; 10289 var noResults = !this.queryResult || this.queryResult.results == null;
14404 if (queryState.queryingDisabled || 10290 if (queryState.queryingDisabled ||
14405 (!this.queryState.searchTerm && noResults)) { 10291 (!this.queryState.searchTerm && noResults)) {
14406 return; 10292 return;
14407 } 10293 }
14408 10294
14409 // Close any open dialog if a new query is initiated.
14410 var dialog = this.$.dialog.getIfExists(); 10295 var dialog = this.$.dialog.getIfExists();
14411 if (!incremental && dialog && dialog.open) 10296 if (!incremental && dialog && dialog.open)
14412 dialog.close(); 10297 dialog.close();
14413 10298
14414 this.set('queryState.querying', true); 10299 this.set('queryState.querying', true);
14415 this.set('queryState.incremental', incremental); 10300 this.set('queryState.incremental', incremental);
14416 10301
14417 var lastVisitTime = 0; 10302 var lastVisitTime = 0;
14418 if (incremental) { 10303 if (incremental) {
14419 var lastVisit = this.queryResult.results.slice(-1)[0]; 10304 var lastVisit = this.queryResult.results.slice(-1)[0];
(...skipping 10 matching lines...) Expand all
14430 10315
14431 /** @return {number} */ 10316 /** @return {number} */
14432 getSelectedItemCount: function() { 10317 getSelectedItemCount: function() {
14433 return this.getSelectedList_().selectedPaths.size; 10318 return this.getSelectedList_().selectedPaths.size;
14434 }, 10319 },
14435 10320
14436 unselectAllItems: function(count) { 10321 unselectAllItems: function(count) {
14437 this.getSelectedList_().unselectAllItems(count); 10322 this.getSelectedList_().unselectAllItems(count);
14438 }, 10323 },
14439 10324
14440 /**
14441 * Delete all the currently selected history items. Will prompt the user with
14442 * a dialog to confirm that the deletion should be performed.
14443 */
14444 deleteSelectedWithPrompt: function() { 10325 deleteSelectedWithPrompt: function() {
14445 if (!loadTimeData.getBoolean('allowDeletingHistory')) 10326 if (!loadTimeData.getBoolean('allowDeletingHistory'))
14446 return; 10327 return;
14447 this.$.dialog.get().then(function(dialog) { 10328 this.$.dialog.get().then(function(dialog) {
14448 dialog.showModal(); 10329 dialog.showModal();
14449 }); 10330 });
14450 }, 10331 },
14451 10332
14452 /**
14453 * @param {HistoryRange} range
14454 * @private
14455 */
14456 groupedRangeChanged_: function(range) { 10333 groupedRangeChanged_: function(range) {
14457 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? 10334 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ?
14458 'infinite-list' : 'grouped-list'; 10335 'infinite-list' : 'grouped-list';
14459 10336
14460 this.queryHistory(false); 10337 this.queryHistory(false);
14461 }, 10338 },
14462 10339
14463 /** @private */ 10340 /** @private */
14464 loadMoreHistory_: function() { this.queryHistory(true); }, 10341 loadMoreHistory_: function() { this.queryHistory(true); },
14465 10342
14466 /**
14467 * @param {HistoryQuery} info
14468 * @param {!Array<HistoryEntry>} results
14469 * @private
14470 */
14471 initializeResults_: function(info, results) { 10343 initializeResults_: function(info, results) {
14472 if (results.length == 0) 10344 if (results.length == 0)
14473 return; 10345 return;
14474 10346
14475 var currentDate = results[0].dateRelativeDay; 10347 var currentDate = results[0].dateRelativeDay;
14476 10348
14477 for (var i = 0; i < results.length; i++) { 10349 for (var i = 0; i < results.length; i++) {
14478 // Sets the default values for these fields to prevent undefined types.
14479 results[i].selected = false; 10350 results[i].selected = false;
14480 results[i].readableTimestamp = 10351 results[i].readableTimestamp =
14481 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; 10352 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
14482 10353
14483 if (results[i].dateRelativeDay != currentDate) { 10354 if (results[i].dateRelativeDay != currentDate) {
14484 currentDate = results[i].dateRelativeDay; 10355 currentDate = results[i].dateRelativeDay;
14485 } 10356 }
14486 } 10357 }
14487 }, 10358 },
14488 10359
14489 /** @private */ 10360 /** @private */
14490 onDialogConfirmTap_: function() { 10361 onDialogConfirmTap_: function() {
14491 this.getSelectedList_().deleteSelected(); 10362 this.getSelectedList_().deleteSelected();
14492 var dialog = assert(this.$.dialog.getIfExists()); 10363 var dialog = assert(this.$.dialog.getIfExists());
14493 dialog.close(); 10364 dialog.close();
14494 }, 10365 },
14495 10366
14496 /** @private */ 10367 /** @private */
14497 onDialogCancelTap_: function() { 10368 onDialogCancelTap_: function() {
14498 var dialog = assert(this.$.dialog.getIfExists()); 10369 var dialog = assert(this.$.dialog.getIfExists());
14499 dialog.close(); 10370 dialog.close();
14500 }, 10371 },
14501 10372
14502 /**
14503 * Closes the overflow menu.
14504 * @private
14505 */
14506 closeMenu_: function() { 10373 closeMenu_: function() {
14507 var menu = this.$.sharedMenu.getIfExists(); 10374 var menu = this.$.sharedMenu.getIfExists();
14508 if (menu) 10375 if (menu)
14509 menu.closeMenu(); 10376 menu.closeMenu();
14510 }, 10377 },
14511 10378
14512 /**
14513 * Opens the overflow menu unless the menu is already open and the same button
14514 * is pressed.
14515 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e
14516 * @return {Promise<Element>}
14517 * @private
14518 */
14519 toggleMenu_: function(e) { 10379 toggleMenu_: function(e) {
14520 var target = e.detail.target; 10380 var target = e.detail.target;
14521 return this.$.sharedMenu.get().then(function(menu) { 10381 return this.$.sharedMenu.get().then(function(menu) {
14522 /** @type {CrSharedMenuElement} */(menu).toggleMenu( 10382 /** @type {CrSharedMenuElement} */(menu).toggleMenu(
14523 target, e.detail); 10383 target, e.detail);
14524 }); 10384 });
14525 }, 10385 },
14526 10386
14527 /** @private */ 10387 /** @private */
14528 onMoreFromSiteTap_: function() { 10388 onMoreFromSiteTap_: function() {
14529 var menu = assert(this.$.sharedMenu.getIfExists()); 10389 var menu = assert(this.$.sharedMenu.getIfExists());
14530 this.fire('search-domain', {domain: menu.itemData.item.domain}); 10390 this.fire('search-domain', {domain: menu.itemData.item.domain});
14531 menu.closeMenu(); 10391 menu.closeMenu();
14532 }, 10392 },
14533 10393
14534 /** @private */ 10394 /** @private */
14535 onRemoveFromHistoryTap_: function() { 10395 onRemoveFromHistoryTap_: function() {
14536 var menu = assert(this.$.sharedMenu.getIfExists()); 10396 var menu = assert(this.$.sharedMenu.getIfExists());
14537 var itemData = menu.itemData; 10397 var itemData = menu.itemData;
14538 md_history.BrowserService.getInstance() 10398 md_history.BrowserService.getInstance()
14539 .deleteItems([itemData.item]) 10399 .deleteItems([itemData.item])
14540 .then(function(items) { 10400 .then(function(items) {
14541 this.getSelectedList_().removeItemsByPath([itemData.path]); 10401 this.getSelectedList_().removeItemsByPath([itemData.path]);
14542 // This unselect-all is to reset the toolbar when deleting a selected
14543 // item. TODO(tsergeant): Make this automatic based on observing list
14544 // modifications.
14545 this.fire('unselect-all'); 10402 this.fire('unselect-all');
14546 }.bind(this)); 10403 }.bind(this));
14547 menu.closeMenu(); 10404 menu.closeMenu();
14548 }, 10405 },
14549 10406
14550 /**
14551 * @return {HTMLElement}
14552 * @private
14553 */
14554 getSelectedList_: function() { 10407 getSelectedList_: function() {
14555 return this.$.content.selectedItem; 10408 return this.$.content.selectedItem;
14556 }, 10409 },
14557 }); 10410 });
14558 // Copyright 2016 The Chromium Authors. All rights reserved. 10411 // Copyright 2016 The Chromium Authors. All rights reserved.
14559 // Use of this source code is governed by a BSD-style license that can be 10412 // Use of this source code is governed by a BSD-style license that can be
14560 // found in the LICENSE file. 10413 // found in the LICENSE file.
14561 10414
14562 Polymer({ 10415 Polymer({
14563 is: 'history-synced-device-card', 10416 is: 'history-synced-device-card',
14564 10417
14565 properties: { 10418 properties: {
14566 // Name of the synced device.
14567 device: String, 10419 device: String,
14568 10420
14569 // When the device information was last updated.
14570 lastUpdateTime: String, 10421 lastUpdateTime: String,
14571 10422
14572 /**
14573 * The list of tabs open for this device.
14574 * @type {!Array<!ForeignSessionTab>}
14575 */
14576 tabs: { 10423 tabs: {
14577 type: Array, 10424 type: Array,
14578 value: function() { return []; }, 10425 value: function() { return []; },
14579 observer: 'updateIcons_' 10426 observer: 'updateIcons_'
14580 }, 10427 },
14581 10428
14582 /**
14583 * The indexes where a window separator should be shown. The use of a
14584 * separate array here is necessary for window separators to appear
14585 * correctly in search. See http://crrev.com/2022003002 for more details.
14586 * @type {!Array<number>}
14587 */
14588 separatorIndexes: Array, 10429 separatorIndexes: Array,
14589 10430
14590 // Whether the card is open.
14591 cardOpen_: {type: Boolean, value: true}, 10431 cardOpen_: {type: Boolean, value: true},
14592 10432
14593 searchTerm: String, 10433 searchTerm: String,
14594 10434
14595 // Internal identifier for the device.
14596 sessionTag: String, 10435 sessionTag: String,
14597 }, 10436 },
14598 10437
14599 /**
14600 * Open a single synced tab. Listens to 'click' rather than 'tap'
14601 * to determine what modifier keys were pressed.
14602 * @param {DomRepeatClickEvent} e
14603 * @private
14604 */
14605 openTab_: function(e) { 10438 openTab_: function(e) {
14606 var tab = /** @type {ForeignSessionTab} */(e.model.tab); 10439 var tab = /** @type {ForeignSessionTab} */(e.model.tab);
14607 md_history.BrowserService.getInstance().openForeignSessionTab( 10440 md_history.BrowserService.getInstance().openForeignSessionTab(
14608 this.sessionTag, tab.windowId, tab.sessionId, e); 10441 this.sessionTag, tab.windowId, tab.sessionId, e);
14609 e.preventDefault(); 10442 e.preventDefault();
14610 }, 10443 },
14611 10444
14612 /**
14613 * Toggles the dropdown display of synced tabs for each device card.
14614 */
14615 toggleTabCard: function() { 10445 toggleTabCard: function() {
14616 this.$.collapse.toggle(); 10446 this.$.collapse.toggle();
14617 this.$['dropdown-indicator'].icon = 10447 this.$['dropdown-indicator'].icon =
14618 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; 10448 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more';
14619 }, 10449 },
14620 10450
14621 /**
14622 * When the synced tab information is set, the icon associated with the tab
14623 * website is also set.
14624 * @private
14625 */
14626 updateIcons_: function() { 10451 updateIcons_: function() {
14627 this.async(function() { 10452 this.async(function() {
14628 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); 10453 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon');
14629 10454
14630 for (var i = 0; i < this.tabs.length; i++) { 10455 for (var i = 0; i < this.tabs.length; i++) {
14631 icons[i].style.backgroundImage = 10456 icons[i].style.backgroundImage =
14632 cr.icon.getFaviconImageSet(this.tabs[i].url); 10457 cr.icon.getFaviconImageSet(this.tabs[i].url);
14633 } 10458 }
14634 }); 10459 });
14635 }, 10460 },
14636 10461
14637 /** @private */ 10462 /** @private */
14638 isWindowSeparatorIndex_: function(index, separatorIndexes) { 10463 isWindowSeparatorIndex_: function(index, separatorIndexes) {
14639 return this.separatorIndexes.indexOf(index) != -1; 10464 return this.separatorIndexes.indexOf(index) != -1;
14640 }, 10465 },
14641 10466
14642 /**
14643 * @param {boolean} cardOpen
14644 * @return {string}
14645 */
14646 getCollapseTitle_: function(cardOpen) { 10467 getCollapseTitle_: function(cardOpen) {
14647 return cardOpen ? loadTimeData.getString('collapseSessionButton') : 10468 return cardOpen ? loadTimeData.getString('collapseSessionButton') :
14648 loadTimeData.getString('expandSessionButton'); 10469 loadTimeData.getString('expandSessionButton');
14649 }, 10470 },
14650 10471
14651 /**
14652 * @param {CustomEvent} e
14653 * @private
14654 */
14655 onMenuButtonTap_: function(e) { 10472 onMenuButtonTap_: function(e) {
14656 this.fire('toggle-menu', { 10473 this.fire('toggle-menu', {
14657 target: Polymer.dom(e).localTarget, 10474 target: Polymer.dom(e).localTarget,
14658 tag: this.sessionTag 10475 tag: this.sessionTag
14659 }); 10476 });
14660 e.stopPropagation(); // Prevent iron-collapse. 10477 e.stopPropagation(); // Prevent iron-collapse.
14661 }, 10478 },
14662 }); 10479 });
14663 // Copyright 2016 The Chromium Authors. All rights reserved. 10480 // Copyright 2016 The Chromium Authors. All rights reserved.
14664 // Use of this source code is governed by a BSD-style license that can be 10481 // Use of this source code is governed by a BSD-style license that can be
14665 // found in the LICENSE file. 10482 // found in the LICENSE file.
14666 10483
14667 /**
14668 * @typedef {{device: string,
14669 * lastUpdateTime: string,
14670 * separatorIndexes: !Array<number>,
14671 * timestamp: number,
14672 * tabs: !Array<!ForeignSessionTab>,
14673 * tag: string}}
14674 */
14675 var ForeignDeviceInternal; 10484 var ForeignDeviceInternal;
14676 10485
14677 Polymer({ 10486 Polymer({
14678 is: 'history-synced-device-manager', 10487 is: 'history-synced-device-manager',
14679 10488
14680 properties: { 10489 properties: {
14681 /**
14682 * @type {?Array<!ForeignSession>}
14683 */
14684 sessionList: { 10490 sessionList: {
14685 type: Array, 10491 type: Array,
14686 observer: 'updateSyncedDevices' 10492 observer: 'updateSyncedDevices'
14687 }, 10493 },
14688 10494
14689 searchTerm: { 10495 searchTerm: {
14690 type: String, 10496 type: String,
14691 observer: 'searchTermChanged' 10497 observer: 'searchTermChanged'
14692 }, 10498 },
14693 10499
14694 /**
14695 * An array of synced devices with synced tab data.
14696 * @type {!Array<!ForeignDeviceInternal>}
14697 */
14698 syncedDevices_: { 10500 syncedDevices_: {
14699 type: Array, 10501 type: Array,
14700 value: function() { return []; } 10502 value: function() { return []; }
14701 }, 10503 },
14702 10504
14703 /** @private */ 10505 /** @private */
14704 signInState_: { 10506 signInState_: {
14705 type: Boolean, 10507 type: Boolean,
14706 value: loadTimeData.getBoolean('isUserSignedIn'), 10508 value: loadTimeData.getBoolean('isUserSignedIn'),
14707 }, 10509 },
(...skipping 10 matching lines...) Expand all
14718 value: false, 10520 value: false,
14719 } 10521 }
14720 }, 10522 },
14721 10523
14722 listeners: { 10524 listeners: {
14723 'toggle-menu': 'onToggleMenu_', 10525 'toggle-menu': 'onToggleMenu_',
14724 }, 10526 },
14725 10527
14726 /** @override */ 10528 /** @override */
14727 attached: function() { 10529 attached: function() {
14728 // Update the sign in state.
14729 chrome.send('otherDevicesInitialized'); 10530 chrome.send('otherDevicesInitialized');
14730 }, 10531 },
14731 10532
14732 /**
14733 * @param {!ForeignSession} session
14734 * @return {!ForeignDeviceInternal}
14735 */
14736 createInternalDevice_: function(session) { 10533 createInternalDevice_: function(session) {
14737 var tabs = []; 10534 var tabs = [];
14738 var separatorIndexes = []; 10535 var separatorIndexes = [];
14739 for (var i = 0; i < session.windows.length; i++) { 10536 for (var i = 0; i < session.windows.length; i++) {
14740 var windowId = session.windows[i].sessionId; 10537 var windowId = session.windows[i].sessionId;
14741 var newTabs = session.windows[i].tabs; 10538 var newTabs = session.windows[i].tabs;
14742 if (newTabs.length == 0) 10539 if (newTabs.length == 0)
14743 continue; 10540 continue;
14744 10541
14745 newTabs.forEach(function(tab) { 10542 newTabs.forEach(function(tab) {
14746 tab.windowId = windowId; 10543 tab.windowId = windowId;
14747 }); 10544 });
14748 10545
14749 var windowAdded = false; 10546 var windowAdded = false;
14750 if (!this.searchTerm) { 10547 if (!this.searchTerm) {
14751 // Add all the tabs if there is no search term.
14752 tabs = tabs.concat(newTabs); 10548 tabs = tabs.concat(newTabs);
14753 windowAdded = true; 10549 windowAdded = true;
14754 } else { 10550 } else {
14755 var searchText = this.searchTerm.toLowerCase(); 10551 var searchText = this.searchTerm.toLowerCase();
14756 for (var j = 0; j < newTabs.length; j++) { 10552 for (var j = 0; j < newTabs.length; j++) {
14757 var tab = newTabs[j]; 10553 var tab = newTabs[j];
14758 if (tab.title.toLowerCase().indexOf(searchText) != -1) { 10554 if (tab.title.toLowerCase().indexOf(searchText) != -1) {
14759 tabs.push(tab); 10555 tabs.push(tab);
14760 windowAdded = true; 10556 windowAdded = true;
14761 } 10557 }
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
14796 md_history.BrowserService.getInstance().deleteForeignSession( 10592 md_history.BrowserService.getInstance().deleteForeignSession(
14797 menu.itemData); 10593 menu.itemData);
14798 menu.closeMenu(); 10594 menu.closeMenu();
14799 }, 10595 },
14800 10596
14801 /** @private */ 10597 /** @private */
14802 clearDisplayedSyncedDevices_: function() { 10598 clearDisplayedSyncedDevices_: function() {
14803 this.syncedDevices_ = []; 10599 this.syncedDevices_ = [];
14804 }, 10600 },
14805 10601
14806 /**
14807 * Decide whether or not should display no synced tabs message.
14808 * @param {boolean} signInState
14809 * @param {number} syncedDevicesLength
14810 * @param {boolean} guestSession
14811 * @return {boolean}
14812 */
14813 showNoSyncedMessage: function( 10602 showNoSyncedMessage: function(
14814 signInState, syncedDevicesLength, guestSession) { 10603 signInState, syncedDevicesLength, guestSession) {
14815 if (guestSession) 10604 if (guestSession)
14816 return true; 10605 return true;
14817 10606
14818 return signInState && syncedDevicesLength == 0; 10607 return signInState && syncedDevicesLength == 0;
14819 }, 10608 },
14820 10609
14821 /**
14822 * Shows the signin guide when the user is not signed in and not in a guest
14823 * session.
14824 * @param {boolean} signInState
14825 * @param {boolean} guestSession
14826 * @return {boolean}
14827 */
14828 showSignInGuide: function(signInState, guestSession) { 10610 showSignInGuide: function(signInState, guestSession) {
14829 var show = !signInState && !guestSession; 10611 var show = !signInState && !guestSession;
14830 if (show) { 10612 if (show) {
14831 md_history.BrowserService.getInstance().recordAction( 10613 md_history.BrowserService.getInstance().recordAction(
14832 'Signin_Impression_FromRecentTabs'); 10614 'Signin_Impression_FromRecentTabs');
14833 } 10615 }
14834 10616
14835 return show; 10617 return show;
14836 }, 10618 },
14837 10619
14838 /**
14839 * Decide what message should be displayed when user is logged in and there
14840 * are no synced tabs.
14841 * @param {boolean} fetchingSyncedTabs
14842 * @return {string}
14843 */
14844 noSyncedTabsMessage: function(fetchingSyncedTabs) { 10620 noSyncedTabsMessage: function(fetchingSyncedTabs) {
14845 return loadTimeData.getString( 10621 return loadTimeData.getString(
14846 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); 10622 fetchingSyncedTabs ? 'loading' : 'noSyncedResults');
14847 }, 10623 },
14848 10624
14849 /**
14850 * Replaces the currently displayed synced tabs with |sessionList|. It is
14851 * common for only a single session within the list to have changed, We try to
14852 * avoid doing extra work in this case. The logic could be more intelligent
14853 * about updating individual tabs rather than replacing whole sessions, but
14854 * this approach seems to have acceptable performance.
14855 * @param {?Array<!ForeignSession>} sessionList
14856 */
14857 updateSyncedDevices: function(sessionList) { 10625 updateSyncedDevices: function(sessionList) {
14858 this.fetchingSyncedTabs_ = false; 10626 this.fetchingSyncedTabs_ = false;
14859 10627
14860 if (!sessionList) 10628 if (!sessionList)
14861 return; 10629 return;
14862 10630
14863 // First, update any existing devices that have changed.
14864 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); 10631 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length);
14865 for (var i = 0; i < updateCount; i++) { 10632 for (var i = 0; i < updateCount; i++) {
14866 var oldDevice = this.syncedDevices_[i]; 10633 var oldDevice = this.syncedDevices_[i];
14867 if (oldDevice.tag != sessionList[i].tag || 10634 if (oldDevice.tag != sessionList[i].tag ||
14868 oldDevice.timestamp != sessionList[i].timestamp) { 10635 oldDevice.timestamp != sessionList[i].timestamp) {
14869 this.splice( 10636 this.splice(
14870 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); 10637 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i]));
14871 } 10638 }
14872 } 10639 }
14873 10640
14874 // Then, append any new devices.
14875 for (var i = updateCount; i < sessionList.length; i++) { 10641 for (var i = updateCount; i < sessionList.length; i++) {
14876 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); 10642 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i]));
14877 } 10643 }
14878 }, 10644 },
14879 10645
14880 /**
14881 * End fetching synced tabs when sync is disabled.
14882 */
14883 tabSyncDisabled: function() { 10646 tabSyncDisabled: function() {
14884 this.fetchingSyncedTabs_ = false; 10647 this.fetchingSyncedTabs_ = false;
14885 this.clearDisplayedSyncedDevices_(); 10648 this.clearDisplayedSyncedDevices_();
14886 }, 10649 },
14887 10650
14888 /**
14889 * Get called when user's sign in state changes, this will affect UI of synced
14890 * tabs page. Sign in promo gets displayed when user is signed out, and
14891 * different messages are shown when there are no synced tabs.
14892 * @param {boolean} isUserSignedIn
14893 */
14894 updateSignInState: function(isUserSignedIn) { 10651 updateSignInState: function(isUserSignedIn) {
14895 // If user's sign in state didn't change, then don't change message or
14896 // update UI.
14897 if (this.signInState_ == isUserSignedIn) 10652 if (this.signInState_ == isUserSignedIn)
14898 return; 10653 return;
14899 10654
14900 this.signInState_ = isUserSignedIn; 10655 this.signInState_ = isUserSignedIn;
14901 10656
14902 // User signed out, clear synced device list and show the sign in promo.
14903 if (!isUserSignedIn) { 10657 if (!isUserSignedIn) {
14904 this.clearDisplayedSyncedDevices_(); 10658 this.clearDisplayedSyncedDevices_();
14905 return; 10659 return;
14906 } 10660 }
14907 // User signed in, show the loading message when querying for synced
14908 // devices.
14909 this.fetchingSyncedTabs_ = true; 10661 this.fetchingSyncedTabs_ = true;
14910 }, 10662 },
14911 10663
14912 searchTermChanged: function(searchTerm) { 10664 searchTermChanged: function(searchTerm) {
14913 this.clearDisplayedSyncedDevices_(); 10665 this.clearDisplayedSyncedDevices_();
14914 this.updateSyncedDevices(this.sessionList); 10666 this.updateSyncedDevices(this.sessionList);
14915 } 10667 }
14916 }); 10668 });
14917 /**
14918 `iron-selector` is an element which can be used to manage a list of elements
14919 that can be selected. Tapping on the item will make the item selected. The ` selected` indicates
14920 which item is being selected. The default is to use the index of the item.
14921
14922 Example:
14923
14924 <iron-selector selected="0">
14925 <div>Item 1</div>
14926 <div>Item 2</div>
14927 <div>Item 3</div>
14928 </iron-selector>
14929
14930 If you want to use the attribute value of an element for `selected` instead of the index,
14931 set `attrForSelected` to the name of the attribute. For example, if you want to select item by
14932 `name`, set `attrForSelected` to `name`.
14933
14934 Example:
14935
14936 <iron-selector attr-for-selected="name" selected="foo">
14937 <div name="foo">Foo</div>
14938 <div name="bar">Bar</div>
14939 <div name="zot">Zot</div>
14940 </iron-selector>
14941
14942 You can specify a default fallback with `fallbackSelection` in case the `selec ted` attribute does
14943 not match the `attrForSelected` attribute of any elements.
14944
14945 Example:
14946
14947 <iron-selector attr-for-selected="name" selected="non-existing"
14948 fallback-selection="default">
14949 <div name="foo">Foo</div>
14950 <div name="bar">Bar</div>
14951 <div name="default">Default</div>
14952 </iron-selector>
14953
14954 Note: When the selector is multi, the selection will set to `fallbackSelection ` iff
14955 the number of matching elements is zero.
14956
14957 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the selected element.
14958
14959 Example:
14960
14961 <style>
14962 .iron-selected {
14963 background: #eee;
14964 }
14965 </style>
14966
14967 ...
14968
14969 <iron-selector selected="0">
14970 <div>Item 1</div>
14971 <div>Item 2</div>
14972 <div>Item 3</div>
14973 </iron-selector>
14974
14975 @demo demo/index.html
14976 */
14977 10669
14978 Polymer({ 10670 Polymer({
14979 10671
14980 is: 'iron-selector', 10672 is: 'iron-selector',
14981 10673
14982 behaviors: [ 10674 behaviors: [
14983 Polymer.IronMultiSelectableBehavior 10675 Polymer.IronMultiSelectableBehavior
14984 ] 10676 ]
14985 10677
14986 }); 10678 });
14987 // Copyright 2016 The Chromium Authors. All rights reserved. 10679 // Copyright 2016 The Chromium Authors. All rights reserved.
14988 // Use of this source code is governed by a BSD-style license that can be 10680 // Use of this source code is governed by a BSD-style license that can be
14989 // found in the LICENSE file. 10681 // found in the LICENSE file.
14990 10682
14991 Polymer({ 10683 Polymer({
14992 is: 'history-side-bar', 10684 is: 'history-side-bar',
14993 10685
14994 properties: { 10686 properties: {
14995 selectedPage: { 10687 selectedPage: {
14996 type: String, 10688 type: String,
14997 notify: true 10689 notify: true
14998 }, 10690 },
14999 10691
15000 route: Object, 10692 route: Object,
15001 10693
15002 showFooter: Boolean, 10694 showFooter: Boolean,
15003 10695
15004 // If true, the sidebar is contained within an app-drawer.
15005 drawer: { 10696 drawer: {
15006 type: Boolean, 10697 type: Boolean,
15007 reflectToAttribute: true 10698 reflectToAttribute: true
15008 }, 10699 },
15009 }, 10700 },
15010 10701
15011 /** @private */ 10702 /** @private */
15012 onSelectorActivate_: function() { 10703 onSelectorActivate_: function() {
15013 this.fire('history-close-drawer'); 10704 this.fire('history-close-drawer');
15014 }, 10705 },
15015 10706
15016 /**
15017 * Relocates the user to the clear browsing data section of the settings page.
15018 * @param {Event} e
15019 * @private
15020 */
15021 onClearBrowsingDataTap_: function(e) { 10707 onClearBrowsingDataTap_: function(e) {
15022 md_history.BrowserService.getInstance().openClearBrowsingData(); 10708 md_history.BrowserService.getInstance().openClearBrowsingData();
15023 e.preventDefault(); 10709 e.preventDefault();
15024 }, 10710 },
15025 10711
15026 /**
15027 * @param {Object} route
15028 * @private
15029 */
15030 getQueryString_: function(route) { 10712 getQueryString_: function(route) {
15031 return window.location.search; 10713 return window.location.search;
15032 } 10714 }
15033 }); 10715 });
15034 // Copyright 2016 The Chromium Authors. All rights reserved. 10716 // Copyright 2016 The Chromium Authors. All rights reserved.
15035 // Use of this source code is governed by a BSD-style license that can be 10717 // Use of this source code is governed by a BSD-style license that can be
15036 // found in the LICENSE file. 10718 // found in the LICENSE file.
15037 10719
15038 Polymer({ 10720 Polymer({
15039 is: 'history-app', 10721 is: 'history-app',
15040 10722
15041 properties: { 10723 properties: {
15042 showSidebarFooter: Boolean, 10724 showSidebarFooter: Boolean,
15043 10725
15044 // The id of the currently selected page.
15045 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, 10726 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'},
15046 10727
15047 // Whether domain-grouped history is enabled.
15048 grouped_: {type: Boolean, reflectToAttribute: true}, 10728 grouped_: {type: Boolean, reflectToAttribute: true},
15049 10729
15050 /** @type {!QueryState} */ 10730 /** @type {!QueryState} */
15051 queryState_: { 10731 queryState_: {
15052 type: Object, 10732 type: Object,
15053 value: function() { 10733 value: function() {
15054 return { 10734 return {
15055 // Whether the most recent query was incremental.
15056 incremental: false, 10735 incremental: false,
15057 // A query is initiated by page load.
15058 querying: true, 10736 querying: true,
15059 queryingDisabled: false, 10737 queryingDisabled: false,
15060 _range: HistoryRange.ALL_TIME, 10738 _range: HistoryRange.ALL_TIME,
15061 searchTerm: '', 10739 searchTerm: '',
15062 // TODO(calamity): Make history toolbar buttons change the offset
15063 groupedOffset: 0, 10740 groupedOffset: 0,
15064 10741
15065 set range(val) { this._range = Number(val); }, 10742 set range(val) { this._range = Number(val); },
15066 get range() { return this._range; }, 10743 get range() { return this._range; },
15067 }; 10744 };
15068 } 10745 }
15069 }, 10746 },
15070 10747
15071 /** @type {!QueryResult} */ 10748 /** @type {!QueryResult} */
15072 queryResult_: { 10749 queryResult_: {
15073 type: Object, 10750 type: Object,
15074 value: function() { 10751 value: function() {
15075 return { 10752 return {
15076 info: null, 10753 info: null,
15077 results: null, 10754 results: null,
15078 sessionList: null, 10755 sessionList: null,
15079 }; 10756 };
15080 } 10757 }
15081 }, 10758 },
15082 10759
15083 // Route data for the current page.
15084 routeData_: Object, 10760 routeData_: Object,
15085 10761
15086 // The query params for the page.
15087 queryParams_: Object, 10762 queryParams_: Object,
15088 10763
15089 // True if the window is narrow enough for the page to have a drawer.
15090 hasDrawer_: Boolean, 10764 hasDrawer_: Boolean,
15091 }, 10765 },
15092 10766
15093 observers: [ 10767 observers: [
15094 // routeData_.page <=> selectedPage
15095 'routeDataChanged_(routeData_.page)', 10768 'routeDataChanged_(routeData_.page)',
15096 'selectedPageChanged_(selectedPage_)', 10769 'selectedPageChanged_(selectedPage_)',
15097 10770
15098 // queryParams_.q <=> queryState.searchTerm
15099 'searchTermChanged_(queryState_.searchTerm)', 10771 'searchTermChanged_(queryState_.searchTerm)',
15100 'searchQueryParamChanged_(queryParams_.q)', 10772 'searchQueryParamChanged_(queryParams_.q)',
15101 10773
15102 ], 10774 ],
15103 10775
15104 // TODO(calamity): Replace these event listeners with data bound properties.
15105 listeners: { 10776 listeners: {
15106 'cr-menu-tap': 'onMenuTap_', 10777 'cr-menu-tap': 'onMenuTap_',
15107 'history-checkbox-select': 'checkboxSelected', 10778 'history-checkbox-select': 'checkboxSelected',
15108 'unselect-all': 'unselectAll', 10779 'unselect-all': 'unselectAll',
15109 'delete-selected': 'deleteSelected', 10780 'delete-selected': 'deleteSelected',
15110 'search-domain': 'searchDomain_', 10781 'search-domain': 'searchDomain_',
15111 'history-close-drawer': 'closeDrawer_', 10782 'history-close-drawer': 'closeDrawer_',
15112 }, 10783 },
15113 10784
15114 /** @override */ 10785 /** @override */
15115 ready: function() { 10786 ready: function() {
15116 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); 10787 this.grouped_ = loadTimeData.getBoolean('groupByDomain');
15117 10788
15118 cr.ui.decorate('command', cr.ui.Command); 10789 cr.ui.decorate('command', cr.ui.Command);
15119 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); 10790 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
15120 document.addEventListener('command', this.onCommand_.bind(this)); 10791 document.addEventListener('command', this.onCommand_.bind(this));
15121 10792
15122 // Redirect legacy search URLs to URLs compatible with material history.
15123 if (window.location.hash) { 10793 if (window.location.hash) {
15124 window.location.href = window.location.href.split('#')[0] + '?' + 10794 window.location.href = window.location.href.split('#')[0] + '?' +
15125 window.location.hash.substr(1); 10795 window.location.hash.substr(1);
15126 } 10796 }
15127 }, 10797 },
15128 10798
15129 /** @private */ 10799 /** @private */
15130 onMenuTap_: function() { 10800 onMenuTap_: function() {
15131 var drawer = this.$$('#drawer'); 10801 var drawer = this.$$('#drawer');
15132 if (drawer) 10802 if (drawer)
15133 drawer.toggle(); 10803 drawer.toggle();
15134 }, 10804 },
15135 10805
15136 /**
15137 * Listens for history-item being selected or deselected (through checkbox)
15138 * and changes the view of the top toolbar.
15139 * @param {{detail: {countAddition: number}}} e
15140 */
15141 checkboxSelected: function(e) { 10806 checkboxSelected: function(e) {
15142 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); 10807 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
15143 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history) 10808 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history)
15144 .getSelectedItemCount(); 10809 .getSelectedItemCount();
15145 }, 10810 },
15146 10811
15147 /**
15148 * Listens for call to cancel selection and loops through all items to set
15149 * checkbox to be unselected.
15150 * @private
15151 */
15152 unselectAll: function() { 10812 unselectAll: function() {
15153 var listContainer = 10813 var listContainer =
15154 /** @type {HistoryListContainerElement} */ (this.$.history); 10814 /** @type {HistoryListContainerElement} */ (this.$.history);
15155 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); 10815 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
15156 listContainer.unselectAllItems(toolbar.count); 10816 listContainer.unselectAllItems(toolbar.count);
15157 toolbar.count = 0; 10817 toolbar.count = 0;
15158 }, 10818 },
15159 10819
15160 deleteSelected: function() { 10820 deleteSelected: function() {
15161 this.$.history.deleteSelectedWithPrompt(); 10821 this.$.history.deleteSelectedWithPrompt();
15162 }, 10822 },
15163 10823
15164 /**
15165 * @param {HistoryQuery} info An object containing information about the
15166 * query.
15167 * @param {!Array<HistoryEntry>} results A list of results.
15168 */
15169 historyResult: function(info, results) { 10824 historyResult: function(info, results) {
15170 this.set('queryState_.querying', false); 10825 this.set('queryState_.querying', false);
15171 this.set('queryResult_.info', info); 10826 this.set('queryResult_.info', info);
15172 this.set('queryResult_.results', results); 10827 this.set('queryResult_.results', results);
15173 var listContainer = 10828 var listContainer =
15174 /** @type {HistoryListContainerElement} */ (this.$['history']); 10829 /** @type {HistoryListContainerElement} */ (this.$['history']);
15175 listContainer.historyResult(info, results); 10830 listContainer.historyResult(info, results);
15176 }, 10831 },
15177 10832
15178 /**
15179 * Fired when the user presses 'More from this site'.
15180 * @param {{detail: {domain: string}}} e
15181 */
15182 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); }, 10833 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); },
15183 10834
15184 /**
15185 * @param {Event} e
15186 * @private
15187 */
15188 onCanExecute_: function(e) { 10835 onCanExecute_: function(e) {
15189 e = /** @type {cr.ui.CanExecuteEvent} */(e); 10836 e = /** @type {cr.ui.CanExecuteEvent} */(e);
15190 switch (e.command.id) { 10837 switch (e.command.id) {
15191 case 'find-command': 10838 case 'find-command':
15192 e.canExecute = true; 10839 e.canExecute = true;
15193 break; 10840 break;
15194 case 'slash-command': 10841 case 'slash-command':
15195 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); 10842 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused();
15196 break; 10843 break;
15197 case 'delete-command': 10844 case 'delete-command':
15198 e.canExecute = this.$.toolbar.count > 0; 10845 e.canExecute = this.$.toolbar.count > 0;
15199 break; 10846 break;
15200 } 10847 }
15201 }, 10848 },
15202 10849
15203 /**
15204 * @param {string} searchTerm
15205 * @private
15206 */
15207 searchTermChanged_: function(searchTerm) { 10850 searchTermChanged_: function(searchTerm) {
15208 this.set('queryParams_.q', searchTerm || null); 10851 this.set('queryParams_.q', searchTerm || null);
15209 this.$['history'].queryHistory(false); 10852 this.$['history'].queryHistory(false);
15210 }, 10853 },
15211 10854
15212 /**
15213 * @param {string} searchQuery
15214 * @private
15215 */
15216 searchQueryParamChanged_: function(searchQuery) { 10855 searchQueryParamChanged_: function(searchQuery) {
15217 this.$.toolbar.setSearchTerm(searchQuery || ''); 10856 this.$.toolbar.setSearchTerm(searchQuery || '');
15218 }, 10857 },
15219 10858
15220 /**
15221 * @param {Event} e
15222 * @private
15223 */
15224 onCommand_: function(e) { 10859 onCommand_: function(e) {
15225 if (e.command.id == 'find-command' || e.command.id == 'slash-command') 10860 if (e.command.id == 'find-command' || e.command.id == 'slash-command')
15226 this.$.toolbar.showSearchField(); 10861 this.$.toolbar.showSearchField();
15227 if (e.command.id == 'delete-command') 10862 if (e.command.id == 'delete-command')
15228 this.deleteSelected(); 10863 this.deleteSelected();
15229 }, 10864 },
15230 10865
15231 /**
15232 * @param {!Array<!ForeignSession>} sessionList Array of objects describing
15233 * the sessions from other devices.
15234 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
15235 */
15236 setForeignSessions: function(sessionList, isTabSyncEnabled) { 10866 setForeignSessions: function(sessionList, isTabSyncEnabled) {
15237 if (!isTabSyncEnabled) { 10867 if (!isTabSyncEnabled) {
15238 var syncedDeviceManagerElem = 10868 var syncedDeviceManagerElem =
15239 /** @type {HistorySyncedDeviceManagerElement} */this 10869 /** @type {HistorySyncedDeviceManagerElement} */this
15240 .$$('history-synced-device-manager'); 10870 .$$('history-synced-device-manager');
15241 if (syncedDeviceManagerElem) 10871 if (syncedDeviceManagerElem)
15242 syncedDeviceManagerElem.tabSyncDisabled(); 10872 syncedDeviceManagerElem.tabSyncDisabled();
15243 return; 10873 return;
15244 } 10874 }
15245 10875
15246 this.set('queryResult_.sessionList', sessionList); 10876 this.set('queryResult_.sessionList', sessionList);
15247 }, 10877 },
15248 10878
15249 /**
15250 * Update sign in state of synced device manager after user logs in or out.
15251 * @param {boolean} isUserSignedIn
15252 */
15253 updateSignInState: function(isUserSignedIn) { 10879 updateSignInState: function(isUserSignedIn) {
15254 var syncedDeviceManagerElem = 10880 var syncedDeviceManagerElem =
15255 /** @type {HistorySyncedDeviceManagerElement} */this 10881 /** @type {HistorySyncedDeviceManagerElement} */this
15256 .$$('history-synced-device-manager'); 10882 .$$('history-synced-device-manager');
15257 if (syncedDeviceManagerElem) 10883 if (syncedDeviceManagerElem)
15258 syncedDeviceManagerElem.updateSignInState(isUserSignedIn); 10884 syncedDeviceManagerElem.updateSignInState(isUserSignedIn);
15259 }, 10885 },
15260 10886
15261 /**
15262 * @param {string} selectedPage
15263 * @return {boolean}
15264 * @private
15265 */
15266 syncedTabsSelected_: function(selectedPage) { 10887 syncedTabsSelected_: function(selectedPage) {
15267 return selectedPage == 'syncedTabs'; 10888 return selectedPage == 'syncedTabs';
15268 }, 10889 },
15269 10890
15270 /**
15271 * @param {boolean} querying
15272 * @param {boolean} incremental
15273 * @param {string} searchTerm
15274 * @return {boolean} Whether a loading spinner should be shown (implies the
15275 * backend is querying a new search term).
15276 * @private
15277 */
15278 shouldShowSpinner_: function(querying, incremental, searchTerm) { 10891 shouldShowSpinner_: function(querying, incremental, searchTerm) {
15279 return querying && !incremental && searchTerm != ''; 10892 return querying && !incremental && searchTerm != '';
15280 }, 10893 },
15281 10894
15282 /**
15283 * @param {string} page
15284 * @private
15285 */
15286 routeDataChanged_: function(page) { 10895 routeDataChanged_: function(page) {
15287 this.selectedPage_ = page; 10896 this.selectedPage_ = page;
15288 }, 10897 },
15289 10898
15290 /**
15291 * @param {string} selectedPage
15292 * @private
15293 */
15294 selectedPageChanged_: function(selectedPage) { 10899 selectedPageChanged_: function(selectedPage) {
15295 this.set('routeData_.page', selectedPage); 10900 this.set('routeData_.page', selectedPage);
15296 }, 10901 },
15297 10902
15298 /**
15299 * This computed binding is needed to make the iron-pages selector update when
15300 * the synced-device-manager is instantiated for the first time. Otherwise the
15301 * fallback selection will continue to be used after the corresponding item is
15302 * added as a child of iron-pages.
15303 * @param {string} selectedPage
15304 * @param {Array} items
15305 * @return {string}
15306 * @private
15307 */
15308 getSelectedPage_: function(selectedPage, items) { 10903 getSelectedPage_: function(selectedPage, items) {
15309 return selectedPage; 10904 return selectedPage;
15310 }, 10905 },
15311 10906
15312 /** @private */ 10907 /** @private */
15313 closeDrawer_: function() { 10908 closeDrawer_: function() {
15314 var drawer = this.$$('#drawer'); 10909 var drawer = this.$$('#drawer');
15315 if (drawer) 10910 if (drawer)
15316 drawer.close(); 10911 drawer.close();
15317 }, 10912 },
15318 }); 10913 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698