OLD | NEW |
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | |
5 /** | |
6 * @fileoverview Assertion support. | |
7 */ | |
8 | |
9 /** | |
10 * Verify |condition| is truthy and return |condition| if so. | |
11 * @template T | |
12 * @param {T} condition A condition to check for truthiness. Note that this | |
13 * may be used to test whether a value is defined or not, and we don't want | |
14 * to force a cast to Boolean. | |
15 * @param {string=} opt_message A message to show on failure. | |
16 * @return {T} A non-null |condition|. | |
17 */ | |
18 function assert(condition, opt_message) { | 4 function assert(condition, opt_message) { |
19 if (!condition) { | 5 if (!condition) { |
20 var message = 'Assertion failed'; | 6 var message = 'Assertion failed'; |
21 if (opt_message) | 7 if (opt_message) message = message + ': ' + opt_message; |
22 message = message + ': ' + opt_message; | |
23 var error = new Error(message); | 8 var error = new Error(message); |
24 var global = function() { return this; }(); | 9 var global = function() { |
25 if (global.traceAssertionsForTesting) | 10 return this; |
26 console.warn(error.stack); | 11 }(); |
| 12 if (global.traceAssertionsForTesting) console.warn(error.stack); |
27 throw error; | 13 throw error; |
28 } | 14 } |
29 return condition; | 15 return condition; |
30 } | 16 } |
31 | 17 |
32 /** | |
33 * Call this from places in the code that should never be reached. | |
34 * | |
35 * For example, handling all the values of enum with a switch() like this: | |
36 * | |
37 * function getValueFromEnum(enum) { | |
38 * switch (enum) { | |
39 * case ENUM_FIRST_OF_TWO: | |
40 * return first | |
41 * case ENUM_LAST_OF_TWO: | |
42 * return last; | |
43 * } | |
44 * assertNotReached(); | |
45 * return document; | |
46 * } | |
47 * | |
48 * This code should only be hit in the case of serious programmer error or | |
49 * unexpected input. | |
50 * | |
51 * @param {string=} opt_message A message to show when this is hit. | |
52 */ | |
53 function assertNotReached(opt_message) { | 18 function assertNotReached(opt_message) { |
54 assert(false, opt_message || 'Unreachable code hit'); | 19 assert(false, opt_message || 'Unreachable code hit'); |
55 } | 20 } |
56 | 21 |
57 /** | |
58 * @param {*} value The value to check. | |
59 * @param {function(new: T, ...)} type A user-defined constructor. | |
60 * @param {string=} opt_message A message to show when this is hit. | |
61 * @return {T} | |
62 * @template T | |
63 */ | |
64 function assertInstanceof(value, type, opt_message) { | 22 function assertInstanceof(value, type, opt_message) { |
65 // We don't use assert immediately here so that we avoid constructing an error | |
66 // message if we don't have to. | |
67 if (!(value instanceof type)) { | 23 if (!(value instanceof type)) { |
68 assertNotReached(opt_message || 'Value ' + value + | 24 assertNotReached(opt_message || 'Value ' + value + ' is not a[n] ' + (type.n
ame || typeof type)); |
69 ' is not a[n] ' + (type.name || typeof type)); | |
70 } | 25 } |
71 return value; | 26 return value; |
72 }; | 27 } |
| 28 |
73 // Copyright 2016 The Chromium Authors. All rights reserved. | 29 // Copyright 2016 The Chromium Authors. All rights reserved. |
74 // Use of this source code is governed by a BSD-style license that can be | 30 // Use of this source code is governed by a BSD-style license that can be |
75 // found in the LICENSE file. | 31 // found in the LICENSE file. |
76 | |
77 /** | |
78 * @fileoverview PromiseResolver is a helper class that allows creating a | |
79 * Promise that will be fulfilled (resolved or rejected) some time later. | |
80 * | |
81 * Example: | |
82 * var resolver = new PromiseResolver(); | |
83 * resolver.promise.then(function(result) { | |
84 * console.log('resolved with', result); | |
85 * }); | |
86 * ... | |
87 * ... | |
88 * resolver.resolve({hello: 'world'}); | |
89 */ | |
90 | |
91 /** | |
92 * @constructor @struct | |
93 * @template T | |
94 */ | |
95 function PromiseResolver() { | 32 function PromiseResolver() { |
96 /** @private {function(T=): void} */ | |
97 this.resolve_; | 33 this.resolve_; |
98 | |
99 /** @private {function(*=): void} */ | |
100 this.reject_; | 34 this.reject_; |
101 | |
102 /** @private {!Promise<T>} */ | |
103 this.promise_ = new Promise(function(resolve, reject) { | 35 this.promise_ = new Promise(function(resolve, reject) { |
104 this.resolve_ = resolve; | 36 this.resolve_ = resolve; |
105 this.reject_ = reject; | 37 this.reject_ = reject; |
106 }.bind(this)); | 38 }.bind(this)); |
107 } | 39 } |
108 | 40 |
109 PromiseResolver.prototype = { | 41 PromiseResolver.prototype = { |
110 /** @return {!Promise<T>} */ | 42 get promise() { |
111 get promise() { return this.promise_; }, | 43 return this.promise_; |
112 set promise(p) { assertNotReached(); }, | 44 }, |
| 45 set promise(p) { |
| 46 assertNotReached(); |
| 47 }, |
| 48 get resolve() { |
| 49 return this.resolve_; |
| 50 }, |
| 51 set resolve(r) { |
| 52 assertNotReached(); |
| 53 }, |
| 54 get reject() { |
| 55 return this.reject_; |
| 56 }, |
| 57 set reject(s) { |
| 58 assertNotReached(); |
| 59 } |
| 60 }; |
113 | 61 |
114 /** @return {function(T=): void} */ | |
115 get resolve() { return this.resolve_; }, | |
116 set resolve(r) { assertNotReached(); }, | |
117 | |
118 /** @return {function(*=): void} */ | |
119 get reject() { return this.reject_; }, | |
120 set reject(s) { assertNotReached(); }, | |
121 }; | |
122 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 62 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
123 // Use of this source code is governed by a BSD-style license that can be | 63 // Use of this source code is governed by a BSD-style license that can be |
124 // found in the LICENSE file. | 64 // found in the LICENSE file. |
125 | |
126 /** | |
127 * The global object. | |
128 * @type {!Object} | |
129 * @const | |
130 */ | |
131 var global = this; | 65 var global = this; |
132 | 66 |
133 /** @typedef {{eventName: string, uid: number}} */ | |
134 var WebUIListener; | 67 var WebUIListener; |
135 | 68 |
136 /** Platform, package, object property, and Event support. **/ | |
137 var cr = cr || function() { | 69 var cr = cr || function() { |
138 'use strict'; | 70 'use strict'; |
139 | |
140 /** | |
141 * Builds an object structure for the provided namespace path, | |
142 * ensuring that names that already exist are not overwritten. For | |
143 * example: | |
144 * "a.b.c" -> a = {};a.b={};a.b.c={}; | |
145 * @param {string} name Name of the object that this file defines. | |
146 * @param {*=} opt_object The object to expose at the end of the path. | |
147 * @param {Object=} opt_objectToExportTo The object to add the path to; | |
148 * default is {@code global}. | |
149 * @return {!Object} The last object exported (i.e. exportPath('cr.ui') | |
150 * returns a reference to the ui property of window.cr). | |
151 * @private | |
152 */ | |
153 function exportPath(name, opt_object, opt_objectToExportTo) { | 71 function exportPath(name, opt_object, opt_objectToExportTo) { |
154 var parts = name.split('.'); | 72 var parts = name.split('.'); |
155 var cur = opt_objectToExportTo || global; | 73 var cur = opt_objectToExportTo || global; |
156 | 74 for (var part; parts.length && (part = parts.shift()); ) { |
157 for (var part; parts.length && (part = parts.shift());) { | |
158 if (!parts.length && opt_object !== undefined) { | 75 if (!parts.length && opt_object !== undefined) { |
159 // last part and we have an object; use it | |
160 cur[part] = opt_object; | 76 cur[part] = opt_object; |
161 } else if (part in cur) { | 77 } else if (part in cur) { |
162 cur = cur[part]; | 78 cur = cur[part]; |
163 } else { | 79 } else { |
164 cur = cur[part] = {}; | 80 cur = cur[part] = {}; |
165 } | 81 } |
166 } | 82 } |
167 return cur; | 83 return cur; |
168 } | 84 } |
169 | |
170 /** | |
171 * Fires a property change event on the target. | |
172 * @param {EventTarget} target The target to dispatch the event on. | |
173 * @param {string} propertyName The name of the property that changed. | |
174 * @param {*} newValue The new value for the property. | |
175 * @param {*} oldValue The old value for the property. | |
176 */ | |
177 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { | 85 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
178 var e = new Event(propertyName + 'Change'); | 86 var e = new Event(propertyName + 'Change'); |
179 e.propertyName = propertyName; | 87 e.propertyName = propertyName; |
180 e.newValue = newValue; | 88 e.newValue = newValue; |
181 e.oldValue = oldValue; | 89 e.oldValue = oldValue; |
182 target.dispatchEvent(e); | 90 target.dispatchEvent(e); |
183 } | 91 } |
184 | |
185 /** | |
186 * Converts a camelCase javascript property name to a hyphenated-lower-case | |
187 * attribute name. | |
188 * @param {string} jsName The javascript camelCase property name. | |
189 * @return {string} The equivalent hyphenated-lower-case attribute name. | |
190 */ | |
191 function getAttributeName(jsName) { | 92 function getAttributeName(jsName) { |
192 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); | 93 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
193 } | 94 } |
194 | |
195 /** | |
196 * The kind of property to define in {@code defineProperty}. | |
197 * @enum {string} | |
198 * @const | |
199 */ | |
200 var PropertyKind = { | 95 var PropertyKind = { |
201 /** | |
202 * Plain old JS property where the backing data is stored as a "private" | |
203 * field on the object. | |
204 * Use for properties of any type. Type will not be checked. | |
205 */ | |
206 JS: 'js', | 96 JS: 'js', |
207 | |
208 /** | |
209 * The property backing data is stored as an attribute on an element. | |
210 * Use only for properties of type {string}. | |
211 */ | |
212 ATTR: 'attr', | 97 ATTR: 'attr', |
213 | |
214 /** | |
215 * The property backing data is stored as an attribute on an element. If the | |
216 * element has the attribute then the value is true. | |
217 * Use only for properties of type {boolean}. | |
218 */ | |
219 BOOL_ATTR: 'boolAttr' | 98 BOOL_ATTR: 'boolAttr' |
220 }; | 99 }; |
221 | |
222 /** | |
223 * Helper function for defineProperty that returns the getter to use for the | |
224 * property. | |
225 * @param {string} name The name of the property. | |
226 * @param {PropertyKind} kind The kind of the property. | |
227 * @return {function():*} The getter for the property. | |
228 */ | |
229 function getGetter(name, kind) { | 100 function getGetter(name, kind) { |
230 switch (kind) { | 101 switch (kind) { |
231 case PropertyKind.JS: | 102 case PropertyKind.JS: |
232 var privateName = name + '_'; | 103 var privateName = name + '_'; |
233 return function() { | 104 return function() { |
234 return this[privateName]; | 105 return this[privateName]; |
235 }; | 106 }; |
236 case PropertyKind.ATTR: | 107 |
237 var attributeName = getAttributeName(name); | 108 case PropertyKind.ATTR: |
238 return function() { | 109 var attributeName = getAttributeName(name); |
239 return this.getAttribute(attributeName); | 110 return function() { |
240 }; | 111 return this.getAttribute(attributeName); |
241 case PropertyKind.BOOL_ATTR: | 112 }; |
242 var attributeName = getAttributeName(name); | 113 |
243 return function() { | 114 case PropertyKind.BOOL_ATTR: |
244 return this.hasAttribute(attributeName); | 115 var attributeName = getAttributeName(name); |
245 }; | 116 return function() { |
| 117 return this.hasAttribute(attributeName); |
| 118 }; |
246 } | 119 } |
247 | |
248 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
249 // the browser/unit tests to preprocess this file through grit. | |
250 throw 'not reached'; | 120 throw 'not reached'; |
251 } | 121 } |
252 | |
253 /** | |
254 * Helper function for defineProperty that returns the setter of the right | |
255 * kind. | |
256 * @param {string} name The name of the property we are defining the setter | |
257 * for. | |
258 * @param {PropertyKind} kind The kind of property we are getting the | |
259 * setter for. | |
260 * @param {function(*, *):void=} opt_setHook A function to run after the | |
261 * property is set, but before the propertyChange event is fired. | |
262 * @return {function(*):void} The function to use as a setter. | |
263 */ | |
264 function getSetter(name, kind, opt_setHook) { | 122 function getSetter(name, kind, opt_setHook) { |
265 switch (kind) { | 123 switch (kind) { |
266 case PropertyKind.JS: | 124 case PropertyKind.JS: |
267 var privateName = name + '_'; | 125 var privateName = name + '_'; |
268 return function(value) { | 126 return function(value) { |
269 var oldValue = this[name]; | 127 var oldValue = this[name]; |
270 if (value !== oldValue) { | 128 if (value !== oldValue) { |
271 this[privateName] = value; | 129 this[privateName] = value; |
272 if (opt_setHook) | 130 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
273 opt_setHook.call(this, value, oldValue); | 131 dispatchPropertyChange(this, name, value, oldValue); |
274 dispatchPropertyChange(this, name, value, oldValue); | 132 } |
275 } | 133 }; |
276 }; | |
277 | 134 |
278 case PropertyKind.ATTR: | 135 case PropertyKind.ATTR: |
279 var attributeName = getAttributeName(name); | 136 var attributeName = getAttributeName(name); |
280 return function(value) { | 137 return function(value) { |
281 var oldValue = this[name]; | 138 var oldValue = this[name]; |
282 if (value !== oldValue) { | 139 if (value !== oldValue) { |
283 if (value == undefined) | 140 if (value == undefined) this.removeAttribute(attributeName); else this
.setAttribute(attributeName, value); |
284 this.removeAttribute(attributeName); | 141 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
285 else | 142 dispatchPropertyChange(this, name, value, oldValue); |
286 this.setAttribute(attributeName, value); | 143 } |
287 if (opt_setHook) | 144 }; |
288 opt_setHook.call(this, value, oldValue); | |
289 dispatchPropertyChange(this, name, value, oldValue); | |
290 } | |
291 }; | |
292 | 145 |
293 case PropertyKind.BOOL_ATTR: | 146 case PropertyKind.BOOL_ATTR: |
294 var attributeName = getAttributeName(name); | 147 var attributeName = getAttributeName(name); |
295 return function(value) { | 148 return function(value) { |
296 var oldValue = this[name]; | 149 var oldValue = this[name]; |
297 if (value !== oldValue) { | 150 if (value !== oldValue) { |
298 if (value) | 151 if (value) this.setAttribute(attributeName, name); else this.removeAtt
ribute(attributeName); |
299 this.setAttribute(attributeName, name); | 152 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
300 else | 153 dispatchPropertyChange(this, name, value, oldValue); |
301 this.removeAttribute(attributeName); | 154 } |
302 if (opt_setHook) | 155 }; |
303 opt_setHook.call(this, value, oldValue); | |
304 dispatchPropertyChange(this, name, value, oldValue); | |
305 } | |
306 }; | |
307 } | 156 } |
308 | |
309 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
310 // the browser/unit tests to preprocess this file through grit. | |
311 throw 'not reached'; | 157 throw 'not reached'; |
312 } | 158 } |
313 | |
314 /** | |
315 * Defines a property on an object. When the setter changes the value a | |
316 * property change event with the type {@code name + 'Change'} is fired. | |
317 * @param {!Object} obj The object to define the property for. | |
318 * @param {string} name The name of the property. | |
319 * @param {PropertyKind=} opt_kind What kind of underlying storage to use. | |
320 * @param {function(*, *):void=} opt_setHook A function to run after the | |
321 * property is set, but before the propertyChange event is fired. | |
322 */ | |
323 function defineProperty(obj, name, opt_kind, opt_setHook) { | 159 function defineProperty(obj, name, opt_kind, opt_setHook) { |
324 if (typeof obj == 'function') | 160 if (typeof obj == 'function') obj = obj.prototype; |
325 obj = obj.prototype; | 161 var kind = opt_kind || PropertyKind.JS; |
326 | 162 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name,
kind)); |
327 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); | 163 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name,
kind, opt_setHook)); |
328 | |
329 if (!obj.__lookupGetter__(name)) | |
330 obj.__defineGetter__(name, getGetter(name, kind)); | |
331 | |
332 if (!obj.__lookupSetter__(name)) | |
333 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); | |
334 } | 164 } |
335 | |
336 /** | |
337 * Counter for use with createUid | |
338 */ | |
339 var uidCounter = 1; | 165 var uidCounter = 1; |
340 | |
341 /** | |
342 * @return {number} A new unique ID. | |
343 */ | |
344 function createUid() { | 166 function createUid() { |
345 return uidCounter++; | 167 return uidCounter++; |
346 } | 168 } |
347 | |
348 /** | |
349 * Returns a unique ID for the item. This mutates the item so it needs to be | |
350 * an object | |
351 * @param {!Object} item The item to get the unique ID for. | |
352 * @return {number} The unique ID for the item. | |
353 */ | |
354 function getUid(item) { | 169 function getUid(item) { |
355 if (item.hasOwnProperty('uid')) | 170 if (item.hasOwnProperty('uid')) return item.uid; |
356 return item.uid; | |
357 return item.uid = createUid(); | 171 return item.uid = createUid(); |
358 } | 172 } |
359 | |
360 /** | |
361 * Dispatches a simple event on an event target. | |
362 * @param {!EventTarget} target The event target to dispatch the event on. | |
363 * @param {string} type The type of the event. | |
364 * @param {boolean=} opt_bubbles Whether the event bubbles or not. | |
365 * @param {boolean=} opt_cancelable Whether the default action of the event | |
366 * can be prevented. Default is true. | |
367 * @return {boolean} If any of the listeners called {@code preventDefault} | |
368 * during the dispatch this will return false. | |
369 */ | |
370 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { | 173 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
371 var e = new Event(type, { | 174 var e = new Event(type, { |
372 bubbles: opt_bubbles, | 175 bubbles: opt_bubbles, |
373 cancelable: opt_cancelable === undefined || opt_cancelable | 176 cancelable: opt_cancelable === undefined || opt_cancelable |
374 }); | 177 }); |
375 return target.dispatchEvent(e); | 178 return target.dispatchEvent(e); |
376 } | 179 } |
377 | |
378 /** | |
379 * Calls |fun| and adds all the fields of the returned object to the object | |
380 * named by |name|. For example, cr.define('cr.ui', function() { | |
381 * function List() { | |
382 * ... | |
383 * } | |
384 * function ListItem() { | |
385 * ... | |
386 * } | |
387 * return { | |
388 * List: List, | |
389 * ListItem: ListItem, | |
390 * }; | |
391 * }); | |
392 * defines the functions cr.ui.List and cr.ui.ListItem. | |
393 * @param {string} name The name of the object that we are adding fields to. | |
394 * @param {!Function} fun The function that will return an object containing | |
395 * the names and values of the new fields. | |
396 */ | |
397 function define(name, fun) { | 180 function define(name, fun) { |
398 var obj = exportPath(name); | 181 var obj = exportPath(name); |
399 var exports = fun(); | 182 var exports = fun(); |
400 for (var propertyName in exports) { | 183 for (var propertyName in exports) { |
401 // Maybe we should check the prototype chain here? The current usage | 184 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property
Name); |
402 // pattern is always using an object literal so we only care about own | 185 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD
escriptor); |
403 // properties. | |
404 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, | |
405 propertyName); | |
406 if (propertyDescriptor) | |
407 Object.defineProperty(obj, propertyName, propertyDescriptor); | |
408 } | 186 } |
409 } | 187 } |
410 | |
411 /** | |
412 * Adds a {@code getInstance} static method that always return the same | |
413 * instance object. | |
414 * @param {!Function} ctor The constructor for the class to add the static | |
415 * method to. | |
416 */ | |
417 function addSingletonGetter(ctor) { | 188 function addSingletonGetter(ctor) { |
418 ctor.getInstance = function() { | 189 ctor.getInstance = function() { |
419 return ctor.instance_ || (ctor.instance_ = new ctor()); | 190 return ctor.instance_ || (ctor.instance_ = new ctor()); |
420 }; | 191 }; |
421 } | 192 } |
422 | |
423 /** | |
424 * Forwards public APIs to private implementations. | |
425 * @param {Function} ctor Constructor that have private implementations in its | |
426 * prototype. | |
427 * @param {Array<string>} methods List of public method names that have their | |
428 * underscored counterparts in constructor's prototype. | |
429 * @param {string=} opt_target Selector for target node. | |
430 */ | |
431 function makePublic(ctor, methods, opt_target) { | 193 function makePublic(ctor, methods, opt_target) { |
432 methods.forEach(function(method) { | 194 methods.forEach(function(method) { |
433 ctor[method] = function() { | 195 ctor[method] = function() { |
434 var target = opt_target ? document.getElementById(opt_target) : | 196 var target = opt_target ? document.getElementById(opt_target) : ctor.get
Instance(); |
435 ctor.getInstance(); | |
436 return target[method + '_'].apply(target, arguments); | 197 return target[method + '_'].apply(target, arguments); |
437 }; | 198 }; |
438 }); | 199 }); |
439 } | 200 } |
440 | |
441 /** | |
442 * The mapping used by the sendWithPromise mechanism to tie the Promise | |
443 * returned to callers with the corresponding WebUI response. The mapping is | |
444 * from ID to the PromiseResolver helper; the ID is generated by | |
445 * sendWithPromise and is unique across all invocations of said method. | |
446 * @type {!Object<!PromiseResolver>} | |
447 */ | |
448 var chromeSendResolverMap = {}; | 201 var chromeSendResolverMap = {}; |
449 | |
450 /** | |
451 * The named method the WebUI handler calls directly in response to a | |
452 * chrome.send call that expects a response. The handler requires no knowledge | |
453 * of the specific name of this method, as the name is passed to the handler | |
454 * as the first argument in the arguments list of chrome.send. The handler | |
455 * must pass the ID, also sent via the chrome.send arguments list, as the | |
456 * first argument of the JS invocation; additionally, the handler may | |
457 * supply any number of other arguments that will be included in the response. | |
458 * @param {string} id The unique ID identifying the Promise this response is | |
459 * tied to. | |
460 * @param {boolean} isSuccess Whether the request was successful. | |
461 * @param {*} response The response as sent from C++. | |
462 */ | |
463 function webUIResponse(id, isSuccess, response) { | 202 function webUIResponse(id, isSuccess, response) { |
464 var resolver = chromeSendResolverMap[id]; | 203 var resolver = chromeSendResolverMap[id]; |
465 delete chromeSendResolverMap[id]; | 204 delete chromeSendResolverMap[id]; |
466 | 205 if (isSuccess) resolver.resolve(response); else resolver.reject(response); |
467 if (isSuccess) | |
468 resolver.resolve(response); | |
469 else | |
470 resolver.reject(response); | |
471 } | 206 } |
472 | |
473 /** | |
474 * A variation of chrome.send, suitable for messages that expect a single | |
475 * response from C++. | |
476 * @param {string} methodName The name of the WebUI handler API. | |
477 * @param {...*} var_args Varibale number of arguments to be forwarded to the | |
478 * C++ call. | |
479 * @return {!Promise} | |
480 */ | |
481 function sendWithPromise(methodName, var_args) { | 207 function sendWithPromise(methodName, var_args) { |
482 var args = Array.prototype.slice.call(arguments, 1); | 208 var args = Array.prototype.slice.call(arguments, 1); |
483 var promiseResolver = new PromiseResolver(); | 209 var promiseResolver = new PromiseResolver(); |
484 var id = methodName + '_' + createUid(); | 210 var id = methodName + '_' + createUid(); |
485 chromeSendResolverMap[id] = promiseResolver; | 211 chromeSendResolverMap[id] = promiseResolver; |
486 chrome.send(methodName, [id].concat(args)); | 212 chrome.send(methodName, [ id ].concat(args)); |
487 return promiseResolver.promise; | 213 return promiseResolver.promise; |
488 } | 214 } |
489 | |
490 /** | |
491 * A map of maps associating event names with listeners. The 2nd level map | |
492 * associates a listener ID with the callback function, such that individual | |
493 * listeners can be removed from an event without affecting other listeners of | |
494 * the same event. | |
495 * @type {!Object<!Object<!Function>>} | |
496 */ | |
497 var webUIListenerMap = {}; | 215 var webUIListenerMap = {}; |
498 | |
499 /** | |
500 * The named method the WebUI handler calls directly when an event occurs. | |
501 * The WebUI handler must supply the name of the event as the first argument | |
502 * of the JS invocation; additionally, the handler may supply any number of | |
503 * other arguments that will be forwarded to the listener callbacks. | |
504 * @param {string} event The name of the event that has occurred. | |
505 * @param {...*} var_args Additional arguments passed from C++. | |
506 */ | |
507 function webUIListenerCallback(event, var_args) { | 216 function webUIListenerCallback(event, var_args) { |
508 var eventListenersMap = webUIListenerMap[event]; | 217 var eventListenersMap = webUIListenerMap[event]; |
509 if (!eventListenersMap) { | 218 if (!eventListenersMap) { |
510 // C++ event sent for an event that has no listeners. | |
511 // TODO(dpapad): Should a warning be displayed here? | |
512 return; | 219 return; |
513 } | 220 } |
514 | |
515 var args = Array.prototype.slice.call(arguments, 1); | 221 var args = Array.prototype.slice.call(arguments, 1); |
516 for (var listenerId in eventListenersMap) { | 222 for (var listenerId in eventListenersMap) { |
517 eventListenersMap[listenerId].apply(null, args); | 223 eventListenersMap[listenerId].apply(null, args); |
518 } | 224 } |
519 } | 225 } |
520 | |
521 /** | |
522 * Registers a listener for an event fired from WebUI handlers. Any number of | |
523 * listeners may register for a single event. | |
524 * @param {string} eventName The event to listen to. | |
525 * @param {!Function} callback The callback run when the event is fired. | |
526 * @return {!WebUIListener} An object to be used for removing a listener via | |
527 * cr.removeWebUIListener. Should be treated as read-only. | |
528 */ | |
529 function addWebUIListener(eventName, callback) { | 226 function addWebUIListener(eventName, callback) { |
530 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; | 227 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
531 var uid = createUid(); | 228 var uid = createUid(); |
532 webUIListenerMap[eventName][uid] = callback; | 229 webUIListenerMap[eventName][uid] = callback; |
533 return {eventName: eventName, uid: uid}; | 230 return { |
| 231 eventName: eventName, |
| 232 uid: uid |
| 233 }; |
534 } | 234 } |
535 | |
536 /** | |
537 * Removes a listener. Does nothing if the specified listener is not found. | |
538 * @param {!WebUIListener} listener The listener to be removed (as returned by | |
539 * addWebUIListener). | |
540 * @return {boolean} Whether the given listener was found and actually | |
541 * removed. | |
542 */ | |
543 function removeWebUIListener(listener) { | 235 function removeWebUIListener(listener) { |
544 var listenerExists = webUIListenerMap[listener.eventName] && | 236 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa
p[listener.eventName][listener.uid]; |
545 webUIListenerMap[listener.eventName][listener.uid]; | |
546 if (listenerExists) { | 237 if (listenerExists) { |
547 delete webUIListenerMap[listener.eventName][listener.uid]; | 238 delete webUIListenerMap[listener.eventName][listener.uid]; |
548 return true; | 239 return true; |
549 } | 240 } |
550 return false; | 241 return false; |
551 } | 242 } |
552 | |
553 return { | 243 return { |
554 addSingletonGetter: addSingletonGetter, | 244 addSingletonGetter: addSingletonGetter, |
555 createUid: createUid, | 245 createUid: createUid, |
556 define: define, | 246 define: define, |
557 defineProperty: defineProperty, | 247 defineProperty: defineProperty, |
558 dispatchPropertyChange: dispatchPropertyChange, | 248 dispatchPropertyChange: dispatchPropertyChange, |
559 dispatchSimpleEvent: dispatchSimpleEvent, | 249 dispatchSimpleEvent: dispatchSimpleEvent, |
560 exportPath: exportPath, | 250 exportPath: exportPath, |
561 getUid: getUid, | 251 getUid: getUid, |
562 makePublic: makePublic, | 252 makePublic: makePublic, |
563 PropertyKind: PropertyKind, | 253 PropertyKind: PropertyKind, |
564 | |
565 // C++ <-> JS communication related methods. | |
566 addWebUIListener: addWebUIListener, | 254 addWebUIListener: addWebUIListener, |
567 removeWebUIListener: removeWebUIListener, | 255 removeWebUIListener: removeWebUIListener, |
568 sendWithPromise: sendWithPromise, | 256 sendWithPromise: sendWithPromise, |
569 webUIListenerCallback: webUIListenerCallback, | 257 webUIListenerCallback: webUIListenerCallback, |
570 webUIResponse: webUIResponse, | 258 webUIResponse: webUIResponse, |
571 | |
572 get doc() { | 259 get doc() { |
573 return document; | 260 return document; |
574 }, | 261 }, |
575 | |
576 /** Whether we are using a Mac or not. */ | |
577 get isMac() { | 262 get isMac() { |
578 return /Mac/.test(navigator.platform); | 263 return /Mac/.test(navigator.platform); |
579 }, | 264 }, |
580 | |
581 /** Whether this is on the Windows platform or not. */ | |
582 get isWindows() { | 265 get isWindows() { |
583 return /Win/.test(navigator.platform); | 266 return /Win/.test(navigator.platform); |
584 }, | 267 }, |
585 | |
586 /** Whether this is on chromeOS or not. */ | |
587 get isChromeOS() { | 268 get isChromeOS() { |
588 return /CrOS/.test(navigator.userAgent); | 269 return /CrOS/.test(navigator.userAgent); |
589 }, | 270 }, |
590 | |
591 /** Whether this is on vanilla Linux (not chromeOS). */ | |
592 get isLinux() { | 271 get isLinux() { |
593 return /Linux/.test(navigator.userAgent); | 272 return /Linux/.test(navigator.userAgent); |
594 }, | 273 }, |
595 | |
596 /** Whether this is on Android. */ | |
597 get isAndroid() { | 274 get isAndroid() { |
598 return /Android/.test(navigator.userAgent); | 275 return /Android/.test(navigator.userAgent); |
599 }, | 276 }, |
600 | |
601 /** Whether this is on iOS. */ | |
602 get isIOS() { | 277 get isIOS() { |
603 return /iPad|iPhone|iPod/.test(navigator.platform); | 278 return /iPad|iPhone|iPod/.test(navigator.platform); |
604 } | 279 } |
605 }; | 280 }; |
606 }(); | 281 }(); |
| 282 |
607 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 283 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
608 // Use of this source code is governed by a BSD-style license that can be | 284 // Use of this source code is governed by a BSD-style license that can be |
609 // found in the LICENSE file. | 285 // found in the LICENSE file. |
610 | |
611 cr.define('cr.ui', function() { | 286 cr.define('cr.ui', function() { |
612 | |
613 /** | |
614 * Decorates elements as an instance of a class. | |
615 * @param {string|!Element} source The way to find the element(s) to decorate. | |
616 * If this is a string then {@code querySeletorAll} is used to find the | |
617 * elements to decorate. | |
618 * @param {!Function} constr The constructor to decorate with. The constr | |
619 * needs to have a {@code decorate} function. | |
620 */ | |
621 function decorate(source, constr) { | 287 function decorate(source, constr) { |
622 var elements; | 288 var elements; |
623 if (typeof source == 'string') | 289 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e
lse elements = [ source ]; |
624 elements = cr.doc.querySelectorAll(source); | |
625 else | |
626 elements = [source]; | |
627 | |
628 for (var i = 0, el; el = elements[i]; i++) { | 290 for (var i = 0, el; el = elements[i]; i++) { |
629 if (!(el instanceof constr)) | 291 if (!(el instanceof constr)) constr.decorate(el); |
630 constr.decorate(el); | |
631 } | 292 } |
632 } | 293 } |
633 | |
634 /** | |
635 * Helper function for creating new element for define. | |
636 */ | |
637 function createElementHelper(tagName, opt_bag) { | 294 function createElementHelper(tagName, opt_bag) { |
638 // Allow passing in ownerDocument to create in a different document. | |
639 var doc; | 295 var doc; |
640 if (opt_bag && opt_bag.ownerDocument) | 296 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc
= cr.doc; |
641 doc = opt_bag.ownerDocument; | |
642 else | |
643 doc = cr.doc; | |
644 return doc.createElement(tagName); | 297 return doc.createElement(tagName); |
645 } | 298 } |
646 | |
647 /** | |
648 * Creates the constructor for a UI element class. | |
649 * | |
650 * Usage: | |
651 * <pre> | |
652 * var List = cr.ui.define('list'); | |
653 * List.prototype = { | |
654 * __proto__: HTMLUListElement.prototype, | |
655 * decorate: function() { | |
656 * ... | |
657 * }, | |
658 * ... | |
659 * }; | |
660 * </pre> | |
661 * | |
662 * @param {string|Function} tagNameOrFunction The tagName or | |
663 * function to use for newly created elements. If this is a function it | |
664 * needs to return a new element when called. | |
665 * @return {function(Object=):Element} The constructor function which takes | |
666 * an optional property bag. The function also has a static | |
667 * {@code decorate} method added to it. | |
668 */ | |
669 function define(tagNameOrFunction) { | 299 function define(tagNameOrFunction) { |
670 var createFunction, tagName; | 300 var createFunction, tagName; |
671 if (typeof tagNameOrFunction == 'function') { | 301 if (typeof tagNameOrFunction == 'function') { |
672 createFunction = tagNameOrFunction; | 302 createFunction = tagNameOrFunction; |
673 tagName = ''; | 303 tagName = ''; |
674 } else { | 304 } else { |
675 createFunction = createElementHelper; | 305 createFunction = createElementHelper; |
676 tagName = tagNameOrFunction; | 306 tagName = tagNameOrFunction; |
677 } | 307 } |
678 | |
679 /** | |
680 * Creates a new UI element constructor. | |
681 * @param {Object=} opt_propertyBag Optional bag of properties to set on the | |
682 * object after created. The property {@code ownerDocument} is special | |
683 * cased and it allows you to create the element in a different | |
684 * document than the default. | |
685 * @constructor | |
686 */ | |
687 function f(opt_propertyBag) { | 308 function f(opt_propertyBag) { |
688 var el = createFunction(tagName, opt_propertyBag); | 309 var el = createFunction(tagName, opt_propertyBag); |
689 f.decorate(el); | 310 f.decorate(el); |
690 for (var propertyName in opt_propertyBag) { | 311 for (var propertyName in opt_propertyBag) { |
691 el[propertyName] = opt_propertyBag[propertyName]; | 312 el[propertyName] = opt_propertyBag[propertyName]; |
692 } | 313 } |
693 return el; | 314 return el; |
694 } | 315 } |
695 | |
696 /** | |
697 * Decorates an element as a UI element class. | |
698 * @param {!Element} el The element to decorate. | |
699 */ | |
700 f.decorate = function(el) { | 316 f.decorate = function(el) { |
701 el.__proto__ = f.prototype; | 317 el.__proto__ = f.prototype; |
702 el.decorate(); | 318 el.decorate(); |
703 }; | 319 }; |
704 | |
705 return f; | 320 return f; |
706 } | 321 } |
707 | |
708 /** | |
709 * Input elements do not grow and shrink with their content. This is a simple | |
710 * (and not very efficient) way of handling shrinking to content with support | |
711 * for min width and limited by the width of the parent element. | |
712 * @param {!HTMLElement} el The element to limit the width for. | |
713 * @param {!HTMLElement} parentEl The parent element that should limit the | |
714 * size. | |
715 * @param {number} min The minimum width. | |
716 * @param {number=} opt_scale Optional scale factor to apply to the width. | |
717 */ | |
718 function limitInputWidth(el, parentEl, min, opt_scale) { | 322 function limitInputWidth(el, parentEl, min, opt_scale) { |
719 // Needs a size larger than borders | |
720 el.style.width = '10px'; | 323 el.style.width = '10px'; |
721 var doc = el.ownerDocument; | 324 var doc = el.ownerDocument; |
722 var win = doc.defaultView; | 325 var win = doc.defaultView; |
723 var computedStyle = win.getComputedStyle(el); | 326 var computedStyle = win.getComputedStyle(el); |
724 var parentComputedStyle = win.getComputedStyle(parentEl); | 327 var parentComputedStyle = win.getComputedStyle(parentEl); |
725 var rtl = computedStyle.direction == 'rtl'; | 328 var rtl = computedStyle.direction == 'rtl'; |
726 | 329 var inputRect = el.getBoundingClientRect(); |
727 // To get the max width we get the width of the treeItem minus the position | |
728 // of the input. | |
729 var inputRect = el.getBoundingClientRect(); // box-sizing | |
730 var parentRect = parentEl.getBoundingClientRect(); | 330 var parentRect = parentEl.getBoundingClientRect(); |
731 var startPos = rtl ? parentRect.right - inputRect.right : | 331 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p
arentRect.left; |
732 inputRect.left - parentRect.left; | 332 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS
tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp
utedStyle.borderRightWidth, 10); |
733 | 333 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa
rseInt(parentComputedStyle.paddingRight, 10); |
734 // Add up border and padding of the input. | |
735 var inner = parseInt(computedStyle.borderLeftWidth, 10) + | |
736 parseInt(computedStyle.paddingLeft, 10) + | |
737 parseInt(computedStyle.paddingRight, 10) + | |
738 parseInt(computedStyle.borderRightWidth, 10); | |
739 | |
740 // We also need to subtract the padding of parent to prevent it to overflow. | |
741 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : | |
742 parseInt(parentComputedStyle.paddingRight, 10); | |
743 | |
744 var max = parentEl.clientWidth - startPos - inner - parentPadding; | 334 var max = parentEl.clientWidth - startPos - inner - parentPadding; |
745 if (opt_scale) | 335 if (opt_scale) max *= opt_scale; |
746 max *= opt_scale; | |
747 | |
748 function limit() { | 336 function limit() { |
749 if (el.scrollWidth > max) { | 337 if (el.scrollWidth > max) { |
750 el.style.width = max + 'px'; | 338 el.style.width = max + 'px'; |
751 } else { | 339 } else { |
752 el.style.width = 0; | 340 el.style.width = 0; |
753 var sw = el.scrollWidth; | 341 var sw = el.scrollWidth; |
754 if (sw < min) { | 342 if (sw < min) { |
755 el.style.width = min + 'px'; | 343 el.style.width = min + 'px'; |
756 } else { | 344 } else { |
757 el.style.width = sw + 'px'; | 345 el.style.width = sw + 'px'; |
758 } | 346 } |
759 } | 347 } |
760 } | 348 } |
761 | |
762 el.addEventListener('input', limit); | 349 el.addEventListener('input', limit); |
763 limit(); | 350 limit(); |
764 } | 351 } |
765 | |
766 /** | |
767 * Takes a number and spits out a value CSS will be happy with. To avoid | |
768 * subpixel layout issues, the value is rounded to the nearest integral value. | |
769 * @param {number} pixels The number of pixels. | |
770 * @return {string} e.g. '16px'. | |
771 */ | |
772 function toCssPx(pixels) { | 352 function toCssPx(pixels) { |
773 if (!window.isFinite(pixels)) | 353 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: '
+ pixels); |
774 console.error('Pixel value is not a number: ' + pixels); | |
775 return Math.round(pixels) + 'px'; | 354 return Math.round(pixels) + 'px'; |
776 } | 355 } |
777 | |
778 /** | |
779 * Users complain they occasionaly use doubleclicks instead of clicks | |
780 * (http://crbug.com/140364). To fix it we freeze click handling for | |
781 * the doubleclick time interval. | |
782 * @param {MouseEvent} e Initial click event. | |
783 */ | |
784 function swallowDoubleClick(e) { | 356 function swallowDoubleClick(e) { |
785 var doc = e.target.ownerDocument; | 357 var doc = e.target.ownerDocument; |
786 var counter = Math.min(1, e.detail); | 358 var counter = Math.min(1, e.detail); |
787 function swallow(e) { | 359 function swallow(e) { |
788 e.stopPropagation(); | 360 e.stopPropagation(); |
789 e.preventDefault(); | 361 e.preventDefault(); |
790 } | 362 } |
791 function onclick(e) { | 363 function onclick(e) { |
792 if (e.detail > counter) { | 364 if (e.detail > counter) { |
793 counter = e.detail; | 365 counter = e.detail; |
794 // Swallow the click since it's a click inside the doubleclick timeout. | |
795 swallow(e); | 366 swallow(e); |
796 } else { | 367 } else { |
797 // Stop tracking clicks and let regular handling. | |
798 doc.removeEventListener('dblclick', swallow, true); | 368 doc.removeEventListener('dblclick', swallow, true); |
799 doc.removeEventListener('click', onclick, true); | 369 doc.removeEventListener('click', onclick, true); |
800 } | 370 } |
801 } | 371 } |
802 // The following 'click' event (if e.type == 'mouseup') mustn't be taken | |
803 // into account (it mustn't stop tracking clicks). Start event listening | |
804 // after zero timeout. | |
805 setTimeout(function() { | 372 setTimeout(function() { |
806 doc.addEventListener('click', onclick, true); | 373 doc.addEventListener('click', onclick, true); |
807 doc.addEventListener('dblclick', swallow, true); | 374 doc.addEventListener('dblclick', swallow, true); |
808 }, 0); | 375 }, 0); |
809 } | 376 } |
810 | |
811 return { | 377 return { |
812 decorate: decorate, | 378 decorate: decorate, |
813 define: define, | 379 define: define, |
814 limitInputWidth: limitInputWidth, | 380 limitInputWidth: limitInputWidth, |
815 toCssPx: toCssPx, | 381 toCssPx: toCssPx, |
816 swallowDoubleClick: swallowDoubleClick | 382 swallowDoubleClick: swallowDoubleClick |
817 }; | 383 }; |
818 }); | 384 }); |
| 385 |
819 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 386 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
820 // Use of this source code is governed by a BSD-style license that can be | 387 // Use of this source code is governed by a BSD-style license that can be |
821 // found in the LICENSE file. | 388 // found in the LICENSE file. |
822 | |
823 /** | |
824 * @fileoverview A command is an abstraction of an action a user can do in the | |
825 * UI. | |
826 * | |
827 * When the focus changes in the document for each command a canExecute event | |
828 * is dispatched on the active element. By listening to this event you can | |
829 * enable and disable the command by setting the event.canExecute property. | |
830 * | |
831 * When a command is executed a command event is dispatched on the active | |
832 * element. Note that you should stop the propagation after you have handled the | |
833 * command if there might be other command listeners higher up in the DOM tree. | |
834 */ | |
835 | |
836 cr.define('cr.ui', function() { | 389 cr.define('cr.ui', function() { |
837 | |
838 /** | |
839 * This is used to identify keyboard shortcuts. | |
840 * @param {string} shortcut The text used to describe the keys for this | |
841 * keyboard shortcut. | |
842 * @constructor | |
843 */ | |
844 function KeyboardShortcut(shortcut) { | 390 function KeyboardShortcut(shortcut) { |
845 var mods = {}; | 391 var mods = {}; |
846 var ident = ''; | 392 var ident = ''; |
847 shortcut.split('|').forEach(function(part) { | 393 shortcut.split('|').forEach(function(part) { |
848 var partLc = part.toLowerCase(); | 394 var partLc = part.toLowerCase(); |
849 switch (partLc) { | 395 switch (partLc) { |
850 case 'alt': | 396 case 'alt': |
851 case 'ctrl': | 397 case 'ctrl': |
852 case 'meta': | 398 case 'meta': |
853 case 'shift': | 399 case 'shift': |
854 mods[partLc + 'Key'] = true; | 400 mods[partLc + 'Key'] = true; |
855 break; | 401 break; |
856 default: | 402 |
857 if (ident) | 403 default: |
858 throw Error('Invalid shortcut'); | 404 if (ident) throw Error('Invalid shortcut'); |
859 ident = part; | 405 ident = part; |
860 } | 406 } |
861 }); | 407 }); |
862 | |
863 this.ident_ = ident; | 408 this.ident_ = ident; |
864 this.mods_ = mods; | 409 this.mods_ = mods; |
865 } | 410 } |
866 | |
867 KeyboardShortcut.prototype = { | 411 KeyboardShortcut.prototype = { |
868 /** | |
869 * Whether the keyboard shortcut object matches a keyboard event. | |
870 * @param {!Event} e The keyboard event object. | |
871 * @return {boolean} Whether we found a match or not. | |
872 */ | |
873 matchesEvent: function(e) { | 412 matchesEvent: function(e) { |
874 if (e.key == this.ident_) { | 413 if (e.key == this.ident_) { |
875 // All keyboard modifiers needs to match. | |
876 var mods = this.mods_; | 414 var mods = this.mods_; |
877 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { | 415 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k)
{ |
878 return e[k] == !!mods[k]; | 416 return e[k] == !!mods[k]; |
879 }); | 417 }); |
880 } | 418 } |
881 return false; | 419 return false; |
882 } | 420 } |
883 }; | 421 }; |
884 | |
885 /** | |
886 * Creates a new command element. | |
887 * @constructor | |
888 * @extends {HTMLElement} | |
889 */ | |
890 var Command = cr.ui.define('command'); | 422 var Command = cr.ui.define('command'); |
891 | |
892 Command.prototype = { | 423 Command.prototype = { |
893 __proto__: HTMLElement.prototype, | 424 __proto__: HTMLElement.prototype, |
894 | |
895 /** | |
896 * Initializes the command. | |
897 */ | |
898 decorate: function() { | 425 decorate: function() { |
899 CommandManager.init(assert(this.ownerDocument)); | 426 CommandManager.init(assert(this.ownerDocument)); |
900 | 427 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor
tcut'); |
901 if (this.hasAttribute('shortcut')) | |
902 this.shortcut = this.getAttribute('shortcut'); | |
903 }, | 428 }, |
904 | |
905 /** | |
906 * Executes the command by dispatching a command event on the given element. | |
907 * If |element| isn't given, the active element is used instead. | |
908 * If the command is {@code disabled} this does nothing. | |
909 * @param {HTMLElement=} opt_element Optional element to dispatch event on. | |
910 */ | |
911 execute: function(opt_element) { | 429 execute: function(opt_element) { |
912 if (this.disabled) | 430 if (this.disabled) return; |
913 return; | |
914 var doc = this.ownerDocument; | 431 var doc = this.ownerDocument; |
915 if (doc.activeElement) { | 432 if (doc.activeElement) { |
916 var e = new Event('command', {bubbles: true}); | 433 var e = new Event('command', { |
| 434 bubbles: true |
| 435 }); |
917 e.command = this; | 436 e.command = this; |
918 | |
919 (opt_element || doc.activeElement).dispatchEvent(e); | 437 (opt_element || doc.activeElement).dispatchEvent(e); |
920 } | 438 } |
921 }, | 439 }, |
922 | |
923 /** | |
924 * Call this when there have been changes that might change whether the | |
925 * command can be executed or not. | |
926 * @param {Node=} opt_node Node for which to actuate command state. | |
927 */ | |
928 canExecuteChange: function(opt_node) { | 440 canExecuteChange: function(opt_node) { |
929 dispatchCanExecuteEvent(this, | 441 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement
); |
930 opt_node || this.ownerDocument.activeElement); | |
931 }, | 442 }, |
932 | |
933 /** | |
934 * The keyboard shortcut that triggers the command. This is a string | |
935 * consisting of a key (as reported by WebKit in keydown) as | |
936 * well as optional key modifiers joinded with a '|'. | |
937 * | |
938 * Multiple keyboard shortcuts can be provided by separating them by | |
939 * whitespace. | |
940 * | |
941 * For example: | |
942 * "F1" | |
943 * "Backspace|Meta" for Apple command backspace. | |
944 * "a|Ctrl" for Control A | |
945 * "Delete Backspace|Meta" for Delete and Command Backspace | |
946 * | |
947 * @type {string} | |
948 */ | |
949 shortcut_: '', | 443 shortcut_: '', |
950 get shortcut() { | 444 get shortcut() { |
951 return this.shortcut_; | 445 return this.shortcut_; |
952 }, | 446 }, |
953 set shortcut(shortcut) { | 447 set shortcut(shortcut) { |
954 var oldShortcut = this.shortcut_; | 448 var oldShortcut = this.shortcut_; |
955 if (shortcut !== oldShortcut) { | 449 if (shortcut !== oldShortcut) { |
956 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { | 450 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { |
957 return new KeyboardShortcut(shortcut); | 451 return new KeyboardShortcut(shortcut); |
958 }); | 452 }); |
959 | |
960 // Set this after the keyboardShortcuts_ since that might throw. | |
961 this.shortcut_ = shortcut; | 453 this.shortcut_ = shortcut; |
962 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, | 454 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut)
; |
963 oldShortcut); | |
964 } | 455 } |
965 }, | 456 }, |
966 | |
967 /** | |
968 * Whether the event object matches the shortcut for this command. | |
969 * @param {!Event} e The key event object. | |
970 * @return {boolean} Whether it matched or not. | |
971 */ | |
972 matchesEvent: function(e) { | 457 matchesEvent: function(e) { |
973 if (!this.keyboardShortcuts_) | 458 if (!this.keyboardShortcuts_) return false; |
974 return false; | |
975 | |
976 return this.keyboardShortcuts_.some(function(keyboardShortcut) { | 459 return this.keyboardShortcuts_.some(function(keyboardShortcut) { |
977 return keyboardShortcut.matchesEvent(e); | 460 return keyboardShortcut.matchesEvent(e); |
978 }); | 461 }); |
979 }, | 462 } |
980 }; | 463 }; |
981 | |
982 /** | |
983 * The label of the command. | |
984 */ | |
985 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); | 464 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); |
986 | |
987 /** | |
988 * Whether the command is disabled or not. | |
989 */ | |
990 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); | 465 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); |
991 | |
992 /** | |
993 * Whether the command is hidden or not. | |
994 */ | |
995 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); | 466 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
996 | |
997 /** | |
998 * Whether the command is checked or not. | |
999 */ | |
1000 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); | 467 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
1001 | |
1002 /** | |
1003 * The flag that prevents the shortcut text from being displayed on menu. | |
1004 * | |
1005 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command) | |
1006 * is displayed in menu when the command is assosiated with a menu item. | |
1007 * Otherwise, no text is displayed. | |
1008 */ | |
1009 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); | 468 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); |
1010 | |
1011 /** | |
1012 * Dispatches a canExecute event on the target. | |
1013 * @param {!cr.ui.Command} command The command that we are testing for. | |
1014 * @param {EventTarget} target The target element to dispatch the event on. | |
1015 */ | |
1016 function dispatchCanExecuteEvent(command, target) { | 469 function dispatchCanExecuteEvent(command, target) { |
1017 var e = new CanExecuteEvent(command); | 470 var e = new CanExecuteEvent(command); |
1018 target.dispatchEvent(e); | 471 target.dispatchEvent(e); |
1019 command.disabled = !e.canExecute; | 472 command.disabled = !e.canExecute; |
1020 } | 473 } |
1021 | |
1022 /** | |
1023 * The command managers for different documents. | |
1024 */ | |
1025 var commandManagers = {}; | 474 var commandManagers = {}; |
1026 | |
1027 /** | |
1028 * Keeps track of the focused element and updates the commands when the focus | |
1029 * changes. | |
1030 * @param {!Document} doc The document that we are managing the commands for. | |
1031 * @constructor | |
1032 */ | |
1033 function CommandManager(doc) { | 475 function CommandManager(doc) { |
1034 doc.addEventListener('focus', this.handleFocus_.bind(this), true); | 476 doc.addEventListener('focus', this.handleFocus_.bind(this), true); |
1035 // Make sure we add the listener to the bubbling phase so that elements can | |
1036 // prevent the command. | |
1037 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); | 477 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); |
1038 } | 478 } |
1039 | |
1040 /** | |
1041 * Initializes a command manager for the document as needed. | |
1042 * @param {!Document} doc The document to manage the commands for. | |
1043 */ | |
1044 CommandManager.init = function(doc) { | 479 CommandManager.init = function(doc) { |
1045 var uid = cr.getUid(doc); | 480 var uid = cr.getUid(doc); |
1046 if (!(uid in commandManagers)) { | 481 if (!(uid in commandManagers)) { |
1047 commandManagers[uid] = new CommandManager(doc); | 482 commandManagers[uid] = new CommandManager(doc); |
1048 } | 483 } |
1049 }; | 484 }; |
1050 | |
1051 CommandManager.prototype = { | 485 CommandManager.prototype = { |
1052 | |
1053 /** | |
1054 * Handles focus changes on the document. | |
1055 * @param {Event} e The focus event object. | |
1056 * @private | |
1057 * @suppress {checkTypes} | |
1058 * TODO(vitalyp): remove the suppression. | |
1059 */ | |
1060 handleFocus_: function(e) { | 486 handleFocus_: function(e) { |
1061 var target = e.target; | 487 var target = e.target; |
1062 | 488 if (target.menu || target.command) return; |
1063 // Ignore focus on a menu button or command item. | 489 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
1064 if (target.menu || target.command) | |
1065 return; | |
1066 | |
1067 var commands = Array.prototype.slice.call( | |
1068 target.ownerDocument.querySelectorAll('command')); | |
1069 | |
1070 commands.forEach(function(command) { | 490 commands.forEach(function(command) { |
1071 dispatchCanExecuteEvent(command, target); | 491 dispatchCanExecuteEvent(command, target); |
1072 }); | 492 }); |
1073 }, | 493 }, |
1074 | |
1075 /** | |
1076 * Handles the keydown event and routes it to the right command. | |
1077 * @param {!Event} e The keydown event. | |
1078 */ | |
1079 handleKeyDown_: function(e) { | 494 handleKeyDown_: function(e) { |
1080 var target = e.target; | 495 var target = e.target; |
1081 var commands = Array.prototype.slice.call( | 496 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
1082 target.ownerDocument.querySelectorAll('command')); | |
1083 | |
1084 for (var i = 0, command; command = commands[i]; i++) { | 497 for (var i = 0, command; command = commands[i]; i++) { |
1085 if (command.matchesEvent(e)) { | 498 if (command.matchesEvent(e)) { |
1086 // When invoking a command via a shortcut, we have to manually check | |
1087 // if it can be executed, since focus might not have been changed | |
1088 // what would have updated the command's state. | |
1089 command.canExecuteChange(); | 499 command.canExecuteChange(); |
1090 | |
1091 if (!command.disabled) { | 500 if (!command.disabled) { |
1092 e.preventDefault(); | 501 e.preventDefault(); |
1093 // We do not want any other element to handle this. | |
1094 e.stopPropagation(); | 502 e.stopPropagation(); |
1095 command.execute(); | 503 command.execute(); |
1096 return; | 504 return; |
1097 } | 505 } |
1098 } | 506 } |
1099 } | 507 } |
1100 } | 508 } |
1101 }; | 509 }; |
1102 | |
1103 /** | |
1104 * The event type used for canExecute events. | |
1105 * @param {!cr.ui.Command} command The command that we are evaluating. | |
1106 * @extends {Event} | |
1107 * @constructor | |
1108 * @class | |
1109 */ | |
1110 function CanExecuteEvent(command) { | 510 function CanExecuteEvent(command) { |
1111 var e = new Event('canExecute', {bubbles: true, cancelable: true}); | 511 var e = new Event('canExecute', { |
| 512 bubbles: true, |
| 513 cancelable: true |
| 514 }); |
1112 e.__proto__ = CanExecuteEvent.prototype; | 515 e.__proto__ = CanExecuteEvent.prototype; |
1113 e.command = command; | 516 e.command = command; |
1114 return e; | 517 return e; |
1115 } | 518 } |
1116 | |
1117 CanExecuteEvent.prototype = { | 519 CanExecuteEvent.prototype = { |
1118 __proto__: Event.prototype, | 520 __proto__: Event.prototype, |
1119 | |
1120 /** | |
1121 * The current command | |
1122 * @type {cr.ui.Command} | |
1123 */ | |
1124 command: null, | 521 command: null, |
1125 | |
1126 /** | |
1127 * Whether the target can execute the command. Setting this also stops the | |
1128 * propagation and prevents the default. Callers can tell if an event has | |
1129 * been handled via |this.defaultPrevented|. | |
1130 * @type {boolean} | |
1131 */ | |
1132 canExecute_: false, | 522 canExecute_: false, |
1133 get canExecute() { | 523 get canExecute() { |
1134 return this.canExecute_; | 524 return this.canExecute_; |
1135 }, | 525 }, |
1136 set canExecute(canExecute) { | 526 set canExecute(canExecute) { |
1137 this.canExecute_ = !!canExecute; | 527 this.canExecute_ = !!canExecute; |
1138 this.stopPropagation(); | 528 this.stopPropagation(); |
1139 this.preventDefault(); | 529 this.preventDefault(); |
1140 } | 530 } |
1141 }; | 531 }; |
1142 | |
1143 // Export | |
1144 return { | 532 return { |
1145 Command: Command, | 533 Command: Command, |
1146 CanExecuteEvent: CanExecuteEvent | 534 CanExecuteEvent: CanExecuteEvent |
1147 }; | 535 }; |
1148 }); | 536 }); |
| 537 |
1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 538 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
1150 // Use of this source code is governed by a BSD-style license that can be | 539 // Use of this source code is governed by a BSD-style license that can be |
1151 // found in the LICENSE file. | 540 // found in the LICENSE file. |
1152 | |
1153 // <include src-disabled="assert.js"> | |
1154 | |
1155 /** | |
1156 * Alias for document.getElementById. Found elements must be HTMLElements. | |
1157 * @param {string} id The ID of the element to find. | |
1158 * @return {HTMLElement} The found element or null if not found. | |
1159 */ | |
1160 function $(id) { | 541 function $(id) { |
1161 var el = document.getElementById(id); | 542 var el = document.getElementById(id); |
1162 return el ? assertInstanceof(el, HTMLElement) : null; | 543 return el ? assertInstanceof(el, HTMLElement) : null; |
1163 } | 544 } |
1164 | 545 |
1165 // TODO(devlin): This should return SVGElement, but closure compiler is missing | |
1166 // those externs. | |
1167 /** | |
1168 * Alias for document.getElementById. Found elements must be SVGElements. | |
1169 * @param {string} id The ID of the element to find. | |
1170 * @return {Element} The found element or null if not found. | |
1171 */ | |
1172 function getSVGElement(id) { | 546 function getSVGElement(id) { |
1173 var el = document.getElementById(id); | 547 var el = document.getElementById(id); |
1174 return el ? assertInstanceof(el, Element) : null; | 548 return el ? assertInstanceof(el, Element) : null; |
1175 } | 549 } |
1176 | 550 |
1177 /** | |
1178 * Add an accessible message to the page that will be announced to | |
1179 * users who have spoken feedback on, but will be invisible to all | |
1180 * other users. It's removed right away so it doesn't clutter the DOM. | |
1181 * @param {string} msg The text to be pronounced. | |
1182 */ | |
1183 function announceAccessibleMessage(msg) { | 551 function announceAccessibleMessage(msg) { |
1184 var element = document.createElement('div'); | 552 var element = document.createElement('div'); |
1185 element.setAttribute('aria-live', 'polite'); | 553 element.setAttribute('aria-live', 'polite'); |
1186 element.style.position = 'relative'; | 554 element.style.position = 'relative'; |
1187 element.style.left = '-9999px'; | 555 element.style.left = '-9999px'; |
1188 element.style.height = '0px'; | 556 element.style.height = '0px'; |
1189 element.innerText = msg; | 557 element.innerText = msg; |
1190 document.body.appendChild(element); | 558 document.body.appendChild(element); |
1191 window.setTimeout(function() { | 559 window.setTimeout(function() { |
1192 document.body.removeChild(element); | 560 document.body.removeChild(element); |
1193 }, 0); | 561 }, 0); |
1194 } | 562 } |
1195 | 563 |
1196 /** | |
1197 * Generates a CSS url string. | |
1198 * @param {string} s The URL to generate the CSS url for. | |
1199 * @return {string} The CSS url string. | |
1200 */ | |
1201 function url(s) { | 564 function url(s) { |
1202 // http://www.w3.org/TR/css3-values/#uris | |
1203 // Parentheses, commas, whitespace characters, single quotes (') and double | |
1204 // quotes (") appearing in a URI must be escaped with a backslash | |
1205 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | 565 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); |
1206 // WebKit has a bug when it comes to URLs that end with \ | |
1207 // https://bugs.webkit.org/show_bug.cgi?id=28885 | |
1208 if (/\\\\$/.test(s2)) { | 566 if (/\\\\$/.test(s2)) { |
1209 // Add a space to work around the WebKit bug. | |
1210 s2 += ' '; | 567 s2 += ' '; |
1211 } | 568 } |
1212 return 'url("' + s2 + '")'; | 569 return 'url("' + s2 + '")'; |
1213 } | 570 } |
1214 | 571 |
1215 /** | |
1216 * Parses query parameters from Location. | |
1217 * @param {Location} location The URL to generate the CSS url for. | |
1218 * @return {Object} Dictionary containing name value pairs for URL | |
1219 */ | |
1220 function parseQueryParams(location) { | 572 function parseQueryParams(location) { |
1221 var params = {}; | 573 var params = {}; |
1222 var query = unescape(location.search.substring(1)); | 574 var query = unescape(location.search.substring(1)); |
1223 var vars = query.split('&'); | 575 var vars = query.split('&'); |
1224 for (var i = 0; i < vars.length; i++) { | 576 for (var i = 0; i < vars.length; i++) { |
1225 var pair = vars[i].split('='); | 577 var pair = vars[i].split('='); |
1226 params[pair[0]] = pair[1]; | 578 params[pair[0]] = pair[1]; |
1227 } | 579 } |
1228 return params; | 580 return params; |
1229 } | 581 } |
1230 | 582 |
1231 /** | |
1232 * Creates a new URL by appending or replacing the given query key and value. | |
1233 * Not supporting URL with username and password. | |
1234 * @param {Location} location The original URL. | |
1235 * @param {string} key The query parameter name. | |
1236 * @param {string} value The query parameter value. | |
1237 * @return {string} The constructed new URL. | |
1238 */ | |
1239 function setQueryParam(location, key, value) { | 583 function setQueryParam(location, key, value) { |
1240 var query = parseQueryParams(location); | 584 var query = parseQueryParams(location); |
1241 query[encodeURIComponent(key)] = encodeURIComponent(value); | 585 query[encodeURIComponent(key)] = encodeURIComponent(value); |
1242 | |
1243 var newQuery = ''; | 586 var newQuery = ''; |
1244 for (var q in query) { | 587 for (var q in query) { |
1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; | 588 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; |
1246 } | 589 } |
1247 | |
1248 return location.origin + location.pathname + newQuery + location.hash; | 590 return location.origin + location.pathname + newQuery + location.hash; |
1249 } | 591 } |
1250 | 592 |
1251 /** | |
1252 * @param {Node} el A node to search for ancestors with |className|. | |
1253 * @param {string} className A class to search for. | |
1254 * @return {Element} A node with class of |className| or null if none is found. | |
1255 */ | |
1256 function findAncestorByClass(el, className) { | 593 function findAncestorByClass(el, className) { |
1257 return /** @type {Element} */(findAncestor(el, function(el) { | 594 return findAncestor(el, function(el) { |
1258 return el.classList && el.classList.contains(className); | 595 return el.classList && el.classList.contains(className); |
1259 })); | 596 }); |
1260 } | 597 } |
1261 | 598 |
1262 /** | |
1263 * Return the first ancestor for which the {@code predicate} returns true. | |
1264 * @param {Node} node The node to check. | |
1265 * @param {function(Node):boolean} predicate The function that tests the | |
1266 * nodes. | |
1267 * @return {Node} The found ancestor or null if not found. | |
1268 */ | |
1269 function findAncestor(node, predicate) { | 599 function findAncestor(node, predicate) { |
1270 var last = false; | 600 var last = false; |
1271 while (node != null && !(last = predicate(node))) { | 601 while (node != null && !(last = predicate(node))) { |
1272 node = node.parentNode; | 602 node = node.parentNode; |
1273 } | 603 } |
1274 return last ? node : null; | 604 return last ? node : null; |
1275 } | 605 } |
1276 | 606 |
1277 function swapDomNodes(a, b) { | 607 function swapDomNodes(a, b) { |
1278 var afterA = a.nextSibling; | 608 var afterA = a.nextSibling; |
1279 if (afterA == b) { | 609 if (afterA == b) { |
1280 swapDomNodes(b, a); | 610 swapDomNodes(b, a); |
1281 return; | 611 return; |
1282 } | 612 } |
1283 var aParent = a.parentNode; | 613 var aParent = a.parentNode; |
1284 b.parentNode.replaceChild(a, b); | 614 b.parentNode.replaceChild(a, b); |
1285 aParent.insertBefore(b, afterA); | 615 aParent.insertBefore(b, afterA); |
1286 } | 616 } |
1287 | 617 |
1288 /** | |
1289 * Disables text selection and dragging, with optional whitelist callbacks. | |
1290 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function | |
1291 * is defined and returns true, the onselectionstart event will be | |
1292 * surpressed. | |
1293 * @param {function(Event):boolean=} opt_allowDragStart Unless this function | |
1294 * is defined and returns true, the ondragstart event will be surpressed. | |
1295 */ | |
1296 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { | 618 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { |
1297 // Disable text selection. | |
1298 document.onselectstart = function(e) { | 619 document.onselectstart = function(e) { |
1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) | 620 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) e.prevent
Default(); |
1300 e.preventDefault(); | |
1301 }; | 621 }; |
1302 | |
1303 // Disable dragging. | |
1304 document.ondragstart = function(e) { | 622 document.ondragstart = function(e) { |
1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) | 623 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) e.preventDefa
ult(); |
1306 e.preventDefault(); | |
1307 }; | 624 }; |
1308 } | 625 } |
1309 | 626 |
1310 /** | |
1311 * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead. | |
1312 * Call this to stop clicks on <a href="#"> links from scrolling to the top of | |
1313 * the page (and possibly showing a # in the link). | |
1314 */ | |
1315 function preventDefaultOnPoundLinkClicks() { | 627 function preventDefaultOnPoundLinkClicks() { |
1316 document.addEventListener('click', function(e) { | 628 document.addEventListener('click', function(e) { |
1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { | 629 var anchor = findAncestor(e.target, function(el) { |
1318 return el.tagName == 'A'; | 630 return el.tagName == 'A'; |
1319 }); | 631 }); |
1320 // Use getAttribute() to prevent URL normalization. | 632 if (anchor && anchor.getAttribute('href') == '#') e.preventDefault(); |
1321 if (anchor && anchor.getAttribute('href') == '#') | |
1322 e.preventDefault(); | |
1323 }); | 633 }); |
1324 } | 634 } |
1325 | 635 |
1326 /** | |
1327 * Check the directionality of the page. | |
1328 * @return {boolean} True if Chrome is running an RTL UI. | |
1329 */ | |
1330 function isRTL() { | 636 function isRTL() { |
1331 return document.documentElement.dir == 'rtl'; | 637 return document.documentElement.dir == 'rtl'; |
1332 } | 638 } |
1333 | 639 |
1334 /** | |
1335 * Get an element that's known to exist by its ID. We use this instead of just | |
1336 * calling getElementById and not checking the result because this lets us | |
1337 * satisfy the JSCompiler type system. | |
1338 * @param {string} id The identifier name. | |
1339 * @return {!HTMLElement} the Element. | |
1340 */ | |
1341 function getRequiredElement(id) { | 640 function getRequiredElement(id) { |
1342 return assertInstanceof($(id), HTMLElement, | 641 return assertInstanceof($(id), HTMLElement, 'Missing required element: ' + id)
; |
1343 'Missing required element: ' + id); | |
1344 } | 642 } |
1345 | 643 |
1346 /** | |
1347 * Query an element that's known to exist by a selector. We use this instead of | |
1348 * just calling querySelector and not checking the result because this lets us | |
1349 * satisfy the JSCompiler type system. | |
1350 * @param {string} selectors CSS selectors to query the element. | |
1351 * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional | |
1352 * context object for querySelector. | |
1353 * @return {!HTMLElement} the Element. | |
1354 */ | |
1355 function queryRequiredElement(selectors, opt_context) { | 644 function queryRequiredElement(selectors, opt_context) { |
1356 var element = (opt_context || document).querySelector(selectors); | 645 var element = (opt_context || document).querySelector(selectors); |
1357 return assertInstanceof(element, HTMLElement, | 646 return assertInstanceof(element, HTMLElement, 'Missing required element: ' + s
electors); |
1358 'Missing required element: ' + selectors); | |
1359 } | 647 } |
1360 | 648 |
1361 // Handle click on a link. If the link points to a chrome: or file: url, then | 649 [ 'click', 'auxclick' ].forEach(function(eventName) { |
1362 // call into the browser to do the navigation. | |
1363 ['click', 'auxclick'].forEach(function(eventName) { | |
1364 document.addEventListener(eventName, function(e) { | 650 document.addEventListener(eventName, function(e) { |
1365 if (e.defaultPrevented) | 651 if (e.defaultPrevented) return; |
1366 return; | |
1367 | |
1368 var eventPath = e.path; | 652 var eventPath = e.path; |
1369 var anchor = null; | 653 var anchor = null; |
1370 if (eventPath) { | 654 if (eventPath) { |
1371 for (var i = 0; i < eventPath.length; i++) { | 655 for (var i = 0; i < eventPath.length; i++) { |
1372 var element = eventPath[i]; | 656 var element = eventPath[i]; |
1373 if (element.tagName === 'A' && element.href) { | 657 if (element.tagName === 'A' && element.href) { |
1374 anchor = element; | 658 anchor = element; |
1375 break; | 659 break; |
1376 } | 660 } |
1377 } | 661 } |
1378 } | 662 } |
1379 | |
1380 // Fallback if Event.path is not available. | |
1381 var el = e.target; | 663 var el = e.target; |
1382 if (!anchor && el.nodeType == Node.ELEMENT_NODE && | 664 if (!anchor && el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector(
'A, A *')) { |
1383 el.webkitMatchesSelector('A, A *')) { | |
1384 while (el.tagName != 'A') { | 665 while (el.tagName != 'A') { |
1385 el = el.parentElement; | 666 el = el.parentElement; |
1386 } | 667 } |
1387 anchor = el; | 668 anchor = el; |
1388 } | 669 } |
1389 | 670 if (!anchor) return; |
1390 if (!anchor) | 671 anchor = anchor; |
1391 return; | 672 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && (e.button
== 0 || e.button == 1)) { |
1392 | 673 chrome.send('navigateToUrl', [ anchor.href, anchor.target, e.button, e.alt
Key, e.ctrlKey, e.metaKey, e.shiftKey ]); |
1393 anchor = /** @type {!HTMLAnchorElement} */(anchor); | |
1394 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && | |
1395 (e.button == 0 || e.button == 1)) { | |
1396 chrome.send('navigateToUrl', [ | |
1397 anchor.href, | |
1398 anchor.target, | |
1399 e.button, | |
1400 e.altKey, | |
1401 e.ctrlKey, | |
1402 e.metaKey, | |
1403 e.shiftKey | |
1404 ]); | |
1405 e.preventDefault(); | 674 e.preventDefault(); |
1406 } | 675 } |
1407 }); | 676 }); |
1408 }); | 677 }); |
1409 | 678 |
1410 /** | |
1411 * Creates a new URL which is the old URL with a GET param of key=value. | |
1412 * @param {string} url The base URL. There is not sanity checking on the URL so | |
1413 * it must be passed in a proper format. | |
1414 * @param {string} key The key of the param. | |
1415 * @param {string} value The value of the param. | |
1416 * @return {string} The new URL. | |
1417 */ | |
1418 function appendParam(url, key, value) { | 679 function appendParam(url, key, value) { |
1419 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 680 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); |
1420 | 681 if (url.indexOf('?') == -1) return url + '?' + param; |
1421 if (url.indexOf('?') == -1) | |
1422 return url + '?' + param; | |
1423 return url + '&' + param; | 682 return url + '&' + param; |
1424 } | 683 } |
1425 | 684 |
1426 /** | |
1427 * Creates an element of a specified type with a specified class name. | |
1428 * @param {string} type The node type. | |
1429 * @param {string} className The class name to use. | |
1430 * @return {Element} The created element. | |
1431 */ | |
1432 function createElementWithClassName(type, className) { | 685 function createElementWithClassName(type, className) { |
1433 var elm = document.createElement(type); | 686 var elm = document.createElement(type); |
1434 elm.className = className; | 687 elm.className = className; |
1435 return elm; | 688 return elm; |
1436 } | 689 } |
1437 | 690 |
1438 /** | |
1439 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | |
1440 * or when no paint happens during the animation). This function sets up | |
1441 * a timer and emulate the event if it is not fired when the timer expires. | |
1442 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | |
1443 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | |
1444 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | |
1445 * using the transitionDuration style value. | |
1446 */ | |
1447 function ensureTransitionEndEvent(el, opt_timeOut) { | 691 function ensureTransitionEndEvent(el, opt_timeOut) { |
1448 if (opt_timeOut === undefined) { | 692 if (opt_timeOut === undefined) { |
1449 var style = getComputedStyle(el); | 693 var style = getComputedStyle(el); |
1450 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 694 opt_timeOut = parseFloat(style.transitionDuration) * 1e3; |
1451 | |
1452 // Give an additional 50ms buffer for the animation to complete. | |
1453 opt_timeOut += 50; | 695 opt_timeOut += 50; |
1454 } | 696 } |
1455 | |
1456 var fired = false; | 697 var fired = false; |
1457 el.addEventListener('webkitTransitionEnd', function f(e) { | 698 el.addEventListener('webkitTransitionEnd', function f(e) { |
1458 el.removeEventListener('webkitTransitionEnd', f); | 699 el.removeEventListener('webkitTransitionEnd', f); |
1459 fired = true; | 700 fired = true; |
1460 }); | 701 }); |
1461 window.setTimeout(function() { | 702 window.setTimeout(function() { |
1462 if (!fired) | 703 if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); |
1463 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | |
1464 }, opt_timeOut); | 704 }, opt_timeOut); |
1465 } | 705 } |
1466 | 706 |
1467 /** | |
1468 * Alias for document.scrollTop getter. | |
1469 * @param {!HTMLDocument} doc The document node where information will be | |
1470 * queried from. | |
1471 * @return {number} The Y document scroll offset. | |
1472 */ | |
1473 function scrollTopForDocument(doc) { | 707 function scrollTopForDocument(doc) { |
1474 return doc.documentElement.scrollTop || doc.body.scrollTop; | 708 return doc.documentElement.scrollTop || doc.body.scrollTop; |
1475 } | 709 } |
1476 | 710 |
1477 /** | |
1478 * Alias for document.scrollTop setter. | |
1479 * @param {!HTMLDocument} doc The document node where information will be | |
1480 * queried from. | |
1481 * @param {number} value The target Y scroll offset. | |
1482 */ | |
1483 function setScrollTopForDocument(doc, value) { | 711 function setScrollTopForDocument(doc, value) { |
1484 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 712 doc.documentElement.scrollTop = doc.body.scrollTop = value; |
1485 } | 713 } |
1486 | 714 |
1487 /** | |
1488 * Alias for document.scrollLeft getter. | |
1489 * @param {!HTMLDocument} doc The document node where information will be | |
1490 * queried from. | |
1491 * @return {number} The X document scroll offset. | |
1492 */ | |
1493 function scrollLeftForDocument(doc) { | 715 function scrollLeftForDocument(doc) { |
1494 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 716 return doc.documentElement.scrollLeft || doc.body.scrollLeft; |
1495 } | 717 } |
1496 | 718 |
1497 /** | |
1498 * Alias for document.scrollLeft setter. | |
1499 * @param {!HTMLDocument} doc The document node where information will be | |
1500 * queried from. | |
1501 * @param {number} value The target X scroll offset. | |
1502 */ | |
1503 function setScrollLeftForDocument(doc, value) { | 719 function setScrollLeftForDocument(doc, value) { |
1504 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 720 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; |
1505 } | 721 } |
1506 | 722 |
1507 /** | |
1508 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | |
1509 * @param {string} original The original string. | |
1510 * @return {string} The string with all the characters mentioned above replaced. | |
1511 */ | |
1512 function HTMLEscape(original) { | 723 function HTMLEscape(original) { |
1513 return original.replace(/&/g, '&') | 724 return original.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '&g
t;').replace(/"/g, '"').replace(/'/g, '''); |
1514 .replace(/</g, '<') | 725 } |
1515 .replace(/>/g, '>') | 726 |
1516 .replace(/"/g, '"') | |
1517 .replace(/'/g, '''); | |
1518 } | |
1519 | |
1520 /** | |
1521 * Shortens the provided string (if necessary) to a string of length at most | |
1522 * |maxLength|. | |
1523 * @param {string} original The original string. | |
1524 * @param {number} maxLength The maximum length allowed for the string. | |
1525 * @return {string} The original string if its length does not exceed | |
1526 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | |
1527 * appended. | |
1528 */ | |
1529 function elide(original, maxLength) { | 727 function elide(original, maxLength) { |
1530 if (original.length <= maxLength) | 728 if (original.length <= maxLength) return original; |
1531 return original; | 729 return original.substring(0, maxLength - 1) + '…'; |
1532 return original.substring(0, maxLength - 1) + '\u2026'; | 730 } |
1533 } | 731 |
1534 | |
1535 /** | |
1536 * Quote a string so it can be used in a regular expression. | |
1537 * @param {string} str The source string. | |
1538 * @return {string} The escaped string. | |
1539 */ | |
1540 function quoteString(str) { | 732 function quoteString(str) { |
1541 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 733 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); |
1542 } | 734 } |
1543 | 735 |
1544 // <if expr="is_ios"> | 736 // <if expr="is_ios"> |
1545 // Polyfill 'key' in KeyboardEvent for iOS. | |
1546 // This function is not intended to be complete but should | |
1547 // be sufficient enough to have iOS work correctly while | |
1548 // it does not support key yet. | |
1549 if (!('key' in KeyboardEvent.prototype)) { | 737 if (!('key' in KeyboardEvent.prototype)) { |
1550 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 738 Object.defineProperty(KeyboardEvent.prototype, 'key', { |
1551 /** @this {KeyboardEvent} */ | 739 get: function() { |
1552 get: function () { | 740 if (this.keyCode >= 48 && this.keyCode <= 57) return String.fromCharCode(t
his.keyCode); |
1553 // 0-9 | 741 if (this.keyCode >= 65 && this.keyCode <= 90) { |
1554 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | |
1555 return String.fromCharCode(this.keyCode); | |
1556 | |
1557 // A-Z | |
1558 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | |
1559 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 742 var result = String.fromCharCode(this.keyCode).toLowerCase(); |
1560 if (this.shiftKey) | 743 if (this.shiftKey) result = result.toUpperCase(); |
1561 result = result.toUpperCase(); | |
1562 return result; | 744 return result; |
1563 } | 745 } |
1564 | 746 switch (this.keyCode) { |
1565 // Special characters | 747 case 8: |
1566 switch(this.keyCode) { | 748 return 'Backspace'; |
1567 case 0x08: return 'Backspace'; | 749 |
1568 case 0x09: return 'Tab'; | 750 case 9: |
1569 case 0x0d: return 'Enter'; | 751 return 'Tab'; |
1570 case 0x10: return 'Shift'; | 752 |
1571 case 0x11: return 'Control'; | 753 case 13: |
1572 case 0x12: return 'Alt'; | 754 return 'Enter'; |
1573 case 0x1b: return 'Escape'; | 755 |
1574 case 0x20: return ' '; | 756 case 16: |
1575 case 0x21: return 'PageUp'; | 757 return 'Shift'; |
1576 case 0x22: return 'PageDown'; | 758 |
1577 case 0x23: return 'End'; | 759 case 17: |
1578 case 0x24: return 'Home'; | 760 return 'Control'; |
1579 case 0x25: return 'ArrowLeft'; | 761 |
1580 case 0x26: return 'ArrowUp'; | 762 case 18: |
1581 case 0x27: return 'ArrowRight'; | 763 return 'Alt'; |
1582 case 0x28: return 'ArrowDown'; | 764 |
1583 case 0x2d: return 'Insert'; | 765 case 27: |
1584 case 0x2e: return 'Delete'; | 766 return 'Escape'; |
1585 case 0x5b: return 'Meta'; | 767 |
1586 case 0x70: return 'F1'; | 768 case 32: |
1587 case 0x71: return 'F2'; | 769 return ' '; |
1588 case 0x72: return 'F3'; | 770 |
1589 case 0x73: return 'F4'; | 771 case 33: |
1590 case 0x74: return 'F5'; | 772 return 'PageUp'; |
1591 case 0x75: return 'F6'; | 773 |
1592 case 0x76: return 'F7'; | 774 case 34: |
1593 case 0x77: return 'F8'; | 775 return 'PageDown'; |
1594 case 0x78: return 'F9'; | 776 |
1595 case 0x79: return 'F10'; | 777 case 35: |
1596 case 0x7a: return 'F11'; | 778 return 'End'; |
1597 case 0x7b: return 'F12'; | 779 |
1598 case 0xbb: return '='; | 780 case 36: |
1599 case 0xbd: return '-'; | 781 return 'Home'; |
1600 case 0xdb: return '['; | 782 |
1601 case 0xdd: return ']'; | 783 case 37: |
| 784 return 'ArrowLeft'; |
| 785 |
| 786 case 38: |
| 787 return 'ArrowUp'; |
| 788 |
| 789 case 39: |
| 790 return 'ArrowRight'; |
| 791 |
| 792 case 40: |
| 793 return 'ArrowDown'; |
| 794 |
| 795 case 45: |
| 796 return 'Insert'; |
| 797 |
| 798 case 46: |
| 799 return 'Delete'; |
| 800 |
| 801 case 91: |
| 802 return 'Meta'; |
| 803 |
| 804 case 112: |
| 805 return 'F1'; |
| 806 |
| 807 case 113: |
| 808 return 'F2'; |
| 809 |
| 810 case 114: |
| 811 return 'F3'; |
| 812 |
| 813 case 115: |
| 814 return 'F4'; |
| 815 |
| 816 case 116: |
| 817 return 'F5'; |
| 818 |
| 819 case 117: |
| 820 return 'F6'; |
| 821 |
| 822 case 118: |
| 823 return 'F7'; |
| 824 |
| 825 case 119: |
| 826 return 'F8'; |
| 827 |
| 828 case 120: |
| 829 return 'F9'; |
| 830 |
| 831 case 121: |
| 832 return 'F10'; |
| 833 |
| 834 case 122: |
| 835 return 'F11'; |
| 836 |
| 837 case 123: |
| 838 return 'F12'; |
| 839 |
| 840 case 187: |
| 841 return '='; |
| 842 |
| 843 case 189: |
| 844 return '-'; |
| 845 |
| 846 case 219: |
| 847 return '['; |
| 848 |
| 849 case 221: |
| 850 return ']'; |
1602 } | 851 } |
1603 return 'Unidentified'; | 852 return 'Unidentified'; |
1604 } | 853 } |
1605 }); | 854 }); |
1606 } else { | 855 } else { |
1607 window.console.log("KeyboardEvent.Key polyfill not required"); | 856 window.console.log("KeyboardEvent.Key polyfill not required"); |
1608 } | 857 } |
| 858 |
1609 // </if> /* is_ios */ | 859 // </if> /* is_ios */ |
1610 /** | 860 Polymer.IronResizableBehavior = { |
1611 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | 861 properties: { |
1612 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | 862 _parentResizable: { |
1613 * size or hidden state of their children) and "resizables" (elements that nee
d to be | 863 type: Object, |
1614 * notified when they are resized or un-hidden by their parents in order to ta
ke | 864 observer: '_parentResizableChanged' |
1615 * action on their new measurements). | 865 }, |
1616 * | 866 _notifyingDescendant: { |
1617 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | 867 type: Boolean, |
1618 * their element definition and listen for the `iron-resize` event on themselv
es. | 868 value: false |
1619 * This event will be fired when they become showing after having been hidden, | 869 } |
1620 * when they are resized explicitly by another resizable, or when the window h
as been | 870 }, |
1621 * resized. | 871 listeners: { |
1622 * | 872 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
1623 * Note, the `iron-resize` event is non-bubbling. | 873 }, |
1624 * | 874 created: function() { |
1625 * @polymerBehavior Polymer.IronResizableBehavior | 875 this._interestedResizables = []; |
1626 * @demo demo/index.html | 876 this._boundNotifyResize = this.notifyResize.bind(this); |
1627 **/ | 877 }, |
1628 Polymer.IronResizableBehavior = { | 878 attached: function() { |
| 879 this.fire('iron-request-resize-notifications', null, { |
| 880 node: this, |
| 881 bubbles: true, |
| 882 cancelable: true |
| 883 }); |
| 884 if (!this._parentResizable) { |
| 885 window.addEventListener('resize', this._boundNotifyResize); |
| 886 this.notifyResize(); |
| 887 } |
| 888 }, |
| 889 detached: function() { |
| 890 if (this._parentResizable) { |
| 891 this._parentResizable.stopResizeNotificationsFor(this); |
| 892 } else { |
| 893 window.removeEventListener('resize', this._boundNotifyResize); |
| 894 } |
| 895 this._parentResizable = null; |
| 896 }, |
| 897 notifyResize: function() { |
| 898 if (!this.isAttached) { |
| 899 return; |
| 900 } |
| 901 this._interestedResizables.forEach(function(resizable) { |
| 902 if (this.resizerShouldNotify(resizable)) { |
| 903 this._notifyDescendant(resizable); |
| 904 } |
| 905 }, this); |
| 906 this._fireResize(); |
| 907 }, |
| 908 assignParentResizable: function(parentResizable) { |
| 909 this._parentResizable = parentResizable; |
| 910 }, |
| 911 stopResizeNotificationsFor: function(target) { |
| 912 var index = this._interestedResizables.indexOf(target); |
| 913 if (index > -1) { |
| 914 this._interestedResizables.splice(index, 1); |
| 915 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
| 916 } |
| 917 }, |
| 918 resizerShouldNotify: function(element) { |
| 919 return true; |
| 920 }, |
| 921 _onDescendantIronResize: function(event) { |
| 922 if (this._notifyingDescendant) { |
| 923 event.stopPropagation(); |
| 924 return; |
| 925 } |
| 926 if (!Polymer.Settings.useShadow) { |
| 927 this._fireResize(); |
| 928 } |
| 929 }, |
| 930 _fireResize: function() { |
| 931 this.fire('iron-resize', null, { |
| 932 node: this, |
| 933 bubbles: false |
| 934 }); |
| 935 }, |
| 936 _onIronRequestResizeNotifications: function(event) { |
| 937 var target = event.path ? event.path[0] : event.target; |
| 938 if (target === this) { |
| 939 return; |
| 940 } |
| 941 if (this._interestedResizables.indexOf(target) === -1) { |
| 942 this._interestedResizables.push(target); |
| 943 this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| 944 } |
| 945 target.assignParentResizable(this); |
| 946 this._notifyDescendant(target); |
| 947 event.stopPropagation(); |
| 948 }, |
| 949 _parentResizableChanged: function(parentResizable) { |
| 950 if (parentResizable) { |
| 951 window.removeEventListener('resize', this._boundNotifyResize); |
| 952 } |
| 953 }, |
| 954 _notifyDescendant: function(descendant) { |
| 955 if (!this.isAttached) { |
| 956 return; |
| 957 } |
| 958 this._notifyingDescendant = true; |
| 959 descendant.notifyResize(); |
| 960 this._notifyingDescendant = false; |
| 961 } |
| 962 }; |
| 963 |
| 964 (function() { |
| 965 'use strict'; |
| 966 var KEY_IDENTIFIER = { |
| 967 'U+0008': 'backspace', |
| 968 'U+0009': 'tab', |
| 969 'U+001B': 'esc', |
| 970 'U+0020': 'space', |
| 971 'U+007F': 'del' |
| 972 }; |
| 973 var KEY_CODE = { |
| 974 8: 'backspace', |
| 975 9: 'tab', |
| 976 13: 'enter', |
| 977 27: 'esc', |
| 978 33: 'pageup', |
| 979 34: 'pagedown', |
| 980 35: 'end', |
| 981 36: 'home', |
| 982 32: 'space', |
| 983 37: 'left', |
| 984 38: 'up', |
| 985 39: 'right', |
| 986 40: 'down', |
| 987 46: 'del', |
| 988 106: '*' |
| 989 }; |
| 990 var MODIFIER_KEYS = { |
| 991 shift: 'shiftKey', |
| 992 ctrl: 'ctrlKey', |
| 993 alt: 'altKey', |
| 994 meta: 'metaKey' |
| 995 }; |
| 996 var KEY_CHAR = /[a-z0-9*]/; |
| 997 var IDENT_CHAR = /U\+/; |
| 998 var ARROW_KEY = /^arrow/; |
| 999 var SPACE_KEY = /^space(bar)?/; |
| 1000 var ESC_KEY = /^escape$/; |
| 1001 function transformKey(key, noSpecialChars) { |
| 1002 var validKey = ''; |
| 1003 if (key) { |
| 1004 var lKey = key.toLowerCase(); |
| 1005 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 1006 validKey = 'space'; |
| 1007 } else if (ESC_KEY.test(lKey)) { |
| 1008 validKey = 'esc'; |
| 1009 } else if (lKey.length == 1) { |
| 1010 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 1011 validKey = lKey; |
| 1012 } |
| 1013 } else if (ARROW_KEY.test(lKey)) { |
| 1014 validKey = lKey.replace('arrow', ''); |
| 1015 } else if (lKey == 'multiply') { |
| 1016 validKey = '*'; |
| 1017 } else { |
| 1018 validKey = lKey; |
| 1019 } |
| 1020 } |
| 1021 return validKey; |
| 1022 } |
| 1023 function transformKeyIdentifier(keyIdent) { |
| 1024 var validKey = ''; |
| 1025 if (keyIdent) { |
| 1026 if (keyIdent in KEY_IDENTIFIER) { |
| 1027 validKey = KEY_IDENTIFIER[keyIdent]; |
| 1028 } else if (IDENT_CHAR.test(keyIdent)) { |
| 1029 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 1030 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 1031 } else { |
| 1032 validKey = keyIdent.toLowerCase(); |
| 1033 } |
| 1034 } |
| 1035 return validKey; |
| 1036 } |
| 1037 function transformKeyCode(keyCode) { |
| 1038 var validKey = ''; |
| 1039 if (Number(keyCode)) { |
| 1040 if (keyCode >= 65 && keyCode <= 90) { |
| 1041 validKey = String.fromCharCode(32 + keyCode); |
| 1042 } else if (keyCode >= 112 && keyCode <= 123) { |
| 1043 validKey = 'f' + (keyCode - 112); |
| 1044 } else if (keyCode >= 48 && keyCode <= 57) { |
| 1045 validKey = String(keyCode - 48); |
| 1046 } else if (keyCode >= 96 && keyCode <= 105) { |
| 1047 validKey = String(keyCode - 96); |
| 1048 } else { |
| 1049 validKey = KEY_CODE[keyCode]; |
| 1050 } |
| 1051 } |
| 1052 return validKey; |
| 1053 } |
| 1054 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 1055 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier(
keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke
yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| 1056 } |
| 1057 function keyComboMatchesEvent(keyCombo, event) { |
| 1058 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 1059 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift
Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event
.altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey); |
| 1060 } |
| 1061 function parseKeyComboString(keyComboString) { |
| 1062 if (keyComboString.length === 1) { |
| 1063 return { |
| 1064 combo: keyComboString, |
| 1065 key: keyComboString, |
| 1066 event: 'keydown' |
| 1067 }; |
| 1068 } |
| 1069 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar
t) { |
| 1070 var eventParts = keyComboPart.split(':'); |
| 1071 var keyName = eventParts[0]; |
| 1072 var event = eventParts[1]; |
| 1073 if (keyName in MODIFIER_KEYS) { |
| 1074 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 1075 parsedKeyCombo.hasModifiers = true; |
| 1076 } else { |
| 1077 parsedKeyCombo.key = keyName; |
| 1078 parsedKeyCombo.event = event || 'keydown'; |
| 1079 } |
| 1080 return parsedKeyCombo; |
| 1081 }, { |
| 1082 combo: keyComboString.split(':').shift() |
| 1083 }); |
| 1084 } |
| 1085 function parseEventString(eventString) { |
| 1086 return eventString.trim().split(' ').map(function(keyComboString) { |
| 1087 return parseKeyComboString(keyComboString); |
| 1088 }); |
| 1089 } |
| 1090 Polymer.IronA11yKeysBehavior = { |
1629 properties: { | 1091 properties: { |
1630 /** | 1092 keyEventTarget: { |
1631 * The closest ancestor element that implements `IronResizableBehavior`. | |
1632 */ | |
1633 _parentResizable: { | |
1634 type: Object, | 1093 type: Object, |
1635 observer: '_parentResizableChanged' | 1094 value: function() { |
1636 }, | 1095 return this; |
1637 | 1096 } |
1638 /** | 1097 }, |
1639 * True if this element is currently notifying its descedant elements of | 1098 stopKeyboardEventPropagation: { |
1640 * resize. | |
1641 */ | |
1642 _notifyingDescendant: { | |
1643 type: Boolean, | 1099 type: Boolean, |
1644 value: false | 1100 value: false |
1645 } | 1101 }, |
1646 }, | 1102 _boundKeyHandlers: { |
1647 | 1103 type: Array, |
1648 listeners: { | 1104 value: function() { |
1649 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | 1105 return []; |
1650 }, | 1106 } |
1651 | 1107 }, |
1652 created: function() { | 1108 _imperativeKeyBindings: { |
1653 // We don't really need property effects on these, and also we want them | 1109 type: Object, |
1654 // to be created before the `_parentResizable` observer fires: | 1110 value: function() { |
1655 this._interestedResizables = []; | 1111 return {}; |
1656 this._boundNotifyResize = this.notifyResize.bind(this); | 1112 } |
1657 }, | 1113 } |
1658 | 1114 }, |
| 1115 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], |
| 1116 keyBindings: {}, |
| 1117 registered: function() { |
| 1118 this._prepKeyBindings(); |
| 1119 }, |
1659 attached: function() { | 1120 attached: function() { |
1660 this.fire('iron-request-resize-notifications', null, { | 1121 this._listenKeyEventListeners(); |
1661 node: this, | 1122 }, |
1662 bubbles: true, | 1123 detached: function() { |
| 1124 this._unlistenKeyEventListeners(); |
| 1125 }, |
| 1126 addOwnKeyBinding: function(eventString, handlerName) { |
| 1127 this._imperativeKeyBindings[eventString] = handlerName; |
| 1128 this._prepKeyBindings(); |
| 1129 this._resetKeyEventListeners(); |
| 1130 }, |
| 1131 removeOwnKeyBindings: function() { |
| 1132 this._imperativeKeyBindings = {}; |
| 1133 this._prepKeyBindings(); |
| 1134 this._resetKeyEventListeners(); |
| 1135 }, |
| 1136 keyboardEventMatchesKeys: function(event, eventString) { |
| 1137 var keyCombos = parseEventString(eventString); |
| 1138 for (var i = 0; i < keyCombos.length; ++i) { |
| 1139 if (keyComboMatchesEvent(keyCombos[i], event)) { |
| 1140 return true; |
| 1141 } |
| 1142 } |
| 1143 return false; |
| 1144 }, |
| 1145 _collectKeyBindings: function() { |
| 1146 var keyBindings = this.behaviors.map(function(behavior) { |
| 1147 return behavior.keyBindings; |
| 1148 }); |
| 1149 if (keyBindings.indexOf(this.keyBindings) === -1) { |
| 1150 keyBindings.push(this.keyBindings); |
| 1151 } |
| 1152 return keyBindings; |
| 1153 }, |
| 1154 _prepKeyBindings: function() { |
| 1155 this._keyBindings = {}; |
| 1156 this._collectKeyBindings().forEach(function(keyBindings) { |
| 1157 for (var eventString in keyBindings) { |
| 1158 this._addKeyBinding(eventString, keyBindings[eventString]); |
| 1159 } |
| 1160 }, this); |
| 1161 for (var eventString in this._imperativeKeyBindings) { |
| 1162 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString
]); |
| 1163 } |
| 1164 for (var eventName in this._keyBindings) { |
| 1165 this._keyBindings[eventName].sort(function(kb1, kb2) { |
| 1166 var b1 = kb1[0].hasModifiers; |
| 1167 var b2 = kb2[0].hasModifiers; |
| 1168 return b1 === b2 ? 0 : b1 ? -1 : 1; |
| 1169 }); |
| 1170 } |
| 1171 }, |
| 1172 _addKeyBinding: function(eventString, handlerName) { |
| 1173 parseEventString(eventString).forEach(function(keyCombo) { |
| 1174 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] ||
[]; |
| 1175 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); |
| 1176 }, this); |
| 1177 }, |
| 1178 _resetKeyEventListeners: function() { |
| 1179 this._unlistenKeyEventListeners(); |
| 1180 if (this.isAttached) { |
| 1181 this._listenKeyEventListeners(); |
| 1182 } |
| 1183 }, |
| 1184 _listenKeyEventListeners: function() { |
| 1185 if (!this.keyEventTarget) { |
| 1186 return; |
| 1187 } |
| 1188 Object.keys(this._keyBindings).forEach(function(eventName) { |
| 1189 var keyBindings = this._keyBindings[eventName]; |
| 1190 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
| 1191 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa
ndler ]); |
| 1192 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| 1193 }, this); |
| 1194 }, |
| 1195 _unlistenKeyEventListeners: function() { |
| 1196 var keyHandlerTuple; |
| 1197 var keyEventTarget; |
| 1198 var eventName; |
| 1199 var boundKeyHandler; |
| 1200 while (this._boundKeyHandlers.length) { |
| 1201 keyHandlerTuple = this._boundKeyHandlers.pop(); |
| 1202 keyEventTarget = keyHandlerTuple[0]; |
| 1203 eventName = keyHandlerTuple[1]; |
| 1204 boundKeyHandler = keyHandlerTuple[2]; |
| 1205 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| 1206 } |
| 1207 }, |
| 1208 _onKeyBindingEvent: function(keyBindings, event) { |
| 1209 if (this.stopKeyboardEventPropagation) { |
| 1210 event.stopPropagation(); |
| 1211 } |
| 1212 if (event.defaultPrevented) { |
| 1213 return; |
| 1214 } |
| 1215 for (var i = 0; i < keyBindings.length; i++) { |
| 1216 var keyCombo = keyBindings[i][0]; |
| 1217 var handlerName = keyBindings[i][1]; |
| 1218 if (keyComboMatchesEvent(keyCombo, event)) { |
| 1219 this._triggerKeyHandler(keyCombo, handlerName, event); |
| 1220 if (event.defaultPrevented) { |
| 1221 return; |
| 1222 } |
| 1223 } |
| 1224 } |
| 1225 }, |
| 1226 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 1227 var detail = Object.create(keyCombo); |
| 1228 detail.keyboardEvent = keyboardEvent; |
| 1229 var event = new CustomEvent(keyCombo.event, { |
| 1230 detail: detail, |
1663 cancelable: true | 1231 cancelable: true |
1664 }); | 1232 }); |
1665 | 1233 this[handlerName].call(this, event); |
1666 if (!this._parentResizable) { | 1234 if (event.defaultPrevented) { |
1667 window.addEventListener('resize', this._boundNotifyResize); | 1235 keyboardEvent.preventDefault(); |
1668 this.notifyResize(); | 1236 } |
1669 } | |
1670 }, | |
1671 | |
1672 detached: function() { | |
1673 if (this._parentResizable) { | |
1674 this._parentResizable.stopResizeNotificationsFor(this); | |
1675 } else { | |
1676 window.removeEventListener('resize', this._boundNotifyResize); | |
1677 } | |
1678 | |
1679 this._parentResizable = null; | |
1680 }, | |
1681 | |
1682 /** | |
1683 * Can be called to manually notify a resizable and its descendant | |
1684 * resizables of a resize change. | |
1685 */ | |
1686 notifyResize: function() { | |
1687 if (!this.isAttached) { | |
1688 return; | |
1689 } | |
1690 | |
1691 this._interestedResizables.forEach(function(resizable) { | |
1692 if (this.resizerShouldNotify(resizable)) { | |
1693 this._notifyDescendant(resizable); | |
1694 } | |
1695 }, this); | |
1696 | |
1697 this._fireResize(); | |
1698 }, | |
1699 | |
1700 /** | |
1701 * Used to assign the closest resizable ancestor to this resizable | |
1702 * if the ancestor detects a request for notifications. | |
1703 */ | |
1704 assignParentResizable: function(parentResizable) { | |
1705 this._parentResizable = parentResizable; | |
1706 }, | |
1707 | |
1708 /** | |
1709 * Used to remove a resizable descendant from the list of descendants | |
1710 * that should be notified of a resize change. | |
1711 */ | |
1712 stopResizeNotificationsFor: function(target) { | |
1713 var index = this._interestedResizables.indexOf(target); | |
1714 | |
1715 if (index > -1) { | |
1716 this._interestedResizables.splice(index, 1); | |
1717 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | |
1718 } | |
1719 }, | |
1720 | |
1721 /** | |
1722 * This method can be overridden to filter nested elements that should or | |
1723 * should not be notified by the current element. Return true if an element | |
1724 * should be notified, or false if it should not be notified. | |
1725 * | |
1726 * @param {HTMLElement} element A candidate descendant element that | |
1727 * implements `IronResizableBehavior`. | |
1728 * @return {boolean} True if the `element` should be notified of resize. | |
1729 */ | |
1730 resizerShouldNotify: function(element) { return true; }, | |
1731 | |
1732 _onDescendantIronResize: function(event) { | |
1733 if (this._notifyingDescendant) { | |
1734 event.stopPropagation(); | |
1735 return; | |
1736 } | |
1737 | |
1738 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
1739 // otherwise non-bubbling event "just work." We do it manually here for | |
1740 // the case where Polymer is not using shadow roots for whatever reason: | |
1741 if (!Polymer.Settings.useShadow) { | |
1742 this._fireResize(); | |
1743 } | |
1744 }, | |
1745 | |
1746 _fireResize: function() { | |
1747 this.fire('iron-resize', null, { | |
1748 node: this, | |
1749 bubbles: false | |
1750 }); | |
1751 }, | |
1752 | |
1753 _onIronRequestResizeNotifications: function(event) { | |
1754 var target = event.path ? event.path[0] : event.target; | |
1755 | |
1756 if (target === this) { | |
1757 return; | |
1758 } | |
1759 | |
1760 if (this._interestedResizables.indexOf(target) === -1) { | |
1761 this._interestedResizables.push(target); | |
1762 this.listen(target, 'iron-resize', '_onDescendantIronResize'); | |
1763 } | |
1764 | |
1765 target.assignParentResizable(this); | |
1766 this._notifyDescendant(target); | |
1767 | |
1768 event.stopPropagation(); | |
1769 }, | |
1770 | |
1771 _parentResizableChanged: function(parentResizable) { | |
1772 if (parentResizable) { | |
1773 window.removeEventListener('resize', this._boundNotifyResize); | |
1774 } | |
1775 }, | |
1776 | |
1777 _notifyDescendant: function(descendant) { | |
1778 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
1779 // important not to notify them if the parent is not attached yet (or | |
1780 // else they will get redundantly notified when the parent attaches). | |
1781 if (!this.isAttached) { | |
1782 return; | |
1783 } | |
1784 | |
1785 this._notifyingDescendant = true; | |
1786 descendant.notifyResize(); | |
1787 this._notifyingDescendant = false; | |
1788 } | 1237 } |
1789 }; | 1238 }; |
| 1239 })(); |
| 1240 |
| 1241 Polymer.IronScrollTargetBehavior = { |
| 1242 properties: { |
| 1243 scrollTarget: { |
| 1244 type: HTMLElement, |
| 1245 value: function() { |
| 1246 return this._defaultScrollTarget; |
| 1247 } |
| 1248 } |
| 1249 }, |
| 1250 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ], |
| 1251 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 1252 var eventTarget; |
| 1253 if (this._oldScrollTarget) { |
| 1254 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro
llTarget; |
| 1255 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 1256 this._oldScrollTarget = null; |
| 1257 } |
| 1258 if (!isAttached) { |
| 1259 return; |
| 1260 } |
| 1261 if (scrollTarget === 'document') { |
| 1262 this.scrollTarget = this._doc; |
| 1263 } else if (typeof scrollTarget === 'string') { |
| 1264 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer.
dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 1265 } else if (this._isValidScrollTarget()) { |
| 1266 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 1267 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler
.bind(this); |
| 1268 this._oldScrollTarget = scrollTarget; |
| 1269 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 1270 } |
| 1271 }, |
| 1272 _scrollHandler: function scrollHandler() {}, |
| 1273 get _defaultScrollTarget() { |
| 1274 return this._doc; |
| 1275 }, |
| 1276 get _doc() { |
| 1277 return this.ownerDocument.documentElement; |
| 1278 }, |
| 1279 get _scrollTop() { |
| 1280 if (this._isValidScrollTarget()) { |
| 1281 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT
arget.scrollTop; |
| 1282 } |
| 1283 return 0; |
| 1284 }, |
| 1285 get _scrollLeft() { |
| 1286 if (this._isValidScrollTarget()) { |
| 1287 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT
arget.scrollLeft; |
| 1288 } |
| 1289 return 0; |
| 1290 }, |
| 1291 set _scrollTop(top) { |
| 1292 if (this.scrollTarget === this._doc) { |
| 1293 window.scrollTo(window.pageXOffset, top); |
| 1294 } else if (this._isValidScrollTarget()) { |
| 1295 this.scrollTarget.scrollTop = top; |
| 1296 } |
| 1297 }, |
| 1298 set _scrollLeft(left) { |
| 1299 if (this.scrollTarget === this._doc) { |
| 1300 window.scrollTo(left, window.pageYOffset); |
| 1301 } else if (this._isValidScrollTarget()) { |
| 1302 this.scrollTarget.scrollLeft = left; |
| 1303 } |
| 1304 }, |
| 1305 scroll: function(left, top) { |
| 1306 if (this.scrollTarget === this._doc) { |
| 1307 window.scrollTo(left, top); |
| 1308 } else if (this._isValidScrollTarget()) { |
| 1309 this.scrollTarget.scrollLeft = left; |
| 1310 this.scrollTarget.scrollTop = top; |
| 1311 } |
| 1312 }, |
| 1313 get _scrollTargetWidth() { |
| 1314 if (this._isValidScrollTarget()) { |
| 1315 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa
rget.offsetWidth; |
| 1316 } |
| 1317 return 0; |
| 1318 }, |
| 1319 get _scrollTargetHeight() { |
| 1320 if (this._isValidScrollTarget()) { |
| 1321 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT
arget.offsetHeight; |
| 1322 } |
| 1323 return 0; |
| 1324 }, |
| 1325 _isValidScrollTarget: function() { |
| 1326 return this.scrollTarget instanceof HTMLElement; |
| 1327 } |
| 1328 }; |
| 1329 |
1790 (function() { | 1330 (function() { |
1791 'use strict'; | |
1792 | |
1793 /** | |
1794 * Chrome uses an older version of DOM Level 3 Keyboard Events | |
1795 * | |
1796 * Most keys are labeled as text, but some are Unicode codepoints. | |
1797 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | |
1798 */ | |
1799 var KEY_IDENTIFIER = { | |
1800 'U+0008': 'backspace', | |
1801 'U+0009': 'tab', | |
1802 'U+001B': 'esc', | |
1803 'U+0020': 'space', | |
1804 'U+007F': 'del' | |
1805 }; | |
1806 | |
1807 /** | |
1808 * Special table for KeyboardEvent.keyCode. | |
1809 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | |
1810 * than that. | |
1811 * | |
1812 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
1813 */ | |
1814 var KEY_CODE = { | |
1815 8: 'backspace', | |
1816 9: 'tab', | |
1817 13: 'enter', | |
1818 27: 'esc', | |
1819 33: 'pageup', | |
1820 34: 'pagedown', | |
1821 35: 'end', | |
1822 36: 'home', | |
1823 32: 'space', | |
1824 37: 'left', | |
1825 38: 'up', | |
1826 39: 'right', | |
1827 40: 'down', | |
1828 46: 'del', | |
1829 106: '*' | |
1830 }; | |
1831 | |
1832 /** | |
1833 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
1834 * combo string to the property name that references those same keys | |
1835 * in a KeyboardEvent instance. | |
1836 */ | |
1837 var MODIFIER_KEYS = { | |
1838 'shift': 'shiftKey', | |
1839 'ctrl': 'ctrlKey', | |
1840 'alt': 'altKey', | |
1841 'meta': 'metaKey' | |
1842 }; | |
1843 | |
1844 /** | |
1845 * KeyboardEvent.key is mostly represented by printable character made by | |
1846 * the keyboard, with unprintable keys labeled nicely. | |
1847 * | |
1848 * However, on OS X, Alt+char can make a Unicode character that follows an | |
1849 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
1850 */ | |
1851 var KEY_CHAR = /[a-z0-9*]/; | |
1852 | |
1853 /** | |
1854 * Matches a keyIdentifier string. | |
1855 */ | |
1856 var IDENT_CHAR = /U\+/; | |
1857 | |
1858 /** | |
1859 * Matches arrow keys in Gecko 27.0+ | |
1860 */ | |
1861 var ARROW_KEY = /^arrow/; | |
1862 | |
1863 /** | |
1864 * Matches space keys everywhere (notably including IE10's exceptional name | |
1865 * `spacebar`). | |
1866 */ | |
1867 var SPACE_KEY = /^space(bar)?/; | |
1868 | |
1869 /** | |
1870 * Matches ESC key. | |
1871 * | |
1872 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
1873 */ | |
1874 var ESC_KEY = /^escape$/; | |
1875 | |
1876 /** | |
1877 * Transforms the key. | |
1878 * @param {string} key The KeyBoardEvent.key | |
1879 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
1880 * alpha-numeric characters. | |
1881 */ | |
1882 function transformKey(key, noSpecialChars) { | |
1883 var validKey = ''; | |
1884 if (key) { | |
1885 var lKey = key.toLowerCase(); | |
1886 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
1887 validKey = 'space'; | |
1888 } else if (ESC_KEY.test(lKey)) { | |
1889 validKey = 'esc'; | |
1890 } else if (lKey.length == 1) { | |
1891 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
1892 validKey = lKey; | |
1893 } | |
1894 } else if (ARROW_KEY.test(lKey)) { | |
1895 validKey = lKey.replace('arrow', ''); | |
1896 } else if (lKey == 'multiply') { | |
1897 // numpad '*' can map to Multiply on IE/Windows | |
1898 validKey = '*'; | |
1899 } else { | |
1900 validKey = lKey; | |
1901 } | |
1902 } | |
1903 return validKey; | |
1904 } | |
1905 | |
1906 function transformKeyIdentifier(keyIdent) { | |
1907 var validKey = ''; | |
1908 if (keyIdent) { | |
1909 if (keyIdent in KEY_IDENTIFIER) { | |
1910 validKey = KEY_IDENTIFIER[keyIdent]; | |
1911 } else if (IDENT_CHAR.test(keyIdent)) { | |
1912 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
1913 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
1914 } else { | |
1915 validKey = keyIdent.toLowerCase(); | |
1916 } | |
1917 } | |
1918 return validKey; | |
1919 } | |
1920 | |
1921 function transformKeyCode(keyCode) { | |
1922 var validKey = ''; | |
1923 if (Number(keyCode)) { | |
1924 if (keyCode >= 65 && keyCode <= 90) { | |
1925 // ascii a-z | |
1926 // lowercase is 32 offset from uppercase | |
1927 validKey = String.fromCharCode(32 + keyCode); | |
1928 } else if (keyCode >= 112 && keyCode <= 123) { | |
1929 // function keys f1-f12 | |
1930 validKey = 'f' + (keyCode - 112); | |
1931 } else if (keyCode >= 48 && keyCode <= 57) { | |
1932 // top 0-9 keys | |
1933 validKey = String(keyCode - 48); | |
1934 } else if (keyCode >= 96 && keyCode <= 105) { | |
1935 // num pad 0-9 | |
1936 validKey = String(keyCode - 96); | |
1937 } else { | |
1938 validKey = KEY_CODE[keyCode]; | |
1939 } | |
1940 } | |
1941 return validKey; | |
1942 } | |
1943 | |
1944 /** | |
1945 * Calculates the normalized key for a KeyboardEvent. | |
1946 * @param {KeyboardEvent} keyEvent | |
1947 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
1948 * transformation to alpha-numeric chars. This is useful with key | |
1949 * combinations like shift + 2, which on FF for MacOS produces | |
1950 * keyEvent.key = @ | |
1951 * To get 2 returned, set noSpecialChars = true | |
1952 * To get @ returned, set noSpecialChars = false | |
1953 */ | |
1954 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
1955 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
1956 // .detail.key to support artificial keyboard events. | |
1957 return transformKey(keyEvent.key, noSpecialChars) || | |
1958 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
1959 transformKeyCode(keyEvent.keyCode) || | |
1960 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | |
1961 } | |
1962 | |
1963 function keyComboMatchesEvent(keyCombo, event) { | |
1964 // For combos with modifiers we support only alpha-numeric keys | |
1965 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
1966 return keyEvent === keyCombo.key && | |
1967 (!keyCombo.hasModifiers || ( | |
1968 !!event.shiftKey === !!keyCombo.shiftKey && | |
1969 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
1970 !!event.altKey === !!keyCombo.altKey && | |
1971 !!event.metaKey === !!keyCombo.metaKey) | |
1972 ); | |
1973 } | |
1974 | |
1975 function parseKeyComboString(keyComboString) { | |
1976 if (keyComboString.length === 1) { | |
1977 return { | |
1978 combo: keyComboString, | |
1979 key: keyComboString, | |
1980 event: 'keydown' | |
1981 }; | |
1982 } | |
1983 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
1984 var eventParts = keyComboPart.split(':'); | |
1985 var keyName = eventParts[0]; | |
1986 var event = eventParts[1]; | |
1987 | |
1988 if (keyName in MODIFIER_KEYS) { | |
1989 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
1990 parsedKeyCombo.hasModifiers = true; | |
1991 } else { | |
1992 parsedKeyCombo.key = keyName; | |
1993 parsedKeyCombo.event = event || 'keydown'; | |
1994 } | |
1995 | |
1996 return parsedKeyCombo; | |
1997 }, { | |
1998 combo: keyComboString.split(':').shift() | |
1999 }); | |
2000 } | |
2001 | |
2002 function parseEventString(eventString) { | |
2003 return eventString.trim().split(' ').map(function(keyComboString) { | |
2004 return parseKeyComboString(keyComboString); | |
2005 }); | |
2006 } | |
2007 | |
2008 /** | |
2009 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
2010 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
2011 * The element takes care of browser differences with respect to Keyboard ev
ents | |
2012 * and uses an expressive syntax to filter key presses. | |
2013 * | |
2014 * Use the `keyBindings` prototype property to express what combination of k
eys | |
2015 * will trigger the callback. A key binding has the format | |
2016 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
2017 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
2018 * | |
2019 * keyBindings: { | |
2020 * 'space': '_onKeydown', // same as 'space:keydown' | |
2021 * 'shift+tab': '_onKeydown', | |
2022 * 'enter:keypress': '_onKeypress', | |
2023 * 'esc:keyup': '_onKeyup' | |
2024 * } | |
2025 * | |
2026 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
2027 * | |
2028 * _onKeydown: function(event) { | |
2029 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
2030 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
2031 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
2032 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
2033 * } | |
2034 * | |
2035 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
2036 * node. | |
2037 * | |
2038 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
2039 * for an example. | |
2040 * | |
2041 * @demo demo/index.html | |
2042 * @polymerBehavior | |
2043 */ | |
2044 Polymer.IronA11yKeysBehavior = { | |
2045 properties: { | |
2046 /** | |
2047 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
2048 * `null` to disable the listeners. | |
2049 * @type {?EventTarget} | |
2050 */ | |
2051 keyEventTarget: { | |
2052 type: Object, | |
2053 value: function() { | |
2054 return this; | |
2055 } | |
2056 }, | |
2057 | |
2058 /** | |
2059 * If true, this property will cause the implementing element to | |
2060 * automatically stop propagation on any handled KeyboardEvents. | |
2061 */ | |
2062 stopKeyboardEventPropagation: { | |
2063 type: Boolean, | |
2064 value: false | |
2065 }, | |
2066 | |
2067 _boundKeyHandlers: { | |
2068 type: Array, | |
2069 value: function() { | |
2070 return []; | |
2071 } | |
2072 }, | |
2073 | |
2074 // We use this due to a limitation in IE10 where instances will have | |
2075 // own properties of everything on the "prototype". | |
2076 _imperativeKeyBindings: { | |
2077 type: Object, | |
2078 value: function() { | |
2079 return {}; | |
2080 } | |
2081 } | |
2082 }, | |
2083 | |
2084 observers: [ | |
2085 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | |
2086 ], | |
2087 | |
2088 | |
2089 /** | |
2090 * To be used to express what combination of keys will trigger the relati
ve | |
2091 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
2092 * @type {Object} | |
2093 */ | |
2094 keyBindings: {}, | |
2095 | |
2096 registered: function() { | |
2097 this._prepKeyBindings(); | |
2098 }, | |
2099 | |
2100 attached: function() { | |
2101 this._listenKeyEventListeners(); | |
2102 }, | |
2103 | |
2104 detached: function() { | |
2105 this._unlistenKeyEventListeners(); | |
2106 }, | |
2107 | |
2108 /** | |
2109 * Can be used to imperatively add a key binding to the implementing | |
2110 * element. This is the imperative equivalent of declaring a keybinding | |
2111 * in the `keyBindings` prototype property. | |
2112 */ | |
2113 addOwnKeyBinding: function(eventString, handlerName) { | |
2114 this._imperativeKeyBindings[eventString] = handlerName; | |
2115 this._prepKeyBindings(); | |
2116 this._resetKeyEventListeners(); | |
2117 }, | |
2118 | |
2119 /** | |
2120 * When called, will remove all imperatively-added key bindings. | |
2121 */ | |
2122 removeOwnKeyBindings: function() { | |
2123 this._imperativeKeyBindings = {}; | |
2124 this._prepKeyBindings(); | |
2125 this._resetKeyEventListeners(); | |
2126 }, | |
2127 | |
2128 /** | |
2129 * Returns true if a keyboard event matches `eventString`. | |
2130 * | |
2131 * @param {KeyboardEvent} event | |
2132 * @param {string} eventString | |
2133 * @return {boolean} | |
2134 */ | |
2135 keyboardEventMatchesKeys: function(event, eventString) { | |
2136 var keyCombos = parseEventString(eventString); | |
2137 for (var i = 0; i < keyCombos.length; ++i) { | |
2138 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
2139 return true; | |
2140 } | |
2141 } | |
2142 return false; | |
2143 }, | |
2144 | |
2145 _collectKeyBindings: function() { | |
2146 var keyBindings = this.behaviors.map(function(behavior) { | |
2147 return behavior.keyBindings; | |
2148 }); | |
2149 | |
2150 if (keyBindings.indexOf(this.keyBindings) === -1) { | |
2151 keyBindings.push(this.keyBindings); | |
2152 } | |
2153 | |
2154 return keyBindings; | |
2155 }, | |
2156 | |
2157 _prepKeyBindings: function() { | |
2158 this._keyBindings = {}; | |
2159 | |
2160 this._collectKeyBindings().forEach(function(keyBindings) { | |
2161 for (var eventString in keyBindings) { | |
2162 this._addKeyBinding(eventString, keyBindings[eventString]); | |
2163 } | |
2164 }, this); | |
2165 | |
2166 for (var eventString in this._imperativeKeyBindings) { | |
2167 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
2168 } | |
2169 | |
2170 // Give precedence to combos with modifiers to be checked first. | |
2171 for (var eventName in this._keyBindings) { | |
2172 this._keyBindings[eventName].sort(function (kb1, kb2) { | |
2173 var b1 = kb1[0].hasModifiers; | |
2174 var b2 = kb2[0].hasModifiers; | |
2175 return (b1 === b2) ? 0 : b1 ? -1 : 1; | |
2176 }) | |
2177 } | |
2178 }, | |
2179 | |
2180 _addKeyBinding: function(eventString, handlerName) { | |
2181 parseEventString(eventString).forEach(function(keyCombo) { | |
2182 this._keyBindings[keyCombo.event] = | |
2183 this._keyBindings[keyCombo.event] || []; | |
2184 | |
2185 this._keyBindings[keyCombo.event].push([ | |
2186 keyCombo, | |
2187 handlerName | |
2188 ]); | |
2189 }, this); | |
2190 }, | |
2191 | |
2192 _resetKeyEventListeners: function() { | |
2193 this._unlistenKeyEventListeners(); | |
2194 | |
2195 if (this.isAttached) { | |
2196 this._listenKeyEventListeners(); | |
2197 } | |
2198 }, | |
2199 | |
2200 _listenKeyEventListeners: function() { | |
2201 if (!this.keyEventTarget) { | |
2202 return; | |
2203 } | |
2204 Object.keys(this._keyBindings).forEach(function(eventName) { | |
2205 var keyBindings = this._keyBindings[eventName]; | |
2206 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | |
2207 | |
2208 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | |
2209 | |
2210 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | |
2211 }, this); | |
2212 }, | |
2213 | |
2214 _unlistenKeyEventListeners: function() { | |
2215 var keyHandlerTuple; | |
2216 var keyEventTarget; | |
2217 var eventName; | |
2218 var boundKeyHandler; | |
2219 | |
2220 while (this._boundKeyHandlers.length) { | |
2221 // My kingdom for block-scope binding and destructuring assignment.. | |
2222 keyHandlerTuple = this._boundKeyHandlers.pop(); | |
2223 keyEventTarget = keyHandlerTuple[0]; | |
2224 eventName = keyHandlerTuple[1]; | |
2225 boundKeyHandler = keyHandlerTuple[2]; | |
2226 | |
2227 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | |
2228 } | |
2229 }, | |
2230 | |
2231 _onKeyBindingEvent: function(keyBindings, event) { | |
2232 if (this.stopKeyboardEventPropagation) { | |
2233 event.stopPropagation(); | |
2234 } | |
2235 | |
2236 // if event has been already prevented, don't do anything | |
2237 if (event.defaultPrevented) { | |
2238 return; | |
2239 } | |
2240 | |
2241 for (var i = 0; i < keyBindings.length; i++) { | |
2242 var keyCombo = keyBindings[i][0]; | |
2243 var handlerName = keyBindings[i][1]; | |
2244 if (keyComboMatchesEvent(keyCombo, event)) { | |
2245 this._triggerKeyHandler(keyCombo, handlerName, event); | |
2246 // exit the loop if eventDefault was prevented | |
2247 if (event.defaultPrevented) { | |
2248 return; | |
2249 } | |
2250 } | |
2251 } | |
2252 }, | |
2253 | |
2254 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | |
2255 var detail = Object.create(keyCombo); | |
2256 detail.keyboardEvent = keyboardEvent; | |
2257 var event = new CustomEvent(keyCombo.event, { | |
2258 detail: detail, | |
2259 cancelable: true | |
2260 }); | |
2261 this[handlerName].call(this, event); | |
2262 if (event.defaultPrevented) { | |
2263 keyboardEvent.preventDefault(); | |
2264 } | |
2265 } | |
2266 }; | |
2267 })(); | |
2268 /** | |
2269 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
2270 * designated scroll target. | |
2271 * | |
2272 * Elements that consume this behavior can override the `_scrollHandler` | |
2273 * method to add logic on the scroll event. | |
2274 * | |
2275 * @demo demo/scrolling-region.html Scrolling Region | |
2276 * @demo demo/document.html Document Element | |
2277 * @polymerBehavior | |
2278 */ | |
2279 Polymer.IronScrollTargetBehavior = { | |
2280 | |
2281 properties: { | |
2282 | |
2283 /** | |
2284 * Specifies the element that will handle the scroll event | |
2285 * on the behalf of the current element. This is typically a reference to
an element, | |
2286 * but there are a few more posibilities: | |
2287 * | |
2288 * ### Elements id | |
2289 * | |
2290 *```html | |
2291 * <div id="scrollable-element" style="overflow: auto;"> | |
2292 * <x-element scroll-target="scrollable-element"> | |
2293 * \x3c!-- Content--\x3e | |
2294 * </x-element> | |
2295 * </div> | |
2296 *``` | |
2297 * In this case, the `scrollTarget` will point to the outer div element. | |
2298 * | |
2299 * ### Document scrolling | |
2300 * | |
2301 * For document scrolling, you can use the reserved word `document`: | |
2302 * | |
2303 *```html | |
2304 * <x-element scroll-target="document"> | |
2305 * \x3c!-- Content --\x3e | |
2306 * </x-element> | |
2307 *``` | |
2308 * | |
2309 * ### Elements reference | |
2310 * | |
2311 *```js | |
2312 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
2313 *``` | |
2314 * | |
2315 * @type {HTMLElement} | |
2316 */ | |
2317 scrollTarget: { | |
2318 type: HTMLElement, | |
2319 value: function() { | |
2320 return this._defaultScrollTarget; | |
2321 } | |
2322 } | |
2323 }, | |
2324 | |
2325 observers: [ | |
2326 '_scrollTargetChanged(scrollTarget, isAttached)' | |
2327 ], | |
2328 | |
2329 _scrollTargetChanged: function(scrollTarget, isAttached) { | |
2330 var eventTarget; | |
2331 | |
2332 if (this._oldScrollTarget) { | |
2333 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | |
2334 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | |
2335 this._oldScrollTarget = null; | |
2336 } | |
2337 | |
2338 if (!isAttached) { | |
2339 return; | |
2340 } | |
2341 // Support element id references | |
2342 if (scrollTarget === 'document') { | |
2343 | |
2344 this.scrollTarget = this._doc; | |
2345 | |
2346 } else if (typeof scrollTarget === 'string') { | |
2347 | |
2348 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | |
2349 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | |
2350 | |
2351 } else if (this._isValidScrollTarget()) { | |
2352 | |
2353 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | |
2354 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | |
2355 this._oldScrollTarget = scrollTarget; | |
2356 | |
2357 eventTarget.addEventListener('scroll', this._boundScrollHandler); | |
2358 } | |
2359 }, | |
2360 | |
2361 /** | |
2362 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
2363 * | |
2364 * @protected | |
2365 */ | |
2366 _scrollHandler: function scrollHandler() {}, | |
2367 | |
2368 /** | |
2369 * The default scroll target. Consumers of this behavior may want to customi
ze | |
2370 * the default scroll target. | |
2371 * | |
2372 * @type {Element} | |
2373 */ | |
2374 get _defaultScrollTarget() { | |
2375 return this._doc; | |
2376 }, | |
2377 | |
2378 /** | |
2379 * Shortcut for the document element | |
2380 * | |
2381 * @type {Element} | |
2382 */ | |
2383 get _doc() { | |
2384 return this.ownerDocument.documentElement; | |
2385 }, | |
2386 | |
2387 /** | |
2388 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
2389 * | |
2390 * @type {number} | |
2391 */ | |
2392 get _scrollTop() { | |
2393 if (this._isValidScrollTarget()) { | |
2394 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
2395 } | |
2396 return 0; | |
2397 }, | |
2398 | |
2399 /** | |
2400 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
2401 * | |
2402 * @type {number} | |
2403 */ | |
2404 get _scrollLeft() { | |
2405 if (this._isValidScrollTarget()) { | |
2406 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
2407 } | |
2408 return 0; | |
2409 }, | |
2410 | |
2411 /** | |
2412 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
2413 * | |
2414 * @type {number} | |
2415 */ | |
2416 set _scrollTop(top) { | |
2417 if (this.scrollTarget === this._doc) { | |
2418 window.scrollTo(window.pageXOffset, top); | |
2419 } else if (this._isValidScrollTarget()) { | |
2420 this.scrollTarget.scrollTop = top; | |
2421 } | |
2422 }, | |
2423 | |
2424 /** | |
2425 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
2426 * | |
2427 * @type {number} | |
2428 */ | |
2429 set _scrollLeft(left) { | |
2430 if (this.scrollTarget === this._doc) { | |
2431 window.scrollTo(left, window.pageYOffset); | |
2432 } else if (this._isValidScrollTarget()) { | |
2433 this.scrollTarget.scrollLeft = left; | |
2434 } | |
2435 }, | |
2436 | |
2437 /** | |
2438 * Scrolls the content to a particular place. | |
2439 * | |
2440 * @method scroll | |
2441 * @param {number} left The left position | |
2442 * @param {number} top The top position | |
2443 */ | |
2444 scroll: function(left, top) { | |
2445 if (this.scrollTarget === this._doc) { | |
2446 window.scrollTo(left, top); | |
2447 } else if (this._isValidScrollTarget()) { | |
2448 this.scrollTarget.scrollLeft = left; | |
2449 this.scrollTarget.scrollTop = top; | |
2450 } | |
2451 }, | |
2452 | |
2453 /** | |
2454 * Gets the width of the scroll target. | |
2455 * | |
2456 * @type {number} | |
2457 */ | |
2458 get _scrollTargetWidth() { | |
2459 if (this._isValidScrollTarget()) { | |
2460 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
2461 } | |
2462 return 0; | |
2463 }, | |
2464 | |
2465 /** | |
2466 * Gets the height of the scroll target. | |
2467 * | |
2468 * @type {number} | |
2469 */ | |
2470 get _scrollTargetHeight() { | |
2471 if (this._isValidScrollTarget()) { | |
2472 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
2473 } | |
2474 return 0; | |
2475 }, | |
2476 | |
2477 /** | |
2478 * Returns true if the scroll target is a valid HTMLElement. | |
2479 * | |
2480 * @return {boolean} | |
2481 */ | |
2482 _isValidScrollTarget: function() { | |
2483 return this.scrollTarget instanceof HTMLElement; | |
2484 } | |
2485 }; | |
2486 (function() { | |
2487 | |
2488 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 1331 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
2489 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 1332 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
2490 var DEFAULT_PHYSICAL_COUNT = 3; | 1333 var DEFAULT_PHYSICAL_COUNT = 3; |
2491 var HIDDEN_Y = '-10000px'; | 1334 var HIDDEN_Y = '-10000px'; |
2492 var DEFAULT_GRID_SIZE = 200; | 1335 var DEFAULT_GRID_SIZE = 200; |
2493 var SECRET_TABINDEX = -100; | 1336 var SECRET_TABINDEX = -100; |
2494 | |
2495 Polymer({ | 1337 Polymer({ |
2496 | |
2497 is: 'iron-list', | 1338 is: 'iron-list', |
2498 | |
2499 properties: { | 1339 properties: { |
2500 | |
2501 /** | |
2502 * An array containing items determining how many instances of the templat
e | |
2503 * to stamp and that that each template instance should bind to. | |
2504 */ | |
2505 items: { | 1340 items: { |
2506 type: Array | 1341 type: Array |
2507 }, | 1342 }, |
2508 | |
2509 /** | |
2510 * The max count of physical items the pool can extend to. | |
2511 */ | |
2512 maxPhysicalCount: { | 1343 maxPhysicalCount: { |
2513 type: Number, | 1344 type: Number, |
2514 value: 500 | 1345 value: 500 |
2515 }, | 1346 }, |
2516 | |
2517 /** | |
2518 * The name of the variable to add to the binding scope for the array | |
2519 * element associated with a given template instance. | |
2520 */ | |
2521 as: { | 1347 as: { |
2522 type: String, | 1348 type: String, |
2523 value: 'item' | 1349 value: 'item' |
2524 }, | 1350 }, |
2525 | |
2526 /** | |
2527 * The name of the variable to add to the binding scope with the index | |
2528 * for the row. | |
2529 */ | |
2530 indexAs: { | 1351 indexAs: { |
2531 type: String, | 1352 type: String, |
2532 value: 'index' | 1353 value: 'index' |
2533 }, | 1354 }, |
2534 | |
2535 /** | |
2536 * The name of the variable to add to the binding scope to indicate | |
2537 * if the row is selected. | |
2538 */ | |
2539 selectedAs: { | 1355 selectedAs: { |
2540 type: String, | 1356 type: String, |
2541 value: 'selected' | 1357 value: 'selected' |
2542 }, | 1358 }, |
2543 | |
2544 /** | |
2545 * When true, the list is rendered as a grid. Grid items must have | |
2546 * fixed width and height set via CSS. e.g. | |
2547 * | |
2548 * ```html | |
2549 * <iron-list grid> | |
2550 * <template> | |
2551 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
2552 * </template> | |
2553 * </iron-list> | |
2554 * ``` | |
2555 */ | |
2556 grid: { | 1359 grid: { |
2557 type: Boolean, | 1360 type: Boolean, |
2558 value: false, | 1361 value: false, |
2559 reflectToAttribute: true | 1362 reflectToAttribute: true |
2560 }, | 1363 }, |
2561 | |
2562 /** | |
2563 * When true, tapping a row will select the item, placing its data model | |
2564 * in the set of selected items retrievable via the selection property. | |
2565 * | |
2566 * Note that tapping focusable elements within the list item will not | |
2567 * result in selection, since they are presumed to have their * own action
. | |
2568 */ | |
2569 selectionEnabled: { | 1364 selectionEnabled: { |
2570 type: Boolean, | 1365 type: Boolean, |
2571 value: false | 1366 value: false |
2572 }, | 1367 }, |
2573 | |
2574 /** | |
2575 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
2576 * if no item is selected. | |
2577 */ | |
2578 selectedItem: { | 1368 selectedItem: { |
2579 type: Object, | 1369 type: Object, |
2580 notify: true | 1370 notify: true |
2581 }, | 1371 }, |
2582 | |
2583 /** | |
2584 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
2585 */ | |
2586 selectedItems: { | 1372 selectedItems: { |
2587 type: Object, | 1373 type: Object, |
2588 notify: true | 1374 notify: true |
2589 }, | 1375 }, |
2590 | |
2591 /** | |
2592 * When `true`, multiple items may be selected at once (in this case, | |
2593 * `selected` is an array of currently selected items). When `false`, | |
2594 * only one item may be selected at a time. | |
2595 */ | |
2596 multiSelection: { | 1376 multiSelection: { |
2597 type: Boolean, | 1377 type: Boolean, |
2598 value: false | 1378 value: false |
2599 } | 1379 } |
2600 }, | 1380 }, |
2601 | 1381 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn
abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)'
], |
2602 observers: [ | 1382 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro
nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ], |
2603 '_itemsChanged(items.*)', | |
2604 '_selectionEnabledChanged(selectionEnabled)', | |
2605 '_multiSelectionChanged(multiSelection)', | |
2606 '_setOverflow(scrollTarget)' | |
2607 ], | |
2608 | |
2609 behaviors: [ | |
2610 Polymer.Templatizer, | |
2611 Polymer.IronResizableBehavior, | |
2612 Polymer.IronA11yKeysBehavior, | |
2613 Polymer.IronScrollTargetBehavior | |
2614 ], | |
2615 | |
2616 keyBindings: { | 1383 keyBindings: { |
2617 'up': '_didMoveUp', | 1384 up: '_didMoveUp', |
2618 'down': '_didMoveDown', | 1385 down: '_didMoveDown', |
2619 'enter': '_didEnter' | 1386 enter: '_didEnter' |
2620 }, | 1387 }, |
2621 | 1388 _ratio: .5, |
2622 /** | |
2623 * The ratio of hidden tiles that should remain in the scroll direction. | |
2624 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
2625 */ | |
2626 _ratio: 0.5, | |
2627 | |
2628 /** | |
2629 * The padding-top value for the list. | |
2630 */ | |
2631 _scrollerPaddingTop: 0, | 1389 _scrollerPaddingTop: 0, |
2632 | |
2633 /** | |
2634 * This value is the same as `scrollTop`. | |
2635 */ | |
2636 _scrollPosition: 0, | 1390 _scrollPosition: 0, |
2637 | |
2638 /** | |
2639 * The sum of the heights of all the tiles in the DOM. | |
2640 */ | |
2641 _physicalSize: 0, | 1391 _physicalSize: 0, |
2642 | |
2643 /** | |
2644 * The average `offsetHeight` of the tiles observed till now. | |
2645 */ | |
2646 _physicalAverage: 0, | 1392 _physicalAverage: 0, |
2647 | |
2648 /** | |
2649 * The number of tiles which `offsetHeight` > 0 observed until now. | |
2650 */ | |
2651 _physicalAverageCount: 0, | 1393 _physicalAverageCount: 0, |
2652 | |
2653 /** | |
2654 * The Y position of the item rendered in the `_physicalStart` | |
2655 * tile relative to the scrolling list. | |
2656 */ | |
2657 _physicalTop: 0, | 1394 _physicalTop: 0, |
2658 | |
2659 /** | |
2660 * The number of items in the list. | |
2661 */ | |
2662 _virtualCount: 0, | 1395 _virtualCount: 0, |
2663 | |
2664 /** | |
2665 * A map between an item key and its physical item index | |
2666 */ | |
2667 _physicalIndexForKey: null, | 1396 _physicalIndexForKey: null, |
2668 | |
2669 /** | |
2670 * The estimated scroll height based on `_physicalAverage` | |
2671 */ | |
2672 _estScrollHeight: 0, | 1397 _estScrollHeight: 0, |
2673 | |
2674 /** | |
2675 * The scroll height of the dom node | |
2676 */ | |
2677 _scrollHeight: 0, | 1398 _scrollHeight: 0, |
2678 | |
2679 /** | |
2680 * The height of the list. This is referred as the viewport in the context o
f list. | |
2681 */ | |
2682 _viewportHeight: 0, | 1399 _viewportHeight: 0, |
2683 | |
2684 /** | |
2685 * The width of the list. This is referred as the viewport in the context of
list. | |
2686 */ | |
2687 _viewportWidth: 0, | 1400 _viewportWidth: 0, |
2688 | |
2689 /** | |
2690 * An array of DOM nodes that are currently in the tree | |
2691 * @type {?Array<!TemplatizerNode>} | |
2692 */ | |
2693 _physicalItems: null, | 1401 _physicalItems: null, |
2694 | |
2695 /** | |
2696 * An array of heights for each item in `_physicalItems` | |
2697 * @type {?Array<number>} | |
2698 */ | |
2699 _physicalSizes: null, | 1402 _physicalSizes: null, |
2700 | |
2701 /** | |
2702 * A cached value for the first visible index. | |
2703 * See `firstVisibleIndex` | |
2704 * @type {?number} | |
2705 */ | |
2706 _firstVisibleIndexVal: null, | 1403 _firstVisibleIndexVal: null, |
2707 | |
2708 /** | |
2709 * A cached value for the last visible index. | |
2710 * See `lastVisibleIndex` | |
2711 * @type {?number} | |
2712 */ | |
2713 _lastVisibleIndexVal: null, | 1404 _lastVisibleIndexVal: null, |
2714 | |
2715 /** | |
2716 * A Polymer collection for the items. | |
2717 * @type {?Polymer.Collection} | |
2718 */ | |
2719 _collection: null, | 1405 _collection: null, |
2720 | |
2721 /** | |
2722 * True if the current item list was rendered for the first time | |
2723 * after attached. | |
2724 */ | |
2725 _itemsRendered: false, | 1406 _itemsRendered: false, |
2726 | |
2727 /** | |
2728 * The page that is currently rendered. | |
2729 */ | |
2730 _lastPage: null, | 1407 _lastPage: null, |
2731 | |
2732 /** | |
2733 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
2734 */ | |
2735 _maxPages: 3, | 1408 _maxPages: 3, |
2736 | |
2737 /** | |
2738 * The currently focused physical item. | |
2739 */ | |
2740 _focusedItem: null, | 1409 _focusedItem: null, |
2741 | |
2742 /** | |
2743 * The index of the `_focusedItem`. | |
2744 */ | |
2745 _focusedIndex: -1, | 1410 _focusedIndex: -1, |
2746 | |
2747 /** | |
2748 * The the item that is focused if it is moved offscreen. | |
2749 * @private {?TemplatizerNode} | |
2750 */ | |
2751 _offscreenFocusedItem: null, | 1411 _offscreenFocusedItem: null, |
2752 | |
2753 /** | |
2754 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
2755 * list when that item is moved offscreen. | |
2756 */ | |
2757 _focusBackfillItem: null, | 1412 _focusBackfillItem: null, |
2758 | |
2759 /** | |
2760 * The maximum items per row | |
2761 */ | |
2762 _itemsPerRow: 1, | 1413 _itemsPerRow: 1, |
2763 | |
2764 /** | |
2765 * The width of each grid item | |
2766 */ | |
2767 _itemWidth: 0, | 1414 _itemWidth: 0, |
2768 | |
2769 /** | |
2770 * The height of the row in grid layout. | |
2771 */ | |
2772 _rowHeight: 0, | 1415 _rowHeight: 0, |
2773 | |
2774 /** | |
2775 * The bottom of the physical content. | |
2776 */ | |
2777 get _physicalBottom() { | 1416 get _physicalBottom() { |
2778 return this._physicalTop + this._physicalSize; | 1417 return this._physicalTop + this._physicalSize; |
2779 }, | 1418 }, |
2780 | |
2781 /** | |
2782 * The bottom of the scroll. | |
2783 */ | |
2784 get _scrollBottom() { | 1419 get _scrollBottom() { |
2785 return this._scrollPosition + this._viewportHeight; | 1420 return this._scrollPosition + this._viewportHeight; |
2786 }, | 1421 }, |
2787 | |
2788 /** | |
2789 * The n-th item rendered in the last physical item. | |
2790 */ | |
2791 get _virtualEnd() { | 1422 get _virtualEnd() { |
2792 return this._virtualStart + this._physicalCount - 1; | 1423 return this._virtualStart + this._physicalCount - 1; |
2793 }, | 1424 }, |
2794 | |
2795 /** | |
2796 * The height of the physical content that isn't on the screen. | |
2797 */ | |
2798 get _hiddenContentSize() { | 1425 get _hiddenContentSize() { |
2799 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 1426 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
2800 return size - this._viewportHeight; | 1427 return size - this._viewportHeight; |
2801 }, | 1428 }, |
2802 | |
2803 /** | |
2804 * The maximum scroll top value. | |
2805 */ | |
2806 get _maxScrollTop() { | 1429 get _maxScrollTop() { |
2807 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 1430 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
2808 }, | 1431 }, |
2809 | |
2810 /** | |
2811 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
2812 */ | |
2813 _minVirtualStart: 0, | 1432 _minVirtualStart: 0, |
2814 | |
2815 /** | |
2816 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
2817 */ | |
2818 get _maxVirtualStart() { | 1433 get _maxVirtualStart() { |
2819 return Math.max(0, this._virtualCount - this._physicalCount); | 1434 return Math.max(0, this._virtualCount - this._physicalCount); |
2820 }, | 1435 }, |
2821 | |
2822 /** | |
2823 * The n-th item rendered in the `_physicalStart` tile. | |
2824 */ | |
2825 _virtualStartVal: 0, | 1436 _virtualStartVal: 0, |
2826 | |
2827 set _virtualStart(val) { | 1437 set _virtualStart(val) { |
2828 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 1438 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
2829 }, | 1439 }, |
2830 | |
2831 get _virtualStart() { | 1440 get _virtualStart() { |
2832 return this._virtualStartVal || 0; | 1441 return this._virtualStartVal || 0; |
2833 }, | 1442 }, |
2834 | |
2835 /** | |
2836 * The k-th tile that is at the top of the scrolling list. | |
2837 */ | |
2838 _physicalStartVal: 0, | 1443 _physicalStartVal: 0, |
2839 | |
2840 set _physicalStart(val) { | 1444 set _physicalStart(val) { |
2841 this._physicalStartVal = val % this._physicalCount; | 1445 this._physicalStartVal = val % this._physicalCount; |
2842 if (this._physicalStartVal < 0) { | 1446 if (this._physicalStartVal < 0) { |
2843 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 1447 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
2844 } | 1448 } |
2845 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1449 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2846 }, | 1450 }, |
2847 | |
2848 get _physicalStart() { | 1451 get _physicalStart() { |
2849 return this._physicalStartVal || 0; | 1452 return this._physicalStartVal || 0; |
2850 }, | 1453 }, |
2851 | |
2852 /** | |
2853 * The number of tiles in the DOM. | |
2854 */ | |
2855 _physicalCountVal: 0, | 1454 _physicalCountVal: 0, |
2856 | |
2857 set _physicalCount(val) { | 1455 set _physicalCount(val) { |
2858 this._physicalCountVal = val; | 1456 this._physicalCountVal = val; |
2859 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1457 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2860 }, | 1458 }, |
2861 | |
2862 get _physicalCount() { | 1459 get _physicalCount() { |
2863 return this._physicalCountVal; | 1460 return this._physicalCountVal; |
2864 }, | 1461 }, |
2865 | |
2866 /** | |
2867 * The k-th tile that is at the bottom of the scrolling list. | |
2868 */ | |
2869 _physicalEnd: 0, | 1462 _physicalEnd: 0, |
2870 | |
2871 /** | |
2872 * An optimal physical size such that we will have enough physical items | |
2873 * to fill up the viewport and recycle when the user scrolls. | |
2874 * | |
2875 * This default value assumes that we will at least have the equivalent | |
2876 * to a viewport of physical items above and below the user's viewport. | |
2877 */ | |
2878 get _optPhysicalSize() { | 1463 get _optPhysicalSize() { |
2879 if (this.grid) { | 1464 if (this.grid) { |
2880 return this._estRowsInView * this._rowHeight * this._maxPages; | 1465 return this._estRowsInView * this._rowHeight * this._maxPages; |
2881 } | 1466 } |
2882 return this._viewportHeight * this._maxPages; | 1467 return this._viewportHeight * this._maxPages; |
2883 }, | 1468 }, |
2884 | |
2885 get _optPhysicalCount() { | 1469 get _optPhysicalCount() { |
2886 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 1470 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
2887 }, | 1471 }, |
2888 | |
2889 /** | |
2890 * True if the current list is visible. | |
2891 */ | |
2892 get _isVisible() { | 1472 get _isVisible() { |
2893 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 1473 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
2894 }, | 1474 }, |
2895 | |
2896 /** | |
2897 * Gets the index of the first visible item in the viewport. | |
2898 * | |
2899 * @type {number} | |
2900 */ | |
2901 get firstVisibleIndex() { | 1475 get firstVisibleIndex() { |
2902 if (this._firstVisibleIndexVal === null) { | 1476 if (this._firstVisibleIndexVal === null) { |
2903 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 1477 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
2904 | 1478 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) { |
2905 this._firstVisibleIndexVal = this._iterateItems( | 1479 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2906 function(pidx, vidx) { | 1480 if (physicalOffset > this._scrollPosition) { |
2907 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1481 return this.grid ? vidx - vidx % this._itemsPerRow : vidx; |
2908 | 1482 } |
2909 if (physicalOffset > this._scrollPosition) { | 1483 if (this.grid && this._virtualCount - 1 === vidx) { |
2910 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 1484 return vidx - vidx % this._itemsPerRow; |
2911 } | 1485 } |
2912 // Handle a partially rendered final row in grid mode | 1486 }) || 0; |
2913 if (this.grid && this._virtualCount - 1 === vidx) { | |
2914 return vidx - (vidx % this._itemsPerRow); | |
2915 } | |
2916 }) || 0; | |
2917 } | 1487 } |
2918 return this._firstVisibleIndexVal; | 1488 return this._firstVisibleIndexVal; |
2919 }, | 1489 }, |
2920 | |
2921 /** | |
2922 * Gets the index of the last visible item in the viewport. | |
2923 * | |
2924 * @type {number} | |
2925 */ | |
2926 get lastVisibleIndex() { | 1490 get lastVisibleIndex() { |
2927 if (this._lastVisibleIndexVal === null) { | 1491 if (this._lastVisibleIndexVal === null) { |
2928 if (this.grid) { | 1492 if (this.grid) { |
2929 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 1493 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
2930 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 1494 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
2931 } else { | 1495 } else { |
2932 var physicalOffset = this._physicalTop; | 1496 var physicalOffset = this._physicalTop; |
2933 this._iterateItems(function(pidx, vidx) { | 1497 this._iterateItems(function(pidx, vidx) { |
2934 if (physicalOffset < this._scrollBottom) { | 1498 if (physicalOffset < this._scrollBottom) { |
2935 this._lastVisibleIndexVal = vidx; | 1499 this._lastVisibleIndexVal = vidx; |
2936 } else { | 1500 } else { |
2937 // Break _iterateItems | |
2938 return true; | 1501 return true; |
2939 } | 1502 } |
2940 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1503 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2941 }); | 1504 }); |
2942 } | 1505 } |
2943 } | 1506 } |
2944 return this._lastVisibleIndexVal; | 1507 return this._lastVisibleIndexVal; |
2945 }, | 1508 }, |
2946 | |
2947 get _defaultScrollTarget() { | 1509 get _defaultScrollTarget() { |
2948 return this; | 1510 return this; |
2949 }, | 1511 }, |
2950 get _virtualRowCount() { | 1512 get _virtualRowCount() { |
2951 return Math.ceil(this._virtualCount / this._itemsPerRow); | 1513 return Math.ceil(this._virtualCount / this._itemsPerRow); |
2952 }, | 1514 }, |
2953 | |
2954 get _estRowsInView() { | 1515 get _estRowsInView() { |
2955 return Math.ceil(this._viewportHeight / this._rowHeight); | 1516 return Math.ceil(this._viewportHeight / this._rowHeight); |
2956 }, | 1517 }, |
2957 | |
2958 get _physicalRows() { | 1518 get _physicalRows() { |
2959 return Math.ceil(this._physicalCount / this._itemsPerRow); | 1519 return Math.ceil(this._physicalCount / this._itemsPerRow); |
2960 }, | 1520 }, |
2961 | |
2962 ready: function() { | 1521 ready: function() { |
2963 this.addEventListener('focus', this._didFocus.bind(this), true); | 1522 this.addEventListener('focus', this._didFocus.bind(this), true); |
2964 }, | 1523 }, |
2965 | |
2966 attached: function() { | 1524 attached: function() { |
2967 this.updateViewportBoundaries(); | 1525 this.updateViewportBoundaries(); |
2968 this._render(); | 1526 this._render(); |
2969 // `iron-resize` is fired when the list is attached if the event is added | |
2970 // before attached causing unnecessary work. | |
2971 this.listen(this, 'iron-resize', '_resizeHandler'); | 1527 this.listen(this, 'iron-resize', '_resizeHandler'); |
2972 }, | 1528 }, |
2973 | |
2974 detached: function() { | 1529 detached: function() { |
2975 this._itemsRendered = false; | 1530 this._itemsRendered = false; |
2976 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 1531 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
2977 }, | 1532 }, |
2978 | |
2979 /** | |
2980 * Set the overflow property if this element has its own scrolling region | |
2981 */ | |
2982 _setOverflow: function(scrollTarget) { | 1533 _setOverflow: function(scrollTarget) { |
2983 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 1534 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
2984 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 1535 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
2985 }, | 1536 }, |
2986 | |
2987 /** | |
2988 * Invoke this method if you dynamically update the viewport's | |
2989 * size or CSS padding. | |
2990 * | |
2991 * @method updateViewportBoundaries | |
2992 */ | |
2993 updateViewportBoundaries: function() { | 1537 updateViewportBoundaries: function() { |
2994 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 1538 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo
w.getComputedStyle(this)['padding-top'], 10); |
2995 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
2996 | |
2997 this._viewportHeight = this._scrollTargetHeight; | 1539 this._viewportHeight = this._scrollTargetHeight; |
2998 if (this.grid) { | 1540 if (this.grid) { |
2999 this._updateGridMetrics(); | 1541 this._updateGridMetrics(); |
3000 } | 1542 } |
3001 }, | 1543 }, |
3002 | |
3003 /** | |
3004 * Update the models, the position of the | |
3005 * items in the viewport and recycle tiles as needed. | |
3006 */ | |
3007 _scrollHandler: function() { | 1544 _scrollHandler: function() { |
3008 // clamp the `scrollTop` value | |
3009 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 1545 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
3010 var delta = scrollTop - this._scrollPosition; | 1546 var delta = scrollTop - this._scrollPosition; |
3011 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 1547 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
3012 var ratio = this._ratio; | 1548 var ratio = this._ratio; |
3013 var recycledTiles = 0; | 1549 var recycledTiles = 0; |
3014 var hiddenContentSize = this._hiddenContentSize; | 1550 var hiddenContentSize = this._hiddenContentSize; |
3015 var currentRatio = ratio; | 1551 var currentRatio = ratio; |
3016 var movingUp = []; | 1552 var movingUp = []; |
3017 | |
3018 // track the last `scrollTop` | |
3019 this._scrollPosition = scrollTop; | 1553 this._scrollPosition = scrollTop; |
3020 | |
3021 // clear cached visible indexes | |
3022 this._firstVisibleIndexVal = null; | 1554 this._firstVisibleIndexVal = null; |
3023 this._lastVisibleIndexVal = null; | 1555 this._lastVisibleIndexVal = null; |
3024 | |
3025 scrollBottom = this._scrollBottom; | 1556 scrollBottom = this._scrollBottom; |
3026 physicalBottom = this._physicalBottom; | 1557 physicalBottom = this._physicalBottom; |
3027 | |
3028 // random access | |
3029 if (Math.abs(delta) > this._physicalSize) { | 1558 if (Math.abs(delta) > this._physicalSize) { |
3030 this._physicalTop += delta; | 1559 this._physicalTop += delta; |
3031 recycledTiles = Math.round(delta / this._physicalAverage); | 1560 recycledTiles = Math.round(delta / this._physicalAverage); |
3032 } | 1561 } else if (delta < 0) { |
3033 // scroll up | |
3034 else if (delta < 0) { | |
3035 var topSpace = scrollTop - this._physicalTop; | 1562 var topSpace = scrollTop - this._physicalTop; |
3036 var virtualStart = this._virtualStart; | 1563 var virtualStart = this._virtualStart; |
3037 | |
3038 recycledTileSet = []; | 1564 recycledTileSet = []; |
3039 | |
3040 kth = this._physicalEnd; | 1565 kth = this._physicalEnd; |
3041 currentRatio = topSpace / hiddenContentSize; | 1566 currentRatio = topSpace / hiddenContentSize; |
3042 | 1567 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen
t(kth) > scrollBottom) { |
3043 // move tiles from bottom to top | |
3044 while ( | |
3045 // approximate `currentRatio` to `ratio` | |
3046 currentRatio < ratio && | |
3047 // recycle less physical items than the total | |
3048 recycledTiles < this._physicalCount && | |
3049 // ensure that these recycled tiles are needed | |
3050 virtualStart - recycledTiles > 0 && | |
3051 // ensure that the tile is not visible | |
3052 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
3053 ) { | |
3054 | |
3055 tileHeight = this._getPhysicalSizeIncrement(kth); | 1568 tileHeight = this._getPhysicalSizeIncrement(kth); |
3056 currentRatio += tileHeight / hiddenContentSize; | 1569 currentRatio += tileHeight / hiddenContentSize; |
3057 physicalBottom -= tileHeight; | 1570 physicalBottom -= tileHeight; |
3058 recycledTileSet.push(kth); | 1571 recycledTileSet.push(kth); |
3059 recycledTiles++; | 1572 recycledTiles++; |
3060 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 1573 kth = kth === 0 ? this._physicalCount - 1 : kth - 1; |
3061 } | 1574 } |
3062 | |
3063 movingUp = recycledTileSet; | 1575 movingUp = recycledTileSet; |
3064 recycledTiles = -recycledTiles; | 1576 recycledTiles = -recycledTiles; |
3065 } | 1577 } else if (delta > 0) { |
3066 // scroll down | |
3067 else if (delta > 0) { | |
3068 var bottomSpace = physicalBottom - scrollBottom; | 1578 var bottomSpace = physicalBottom - scrollBottom; |
3069 var virtualEnd = this._virtualEnd; | 1579 var virtualEnd = this._virtualEnd; |
3070 var lastVirtualItemIndex = this._virtualCount-1; | 1580 var lastVirtualItemIndex = this._virtualCount - 1; |
3071 | |
3072 recycledTileSet = []; | 1581 recycledTileSet = []; |
3073 | |
3074 kth = this._physicalStart; | 1582 kth = this._physicalStart; |
3075 currentRatio = bottomSpace / hiddenContentSize; | 1583 currentRatio = bottomSpace / hiddenContentSize; |
3076 | 1584 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get
PhysicalSizeIncrement(kth) < scrollTop) { |
3077 // move tiles from top to bottom | |
3078 while ( | |
3079 // approximate `currentRatio` to `ratio` | |
3080 currentRatio < ratio && | |
3081 // recycle less physical items than the total | |
3082 recycledTiles < this._physicalCount && | |
3083 // ensure that these recycled tiles are needed | |
3084 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
3085 // ensure that the tile is not visible | |
3086 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
3087 ) { | |
3088 | |
3089 tileHeight = this._getPhysicalSizeIncrement(kth); | 1585 tileHeight = this._getPhysicalSizeIncrement(kth); |
3090 currentRatio += tileHeight / hiddenContentSize; | 1586 currentRatio += tileHeight / hiddenContentSize; |
3091 | |
3092 this._physicalTop += tileHeight; | 1587 this._physicalTop += tileHeight; |
3093 recycledTileSet.push(kth); | 1588 recycledTileSet.push(kth); |
3094 recycledTiles++; | 1589 recycledTiles++; |
3095 kth = (kth + 1) % this._physicalCount; | 1590 kth = (kth + 1) % this._physicalCount; |
3096 } | 1591 } |
3097 } | 1592 } |
3098 | |
3099 if (recycledTiles === 0) { | 1593 if (recycledTiles === 0) { |
3100 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
3101 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 1594 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
3102 this._increasePoolIfNeeded(); | 1595 this._increasePoolIfNeeded(); |
3103 } | 1596 } |
3104 } else { | 1597 } else { |
3105 this._virtualStart = this._virtualStart + recycledTiles; | 1598 this._virtualStart = this._virtualStart + recycledTiles; |
3106 this._physicalStart = this._physicalStart + recycledTiles; | 1599 this._physicalStart = this._physicalStart + recycledTiles; |
3107 this._update(recycledTileSet, movingUp); | 1600 this._update(recycledTileSet, movingUp); |
3108 } | 1601 } |
3109 }, | 1602 }, |
3110 | |
3111 /** | |
3112 * Update the list of items, starting from the `_virtualStart` item. | |
3113 * @param {!Array<number>=} itemSet | |
3114 * @param {!Array<number>=} movingUp | |
3115 */ | |
3116 _update: function(itemSet, movingUp) { | 1603 _update: function(itemSet, movingUp) { |
3117 // manage focus | |
3118 this._manageFocus(); | 1604 this._manageFocus(); |
3119 // update models | |
3120 this._assignModels(itemSet); | 1605 this._assignModels(itemSet); |
3121 // measure heights | |
3122 this._updateMetrics(itemSet); | 1606 this._updateMetrics(itemSet); |
3123 // adjust offset after measuring | |
3124 if (movingUp) { | 1607 if (movingUp) { |
3125 while (movingUp.length) { | 1608 while (movingUp.length) { |
3126 var idx = movingUp.pop(); | 1609 var idx = movingUp.pop(); |
3127 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 1610 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
3128 } | 1611 } |
3129 } | 1612 } |
3130 // update the position of the items | |
3131 this._positionItems(); | 1613 this._positionItems(); |
3132 // set the scroller size | |
3133 this._updateScrollerSize(); | 1614 this._updateScrollerSize(); |
3134 // increase the pool of physical items | |
3135 this._increasePoolIfNeeded(); | 1615 this._increasePoolIfNeeded(); |
3136 }, | 1616 }, |
3137 | |
3138 /** | |
3139 * Creates a pool of DOM elements and attaches them to the local dom. | |
3140 */ | |
3141 _createPool: function(size) { | 1617 _createPool: function(size) { |
3142 var physicalItems = new Array(size); | 1618 var physicalItems = new Array(size); |
3143 | |
3144 this._ensureTemplatized(); | 1619 this._ensureTemplatized(); |
3145 | |
3146 for (var i = 0; i < size; i++) { | 1620 for (var i = 0; i < size; i++) { |
3147 var inst = this.stamp(null); | 1621 var inst = this.stamp(null); |
3148 // First element child is item; Safari doesn't support children[0] | |
3149 // on a doc fragment | |
3150 physicalItems[i] = inst.root.querySelector('*'); | 1622 physicalItems[i] = inst.root.querySelector('*'); |
3151 Polymer.dom(this).appendChild(inst.root); | 1623 Polymer.dom(this).appendChild(inst.root); |
3152 } | 1624 } |
3153 return physicalItems; | 1625 return physicalItems; |
3154 }, | 1626 }, |
3155 | |
3156 /** | |
3157 * Increases the pool of physical items only if needed. | |
3158 * | |
3159 * @return {boolean} True if the pool was increased. | |
3160 */ | |
3161 _increasePoolIfNeeded: function() { | 1627 _increasePoolIfNeeded: function() { |
3162 // Base case 1: the list has no height. | |
3163 if (this._viewportHeight === 0) { | 1628 if (this._viewportHeight === 0) { |
3164 return false; | 1629 return false; |
3165 } | 1630 } |
3166 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
3167 // with physical items, don't increase the pool. | |
3168 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 1631 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
3169 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 1632 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
3170 return false; | 1633 return false; |
3171 } | 1634 } |
3172 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
3173 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 1635 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
3174 | |
3175 if (currentPage === 0) { | 1636 if (currentPage === 0) { |
3176 // fill the first page | 1637 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * .5))); |
3177 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
3178 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 1638 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
3179 // paint the page and defer the next increase | |
3180 // wait 16ms which is rough enough to get paint cycle. | |
3181 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 1639 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
3182 } else { | 1640 } else { |
3183 // fill the rest of the pages | |
3184 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 1641 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
3185 } | 1642 } |
3186 | |
3187 this._lastPage = currentPage; | 1643 this._lastPage = currentPage; |
3188 | |
3189 return true; | 1644 return true; |
3190 }, | 1645 }, |
3191 | |
3192 /** | |
3193 * Increases the pool size. | |
3194 */ | |
3195 _increasePool: function(missingItems) { | 1646 _increasePool: function(missingItems) { |
3196 var nextPhysicalCount = Math.min( | 1647 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this.
_virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS
ICAL_COUNT)); |
3197 this._physicalCount + missingItems, | |
3198 this._virtualCount - this._virtualStart, | |
3199 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
3200 ); | |
3201 var prevPhysicalCount = this._physicalCount; | 1648 var prevPhysicalCount = this._physicalCount; |
3202 var delta = nextPhysicalCount - prevPhysicalCount; | 1649 var delta = nextPhysicalCount - prevPhysicalCount; |
3203 | |
3204 if (delta <= 0) { | 1650 if (delta <= 0) { |
3205 return; | 1651 return; |
3206 } | 1652 } |
3207 | |
3208 [].push.apply(this._physicalItems, this._createPool(delta)); | 1653 [].push.apply(this._physicalItems, this._createPool(delta)); |
3209 [].push.apply(this._physicalSizes, new Array(delta)); | 1654 [].push.apply(this._physicalSizes, new Array(delta)); |
3210 | |
3211 this._physicalCount = prevPhysicalCount + delta; | 1655 this._physicalCount = prevPhysicalCount + delta; |
3212 | 1656 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this.
_focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd
) { |
3213 // update the physical start if we need to preserve the model of the focus
ed item. | |
3214 // In this situation, the focused item is currently rendered and its model
would | |
3215 // have changed after increasing the pool if the physical start remained u
nchanged. | |
3216 if (this._physicalStart > this._physicalEnd && | |
3217 this._isIndexRendered(this._focusedIndex) && | |
3218 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
3219 this._physicalStart = this._physicalStart + delta; | 1657 this._physicalStart = this._physicalStart + delta; |
3220 } | 1658 } |
3221 this._update(); | 1659 this._update(); |
3222 }, | 1660 }, |
3223 | |
3224 /** | |
3225 * Render a new list of items. This method does exactly the same as `update`
, | |
3226 * but it also ensures that only one `update` cycle is created. | |
3227 */ | |
3228 _render: function() { | 1661 _render: function() { |
3229 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 1662 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
3230 | |
3231 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 1663 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
3232 this._lastPage = 0; | 1664 this._lastPage = 0; |
3233 this._update(); | 1665 this._update(); |
3234 this._itemsRendered = true; | 1666 this._itemsRendered = true; |
3235 } | 1667 } |
3236 }, | 1668 }, |
3237 | |
3238 /** | |
3239 * Templetizes the user template. | |
3240 */ | |
3241 _ensureTemplatized: function() { | 1669 _ensureTemplatized: function() { |
3242 if (!this.ctor) { | 1670 if (!this.ctor) { |
3243 // Template instance props that should be excluded from forwarding | |
3244 var props = {}; | 1671 var props = {}; |
3245 props.__key__ = true; | 1672 props.__key__ = true; |
3246 props[this.as] = true; | 1673 props[this.as] = true; |
3247 props[this.indexAs] = true; | 1674 props[this.indexAs] = true; |
3248 props[this.selectedAs] = true; | 1675 props[this.selectedAs] = true; |
3249 props.tabIndex = true; | 1676 props.tabIndex = true; |
3250 | |
3251 this._instanceProps = props; | 1677 this._instanceProps = props; |
3252 this._userTemplate = Polymer.dom(this).querySelector('template'); | 1678 this._userTemplate = Polymer.dom(this).querySelector('template'); |
3253 | |
3254 if (this._userTemplate) { | 1679 if (this._userTemplate) { |
3255 this.templatize(this._userTemplate); | 1680 this.templatize(this._userTemplate); |
3256 } else { | 1681 } else { |
3257 console.warn('iron-list requires a template to be provided in light-do
m'); | 1682 console.warn('iron-list requires a template to be provided in light-do
m'); |
3258 } | 1683 } |
3259 } | 1684 } |
3260 }, | 1685 }, |
3261 | |
3262 /** | |
3263 * Implements extension point from Templatizer mixin. | |
3264 */ | |
3265 _getStampedChildren: function() { | 1686 _getStampedChildren: function() { |
3266 return this._physicalItems; | 1687 return this._physicalItems; |
3267 }, | 1688 }, |
3268 | |
3269 /** | |
3270 * Implements extension point from Templatizer | |
3271 * Called as a side effect of a template instance path change, responsible | |
3272 * for notifying items.<key-for-instance>.<path> change up to host. | |
3273 */ | |
3274 _forwardInstancePath: function(inst, path, value) { | 1689 _forwardInstancePath: function(inst, path, value) { |
3275 if (path.indexOf(this.as + '.') === 0) { | 1690 if (path.indexOf(this.as + '.') === 0) { |
3276 this.notifyPath('items.' + inst.__key__ + '.' + | 1691 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt
h + 1), value); |
3277 path.slice(this.as.length + 1), value); | |
3278 } | 1692 } |
3279 }, | 1693 }, |
3280 | |
3281 /** | |
3282 * Implements extension point from Templatizer mixin | |
3283 * Called as side-effect of a host property change, responsible for | |
3284 * notifying parent path change on each row. | |
3285 */ | |
3286 _forwardParentProp: function(prop, value) { | 1694 _forwardParentProp: function(prop, value) { |
3287 if (this._physicalItems) { | 1695 if (this._physicalItems) { |
3288 this._physicalItems.forEach(function(item) { | 1696 this._physicalItems.forEach(function(item) { |
3289 item._templateInstance[prop] = value; | 1697 item._templateInstance[prop] = value; |
3290 }, this); | 1698 }, this); |
3291 } | 1699 } |
3292 }, | 1700 }, |
3293 | |
3294 /** | |
3295 * Implements extension point from Templatizer | |
3296 * Called as side-effect of a host path change, responsible for | |
3297 * notifying parent.<path> path change on each row. | |
3298 */ | |
3299 _forwardParentPath: function(path, value) { | 1701 _forwardParentPath: function(path, value) { |
3300 if (this._physicalItems) { | 1702 if (this._physicalItems) { |
3301 this._physicalItems.forEach(function(item) { | 1703 this._physicalItems.forEach(function(item) { |
3302 item._templateInstance.notifyPath(path, value, true); | 1704 item._templateInstance.notifyPath(path, value, true); |
3303 }, this); | 1705 }, this); |
3304 } | 1706 } |
3305 }, | 1707 }, |
3306 | |
3307 /** | |
3308 * Called as a side effect of a host items.<key>.<path> path change, | |
3309 * responsible for notifying item.<path> changes. | |
3310 */ | |
3311 _forwardItemPath: function(path, value) { | 1708 _forwardItemPath: function(path, value) { |
3312 if (!this._physicalIndexForKey) { | 1709 if (!this._physicalIndexForKey) { |
3313 return; | 1710 return; |
3314 } | 1711 } |
3315 var dot = path.indexOf('.'); | 1712 var dot = path.indexOf('.'); |
3316 var key = path.substring(0, dot < 0 ? path.length : dot); | 1713 var key = path.substring(0, dot < 0 ? path.length : dot); |
3317 var idx = this._physicalIndexForKey[key]; | 1714 var idx = this._physicalIndexForKey[key]; |
3318 var offscreenItem = this._offscreenFocusedItem; | 1715 var offscreenItem = this._offscreenFocusedItem; |
3319 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 1716 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? offscreenItem : this._physicalItems[idx]; |
3320 offscreenItem : this._physicalItems[idx]; | |
3321 | |
3322 if (!el || el._templateInstance.__key__ !== key) { | 1717 if (!el || el._templateInstance.__key__ !== key) { |
3323 return; | 1718 return; |
3324 } | 1719 } |
3325 if (dot >= 0) { | 1720 if (dot >= 0) { |
3326 path = this.as + '.' + path.substring(dot+1); | 1721 path = this.as + '.' + path.substring(dot + 1); |
3327 el._templateInstance.notifyPath(path, value, true); | 1722 el._templateInstance.notifyPath(path, value, true); |
3328 } else { | 1723 } else { |
3329 // Update selection if needed | |
3330 var currentItem = el._templateInstance[this.as]; | 1724 var currentItem = el._templateInstance[this.as]; |
3331 if (Array.isArray(this.selectedItems)) { | 1725 if (Array.isArray(this.selectedItems)) { |
3332 for (var i = 0; i < this.selectedItems.length; i++) { | 1726 for (var i = 0; i < this.selectedItems.length; i++) { |
3333 if (this.selectedItems[i] === currentItem) { | 1727 if (this.selectedItems[i] === currentItem) { |
3334 this.set('selectedItems.' + i, value); | 1728 this.set('selectedItems.' + i, value); |
3335 break; | 1729 break; |
3336 } | 1730 } |
3337 } | 1731 } |
3338 } else if (this.selectedItem === currentItem) { | 1732 } else if (this.selectedItem === currentItem) { |
3339 this.set('selectedItem', value); | 1733 this.set('selectedItem', value); |
3340 } | 1734 } |
3341 el._templateInstance[this.as] = value; | 1735 el._templateInstance[this.as] = value; |
3342 } | 1736 } |
3343 }, | 1737 }, |
3344 | |
3345 /** | |
3346 * Called when the items have changed. That is, ressignments | |
3347 * to `items`, splices or updates to a single item. | |
3348 */ | |
3349 _itemsChanged: function(change) { | 1738 _itemsChanged: function(change) { |
3350 if (change.path === 'items') { | 1739 if (change.path === 'items') { |
3351 // reset items | |
3352 this._virtualStart = 0; | 1740 this._virtualStart = 0; |
3353 this._physicalTop = 0; | 1741 this._physicalTop = 0; |
3354 this._virtualCount = this.items ? this.items.length : 0; | 1742 this._virtualCount = this.items ? this.items.length : 0; |
3355 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 1743 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
3356 this._physicalIndexForKey = {}; | 1744 this._physicalIndexForKey = {}; |
3357 this._firstVisibleIndexVal = null; | 1745 this._firstVisibleIndexVal = null; |
3358 this._lastVisibleIndexVal = null; | 1746 this._lastVisibleIndexVal = null; |
3359 | |
3360 this._resetScrollPosition(0); | 1747 this._resetScrollPosition(0); |
3361 this._removeFocusedItem(); | 1748 this._removeFocusedItem(); |
3362 // create the initial physical items | |
3363 if (!this._physicalItems) { | 1749 if (!this._physicalItems) { |
3364 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 1750 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
3365 this._physicalItems = this._createPool(this._physicalCount); | 1751 this._physicalItems = this._createPool(this._physicalCount); |
3366 this._physicalSizes = new Array(this._physicalCount); | 1752 this._physicalSizes = new Array(this._physicalCount); |
3367 } | 1753 } |
3368 | |
3369 this._physicalStart = 0; | 1754 this._physicalStart = 0; |
3370 | |
3371 } else if (change.path === 'items.splices') { | 1755 } else if (change.path === 'items.splices') { |
3372 | |
3373 this._adjustVirtualIndex(change.value.indexSplices); | 1756 this._adjustVirtualIndex(change.value.indexSplices); |
3374 this._virtualCount = this.items ? this.items.length : 0; | 1757 this._virtualCount = this.items ? this.items.length : 0; |
3375 | |
3376 } else { | 1758 } else { |
3377 // update a single item | |
3378 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 1759 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
3379 return; | 1760 return; |
3380 } | 1761 } |
3381 | |
3382 this._itemsRendered = false; | 1762 this._itemsRendered = false; |
3383 this._debounceTemplate(this._render); | 1763 this._debounceTemplate(this._render); |
3384 }, | 1764 }, |
3385 | |
3386 /** | |
3387 * @param {!Array<!PolymerSplice>} splices | |
3388 */ | |
3389 _adjustVirtualIndex: function(splices) { | 1765 _adjustVirtualIndex: function(splices) { |
3390 splices.forEach(function(splice) { | 1766 splices.forEach(function(splice) { |
3391 // deselect removed items | |
3392 splice.removed.forEach(this._removeItem, this); | 1767 splice.removed.forEach(this._removeItem, this); |
3393 // We only need to care about changes happening above the current positi
on | |
3394 if (splice.index < this._virtualStart) { | 1768 if (splice.index < this._virtualStart) { |
3395 var delta = Math.max( | 1769 var delta = Math.max(splice.addedCount - splice.removed.length, splice
.index - this._virtualStart); |
3396 splice.addedCount - splice.removed.length, | |
3397 splice.index - this._virtualStart); | |
3398 | |
3399 this._virtualStart = this._virtualStart + delta; | 1770 this._virtualStart = this._virtualStart + delta; |
3400 | |
3401 if (this._focusedIndex >= 0) { | 1771 if (this._focusedIndex >= 0) { |
3402 this._focusedIndex = this._focusedIndex + delta; | 1772 this._focusedIndex = this._focusedIndex + delta; |
3403 } | 1773 } |
3404 } | 1774 } |
3405 }, this); | 1775 }, this); |
3406 }, | 1776 }, |
3407 | |
3408 _removeItem: function(item) { | 1777 _removeItem: function(item) { |
3409 this.$.selector.deselect(item); | 1778 this.$.selector.deselect(item); |
3410 // remove the current focused item | |
3411 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 1779 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
3412 this._removeFocusedItem(); | 1780 this._removeFocusedItem(); |
3413 } | 1781 } |
3414 }, | 1782 }, |
3415 | |
3416 /** | |
3417 * Executes a provided function per every physical index in `itemSet` | |
3418 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
3419 * | |
3420 * @param {!function(number, number)} fn | |
3421 * @param {!Array<number>=} itemSet | |
3422 */ | |
3423 _iterateItems: function(fn, itemSet) { | 1783 _iterateItems: function(fn, itemSet) { |
3424 var pidx, vidx, rtn, i; | 1784 var pidx, vidx, rtn, i; |
3425 | |
3426 if (arguments.length === 2 && itemSet) { | 1785 if (arguments.length === 2 && itemSet) { |
3427 for (i = 0; i < itemSet.length; i++) { | 1786 for (i = 0; i < itemSet.length; i++) { |
3428 pidx = itemSet[i]; | 1787 pidx = itemSet[i]; |
3429 vidx = this._computeVidx(pidx); | 1788 vidx = this._computeVidx(pidx); |
3430 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1789 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3431 return rtn; | 1790 return rtn; |
3432 } | 1791 } |
3433 } | 1792 } |
3434 } else { | 1793 } else { |
3435 pidx = this._physicalStart; | 1794 pidx = this._physicalStart; |
3436 vidx = this._virtualStart; | 1795 vidx = this._virtualStart; |
3437 | 1796 for (;pidx < this._physicalCount; pidx++, vidx++) { |
3438 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
3439 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1797 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3440 return rtn; | 1798 return rtn; |
3441 } | 1799 } |
3442 } | 1800 } |
3443 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 1801 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
3444 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1802 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3445 return rtn; | 1803 return rtn; |
3446 } | 1804 } |
3447 } | 1805 } |
3448 } | 1806 } |
3449 }, | 1807 }, |
3450 | |
3451 /** | |
3452 * Returns the virtual index for a given physical index | |
3453 * | |
3454 * @param {number} pidx Physical index | |
3455 * @return {number} | |
3456 */ | |
3457 _computeVidx: function(pidx) { | 1808 _computeVidx: function(pidx) { |
3458 if (pidx >= this._physicalStart) { | 1809 if (pidx >= this._physicalStart) { |
3459 return this._virtualStart + (pidx - this._physicalStart); | 1810 return this._virtualStart + (pidx - this._physicalStart); |
3460 } | 1811 } |
3461 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 1812 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
3462 }, | 1813 }, |
3463 | |
3464 /** | |
3465 * Assigns the data models to a given set of items. | |
3466 * @param {!Array<number>=} itemSet | |
3467 */ | |
3468 _assignModels: function(itemSet) { | 1814 _assignModels: function(itemSet) { |
3469 this._iterateItems(function(pidx, vidx) { | 1815 this._iterateItems(function(pidx, vidx) { |
3470 var el = this._physicalItems[pidx]; | 1816 var el = this._physicalItems[pidx]; |
3471 var inst = el._templateInstance; | 1817 var inst = el._templateInstance; |
3472 var item = this.items && this.items[vidx]; | 1818 var item = this.items && this.items[vidx]; |
3473 | |
3474 if (item != null) { | 1819 if (item != null) { |
3475 inst[this.as] = item; | 1820 inst[this.as] = item; |
3476 inst.__key__ = this._collection.getKey(item); | 1821 inst.__key__ = this._collection.getKey(item); |
3477 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 1822 inst[this.selectedAs] = this.$.selector.isSelected(item); |
3478 inst[this.indexAs] = vidx; | 1823 inst[this.indexAs] = vidx; |
3479 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 1824 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
3480 this._physicalIndexForKey[inst.__key__] = pidx; | 1825 this._physicalIndexForKey[inst.__key__] = pidx; |
3481 el.removeAttribute('hidden'); | 1826 el.removeAttribute('hidden'); |
3482 } else { | 1827 } else { |
3483 inst.__key__ = null; | 1828 inst.__key__ = null; |
3484 el.setAttribute('hidden', ''); | 1829 el.setAttribute('hidden', ''); |
3485 } | 1830 } |
3486 }, itemSet); | 1831 }, itemSet); |
3487 }, | 1832 }, |
3488 | 1833 _updateMetrics: function(itemSet) { |
3489 /** | |
3490 * Updates the height for a given set of items. | |
3491 * | |
3492 * @param {!Array<number>=} itemSet | |
3493 */ | |
3494 _updateMetrics: function(itemSet) { | |
3495 // Make sure we distributed all the physical items | |
3496 // so we can measure them | |
3497 Polymer.dom.flush(); | 1834 Polymer.dom.flush(); |
3498 | |
3499 var newPhysicalSize = 0; | 1835 var newPhysicalSize = 0; |
3500 var oldPhysicalSize = 0; | 1836 var oldPhysicalSize = 0; |
3501 var prevAvgCount = this._physicalAverageCount; | 1837 var prevAvgCount = this._physicalAverageCount; |
3502 var prevPhysicalAvg = this._physicalAverage; | 1838 var prevPhysicalAvg = this._physicalAverage; |
3503 | |
3504 this._iterateItems(function(pidx, vidx) { | 1839 this._iterateItems(function(pidx, vidx) { |
3505 | |
3506 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 1840 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
3507 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 1841 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
3508 newPhysicalSize += this._physicalSizes[pidx]; | 1842 newPhysicalSize += this._physicalSizes[pidx]; |
3509 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 1843 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
3510 | |
3511 }, itemSet); | 1844 }, itemSet); |
3512 | |
3513 this._viewportHeight = this._scrollTargetHeight; | 1845 this._viewportHeight = this._scrollTargetHeight; |
3514 if (this.grid) { | 1846 if (this.grid) { |
3515 this._updateGridMetrics(); | 1847 this._updateGridMetrics(); |
3516 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 1848 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
3517 } else { | 1849 } else { |
3518 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 1850 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
3519 } | 1851 } |
3520 | |
3521 // update the average if we measured something | |
3522 if (this._physicalAverageCount !== prevAvgCount) { | 1852 if (this._physicalAverageCount !== prevAvgCount) { |
3523 this._physicalAverage = Math.round( | 1853 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new
PhysicalSize) / this._physicalAverageCount); |
3524 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
3525 this._physicalAverageCount); | |
3526 } | 1854 } |
3527 }, | 1855 }, |
3528 | |
3529 _updateGridMetrics: function() { | 1856 _updateGridMetrics: function() { |
3530 this._viewportWidth = this.$.items.offsetWidth; | 1857 this._viewportWidth = this.$.items.offsetWidth; |
3531 // Set item width to the value of the _physicalItems offsetWidth | |
3532 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 1858 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
3533 // Set row height to the value of the _physicalItems offsetHeight | |
3534 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 1859 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
3535 // If in grid mode compute how many items with exist in each row | |
3536 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 1860 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
3537 }, | 1861 }, |
3538 | |
3539 /** | |
3540 * Updates the position of the physical items. | |
3541 */ | |
3542 _positionItems: function() { | 1862 _positionItems: function() { |
3543 this._adjustScrollPosition(); | 1863 this._adjustScrollPosition(); |
3544 | |
3545 var y = this._physicalTop; | 1864 var y = this._physicalTop; |
3546 | |
3547 if (this.grid) { | 1865 if (this.grid) { |
3548 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 1866 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
3549 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 1867 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
3550 | |
3551 this._iterateItems(function(pidx, vidx) { | 1868 this._iterateItems(function(pidx, vidx) { |
3552 | |
3553 var modulus = vidx % this._itemsPerRow; | 1869 var modulus = vidx % this._itemsPerRow; |
3554 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | 1870 var x = Math.floor(modulus * this._itemWidth + rowOffset); |
3555 | |
3556 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | 1871 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
3557 | |
3558 if (this._shouldRenderNextRow(vidx)) { | 1872 if (this._shouldRenderNextRow(vidx)) { |
3559 y += this._rowHeight; | 1873 y += this._rowHeight; |
3560 } | 1874 } |
3561 | |
3562 }); | 1875 }); |
3563 } else { | 1876 } else { |
3564 this._iterateItems(function(pidx, vidx) { | 1877 this._iterateItems(function(pidx, vidx) { |
3565 | |
3566 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 1878 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
3567 y += this._physicalSizes[pidx]; | 1879 y += this._physicalSizes[pidx]; |
3568 | |
3569 }); | 1880 }); |
3570 } | 1881 } |
3571 }, | 1882 }, |
3572 | |
3573 _getPhysicalSizeIncrement: function(pidx) { | 1883 _getPhysicalSizeIncrement: function(pidx) { |
3574 if (!this.grid) { | 1884 if (!this.grid) { |
3575 return this._physicalSizes[pidx]; | 1885 return this._physicalSizes[pidx]; |
3576 } | 1886 } |
3577 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 1887 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
3578 return 0; | 1888 return 0; |
3579 } | 1889 } |
3580 return this._rowHeight; | 1890 return this._rowHeight; |
3581 }, | 1891 }, |
3582 | |
3583 /** | |
3584 * Returns, based on the current index, | |
3585 * whether or not the next index will need | |
3586 * to be rendered on a new row. | |
3587 * | |
3588 * @param {number} vidx Virtual index | |
3589 * @return {boolean} | |
3590 */ | |
3591 _shouldRenderNextRow: function(vidx) { | 1892 _shouldRenderNextRow: function(vidx) { |
3592 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 1893 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
3593 }, | 1894 }, |
3594 | |
3595 /** | |
3596 * Adjusts the scroll position when it was overestimated. | |
3597 */ | |
3598 _adjustScrollPosition: function() { | 1895 _adjustScrollPosition: function() { |
3599 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 1896 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(
this._scrollPosition + this._physicalTop, 0); |
3600 Math.min(this._scrollPosition + this._physicalTop, 0); | |
3601 | |
3602 if (deltaHeight) { | 1897 if (deltaHeight) { |
3603 this._physicalTop = this._physicalTop - deltaHeight; | 1898 this._physicalTop = this._physicalTop - deltaHeight; |
3604 // juking scroll position during interial scrolling on iOS is no bueno | |
3605 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 1899 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
3606 this._resetScrollPosition(this._scrollTop - deltaHeight); | 1900 this._resetScrollPosition(this._scrollTop - deltaHeight); |
3607 } | 1901 } |
3608 } | 1902 } |
3609 }, | 1903 }, |
3610 | |
3611 /** | |
3612 * Sets the position of the scroll. | |
3613 */ | |
3614 _resetScrollPosition: function(pos) { | 1904 _resetScrollPosition: function(pos) { |
3615 if (this.scrollTarget) { | 1905 if (this.scrollTarget) { |
3616 this._scrollTop = pos; | 1906 this._scrollTop = pos; |
3617 this._scrollPosition = this._scrollTop; | 1907 this._scrollPosition = this._scrollTop; |
3618 } | 1908 } |
3619 }, | 1909 }, |
3620 | |
3621 /** | |
3622 * Sets the scroll height, that's the height of the content, | |
3623 * | |
3624 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
3625 */ | |
3626 _updateScrollerSize: function(forceUpdate) { | 1910 _updateScrollerSize: function(forceUpdate) { |
3627 if (this.grid) { | 1911 if (this.grid) { |
3628 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 1912 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
3629 } else { | 1913 } else { |
3630 this._estScrollHeight = (this._physicalBottom + | 1914 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou
nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage; |
3631 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
3632 } | 1915 } |
3633 | |
3634 forceUpdate = forceUpdate || this._scrollHeight === 0; | 1916 forceUpdate = forceUpdate || this._scrollHeight === 0; |
3635 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 1917 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
3636 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 1918 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
3637 | |
3638 // amortize height adjustment, so it won't trigger repaints very often | |
3639 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 1919 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
3640 this.$.items.style.height = this._estScrollHeight + 'px'; | 1920 this.$.items.style.height = this._estScrollHeight + 'px'; |
3641 this._scrollHeight = this._estScrollHeight; | 1921 this._scrollHeight = this._estScrollHeight; |
3642 } | 1922 } |
3643 }, | 1923 }, |
3644 | 1924 scrollToItem: function(item) { |
3645 /** | |
3646 * Scroll to a specific item in the virtual list regardless | |
3647 * of the physical items in the DOM tree. | |
3648 * | |
3649 * @method scrollToItem | |
3650 * @param {(Object)} item The item to be scrolled to | |
3651 */ | |
3652 scrollToItem: function(item){ | |
3653 return this.scrollToIndex(this.items.indexOf(item)); | 1925 return this.scrollToIndex(this.items.indexOf(item)); |
3654 }, | 1926 }, |
3655 | |
3656 /** | |
3657 * Scroll to a specific index in the virtual list regardless | |
3658 * of the physical items in the DOM tree. | |
3659 * | |
3660 * @method scrollToIndex | |
3661 * @param {number} idx The index of the item | |
3662 */ | |
3663 scrollToIndex: function(idx) { | 1927 scrollToIndex: function(idx) { |
3664 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 1928 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
3665 return; | 1929 return; |
3666 } | 1930 } |
3667 | |
3668 Polymer.dom.flush(); | 1931 Polymer.dom.flush(); |
3669 | 1932 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1); |
3670 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
3671 // update the virtual start only when needed | |
3672 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 1933 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
3673 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 1934 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1; |
3674 } | 1935 } |
3675 // manage focus | |
3676 this._manageFocus(); | 1936 this._manageFocus(); |
3677 // assign new models | |
3678 this._assignModels(); | 1937 this._assignModels(); |
3679 // measure the new sizes | |
3680 this._updateMetrics(); | 1938 this._updateMetrics(); |
3681 | 1939 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
3682 // estimate new physical offset | |
3683 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
3684 this._physicalTop = estPhysicalTop; | 1940 this._physicalTop = estPhysicalTop; |
3685 | |
3686 var currentTopItem = this._physicalStart; | 1941 var currentTopItem = this._physicalStart; |
3687 var currentVirtualItem = this._virtualStart; | 1942 var currentVirtualItem = this._virtualStart; |
3688 var targetOffsetTop = 0; | 1943 var targetOffsetTop = 0; |
3689 var hiddenContentSize = this._hiddenContentSize; | 1944 var hiddenContentSize = this._hiddenContentSize; |
3690 | |
3691 // scroll to the item as much as we can | |
3692 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 1945 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
3693 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 1946 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
3694 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 1947 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
3695 currentVirtualItem++; | 1948 currentVirtualItem++; |
3696 } | 1949 } |
3697 // update the scroller size | |
3698 this._updateScrollerSize(true); | 1950 this._updateScrollerSize(true); |
3699 // update the position of the items | |
3700 this._positionItems(); | 1951 this._positionItems(); |
3701 // set the new scroll position | |
3702 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 1952 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
3703 // increase the pool of physical items if needed | |
3704 this._increasePoolIfNeeded(); | 1953 this._increasePoolIfNeeded(); |
3705 // clear cached visible index | |
3706 this._firstVisibleIndexVal = null; | 1954 this._firstVisibleIndexVal = null; |
3707 this._lastVisibleIndexVal = null; | 1955 this._lastVisibleIndexVal = null; |
3708 }, | 1956 }, |
3709 | |
3710 /** | |
3711 * Reset the physical average and the average count. | |
3712 */ | |
3713 _resetAverage: function() { | 1957 _resetAverage: function() { |
3714 this._physicalAverage = 0; | 1958 this._physicalAverage = 0; |
3715 this._physicalAverageCount = 0; | 1959 this._physicalAverageCount = 0; |
3716 }, | 1960 }, |
3717 | |
3718 /** | |
3719 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
3720 * when the element is resized. | |
3721 */ | |
3722 _resizeHandler: function() { | 1961 _resizeHandler: function() { |
3723 // iOS fires the resize event when the address bar slides up | |
3724 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 1962 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
3725 return; | 1963 return; |
3726 } | 1964 } |
3727 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
3728 // changing the scroll position from a resize handler would result in | |
3729 // the scroll position being reset. Waiting 1ms fixes the issue. | |
3730 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 1965 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
3731 this.updateViewportBoundaries(); | 1966 this.updateViewportBoundaries(); |
3732 this._render(); | 1967 this._render(); |
3733 | |
3734 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 1968 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
3735 this._resetAverage(); | 1969 this._resetAverage(); |
3736 this.scrollToIndex(this.firstVisibleIndex); | 1970 this.scrollToIndex(this.firstVisibleIndex); |
3737 } | 1971 } |
3738 }.bind(this), 1)); | 1972 }.bind(this), 1)); |
3739 }, | 1973 }, |
3740 | |
3741 _getModelFromItem: function(item) { | 1974 _getModelFromItem: function(item) { |
3742 var key = this._collection.getKey(item); | 1975 var key = this._collection.getKey(item); |
3743 var pidx = this._physicalIndexForKey[key]; | 1976 var pidx = this._physicalIndexForKey[key]; |
3744 | |
3745 if (pidx != null) { | 1977 if (pidx != null) { |
3746 return this._physicalItems[pidx]._templateInstance; | 1978 return this._physicalItems[pidx]._templateInstance; |
3747 } | 1979 } |
3748 return null; | 1980 return null; |
3749 }, | 1981 }, |
3750 | |
3751 /** | |
3752 * Gets a valid item instance from its index or the object value. | |
3753 * | |
3754 * @param {(Object|number)} item The item object or its index | |
3755 */ | |
3756 _getNormalizedItem: function(item) { | 1982 _getNormalizedItem: function(item) { |
3757 if (this._collection.getKey(item) === undefined) { | 1983 if (this._collection.getKey(item) === undefined) { |
3758 if (typeof item === 'number') { | 1984 if (typeof item === 'number') { |
3759 item = this.items[item]; | 1985 item = this.items[item]; |
3760 if (!item) { | 1986 if (!item) { |
3761 throw new RangeError('<item> not found'); | 1987 throw new RangeError('<item> not found'); |
3762 } | 1988 } |
3763 return item; | 1989 return item; |
3764 } | 1990 } |
3765 throw new TypeError('<item> should be a valid item'); | 1991 throw new TypeError('<item> should be a valid item'); |
3766 } | 1992 } |
3767 return item; | 1993 return item; |
3768 }, | 1994 }, |
3769 | |
3770 /** | |
3771 * Select the list item at the given index. | |
3772 * | |
3773 * @method selectItem | |
3774 * @param {(Object|number)} item The item object or its index | |
3775 */ | |
3776 selectItem: function(item) { | 1995 selectItem: function(item) { |
3777 item = this._getNormalizedItem(item); | 1996 item = this._getNormalizedItem(item); |
3778 var model = this._getModelFromItem(item); | 1997 var model = this._getModelFromItem(item); |
3779 | |
3780 if (!this.multiSelection && this.selectedItem) { | 1998 if (!this.multiSelection && this.selectedItem) { |
3781 this.deselectItem(this.selectedItem); | 1999 this.deselectItem(this.selectedItem); |
3782 } | 2000 } |
3783 if (model) { | 2001 if (model) { |
3784 model[this.selectedAs] = true; | 2002 model[this.selectedAs] = true; |
3785 } | 2003 } |
3786 this.$.selector.select(item); | 2004 this.$.selector.select(item); |
3787 this.updateSizeForItem(item); | 2005 this.updateSizeForItem(item); |
3788 }, | 2006 }, |
3789 | |
3790 /** | |
3791 * Deselects the given item list if it is already selected. | |
3792 * | |
3793 | |
3794 * @method deselect | |
3795 * @param {(Object|number)} item The item object or its index | |
3796 */ | |
3797 deselectItem: function(item) { | 2007 deselectItem: function(item) { |
3798 item = this._getNormalizedItem(item); | 2008 item = this._getNormalizedItem(item); |
3799 var model = this._getModelFromItem(item); | 2009 var model = this._getModelFromItem(item); |
3800 | |
3801 if (model) { | 2010 if (model) { |
3802 model[this.selectedAs] = false; | 2011 model[this.selectedAs] = false; |
3803 } | 2012 } |
3804 this.$.selector.deselect(item); | 2013 this.$.selector.deselect(item); |
3805 this.updateSizeForItem(item); | 2014 this.updateSizeForItem(item); |
3806 }, | 2015 }, |
3807 | |
3808 /** | |
3809 * Select or deselect a given item depending on whether the item | |
3810 * has already been selected. | |
3811 * | |
3812 * @method toggleSelectionForItem | |
3813 * @param {(Object|number)} item The item object or its index | |
3814 */ | |
3815 toggleSelectionForItem: function(item) { | 2016 toggleSelectionForItem: function(item) { |
3816 item = this._getNormalizedItem(item); | 2017 item = this._getNormalizedItem(item); |
3817 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 2018 if (this.$.selector.isSelected(item)) { |
3818 this.deselectItem(item); | 2019 this.deselectItem(item); |
3819 } else { | 2020 } else { |
3820 this.selectItem(item); | 2021 this.selectItem(item); |
3821 } | 2022 } |
3822 }, | 2023 }, |
3823 | |
3824 /** | |
3825 * Clears the current selection state of the list. | |
3826 * | |
3827 * @method clearSelection | |
3828 */ | |
3829 clearSelection: function() { | 2024 clearSelection: function() { |
3830 function unselect(item) { | 2025 function unselect(item) { |
3831 var model = this._getModelFromItem(item); | 2026 var model = this._getModelFromItem(item); |
3832 if (model) { | 2027 if (model) { |
3833 model[this.selectedAs] = false; | 2028 model[this.selectedAs] = false; |
3834 } | 2029 } |
3835 } | 2030 } |
3836 | |
3837 if (Array.isArray(this.selectedItems)) { | 2031 if (Array.isArray(this.selectedItems)) { |
3838 this.selectedItems.forEach(unselect, this); | 2032 this.selectedItems.forEach(unselect, this); |
3839 } else if (this.selectedItem) { | 2033 } else if (this.selectedItem) { |
3840 unselect.call(this, this.selectedItem); | 2034 unselect.call(this, this.selectedItem); |
3841 } | 2035 } |
3842 | 2036 this.$.selector.clearSelection(); |
3843 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
3844 }, | 2037 }, |
3845 | |
3846 /** | |
3847 * Add an event listener to `tap` if `selectionEnabled` is true, | |
3848 * it will remove the listener otherwise. | |
3849 */ | |
3850 _selectionEnabledChanged: function(selectionEnabled) { | 2038 _selectionEnabledChanged: function(selectionEnabled) { |
3851 var handler = selectionEnabled ? this.listen : this.unlisten; | 2039 var handler = selectionEnabled ? this.listen : this.unlisten; |
3852 handler.call(this, this, 'tap', '_selectionHandler'); | 2040 handler.call(this, this, 'tap', '_selectionHandler'); |
3853 }, | 2041 }, |
3854 | |
3855 /** | |
3856 * Select an item from an event object. | |
3857 */ | |
3858 _selectionHandler: function(e) { | 2042 _selectionHandler: function(e) { |
3859 var model = this.modelForElement(e.target); | 2043 var model = this.modelForElement(e.target); |
3860 if (!model) { | 2044 if (!model) { |
3861 return; | 2045 return; |
3862 } | 2046 } |
3863 var modelTabIndex, activeElTabIndex; | 2047 var modelTabIndex, activeElTabIndex; |
3864 var target = Polymer.dom(e).path[0]; | 2048 var target = Polymer.dom(e).path[0]; |
3865 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 2049 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
3866 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 2050 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
3867 // Safari does not focus certain form controls via mouse | 2051 if (target.localName === 'input' || target.localName === 'button' || targe
t.localName === 'select') { |
3868 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
3869 if (target.localName === 'input' || | |
3870 target.localName === 'button' || | |
3871 target.localName === 'select') { | |
3872 return; | 2052 return; |
3873 } | 2053 } |
3874 // Set a temporary tabindex | |
3875 modelTabIndex = model.tabIndex; | 2054 modelTabIndex = model.tabIndex; |
3876 model.tabIndex = SECRET_TABINDEX; | 2055 model.tabIndex = SECRET_TABINDEX; |
3877 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 2056 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
3878 model.tabIndex = modelTabIndex; | 2057 model.tabIndex = modelTabIndex; |
3879 // Only select the item if the tap wasn't on a focusable child | |
3880 // or the element bound to `tabIndex` | |
3881 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 2058 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
3882 return; | 2059 return; |
3883 } | 2060 } |
3884 this.toggleSelectionForItem(model[this.as]); | 2061 this.toggleSelectionForItem(model[this.as]); |
3885 }, | 2062 }, |
3886 | |
3887 _multiSelectionChanged: function(multiSelection) { | 2063 _multiSelectionChanged: function(multiSelection) { |
3888 this.clearSelection(); | 2064 this.clearSelection(); |
3889 this.$.selector.multi = multiSelection; | 2065 this.$.selector.multi = multiSelection; |
3890 }, | 2066 }, |
3891 | |
3892 /** | |
3893 * Updates the size of an item. | |
3894 * | |
3895 * @method updateSizeForItem | |
3896 * @param {(Object|number)} item The item object or its index | |
3897 */ | |
3898 updateSizeForItem: function(item) { | 2067 updateSizeForItem: function(item) { |
3899 item = this._getNormalizedItem(item); | 2068 item = this._getNormalizedItem(item); |
3900 var key = this._collection.getKey(item); | 2069 var key = this._collection.getKey(item); |
3901 var pidx = this._physicalIndexForKey[key]; | 2070 var pidx = this._physicalIndexForKey[key]; |
3902 | |
3903 if (pidx != null) { | 2071 if (pidx != null) { |
3904 this._updateMetrics([pidx]); | 2072 this._updateMetrics([ pidx ]); |
3905 this._positionItems(); | 2073 this._positionItems(); |
3906 } | 2074 } |
3907 }, | 2075 }, |
3908 | |
3909 /** | |
3910 * Creates a temporary backfill item in the rendered pool of physical items | |
3911 * to replace the main focused item. The focused item has tabIndex = 0 | |
3912 * and might be currently focused by the user. | |
3913 * | |
3914 * This dynamic replacement helps to preserve the focus state. | |
3915 */ | |
3916 _manageFocus: function() { | 2076 _manageFocus: function() { |
3917 var fidx = this._focusedIndex; | 2077 var fidx = this._focusedIndex; |
3918 | |
3919 if (fidx >= 0 && fidx < this._virtualCount) { | 2078 if (fidx >= 0 && fidx < this._virtualCount) { |
3920 // if it's a valid index, check if that index is rendered | |
3921 // in a physical item. | |
3922 if (this._isIndexRendered(fidx)) { | 2079 if (this._isIndexRendered(fidx)) { |
3923 this._restoreFocusedItem(); | 2080 this._restoreFocusedItem(); |
3924 } else { | 2081 } else { |
3925 this._createFocusBackfillItem(); | 2082 this._createFocusBackfillItem(); |
3926 } | 2083 } |
3927 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 2084 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
3928 // otherwise, assign the initial focused index. | |
3929 this._focusedIndex = this._virtualStart; | 2085 this._focusedIndex = this._virtualStart; |
3930 this._focusedItem = this._physicalItems[this._physicalStart]; | 2086 this._focusedItem = this._physicalItems[this._physicalStart]; |
3931 } | 2087 } |
3932 }, | 2088 }, |
3933 | |
3934 _isIndexRendered: function(idx) { | 2089 _isIndexRendered: function(idx) { |
3935 return idx >= this._virtualStart && idx <= this._virtualEnd; | 2090 return idx >= this._virtualStart && idx <= this._virtualEnd; |
3936 }, | 2091 }, |
3937 | |
3938 _isIndexVisible: function(idx) { | 2092 _isIndexVisible: function(idx) { |
3939 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 2093 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
3940 }, | 2094 }, |
3941 | |
3942 _getPhysicalIndex: function(idx) { | 2095 _getPhysicalIndex: function(idx) { |
3943 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 2096 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
3944 }, | 2097 }, |
3945 | |
3946 _focusPhysicalItem: function(idx) { | 2098 _focusPhysicalItem: function(idx) { |
3947 if (idx < 0 || idx >= this._virtualCount) { | 2099 if (idx < 0 || idx >= this._virtualCount) { |
3948 return; | 2100 return; |
3949 } | 2101 } |
3950 this._restoreFocusedItem(); | 2102 this._restoreFocusedItem(); |
3951 // scroll to index to make sure it's rendered | |
3952 if (!this._isIndexRendered(idx)) { | 2103 if (!this._isIndexRendered(idx)) { |
3953 this.scrollToIndex(idx); | 2104 this.scrollToIndex(idx); |
3954 } | 2105 } |
3955 | |
3956 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 2106 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
3957 var model = physicalItem._templateInstance; | 2107 var model = physicalItem._templateInstance; |
3958 var focusable; | 2108 var focusable; |
3959 | |
3960 // set a secret tab index | |
3961 model.tabIndex = SECRET_TABINDEX; | 2109 model.tabIndex = SECRET_TABINDEX; |
3962 // check if focusable element is the physical item | |
3963 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 2110 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
3964 focusable = physicalItem; | 2111 focusable = physicalItem; |
3965 } | 2112 } |
3966 // search for the element which tabindex is bound to the secret tab index | |
3967 if (!focusable) { | 2113 if (!focusable) { |
3968 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 2114 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
3969 } | 2115 } |
3970 // restore the tab index | |
3971 model.tabIndex = 0; | 2116 model.tabIndex = 0; |
3972 // focus the focusable element | |
3973 this._focusedIndex = idx; | 2117 this._focusedIndex = idx; |
3974 focusable && focusable.focus(); | 2118 focusable && focusable.focus(); |
3975 }, | 2119 }, |
3976 | |
3977 _removeFocusedItem: function() { | 2120 _removeFocusedItem: function() { |
3978 if (this._offscreenFocusedItem) { | 2121 if (this._offscreenFocusedItem) { |
3979 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 2122 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
3980 } | 2123 } |
3981 this._offscreenFocusedItem = null; | 2124 this._offscreenFocusedItem = null; |
3982 this._focusBackfillItem = null; | 2125 this._focusBackfillItem = null; |
3983 this._focusedItem = null; | 2126 this._focusedItem = null; |
3984 this._focusedIndex = -1; | 2127 this._focusedIndex = -1; |
3985 }, | 2128 }, |
3986 | |
3987 _createFocusBackfillItem: function() { | 2129 _createFocusBackfillItem: function() { |
3988 var pidx, fidx = this._focusedIndex; | 2130 var pidx, fidx = this._focusedIndex; |
3989 if (this._offscreenFocusedItem || fidx < 0) { | 2131 if (this._offscreenFocusedItem || fidx < 0) { |
3990 return; | 2132 return; |
3991 } | 2133 } |
3992 if (!this._focusBackfillItem) { | 2134 if (!this._focusBackfillItem) { |
3993 // create a physical item, so that it backfills the focused item. | |
3994 var stampedTemplate = this.stamp(null); | 2135 var stampedTemplate = this.stamp(null); |
3995 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 2136 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
3996 Polymer.dom(this).appendChild(stampedTemplate.root); | 2137 Polymer.dom(this).appendChild(stampedTemplate.root); |
3997 } | 2138 } |
3998 // get the physical index for the focused index | |
3999 pidx = this._getPhysicalIndex(fidx); | 2139 pidx = this._getPhysicalIndex(fidx); |
4000 | |
4001 if (pidx != null) { | 2140 if (pidx != null) { |
4002 // set the offcreen focused physical item | |
4003 this._offscreenFocusedItem = this._physicalItems[pidx]; | 2141 this._offscreenFocusedItem = this._physicalItems[pidx]; |
4004 // backfill the focused physical item | |
4005 this._physicalItems[pidx] = this._focusBackfillItem; | 2142 this._physicalItems[pidx] = this._focusBackfillItem; |
4006 // hide the focused physical | |
4007 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 2143 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
4008 } | 2144 } |
4009 }, | 2145 }, |
4010 | |
4011 _restoreFocusedItem: function() { | 2146 _restoreFocusedItem: function() { |
4012 var pidx, fidx = this._focusedIndex; | 2147 var pidx, fidx = this._focusedIndex; |
4013 | |
4014 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 2148 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
4015 return; | 2149 return; |
4016 } | 2150 } |
4017 // assign models to the focused index | |
4018 this._assignModels(); | 2151 this._assignModels(); |
4019 // get the new physical index for the focused index | |
4020 pidx = this._getPhysicalIndex(fidx); | 2152 pidx = this._getPhysicalIndex(fidx); |
4021 | |
4022 if (pidx != null) { | 2153 if (pidx != null) { |
4023 // flip the focus backfill | |
4024 this._focusBackfillItem = this._physicalItems[pidx]; | 2154 this._focusBackfillItem = this._physicalItems[pidx]; |
4025 // restore the focused physical item | |
4026 this._physicalItems[pidx] = this._offscreenFocusedItem; | 2155 this._physicalItems[pidx] = this._offscreenFocusedItem; |
4027 // reset the offscreen focused item | |
4028 this._offscreenFocusedItem = null; | 2156 this._offscreenFocusedItem = null; |
4029 // hide the physical item that backfills | |
4030 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 2157 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
4031 } | 2158 } |
4032 }, | 2159 }, |
4033 | |
4034 _didFocus: function(e) { | 2160 _didFocus: function(e) { |
4035 var targetModel = this.modelForElement(e.target); | 2161 var targetModel = this.modelForElement(e.target); |
4036 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 2162 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
4037 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 2163 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
4038 var fidx = this._focusedIndex; | 2164 var fidx = this._focusedIndex; |
4039 | |
4040 if (!targetModel || !focusedModel) { | 2165 if (!targetModel || !focusedModel) { |
4041 return; | 2166 return; |
4042 } | 2167 } |
4043 if (focusedModel === targetModel) { | 2168 if (focusedModel === targetModel) { |
4044 // if the user focused the same item, then bring it into view if it's no
t visible | |
4045 if (!this._isIndexVisible(fidx)) { | 2169 if (!this._isIndexVisible(fidx)) { |
4046 this.scrollToIndex(fidx); | 2170 this.scrollToIndex(fidx); |
4047 } | 2171 } |
4048 } else { | 2172 } else { |
4049 this._restoreFocusedItem(); | 2173 this._restoreFocusedItem(); |
4050 // restore tabIndex for the currently focused item | |
4051 focusedModel.tabIndex = -1; | 2174 focusedModel.tabIndex = -1; |
4052 // set the tabIndex for the next focused item | |
4053 targetModel.tabIndex = 0; | 2175 targetModel.tabIndex = 0; |
4054 fidx = targetModel[this.indexAs]; | 2176 fidx = targetModel[this.indexAs]; |
4055 this._focusedIndex = fidx; | 2177 this._focusedIndex = fidx; |
4056 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 2178 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
4057 | |
4058 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 2179 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
4059 this._update(); | 2180 this._update(); |
4060 } | 2181 } |
4061 } | 2182 } |
4062 }, | 2183 }, |
4063 | |
4064 _didMoveUp: function() { | 2184 _didMoveUp: function() { |
4065 this._focusPhysicalItem(this._focusedIndex - 1); | 2185 this._focusPhysicalItem(this._focusedIndex - 1); |
4066 }, | 2186 }, |
4067 | |
4068 _didMoveDown: function(e) { | 2187 _didMoveDown: function(e) { |
4069 // disable scroll when pressing the down key | |
4070 e.detail.keyboardEvent.preventDefault(); | 2188 e.detail.keyboardEvent.preventDefault(); |
4071 this._focusPhysicalItem(this._focusedIndex + 1); | 2189 this._focusPhysicalItem(this._focusedIndex + 1); |
4072 }, | 2190 }, |
4073 | |
4074 _didEnter: function(e) { | 2191 _didEnter: function(e) { |
4075 this._focusPhysicalItem(this._focusedIndex); | 2192 this._focusPhysicalItem(this._focusedIndex); |
4076 this._selectionHandler(e.detail.keyboardEvent); | 2193 this._selectionHandler(e.detail.keyboardEvent); |
4077 } | 2194 } |
4078 }); | 2195 }); |
| 2196 })(); |
4079 | 2197 |
4080 })(); | |
4081 // Copyright 2015 The Chromium Authors. All rights reserved. | 2198 // Copyright 2015 The Chromium Authors. All rights reserved. |
4082 // Use of this source code is governed by a BSD-style license that can be | 2199 // Use of this source code is governed by a BSD-style license that can be |
4083 // found in the LICENSE file. | 2200 // found in the LICENSE file. |
4084 | |
4085 cr.define('downloads', function() { | 2201 cr.define('downloads', function() { |
4086 /** | |
4087 * @param {string} chromeSendName | |
4088 * @return {function(string):void} A chrome.send() callback with curried name. | |
4089 */ | |
4090 function chromeSendWithId(chromeSendName) { | 2202 function chromeSendWithId(chromeSendName) { |
4091 return function(id) { chrome.send(chromeSendName, [id]); }; | 2203 return function(id) { |
| 2204 chrome.send(chromeSendName, [ id ]); |
| 2205 }; |
4092 } | 2206 } |
4093 | |
4094 /** @constructor */ | |
4095 function ActionService() { | 2207 function ActionService() { |
4096 /** @private {Array<string>} */ | |
4097 this.searchTerms_ = []; | 2208 this.searchTerms_ = []; |
4098 } | 2209 } |
4099 | 2210 function trim(s) { |
4100 /** | 2211 return s.trim(); |
4101 * @param {string} s | 2212 } |
4102 * @return {string} |s| without whitespace at the beginning or end. | 2213 function truthy(value) { |
4103 */ | 2214 return !!value; |
4104 function trim(s) { return s.trim(); } | 2215 } |
4105 | |
4106 /** | |
4107 * @param {string|undefined} value | |
4108 * @return {boolean} Whether |value| is truthy. | |
4109 */ | |
4110 function truthy(value) { return !!value; } | |
4111 | |
4112 /** | |
4113 * @param {string} searchText Input typed by the user into a search box. | |
4114 * @return {Array<string>} A list of terms extracted from |searchText|. | |
4115 */ | |
4116 ActionService.splitTerms = function(searchText) { | 2216 ActionService.splitTerms = function(searchText) { |
4117 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
4118 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | 2217 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); |
4119 }; | 2218 }; |
4120 | |
4121 ActionService.prototype = { | 2219 ActionService.prototype = { |
4122 /** @param {string} id ID of the download to cancel. */ | |
4123 cancel: chromeSendWithId('cancel'), | 2220 cancel: chromeSendWithId('cancel'), |
4124 | |
4125 /** Instructs the browser to clear all finished downloads. */ | |
4126 clearAll: function() { | 2221 clearAll: function() { |
4127 if (loadTimeData.getBoolean('allowDeletingHistory')) { | 2222 if (loadTimeData.getBoolean('allowDeletingHistory')) { |
4128 chrome.send('clearAll'); | 2223 chrome.send('clearAll'); |
4129 this.search(''); | 2224 this.search(''); |
4130 } | 2225 } |
4131 }, | 2226 }, |
4132 | |
4133 /** @param {string} id ID of the dangerous download to discard. */ | |
4134 discardDangerous: chromeSendWithId('discardDangerous'), | 2227 discardDangerous: chromeSendWithId('discardDangerous'), |
4135 | |
4136 /** @param {string} url URL of a file to download. */ | |
4137 download: function(url) { | 2228 download: function(url) { |
4138 var a = document.createElement('a'); | 2229 var a = document.createElement('a'); |
4139 a.href = url; | 2230 a.href = url; |
4140 a.setAttribute('download', ''); | 2231 a.setAttribute('download', ''); |
4141 a.click(); | 2232 a.click(); |
4142 }, | 2233 }, |
4143 | |
4144 /** @param {string} id ID of the download that the user started dragging. */ | |
4145 drag: chromeSendWithId('drag'), | 2234 drag: chromeSendWithId('drag'), |
4146 | |
4147 /** Loads more downloads with the current search terms. */ | |
4148 loadMore: function() { | 2235 loadMore: function() { |
4149 chrome.send('getDownloads', this.searchTerms_); | 2236 chrome.send('getDownloads', this.searchTerms_); |
4150 }, | 2237 }, |
4151 | |
4152 /** | |
4153 * @return {boolean} Whether the user is currently searching for downloads | |
4154 * (i.e. has a non-empty search term). | |
4155 */ | |
4156 isSearching: function() { | 2238 isSearching: function() { |
4157 return this.searchTerms_.length > 0; | 2239 return this.searchTerms_.length > 0; |
4158 }, | 2240 }, |
4159 | |
4160 /** Opens the current local destination for downloads. */ | |
4161 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | 2241 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), |
4162 | |
4163 /** | |
4164 * @param {string} id ID of the download to run locally on the user's box. | |
4165 */ | |
4166 openFile: chromeSendWithId('openFile'), | 2242 openFile: chromeSendWithId('openFile'), |
4167 | |
4168 /** @param {string} id ID the of the progressing download to pause. */ | |
4169 pause: chromeSendWithId('pause'), | 2243 pause: chromeSendWithId('pause'), |
4170 | |
4171 /** @param {string} id ID of the finished download to remove. */ | |
4172 remove: chromeSendWithId('remove'), | 2244 remove: chromeSendWithId('remove'), |
4173 | |
4174 /** @param {string} id ID of the paused download to resume. */ | |
4175 resume: chromeSendWithId('resume'), | 2245 resume: chromeSendWithId('resume'), |
4176 | |
4177 /** | |
4178 * @param {string} id ID of the dangerous download to save despite | |
4179 * warnings. | |
4180 */ | |
4181 saveDangerous: chromeSendWithId('saveDangerous'), | 2246 saveDangerous: chromeSendWithId('saveDangerous'), |
4182 | |
4183 /** @param {string} searchText What to search for. */ | |
4184 search: function(searchText) { | 2247 search: function(searchText) { |
4185 var searchTerms = ActionService.splitTerms(searchText); | 2248 var searchTerms = ActionService.splitTerms(searchText); |
4186 var sameTerms = searchTerms.length == this.searchTerms_.length; | 2249 var sameTerms = searchTerms.length == this.searchTerms_.length; |
4187 | |
4188 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | 2250 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { |
4189 if (searchTerms[i] != this.searchTerms_[i]) | 2251 if (searchTerms[i] != this.searchTerms_[i]) sameTerms = false; |
4190 sameTerms = false; | |
4191 } | 2252 } |
4192 | 2253 if (sameTerms) return; |
4193 if (sameTerms) | |
4194 return; | |
4195 | |
4196 this.searchTerms_ = searchTerms; | 2254 this.searchTerms_ = searchTerms; |
4197 this.loadMore(); | 2255 this.loadMore(); |
4198 }, | 2256 }, |
| 2257 show: chromeSendWithId('show'), |
| 2258 undo: chrome.send.bind(chrome, 'undo') |
| 2259 }; |
| 2260 cr.addSingletonGetter(ActionService); |
| 2261 return { |
| 2262 ActionService: ActionService |
| 2263 }; |
| 2264 }); |
4199 | 2265 |
4200 /** | |
4201 * Shows the local folder a finished download resides in. | |
4202 * @param {string} id ID of the download to show. | |
4203 */ | |
4204 show: chromeSendWithId('show'), | |
4205 | |
4206 /** Undo download removal. */ | |
4207 undo: chrome.send.bind(chrome, 'undo'), | |
4208 }; | |
4209 | |
4210 cr.addSingletonGetter(ActionService); | |
4211 | |
4212 return {ActionService: ActionService}; | |
4213 }); | |
4214 // Copyright 2015 The Chromium Authors. All rights reserved. | 2266 // Copyright 2015 The Chromium Authors. All rights reserved. |
4215 // Use of this source code is governed by a BSD-style license that can be | 2267 // Use of this source code is governed by a BSD-style license that can be |
4216 // found in the LICENSE file. | 2268 // found in the LICENSE file. |
4217 | |
4218 cr.define('downloads', function() { | 2269 cr.define('downloads', function() { |
4219 /** | |
4220 * Explains why a download is in DANGEROUS state. | |
4221 * @enum {string} | |
4222 */ | |
4223 var DangerType = { | 2270 var DangerType = { |
4224 NOT_DANGEROUS: 'NOT_DANGEROUS', | 2271 NOT_DANGEROUS: 'NOT_DANGEROUS', |
4225 DANGEROUS_FILE: 'DANGEROUS_FILE', | 2272 DANGEROUS_FILE: 'DANGEROUS_FILE', |
4226 DANGEROUS_URL: 'DANGEROUS_URL', | 2273 DANGEROUS_URL: 'DANGEROUS_URL', |
4227 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | 2274 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', |
4228 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | 2275 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', |
4229 DANGEROUS_HOST: 'DANGEROUS_HOST', | 2276 DANGEROUS_HOST: 'DANGEROUS_HOST', |
4230 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | 2277 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED' |
4231 }; | 2278 }; |
4232 | |
4233 /** | |
4234 * The states a download can be in. These correspond to states defined in | |
4235 * DownloadsDOMHandler::CreateDownloadItemValue | |
4236 * @enum {string} | |
4237 */ | |
4238 var States = { | 2279 var States = { |
4239 IN_PROGRESS: 'IN_PROGRESS', | 2280 IN_PROGRESS: 'IN_PROGRESS', |
4240 CANCELLED: 'CANCELLED', | 2281 CANCELLED: 'CANCELLED', |
4241 COMPLETE: 'COMPLETE', | 2282 COMPLETE: 'COMPLETE', |
4242 PAUSED: 'PAUSED', | 2283 PAUSED: 'PAUSED', |
4243 DANGEROUS: 'DANGEROUS', | 2284 DANGEROUS: 'DANGEROUS', |
4244 INTERRUPTED: 'INTERRUPTED', | 2285 INTERRUPTED: 'INTERRUPTED' |
4245 }; | 2286 }; |
4246 | |
4247 return { | 2287 return { |
4248 DangerType: DangerType, | 2288 DangerType: DangerType, |
4249 States: States, | 2289 States: States |
4250 }; | 2290 }; |
4251 }); | 2291 }); |
| 2292 |
4252 // Copyright 2014 The Chromium Authors. All rights reserved. | 2293 // Copyright 2014 The Chromium Authors. All rights reserved. |
4253 // Use of this source code is governed by a BSD-style license that can be | 2294 // Use of this source code is governed by a BSD-style license that can be |
4254 // found in the LICENSE file. | 2295 // found in the LICENSE file. |
4255 | |
4256 // Action links are elements that are used to perform an in-page navigation or | |
4257 // action (e.g. showing a dialog). | |
4258 // | |
4259 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
4260 // they're subtly different as they're not initially underlined (giving users a | |
4261 // clue that underlined links navigate while action links don't). | |
4262 // | |
4263 // Action links look very similar to normal links when hovered (hand cursor, | |
4264 // underlined). This gives the user an idea that clicking this link will do | |
4265 // something similar to navigation but in the same page. | |
4266 // | |
4267 // They can be created in JavaScript like this: | |
4268 // | |
4269 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
4270 // | |
4271 // or with a constructor like this: | |
4272 // | |
4273 // var link = new ActionLink(); | |
4274 // | |
4275 // They can be used easily from HTML as well, like so: | |
4276 // | |
4277 // <a is="action-link">Click me!</a> | |
4278 // | |
4279 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
4280 | |
4281 /** | |
4282 * @constructor | |
4283 * @extends {HTMLAnchorElement} | |
4284 */ | |
4285 var ActionLink = document.registerElement('action-link', { | 2296 var ActionLink = document.registerElement('action-link', { |
4286 prototype: { | 2297 prototype: { |
4287 __proto__: HTMLAnchorElement.prototype, | 2298 __proto__: HTMLAnchorElement.prototype, |
4288 | |
4289 /** @this {ActionLink} */ | |
4290 createdCallback: function() { | 2299 createdCallback: function() { |
4291 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
4292 this.tabIndex = this.disabled ? -1 : 0; | 2300 this.tabIndex = this.disabled ? -1 : 0; |
4293 | 2301 if (!this.hasAttribute('role')) this.setAttribute('role', 'link'); |
4294 if (!this.hasAttribute('role')) | |
4295 this.setAttribute('role', 'link'); | |
4296 | |
4297 this.addEventListener('keydown', function(e) { | 2302 this.addEventListener('keydown', function(e) { |
4298 if (!this.disabled && e.key == 'Enter' && !this.href) { | 2303 if (!this.disabled && e.key == 'Enter' && !this.href) { |
4299 // Schedule a click asynchronously because other 'keydown' handlers | |
4300 // may still run later (e.g. document.addEventListener('keydown')). | |
4301 // Specifically options dialogs break when this timeout isn't here. | |
4302 // NOTE: this affects the "trusted" state of the ensuing click. I | |
4303 // haven't found anything that breaks because of this (yet). | |
4304 window.setTimeout(this.click.bind(this), 0); | 2304 window.setTimeout(this.click.bind(this), 0); |
4305 } | 2305 } |
4306 }); | 2306 }); |
4307 | |
4308 function preventDefault(e) { | 2307 function preventDefault(e) { |
4309 e.preventDefault(); | 2308 e.preventDefault(); |
4310 } | 2309 } |
4311 | |
4312 function removePreventDefault() { | 2310 function removePreventDefault() { |
4313 document.removeEventListener('selectstart', preventDefault); | 2311 document.removeEventListener('selectstart', preventDefault); |
4314 document.removeEventListener('mouseup', removePreventDefault); | 2312 document.removeEventListener('mouseup', removePreventDefault); |
4315 } | 2313 } |
4316 | |
4317 this.addEventListener('mousedown', function() { | 2314 this.addEventListener('mousedown', function() { |
4318 // This handlers strives to match the behavior of <a href="...">. | |
4319 | |
4320 // While the mouse is down, prevent text selection from dragging. | |
4321 document.addEventListener('selectstart', preventDefault); | 2315 document.addEventListener('selectstart', preventDefault); |
4322 document.addEventListener('mouseup', removePreventDefault); | 2316 document.addEventListener('mouseup', removePreventDefault); |
4323 | 2317 if (document.activeElement != this) this.classList.add('no-outline'); |
4324 // If focus started via mouse press, don't show an outline. | |
4325 if (document.activeElement != this) | |
4326 this.classList.add('no-outline'); | |
4327 }); | 2318 }); |
4328 | |
4329 this.addEventListener('blur', function() { | 2319 this.addEventListener('blur', function() { |
4330 this.classList.remove('no-outline'); | 2320 this.classList.remove('no-outline'); |
4331 }); | 2321 }); |
4332 }, | 2322 }, |
4333 | |
4334 /** @type {boolean} */ | |
4335 set disabled(disabled) { | 2323 set disabled(disabled) { |
4336 if (disabled) | 2324 if (disabled) HTMLAnchorElement.prototype.setAttribute.call(this, 'disable
d', ''); else HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled')
; |
4337 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', ''); | |
4338 else | |
4339 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled'); | |
4340 this.tabIndex = disabled ? -1 : 0; | 2325 this.tabIndex = disabled ? -1 : 0; |
4341 }, | 2326 }, |
4342 get disabled() { | 2327 get disabled() { |
4343 return this.hasAttribute('disabled'); | 2328 return this.hasAttribute('disabled'); |
4344 }, | 2329 }, |
4345 | |
4346 /** @override */ | |
4347 setAttribute: function(attr, val) { | 2330 setAttribute: function(attr, val) { |
4348 if (attr.toLowerCase() == 'disabled') | 2331 if (attr.toLowerCase() == 'disabled') this.disabled = true; else HTMLAncho
rElement.prototype.setAttribute.apply(this, arguments); |
4349 this.disabled = true; | 2332 }, |
4350 else | |
4351 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments); | |
4352 }, | |
4353 | |
4354 /** @override */ | |
4355 removeAttribute: function(attr) { | 2333 removeAttribute: function(attr) { |
4356 if (attr.toLowerCase() == 'disabled') | 2334 if (attr.toLowerCase() == 'disabled') this.disabled = false; else HTMLAnch
orElement.prototype.removeAttribute.apply(this, arguments); |
4357 this.disabled = false; | 2335 } |
4358 else | 2336 }, |
4359 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | 2337 "extends": 'a' |
4360 }, | |
4361 }, | |
4362 | |
4363 extends: 'a', | |
4364 }); | 2338 }); |
| 2339 |
4365 (function() { | 2340 (function() { |
4366 | 2341 var metaDatas = {}; |
4367 // monostate data | 2342 var metaArrays = {}; |
4368 var metaDatas = {}; | 2343 var singleton = null; |
4369 var metaArrays = {}; | 2344 Polymer.IronMeta = Polymer({ |
4370 var singleton = null; | 2345 is: 'iron-meta', |
4371 | 2346 properties: { |
4372 Polymer.IronMeta = Polymer({ | 2347 type: { |
4373 | 2348 type: String, |
4374 is: 'iron-meta', | 2349 value: 'default', |
4375 | 2350 observer: '_typeChanged' |
4376 properties: { | 2351 }, |
4377 | 2352 key: { |
4378 /** | 2353 type: String, |
4379 * The type of meta-data. All meta-data of the same type is stored | 2354 observer: '_keyChanged' |
4380 * together. | 2355 }, |
4381 */ | 2356 value: { |
4382 type: { | 2357 type: Object, |
4383 type: String, | 2358 notify: true, |
4384 value: 'default', | 2359 observer: '_valueChanged' |
4385 observer: '_typeChanged' | 2360 }, |
4386 }, | 2361 self: { |
4387 | 2362 type: Boolean, |
4388 /** | 2363 observer: '_selfChanged' |
4389 * The key used to store `value` under the `type` namespace. | 2364 }, |
4390 */ | 2365 list: { |
4391 key: { | 2366 type: Array, |
4392 type: String, | 2367 notify: true |
4393 observer: '_keyChanged' | 2368 } |
4394 }, | 2369 }, |
4395 | 2370 hostAttributes: { |
4396 /** | 2371 hidden: true |
4397 * The meta-data to store or retrieve. | 2372 }, |
4398 */ | 2373 factoryImpl: function(config) { |
4399 value: { | 2374 if (config) { |
4400 type: Object, | 2375 for (var n in config) { |
4401 notify: true, | 2376 switch (n) { |
4402 observer: '_valueChanged' | 2377 case 'type': |
4403 }, | 2378 case 'key': |
4404 | 2379 case 'value': |
4405 /** | 2380 this[n] = config[n]; |
4406 * If true, `value` is set to the iron-meta instance itself. | 2381 break; |
4407 */ | |
4408 self: { | |
4409 type: Boolean, | |
4410 observer: '_selfChanged' | |
4411 }, | |
4412 | |
4413 /** | |
4414 * Array of all meta-data values for the given type. | |
4415 */ | |
4416 list: { | |
4417 type: Array, | |
4418 notify: true | |
4419 } | |
4420 | |
4421 }, | |
4422 | |
4423 hostAttributes: { | |
4424 hidden: true | |
4425 }, | |
4426 | |
4427 /** | |
4428 * Only runs if someone invokes the factory/constructor directly | |
4429 * e.g. `new Polymer.IronMeta()` | |
4430 * | |
4431 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
4432 */ | |
4433 factoryImpl: function(config) { | |
4434 if (config) { | |
4435 for (var n in config) { | |
4436 switch(n) { | |
4437 case 'type': | |
4438 case 'key': | |
4439 case 'value': | |
4440 this[n] = config[n]; | |
4441 break; | |
4442 } | |
4443 } | 2382 } |
4444 } | 2383 } |
4445 }, | 2384 } |
4446 | 2385 }, |
4447 created: function() { | 2386 created: function() { |
4448 // TODO(sjmiles): good for debugging? | 2387 this._metaDatas = metaDatas; |
4449 this._metaDatas = metaDatas; | 2388 this._metaArrays = metaArrays; |
4450 this._metaArrays = metaArrays; | 2389 }, |
4451 }, | 2390 _keyChanged: function(key, old) { |
4452 | 2391 this._resetRegistration(old); |
4453 _keyChanged: function(key, old) { | 2392 }, |
4454 this._resetRegistration(old); | 2393 _valueChanged: function(value) { |
4455 }, | 2394 this._resetRegistration(this.key); |
4456 | 2395 }, |
4457 _valueChanged: function(value) { | 2396 _selfChanged: function(self) { |
4458 this._resetRegistration(this.key); | 2397 if (self) { |
4459 }, | 2398 this.value = this; |
4460 | 2399 } |
4461 _selfChanged: function(self) { | 2400 }, |
4462 if (self) { | 2401 _typeChanged: function(type) { |
4463 this.value = this; | 2402 this._unregisterKey(this.key); |
| 2403 if (!metaDatas[type]) { |
| 2404 metaDatas[type] = {}; |
| 2405 } |
| 2406 this._metaData = metaDatas[type]; |
| 2407 if (!metaArrays[type]) { |
| 2408 metaArrays[type] = []; |
| 2409 } |
| 2410 this.list = metaArrays[type]; |
| 2411 this._registerKeyValue(this.key, this.value); |
| 2412 }, |
| 2413 byKey: function(key) { |
| 2414 return this._metaData && this._metaData[key]; |
| 2415 }, |
| 2416 _resetRegistration: function(oldKey) { |
| 2417 this._unregisterKey(oldKey); |
| 2418 this._registerKeyValue(this.key, this.value); |
| 2419 }, |
| 2420 _unregisterKey: function(key) { |
| 2421 this._unregister(key, this._metaData, this.list); |
| 2422 }, |
| 2423 _registerKeyValue: function(key, value) { |
| 2424 this._register(key, value, this._metaData, this.list); |
| 2425 }, |
| 2426 _register: function(key, value, data, list) { |
| 2427 if (key && data && value !== undefined) { |
| 2428 data[key] = value; |
| 2429 list.push(value); |
| 2430 } |
| 2431 }, |
| 2432 _unregister: function(key, data, list) { |
| 2433 if (key && data) { |
| 2434 if (key in data) { |
| 2435 var value = data[key]; |
| 2436 delete data[key]; |
| 2437 this.arrayDelete(list, value); |
4464 } | 2438 } |
4465 }, | 2439 } |
4466 | 2440 } |
4467 _typeChanged: function(type) { | 2441 }); |
4468 this._unregisterKey(this.key); | 2442 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
4469 if (!metaDatas[type]) { | 2443 if (singleton === null) { |
4470 metaDatas[type] = {}; | 2444 singleton = new Polymer.IronMeta(); |
4471 } | 2445 } |
4472 this._metaData = metaDatas[type]; | 2446 return singleton; |
4473 if (!metaArrays[type]) { | 2447 }; |
4474 metaArrays[type] = []; | 2448 Polymer.IronMetaQuery = Polymer({ |
4475 } | 2449 is: 'iron-meta-query', |
4476 this.list = metaArrays[type]; | 2450 properties: { |
4477 this._registerKeyValue(this.key, this.value); | 2451 type: { |
4478 }, | 2452 type: String, |
4479 | 2453 value: 'default', |
4480 /** | 2454 observer: '_typeChanged' |
4481 * Retrieves meta data value by key. | 2455 }, |
4482 * | 2456 key: { |
4483 * @method byKey | 2457 type: String, |
4484 * @param {string} key The key of the meta-data to be returned. | 2458 observer: '_keyChanged' |
4485 * @return {*} | 2459 }, |
4486 */ | 2460 value: { |
4487 byKey: function(key) { | 2461 type: Object, |
4488 return this._metaData && this._metaData[key]; | 2462 notify: true, |
4489 }, | 2463 readOnly: true |
4490 | 2464 }, |
4491 _resetRegistration: function(oldKey) { | 2465 list: { |
4492 this._unregisterKey(oldKey); | 2466 type: Array, |
4493 this._registerKeyValue(this.key, this.value); | 2467 notify: true |
4494 }, | 2468 } |
4495 | 2469 }, |
4496 _unregisterKey: function(key) { | 2470 factoryImpl: function(config) { |
4497 this._unregister(key, this._metaData, this.list); | 2471 if (config) { |
4498 }, | 2472 for (var n in config) { |
4499 | 2473 switch (n) { |
4500 _registerKeyValue: function(key, value) { | 2474 case 'type': |
4501 this._register(key, value, this._metaData, this.list); | 2475 case 'key': |
4502 }, | 2476 this[n] = config[n]; |
4503 | 2477 break; |
4504 _register: function(key, value, data, list) { | |
4505 if (key && data && value !== undefined) { | |
4506 data[key] = value; | |
4507 list.push(value); | |
4508 } | |
4509 }, | |
4510 | |
4511 _unregister: function(key, data, list) { | |
4512 if (key && data) { | |
4513 if (key in data) { | |
4514 var value = data[key]; | |
4515 delete data[key]; | |
4516 this.arrayDelete(list, value); | |
4517 } | 2478 } |
4518 } | 2479 } |
4519 } | 2480 } |
4520 | 2481 }, |
4521 }); | 2482 created: function() { |
4522 | 2483 this._metaDatas = metaDatas; |
4523 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 2484 this._metaArrays = metaArrays; |
4524 if (singleton === null) { | 2485 }, |
4525 singleton = new Polymer.IronMeta(); | 2486 _keyChanged: function(key) { |
4526 } | 2487 this._setValue(this._metaData && this._metaData[key]); |
4527 return singleton; | 2488 }, |
4528 }; | 2489 _typeChanged: function(type) { |
4529 | 2490 this._metaData = metaDatas[type]; |
4530 /** | 2491 this.list = metaArrays[type]; |
4531 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | 2492 if (this.key) { |
4532 | 2493 this._keyChanged(this.key); |
4533 Examples: | 2494 } |
4534 | 2495 }, |
4535 If I create an instance like this: | 2496 byKey: function(key) { |
4536 | 2497 return this._metaData && this._metaData[key]; |
4537 <iron-meta key="info" value="foo/bar"></iron-meta> | 2498 } |
4538 | 2499 }); |
4539 Note that value="foo/bar" is the metadata I've defined. I could define more | 2500 })(); |
4540 attributes or use child nodes to define additional metadata. | 2501 |
4541 | 2502 Polymer({ |
4542 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | 2503 is: 'iron-icon', |
4543 | 2504 properties: { |
4544 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | 2505 icon: { |
4545 | 2506 type: String, |
4546 @group Polymer Iron Elements | 2507 observer: '_iconChanged' |
4547 @element iron-meta-query | 2508 }, |
4548 */ | 2509 theme: { |
4549 Polymer.IronMetaQuery = Polymer({ | 2510 type: String, |
4550 | 2511 observer: '_updateIcon' |
4551 is: 'iron-meta-query', | 2512 }, |
4552 | 2513 src: { |
4553 properties: { | 2514 type: String, |
4554 | 2515 observer: '_srcChanged' |
4555 /** | 2516 }, |
4556 * The type of meta-data. All meta-data of the same type is stored | 2517 _meta: { |
4557 * together. | 2518 value: Polymer.Base.create('iron-meta', { |
4558 */ | 2519 type: 'iconset' |
4559 type: { | 2520 }), |
4560 type: String, | 2521 observer: '_updateIcon' |
4561 value: 'default', | 2522 } |
4562 observer: '_typeChanged' | 2523 }, |
4563 }, | 2524 _DEFAULT_ICONSET: 'icons', |
4564 | 2525 _iconChanged: function(icon) { |
4565 /** | 2526 var parts = (icon || '').split(':'); |
4566 * Specifies a key to use for retrieving `value` from the `type` | 2527 this._iconName = parts.pop(); |
4567 * namespace. | 2528 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
4568 */ | 2529 this._updateIcon(); |
4569 key: { | 2530 }, |
4570 type: String, | 2531 _srcChanged: function(src) { |
4571 observer: '_keyChanged' | 2532 this._updateIcon(); |
4572 }, | 2533 }, |
4573 | 2534 _usesIconset: function() { |
4574 /** | 2535 return this.icon || !this.src; |
4575 * The meta-data to store or retrieve. | 2536 }, |
4576 */ | 2537 _updateIcon: function() { |
4577 value: { | 2538 if (this._usesIconset()) { |
4578 type: Object, | 2539 if (this._img && this._img.parentNode) { |
4579 notify: true, | 2540 Polymer.dom(this.root).removeChild(this._img); |
4580 readOnly: true | 2541 } |
4581 }, | 2542 if (this._iconName === "") { |
4582 | 2543 if (this._iconset) { |
4583 /** | 2544 this._iconset.removeIcon(this); |
4584 * Array of all meta-data values for the given type. | |
4585 */ | |
4586 list: { | |
4587 type: Array, | |
4588 notify: true | |
4589 } | 2545 } |
4590 | 2546 } else if (this._iconsetName && this._meta) { |
4591 }, | 2547 this._iconset = this._meta.byKey(this._iconsetName); |
4592 | 2548 if (this._iconset) { |
4593 /** | 2549 this._iconset.applyIcon(this, this._iconName, this.theme); |
4594 * Actually a factory method, not a true constructor. Only runs if | 2550 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
4595 * someone invokes it directly (via `new Polymer.IronMeta()`); | 2551 } else { |
4596 * | 2552 this.listen(window, 'iron-iconset-added', '_updateIcon'); |
4597 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
4598 */ | |
4599 factoryImpl: function(config) { | |
4600 if (config) { | |
4601 for (var n in config) { | |
4602 switch(n) { | |
4603 case 'type': | |
4604 case 'key': | |
4605 this[n] = config[n]; | |
4606 break; | |
4607 } | |
4608 } | |
4609 } | 2553 } |
4610 }, | 2554 } |
4611 | 2555 } else { |
4612 created: function() { | 2556 if (this._iconset) { |
4613 // TODO(sjmiles): good for debugging? | 2557 this._iconset.removeIcon(this); |
4614 this._metaDatas = metaDatas; | 2558 } |
4615 this._metaArrays = metaArrays; | 2559 if (!this._img) { |
4616 }, | 2560 this._img = document.createElement('img'); |
4617 | 2561 this._img.style.width = '100%'; |
4618 _keyChanged: function(key) { | 2562 this._img.style.height = '100%'; |
4619 this._setValue(this._metaData && this._metaData[key]); | 2563 this._img.draggable = false; |
4620 }, | 2564 } |
4621 | 2565 this._img.src = this.src; |
4622 _typeChanged: function(type) { | 2566 Polymer.dom(this.root).appendChild(this._img); |
4623 this._metaData = metaDatas[type]; | 2567 } |
4624 this.list = metaArrays[type]; | 2568 } |
4625 if (this.key) { | 2569 }); |
4626 this._keyChanged(this.key); | 2570 |
| 2571 Polymer.IronControlState = { |
| 2572 properties: { |
| 2573 focused: { |
| 2574 type: Boolean, |
| 2575 value: false, |
| 2576 notify: true, |
| 2577 readOnly: true, |
| 2578 reflectToAttribute: true |
| 2579 }, |
| 2580 disabled: { |
| 2581 type: Boolean, |
| 2582 value: false, |
| 2583 notify: true, |
| 2584 observer: '_disabledChanged', |
| 2585 reflectToAttribute: true |
| 2586 }, |
| 2587 _oldTabIndex: { |
| 2588 type: Number |
| 2589 }, |
| 2590 _boundFocusBlurHandler: { |
| 2591 type: Function, |
| 2592 value: function() { |
| 2593 return this._focusBlurHandler.bind(this); |
| 2594 } |
| 2595 } |
| 2596 }, |
| 2597 observers: [ '_changedControlState(focused, disabled)' ], |
| 2598 ready: function() { |
| 2599 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2600 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2601 }, |
| 2602 _focusBlurHandler: function(event) { |
| 2603 if (event.target === this) { |
| 2604 this._setFocused(event.type === 'focus'); |
| 2605 } else if (!this.shadowRoot) { |
| 2606 var target = Polymer.dom(event).localTarget; |
| 2607 if (!this.isLightDescendant(target)) { |
| 2608 this.fire(event.type, { |
| 2609 sourceEvent: event |
| 2610 }, { |
| 2611 node: this, |
| 2612 bubbles: event.bubbles, |
| 2613 cancelable: event.cancelable |
| 2614 }); |
| 2615 } |
| 2616 } |
| 2617 }, |
| 2618 _disabledChanged: function(disabled, old) { |
| 2619 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2620 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2621 if (disabled) { |
| 2622 this._oldTabIndex = this.tabIndex; |
| 2623 this._setFocused(false); |
| 2624 this.tabIndex = -1; |
| 2625 this.blur(); |
| 2626 } else if (this._oldTabIndex !== undefined) { |
| 2627 this.tabIndex = this._oldTabIndex; |
| 2628 } |
| 2629 }, |
| 2630 _changedControlState: function() { |
| 2631 if (this._controlStateChanged) { |
| 2632 this._controlStateChanged(); |
| 2633 } |
| 2634 } |
| 2635 }; |
| 2636 |
| 2637 Polymer.IronButtonStateImpl = { |
| 2638 properties: { |
| 2639 pressed: { |
| 2640 type: Boolean, |
| 2641 readOnly: true, |
| 2642 value: false, |
| 2643 reflectToAttribute: true, |
| 2644 observer: '_pressedChanged' |
| 2645 }, |
| 2646 toggles: { |
| 2647 type: Boolean, |
| 2648 value: false, |
| 2649 reflectToAttribute: true |
| 2650 }, |
| 2651 active: { |
| 2652 type: Boolean, |
| 2653 value: false, |
| 2654 notify: true, |
| 2655 reflectToAttribute: true |
| 2656 }, |
| 2657 pointerDown: { |
| 2658 type: Boolean, |
| 2659 readOnly: true, |
| 2660 value: false |
| 2661 }, |
| 2662 receivedFocusFromKeyboard: { |
| 2663 type: Boolean, |
| 2664 readOnly: true |
| 2665 }, |
| 2666 ariaActiveAttribute: { |
| 2667 type: String, |
| 2668 value: 'aria-pressed', |
| 2669 observer: '_ariaActiveAttributeChanged' |
| 2670 } |
| 2671 }, |
| 2672 listeners: { |
| 2673 down: '_downHandler', |
| 2674 up: '_upHandler', |
| 2675 tap: '_tapHandler' |
| 2676 }, |
| 2677 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi
veAttribute)' ], |
| 2678 keyBindings: { |
| 2679 'enter:keydown': '_asyncClick', |
| 2680 'space:keydown': '_spaceKeyDownHandler', |
| 2681 'space:keyup': '_spaceKeyUpHandler' |
| 2682 }, |
| 2683 _mouseEventRe: /^mouse/, |
| 2684 _tapHandler: function() { |
| 2685 if (this.toggles) { |
| 2686 this._userActivate(!this.active); |
| 2687 } else { |
| 2688 this.active = false; |
| 2689 } |
| 2690 }, |
| 2691 _detectKeyboardFocus: function(focused) { |
| 2692 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 2693 }, |
| 2694 _userActivate: function(active) { |
| 2695 if (this.active !== active) { |
| 2696 this.active = active; |
| 2697 this.fire('change'); |
| 2698 } |
| 2699 }, |
| 2700 _downHandler: function(event) { |
| 2701 this._setPointerDown(true); |
| 2702 this._setPressed(true); |
| 2703 this._setReceivedFocusFromKeyboard(false); |
| 2704 }, |
| 2705 _upHandler: function() { |
| 2706 this._setPointerDown(false); |
| 2707 this._setPressed(false); |
| 2708 }, |
| 2709 _spaceKeyDownHandler: function(event) { |
| 2710 var keyboardEvent = event.detail.keyboardEvent; |
| 2711 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2712 if (this.isLightDescendant(target)) return; |
| 2713 keyboardEvent.preventDefault(); |
| 2714 keyboardEvent.stopImmediatePropagation(); |
| 2715 this._setPressed(true); |
| 2716 }, |
| 2717 _spaceKeyUpHandler: function(event) { |
| 2718 var keyboardEvent = event.detail.keyboardEvent; |
| 2719 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2720 if (this.isLightDescendant(target)) return; |
| 2721 if (this.pressed) { |
| 2722 this._asyncClick(); |
| 2723 } |
| 2724 this._setPressed(false); |
| 2725 }, |
| 2726 _asyncClick: function() { |
| 2727 this.async(function() { |
| 2728 this.click(); |
| 2729 }, 1); |
| 2730 }, |
| 2731 _pressedChanged: function(pressed) { |
| 2732 this._changedButtonState(); |
| 2733 }, |
| 2734 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 2735 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 2736 this.removeAttribute(oldValue); |
| 2737 } |
| 2738 }, |
| 2739 _activeChanged: function(active, ariaActiveAttribute) { |
| 2740 if (this.toggles) { |
| 2741 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false'); |
| 2742 } else { |
| 2743 this.removeAttribute(this.ariaActiveAttribute); |
| 2744 } |
| 2745 this._changedButtonState(); |
| 2746 }, |
| 2747 _controlStateChanged: function() { |
| 2748 if (this.disabled) { |
| 2749 this._setPressed(false); |
| 2750 } else { |
| 2751 this._changedButtonState(); |
| 2752 } |
| 2753 }, |
| 2754 _changedButtonState: function() { |
| 2755 if (this._buttonStateChanged) { |
| 2756 this._buttonStateChanged(); |
| 2757 } |
| 2758 } |
| 2759 }; |
| 2760 |
| 2761 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat
eImpl ]; |
| 2762 |
| 2763 (function() { |
| 2764 var Utility = { |
| 2765 distance: function(x1, y1, x2, y2) { |
| 2766 var xDelta = x1 - x2; |
| 2767 var yDelta = y1 - y2; |
| 2768 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 2769 }, |
| 2770 now: window.performance && window.performance.now ? window.performance.now.b
ind(window.performance) : Date.now |
| 2771 }; |
| 2772 function ElementMetrics(element) { |
| 2773 this.element = element; |
| 2774 this.width = this.boundingRect.width; |
| 2775 this.height = this.boundingRect.height; |
| 2776 this.size = Math.max(this.width, this.height); |
| 2777 } |
| 2778 ElementMetrics.prototype = { |
| 2779 get boundingRect() { |
| 2780 return this.element.getBoundingClientRect(); |
| 2781 }, |
| 2782 furthestCornerDistanceFrom: function(x, y) { |
| 2783 var topLeft = Utility.distance(x, y, 0, 0); |
| 2784 var topRight = Utility.distance(x, y, this.width, 0); |
| 2785 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 2786 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 2787 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 2788 } |
| 2789 }; |
| 2790 function Ripple(element) { |
| 2791 this.element = element; |
| 2792 this.color = window.getComputedStyle(element).color; |
| 2793 this.wave = document.createElement('div'); |
| 2794 this.waveContainer = document.createElement('div'); |
| 2795 this.wave.style.backgroundColor = this.color; |
| 2796 this.wave.classList.add('wave'); |
| 2797 this.waveContainer.classList.add('wave-container'); |
| 2798 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 2799 this.resetInteractionState(); |
| 2800 } |
| 2801 Ripple.MAX_RADIUS = 300; |
| 2802 Ripple.prototype = { |
| 2803 get recenters() { |
| 2804 return this.element.recenters; |
| 2805 }, |
| 2806 get center() { |
| 2807 return this.element.center; |
| 2808 }, |
| 2809 get mouseDownElapsed() { |
| 2810 var elapsed; |
| 2811 if (!this.mouseDownStart) { |
| 2812 return 0; |
| 2813 } |
| 2814 elapsed = Utility.now() - this.mouseDownStart; |
| 2815 if (this.mouseUpStart) { |
| 2816 elapsed -= this.mouseUpElapsed; |
| 2817 } |
| 2818 return elapsed; |
| 2819 }, |
| 2820 get mouseUpElapsed() { |
| 2821 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0; |
| 2822 }, |
| 2823 get mouseDownElapsedSeconds() { |
| 2824 return this.mouseDownElapsed / 1e3; |
| 2825 }, |
| 2826 get mouseUpElapsedSeconds() { |
| 2827 return this.mouseUpElapsed / 1e3; |
| 2828 }, |
| 2829 get mouseInteractionSeconds() { |
| 2830 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 2831 }, |
| 2832 get initialOpacity() { |
| 2833 return this.element.initialOpacity; |
| 2834 }, |
| 2835 get opacityDecayVelocity() { |
| 2836 return this.element.opacityDecayVelocity; |
| 2837 }, |
| 2838 get radius() { |
| 2839 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 2840 var height2 = this.containerMetrics.height * this.containerMetrics.height; |
| 2841 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS)
* 1.1 + 5; |
| 2842 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS); |
| 2843 var timeNow = this.mouseInteractionSeconds / duration; |
| 2844 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 2845 return Math.abs(size); |
| 2846 }, |
| 2847 get opacity() { |
| 2848 if (!this.mouseUpStart) { |
| 2849 return this.initialOpacity; |
| 2850 } |
| 2851 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this
.opacityDecayVelocity); |
| 2852 }, |
| 2853 get outerOpacity() { |
| 2854 var outerOpacity = this.mouseUpElapsedSeconds * .3; |
| 2855 var waveOpacity = this.opacity; |
| 2856 return Math.max(0, Math.min(outerOpacity, waveOpacity)); |
| 2857 }, |
| 2858 get isOpacityFullyDecayed() { |
| 2859 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl
e.MAX_RADIUS); |
| 2860 }, |
| 2861 get isRestingAtMaxRadius() { |
| 2862 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this
.maxRadius, Ripple.MAX_RADIUS); |
| 2863 }, |
| 2864 get isAnimationComplete() { |
| 2865 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa
xRadius; |
| 2866 }, |
| 2867 get translationFraction() { |
| 2868 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr
t(2)); |
| 2869 }, |
| 2870 get xNow() { |
| 2871 if (this.xEnd) { |
| 2872 return this.xStart + this.translationFraction * (this.xEnd - this.xStart
); |
| 2873 } |
| 2874 return this.xStart; |
| 2875 }, |
| 2876 get yNow() { |
| 2877 if (this.yEnd) { |
| 2878 return this.yStart + this.translationFraction * (this.yEnd - this.yStart
); |
| 2879 } |
| 2880 return this.yStart; |
| 2881 }, |
| 2882 get isMouseDown() { |
| 2883 return this.mouseDownStart && !this.mouseUpStart; |
| 2884 }, |
| 2885 resetInteractionState: function() { |
| 2886 this.maxRadius = 0; |
| 2887 this.mouseDownStart = 0; |
| 2888 this.mouseUpStart = 0; |
| 2889 this.xStart = 0; |
| 2890 this.yStart = 0; |
| 2891 this.xEnd = 0; |
| 2892 this.yEnd = 0; |
| 2893 this.slideDistance = 0; |
| 2894 this.containerMetrics = new ElementMetrics(this.element); |
| 2895 }, |
| 2896 draw: function() { |
| 2897 var scale; |
| 2898 var translateString; |
| 2899 var dx; |
| 2900 var dy; |
| 2901 this.wave.style.opacity = this.opacity; |
| 2902 scale = this.radius / (this.containerMetrics.size / 2); |
| 2903 dx = this.xNow - this.containerMetrics.width / 2; |
| 2904 dy = this.yNow - this.containerMetrics.height / 2; |
| 2905 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy
+ 'px)'; |
| 2906 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + '
px, 0)'; |
| 2907 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 2908 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 2909 }, |
| 2910 downAction: function(event) { |
| 2911 var xCenter = this.containerMetrics.width / 2; |
| 2912 var yCenter = this.containerMetrics.height / 2; |
| 2913 this.resetInteractionState(); |
| 2914 this.mouseDownStart = Utility.now(); |
| 2915 if (this.center) { |
| 2916 this.xStart = xCenter; |
| 2917 this.yStart = yCenter; |
| 2918 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2919 } else { |
| 2920 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec
t.left : this.containerMetrics.width / 2; |
| 2921 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec
t.top : this.containerMetrics.height / 2; |
| 2922 } |
| 2923 if (this.recenters) { |
| 2924 this.xEnd = xCenter; |
| 2925 this.yEnd = yCenter; |
| 2926 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2927 } |
| 2928 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt
art, this.yStart); |
| 2929 this.waveContainer.style.top = (this.containerMetrics.height - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2930 this.waveContainer.style.left = (this.containerMetrics.width - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2931 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 2932 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 2933 }, |
| 2934 upAction: function(event) { |
| 2935 if (!this.isMouseDown) { |
| 2936 return; |
| 2937 } |
| 2938 this.mouseUpStart = Utility.now(); |
| 2939 }, |
| 2940 remove: function() { |
| 2941 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer)
; |
| 2942 } |
| 2943 }; |
| 2944 Polymer({ |
| 2945 is: 'paper-ripple', |
| 2946 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 2947 properties: { |
| 2948 initialOpacity: { |
| 2949 type: Number, |
| 2950 value: .25 |
| 2951 }, |
| 2952 opacityDecayVelocity: { |
| 2953 type: Number, |
| 2954 value: .8 |
| 2955 }, |
| 2956 recenters: { |
| 2957 type: Boolean, |
| 2958 value: false |
| 2959 }, |
| 2960 center: { |
| 2961 type: Boolean, |
| 2962 value: false |
| 2963 }, |
| 2964 ripples: { |
| 2965 type: Array, |
| 2966 value: function() { |
| 2967 return []; |
4627 } | 2968 } |
4628 }, | 2969 }, |
4629 | 2970 animating: { |
4630 /** | 2971 type: Boolean, |
4631 * Retrieves meta data value by key. | 2972 readOnly: true, |
4632 * @param {string} key The key of the meta-data to be returned. | 2973 reflectToAttribute: true, |
4633 * @return {*} | 2974 value: false |
4634 */ | 2975 }, |
4635 byKey: function(key) { | 2976 holdDown: { |
4636 return this._metaData && this._metaData[key]; | |
4637 } | |
4638 | |
4639 }); | |
4640 | |
4641 })(); | |
4642 Polymer({ | |
4643 | |
4644 is: 'iron-icon', | |
4645 | |
4646 properties: { | |
4647 | |
4648 /** | |
4649 * The name of the icon to use. The name should be of the form: | |
4650 * `iconset_name:icon_name`. | |
4651 */ | |
4652 icon: { | |
4653 type: String, | |
4654 observer: '_iconChanged' | |
4655 }, | |
4656 | |
4657 /** | |
4658 * The name of the theme to used, if one is specified by the | |
4659 * iconset. | |
4660 */ | |
4661 theme: { | |
4662 type: String, | |
4663 observer: '_updateIcon' | |
4664 }, | |
4665 | |
4666 /** | |
4667 * If using iron-icon without an iconset, you can set the src to be | |
4668 * the URL of an individual icon image file. Note that this will take | |
4669 * precedence over a given icon attribute. | |
4670 */ | |
4671 src: { | |
4672 type: String, | |
4673 observer: '_srcChanged' | |
4674 }, | |
4675 | |
4676 /** | |
4677 * @type {!Polymer.IronMeta} | |
4678 */ | |
4679 _meta: { | |
4680 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | |
4681 observer: '_updateIcon' | |
4682 } | |
4683 | |
4684 }, | |
4685 | |
4686 _DEFAULT_ICONSET: 'icons', | |
4687 | |
4688 _iconChanged: function(icon) { | |
4689 var parts = (icon || '').split(':'); | |
4690 this._iconName = parts.pop(); | |
4691 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; | |
4692 this._updateIcon(); | |
4693 }, | |
4694 | |
4695 _srcChanged: function(src) { | |
4696 this._updateIcon(); | |
4697 }, | |
4698 | |
4699 _usesIconset: function() { | |
4700 return this.icon || !this.src; | |
4701 }, | |
4702 | |
4703 /** @suppress {visibility} */ | |
4704 _updateIcon: function() { | |
4705 if (this._usesIconset()) { | |
4706 if (this._img && this._img.parentNode) { | |
4707 Polymer.dom(this.root).removeChild(this._img); | |
4708 } | |
4709 if (this._iconName === "") { | |
4710 if (this._iconset) { | |
4711 this._iconset.removeIcon(this); | |
4712 } | |
4713 } else if (this._iconsetName && this._meta) { | |
4714 this._iconset = /** @type {?Polymer.Iconset} */ ( | |
4715 this._meta.byKey(this._iconsetName)); | |
4716 if (this._iconset) { | |
4717 this._iconset.applyIcon(this, this._iconName, this.theme); | |
4718 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); | |
4719 } else { | |
4720 this.listen(window, 'iron-iconset-added', '_updateIcon'); | |
4721 } | |
4722 } | |
4723 } else { | |
4724 if (this._iconset) { | |
4725 this._iconset.removeIcon(this); | |
4726 } | |
4727 if (!this._img) { | |
4728 this._img = document.createElement('img'); | |
4729 this._img.style.width = '100%'; | |
4730 this._img.style.height = '100%'; | |
4731 this._img.draggable = false; | |
4732 } | |
4733 this._img.src = this.src; | |
4734 Polymer.dom(this.root).appendChild(this._img); | |
4735 } | |
4736 } | |
4737 | |
4738 }); | |
4739 /** | |
4740 * @demo demo/index.html | |
4741 * @polymerBehavior | |
4742 */ | |
4743 Polymer.IronControlState = { | |
4744 | |
4745 properties: { | |
4746 | |
4747 /** | |
4748 * If true, the element currently has focus. | |
4749 */ | |
4750 focused: { | |
4751 type: Boolean, | 2977 type: Boolean, |
4752 value: false, | 2978 value: false, |
4753 notify: true, | 2979 observer: '_holdDownChanged' |
4754 readOnly: true, | 2980 }, |
4755 reflectToAttribute: true | 2981 noink: { |
4756 }, | |
4757 | |
4758 /** | |
4759 * If true, the user cannot interact with this element. | |
4760 */ | |
4761 disabled: { | |
4762 type: Boolean, | 2982 type: Boolean, |
4763 value: false, | 2983 value: false |
4764 notify: true, | 2984 }, |
4765 observer: '_disabledChanged', | 2985 _animating: { |
4766 reflectToAttribute: true | 2986 type: Boolean |
4767 }, | 2987 }, |
4768 | 2988 _boundAnimate: { |
4769 _oldTabIndex: { | |
4770 type: Number | |
4771 }, | |
4772 | |
4773 _boundFocusBlurHandler: { | |
4774 type: Function, | 2989 type: Function, |
4775 value: function() { | 2990 value: function() { |
4776 return this._focusBlurHandler.bind(this); | 2991 return this.animate.bind(this); |
4777 } | 2992 } |
4778 } | 2993 } |
4779 | 2994 }, |
4780 }, | 2995 get target() { |
4781 | 2996 return this.keyEventTarget; |
4782 observers: [ | 2997 }, |
4783 '_changedControlState(focused, disabled)' | 2998 keyBindings: { |
4784 ], | 2999 'enter:keydown': '_onEnterKeydown', |
4785 | 3000 'space:keydown': '_onSpaceKeydown', |
4786 ready: function() { | 3001 'space:keyup': '_onSpaceKeyup' |
4787 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 3002 }, |
4788 this.addEventListener('blur', this._boundFocusBlurHandler, true); | 3003 attached: function() { |
4789 }, | 3004 if (this.parentNode.nodeType == 11) { |
4790 | 3005 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
4791 _focusBlurHandler: function(event) { | 3006 } else { |
4792 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | 3007 this.keyEventTarget = this.parentNode; |
4793 // eventually become `this` due to retargeting; if we are not in | 3008 } |
4794 // ShadowDOM land, `event.target` will eventually become `this` due | 3009 var keyEventTarget = this.keyEventTarget; |
4795 // to the second conditional which fires a synthetic event (that is also | 3010 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
4796 // handled). In either case, we can disregard `event.path`. | 3011 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
4797 | 3012 }, |
4798 if (event.target === this) { | 3013 detached: function() { |
4799 this._setFocused(event.type === 'focus'); | 3014 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
4800 } else if (!this.shadowRoot) { | 3015 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
4801 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | 3016 this.keyEventTarget = null; |
4802 if (!this.isLightDescendant(target)) { | 3017 }, |
4803 this.fire(event.type, {sourceEvent: event}, { | 3018 get shouldKeepAnimating() { |
4804 node: this, | 3019 for (var index = 0; index < this.ripples.length; ++index) { |
4805 bubbles: event.bubbles, | 3020 if (!this.ripples[index].isAnimationComplete) { |
4806 cancelable: event.cancelable | 3021 return true; |
4807 }); | |
4808 } | 3022 } |
4809 } | 3023 } |
4810 }, | 3024 return false; |
4811 | 3025 }, |
4812 _disabledChanged: function(disabled, old) { | 3026 simulatedRipple: function() { |
4813 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3027 this.downAction(null); |
4814 this.style.pointerEvents = disabled ? 'none' : ''; | 3028 this.async(function() { |
4815 if (disabled) { | 3029 this.upAction(); |
4816 this._oldTabIndex = this.tabIndex; | 3030 }, 1); |
4817 this._setFocused(false); | 3031 }, |
4818 this.tabIndex = -1; | 3032 uiDownAction: function(event) { |
4819 this.blur(); | 3033 if (!this.noink) { |
4820 } else if (this._oldTabIndex !== undefined) { | 3034 this.downAction(event); |
4821 this.tabIndex = this._oldTabIndex; | 3035 } |
4822 } | 3036 }, |
4823 }, | 3037 downAction: function(event) { |
4824 | 3038 if (this.holdDown && this.ripples.length > 0) { |
4825 _changedControlState: function() { | |
4826 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
4827 if (this._controlStateChanged) { | |
4828 this._controlStateChanged(); | |
4829 } | |
4830 } | |
4831 | |
4832 }; | |
4833 /** | |
4834 * @demo demo/index.html | |
4835 * @polymerBehavior Polymer.IronButtonState | |
4836 */ | |
4837 Polymer.IronButtonStateImpl = { | |
4838 | |
4839 properties: { | |
4840 | |
4841 /** | |
4842 * If true, the user is currently holding down the button. | |
4843 */ | |
4844 pressed: { | |
4845 type: Boolean, | |
4846 readOnly: true, | |
4847 value: false, | |
4848 reflectToAttribute: true, | |
4849 observer: '_pressedChanged' | |
4850 }, | |
4851 | |
4852 /** | |
4853 * If true, the button toggles the active state with each tap or press | |
4854 * of the spacebar. | |
4855 */ | |
4856 toggles: { | |
4857 type: Boolean, | |
4858 value: false, | |
4859 reflectToAttribute: true | |
4860 }, | |
4861 | |
4862 /** | |
4863 * If true, the button is a toggle and is currently in the active state. | |
4864 */ | |
4865 active: { | |
4866 type: Boolean, | |
4867 value: false, | |
4868 notify: true, | |
4869 reflectToAttribute: true | |
4870 }, | |
4871 | |
4872 /** | |
4873 * True if the element is currently being pressed by a "pointer," which | |
4874 * is loosely defined as mouse or touch input (but specifically excluding | |
4875 * keyboard input). | |
4876 */ | |
4877 pointerDown: { | |
4878 type: Boolean, | |
4879 readOnly: true, | |
4880 value: false | |
4881 }, | |
4882 | |
4883 /** | |
4884 * True if the input device that caused the element to receive focus | |
4885 * was a keyboard. | |
4886 */ | |
4887 receivedFocusFromKeyboard: { | |
4888 type: Boolean, | |
4889 readOnly: true | |
4890 }, | |
4891 | |
4892 /** | |
4893 * The aria attribute to be set if the button is a toggle and in the | |
4894 * active state. | |
4895 */ | |
4896 ariaActiveAttribute: { | |
4897 type: String, | |
4898 value: 'aria-pressed', | |
4899 observer: '_ariaActiveAttributeChanged' | |
4900 } | |
4901 }, | |
4902 | |
4903 listeners: { | |
4904 down: '_downHandler', | |
4905 up: '_upHandler', | |
4906 tap: '_tapHandler' | |
4907 }, | |
4908 | |
4909 observers: [ | |
4910 '_detectKeyboardFocus(focused)', | |
4911 '_activeChanged(active, ariaActiveAttribute)' | |
4912 ], | |
4913 | |
4914 keyBindings: { | |
4915 'enter:keydown': '_asyncClick', | |
4916 'space:keydown': '_spaceKeyDownHandler', | |
4917 'space:keyup': '_spaceKeyUpHandler', | |
4918 }, | |
4919 | |
4920 _mouseEventRe: /^mouse/, | |
4921 | |
4922 _tapHandler: function() { | |
4923 if (this.toggles) { | |
4924 // a tap is needed to toggle the active state | |
4925 this._userActivate(!this.active); | |
4926 } else { | |
4927 this.active = false; | |
4928 } | |
4929 }, | |
4930 | |
4931 _detectKeyboardFocus: function(focused) { | |
4932 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
4933 }, | |
4934 | |
4935 // to emulate native checkbox, (de-)activations from a user interaction fire | |
4936 // 'change' events | |
4937 _userActivate: function(active) { | |
4938 if (this.active !== active) { | |
4939 this.active = active; | |
4940 this.fire('change'); | |
4941 } | |
4942 }, | |
4943 | |
4944 _downHandler: function(event) { | |
4945 this._setPointerDown(true); | |
4946 this._setPressed(true); | |
4947 this._setReceivedFocusFromKeyboard(false); | |
4948 }, | |
4949 | |
4950 _upHandler: function() { | |
4951 this._setPointerDown(false); | |
4952 this._setPressed(false); | |
4953 }, | |
4954 | |
4955 /** | |
4956 * @param {!KeyboardEvent} event . | |
4957 */ | |
4958 _spaceKeyDownHandler: function(event) { | |
4959 var keyboardEvent = event.detail.keyboardEvent; | |
4960 var target = Polymer.dom(keyboardEvent).localTarget; | |
4961 | |
4962 // Ignore the event if this is coming from a focused light child, since th
at | |
4963 // element will deal with it. | |
4964 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4965 return; | 3039 return; |
4966 | 3040 } |
4967 keyboardEvent.preventDefault(); | 3041 var ripple = this.addRipple(); |
4968 keyboardEvent.stopImmediatePropagation(); | 3042 ripple.downAction(event); |
4969 this._setPressed(true); | 3043 if (!this._animating) { |
4970 }, | |
4971 | |
4972 /** | |
4973 * @param {!KeyboardEvent} event . | |
4974 */ | |
4975 _spaceKeyUpHandler: function(event) { | |
4976 var keyboardEvent = event.detail.keyboardEvent; | |
4977 var target = Polymer.dom(keyboardEvent).localTarget; | |
4978 | |
4979 // Ignore the event if this is coming from a focused light child, since th
at | |
4980 // element will deal with it. | |
4981 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4982 return; | |
4983 | |
4984 if (this.pressed) { | |
4985 this._asyncClick(); | |
4986 } | |
4987 this._setPressed(false); | |
4988 }, | |
4989 | |
4990 // trigger click asynchronously, the asynchrony is useful to allow one | |
4991 // event handler to unwind before triggering another event | |
4992 _asyncClick: function() { | |
4993 this.async(function() { | |
4994 this.click(); | |
4995 }, 1); | |
4996 }, | |
4997 | |
4998 // any of these changes are considered a change to button state | |
4999 | |
5000 _pressedChanged: function(pressed) { | |
5001 this._changedButtonState(); | |
5002 }, | |
5003 | |
5004 _ariaActiveAttributeChanged: function(value, oldValue) { | |
5005 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | |
5006 this.removeAttribute(oldValue); | |
5007 } | |
5008 }, | |
5009 | |
5010 _activeChanged: function(active, ariaActiveAttribute) { | |
5011 if (this.toggles) { | |
5012 this.setAttribute(this.ariaActiveAttribute, | |
5013 active ? 'true' : 'false'); | |
5014 } else { | |
5015 this.removeAttribute(this.ariaActiveAttribute); | |
5016 } | |
5017 this._changedButtonState(); | |
5018 }, | |
5019 | |
5020 _controlStateChanged: function() { | |
5021 if (this.disabled) { | |
5022 this._setPressed(false); | |
5023 } else { | |
5024 this._changedButtonState(); | |
5025 } | |
5026 }, | |
5027 | |
5028 // provide hook for follow-on behaviors to react to button-state | |
5029 | |
5030 _changedButtonState: function() { | |
5031 if (this._buttonStateChanged) { | |
5032 this._buttonStateChanged(); // abstract | |
5033 } | |
5034 } | |
5035 | |
5036 }; | |
5037 | |
5038 /** @polymerBehavior */ | |
5039 Polymer.IronButtonState = [ | |
5040 Polymer.IronA11yKeysBehavior, | |
5041 Polymer.IronButtonStateImpl | |
5042 ]; | |
5043 (function() { | |
5044 var Utility = { | |
5045 distance: function(x1, y1, x2, y2) { | |
5046 var xDelta = (x1 - x2); | |
5047 var yDelta = (y1 - y2); | |
5048 | |
5049 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
5050 }, | |
5051 | |
5052 now: window.performance && window.performance.now ? | |
5053 window.performance.now.bind(window.performance) : Date.now | |
5054 }; | |
5055 | |
5056 /** | |
5057 * @param {HTMLElement} element | |
5058 * @constructor | |
5059 */ | |
5060 function ElementMetrics(element) { | |
5061 this.element = element; | |
5062 this.width = this.boundingRect.width; | |
5063 this.height = this.boundingRect.height; | |
5064 | |
5065 this.size = Math.max(this.width, this.height); | |
5066 } | |
5067 | |
5068 ElementMetrics.prototype = { | |
5069 get boundingRect () { | |
5070 return this.element.getBoundingClientRect(); | |
5071 }, | |
5072 | |
5073 furthestCornerDistanceFrom: function(x, y) { | |
5074 var topLeft = Utility.distance(x, y, 0, 0); | |
5075 var topRight = Utility.distance(x, y, this.width, 0); | |
5076 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
5077 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
5078 | |
5079 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
5080 } | |
5081 }; | |
5082 | |
5083 /** | |
5084 * @param {HTMLElement} element | |
5085 * @constructor | |
5086 */ | |
5087 function Ripple(element) { | |
5088 this.element = element; | |
5089 this.color = window.getComputedStyle(element).color; | |
5090 | |
5091 this.wave = document.createElement('div'); | |
5092 this.waveContainer = document.createElement('div'); | |
5093 this.wave.style.backgroundColor = this.color; | |
5094 this.wave.classList.add('wave'); | |
5095 this.waveContainer.classList.add('wave-container'); | |
5096 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
5097 | |
5098 this.resetInteractionState(); | |
5099 } | |
5100 | |
5101 Ripple.MAX_RADIUS = 300; | |
5102 | |
5103 Ripple.prototype = { | |
5104 get recenters() { | |
5105 return this.element.recenters; | |
5106 }, | |
5107 | |
5108 get center() { | |
5109 return this.element.center; | |
5110 }, | |
5111 | |
5112 get mouseDownElapsed() { | |
5113 var elapsed; | |
5114 | |
5115 if (!this.mouseDownStart) { | |
5116 return 0; | |
5117 } | |
5118 | |
5119 elapsed = Utility.now() - this.mouseDownStart; | |
5120 | |
5121 if (this.mouseUpStart) { | |
5122 elapsed -= this.mouseUpElapsed; | |
5123 } | |
5124 | |
5125 return elapsed; | |
5126 }, | |
5127 | |
5128 get mouseUpElapsed() { | |
5129 return this.mouseUpStart ? | |
5130 Utility.now () - this.mouseUpStart : 0; | |
5131 }, | |
5132 | |
5133 get mouseDownElapsedSeconds() { | |
5134 return this.mouseDownElapsed / 1000; | |
5135 }, | |
5136 | |
5137 get mouseUpElapsedSeconds() { | |
5138 return this.mouseUpElapsed / 1000; | |
5139 }, | |
5140 | |
5141 get mouseInteractionSeconds() { | |
5142 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
5143 }, | |
5144 | |
5145 get initialOpacity() { | |
5146 return this.element.initialOpacity; | |
5147 }, | |
5148 | |
5149 get opacityDecayVelocity() { | |
5150 return this.element.opacityDecayVelocity; | |
5151 }, | |
5152 | |
5153 get radius() { | |
5154 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
5155 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
5156 var waveRadius = Math.min( | |
5157 Math.sqrt(width2 + height2), | |
5158 Ripple.MAX_RADIUS | |
5159 ) * 1.1 + 5; | |
5160 | |
5161 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
5162 var timeNow = this.mouseInteractionSeconds / duration; | |
5163 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
5164 | |
5165 return Math.abs(size); | |
5166 }, | |
5167 | |
5168 get opacity() { | |
5169 if (!this.mouseUpStart) { | |
5170 return this.initialOpacity; | |
5171 } | |
5172 | |
5173 return Math.max( | |
5174 0, | |
5175 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
5176 ); | |
5177 }, | |
5178 | |
5179 get outerOpacity() { | |
5180 // Linear increase in background opacity, capped at the opacity | |
5181 // of the wavefront (waveOpacity). | |
5182 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
5183 var waveOpacity = this.opacity; | |
5184 | |
5185 return Math.max( | |
5186 0, | |
5187 Math.min(outerOpacity, waveOpacity) | |
5188 ); | |
5189 }, | |
5190 | |
5191 get isOpacityFullyDecayed() { | |
5192 return this.opacity < 0.01 && | |
5193 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5194 }, | |
5195 | |
5196 get isRestingAtMaxRadius() { | |
5197 return this.opacity >= this.initialOpacity && | |
5198 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5199 }, | |
5200 | |
5201 get isAnimationComplete() { | |
5202 return this.mouseUpStart ? | |
5203 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
5204 }, | |
5205 | |
5206 get translationFraction() { | |
5207 return Math.min( | |
5208 1, | |
5209 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
5210 ); | |
5211 }, | |
5212 | |
5213 get xNow() { | |
5214 if (this.xEnd) { | |
5215 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
5216 } | |
5217 | |
5218 return this.xStart; | |
5219 }, | |
5220 | |
5221 get yNow() { | |
5222 if (this.yEnd) { | |
5223 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
5224 } | |
5225 | |
5226 return this.yStart; | |
5227 }, | |
5228 | |
5229 get isMouseDown() { | |
5230 return this.mouseDownStart && !this.mouseUpStart; | |
5231 }, | |
5232 | |
5233 resetInteractionState: function() { | |
5234 this.maxRadius = 0; | |
5235 this.mouseDownStart = 0; | |
5236 this.mouseUpStart = 0; | |
5237 | |
5238 this.xStart = 0; | |
5239 this.yStart = 0; | |
5240 this.xEnd = 0; | |
5241 this.yEnd = 0; | |
5242 this.slideDistance = 0; | |
5243 | |
5244 this.containerMetrics = new ElementMetrics(this.element); | |
5245 }, | |
5246 | |
5247 draw: function() { | |
5248 var scale; | |
5249 var translateString; | |
5250 var dx; | |
5251 var dy; | |
5252 | |
5253 this.wave.style.opacity = this.opacity; | |
5254 | |
5255 scale = this.radius / (this.containerMetrics.size / 2); | |
5256 dx = this.xNow - (this.containerMetrics.width / 2); | |
5257 dy = this.yNow - (this.containerMetrics.height / 2); | |
5258 | |
5259 | |
5260 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
5261 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
5262 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
5263 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
5264 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
5265 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
5266 }, | |
5267 | |
5268 /** @param {Event=} event */ | |
5269 downAction: function(event) { | |
5270 var xCenter = this.containerMetrics.width / 2; | |
5271 var yCenter = this.containerMetrics.height / 2; | |
5272 | |
5273 this.resetInteractionState(); | |
5274 this.mouseDownStart = Utility.now(); | |
5275 | |
5276 if (this.center) { | |
5277 this.xStart = xCenter; | |
5278 this.yStart = yCenter; | |
5279 this.slideDistance = Utility.distance( | |
5280 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5281 ); | |
5282 } else { | |
5283 this.xStart = event ? | |
5284 event.detail.x - this.containerMetrics.boundingRect.left : | |
5285 this.containerMetrics.width / 2; | |
5286 this.yStart = event ? | |
5287 event.detail.y - this.containerMetrics.boundingRect.top : | |
5288 this.containerMetrics.height / 2; | |
5289 } | |
5290 | |
5291 if (this.recenters) { | |
5292 this.xEnd = xCenter; | |
5293 this.yEnd = yCenter; | |
5294 this.slideDistance = Utility.distance( | |
5295 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5296 ); | |
5297 } | |
5298 | |
5299 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
5300 this.xStart, | |
5301 this.yStart | |
5302 ); | |
5303 | |
5304 this.waveContainer.style.top = | |
5305 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
5306 this.waveContainer.style.left = | |
5307 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
5308 | |
5309 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
5310 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
5311 }, | |
5312 | |
5313 /** @param {Event=} event */ | |
5314 upAction: function(event) { | |
5315 if (!this.isMouseDown) { | |
5316 return; | |
5317 } | |
5318 | |
5319 this.mouseUpStart = Utility.now(); | |
5320 }, | |
5321 | |
5322 remove: function() { | |
5323 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
5324 this.waveContainer | |
5325 ); | |
5326 } | |
5327 }; | |
5328 | |
5329 Polymer({ | |
5330 is: 'paper-ripple', | |
5331 | |
5332 behaviors: [ | |
5333 Polymer.IronA11yKeysBehavior | |
5334 ], | |
5335 | |
5336 properties: { | |
5337 /** | |
5338 * The initial opacity set on the wave. | |
5339 * | |
5340 * @attribute initialOpacity | |
5341 * @type number | |
5342 * @default 0.25 | |
5343 */ | |
5344 initialOpacity: { | |
5345 type: Number, | |
5346 value: 0.25 | |
5347 }, | |
5348 | |
5349 /** | |
5350 * How fast (opacity per second) the wave fades out. | |
5351 * | |
5352 * @attribute opacityDecayVelocity | |
5353 * @type number | |
5354 * @default 0.8 | |
5355 */ | |
5356 opacityDecayVelocity: { | |
5357 type: Number, | |
5358 value: 0.8 | |
5359 }, | |
5360 | |
5361 /** | |
5362 * If true, ripples will exhibit a gravitational pull towards | |
5363 * the center of their container as they fade away. | |
5364 * | |
5365 * @attribute recenters | |
5366 * @type boolean | |
5367 * @default false | |
5368 */ | |
5369 recenters: { | |
5370 type: Boolean, | |
5371 value: false | |
5372 }, | |
5373 | |
5374 /** | |
5375 * If true, ripples will center inside its container | |
5376 * | |
5377 * @attribute recenters | |
5378 * @type boolean | |
5379 * @default false | |
5380 */ | |
5381 center: { | |
5382 type: Boolean, | |
5383 value: false | |
5384 }, | |
5385 | |
5386 /** | |
5387 * A list of the visual ripples. | |
5388 * | |
5389 * @attribute ripples | |
5390 * @type Array | |
5391 * @default [] | |
5392 */ | |
5393 ripples: { | |
5394 type: Array, | |
5395 value: function() { | |
5396 return []; | |
5397 } | |
5398 }, | |
5399 | |
5400 /** | |
5401 * True when there are visible ripples animating within the | |
5402 * element. | |
5403 */ | |
5404 animating: { | |
5405 type: Boolean, | |
5406 readOnly: true, | |
5407 reflectToAttribute: true, | |
5408 value: false | |
5409 }, | |
5410 | |
5411 /** | |
5412 * If true, the ripple will remain in the "down" state until `holdDown` | |
5413 * is set to false again. | |
5414 */ | |
5415 holdDown: { | |
5416 type: Boolean, | |
5417 value: false, | |
5418 observer: '_holdDownChanged' | |
5419 }, | |
5420 | |
5421 /** | |
5422 * If true, the ripple will not generate a ripple effect | |
5423 * via pointer interaction. | |
5424 * Calling ripple's imperative api like `simulatedRipple` will | |
5425 * still generate the ripple effect. | |
5426 */ | |
5427 noink: { | |
5428 type: Boolean, | |
5429 value: false | |
5430 }, | |
5431 | |
5432 _animating: { | |
5433 type: Boolean | |
5434 }, | |
5435 | |
5436 _boundAnimate: { | |
5437 type: Function, | |
5438 value: function() { | |
5439 return this.animate.bind(this); | |
5440 } | |
5441 } | |
5442 }, | |
5443 | |
5444 get target () { | |
5445 return this.keyEventTarget; | |
5446 }, | |
5447 | |
5448 keyBindings: { | |
5449 'enter:keydown': '_onEnterKeydown', | |
5450 'space:keydown': '_onSpaceKeydown', | |
5451 'space:keyup': '_onSpaceKeyup' | |
5452 }, | |
5453 | |
5454 attached: function() { | |
5455 // Set up a11yKeysBehavior to listen to key events on the target, | |
5456 // so that space and enter activate the ripple even if the target doesn'
t | |
5457 // handle key events. The key handlers deal with `noink` themselves. | |
5458 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
5459 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
5460 } else { | |
5461 this.keyEventTarget = this.parentNode; | |
5462 } | |
5463 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
5464 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
5465 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
5466 }, | |
5467 | |
5468 detached: function() { | |
5469 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
5470 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
5471 this.keyEventTarget = null; | |
5472 }, | |
5473 | |
5474 get shouldKeepAnimating () { | |
5475 for (var index = 0; index < this.ripples.length; ++index) { | |
5476 if (!this.ripples[index].isAnimationComplete) { | |
5477 return true; | |
5478 } | |
5479 } | |
5480 | |
5481 return false; | |
5482 }, | |
5483 | |
5484 simulatedRipple: function() { | |
5485 this.downAction(null); | |
5486 | |
5487 // Please see polymer/polymer#1305 | |
5488 this.async(function() { | |
5489 this.upAction(); | |
5490 }, 1); | |
5491 }, | |
5492 | |
5493 /** | |
5494 * Provokes a ripple down effect via a UI event, | |
5495 * respecting the `noink` property. | |
5496 * @param {Event=} event | |
5497 */ | |
5498 uiDownAction: function(event) { | |
5499 if (!this.noink) { | |
5500 this.downAction(event); | |
5501 } | |
5502 }, | |
5503 | |
5504 /** | |
5505 * Provokes a ripple down effect via a UI event, | |
5506 * *not* respecting the `noink` property. | |
5507 * @param {Event=} event | |
5508 */ | |
5509 downAction: function(event) { | |
5510 if (this.holdDown && this.ripples.length > 0) { | |
5511 return; | |
5512 } | |
5513 | |
5514 var ripple = this.addRipple(); | |
5515 | |
5516 ripple.downAction(event); | |
5517 | |
5518 if (!this._animating) { | |
5519 this._animating = true; | |
5520 this.animate(); | |
5521 } | |
5522 }, | |
5523 | |
5524 /** | |
5525 * Provokes a ripple up effect via a UI event, | |
5526 * respecting the `noink` property. | |
5527 * @param {Event=} event | |
5528 */ | |
5529 uiUpAction: function(event) { | |
5530 if (!this.noink) { | |
5531 this.upAction(event); | |
5532 } | |
5533 }, | |
5534 | |
5535 /** | |
5536 * Provokes a ripple up effect via a UI event, | |
5537 * *not* respecting the `noink` property. | |
5538 * @param {Event=} event | |
5539 */ | |
5540 upAction: function(event) { | |
5541 if (this.holdDown) { | |
5542 return; | |
5543 } | |
5544 | |
5545 this.ripples.forEach(function(ripple) { | |
5546 ripple.upAction(event); | |
5547 }); | |
5548 | |
5549 this._animating = true; | 3044 this._animating = true; |
5550 this.animate(); | 3045 this.animate(); |
5551 }, | 3046 } |
5552 | 3047 }, |
5553 onAnimationComplete: function() { | 3048 uiUpAction: function(event) { |
5554 this._animating = false; | 3049 if (!this.noink) { |
5555 this.$.background.style.backgroundColor = null; | 3050 this.upAction(event); |
5556 this.fire('transitionend'); | 3051 } |
5557 }, | 3052 }, |
5558 | 3053 upAction: function(event) { |
5559 addRipple: function() { | 3054 if (this.holdDown) { |
5560 var ripple = new Ripple(this); | 3055 return; |
5561 | 3056 } |
5562 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | 3057 this.ripples.forEach(function(ripple) { |
5563 this.$.background.style.backgroundColor = ripple.color; | 3058 ripple.upAction(event); |
5564 this.ripples.push(ripple); | 3059 }); |
5565 | 3060 this._animating = true; |
5566 this._setAnimating(true); | 3061 this.animate(); |
5567 | 3062 }, |
5568 return ripple; | 3063 onAnimationComplete: function() { |
5569 }, | 3064 this._animating = false; |
5570 | 3065 this.$.background.style.backgroundColor = null; |
5571 removeRipple: function(ripple) { | 3066 this.fire('transitionend'); |
5572 var rippleIndex = this.ripples.indexOf(ripple); | 3067 }, |
5573 | 3068 addRipple: function() { |
5574 if (rippleIndex < 0) { | 3069 var ripple = new Ripple(this); |
5575 return; | 3070 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 3071 this.$.background.style.backgroundColor = ripple.color; |
| 3072 this.ripples.push(ripple); |
| 3073 this._setAnimating(true); |
| 3074 return ripple; |
| 3075 }, |
| 3076 removeRipple: function(ripple) { |
| 3077 var rippleIndex = this.ripples.indexOf(ripple); |
| 3078 if (rippleIndex < 0) { |
| 3079 return; |
| 3080 } |
| 3081 this.ripples.splice(rippleIndex, 1); |
| 3082 ripple.remove(); |
| 3083 if (!this.ripples.length) { |
| 3084 this._setAnimating(false); |
| 3085 } |
| 3086 }, |
| 3087 animate: function() { |
| 3088 if (!this._animating) { |
| 3089 return; |
| 3090 } |
| 3091 var index; |
| 3092 var ripple; |
| 3093 for (index = 0; index < this.ripples.length; ++index) { |
| 3094 ripple = this.ripples[index]; |
| 3095 ripple.draw(); |
| 3096 this.$.background.style.opacity = ripple.outerOpacity; |
| 3097 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 3098 this.removeRipple(ripple); |
5576 } | 3099 } |
5577 | 3100 } |
5578 this.ripples.splice(rippleIndex, 1); | 3101 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
5579 | 3102 this.onAnimationComplete(); |
5580 ripple.remove(); | 3103 } else { |
5581 | 3104 window.requestAnimationFrame(this._boundAnimate); |
5582 if (!this.ripples.length) { | 3105 } |
5583 this._setAnimating(false); | 3106 }, |
| 3107 _onEnterKeydown: function() { |
| 3108 this.uiDownAction(); |
| 3109 this.async(this.uiUpAction, 1); |
| 3110 }, |
| 3111 _onSpaceKeydown: function() { |
| 3112 this.uiDownAction(); |
| 3113 }, |
| 3114 _onSpaceKeyup: function() { |
| 3115 this.uiUpAction(); |
| 3116 }, |
| 3117 _holdDownChanged: function(newVal, oldVal) { |
| 3118 if (oldVal === undefined) { |
| 3119 return; |
| 3120 } |
| 3121 if (newVal) { |
| 3122 this.downAction(); |
| 3123 } else { |
| 3124 this.upAction(); |
| 3125 } |
| 3126 } |
| 3127 }); |
| 3128 })(); |
| 3129 |
| 3130 Polymer.PaperRippleBehavior = { |
| 3131 properties: { |
| 3132 noink: { |
| 3133 type: Boolean, |
| 3134 observer: '_noinkChanged' |
| 3135 }, |
| 3136 _rippleContainer: { |
| 3137 type: Object |
| 3138 } |
| 3139 }, |
| 3140 _buttonStateChanged: function() { |
| 3141 if (this.focused) { |
| 3142 this.ensureRipple(); |
| 3143 } |
| 3144 }, |
| 3145 _downHandler: function(event) { |
| 3146 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 3147 if (this.pressed) { |
| 3148 this.ensureRipple(event); |
| 3149 } |
| 3150 }, |
| 3151 ensureRipple: function(optTriggeringEvent) { |
| 3152 if (!this.hasRipple()) { |
| 3153 this._ripple = this._createRipple(); |
| 3154 this._ripple.noink = this.noink; |
| 3155 var rippleContainer = this._rippleContainer || this.root; |
| 3156 if (rippleContainer) { |
| 3157 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 3158 } |
| 3159 if (optTriggeringEvent) { |
| 3160 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 3161 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 3162 if (domContainer.deepContains(target)) { |
| 3163 this._ripple.uiDownAction(optTriggeringEvent); |
5584 } | 3164 } |
5585 }, | 3165 } |
5586 | 3166 } |
5587 animate: function() { | 3167 }, |
5588 if (!this._animating) { | 3168 getRipple: function() { |
5589 return; | 3169 this.ensureRipple(); |
5590 } | 3170 return this._ripple; |
5591 var index; | 3171 }, |
5592 var ripple; | 3172 hasRipple: function() { |
5593 | 3173 return Boolean(this._ripple); |
5594 for (index = 0; index < this.ripples.length; ++index) { | 3174 }, |
5595 ripple = this.ripples[index]; | 3175 _createRipple: function() { |
5596 | 3176 return document.createElement('paper-ripple'); |
5597 ripple.draw(); | 3177 }, |
5598 | 3178 _noinkChanged: function(noink) { |
5599 this.$.background.style.opacity = ripple.outerOpacity; | 3179 if (this.hasRipple()) { |
5600 | 3180 this._ripple.noink = noink; |
5601 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | 3181 } |
5602 this.removeRipple(ripple); | 3182 } |
5603 } | 3183 }; |
5604 } | 3184 |
5605 | 3185 Polymer.PaperButtonBehaviorImpl = { |
5606 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | 3186 properties: { |
5607 this.onAnimationComplete(); | 3187 elevation: { |
5608 } else { | 3188 type: Number, |
5609 window.requestAnimationFrame(this._boundAnimate); | 3189 reflectToAttribute: true, |
5610 } | 3190 readOnly: true |
5611 }, | 3191 } |
5612 | 3192 }, |
5613 _onEnterKeydown: function() { | 3193 observers: [ '_calculateElevation(focused, disabled, active, pressed, received
FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ], |
5614 this.uiDownAction(); | 3194 hostAttributes: { |
5615 this.async(this.uiUpAction, 1); | 3195 role: 'button', |
5616 }, | 3196 tabindex: '0', |
5617 | 3197 animated: true |
5618 _onSpaceKeydown: function() { | 3198 }, |
5619 this.uiDownAction(); | 3199 _calculateElevation: function() { |
5620 }, | 3200 var e = 1; |
5621 | 3201 if (this.disabled) { |
5622 _onSpaceKeyup: function() { | 3202 e = 0; |
5623 this.uiUpAction(); | 3203 } else if (this.active || this.pressed) { |
5624 }, | 3204 e = 4; |
5625 | 3205 } else if (this.receivedFocusFromKeyboard) { |
5626 // note: holdDown does not respect noink since it can be a focus based | 3206 e = 3; |
5627 // effect. | 3207 } |
5628 _holdDownChanged: function(newVal, oldVal) { | 3208 this._setElevation(e); |
5629 if (oldVal === undefined) { | 3209 }, |
5630 return; | 3210 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
5631 } | 3211 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
5632 if (newVal) { | 3212 }, |
5633 this.downAction(); | 3213 _spaceKeyDownHandler: function(event) { |
5634 } else { | 3214 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
5635 this.upAction(); | 3215 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
5636 } | 3216 this._ripple.uiDownAction(); |
5637 } | 3217 } |
5638 | 3218 }, |
5639 /** | 3219 _spaceKeyUpHandler: function(event) { |
5640 Fired when the animation finishes. | 3220 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
5641 This is useful if you want to wait until | 3221 if (this.hasRipple()) { |
5642 the ripple animation finishes to perform some action. | 3222 this._ripple.uiUpAction(); |
5643 | 3223 } |
5644 @event transitionend | 3224 } |
5645 @param {{node: Object}} detail Contains the animated node. | 3225 }; |
5646 */ | 3226 |
5647 }); | 3227 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat
e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ]; |
5648 })(); | 3228 |
5649 /** | |
5650 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
5651 * when the element has focus via pointer or keyboard. | |
5652 * | |
5653 * NOTE: This behavior is intended to be used in conjunction with and after | |
5654 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
5655 * | |
5656 * @polymerBehavior Polymer.PaperRippleBehavior | |
5657 */ | |
5658 Polymer.PaperRippleBehavior = { | |
5659 properties: { | |
5660 /** | |
5661 * If true, the element will not produce a ripple effect when interacted | |
5662 * with via the pointer. | |
5663 */ | |
5664 noink: { | |
5665 type: Boolean, | |
5666 observer: '_noinkChanged' | |
5667 }, | |
5668 | |
5669 /** | |
5670 * @type {Element|undefined} | |
5671 */ | |
5672 _rippleContainer: { | |
5673 type: Object, | |
5674 } | |
5675 }, | |
5676 | |
5677 /** | |
5678 * Ensures a `<paper-ripple>` element is available when the element is | |
5679 * focused. | |
5680 */ | |
5681 _buttonStateChanged: function() { | |
5682 if (this.focused) { | |
5683 this.ensureRipple(); | |
5684 } | |
5685 }, | |
5686 | |
5687 /** | |
5688 * In addition to the functionality provided in `IronButtonState`, ensures | |
5689 * a ripple effect is created when the element is in a `pressed` state. | |
5690 */ | |
5691 _downHandler: function(event) { | |
5692 Polymer.IronButtonStateImpl._downHandler.call(this, event); | |
5693 if (this.pressed) { | |
5694 this.ensureRipple(event); | |
5695 } | |
5696 }, | |
5697 | |
5698 /** | |
5699 * Ensures this element contains a ripple effect. For startup efficiency | |
5700 * the ripple effect is dynamically on demand when needed. | |
5701 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
5702 * ripple. | |
5703 */ | |
5704 ensureRipple: function(optTriggeringEvent) { | |
5705 if (!this.hasRipple()) { | |
5706 this._ripple = this._createRipple(); | |
5707 this._ripple.noink = this.noink; | |
5708 var rippleContainer = this._rippleContainer || this.root; | |
5709 if (rippleContainer) { | |
5710 Polymer.dom(rippleContainer).appendChild(this._ripple); | |
5711 } | |
5712 if (optTriggeringEvent) { | |
5713 // Check if the event happened inside of the ripple container | |
5714 // Fall back to host instead of the root because distributed text | |
5715 // nodes are not valid event targets | |
5716 var domContainer = Polymer.dom(this._rippleContainer || this); | |
5717 var target = Polymer.dom(optTriggeringEvent).rootTarget; | |
5718 if (domContainer.deepContains( /** @type {Node} */(target))) { | |
5719 this._ripple.uiDownAction(optTriggeringEvent); | |
5720 } | |
5721 } | |
5722 } | |
5723 }, | |
5724 | |
5725 /** | |
5726 * Returns the `<paper-ripple>` element used by this element to create | |
5727 * ripple effects. The element's ripple is created on demand, when | |
5728 * necessary, and calling this method will force the | |
5729 * ripple to be created. | |
5730 */ | |
5731 getRipple: function() { | |
5732 this.ensureRipple(); | |
5733 return this._ripple; | |
5734 }, | |
5735 | |
5736 /** | |
5737 * Returns true if this element currently contains a ripple effect. | |
5738 * @return {boolean} | |
5739 */ | |
5740 hasRipple: function() { | |
5741 return Boolean(this._ripple); | |
5742 }, | |
5743 | |
5744 /** | |
5745 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
5746 * Override this method to customize the ripple element. | |
5747 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
5748 */ | |
5749 _createRipple: function() { | |
5750 return /** @type {!PaperRippleElement} */ ( | |
5751 document.createElement('paper-ripple')); | |
5752 }, | |
5753 | |
5754 _noinkChanged: function(noink) { | |
5755 if (this.hasRipple()) { | |
5756 this._ripple.noink = noink; | |
5757 } | |
5758 } | |
5759 }; | |
5760 /** @polymerBehavior Polymer.PaperButtonBehavior */ | |
5761 Polymer.PaperButtonBehaviorImpl = { | |
5762 properties: { | |
5763 /** | |
5764 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
5765 * shadow, and each increasing number greater than 0 will be "deeper" | |
5766 * than the last. | |
5767 * | |
5768 * @attribute elevation | |
5769 * @type number | |
5770 * @default 1 | |
5771 */ | |
5772 elevation: { | |
5773 type: Number, | |
5774 reflectToAttribute: true, | |
5775 readOnly: true | |
5776 } | |
5777 }, | |
5778 | |
5779 observers: [ | |
5780 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
5781 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
5782 ], | |
5783 | |
5784 hostAttributes: { | |
5785 role: 'button', | |
5786 tabindex: '0', | |
5787 animated: true | |
5788 }, | |
5789 | |
5790 _calculateElevation: function() { | |
5791 var e = 1; | |
5792 if (this.disabled) { | |
5793 e = 0; | |
5794 } else if (this.active || this.pressed) { | |
5795 e = 4; | |
5796 } else if (this.receivedFocusFromKeyboard) { | |
5797 e = 3; | |
5798 } | |
5799 this._setElevation(e); | |
5800 }, | |
5801 | |
5802 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
5803 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
5804 }, | |
5805 | |
5806 /** | |
5807 * In addition to `IronButtonState` behavior, when space key goes down, | |
5808 * create a ripple down effect. | |
5809 * | |
5810 * @param {!KeyboardEvent} event . | |
5811 */ | |
5812 _spaceKeyDownHandler: function(event) { | |
5813 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
5814 // Ensure that there is at most one ripple when the space key is held down
. | |
5815 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
5816 this._ripple.uiDownAction(); | |
5817 } | |
5818 }, | |
5819 | |
5820 /** | |
5821 * In addition to `IronButtonState` behavior, when space key goes up, | |
5822 * create a ripple up effect. | |
5823 * | |
5824 * @param {!KeyboardEvent} event . | |
5825 */ | |
5826 _spaceKeyUpHandler: function(event) { | |
5827 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
5828 if (this.hasRipple()) { | |
5829 this._ripple.uiUpAction(); | |
5830 } | |
5831 } | |
5832 }; | |
5833 | |
5834 /** @polymerBehavior */ | |
5835 Polymer.PaperButtonBehavior = [ | |
5836 Polymer.IronButtonState, | |
5837 Polymer.IronControlState, | |
5838 Polymer.PaperRippleBehavior, | |
5839 Polymer.PaperButtonBehaviorImpl | |
5840 ]; | |
5841 Polymer({ | 3229 Polymer({ |
5842 is: 'paper-button', | 3230 is: 'paper-button', |
5843 | 3231 behaviors: [ Polymer.PaperButtonBehavior ], |
5844 behaviors: [ | 3232 properties: { |
5845 Polymer.PaperButtonBehavior | 3233 raised: { |
5846 ], | 3234 type: Boolean, |
5847 | 3235 reflectToAttribute: true, |
5848 properties: { | 3236 value: false, |
5849 /** | 3237 observer: '_calculateElevation' |
5850 * If true, the button should be styled with a shadow. | 3238 } |
5851 */ | 3239 }, |
5852 raised: { | 3240 _calculateElevation: function() { |
5853 type: Boolean, | 3241 if (!this.raised) { |
5854 reflectToAttribute: true, | 3242 this._setElevation(0); |
5855 value: false, | 3243 } else { |
5856 observer: '_calculateElevation' | 3244 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
5857 } | 3245 } |
5858 }, | 3246 } |
5859 | 3247 }); |
5860 _calculateElevation: function() { | 3248 |
5861 if (!this.raised) { | |
5862 this._setElevation(0); | |
5863 } else { | |
5864 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | |
5865 } | |
5866 } | |
5867 | |
5868 /** | |
5869 Fired when the animation finishes. | |
5870 This is useful if you want to wait until | |
5871 the ripple animation finishes to perform some action. | |
5872 | |
5873 @event transitionend | |
5874 Event param: {{node: Object}} detail Contains the animated node. | |
5875 */ | |
5876 }); | |
5877 Polymer({ | 3249 Polymer({ |
5878 is: 'paper-icon-button-light', | 3250 is: 'paper-icon-button-light', |
5879 extends: 'button', | 3251 "extends": 'button', |
5880 | 3252 behaviors: [ Polymer.PaperRippleBehavior ], |
5881 behaviors: [ | 3253 listeners: { |
5882 Polymer.PaperRippleBehavior | 3254 down: '_rippleDown', |
5883 ], | 3255 up: '_rippleUp', |
5884 | 3256 focus: '_rippleDown', |
5885 listeners: { | 3257 blur: '_rippleUp' |
5886 'down': '_rippleDown', | 3258 }, |
5887 'up': '_rippleUp', | 3259 _rippleDown: function() { |
5888 'focus': '_rippleDown', | 3260 this.getRipple().downAction(); |
5889 'blur': '_rippleUp', | 3261 }, |
5890 }, | 3262 _rippleUp: function() { |
5891 | 3263 this.getRipple().upAction(); |
5892 _rippleDown: function() { | 3264 }, |
5893 this.getRipple().downAction(); | 3265 ensureRipple: function(var_args) { |
5894 }, | 3266 var lastRipple = this._ripple; |
5895 | 3267 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
5896 _rippleUp: function() { | 3268 if (this._ripple && this._ripple !== lastRipple) { |
5897 this.getRipple().upAction(); | 3269 this._ripple.center = true; |
5898 }, | 3270 this._ripple.classList.add('circle'); |
5899 | 3271 } |
5900 /** | 3272 } |
5901 * @param {...*} var_args | 3273 }); |
5902 */ | 3274 |
5903 ensureRipple: function(var_args) { | 3275 Polymer.IronRangeBehavior = { |
5904 var lastRipple = this._ripple; | |
5905 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | |
5906 if (this._ripple && this._ripple !== lastRipple) { | |
5907 this._ripple.center = true; | |
5908 this._ripple.classList.add('circle'); | |
5909 } | |
5910 } | |
5911 }); | |
5912 /** | |
5913 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | |
5914 * | |
5915 * @demo demo/index.html | |
5916 * @polymerBehavior | |
5917 */ | |
5918 Polymer.IronRangeBehavior = { | |
5919 | |
5920 properties: { | 3276 properties: { |
5921 | |
5922 /** | |
5923 * The number that represents the current value. | |
5924 */ | |
5925 value: { | 3277 value: { |
5926 type: Number, | 3278 type: Number, |
5927 value: 0, | 3279 value: 0, |
5928 notify: true, | 3280 notify: true, |
5929 reflectToAttribute: true | 3281 reflectToAttribute: true |
5930 }, | 3282 }, |
5931 | |
5932 /** | |
5933 * The number that indicates the minimum value of the range. | |
5934 */ | |
5935 min: { | 3283 min: { |
5936 type: Number, | 3284 type: Number, |
5937 value: 0, | 3285 value: 0, |
5938 notify: true | 3286 notify: true |
5939 }, | 3287 }, |
5940 | |
5941 /** | |
5942 * The number that indicates the maximum value of the range. | |
5943 */ | |
5944 max: { | 3288 max: { |
5945 type: Number, | 3289 type: Number, |
5946 value: 100, | 3290 value: 100, |
5947 notify: true | 3291 notify: true |
5948 }, | 3292 }, |
5949 | |
5950 /** | |
5951 * Specifies the value granularity of the range's value. | |
5952 */ | |
5953 step: { | 3293 step: { |
5954 type: Number, | 3294 type: Number, |
5955 value: 1, | 3295 value: 1, |
5956 notify: true | 3296 notify: true |
5957 }, | 3297 }, |
5958 | |
5959 /** | |
5960 * Returns the ratio of the value. | |
5961 */ | |
5962 ratio: { | 3298 ratio: { |
5963 type: Number, | 3299 type: Number, |
5964 value: 0, | 3300 value: 0, |
5965 readOnly: true, | 3301 readOnly: true, |
5966 notify: true | 3302 notify: true |
5967 }, | 3303 } |
5968 }, | 3304 }, |
5969 | 3305 observers: [ '_update(value, min, max, step)' ], |
5970 observers: [ | |
5971 '_update(value, min, max, step)' | |
5972 ], | |
5973 | |
5974 _calcRatio: function(value) { | 3306 _calcRatio: function(value) { |
5975 return (this._clampValue(value) - this.min) / (this.max - this.min); | 3307 return (this._clampValue(value) - this.min) / (this.max - this.min); |
5976 }, | 3308 }, |
5977 | |
5978 _clampValue: function(value) { | 3309 _clampValue: function(value) { |
5979 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | 3310 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); |
5980 }, | 3311 }, |
5981 | |
5982 _calcStep: function(value) { | 3312 _calcStep: function(value) { |
5983 // polymer/issues/2493 | |
5984 value = parseFloat(value); | 3313 value = parseFloat(value); |
5985 | |
5986 if (!this.step) { | 3314 if (!this.step) { |
5987 return value; | 3315 return value; |
5988 } | 3316 } |
5989 | |
5990 var numSteps = Math.round((value - this.min) / this.step); | 3317 var numSteps = Math.round((value - this.min) / this.step); |
5991 if (this.step < 1) { | 3318 if (this.step < 1) { |
5992 /** | |
5993 * For small values of this.step, if we calculate the step using | |
5994 * `Math.round(value / step) * step` we may hit a precision point issue | |
5995 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
5996 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
5997 * | |
5998 * as a work around we can divide by the reciprocal of `step` | |
5999 */ | |
6000 return numSteps / (1 / this.step) + this.min; | 3319 return numSteps / (1 / this.step) + this.min; |
6001 } else { | 3320 } else { |
6002 return numSteps * this.step + this.min; | 3321 return numSteps * this.step + this.min; |
6003 } | 3322 } |
6004 }, | 3323 }, |
6005 | |
6006 _validateValue: function() { | 3324 _validateValue: function() { |
6007 var v = this._clampValue(this.value); | 3325 var v = this._clampValue(this.value); |
6008 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | 3326 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; |
6009 return this.value !== v; | 3327 return this.value !== v; |
6010 }, | 3328 }, |
6011 | |
6012 _update: function() { | 3329 _update: function() { |
6013 this._validateValue(); | 3330 this._validateValue(); |
6014 this._setRatio(this._calcRatio(this.value) * 100); | 3331 this._setRatio(this._calcRatio(this.value) * 100); |
6015 } | 3332 } |
6016 | |
6017 }; | 3333 }; |
| 3334 |
6018 Polymer({ | 3335 Polymer({ |
6019 is: 'paper-progress', | 3336 is: 'paper-progress', |
6020 | 3337 behaviors: [ Polymer.IronRangeBehavior ], |
6021 behaviors: [ | 3338 properties: { |
6022 Polymer.IronRangeBehavior | 3339 secondaryProgress: { |
6023 ], | 3340 type: Number, |
6024 | 3341 value: 0 |
6025 properties: { | 3342 }, |
6026 /** | 3343 secondaryRatio: { |
6027 * The number that represents the current secondary progress. | 3344 type: Number, |
6028 */ | 3345 value: 0, |
6029 secondaryProgress: { | 3346 readOnly: true |
6030 type: Number, | 3347 }, |
6031 value: 0 | 3348 indeterminate: { |
6032 }, | 3349 type: Boolean, |
6033 | 3350 value: false, |
6034 /** | 3351 observer: '_toggleIndeterminate' |
6035 * The secondary ratio | 3352 }, |
6036 */ | 3353 disabled: { |
6037 secondaryRatio: { | 3354 type: Boolean, |
6038 type: Number, | 3355 value: false, |
6039 value: 0, | 3356 reflectToAttribute: true, |
6040 readOnly: true | 3357 observer: '_disabledChanged' |
6041 }, | 3358 } |
6042 | 3359 }, |
6043 /** | 3360 observers: [ '_progressChanged(secondaryProgress, value, min, max)' ], |
6044 * Use an indeterminate progress indicator. | 3361 hostAttributes: { |
6045 */ | 3362 role: 'progressbar' |
6046 indeterminate: { | 3363 }, |
6047 type: Boolean, | 3364 _toggleIndeterminate: function(indeterminate) { |
6048 value: false, | 3365 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); |
6049 observer: '_toggleIndeterminate' | 3366 }, |
6050 }, | 3367 _transformProgress: function(progress, ratio) { |
6051 | 3368 var transform = 'scaleX(' + ratio / 100 + ')'; |
6052 /** | 3369 progress.style.transform = progress.style.webkitTransform = transform; |
6053 * True if the progress is disabled. | 3370 }, |
6054 */ | 3371 _mainRatioChanged: function(ratio) { |
6055 disabled: { | 3372 this._transformProgress(this.$.primaryProgress, ratio); |
6056 type: Boolean, | 3373 }, |
6057 value: false, | 3374 _progressChanged: function(secondaryProgress, value, min, max) { |
6058 reflectToAttribute: true, | 3375 secondaryProgress = this._clampValue(secondaryProgress); |
6059 observer: '_disabledChanged' | 3376 value = this._clampValue(value); |
6060 } | 3377 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; |
6061 }, | 3378 var mainRatio = this._calcRatio(value) * 100; |
6062 | 3379 this._setSecondaryRatio(secondaryRatio); |
6063 observers: [ | 3380 this._transformProgress(this.$.secondaryProgress, secondaryRatio); |
6064 '_progressChanged(secondaryProgress, value, min, max)' | 3381 this._transformProgress(this.$.primaryProgress, mainRatio); |
6065 ], | 3382 this.secondaryProgress = secondaryProgress; |
6066 | 3383 this.setAttribute('aria-valuenow', value); |
6067 hostAttributes: { | 3384 this.setAttribute('aria-valuemin', min); |
6068 role: 'progressbar' | 3385 this.setAttribute('aria-valuemax', max); |
6069 }, | 3386 }, |
6070 | 3387 _disabledChanged: function(disabled) { |
6071 _toggleIndeterminate: function(indeterminate) { | 3388 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
6072 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | 3389 }, |
6073 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | 3390 _hideSecondaryProgress: function(secondaryRatio) { |
6074 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 3391 return secondaryRatio === 0; |
6075 }, | 3392 } |
6076 | 3393 }); |
6077 _transformProgress: function(progress, ratio) { | 3394 |
6078 var transform = 'scaleX(' + (ratio / 100) + ')'; | 3395 Polymer({ |
6079 progress.style.transform = progress.style.webkitTransform = transform; | 3396 is: 'iron-iconset-svg', |
6080 }, | 3397 properties: { |
6081 | 3398 name: { |
6082 _mainRatioChanged: function(ratio) { | 3399 type: String, |
6083 this._transformProgress(this.$.primaryProgress, ratio); | 3400 observer: '_nameChanged' |
6084 }, | 3401 }, |
6085 | 3402 size: { |
6086 _progressChanged: function(secondaryProgress, value, min, max) { | 3403 type: Number, |
6087 secondaryProgress = this._clampValue(secondaryProgress); | 3404 value: 24 |
6088 value = this._clampValue(value); | 3405 } |
6089 | 3406 }, |
6090 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; | 3407 attached: function() { |
6091 var mainRatio = this._calcRatio(value) * 100; | 3408 this.style.display = 'none'; |
6092 | 3409 }, |
6093 this._setSecondaryRatio(secondaryRatio); | 3410 getIconNames: function() { |
6094 this._transformProgress(this.$.secondaryProgress, secondaryRatio); | 3411 this._icons = this._createIconMap(); |
6095 this._transformProgress(this.$.primaryProgress, mainRatio); | 3412 return Object.keys(this._icons).map(function(n) { |
6096 | 3413 return this.name + ':' + n; |
6097 this.secondaryProgress = secondaryProgress; | 3414 }, this); |
6098 | 3415 }, |
6099 this.setAttribute('aria-valuenow', value); | 3416 applyIcon: function(element, iconName) { |
6100 this.setAttribute('aria-valuemin', min); | 3417 element = element.root || element; |
6101 this.setAttribute('aria-valuemax', max); | 3418 this.removeIcon(element); |
6102 }, | 3419 var svg = this._cloneIcon(iconName); |
6103 | 3420 if (svg) { |
6104 _disabledChanged: function(disabled) { | 3421 var pde = Polymer.dom(element); |
6105 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3422 pde.insertBefore(svg, pde.childNodes[0]); |
6106 }, | 3423 return element._svgIcon = svg; |
6107 | 3424 } |
6108 _hideSecondaryProgress: function(secondaryRatio) { | 3425 return null; |
6109 return secondaryRatio === 0; | 3426 }, |
6110 } | 3427 removeIcon: function(element) { |
6111 }); | 3428 if (element._svgIcon) { |
6112 /** | 3429 Polymer.dom(element).removeChild(element._svgIcon); |
6113 * The `iron-iconset-svg` element allows users to define their own icon sets | 3430 element._svgIcon = null; |
6114 * that contain svg icons. The svg icon elements should be children of the | 3431 } |
6115 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | 3432 }, |
6116 * | 3433 _nameChanged: function() { |
6117 * Using svg elements to create icons has a few advantages over traditional | 3434 new Polymer.IronMeta({ |
6118 * bitmap graphics like jpg or png. Icons that use svg are vector based so | 3435 type: 'iconset', |
6119 * they are resolution independent and should look good on any device. They | 3436 key: this.name, |
6120 * are stylable via css. Icons can be themed, colorized, and even animated. | 3437 value: this |
6121 * | 3438 }); |
6122 * Example: | 3439 this.async(function() { |
6123 * | 3440 this.fire('iron-iconset-added', this, { |
6124 * <iron-iconset-svg name="my-svg-icons" size="24"> | 3441 node: window |
6125 * <svg> | |
6126 * <defs> | |
6127 * <g id="shape"> | |
6128 * <rect x="12" y="0" width="12" height="24" /> | |
6129 * <circle cx="12" cy="12" r="12" /> | |
6130 * </g> | |
6131 * </defs> | |
6132 * </svg> | |
6133 * </iron-iconset-svg> | |
6134 * | |
6135 * This will automatically register the icon set "my-svg-icons" to the iconset | |
6136 * database. To use these icons from within another element, make a | |
6137 * `iron-iconset` element and call the `byId` method | |
6138 * to retrieve a given iconset. To apply a particular icon inside an | |
6139 * element use the `applyIcon` method. For example: | |
6140 * | |
6141 * iconset.applyIcon(iconNode, 'car'); | |
6142 * | |
6143 * @element iron-iconset-svg | |
6144 * @demo demo/index.html | |
6145 * @implements {Polymer.Iconset} | |
6146 */ | |
6147 Polymer({ | |
6148 is: 'iron-iconset-svg', | |
6149 | |
6150 properties: { | |
6151 | |
6152 /** | |
6153 * The name of the iconset. | |
6154 */ | |
6155 name: { | |
6156 type: String, | |
6157 observer: '_nameChanged' | |
6158 }, | |
6159 | |
6160 /** | |
6161 * The size of an individual icon. Note that icons must be square. | |
6162 */ | |
6163 size: { | |
6164 type: Number, | |
6165 value: 24 | |
6166 } | |
6167 | |
6168 }, | |
6169 | |
6170 attached: function() { | |
6171 this.style.display = 'none'; | |
6172 }, | |
6173 | |
6174 /** | |
6175 * Construct an array of all icon names in this iconset. | |
6176 * | |
6177 * @return {!Array} Array of icon names. | |
6178 */ | |
6179 getIconNames: function() { | |
6180 this._icons = this._createIconMap(); | |
6181 return Object.keys(this._icons).map(function(n) { | |
6182 return this.name + ':' + n; | |
6183 }, this); | |
6184 }, | |
6185 | |
6186 /** | |
6187 * Applies an icon to the given element. | |
6188 * | |
6189 * An svg icon is prepended to the element's shadowRoot if it exists, | |
6190 * otherwise to the element itself. | |
6191 * | |
6192 * @method applyIcon | |
6193 * @param {Element} element Element to which the icon is applied. | |
6194 * @param {string} iconName Name of the icon to apply. | |
6195 * @return {?Element} The svg element which renders the icon. | |
6196 */ | |
6197 applyIcon: function(element, iconName) { | |
6198 // insert svg element into shadow root, if it exists | |
6199 element = element.root || element; | |
6200 // Remove old svg element | |
6201 this.removeIcon(element); | |
6202 // install new svg element | |
6203 var svg = this._cloneIcon(iconName); | |
6204 if (svg) { | |
6205 var pde = Polymer.dom(element); | |
6206 pde.insertBefore(svg, pde.childNodes[0]); | |
6207 return element._svgIcon = svg; | |
6208 } | |
6209 return null; | |
6210 }, | |
6211 | |
6212 /** | |
6213 * Remove an icon from the given element by undoing the changes effected | |
6214 * by `applyIcon`. | |
6215 * | |
6216 * @param {Element} element The element from which the icon is removed. | |
6217 */ | |
6218 removeIcon: function(element) { | |
6219 // Remove old svg element | |
6220 if (element._svgIcon) { | |
6221 Polymer.dom(element).removeChild(element._svgIcon); | |
6222 element._svgIcon = null; | |
6223 } | |
6224 }, | |
6225 | |
6226 /** | |
6227 * | |
6228 * When name is changed, register iconset metadata | |
6229 * | |
6230 */ | |
6231 _nameChanged: function() { | |
6232 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | |
6233 this.async(function() { | |
6234 this.fire('iron-iconset-added', this, {node: window}); | |
6235 }); | 3442 }); |
6236 }, | 3443 }); |
6237 | 3444 }, |
6238 /** | 3445 _createIconMap: function() { |
6239 * Create a map of child SVG elements by id. | 3446 var icons = Object.create(null); |
6240 * | 3447 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) { |
6241 * @return {!Object} Map of id's to SVG elements. | 3448 icons[icon.id] = icon; |
6242 */ | 3449 }); |
6243 _createIconMap: function() { | 3450 return icons; |
6244 // Objects chained to Object.prototype (`{}`) have members. Specifically, | 3451 }, |
6245 // on FF there is a `watch` method that confuses the icon map, so we | 3452 _cloneIcon: function(id) { |
6246 // need to use a null-based object here. | 3453 this._icons = this._icons || this._createIconMap(); |
6247 var icons = Object.create(null); | 3454 return this._prepareSvgClone(this._icons[id], this.size); |
6248 Polymer.dom(this).querySelectorAll('[id]') | 3455 }, |
6249 .forEach(function(icon) { | 3456 _prepareSvgClone: function(sourceSvg, size) { |
6250 icons[icon.id] = icon; | 3457 if (sourceSvg) { |
6251 }); | 3458 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h
ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') ||
'0 0 ' + size + ' ' + size; |
6252 return icons; | 3459 svg.setAttribute('viewBox', viewBox); |
6253 }, | 3460 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
6254 | 3461 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he
ight: 100%;'; |
6255 /** | 3462 svg.appendChild(content).removeAttribute('id'); |
6256 * Produce installable clone of the SVG element matching `id` in this | 3463 return svg; |
6257 * iconset, or `undefined` if there is no matching element. | 3464 } |
6258 * | 3465 return null; |
6259 * @return {Element} Returns an installable clone of the SVG element | 3466 } |
6260 * matching `id`. | 3467 }); |
6261 */ | 3468 |
6262 _cloneIcon: function(id) { | |
6263 // create the icon map on-demand, since the iconset itself has no discrete | |
6264 // signal to know when it's children are fully parsed | |
6265 this._icons = this._icons || this._createIconMap(); | |
6266 return this._prepareSvgClone(this._icons[id], this.size); | |
6267 }, | |
6268 | |
6269 /** | |
6270 * @param {Element} sourceSvg | |
6271 * @param {number} size | |
6272 * @return {Element} | |
6273 */ | |
6274 _prepareSvgClone: function(sourceSvg, size) { | |
6275 if (sourceSvg) { | |
6276 var content = sourceSvg.cloneNode(true), | |
6277 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | |
6278 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | |
6279 svg.setAttribute('viewBox', viewBox); | |
6280 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | |
6281 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
6282 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
6283 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | |
6284 svg.appendChild(content).removeAttribute('id'); | |
6285 return svg; | |
6286 } | |
6287 return null; | |
6288 } | |
6289 | |
6290 }); | |
6291 // Copyright 2015 The Chromium Authors. All rights reserved. | 3469 // Copyright 2015 The Chromium Authors. All rights reserved. |
6292 // Use of this source code is governed by a BSD-style license that can be | 3470 // Use of this source code is governed by a BSD-style license that can be |
6293 // found in the LICENSE file. | 3471 // found in the LICENSE file. |
6294 | |
6295 cr.define('downloads', function() { | 3472 cr.define('downloads', function() { |
6296 var Item = Polymer({ | 3473 var Item = Polymer({ |
6297 is: 'downloads-item', | 3474 is: 'downloads-item', |
6298 | |
6299 properties: { | 3475 properties: { |
6300 data: { | 3476 data: { |
6301 type: Object, | 3477 type: Object |
6302 }, | 3478 }, |
6303 | |
6304 completelyOnDisk_: { | 3479 completelyOnDisk_: { |
6305 computed: 'computeCompletelyOnDisk_(' + | 3480 computed: 'computeCompletelyOnDisk_(' + 'data.state, data.file_externall
y_removed)', |
6306 'data.state, data.file_externally_removed)', | |
6307 type: Boolean, | 3481 type: Boolean, |
6308 value: true, | 3482 value: true |
6309 }, | 3483 }, |
6310 | |
6311 controlledBy_: { | 3484 controlledBy_: { |
6312 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', | 3485 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', |
6313 type: String, | 3486 type: String, |
6314 value: '', | 3487 value: '' |
6315 }, | 3488 }, |
6316 | |
6317 isActive_: { | 3489 isActive_: { |
6318 computed: 'computeIsActive_(' + | 3490 computed: 'computeIsActive_(' + 'data.state, data.file_externally_remove
d)', |
6319 'data.state, data.file_externally_removed)', | |
6320 type: Boolean, | 3491 type: Boolean, |
6321 value: true, | 3492 value: true |
6322 }, | 3493 }, |
6323 | |
6324 isDangerous_: { | 3494 isDangerous_: { |
6325 computed: 'computeIsDangerous_(data.state)', | 3495 computed: 'computeIsDangerous_(data.state)', |
6326 type: Boolean, | 3496 type: Boolean, |
6327 value: false, | 3497 value: false |
6328 }, | 3498 }, |
6329 | |
6330 isMalware_: { | 3499 isMalware_: { |
6331 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', | 3500 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', |
6332 type: Boolean, | 3501 type: Boolean, |
6333 value: false, | 3502 value: false |
6334 }, | 3503 }, |
6335 | |
6336 isInProgress_: { | 3504 isInProgress_: { |
6337 computed: 'computeIsInProgress_(data.state)', | 3505 computed: 'computeIsInProgress_(data.state)', |
6338 type: Boolean, | 3506 type: Boolean, |
6339 value: false, | 3507 value: false |
6340 }, | 3508 }, |
6341 | |
6342 pauseOrResumeText_: { | 3509 pauseOrResumeText_: { |
6343 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', | 3510 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', |
6344 type: String, | 3511 type: String |
6345 }, | 3512 }, |
6346 | |
6347 showCancel_: { | 3513 showCancel_: { |
6348 computed: 'computeShowCancel_(data.state)', | 3514 computed: 'computeShowCancel_(data.state)', |
6349 type: Boolean, | 3515 type: Boolean, |
6350 value: false, | 3516 value: false |
6351 }, | 3517 }, |
6352 | |
6353 showProgress_: { | 3518 showProgress_: { |
6354 computed: 'computeShowProgress_(showCancel_, data.percent)', | 3519 computed: 'computeShowProgress_(showCancel_, data.percent)', |
6355 type: Boolean, | 3520 type: Boolean, |
6356 value: false, | 3521 value: false |
6357 }, | 3522 } |
6358 }, | 3523 }, |
6359 | 3524 observers: [ 'observeControlledBy_(controlledBy_)', 'observeIsDangerous_(isD
angerous_, data)' ], |
6360 observers: [ | |
6361 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | |
6362 // and data.by_ext_name directly. Why? | |
6363 'observeControlledBy_(controlledBy_)', | |
6364 'observeIsDangerous_(isDangerous_, data)', | |
6365 ], | |
6366 | |
6367 ready: function() { | 3525 ready: function() { |
6368 this.content = this.$.content; | 3526 this.content = this.$.content; |
6369 }, | 3527 }, |
6370 | |
6371 /** @private */ | |
6372 computeClass_: function() { | 3528 computeClass_: function() { |
6373 var classes = []; | 3529 var classes = []; |
6374 | 3530 if (this.isActive_) classes.push('is-active'); |
6375 if (this.isActive_) | 3531 if (this.isDangerous_) classes.push('dangerous'); |
6376 classes.push('is-active'); | 3532 if (this.showProgress_) classes.push('show-progress'); |
6377 | |
6378 if (this.isDangerous_) | |
6379 classes.push('dangerous'); | |
6380 | |
6381 if (this.showProgress_) | |
6382 classes.push('show-progress'); | |
6383 | |
6384 return classes.join(' '); | 3533 return classes.join(' '); |
6385 }, | 3534 }, |
6386 | |
6387 /** @private */ | |
6388 computeCompletelyOnDisk_: function() { | 3535 computeCompletelyOnDisk_: function() { |
6389 return this.data.state == downloads.States.COMPLETE && | 3536 return this.data.state == downloads.States.COMPLETE && !this.data.file_ext
ernally_removed; |
6390 !this.data.file_externally_removed; | 3537 }, |
6391 }, | |
6392 | |
6393 /** @private */ | |
6394 computeControlledBy_: function() { | 3538 computeControlledBy_: function() { |
6395 if (!this.data.by_ext_id || !this.data.by_ext_name) | 3539 if (!this.data.by_ext_id || !this.data.by_ext_name) return ''; |
6396 return ''; | |
6397 | |
6398 var url = 'chrome://extensions#' + this.data.by_ext_id; | 3540 var url = 'chrome://extensions#' + this.data.by_ext_id; |
6399 var name = this.data.by_ext_name; | 3541 var name = this.data.by_ext_name; |
6400 return loadTimeData.getStringF('controlledByUrl', url, name); | 3542 return loadTimeData.getStringF('controlledByUrl', url, name); |
6401 }, | 3543 }, |
6402 | |
6403 /** @private */ | |
6404 computeDangerIcon_: function() { | 3544 computeDangerIcon_: function() { |
6405 if (!this.isDangerous_) | 3545 if (!this.isDangerous_) return ''; |
6406 return ''; | |
6407 | |
6408 switch (this.data.danger_type) { | 3546 switch (this.data.danger_type) { |
6409 case downloads.DangerType.DANGEROUS_CONTENT: | 3547 case downloads.DangerType.DANGEROUS_CONTENT: |
6410 case downloads.DangerType.DANGEROUS_HOST: | 3548 case downloads.DangerType.DANGEROUS_HOST: |
6411 case downloads.DangerType.DANGEROUS_URL: | 3549 case downloads.DangerType.DANGEROUS_URL: |
6412 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3550 case downloads.DangerType.POTENTIALLY_UNWANTED: |
6413 case downloads.DangerType.UNCOMMON_CONTENT: | 3551 case downloads.DangerType.UNCOMMON_CONTENT: |
6414 return 'downloads:remove-circle'; | 3552 return 'downloads:remove-circle'; |
6415 default: | 3553 |
6416 return 'cr:warning'; | 3554 default: |
6417 } | 3555 return 'cr:warning'; |
6418 }, | 3556 } |
6419 | 3557 }, |
6420 /** @private */ | |
6421 computeDate_: function() { | 3558 computeDate_: function() { |
6422 assert(typeof this.data.hideDate == 'boolean'); | 3559 assert(typeof this.data.hideDate == 'boolean'); |
6423 if (this.data.hideDate) | 3560 if (this.data.hideDate) return ''; |
6424 return ''; | |
6425 return assert(this.data.since_string || this.data.date_string); | 3561 return assert(this.data.since_string || this.data.date_string); |
6426 }, | 3562 }, |
6427 | |
6428 /** @private */ | |
6429 computeDescription_: function() { | 3563 computeDescription_: function() { |
6430 var data = this.data; | 3564 var data = this.data; |
6431 | |
6432 switch (data.state) { | 3565 switch (data.state) { |
6433 case downloads.States.DANGEROUS: | 3566 case downloads.States.DANGEROUS: |
6434 var fileName = data.file_name; | 3567 var fileName = data.file_name; |
6435 switch (data.danger_type) { | 3568 switch (data.danger_type) { |
6436 case downloads.DangerType.DANGEROUS_FILE: | 3569 case downloads.DangerType.DANGEROUS_FILE: |
6437 return loadTimeData.getStringF('dangerFileDesc', fileName); | 3570 return loadTimeData.getStringF('dangerFileDesc', fileName); |
6438 case downloads.DangerType.DANGEROUS_URL: | 3571 |
6439 return loadTimeData.getString('dangerUrlDesc'); | 3572 case downloads.DangerType.DANGEROUS_URL: |
6440 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. | 3573 return loadTimeData.getString('dangerUrlDesc'); |
6441 case downloads.DangerType.DANGEROUS_HOST: | 3574 |
6442 return loadTimeData.getStringF('dangerContentDesc', fileName); | 3575 case downloads.DangerType.DANGEROUS_CONTENT: |
6443 case downloads.DangerType.UNCOMMON_CONTENT: | 3576 case downloads.DangerType.DANGEROUS_HOST: |
6444 return loadTimeData.getStringF('dangerUncommonDesc', fileName); | 3577 return loadTimeData.getStringF('dangerContentDesc', fileName); |
6445 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3578 |
6446 return loadTimeData.getStringF('dangerSettingsDesc', fileName); | 3579 case downloads.DangerType.UNCOMMON_CONTENT: |
6447 } | 3580 return loadTimeData.getStringF('dangerUncommonDesc', fileName); |
6448 break; | 3581 |
6449 | 3582 case downloads.DangerType.POTENTIALLY_UNWANTED: |
6450 case downloads.States.IN_PROGRESS: | 3583 return loadTimeData.getStringF('dangerSettingsDesc', fileName); |
6451 case downloads.States.PAUSED: // Fallthrough. | 3584 } |
6452 return data.progress_status_text; | 3585 break; |
6453 } | 3586 |
6454 | 3587 case downloads.States.IN_PROGRESS: |
| 3588 case downloads.States.PAUSED: |
| 3589 return data.progress_status_text; |
| 3590 } |
6455 return ''; | 3591 return ''; |
6456 }, | 3592 }, |
6457 | |
6458 /** @private */ | |
6459 computeIsActive_: function() { | 3593 computeIsActive_: function() { |
6460 return this.data.state != downloads.States.CANCELLED && | 3594 return this.data.state != downloads.States.CANCELLED && this.data.state !=
downloads.States.INTERRUPTED && !this.data.file_externally_removed; |
6461 this.data.state != downloads.States.INTERRUPTED && | 3595 }, |
6462 !this.data.file_externally_removed; | |
6463 }, | |
6464 | |
6465 /** @private */ | |
6466 computeIsDangerous_: function() { | 3596 computeIsDangerous_: function() { |
6467 return this.data.state == downloads.States.DANGEROUS; | 3597 return this.data.state == downloads.States.DANGEROUS; |
6468 }, | 3598 }, |
6469 | |
6470 /** @private */ | |
6471 computeIsInProgress_: function() { | 3599 computeIsInProgress_: function() { |
6472 return this.data.state == downloads.States.IN_PROGRESS; | 3600 return this.data.state == downloads.States.IN_PROGRESS; |
6473 }, | 3601 }, |
6474 | |
6475 /** @private */ | |
6476 computeIsMalware_: function() { | 3602 computeIsMalware_: function() { |
6477 return this.isDangerous_ && | 3603 return this.isDangerous_ && (this.data.danger_type == downloads.DangerType
.DANGEROUS_CONTENT || this.data.danger_type == downloads.DangerType.DANGEROUS_HO
ST || this.data.danger_type == downloads.DangerType.DANGEROUS_URL || this.data.d
anger_type == downloads.DangerType.POTENTIALLY_UNWANTED); |
6478 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || | 3604 }, |
6479 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || | |
6480 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || | |
6481 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); | |
6482 }, | |
6483 | |
6484 /** @private */ | |
6485 computePauseOrResumeText_: function() { | 3605 computePauseOrResumeText_: function() { |
6486 if (this.isInProgress_) | 3606 if (this.isInProgress_) return loadTimeData.getString('controlPause'); |
6487 return loadTimeData.getString('controlPause'); | 3607 if (this.data.resume) return loadTimeData.getString('controlResume'); |
6488 if (this.data.resume) | |
6489 return loadTimeData.getString('controlResume'); | |
6490 return ''; | 3608 return ''; |
6491 }, | 3609 }, |
6492 | |
6493 /** @private */ | |
6494 computeRemoveStyle_: function() { | 3610 computeRemoveStyle_: function() { |
6495 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); | 3611 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); |
6496 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; | 3612 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; |
6497 return hideRemove ? 'visibility: hidden' : ''; | 3613 return hideRemove ? 'visibility: hidden' : ''; |
6498 }, | 3614 }, |
6499 | |
6500 /** @private */ | |
6501 computeShowCancel_: function() { | 3615 computeShowCancel_: function() { |
6502 return this.data.state == downloads.States.IN_PROGRESS || | 3616 return this.data.state == downloads.States.IN_PROGRESS || this.data.state
== downloads.States.PAUSED; |
6503 this.data.state == downloads.States.PAUSED; | 3617 }, |
6504 }, | |
6505 | |
6506 /** @private */ | |
6507 computeShowProgress_: function() { | 3618 computeShowProgress_: function() { |
6508 return this.showCancel_ && this.data.percent >= -1; | 3619 return this.showCancel_ && this.data.percent >= -1; |
6509 }, | 3620 }, |
6510 | |
6511 /** @private */ | |
6512 computeTag_: function() { | 3621 computeTag_: function() { |
6513 switch (this.data.state) { | 3622 switch (this.data.state) { |
6514 case downloads.States.CANCELLED: | 3623 case downloads.States.CANCELLED: |
6515 return loadTimeData.getString('statusCancelled'); | 3624 return loadTimeData.getString('statusCancelled'); |
6516 | 3625 |
6517 case downloads.States.INTERRUPTED: | 3626 case downloads.States.INTERRUPTED: |
6518 return this.data.last_reason_text; | 3627 return this.data.last_reason_text; |
6519 | 3628 |
6520 case downloads.States.COMPLETE: | 3629 case downloads.States.COMPLETE: |
6521 return this.data.file_externally_removed ? | 3630 return this.data.file_externally_removed ? loadTimeData.getString('statu
sRemoved') : ''; |
6522 loadTimeData.getString('statusRemoved') : ''; | 3631 } |
6523 } | |
6524 | |
6525 return ''; | 3632 return ''; |
6526 }, | 3633 }, |
6527 | |
6528 /** @private */ | |
6529 isIndeterminate_: function() { | 3634 isIndeterminate_: function() { |
6530 return this.data.percent == -1; | 3635 return this.data.percent == -1; |
6531 }, | 3636 }, |
6532 | |
6533 /** @private */ | |
6534 observeControlledBy_: function() { | 3637 observeControlledBy_: function() { |
6535 this.$['controlled-by'].innerHTML = this.controlledBy_; | 3638 this.$['controlled-by'].innerHTML = this.controlledBy_; |
6536 }, | 3639 }, |
6537 | |
6538 /** @private */ | |
6539 observeIsDangerous_: function() { | 3640 observeIsDangerous_: function() { |
6540 if (!this.data) | 3641 if (!this.data) return; |
6541 return; | |
6542 | |
6543 if (this.isDangerous_) { | 3642 if (this.isDangerous_) { |
6544 this.$.url.removeAttribute('href'); | 3643 this.$.url.removeAttribute('href'); |
6545 } else { | 3644 } else { |
6546 this.$.url.href = assert(this.data.url); | 3645 this.$.url.href = assert(this.data.url); |
6547 var filePath = encodeURIComponent(this.data.file_path); | 3646 var filePath = encodeURIComponent(this.data.file_path); |
6548 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; | 3647 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; |
6549 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; | 3648 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; |
6550 } | 3649 } |
6551 }, | 3650 }, |
6552 | |
6553 /** @private */ | |
6554 onCancelTap_: function() { | 3651 onCancelTap_: function() { |
6555 downloads.ActionService.getInstance().cancel(this.data.id); | 3652 downloads.ActionService.getInstance().cancel(this.data.id); |
6556 }, | 3653 }, |
6557 | |
6558 /** @private */ | |
6559 onDiscardDangerousTap_: function() { | 3654 onDiscardDangerousTap_: function() { |
6560 downloads.ActionService.getInstance().discardDangerous(this.data.id); | 3655 downloads.ActionService.getInstance().discardDangerous(this.data.id); |
6561 }, | 3656 }, |
6562 | |
6563 /** | |
6564 * @private | |
6565 * @param {Event} e | |
6566 */ | |
6567 onDragStart_: function(e) { | 3657 onDragStart_: function(e) { |
6568 e.preventDefault(); | 3658 e.preventDefault(); |
6569 downloads.ActionService.getInstance().drag(this.data.id); | 3659 downloads.ActionService.getInstance().drag(this.data.id); |
6570 }, | 3660 }, |
6571 | |
6572 /** | |
6573 * @param {Event} e | |
6574 * @private | |
6575 */ | |
6576 onFileLinkTap_: function(e) { | 3661 onFileLinkTap_: function(e) { |
6577 e.preventDefault(); | 3662 e.preventDefault(); |
6578 downloads.ActionService.getInstance().openFile(this.data.id); | 3663 downloads.ActionService.getInstance().openFile(this.data.id); |
6579 }, | 3664 }, |
6580 | |
6581 /** @private */ | |
6582 onPauseOrResumeTap_: function() { | 3665 onPauseOrResumeTap_: function() { |
6583 if (this.isInProgress_) | 3666 if (this.isInProgress_) downloads.ActionService.getInstance().pause(this.d
ata.id); else downloads.ActionService.getInstance().resume(this.data.id); |
6584 downloads.ActionService.getInstance().pause(this.data.id); | 3667 }, |
6585 else | |
6586 downloads.ActionService.getInstance().resume(this.data.id); | |
6587 }, | |
6588 | |
6589 /** @private */ | |
6590 onRemoveTap_: function() { | 3668 onRemoveTap_: function() { |
6591 downloads.ActionService.getInstance().remove(this.data.id); | 3669 downloads.ActionService.getInstance().remove(this.data.id); |
6592 }, | 3670 }, |
6593 | |
6594 /** @private */ | |
6595 onRetryTap_: function() { | 3671 onRetryTap_: function() { |
6596 downloads.ActionService.getInstance().download(this.data.url); | 3672 downloads.ActionService.getInstance().download(this.data.url); |
6597 }, | 3673 }, |
6598 | |
6599 /** @private */ | |
6600 onSaveDangerousTap_: function() { | 3674 onSaveDangerousTap_: function() { |
6601 downloads.ActionService.getInstance().saveDangerous(this.data.id); | 3675 downloads.ActionService.getInstance().saveDangerous(this.data.id); |
6602 }, | 3676 }, |
6603 | |
6604 /** @private */ | |
6605 onShowTap_: function() { | 3677 onShowTap_: function() { |
6606 downloads.ActionService.getInstance().show(this.data.id); | 3678 downloads.ActionService.getInstance().show(this.data.id); |
6607 }, | 3679 } |
6608 }); | 3680 }); |
6609 | 3681 return { |
6610 return {Item: Item}; | 3682 Item: Item |
| 3683 }; |
6611 }); | 3684 }); |
6612 /** @polymerBehavior Polymer.PaperItemBehavior */ | 3685 |
6613 Polymer.PaperItemBehaviorImpl = { | 3686 Polymer.PaperItemBehaviorImpl = { |
6614 hostAttributes: { | 3687 hostAttributes: { |
6615 role: 'option', | 3688 role: 'option', |
6616 tabindex: '0' | 3689 tabindex: '0' |
6617 } | 3690 } |
6618 }; | 3691 }; |
6619 | 3692 |
6620 /** @polymerBehavior */ | 3693 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState,
Polymer.PaperItemBehaviorImpl ]; |
6621 Polymer.PaperItemBehavior = [ | 3694 |
6622 Polymer.IronButtonState, | |
6623 Polymer.IronControlState, | |
6624 Polymer.PaperItemBehaviorImpl | |
6625 ]; | |
6626 Polymer({ | 3695 Polymer({ |
6627 is: 'paper-item', | 3696 is: 'paper-item', |
6628 | 3697 behaviors: [ Polymer.PaperItemBehavior ] |
6629 behaviors: [ | 3698 }); |
6630 Polymer.PaperItemBehavior | 3699 |
6631 ] | 3700 Polymer.IronSelection = function(selectCallback) { |
6632 }); | 3701 this.selection = []; |
6633 /** | 3702 this.selectCallback = selectCallback; |
6634 * @param {!Function} selectCallback | 3703 }; |
6635 * @constructor | 3704 |
6636 */ | 3705 Polymer.IronSelection.prototype = { |
6637 Polymer.IronSelection = function(selectCallback) { | 3706 get: function() { |
6638 this.selection = []; | 3707 return this.multi ? this.selection.slice() : this.selection[0]; |
6639 this.selectCallback = selectCallback; | 3708 }, |
6640 }; | 3709 clear: function(excludes) { |
6641 | 3710 this.selection.slice().forEach(function(item) { |
6642 Polymer.IronSelection.prototype = { | 3711 if (!excludes || excludes.indexOf(item) < 0) { |
6643 | 3712 this.setItemSelected(item, false); |
6644 /** | 3713 } |
6645 * Retrieves the selected item(s). | 3714 }, this); |
6646 * | 3715 }, |
6647 * @method get | 3716 isSelected: function(item) { |
6648 * @returns Returns the selected item(s). If the multi property is true, | 3717 return this.selection.indexOf(item) >= 0; |
6649 * `get` will return an array, otherwise it will return | 3718 }, |
6650 * the selected item or undefined if there is no selection. | 3719 setItemSelected: function(item, isSelected) { |
6651 */ | 3720 if (item != null) { |
6652 get: function() { | 3721 if (isSelected !== this.isSelected(item)) { |
6653 return this.multi ? this.selection.slice() : this.selection[0]; | 3722 if (isSelected) { |
6654 }, | 3723 this.selection.push(item); |
6655 | 3724 } else { |
6656 /** | 3725 var i = this.selection.indexOf(item); |
6657 * Clears all the selection except the ones indicated. | 3726 if (i >= 0) { |
6658 * | 3727 this.selection.splice(i, 1); |
6659 * @method clear | |
6660 * @param {Array} excludes items to be excluded. | |
6661 */ | |
6662 clear: function(excludes) { | |
6663 this.selection.slice().forEach(function(item) { | |
6664 if (!excludes || excludes.indexOf(item) < 0) { | |
6665 this.setItemSelected(item, false); | |
6666 } | |
6667 }, this); | |
6668 }, | |
6669 | |
6670 /** | |
6671 * Indicates if a given item is selected. | |
6672 * | |
6673 * @method isSelected | |
6674 * @param {*} item The item whose selection state should be checked. | |
6675 * @returns Returns true if `item` is selected. | |
6676 */ | |
6677 isSelected: function(item) { | |
6678 return this.selection.indexOf(item) >= 0; | |
6679 }, | |
6680 | |
6681 /** | |
6682 * Sets the selection state for a given item to either selected or deselecte
d. | |
6683 * | |
6684 * @method setItemSelected | |
6685 * @param {*} item The item to select. | |
6686 * @param {boolean} isSelected True for selected, false for deselected. | |
6687 */ | |
6688 setItemSelected: function(item, isSelected) { | |
6689 if (item != null) { | |
6690 if (isSelected !== this.isSelected(item)) { | |
6691 // proceed to update selection only if requested state differs from cu
rrent | |
6692 if (isSelected) { | |
6693 this.selection.push(item); | |
6694 } else { | |
6695 var i = this.selection.indexOf(item); | |
6696 if (i >= 0) { | |
6697 this.selection.splice(i, 1); | |
6698 } | |
6699 } | |
6700 if (this.selectCallback) { | |
6701 this.selectCallback(item, isSelected); | |
6702 } | 3728 } |
6703 } | 3729 } |
6704 } | 3730 if (this.selectCallback) { |
6705 }, | 3731 this.selectCallback(item, isSelected); |
6706 | |
6707 /** | |
6708 * Sets the selection state for a given item. If the `multi` property | |
6709 * is true, then the selected state of `item` will be toggled; otherwise | |
6710 * the `item` will be selected. | |
6711 * | |
6712 * @method select | |
6713 * @param {*} item The item to select. | |
6714 */ | |
6715 select: function(item) { | |
6716 if (this.multi) { | |
6717 this.toggle(item); | |
6718 } else if (this.get() !== item) { | |
6719 this.setItemSelected(this.get(), false); | |
6720 this.setItemSelected(item, true); | |
6721 } | |
6722 }, | |
6723 | |
6724 /** | |
6725 * Toggles the selection state for `item`. | |
6726 * | |
6727 * @method toggle | |
6728 * @param {*} item The item to toggle. | |
6729 */ | |
6730 toggle: function(item) { | |
6731 this.setItemSelected(item, !this.isSelected(item)); | |
6732 } | |
6733 | |
6734 }; | |
6735 /** @polymerBehavior */ | |
6736 Polymer.IronSelectableBehavior = { | |
6737 | |
6738 /** | |
6739 * Fired when iron-selector is activated (selected or deselected). | |
6740 * It is fired before the selected items are changed. | |
6741 * Cancel the event to abort selection. | |
6742 * | |
6743 * @event iron-activate | |
6744 */ | |
6745 | |
6746 /** | |
6747 * Fired when an item is selected | |
6748 * | |
6749 * @event iron-select | |
6750 */ | |
6751 | |
6752 /** | |
6753 * Fired when an item is deselected | |
6754 * | |
6755 * @event iron-deselect | |
6756 */ | |
6757 | |
6758 /** | |
6759 * Fired when the list of selectable items changes (e.g., items are | |
6760 * added or removed). The detail of the event is a mutation record that | |
6761 * describes what changed. | |
6762 * | |
6763 * @event iron-items-changed | |
6764 */ | |
6765 | |
6766 properties: { | |
6767 | |
6768 /** | |
6769 * If you want to use an attribute value or property of an element for | |
6770 * `selected` instead of the index, set this to the name of the attribute | |
6771 * or property. Hyphenated values are converted to camel case when used to | |
6772 * look up the property of a selectable element. Camel cased values are | |
6773 * *not* converted to hyphenated values for attribute lookup. It's | |
6774 * recommended that you provide the hyphenated form of the name so that | |
6775 * selection works in both cases. (Use `attr-or-property-name` instead of | |
6776 * `attrOrPropertyName`.) | |
6777 */ | |
6778 attrForSelected: { | |
6779 type: String, | |
6780 value: null | |
6781 }, | |
6782 | |
6783 /** | |
6784 * Gets or sets the selected element. The default is to use the index of t
he item. | |
6785 * @type {string|number} | |
6786 */ | |
6787 selected: { | |
6788 type: String, | |
6789 notify: true | |
6790 }, | |
6791 | |
6792 /** | |
6793 * Returns the currently selected item. | |
6794 * | |
6795 * @type {?Object} | |
6796 */ | |
6797 selectedItem: { | |
6798 type: Object, | |
6799 readOnly: true, | |
6800 notify: true | |
6801 }, | |
6802 | |
6803 /** | |
6804 * The event that fires from items when they are selected. Selectable | |
6805 * will listen for this event from items and update the selection state. | |
6806 * Set to empty string to listen to no events. | |
6807 */ | |
6808 activateEvent: { | |
6809 type: String, | |
6810 value: 'tap', | |
6811 observer: '_activateEventChanged' | |
6812 }, | |
6813 | |
6814 /** | |
6815 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
6816 * are selectable. | |
6817 */ | |
6818 selectable: String, | |
6819 | |
6820 /** | |
6821 * The class to set on elements when selected. | |
6822 */ | |
6823 selectedClass: { | |
6824 type: String, | |
6825 value: 'iron-selected' | |
6826 }, | |
6827 | |
6828 /** | |
6829 * The attribute to set on elements when selected. | |
6830 */ | |
6831 selectedAttribute: { | |
6832 type: String, | |
6833 value: null | |
6834 }, | |
6835 | |
6836 /** | |
6837 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
6838 * is not found. | |
6839 */ | |
6840 fallbackSelection: { | |
6841 type: String, | |
6842 value: null | |
6843 }, | |
6844 | |
6845 /** | |
6846 * The list of items from which a selection can be made. | |
6847 */ | |
6848 items: { | |
6849 type: Array, | |
6850 readOnly: true, | |
6851 notify: true, | |
6852 value: function() { | |
6853 return []; | |
6854 } | 3732 } |
6855 }, | 3733 } |
6856 | 3734 } |
6857 /** | 3735 }, |
6858 * The set of excluded elements where the key is the `localName` | 3736 select: function(item) { |
6859 * of the element that will be ignored from the item list. | 3737 if (this.multi) { |
6860 * | 3738 this.toggle(item); |
6861 * @default {template: 1} | 3739 } else if (this.get() !== item) { |
6862 */ | 3740 this.setItemSelected(this.get(), false); |
6863 _excludedLocalNames: { | 3741 this.setItemSelected(item, true); |
6864 type: Object, | 3742 } |
6865 value: function() { | 3743 }, |
6866 return { | 3744 toggle: function(item) { |
6867 'template': 1 | 3745 this.setItemSelected(item, !this.isSelected(item)); |
6868 }; | 3746 } |
| 3747 }; |
| 3748 |
| 3749 Polymer.IronSelectableBehavior = { |
| 3750 properties: { |
| 3751 attrForSelected: { |
| 3752 type: String, |
| 3753 value: null |
| 3754 }, |
| 3755 selected: { |
| 3756 type: String, |
| 3757 notify: true |
| 3758 }, |
| 3759 selectedItem: { |
| 3760 type: Object, |
| 3761 readOnly: true, |
| 3762 notify: true |
| 3763 }, |
| 3764 activateEvent: { |
| 3765 type: String, |
| 3766 value: 'tap', |
| 3767 observer: '_activateEventChanged' |
| 3768 }, |
| 3769 selectable: String, |
| 3770 selectedClass: { |
| 3771 type: String, |
| 3772 value: 'iron-selected' |
| 3773 }, |
| 3774 selectedAttribute: { |
| 3775 type: String, |
| 3776 value: null |
| 3777 }, |
| 3778 fallbackSelection: { |
| 3779 type: String, |
| 3780 value: null |
| 3781 }, |
| 3782 items: { |
| 3783 type: Array, |
| 3784 readOnly: true, |
| 3785 notify: true, |
| 3786 value: function() { |
| 3787 return []; |
| 3788 } |
| 3789 }, |
| 3790 _excludedLocalNames: { |
| 3791 type: Object, |
| 3792 value: function() { |
| 3793 return { |
| 3794 template: 1 |
| 3795 }; |
| 3796 } |
| 3797 } |
| 3798 }, |
| 3799 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec
ted)', '_checkFallback(fallbackSelection)' ], |
| 3800 created: function() { |
| 3801 this._bindFilterItem = this._filterItem.bind(this); |
| 3802 this._selection = new Polymer.IronSelection(this._applySelection.bind(this))
; |
| 3803 }, |
| 3804 attached: function() { |
| 3805 this._observer = this._observeItems(this); |
| 3806 this._updateItems(); |
| 3807 if (!this._shouldUpdateSelection) { |
| 3808 this._updateSelected(); |
| 3809 } |
| 3810 this._addListener(this.activateEvent); |
| 3811 }, |
| 3812 detached: function() { |
| 3813 if (this._observer) { |
| 3814 Polymer.dom(this).unobserveNodes(this._observer); |
| 3815 } |
| 3816 this._removeListener(this.activateEvent); |
| 3817 }, |
| 3818 indexOf: function(item) { |
| 3819 return this.items.indexOf(item); |
| 3820 }, |
| 3821 select: function(value) { |
| 3822 this.selected = value; |
| 3823 }, |
| 3824 selectPrevious: function() { |
| 3825 var length = this.items.length; |
| 3826 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt
h; |
| 3827 this.selected = this._indexToValue(index); |
| 3828 }, |
| 3829 selectNext: function() { |
| 3830 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len
gth; |
| 3831 this.selected = this._indexToValue(index); |
| 3832 }, |
| 3833 selectIndex: function(index) { |
| 3834 this.select(this._indexToValue(index)); |
| 3835 }, |
| 3836 forceSynchronousItemUpdate: function() { |
| 3837 this._updateItems(); |
| 3838 }, |
| 3839 get _shouldUpdateSelection() { |
| 3840 return this.selected != null; |
| 3841 }, |
| 3842 _checkFallback: function() { |
| 3843 if (this._shouldUpdateSelection) { |
| 3844 this._updateSelected(); |
| 3845 } |
| 3846 }, |
| 3847 _addListener: function(eventName) { |
| 3848 this.listen(this, eventName, '_activateHandler'); |
| 3849 }, |
| 3850 _removeListener: function(eventName) { |
| 3851 this.unlisten(this, eventName, '_activateHandler'); |
| 3852 }, |
| 3853 _activateEventChanged: function(eventName, old) { |
| 3854 this._removeListener(old); |
| 3855 this._addListener(eventName); |
| 3856 }, |
| 3857 _updateItems: function() { |
| 3858 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*
'); |
| 3859 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 3860 this._setItems(nodes); |
| 3861 }, |
| 3862 _updateAttrForSelected: function() { |
| 3863 if (this._shouldUpdateSelection) { |
| 3864 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 3865 } |
| 3866 }, |
| 3867 _updateSelected: function() { |
| 3868 this._selectSelected(this.selected); |
| 3869 }, |
| 3870 _selectSelected: function(selected) { |
| 3871 this._selection.select(this._valueToItem(this.selected)); |
| 3872 if (this.fallbackSelection && this.items.length && this._selection.get() ===
undefined) { |
| 3873 this.selected = this.fallbackSelection; |
| 3874 } |
| 3875 }, |
| 3876 _filterItem: function(node) { |
| 3877 return !this._excludedLocalNames[node.localName]; |
| 3878 }, |
| 3879 _valueToItem: function(value) { |
| 3880 return value == null ? null : this.items[this._valueToIndex(value)]; |
| 3881 }, |
| 3882 _valueToIndex: function(value) { |
| 3883 if (this.attrForSelected) { |
| 3884 for (var i = 0, item; item = this.items[i]; i++) { |
| 3885 if (this._valueForItem(item) == value) { |
| 3886 return i; |
6869 } | 3887 } |
6870 } | 3888 } |
6871 }, | 3889 } else { |
6872 | 3890 return Number(value); |
6873 observers: [ | 3891 } |
6874 '_updateAttrForSelected(attrForSelected)', | 3892 }, |
6875 '_updateSelected(selected)', | 3893 _indexToValue: function(index) { |
6876 '_checkFallback(fallbackSelection)' | 3894 if (this.attrForSelected) { |
6877 ], | 3895 var item = this.items[index]; |
6878 | 3896 if (item) { |
6879 created: function() { | 3897 return this._valueForItem(item); |
6880 this._bindFilterItem = this._filterItem.bind(this); | 3898 } |
6881 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | 3899 } else { |
6882 }, | 3900 return index; |
6883 | 3901 } |
6884 attached: function() { | 3902 }, |
6885 this._observer = this._observeItems(this); | 3903 _valueForItem: function(item) { |
| 3904 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; |
| 3905 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe
lected); |
| 3906 }, |
| 3907 _applySelection: function(item, isSelected) { |
| 3908 if (this.selectedClass) { |
| 3909 this.toggleClass(this.selectedClass, isSelected, item); |
| 3910 } |
| 3911 if (this.selectedAttribute) { |
| 3912 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 3913 } |
| 3914 this._selectionChange(); |
| 3915 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), { |
| 3916 item: item |
| 3917 }); |
| 3918 }, |
| 3919 _selectionChange: function() { |
| 3920 this._setSelectedItem(this._selection.get()); |
| 3921 }, |
| 3922 _observeItems: function(node) { |
| 3923 return Polymer.dom(node).observeNodes(function(mutation) { |
6886 this._updateItems(); | 3924 this._updateItems(); |
6887 if (!this._shouldUpdateSelection) { | |
6888 this._updateSelected(); | |
6889 } | |
6890 this._addListener(this.activateEvent); | |
6891 }, | |
6892 | |
6893 detached: function() { | |
6894 if (this._observer) { | |
6895 Polymer.dom(this).unobserveNodes(this._observer); | |
6896 } | |
6897 this._removeListener(this.activateEvent); | |
6898 }, | |
6899 | |
6900 /** | |
6901 * Returns the index of the given item. | |
6902 * | |
6903 * @method indexOf | |
6904 * @param {Object} item | |
6905 * @returns Returns the index of the item | |
6906 */ | |
6907 indexOf: function(item) { | |
6908 return this.items.indexOf(item); | |
6909 }, | |
6910 | |
6911 /** | |
6912 * Selects the given value. | |
6913 * | |
6914 * @method select | |
6915 * @param {string|number} value the value to select. | |
6916 */ | |
6917 select: function(value) { | |
6918 this.selected = value; | |
6919 }, | |
6920 | |
6921 /** | |
6922 * Selects the previous item. | |
6923 * | |
6924 * @method selectPrevious | |
6925 */ | |
6926 selectPrevious: function() { | |
6927 var length = this.items.length; | |
6928 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | |
6929 this.selected = this._indexToValue(index); | |
6930 }, | |
6931 | |
6932 /** | |
6933 * Selects the next item. | |
6934 * | |
6935 * @method selectNext | |
6936 */ | |
6937 selectNext: function() { | |
6938 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | |
6939 this.selected = this._indexToValue(index); | |
6940 }, | |
6941 | |
6942 /** | |
6943 * Selects the item at the given index. | |
6944 * | |
6945 * @method selectIndex | |
6946 */ | |
6947 selectIndex: function(index) { | |
6948 this.select(this._indexToValue(index)); | |
6949 }, | |
6950 | |
6951 /** | |
6952 * Force a synchronous update of the `items` property. | |
6953 * | |
6954 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
6955 * updates to the set of selectable items after updates to the DOM list and | |
6956 * selection state have been made. | |
6957 * | |
6958 * WARNING: If you are using this method, you should probably consider an | |
6959 * alternate approach. Synchronously querying for items is potentially | |
6960 * slow for many use cases. The `items` property will update asynchronously | |
6961 * on its own to reflect selectable items in the DOM. | |
6962 */ | |
6963 forceSynchronousItemUpdate: function() { | |
6964 this._updateItems(); | |
6965 }, | |
6966 | |
6967 get _shouldUpdateSelection() { | |
6968 return this.selected != null; | |
6969 }, | |
6970 | |
6971 _checkFallback: function() { | |
6972 if (this._shouldUpdateSelection) { | 3925 if (this._shouldUpdateSelection) { |
6973 this._updateSelected(); | 3926 this._updateSelected(); |
6974 } | 3927 } |
6975 }, | 3928 this.fire('iron-items-changed', mutation, { |
6976 | 3929 bubbles: false, |
6977 _addListener: function(eventName) { | 3930 cancelable: false |
6978 this.listen(this, eventName, '_activateHandler'); | 3931 }); |
6979 }, | 3932 }); |
6980 | 3933 }, |
6981 _removeListener: function(eventName) { | 3934 _activateHandler: function(e) { |
6982 this.unlisten(this, eventName, '_activateHandler'); | 3935 var t = e.target; |
6983 }, | 3936 var items = this.items; |
6984 | 3937 while (t && t != this) { |
6985 _activateEventChanged: function(eventName, old) { | 3938 var i = items.indexOf(t); |
6986 this._removeListener(old); | 3939 if (i >= 0) { |
6987 this._addListener(eventName); | 3940 var value = this._indexToValue(i); |
6988 }, | 3941 this._itemActivate(value, t); |
6989 | 3942 return; |
6990 _updateItems: function() { | 3943 } |
6991 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | 3944 t = t.parentNode; |
6992 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | 3945 } |
6993 this._setItems(nodes); | 3946 }, |
6994 }, | 3947 _itemActivate: function(value, item) { |
6995 | 3948 if (!this.fire('iron-activate', { |
6996 _updateAttrForSelected: function() { | 3949 selected: value, |
6997 if (this._shouldUpdateSelection) { | 3950 item: item |
6998 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | 3951 }, { |
6999 } | 3952 cancelable: true |
7000 }, | 3953 }).defaultPrevented) { |
7001 | 3954 this.select(value); |
7002 _updateSelected: function() { | 3955 } |
| 3956 } |
| 3957 }; |
| 3958 |
| 3959 Polymer.IronMultiSelectableBehaviorImpl = { |
| 3960 properties: { |
| 3961 multi: { |
| 3962 type: Boolean, |
| 3963 value: false, |
| 3964 observer: 'multiChanged' |
| 3965 }, |
| 3966 selectedValues: { |
| 3967 type: Array, |
| 3968 notify: true |
| 3969 }, |
| 3970 selectedItems: { |
| 3971 type: Array, |
| 3972 readOnly: true, |
| 3973 notify: true |
| 3974 } |
| 3975 }, |
| 3976 observers: [ '_updateSelected(selectedValues.splices)' ], |
| 3977 select: function(value) { |
| 3978 if (this.multi) { |
| 3979 if (this.selectedValues) { |
| 3980 this._toggleSelected(value); |
| 3981 } else { |
| 3982 this.selectedValues = [ value ]; |
| 3983 } |
| 3984 } else { |
| 3985 this.selected = value; |
| 3986 } |
| 3987 }, |
| 3988 multiChanged: function(multi) { |
| 3989 this._selection.multi = multi; |
| 3990 }, |
| 3991 get _shouldUpdateSelection() { |
| 3992 return this.selected != null || this.selectedValues != null && this.selected
Values.length; |
| 3993 }, |
| 3994 _updateAttrForSelected: function() { |
| 3995 if (!this.multi) { |
| 3996 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 3997 } else if (this._shouldUpdateSelection) { |
| 3998 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 3999 return this._indexToValue(this.indexOf(selectedItem)); |
| 4000 }, this).filter(function(unfilteredValue) { |
| 4001 return unfilteredValue != null; |
| 4002 }, this); |
| 4003 } |
| 4004 }, |
| 4005 _updateSelected: function() { |
| 4006 if (this.multi) { |
| 4007 this._selectMulti(this.selectedValues); |
| 4008 } else { |
7003 this._selectSelected(this.selected); | 4009 this._selectSelected(this.selected); |
7004 }, | 4010 } |
7005 | 4011 }, |
7006 _selectSelected: function(selected) { | 4012 _selectMulti: function(values) { |
7007 this._selection.select(this._valueToItem(this.selected)); | 4013 if (values) { |
7008 // Check for items, since this array is populated only when attached | 4014 var selectedItems = this._valuesToItems(values); |
7009 // Since Number(0) is falsy, explicitly check for undefined | 4015 this._selection.clear(selectedItems); |
7010 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | 4016 for (var i = 0; i < selectedItems.length; i++) { |
7011 this.selected = this.fallbackSelection; | 4017 this._selection.setItemSelected(selectedItems[i], true); |
7012 } | 4018 } |
7013 }, | 4019 if (this.fallbackSelection && this.items.length && !this._selection.get().
length) { |
7014 | 4020 var fallback = this._valueToItem(this.fallbackSelection); |
7015 _filterItem: function(node) { | 4021 if (fallback) { |
7016 return !this._excludedLocalNames[node.localName]; | 4022 this.selectedValues = [ this.fallbackSelection ]; |
7017 }, | |
7018 | |
7019 _valueToItem: function(value) { | |
7020 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
7021 }, | |
7022 | |
7023 _valueToIndex: function(value) { | |
7024 if (this.attrForSelected) { | |
7025 for (var i = 0, item; item = this.items[i]; i++) { | |
7026 if (this._valueForItem(item) == value) { | |
7027 return i; | |
7028 } | |
7029 } | 4023 } |
| 4024 } |
| 4025 } else { |
| 4026 this._selection.clear(); |
| 4027 } |
| 4028 }, |
| 4029 _selectionChange: function() { |
| 4030 var s = this._selection.get(); |
| 4031 if (this.multi) { |
| 4032 this._setSelectedItems(s); |
| 4033 } else { |
| 4034 this._setSelectedItems([ s ]); |
| 4035 this._setSelectedItem(s); |
| 4036 } |
| 4037 }, |
| 4038 _toggleSelected: function(value) { |
| 4039 var i = this.selectedValues.indexOf(value); |
| 4040 var unselected = i < 0; |
| 4041 if (unselected) { |
| 4042 this.push('selectedValues', value); |
| 4043 } else { |
| 4044 this.splice('selectedValues', i, 1); |
| 4045 } |
| 4046 }, |
| 4047 _valuesToItems: function(values) { |
| 4048 return values == null ? null : values.map(function(value) { |
| 4049 return this._valueToItem(value); |
| 4050 }, this); |
| 4051 } |
| 4052 }; |
| 4053 |
| 4054 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer.
IronMultiSelectableBehaviorImpl ]; |
| 4055 |
| 4056 Polymer.IronMenuBehaviorImpl = { |
| 4057 properties: { |
| 4058 focusedItem: { |
| 4059 observer: '_focusedItemChanged', |
| 4060 readOnly: true, |
| 4061 type: Object |
| 4062 }, |
| 4063 attrForItemTitle: { |
| 4064 type: String |
| 4065 } |
| 4066 }, |
| 4067 hostAttributes: { |
| 4068 role: 'menu', |
| 4069 tabindex: '0' |
| 4070 }, |
| 4071 observers: [ '_updateMultiselectable(multi)' ], |
| 4072 listeners: { |
| 4073 focus: '_onFocus', |
| 4074 keydown: '_onKeydown', |
| 4075 'iron-items-changed': '_onIronItemsChanged' |
| 4076 }, |
| 4077 keyBindings: { |
| 4078 up: '_onUpKey', |
| 4079 down: '_onDownKey', |
| 4080 esc: '_onEscKey', |
| 4081 'shift+tab:keydown': '_onShiftTabDown' |
| 4082 }, |
| 4083 attached: function() { |
| 4084 this._resetTabindices(); |
| 4085 }, |
| 4086 select: function(value) { |
| 4087 if (this._defaultFocusAsync) { |
| 4088 this.cancelAsync(this._defaultFocusAsync); |
| 4089 this._defaultFocusAsync = null; |
| 4090 } |
| 4091 var item = this._valueToItem(value); |
| 4092 if (item && item.hasAttribute('disabled')) return; |
| 4093 this._setFocusedItem(item); |
| 4094 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| 4095 }, |
| 4096 _resetTabindices: function() { |
| 4097 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0]
: this.selectedItem; |
| 4098 this.items.forEach(function(item) { |
| 4099 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| 4100 }, this); |
| 4101 }, |
| 4102 _updateMultiselectable: function(multi) { |
| 4103 if (multi) { |
| 4104 this.setAttribute('aria-multiselectable', 'true'); |
| 4105 } else { |
| 4106 this.removeAttribute('aria-multiselectable'); |
| 4107 } |
| 4108 }, |
| 4109 _focusWithKeyboardEvent: function(event) { |
| 4110 for (var i = 0, item; item = this.items[i]; i++) { |
| 4111 var attr = this.attrForItemTitle || 'textContent'; |
| 4112 var title = item[attr] || item.getAttribute(attr); |
| 4113 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo
werCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| 4114 this._setFocusedItem(item); |
| 4115 break; |
| 4116 } |
| 4117 } |
| 4118 }, |
| 4119 _focusPrevious: function() { |
| 4120 var length = this.items.length; |
| 4121 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4122 for (var i = 1; i < length + 1; i++) { |
| 4123 var item = this.items[(curFocusIndex - i + length) % length]; |
| 4124 if (!item.hasAttribute('disabled')) { |
| 4125 this._setFocusedItem(item); |
| 4126 return; |
| 4127 } |
| 4128 } |
| 4129 }, |
| 4130 _focusNext: function() { |
| 4131 var length = this.items.length; |
| 4132 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4133 for (var i = 1; i < length + 1; i++) { |
| 4134 var item = this.items[(curFocusIndex + i) % length]; |
| 4135 if (!item.hasAttribute('disabled')) { |
| 4136 this._setFocusedItem(item); |
| 4137 return; |
| 4138 } |
| 4139 } |
| 4140 }, |
| 4141 _applySelection: function(item, isSelected) { |
| 4142 if (isSelected) { |
| 4143 item.setAttribute('aria-selected', 'true'); |
| 4144 } else { |
| 4145 item.removeAttribute('aria-selected'); |
| 4146 } |
| 4147 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| 4148 }, |
| 4149 _focusedItemChanged: function(focusedItem, old) { |
| 4150 old && old.setAttribute('tabindex', '-1'); |
| 4151 if (focusedItem) { |
| 4152 focusedItem.setAttribute('tabindex', '0'); |
| 4153 focusedItem.focus(); |
| 4154 } |
| 4155 }, |
| 4156 _onIronItemsChanged: function(event) { |
| 4157 if (event.detail.addedNodes.length) { |
| 4158 this._resetTabindices(); |
| 4159 } |
| 4160 }, |
| 4161 _onShiftTabDown: function(event) { |
| 4162 var oldTabIndex = this.getAttribute('tabindex'); |
| 4163 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| 4164 this._setFocusedItem(null); |
| 4165 this.setAttribute('tabindex', '-1'); |
| 4166 this.async(function() { |
| 4167 this.setAttribute('tabindex', oldTabIndex); |
| 4168 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4169 }, 1); |
| 4170 }, |
| 4171 _onFocus: function(event) { |
| 4172 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| 4173 return; |
| 4174 } |
| 4175 var rootTarget = Polymer.dom(event).rootTarget; |
| 4176 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th
is.isLightDescendant(rootTarget)) { |
| 4177 return; |
| 4178 } |
| 4179 this._defaultFocusAsync = this.async(function() { |
| 4180 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0
] : this.selectedItem; |
| 4181 this._setFocusedItem(null); |
| 4182 if (selectedItem) { |
| 4183 this._setFocusedItem(selectedItem); |
| 4184 } else if (this.items[0]) { |
| 4185 this._focusNext(); |
| 4186 } |
| 4187 }); |
| 4188 }, |
| 4189 _onUpKey: function(event) { |
| 4190 this._focusPrevious(); |
| 4191 event.detail.keyboardEvent.preventDefault(); |
| 4192 }, |
| 4193 _onDownKey: function(event) { |
| 4194 this._focusNext(); |
| 4195 event.detail.keyboardEvent.preventDefault(); |
| 4196 }, |
| 4197 _onEscKey: function(event) { |
| 4198 this.focusedItem.blur(); |
| 4199 }, |
| 4200 _onKeydown: function(event) { |
| 4201 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| 4202 this._focusWithKeyboardEvent(event); |
| 4203 } |
| 4204 event.stopPropagation(); |
| 4205 }, |
| 4206 _activateHandler: function(event) { |
| 4207 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| 4208 event.stopPropagation(); |
| 4209 } |
| 4210 }; |
| 4211 |
| 4212 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4213 |
| 4214 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1
1yKeysBehavior, Polymer.IronMenuBehaviorImpl ]; |
| 4215 |
| 4216 (function() { |
| 4217 Polymer({ |
| 4218 is: 'paper-menu', |
| 4219 behaviors: [ Polymer.IronMenuBehavior ] |
| 4220 }); |
| 4221 })(); |
| 4222 |
| 4223 Polymer.IronFitBehavior = { |
| 4224 properties: { |
| 4225 sizingTarget: { |
| 4226 type: Object, |
| 4227 value: function() { |
| 4228 return this; |
| 4229 } |
| 4230 }, |
| 4231 fitInto: { |
| 4232 type: Object, |
| 4233 value: window |
| 4234 }, |
| 4235 noOverlap: { |
| 4236 type: Boolean |
| 4237 }, |
| 4238 positionTarget: { |
| 4239 type: Element |
| 4240 }, |
| 4241 horizontalAlign: { |
| 4242 type: String |
| 4243 }, |
| 4244 verticalAlign: { |
| 4245 type: String |
| 4246 }, |
| 4247 dynamicAlign: { |
| 4248 type: Boolean |
| 4249 }, |
| 4250 horizontalOffset: { |
| 4251 type: Number, |
| 4252 value: 0, |
| 4253 notify: true |
| 4254 }, |
| 4255 verticalOffset: { |
| 4256 type: Number, |
| 4257 value: 0, |
| 4258 notify: true |
| 4259 }, |
| 4260 autoFitOnAttach: { |
| 4261 type: Boolean, |
| 4262 value: false |
| 4263 }, |
| 4264 _fitInfo: { |
| 4265 type: Object |
| 4266 } |
| 4267 }, |
| 4268 get _fitWidth() { |
| 4269 var fitWidth; |
| 4270 if (this.fitInto === window) { |
| 4271 fitWidth = this.fitInto.innerWidth; |
| 4272 } else { |
| 4273 fitWidth = this.fitInto.getBoundingClientRect().width; |
| 4274 } |
| 4275 return fitWidth; |
| 4276 }, |
| 4277 get _fitHeight() { |
| 4278 var fitHeight; |
| 4279 if (this.fitInto === window) { |
| 4280 fitHeight = this.fitInto.innerHeight; |
| 4281 } else { |
| 4282 fitHeight = this.fitInto.getBoundingClientRect().height; |
| 4283 } |
| 4284 return fitHeight; |
| 4285 }, |
| 4286 get _fitLeft() { |
| 4287 var fitLeft; |
| 4288 if (this.fitInto === window) { |
| 4289 fitLeft = 0; |
| 4290 } else { |
| 4291 fitLeft = this.fitInto.getBoundingClientRect().left; |
| 4292 } |
| 4293 return fitLeft; |
| 4294 }, |
| 4295 get _fitTop() { |
| 4296 var fitTop; |
| 4297 if (this.fitInto === window) { |
| 4298 fitTop = 0; |
| 4299 } else { |
| 4300 fitTop = this.fitInto.getBoundingClientRect().top; |
| 4301 } |
| 4302 return fitTop; |
| 4303 }, |
| 4304 get _defaultPositionTarget() { |
| 4305 var parent = Polymer.dom(this).parentNode; |
| 4306 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 4307 parent = parent.host; |
| 4308 } |
| 4309 return parent; |
| 4310 }, |
| 4311 get _localeHorizontalAlign() { |
| 4312 if (this._isRTL) { |
| 4313 if (this.horizontalAlign === 'right') { |
| 4314 return 'left'; |
| 4315 } |
| 4316 if (this.horizontalAlign === 'left') { |
| 4317 return 'right'; |
| 4318 } |
| 4319 } |
| 4320 return this.horizontalAlign; |
| 4321 }, |
| 4322 attached: function() { |
| 4323 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
| 4324 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
| 4325 if (this.autoFitOnAttach) { |
| 4326 if (window.getComputedStyle(this).display === 'none') { |
| 4327 setTimeout(function() { |
| 4328 this.fit(); |
| 4329 }.bind(this)); |
7030 } else { | 4330 } else { |
7031 return Number(value); | 4331 this.fit(); |
7032 } | 4332 } |
7033 }, | 4333 } |
7034 | 4334 }, |
7035 _indexToValue: function(index) { | 4335 fit: function() { |
7036 if (this.attrForSelected) { | 4336 this.position(); |
7037 var item = this.items[index]; | 4337 this.constrain(); |
7038 if (item) { | 4338 this.center(); |
7039 return this._valueForItem(item); | 4339 }, |
| 4340 _discoverInfo: function() { |
| 4341 if (this._fitInfo) { |
| 4342 return; |
| 4343 } |
| 4344 var target = window.getComputedStyle(this); |
| 4345 var sizer = window.getComputedStyle(this.sizingTarget); |
| 4346 this._fitInfo = { |
| 4347 inlineStyle: { |
| 4348 top: this.style.top || '', |
| 4349 left: this.style.left || '', |
| 4350 position: this.style.position || '' |
| 4351 }, |
| 4352 sizerInlineStyle: { |
| 4353 maxWidth: this.sizingTarget.style.maxWidth || '', |
| 4354 maxHeight: this.sizingTarget.style.maxHeight || '', |
| 4355 boxSizing: this.sizingTarget.style.boxSizing || '' |
| 4356 }, |
| 4357 positionedBy: { |
| 4358 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? '
bottom' : null, |
| 4359 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto'
? 'right' : null |
| 4360 }, |
| 4361 sizedBy: { |
| 4362 height: sizer.maxHeight !== 'none', |
| 4363 width: sizer.maxWidth !== 'none', |
| 4364 minWidth: parseInt(sizer.minWidth, 10) || 0, |
| 4365 minHeight: parseInt(sizer.minHeight, 10) || 0 |
| 4366 }, |
| 4367 margin: { |
| 4368 top: parseInt(target.marginTop, 10) || 0, |
| 4369 right: parseInt(target.marginRight, 10) || 0, |
| 4370 bottom: parseInt(target.marginBottom, 10) || 0, |
| 4371 left: parseInt(target.marginLeft, 10) || 0 |
| 4372 } |
| 4373 }; |
| 4374 if (this.verticalOffset) { |
| 4375 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs
et; |
| 4376 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
| 4377 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
| 4378 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px
'; |
| 4379 } |
| 4380 if (this.horizontalOffset) { |
| 4381 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf
fset; |
| 4382 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
| 4383 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
| 4384 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + '
px'; |
| 4385 } |
| 4386 }, |
| 4387 resetFit: function() { |
| 4388 var info = this._fitInfo || {}; |
| 4389 for (var property in info.sizerInlineStyle) { |
| 4390 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
| 4391 } |
| 4392 for (var property in info.inlineStyle) { |
| 4393 this.style[property] = info.inlineStyle[property]; |
| 4394 } |
| 4395 this._fitInfo = null; |
| 4396 }, |
| 4397 refit: function() { |
| 4398 var scrollLeft = this.sizingTarget.scrollLeft; |
| 4399 var scrollTop = this.sizingTarget.scrollTop; |
| 4400 this.resetFit(); |
| 4401 this.fit(); |
| 4402 this.sizingTarget.scrollLeft = scrollLeft; |
| 4403 this.sizingTarget.scrollTop = scrollTop; |
| 4404 }, |
| 4405 position: function() { |
| 4406 if (!this.horizontalAlign && !this.verticalAlign) { |
| 4407 return; |
| 4408 } |
| 4409 this._discoverInfo(); |
| 4410 this.style.position = 'fixed'; |
| 4411 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4412 this.style.left = '0px'; |
| 4413 this.style.top = '0px'; |
| 4414 var rect = this.getBoundingClientRect(); |
| 4415 var positionRect = this.__getNormalizedRect(this.positionTarget); |
| 4416 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4417 var margin = this._fitInfo.margin; |
| 4418 var size = { |
| 4419 width: rect.width + margin.left + margin.right, |
| 4420 height: rect.height + margin.top + margin.bottom |
| 4421 }; |
| 4422 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical
Align, size, positionRect, fitRect); |
| 4423 var left = position.left + margin.left; |
| 4424 var top = position.top + margin.top; |
| 4425 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
| 4426 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
| 4427 var minWidth = this._fitInfo.sizedBy.minWidth; |
| 4428 var minHeight = this._fitInfo.sizedBy.minHeight; |
| 4429 if (left < margin.left) { |
| 4430 left = margin.left; |
| 4431 if (right - left < minWidth) { |
| 4432 left = right - minWidth; |
| 4433 } |
| 4434 } |
| 4435 if (top < margin.top) { |
| 4436 top = margin.top; |
| 4437 if (bottom - top < minHeight) { |
| 4438 top = bottom - minHeight; |
| 4439 } |
| 4440 } |
| 4441 this.sizingTarget.style.maxWidth = right - left + 'px'; |
| 4442 this.sizingTarget.style.maxHeight = bottom - top + 'px'; |
| 4443 this.style.left = left - rect.left + 'px'; |
| 4444 this.style.top = top - rect.top + 'px'; |
| 4445 }, |
| 4446 constrain: function() { |
| 4447 if (this.horizontalAlign || this.verticalAlign) { |
| 4448 return; |
| 4449 } |
| 4450 this._discoverInfo(); |
| 4451 var info = this._fitInfo; |
| 4452 if (!info.positionedBy.vertically) { |
| 4453 this.style.position = 'fixed'; |
| 4454 this.style.top = '0px'; |
| 4455 } |
| 4456 if (!info.positionedBy.horizontally) { |
| 4457 this.style.position = 'fixed'; |
| 4458 this.style.left = '0px'; |
| 4459 } |
| 4460 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4461 var rect = this.getBoundingClientRect(); |
| 4462 if (!info.sizedBy.height) { |
| 4463 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom',
'Height'); |
| 4464 } |
| 4465 if (!info.sizedBy.width) { |
| 4466 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right'
, 'Width'); |
| 4467 } |
| 4468 }, |
| 4469 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4470 this.__sizeDimension(rect, positionedBy, start, end, extent); |
| 4471 }, |
| 4472 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4473 var info = this._fitInfo; |
| 4474 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4475 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
| 4476 var flip = positionedBy === end; |
| 4477 var offset = flip ? max - rect[end] : rect[start]; |
| 4478 var margin = info.margin[flip ? start : end]; |
| 4479 var offsetExtent = 'offset' + extent; |
| 4480 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
| 4481 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs
et + 'px'; |
| 4482 }, |
| 4483 center: function() { |
| 4484 if (this.horizontalAlign || this.verticalAlign) { |
| 4485 return; |
| 4486 } |
| 4487 this._discoverInfo(); |
| 4488 var positionedBy = this._fitInfo.positionedBy; |
| 4489 if (positionedBy.vertically && positionedBy.horizontally) { |
| 4490 return; |
| 4491 } |
| 4492 this.style.position = 'fixed'; |
| 4493 if (!positionedBy.vertically) { |
| 4494 this.style.top = '0px'; |
| 4495 } |
| 4496 if (!positionedBy.horizontally) { |
| 4497 this.style.left = '0px'; |
| 4498 } |
| 4499 var rect = this.getBoundingClientRect(); |
| 4500 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4501 if (!positionedBy.vertically) { |
| 4502 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
| 4503 this.style.top = top + 'px'; |
| 4504 } |
| 4505 if (!positionedBy.horizontally) { |
| 4506 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
| 4507 this.style.left = left + 'px'; |
| 4508 } |
| 4509 }, |
| 4510 __getNormalizedRect: function(target) { |
| 4511 if (target === document.documentElement || target === window) { |
| 4512 return { |
| 4513 top: 0, |
| 4514 left: 0, |
| 4515 width: window.innerWidth, |
| 4516 height: window.innerHeight, |
| 4517 right: window.innerWidth, |
| 4518 bottom: window.innerHeight |
| 4519 }; |
| 4520 } |
| 4521 return target.getBoundingClientRect(); |
| 4522 }, |
| 4523 __getCroppedArea: function(position, size, fitRect) { |
| 4524 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom -
(position.top + size.height)); |
| 4525 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right
- (position.left + size.width)); |
| 4526 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size
.height; |
| 4527 }, |
| 4528 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
| 4529 var positions = [ { |
| 4530 verticalAlign: 'top', |
| 4531 horizontalAlign: 'left', |
| 4532 top: positionRect.top, |
| 4533 left: positionRect.left |
| 4534 }, { |
| 4535 verticalAlign: 'top', |
| 4536 horizontalAlign: 'right', |
| 4537 top: positionRect.top, |
| 4538 left: positionRect.right - size.width |
| 4539 }, { |
| 4540 verticalAlign: 'bottom', |
| 4541 horizontalAlign: 'left', |
| 4542 top: positionRect.bottom - size.height, |
| 4543 left: positionRect.left |
| 4544 }, { |
| 4545 verticalAlign: 'bottom', |
| 4546 horizontalAlign: 'right', |
| 4547 top: positionRect.bottom - size.height, |
| 4548 left: positionRect.right - size.width |
| 4549 } ]; |
| 4550 if (this.noOverlap) { |
| 4551 for (var i = 0, l = positions.length; i < l; i++) { |
| 4552 var copy = {}; |
| 4553 for (var key in positions[i]) { |
| 4554 copy[key] = positions[i][key]; |
7040 } | 4555 } |
7041 } else { | 4556 positions.push(copy); |
7042 return index; | 4557 } |
7043 } | 4558 positions[0].top = positions[1].top += positionRect.height; |
7044 }, | 4559 positions[2].top = positions[3].top -= positionRect.height; |
7045 | 4560 positions[4].left = positions[6].left += positionRect.width; |
7046 _valueForItem: function(item) { | 4561 positions[5].left = positions[7].left -= positionRect.width; |
7047 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | 4562 } |
7048 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | 4563 vAlign = vAlign === 'auto' ? null : vAlign; |
7049 }, | 4564 hAlign = hAlign === 'auto' ? null : hAlign; |
7050 | 4565 var position; |
7051 _applySelection: function(item, isSelected) { | 4566 for (var i = 0; i < positions.length; i++) { |
7052 if (this.selectedClass) { | 4567 var pos = positions[i]; |
7053 this.toggleClass(this.selectedClass, isSelected, item); | 4568 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign
&& pos.horizontalAlign === hAlign) { |
7054 } | 4569 position = pos; |
7055 if (this.selectedAttribute) { | 4570 break; |
7056 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 4571 } |
7057 } | 4572 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos
.horizontalAlign === hAlign); |
7058 this._selectionChange(); | 4573 if (!this.dynamicAlign && !alignOk) { |
7059 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | 4574 continue; |
7060 }, | 4575 } |
7061 | 4576 position = position || pos; |
7062 _selectionChange: function() { | 4577 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
7063 this._setSelectedItem(this._selection.get()); | 4578 var diff = pos.croppedArea - position.croppedArea; |
7064 }, | 4579 if (diff < 0 || diff === 0 && alignOk) { |
7065 | 4580 position = pos; |
7066 // observe items change under the given node. | 4581 } |
7067 _observeItems: function(node) { | 4582 if (position.croppedArea === 0 && alignOk) { |
7068 return Polymer.dom(node).observeNodes(function(mutation) { | 4583 break; |
7069 this._updateItems(); | 4584 } |
7070 | 4585 } |
7071 if (this._shouldUpdateSelection) { | 4586 return position; |
7072 this._updateSelected(); | 4587 } |
7073 } | 4588 }; |
7074 | 4589 |
7075 // Let other interested parties know about the change so that | 4590 (function() { |
7076 // we don't have to recreate mutation observers everywhere. | 4591 'use strict'; |
7077 this.fire('iron-items-changed', mutation, { | 4592 Polymer({ |
7078 bubbles: false, | 4593 is: 'iron-overlay-backdrop', |
7079 cancelable: false | |
7080 }); | |
7081 }); | |
7082 }, | |
7083 | |
7084 _activateHandler: function(e) { | |
7085 var t = e.target; | |
7086 var items = this.items; | |
7087 while (t && t != this) { | |
7088 var i = items.indexOf(t); | |
7089 if (i >= 0) { | |
7090 var value = this._indexToValue(i); | |
7091 this._itemActivate(value, t); | |
7092 return; | |
7093 } | |
7094 t = t.parentNode; | |
7095 } | |
7096 }, | |
7097 | |
7098 _itemActivate: function(value, item) { | |
7099 if (!this.fire('iron-activate', | |
7100 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | |
7101 this.select(value); | |
7102 } | |
7103 } | |
7104 | |
7105 }; | |
7106 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
7107 Polymer.IronMultiSelectableBehaviorImpl = { | |
7108 properties: { | 4594 properties: { |
7109 | |
7110 /** | |
7111 * If true, multiple selections are allowed. | |
7112 */ | |
7113 multi: { | |
7114 type: Boolean, | |
7115 value: false, | |
7116 observer: 'multiChanged' | |
7117 }, | |
7118 | |
7119 /** | |
7120 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
7121 * is true. | |
7122 */ | |
7123 selectedValues: { | |
7124 type: Array, | |
7125 notify: true | |
7126 }, | |
7127 | |
7128 /** | |
7129 * Returns an array of currently selected items. | |
7130 */ | |
7131 selectedItems: { | |
7132 type: Array, | |
7133 readOnly: true, | |
7134 notify: true | |
7135 }, | |
7136 | |
7137 }, | |
7138 | |
7139 observers: [ | |
7140 '_updateSelected(selectedValues.splices)' | |
7141 ], | |
7142 | |
7143 /** | |
7144 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7145 * `value` will be toggled; otherwise the `value` will be selected. | |
7146 * | |
7147 * @method select | |
7148 * @param {string|number} value the value to select. | |
7149 */ | |
7150 select: function(value) { | |
7151 if (this.multi) { | |
7152 if (this.selectedValues) { | |
7153 this._toggleSelected(value); | |
7154 } else { | |
7155 this.selectedValues = [value]; | |
7156 } | |
7157 } else { | |
7158 this.selected = value; | |
7159 } | |
7160 }, | |
7161 | |
7162 multiChanged: function(multi) { | |
7163 this._selection.multi = multi; | |
7164 }, | |
7165 | |
7166 get _shouldUpdateSelection() { | |
7167 return this.selected != null || | |
7168 (this.selectedValues != null && this.selectedValues.length); | |
7169 }, | |
7170 | |
7171 _updateAttrForSelected: function() { | |
7172 if (!this.multi) { | |
7173 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
7174 } else if (this._shouldUpdateSelection) { | |
7175 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
7176 return this._indexToValue(this.indexOf(selectedItem)); | |
7177 }, this).filter(function(unfilteredValue) { | |
7178 return unfilteredValue != null; | |
7179 }, this); | |
7180 } | |
7181 }, | |
7182 | |
7183 _updateSelected: function() { | |
7184 if (this.multi) { | |
7185 this._selectMulti(this.selectedValues); | |
7186 } else { | |
7187 this._selectSelected(this.selected); | |
7188 } | |
7189 }, | |
7190 | |
7191 _selectMulti: function(values) { | |
7192 if (values) { | |
7193 var selectedItems = this._valuesToItems(values); | |
7194 // clear all but the current selected items | |
7195 this._selection.clear(selectedItems); | |
7196 // select only those not selected yet | |
7197 for (var i = 0; i < selectedItems.length; i++) { | |
7198 this._selection.setItemSelected(selectedItems[i], true); | |
7199 } | |
7200 // Check for items, since this array is populated only when attached | |
7201 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
7202 var fallback = this._valueToItem(this.fallbackSelection); | |
7203 if (fallback) { | |
7204 this.selectedValues = [this.fallbackSelection]; | |
7205 } | |
7206 } | |
7207 } else { | |
7208 this._selection.clear(); | |
7209 } | |
7210 }, | |
7211 | |
7212 _selectionChange: function() { | |
7213 var s = this._selection.get(); | |
7214 if (this.multi) { | |
7215 this._setSelectedItems(s); | |
7216 } else { | |
7217 this._setSelectedItems([s]); | |
7218 this._setSelectedItem(s); | |
7219 } | |
7220 }, | |
7221 | |
7222 _toggleSelected: function(value) { | |
7223 var i = this.selectedValues.indexOf(value); | |
7224 var unselected = i < 0; | |
7225 if (unselected) { | |
7226 this.push('selectedValues',value); | |
7227 } else { | |
7228 this.splice('selectedValues',i,1); | |
7229 } | |
7230 }, | |
7231 | |
7232 _valuesToItems: function(values) { | |
7233 return (values == null) ? null : values.map(function(value) { | |
7234 return this._valueToItem(value); | |
7235 }, this); | |
7236 } | |
7237 }; | |
7238 | |
7239 /** @polymerBehavior */ | |
7240 Polymer.IronMultiSelectableBehavior = [ | |
7241 Polymer.IronSelectableBehavior, | |
7242 Polymer.IronMultiSelectableBehaviorImpl | |
7243 ]; | |
7244 /** | |
7245 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
7246 * | |
7247 * @demo demo/index.html | |
7248 * @polymerBehavior Polymer.IronMenuBehavior | |
7249 */ | |
7250 Polymer.IronMenuBehaviorImpl = { | |
7251 | |
7252 properties: { | |
7253 | |
7254 /** | |
7255 * Returns the currently focused item. | |
7256 * @type {?Object} | |
7257 */ | |
7258 focusedItem: { | |
7259 observer: '_focusedItemChanged', | |
7260 readOnly: true, | |
7261 type: Object | |
7262 }, | |
7263 | |
7264 /** | |
7265 * The attribute to use on menu items to look up the item title. Typing th
e first | |
7266 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
7267 * will be used. | |
7268 */ | |
7269 attrForItemTitle: { | |
7270 type: String | |
7271 } | |
7272 }, | |
7273 | |
7274 hostAttributes: { | |
7275 'role': 'menu', | |
7276 'tabindex': '0' | |
7277 }, | |
7278 | |
7279 observers: [ | |
7280 '_updateMultiselectable(multi)' | |
7281 ], | |
7282 | |
7283 listeners: { | |
7284 'focus': '_onFocus', | |
7285 'keydown': '_onKeydown', | |
7286 'iron-items-changed': '_onIronItemsChanged' | |
7287 }, | |
7288 | |
7289 keyBindings: { | |
7290 'up': '_onUpKey', | |
7291 'down': '_onDownKey', | |
7292 'esc': '_onEscKey', | |
7293 'shift+tab:keydown': '_onShiftTabDown' | |
7294 }, | |
7295 | |
7296 attached: function() { | |
7297 this._resetTabindices(); | |
7298 }, | |
7299 | |
7300 /** | |
7301 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7302 * `value` will be toggled; otherwise the `value` will be selected. | |
7303 * | |
7304 * @param {string|number} value the value to select. | |
7305 */ | |
7306 select: function(value) { | |
7307 // Cancel automatically focusing a default item if the menu received focus | |
7308 // through a user action selecting a particular item. | |
7309 if (this._defaultFocusAsync) { | |
7310 this.cancelAsync(this._defaultFocusAsync); | |
7311 this._defaultFocusAsync = null; | |
7312 } | |
7313 var item = this._valueToItem(value); | |
7314 if (item && item.hasAttribute('disabled')) return; | |
7315 this._setFocusedItem(item); | |
7316 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | |
7317 }, | |
7318 | |
7319 /** | |
7320 * Resets all tabindex attributes to the appropriate value based on the | |
7321 * current selection state. The appropriate value is `0` (focusable) for | |
7322 * the default selected item, and `-1` (not keyboard focusable) for all | |
7323 * other items. | |
7324 */ | |
7325 _resetTabindices: function() { | |
7326 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | |
7327 | |
7328 this.items.forEach(function(item) { | |
7329 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | |
7330 }, this); | |
7331 }, | |
7332 | |
7333 /** | |
7334 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
7335 * multi-selectable. | |
7336 * | |
7337 * @param {boolean} multi True if the menu should be multi-selectable. | |
7338 */ | |
7339 _updateMultiselectable: function(multi) { | |
7340 if (multi) { | |
7341 this.setAttribute('aria-multiselectable', 'true'); | |
7342 } else { | |
7343 this.removeAttribute('aria-multiselectable'); | |
7344 } | |
7345 }, | |
7346 | |
7347 /** | |
7348 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
7349 * menu (if there is a relevant item, and it is possible to focus it). | |
7350 * | |
7351 * @param {KeyboardEvent} event A KeyboardEvent. | |
7352 */ | |
7353 _focusWithKeyboardEvent: function(event) { | |
7354 for (var i = 0, item; item = this.items[i]; i++) { | |
7355 var attr = this.attrForItemTitle || 'textContent'; | |
7356 var title = item[attr] || item.getAttribute(attr); | |
7357 | |
7358 if (!item.hasAttribute('disabled') && title && | |
7359 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | |
7360 this._setFocusedItem(item); | |
7361 break; | |
7362 } | |
7363 } | |
7364 }, | |
7365 | |
7366 /** | |
7367 * Focuses the previous item (relative to the currently focused item) in the | |
7368 * menu, disabled items will be skipped. | |
7369 * Loop until length + 1 to handle case of single item in menu. | |
7370 */ | |
7371 _focusPrevious: function() { | |
7372 var length = this.items.length; | |
7373 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7374 for (var i = 1; i < length + 1; i++) { | |
7375 var item = this.items[(curFocusIndex - i + length) % length]; | |
7376 if (!item.hasAttribute('disabled')) { | |
7377 this._setFocusedItem(item); | |
7378 return; | |
7379 } | |
7380 } | |
7381 }, | |
7382 | |
7383 /** | |
7384 * Focuses the next item (relative to the currently focused item) in the | |
7385 * menu, disabled items will be skipped. | |
7386 * Loop until length + 1 to handle case of single item in menu. | |
7387 */ | |
7388 _focusNext: function() { | |
7389 var length = this.items.length; | |
7390 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7391 for (var i = 1; i < length + 1; i++) { | |
7392 var item = this.items[(curFocusIndex + i) % length]; | |
7393 if (!item.hasAttribute('disabled')) { | |
7394 this._setFocusedItem(item); | |
7395 return; | |
7396 } | |
7397 } | |
7398 }, | |
7399 | |
7400 /** | |
7401 * Mutates items in the menu based on provided selection details, so that | |
7402 * all items correctly reflect selection state. | |
7403 * | |
7404 * @param {Element} item An item in the menu. | |
7405 * @param {boolean} isSelected True if the item should be shown in a | |
7406 * selected state, otherwise false. | |
7407 */ | |
7408 _applySelection: function(item, isSelected) { | |
7409 if (isSelected) { | |
7410 item.setAttribute('aria-selected', 'true'); | |
7411 } else { | |
7412 item.removeAttribute('aria-selected'); | |
7413 } | |
7414 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | |
7415 }, | |
7416 | |
7417 /** | |
7418 * Discretely updates tabindex values among menu items as the focused item | |
7419 * changes. | |
7420 * | |
7421 * @param {Element} focusedItem The element that is currently focused. | |
7422 * @param {?Element} old The last element that was considered focused, if | |
7423 * applicable. | |
7424 */ | |
7425 _focusedItemChanged: function(focusedItem, old) { | |
7426 old && old.setAttribute('tabindex', '-1'); | |
7427 if (focusedItem) { | |
7428 focusedItem.setAttribute('tabindex', '0'); | |
7429 focusedItem.focus(); | |
7430 } | |
7431 }, | |
7432 | |
7433 /** | |
7434 * A handler that responds to mutation changes related to the list of items | |
7435 * in the menu. | |
7436 * | |
7437 * @param {CustomEvent} event An event containing mutation records as its | |
7438 * detail. | |
7439 */ | |
7440 _onIronItemsChanged: function(event) { | |
7441 if (event.detail.addedNodes.length) { | |
7442 this._resetTabindices(); | |
7443 } | |
7444 }, | |
7445 | |
7446 /** | |
7447 * Handler that is called when a shift+tab keypress is detected by the menu. | |
7448 * | |
7449 * @param {CustomEvent} event A key combination event. | |
7450 */ | |
7451 _onShiftTabDown: function(event) { | |
7452 var oldTabIndex = this.getAttribute('tabindex'); | |
7453 | |
7454 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
7455 | |
7456 this._setFocusedItem(null); | |
7457 | |
7458 this.setAttribute('tabindex', '-1'); | |
7459 | |
7460 this.async(function() { | |
7461 this.setAttribute('tabindex', oldTabIndex); | |
7462 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7463 // NOTE(cdata): polymer/polymer#1305 | |
7464 }, 1); | |
7465 }, | |
7466 | |
7467 /** | |
7468 * Handler that is called when the menu receives focus. | |
7469 * | |
7470 * @param {FocusEvent} event A focus event. | |
7471 */ | |
7472 _onFocus: function(event) { | |
7473 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
7474 // do not focus the menu itself | |
7475 return; | |
7476 } | |
7477 | |
7478 // Do not focus the selected tab if the deepest target is part of the | |
7479 // menu element's local DOM and is focusable. | |
7480 var rootTarget = /** @type {?HTMLElement} */( | |
7481 Polymer.dom(event).rootTarget); | |
7482 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
7483 return; | |
7484 } | |
7485 | |
7486 // clear the cached focus item | |
7487 this._defaultFocusAsync = this.async(function() { | |
7488 // focus the selected item when the menu receives focus, or the first it
em | |
7489 // if no item is selected | |
7490 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
7491 | |
7492 this._setFocusedItem(null); | |
7493 | |
7494 if (selectedItem) { | |
7495 this._setFocusedItem(selectedItem); | |
7496 } else if (this.items[0]) { | |
7497 // We find the first none-disabled item (if one exists) | |
7498 this._focusNext(); | |
7499 } | |
7500 }); | |
7501 }, | |
7502 | |
7503 /** | |
7504 * Handler that is called when the up key is pressed. | |
7505 * | |
7506 * @param {CustomEvent} event A key combination event. | |
7507 */ | |
7508 _onUpKey: function(event) { | |
7509 // up and down arrows moves the focus | |
7510 this._focusPrevious(); | |
7511 event.detail.keyboardEvent.preventDefault(); | |
7512 }, | |
7513 | |
7514 /** | |
7515 * Handler that is called when the down key is pressed. | |
7516 * | |
7517 * @param {CustomEvent} event A key combination event. | |
7518 */ | |
7519 _onDownKey: function(event) { | |
7520 this._focusNext(); | |
7521 event.detail.keyboardEvent.preventDefault(); | |
7522 }, | |
7523 | |
7524 /** | |
7525 * Handler that is called when the esc key is pressed. | |
7526 * | |
7527 * @param {CustomEvent} event A key combination event. | |
7528 */ | |
7529 _onEscKey: function(event) { | |
7530 // esc blurs the control | |
7531 this.focusedItem.blur(); | |
7532 }, | |
7533 | |
7534 /** | |
7535 * Handler that is called when a keydown event is detected. | |
7536 * | |
7537 * @param {KeyboardEvent} event A keyboard event. | |
7538 */ | |
7539 _onKeydown: function(event) { | |
7540 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
7541 // all other keys focus the menu item starting with that character | |
7542 this._focusWithKeyboardEvent(event); | |
7543 } | |
7544 event.stopPropagation(); | |
7545 }, | |
7546 | |
7547 // override _activateHandler | |
7548 _activateHandler: function(event) { | |
7549 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
7550 event.stopPropagation(); | |
7551 } | |
7552 }; | |
7553 | |
7554 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7555 | |
7556 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
7557 Polymer.IronMenuBehavior = [ | |
7558 Polymer.IronMultiSelectableBehavior, | |
7559 Polymer.IronA11yKeysBehavior, | |
7560 Polymer.IronMenuBehaviorImpl | |
7561 ]; | |
7562 (function() { | |
7563 Polymer({ | |
7564 is: 'paper-menu', | |
7565 | |
7566 behaviors: [ | |
7567 Polymer.IronMenuBehavior | |
7568 ] | |
7569 }); | |
7570 })(); | |
7571 /** | |
7572 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
7573 optionally centers it in the window or another element. | |
7574 | |
7575 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | |
7576 by CSS. | |
7577 | |
7578 CSS properties | Action | |
7579 -----------------------------|------------------------------------------- | |
7580 `position` set | Element is not centered horizontally or verticall
y | |
7581 `top` or `bottom` set | Element is not vertically centered | |
7582 `left` or `right` set | Element is not horizontally centered | |
7583 `max-height` set | Element respects `max-height` | |
7584 `max-width` set | Element respects `max-width` | |
7585 | |
7586 `Polymer.IronFitBehavior` can position an element into another element using | |
7587 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
7588 | |
7589 <div class="container"> | |
7590 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
7591 Positioned into the container | |
7592 </iron-fit-impl> | |
7593 </div> | |
7594 | |
7595 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
7596 | |
7597 <div class="container"> | |
7598 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
7599 Positioned around the container | |
7600 </iron-fit-impl> | |
7601 </div> | |
7602 | |
7603 @demo demo/index.html | |
7604 @polymerBehavior | |
7605 */ | |
7606 | |
7607 Polymer.IronFitBehavior = { | |
7608 | |
7609 properties: { | |
7610 | |
7611 /** | |
7612 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
7613 * but it can be set to a child element. This is useful, for example, for
implementing a | |
7614 * scrolling region inside the element. | |
7615 * @type {!Element} | |
7616 */ | |
7617 sizingTarget: { | |
7618 type: Object, | |
7619 value: function() { | |
7620 return this; | |
7621 } | |
7622 }, | |
7623 | |
7624 /** | |
7625 * The element to fit `this` into. | |
7626 */ | |
7627 fitInto: { | |
7628 type: Object, | |
7629 value: window | |
7630 }, | |
7631 | |
7632 /** | |
7633 * Will position the element around the positionTarget without overlapping
it. | |
7634 */ | |
7635 noOverlap: { | |
7636 type: Boolean | |
7637 }, | |
7638 | |
7639 /** | |
7640 * The element that should be used to position the element. If not set, it
will | |
7641 * default to the parent node. | |
7642 * @type {!Element} | |
7643 */ | |
7644 positionTarget: { | |
7645 type: Element | |
7646 }, | |
7647 | |
7648 /** | |
7649 * The orientation against which to align the element horizontally | |
7650 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
7651 */ | |
7652 horizontalAlign: { | |
7653 type: String | |
7654 }, | |
7655 | |
7656 /** | |
7657 * The orientation against which to align the element vertically | |
7658 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
7659 */ | |
7660 verticalAlign: { | |
7661 type: String | |
7662 }, | |
7663 | |
7664 /** | |
7665 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
7666 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
7667 */ | |
7668 dynamicAlign: { | |
7669 type: Boolean | |
7670 }, | |
7671 | |
7672 /** | |
7673 * The same as setting margin-left and margin-right css properties. | |
7674 * @deprecated | |
7675 */ | |
7676 horizontalOffset: { | |
7677 type: Number, | |
7678 value: 0, | |
7679 notify: true | |
7680 }, | |
7681 | |
7682 /** | |
7683 * The same as setting margin-top and margin-bottom css properties. | |
7684 * @deprecated | |
7685 */ | |
7686 verticalOffset: { | |
7687 type: Number, | |
7688 value: 0, | |
7689 notify: true | |
7690 }, | |
7691 | |
7692 /** | |
7693 * Set to true to auto-fit on attach. | |
7694 */ | |
7695 autoFitOnAttach: { | |
7696 type: Boolean, | |
7697 value: false | |
7698 }, | |
7699 | |
7700 /** @type {?Object} */ | |
7701 _fitInfo: { | |
7702 type: Object | |
7703 } | |
7704 }, | |
7705 | |
7706 get _fitWidth() { | |
7707 var fitWidth; | |
7708 if (this.fitInto === window) { | |
7709 fitWidth = this.fitInto.innerWidth; | |
7710 } else { | |
7711 fitWidth = this.fitInto.getBoundingClientRect().width; | |
7712 } | |
7713 return fitWidth; | |
7714 }, | |
7715 | |
7716 get _fitHeight() { | |
7717 var fitHeight; | |
7718 if (this.fitInto === window) { | |
7719 fitHeight = this.fitInto.innerHeight; | |
7720 } else { | |
7721 fitHeight = this.fitInto.getBoundingClientRect().height; | |
7722 } | |
7723 return fitHeight; | |
7724 }, | |
7725 | |
7726 get _fitLeft() { | |
7727 var fitLeft; | |
7728 if (this.fitInto === window) { | |
7729 fitLeft = 0; | |
7730 } else { | |
7731 fitLeft = this.fitInto.getBoundingClientRect().left; | |
7732 } | |
7733 return fitLeft; | |
7734 }, | |
7735 | |
7736 get _fitTop() { | |
7737 var fitTop; | |
7738 if (this.fitInto === window) { | |
7739 fitTop = 0; | |
7740 } else { | |
7741 fitTop = this.fitInto.getBoundingClientRect().top; | |
7742 } | |
7743 return fitTop; | |
7744 }, | |
7745 | |
7746 /** | |
7747 * The element that should be used to position the element, | |
7748 * if no position target is configured. | |
7749 */ | |
7750 get _defaultPositionTarget() { | |
7751 var parent = Polymer.dom(this).parentNode; | |
7752 | |
7753 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
7754 parent = parent.host; | |
7755 } | |
7756 | |
7757 return parent; | |
7758 }, | |
7759 | |
7760 /** | |
7761 * The horizontal align value, accounting for the RTL/LTR text direction. | |
7762 */ | |
7763 get _localeHorizontalAlign() { | |
7764 if (this._isRTL) { | |
7765 // In RTL, "left" becomes "right". | |
7766 if (this.horizontalAlign === 'right') { | |
7767 return 'left'; | |
7768 } | |
7769 if (this.horizontalAlign === 'left') { | |
7770 return 'right'; | |
7771 } | |
7772 } | |
7773 return this.horizontalAlign; | |
7774 }, | |
7775 | |
7776 attached: function() { | |
7777 // Memoize this to avoid expensive calculations & relayouts. | |
7778 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | |
7779 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | |
7780 if (this.autoFitOnAttach) { | |
7781 if (window.getComputedStyle(this).display === 'none') { | |
7782 setTimeout(function() { | |
7783 this.fit(); | |
7784 }.bind(this)); | |
7785 } else { | |
7786 this.fit(); | |
7787 } | |
7788 } | |
7789 }, | |
7790 | |
7791 /** | |
7792 * Positions and fits the element into the `fitInto` element. | |
7793 */ | |
7794 fit: function() { | |
7795 this.position(); | |
7796 this.constrain(); | |
7797 this.center(); | |
7798 }, | |
7799 | |
7800 /** | |
7801 * Memoize information needed to position and size the target element. | |
7802 * @suppress {deprecated} | |
7803 */ | |
7804 _discoverInfo: function() { | |
7805 if (this._fitInfo) { | |
7806 return; | |
7807 } | |
7808 var target = window.getComputedStyle(this); | |
7809 var sizer = window.getComputedStyle(this.sizingTarget); | |
7810 | |
7811 this._fitInfo = { | |
7812 inlineStyle: { | |
7813 top: this.style.top || '', | |
7814 left: this.style.left || '', | |
7815 position: this.style.position || '' | |
7816 }, | |
7817 sizerInlineStyle: { | |
7818 maxWidth: this.sizingTarget.style.maxWidth || '', | |
7819 maxHeight: this.sizingTarget.style.maxHeight || '', | |
7820 boxSizing: this.sizingTarget.style.boxSizing || '' | |
7821 }, | |
7822 positionedBy: { | |
7823 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto'
? | |
7824 'bottom' : null), | |
7825 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut
o' ? | |
7826 'right' : null) | |
7827 }, | |
7828 sizedBy: { | |
7829 height: sizer.maxHeight !== 'none', | |
7830 width: sizer.maxWidth !== 'none', | |
7831 minWidth: parseInt(sizer.minWidth, 10) || 0, | |
7832 minHeight: parseInt(sizer.minHeight, 10) || 0 | |
7833 }, | |
7834 margin: { | |
7835 top: parseInt(target.marginTop, 10) || 0, | |
7836 right: parseInt(target.marginRight, 10) || 0, | |
7837 bottom: parseInt(target.marginBottom, 10) || 0, | |
7838 left: parseInt(target.marginLeft, 10) || 0 | |
7839 } | |
7840 }; | |
7841 | |
7842 // Support these properties until they are removed. | |
7843 if (this.verticalOffset) { | |
7844 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | |
7845 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | |
7846 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | |
7847 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | |
7848 } | |
7849 if (this.horizontalOffset) { | |
7850 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | |
7851 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | |
7852 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | |
7853 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | |
7854 } | |
7855 }, | |
7856 | |
7857 /** | |
7858 * Resets the target element's position and size constraints, and clear | |
7859 * the memoized data. | |
7860 */ | |
7861 resetFit: function() { | |
7862 var info = this._fitInfo || {}; | |
7863 for (var property in info.sizerInlineStyle) { | |
7864 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | |
7865 } | |
7866 for (var property in info.inlineStyle) { | |
7867 this.style[property] = info.inlineStyle[property]; | |
7868 } | |
7869 | |
7870 this._fitInfo = null; | |
7871 }, | |
7872 | |
7873 /** | |
7874 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
7875 * the element or the `fitInto` element has been resized, or if any of the | |
7876 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
7877 * It preserves the scroll position of the sizingTarget. | |
7878 */ | |
7879 refit: function() { | |
7880 var scrollLeft = this.sizingTarget.scrollLeft; | |
7881 var scrollTop = this.sizingTarget.scrollTop; | |
7882 this.resetFit(); | |
7883 this.fit(); | |
7884 this.sizingTarget.scrollLeft = scrollLeft; | |
7885 this.sizingTarget.scrollTop = scrollTop; | |
7886 }, | |
7887 | |
7888 /** | |
7889 * Positions the element according to `horizontalAlign, verticalAlign`. | |
7890 */ | |
7891 position: function() { | |
7892 if (!this.horizontalAlign && !this.verticalAlign) { | |
7893 // needs to be centered, and it is done after constrain. | |
7894 return; | |
7895 } | |
7896 this._discoverInfo(); | |
7897 | |
7898 this.style.position = 'fixed'; | |
7899 // Need border-box for margin/padding. | |
7900 this.sizingTarget.style.boxSizing = 'border-box'; | |
7901 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
7902 this.style.left = '0px'; | |
7903 this.style.top = '0px'; | |
7904 | |
7905 var rect = this.getBoundingClientRect(); | |
7906 var positionRect = this.__getNormalizedRect(this.positionTarget); | |
7907 var fitRect = this.__getNormalizedRect(this.fitInto); | |
7908 | |
7909 var margin = this._fitInfo.margin; | |
7910 | |
7911 // Consider the margin as part of the size for position calculations. | |
7912 var size = { | |
7913 width: rect.width + margin.left + margin.right, | |
7914 height: rect.height + margin.top + margin.bottom | |
7915 }; | |
7916 | |
7917 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | |
7918 | |
7919 var left = position.left + margin.left; | |
7920 var top = position.top + margin.top; | |
7921 | |
7922 // Use original size (without margin). | |
7923 var right = Math.min(fitRect.right - margin.right, left + rect.width); | |
7924 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | |
7925 | |
7926 var minWidth = this._fitInfo.sizedBy.minWidth; | |
7927 var minHeight = this._fitInfo.sizedBy.minHeight; | |
7928 if (left < margin.left) { | |
7929 left = margin.left; | |
7930 if (right - left < minWidth) { | |
7931 left = right - minWidth; | |
7932 } | |
7933 } | |
7934 if (top < margin.top) { | |
7935 top = margin.top; | |
7936 if (bottom - top < minHeight) { | |
7937 top = bottom - minHeight; | |
7938 } | |
7939 } | |
7940 | |
7941 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | |
7942 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | |
7943 | |
7944 // Remove the offset caused by any stacking context. | |
7945 this.style.left = (left - rect.left) + 'px'; | |
7946 this.style.top = (top - rect.top) + 'px'; | |
7947 }, | |
7948 | |
7949 /** | |
7950 * Constrains the size of the element to `fitInto` by setting `max-height` | |
7951 * and/or `max-width`. | |
7952 */ | |
7953 constrain: function() { | |
7954 if (this.horizontalAlign || this.verticalAlign) { | |
7955 return; | |
7956 } | |
7957 this._discoverInfo(); | |
7958 | |
7959 var info = this._fitInfo; | |
7960 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
7961 if (!info.positionedBy.vertically) { | |
7962 this.style.position = 'fixed'; | |
7963 this.style.top = '0px'; | |
7964 } | |
7965 if (!info.positionedBy.horizontally) { | |
7966 this.style.position = 'fixed'; | |
7967 this.style.left = '0px'; | |
7968 } | |
7969 | |
7970 // need border-box for margin/padding | |
7971 this.sizingTarget.style.boxSizing = 'border-box'; | |
7972 // constrain the width and height if not already set | |
7973 var rect = this.getBoundingClientRect(); | |
7974 if (!info.sizedBy.height) { | |
7975 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | |
7976 } | |
7977 if (!info.sizedBy.width) { | |
7978 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | |
7979 } | |
7980 }, | |
7981 | |
7982 /** | |
7983 * @protected | |
7984 * @deprecated | |
7985 */ | |
7986 _sizeDimension: function(rect, positionedBy, start, end, extent) { | |
7987 this.__sizeDimension(rect, positionedBy, start, end, extent); | |
7988 }, | |
7989 | |
7990 /** | |
7991 * @private | |
7992 */ | |
7993 __sizeDimension: function(rect, positionedBy, start, end, extent) { | |
7994 var info = this._fitInfo; | |
7995 var fitRect = this.__getNormalizedRect(this.fitInto); | |
7996 var max = extent === 'Width' ? fitRect.width : fitRect.height; | |
7997 var flip = (positionedBy === end); | |
7998 var offset = flip ? max - rect[end] : rect[start]; | |
7999 var margin = info.margin[flip ? start : end]; | |
8000 var offsetExtent = 'offset' + extent; | |
8001 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | |
8002 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | |
8003 }, | |
8004 | |
8005 /** | |
8006 * Centers horizontally and vertically if not already positioned. This also
sets | |
8007 * `position:fixed`. | |
8008 */ | |
8009 center: function() { | |
8010 if (this.horizontalAlign || this.verticalAlign) { | |
8011 return; | |
8012 } | |
8013 this._discoverInfo(); | |
8014 | |
8015 var positionedBy = this._fitInfo.positionedBy; | |
8016 if (positionedBy.vertically && positionedBy.horizontally) { | |
8017 // Already positioned. | |
8018 return; | |
8019 } | |
8020 // Need position:fixed to center | |
8021 this.style.position = 'fixed'; | |
8022 // Take into account the offset caused by parents that create stacking | |
8023 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
8024 // measure the bounding rect. | |
8025 if (!positionedBy.vertically) { | |
8026 this.style.top = '0px'; | |
8027 } | |
8028 if (!positionedBy.horizontally) { | |
8029 this.style.left = '0px'; | |
8030 } | |
8031 // It will take in consideration margins and transforms | |
8032 var rect = this.getBoundingClientRect(); | |
8033 var fitRect = this.__getNormalizedRect(this.fitInto); | |
8034 if (!positionedBy.vertically) { | |
8035 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | |
8036 this.style.top = top + 'px'; | |
8037 } | |
8038 if (!positionedBy.horizontally) { | |
8039 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | |
8040 this.style.left = left + 'px'; | |
8041 } | |
8042 }, | |
8043 | |
8044 __getNormalizedRect: function(target) { | |
8045 if (target === document.documentElement || target === window) { | |
8046 return { | |
8047 top: 0, | |
8048 left: 0, | |
8049 width: window.innerWidth, | |
8050 height: window.innerHeight, | |
8051 right: window.innerWidth, | |
8052 bottom: window.innerHeight | |
8053 }; | |
8054 } | |
8055 return target.getBoundingClientRect(); | |
8056 }, | |
8057 | |
8058 __getCroppedArea: function(position, size, fitRect) { | |
8059 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | |
8060 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | |
8061 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | |
8062 }, | |
8063 | |
8064 | |
8065 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | |
8066 // All the possible configurations. | |
8067 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
8068 var positions = [{ | |
8069 verticalAlign: 'top', | |
8070 horizontalAlign: 'left', | |
8071 top: positionRect.top, | |
8072 left: positionRect.left | |
8073 }, { | |
8074 verticalAlign: 'top', | |
8075 horizontalAlign: 'right', | |
8076 top: positionRect.top, | |
8077 left: positionRect.right - size.width | |
8078 }, { | |
8079 verticalAlign: 'bottom', | |
8080 horizontalAlign: 'left', | |
8081 top: positionRect.bottom - size.height, | |
8082 left: positionRect.left | |
8083 }, { | |
8084 verticalAlign: 'bottom', | |
8085 horizontalAlign: 'right', | |
8086 top: positionRect.bottom - size.height, | |
8087 left: positionRect.right - size.width | |
8088 }]; | |
8089 | |
8090 if (this.noOverlap) { | |
8091 // Duplicate. | |
8092 for (var i = 0, l = positions.length; i < l; i++) { | |
8093 var copy = {}; | |
8094 for (var key in positions[i]) { | |
8095 copy[key] = positions[i][key]; | |
8096 } | |
8097 positions.push(copy); | |
8098 } | |
8099 // Horizontal overlap only. | |
8100 positions[0].top = positions[1].top += positionRect.height; | |
8101 positions[2].top = positions[3].top -= positionRect.height; | |
8102 // Vertical overlap only. | |
8103 positions[4].left = positions[6].left += positionRect.width; | |
8104 positions[5].left = positions[7].left -= positionRect.width; | |
8105 } | |
8106 | |
8107 // Consider auto as null for coding convenience. | |
8108 vAlign = vAlign === 'auto' ? null : vAlign; | |
8109 hAlign = hAlign === 'auto' ? null : hAlign; | |
8110 | |
8111 var position; | |
8112 for (var i = 0; i < positions.length; i++) { | |
8113 var pos = positions[i]; | |
8114 | |
8115 // If both vAlign and hAlign are defined, return exact match. | |
8116 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
8117 // we'll have to check the croppedArea to make the best choice. | |
8118 if (!this.dynamicAlign && !this.noOverlap && | |
8119 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | |
8120 position = pos; | |
8121 break; | |
8122 } | |
8123 | |
8124 // Align is ok if alignment preferences are respected. If no preferences
, | |
8125 // it is considered ok. | |
8126 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | |
8127 (!hAlign || pos.horizontalAlign === hAlign); | |
8128 | |
8129 // Filter out elements that don't match the alignment (if defined). | |
8130 // With dynamicAlign, we need to consider all the positions to find the | |
8131 // one that minimizes the cropped area. | |
8132 if (!this.dynamicAlign && !alignOk) { | |
8133 continue; | |
8134 } | |
8135 | |
8136 position = position || pos; | |
8137 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | |
8138 var diff = pos.croppedArea - position.croppedArea; | |
8139 // Check which crops less. If it crops equally, check if align is ok. | |
8140 if (diff < 0 || (diff === 0 && alignOk)) { | |
8141 position = pos; | |
8142 } | |
8143 // If not cropped and respects the align requirements, keep it. | |
8144 // This allows to prefer positions overlapping horizontally over the | |
8145 // ones overlapping vertically. | |
8146 if (position.croppedArea === 0 && alignOk) { | |
8147 break; | |
8148 } | |
8149 } | |
8150 | |
8151 return position; | |
8152 } | |
8153 | |
8154 }; | |
8155 (function() { | |
8156 'use strict'; | |
8157 | |
8158 Polymer({ | |
8159 | |
8160 is: 'iron-overlay-backdrop', | |
8161 | |
8162 properties: { | |
8163 | |
8164 /** | |
8165 * Returns true if the backdrop is opened. | |
8166 */ | |
8167 opened: { | 4595 opened: { |
8168 reflectToAttribute: true, | 4596 reflectToAttribute: true, |
8169 type: Boolean, | 4597 type: Boolean, |
8170 value: false, | 4598 value: false, |
8171 observer: '_openedChanged' | 4599 observer: '_openedChanged' |
8172 } | 4600 } |
8173 | 4601 }, |
8174 }, | |
8175 | |
8176 listeners: { | 4602 listeners: { |
8177 'transitionend': '_onTransitionend' | 4603 transitionend: '_onTransitionend' |
8178 }, | 4604 }, |
8179 | |
8180 created: function() { | 4605 created: function() { |
8181 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
8182 this.__openedRaf = null; | 4606 this.__openedRaf = null; |
8183 }, | 4607 }, |
8184 | |
8185 attached: function() { | 4608 attached: function() { |
8186 this.opened && this._openedChanged(this.opened); | 4609 this.opened && this._openedChanged(this.opened); |
8187 }, | 4610 }, |
8188 | |
8189 /** | |
8190 * Appends the backdrop to document body if needed. | |
8191 */ | |
8192 prepare: function() { | 4611 prepare: function() { |
8193 if (this.opened && !this.parentNode) { | 4612 if (this.opened && !this.parentNode) { |
8194 Polymer.dom(document.body).appendChild(this); | 4613 Polymer.dom(document.body).appendChild(this); |
8195 } | 4614 } |
8196 }, | 4615 }, |
8197 | |
8198 /** | |
8199 * Shows the backdrop. | |
8200 */ | |
8201 open: function() { | 4616 open: function() { |
8202 this.opened = true; | 4617 this.opened = true; |
8203 }, | 4618 }, |
8204 | |
8205 /** | |
8206 * Hides the backdrop. | |
8207 */ | |
8208 close: function() { | 4619 close: function() { |
8209 this.opened = false; | 4620 this.opened = false; |
8210 }, | 4621 }, |
8211 | |
8212 /** | |
8213 * Removes the backdrop from document body if needed. | |
8214 */ | |
8215 complete: function() { | 4622 complete: function() { |
8216 if (!this.opened && this.parentNode === document.body) { | 4623 if (!this.opened && this.parentNode === document.body) { |
8217 Polymer.dom(this.parentNode).removeChild(this); | 4624 Polymer.dom(this.parentNode).removeChild(this); |
8218 } | 4625 } |
8219 }, | 4626 }, |
8220 | |
8221 _onTransitionend: function(event) { | 4627 _onTransitionend: function(event) { |
8222 if (event && event.target === this) { | 4628 if (event && event.target === this) { |
8223 this.complete(); | 4629 this.complete(); |
8224 } | 4630 } |
8225 }, | 4631 }, |
8226 | |
8227 /** | |
8228 * @param {boolean} opened | |
8229 * @private | |
8230 */ | |
8231 _openedChanged: function(opened) { | 4632 _openedChanged: function(opened) { |
8232 if (opened) { | 4633 if (opened) { |
8233 // Auto-attach. | |
8234 this.prepare(); | 4634 this.prepare(); |
8235 } else { | 4635 } else { |
8236 // Animation might be disabled via the mixin or opacity custom property. | |
8237 // If it is disabled in other ways, it's up to the user to call complete
. | |
8238 var cs = window.getComputedStyle(this); | 4636 var cs = window.getComputedStyle(this); |
8239 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 4637 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
8240 this.complete(); | 4638 this.complete(); |
8241 } | 4639 } |
8242 } | 4640 } |
8243 | |
8244 if (!this.isAttached) { | 4641 if (!this.isAttached) { |
8245 return; | 4642 return; |
8246 } | 4643 } |
8247 | |
8248 // Always cancel previous requestAnimationFrame. | |
8249 if (this.__openedRaf) { | 4644 if (this.__openedRaf) { |
8250 window.cancelAnimationFrame(this.__openedRaf); | 4645 window.cancelAnimationFrame(this.__openedRaf); |
8251 this.__openedRaf = null; | 4646 this.__openedRaf = null; |
8252 } | 4647 } |
8253 // Force relayout to ensure proper transitions. | |
8254 this.scrollTop = this.scrollTop; | 4648 this.scrollTop = this.scrollTop; |
8255 this.__openedRaf = window.requestAnimationFrame(function() { | 4649 this.__openedRaf = window.requestAnimationFrame(function() { |
8256 this.__openedRaf = null; | 4650 this.__openedRaf = null; |
8257 this.toggleClass('opened', this.opened); | 4651 this.toggleClass('opened', this.opened); |
8258 }.bind(this)); | 4652 }.bind(this)); |
8259 } | 4653 } |
8260 }); | 4654 }); |
8261 | |
8262 })(); | 4655 })(); |
8263 /** | 4656 |
8264 * @struct | 4657 Polymer.IronOverlayManagerClass = function() { |
8265 * @constructor | 4658 this._overlays = []; |
8266 * @private | 4659 this._minimumZ = 101; |
8267 */ | 4660 this._backdropElement = null; |
8268 Polymer.IronOverlayManagerClass = function() { | 4661 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
8269 /** | 4662 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
8270 * Used to keep track of the opened overlays. | 4663 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
8271 * @private {Array<Element>} | 4664 }; |
8272 */ | 4665 |
8273 this._overlays = []; | 4666 Polymer.IronOverlayManagerClass.prototype = { |
8274 | 4667 constructor: Polymer.IronOverlayManagerClass, |
8275 /** | 4668 get backdropElement() { |
8276 * iframes have a default z-index of 100, | 4669 if (!this._backdropElement) { |
8277 * so this default should be at least that. | 4670 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
8278 * @private {number} | 4671 } |
8279 */ | 4672 return this._backdropElement; |
8280 this._minimumZ = 101; | 4673 }, |
8281 | 4674 get deepActiveElement() { |
8282 /** | 4675 var active = document.activeElement || document.body; |
8283 * Memoized backdrop element. | 4676 while (active.root && Polymer.dom(active.root).activeElement) { |
8284 * @private {Element|null} | 4677 active = Polymer.dom(active.root).activeElement; |
8285 */ | 4678 } |
8286 this._backdropElement = null; | 4679 return active; |
8287 | 4680 }, |
8288 // Enable document-wide tap recognizer. | 4681 _bringOverlayAtIndexToFront: function(i) { |
8289 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 4682 var overlay = this._overlays[i]; |
8290 | 4683 if (!overlay) { |
8291 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 4684 return; |
8292 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 4685 } |
8293 }; | 4686 var lastI = this._overlays.length - 1; |
8294 | 4687 var currentOverlay = this._overlays[lastI]; |
8295 Polymer.IronOverlayManagerClass.prototype = { | 4688 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8296 | 4689 lastI--; |
8297 constructor: Polymer.IronOverlayManagerClass, | 4690 } |
8298 | 4691 if (i >= lastI) { |
8299 /** | 4692 return; |
8300 * The shared backdrop element. | 4693 } |
8301 * @type {!Element} backdropElement | 4694 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
8302 */ | 4695 if (this._getZ(overlay) <= minimumZ) { |
8303 get backdropElement() { | 4696 this._applyOverlayZ(overlay, minimumZ); |
8304 if (!this._backdropElement) { | 4697 } |
8305 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 4698 while (i < lastI) { |
8306 } | 4699 this._overlays[i] = this._overlays[i + 1]; |
8307 return this._backdropElement; | 4700 i++; |
8308 }, | 4701 } |
8309 | 4702 this._overlays[lastI] = overlay; |
8310 /** | 4703 }, |
8311 * The deepest active element. | 4704 addOrRemoveOverlay: function(overlay) { |
8312 * @type {!Element} activeElement the active element | 4705 if (overlay.opened) { |
8313 */ | 4706 this.addOverlay(overlay); |
8314 get deepActiveElement() { | 4707 } else { |
8315 // document.activeElement can be null | 4708 this.removeOverlay(overlay); |
8316 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | 4709 } |
8317 // In case of null, default it to document.body. | 4710 }, |
8318 var active = document.activeElement || document.body; | 4711 addOverlay: function(overlay) { |
8319 while (active.root && Polymer.dom(active.root).activeElement) { | 4712 var i = this._overlays.indexOf(overlay); |
8320 active = Polymer.dom(active.root).activeElement; | 4713 if (i >= 0) { |
8321 } | 4714 this._bringOverlayAtIndexToFront(i); |
8322 return active; | |
8323 }, | |
8324 | |
8325 /** | |
8326 * Brings the overlay at the specified index to the front. | |
8327 * @param {number} i | |
8328 * @private | |
8329 */ | |
8330 _bringOverlayAtIndexToFront: function(i) { | |
8331 var overlay = this._overlays[i]; | |
8332 if (!overlay) { | |
8333 return; | |
8334 } | |
8335 var lastI = this._overlays.length - 1; | |
8336 var currentOverlay = this._overlays[lastI]; | |
8337 // Ensure always-on-top overlay stays on top. | |
8338 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8339 lastI--; | |
8340 } | |
8341 // If already the top element, return. | |
8342 if (i >= lastI) { | |
8343 return; | |
8344 } | |
8345 // Update z-index to be on top. | |
8346 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | |
8347 if (this._getZ(overlay) <= minimumZ) { | |
8348 this._applyOverlayZ(overlay, minimumZ); | |
8349 } | |
8350 | |
8351 // Shift other overlays behind the new on top. | |
8352 while (i < lastI) { | |
8353 this._overlays[i] = this._overlays[i + 1]; | |
8354 i++; | |
8355 } | |
8356 this._overlays[lastI] = overlay; | |
8357 }, | |
8358 | |
8359 /** | |
8360 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
8361 * Also updates the backdrop z-index. | |
8362 * @param {!Element} overlay | |
8363 */ | |
8364 addOrRemoveOverlay: function(overlay) { | |
8365 if (overlay.opened) { | |
8366 this.addOverlay(overlay); | |
8367 } else { | |
8368 this.removeOverlay(overlay); | |
8369 } | |
8370 }, | |
8371 | |
8372 /** | |
8373 * Tracks overlays for z-index and focus management. | |
8374 * Ensures the last added overlay with always-on-top remains on top. | |
8375 * @param {!Element} overlay | |
8376 */ | |
8377 addOverlay: function(overlay) { | |
8378 var i = this._overlays.indexOf(overlay); | |
8379 if (i >= 0) { | |
8380 this._bringOverlayAtIndexToFront(i); | |
8381 this.trackBackdrop(); | |
8382 return; | |
8383 } | |
8384 var insertionIndex = this._overlays.length; | |
8385 var currentOverlay = this._overlays[insertionIndex - 1]; | |
8386 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | |
8387 var newZ = this._getZ(overlay); | |
8388 | |
8389 // Ensure always-on-top overlay stays on top. | |
8390 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8391 // This bumps the z-index of +2. | |
8392 this._applyOverlayZ(currentOverlay, minimumZ); | |
8393 insertionIndex--; | |
8394 // Update minimumZ to match previous overlay's z-index. | |
8395 var previousOverlay = this._overlays[insertionIndex - 1]; | |
8396 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | |
8397 } | |
8398 | |
8399 // Update z-index and insert overlay. | |
8400 if (newZ <= minimumZ) { | |
8401 this._applyOverlayZ(overlay, minimumZ); | |
8402 } | |
8403 this._overlays.splice(insertionIndex, 0, overlay); | |
8404 | |
8405 this.trackBackdrop(); | 4715 this.trackBackdrop(); |
8406 }, | 4716 return; |
8407 | 4717 } |
8408 /** | 4718 var insertionIndex = this._overlays.length; |
8409 * @param {!Element} overlay | 4719 var currentOverlay = this._overlays[insertionIndex - 1]; |
8410 */ | 4720 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
8411 removeOverlay: function(overlay) { | 4721 var newZ = this._getZ(overlay); |
8412 var i = this._overlays.indexOf(overlay); | 4722 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8413 if (i === -1) { | 4723 this._applyOverlayZ(currentOverlay, minimumZ); |
8414 return; | 4724 insertionIndex--; |
8415 } | 4725 var previousOverlay = this._overlays[insertionIndex - 1]; |
8416 this._overlays.splice(i, 1); | 4726 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
8417 | 4727 } |
8418 this.trackBackdrop(); | 4728 if (newZ <= minimumZ) { |
8419 }, | 4729 this._applyOverlayZ(overlay, minimumZ); |
8420 | 4730 } |
8421 /** | 4731 this._overlays.splice(insertionIndex, 0, overlay); |
8422 * Returns the current overlay. | 4732 this.trackBackdrop(); |
8423 * @return {Element|undefined} | 4733 }, |
8424 */ | 4734 removeOverlay: function(overlay) { |
8425 currentOverlay: function() { | 4735 var i = this._overlays.indexOf(overlay); |
8426 var i = this._overlays.length - 1; | 4736 if (i === -1) { |
8427 return this._overlays[i]; | 4737 return; |
8428 }, | 4738 } |
8429 | 4739 this._overlays.splice(i, 1); |
8430 /** | 4740 this.trackBackdrop(); |
8431 * Returns the current overlay z-index. | 4741 }, |
8432 * @return {number} | 4742 currentOverlay: function() { |
8433 */ | 4743 var i = this._overlays.length - 1; |
8434 currentOverlayZ: function() { | 4744 return this._overlays[i]; |
8435 return this._getZ(this.currentOverlay()); | 4745 }, |
8436 }, | 4746 currentOverlayZ: function() { |
8437 | 4747 return this._getZ(this.currentOverlay()); |
8438 /** | 4748 }, |
8439 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | 4749 ensureMinimumZ: function(minimumZ) { |
8440 * This does not effect the z-index of any existing overlays. | 4750 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
8441 * @param {number} minimumZ | 4751 }, |
8442 */ | 4752 focusOverlay: function() { |
8443 ensureMinimumZ: function(minimumZ) { | 4753 var current = this.currentOverlay(); |
8444 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 4754 if (current) { |
8445 }, | 4755 current._applyFocus(); |
8446 | 4756 } |
8447 focusOverlay: function() { | 4757 }, |
8448 var current = /** @type {?} */ (this.currentOverlay()); | 4758 trackBackdrop: function() { |
8449 if (current) { | 4759 var overlay = this._overlayWithBackdrop(); |
8450 current._applyFocus(); | 4760 if (!overlay && !this._backdropElement) { |
8451 } | 4761 return; |
8452 }, | 4762 } |
8453 | 4763 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
8454 /** | 4764 this.backdropElement.opened = !!overlay; |
8455 * Updates the backdrop z-index. | 4765 }, |
8456 */ | 4766 getBackdrops: function() { |
8457 trackBackdrop: function() { | 4767 var backdrops = []; |
8458 var overlay = this._overlayWithBackdrop(); | 4768 for (var i = 0; i < this._overlays.length; i++) { |
8459 // Avoid creating the backdrop if there is no overlay with backdrop. | 4769 if (this._overlays[i].withBackdrop) { |
8460 if (!overlay && !this._backdropElement) { | 4770 backdrops.push(this._overlays[i]); |
8461 return; | 4771 } |
8462 } | 4772 } |
8463 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 4773 return backdrops; |
8464 this.backdropElement.opened = !!overlay; | 4774 }, |
8465 }, | 4775 backdropZ: function() { |
8466 | 4776 return this._getZ(this._overlayWithBackdrop()) - 1; |
8467 /** | 4777 }, |
8468 * @return {Array<Element>} | 4778 _overlayWithBackdrop: function() { |
8469 */ | 4779 for (var i = 0; i < this._overlays.length; i++) { |
8470 getBackdrops: function() { | 4780 if (this._overlays[i].withBackdrop) { |
8471 var backdrops = []; | 4781 return this._overlays[i]; |
8472 for (var i = 0; i < this._overlays.length; i++) { | 4782 } |
8473 if (this._overlays[i].withBackdrop) { | 4783 } |
8474 backdrops.push(this._overlays[i]); | 4784 }, |
8475 } | 4785 _getZ: function(overlay) { |
8476 } | 4786 var z = this._minimumZ; |
8477 return backdrops; | 4787 if (overlay) { |
8478 }, | 4788 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z
Index); |
8479 | 4789 if (z1 === z1) { |
8480 /** | 4790 z = z1; |
8481 * Returns the z-index for the backdrop. | 4791 } |
8482 * @return {number} | 4792 } |
8483 */ | 4793 return z; |
8484 backdropZ: function() { | 4794 }, |
8485 return this._getZ(this._overlayWithBackdrop()) - 1; | 4795 _setZ: function(element, z) { |
8486 }, | 4796 element.style.zIndex = z; |
8487 | 4797 }, |
8488 /** | 4798 _applyOverlayZ: function(overlay, aboveZ) { |
8489 * Returns the first opened overlay that has a backdrop. | 4799 this._setZ(overlay, aboveZ + 2); |
8490 * @return {Element|undefined} | 4800 }, |
8491 * @private | 4801 _overlayInPath: function(path) { |
8492 */ | 4802 path = path || []; |
8493 _overlayWithBackdrop: function() { | 4803 for (var i = 0; i < path.length; i++) { |
8494 for (var i = 0; i < this._overlays.length; i++) { | 4804 if (path[i]._manager === this) { |
8495 if (this._overlays[i].withBackdrop) { | 4805 return path[i]; |
8496 return this._overlays[i]; | 4806 } |
8497 } | 4807 } |
8498 } | 4808 }, |
8499 }, | 4809 _onCaptureClick: function(event) { |
8500 | 4810 var overlay = this.currentOverlay(); |
8501 /** | 4811 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
8502 * Calculates the minimum z-index for the overlay. | 4812 overlay._onCaptureClick(event); |
8503 * @param {Element=} overlay | 4813 } |
8504 * @private | 4814 }, |
8505 */ | 4815 _onCaptureFocus: function(event) { |
8506 _getZ: function(overlay) { | 4816 var overlay = this.currentOverlay(); |
8507 var z = this._minimumZ; | 4817 if (overlay) { |
8508 if (overlay) { | 4818 overlay._onCaptureFocus(event); |
8509 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 4819 } |
8510 // Check if is a number | 4820 }, |
8511 // Number.isNaN not supported in IE 10+ | 4821 _onCaptureKeyDown: function(event) { |
8512 if (z1 === z1) { | 4822 var overlay = this.currentOverlay(); |
8513 z = z1; | 4823 if (overlay) { |
8514 } | 4824 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
8515 } | 4825 overlay._onCaptureEsc(event); |
8516 return z; | 4826 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't
ab')) { |
8517 }, | 4827 overlay._onCaptureTab(event); |
8518 | 4828 } |
8519 /** | 4829 } |
8520 * @param {!Element} element | 4830 }, |
8521 * @param {number|string} z | 4831 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
8522 * @private | 4832 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
8523 */ | 4833 } |
8524 _setZ: function(element, z) { | 4834 }; |
8525 element.style.zIndex = z; | 4835 |
8526 }, | 4836 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
8527 | 4837 |
8528 /** | |
8529 * @param {!Element} overlay | |
8530 * @param {number} aboveZ | |
8531 * @private | |
8532 */ | |
8533 _applyOverlayZ: function(overlay, aboveZ) { | |
8534 this._setZ(overlay, aboveZ + 2); | |
8535 }, | |
8536 | |
8537 /** | |
8538 * Returns the deepest overlay in the path. | |
8539 * @param {Array<Element>=} path | |
8540 * @return {Element|undefined} | |
8541 * @suppress {missingProperties} | |
8542 * @private | |
8543 */ | |
8544 _overlayInPath: function(path) { | |
8545 path = path || []; | |
8546 for (var i = 0; i < path.length; i++) { | |
8547 if (path[i]._manager === this) { | |
8548 return path[i]; | |
8549 } | |
8550 } | |
8551 }, | |
8552 | |
8553 /** | |
8554 * Ensures the click event is delegated to the right overlay. | |
8555 * @param {!Event} event | |
8556 * @private | |
8557 */ | |
8558 _onCaptureClick: function(event) { | |
8559 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8560 // Check if clicked outside of top overlay. | |
8561 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | |
8562 overlay._onCaptureClick(event); | |
8563 } | |
8564 }, | |
8565 | |
8566 /** | |
8567 * Ensures the focus event is delegated to the right overlay. | |
8568 * @param {!Event} event | |
8569 * @private | |
8570 */ | |
8571 _onCaptureFocus: function(event) { | |
8572 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8573 if (overlay) { | |
8574 overlay._onCaptureFocus(event); | |
8575 } | |
8576 }, | |
8577 | |
8578 /** | |
8579 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
8580 * @param {!Event} event | |
8581 * @private | |
8582 */ | |
8583 _onCaptureKeyDown: function(event) { | |
8584 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8585 if (overlay) { | |
8586 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | |
8587 overlay._onCaptureEsc(event); | |
8588 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | |
8589 overlay._onCaptureTab(event); | |
8590 } | |
8591 } | |
8592 }, | |
8593 | |
8594 /** | |
8595 * Returns if the overlay1 should be behind overlay2. | |
8596 * @param {!Element} overlay1 | |
8597 * @param {!Element} overlay2 | |
8598 * @return {boolean} | |
8599 * @suppress {missingProperties} | |
8600 * @private | |
8601 */ | |
8602 _shouldBeBehindOverlay: function(overlay1, overlay2) { | |
8603 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | |
8604 } | |
8605 }; | |
8606 | |
8607 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | |
8608 (function() { | 4838 (function() { |
8609 'use strict'; | 4839 'use strict'; |
8610 | |
8611 /** | |
8612 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
8613 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
8614 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
8615 | |
8616 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
8617 for an example. | |
8618 | |
8619 ### Closing and canceling | |
8620 | |
8621 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
8622 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
8623 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
8624 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
8625 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
8626 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
8627 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
8628 | |
8629 ### Positioning | |
8630 | |
8631 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
8632 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
8633 | |
8634 ### Backdrop | |
8635 | |
8636 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
8637 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
8638 options. | |
8639 | |
8640 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
8641 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
8642 to achieve a different behavior. | |
8643 | |
8644 ### Limitations | |
8645 | |
8646 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
8647 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
8648 context. You should place this element as a child of `<body>` whenever possible. | |
8649 | |
8650 @demo demo/index.html | |
8651 @polymerBehavior Polymer.IronOverlayBehavior | |
8652 */ | |
8653 | |
8654 Polymer.IronOverlayBehaviorImpl = { | 4840 Polymer.IronOverlayBehaviorImpl = { |
8655 | |
8656 properties: { | 4841 properties: { |
8657 | |
8658 /** | |
8659 * True if the overlay is currently displayed. | |
8660 */ | |
8661 opened: { | 4842 opened: { |
8662 observer: '_openedChanged', | 4843 observer: '_openedChanged', |
8663 type: Boolean, | 4844 type: Boolean, |
8664 value: false, | 4845 value: false, |
8665 notify: true | 4846 notify: true |
8666 }, | 4847 }, |
8667 | |
8668 /** | |
8669 * True if the overlay was canceled when it was last closed. | |
8670 */ | |
8671 canceled: { | 4848 canceled: { |
8672 observer: '_canceledChanged', | 4849 observer: '_canceledChanged', |
8673 readOnly: true, | 4850 readOnly: true, |
8674 type: Boolean, | 4851 type: Boolean, |
8675 value: false | 4852 value: false |
8676 }, | 4853 }, |
8677 | |
8678 /** | |
8679 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
8680 * within the light DOM of the overlay. | |
8681 */ | |
8682 withBackdrop: { | 4854 withBackdrop: { |
8683 observer: '_withBackdropChanged', | 4855 observer: '_withBackdropChanged', |
8684 type: Boolean | 4856 type: Boolean |
8685 }, | 4857 }, |
8686 | |
8687 /** | |
8688 * Set to true to disable auto-focusing the overlay or child nodes with | |
8689 * the `autofocus` attribute` when the overlay is opened. | |
8690 */ | |
8691 noAutoFocus: { | 4858 noAutoFocus: { |
8692 type: Boolean, | 4859 type: Boolean, |
8693 value: false | 4860 value: false |
8694 }, | 4861 }, |
8695 | |
8696 /** | |
8697 * Set to true to disable canceling the overlay with the ESC key. | |
8698 */ | |
8699 noCancelOnEscKey: { | 4862 noCancelOnEscKey: { |
8700 type: Boolean, | 4863 type: Boolean, |
8701 value: false | 4864 value: false |
8702 }, | 4865 }, |
8703 | |
8704 /** | |
8705 * Set to true to disable canceling the overlay by clicking outside it. | |
8706 */ | |
8707 noCancelOnOutsideClick: { | 4866 noCancelOnOutsideClick: { |
8708 type: Boolean, | 4867 type: Boolean, |
8709 value: false | 4868 value: false |
8710 }, | 4869 }, |
8711 | |
8712 /** | |
8713 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
8714 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
8715 * behavior can provide other reasons in addition to `canceled`. | |
8716 */ | |
8717 closingReason: { | 4870 closingReason: { |
8718 // was a getter before, but needs to be a property so other | |
8719 // behaviors can override this. | |
8720 type: Object | 4871 type: Object |
8721 }, | 4872 }, |
8722 | |
8723 /** | |
8724 * Set to true to enable restoring of focus when overlay is closed. | |
8725 */ | |
8726 restoreFocusOnClose: { | 4873 restoreFocusOnClose: { |
8727 type: Boolean, | 4874 type: Boolean, |
8728 value: false | 4875 value: false |
8729 }, | 4876 }, |
8730 | |
8731 /** | |
8732 * Set to true to keep overlay always on top. | |
8733 */ | |
8734 alwaysOnTop: { | 4877 alwaysOnTop: { |
8735 type: Boolean | 4878 type: Boolean |
8736 }, | 4879 }, |
8737 | |
8738 /** | |
8739 * Shortcut to access to the overlay manager. | |
8740 * @private | |
8741 * @type {Polymer.IronOverlayManagerClass} | |
8742 */ | |
8743 _manager: { | 4880 _manager: { |
8744 type: Object, | 4881 type: Object, |
8745 value: Polymer.IronOverlayManager | 4882 value: Polymer.IronOverlayManager |
8746 }, | 4883 }, |
8747 | |
8748 /** | |
8749 * The node being focused. | |
8750 * @type {?Node} | |
8751 */ | |
8752 _focusedChild: { | 4884 _focusedChild: { |
8753 type: Object | 4885 type: Object |
8754 } | 4886 } |
8755 | 4887 }, |
8756 }, | |
8757 | |
8758 listeners: { | 4888 listeners: { |
8759 'iron-resize': '_onIronResize' | 4889 'iron-resize': '_onIronResize' |
8760 }, | 4890 }, |
8761 | |
8762 /** | |
8763 * The backdrop element. | |
8764 * @type {Element} | |
8765 */ | |
8766 get backdropElement() { | 4891 get backdropElement() { |
8767 return this._manager.backdropElement; | 4892 return this._manager.backdropElement; |
8768 }, | 4893 }, |
8769 | |
8770 /** | |
8771 * Returns the node to give focus to. | |
8772 * @type {Node} | |
8773 */ | |
8774 get _focusNode() { | 4894 get _focusNode() { |
8775 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 4895 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
8776 }, | 4896 }, |
8777 | |
8778 /** | |
8779 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
8780 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
8781 * to wrap the focus for overlays `with-backdrop`. | |
8782 * | |
8783 * If you know what is your content (specifically the first and last focusab
le children), | |
8784 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
8785 * @type {Array<Node>} | |
8786 * @protected | |
8787 */ | |
8788 get _focusableNodes() { | 4897 get _focusableNodes() { |
8789 // Elements that can be focused even if they have [disabled] attribute. | 4898 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin
dex]', '[contentEditable=true]' ]; |
8790 var FOCUSABLE_WITH_DISABLED = [ | 4899 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button'
]; |
8791 'a[href]', | 4900 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + ':
not([tabindex="-1"]),' + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([
tabindex="-1"]),') + ':not([disabled]):not([tabindex="-1"])'; |
8792 'area[href]', | |
8793 'iframe', | |
8794 '[tabindex]', | |
8795 '[contentEditable=true]' | |
8796 ]; | |
8797 | |
8798 // Elements that cannot be focused if they have [disabled] attribute. | |
8799 var FOCUSABLE_WITHOUT_DISABLED = [ | |
8800 'input', | |
8801 'select', | |
8802 'textarea', | |
8803 'button' | |
8804 ]; | |
8805 | |
8806 // Discard elements with tabindex=-1 (makes them not focusable). | |
8807 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | |
8808 ':not([tabindex="-1"]),' + | |
8809 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | |
8810 ':not([disabled]):not([tabindex="-1"])'; | |
8811 | |
8812 var focusables = Polymer.dom(this).querySelectorAll(selector); | 4901 var focusables = Polymer.dom(this).querySelectorAll(selector); |
8813 if (this.tabIndex >= 0) { | 4902 if (this.tabIndex >= 0) { |
8814 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
8815 // and the overlay should be the first of the list. | |
8816 focusables.splice(0, 0, this); | 4903 focusables.splice(0, 0, this); |
8817 } | 4904 } |
8818 // Sort by tabindex. | 4905 return focusables.sort(function(a, b) { |
8819 return focusables.sort(function (a, b) { | |
8820 if (a.tabIndex === b.tabIndex) { | 4906 if (a.tabIndex === b.tabIndex) { |
8821 return 0; | 4907 return 0; |
8822 } | 4908 } |
8823 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 4909 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
8824 return 1; | 4910 return 1; |
8825 } | 4911 } |
8826 return -1; | 4912 return -1; |
8827 }); | 4913 }); |
8828 }, | 4914 }, |
8829 | |
8830 ready: function() { | 4915 ready: function() { |
8831 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
8832 this.__isAnimating = false; | 4916 this.__isAnimating = false; |
8833 // with-backdrop needs tabindex to be set in order to trap the focus. | |
8834 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
8835 this.__shouldRemoveTabIndex = false; | 4917 this.__shouldRemoveTabIndex = false; |
8836 // Used for wrapping the focus on TAB / Shift+TAB. | |
8837 this.__firstFocusableNode = this.__lastFocusableNode = null; | 4918 this.__firstFocusableNode = this.__lastFocusableNode = null; |
8838 // Used by __onNextAnimationFrame to cancel any previous callback. | |
8839 this.__raf = null; | 4919 this.__raf = null; |
8840 // Focused node before overlay gets opened. Can be restored on close. | |
8841 this.__restoreFocusNode = null; | 4920 this.__restoreFocusNode = null; |
8842 this._ensureSetup(); | 4921 this._ensureSetup(); |
8843 }, | 4922 }, |
8844 | |
8845 attached: function() { | 4923 attached: function() { |
8846 // Call _openedChanged here so that position can be computed correctly. | |
8847 if (this.opened) { | 4924 if (this.opened) { |
8848 this._openedChanged(this.opened); | 4925 this._openedChanged(this.opened); |
8849 } | 4926 } |
8850 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 4927 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
8851 }, | 4928 }, |
8852 | |
8853 detached: function() { | 4929 detached: function() { |
8854 Polymer.dom(this).unobserveNodes(this._observer); | 4930 Polymer.dom(this).unobserveNodes(this._observer); |
8855 this._observer = null; | 4931 this._observer = null; |
8856 if (this.__raf) { | 4932 if (this.__raf) { |
8857 window.cancelAnimationFrame(this.__raf); | 4933 window.cancelAnimationFrame(this.__raf); |
8858 this.__raf = null; | 4934 this.__raf = null; |
8859 } | 4935 } |
8860 this._manager.removeOverlay(this); | 4936 this._manager.removeOverlay(this); |
8861 }, | 4937 }, |
8862 | |
8863 /** | |
8864 * Toggle the opened state of the overlay. | |
8865 */ | |
8866 toggle: function() { | 4938 toggle: function() { |
8867 this._setCanceled(false); | 4939 this._setCanceled(false); |
8868 this.opened = !this.opened; | 4940 this.opened = !this.opened; |
8869 }, | 4941 }, |
8870 | |
8871 /** | |
8872 * Open the overlay. | |
8873 */ | |
8874 open: function() { | 4942 open: function() { |
8875 this._setCanceled(false); | 4943 this._setCanceled(false); |
8876 this.opened = true; | 4944 this.opened = true; |
8877 }, | 4945 }, |
8878 | |
8879 /** | |
8880 * Close the overlay. | |
8881 */ | |
8882 close: function() { | 4946 close: function() { |
8883 this._setCanceled(false); | 4947 this._setCanceled(false); |
8884 this.opened = false; | 4948 this.opened = false; |
8885 }, | 4949 }, |
8886 | |
8887 /** | |
8888 * Cancels the overlay. | |
8889 * @param {Event=} event The original event | |
8890 */ | |
8891 cancel: function(event) { | 4950 cancel: function(event) { |
8892 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 4951 var cancelEvent = this.fire('iron-overlay-canceled', event, { |
| 4952 cancelable: true |
| 4953 }); |
8893 if (cancelEvent.defaultPrevented) { | 4954 if (cancelEvent.defaultPrevented) { |
8894 return; | 4955 return; |
8895 } | 4956 } |
8896 | |
8897 this._setCanceled(true); | 4957 this._setCanceled(true); |
8898 this.opened = false; | 4958 this.opened = false; |
8899 }, | 4959 }, |
8900 | |
8901 _ensureSetup: function() { | 4960 _ensureSetup: function() { |
8902 if (this._overlaySetup) { | 4961 if (this._overlaySetup) { |
8903 return; | 4962 return; |
8904 } | 4963 } |
8905 this._overlaySetup = true; | 4964 this._overlaySetup = true; |
8906 this.style.outline = 'none'; | 4965 this.style.outline = 'none'; |
8907 this.style.display = 'none'; | 4966 this.style.display = 'none'; |
8908 }, | 4967 }, |
8909 | |
8910 /** | |
8911 * Called when `opened` changes. | |
8912 * @param {boolean=} opened | |
8913 * @protected | |
8914 */ | |
8915 _openedChanged: function(opened) { | 4968 _openedChanged: function(opened) { |
8916 if (opened) { | 4969 if (opened) { |
8917 this.removeAttribute('aria-hidden'); | 4970 this.removeAttribute('aria-hidden'); |
8918 } else { | 4971 } else { |
8919 this.setAttribute('aria-hidden', 'true'); | 4972 this.setAttribute('aria-hidden', 'true'); |
8920 } | 4973 } |
8921 | |
8922 // Defer any animation-related code on attached | |
8923 // (_openedChanged gets called again on attached). | |
8924 if (!this.isAttached) { | 4974 if (!this.isAttached) { |
8925 return; | 4975 return; |
8926 } | 4976 } |
8927 | |
8928 this.__isAnimating = true; | 4977 this.__isAnimating = true; |
8929 | |
8930 // Use requestAnimationFrame for non-blocking rendering. | |
8931 this.__onNextAnimationFrame(this.__openedChanged); | 4978 this.__onNextAnimationFrame(this.__openedChanged); |
8932 }, | 4979 }, |
8933 | |
8934 _canceledChanged: function() { | 4980 _canceledChanged: function() { |
8935 this.closingReason = this.closingReason || {}; | 4981 this.closingReason = this.closingReason || {}; |
8936 this.closingReason.canceled = this.canceled; | 4982 this.closingReason.canceled = this.canceled; |
8937 }, | 4983 }, |
8938 | |
8939 _withBackdropChanged: function() { | 4984 _withBackdropChanged: function() { |
8940 // If tabindex is already set, no need to override it. | |
8941 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 4985 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
8942 this.setAttribute('tabindex', '-1'); | 4986 this.setAttribute('tabindex', '-1'); |
8943 this.__shouldRemoveTabIndex = true; | 4987 this.__shouldRemoveTabIndex = true; |
8944 } else if (this.__shouldRemoveTabIndex) { | 4988 } else if (this.__shouldRemoveTabIndex) { |
8945 this.removeAttribute('tabindex'); | 4989 this.removeAttribute('tabindex'); |
8946 this.__shouldRemoveTabIndex = false; | 4990 this.__shouldRemoveTabIndex = false; |
8947 } | 4991 } |
8948 if (this.opened && this.isAttached) { | 4992 if (this.opened && this.isAttached) { |
8949 this._manager.trackBackdrop(); | 4993 this._manager.trackBackdrop(); |
8950 } | 4994 } |
8951 }, | 4995 }, |
8952 | |
8953 /** | |
8954 * tasks which must occur before opening; e.g. making the element visible. | |
8955 * @protected | |
8956 */ | |
8957 _prepareRenderOpened: function() { | 4996 _prepareRenderOpened: function() { |
8958 // Store focused node. | |
8959 this.__restoreFocusNode = this._manager.deepActiveElement; | 4997 this.__restoreFocusNode = this._manager.deepActiveElement; |
8960 | |
8961 // Needed to calculate the size of the overlay so that transitions on its
size | |
8962 // will have the correct starting points. | |
8963 this._preparePositioning(); | 4998 this._preparePositioning(); |
8964 this.refit(); | 4999 this.refit(); |
8965 this._finishPositioning(); | 5000 this._finishPositioning(); |
8966 | |
8967 // Safari will apply the focus to the autofocus element when displayed | |
8968 // for the first time, so we make sure to return the focus where it was. | |
8969 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 5001 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
8970 this._focusNode.blur(); | 5002 this._focusNode.blur(); |
8971 this.__restoreFocusNode.focus(); | 5003 this.__restoreFocusNode.focus(); |
8972 } | 5004 } |
8973 }, | 5005 }, |
8974 | |
8975 /** | |
8976 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
8977 * @protected | |
8978 */ | |
8979 _renderOpened: function() { | 5006 _renderOpened: function() { |
8980 this._finishRenderOpened(); | 5007 this._finishRenderOpened(); |
8981 }, | 5008 }, |
8982 | |
8983 /** | |
8984 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
8985 * @protected | |
8986 */ | |
8987 _renderClosed: function() { | 5009 _renderClosed: function() { |
8988 this._finishRenderClosed(); | 5010 this._finishRenderClosed(); |
8989 }, | 5011 }, |
8990 | |
8991 /** | |
8992 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
8993 * @protected | |
8994 */ | |
8995 _finishRenderOpened: function() { | 5012 _finishRenderOpened: function() { |
8996 this.notifyResize(); | 5013 this.notifyResize(); |
8997 this.__isAnimating = false; | 5014 this.__isAnimating = false; |
8998 | |
8999 // Store it so we don't query too much. | |
9000 var focusableNodes = this._focusableNodes; | 5015 var focusableNodes = this._focusableNodes; |
9001 this.__firstFocusableNode = focusableNodes[0]; | 5016 this.__firstFocusableNode = focusableNodes[0]; |
9002 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 5017 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
9003 | |
9004 this.fire('iron-overlay-opened'); | 5018 this.fire('iron-overlay-opened'); |
9005 }, | 5019 }, |
9006 | |
9007 /** | |
9008 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
9009 * @protected | |
9010 */ | |
9011 _finishRenderClosed: function() { | 5020 _finishRenderClosed: function() { |
9012 // Hide the overlay. | |
9013 this.style.display = 'none'; | 5021 this.style.display = 'none'; |
9014 // Reset z-index only at the end of the animation. | |
9015 this.style.zIndex = ''; | 5022 this.style.zIndex = ''; |
9016 this.notifyResize(); | 5023 this.notifyResize(); |
9017 this.__isAnimating = false; | 5024 this.__isAnimating = false; |
9018 this.fire('iron-overlay-closed', this.closingReason); | 5025 this.fire('iron-overlay-closed', this.closingReason); |
9019 }, | 5026 }, |
9020 | |
9021 _preparePositioning: function() { | 5027 _preparePositioning: function() { |
9022 this.style.transition = this.style.webkitTransition = 'none'; | 5028 this.style.transition = this.style.webkitTransition = 'none'; |
9023 this.style.transform = this.style.webkitTransform = 'none'; | 5029 this.style.transform = this.style.webkitTransform = 'none'; |
9024 this.style.display = ''; | 5030 this.style.display = ''; |
9025 }, | 5031 }, |
9026 | |
9027 _finishPositioning: function() { | 5032 _finishPositioning: function() { |
9028 // First, make it invisible & reactivate animations. | |
9029 this.style.display = 'none'; | 5033 this.style.display = 'none'; |
9030 // Force reflow before re-enabling animations so that they don't start. | |
9031 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9032 this.scrollTop = this.scrollTop; | 5034 this.scrollTop = this.scrollTop; |
9033 this.style.transition = this.style.webkitTransition = ''; | 5035 this.style.transition = this.style.webkitTransition = ''; |
9034 this.style.transform = this.style.webkitTransform = ''; | 5036 this.style.transform = this.style.webkitTransform = ''; |
9035 // Now that animations are enabled, make it visible again | |
9036 this.style.display = ''; | 5037 this.style.display = ''; |
9037 // Force reflow, so that following animations are properly started. | |
9038 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9039 this.scrollTop = this.scrollTop; | 5038 this.scrollTop = this.scrollTop; |
9040 }, | 5039 }, |
9041 | |
9042 /** | |
9043 * Applies focus according to the opened state. | |
9044 * @protected | |
9045 */ | |
9046 _applyFocus: function() { | 5040 _applyFocus: function() { |
9047 if (this.opened) { | 5041 if (this.opened) { |
9048 if (!this.noAutoFocus) { | 5042 if (!this.noAutoFocus) { |
9049 this._focusNode.focus(); | 5043 this._focusNode.focus(); |
9050 } | 5044 } |
9051 } | 5045 } else { |
9052 else { | |
9053 this._focusNode.blur(); | 5046 this._focusNode.blur(); |
9054 this._focusedChild = null; | 5047 this._focusedChild = null; |
9055 // Restore focus. | |
9056 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 5048 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
9057 this.__restoreFocusNode.focus(); | 5049 this.__restoreFocusNode.focus(); |
9058 } | 5050 } |
9059 this.__restoreFocusNode = null; | 5051 this.__restoreFocusNode = null; |
9060 // If many overlays get closed at the same time, one of them would still | |
9061 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
9062 // infinitely, so we check for this not to be the current overlay. | |
9063 var currentOverlay = this._manager.currentOverlay(); | 5052 var currentOverlay = this._manager.currentOverlay(); |
9064 if (currentOverlay && this !== currentOverlay) { | 5053 if (currentOverlay && this !== currentOverlay) { |
9065 currentOverlay._applyFocus(); | 5054 currentOverlay._applyFocus(); |
9066 } | 5055 } |
9067 } | 5056 } |
9068 }, | 5057 }, |
9069 | |
9070 /** | |
9071 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
9072 * @param {!Event} event | |
9073 * @protected | |
9074 */ | |
9075 _onCaptureClick: function(event) { | 5058 _onCaptureClick: function(event) { |
9076 if (!this.noCancelOnOutsideClick) { | 5059 if (!this.noCancelOnOutsideClick) { |
9077 this.cancel(event); | 5060 this.cancel(event); |
9078 } | 5061 } |
9079 }, | 5062 }, |
9080 | 5063 _onCaptureFocus: function(event) { |
9081 /** | |
9082 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
9083 * @param {!Event} event | |
9084 * @protected | |
9085 */ | |
9086 _onCaptureFocus: function (event) { | |
9087 if (!this.withBackdrop) { | 5064 if (!this.withBackdrop) { |
9088 return; | 5065 return; |
9089 } | 5066 } |
9090 var path = Polymer.dom(event).path; | 5067 var path = Polymer.dom(event).path; |
9091 if (path.indexOf(this) === -1) { | 5068 if (path.indexOf(this) === -1) { |
9092 event.stopPropagation(); | 5069 event.stopPropagation(); |
9093 this._applyFocus(); | 5070 this._applyFocus(); |
9094 } else { | 5071 } else { |
9095 this._focusedChild = path[0]; | 5072 this._focusedChild = path[0]; |
9096 } | 5073 } |
9097 }, | 5074 }, |
9098 | |
9099 /** | |
9100 * Handles the ESC key event and cancels (closes) the overlay. | |
9101 * @param {!Event} event | |
9102 * @protected | |
9103 */ | |
9104 _onCaptureEsc: function(event) { | 5075 _onCaptureEsc: function(event) { |
9105 if (!this.noCancelOnEscKey) { | 5076 if (!this.noCancelOnEscKey) { |
9106 this.cancel(event); | 5077 this.cancel(event); |
9107 } | 5078 } |
9108 }, | 5079 }, |
9109 | |
9110 /** | |
9111 * Handles TAB key events to track focus changes. | |
9112 * Will wrap focus for overlays withBackdrop. | |
9113 * @param {!Event} event | |
9114 * @protected | |
9115 */ | |
9116 _onCaptureTab: function(event) { | 5080 _onCaptureTab: function(event) { |
9117 if (!this.withBackdrop) { | 5081 if (!this.withBackdrop) { |
9118 return; | 5082 return; |
9119 } | 5083 } |
9120 // TAB wraps from last to first focusable. | |
9121 // Shift + TAB wraps from first to last focusable. | |
9122 var shift = event.shiftKey; | 5084 var shift = event.shiftKey; |
9123 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 5085 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
9124 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 5086 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
9125 var shouldWrap = false; | 5087 var shouldWrap = false; |
9126 if (nodeToCheck === nodeToSet) { | 5088 if (nodeToCheck === nodeToSet) { |
9127 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
9128 // with 0 or 1 focusables; in either case we still need to trap the | |
9129 // focus within the overlay. | |
9130 shouldWrap = true; | 5089 shouldWrap = true; |
9131 } else { | 5090 } else { |
9132 // In dom=shadow, the manager will receive focus changes on the main | |
9133 // root but not the ones within other shadow roots, so we can't rely on | |
9134 // _focusedChild, but we should check the deepest active element. | |
9135 var focusedNode = this._manager.deepActiveElement; | 5091 var focusedNode = this._manager.deepActiveElement; |
9136 // If the active element is not the nodeToCheck but the overlay itself, | 5092 shouldWrap = focusedNode === nodeToCheck || focusedNode === this; |
9137 // it means the focus is about to go outside the overlay, hence we | |
9138 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
9139 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | |
9140 } | 5093 } |
9141 | |
9142 if (shouldWrap) { | 5094 if (shouldWrap) { |
9143 // When the overlay contains the last focusable element of the document | |
9144 // and it's already focused, pressing TAB would move the focus outside | |
9145 // the document (e.g. to the browser search bar). Similarly, when the | |
9146 // overlay contains the first focusable element of the document and it's | |
9147 // already focused, pressing Shift+TAB would move the focus outside the | |
9148 // document (e.g. to the browser search bar). | |
9149 // In both cases, we would not receive a focus event, but only a blur. | |
9150 // In order to achieve focus wrapping, we prevent this TAB event and | |
9151 // force the focus. This will also prevent the focus to temporarily move | |
9152 // outside the overlay, which might cause scrolling. | |
9153 event.preventDefault(); | 5095 event.preventDefault(); |
9154 this._focusedChild = nodeToSet; | 5096 this._focusedChild = nodeToSet; |
9155 this._applyFocus(); | 5097 this._applyFocus(); |
9156 } | 5098 } |
9157 }, | 5099 }, |
9158 | |
9159 /** | |
9160 * Refits if the overlay is opened and not animating. | |
9161 * @protected | |
9162 */ | |
9163 _onIronResize: function() { | 5100 _onIronResize: function() { |
9164 if (this.opened && !this.__isAnimating) { | 5101 if (this.opened && !this.__isAnimating) { |
9165 this.__onNextAnimationFrame(this.refit); | 5102 this.__onNextAnimationFrame(this.refit); |
9166 } | 5103 } |
9167 }, | 5104 }, |
9168 | |
9169 /** | |
9170 * Will call notifyResize if overlay is opened. | |
9171 * Can be overridden in order to avoid multiple observers on the same node. | |
9172 * @protected | |
9173 */ | |
9174 _onNodesChange: function() { | 5105 _onNodesChange: function() { |
9175 if (this.opened && !this.__isAnimating) { | 5106 if (this.opened && !this.__isAnimating) { |
9176 this.notifyResize(); | 5107 this.notifyResize(); |
9177 } | 5108 } |
9178 }, | 5109 }, |
9179 | |
9180 /** | |
9181 * Tasks executed when opened changes: prepare for the opening, move the | |
9182 * focus, update the manager, render opened/closed. | |
9183 * @private | |
9184 */ | |
9185 __openedChanged: function() { | 5110 __openedChanged: function() { |
9186 if (this.opened) { | 5111 if (this.opened) { |
9187 // Make overlay visible, then add it to the manager. | |
9188 this._prepareRenderOpened(); | 5112 this._prepareRenderOpened(); |
9189 this._manager.addOverlay(this); | 5113 this._manager.addOverlay(this); |
9190 // Move the focus to the child node with [autofocus]. | |
9191 this._applyFocus(); | 5114 this._applyFocus(); |
9192 | |
9193 this._renderOpened(); | 5115 this._renderOpened(); |
9194 } else { | 5116 } else { |
9195 // Remove overlay, then restore the focus before actually closing. | |
9196 this._manager.removeOverlay(this); | 5117 this._manager.removeOverlay(this); |
9197 this._applyFocus(); | 5118 this._applyFocus(); |
9198 | |
9199 this._renderClosed(); | 5119 this._renderClosed(); |
9200 } | 5120 } |
9201 }, | 5121 }, |
9202 | |
9203 /** | |
9204 * Executes a callback on the next animation frame, overriding any previous | |
9205 * callback awaiting for the next animation frame. e.g. | |
9206 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
9207 * `callback1` will never be invoked. | |
9208 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
9209 * @private | |
9210 */ | |
9211 __onNextAnimationFrame: function(callback) { | 5122 __onNextAnimationFrame: function(callback) { |
9212 if (this.__raf) { | 5123 if (this.__raf) { |
9213 window.cancelAnimationFrame(this.__raf); | 5124 window.cancelAnimationFrame(this.__raf); |
9214 } | 5125 } |
9215 var self = this; | 5126 var self = this; |
9216 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 5127 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
9217 self.__raf = null; | 5128 self.__raf = null; |
9218 callback.call(self); | 5129 callback.call(self); |
9219 }); | 5130 }); |
9220 } | 5131 } |
9221 | |
9222 }; | 5132 }; |
9223 | 5133 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable
Behavior, Polymer.IronOverlayBehaviorImpl ]; |
9224 /** @polymerBehavior */ | |
9225 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
9226 | |
9227 /** | |
9228 * Fired after the overlay opens. | |
9229 * @event iron-overlay-opened | |
9230 */ | |
9231 | |
9232 /** | |
9233 * Fired when the overlay is canceled, but before it is closed. | |
9234 * @event iron-overlay-canceled | |
9235 * @param {Event} event The closing of the overlay can be prevented | |
9236 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
9237 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
9238 */ | |
9239 | |
9240 /** | |
9241 * Fired after the overlay closes. | |
9242 * @event iron-overlay-closed | |
9243 * @param {Event} event The `event.detail` is the `closingReason` property | |
9244 * (contains `canceled`, whether the overlay was canceled). | |
9245 */ | |
9246 | |
9247 })(); | 5134 })(); |
9248 /** | 5135 |
9249 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | 5136 Polymer.NeonAnimatableBehavior = { |
9250 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | 5137 properties: { |
9251 * @polymerBehavior | 5138 animationConfig: { |
9252 */ | 5139 type: Object |
9253 Polymer.NeonAnimatableBehavior = { | 5140 }, |
9254 | 5141 entryAnimation: { |
9255 properties: { | 5142 observer: '_entryAnimationChanged', |
9256 | 5143 type: String |
9257 /** | 5144 }, |
9258 * Animation configuration. See README for more info. | 5145 exitAnimation: { |
9259 */ | 5146 observer: '_exitAnimationChanged', |
9260 animationConfig: { | 5147 type: String |
9261 type: Object | 5148 } |
9262 }, | 5149 }, |
9263 | 5150 _entryAnimationChanged: function() { |
9264 /** | 5151 this.animationConfig = this.animationConfig || {}; |
9265 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | 5152 this.animationConfig['entry'] = [ { |
9266 * manually if using this. The animated node is set to `this` if using thi
s property. | 5153 name: this.entryAnimation, |
9267 */ | 5154 node: this |
9268 entryAnimation: { | 5155 } ]; |
9269 observer: '_entryAnimationChanged', | 5156 }, |
9270 type: String | 5157 _exitAnimationChanged: function() { |
9271 }, | 5158 this.animationConfig = this.animationConfig || {}; |
9272 | 5159 this.animationConfig['exit'] = [ { |
9273 /** | 5160 name: this.exitAnimation, |
9274 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | 5161 node: this |
9275 * manually if using this. The animated node is set to `this` if using thi
s property. | 5162 } ]; |
9276 */ | 5163 }, |
9277 exitAnimation: { | 5164 _copyProperties: function(config1, config2) { |
9278 observer: '_exitAnimationChanged', | 5165 for (var property in config2) { |
9279 type: String | 5166 config1[property] = config2[property]; |
9280 } | 5167 } |
9281 | 5168 }, |
9282 }, | 5169 _cloneConfig: function(config) { |
9283 | 5170 var clone = { |
9284 _entryAnimationChanged: function() { | 5171 isClone: true |
9285 this.animationConfig = this.animationConfig || {}; | 5172 }; |
9286 this.animationConfig['entry'] = [{ | 5173 this._copyProperties(clone, config); |
9287 name: this.entryAnimation, | 5174 return clone; |
9288 node: this | 5175 }, |
9289 }]; | 5176 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
9290 }, | 5177 if (!this.animationConfig) { |
9291 | 5178 return; |
9292 _exitAnimationChanged: function() { | 5179 } |
9293 this.animationConfig = this.animationConfig || {}; | 5180 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun
ction') { |
9294 this.animationConfig['exit'] = [{ | 5181 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid
e of your components 'properties' object instead of outside of it.")); |
9295 name: this.exitAnimation, | 5182 return; |
9296 node: this | 5183 } |
9297 }]; | 5184 var thisConfig; |
9298 }, | 5185 if (type) { |
9299 | 5186 thisConfig = this.animationConfig[type]; |
9300 _copyProperties: function(config1, config2) { | 5187 } else { |
9301 // shallowly copy properties from config2 to config1 | 5188 thisConfig = this.animationConfig; |
9302 for (var property in config2) { | 5189 } |
9303 config1[property] = config2[property]; | 5190 if (!Array.isArray(thisConfig)) { |
9304 } | 5191 thisConfig = [ thisConfig ]; |
9305 }, | 5192 } |
9306 | 5193 if (thisConfig) { |
9307 _cloneConfig: function(config) { | 5194 for (var config, index = 0; config = thisConfig[index]; index++) { |
9308 var clone = { | 5195 if (config.animatable) { |
9309 isClone: true | 5196 config.animatable._getAnimationConfigRecursive(config.type || type, ma
p, allConfigs); |
9310 }; | 5197 } else { |
9311 this._copyProperties(clone, config); | 5198 if (config.id) { |
9312 return clone; | 5199 var cachedConfig = map[config.id]; |
9313 }, | 5200 if (cachedConfig) { |
9314 | 5201 if (!cachedConfig.isClone) { |
9315 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 5202 map[config.id] = this._cloneConfig(cachedConfig); |
9316 if (!this.animationConfig) { | 5203 cachedConfig = map[config.id]; |
9317 return; | |
9318 } | |
9319 | |
9320 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | |
9321 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | |
9322 » return; | |
9323 } | |
9324 | |
9325 // type is optional | |
9326 var thisConfig; | |
9327 if (type) { | |
9328 thisConfig = this.animationConfig[type]; | |
9329 } else { | |
9330 thisConfig = this.animationConfig; | |
9331 } | |
9332 | |
9333 if (!Array.isArray(thisConfig)) { | |
9334 thisConfig = [thisConfig]; | |
9335 } | |
9336 | |
9337 // iterate animations and recurse to process configurations from child nod
es | |
9338 if (thisConfig) { | |
9339 for (var config, index = 0; config = thisConfig[index]; index++) { | |
9340 if (config.animatable) { | |
9341 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | |
9342 } else { | |
9343 if (config.id) { | |
9344 var cachedConfig = map[config.id]; | |
9345 if (cachedConfig) { | |
9346 // merge configurations with the same id, making a clone lazily | |
9347 if (!cachedConfig.isClone) { | |
9348 map[config.id] = this._cloneConfig(cachedConfig) | |
9349 cachedConfig = map[config.id]; | |
9350 } | |
9351 this._copyProperties(cachedConfig, config); | |
9352 } else { | |
9353 // put any configs with an id into a map | |
9354 map[config.id] = config; | |
9355 } | 5204 } |
| 5205 this._copyProperties(cachedConfig, config); |
9356 } else { | 5206 } else { |
9357 allConfigs.push(config); | 5207 map[config.id] = config; |
9358 } | |
9359 } | |
9360 } | |
9361 } | |
9362 }, | |
9363 | |
9364 /** | |
9365 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
9366 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
9367 * should define the property `animationConfig`, which is either a configura
tion object | |
9368 * or a map of animation type to array of configuration objects. | |
9369 */ | |
9370 getAnimationConfig: function(type) { | |
9371 var map = {}; | |
9372 var allConfigs = []; | |
9373 this._getAnimationConfigRecursive(type, map, allConfigs); | |
9374 // append the configurations saved in the map to the array | |
9375 for (var key in map) { | |
9376 allConfigs.push(map[key]); | |
9377 } | |
9378 return allConfigs; | |
9379 } | |
9380 | |
9381 }; | |
9382 /** | |
9383 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
9384 * | |
9385 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
9386 */ | |
9387 Polymer.NeonAnimationRunnerBehaviorImpl = { | |
9388 | |
9389 _configureAnimations: function(configs) { | |
9390 var results = []; | |
9391 if (configs.length > 0) { | |
9392 for (var config, index = 0; config = configs[index]; index++) { | |
9393 var neonAnimation = document.createElement(config.name); | |
9394 // is this element actually a neon animation? | |
9395 if (neonAnimation.isNeonAnimation) { | |
9396 var result = null; | |
9397 // configuration or play could fail if polyfills aren't loaded | |
9398 try { | |
9399 result = neonAnimation.configure(config); | |
9400 // Check if we have an Effect rather than an Animation | |
9401 if (typeof result.cancel != 'function') { | |
9402 result = document.timeline.play(result); | |
9403 } | |
9404 } catch (e) { | |
9405 result = null; | |
9406 console.warn('Couldnt play', '(', config.name, ').', e); | |
9407 } | |
9408 if (result) { | |
9409 results.push({ | |
9410 neonAnimation: neonAnimation, | |
9411 config: config, | |
9412 animation: result, | |
9413 }); | |
9414 } | 5208 } |
9415 } else { | 5209 } else { |
9416 console.warn(this.is + ':', config.name, 'not found!'); | 5210 allConfigs.push(config); |
9417 } | 5211 } |
9418 } | 5212 } |
9419 } | 5213 } |
9420 return results; | 5214 } |
9421 }, | 5215 }, |
9422 | 5216 getAnimationConfig: function(type) { |
9423 _shouldComplete: function(activeEntries) { | 5217 var map = {}; |
9424 var finished = true; | 5218 var allConfigs = []; |
9425 for (var i = 0; i < activeEntries.length; i++) { | 5219 this._getAnimationConfigRecursive(type, map, allConfigs); |
9426 if (activeEntries[i].animation.playState != 'finished') { | 5220 for (var key in map) { |
9427 finished = false; | 5221 allConfigs.push(map[key]); |
9428 break; | 5222 } |
9429 } | 5223 return allConfigs; |
9430 } | 5224 } |
9431 return finished; | 5225 }; |
9432 }, | 5226 |
9433 | 5227 Polymer.NeonAnimationRunnerBehaviorImpl = { |
9434 _complete: function(activeEntries) { | 5228 _configureAnimations: function(configs) { |
9435 for (var i = 0; i < activeEntries.length; i++) { | 5229 var results = []; |
9436 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | 5230 if (configs.length > 0) { |
9437 } | 5231 for (var config, index = 0; config = configs[index]; index++) { |
9438 for (var i = 0; i < activeEntries.length; i++) { | 5232 var neonAnimation = document.createElement(config.name); |
9439 activeEntries[i].animation.cancel(); | 5233 if (neonAnimation.isNeonAnimation) { |
9440 } | 5234 var result = null; |
9441 }, | 5235 try { |
9442 | 5236 result = neonAnimation.configure(config); |
9443 /** | 5237 if (typeof result.cancel != 'function') { |
9444 * Plays an animation with an optional `type`. | 5238 result = document.timeline.play(result); |
9445 * @param {string=} type | 5239 } |
9446 * @param {!Object=} cookie | 5240 } catch (e) { |
9447 */ | 5241 result = null; |
9448 playAnimation: function(type, cookie) { | 5242 console.warn('Couldnt play', '(', config.name, ').', e); |
9449 var configs = this.getAnimationConfig(type); | 5243 } |
9450 if (!configs) { | 5244 if (result) { |
| 5245 results.push({ |
| 5246 neonAnimation: neonAnimation, |
| 5247 config: config, |
| 5248 animation: result |
| 5249 }); |
| 5250 } |
| 5251 } else { |
| 5252 console.warn(this.is + ':', config.name, 'not found!'); |
| 5253 } |
| 5254 } |
| 5255 } |
| 5256 return results; |
| 5257 }, |
| 5258 _shouldComplete: function(activeEntries) { |
| 5259 var finished = true; |
| 5260 for (var i = 0; i < activeEntries.length; i++) { |
| 5261 if (activeEntries[i].animation.playState != 'finished') { |
| 5262 finished = false; |
| 5263 break; |
| 5264 } |
| 5265 } |
| 5266 return finished; |
| 5267 }, |
| 5268 _complete: function(activeEntries) { |
| 5269 for (var i = 0; i < activeEntries.length; i++) { |
| 5270 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
| 5271 } |
| 5272 for (var i = 0; i < activeEntries.length; i++) { |
| 5273 activeEntries[i].animation.cancel(); |
| 5274 } |
| 5275 }, |
| 5276 playAnimation: function(type, cookie) { |
| 5277 var configs = this.getAnimationConfig(type); |
| 5278 if (!configs) { |
| 5279 return; |
| 5280 } |
| 5281 this._active = this._active || {}; |
| 5282 if (this._active[type]) { |
| 5283 this._complete(this._active[type]); |
| 5284 delete this._active[type]; |
| 5285 } |
| 5286 var activeEntries = this._configureAnimations(configs); |
| 5287 if (activeEntries.length == 0) { |
| 5288 this.fire('neon-animation-finish', cookie, { |
| 5289 bubbles: false |
| 5290 }); |
| 5291 return; |
| 5292 } |
| 5293 this._active[type] = activeEntries; |
| 5294 for (var i = 0; i < activeEntries.length; i++) { |
| 5295 activeEntries[i].animation.onfinish = function() { |
| 5296 if (this._shouldComplete(activeEntries)) { |
| 5297 this._complete(activeEntries); |
| 5298 delete this._active[type]; |
| 5299 this.fire('neon-animation-finish', cookie, { |
| 5300 bubbles: false |
| 5301 }); |
| 5302 } |
| 5303 }.bind(this); |
| 5304 } |
| 5305 }, |
| 5306 cancelAnimation: function() { |
| 5307 for (var k in this._animations) { |
| 5308 this._animations[k].cancel(); |
| 5309 } |
| 5310 this._animations = {}; |
| 5311 } |
| 5312 }; |
| 5313 |
| 5314 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer.
NeonAnimationRunnerBehaviorImpl ]; |
| 5315 |
| 5316 Polymer.NeonAnimationBehavior = { |
| 5317 properties: { |
| 5318 animationTiming: { |
| 5319 type: Object, |
| 5320 value: function() { |
| 5321 return { |
| 5322 duration: 500, |
| 5323 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
| 5324 fill: 'both' |
| 5325 }; |
| 5326 } |
| 5327 } |
| 5328 }, |
| 5329 isNeonAnimation: true, |
| 5330 timingFromConfig: function(config) { |
| 5331 if (config.timing) { |
| 5332 for (var property in config.timing) { |
| 5333 this.animationTiming[property] = config.timing[property]; |
| 5334 } |
| 5335 } |
| 5336 return this.animationTiming; |
| 5337 }, |
| 5338 setPrefixedProperty: function(node, property, value) { |
| 5339 var map = { |
| 5340 transform: [ 'webkitTransform' ], |
| 5341 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ] |
| 5342 }; |
| 5343 var prefixes = map[property]; |
| 5344 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
| 5345 node.style[prefix] = value; |
| 5346 } |
| 5347 node.style[property] = value; |
| 5348 }, |
| 5349 complete: function() {} |
| 5350 }; |
| 5351 |
| 5352 Polymer({ |
| 5353 is: 'opaque-animation', |
| 5354 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5355 configure: function(config) { |
| 5356 var node = config.node; |
| 5357 this._effect = new KeyframeEffect(node, [ { |
| 5358 opacity: '1' |
| 5359 }, { |
| 5360 opacity: '1' |
| 5361 } ], this.timingFromConfig(config)); |
| 5362 node.style.opacity = '0'; |
| 5363 return this._effect; |
| 5364 }, |
| 5365 complete: function(config) { |
| 5366 config.node.style.opacity = ''; |
| 5367 } |
| 5368 }); |
| 5369 |
| 5370 (function() { |
| 5371 'use strict'; |
| 5372 var LAST_TOUCH_POSITION = { |
| 5373 pageX: 0, |
| 5374 pageY: 0 |
| 5375 }; |
| 5376 var ROOT_TARGET = null; |
| 5377 var SCROLLABLE_NODES = []; |
| 5378 Polymer.IronDropdownScrollManager = { |
| 5379 get currentLockingElement() { |
| 5380 return this._lockingElements[this._lockingElements.length - 1]; |
| 5381 }, |
| 5382 elementIsScrollLocked: function(element) { |
| 5383 var currentLockingElement = this.currentLockingElement; |
| 5384 if (currentLockingElement === undefined) return false; |
| 5385 var scrollLocked; |
| 5386 if (this._hasCachedLockedElement(element)) { |
| 5387 return true; |
| 5388 } |
| 5389 if (this._hasCachedUnlockedElement(element)) { |
| 5390 return false; |
| 5391 } |
| 5392 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen
t && !this._composedTreeContains(currentLockingElement, element); |
| 5393 if (scrollLocked) { |
| 5394 this._lockedElementCache.push(element); |
| 5395 } else { |
| 5396 this._unlockedElementCache.push(element); |
| 5397 } |
| 5398 return scrollLocked; |
| 5399 }, |
| 5400 pushScrollLock: function(element) { |
| 5401 if (this._lockingElements.indexOf(element) >= 0) { |
9451 return; | 5402 return; |
9452 } | 5403 } |
9453 this._active = this._active || {}; | 5404 if (this._lockingElements.length === 0) { |
9454 if (this._active[type]) { | 5405 this._lockScrollInteractions(); |
9455 this._complete(this._active[type]); | 5406 } |
9456 delete this._active[type]; | 5407 this._lockingElements.push(element); |
9457 } | 5408 this._lockedElementCache = []; |
9458 | 5409 this._unlockedElementCache = []; |
9459 var activeEntries = this._configureAnimations(configs); | 5410 }, |
9460 | 5411 removeScrollLock: function(element) { |
9461 if (activeEntries.length == 0) { | 5412 var index = this._lockingElements.indexOf(element); |
9462 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5413 if (index === -1) { |
9463 return; | 5414 return; |
9464 } | 5415 } |
9465 | 5416 this._lockingElements.splice(index, 1); |
9466 this._active[type] = activeEntries; | 5417 this._lockedElementCache = []; |
9467 | 5418 this._unlockedElementCache = []; |
9468 for (var i = 0; i < activeEntries.length; i++) { | 5419 if (this._lockingElements.length === 0) { |
9469 activeEntries[i].animation.onfinish = function() { | 5420 this._unlockScrollInteractions(); |
9470 if (this._shouldComplete(activeEntries)) { | 5421 } |
9471 this._complete(activeEntries); | 5422 }, |
9472 delete this._active[type]; | 5423 _lockingElements: [], |
9473 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5424 _lockedElementCache: null, |
| 5425 _unlockedElementCache: null, |
| 5426 _hasCachedLockedElement: function(element) { |
| 5427 return this._lockedElementCache.indexOf(element) > -1; |
| 5428 }, |
| 5429 _hasCachedUnlockedElement: function(element) { |
| 5430 return this._unlockedElementCache.indexOf(element) > -1; |
| 5431 }, |
| 5432 _composedTreeContains: function(element, child) { |
| 5433 var contentElements; |
| 5434 var distributedNodes; |
| 5435 var contentIndex; |
| 5436 var nodeIndex; |
| 5437 if (element.contains(child)) { |
| 5438 return true; |
| 5439 } |
| 5440 contentElements = Polymer.dom(element).querySelectorAll('content'); |
| 5441 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd
ex) { |
| 5442 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib
utedNodes(); |
| 5443 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { |
| 5444 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { |
| 5445 return true; |
9474 } | 5446 } |
9475 }.bind(this); | 5447 } |
9476 } | 5448 } |
9477 }, | 5449 return false; |
9478 | 5450 }, |
9479 /** | 5451 _scrollInteractionHandler: function(event) { |
9480 * Cancels the currently running animations. | 5452 if (event.cancelable && this._shouldPreventScrolling(event)) { |
9481 */ | 5453 event.preventDefault(); |
9482 cancelAnimation: function() { | 5454 } |
9483 for (var k in this._animations) { | 5455 if (event.targetTouches) { |
9484 this._animations[k].cancel(); | 5456 var touch = event.targetTouches[0]; |
9485 } | 5457 LAST_TOUCH_POSITION.pageX = touch.pageX; |
9486 this._animations = {}; | 5458 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 5459 } |
| 5460 }, |
| 5461 _lockScrollInteractions: function() { |
| 5462 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac
tionHandler.bind(this); |
| 5463 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 5464 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 5465 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true
); |
| 5466 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 5467 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 5468 }, |
| 5469 _unlockScrollInteractions: function() { |
| 5470 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 5471 document.removeEventListener('mousewheel', this._boundScrollHandler, true)
; |
| 5472 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t
rue); |
| 5473 document.removeEventListener('touchstart', this._boundScrollHandler, true)
; |
| 5474 document.removeEventListener('touchmove', this._boundScrollHandler, true); |
| 5475 }, |
| 5476 _shouldPreventScrolling: function(event) { |
| 5477 var target = Polymer.dom(event).rootTarget; |
| 5478 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 5479 ROOT_TARGET = target; |
| 5480 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 5481 } |
| 5482 if (!SCROLLABLE_NODES.length) { |
| 5483 return true; |
| 5484 } |
| 5485 if (event.type === 'touchstart') { |
| 5486 return false; |
| 5487 } |
| 5488 var info = this._getScrollInfo(event); |
| 5489 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY)
; |
| 5490 }, |
| 5491 _getScrollableNodes: function(nodes) { |
| 5492 var scrollables = []; |
| 5493 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 5494 for (var i = 0; i <= lockingIndex; i++) { |
| 5495 var node = nodes[i]; |
| 5496 if (node.nodeType === 11) { |
| 5497 continue; |
| 5498 } |
| 5499 var style = node.style; |
| 5500 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 5501 style = window.getComputedStyle(node); |
| 5502 } |
| 5503 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 5504 scrollables.push(node); |
| 5505 } |
| 5506 } |
| 5507 return scrollables; |
| 5508 }, |
| 5509 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 5510 if (!deltaX && !deltaY) { |
| 5511 return; |
| 5512 } |
| 5513 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 5514 for (var i = 0; i < nodes.length; i++) { |
| 5515 var node = nodes[i]; |
| 5516 var canScroll = false; |
| 5517 if (verticalScroll) { |
| 5518 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc
rollHeight - node.clientHeight; |
| 5519 } else { |
| 5520 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.
scrollWidth - node.clientWidth; |
| 5521 } |
| 5522 if (canScroll) { |
| 5523 return node; |
| 5524 } |
| 5525 } |
| 5526 }, |
| 5527 _getScrollInfo: function(event) { |
| 5528 var info = { |
| 5529 deltaX: event.deltaX, |
| 5530 deltaY: event.deltaY |
| 5531 }; |
| 5532 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) { |
| 5533 info.deltaX = -event.wheelDeltaX; |
| 5534 info.deltaY = -event.wheelDeltaY; |
| 5535 } else if ('axis' in event) { |
| 5536 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 5537 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 5538 } else if (event.targetTouches) { |
| 5539 var touch = event.targetTouches[0]; |
| 5540 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 5541 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 5542 } |
| 5543 return info; |
9487 } | 5544 } |
9488 }; | 5545 }; |
9489 | 5546 })(); |
9490 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | 5547 |
9491 Polymer.NeonAnimationRunnerBehavior = [ | 5548 (function() { |
9492 Polymer.NeonAnimatableBehavior, | 5549 'use strict'; |
9493 Polymer.NeonAnimationRunnerBehaviorImpl | 5550 Polymer({ |
9494 ]; | 5551 is: 'iron-dropdown', |
9495 /** | 5552 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer
.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], |
9496 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
9497 * @polymerBehavior | |
9498 */ | |
9499 Polymer.NeonAnimationBehavior = { | |
9500 | |
9501 properties: { | 5553 properties: { |
9502 | 5554 horizontalAlign: { |
9503 /** | 5555 type: String, |
9504 * Defines the animation timing. | 5556 value: 'left', |
9505 */ | 5557 reflectToAttribute: true |
9506 animationTiming: { | 5558 }, |
| 5559 verticalAlign: { |
| 5560 type: String, |
| 5561 value: 'top', |
| 5562 reflectToAttribute: true |
| 5563 }, |
| 5564 openAnimationConfig: { |
| 5565 type: Object |
| 5566 }, |
| 5567 closeAnimationConfig: { |
| 5568 type: Object |
| 5569 }, |
| 5570 focusTarget: { |
| 5571 type: Object |
| 5572 }, |
| 5573 noAnimations: { |
| 5574 type: Boolean, |
| 5575 value: false |
| 5576 }, |
| 5577 allowOutsideScroll: { |
| 5578 type: Boolean, |
| 5579 value: false |
| 5580 }, |
| 5581 _boundOnCaptureScroll: { |
| 5582 type: Function, |
| 5583 value: function() { |
| 5584 return this._onCaptureScroll.bind(this); |
| 5585 } |
| 5586 } |
| 5587 }, |
| 5588 listeners: { |
| 5589 'neon-animation-finish': '_onNeonAnimationFinish' |
| 5590 }, |
| 5591 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont
alAlign, verticalOffset, horizontalOffset)' ], |
| 5592 get containedElement() { |
| 5593 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5594 }, |
| 5595 get _focusTarget() { |
| 5596 return this.focusTarget || this.containedElement; |
| 5597 }, |
| 5598 ready: function() { |
| 5599 this._scrollTop = 0; |
| 5600 this._scrollLeft = 0; |
| 5601 this._refitOnScrollRAF = null; |
| 5602 }, |
| 5603 detached: function() { |
| 5604 this.cancelAnimation(); |
| 5605 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5606 }, |
| 5607 _openedChanged: function() { |
| 5608 if (this.opened && this.disabled) { |
| 5609 this.cancel(); |
| 5610 } else { |
| 5611 this.cancelAnimation(); |
| 5612 this.sizingTarget = this.containedElement || this.sizingTarget; |
| 5613 this._updateAnimationConfig(); |
| 5614 this._saveScrollPosition(); |
| 5615 if (this.opened) { |
| 5616 document.addEventListener('scroll', this._boundOnCaptureScroll); |
| 5617 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro
llLock(this); |
| 5618 } else { |
| 5619 document.removeEventListener('scroll', this._boundOnCaptureScroll); |
| 5620 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5621 } |
| 5622 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); |
| 5623 } |
| 5624 }, |
| 5625 _renderOpened: function() { |
| 5626 if (!this.noAnimations && this.animationConfig.open) { |
| 5627 this.$.contentWrapper.classList.add('animating'); |
| 5628 this.playAnimation('open'); |
| 5629 } else { |
| 5630 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); |
| 5631 } |
| 5632 }, |
| 5633 _renderClosed: function() { |
| 5634 if (!this.noAnimations && this.animationConfig.close) { |
| 5635 this.$.contentWrapper.classList.add('animating'); |
| 5636 this.playAnimation('close'); |
| 5637 } else { |
| 5638 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); |
| 5639 } |
| 5640 }, |
| 5641 _onNeonAnimationFinish: function() { |
| 5642 this.$.contentWrapper.classList.remove('animating'); |
| 5643 if (this.opened) { |
| 5644 this._finishRenderOpened(); |
| 5645 } else { |
| 5646 this._finishRenderClosed(); |
| 5647 } |
| 5648 }, |
| 5649 _onCaptureScroll: function() { |
| 5650 if (!this.allowOutsideScroll) { |
| 5651 this._restoreScrollPosition(); |
| 5652 } else { |
| 5653 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol
lRAF); |
| 5654 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th
is)); |
| 5655 } |
| 5656 }, |
| 5657 _saveScrollPosition: function() { |
| 5658 if (document.scrollingElement) { |
| 5659 this._scrollTop = document.scrollingElement.scrollTop; |
| 5660 this._scrollLeft = document.scrollingElement.scrollLeft; |
| 5661 } else { |
| 5662 this._scrollTop = Math.max(document.documentElement.scrollTop, document.
body.scrollTop); |
| 5663 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen
t.body.scrollLeft); |
| 5664 } |
| 5665 }, |
| 5666 _restoreScrollPosition: function() { |
| 5667 if (document.scrollingElement) { |
| 5668 document.scrollingElement.scrollTop = this._scrollTop; |
| 5669 document.scrollingElement.scrollLeft = this._scrollLeft; |
| 5670 } else { |
| 5671 document.documentElement.scrollTop = this._scrollTop; |
| 5672 document.documentElement.scrollLeft = this._scrollLeft; |
| 5673 document.body.scrollTop = this._scrollTop; |
| 5674 document.body.scrollLeft = this._scrollLeft; |
| 5675 } |
| 5676 }, |
| 5677 _updateAnimationConfig: function() { |
| 5678 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati
onConfig || []); |
| 5679 for (var i = 0; i < animations.length; i++) { |
| 5680 animations[i].node = this.containedElement; |
| 5681 } |
| 5682 this.animationConfig = { |
| 5683 open: this.openAnimationConfig, |
| 5684 close: this.closeAnimationConfig |
| 5685 }; |
| 5686 }, |
| 5687 _updateOverlayPosition: function() { |
| 5688 if (this.isAttached) { |
| 5689 this.notifyResize(); |
| 5690 } |
| 5691 }, |
| 5692 _applyFocus: function() { |
| 5693 var focusTarget = this.focusTarget || this.containedElement; |
| 5694 if (focusTarget && this.opened && !this.noAutoFocus) { |
| 5695 focusTarget.focus(); |
| 5696 } else { |
| 5697 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
| 5698 } |
| 5699 } |
| 5700 }); |
| 5701 })(); |
| 5702 |
| 5703 Polymer({ |
| 5704 is: 'fade-in-animation', |
| 5705 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5706 configure: function(config) { |
| 5707 var node = config.node; |
| 5708 this._effect = new KeyframeEffect(node, [ { |
| 5709 opacity: '0' |
| 5710 }, { |
| 5711 opacity: '1' |
| 5712 } ], this.timingFromConfig(config)); |
| 5713 return this._effect; |
| 5714 } |
| 5715 }); |
| 5716 |
| 5717 Polymer({ |
| 5718 is: 'fade-out-animation', |
| 5719 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5720 configure: function(config) { |
| 5721 var node = config.node; |
| 5722 this._effect = new KeyframeEffect(node, [ { |
| 5723 opacity: '1' |
| 5724 }, { |
| 5725 opacity: '0' |
| 5726 } ], this.timingFromConfig(config)); |
| 5727 return this._effect; |
| 5728 } |
| 5729 }); |
| 5730 |
| 5731 Polymer({ |
| 5732 is: 'paper-menu-grow-height-animation', |
| 5733 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5734 configure: function(config) { |
| 5735 var node = config.node; |
| 5736 var rect = node.getBoundingClientRect(); |
| 5737 var height = rect.height; |
| 5738 this._effect = new KeyframeEffect(node, [ { |
| 5739 height: height / 2 + 'px' |
| 5740 }, { |
| 5741 height: height + 'px' |
| 5742 } ], this.timingFromConfig(config)); |
| 5743 return this._effect; |
| 5744 } |
| 5745 }); |
| 5746 |
| 5747 Polymer({ |
| 5748 is: 'paper-menu-grow-width-animation', |
| 5749 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5750 configure: function(config) { |
| 5751 var node = config.node; |
| 5752 var rect = node.getBoundingClientRect(); |
| 5753 var width = rect.width; |
| 5754 this._effect = new KeyframeEffect(node, [ { |
| 5755 width: width / 2 + 'px' |
| 5756 }, { |
| 5757 width: width + 'px' |
| 5758 } ], this.timingFromConfig(config)); |
| 5759 return this._effect; |
| 5760 } |
| 5761 }); |
| 5762 |
| 5763 Polymer({ |
| 5764 is: 'paper-menu-shrink-width-animation', |
| 5765 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5766 configure: function(config) { |
| 5767 var node = config.node; |
| 5768 var rect = node.getBoundingClientRect(); |
| 5769 var width = rect.width; |
| 5770 this._effect = new KeyframeEffect(node, [ { |
| 5771 width: width + 'px' |
| 5772 }, { |
| 5773 width: width - width / 20 + 'px' |
| 5774 } ], this.timingFromConfig(config)); |
| 5775 return this._effect; |
| 5776 } |
| 5777 }); |
| 5778 |
| 5779 Polymer({ |
| 5780 is: 'paper-menu-shrink-height-animation', |
| 5781 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5782 configure: function(config) { |
| 5783 var node = config.node; |
| 5784 var rect = node.getBoundingClientRect(); |
| 5785 var height = rect.height; |
| 5786 var top = rect.top; |
| 5787 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
| 5788 this._effect = new KeyframeEffect(node, [ { |
| 5789 height: height + 'px', |
| 5790 transform: 'translateY(0)' |
| 5791 }, { |
| 5792 height: height / 2 + 'px', |
| 5793 transform: 'translateY(-20px)' |
| 5794 } ], this.timingFromConfig(config)); |
| 5795 return this._effect; |
| 5796 } |
| 5797 }); |
| 5798 |
| 5799 (function() { |
| 5800 'use strict'; |
| 5801 var config = { |
| 5802 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', |
| 5803 MAX_ANIMATION_TIME_MS: 400 |
| 5804 }; |
| 5805 var PaperMenuButton = Polymer({ |
| 5806 is: 'paper-menu-button', |
| 5807 behaviors: [ Polymer.IronA11yKeysBehavior, Polymer.IronControlState ], |
| 5808 properties: { |
| 5809 opened: { |
| 5810 type: Boolean, |
| 5811 value: false, |
| 5812 notify: true, |
| 5813 observer: '_openedChanged' |
| 5814 }, |
| 5815 horizontalAlign: { |
| 5816 type: String, |
| 5817 value: 'left', |
| 5818 reflectToAttribute: true |
| 5819 }, |
| 5820 verticalAlign: { |
| 5821 type: String, |
| 5822 value: 'top', |
| 5823 reflectToAttribute: true |
| 5824 }, |
| 5825 dynamicAlign: { |
| 5826 type: Boolean |
| 5827 }, |
| 5828 horizontalOffset: { |
| 5829 type: Number, |
| 5830 value: 0, |
| 5831 notify: true |
| 5832 }, |
| 5833 verticalOffset: { |
| 5834 type: Number, |
| 5835 value: 0, |
| 5836 notify: true |
| 5837 }, |
| 5838 noOverlap: { |
| 5839 type: Boolean |
| 5840 }, |
| 5841 noAnimations: { |
| 5842 type: Boolean, |
| 5843 value: false |
| 5844 }, |
| 5845 ignoreSelect: { |
| 5846 type: Boolean, |
| 5847 value: false |
| 5848 }, |
| 5849 closeOnActivate: { |
| 5850 type: Boolean, |
| 5851 value: false |
| 5852 }, |
| 5853 openAnimationConfig: { |
9507 type: Object, | 5854 type: Object, |
9508 value: function() { | 5855 value: function() { |
9509 return { | 5856 return [ { |
9510 duration: 500, | 5857 name: 'fade-in-animation', |
9511 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | 5858 timing: { |
9512 fill: 'both' | 5859 delay: 100, |
9513 } | 5860 duration: 200 |
9514 } | 5861 } |
9515 } | 5862 }, { |
9516 | 5863 name: 'paper-menu-grow-width-animation', |
9517 }, | 5864 timing: { |
9518 | 5865 delay: 100, |
9519 /** | 5866 duration: 150, |
9520 * Can be used to determine that elements implement this behavior. | 5867 easing: config.ANIMATION_CUBIC_BEZIER |
9521 */ | 5868 } |
9522 isNeonAnimation: true, | 5869 }, { |
9523 | 5870 name: 'paper-menu-grow-height-animation', |
9524 /** | 5871 timing: { |
9525 * Do any animation configuration here. | 5872 delay: 100, |
9526 */ | 5873 duration: 275, |
9527 // configure: function(config) { | 5874 easing: config.ANIMATION_CUBIC_BEZIER |
9528 // }, | 5875 } |
9529 | 5876 } ]; |
9530 /** | 5877 } |
9531 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | 5878 }, |
9532 * by the animation. | 5879 closeAnimationConfig: { |
9533 */ | 5880 type: Object, |
9534 timingFromConfig: function(config) { | 5881 value: function() { |
9535 if (config.timing) { | 5882 return [ { |
9536 for (var property in config.timing) { | 5883 name: 'fade-out-animation', |
9537 this.animationTiming[property] = config.timing[property]; | 5884 timing: { |
9538 } | 5885 duration: 150 |
9539 } | 5886 } |
9540 return this.animationTiming; | 5887 }, { |
9541 }, | 5888 name: 'paper-menu-shrink-width-animation', |
9542 | 5889 timing: { |
9543 /** | 5890 delay: 100, |
9544 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | 5891 duration: 50, |
9545 */ | 5892 easing: config.ANIMATION_CUBIC_BEZIER |
9546 setPrefixedProperty: function(node, property, value) { | 5893 } |
9547 var map = { | 5894 }, { |
9548 'transform': ['webkitTransform'], | 5895 name: 'paper-menu-shrink-height-animation', |
9549 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | 5896 timing: { |
9550 }; | 5897 duration: 200, |
9551 var prefixes = map[property]; | 5898 easing: 'ease-in' |
9552 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | 5899 } |
9553 node.style[prefix] = value; | 5900 } ]; |
9554 } | 5901 } |
9555 node.style[property] = value; | 5902 }, |
9556 }, | 5903 allowOutsideScroll: { |
9557 | 5904 type: Boolean, |
9558 /** | 5905 value: false |
9559 * Called when the animation finishes. | 5906 }, |
9560 */ | 5907 restoreFocusOnClose: { |
9561 complete: function() {} | 5908 type: Boolean, |
9562 | 5909 value: true |
9563 }; | 5910 }, |
| 5911 _dropdownContent: { |
| 5912 type: Object |
| 5913 } |
| 5914 }, |
| 5915 hostAttributes: { |
| 5916 role: 'group', |
| 5917 'aria-haspopup': 'true' |
| 5918 }, |
| 5919 listeners: { |
| 5920 'iron-activate': '_onIronActivate', |
| 5921 'iron-select': '_onIronSelect' |
| 5922 }, |
| 5923 get contentElement() { |
| 5924 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5925 }, |
| 5926 toggle: function() { |
| 5927 if (this.opened) { |
| 5928 this.close(); |
| 5929 } else { |
| 5930 this.open(); |
| 5931 } |
| 5932 }, |
| 5933 open: function() { |
| 5934 if (this.disabled) { |
| 5935 return; |
| 5936 } |
| 5937 this.$.dropdown.open(); |
| 5938 }, |
| 5939 close: function() { |
| 5940 this.$.dropdown.close(); |
| 5941 }, |
| 5942 _onIronSelect: function(event) { |
| 5943 if (!this.ignoreSelect) { |
| 5944 this.close(); |
| 5945 } |
| 5946 }, |
| 5947 _onIronActivate: function(event) { |
| 5948 if (this.closeOnActivate) { |
| 5949 this.close(); |
| 5950 } |
| 5951 }, |
| 5952 _openedChanged: function(opened, oldOpened) { |
| 5953 if (opened) { |
| 5954 this._dropdownContent = this.contentElement; |
| 5955 this.fire('paper-dropdown-open'); |
| 5956 } else if (oldOpened != null) { |
| 5957 this.fire('paper-dropdown-close'); |
| 5958 } |
| 5959 }, |
| 5960 _disabledChanged: function(disabled) { |
| 5961 Polymer.IronControlState._disabledChanged.apply(this, arguments); |
| 5962 if (disabled && this.opened) { |
| 5963 this.close(); |
| 5964 } |
| 5965 }, |
| 5966 __onIronOverlayCanceled: function(event) { |
| 5967 var uiEvent = event.detail; |
| 5968 var target = Polymer.dom(uiEvent).rootTarget; |
| 5969 var trigger = this.$.trigger; |
| 5970 var path = Polymer.dom(uiEvent).path; |
| 5971 if (path.indexOf(trigger) > -1) { |
| 5972 event.preventDefault(); |
| 5973 } |
| 5974 } |
| 5975 }); |
| 5976 Object.keys(config).forEach(function(key) { |
| 5977 PaperMenuButton[key] = config[key]; |
| 5978 }); |
| 5979 Polymer.PaperMenuButton = PaperMenuButton; |
| 5980 })(); |
| 5981 |
| 5982 Polymer.PaperInkyFocusBehaviorImpl = { |
| 5983 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ], |
| 5984 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 5985 if (receivedFocusFromKeyboard) { |
| 5986 this.ensureRipple(); |
| 5987 } |
| 5988 if (this.hasRipple()) { |
| 5989 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 5990 } |
| 5991 }, |
| 5992 _createRipple: function() { |
| 5993 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 5994 ripple.id = 'ink'; |
| 5995 ripple.setAttribute('center', ''); |
| 5996 ripple.classList.add('circle'); |
| 5997 return ripple; |
| 5998 } |
| 5999 }; |
| 6000 |
| 6001 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS
tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ]; |
| 6002 |
9564 Polymer({ | 6003 Polymer({ |
9565 | 6004 is: 'paper-icon-button', |
9566 is: 'opaque-animation', | 6005 hostAttributes: { |
9567 | 6006 role: 'button', |
9568 behaviors: [ | 6007 tabindex: '0' |
9569 Polymer.NeonAnimationBehavior | 6008 }, |
9570 ], | 6009 behaviors: [ Polymer.PaperInkyFocusBehavior ], |
9571 | 6010 properties: { |
9572 configure: function(config) { | 6011 src: { |
9573 var node = config.node; | 6012 type: String |
9574 this._effect = new KeyframeEffect(node, [ | 6013 }, |
9575 {'opacity': '1'}, | 6014 icon: { |
9576 {'opacity': '1'} | 6015 type: String |
9577 ], this.timingFromConfig(config)); | 6016 }, |
9578 node.style.opacity = '0'; | 6017 alt: { |
9579 return this._effect; | 6018 type: String, |
9580 }, | 6019 observer: "_altChanged" |
9581 | 6020 } |
9582 complete: function(config) { | 6021 }, |
9583 config.node.style.opacity = ''; | 6022 _altChanged: function(newValue, oldValue) { |
9584 } | 6023 var label = this.getAttribute('aria-label'); |
9585 | 6024 if (!label || oldValue == label) { |
9586 }); | 6025 this.setAttribute('aria-label', newValue); |
9587 (function() { | 6026 } |
9588 'use strict'; | 6027 } |
9589 // Used to calculate the scroll direction during touch events. | 6028 }); |
9590 var LAST_TOUCH_POSITION = { | 6029 |
9591 pageX: 0, | |
9592 pageY: 0 | |
9593 }; | |
9594 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
9595 var ROOT_TARGET = null; | |
9596 var SCROLLABLE_NODES = []; | |
9597 | |
9598 /** | |
9599 * The IronDropdownScrollManager is intended to provide a central source | |
9600 * of authority and control over which elements in a document are currently | |
9601 * allowed to scroll. | |
9602 */ | |
9603 | |
9604 Polymer.IronDropdownScrollManager = { | |
9605 | |
9606 /** | |
9607 * The current element that defines the DOM boundaries of the | |
9608 * scroll lock. This is always the most recently locking element. | |
9609 */ | |
9610 get currentLockingElement() { | |
9611 return this._lockingElements[this._lockingElements.length - 1]; | |
9612 }, | |
9613 | |
9614 /** | |
9615 * Returns true if the provided element is "scroll locked", which is to | |
9616 * say that it cannot be scrolled via pointer or keyboard interactions. | |
9617 * | |
9618 * @param {HTMLElement} element An HTML element instance which may or may | |
9619 * not be scroll locked. | |
9620 */ | |
9621 elementIsScrollLocked: function(element) { | |
9622 var currentLockingElement = this.currentLockingElement; | |
9623 | |
9624 if (currentLockingElement === undefined) | |
9625 return false; | |
9626 | |
9627 var scrollLocked; | |
9628 | |
9629 if (this._hasCachedLockedElement(element)) { | |
9630 return true; | |
9631 } | |
9632 | |
9633 if (this._hasCachedUnlockedElement(element)) { | |
9634 return false; | |
9635 } | |
9636 | |
9637 scrollLocked = !!currentLockingElement && | |
9638 currentLockingElement !== element && | |
9639 !this._composedTreeContains(currentLockingElement, element); | |
9640 | |
9641 if (scrollLocked) { | |
9642 this._lockedElementCache.push(element); | |
9643 } else { | |
9644 this._unlockedElementCache.push(element); | |
9645 } | |
9646 | |
9647 return scrollLocked; | |
9648 }, | |
9649 | |
9650 /** | |
9651 * Push an element onto the current scroll lock stack. The most recently | |
9652 * pushed element and its children will be considered scrollable. All | |
9653 * other elements will not be scrollable. | |
9654 * | |
9655 * Scroll locking is implemented as a stack so that cases such as | |
9656 * dropdowns within dropdowns are handled well. | |
9657 * | |
9658 * @param {HTMLElement} element The element that should lock scroll. | |
9659 */ | |
9660 pushScrollLock: function(element) { | |
9661 // Prevent pushing the same element twice | |
9662 if (this._lockingElements.indexOf(element) >= 0) { | |
9663 return; | |
9664 } | |
9665 | |
9666 if (this._lockingElements.length === 0) { | |
9667 this._lockScrollInteractions(); | |
9668 } | |
9669 | |
9670 this._lockingElements.push(element); | |
9671 | |
9672 this._lockedElementCache = []; | |
9673 this._unlockedElementCache = []; | |
9674 }, | |
9675 | |
9676 /** | |
9677 * Remove an element from the scroll lock stack. The element being | |
9678 * removed does not need to be the most recently pushed element. However, | |
9679 * the scroll lock constraints only change when the most recently pushed | |
9680 * element is removed. | |
9681 * | |
9682 * @param {HTMLElement} element The element to remove from the scroll | |
9683 * lock stack. | |
9684 */ | |
9685 removeScrollLock: function(element) { | |
9686 var index = this._lockingElements.indexOf(element); | |
9687 | |
9688 if (index === -1) { | |
9689 return; | |
9690 } | |
9691 | |
9692 this._lockingElements.splice(index, 1); | |
9693 | |
9694 this._lockedElementCache = []; | |
9695 this._unlockedElementCache = []; | |
9696 | |
9697 if (this._lockingElements.length === 0) { | |
9698 this._unlockScrollInteractions(); | |
9699 } | |
9700 }, | |
9701 | |
9702 _lockingElements: [], | |
9703 | |
9704 _lockedElementCache: null, | |
9705 | |
9706 _unlockedElementCache: null, | |
9707 | |
9708 _hasCachedLockedElement: function(element) { | |
9709 return this._lockedElementCache.indexOf(element) > -1; | |
9710 }, | |
9711 | |
9712 _hasCachedUnlockedElement: function(element) { | |
9713 return this._unlockedElementCache.indexOf(element) > -1; | |
9714 }, | |
9715 | |
9716 _composedTreeContains: function(element, child) { | |
9717 // NOTE(cdata): This method iterates over content elements and their | |
9718 // corresponding distributed nodes to implement a contains-like method | |
9719 // that pierces through the composed tree of the ShadowDOM. Results of | |
9720 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
9721 // guard against potentially expensive lookups happening repeatedly as | |
9722 // a user scrolls / touchmoves. | |
9723 var contentElements; | |
9724 var distributedNodes; | |
9725 var contentIndex; | |
9726 var nodeIndex; | |
9727 | |
9728 if (element.contains(child)) { | |
9729 return true; | |
9730 } | |
9731 | |
9732 contentElements = Polymer.dom(element).querySelectorAll('content'); | |
9733 | |
9734 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | |
9735 | |
9736 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | |
9737 | |
9738 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | |
9739 | |
9740 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | |
9741 return true; | |
9742 } | |
9743 } | |
9744 } | |
9745 | |
9746 return false; | |
9747 }, | |
9748 | |
9749 _scrollInteractionHandler: function(event) { | |
9750 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
9751 // progress and cannot be interrupted. | |
9752 if (event.cancelable && this._shouldPreventScrolling(event)) { | |
9753 event.preventDefault(); | |
9754 } | |
9755 // If event has targetTouches (touch event), update last touch position. | |
9756 if (event.targetTouches) { | |
9757 var touch = event.targetTouches[0]; | |
9758 LAST_TOUCH_POSITION.pageX = touch.pageX; | |
9759 LAST_TOUCH_POSITION.pageY = touch.pageY; | |
9760 } | |
9761 }, | |
9762 | |
9763 _lockScrollInteractions: function() { | |
9764 this._boundScrollHandler = this._boundScrollHandler || | |
9765 this._scrollInteractionHandler.bind(this); | |
9766 // Modern `wheel` event for mouse wheel scrolling: | |
9767 document.addEventListener('wheel', this._boundScrollHandler, true); | |
9768 // Older, non-standard `mousewheel` event for some FF: | |
9769 document.addEventListener('mousewheel', this._boundScrollHandler, true); | |
9770 // IE: | |
9771 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | |
9772 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
9773 document.addEventListener('touchstart', this._boundScrollHandler, true); | |
9774 // Mobile devices can scroll on touch move: | |
9775 document.addEventListener('touchmove', this._boundScrollHandler, true); | |
9776 }, | |
9777 | |
9778 _unlockScrollInteractions: function() { | |
9779 document.removeEventListener('wheel', this._boundScrollHandler, true); | |
9780 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | |
9781 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | |
9782 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | |
9783 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | |
9784 }, | |
9785 | |
9786 /** | |
9787 * Returns true if the event causes scroll outside the current locking | |
9788 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
9789 * outside the locking element when it is already at its scroll boundaries
. | |
9790 * @param {!Event} event | |
9791 * @return {boolean} | |
9792 * @private | |
9793 */ | |
9794 _shouldPreventScrolling: function(event) { | |
9795 | |
9796 // Update if root target changed. For touch events, ensure we don't | |
9797 // update during touchmove. | |
9798 var target = Polymer.dom(event).rootTarget; | |
9799 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | |
9800 ROOT_TARGET = target; | |
9801 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | |
9802 } | |
9803 | |
9804 // Prevent event if no scrollable nodes. | |
9805 if (!SCROLLABLE_NODES.length) { | |
9806 return true; | |
9807 } | |
9808 // Don't prevent touchstart event inside the locking element when it has | |
9809 // scrollable nodes. | |
9810 if (event.type === 'touchstart') { | |
9811 return false; | |
9812 } | |
9813 // Get deltaX/Y. | |
9814 var info = this._getScrollInfo(event); | |
9815 // Prevent if there is no child that can scroll. | |
9816 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | |
9817 }, | |
9818 | |
9819 /** | |
9820 * Returns an array of scrollable nodes up to the current locking element, | |
9821 * which is included too if scrollable. | |
9822 * @param {!Array<Node>} nodes | |
9823 * @return {Array<Node>} scrollables | |
9824 * @private | |
9825 */ | |
9826 _getScrollableNodes: function(nodes) { | |
9827 var scrollables = []; | |
9828 var lockingIndex = nodes.indexOf(this.currentLockingElement); | |
9829 // Loop from root target to locking element (included). | |
9830 for (var i = 0; i <= lockingIndex; i++) { | |
9831 var node = nodes[i]; | |
9832 // Skip document fragments. | |
9833 if (node.nodeType === 11) { | |
9834 continue; | |
9835 } | |
9836 // Check inline style before checking computed style. | |
9837 var style = node.style; | |
9838 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | |
9839 style = window.getComputedStyle(node); | |
9840 } | |
9841 if (style.overflow === 'scroll' || style.overflow === 'auto') { | |
9842 scrollables.push(node); | |
9843 } | |
9844 } | |
9845 return scrollables; | |
9846 }, | |
9847 | |
9848 /** | |
9849 * Returns the node that is scrolling. If there is no scrolling, | |
9850 * returns undefined. | |
9851 * @param {!Array<Node>} nodes | |
9852 * @param {number} deltaX Scroll delta on the x-axis | |
9853 * @param {number} deltaY Scroll delta on the y-axis | |
9854 * @return {Node|undefined} | |
9855 * @private | |
9856 */ | |
9857 _getScrollingNode: function(nodes, deltaX, deltaY) { | |
9858 // No scroll. | |
9859 if (!deltaX && !deltaY) { | |
9860 return; | |
9861 } | |
9862 // Check only one axis according to where there is more scroll. | |
9863 // Prefer vertical to horizontal. | |
9864 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | |
9865 for (var i = 0; i < nodes.length; i++) { | |
9866 var node = nodes[i]; | |
9867 var canScroll = false; | |
9868 if (verticalScroll) { | |
9869 // delta < 0 is scroll up, delta > 0 is scroll down. | |
9870 canScroll = deltaY < 0 ? node.scrollTop > 0 : | |
9871 node.scrollTop < node.scrollHeight - node.clientHeight; | |
9872 } else { | |
9873 // delta < 0 is scroll left, delta > 0 is scroll right. | |
9874 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | |
9875 node.scrollLeft < node.scrollWidth - node.clientWidth; | |
9876 } | |
9877 if (canScroll) { | |
9878 return node; | |
9879 } | |
9880 } | |
9881 }, | |
9882 | |
9883 /** | |
9884 * Returns scroll `deltaX` and `deltaY`. | |
9885 * @param {!Event} event The scroll event | |
9886 * @return {{ | |
9887 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
9888 * negative: scroll left, 0: no scroll), | |
9889 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
9890 * negative: scroll up, 0: no scroll) | |
9891 * }} info | |
9892 * @private | |
9893 */ | |
9894 _getScrollInfo: function(event) { | |
9895 var info = { | |
9896 deltaX: event.deltaX, | |
9897 deltaY: event.deltaY | |
9898 }; | |
9899 // Already available. | |
9900 if ('deltaX' in event) { | |
9901 // do nothing, values are already good. | |
9902 } | |
9903 // Safari has scroll info in `wheelDeltaX/Y`. | |
9904 else if ('wheelDeltaX' in event) { | |
9905 info.deltaX = -event.wheelDeltaX; | |
9906 info.deltaY = -event.wheelDeltaY; | |
9907 } | |
9908 // Firefox has scroll info in `detail` and `axis`. | |
9909 else if ('axis' in event) { | |
9910 info.deltaX = event.axis === 1 ? event.detail : 0; | |
9911 info.deltaY = event.axis === 2 ? event.detail : 0; | |
9912 } | |
9913 // On mobile devices, calculate scroll direction. | |
9914 else if (event.targetTouches) { | |
9915 var touch = event.targetTouches[0]; | |
9916 // Touch moves from right to left => scrolling goes right. | |
9917 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | |
9918 // Touch moves from down to up => scrolling goes down. | |
9919 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | |
9920 } | |
9921 return info; | |
9922 } | |
9923 }; | |
9924 })(); | |
9925 (function() { | |
9926 'use strict'; | |
9927 | |
9928 Polymer({ | |
9929 is: 'iron-dropdown', | |
9930 | |
9931 behaviors: [ | |
9932 Polymer.IronControlState, | |
9933 Polymer.IronA11yKeysBehavior, | |
9934 Polymer.IronOverlayBehavior, | |
9935 Polymer.NeonAnimationRunnerBehavior | |
9936 ], | |
9937 | |
9938 properties: { | |
9939 /** | |
9940 * The orientation against which to align the dropdown content | |
9941 * horizontally relative to the dropdown trigger. | |
9942 * Overridden from `Polymer.IronFitBehavior`. | |
9943 */ | |
9944 horizontalAlign: { | |
9945 type: String, | |
9946 value: 'left', | |
9947 reflectToAttribute: true | |
9948 }, | |
9949 | |
9950 /** | |
9951 * The orientation against which to align the dropdown content | |
9952 * vertically relative to the dropdown trigger. | |
9953 * Overridden from `Polymer.IronFitBehavior`. | |
9954 */ | |
9955 verticalAlign: { | |
9956 type: String, | |
9957 value: 'top', | |
9958 reflectToAttribute: true | |
9959 }, | |
9960 | |
9961 /** | |
9962 * An animation config. If provided, this will be used to animate the | |
9963 * opening of the dropdown. | |
9964 */ | |
9965 openAnimationConfig: { | |
9966 type: Object | |
9967 }, | |
9968 | |
9969 /** | |
9970 * An animation config. If provided, this will be used to animate the | |
9971 * closing of the dropdown. | |
9972 */ | |
9973 closeAnimationConfig: { | |
9974 type: Object | |
9975 }, | |
9976 | |
9977 /** | |
9978 * If provided, this will be the element that will be focused when | |
9979 * the dropdown opens. | |
9980 */ | |
9981 focusTarget: { | |
9982 type: Object | |
9983 }, | |
9984 | |
9985 /** | |
9986 * Set to true to disable animations when opening and closing the | |
9987 * dropdown. | |
9988 */ | |
9989 noAnimations: { | |
9990 type: Boolean, | |
9991 value: false | |
9992 }, | |
9993 | |
9994 /** | |
9995 * By default, the dropdown will constrain scrolling on the page | |
9996 * to itself when opened. | |
9997 * Set to true in order to prevent scroll from being constrained | |
9998 * to the dropdown when it opens. | |
9999 */ | |
10000 allowOutsideScroll: { | |
10001 type: Boolean, | |
10002 value: false | |
10003 }, | |
10004 | |
10005 /** | |
10006 * Callback for scroll events. | |
10007 * @type {Function} | |
10008 * @private | |
10009 */ | |
10010 _boundOnCaptureScroll: { | |
10011 type: Function, | |
10012 value: function() { | |
10013 return this._onCaptureScroll.bind(this); | |
10014 } | |
10015 } | |
10016 }, | |
10017 | |
10018 listeners: { | |
10019 'neon-animation-finish': '_onNeonAnimationFinish' | |
10020 }, | |
10021 | |
10022 observers: [ | |
10023 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | |
10024 ], | |
10025 | |
10026 /** | |
10027 * The element that is contained by the dropdown, if any. | |
10028 */ | |
10029 get containedElement() { | |
10030 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10031 }, | |
10032 | |
10033 /** | |
10034 * The element that should be focused when the dropdown opens. | |
10035 * @deprecated | |
10036 */ | |
10037 get _focusTarget() { | |
10038 return this.focusTarget || this.containedElement; | |
10039 }, | |
10040 | |
10041 ready: function() { | |
10042 // Memoized scrolling position, used to block scrolling outside. | |
10043 this._scrollTop = 0; | |
10044 this._scrollLeft = 0; | |
10045 // Used to perform a non-blocking refit on scroll. | |
10046 this._refitOnScrollRAF = null; | |
10047 }, | |
10048 | |
10049 detached: function() { | |
10050 this.cancelAnimation(); | |
10051 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10052 }, | |
10053 | |
10054 /** | |
10055 * Called when the value of `opened` changes. | |
10056 * Overridden from `IronOverlayBehavior` | |
10057 */ | |
10058 _openedChanged: function() { | |
10059 if (this.opened && this.disabled) { | |
10060 this.cancel(); | |
10061 } else { | |
10062 this.cancelAnimation(); | |
10063 this.sizingTarget = this.containedElement || this.sizingTarget; | |
10064 this._updateAnimationConfig(); | |
10065 this._saveScrollPosition(); | |
10066 if (this.opened) { | |
10067 document.addEventListener('scroll', this._boundOnCaptureScroll); | |
10068 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | |
10069 } else { | |
10070 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | |
10071 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10072 } | |
10073 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | |
10074 } | |
10075 }, | |
10076 | |
10077 /** | |
10078 * Overridden from `IronOverlayBehavior`. | |
10079 */ | |
10080 _renderOpened: function() { | |
10081 if (!this.noAnimations && this.animationConfig.open) { | |
10082 this.$.contentWrapper.classList.add('animating'); | |
10083 this.playAnimation('open'); | |
10084 } else { | |
10085 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | |
10086 } | |
10087 }, | |
10088 | |
10089 /** | |
10090 * Overridden from `IronOverlayBehavior`. | |
10091 */ | |
10092 _renderClosed: function() { | |
10093 | |
10094 if (!this.noAnimations && this.animationConfig.close) { | |
10095 this.$.contentWrapper.classList.add('animating'); | |
10096 this.playAnimation('close'); | |
10097 } else { | |
10098 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | |
10099 } | |
10100 }, | |
10101 | |
10102 /** | |
10103 * Called when animation finishes on the dropdown (when opening or | |
10104 * closing). Responsible for "completing" the process of opening or | |
10105 * closing the dropdown by positioning it or setting its display to | |
10106 * none. | |
10107 */ | |
10108 _onNeonAnimationFinish: function() { | |
10109 this.$.contentWrapper.classList.remove('animating'); | |
10110 if (this.opened) { | |
10111 this._finishRenderOpened(); | |
10112 } else { | |
10113 this._finishRenderClosed(); | |
10114 } | |
10115 }, | |
10116 | |
10117 _onCaptureScroll: function() { | |
10118 if (!this.allowOutsideScroll) { | |
10119 this._restoreScrollPosition(); | |
10120 } else { | |
10121 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | |
10122 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | |
10123 } | |
10124 }, | |
10125 | |
10126 /** | |
10127 * Memoizes the scroll position of the outside scrolling element. | |
10128 * @private | |
10129 */ | |
10130 _saveScrollPosition: function() { | |
10131 if (document.scrollingElement) { | |
10132 this._scrollTop = document.scrollingElement.scrollTop; | |
10133 this._scrollLeft = document.scrollingElement.scrollLeft; | |
10134 } else { | |
10135 // Since we don't know if is the body or html, get max. | |
10136 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | |
10137 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | |
10138 } | |
10139 }, | |
10140 | |
10141 /** | |
10142 * Resets the scroll position of the outside scrolling element. | |
10143 * @private | |
10144 */ | |
10145 _restoreScrollPosition: function() { | |
10146 if (document.scrollingElement) { | |
10147 document.scrollingElement.scrollTop = this._scrollTop; | |
10148 document.scrollingElement.scrollLeft = this._scrollLeft; | |
10149 } else { | |
10150 // Since we don't know if is the body or html, set both. | |
10151 document.documentElement.scrollTop = this._scrollTop; | |
10152 document.documentElement.scrollLeft = this._scrollLeft; | |
10153 document.body.scrollTop = this._scrollTop; | |
10154 document.body.scrollLeft = this._scrollLeft; | |
10155 } | |
10156 }, | |
10157 | |
10158 /** | |
10159 * Constructs the final animation config from different properties used | |
10160 * to configure specific parts of the opening and closing animations. | |
10161 */ | |
10162 _updateAnimationConfig: function() { | |
10163 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | |
10164 for (var i = 0; i < animations.length; i++) { | |
10165 animations[i].node = this.containedElement; | |
10166 } | |
10167 this.animationConfig = { | |
10168 open: this.openAnimationConfig, | |
10169 close: this.closeAnimationConfig | |
10170 }; | |
10171 }, | |
10172 | |
10173 /** | |
10174 * Updates the overlay position based on configured horizontal | |
10175 * and vertical alignment. | |
10176 */ | |
10177 _updateOverlayPosition: function() { | |
10178 if (this.isAttached) { | |
10179 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
10180 this.notifyResize(); | |
10181 } | |
10182 }, | |
10183 | |
10184 /** | |
10185 * Apply focus to focusTarget or containedElement | |
10186 */ | |
10187 _applyFocus: function () { | |
10188 var focusTarget = this.focusTarget || this.containedElement; | |
10189 if (focusTarget && this.opened && !this.noAutoFocus) { | |
10190 focusTarget.focus(); | |
10191 } else { | |
10192 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | |
10193 } | |
10194 } | |
10195 }); | |
10196 })(); | |
10197 Polymer({ | |
10198 | |
10199 is: 'fade-in-animation', | |
10200 | |
10201 behaviors: [ | |
10202 Polymer.NeonAnimationBehavior | |
10203 ], | |
10204 | |
10205 configure: function(config) { | |
10206 var node = config.node; | |
10207 this._effect = new KeyframeEffect(node, [ | |
10208 {'opacity': '0'}, | |
10209 {'opacity': '1'} | |
10210 ], this.timingFromConfig(config)); | |
10211 return this._effect; | |
10212 } | |
10213 | |
10214 }); | |
10215 Polymer({ | |
10216 | |
10217 is: 'fade-out-animation', | |
10218 | |
10219 behaviors: [ | |
10220 Polymer.NeonAnimationBehavior | |
10221 ], | |
10222 | |
10223 configure: function(config) { | |
10224 var node = config.node; | |
10225 this._effect = new KeyframeEffect(node, [ | |
10226 {'opacity': '1'}, | |
10227 {'opacity': '0'} | |
10228 ], this.timingFromConfig(config)); | |
10229 return this._effect; | |
10230 } | |
10231 | |
10232 }); | |
10233 Polymer({ | |
10234 is: 'paper-menu-grow-height-animation', | |
10235 | |
10236 behaviors: [ | |
10237 Polymer.NeonAnimationBehavior | |
10238 ], | |
10239 | |
10240 configure: function(config) { | |
10241 var node = config.node; | |
10242 var rect = node.getBoundingClientRect(); | |
10243 var height = rect.height; | |
10244 | |
10245 this._effect = new KeyframeEffect(node, [{ | |
10246 height: (height / 2) + 'px' | |
10247 }, { | |
10248 height: height + 'px' | |
10249 }], this.timingFromConfig(config)); | |
10250 | |
10251 return this._effect; | |
10252 } | |
10253 }); | |
10254 | |
10255 Polymer({ | |
10256 is: 'paper-menu-grow-width-animation', | |
10257 | |
10258 behaviors: [ | |
10259 Polymer.NeonAnimationBehavior | |
10260 ], | |
10261 | |
10262 configure: function(config) { | |
10263 var node = config.node; | |
10264 var rect = node.getBoundingClientRect(); | |
10265 var width = rect.width; | |
10266 | |
10267 this._effect = new KeyframeEffect(node, [{ | |
10268 width: (width / 2) + 'px' | |
10269 }, { | |
10270 width: width + 'px' | |
10271 }], this.timingFromConfig(config)); | |
10272 | |
10273 return this._effect; | |
10274 } | |
10275 }); | |
10276 | |
10277 Polymer({ | |
10278 is: 'paper-menu-shrink-width-animation', | |
10279 | |
10280 behaviors: [ | |
10281 Polymer.NeonAnimationBehavior | |
10282 ], | |
10283 | |
10284 configure: function(config) { | |
10285 var node = config.node; | |
10286 var rect = node.getBoundingClientRect(); | |
10287 var width = rect.width; | |
10288 | |
10289 this._effect = new KeyframeEffect(node, [{ | |
10290 width: width + 'px' | |
10291 }, { | |
10292 width: width - (width / 20) + 'px' | |
10293 }], this.timingFromConfig(config)); | |
10294 | |
10295 return this._effect; | |
10296 } | |
10297 }); | |
10298 | |
10299 Polymer({ | |
10300 is: 'paper-menu-shrink-height-animation', | |
10301 | |
10302 behaviors: [ | |
10303 Polymer.NeonAnimationBehavior | |
10304 ], | |
10305 | |
10306 configure: function(config) { | |
10307 var node = config.node; | |
10308 var rect = node.getBoundingClientRect(); | |
10309 var height = rect.height; | |
10310 var top = rect.top; | |
10311 | |
10312 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); | |
10313 | |
10314 this._effect = new KeyframeEffect(node, [{ | |
10315 height: height + 'px', | |
10316 transform: 'translateY(0)' | |
10317 }, { | |
10318 height: height / 2 + 'px', | |
10319 transform: 'translateY(-20px)' | |
10320 }], this.timingFromConfig(config)); | |
10321 | |
10322 return this._effect; | |
10323 } | |
10324 }); | |
10325 (function() { | |
10326 'use strict'; | |
10327 | |
10328 var config = { | |
10329 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | |
10330 MAX_ANIMATION_TIME_MS: 400 | |
10331 }; | |
10332 | |
10333 var PaperMenuButton = Polymer({ | |
10334 is: 'paper-menu-button', | |
10335 | |
10336 /** | |
10337 * Fired when the dropdown opens. | |
10338 * | |
10339 * @event paper-dropdown-open | |
10340 */ | |
10341 | |
10342 /** | |
10343 * Fired when the dropdown closes. | |
10344 * | |
10345 * @event paper-dropdown-close | |
10346 */ | |
10347 | |
10348 behaviors: [ | |
10349 Polymer.IronA11yKeysBehavior, | |
10350 Polymer.IronControlState | |
10351 ], | |
10352 | |
10353 properties: { | |
10354 /** | |
10355 * True if the content is currently displayed. | |
10356 */ | |
10357 opened: { | |
10358 type: Boolean, | |
10359 value: false, | |
10360 notify: true, | |
10361 observer: '_openedChanged' | |
10362 }, | |
10363 | |
10364 /** | |
10365 * The orientation against which to align the menu dropdown | |
10366 * horizontally relative to the dropdown trigger. | |
10367 */ | |
10368 horizontalAlign: { | |
10369 type: String, | |
10370 value: 'left', | |
10371 reflectToAttribute: true | |
10372 }, | |
10373 | |
10374 /** | |
10375 * The orientation against which to align the menu dropdown | |
10376 * vertically relative to the dropdown trigger. | |
10377 */ | |
10378 verticalAlign: { | |
10379 type: String, | |
10380 value: 'top', | |
10381 reflectToAttribute: true | |
10382 }, | |
10383 | |
10384 /** | |
10385 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
10386 * be considered preferences instead of strict requirements when | |
10387 * positioning the dropdown and may be changed if doing so reduces | |
10388 * the area of the dropdown falling outside of `fitInto`. | |
10389 */ | |
10390 dynamicAlign: { | |
10391 type: Boolean | |
10392 }, | |
10393 | |
10394 /** | |
10395 * A pixel value that will be added to the position calculated for the | |
10396 * given `horizontalAlign`. Use a negative value to offset to the | |
10397 * left, or a positive value to offset to the right. | |
10398 */ | |
10399 horizontalOffset: { | |
10400 type: Number, | |
10401 value: 0, | |
10402 notify: true | |
10403 }, | |
10404 | |
10405 /** | |
10406 * A pixel value that will be added to the position calculated for the | |
10407 * given `verticalAlign`. Use a negative value to offset towards the | |
10408 * top, or a positive value to offset towards the bottom. | |
10409 */ | |
10410 verticalOffset: { | |
10411 type: Number, | |
10412 value: 0, | |
10413 notify: true | |
10414 }, | |
10415 | |
10416 /** | |
10417 * If true, the dropdown will be positioned so that it doesn't overlap | |
10418 * the button. | |
10419 */ | |
10420 noOverlap: { | |
10421 type: Boolean | |
10422 }, | |
10423 | |
10424 /** | |
10425 * Set to true to disable animations when opening and closing the | |
10426 * dropdown. | |
10427 */ | |
10428 noAnimations: { | |
10429 type: Boolean, | |
10430 value: false | |
10431 }, | |
10432 | |
10433 /** | |
10434 * Set to true to disable automatically closing the dropdown after | |
10435 * a selection has been made. | |
10436 */ | |
10437 ignoreSelect: { | |
10438 type: Boolean, | |
10439 value: false | |
10440 }, | |
10441 | |
10442 /** | |
10443 * Set to true to enable automatically closing the dropdown after an | |
10444 * item has been activated, even if the selection did not change. | |
10445 */ | |
10446 closeOnActivate: { | |
10447 type: Boolean, | |
10448 value: false | |
10449 }, | |
10450 | |
10451 /** | |
10452 * An animation config. If provided, this will be used to animate the | |
10453 * opening of the dropdown. | |
10454 */ | |
10455 openAnimationConfig: { | |
10456 type: Object, | |
10457 value: function() { | |
10458 return [{ | |
10459 name: 'fade-in-animation', | |
10460 timing: { | |
10461 delay: 100, | |
10462 duration: 200 | |
10463 } | |
10464 }, { | |
10465 name: 'paper-menu-grow-width-animation', | |
10466 timing: { | |
10467 delay: 100, | |
10468 duration: 150, | |
10469 easing: config.ANIMATION_CUBIC_BEZIER | |
10470 } | |
10471 }, { | |
10472 name: 'paper-menu-grow-height-animation', | |
10473 timing: { | |
10474 delay: 100, | |
10475 duration: 275, | |
10476 easing: config.ANIMATION_CUBIC_BEZIER | |
10477 } | |
10478 }]; | |
10479 } | |
10480 }, | |
10481 | |
10482 /** | |
10483 * An animation config. If provided, this will be used to animate the | |
10484 * closing of the dropdown. | |
10485 */ | |
10486 closeAnimationConfig: { | |
10487 type: Object, | |
10488 value: function() { | |
10489 return [{ | |
10490 name: 'fade-out-animation', | |
10491 timing: { | |
10492 duration: 150 | |
10493 } | |
10494 }, { | |
10495 name: 'paper-menu-shrink-width-animation', | |
10496 timing: { | |
10497 delay: 100, | |
10498 duration: 50, | |
10499 easing: config.ANIMATION_CUBIC_BEZIER | |
10500 } | |
10501 }, { | |
10502 name: 'paper-menu-shrink-height-animation', | |
10503 timing: { | |
10504 duration: 200, | |
10505 easing: 'ease-in' | |
10506 } | |
10507 }]; | |
10508 } | |
10509 }, | |
10510 | |
10511 /** | |
10512 * By default, the dropdown will constrain scrolling on the page | |
10513 * to itself when opened. | |
10514 * Set to true in order to prevent scroll from being constrained | |
10515 * to the dropdown when it opens. | |
10516 */ | |
10517 allowOutsideScroll: { | |
10518 type: Boolean, | |
10519 value: false | |
10520 }, | |
10521 | |
10522 /** | |
10523 * Whether focus should be restored to the button when the menu closes
. | |
10524 */ | |
10525 restoreFocusOnClose: { | |
10526 type: Boolean, | |
10527 value: true | |
10528 }, | |
10529 | |
10530 /** | |
10531 * This is the element intended to be bound as the focus target | |
10532 * for the `iron-dropdown` contained by `paper-menu-button`. | |
10533 */ | |
10534 _dropdownContent: { | |
10535 type: Object | |
10536 } | |
10537 }, | |
10538 | |
10539 hostAttributes: { | |
10540 role: 'group', | |
10541 'aria-haspopup': 'true' | |
10542 }, | |
10543 | |
10544 listeners: { | |
10545 'iron-activate': '_onIronActivate', | |
10546 'iron-select': '_onIronSelect' | |
10547 }, | |
10548 | |
10549 /** | |
10550 * The content element that is contained by the menu button, if any. | |
10551 */ | |
10552 get contentElement() { | |
10553 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10554 }, | |
10555 | |
10556 /** | |
10557 * Toggles the drowpdown content between opened and closed. | |
10558 */ | |
10559 toggle: function() { | |
10560 if (this.opened) { | |
10561 this.close(); | |
10562 } else { | |
10563 this.open(); | |
10564 } | |
10565 }, | |
10566 | |
10567 /** | |
10568 * Make the dropdown content appear as an overlay positioned relative | |
10569 * to the dropdown trigger. | |
10570 */ | |
10571 open: function() { | |
10572 if (this.disabled) { | |
10573 return; | |
10574 } | |
10575 | |
10576 this.$.dropdown.open(); | |
10577 }, | |
10578 | |
10579 /** | |
10580 * Hide the dropdown content. | |
10581 */ | |
10582 close: function() { | |
10583 this.$.dropdown.close(); | |
10584 }, | |
10585 | |
10586 /** | |
10587 * When an `iron-select` event is received, the dropdown should | |
10588 * automatically close on the assumption that a value has been chosen. | |
10589 * | |
10590 * @param {CustomEvent} event A CustomEvent instance with type | |
10591 * set to `"iron-select"`. | |
10592 */ | |
10593 _onIronSelect: function(event) { | |
10594 if (!this.ignoreSelect) { | |
10595 this.close(); | |
10596 } | |
10597 }, | |
10598 | |
10599 /** | |
10600 * Closes the dropdown when an `iron-activate` event is received if | |
10601 * `closeOnActivate` is true. | |
10602 * | |
10603 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | |
10604 */ | |
10605 _onIronActivate: function(event) { | |
10606 if (this.closeOnActivate) { | |
10607 this.close(); | |
10608 } | |
10609 }, | |
10610 | |
10611 /** | |
10612 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | |
10613 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | |
10614 * | |
10615 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | |
10616 * @param {boolean} oldOpened The previous value of `opened`. | |
10617 */ | |
10618 _openedChanged: function(opened, oldOpened) { | |
10619 if (opened) { | |
10620 // TODO(cdata): Update this when we can measure changes in distribut
ed | |
10621 // children in an idiomatic way. | |
10622 // We poke this property in case the element has changed. This will | |
10623 // cause the focus target for the `iron-dropdown` to be updated as | |
10624 // necessary: | |
10625 this._dropdownContent = this.contentElement; | |
10626 this.fire('paper-dropdown-open'); | |
10627 } else if (oldOpened != null) { | |
10628 this.fire('paper-dropdown-close'); | |
10629 } | |
10630 }, | |
10631 | |
10632 /** | |
10633 * If the dropdown is open when disabled becomes true, close the | |
10634 * dropdown. | |
10635 * | |
10636 * @param {boolean} disabled True if disabled, otherwise false. | |
10637 */ | |
10638 _disabledChanged: function(disabled) { | |
10639 Polymer.IronControlState._disabledChanged.apply(this, arguments); | |
10640 if (disabled && this.opened) { | |
10641 this.close(); | |
10642 } | |
10643 }, | |
10644 | |
10645 __onIronOverlayCanceled: function(event) { | |
10646 var uiEvent = event.detail; | |
10647 var target = Polymer.dom(uiEvent).rootTarget; | |
10648 var trigger = this.$.trigger; | |
10649 var path = Polymer.dom(uiEvent).path; | |
10650 | |
10651 if (path.indexOf(trigger) > -1) { | |
10652 event.preventDefault(); | |
10653 } | |
10654 } | |
10655 }); | |
10656 | |
10657 Object.keys(config).forEach(function (key) { | |
10658 PaperMenuButton[key] = config[key]; | |
10659 }); | |
10660 | |
10661 Polymer.PaperMenuButton = PaperMenuButton; | |
10662 })(); | |
10663 /** | |
10664 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | |
10665 * | |
10666 * @polymerBehavior Polymer.PaperInkyFocusBehavior | |
10667 */ | |
10668 Polymer.PaperInkyFocusBehaviorImpl = { | |
10669 observers: [ | |
10670 '_focusedChanged(receivedFocusFromKeyboard)' | |
10671 ], | |
10672 | |
10673 _focusedChanged: function(receivedFocusFromKeyboard) { | |
10674 if (receivedFocusFromKeyboard) { | |
10675 this.ensureRipple(); | |
10676 } | |
10677 if (this.hasRipple()) { | |
10678 this._ripple.holdDown = receivedFocusFromKeyboard; | |
10679 } | |
10680 }, | |
10681 | |
10682 _createRipple: function() { | |
10683 var ripple = Polymer.PaperRippleBehavior._createRipple(); | |
10684 ripple.id = 'ink'; | |
10685 ripple.setAttribute('center', ''); | |
10686 ripple.classList.add('circle'); | |
10687 return ripple; | |
10688 } | |
10689 }; | |
10690 | |
10691 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | |
10692 Polymer.PaperInkyFocusBehavior = [ | |
10693 Polymer.IronButtonState, | |
10694 Polymer.IronControlState, | |
10695 Polymer.PaperRippleBehavior, | |
10696 Polymer.PaperInkyFocusBehaviorImpl | |
10697 ]; | |
10698 Polymer({ | |
10699 is: 'paper-icon-button', | |
10700 | |
10701 hostAttributes: { | |
10702 role: 'button', | |
10703 tabindex: '0' | |
10704 }, | |
10705 | |
10706 behaviors: [ | |
10707 Polymer.PaperInkyFocusBehavior | |
10708 ], | |
10709 | |
10710 properties: { | |
10711 /** | |
10712 * The URL of an image for the icon. If the src property is specified, | |
10713 * the icon property should not be. | |
10714 */ | |
10715 src: { | |
10716 type: String | |
10717 }, | |
10718 | |
10719 /** | |
10720 * Specifies the icon name or index in the set of icons available in | |
10721 * the icon's icon set. If the icon property is specified, | |
10722 * the src property should not be. | |
10723 */ | |
10724 icon: { | |
10725 type: String | |
10726 }, | |
10727 | |
10728 /** | |
10729 * Specifies the alternate text for the button, for accessibility. | |
10730 */ | |
10731 alt: { | |
10732 type: String, | |
10733 observer: "_altChanged" | |
10734 } | |
10735 }, | |
10736 | |
10737 _altChanged: function(newValue, oldValue) { | |
10738 var label = this.getAttribute('aria-label'); | |
10739 | |
10740 // Don't stomp over a user-set aria-label. | |
10741 if (!label || oldValue == label) { | |
10742 this.setAttribute('aria-label', newValue); | |
10743 } | |
10744 } | |
10745 }); | |
10746 // Copyright 2016 The Chromium Authors. All rights reserved. | 6030 // Copyright 2016 The Chromium Authors. All rights reserved. |
10747 // Use of this source code is governed by a BSD-style license that can be | 6031 // Use of this source code is governed by a BSD-style license that can be |
10748 // found in the LICENSE file. | 6032 // found in the LICENSE file. |
10749 | |
10750 /** | |
10751 * Implements an incremental search field which can be shown and hidden. | |
10752 * Canonical implementation is <cr-search-field>. | |
10753 * @polymerBehavior | |
10754 */ | |
10755 var CrSearchFieldBehavior = { | 6033 var CrSearchFieldBehavior = { |
10756 properties: { | 6034 properties: { |
10757 label: { | 6035 label: { |
10758 type: String, | 6036 type: String, |
10759 value: '', | 6037 value: '' |
10760 }, | 6038 }, |
10761 | |
10762 clearLabel: { | 6039 clearLabel: { |
10763 type: String, | 6040 type: String, |
10764 value: '', | 6041 value: '' |
10765 }, | 6042 }, |
10766 | |
10767 showingSearch: { | 6043 showingSearch: { |
10768 type: Boolean, | 6044 type: Boolean, |
10769 value: false, | 6045 value: false, |
10770 notify: true, | 6046 notify: true, |
10771 observer: 'showingSearchChanged_', | 6047 observer: 'showingSearchChanged_', |
10772 reflectToAttribute: true | 6048 reflectToAttribute: true |
10773 }, | 6049 }, |
10774 | |
10775 /** @private */ | |
10776 lastValue_: { | 6050 lastValue_: { |
10777 type: String, | 6051 type: String, |
10778 value: '', | 6052 value: '' |
10779 }, | 6053 } |
10780 }, | 6054 }, |
10781 | |
10782 /** | |
10783 * @abstract | |
10784 * @return {!HTMLInputElement} The input field element the behavior should | |
10785 * use. | |
10786 */ | |
10787 getSearchInput: function() {}, | 6055 getSearchInput: function() {}, |
10788 | |
10789 /** | |
10790 * @return {string} The value of the search field. | |
10791 */ | |
10792 getValue: function() { | 6056 getValue: function() { |
10793 return this.getSearchInput().value; | 6057 return this.getSearchInput().value; |
10794 }, | 6058 }, |
10795 | |
10796 /** | |
10797 * Sets the value of the search field. | |
10798 * @param {string} value | |
10799 */ | |
10800 setValue: function(value) { | 6059 setValue: function(value) { |
10801 // Use bindValue when setting the input value so that changes propagate | |
10802 // correctly. | |
10803 this.getSearchInput().bindValue = value; | 6060 this.getSearchInput().bindValue = value; |
10804 this.onValueChanged_(value); | 6061 this.onValueChanged_(value); |
10805 }, | 6062 }, |
10806 | |
10807 showAndFocus: function() { | 6063 showAndFocus: function() { |
10808 this.showingSearch = true; | 6064 this.showingSearch = true; |
10809 this.focus_(); | 6065 this.focus_(); |
10810 }, | 6066 }, |
10811 | |
10812 /** @private */ | |
10813 focus_: function() { | 6067 focus_: function() { |
10814 this.getSearchInput().focus(); | 6068 this.getSearchInput().focus(); |
10815 }, | 6069 }, |
10816 | |
10817 onSearchTermSearch: function() { | 6070 onSearchTermSearch: function() { |
10818 this.onValueChanged_(this.getValue()); | 6071 this.onValueChanged_(this.getValue()); |
10819 }, | 6072 }, |
10820 | |
10821 /** | |
10822 * Updates the internal state of the search field based on a change that has | |
10823 * already happened. | |
10824 * @param {string} newValue | |
10825 * @private | |
10826 */ | |
10827 onValueChanged_: function(newValue) { | 6073 onValueChanged_: function(newValue) { |
10828 if (newValue == this.lastValue_) | 6074 if (newValue == this.lastValue_) return; |
10829 return; | |
10830 | |
10831 this.fire('search-changed', newValue); | 6075 this.fire('search-changed', newValue); |
10832 this.lastValue_ = newValue; | 6076 this.lastValue_ = newValue; |
10833 }, | 6077 }, |
10834 | |
10835 onSearchTermKeydown: function(e) { | 6078 onSearchTermKeydown: function(e) { |
10836 if (e.key == 'Escape') | 6079 if (e.key == 'Escape') this.showingSearch = false; |
10837 this.showingSearch = false; | 6080 }, |
10838 }, | |
10839 | |
10840 /** @private */ | |
10841 showingSearchChanged_: function() { | 6081 showingSearchChanged_: function() { |
10842 if (this.showingSearch) { | 6082 if (this.showingSearch) { |
10843 this.focus_(); | 6083 this.focus_(); |
10844 return; | 6084 return; |
10845 } | 6085 } |
10846 | |
10847 this.setValue(''); | 6086 this.setValue(''); |
10848 this.getSearchInput().blur(); | 6087 this.getSearchInput().blur(); |
10849 } | 6088 } |
10850 }; | 6089 }; |
| 6090 |
10851 (function() { | 6091 (function() { |
10852 'use strict'; | 6092 'use strict'; |
10853 | 6093 Polymer.IronA11yAnnouncer = Polymer({ |
10854 Polymer.IronA11yAnnouncer = Polymer({ | 6094 is: 'iron-a11y-announcer', |
10855 is: 'iron-a11y-announcer', | |
10856 | |
10857 properties: { | |
10858 | |
10859 /** | |
10860 * The value of mode is used to set the `aria-live` attribute | |
10861 * for the element that will be announced. Valid values are: `off`, | |
10862 * `polite` and `assertive`. | |
10863 */ | |
10864 mode: { | |
10865 type: String, | |
10866 value: 'polite' | |
10867 }, | |
10868 | |
10869 _text: { | |
10870 type: String, | |
10871 value: '' | |
10872 } | |
10873 }, | |
10874 | |
10875 created: function() { | |
10876 if (!Polymer.IronA11yAnnouncer.instance) { | |
10877 Polymer.IronA11yAnnouncer.instance = this; | |
10878 } | |
10879 | |
10880 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
10881 }, | |
10882 | |
10883 /** | |
10884 * Cause a text string to be announced by screen readers. | |
10885 * | |
10886 * @param {string} text The text that should be announced. | |
10887 */ | |
10888 announce: function(text) { | |
10889 this._text = ''; | |
10890 this.async(function() { | |
10891 this._text = text; | |
10892 }, 100); | |
10893 }, | |
10894 | |
10895 _onIronAnnounce: function(event) { | |
10896 if (event.detail && event.detail.text) { | |
10897 this.announce(event.detail.text); | |
10898 } | |
10899 } | |
10900 }); | |
10901 | |
10902 Polymer.IronA11yAnnouncer.instance = null; | |
10903 | |
10904 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
10905 if (!Polymer.IronA11yAnnouncer.instance) { | |
10906 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
10907 } | |
10908 | |
10909 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
10910 }; | |
10911 })(); | |
10912 /** | |
10913 * Singleton IronMeta instance. | |
10914 */ | |
10915 Polymer.IronValidatableBehaviorMeta = null; | |
10916 | |
10917 /** | |
10918 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
10919 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
10920 * | |
10921 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
10922 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
10923 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
10924 * custom element will get a public `validate()`, which | |
10925 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
10926 * which can be used for styling. | |
10927 * | |
10928 * To implement the custom validation logic of your element, you must override | |
10929 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
10930 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
10931 * for an example. | |
10932 * | |
10933 * ### Accessibility | |
10934 * | |
10935 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
10936 * `aria-invalid` attribute. | |
10937 * | |
10938 * @demo demo/index.html | |
10939 * @polymerBehavior | |
10940 */ | |
10941 Polymer.IronValidatableBehavior = { | |
10942 | |
10943 properties: { | 6095 properties: { |
10944 | 6096 mode: { |
10945 /** | 6097 type: String, |
10946 * Name of the validator to use. | 6098 value: 'polite' |
10947 */ | |
10948 validator: { | |
10949 type: String | |
10950 }, | 6099 }, |
10951 | 6100 _text: { |
10952 /** | |
10953 * True if the last call to `validate` is invalid. | |
10954 */ | |
10955 invalid: { | |
10956 notify: true, | |
10957 reflectToAttribute: true, | |
10958 type: Boolean, | |
10959 value: false | |
10960 }, | |
10961 | |
10962 /** | |
10963 * This property is deprecated and should not be used. Use the global | |
10964 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
10965 */ | |
10966 _validatorMeta: { | |
10967 type: Object | |
10968 }, | |
10969 | |
10970 /** | |
10971 * Namespace for this validator. This property is deprecated and should | |
10972 * not be used. For all intents and purposes, please consider it a | |
10973 * read-only, config-time property. | |
10974 */ | |
10975 validatorType: { | |
10976 type: String, | |
10977 value: 'validator' | |
10978 }, | |
10979 | |
10980 _validator: { | |
10981 type: Object, | |
10982 computed: '__computeValidator(validator)' | |
10983 } | |
10984 }, | |
10985 | |
10986 observers: [ | |
10987 '_invalidChanged(invalid)' | |
10988 ], | |
10989 | |
10990 registered: function() { | |
10991 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
10992 }, | |
10993 | |
10994 _invalidChanged: function() { | |
10995 if (this.invalid) { | |
10996 this.setAttribute('aria-invalid', 'true'); | |
10997 } else { | |
10998 this.removeAttribute('aria-invalid'); | |
10999 } | |
11000 }, | |
11001 | |
11002 /** | |
11003 * @return {boolean} True if the validator `validator` exists. | |
11004 */ | |
11005 hasValidator: function() { | |
11006 return this._validator != null; | |
11007 }, | |
11008 | |
11009 /** | |
11010 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
11011 * your element to have custom validation logic, do not override this method
; | |
11012 * override `_getValidity(value)` instead. | |
11013 | |
11014 * @param {Object} value The value to be validated. By default, it is passed | |
11015 * to the validator's `validate()` function, if a validator is set. | |
11016 * @return {boolean} True if `value` is valid. | |
11017 */ | |
11018 validate: function(value) { | |
11019 this.invalid = !this._getValidity(value); | |
11020 return !this.invalid; | |
11021 }, | |
11022 | |
11023 /** | |
11024 * Returns true if `value` is valid. By default, it is passed | |
11025 * to the validator's `validate()` function, if a validator is set. You | |
11026 * should override this method if you want to implement custom validity | |
11027 * logic for your element. | |
11028 * | |
11029 * @param {Object} value The value to be validated. | |
11030 * @return {boolean} True if `value` is valid. | |
11031 */ | |
11032 | |
11033 _getValidity: function(value) { | |
11034 if (this.hasValidator()) { | |
11035 return this._validator.validate(value); | |
11036 } | |
11037 return true; | |
11038 }, | |
11039 | |
11040 __computeValidator: function() { | |
11041 return Polymer.IronValidatableBehaviorMeta && | |
11042 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
11043 } | |
11044 }; | |
11045 /* | |
11046 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
11047 to `<input>`. | |
11048 | |
11049 ### Two-way binding | |
11050 | |
11051 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
11052 | |
11053 <input value="{{myValue::input}}"> | |
11054 | |
11055 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
11056 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
11057 | |
11058 <input is="iron-input" bind-value="{{myValue}}"> | |
11059 | |
11060 ### Custom validators | |
11061 | |
11062 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
11063 | |
11064 <input is="iron-input" validator="my-custom-validator"> | |
11065 | |
11066 ### Stopping invalid input | |
11067 | |
11068 It may be desirable to only allow users to enter certain characters. You can use
the | |
11069 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
11070 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
11071 | |
11072 \x3c!-- only allow characters that match [0-9] --\x3e | |
11073 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
11074 | |
11075 @hero hero.svg | |
11076 @demo demo/index.html | |
11077 */ | |
11078 | |
11079 Polymer({ | |
11080 | |
11081 is: 'iron-input', | |
11082 | |
11083 extends: 'input', | |
11084 | |
11085 behaviors: [ | |
11086 Polymer.IronValidatableBehavior | |
11087 ], | |
11088 | |
11089 properties: { | |
11090 | |
11091 /** | |
11092 * Use this property instead of `value` for two-way data binding. | |
11093 */ | |
11094 bindValue: { | |
11095 observer: '_bindValueChanged', | |
11096 type: String | |
11097 }, | |
11098 | |
11099 /** | |
11100 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
11101 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
11102 * Pasted input will have each character checked individually; if any char
acter | |
11103 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
11104 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
11105 */ | |
11106 preventInvalidInput: { | |
11107 type: Boolean | |
11108 }, | |
11109 | |
11110 /** | |
11111 * Regular expression that list the characters allowed as input. | |
11112 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
11113 * each individual character will be checked against the pattern (rather t
han checking | |
11114 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
11115 * for example, `[a-zA-Z0-9.+-!;:]` | |
11116 */ | |
11117 allowedPattern: { | |
11118 type: String, | |
11119 observer: "_allowedPatternChanged" | |
11120 }, | |
11121 | |
11122 _previousValidInput: { | |
11123 type: String, | 6101 type: String, |
11124 value: '' | 6102 value: '' |
11125 }, | 6103 } |
11126 | 6104 }, |
11127 _patternAlreadyChecked: { | |
11128 type: Boolean, | |
11129 value: false | |
11130 } | |
11131 | |
11132 }, | |
11133 | |
11134 listeners: { | |
11135 'input': '_onInput', | |
11136 'keypress': '_onKeypress' | |
11137 }, | |
11138 | |
11139 /** @suppress {checkTypes} */ | |
11140 registered: function() { | |
11141 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
11142 if (!this._canDispatchEventOnDisabled()) { | |
11143 this._origDispatchEvent = this.dispatchEvent; | |
11144 this.dispatchEvent = this._dispatchEventFirefoxIE; | |
11145 } | |
11146 }, | |
11147 | |
11148 created: function() { | 6105 created: function() { |
11149 Polymer.IronA11yAnnouncer.requestAvailability(); | 6106 if (!Polymer.IronA11yAnnouncer.instance) { |
11150 }, | 6107 Polymer.IronA11yAnnouncer.instance = this; |
11151 | 6108 } |
11152 _canDispatchEventOnDisabled: function() { | 6109 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(
this)); |
11153 var input = document.createElement('input'); | 6110 }, |
11154 var canDispatch = false; | 6111 announce: function(text) { |
11155 input.disabled = true; | 6112 this._text = ''; |
11156 | 6113 this.async(function() { |
11157 input.addEventListener('feature-check-dispatch-event', function() { | 6114 this._text = text; |
11158 canDispatch = true; | 6115 }, 100); |
11159 }); | 6116 }, |
11160 | 6117 _onIronAnnounce: function(event) { |
11161 try { | 6118 if (event.detail && event.detail.text) { |
11162 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 6119 this.announce(event.detail.text); |
11163 } catch(e) {} | 6120 } |
11164 | 6121 } |
11165 return canDispatch; | 6122 }); |
11166 }, | 6123 Polymer.IronA11yAnnouncer.instance = null; |
11167 | 6124 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
11168 _dispatchEventFirefoxIE: function() { | 6125 if (!Polymer.IronA11yAnnouncer.instance) { |
11169 // Due to Firefox bug, events fired on disabled form controls can throw | 6126 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann
ouncer'); |
11170 // errors; furthermore, neither IE nor Firefox will actually dispatch | 6127 } |
11171 // events from disabled form controls; as such, we toggle disable around | 6128 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
11172 // the dispatch to allow notifying properties to notify | 6129 }; |
11173 // See issue #47 for details | 6130 })(); |
11174 var disabled = this.disabled; | 6131 |
11175 this.disabled = false; | 6132 Polymer.IronValidatableBehaviorMeta = null; |
11176 this._origDispatchEvent.apply(this, arguments); | 6133 |
11177 this.disabled = disabled; | 6134 Polymer.IronValidatableBehavior = { |
11178 }, | 6135 properties: { |
11179 | 6136 validator: { |
11180 get _patternRegExp() { | 6137 type: String |
11181 var pattern; | 6138 }, |
11182 if (this.allowedPattern) { | 6139 invalid: { |
11183 pattern = new RegExp(this.allowedPattern); | 6140 notify: true, |
| 6141 reflectToAttribute: true, |
| 6142 type: Boolean, |
| 6143 value: false |
| 6144 }, |
| 6145 _validatorMeta: { |
| 6146 type: Object |
| 6147 }, |
| 6148 validatorType: { |
| 6149 type: String, |
| 6150 value: 'validator' |
| 6151 }, |
| 6152 _validator: { |
| 6153 type: Object, |
| 6154 computed: '__computeValidator(validator)' |
| 6155 } |
| 6156 }, |
| 6157 observers: [ '_invalidChanged(invalid)' ], |
| 6158 registered: function() { |
| 6159 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({ |
| 6160 type: 'validator' |
| 6161 }); |
| 6162 }, |
| 6163 _invalidChanged: function() { |
| 6164 if (this.invalid) { |
| 6165 this.setAttribute('aria-invalid', 'true'); |
| 6166 } else { |
| 6167 this.removeAttribute('aria-invalid'); |
| 6168 } |
| 6169 }, |
| 6170 hasValidator: function() { |
| 6171 return this._validator != null; |
| 6172 }, |
| 6173 validate: function(value) { |
| 6174 this.invalid = !this._getValidity(value); |
| 6175 return !this.invalid; |
| 6176 }, |
| 6177 _getValidity: function(value) { |
| 6178 if (this.hasValidator()) { |
| 6179 return this._validator.validate(value); |
| 6180 } |
| 6181 return true; |
| 6182 }, |
| 6183 __computeValidator: function() { |
| 6184 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio
rMeta.byKey(this.validator); |
| 6185 } |
| 6186 }; |
| 6187 |
| 6188 Polymer({ |
| 6189 is: 'iron-input', |
| 6190 "extends": 'input', |
| 6191 behaviors: [ Polymer.IronValidatableBehavior ], |
| 6192 properties: { |
| 6193 bindValue: { |
| 6194 observer: '_bindValueChanged', |
| 6195 type: String |
| 6196 }, |
| 6197 preventInvalidInput: { |
| 6198 type: Boolean |
| 6199 }, |
| 6200 allowedPattern: { |
| 6201 type: String, |
| 6202 observer: "_allowedPatternChanged" |
| 6203 }, |
| 6204 _previousValidInput: { |
| 6205 type: String, |
| 6206 value: '' |
| 6207 }, |
| 6208 _patternAlreadyChecked: { |
| 6209 type: Boolean, |
| 6210 value: false |
| 6211 } |
| 6212 }, |
| 6213 listeners: { |
| 6214 input: '_onInput', |
| 6215 keypress: '_onKeypress' |
| 6216 }, |
| 6217 registered: function() { |
| 6218 if (!this._canDispatchEventOnDisabled()) { |
| 6219 this._origDispatchEvent = this.dispatchEvent; |
| 6220 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 6221 } |
| 6222 }, |
| 6223 created: function() { |
| 6224 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 6225 }, |
| 6226 _canDispatchEventOnDisabled: function() { |
| 6227 var input = document.createElement('input'); |
| 6228 var canDispatch = false; |
| 6229 input.disabled = true; |
| 6230 input.addEventListener('feature-check-dispatch-event', function() { |
| 6231 canDispatch = true; |
| 6232 }); |
| 6233 try { |
| 6234 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 6235 } catch (e) {} |
| 6236 return canDispatch; |
| 6237 }, |
| 6238 _dispatchEventFirefoxIE: function() { |
| 6239 var disabled = this.disabled; |
| 6240 this.disabled = false; |
| 6241 this._origDispatchEvent.apply(this, arguments); |
| 6242 this.disabled = disabled; |
| 6243 }, |
| 6244 get _patternRegExp() { |
| 6245 var pattern; |
| 6246 if (this.allowedPattern) { |
| 6247 pattern = new RegExp(this.allowedPattern); |
| 6248 } else { |
| 6249 switch (this.type) { |
| 6250 case 'number': |
| 6251 pattern = /[0-9.,e-]/; |
| 6252 break; |
| 6253 } |
| 6254 } |
| 6255 return pattern; |
| 6256 }, |
| 6257 ready: function() { |
| 6258 this.bindValue = this.value; |
| 6259 }, |
| 6260 _bindValueChanged: function() { |
| 6261 if (this.value !== this.bindValue) { |
| 6262 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue ==
= false) ? '' : this.bindValue; |
| 6263 } |
| 6264 this.fire('bind-value-changed', { |
| 6265 value: this.bindValue |
| 6266 }); |
| 6267 }, |
| 6268 _allowedPatternChanged: function() { |
| 6269 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 6270 }, |
| 6271 _onInput: function() { |
| 6272 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 6273 var valid = this._checkPatternValidity(); |
| 6274 if (!valid) { |
| 6275 this._announceInvalidCharacter('Invalid string of characters not entered
.'); |
| 6276 this.value = this._previousValidInput; |
| 6277 } |
| 6278 } |
| 6279 this.bindValue = this.value; |
| 6280 this._previousValidInput = this.value; |
| 6281 this._patternAlreadyChecked = false; |
| 6282 }, |
| 6283 _isPrintable: function(event) { |
| 6284 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC
ode == 13 || event.keyCode == 27; |
| 6285 var mozNonPrintable = event.keyCode == 19 || event.keyCode == 20 || event.ke
yCode == 45 || event.keyCode == 46 || event.keyCode == 144 || event.keyCode == 1
45 || event.keyCode > 32 && event.keyCode < 41 || event.keyCode > 111 && event.k
eyCode < 124; |
| 6286 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 6287 }, |
| 6288 _onKeypress: function(event) { |
| 6289 if (!this.preventInvalidInput && this.type !== 'number') { |
| 6290 return; |
| 6291 } |
| 6292 var regexp = this._patternRegExp; |
| 6293 if (!regexp) { |
| 6294 return; |
| 6295 } |
| 6296 if (event.metaKey || event.ctrlKey || event.altKey) return; |
| 6297 this._patternAlreadyChecked = true; |
| 6298 var thisChar = String.fromCharCode(event.charCode); |
| 6299 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 6300 event.preventDefault(); |
| 6301 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent
ered.'); |
| 6302 } |
| 6303 }, |
| 6304 _checkPatternValidity: function() { |
| 6305 var regexp = this._patternRegExp; |
| 6306 if (!regexp) { |
| 6307 return true; |
| 6308 } |
| 6309 for (var i = 0; i < this.value.length; i++) { |
| 6310 if (!regexp.test(this.value[i])) { |
| 6311 return false; |
| 6312 } |
| 6313 } |
| 6314 return true; |
| 6315 }, |
| 6316 validate: function() { |
| 6317 var valid = this.checkValidity(); |
| 6318 if (valid) { |
| 6319 if (this.required && this.value === '') { |
| 6320 valid = false; |
| 6321 } else if (this.hasValidator()) { |
| 6322 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); |
| 6323 } |
| 6324 } |
| 6325 this.invalid = !valid; |
| 6326 this.fire('iron-input-validate'); |
| 6327 return valid; |
| 6328 }, |
| 6329 _announceInvalidCharacter: function(message) { |
| 6330 this.fire('iron-announce', { |
| 6331 text: message |
| 6332 }); |
| 6333 } |
| 6334 }); |
| 6335 |
| 6336 Polymer({ |
| 6337 is: 'paper-input-container', |
| 6338 properties: { |
| 6339 noLabelFloat: { |
| 6340 type: Boolean, |
| 6341 value: false |
| 6342 }, |
| 6343 alwaysFloatLabel: { |
| 6344 type: Boolean, |
| 6345 value: false |
| 6346 }, |
| 6347 attrForValue: { |
| 6348 type: String, |
| 6349 value: 'bind-value' |
| 6350 }, |
| 6351 autoValidate: { |
| 6352 type: Boolean, |
| 6353 value: false |
| 6354 }, |
| 6355 invalid: { |
| 6356 observer: '_invalidChanged', |
| 6357 type: Boolean, |
| 6358 value: false |
| 6359 }, |
| 6360 focused: { |
| 6361 readOnly: true, |
| 6362 type: Boolean, |
| 6363 value: false, |
| 6364 notify: true |
| 6365 }, |
| 6366 _addons: { |
| 6367 type: Array |
| 6368 }, |
| 6369 _inputHasContent: { |
| 6370 type: Boolean, |
| 6371 value: false |
| 6372 }, |
| 6373 _inputSelector: { |
| 6374 type: String, |
| 6375 value: 'input,textarea,.paper-input-input' |
| 6376 }, |
| 6377 _boundOnFocus: { |
| 6378 type: Function, |
| 6379 value: function() { |
| 6380 return this._onFocus.bind(this); |
| 6381 } |
| 6382 }, |
| 6383 _boundOnBlur: { |
| 6384 type: Function, |
| 6385 value: function() { |
| 6386 return this._onBlur.bind(this); |
| 6387 } |
| 6388 }, |
| 6389 _boundOnInput: { |
| 6390 type: Function, |
| 6391 value: function() { |
| 6392 return this._onInput.bind(this); |
| 6393 } |
| 6394 }, |
| 6395 _boundValueChanged: { |
| 6396 type: Function, |
| 6397 value: function() { |
| 6398 return this._onValueChanged.bind(this); |
| 6399 } |
| 6400 } |
| 6401 }, |
| 6402 listeners: { |
| 6403 'addon-attached': '_onAddonAttached', |
| 6404 'iron-input-validate': '_onIronInputValidate' |
| 6405 }, |
| 6406 get _valueChangedEvent() { |
| 6407 return this.attrForValue + '-changed'; |
| 6408 }, |
| 6409 get _propertyForValue() { |
| 6410 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 6411 }, |
| 6412 get _inputElement() { |
| 6413 return Polymer.dom(this).querySelector(this._inputSelector); |
| 6414 }, |
| 6415 get _inputElementValue() { |
| 6416 return this._inputElement[this._propertyForValue] || this._inputElement.valu
e; |
| 6417 }, |
| 6418 ready: function() { |
| 6419 if (!this._addons) { |
| 6420 this._addons = []; |
| 6421 } |
| 6422 this.addEventListener('focus', this._boundOnFocus, true); |
| 6423 this.addEventListener('blur', this._boundOnBlur, true); |
| 6424 }, |
| 6425 attached: function() { |
| 6426 if (this.attrForValue) { |
| 6427 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa
lueChanged); |
| 6428 } else { |
| 6429 this.addEventListener('input', this._onInput); |
| 6430 } |
| 6431 if (this._inputElementValue != '') { |
| 6432 this._handleValueAndAutoValidate(this._inputElement); |
| 6433 } else { |
| 6434 this._handleValue(this._inputElement); |
| 6435 } |
| 6436 }, |
| 6437 _onAddonAttached: function(event) { |
| 6438 if (!this._addons) { |
| 6439 this._addons = []; |
| 6440 } |
| 6441 var target = event.target; |
| 6442 if (this._addons.indexOf(target) === -1) { |
| 6443 this._addons.push(target); |
| 6444 if (this.isAttached) { |
| 6445 this._handleValue(this._inputElement); |
| 6446 } |
| 6447 } |
| 6448 }, |
| 6449 _onFocus: function() { |
| 6450 this._setFocused(true); |
| 6451 }, |
| 6452 _onBlur: function() { |
| 6453 this._setFocused(false); |
| 6454 this._handleValueAndAutoValidate(this._inputElement); |
| 6455 }, |
| 6456 _onInput: function(event) { |
| 6457 this._handleValueAndAutoValidate(event.target); |
| 6458 }, |
| 6459 _onValueChanged: function(event) { |
| 6460 this._handleValueAndAutoValidate(event.target); |
| 6461 }, |
| 6462 _handleValue: function(inputElement) { |
| 6463 var value = this._inputElementValue; |
| 6464 if (value || value === 0 || inputElement.type === 'number' && !inputElement.
checkValidity()) { |
| 6465 this._inputHasContent = true; |
| 6466 } else { |
| 6467 this._inputHasContent = false; |
| 6468 } |
| 6469 this.updateAddons({ |
| 6470 inputElement: inputElement, |
| 6471 value: value, |
| 6472 invalid: this.invalid |
| 6473 }); |
| 6474 }, |
| 6475 _handleValueAndAutoValidate: function(inputElement) { |
| 6476 if (this.autoValidate) { |
| 6477 var valid; |
| 6478 if (inputElement.validate) { |
| 6479 valid = inputElement.validate(this._inputElementValue); |
11184 } else { | 6480 } else { |
11185 switch (this.type) { | 6481 valid = inputElement.checkValidity(); |
11186 case 'number': | 6482 } |
11187 pattern = /[0-9.,e-]/; | |
11188 break; | |
11189 } | |
11190 } | |
11191 return pattern; | |
11192 }, | |
11193 | |
11194 ready: function() { | |
11195 this.bindValue = this.value; | |
11196 }, | |
11197 | |
11198 /** | |
11199 * @suppress {checkTypes} | |
11200 */ | |
11201 _bindValueChanged: function() { | |
11202 if (this.value !== this.bindValue) { | |
11203 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | |
11204 } | |
11205 // manually notify because we don't want to notify until after setting val
ue | |
11206 this.fire('bind-value-changed', {value: this.bindValue}); | |
11207 }, | |
11208 | |
11209 _allowedPatternChanged: function() { | |
11210 // Force to prevent invalid input when an `allowed-pattern` is set | |
11211 this.preventInvalidInput = this.allowedPattern ? true : false; | |
11212 }, | |
11213 | |
11214 _onInput: function() { | |
11215 // Need to validate each of the characters pasted if they haven't | |
11216 // been validated inside `_onKeypress` already. | |
11217 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | |
11218 var valid = this._checkPatternValidity(); | |
11219 if (!valid) { | |
11220 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | |
11221 this.value = this._previousValidInput; | |
11222 } | |
11223 } | |
11224 | |
11225 this.bindValue = this.value; | |
11226 this._previousValidInput = this.value; | |
11227 this._patternAlreadyChecked = false; | |
11228 }, | |
11229 | |
11230 _isPrintable: function(event) { | |
11231 // What a control/printable character is varies wildly based on the browse
r. | |
11232 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
11233 // in Chrome, but the *do* on Firefox | |
11234 // - in Firefox, when they do send a `keypress` event, control chars have | |
11235 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
11236 // - printable characters always send a keypress event. | |
11237 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
11238 // always matches the charCode. | |
11239 // None of this makes any sense. | |
11240 | |
11241 // For these keys, ASCII code == browser keycode. | |
11242 var anyNonPrintable = | |
11243 (event.keyCode == 8) || // backspace | |
11244 (event.keyCode == 9) || // tab | |
11245 (event.keyCode == 13) || // enter | |
11246 (event.keyCode == 27); // escape | |
11247 | |
11248 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
11249 var mozNonPrintable = | |
11250 (event.keyCode == 19) || // pause | |
11251 (event.keyCode == 20) || // caps lock | |
11252 (event.keyCode == 45) || // insert | |
11253 (event.keyCode == 46) || // delete | |
11254 (event.keyCode == 144) || // num lock | |
11255 (event.keyCode == 145) || // scroll lock | |
11256 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | |
11257 (event.keyCode > 111 && event.keyCode < 124); // fn keys | |
11258 | |
11259 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | |
11260 }, | |
11261 | |
11262 _onKeypress: function(event) { | |
11263 if (!this.preventInvalidInput && this.type !== 'number') { | |
11264 return; | |
11265 } | |
11266 var regexp = this._patternRegExp; | |
11267 if (!regexp) { | |
11268 return; | |
11269 } | |
11270 | |
11271 // Handle special keys and backspace | |
11272 if (event.metaKey || event.ctrlKey || event.altKey) | |
11273 return; | |
11274 | |
11275 // Check the pattern either here or in `_onInput`, but not in both. | |
11276 this._patternAlreadyChecked = true; | |
11277 | |
11278 var thisChar = String.fromCharCode(event.charCode); | |
11279 if (this._isPrintable(event) && !regexp.test(thisChar)) { | |
11280 event.preventDefault(); | |
11281 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | |
11282 } | |
11283 }, | |
11284 | |
11285 _checkPatternValidity: function() { | |
11286 var regexp = this._patternRegExp; | |
11287 if (!regexp) { | |
11288 return true; | |
11289 } | |
11290 for (var i = 0; i < this.value.length; i++) { | |
11291 if (!regexp.test(this.value[i])) { | |
11292 return false; | |
11293 } | |
11294 } | |
11295 return true; | |
11296 }, | |
11297 | |
11298 /** | |
11299 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
11300 * then any constraints. | |
11301 * @return {boolean} True if the value is valid. | |
11302 */ | |
11303 validate: function() { | |
11304 // First, check what the browser thinks. Some inputs (like type=number) | |
11305 // behave weirdly and will set the value to "" if something invalid is | |
11306 // entered, but will set the validity correctly. | |
11307 var valid = this.checkValidity(); | |
11308 | |
11309 // Only do extra checking if the browser thought this was valid. | |
11310 if (valid) { | |
11311 // Empty, required input is invalid | |
11312 if (this.required && this.value === '') { | |
11313 valid = false; | |
11314 } else if (this.hasValidator()) { | |
11315 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | |
11316 } | |
11317 } | |
11318 | |
11319 this.invalid = !valid; | 6483 this.invalid = !valid; |
11320 this.fire('iron-input-validate'); | 6484 } |
11321 return valid; | 6485 this._handleValue(inputElement); |
11322 }, | 6486 }, |
11323 | 6487 _onIronInputValidate: function(event) { |
11324 _announceInvalidCharacter: function(message) { | 6488 this.invalid = this._inputElement.invalid; |
11325 this.fire('iron-announce', { text: message }); | 6489 }, |
11326 } | 6490 _invalidChanged: function() { |
11327 }); | 6491 if (this._addons) { |
11328 | |
11329 /* | |
11330 The `iron-input-validate` event is fired whenever `validate()` is called. | |
11331 @event iron-input-validate | |
11332 */ | |
11333 Polymer({ | |
11334 is: 'paper-input-container', | |
11335 | |
11336 properties: { | |
11337 /** | |
11338 * Set to true to disable the floating label. The label disappears when th
e input value is | |
11339 * not null. | |
11340 */ | |
11341 noLabelFloat: { | |
11342 type: Boolean, | |
11343 value: false | |
11344 }, | |
11345 | |
11346 /** | |
11347 * Set to true to always float the floating label. | |
11348 */ | |
11349 alwaysFloatLabel: { | |
11350 type: Boolean, | |
11351 value: false | |
11352 }, | |
11353 | |
11354 /** | |
11355 * The attribute to listen for value changes on. | |
11356 */ | |
11357 attrForValue: { | |
11358 type: String, | |
11359 value: 'bind-value' | |
11360 }, | |
11361 | |
11362 /** | |
11363 * Set to true to auto-validate the input value when it changes. | |
11364 */ | |
11365 autoValidate: { | |
11366 type: Boolean, | |
11367 value: false | |
11368 }, | |
11369 | |
11370 /** | |
11371 * True if the input is invalid. This property is set automatically when t
he input value | |
11372 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
11373 */ | |
11374 invalid: { | |
11375 observer: '_invalidChanged', | |
11376 type: Boolean, | |
11377 value: false | |
11378 }, | |
11379 | |
11380 /** | |
11381 * True if the input has focus. | |
11382 */ | |
11383 focused: { | |
11384 readOnly: true, | |
11385 type: Boolean, | |
11386 value: false, | |
11387 notify: true | |
11388 }, | |
11389 | |
11390 _addons: { | |
11391 type: Array | |
11392 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
11393 // distributed child is attached, which may occur before configuration f
or this element | |
11394 // in polyfill. | |
11395 }, | |
11396 | |
11397 _inputHasContent: { | |
11398 type: Boolean, | |
11399 value: false | |
11400 }, | |
11401 | |
11402 _inputSelector: { | |
11403 type: String, | |
11404 value: 'input,textarea,.paper-input-input' | |
11405 }, | |
11406 | |
11407 _boundOnFocus: { | |
11408 type: Function, | |
11409 value: function() { | |
11410 return this._onFocus.bind(this); | |
11411 } | |
11412 }, | |
11413 | |
11414 _boundOnBlur: { | |
11415 type: Function, | |
11416 value: function() { | |
11417 return this._onBlur.bind(this); | |
11418 } | |
11419 }, | |
11420 | |
11421 _boundOnInput: { | |
11422 type: Function, | |
11423 value: function() { | |
11424 return this._onInput.bind(this); | |
11425 } | |
11426 }, | |
11427 | |
11428 _boundValueChanged: { | |
11429 type: Function, | |
11430 value: function() { | |
11431 return this._onValueChanged.bind(this); | |
11432 } | |
11433 } | |
11434 }, | |
11435 | |
11436 listeners: { | |
11437 'addon-attached': '_onAddonAttached', | |
11438 'iron-input-validate': '_onIronInputValidate' | |
11439 }, | |
11440 | |
11441 get _valueChangedEvent() { | |
11442 return this.attrForValue + '-changed'; | |
11443 }, | |
11444 | |
11445 get _propertyForValue() { | |
11446 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
11447 }, | |
11448 | |
11449 get _inputElement() { | |
11450 return Polymer.dom(this).querySelector(this._inputSelector); | |
11451 }, | |
11452 | |
11453 get _inputElementValue() { | |
11454 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
11455 }, | |
11456 | |
11457 ready: function() { | |
11458 if (!this._addons) { | |
11459 this._addons = []; | |
11460 } | |
11461 this.addEventListener('focus', this._boundOnFocus, true); | |
11462 this.addEventListener('blur', this._boundOnBlur, true); | |
11463 }, | |
11464 | |
11465 attached: function() { | |
11466 if (this.attrForValue) { | |
11467 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
11468 } else { | |
11469 this.addEventListener('input', this._onInput); | |
11470 } | |
11471 | |
11472 // Only validate when attached if the input already has a value. | |
11473 if (this._inputElementValue != '') { | |
11474 this._handleValueAndAutoValidate(this._inputElement); | |
11475 } else { | |
11476 this._handleValue(this._inputElement); | |
11477 } | |
11478 }, | |
11479 | |
11480 _onAddonAttached: function(event) { | |
11481 if (!this._addons) { | |
11482 this._addons = []; | |
11483 } | |
11484 var target = event.target; | |
11485 if (this._addons.indexOf(target) === -1) { | |
11486 this._addons.push(target); | |
11487 if (this.isAttached) { | |
11488 this._handleValue(this._inputElement); | |
11489 } | |
11490 } | |
11491 }, | |
11492 | |
11493 _onFocus: function() { | |
11494 this._setFocused(true); | |
11495 }, | |
11496 | |
11497 _onBlur: function() { | |
11498 this._setFocused(false); | |
11499 this._handleValueAndAutoValidate(this._inputElement); | |
11500 }, | |
11501 | |
11502 _onInput: function(event) { | |
11503 this._handleValueAndAutoValidate(event.target); | |
11504 }, | |
11505 | |
11506 _onValueChanged: function(event) { | |
11507 this._handleValueAndAutoValidate(event.target); | |
11508 }, | |
11509 | |
11510 _handleValue: function(inputElement) { | |
11511 var value = this._inputElementValue; | |
11512 | |
11513 // type="number" hack needed because this.value is empty until it's valid | |
11514 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
11515 this._inputHasContent = true; | |
11516 } else { | |
11517 this._inputHasContent = false; | |
11518 } | |
11519 | |
11520 this.updateAddons({ | 6492 this.updateAddons({ |
11521 inputElement: inputElement, | |
11522 value: value, | |
11523 invalid: this.invalid | 6493 invalid: this.invalid |
11524 }); | 6494 }); |
11525 }, | 6495 } |
11526 | 6496 }, |
11527 _handleValueAndAutoValidate: function(inputElement) { | 6497 updateAddons: function(state) { |
11528 if (this.autoValidate) { | 6498 for (var addon, index = 0; addon = this._addons[index]; index++) { |
11529 var valid; | 6499 addon.update(state); |
11530 if (inputElement.validate) { | 6500 } |
11531 valid = inputElement.validate(this._inputElementValue); | 6501 }, |
11532 } else { | 6502 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i
nvalid, _inputHasContent) { |
11533 valid = inputElement.checkValidity(); | 6503 var cls = 'input-content'; |
11534 } | 6504 if (!noLabelFloat) { |
11535 this.invalid = !valid; | 6505 var label = this.querySelector('label'); |
11536 } | 6506 if (alwaysFloatLabel || _inputHasContent) { |
11537 | 6507 cls += ' label-is-floating'; |
11538 // Call this last to notify the add-ons. | 6508 this.$.labelAndInputContainer.style.position = 'static'; |
11539 this._handleValue(inputElement); | 6509 if (invalid) { |
11540 }, | 6510 cls += ' is-invalid'; |
11541 | 6511 } else if (focused) { |
11542 _onIronInputValidate: function(event) { | 6512 cls += " label-is-highlighted"; |
11543 this.invalid = this._inputElement.invalid; | |
11544 }, | |
11545 | |
11546 _invalidChanged: function() { | |
11547 if (this._addons) { | |
11548 this.updateAddons({invalid: this.invalid}); | |
11549 } | |
11550 }, | |
11551 | |
11552 /** | |
11553 * Call this to update the state of add-ons. | |
11554 * @param {Object} state Add-on state. | |
11555 */ | |
11556 updateAddons: function(state) { | |
11557 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
11558 addon.update(state); | |
11559 } | |
11560 }, | |
11561 | |
11562 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
11563 var cls = 'input-content'; | |
11564 if (!noLabelFloat) { | |
11565 var label = this.querySelector('label'); | |
11566 | |
11567 if (alwaysFloatLabel || _inputHasContent) { | |
11568 cls += ' label-is-floating'; | |
11569 // If the label is floating, ignore any offsets that may have been | |
11570 // applied from a prefix element. | |
11571 this.$.labelAndInputContainer.style.position = 'static'; | |
11572 | |
11573 if (invalid) { | |
11574 cls += ' is-invalid'; | |
11575 } else if (focused) { | |
11576 cls += " label-is-highlighted"; | |
11577 } | |
11578 } else { | |
11579 // When the label is not floating, it should overlap the input element
. | |
11580 if (label) { | |
11581 this.$.labelAndInputContainer.style.position = 'relative'; | |
11582 } | |
11583 } | 6513 } |
11584 } else { | 6514 } else { |
11585 if (_inputHasContent) { | 6515 if (label) { |
11586 cls += ' label-is-hidden'; | 6516 this.$.labelAndInputContainer.style.position = 'relative'; |
11587 } | 6517 } |
11588 } | 6518 } |
11589 return cls; | 6519 } else { |
11590 }, | 6520 if (_inputHasContent) { |
11591 | 6521 cls += ' label-is-hidden'; |
11592 _computeUnderlineClass: function(focused, invalid) { | 6522 } |
11593 var cls = 'underline'; | 6523 } |
11594 if (invalid) { | 6524 return cls; |
11595 cls += ' is-invalid'; | 6525 }, |
11596 } else if (focused) { | 6526 _computeUnderlineClass: function(focused, invalid) { |
11597 cls += ' is-highlighted' | 6527 var cls = 'underline'; |
11598 } | 6528 if (invalid) { |
11599 return cls; | 6529 cls += ' is-invalid'; |
11600 }, | 6530 } else if (focused) { |
11601 | 6531 cls += ' is-highlighted'; |
11602 _computeAddOnContentClass: function(focused, invalid) { | 6532 } |
11603 var cls = 'add-on-content'; | 6533 return cls; |
11604 if (invalid) { | 6534 }, |
11605 cls += ' is-invalid'; | 6535 _computeAddOnContentClass: function(focused, invalid) { |
11606 } else if (focused) { | 6536 var cls = 'add-on-content'; |
11607 cls += ' is-highlighted' | 6537 if (invalid) { |
11608 } | 6538 cls += ' is-invalid'; |
11609 return cls; | 6539 } else if (focused) { |
11610 } | 6540 cls += ' is-highlighted'; |
11611 }); | 6541 } |
| 6542 return cls; |
| 6543 } |
| 6544 }); |
| 6545 |
11612 // Copyright 2015 The Chromium Authors. All rights reserved. | 6546 // Copyright 2015 The Chromium Authors. All rights reserved. |
11613 // Use of this source code is governed by a BSD-style license that can be | 6547 // Use of this source code is governed by a BSD-style license that can be |
11614 // found in the LICENSE file. | 6548 // found in the LICENSE file. |
11615 | |
11616 var SearchField = Polymer({ | 6549 var SearchField = Polymer({ |
11617 is: 'cr-search-field', | 6550 is: 'cr-search-field', |
11618 | 6551 behaviors: [ CrSearchFieldBehavior ], |
11619 behaviors: [CrSearchFieldBehavior], | |
11620 | |
11621 properties: { | 6552 properties: { |
11622 value_: String, | 6553 value_: String |
11623 }, | 6554 }, |
11624 | |
11625 /** @return {!HTMLInputElement} */ | |
11626 getSearchInput: function() { | 6555 getSearchInput: function() { |
11627 return this.$.searchInput; | 6556 return this.$.searchInput; |
11628 }, | 6557 }, |
11629 | |
11630 /** @private */ | |
11631 clearSearch_: function() { | 6558 clearSearch_: function() { |
11632 this.setValue(''); | 6559 this.setValue(''); |
11633 this.getSearchInput().focus(); | 6560 this.getSearchInput().focus(); |
11634 }, | 6561 }, |
11635 | |
11636 /** @private */ | |
11637 toggleShowingSearch_: function() { | 6562 toggleShowingSearch_: function() { |
11638 this.showingSearch = !this.showingSearch; | 6563 this.showingSearch = !this.showingSearch; |
11639 }, | 6564 } |
11640 }); | 6565 }); |
| 6566 |
11641 // Copyright 2015 The Chromium Authors. All rights reserved. | 6567 // Copyright 2015 The Chromium Authors. All rights reserved. |
11642 // Use of this source code is governed by a BSD-style license that can be | 6568 // Use of this source code is governed by a BSD-style license that can be |
11643 // found in the LICENSE file. | 6569 // found in the LICENSE file. |
11644 | |
11645 cr.define('downloads', function() { | 6570 cr.define('downloads', function() { |
11646 var Toolbar = Polymer({ | 6571 var Toolbar = Polymer({ |
11647 is: 'downloads-toolbar', | 6572 is: 'downloads-toolbar', |
11648 | |
11649 attached: function() { | 6573 attached: function() { |
11650 // isRTL() only works after i18n_template.js runs to set <html dir>. | |
11651 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 6574 this.overflowAlign_ = isRTL() ? 'left' : 'right'; |
11652 }, | 6575 }, |
11653 | |
11654 properties: { | 6576 properties: { |
11655 downloadsShowing: { | 6577 downloadsShowing: { |
11656 reflectToAttribute: true, | 6578 reflectToAttribute: true, |
11657 type: Boolean, | 6579 type: Boolean, |
11658 value: false, | 6580 value: false, |
11659 observer: 'downloadsShowingChanged_', | 6581 observer: 'downloadsShowingChanged_' |
11660 }, | 6582 }, |
11661 | |
11662 overflowAlign_: { | 6583 overflowAlign_: { |
11663 type: String, | 6584 type: String, |
11664 value: 'right', | 6585 value: 'right' |
11665 }, | 6586 } |
11666 }, | 6587 }, |
11667 | |
11668 listeners: { | 6588 listeners: { |
11669 'paper-dropdown-close': 'onPaperDropdownClose_', | 6589 'paper-dropdown-close': 'onPaperDropdownClose_', |
11670 'paper-dropdown-open': 'onPaperDropdownOpen_', | 6590 'paper-dropdown-open': 'onPaperDropdownOpen_' |
11671 }, | 6591 }, |
11672 | |
11673 /** @return {boolean} Whether removal can be undone. */ | |
11674 canUndo: function() { | 6592 canUndo: function() { |
11675 return this.$['search-input'] != this.shadowRoot.activeElement; | 6593 return this.$['search-input'] != this.shadowRoot.activeElement; |
11676 }, | 6594 }, |
11677 | |
11678 /** @return {boolean} Whether "Clear all" should be allowed. */ | |
11679 canClearAll: function() { | 6595 canClearAll: function() { |
11680 return !this.$['search-input'].getValue() && this.downloadsShowing; | 6596 return !this.$['search-input'].getValue() && this.downloadsShowing; |
11681 }, | 6597 }, |
11682 | |
11683 onFindCommand: function() { | 6598 onFindCommand: function() { |
11684 this.$['search-input'].showAndFocus(); | 6599 this.$['search-input'].showAndFocus(); |
11685 }, | 6600 }, |
11686 | |
11687 /** @private */ | |
11688 closeMoreActions_: function() { | 6601 closeMoreActions_: function() { |
11689 this.$.more.close(); | 6602 this.$.more.close(); |
11690 }, | 6603 }, |
11691 | |
11692 /** @private */ | |
11693 downloadsShowingChanged_: function() { | 6604 downloadsShowingChanged_: function() { |
11694 this.updateClearAll_(); | 6605 this.updateClearAll_(); |
11695 }, | 6606 }, |
11696 | |
11697 /** @private */ | |
11698 onClearAllTap_: function() { | 6607 onClearAllTap_: function() { |
11699 assert(this.canClearAll()); | 6608 assert(this.canClearAll()); |
11700 downloads.ActionService.getInstance().clearAll(); | 6609 downloads.ActionService.getInstance().clearAll(); |
11701 }, | 6610 }, |
11702 | |
11703 /** @private */ | |
11704 onPaperDropdownClose_: function() { | 6611 onPaperDropdownClose_: function() { |
11705 window.removeEventListener('resize', assert(this.boundClose_)); | 6612 window.removeEventListener('resize', assert(this.boundClose_)); |
11706 }, | 6613 }, |
11707 | |
11708 /** | |
11709 * @param {!Event} e | |
11710 * @private | |
11711 */ | |
11712 onItemBlur_: function(e) { | 6614 onItemBlur_: function(e) { |
11713 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 6615 var menu = this.$$('paper-menu'); |
11714 if (menu.items.indexOf(e.relatedTarget) >= 0) | 6616 if (menu.items.indexOf(e.relatedTarget) >= 0) return; |
11715 return; | |
11716 | |
11717 this.$.more.restoreFocusOnClose = false; | 6617 this.$.more.restoreFocusOnClose = false; |
11718 this.closeMoreActions_(); | 6618 this.closeMoreActions_(); |
11719 this.$.more.restoreFocusOnClose = true; | 6619 this.$.more.restoreFocusOnClose = true; |
11720 }, | 6620 }, |
11721 | |
11722 /** @private */ | |
11723 onPaperDropdownOpen_: function() { | 6621 onPaperDropdownOpen_: function() { |
11724 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 6622 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); |
11725 window.addEventListener('resize', this.boundClose_); | 6623 window.addEventListener('resize', this.boundClose_); |
11726 }, | 6624 }, |
11727 | |
11728 /** | |
11729 * @param {!CustomEvent} event | |
11730 * @private | |
11731 */ | |
11732 onSearchChanged_: function(event) { | 6625 onSearchChanged_: function(event) { |
11733 downloads.ActionService.getInstance().search( | 6626 downloads.ActionService.getInstance().search(event.detail); |
11734 /** @type {string} */ (event.detail)); | |
11735 this.updateClearAll_(); | 6627 this.updateClearAll_(); |
11736 }, | 6628 }, |
11737 | |
11738 /** @private */ | |
11739 onOpenDownloadsFolderTap_: function() { | 6629 onOpenDownloadsFolderTap_: function() { |
11740 downloads.ActionService.getInstance().openDownloadsFolder(); | 6630 downloads.ActionService.getInstance().openDownloadsFolder(); |
11741 }, | 6631 }, |
11742 | |
11743 /** @private */ | |
11744 updateClearAll_: function() { | 6632 updateClearAll_: function() { |
11745 this.$$('#actions .clear-all').hidden = !this.canClearAll(); | 6633 this.$$('#actions .clear-all').hidden = !this.canClearAll(); |
11746 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); | 6634 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); |
11747 }, | 6635 } |
11748 }); | 6636 }); |
11749 | 6637 return { |
11750 return {Toolbar: Toolbar}; | 6638 Toolbar: Toolbar |
| 6639 }; |
11751 }); | 6640 }); |
| 6641 |
11752 // Copyright 2015 The Chromium Authors. All rights reserved. | 6642 // Copyright 2015 The Chromium Authors. All rights reserved. |
11753 // Use of this source code is governed by a BSD-style license that can be | 6643 // Use of this source code is governed by a BSD-style license that can be |
11754 // found in the LICENSE file. | 6644 // found in the LICENSE file. |
11755 | |
11756 cr.define('downloads', function() { | 6645 cr.define('downloads', function() { |
11757 var Manager = Polymer({ | 6646 var Manager = Polymer({ |
11758 is: 'downloads-manager', | 6647 is: 'downloads-manager', |
11759 | |
11760 properties: { | 6648 properties: { |
11761 hasDownloads_: { | 6649 hasDownloads_: { |
11762 observer: 'hasDownloadsChanged_', | 6650 observer: 'hasDownloadsChanged_', |
11763 type: Boolean, | 6651 type: Boolean |
11764 }, | 6652 }, |
11765 | |
11766 items_: { | 6653 items_: { |
11767 type: Array, | 6654 type: Array, |
11768 value: function() { return []; }, | 6655 value: function() { |
11769 }, | 6656 return []; |
11770 }, | 6657 } |
11771 | 6658 } |
| 6659 }, |
11772 hostAttributes: { | 6660 hostAttributes: { |
11773 loading: true, | 6661 loading: true |
11774 }, | 6662 }, |
11775 | |
11776 listeners: { | 6663 listeners: { |
11777 'downloads-list.scroll': 'onListScroll_', | 6664 'downloads-list.scroll': 'onListScroll_' |
11778 }, | 6665 }, |
11779 | 6666 observers: [ 'itemsChanged_(items_.*)' ], |
11780 observers: [ | |
11781 'itemsChanged_(items_.*)', | |
11782 ], | |
11783 | |
11784 /** @private */ | |
11785 clearAll_: function() { | 6667 clearAll_: function() { |
11786 this.set('items_', []); | 6668 this.set('items_', []); |
11787 }, | 6669 }, |
11788 | |
11789 /** @private */ | |
11790 hasDownloadsChanged_: function() { | 6670 hasDownloadsChanged_: function() { |
11791 if (loadTimeData.getBoolean('allowDeletingHistory')) | 6671 if (loadTimeData.getBoolean('allowDeletingHistory')) this.$.toolbar.downlo
adsShowing = this.hasDownloads_; |
11792 this.$.toolbar.downloadsShowing = this.hasDownloads_; | |
11793 | |
11794 if (this.hasDownloads_) { | 6672 if (this.hasDownloads_) { |
11795 this.$['downloads-list'].fire('iron-resize'); | 6673 this.$['downloads-list'].fire('iron-resize'); |
11796 } else { | 6674 } else { |
11797 var isSearching = downloads.ActionService.getInstance().isSearching(); | 6675 var isSearching = downloads.ActionService.getInstance().isSearching(); |
11798 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 6676 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; |
11799 this.$['no-downloads'].querySelector('span').textContent = | 6677 this.$['no-downloads'].querySelector('span').textContent = loadTimeData.
getString(messageToShow); |
11800 loadTimeData.getString(messageToShow); | 6678 } |
11801 } | 6679 }, |
11802 }, | |
11803 | |
11804 /** | |
11805 * @param {number} index | |
11806 * @param {!Array<!downloads.Data>} list | |
11807 * @private | |
11808 */ | |
11809 insertItems_: function(index, list) { | 6680 insertItems_: function(index, list) { |
11810 this.splice.apply(this, ['items_', index, 0].concat(list)); | 6681 this.splice.apply(this, [ 'items_', index, 0 ].concat(list)); |
11811 this.updateHideDates_(index, index + list.length); | 6682 this.updateHideDates_(index, index + list.length); |
11812 this.removeAttribute('loading'); | 6683 this.removeAttribute('loading'); |
11813 }, | 6684 }, |
11814 | |
11815 /** @private */ | |
11816 itemsChanged_: function() { | 6685 itemsChanged_: function() { |
11817 this.hasDownloads_ = this.items_.length > 0; | 6686 this.hasDownloads_ = this.items_.length > 0; |
11818 }, | 6687 }, |
11819 | |
11820 /** | |
11821 * @param {Event} e | |
11822 * @private | |
11823 */ | |
11824 onCanExecute_: function(e) { | 6688 onCanExecute_: function(e) { |
11825 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 6689 e = e; |
11826 switch (e.command.id) { | 6690 switch (e.command.id) { |
11827 case 'undo-command': | 6691 case 'undo-command': |
11828 e.canExecute = this.$.toolbar.canUndo(); | 6692 e.canExecute = this.$.toolbar.canUndo(); |
11829 break; | 6693 break; |
11830 case 'clear-all-command': | 6694 |
11831 e.canExecute = this.$.toolbar.canClearAll(); | 6695 case 'clear-all-command': |
11832 break; | 6696 e.canExecute = this.$.toolbar.canClearAll(); |
11833 case 'find-command': | 6697 break; |
11834 e.canExecute = true; | 6698 |
11835 break; | 6699 case 'find-command': |
11836 } | 6700 e.canExecute = true; |
11837 }, | 6701 break; |
11838 | 6702 } |
11839 /** | 6703 }, |
11840 * @param {Event} e | |
11841 * @private | |
11842 */ | |
11843 onCommand_: function(e) { | 6704 onCommand_: function(e) { |
11844 if (e.command.id == 'clear-all-command') | 6705 if (e.command.id == 'clear-all-command') downloads.ActionService.getInstan
ce().clearAll(); else if (e.command.id == 'undo-command') downloads.ActionServic
e.getInstance().undo(); else if (e.command.id == 'find-command') this.$.toolbar.
onFindCommand(); |
11845 downloads.ActionService.getInstance().clearAll(); | 6706 }, |
11846 else if (e.command.id == 'undo-command') | |
11847 downloads.ActionService.getInstance().undo(); | |
11848 else if (e.command.id == 'find-command') | |
11849 this.$.toolbar.onFindCommand(); | |
11850 }, | |
11851 | |
11852 /** @private */ | |
11853 onListScroll_: function() { | 6707 onListScroll_: function() { |
11854 var list = this.$['downloads-list']; | 6708 var list = this.$['downloads-list']; |
11855 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 6709 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { |
11856 // Approaching the end of the scrollback. Attempt to load more items. | |
11857 downloads.ActionService.getInstance().loadMore(); | 6710 downloads.ActionService.getInstance().loadMore(); |
11858 } | 6711 } |
11859 }, | 6712 }, |
11860 | |
11861 /** @private */ | |
11862 onLoad_: function() { | 6713 onLoad_: function() { |
11863 cr.ui.decorate('command', cr.ui.Command); | 6714 cr.ui.decorate('command', cr.ui.Command); |
11864 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 6715 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
11865 document.addEventListener('command', this.onCommand_.bind(this)); | 6716 document.addEventListener('command', this.onCommand_.bind(this)); |
11866 | |
11867 downloads.ActionService.getInstance().loadMore(); | 6717 downloads.ActionService.getInstance().loadMore(); |
11868 }, | 6718 }, |
11869 | |
11870 /** | |
11871 * @param {number} index | |
11872 * @private | |
11873 */ | |
11874 removeItem_: function(index) { | 6719 removeItem_: function(index) { |
11875 this.splice('items_', index, 1); | 6720 this.splice('items_', index, 1); |
11876 this.updateHideDates_(index, index); | 6721 this.updateHideDates_(index, index); |
11877 this.onListScroll_(); | 6722 this.onListScroll_(); |
11878 }, | 6723 }, |
11879 | |
11880 /** | |
11881 * @param {number} start | |
11882 * @param {number} end | |
11883 * @private | |
11884 */ | |
11885 updateHideDates_: function(start, end) { | 6724 updateHideDates_: function(start, end) { |
11886 for (var i = start; i <= end; ++i) { | 6725 for (var i = start; i <= end; ++i) { |
11887 var current = this.items_[i]; | 6726 var current = this.items_[i]; |
11888 if (!current) | 6727 if (!current) continue; |
11889 continue; | |
11890 var prev = this.items_[i - 1]; | 6728 var prev = this.items_[i - 1]; |
11891 current.hideDate = !!prev && prev.date_string == current.date_string; | 6729 current.hideDate = !!prev && prev.date_string == current.date_string; |
11892 } | 6730 } |
11893 }, | 6731 }, |
11894 | |
11895 /** | |
11896 * @param {number} index | |
11897 * @param {!downloads.Data} data | |
11898 * @private | |
11899 */ | |
11900 updateItem_: function(index, data) { | 6732 updateItem_: function(index, data) { |
11901 this.set('items_.' + index, data); | 6733 this.set('items_.' + index, data); |
11902 this.updateHideDates_(index, index); | 6734 this.updateHideDates_(index, index); |
11903 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | 6735 var list = this.$['downloads-list']; |
11904 list.updateSizeForItem(index); | 6736 list.updateSizeForItem(index); |
11905 }, | 6737 } |
11906 }); | 6738 }); |
11907 | |
11908 Manager.clearAll = function() { | 6739 Manager.clearAll = function() { |
11909 Manager.get().clearAll_(); | 6740 Manager.get().clearAll_(); |
11910 }; | 6741 }; |
11911 | |
11912 /** @return {!downloads.Manager} */ | |
11913 Manager.get = function() { | 6742 Manager.get = function() { |
11914 return /** @type {!downloads.Manager} */( | 6743 return queryRequiredElement('downloads-manager'); |
11915 queryRequiredElement('downloads-manager')); | 6744 }; |
11916 }; | |
11917 | |
11918 Manager.insertItems = function(index, list) { | 6745 Manager.insertItems = function(index, list) { |
11919 Manager.get().insertItems_(index, list); | 6746 Manager.get().insertItems_(index, list); |
11920 }; | 6747 }; |
11921 | |
11922 Manager.onLoad = function() { | 6748 Manager.onLoad = function() { |
11923 Manager.get().onLoad_(); | 6749 Manager.get().onLoad_(); |
11924 }; | 6750 }; |
11925 | |
11926 Manager.removeItem = function(index) { | 6751 Manager.removeItem = function(index) { |
11927 Manager.get().removeItem_(index); | 6752 Manager.get().removeItem_(index); |
11928 }; | 6753 }; |
11929 | |
11930 Manager.updateItem = function(index, data) { | 6754 Manager.updateItem = function(index, data) { |
11931 Manager.get().updateItem_(index, data); | 6755 Manager.get().updateItem_(index, data); |
11932 }; | 6756 }; |
11933 | 6757 return { |
11934 return {Manager: Manager}; | 6758 Manager: Manager |
| 6759 }; |
11935 }); | 6760 }); |
| 6761 |
11936 // Copyright 2015 The Chromium Authors. All rights reserved. | 6762 // Copyright 2015 The Chromium Authors. All rights reserved. |
11937 // Use of this source code is governed by a BSD-style license that can be | 6763 // Use of this source code is governed by a BSD-style license that can be |
11938 // found in the LICENSE file. | 6764 // found in the LICENSE file. |
11939 | |
11940 window.addEventListener('load', downloads.Manager.onLoad); | 6765 window.addEventListener('load', downloads.Manager.onLoad); |
OLD | NEW |