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="../../../../ui/webui/resources/js/assert.js"> | |
1154 | |
1155 /** | |
1156 * Alias for document.getElementById. Found elements must be HTMLElements. | |
1157 * @param {string} id The ID of the element to find. | |
1158 * @return {HTMLElement} The found element or null if not found. | |
1159 */ | |
1160 function $(id) { | 541 function $(id) { |
1161 var el = document.getElementById(id); | 542 var el = document.getElementById(id); |
1162 return el ? assertInstanceof(el, HTMLElement) : null; | 543 return el ? assertInstanceof(el, HTMLElement) : null; |
1163 } | 544 } |
1164 | 545 |
1165 // TODO(devlin): This should return SVGElement, but closure compiler is missing | |
1166 // those externs. | |
1167 /** | |
1168 * Alias for document.getElementById. Found elements must be SVGElements. | |
1169 * @param {string} id The ID of the element to find. | |
1170 * @return {Element} The found element or null if not found. | |
1171 */ | |
1172 function getSVGElement(id) { | 546 function getSVGElement(id) { |
1173 var el = document.getElementById(id); | 547 var el = document.getElementById(id); |
1174 return el ? assertInstanceof(el, Element) : null; | 548 return el ? assertInstanceof(el, Element) : null; |
1175 } | 549 } |
1176 | 550 |
1177 /** | |
1178 * Add an accessible message to the page that will be announced to | |
1179 * users who have spoken feedback on, but will be invisible to all | |
1180 * other users. It's removed right away so it doesn't clutter the DOM. | |
1181 * @param {string} msg The text to be pronounced. | |
1182 */ | |
1183 function announceAccessibleMessage(msg) { | 551 function announceAccessibleMessage(msg) { |
1184 var element = document.createElement('div'); | 552 var element = document.createElement('div'); |
1185 element.setAttribute('aria-live', 'polite'); | 553 element.setAttribute('aria-live', 'polite'); |
1186 element.style.position = 'relative'; | 554 element.style.position = 'relative'; |
1187 element.style.left = '-9999px'; | 555 element.style.left = '-9999px'; |
1188 element.style.height = '0px'; | 556 element.style.height = '0px'; |
1189 element.innerText = msg; | 557 element.innerText = msg; |
1190 document.body.appendChild(element); | 558 document.body.appendChild(element); |
1191 window.setTimeout(function() { | 559 window.setTimeout(function() { |
1192 document.body.removeChild(element); | 560 document.body.removeChild(element); |
1193 }, 0); | 561 }, 0); |
1194 } | 562 } |
1195 | 563 |
1196 /** | |
1197 * Generates a CSS url string. | |
1198 * @param {string} s The URL to generate the CSS url for. | |
1199 * @return {string} The CSS url string. | |
1200 */ | |
1201 function url(s) { | 564 function url(s) { |
1202 // http://www.w3.org/TR/css3-values/#uris | |
1203 // Parentheses, commas, whitespace characters, single quotes (') and double | |
1204 // quotes (") appearing in a URI must be escaped with a backslash | |
1205 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | 565 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); |
1206 // WebKit has a bug when it comes to URLs that end with \ | |
1207 // https://bugs.webkit.org/show_bug.cgi?id=28885 | |
1208 if (/\\\\$/.test(s2)) { | 566 if (/\\\\$/.test(s2)) { |
1209 // Add a space to work around the WebKit bug. | |
1210 s2 += ' '; | 567 s2 += ' '; |
1211 } | 568 } |
1212 return 'url("' + s2 + '")'; | 569 return 'url("' + s2 + '")'; |
1213 } | 570 } |
1214 | 571 |
1215 /** | |
1216 * Parses query parameters from Location. | |
1217 * @param {Location} location The URL to generate the CSS url for. | |
1218 * @return {Object} Dictionary containing name value pairs for URL | |
1219 */ | |
1220 function parseQueryParams(location) { | 572 function parseQueryParams(location) { |
1221 var params = {}; | 573 var params = {}; |
1222 var query = unescape(location.search.substring(1)); | 574 var query = unescape(location.search.substring(1)); |
1223 var vars = query.split('&'); | 575 var vars = query.split('&'); |
1224 for (var i = 0; i < vars.length; i++) { | 576 for (var i = 0; i < vars.length; i++) { |
1225 var pair = vars[i].split('='); | 577 var pair = vars[i].split('='); |
1226 params[pair[0]] = pair[1]; | 578 params[pair[0]] = pair[1]; |
1227 } | 579 } |
1228 return params; | 580 return params; |
1229 } | 581 } |
1230 | 582 |
1231 /** | |
1232 * Creates a new URL by appending or replacing the given query key and value. | |
1233 * Not supporting URL with username and password. | |
1234 * @param {Location} location The original URL. | |
1235 * @param {string} key The query parameter name. | |
1236 * @param {string} value The query parameter value. | |
1237 * @return {string} The constructed new URL. | |
1238 */ | |
1239 function setQueryParam(location, key, value) { | 583 function setQueryParam(location, key, value) { |
1240 var query = parseQueryParams(location); | 584 var query = parseQueryParams(location); |
1241 query[encodeURIComponent(key)] = encodeURIComponent(value); | 585 query[encodeURIComponent(key)] = encodeURIComponent(value); |
1242 | |
1243 var newQuery = ''; | 586 var newQuery = ''; |
1244 for (var q in query) { | 587 for (var q in query) { |
1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; | 588 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; |
1246 } | 589 } |
1247 | |
1248 return location.origin + location.pathname + newQuery + location.hash; | 590 return location.origin + location.pathname + newQuery + location.hash; |
1249 } | 591 } |
1250 | 592 |
1251 /** | |
1252 * @param {Node} el A node to search for ancestors with |className|. | |
1253 * @param {string} className A class to search for. | |
1254 * @return {Element} A node with class of |className| or null if none is found. | |
1255 */ | |
1256 function findAncestorByClass(el, className) { | 593 function findAncestorByClass(el, className) { |
1257 return /** @type {Element} */(findAncestor(el, function(el) { | 594 return findAncestor(el, function(el) { |
1258 return el.classList && el.classList.contains(className); | 595 return el.classList && el.classList.contains(className); |
1259 })); | 596 }); |
1260 } | 597 } |
1261 | 598 |
1262 /** | |
1263 * Return the first ancestor for which the {@code predicate} returns true. | |
1264 * @param {Node} node The node to check. | |
1265 * @param {function(Node):boolean} predicate The function that tests the | |
1266 * nodes. | |
1267 * @return {Node} The found ancestor or null if not found. | |
1268 */ | |
1269 function findAncestor(node, predicate) { | 599 function findAncestor(node, predicate) { |
1270 var last = false; | 600 var last = false; |
1271 while (node != null && !(last = predicate(node))) { | 601 while (node != null && !(last = predicate(node))) { |
1272 node = node.parentNode; | 602 node = node.parentNode; |
1273 } | 603 } |
1274 return last ? node : null; | 604 return last ? node : null; |
1275 } | 605 } |
1276 | 606 |
1277 function swapDomNodes(a, b) { | 607 function swapDomNodes(a, b) { |
1278 var afterA = a.nextSibling; | 608 var afterA = a.nextSibling; |
1279 if (afterA == b) { | 609 if (afterA == b) { |
1280 swapDomNodes(b, a); | 610 swapDomNodes(b, a); |
1281 return; | 611 return; |
1282 } | 612 } |
1283 var aParent = a.parentNode; | 613 var aParent = a.parentNode; |
1284 b.parentNode.replaceChild(a, b); | 614 b.parentNode.replaceChild(a, b); |
1285 aParent.insertBefore(b, afterA); | 615 aParent.insertBefore(b, afterA); |
1286 } | 616 } |
1287 | 617 |
1288 /** | |
1289 * Disables text selection and dragging, with optional whitelist callbacks. | |
1290 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function | |
1291 * is defined and returns true, the onselectionstart event will be | |
1292 * surpressed. | |
1293 * @param {function(Event):boolean=} opt_allowDragStart Unless this function | |
1294 * is defined and returns true, the ondragstart event will be surpressed. | |
1295 */ | |
1296 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { | 618 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { |
1297 // Disable text selection. | |
1298 document.onselectstart = function(e) { | 619 document.onselectstart = function(e) { |
1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) | 620 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) e.prevent
Default(); |
1300 e.preventDefault(); | |
1301 }; | 621 }; |
1302 | |
1303 // Disable dragging. | |
1304 document.ondragstart = function(e) { | 622 document.ondragstart = function(e) { |
1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) | 623 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) e.preventDefa
ult(); |
1306 e.preventDefault(); | |
1307 }; | 624 }; |
1308 } | 625 } |
1309 | 626 |
1310 /** | |
1311 * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead. | |
1312 * Call this to stop clicks on <a href="#"> links from scrolling to the top of | |
1313 * the page (and possibly showing a # in the link). | |
1314 */ | |
1315 function preventDefaultOnPoundLinkClicks() { | 627 function preventDefaultOnPoundLinkClicks() { |
1316 document.addEventListener('click', function(e) { | 628 document.addEventListener('click', function(e) { |
1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { | 629 var anchor = findAncestor(e.target, function(el) { |
1318 return el.tagName == 'A'; | 630 return el.tagName == 'A'; |
1319 }); | 631 }); |
1320 // Use getAttribute() to prevent URL normalization. | 632 if (anchor && anchor.getAttribute('href') == '#') e.preventDefault(); |
1321 if (anchor && anchor.getAttribute('href') == '#') | |
1322 e.preventDefault(); | |
1323 }); | 633 }); |
1324 } | 634 } |
1325 | 635 |
1326 /** | |
1327 * Check the directionality of the page. | |
1328 * @return {boolean} True if Chrome is running an RTL UI. | |
1329 */ | |
1330 function isRTL() { | 636 function isRTL() { |
1331 return document.documentElement.dir == 'rtl'; | 637 return document.documentElement.dir == 'rtl'; |
1332 } | 638 } |
1333 | 639 |
1334 /** | |
1335 * Get an element that's known to exist by its ID. We use this instead of just | |
1336 * calling getElementById and not checking the result because this lets us | |
1337 * satisfy the JSCompiler type system. | |
1338 * @param {string} id The identifier name. | |
1339 * @return {!HTMLElement} the Element. | |
1340 */ | |
1341 function getRequiredElement(id) { | 640 function getRequiredElement(id) { |
1342 return assertInstanceof($(id), HTMLElement, | 641 return assertInstanceof($(id), HTMLElement, 'Missing required element: ' + id)
; |
1343 'Missing required element: ' + id); | 642 } |
1344 } | 643 |
1345 | |
1346 /** | |
1347 * Query an element that's known to exist by a selector. We use this instead of | |
1348 * just calling querySelector and not checking the result because this lets us | |
1349 * satisfy the JSCompiler type system. | |
1350 * @param {string} selectors CSS selectors to query the element. | |
1351 * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional | |
1352 * context object for querySelector. | |
1353 * @return {!HTMLElement} the Element. | |
1354 */ | |
1355 function queryRequiredElement(selectors, opt_context) { | 644 function queryRequiredElement(selectors, opt_context) { |
1356 var element = (opt_context || document).querySelector(selectors); | 645 var element = (opt_context || document).querySelector(selectors); |
1357 return assertInstanceof(element, HTMLElement, | 646 return assertInstanceof(element, HTMLElement, 'Missing required element: ' + s
electors); |
1358 'Missing required element: ' + selectors); | 647 } |
1359 } | 648 |
1360 | |
1361 // Handle click on a link. If the link points to a chrome: or file: url, then | |
1362 // call into the browser to do the navigation. | |
1363 document.addEventListener('click', function(e) { | 649 document.addEventListener('click', function(e) { |
1364 if (e.defaultPrevented) | 650 if (e.defaultPrevented) return; |
1365 return; | |
1366 | |
1367 var el = e.target; | 651 var el = e.target; |
1368 if (el.nodeType == Node.ELEMENT_NODE && | 652 if (el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector('A, A *')) { |
1369 el.webkitMatchesSelector('A, A *')) { | |
1370 while (el.tagName != 'A') { | 653 while (el.tagName != 'A') { |
1371 el = el.parentElement; | 654 el = el.parentElement; |
1372 } | 655 } |
1373 | 656 if ((el.protocol == 'file:' || el.protocol == 'about:') && (e.button == 0 ||
e.button == 1)) { |
1374 if ((el.protocol == 'file:' || el.protocol == 'about:') && | 657 chrome.send('navigateToUrl', [ el.href, el.target, e.button, e.altKey, e.c
trlKey, e.metaKey, e.shiftKey ]); |
1375 (e.button == 0 || e.button == 1)) { | |
1376 chrome.send('navigateToUrl', [ | |
1377 el.href, | |
1378 el.target, | |
1379 e.button, | |
1380 e.altKey, | |
1381 e.ctrlKey, | |
1382 e.metaKey, | |
1383 e.shiftKey | |
1384 ]); | |
1385 e.preventDefault(); | 658 e.preventDefault(); |
1386 } | 659 } |
1387 } | 660 } |
1388 }); | 661 }); |
1389 | 662 |
1390 /** | |
1391 * Creates a new URL which is the old URL with a GET param of key=value. | |
1392 * @param {string} url The base URL. There is not sanity checking on the URL so | |
1393 * it must be passed in a proper format. | |
1394 * @param {string} key The key of the param. | |
1395 * @param {string} value The value of the param. | |
1396 * @return {string} The new URL. | |
1397 */ | |
1398 function appendParam(url, key, value) { | 663 function appendParam(url, key, value) { |
1399 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 664 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); |
1400 | 665 if (url.indexOf('?') == -1) return url + '?' + param; |
1401 if (url.indexOf('?') == -1) | |
1402 return url + '?' + param; | |
1403 return url + '&' + param; | 666 return url + '&' + param; |
1404 } | 667 } |
1405 | 668 |
1406 /** | |
1407 * Creates an element of a specified type with a specified class name. | |
1408 * @param {string} type The node type. | |
1409 * @param {string} className The class name to use. | |
1410 * @return {Element} The created element. | |
1411 */ | |
1412 function createElementWithClassName(type, className) { | 669 function createElementWithClassName(type, className) { |
1413 var elm = document.createElement(type); | 670 var elm = document.createElement(type); |
1414 elm.className = className; | 671 elm.className = className; |
1415 return elm; | 672 return elm; |
1416 } | 673 } |
1417 | 674 |
1418 /** | |
1419 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | |
1420 * or when no paint happens during the animation). This function sets up | |
1421 * a timer and emulate the event if it is not fired when the timer expires. | |
1422 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | |
1423 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | |
1424 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | |
1425 * using the transitionDuration style value. | |
1426 */ | |
1427 function ensureTransitionEndEvent(el, opt_timeOut) { | 675 function ensureTransitionEndEvent(el, opt_timeOut) { |
1428 if (opt_timeOut === undefined) { | 676 if (opt_timeOut === undefined) { |
1429 var style = getComputedStyle(el); | 677 var style = getComputedStyle(el); |
1430 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 678 opt_timeOut = parseFloat(style.transitionDuration) * 1e3; |
1431 | |
1432 // Give an additional 50ms buffer for the animation to complete. | |
1433 opt_timeOut += 50; | 679 opt_timeOut += 50; |
1434 } | 680 } |
1435 | |
1436 var fired = false; | 681 var fired = false; |
1437 el.addEventListener('webkitTransitionEnd', function f(e) { | 682 el.addEventListener('webkitTransitionEnd', function f(e) { |
1438 el.removeEventListener('webkitTransitionEnd', f); | 683 el.removeEventListener('webkitTransitionEnd', f); |
1439 fired = true; | 684 fired = true; |
1440 }); | 685 }); |
1441 window.setTimeout(function() { | 686 window.setTimeout(function() { |
1442 if (!fired) | 687 if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); |
1443 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | |
1444 }, opt_timeOut); | 688 }, opt_timeOut); |
1445 } | 689 } |
1446 | 690 |
1447 /** | |
1448 * Alias for document.scrollTop getter. | |
1449 * @param {!HTMLDocument} doc The document node where information will be | |
1450 * queried from. | |
1451 * @return {number} The Y document scroll offset. | |
1452 */ | |
1453 function scrollTopForDocument(doc) { | 691 function scrollTopForDocument(doc) { |
1454 return doc.documentElement.scrollTop || doc.body.scrollTop; | 692 return doc.documentElement.scrollTop || doc.body.scrollTop; |
1455 } | 693 } |
1456 | 694 |
1457 /** | |
1458 * Alias for document.scrollTop setter. | |
1459 * @param {!HTMLDocument} doc The document node where information will be | |
1460 * queried from. | |
1461 * @param {number} value The target Y scroll offset. | |
1462 */ | |
1463 function setScrollTopForDocument(doc, value) { | 695 function setScrollTopForDocument(doc, value) { |
1464 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 696 doc.documentElement.scrollTop = doc.body.scrollTop = value; |
1465 } | 697 } |
1466 | 698 |
1467 /** | |
1468 * Alias for document.scrollLeft getter. | |
1469 * @param {!HTMLDocument} doc The document node where information will be | |
1470 * queried from. | |
1471 * @return {number} The X document scroll offset. | |
1472 */ | |
1473 function scrollLeftForDocument(doc) { | 699 function scrollLeftForDocument(doc) { |
1474 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 700 return doc.documentElement.scrollLeft || doc.body.scrollLeft; |
1475 } | 701 } |
1476 | 702 |
1477 /** | |
1478 * Alias for document.scrollLeft setter. | |
1479 * @param {!HTMLDocument} doc The document node where information will be | |
1480 * queried from. | |
1481 * @param {number} value The target X scroll offset. | |
1482 */ | |
1483 function setScrollLeftForDocument(doc, value) { | 703 function setScrollLeftForDocument(doc, value) { |
1484 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 704 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; |
1485 } | 705 } |
1486 | 706 |
1487 /** | |
1488 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | |
1489 * @param {string} original The original string. | |
1490 * @return {string} The string with all the characters mentioned above replaced. | |
1491 */ | |
1492 function HTMLEscape(original) { | 707 function HTMLEscape(original) { |
1493 return original.replace(/&/g, '&') | 708 return original.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '&g
t;').replace(/"/g, '"').replace(/'/g, '''); |
1494 .replace(/</g, '<') | 709 } |
1495 .replace(/>/g, '>') | 710 |
1496 .replace(/"/g, '"') | |
1497 .replace(/'/g, '''); | |
1498 } | |
1499 | |
1500 /** | |
1501 * Shortens the provided string (if necessary) to a string of length at most | |
1502 * |maxLength|. | |
1503 * @param {string} original The original string. | |
1504 * @param {number} maxLength The maximum length allowed for the string. | |
1505 * @return {string} The original string if its length does not exceed | |
1506 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | |
1507 * appended. | |
1508 */ | |
1509 function elide(original, maxLength) { | 711 function elide(original, maxLength) { |
1510 if (original.length <= maxLength) | 712 if (original.length <= maxLength) return original; |
1511 return original; | 713 return original.substring(0, maxLength - 1) + '…'; |
1512 return original.substring(0, maxLength - 1) + '\u2026'; | 714 } |
1513 } | 715 |
1514 | |
1515 /** | |
1516 * Quote a string so it can be used in a regular expression. | |
1517 * @param {string} str The source string. | |
1518 * @return {string} The escaped string. | |
1519 */ | |
1520 function quoteString(str) { | 716 function quoteString(str) { |
1521 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 717 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); |
1522 } | 718 } |
1523 | 719 |
1524 // <if expr="is_ios"> | 720 // <if expr="is_ios"> |
1525 // Polyfill 'key' in KeyboardEvent for iOS. | |
1526 // This function is not intended to be complete but should | |
1527 // be sufficient enough to have iOS work correctly while | |
1528 // it does not support key yet. | |
1529 if (!('key' in KeyboardEvent.prototype)) { | 721 if (!('key' in KeyboardEvent.prototype)) { |
1530 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 722 Object.defineProperty(KeyboardEvent.prototype, 'key', { |
1531 /** @this {KeyboardEvent} */ | 723 get: function() { |
1532 get: function () { | 724 if (this.keyCode >= 48 && this.keyCode <= 57) return String.fromCharCode(t
his.keyCode); |
1533 // 0-9 | 725 if (this.keyCode >= 65 && this.keyCode <= 90) { |
1534 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | |
1535 return String.fromCharCode(this.keyCode); | |
1536 | |
1537 // A-Z | |
1538 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | |
1539 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 726 var result = String.fromCharCode(this.keyCode).toLowerCase(); |
1540 if (this.shiftKey) | 727 if (this.shiftKey) result = result.toUpperCase(); |
1541 result = result.toUpperCase(); | |
1542 return result; | 728 return result; |
1543 } | 729 } |
1544 | 730 switch (this.keyCode) { |
1545 // Special characters | 731 case 8: |
1546 switch(this.keyCode) { | 732 return 'Backspace'; |
1547 case 0x08: return 'Backspace'; | 733 |
1548 case 0x09: return 'Tab'; | 734 case 9: |
1549 case 0x0d: return 'Enter'; | 735 return 'Tab'; |
1550 case 0x10: return 'Shift'; | 736 |
1551 case 0x11: return 'Control'; | 737 case 13: |
1552 case 0x12: return 'Alt'; | 738 return 'Enter'; |
1553 case 0x1b: return 'Escape'; | 739 |
1554 case 0x20: return ' '; | 740 case 16: |
1555 case 0x21: return 'PageUp'; | 741 return 'Shift'; |
1556 case 0x22: return 'PageDown'; | 742 |
1557 case 0x23: return 'End'; | 743 case 17: |
1558 case 0x24: return 'Home'; | 744 return 'Control'; |
1559 case 0x25: return 'ArrowLeft'; | 745 |
1560 case 0x26: return 'ArrowUp'; | 746 case 18: |
1561 case 0x27: return 'ArrowRight'; | 747 return 'Alt'; |
1562 case 0x28: return 'ArrowDown'; | 748 |
1563 case 0x2d: return 'Insert'; | 749 case 27: |
1564 case 0x2e: return 'Delete'; | 750 return 'Escape'; |
1565 case 0x5b: return 'Meta'; | 751 |
1566 case 0x70: return 'F1'; | 752 case 32: |
1567 case 0x71: return 'F2'; | 753 return ' '; |
1568 case 0x72: return 'F3'; | 754 |
1569 case 0x73: return 'F4'; | 755 case 33: |
1570 case 0x74: return 'F5'; | 756 return 'PageUp'; |
1571 case 0x75: return 'F6'; | 757 |
1572 case 0x76: return 'F7'; | 758 case 34: |
1573 case 0x77: return 'F8'; | 759 return 'PageDown'; |
1574 case 0x78: return 'F9'; | 760 |
1575 case 0x79: return 'F10'; | 761 case 35: |
1576 case 0x7a: return 'F11'; | 762 return 'End'; |
1577 case 0x7b: return 'F12'; | 763 |
1578 case 0xbb: return '='; | 764 case 36: |
1579 case 0xbd: return '-'; | 765 return 'Home'; |
1580 case 0xdb: return '['; | 766 |
1581 case 0xdd: return ']'; | 767 case 37: |
| 768 return 'ArrowLeft'; |
| 769 |
| 770 case 38: |
| 771 return 'ArrowUp'; |
| 772 |
| 773 case 39: |
| 774 return 'ArrowRight'; |
| 775 |
| 776 case 40: |
| 777 return 'ArrowDown'; |
| 778 |
| 779 case 45: |
| 780 return 'Insert'; |
| 781 |
| 782 case 46: |
| 783 return 'Delete'; |
| 784 |
| 785 case 91: |
| 786 return 'Meta'; |
| 787 |
| 788 case 112: |
| 789 return 'F1'; |
| 790 |
| 791 case 113: |
| 792 return 'F2'; |
| 793 |
| 794 case 114: |
| 795 return 'F3'; |
| 796 |
| 797 case 115: |
| 798 return 'F4'; |
| 799 |
| 800 case 116: |
| 801 return 'F5'; |
| 802 |
| 803 case 117: |
| 804 return 'F6'; |
| 805 |
| 806 case 118: |
| 807 return 'F7'; |
| 808 |
| 809 case 119: |
| 810 return 'F8'; |
| 811 |
| 812 case 120: |
| 813 return 'F9'; |
| 814 |
| 815 case 121: |
| 816 return 'F10'; |
| 817 |
| 818 case 122: |
| 819 return 'F11'; |
| 820 |
| 821 case 123: |
| 822 return 'F12'; |
| 823 |
| 824 case 187: |
| 825 return '='; |
| 826 |
| 827 case 189: |
| 828 return '-'; |
| 829 |
| 830 case 219: |
| 831 return '['; |
| 832 |
| 833 case 221: |
| 834 return ']'; |
1582 } | 835 } |
1583 return 'Unidentified'; | 836 return 'Unidentified'; |
1584 } | 837 } |
1585 }); | 838 }); |
1586 } else { | 839 } else { |
1587 window.console.log("KeyboardEvent.Key polyfill not required"); | 840 window.console.log("KeyboardEvent.Key polyfill not required"); |
1588 } | 841 } |
| 842 |
1589 // </if> /* is_ios */ | 843 // </if> /* is_ios */ |
1590 /** | 844 Polymer.IronResizableBehavior = { |
1591 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | 845 properties: { |
1592 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | 846 _parentResizable: { |
1593 * size or hidden state of their children) and "resizables" (elements that nee
d to be | 847 type: Object, |
1594 * notified when they are resized or un-hidden by their parents in order to ta
ke | 848 observer: '_parentResizableChanged' |
1595 * action on their new measurements). | 849 }, |
1596 * | 850 _notifyingDescendant: { |
1597 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | 851 type: Boolean, |
1598 * their element definition and listen for the `iron-resize` event on themselv
es. | 852 value: false |
1599 * This event will be fired when they become showing after having been hidden, | 853 } |
1600 * when they are resized explicitly by another resizable, or when the window h
as been | 854 }, |
1601 * resized. | 855 listeners: { |
1602 * | 856 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
1603 * Note, the `iron-resize` event is non-bubbling. | 857 }, |
1604 * | 858 created: function() { |
1605 * @polymerBehavior Polymer.IronResizableBehavior | 859 this._interestedResizables = []; |
1606 * @demo demo/index.html | 860 this._boundNotifyResize = this.notifyResize.bind(this); |
1607 **/ | 861 }, |
1608 Polymer.IronResizableBehavior = { | 862 attached: function() { |
| 863 this.fire('iron-request-resize-notifications', null, { |
| 864 node: this, |
| 865 bubbles: true, |
| 866 cancelable: true |
| 867 }); |
| 868 if (!this._parentResizable) { |
| 869 window.addEventListener('resize', this._boundNotifyResize); |
| 870 this.notifyResize(); |
| 871 } |
| 872 }, |
| 873 detached: function() { |
| 874 if (this._parentResizable) { |
| 875 this._parentResizable.stopResizeNotificationsFor(this); |
| 876 } else { |
| 877 window.removeEventListener('resize', this._boundNotifyResize); |
| 878 } |
| 879 this._parentResizable = null; |
| 880 }, |
| 881 notifyResize: function() { |
| 882 if (!this.isAttached) { |
| 883 return; |
| 884 } |
| 885 this._interestedResizables.forEach(function(resizable) { |
| 886 if (this.resizerShouldNotify(resizable)) { |
| 887 this._notifyDescendant(resizable); |
| 888 } |
| 889 }, this); |
| 890 this._fireResize(); |
| 891 }, |
| 892 assignParentResizable: function(parentResizable) { |
| 893 this._parentResizable = parentResizable; |
| 894 }, |
| 895 stopResizeNotificationsFor: function(target) { |
| 896 var index = this._interestedResizables.indexOf(target); |
| 897 if (index > -1) { |
| 898 this._interestedResizables.splice(index, 1); |
| 899 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
| 900 } |
| 901 }, |
| 902 resizerShouldNotify: function(element) { |
| 903 return true; |
| 904 }, |
| 905 _onDescendantIronResize: function(event) { |
| 906 if (this._notifyingDescendant) { |
| 907 event.stopPropagation(); |
| 908 return; |
| 909 } |
| 910 if (!Polymer.Settings.useShadow) { |
| 911 this._fireResize(); |
| 912 } |
| 913 }, |
| 914 _fireResize: function() { |
| 915 this.fire('iron-resize', null, { |
| 916 node: this, |
| 917 bubbles: false |
| 918 }); |
| 919 }, |
| 920 _onIronRequestResizeNotifications: function(event) { |
| 921 var target = event.path ? event.path[0] : event.target; |
| 922 if (target === this) { |
| 923 return; |
| 924 } |
| 925 if (this._interestedResizables.indexOf(target) === -1) { |
| 926 this._interestedResizables.push(target); |
| 927 this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| 928 } |
| 929 target.assignParentResizable(this); |
| 930 this._notifyDescendant(target); |
| 931 event.stopPropagation(); |
| 932 }, |
| 933 _parentResizableChanged: function(parentResizable) { |
| 934 if (parentResizable) { |
| 935 window.removeEventListener('resize', this._boundNotifyResize); |
| 936 } |
| 937 }, |
| 938 _notifyDescendant: function(descendant) { |
| 939 if (!this.isAttached) { |
| 940 return; |
| 941 } |
| 942 this._notifyingDescendant = true; |
| 943 descendant.notifyResize(); |
| 944 this._notifyingDescendant = false; |
| 945 } |
| 946 }; |
| 947 |
| 948 (function() { |
| 949 'use strict'; |
| 950 var KEY_IDENTIFIER = { |
| 951 'U+0008': 'backspace', |
| 952 'U+0009': 'tab', |
| 953 'U+001B': 'esc', |
| 954 'U+0020': 'space', |
| 955 'U+007F': 'del' |
| 956 }; |
| 957 var KEY_CODE = { |
| 958 8: 'backspace', |
| 959 9: 'tab', |
| 960 13: 'enter', |
| 961 27: 'esc', |
| 962 33: 'pageup', |
| 963 34: 'pagedown', |
| 964 35: 'end', |
| 965 36: 'home', |
| 966 32: 'space', |
| 967 37: 'left', |
| 968 38: 'up', |
| 969 39: 'right', |
| 970 40: 'down', |
| 971 46: 'del', |
| 972 106: '*' |
| 973 }; |
| 974 var MODIFIER_KEYS = { |
| 975 shift: 'shiftKey', |
| 976 ctrl: 'ctrlKey', |
| 977 alt: 'altKey', |
| 978 meta: 'metaKey' |
| 979 }; |
| 980 var KEY_CHAR = /[a-z0-9*]/; |
| 981 var IDENT_CHAR = /U\+/; |
| 982 var ARROW_KEY = /^arrow/; |
| 983 var SPACE_KEY = /^space(bar)?/; |
| 984 var ESC_KEY = /^escape$/; |
| 985 function transformKey(key, noSpecialChars) { |
| 986 var validKey = ''; |
| 987 if (key) { |
| 988 var lKey = key.toLowerCase(); |
| 989 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 990 validKey = 'space'; |
| 991 } else if (ESC_KEY.test(lKey)) { |
| 992 validKey = 'esc'; |
| 993 } else if (lKey.length == 1) { |
| 994 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 995 validKey = lKey; |
| 996 } |
| 997 } else if (ARROW_KEY.test(lKey)) { |
| 998 validKey = lKey.replace('arrow', ''); |
| 999 } else if (lKey == 'multiply') { |
| 1000 validKey = '*'; |
| 1001 } else { |
| 1002 validKey = lKey; |
| 1003 } |
| 1004 } |
| 1005 return validKey; |
| 1006 } |
| 1007 function transformKeyIdentifier(keyIdent) { |
| 1008 var validKey = ''; |
| 1009 if (keyIdent) { |
| 1010 if (keyIdent in KEY_IDENTIFIER) { |
| 1011 validKey = KEY_IDENTIFIER[keyIdent]; |
| 1012 } else if (IDENT_CHAR.test(keyIdent)) { |
| 1013 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 1014 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 1015 } else { |
| 1016 validKey = keyIdent.toLowerCase(); |
| 1017 } |
| 1018 } |
| 1019 return validKey; |
| 1020 } |
| 1021 function transformKeyCode(keyCode) { |
| 1022 var validKey = ''; |
| 1023 if (Number(keyCode)) { |
| 1024 if (keyCode >= 65 && keyCode <= 90) { |
| 1025 validKey = String.fromCharCode(32 + keyCode); |
| 1026 } else if (keyCode >= 112 && keyCode <= 123) { |
| 1027 validKey = 'f' + (keyCode - 112); |
| 1028 } else if (keyCode >= 48 && keyCode <= 57) { |
| 1029 validKey = String(keyCode - 48); |
| 1030 } else if (keyCode >= 96 && keyCode <= 105) { |
| 1031 validKey = String(keyCode - 96); |
| 1032 } else { |
| 1033 validKey = KEY_CODE[keyCode]; |
| 1034 } |
| 1035 } |
| 1036 return validKey; |
| 1037 } |
| 1038 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 1039 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier(
keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke
yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| 1040 } |
| 1041 function keyComboMatchesEvent(keyCombo, event) { |
| 1042 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 1043 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift
Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event
.altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey); |
| 1044 } |
| 1045 function parseKeyComboString(keyComboString) { |
| 1046 if (keyComboString.length === 1) { |
| 1047 return { |
| 1048 combo: keyComboString, |
| 1049 key: keyComboString, |
| 1050 event: 'keydown' |
| 1051 }; |
| 1052 } |
| 1053 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar
t) { |
| 1054 var eventParts = keyComboPart.split(':'); |
| 1055 var keyName = eventParts[0]; |
| 1056 var event = eventParts[1]; |
| 1057 if (keyName in MODIFIER_KEYS) { |
| 1058 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 1059 parsedKeyCombo.hasModifiers = true; |
| 1060 } else { |
| 1061 parsedKeyCombo.key = keyName; |
| 1062 parsedKeyCombo.event = event || 'keydown'; |
| 1063 } |
| 1064 return parsedKeyCombo; |
| 1065 }, { |
| 1066 combo: keyComboString.split(':').shift() |
| 1067 }); |
| 1068 } |
| 1069 function parseEventString(eventString) { |
| 1070 return eventString.trim().split(' ').map(function(keyComboString) { |
| 1071 return parseKeyComboString(keyComboString); |
| 1072 }); |
| 1073 } |
| 1074 Polymer.IronA11yKeysBehavior = { |
1609 properties: { | 1075 properties: { |
1610 /** | 1076 keyEventTarget: { |
1611 * The closest ancestor element that implements `IronResizableBehavior`. | |
1612 */ | |
1613 _parentResizable: { | |
1614 type: Object, | 1077 type: Object, |
1615 observer: '_parentResizableChanged' | 1078 value: function() { |
1616 }, | 1079 return this; |
1617 | 1080 } |
1618 /** | 1081 }, |
1619 * True if this element is currently notifying its descedant elements of | 1082 stopKeyboardEventPropagation: { |
1620 * resize. | |
1621 */ | |
1622 _notifyingDescendant: { | |
1623 type: Boolean, | 1083 type: Boolean, |
1624 value: false | 1084 value: false |
1625 } | 1085 }, |
1626 }, | 1086 _boundKeyHandlers: { |
1627 | 1087 type: Array, |
1628 listeners: { | 1088 value: function() { |
1629 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | 1089 return []; |
1630 }, | 1090 } |
1631 | 1091 }, |
1632 created: function() { | 1092 _imperativeKeyBindings: { |
1633 // We don't really need property effects on these, and also we want them | 1093 type: Object, |
1634 // to be created before the `_parentResizable` observer fires: | 1094 value: function() { |
1635 this._interestedResizables = []; | 1095 return {}; |
1636 this._boundNotifyResize = this.notifyResize.bind(this); | 1096 } |
1637 }, | 1097 } |
1638 | 1098 }, |
| 1099 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], |
| 1100 keyBindings: {}, |
| 1101 registered: function() { |
| 1102 this._prepKeyBindings(); |
| 1103 }, |
1639 attached: function() { | 1104 attached: function() { |
1640 this.fire('iron-request-resize-notifications', null, { | 1105 this._listenKeyEventListeners(); |
1641 node: this, | 1106 }, |
1642 bubbles: true, | 1107 detached: function() { |
| 1108 this._unlistenKeyEventListeners(); |
| 1109 }, |
| 1110 addOwnKeyBinding: function(eventString, handlerName) { |
| 1111 this._imperativeKeyBindings[eventString] = handlerName; |
| 1112 this._prepKeyBindings(); |
| 1113 this._resetKeyEventListeners(); |
| 1114 }, |
| 1115 removeOwnKeyBindings: function() { |
| 1116 this._imperativeKeyBindings = {}; |
| 1117 this._prepKeyBindings(); |
| 1118 this._resetKeyEventListeners(); |
| 1119 }, |
| 1120 keyboardEventMatchesKeys: function(event, eventString) { |
| 1121 var keyCombos = parseEventString(eventString); |
| 1122 for (var i = 0; i < keyCombos.length; ++i) { |
| 1123 if (keyComboMatchesEvent(keyCombos[i], event)) { |
| 1124 return true; |
| 1125 } |
| 1126 } |
| 1127 return false; |
| 1128 }, |
| 1129 _collectKeyBindings: function() { |
| 1130 var keyBindings = this.behaviors.map(function(behavior) { |
| 1131 return behavior.keyBindings; |
| 1132 }); |
| 1133 if (keyBindings.indexOf(this.keyBindings) === -1) { |
| 1134 keyBindings.push(this.keyBindings); |
| 1135 } |
| 1136 return keyBindings; |
| 1137 }, |
| 1138 _prepKeyBindings: function() { |
| 1139 this._keyBindings = {}; |
| 1140 this._collectKeyBindings().forEach(function(keyBindings) { |
| 1141 for (var eventString in keyBindings) { |
| 1142 this._addKeyBinding(eventString, keyBindings[eventString]); |
| 1143 } |
| 1144 }, this); |
| 1145 for (var eventString in this._imperativeKeyBindings) { |
| 1146 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString
]); |
| 1147 } |
| 1148 for (var eventName in this._keyBindings) { |
| 1149 this._keyBindings[eventName].sort(function(kb1, kb2) { |
| 1150 var b1 = kb1[0].hasModifiers; |
| 1151 var b2 = kb2[0].hasModifiers; |
| 1152 return b1 === b2 ? 0 : b1 ? -1 : 1; |
| 1153 }); |
| 1154 } |
| 1155 }, |
| 1156 _addKeyBinding: function(eventString, handlerName) { |
| 1157 parseEventString(eventString).forEach(function(keyCombo) { |
| 1158 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] ||
[]; |
| 1159 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); |
| 1160 }, this); |
| 1161 }, |
| 1162 _resetKeyEventListeners: function() { |
| 1163 this._unlistenKeyEventListeners(); |
| 1164 if (this.isAttached) { |
| 1165 this._listenKeyEventListeners(); |
| 1166 } |
| 1167 }, |
| 1168 _listenKeyEventListeners: function() { |
| 1169 if (!this.keyEventTarget) { |
| 1170 return; |
| 1171 } |
| 1172 Object.keys(this._keyBindings).forEach(function(eventName) { |
| 1173 var keyBindings = this._keyBindings[eventName]; |
| 1174 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
| 1175 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa
ndler ]); |
| 1176 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| 1177 }, this); |
| 1178 }, |
| 1179 _unlistenKeyEventListeners: function() { |
| 1180 var keyHandlerTuple; |
| 1181 var keyEventTarget; |
| 1182 var eventName; |
| 1183 var boundKeyHandler; |
| 1184 while (this._boundKeyHandlers.length) { |
| 1185 keyHandlerTuple = this._boundKeyHandlers.pop(); |
| 1186 keyEventTarget = keyHandlerTuple[0]; |
| 1187 eventName = keyHandlerTuple[1]; |
| 1188 boundKeyHandler = keyHandlerTuple[2]; |
| 1189 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| 1190 } |
| 1191 }, |
| 1192 _onKeyBindingEvent: function(keyBindings, event) { |
| 1193 if (this.stopKeyboardEventPropagation) { |
| 1194 event.stopPropagation(); |
| 1195 } |
| 1196 if (event.defaultPrevented) { |
| 1197 return; |
| 1198 } |
| 1199 for (var i = 0; i < keyBindings.length; i++) { |
| 1200 var keyCombo = keyBindings[i][0]; |
| 1201 var handlerName = keyBindings[i][1]; |
| 1202 if (keyComboMatchesEvent(keyCombo, event)) { |
| 1203 this._triggerKeyHandler(keyCombo, handlerName, event); |
| 1204 if (event.defaultPrevented) { |
| 1205 return; |
| 1206 } |
| 1207 } |
| 1208 } |
| 1209 }, |
| 1210 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 1211 var detail = Object.create(keyCombo); |
| 1212 detail.keyboardEvent = keyboardEvent; |
| 1213 var event = new CustomEvent(keyCombo.event, { |
| 1214 detail: detail, |
1643 cancelable: true | 1215 cancelable: true |
1644 }); | 1216 }); |
1645 | 1217 this[handlerName].call(this, event); |
1646 if (!this._parentResizable) { | 1218 if (event.defaultPrevented) { |
1647 window.addEventListener('resize', this._boundNotifyResize); | 1219 keyboardEvent.preventDefault(); |
1648 this.notifyResize(); | 1220 } |
1649 } | |
1650 }, | |
1651 | |
1652 detached: function() { | |
1653 if (this._parentResizable) { | |
1654 this._parentResizable.stopResizeNotificationsFor(this); | |
1655 } else { | |
1656 window.removeEventListener('resize', this._boundNotifyResize); | |
1657 } | |
1658 | |
1659 this._parentResizable = null; | |
1660 }, | |
1661 | |
1662 /** | |
1663 * Can be called to manually notify a resizable and its descendant | |
1664 * resizables of a resize change. | |
1665 */ | |
1666 notifyResize: function() { | |
1667 if (!this.isAttached) { | |
1668 return; | |
1669 } | |
1670 | |
1671 this._interestedResizables.forEach(function(resizable) { | |
1672 if (this.resizerShouldNotify(resizable)) { | |
1673 this._notifyDescendant(resizable); | |
1674 } | |
1675 }, this); | |
1676 | |
1677 this._fireResize(); | |
1678 }, | |
1679 | |
1680 /** | |
1681 * Used to assign the closest resizable ancestor to this resizable | |
1682 * if the ancestor detects a request for notifications. | |
1683 */ | |
1684 assignParentResizable: function(parentResizable) { | |
1685 this._parentResizable = parentResizable; | |
1686 }, | |
1687 | |
1688 /** | |
1689 * Used to remove a resizable descendant from the list of descendants | |
1690 * that should be notified of a resize change. | |
1691 */ | |
1692 stopResizeNotificationsFor: function(target) { | |
1693 var index = this._interestedResizables.indexOf(target); | |
1694 | |
1695 if (index > -1) { | |
1696 this._interestedResizables.splice(index, 1); | |
1697 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | |
1698 } | |
1699 }, | |
1700 | |
1701 /** | |
1702 * This method can be overridden to filter nested elements that should or | |
1703 * should not be notified by the current element. Return true if an element | |
1704 * should be notified, or false if it should not be notified. | |
1705 * | |
1706 * @param {HTMLElement} element A candidate descendant element that | |
1707 * implements `IronResizableBehavior`. | |
1708 * @return {boolean} True if the `element` should be notified of resize. | |
1709 */ | |
1710 resizerShouldNotify: function(element) { return true; }, | |
1711 | |
1712 _onDescendantIronResize: function(event) { | |
1713 if (this._notifyingDescendant) { | |
1714 event.stopPropagation(); | |
1715 return; | |
1716 } | |
1717 | |
1718 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
1719 // otherwise non-bubbling event "just work." We do it manually here for | |
1720 // the case where Polymer is not using shadow roots for whatever reason: | |
1721 if (!Polymer.Settings.useShadow) { | |
1722 this._fireResize(); | |
1723 } | |
1724 }, | |
1725 | |
1726 _fireResize: function() { | |
1727 this.fire('iron-resize', null, { | |
1728 node: this, | |
1729 bubbles: false | |
1730 }); | |
1731 }, | |
1732 | |
1733 _onIronRequestResizeNotifications: function(event) { | |
1734 var target = event.path ? event.path[0] : event.target; | |
1735 | |
1736 if (target === this) { | |
1737 return; | |
1738 } | |
1739 | |
1740 if (this._interestedResizables.indexOf(target) === -1) { | |
1741 this._interestedResizables.push(target); | |
1742 this.listen(target, 'iron-resize', '_onDescendantIronResize'); | |
1743 } | |
1744 | |
1745 target.assignParentResizable(this); | |
1746 this._notifyDescendant(target); | |
1747 | |
1748 event.stopPropagation(); | |
1749 }, | |
1750 | |
1751 _parentResizableChanged: function(parentResizable) { | |
1752 if (parentResizable) { | |
1753 window.removeEventListener('resize', this._boundNotifyResize); | |
1754 } | |
1755 }, | |
1756 | |
1757 _notifyDescendant: function(descendant) { | |
1758 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
1759 // important not to notify them if the parent is not attached yet (or | |
1760 // else they will get redundantly notified when the parent attaches). | |
1761 if (!this.isAttached) { | |
1762 return; | |
1763 } | |
1764 | |
1765 this._notifyingDescendant = true; | |
1766 descendant.notifyResize(); | |
1767 this._notifyingDescendant = false; | |
1768 } | 1221 } |
1769 }; | 1222 }; |
| 1223 })(); |
| 1224 |
| 1225 Polymer.IronScrollTargetBehavior = { |
| 1226 properties: { |
| 1227 scrollTarget: { |
| 1228 type: HTMLElement, |
| 1229 value: function() { |
| 1230 return this._defaultScrollTarget; |
| 1231 } |
| 1232 } |
| 1233 }, |
| 1234 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ], |
| 1235 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 1236 var eventTarget; |
| 1237 if (this._oldScrollTarget) { |
| 1238 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro
llTarget; |
| 1239 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 1240 this._oldScrollTarget = null; |
| 1241 } |
| 1242 if (!isAttached) { |
| 1243 return; |
| 1244 } |
| 1245 if (scrollTarget === 'document') { |
| 1246 this.scrollTarget = this._doc; |
| 1247 } else if (typeof scrollTarget === 'string') { |
| 1248 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer.
dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 1249 } else if (this._isValidScrollTarget()) { |
| 1250 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 1251 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler
.bind(this); |
| 1252 this._oldScrollTarget = scrollTarget; |
| 1253 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 1254 } |
| 1255 }, |
| 1256 _scrollHandler: function scrollHandler() {}, |
| 1257 get _defaultScrollTarget() { |
| 1258 return this._doc; |
| 1259 }, |
| 1260 get _doc() { |
| 1261 return this.ownerDocument.documentElement; |
| 1262 }, |
| 1263 get _scrollTop() { |
| 1264 if (this._isValidScrollTarget()) { |
| 1265 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT
arget.scrollTop; |
| 1266 } |
| 1267 return 0; |
| 1268 }, |
| 1269 get _scrollLeft() { |
| 1270 if (this._isValidScrollTarget()) { |
| 1271 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT
arget.scrollLeft; |
| 1272 } |
| 1273 return 0; |
| 1274 }, |
| 1275 set _scrollTop(top) { |
| 1276 if (this.scrollTarget === this._doc) { |
| 1277 window.scrollTo(window.pageXOffset, top); |
| 1278 } else if (this._isValidScrollTarget()) { |
| 1279 this.scrollTarget.scrollTop = top; |
| 1280 } |
| 1281 }, |
| 1282 set _scrollLeft(left) { |
| 1283 if (this.scrollTarget === this._doc) { |
| 1284 window.scrollTo(left, window.pageYOffset); |
| 1285 } else if (this._isValidScrollTarget()) { |
| 1286 this.scrollTarget.scrollLeft = left; |
| 1287 } |
| 1288 }, |
| 1289 scroll: function(left, top) { |
| 1290 if (this.scrollTarget === this._doc) { |
| 1291 window.scrollTo(left, top); |
| 1292 } else if (this._isValidScrollTarget()) { |
| 1293 this.scrollTarget.scrollLeft = left; |
| 1294 this.scrollTarget.scrollTop = top; |
| 1295 } |
| 1296 }, |
| 1297 get _scrollTargetWidth() { |
| 1298 if (this._isValidScrollTarget()) { |
| 1299 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa
rget.offsetWidth; |
| 1300 } |
| 1301 return 0; |
| 1302 }, |
| 1303 get _scrollTargetHeight() { |
| 1304 if (this._isValidScrollTarget()) { |
| 1305 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT
arget.offsetHeight; |
| 1306 } |
| 1307 return 0; |
| 1308 }, |
| 1309 _isValidScrollTarget: function() { |
| 1310 return this.scrollTarget instanceof HTMLElement; |
| 1311 } |
| 1312 }; |
| 1313 |
1770 (function() { | 1314 (function() { |
1771 'use strict'; | |
1772 | |
1773 /** | |
1774 * Chrome uses an older version of DOM Level 3 Keyboard Events | |
1775 * | |
1776 * Most keys are labeled as text, but some are Unicode codepoints. | |
1777 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | |
1778 */ | |
1779 var KEY_IDENTIFIER = { | |
1780 'U+0008': 'backspace', | |
1781 'U+0009': 'tab', | |
1782 'U+001B': 'esc', | |
1783 'U+0020': 'space', | |
1784 'U+007F': 'del' | |
1785 }; | |
1786 | |
1787 /** | |
1788 * Special table for KeyboardEvent.keyCode. | |
1789 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | |
1790 * than that. | |
1791 * | |
1792 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
1793 */ | |
1794 var KEY_CODE = { | |
1795 8: 'backspace', | |
1796 9: 'tab', | |
1797 13: 'enter', | |
1798 27: 'esc', | |
1799 33: 'pageup', | |
1800 34: 'pagedown', | |
1801 35: 'end', | |
1802 36: 'home', | |
1803 32: 'space', | |
1804 37: 'left', | |
1805 38: 'up', | |
1806 39: 'right', | |
1807 40: 'down', | |
1808 46: 'del', | |
1809 106: '*' | |
1810 }; | |
1811 | |
1812 /** | |
1813 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
1814 * combo string to the property name that references those same keys | |
1815 * in a KeyboardEvent instance. | |
1816 */ | |
1817 var MODIFIER_KEYS = { | |
1818 'shift': 'shiftKey', | |
1819 'ctrl': 'ctrlKey', | |
1820 'alt': 'altKey', | |
1821 'meta': 'metaKey' | |
1822 }; | |
1823 | |
1824 /** | |
1825 * KeyboardEvent.key is mostly represented by printable character made by | |
1826 * the keyboard, with unprintable keys labeled nicely. | |
1827 * | |
1828 * However, on OS X, Alt+char can make a Unicode character that follows an | |
1829 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
1830 */ | |
1831 var KEY_CHAR = /[a-z0-9*]/; | |
1832 | |
1833 /** | |
1834 * Matches a keyIdentifier string. | |
1835 */ | |
1836 var IDENT_CHAR = /U\+/; | |
1837 | |
1838 /** | |
1839 * Matches arrow keys in Gecko 27.0+ | |
1840 */ | |
1841 var ARROW_KEY = /^arrow/; | |
1842 | |
1843 /** | |
1844 * Matches space keys everywhere (notably including IE10's exceptional name | |
1845 * `spacebar`). | |
1846 */ | |
1847 var SPACE_KEY = /^space(bar)?/; | |
1848 | |
1849 /** | |
1850 * Matches ESC key. | |
1851 * | |
1852 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
1853 */ | |
1854 var ESC_KEY = /^escape$/; | |
1855 | |
1856 /** | |
1857 * Transforms the key. | |
1858 * @param {string} key The KeyBoardEvent.key | |
1859 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
1860 * alpha-numeric characters. | |
1861 */ | |
1862 function transformKey(key, noSpecialChars) { | |
1863 var validKey = ''; | |
1864 if (key) { | |
1865 var lKey = key.toLowerCase(); | |
1866 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
1867 validKey = 'space'; | |
1868 } else if (ESC_KEY.test(lKey)) { | |
1869 validKey = 'esc'; | |
1870 } else if (lKey.length == 1) { | |
1871 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
1872 validKey = lKey; | |
1873 } | |
1874 } else if (ARROW_KEY.test(lKey)) { | |
1875 validKey = lKey.replace('arrow', ''); | |
1876 } else if (lKey == 'multiply') { | |
1877 // numpad '*' can map to Multiply on IE/Windows | |
1878 validKey = '*'; | |
1879 } else { | |
1880 validKey = lKey; | |
1881 } | |
1882 } | |
1883 return validKey; | |
1884 } | |
1885 | |
1886 function transformKeyIdentifier(keyIdent) { | |
1887 var validKey = ''; | |
1888 if (keyIdent) { | |
1889 if (keyIdent in KEY_IDENTIFIER) { | |
1890 validKey = KEY_IDENTIFIER[keyIdent]; | |
1891 } else if (IDENT_CHAR.test(keyIdent)) { | |
1892 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
1893 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
1894 } else { | |
1895 validKey = keyIdent.toLowerCase(); | |
1896 } | |
1897 } | |
1898 return validKey; | |
1899 } | |
1900 | |
1901 function transformKeyCode(keyCode) { | |
1902 var validKey = ''; | |
1903 if (Number(keyCode)) { | |
1904 if (keyCode >= 65 && keyCode <= 90) { | |
1905 // ascii a-z | |
1906 // lowercase is 32 offset from uppercase | |
1907 validKey = String.fromCharCode(32 + keyCode); | |
1908 } else if (keyCode >= 112 && keyCode <= 123) { | |
1909 // function keys f1-f12 | |
1910 validKey = 'f' + (keyCode - 112); | |
1911 } else if (keyCode >= 48 && keyCode <= 57) { | |
1912 // top 0-9 keys | |
1913 validKey = String(keyCode - 48); | |
1914 } else if (keyCode >= 96 && keyCode <= 105) { | |
1915 // num pad 0-9 | |
1916 validKey = String(keyCode - 96); | |
1917 } else { | |
1918 validKey = KEY_CODE[keyCode]; | |
1919 } | |
1920 } | |
1921 return validKey; | |
1922 } | |
1923 | |
1924 /** | |
1925 * Calculates the normalized key for a KeyboardEvent. | |
1926 * @param {KeyboardEvent} keyEvent | |
1927 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
1928 * transformation to alpha-numeric chars. This is useful with key | |
1929 * combinations like shift + 2, which on FF for MacOS produces | |
1930 * keyEvent.key = @ | |
1931 * To get 2 returned, set noSpecialChars = true | |
1932 * To get @ returned, set noSpecialChars = false | |
1933 */ | |
1934 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
1935 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
1936 // .detail.key to support artificial keyboard events. | |
1937 return transformKey(keyEvent.key, noSpecialChars) || | |
1938 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
1939 transformKeyCode(keyEvent.keyCode) || | |
1940 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | |
1941 } | |
1942 | |
1943 function keyComboMatchesEvent(keyCombo, event) { | |
1944 // For combos with modifiers we support only alpha-numeric keys | |
1945 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
1946 return keyEvent === keyCombo.key && | |
1947 (!keyCombo.hasModifiers || ( | |
1948 !!event.shiftKey === !!keyCombo.shiftKey && | |
1949 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
1950 !!event.altKey === !!keyCombo.altKey && | |
1951 !!event.metaKey === !!keyCombo.metaKey) | |
1952 ); | |
1953 } | |
1954 | |
1955 function parseKeyComboString(keyComboString) { | |
1956 if (keyComboString.length === 1) { | |
1957 return { | |
1958 combo: keyComboString, | |
1959 key: keyComboString, | |
1960 event: 'keydown' | |
1961 }; | |
1962 } | |
1963 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
1964 var eventParts = keyComboPart.split(':'); | |
1965 var keyName = eventParts[0]; | |
1966 var event = eventParts[1]; | |
1967 | |
1968 if (keyName in MODIFIER_KEYS) { | |
1969 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
1970 parsedKeyCombo.hasModifiers = true; | |
1971 } else { | |
1972 parsedKeyCombo.key = keyName; | |
1973 parsedKeyCombo.event = event || 'keydown'; | |
1974 } | |
1975 | |
1976 return parsedKeyCombo; | |
1977 }, { | |
1978 combo: keyComboString.split(':').shift() | |
1979 }); | |
1980 } | |
1981 | |
1982 function parseEventString(eventString) { | |
1983 return eventString.trim().split(' ').map(function(keyComboString) { | |
1984 return parseKeyComboString(keyComboString); | |
1985 }); | |
1986 } | |
1987 | |
1988 /** | |
1989 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
1990 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
1991 * The element takes care of browser differences with respect to Keyboard ev
ents | |
1992 * and uses an expressive syntax to filter key presses. | |
1993 * | |
1994 * Use the `keyBindings` prototype property to express what combination of k
eys | |
1995 * will trigger the callback. A key binding has the format | |
1996 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
1997 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
1998 * | |
1999 * keyBindings: { | |
2000 * 'space': '_onKeydown', // same as 'space:keydown' | |
2001 * 'shift+tab': '_onKeydown', | |
2002 * 'enter:keypress': '_onKeypress', | |
2003 * 'esc:keyup': '_onKeyup' | |
2004 * } | |
2005 * | |
2006 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
2007 * | |
2008 * _onKeydown: function(event) { | |
2009 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
2010 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
2011 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
2012 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
2013 * } | |
2014 * | |
2015 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
2016 * node. | |
2017 * | |
2018 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
2019 * for an example. | |
2020 * | |
2021 * @demo demo/index.html | |
2022 * @polymerBehavior | |
2023 */ | |
2024 Polymer.IronA11yKeysBehavior = { | |
2025 properties: { | |
2026 /** | |
2027 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
2028 * `null` to disable the listeners. | |
2029 * @type {?EventTarget} | |
2030 */ | |
2031 keyEventTarget: { | |
2032 type: Object, | |
2033 value: function() { | |
2034 return this; | |
2035 } | |
2036 }, | |
2037 | |
2038 /** | |
2039 * If true, this property will cause the implementing element to | |
2040 * automatically stop propagation on any handled KeyboardEvents. | |
2041 */ | |
2042 stopKeyboardEventPropagation: { | |
2043 type: Boolean, | |
2044 value: false | |
2045 }, | |
2046 | |
2047 _boundKeyHandlers: { | |
2048 type: Array, | |
2049 value: function() { | |
2050 return []; | |
2051 } | |
2052 }, | |
2053 | |
2054 // We use this due to a limitation in IE10 where instances will have | |
2055 // own properties of everything on the "prototype". | |
2056 _imperativeKeyBindings: { | |
2057 type: Object, | |
2058 value: function() { | |
2059 return {}; | |
2060 } | |
2061 } | |
2062 }, | |
2063 | |
2064 observers: [ | |
2065 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | |
2066 ], | |
2067 | |
2068 | |
2069 /** | |
2070 * To be used to express what combination of keys will trigger the relati
ve | |
2071 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
2072 * @type {Object} | |
2073 */ | |
2074 keyBindings: {}, | |
2075 | |
2076 registered: function() { | |
2077 this._prepKeyBindings(); | |
2078 }, | |
2079 | |
2080 attached: function() { | |
2081 this._listenKeyEventListeners(); | |
2082 }, | |
2083 | |
2084 detached: function() { | |
2085 this._unlistenKeyEventListeners(); | |
2086 }, | |
2087 | |
2088 /** | |
2089 * Can be used to imperatively add a key binding to the implementing | |
2090 * element. This is the imperative equivalent of declaring a keybinding | |
2091 * in the `keyBindings` prototype property. | |
2092 */ | |
2093 addOwnKeyBinding: function(eventString, handlerName) { | |
2094 this._imperativeKeyBindings[eventString] = handlerName; | |
2095 this._prepKeyBindings(); | |
2096 this._resetKeyEventListeners(); | |
2097 }, | |
2098 | |
2099 /** | |
2100 * When called, will remove all imperatively-added key bindings. | |
2101 */ | |
2102 removeOwnKeyBindings: function() { | |
2103 this._imperativeKeyBindings = {}; | |
2104 this._prepKeyBindings(); | |
2105 this._resetKeyEventListeners(); | |
2106 }, | |
2107 | |
2108 /** | |
2109 * Returns true if a keyboard event matches `eventString`. | |
2110 * | |
2111 * @param {KeyboardEvent} event | |
2112 * @param {string} eventString | |
2113 * @return {boolean} | |
2114 */ | |
2115 keyboardEventMatchesKeys: function(event, eventString) { | |
2116 var keyCombos = parseEventString(eventString); | |
2117 for (var i = 0; i < keyCombos.length; ++i) { | |
2118 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
2119 return true; | |
2120 } | |
2121 } | |
2122 return false; | |
2123 }, | |
2124 | |
2125 _collectKeyBindings: function() { | |
2126 var keyBindings = this.behaviors.map(function(behavior) { | |
2127 return behavior.keyBindings; | |
2128 }); | |
2129 | |
2130 if (keyBindings.indexOf(this.keyBindings) === -1) { | |
2131 keyBindings.push(this.keyBindings); | |
2132 } | |
2133 | |
2134 return keyBindings; | |
2135 }, | |
2136 | |
2137 _prepKeyBindings: function() { | |
2138 this._keyBindings = {}; | |
2139 | |
2140 this._collectKeyBindings().forEach(function(keyBindings) { | |
2141 for (var eventString in keyBindings) { | |
2142 this._addKeyBinding(eventString, keyBindings[eventString]); | |
2143 } | |
2144 }, this); | |
2145 | |
2146 for (var eventString in this._imperativeKeyBindings) { | |
2147 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
2148 } | |
2149 | |
2150 // Give precedence to combos with modifiers to be checked first. | |
2151 for (var eventName in this._keyBindings) { | |
2152 this._keyBindings[eventName].sort(function (kb1, kb2) { | |
2153 var b1 = kb1[0].hasModifiers; | |
2154 var b2 = kb2[0].hasModifiers; | |
2155 return (b1 === b2) ? 0 : b1 ? -1 : 1; | |
2156 }) | |
2157 } | |
2158 }, | |
2159 | |
2160 _addKeyBinding: function(eventString, handlerName) { | |
2161 parseEventString(eventString).forEach(function(keyCombo) { | |
2162 this._keyBindings[keyCombo.event] = | |
2163 this._keyBindings[keyCombo.event] || []; | |
2164 | |
2165 this._keyBindings[keyCombo.event].push([ | |
2166 keyCombo, | |
2167 handlerName | |
2168 ]); | |
2169 }, this); | |
2170 }, | |
2171 | |
2172 _resetKeyEventListeners: function() { | |
2173 this._unlistenKeyEventListeners(); | |
2174 | |
2175 if (this.isAttached) { | |
2176 this._listenKeyEventListeners(); | |
2177 } | |
2178 }, | |
2179 | |
2180 _listenKeyEventListeners: function() { | |
2181 if (!this.keyEventTarget) { | |
2182 return; | |
2183 } | |
2184 Object.keys(this._keyBindings).forEach(function(eventName) { | |
2185 var keyBindings = this._keyBindings[eventName]; | |
2186 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | |
2187 | |
2188 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | |
2189 | |
2190 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | |
2191 }, this); | |
2192 }, | |
2193 | |
2194 _unlistenKeyEventListeners: function() { | |
2195 var keyHandlerTuple; | |
2196 var keyEventTarget; | |
2197 var eventName; | |
2198 var boundKeyHandler; | |
2199 | |
2200 while (this._boundKeyHandlers.length) { | |
2201 // My kingdom for block-scope binding and destructuring assignment.. | |
2202 keyHandlerTuple = this._boundKeyHandlers.pop(); | |
2203 keyEventTarget = keyHandlerTuple[0]; | |
2204 eventName = keyHandlerTuple[1]; | |
2205 boundKeyHandler = keyHandlerTuple[2]; | |
2206 | |
2207 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | |
2208 } | |
2209 }, | |
2210 | |
2211 _onKeyBindingEvent: function(keyBindings, event) { | |
2212 if (this.stopKeyboardEventPropagation) { | |
2213 event.stopPropagation(); | |
2214 } | |
2215 | |
2216 // if event has been already prevented, don't do anything | |
2217 if (event.defaultPrevented) { | |
2218 return; | |
2219 } | |
2220 | |
2221 for (var i = 0; i < keyBindings.length; i++) { | |
2222 var keyCombo = keyBindings[i][0]; | |
2223 var handlerName = keyBindings[i][1]; | |
2224 if (keyComboMatchesEvent(keyCombo, event)) { | |
2225 this._triggerKeyHandler(keyCombo, handlerName, event); | |
2226 // exit the loop if eventDefault was prevented | |
2227 if (event.defaultPrevented) { | |
2228 return; | |
2229 } | |
2230 } | |
2231 } | |
2232 }, | |
2233 | |
2234 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | |
2235 var detail = Object.create(keyCombo); | |
2236 detail.keyboardEvent = keyboardEvent; | |
2237 var event = new CustomEvent(keyCombo.event, { | |
2238 detail: detail, | |
2239 cancelable: true | |
2240 }); | |
2241 this[handlerName].call(this, event); | |
2242 if (event.defaultPrevented) { | |
2243 keyboardEvent.preventDefault(); | |
2244 } | |
2245 } | |
2246 }; | |
2247 })(); | |
2248 /** | |
2249 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
2250 * designated scroll target. | |
2251 * | |
2252 * Elements that consume this behavior can override the `_scrollHandler` | |
2253 * method to add logic on the scroll event. | |
2254 * | |
2255 * @demo demo/scrolling-region.html Scrolling Region | |
2256 * @demo demo/document.html Document Element | |
2257 * @polymerBehavior | |
2258 */ | |
2259 Polymer.IronScrollTargetBehavior = { | |
2260 | |
2261 properties: { | |
2262 | |
2263 /** | |
2264 * Specifies the element that will handle the scroll event | |
2265 * on the behalf of the current element. This is typically a reference to
an element, | |
2266 * but there are a few more posibilities: | |
2267 * | |
2268 * ### Elements id | |
2269 * | |
2270 *```html | |
2271 * <div id="scrollable-element" style="overflow: auto;"> | |
2272 * <x-element scroll-target="scrollable-element"> | |
2273 * \x3c!-- Content--\x3e | |
2274 * </x-element> | |
2275 * </div> | |
2276 *``` | |
2277 * In this case, the `scrollTarget` will point to the outer div element. | |
2278 * | |
2279 * ### Document scrolling | |
2280 * | |
2281 * For document scrolling, you can use the reserved word `document`: | |
2282 * | |
2283 *```html | |
2284 * <x-element scroll-target="document"> | |
2285 * \x3c!-- Content --\x3e | |
2286 * </x-element> | |
2287 *``` | |
2288 * | |
2289 * ### Elements reference | |
2290 * | |
2291 *```js | |
2292 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
2293 *``` | |
2294 * | |
2295 * @type {HTMLElement} | |
2296 */ | |
2297 scrollTarget: { | |
2298 type: HTMLElement, | |
2299 value: function() { | |
2300 return this._defaultScrollTarget; | |
2301 } | |
2302 } | |
2303 }, | |
2304 | |
2305 observers: [ | |
2306 '_scrollTargetChanged(scrollTarget, isAttached)' | |
2307 ], | |
2308 | |
2309 _scrollTargetChanged: function(scrollTarget, isAttached) { | |
2310 var eventTarget; | |
2311 | |
2312 if (this._oldScrollTarget) { | |
2313 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | |
2314 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | |
2315 this._oldScrollTarget = null; | |
2316 } | |
2317 | |
2318 if (!isAttached) { | |
2319 return; | |
2320 } | |
2321 // Support element id references | |
2322 if (scrollTarget === 'document') { | |
2323 | |
2324 this.scrollTarget = this._doc; | |
2325 | |
2326 } else if (typeof scrollTarget === 'string') { | |
2327 | |
2328 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | |
2329 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | |
2330 | |
2331 } else if (this._isValidScrollTarget()) { | |
2332 | |
2333 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | |
2334 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | |
2335 this._oldScrollTarget = scrollTarget; | |
2336 | |
2337 eventTarget.addEventListener('scroll', this._boundScrollHandler); | |
2338 } | |
2339 }, | |
2340 | |
2341 /** | |
2342 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
2343 * | |
2344 * @protected | |
2345 */ | |
2346 _scrollHandler: function scrollHandler() {}, | |
2347 | |
2348 /** | |
2349 * The default scroll target. Consumers of this behavior may want to customi
ze | |
2350 * the default scroll target. | |
2351 * | |
2352 * @type {Element} | |
2353 */ | |
2354 get _defaultScrollTarget() { | |
2355 return this._doc; | |
2356 }, | |
2357 | |
2358 /** | |
2359 * Shortcut for the document element | |
2360 * | |
2361 * @type {Element} | |
2362 */ | |
2363 get _doc() { | |
2364 return this.ownerDocument.documentElement; | |
2365 }, | |
2366 | |
2367 /** | |
2368 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
2369 * | |
2370 * @type {number} | |
2371 */ | |
2372 get _scrollTop() { | |
2373 if (this._isValidScrollTarget()) { | |
2374 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
2375 } | |
2376 return 0; | |
2377 }, | |
2378 | |
2379 /** | |
2380 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
2381 * | |
2382 * @type {number} | |
2383 */ | |
2384 get _scrollLeft() { | |
2385 if (this._isValidScrollTarget()) { | |
2386 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
2387 } | |
2388 return 0; | |
2389 }, | |
2390 | |
2391 /** | |
2392 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
2393 * | |
2394 * @type {number} | |
2395 */ | |
2396 set _scrollTop(top) { | |
2397 if (this.scrollTarget === this._doc) { | |
2398 window.scrollTo(window.pageXOffset, top); | |
2399 } else if (this._isValidScrollTarget()) { | |
2400 this.scrollTarget.scrollTop = top; | |
2401 } | |
2402 }, | |
2403 | |
2404 /** | |
2405 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
2406 * | |
2407 * @type {number} | |
2408 */ | |
2409 set _scrollLeft(left) { | |
2410 if (this.scrollTarget === this._doc) { | |
2411 window.scrollTo(left, window.pageYOffset); | |
2412 } else if (this._isValidScrollTarget()) { | |
2413 this.scrollTarget.scrollLeft = left; | |
2414 } | |
2415 }, | |
2416 | |
2417 /** | |
2418 * Scrolls the content to a particular place. | |
2419 * | |
2420 * @method scroll | |
2421 * @param {number} left The left position | |
2422 * @param {number} top The top position | |
2423 */ | |
2424 scroll: function(left, top) { | |
2425 if (this.scrollTarget === this._doc) { | |
2426 window.scrollTo(left, top); | |
2427 } else if (this._isValidScrollTarget()) { | |
2428 this.scrollTarget.scrollLeft = left; | |
2429 this.scrollTarget.scrollTop = top; | |
2430 } | |
2431 }, | |
2432 | |
2433 /** | |
2434 * Gets the width of the scroll target. | |
2435 * | |
2436 * @type {number} | |
2437 */ | |
2438 get _scrollTargetWidth() { | |
2439 if (this._isValidScrollTarget()) { | |
2440 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
2441 } | |
2442 return 0; | |
2443 }, | |
2444 | |
2445 /** | |
2446 * Gets the height of the scroll target. | |
2447 * | |
2448 * @type {number} | |
2449 */ | |
2450 get _scrollTargetHeight() { | |
2451 if (this._isValidScrollTarget()) { | |
2452 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
2453 } | |
2454 return 0; | |
2455 }, | |
2456 | |
2457 /** | |
2458 * Returns true if the scroll target is a valid HTMLElement. | |
2459 * | |
2460 * @return {boolean} | |
2461 */ | |
2462 _isValidScrollTarget: function() { | |
2463 return this.scrollTarget instanceof HTMLElement; | |
2464 } | |
2465 }; | |
2466 (function() { | |
2467 | |
2468 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 1315 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
2469 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 1316 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
2470 var DEFAULT_PHYSICAL_COUNT = 3; | 1317 var DEFAULT_PHYSICAL_COUNT = 3; |
2471 var HIDDEN_Y = '-10000px'; | 1318 var HIDDEN_Y = '-10000px'; |
2472 var DEFAULT_GRID_SIZE = 200; | 1319 var DEFAULT_GRID_SIZE = 200; |
2473 var SECRET_TABINDEX = -100; | 1320 var SECRET_TABINDEX = -100; |
2474 | |
2475 Polymer({ | 1321 Polymer({ |
2476 | |
2477 is: 'iron-list', | 1322 is: 'iron-list', |
2478 | |
2479 properties: { | 1323 properties: { |
2480 | |
2481 /** | |
2482 * An array containing items determining how many instances of the templat
e | |
2483 * to stamp and that that each template instance should bind to. | |
2484 */ | |
2485 items: { | 1324 items: { |
2486 type: Array | 1325 type: Array |
2487 }, | 1326 }, |
2488 | |
2489 /** | |
2490 * The max count of physical items the pool can extend to. | |
2491 */ | |
2492 maxPhysicalCount: { | 1327 maxPhysicalCount: { |
2493 type: Number, | 1328 type: Number, |
2494 value: 500 | 1329 value: 500 |
2495 }, | 1330 }, |
2496 | |
2497 /** | |
2498 * The name of the variable to add to the binding scope for the array | |
2499 * element associated with a given template instance. | |
2500 */ | |
2501 as: { | 1331 as: { |
2502 type: String, | 1332 type: String, |
2503 value: 'item' | 1333 value: 'item' |
2504 }, | 1334 }, |
2505 | |
2506 /** | |
2507 * The name of the variable to add to the binding scope with the index | |
2508 * for the row. | |
2509 */ | |
2510 indexAs: { | 1335 indexAs: { |
2511 type: String, | 1336 type: String, |
2512 value: 'index' | 1337 value: 'index' |
2513 }, | 1338 }, |
2514 | |
2515 /** | |
2516 * The name of the variable to add to the binding scope to indicate | |
2517 * if the row is selected. | |
2518 */ | |
2519 selectedAs: { | 1339 selectedAs: { |
2520 type: String, | 1340 type: String, |
2521 value: 'selected' | 1341 value: 'selected' |
2522 }, | 1342 }, |
2523 | |
2524 /** | |
2525 * When true, the list is rendered as a grid. Grid items must have | |
2526 * fixed width and height set via CSS. e.g. | |
2527 * | |
2528 * ```html | |
2529 * <iron-list grid> | |
2530 * <template> | |
2531 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
2532 * </template> | |
2533 * </iron-list> | |
2534 * ``` | |
2535 */ | |
2536 grid: { | 1343 grid: { |
2537 type: Boolean, | 1344 type: Boolean, |
2538 value: false, | 1345 value: false, |
2539 reflectToAttribute: true | 1346 reflectToAttribute: true |
2540 }, | 1347 }, |
2541 | |
2542 /** | |
2543 * When true, tapping a row will select the item, placing its data model | |
2544 * in the set of selected items retrievable via the selection property. | |
2545 * | |
2546 * Note that tapping focusable elements within the list item will not | |
2547 * result in selection, since they are presumed to have their * own action
. | |
2548 */ | |
2549 selectionEnabled: { | 1348 selectionEnabled: { |
2550 type: Boolean, | 1349 type: Boolean, |
2551 value: false | 1350 value: false |
2552 }, | 1351 }, |
2553 | |
2554 /** | |
2555 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
2556 * if no item is selected. | |
2557 */ | |
2558 selectedItem: { | 1352 selectedItem: { |
2559 type: Object, | 1353 type: Object, |
2560 notify: true | 1354 notify: true |
2561 }, | 1355 }, |
2562 | |
2563 /** | |
2564 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
2565 */ | |
2566 selectedItems: { | 1356 selectedItems: { |
2567 type: Object, | 1357 type: Object, |
2568 notify: true | 1358 notify: true |
2569 }, | 1359 }, |
2570 | |
2571 /** | |
2572 * When `true`, multiple items may be selected at once (in this case, | |
2573 * `selected` is an array of currently selected items). When `false`, | |
2574 * only one item may be selected at a time. | |
2575 */ | |
2576 multiSelection: { | 1360 multiSelection: { |
2577 type: Boolean, | 1361 type: Boolean, |
2578 value: false | 1362 value: false |
2579 } | 1363 } |
2580 }, | 1364 }, |
2581 | 1365 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn
abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)'
], |
2582 observers: [ | 1366 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro
nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ], |
2583 '_itemsChanged(items.*)', | |
2584 '_selectionEnabledChanged(selectionEnabled)', | |
2585 '_multiSelectionChanged(multiSelection)', | |
2586 '_setOverflow(scrollTarget)' | |
2587 ], | |
2588 | |
2589 behaviors: [ | |
2590 Polymer.Templatizer, | |
2591 Polymer.IronResizableBehavior, | |
2592 Polymer.IronA11yKeysBehavior, | |
2593 Polymer.IronScrollTargetBehavior | |
2594 ], | |
2595 | |
2596 keyBindings: { | 1367 keyBindings: { |
2597 'up': '_didMoveUp', | 1368 up: '_didMoveUp', |
2598 'down': '_didMoveDown', | 1369 down: '_didMoveDown', |
2599 'enter': '_didEnter' | 1370 enter: '_didEnter' |
2600 }, | 1371 }, |
2601 | 1372 _ratio: .5, |
2602 /** | |
2603 * The ratio of hidden tiles that should remain in the scroll direction. | |
2604 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
2605 */ | |
2606 _ratio: 0.5, | |
2607 | |
2608 /** | |
2609 * The padding-top value for the list. | |
2610 */ | |
2611 _scrollerPaddingTop: 0, | 1373 _scrollerPaddingTop: 0, |
2612 | |
2613 /** | |
2614 * This value is the same as `scrollTop`. | |
2615 */ | |
2616 _scrollPosition: 0, | 1374 _scrollPosition: 0, |
2617 | |
2618 /** | |
2619 * The sum of the heights of all the tiles in the DOM. | |
2620 */ | |
2621 _physicalSize: 0, | 1375 _physicalSize: 0, |
2622 | |
2623 /** | |
2624 * The average `offsetHeight` of the tiles observed till now. | |
2625 */ | |
2626 _physicalAverage: 0, | 1376 _physicalAverage: 0, |
2627 | |
2628 /** | |
2629 * The number of tiles which `offsetHeight` > 0 observed until now. | |
2630 */ | |
2631 _physicalAverageCount: 0, | 1377 _physicalAverageCount: 0, |
2632 | |
2633 /** | |
2634 * The Y position of the item rendered in the `_physicalStart` | |
2635 * tile relative to the scrolling list. | |
2636 */ | |
2637 _physicalTop: 0, | 1378 _physicalTop: 0, |
2638 | |
2639 /** | |
2640 * The number of items in the list. | |
2641 */ | |
2642 _virtualCount: 0, | 1379 _virtualCount: 0, |
2643 | |
2644 /** | |
2645 * A map between an item key and its physical item index | |
2646 */ | |
2647 _physicalIndexForKey: null, | 1380 _physicalIndexForKey: null, |
2648 | |
2649 /** | |
2650 * The estimated scroll height based on `_physicalAverage` | |
2651 */ | |
2652 _estScrollHeight: 0, | 1381 _estScrollHeight: 0, |
2653 | |
2654 /** | |
2655 * The scroll height of the dom node | |
2656 */ | |
2657 _scrollHeight: 0, | 1382 _scrollHeight: 0, |
2658 | |
2659 /** | |
2660 * The height of the list. This is referred as the viewport in the context o
f list. | |
2661 */ | |
2662 _viewportHeight: 0, | 1383 _viewportHeight: 0, |
2663 | |
2664 /** | |
2665 * The width of the list. This is referred as the viewport in the context of
list. | |
2666 */ | |
2667 _viewportWidth: 0, | 1384 _viewportWidth: 0, |
2668 | |
2669 /** | |
2670 * An array of DOM nodes that are currently in the tree | |
2671 * @type {?Array<!TemplatizerNode>} | |
2672 */ | |
2673 _physicalItems: null, | 1385 _physicalItems: null, |
2674 | |
2675 /** | |
2676 * An array of heights for each item in `_physicalItems` | |
2677 * @type {?Array<number>} | |
2678 */ | |
2679 _physicalSizes: null, | 1386 _physicalSizes: null, |
2680 | |
2681 /** | |
2682 * A cached value for the first visible index. | |
2683 * See `firstVisibleIndex` | |
2684 * @type {?number} | |
2685 */ | |
2686 _firstVisibleIndexVal: null, | 1387 _firstVisibleIndexVal: null, |
2687 | |
2688 /** | |
2689 * A cached value for the last visible index. | |
2690 * See `lastVisibleIndex` | |
2691 * @type {?number} | |
2692 */ | |
2693 _lastVisibleIndexVal: null, | 1388 _lastVisibleIndexVal: null, |
2694 | |
2695 /** | |
2696 * A Polymer collection for the items. | |
2697 * @type {?Polymer.Collection} | |
2698 */ | |
2699 _collection: null, | 1389 _collection: null, |
2700 | |
2701 /** | |
2702 * True if the current item list was rendered for the first time | |
2703 * after attached. | |
2704 */ | |
2705 _itemsRendered: false, | 1390 _itemsRendered: false, |
2706 | |
2707 /** | |
2708 * The page that is currently rendered. | |
2709 */ | |
2710 _lastPage: null, | 1391 _lastPage: null, |
2711 | |
2712 /** | |
2713 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
2714 */ | |
2715 _maxPages: 3, | 1392 _maxPages: 3, |
2716 | |
2717 /** | |
2718 * The currently focused physical item. | |
2719 */ | |
2720 _focusedItem: null, | 1393 _focusedItem: null, |
2721 | |
2722 /** | |
2723 * The index of the `_focusedItem`. | |
2724 */ | |
2725 _focusedIndex: -1, | 1394 _focusedIndex: -1, |
2726 | |
2727 /** | |
2728 * The the item that is focused if it is moved offscreen. | |
2729 * @private {?TemplatizerNode} | |
2730 */ | |
2731 _offscreenFocusedItem: null, | 1395 _offscreenFocusedItem: null, |
2732 | |
2733 /** | |
2734 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
2735 * list when that item is moved offscreen. | |
2736 */ | |
2737 _focusBackfillItem: null, | 1396 _focusBackfillItem: null, |
2738 | |
2739 /** | |
2740 * The maximum items per row | |
2741 */ | |
2742 _itemsPerRow: 1, | 1397 _itemsPerRow: 1, |
2743 | |
2744 /** | |
2745 * The width of each grid item | |
2746 */ | |
2747 _itemWidth: 0, | 1398 _itemWidth: 0, |
2748 | |
2749 /** | |
2750 * The height of the row in grid layout. | |
2751 */ | |
2752 _rowHeight: 0, | 1399 _rowHeight: 0, |
2753 | |
2754 /** | |
2755 * The bottom of the physical content. | |
2756 */ | |
2757 get _physicalBottom() { | 1400 get _physicalBottom() { |
2758 return this._physicalTop + this._physicalSize; | 1401 return this._physicalTop + this._physicalSize; |
2759 }, | 1402 }, |
2760 | |
2761 /** | |
2762 * The bottom of the scroll. | |
2763 */ | |
2764 get _scrollBottom() { | 1403 get _scrollBottom() { |
2765 return this._scrollPosition + this._viewportHeight; | 1404 return this._scrollPosition + this._viewportHeight; |
2766 }, | 1405 }, |
2767 | |
2768 /** | |
2769 * The n-th item rendered in the last physical item. | |
2770 */ | |
2771 get _virtualEnd() { | 1406 get _virtualEnd() { |
2772 return this._virtualStart + this._physicalCount - 1; | 1407 return this._virtualStart + this._physicalCount - 1; |
2773 }, | 1408 }, |
2774 | |
2775 /** | |
2776 * The height of the physical content that isn't on the screen. | |
2777 */ | |
2778 get _hiddenContentSize() { | 1409 get _hiddenContentSize() { |
2779 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 1410 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
2780 return size - this._viewportHeight; | 1411 return size - this._viewportHeight; |
2781 }, | 1412 }, |
2782 | |
2783 /** | |
2784 * The maximum scroll top value. | |
2785 */ | |
2786 get _maxScrollTop() { | 1413 get _maxScrollTop() { |
2787 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 1414 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
2788 }, | 1415 }, |
2789 | |
2790 /** | |
2791 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
2792 */ | |
2793 _minVirtualStart: 0, | 1416 _minVirtualStart: 0, |
2794 | |
2795 /** | |
2796 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
2797 */ | |
2798 get _maxVirtualStart() { | 1417 get _maxVirtualStart() { |
2799 return Math.max(0, this._virtualCount - this._physicalCount); | 1418 return Math.max(0, this._virtualCount - this._physicalCount); |
2800 }, | 1419 }, |
2801 | |
2802 /** | |
2803 * The n-th item rendered in the `_physicalStart` tile. | |
2804 */ | |
2805 _virtualStartVal: 0, | 1420 _virtualStartVal: 0, |
2806 | |
2807 set _virtualStart(val) { | 1421 set _virtualStart(val) { |
2808 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 1422 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
2809 }, | 1423 }, |
2810 | |
2811 get _virtualStart() { | 1424 get _virtualStart() { |
2812 return this._virtualStartVal || 0; | 1425 return this._virtualStartVal || 0; |
2813 }, | 1426 }, |
2814 | |
2815 /** | |
2816 * The k-th tile that is at the top of the scrolling list. | |
2817 */ | |
2818 _physicalStartVal: 0, | 1427 _physicalStartVal: 0, |
2819 | |
2820 set _physicalStart(val) { | 1428 set _physicalStart(val) { |
2821 this._physicalStartVal = val % this._physicalCount; | 1429 this._physicalStartVal = val % this._physicalCount; |
2822 if (this._physicalStartVal < 0) { | 1430 if (this._physicalStartVal < 0) { |
2823 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 1431 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
2824 } | 1432 } |
2825 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1433 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2826 }, | 1434 }, |
2827 | |
2828 get _physicalStart() { | 1435 get _physicalStart() { |
2829 return this._physicalStartVal || 0; | 1436 return this._physicalStartVal || 0; |
2830 }, | 1437 }, |
2831 | |
2832 /** | |
2833 * The number of tiles in the DOM. | |
2834 */ | |
2835 _physicalCountVal: 0, | 1438 _physicalCountVal: 0, |
2836 | |
2837 set _physicalCount(val) { | 1439 set _physicalCount(val) { |
2838 this._physicalCountVal = val; | 1440 this._physicalCountVal = val; |
2839 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1441 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2840 }, | 1442 }, |
2841 | |
2842 get _physicalCount() { | 1443 get _physicalCount() { |
2843 return this._physicalCountVal; | 1444 return this._physicalCountVal; |
2844 }, | 1445 }, |
2845 | |
2846 /** | |
2847 * The k-th tile that is at the bottom of the scrolling list. | |
2848 */ | |
2849 _physicalEnd: 0, | 1446 _physicalEnd: 0, |
2850 | |
2851 /** | |
2852 * An optimal physical size such that we will have enough physical items | |
2853 * to fill up the viewport and recycle when the user scrolls. | |
2854 * | |
2855 * This default value assumes that we will at least have the equivalent | |
2856 * to a viewport of physical items above and below the user's viewport. | |
2857 */ | |
2858 get _optPhysicalSize() { | 1447 get _optPhysicalSize() { |
2859 if (this.grid) { | 1448 if (this.grid) { |
2860 return this._estRowsInView * this._rowHeight * this._maxPages; | 1449 return this._estRowsInView * this._rowHeight * this._maxPages; |
2861 } | 1450 } |
2862 return this._viewportHeight * this._maxPages; | 1451 return this._viewportHeight * this._maxPages; |
2863 }, | 1452 }, |
2864 | |
2865 get _optPhysicalCount() { | 1453 get _optPhysicalCount() { |
2866 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 1454 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
2867 }, | 1455 }, |
2868 | |
2869 /** | |
2870 * True if the current list is visible. | |
2871 */ | |
2872 get _isVisible() { | 1456 get _isVisible() { |
2873 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 1457 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
2874 }, | 1458 }, |
2875 | |
2876 /** | |
2877 * Gets the index of the first visible item in the viewport. | |
2878 * | |
2879 * @type {number} | |
2880 */ | |
2881 get firstVisibleIndex() { | 1459 get firstVisibleIndex() { |
2882 if (this._firstVisibleIndexVal === null) { | 1460 if (this._firstVisibleIndexVal === null) { |
2883 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 1461 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
2884 | 1462 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) { |
2885 this._firstVisibleIndexVal = this._iterateItems( | 1463 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2886 function(pidx, vidx) { | 1464 if (physicalOffset > this._scrollPosition) { |
2887 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1465 return this.grid ? vidx - vidx % this._itemsPerRow : vidx; |
2888 | 1466 } |
2889 if (physicalOffset > this._scrollPosition) { | 1467 if (this.grid && this._virtualCount - 1 === vidx) { |
2890 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 1468 return vidx - vidx % this._itemsPerRow; |
2891 } | 1469 } |
2892 // Handle a partially rendered final row in grid mode | 1470 }) || 0; |
2893 if (this.grid && this._virtualCount - 1 === vidx) { | |
2894 return vidx - (vidx % this._itemsPerRow); | |
2895 } | |
2896 }) || 0; | |
2897 } | 1471 } |
2898 return this._firstVisibleIndexVal; | 1472 return this._firstVisibleIndexVal; |
2899 }, | 1473 }, |
2900 | |
2901 /** | |
2902 * Gets the index of the last visible item in the viewport. | |
2903 * | |
2904 * @type {number} | |
2905 */ | |
2906 get lastVisibleIndex() { | 1474 get lastVisibleIndex() { |
2907 if (this._lastVisibleIndexVal === null) { | 1475 if (this._lastVisibleIndexVal === null) { |
2908 if (this.grid) { | 1476 if (this.grid) { |
2909 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 1477 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
2910 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 1478 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
2911 } else { | 1479 } else { |
2912 var physicalOffset = this._physicalTop; | 1480 var physicalOffset = this._physicalTop; |
2913 this._iterateItems(function(pidx, vidx) { | 1481 this._iterateItems(function(pidx, vidx) { |
2914 if (physicalOffset < this._scrollBottom) { | 1482 if (physicalOffset < this._scrollBottom) { |
2915 this._lastVisibleIndexVal = vidx; | 1483 this._lastVisibleIndexVal = vidx; |
2916 } else { | 1484 } else { |
2917 // Break _iterateItems | |
2918 return true; | 1485 return true; |
2919 } | 1486 } |
2920 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1487 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2921 }); | 1488 }); |
2922 } | 1489 } |
2923 } | 1490 } |
2924 return this._lastVisibleIndexVal; | 1491 return this._lastVisibleIndexVal; |
2925 }, | 1492 }, |
2926 | |
2927 get _defaultScrollTarget() { | 1493 get _defaultScrollTarget() { |
2928 return this; | 1494 return this; |
2929 }, | 1495 }, |
2930 get _virtualRowCount() { | 1496 get _virtualRowCount() { |
2931 return Math.ceil(this._virtualCount / this._itemsPerRow); | 1497 return Math.ceil(this._virtualCount / this._itemsPerRow); |
2932 }, | 1498 }, |
2933 | |
2934 get _estRowsInView() { | 1499 get _estRowsInView() { |
2935 return Math.ceil(this._viewportHeight / this._rowHeight); | 1500 return Math.ceil(this._viewportHeight / this._rowHeight); |
2936 }, | 1501 }, |
2937 | |
2938 get _physicalRows() { | 1502 get _physicalRows() { |
2939 return Math.ceil(this._physicalCount / this._itemsPerRow); | 1503 return Math.ceil(this._physicalCount / this._itemsPerRow); |
2940 }, | 1504 }, |
2941 | |
2942 ready: function() { | 1505 ready: function() { |
2943 this.addEventListener('focus', this._didFocus.bind(this), true); | 1506 this.addEventListener('focus', this._didFocus.bind(this), true); |
2944 }, | 1507 }, |
2945 | |
2946 attached: function() { | 1508 attached: function() { |
2947 this.updateViewportBoundaries(); | 1509 this.updateViewportBoundaries(); |
2948 this._render(); | 1510 this._render(); |
2949 // `iron-resize` is fired when the list is attached if the event is added | |
2950 // before attached causing unnecessary work. | |
2951 this.listen(this, 'iron-resize', '_resizeHandler'); | 1511 this.listen(this, 'iron-resize', '_resizeHandler'); |
2952 }, | 1512 }, |
2953 | |
2954 detached: function() { | 1513 detached: function() { |
2955 this._itemsRendered = false; | 1514 this._itemsRendered = false; |
2956 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 1515 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
2957 }, | 1516 }, |
2958 | |
2959 /** | |
2960 * Set the overflow property if this element has its own scrolling region | |
2961 */ | |
2962 _setOverflow: function(scrollTarget) { | 1517 _setOverflow: function(scrollTarget) { |
2963 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 1518 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
2964 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 1519 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
2965 }, | 1520 }, |
2966 | |
2967 /** | |
2968 * Invoke this method if you dynamically update the viewport's | |
2969 * size or CSS padding. | |
2970 * | |
2971 * @method updateViewportBoundaries | |
2972 */ | |
2973 updateViewportBoundaries: function() { | 1521 updateViewportBoundaries: function() { |
2974 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 1522 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo
w.getComputedStyle(this)['padding-top'], 10); |
2975 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
2976 | |
2977 this._viewportHeight = this._scrollTargetHeight; | 1523 this._viewportHeight = this._scrollTargetHeight; |
2978 if (this.grid) { | 1524 if (this.grid) { |
2979 this._updateGridMetrics(); | 1525 this._updateGridMetrics(); |
2980 } | 1526 } |
2981 }, | 1527 }, |
2982 | |
2983 /** | |
2984 * Update the models, the position of the | |
2985 * items in the viewport and recycle tiles as needed. | |
2986 */ | |
2987 _scrollHandler: function() { | 1528 _scrollHandler: function() { |
2988 // clamp the `scrollTop` value | |
2989 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 1529 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
2990 var delta = scrollTop - this._scrollPosition; | 1530 var delta = scrollTop - this._scrollPosition; |
2991 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 1531 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
2992 var ratio = this._ratio; | 1532 var ratio = this._ratio; |
2993 var recycledTiles = 0; | 1533 var recycledTiles = 0; |
2994 var hiddenContentSize = this._hiddenContentSize; | 1534 var hiddenContentSize = this._hiddenContentSize; |
2995 var currentRatio = ratio; | 1535 var currentRatio = ratio; |
2996 var movingUp = []; | 1536 var movingUp = []; |
2997 | |
2998 // track the last `scrollTop` | |
2999 this._scrollPosition = scrollTop; | 1537 this._scrollPosition = scrollTop; |
3000 | |
3001 // clear cached visible indexes | |
3002 this._firstVisibleIndexVal = null; | 1538 this._firstVisibleIndexVal = null; |
3003 this._lastVisibleIndexVal = null; | 1539 this._lastVisibleIndexVal = null; |
3004 | |
3005 scrollBottom = this._scrollBottom; | 1540 scrollBottom = this._scrollBottom; |
3006 physicalBottom = this._physicalBottom; | 1541 physicalBottom = this._physicalBottom; |
3007 | |
3008 // random access | |
3009 if (Math.abs(delta) > this._physicalSize) { | 1542 if (Math.abs(delta) > this._physicalSize) { |
3010 this._physicalTop += delta; | 1543 this._physicalTop += delta; |
3011 recycledTiles = Math.round(delta / this._physicalAverage); | 1544 recycledTiles = Math.round(delta / this._physicalAverage); |
3012 } | 1545 } else if (delta < 0) { |
3013 // scroll up | |
3014 else if (delta < 0) { | |
3015 var topSpace = scrollTop - this._physicalTop; | 1546 var topSpace = scrollTop - this._physicalTop; |
3016 var virtualStart = this._virtualStart; | 1547 var virtualStart = this._virtualStart; |
3017 | |
3018 recycledTileSet = []; | 1548 recycledTileSet = []; |
3019 | |
3020 kth = this._physicalEnd; | 1549 kth = this._physicalEnd; |
3021 currentRatio = topSpace / hiddenContentSize; | 1550 currentRatio = topSpace / hiddenContentSize; |
3022 | 1551 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen
t(kth) > scrollBottom) { |
3023 // move tiles from bottom to top | |
3024 while ( | |
3025 // approximate `currentRatio` to `ratio` | |
3026 currentRatio < ratio && | |
3027 // recycle less physical items than the total | |
3028 recycledTiles < this._physicalCount && | |
3029 // ensure that these recycled tiles are needed | |
3030 virtualStart - recycledTiles > 0 && | |
3031 // ensure that the tile is not visible | |
3032 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
3033 ) { | |
3034 | |
3035 tileHeight = this._getPhysicalSizeIncrement(kth); | 1552 tileHeight = this._getPhysicalSizeIncrement(kth); |
3036 currentRatio += tileHeight / hiddenContentSize; | 1553 currentRatio += tileHeight / hiddenContentSize; |
3037 physicalBottom -= tileHeight; | 1554 physicalBottom -= tileHeight; |
3038 recycledTileSet.push(kth); | 1555 recycledTileSet.push(kth); |
3039 recycledTiles++; | 1556 recycledTiles++; |
3040 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 1557 kth = kth === 0 ? this._physicalCount - 1 : kth - 1; |
3041 } | 1558 } |
3042 | |
3043 movingUp = recycledTileSet; | 1559 movingUp = recycledTileSet; |
3044 recycledTiles = -recycledTiles; | 1560 recycledTiles = -recycledTiles; |
3045 } | 1561 } else if (delta > 0) { |
3046 // scroll down | |
3047 else if (delta > 0) { | |
3048 var bottomSpace = physicalBottom - scrollBottom; | 1562 var bottomSpace = physicalBottom - scrollBottom; |
3049 var virtualEnd = this._virtualEnd; | 1563 var virtualEnd = this._virtualEnd; |
3050 var lastVirtualItemIndex = this._virtualCount-1; | 1564 var lastVirtualItemIndex = this._virtualCount - 1; |
3051 | |
3052 recycledTileSet = []; | 1565 recycledTileSet = []; |
3053 | |
3054 kth = this._physicalStart; | 1566 kth = this._physicalStart; |
3055 currentRatio = bottomSpace / hiddenContentSize; | 1567 currentRatio = bottomSpace / hiddenContentSize; |
3056 | 1568 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get
PhysicalSizeIncrement(kth) < scrollTop) { |
3057 // move tiles from top to bottom | |
3058 while ( | |
3059 // approximate `currentRatio` to `ratio` | |
3060 currentRatio < ratio && | |
3061 // recycle less physical items than the total | |
3062 recycledTiles < this._physicalCount && | |
3063 // ensure that these recycled tiles are needed | |
3064 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
3065 // ensure that the tile is not visible | |
3066 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
3067 ) { | |
3068 | |
3069 tileHeight = this._getPhysicalSizeIncrement(kth); | 1569 tileHeight = this._getPhysicalSizeIncrement(kth); |
3070 currentRatio += tileHeight / hiddenContentSize; | 1570 currentRatio += tileHeight / hiddenContentSize; |
3071 | |
3072 this._physicalTop += tileHeight; | 1571 this._physicalTop += tileHeight; |
3073 recycledTileSet.push(kth); | 1572 recycledTileSet.push(kth); |
3074 recycledTiles++; | 1573 recycledTiles++; |
3075 kth = (kth + 1) % this._physicalCount; | 1574 kth = (kth + 1) % this._physicalCount; |
3076 } | 1575 } |
3077 } | 1576 } |
3078 | |
3079 if (recycledTiles === 0) { | 1577 if (recycledTiles === 0) { |
3080 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
3081 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 1578 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
3082 this._increasePoolIfNeeded(); | 1579 this._increasePoolIfNeeded(); |
3083 } | 1580 } |
3084 } else { | 1581 } else { |
3085 this._virtualStart = this._virtualStart + recycledTiles; | 1582 this._virtualStart = this._virtualStart + recycledTiles; |
3086 this._physicalStart = this._physicalStart + recycledTiles; | 1583 this._physicalStart = this._physicalStart + recycledTiles; |
3087 this._update(recycledTileSet, movingUp); | 1584 this._update(recycledTileSet, movingUp); |
3088 } | 1585 } |
3089 }, | 1586 }, |
3090 | |
3091 /** | |
3092 * Update the list of items, starting from the `_virtualStart` item. | |
3093 * @param {!Array<number>=} itemSet | |
3094 * @param {!Array<number>=} movingUp | |
3095 */ | |
3096 _update: function(itemSet, movingUp) { | 1587 _update: function(itemSet, movingUp) { |
3097 // manage focus | |
3098 this._manageFocus(); | 1588 this._manageFocus(); |
3099 // update models | |
3100 this._assignModels(itemSet); | 1589 this._assignModels(itemSet); |
3101 // measure heights | |
3102 this._updateMetrics(itemSet); | 1590 this._updateMetrics(itemSet); |
3103 // adjust offset after measuring | |
3104 if (movingUp) { | 1591 if (movingUp) { |
3105 while (movingUp.length) { | 1592 while (movingUp.length) { |
3106 var idx = movingUp.pop(); | 1593 var idx = movingUp.pop(); |
3107 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 1594 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
3108 } | 1595 } |
3109 } | 1596 } |
3110 // update the position of the items | |
3111 this._positionItems(); | 1597 this._positionItems(); |
3112 // set the scroller size | |
3113 this._updateScrollerSize(); | 1598 this._updateScrollerSize(); |
3114 // increase the pool of physical items | |
3115 this._increasePoolIfNeeded(); | 1599 this._increasePoolIfNeeded(); |
3116 }, | 1600 }, |
3117 | |
3118 /** | |
3119 * Creates a pool of DOM elements and attaches them to the local dom. | |
3120 */ | |
3121 _createPool: function(size) { | 1601 _createPool: function(size) { |
3122 var physicalItems = new Array(size); | 1602 var physicalItems = new Array(size); |
3123 | |
3124 this._ensureTemplatized(); | 1603 this._ensureTemplatized(); |
3125 | |
3126 for (var i = 0; i < size; i++) { | 1604 for (var i = 0; i < size; i++) { |
3127 var inst = this.stamp(null); | 1605 var inst = this.stamp(null); |
3128 // First element child is item; Safari doesn't support children[0] | |
3129 // on a doc fragment | |
3130 physicalItems[i] = inst.root.querySelector('*'); | 1606 physicalItems[i] = inst.root.querySelector('*'); |
3131 Polymer.dom(this).appendChild(inst.root); | 1607 Polymer.dom(this).appendChild(inst.root); |
3132 } | 1608 } |
3133 return physicalItems; | 1609 return physicalItems; |
3134 }, | 1610 }, |
3135 | |
3136 /** | |
3137 * Increases the pool of physical items only if needed. | |
3138 * | |
3139 * @return {boolean} True if the pool was increased. | |
3140 */ | |
3141 _increasePoolIfNeeded: function() { | 1611 _increasePoolIfNeeded: function() { |
3142 // Base case 1: the list has no height. | |
3143 if (this._viewportHeight === 0) { | 1612 if (this._viewportHeight === 0) { |
3144 return false; | 1613 return false; |
3145 } | 1614 } |
3146 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
3147 // with physical items, don't increase the pool. | |
3148 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 1615 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
3149 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 1616 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
3150 return false; | 1617 return false; |
3151 } | 1618 } |
3152 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
3153 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 1619 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
3154 | |
3155 if (currentPage === 0) { | 1620 if (currentPage === 0) { |
3156 // fill the first page | 1621 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * .5))); |
3157 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
3158 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 1622 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
3159 // paint the page and defer the next increase | |
3160 // wait 16ms which is rough enough to get paint cycle. | |
3161 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 1623 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
3162 } else { | 1624 } else { |
3163 // fill the rest of the pages | |
3164 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 1625 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
3165 } | 1626 } |
3166 | |
3167 this._lastPage = currentPage; | 1627 this._lastPage = currentPage; |
3168 | |
3169 return true; | 1628 return true; |
3170 }, | 1629 }, |
3171 | |
3172 /** | |
3173 * Increases the pool size. | |
3174 */ | |
3175 _increasePool: function(missingItems) { | 1630 _increasePool: function(missingItems) { |
3176 var nextPhysicalCount = Math.min( | 1631 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this.
_virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS
ICAL_COUNT)); |
3177 this._physicalCount + missingItems, | |
3178 this._virtualCount - this._virtualStart, | |
3179 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
3180 ); | |
3181 var prevPhysicalCount = this._physicalCount; | 1632 var prevPhysicalCount = this._physicalCount; |
3182 var delta = nextPhysicalCount - prevPhysicalCount; | 1633 var delta = nextPhysicalCount - prevPhysicalCount; |
3183 | |
3184 if (delta <= 0) { | 1634 if (delta <= 0) { |
3185 return; | 1635 return; |
3186 } | 1636 } |
3187 | |
3188 [].push.apply(this._physicalItems, this._createPool(delta)); | 1637 [].push.apply(this._physicalItems, this._createPool(delta)); |
3189 [].push.apply(this._physicalSizes, new Array(delta)); | 1638 [].push.apply(this._physicalSizes, new Array(delta)); |
3190 | |
3191 this._physicalCount = prevPhysicalCount + delta; | 1639 this._physicalCount = prevPhysicalCount + delta; |
3192 | 1640 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this.
_focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd
) { |
3193 // update the physical start if we need to preserve the model of the focus
ed item. | |
3194 // In this situation, the focused item is currently rendered and its model
would | |
3195 // have changed after increasing the pool if the physical start remained u
nchanged. | |
3196 if (this._physicalStart > this._physicalEnd && | |
3197 this._isIndexRendered(this._focusedIndex) && | |
3198 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
3199 this._physicalStart = this._physicalStart + delta; | 1641 this._physicalStart = this._physicalStart + delta; |
3200 } | 1642 } |
3201 this._update(); | 1643 this._update(); |
3202 }, | 1644 }, |
3203 | |
3204 /** | |
3205 * Render a new list of items. This method does exactly the same as `update`
, | |
3206 * but it also ensures that only one `update` cycle is created. | |
3207 */ | |
3208 _render: function() { | 1645 _render: function() { |
3209 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 1646 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
3210 | |
3211 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 1647 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
3212 this._lastPage = 0; | 1648 this._lastPage = 0; |
3213 this._update(); | 1649 this._update(); |
3214 this._itemsRendered = true; | 1650 this._itemsRendered = true; |
3215 } | 1651 } |
3216 }, | 1652 }, |
3217 | |
3218 /** | |
3219 * Templetizes the user template. | |
3220 */ | |
3221 _ensureTemplatized: function() { | 1653 _ensureTemplatized: function() { |
3222 if (!this.ctor) { | 1654 if (!this.ctor) { |
3223 // Template instance props that should be excluded from forwarding | |
3224 var props = {}; | 1655 var props = {}; |
3225 props.__key__ = true; | 1656 props.__key__ = true; |
3226 props[this.as] = true; | 1657 props[this.as] = true; |
3227 props[this.indexAs] = true; | 1658 props[this.indexAs] = true; |
3228 props[this.selectedAs] = true; | 1659 props[this.selectedAs] = true; |
3229 props.tabIndex = true; | 1660 props.tabIndex = true; |
3230 | |
3231 this._instanceProps = props; | 1661 this._instanceProps = props; |
3232 this._userTemplate = Polymer.dom(this).querySelector('template'); | 1662 this._userTemplate = Polymer.dom(this).querySelector('template'); |
3233 | |
3234 if (this._userTemplate) { | 1663 if (this._userTemplate) { |
3235 this.templatize(this._userTemplate); | 1664 this.templatize(this._userTemplate); |
3236 } else { | 1665 } else { |
3237 console.warn('iron-list requires a template to be provided in light-do
m'); | 1666 console.warn('iron-list requires a template to be provided in light-do
m'); |
3238 } | 1667 } |
3239 } | 1668 } |
3240 }, | 1669 }, |
3241 | |
3242 /** | |
3243 * Implements extension point from Templatizer mixin. | |
3244 */ | |
3245 _getStampedChildren: function() { | 1670 _getStampedChildren: function() { |
3246 return this._physicalItems; | 1671 return this._physicalItems; |
3247 }, | 1672 }, |
3248 | |
3249 /** | |
3250 * Implements extension point from Templatizer | |
3251 * Called as a side effect of a template instance path change, responsible | |
3252 * for notifying items.<key-for-instance>.<path> change up to host. | |
3253 */ | |
3254 _forwardInstancePath: function(inst, path, value) { | 1673 _forwardInstancePath: function(inst, path, value) { |
3255 if (path.indexOf(this.as + '.') === 0) { | 1674 if (path.indexOf(this.as + '.') === 0) { |
3256 this.notifyPath('items.' + inst.__key__ + '.' + | 1675 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt
h + 1), value); |
3257 path.slice(this.as.length + 1), value); | |
3258 } | 1676 } |
3259 }, | 1677 }, |
3260 | |
3261 /** | |
3262 * Implements extension point from Templatizer mixin | |
3263 * Called as side-effect of a host property change, responsible for | |
3264 * notifying parent path change on each row. | |
3265 */ | |
3266 _forwardParentProp: function(prop, value) { | 1678 _forwardParentProp: function(prop, value) { |
3267 if (this._physicalItems) { | 1679 if (this._physicalItems) { |
3268 this._physicalItems.forEach(function(item) { | 1680 this._physicalItems.forEach(function(item) { |
3269 item._templateInstance[prop] = value; | 1681 item._templateInstance[prop] = value; |
3270 }, this); | 1682 }, this); |
3271 } | 1683 } |
3272 }, | 1684 }, |
3273 | |
3274 /** | |
3275 * Implements extension point from Templatizer | |
3276 * Called as side-effect of a host path change, responsible for | |
3277 * notifying parent.<path> path change on each row. | |
3278 */ | |
3279 _forwardParentPath: function(path, value) { | 1685 _forwardParentPath: function(path, value) { |
3280 if (this._physicalItems) { | 1686 if (this._physicalItems) { |
3281 this._physicalItems.forEach(function(item) { | 1687 this._physicalItems.forEach(function(item) { |
3282 item._templateInstance.notifyPath(path, value, true); | 1688 item._templateInstance.notifyPath(path, value, true); |
3283 }, this); | 1689 }, this); |
3284 } | 1690 } |
3285 }, | 1691 }, |
3286 | |
3287 /** | |
3288 * Called as a side effect of a host items.<key>.<path> path change, | |
3289 * responsible for notifying item.<path> changes. | |
3290 */ | |
3291 _forwardItemPath: function(path, value) { | 1692 _forwardItemPath: function(path, value) { |
3292 if (!this._physicalIndexForKey) { | 1693 if (!this._physicalIndexForKey) { |
3293 return; | 1694 return; |
3294 } | 1695 } |
3295 var dot = path.indexOf('.'); | 1696 var dot = path.indexOf('.'); |
3296 var key = path.substring(0, dot < 0 ? path.length : dot); | 1697 var key = path.substring(0, dot < 0 ? path.length : dot); |
3297 var idx = this._physicalIndexForKey[key]; | 1698 var idx = this._physicalIndexForKey[key]; |
3298 var offscreenItem = this._offscreenFocusedItem; | 1699 var offscreenItem = this._offscreenFocusedItem; |
3299 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 1700 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? offscreenItem : this._physicalItems[idx]; |
3300 offscreenItem : this._physicalItems[idx]; | |
3301 | |
3302 if (!el || el._templateInstance.__key__ !== key) { | 1701 if (!el || el._templateInstance.__key__ !== key) { |
3303 return; | 1702 return; |
3304 } | 1703 } |
3305 if (dot >= 0) { | 1704 if (dot >= 0) { |
3306 path = this.as + '.' + path.substring(dot+1); | 1705 path = this.as + '.' + path.substring(dot + 1); |
3307 el._templateInstance.notifyPath(path, value, true); | 1706 el._templateInstance.notifyPath(path, value, true); |
3308 } else { | 1707 } else { |
3309 // Update selection if needed | |
3310 var currentItem = el._templateInstance[this.as]; | 1708 var currentItem = el._templateInstance[this.as]; |
3311 if (Array.isArray(this.selectedItems)) { | 1709 if (Array.isArray(this.selectedItems)) { |
3312 for (var i = 0; i < this.selectedItems.length; i++) { | 1710 for (var i = 0; i < this.selectedItems.length; i++) { |
3313 if (this.selectedItems[i] === currentItem) { | 1711 if (this.selectedItems[i] === currentItem) { |
3314 this.set('selectedItems.' + i, value); | 1712 this.set('selectedItems.' + i, value); |
3315 break; | 1713 break; |
3316 } | 1714 } |
3317 } | 1715 } |
3318 } else if (this.selectedItem === currentItem) { | 1716 } else if (this.selectedItem === currentItem) { |
3319 this.set('selectedItem', value); | 1717 this.set('selectedItem', value); |
3320 } | 1718 } |
3321 el._templateInstance[this.as] = value; | 1719 el._templateInstance[this.as] = value; |
3322 } | 1720 } |
3323 }, | 1721 }, |
3324 | |
3325 /** | |
3326 * Called when the items have changed. That is, ressignments | |
3327 * to `items`, splices or updates to a single item. | |
3328 */ | |
3329 _itemsChanged: function(change) { | 1722 _itemsChanged: function(change) { |
3330 if (change.path === 'items') { | 1723 if (change.path === 'items') { |
3331 // reset items | |
3332 this._virtualStart = 0; | 1724 this._virtualStart = 0; |
3333 this._physicalTop = 0; | 1725 this._physicalTop = 0; |
3334 this._virtualCount = this.items ? this.items.length : 0; | 1726 this._virtualCount = this.items ? this.items.length : 0; |
3335 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 1727 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
3336 this._physicalIndexForKey = {}; | 1728 this._physicalIndexForKey = {}; |
3337 this._firstVisibleIndexVal = null; | 1729 this._firstVisibleIndexVal = null; |
3338 this._lastVisibleIndexVal = null; | 1730 this._lastVisibleIndexVal = null; |
3339 | |
3340 this._resetScrollPosition(0); | 1731 this._resetScrollPosition(0); |
3341 this._removeFocusedItem(); | 1732 this._removeFocusedItem(); |
3342 // create the initial physical items | |
3343 if (!this._physicalItems) { | 1733 if (!this._physicalItems) { |
3344 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 1734 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
3345 this._physicalItems = this._createPool(this._physicalCount); | 1735 this._physicalItems = this._createPool(this._physicalCount); |
3346 this._physicalSizes = new Array(this._physicalCount); | 1736 this._physicalSizes = new Array(this._physicalCount); |
3347 } | 1737 } |
3348 | |
3349 this._physicalStart = 0; | 1738 this._physicalStart = 0; |
3350 | |
3351 } else if (change.path === 'items.splices') { | 1739 } else if (change.path === 'items.splices') { |
3352 | |
3353 this._adjustVirtualIndex(change.value.indexSplices); | 1740 this._adjustVirtualIndex(change.value.indexSplices); |
3354 this._virtualCount = this.items ? this.items.length : 0; | 1741 this._virtualCount = this.items ? this.items.length : 0; |
3355 | |
3356 } else { | 1742 } else { |
3357 // update a single item | |
3358 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 1743 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
3359 return; | 1744 return; |
3360 } | 1745 } |
3361 | |
3362 this._itemsRendered = false; | 1746 this._itemsRendered = false; |
3363 this._debounceTemplate(this._render); | 1747 this._debounceTemplate(this._render); |
3364 }, | 1748 }, |
3365 | |
3366 /** | |
3367 * @param {!Array<!PolymerSplice>} splices | |
3368 */ | |
3369 _adjustVirtualIndex: function(splices) { | 1749 _adjustVirtualIndex: function(splices) { |
3370 splices.forEach(function(splice) { | 1750 splices.forEach(function(splice) { |
3371 // deselect removed items | |
3372 splice.removed.forEach(this._removeItem, this); | 1751 splice.removed.forEach(this._removeItem, this); |
3373 // We only need to care about changes happening above the current positi
on | |
3374 if (splice.index < this._virtualStart) { | 1752 if (splice.index < this._virtualStart) { |
3375 var delta = Math.max( | 1753 var delta = Math.max(splice.addedCount - splice.removed.length, splice
.index - this._virtualStart); |
3376 splice.addedCount - splice.removed.length, | |
3377 splice.index - this._virtualStart); | |
3378 | |
3379 this._virtualStart = this._virtualStart + delta; | 1754 this._virtualStart = this._virtualStart + delta; |
3380 | |
3381 if (this._focusedIndex >= 0) { | 1755 if (this._focusedIndex >= 0) { |
3382 this._focusedIndex = this._focusedIndex + delta; | 1756 this._focusedIndex = this._focusedIndex + delta; |
3383 } | 1757 } |
3384 } | 1758 } |
3385 }, this); | 1759 }, this); |
3386 }, | 1760 }, |
3387 | |
3388 _removeItem: function(item) { | 1761 _removeItem: function(item) { |
3389 this.$.selector.deselect(item); | 1762 this.$.selector.deselect(item); |
3390 // remove the current focused item | |
3391 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 1763 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
3392 this._removeFocusedItem(); | 1764 this._removeFocusedItem(); |
3393 } | 1765 } |
3394 }, | 1766 }, |
3395 | |
3396 /** | |
3397 * Executes a provided function per every physical index in `itemSet` | |
3398 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
3399 * | |
3400 * @param {!function(number, number)} fn | |
3401 * @param {!Array<number>=} itemSet | |
3402 */ | |
3403 _iterateItems: function(fn, itemSet) { | 1767 _iterateItems: function(fn, itemSet) { |
3404 var pidx, vidx, rtn, i; | 1768 var pidx, vidx, rtn, i; |
3405 | |
3406 if (arguments.length === 2 && itemSet) { | 1769 if (arguments.length === 2 && itemSet) { |
3407 for (i = 0; i < itemSet.length; i++) { | 1770 for (i = 0; i < itemSet.length; i++) { |
3408 pidx = itemSet[i]; | 1771 pidx = itemSet[i]; |
3409 vidx = this._computeVidx(pidx); | 1772 vidx = this._computeVidx(pidx); |
3410 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1773 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3411 return rtn; | 1774 return rtn; |
3412 } | 1775 } |
3413 } | 1776 } |
3414 } else { | 1777 } else { |
3415 pidx = this._physicalStart; | 1778 pidx = this._physicalStart; |
3416 vidx = this._virtualStart; | 1779 vidx = this._virtualStart; |
3417 | 1780 for (;pidx < this._physicalCount; pidx++, vidx++) { |
3418 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
3419 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1781 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3420 return rtn; | 1782 return rtn; |
3421 } | 1783 } |
3422 } | 1784 } |
3423 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 1785 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
3424 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1786 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3425 return rtn; | 1787 return rtn; |
3426 } | 1788 } |
3427 } | 1789 } |
3428 } | 1790 } |
3429 }, | 1791 }, |
3430 | |
3431 /** | |
3432 * Returns the virtual index for a given physical index | |
3433 * | |
3434 * @param {number} pidx Physical index | |
3435 * @return {number} | |
3436 */ | |
3437 _computeVidx: function(pidx) { | 1792 _computeVidx: function(pidx) { |
3438 if (pidx >= this._physicalStart) { | 1793 if (pidx >= this._physicalStart) { |
3439 return this._virtualStart + (pidx - this._physicalStart); | 1794 return this._virtualStart + (pidx - this._physicalStart); |
3440 } | 1795 } |
3441 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 1796 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
3442 }, | 1797 }, |
3443 | |
3444 /** | |
3445 * Assigns the data models to a given set of items. | |
3446 * @param {!Array<number>=} itemSet | |
3447 */ | |
3448 _assignModels: function(itemSet) { | 1798 _assignModels: function(itemSet) { |
3449 this._iterateItems(function(pidx, vidx) { | 1799 this._iterateItems(function(pidx, vidx) { |
3450 var el = this._physicalItems[pidx]; | 1800 var el = this._physicalItems[pidx]; |
3451 var inst = el._templateInstance; | 1801 var inst = el._templateInstance; |
3452 var item = this.items && this.items[vidx]; | 1802 var item = this.items && this.items[vidx]; |
3453 | |
3454 if (item != null) { | 1803 if (item != null) { |
3455 inst[this.as] = item; | 1804 inst[this.as] = item; |
3456 inst.__key__ = this._collection.getKey(item); | 1805 inst.__key__ = this._collection.getKey(item); |
3457 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 1806 inst[this.selectedAs] = this.$.selector.isSelected(item); |
3458 inst[this.indexAs] = vidx; | 1807 inst[this.indexAs] = vidx; |
3459 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 1808 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
3460 this._physicalIndexForKey[inst.__key__] = pidx; | 1809 this._physicalIndexForKey[inst.__key__] = pidx; |
3461 el.removeAttribute('hidden'); | 1810 el.removeAttribute('hidden'); |
3462 } else { | 1811 } else { |
3463 inst.__key__ = null; | 1812 inst.__key__ = null; |
3464 el.setAttribute('hidden', ''); | 1813 el.setAttribute('hidden', ''); |
3465 } | 1814 } |
3466 }, itemSet); | 1815 }, itemSet); |
3467 }, | 1816 }, |
3468 | 1817 _updateMetrics: function(itemSet) { |
3469 /** | |
3470 * Updates the height for a given set of items. | |
3471 * | |
3472 * @param {!Array<number>=} itemSet | |
3473 */ | |
3474 _updateMetrics: function(itemSet) { | |
3475 // Make sure we distributed all the physical items | |
3476 // so we can measure them | |
3477 Polymer.dom.flush(); | 1818 Polymer.dom.flush(); |
3478 | |
3479 var newPhysicalSize = 0; | 1819 var newPhysicalSize = 0; |
3480 var oldPhysicalSize = 0; | 1820 var oldPhysicalSize = 0; |
3481 var prevAvgCount = this._physicalAverageCount; | 1821 var prevAvgCount = this._physicalAverageCount; |
3482 var prevPhysicalAvg = this._physicalAverage; | 1822 var prevPhysicalAvg = this._physicalAverage; |
3483 | |
3484 this._iterateItems(function(pidx, vidx) { | 1823 this._iterateItems(function(pidx, vidx) { |
3485 | |
3486 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 1824 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
3487 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 1825 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
3488 newPhysicalSize += this._physicalSizes[pidx]; | 1826 newPhysicalSize += this._physicalSizes[pidx]; |
3489 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 1827 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
3490 | |
3491 }, itemSet); | 1828 }, itemSet); |
3492 | |
3493 this._viewportHeight = this._scrollTargetHeight; | 1829 this._viewportHeight = this._scrollTargetHeight; |
3494 if (this.grid) { | 1830 if (this.grid) { |
3495 this._updateGridMetrics(); | 1831 this._updateGridMetrics(); |
3496 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 1832 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
3497 } else { | 1833 } else { |
3498 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 1834 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
3499 } | 1835 } |
3500 | |
3501 // update the average if we measured something | |
3502 if (this._physicalAverageCount !== prevAvgCount) { | 1836 if (this._physicalAverageCount !== prevAvgCount) { |
3503 this._physicalAverage = Math.round( | 1837 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new
PhysicalSize) / this._physicalAverageCount); |
3504 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
3505 this._physicalAverageCount); | |
3506 } | 1838 } |
3507 }, | 1839 }, |
3508 | |
3509 _updateGridMetrics: function() { | 1840 _updateGridMetrics: function() { |
3510 this._viewportWidth = this.$.items.offsetWidth; | 1841 this._viewportWidth = this.$.items.offsetWidth; |
3511 // Set item width to the value of the _physicalItems offsetWidth | |
3512 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 1842 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
3513 // Set row height to the value of the _physicalItems offsetHeight | |
3514 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 1843 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
3515 // If in grid mode compute how many items with exist in each row | |
3516 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 1844 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
3517 }, | 1845 }, |
3518 | |
3519 /** | |
3520 * Updates the position of the physical items. | |
3521 */ | |
3522 _positionItems: function() { | 1846 _positionItems: function() { |
3523 this._adjustScrollPosition(); | 1847 this._adjustScrollPosition(); |
3524 | |
3525 var y = this._physicalTop; | 1848 var y = this._physicalTop; |
3526 | |
3527 if (this.grid) { | 1849 if (this.grid) { |
3528 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 1850 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
3529 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 1851 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
3530 | |
3531 this._iterateItems(function(pidx, vidx) { | 1852 this._iterateItems(function(pidx, vidx) { |
3532 | |
3533 var modulus = vidx % this._itemsPerRow; | 1853 var modulus = vidx % this._itemsPerRow; |
3534 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | 1854 var x = Math.floor(modulus * this._itemWidth + rowOffset); |
3535 | |
3536 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | 1855 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
3537 | |
3538 if (this._shouldRenderNextRow(vidx)) { | 1856 if (this._shouldRenderNextRow(vidx)) { |
3539 y += this._rowHeight; | 1857 y += this._rowHeight; |
3540 } | 1858 } |
3541 | |
3542 }); | 1859 }); |
3543 } else { | 1860 } else { |
3544 this._iterateItems(function(pidx, vidx) { | 1861 this._iterateItems(function(pidx, vidx) { |
3545 | |
3546 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 1862 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
3547 y += this._physicalSizes[pidx]; | 1863 y += this._physicalSizes[pidx]; |
3548 | |
3549 }); | 1864 }); |
3550 } | 1865 } |
3551 }, | 1866 }, |
3552 | |
3553 _getPhysicalSizeIncrement: function(pidx) { | 1867 _getPhysicalSizeIncrement: function(pidx) { |
3554 if (!this.grid) { | 1868 if (!this.grid) { |
3555 return this._physicalSizes[pidx]; | 1869 return this._physicalSizes[pidx]; |
3556 } | 1870 } |
3557 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 1871 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
3558 return 0; | 1872 return 0; |
3559 } | 1873 } |
3560 return this._rowHeight; | 1874 return this._rowHeight; |
3561 }, | 1875 }, |
3562 | |
3563 /** | |
3564 * Returns, based on the current index, | |
3565 * whether or not the next index will need | |
3566 * to be rendered on a new row. | |
3567 * | |
3568 * @param {number} vidx Virtual index | |
3569 * @return {boolean} | |
3570 */ | |
3571 _shouldRenderNextRow: function(vidx) { | 1876 _shouldRenderNextRow: function(vidx) { |
3572 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 1877 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
3573 }, | 1878 }, |
3574 | |
3575 /** | |
3576 * Adjusts the scroll position when it was overestimated. | |
3577 */ | |
3578 _adjustScrollPosition: function() { | 1879 _adjustScrollPosition: function() { |
3579 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 1880 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(
this._scrollPosition + this._physicalTop, 0); |
3580 Math.min(this._scrollPosition + this._physicalTop, 0); | |
3581 | |
3582 if (deltaHeight) { | 1881 if (deltaHeight) { |
3583 this._physicalTop = this._physicalTop - deltaHeight; | 1882 this._physicalTop = this._physicalTop - deltaHeight; |
3584 // juking scroll position during interial scrolling on iOS is no bueno | |
3585 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 1883 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
3586 this._resetScrollPosition(this._scrollTop - deltaHeight); | 1884 this._resetScrollPosition(this._scrollTop - deltaHeight); |
3587 } | 1885 } |
3588 } | 1886 } |
3589 }, | 1887 }, |
3590 | |
3591 /** | |
3592 * Sets the position of the scroll. | |
3593 */ | |
3594 _resetScrollPosition: function(pos) { | 1888 _resetScrollPosition: function(pos) { |
3595 if (this.scrollTarget) { | 1889 if (this.scrollTarget) { |
3596 this._scrollTop = pos; | 1890 this._scrollTop = pos; |
3597 this._scrollPosition = this._scrollTop; | 1891 this._scrollPosition = this._scrollTop; |
3598 } | 1892 } |
3599 }, | 1893 }, |
3600 | |
3601 /** | |
3602 * Sets the scroll height, that's the height of the content, | |
3603 * | |
3604 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
3605 */ | |
3606 _updateScrollerSize: function(forceUpdate) { | 1894 _updateScrollerSize: function(forceUpdate) { |
3607 if (this.grid) { | 1895 if (this.grid) { |
3608 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 1896 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
3609 } else { | 1897 } else { |
3610 this._estScrollHeight = (this._physicalBottom + | 1898 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou
nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage; |
3611 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
3612 } | 1899 } |
3613 | |
3614 forceUpdate = forceUpdate || this._scrollHeight === 0; | 1900 forceUpdate = forceUpdate || this._scrollHeight === 0; |
3615 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 1901 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
3616 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 1902 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
3617 | |
3618 // amortize height adjustment, so it won't trigger repaints very often | |
3619 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 1903 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
3620 this.$.items.style.height = this._estScrollHeight + 'px'; | 1904 this.$.items.style.height = this._estScrollHeight + 'px'; |
3621 this._scrollHeight = this._estScrollHeight; | 1905 this._scrollHeight = this._estScrollHeight; |
3622 } | 1906 } |
3623 }, | 1907 }, |
3624 | 1908 scrollToItem: function(item) { |
3625 /** | |
3626 * Scroll to a specific item in the virtual list regardless | |
3627 * of the physical items in the DOM tree. | |
3628 * | |
3629 * @method scrollToItem | |
3630 * @param {(Object)} item The item to be scrolled to | |
3631 */ | |
3632 scrollToItem: function(item){ | |
3633 return this.scrollToIndex(this.items.indexOf(item)); | 1909 return this.scrollToIndex(this.items.indexOf(item)); |
3634 }, | 1910 }, |
3635 | |
3636 /** | |
3637 * Scroll to a specific index in the virtual list regardless | |
3638 * of the physical items in the DOM tree. | |
3639 * | |
3640 * @method scrollToIndex | |
3641 * @param {number} idx The index of the item | |
3642 */ | |
3643 scrollToIndex: function(idx) { | 1911 scrollToIndex: function(idx) { |
3644 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 1912 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
3645 return; | 1913 return; |
3646 } | 1914 } |
3647 | |
3648 Polymer.dom.flush(); | 1915 Polymer.dom.flush(); |
3649 | 1916 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1); |
3650 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
3651 // update the virtual start only when needed | |
3652 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 1917 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
3653 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 1918 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1; |
3654 } | 1919 } |
3655 // manage focus | |
3656 this._manageFocus(); | 1920 this._manageFocus(); |
3657 // assign new models | |
3658 this._assignModels(); | 1921 this._assignModels(); |
3659 // measure the new sizes | |
3660 this._updateMetrics(); | 1922 this._updateMetrics(); |
3661 | 1923 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
3662 // estimate new physical offset | |
3663 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
3664 this._physicalTop = estPhysicalTop; | 1924 this._physicalTop = estPhysicalTop; |
3665 | |
3666 var currentTopItem = this._physicalStart; | 1925 var currentTopItem = this._physicalStart; |
3667 var currentVirtualItem = this._virtualStart; | 1926 var currentVirtualItem = this._virtualStart; |
3668 var targetOffsetTop = 0; | 1927 var targetOffsetTop = 0; |
3669 var hiddenContentSize = this._hiddenContentSize; | 1928 var hiddenContentSize = this._hiddenContentSize; |
3670 | |
3671 // scroll to the item as much as we can | |
3672 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 1929 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
3673 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 1930 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
3674 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 1931 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
3675 currentVirtualItem++; | 1932 currentVirtualItem++; |
3676 } | 1933 } |
3677 // update the scroller size | |
3678 this._updateScrollerSize(true); | 1934 this._updateScrollerSize(true); |
3679 // update the position of the items | |
3680 this._positionItems(); | 1935 this._positionItems(); |
3681 // set the new scroll position | |
3682 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 1936 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
3683 // increase the pool of physical items if needed | |
3684 this._increasePoolIfNeeded(); | 1937 this._increasePoolIfNeeded(); |
3685 // clear cached visible index | |
3686 this._firstVisibleIndexVal = null; | 1938 this._firstVisibleIndexVal = null; |
3687 this._lastVisibleIndexVal = null; | 1939 this._lastVisibleIndexVal = null; |
3688 }, | 1940 }, |
3689 | |
3690 /** | |
3691 * Reset the physical average and the average count. | |
3692 */ | |
3693 _resetAverage: function() { | 1941 _resetAverage: function() { |
3694 this._physicalAverage = 0; | 1942 this._physicalAverage = 0; |
3695 this._physicalAverageCount = 0; | 1943 this._physicalAverageCount = 0; |
3696 }, | 1944 }, |
3697 | |
3698 /** | |
3699 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
3700 * when the element is resized. | |
3701 */ | |
3702 _resizeHandler: function() { | 1945 _resizeHandler: function() { |
3703 // iOS fires the resize event when the address bar slides up | |
3704 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 1946 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
3705 return; | 1947 return; |
3706 } | 1948 } |
3707 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
3708 // changing the scroll position from a resize handler would result in | |
3709 // the scroll position being reset. Waiting 1ms fixes the issue. | |
3710 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 1949 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
3711 this.updateViewportBoundaries(); | 1950 this.updateViewportBoundaries(); |
3712 this._render(); | 1951 this._render(); |
3713 | |
3714 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 1952 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
3715 this._resetAverage(); | 1953 this._resetAverage(); |
3716 this.scrollToIndex(this.firstVisibleIndex); | 1954 this.scrollToIndex(this.firstVisibleIndex); |
3717 } | 1955 } |
3718 }.bind(this), 1)); | 1956 }.bind(this), 1)); |
3719 }, | 1957 }, |
3720 | |
3721 _getModelFromItem: function(item) { | 1958 _getModelFromItem: function(item) { |
3722 var key = this._collection.getKey(item); | 1959 var key = this._collection.getKey(item); |
3723 var pidx = this._physicalIndexForKey[key]; | 1960 var pidx = this._physicalIndexForKey[key]; |
3724 | |
3725 if (pidx != null) { | 1961 if (pidx != null) { |
3726 return this._physicalItems[pidx]._templateInstance; | 1962 return this._physicalItems[pidx]._templateInstance; |
3727 } | 1963 } |
3728 return null; | 1964 return null; |
3729 }, | 1965 }, |
3730 | |
3731 /** | |
3732 * Gets a valid item instance from its index or the object value. | |
3733 * | |
3734 * @param {(Object|number)} item The item object or its index | |
3735 */ | |
3736 _getNormalizedItem: function(item) { | 1966 _getNormalizedItem: function(item) { |
3737 if (this._collection.getKey(item) === undefined) { | 1967 if (this._collection.getKey(item) === undefined) { |
3738 if (typeof item === 'number') { | 1968 if (typeof item === 'number') { |
3739 item = this.items[item]; | 1969 item = this.items[item]; |
3740 if (!item) { | 1970 if (!item) { |
3741 throw new RangeError('<item> not found'); | 1971 throw new RangeError('<item> not found'); |
3742 } | 1972 } |
3743 return item; | 1973 return item; |
3744 } | 1974 } |
3745 throw new TypeError('<item> should be a valid item'); | 1975 throw new TypeError('<item> should be a valid item'); |
3746 } | 1976 } |
3747 return item; | 1977 return item; |
3748 }, | 1978 }, |
3749 | |
3750 /** | |
3751 * Select the list item at the given index. | |
3752 * | |
3753 * @method selectItem | |
3754 * @param {(Object|number)} item The item object or its index | |
3755 */ | |
3756 selectItem: function(item) { | 1979 selectItem: function(item) { |
3757 item = this._getNormalizedItem(item); | 1980 item = this._getNormalizedItem(item); |
3758 var model = this._getModelFromItem(item); | 1981 var model = this._getModelFromItem(item); |
3759 | |
3760 if (!this.multiSelection && this.selectedItem) { | 1982 if (!this.multiSelection && this.selectedItem) { |
3761 this.deselectItem(this.selectedItem); | 1983 this.deselectItem(this.selectedItem); |
3762 } | 1984 } |
3763 if (model) { | 1985 if (model) { |
3764 model[this.selectedAs] = true; | 1986 model[this.selectedAs] = true; |
3765 } | 1987 } |
3766 this.$.selector.select(item); | 1988 this.$.selector.select(item); |
3767 this.updateSizeForItem(item); | 1989 this.updateSizeForItem(item); |
3768 }, | 1990 }, |
3769 | |
3770 /** | |
3771 * Deselects the given item list if it is already selected. | |
3772 * | |
3773 | |
3774 * @method deselect | |
3775 * @param {(Object|number)} item The item object or its index | |
3776 */ | |
3777 deselectItem: function(item) { | 1991 deselectItem: function(item) { |
3778 item = this._getNormalizedItem(item); | 1992 item = this._getNormalizedItem(item); |
3779 var model = this._getModelFromItem(item); | 1993 var model = this._getModelFromItem(item); |
3780 | |
3781 if (model) { | 1994 if (model) { |
3782 model[this.selectedAs] = false; | 1995 model[this.selectedAs] = false; |
3783 } | 1996 } |
3784 this.$.selector.deselect(item); | 1997 this.$.selector.deselect(item); |
3785 this.updateSizeForItem(item); | 1998 this.updateSizeForItem(item); |
3786 }, | 1999 }, |
3787 | |
3788 /** | |
3789 * Select or deselect a given item depending on whether the item | |
3790 * has already been selected. | |
3791 * | |
3792 * @method toggleSelectionForItem | |
3793 * @param {(Object|number)} item The item object or its index | |
3794 */ | |
3795 toggleSelectionForItem: function(item) { | 2000 toggleSelectionForItem: function(item) { |
3796 item = this._getNormalizedItem(item); | 2001 item = this._getNormalizedItem(item); |
3797 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 2002 if (this.$.selector.isSelected(item)) { |
3798 this.deselectItem(item); | 2003 this.deselectItem(item); |
3799 } else { | 2004 } else { |
3800 this.selectItem(item); | 2005 this.selectItem(item); |
3801 } | 2006 } |
3802 }, | 2007 }, |
3803 | |
3804 /** | |
3805 * Clears the current selection state of the list. | |
3806 * | |
3807 * @method clearSelection | |
3808 */ | |
3809 clearSelection: function() { | 2008 clearSelection: function() { |
3810 function unselect(item) { | 2009 function unselect(item) { |
3811 var model = this._getModelFromItem(item); | 2010 var model = this._getModelFromItem(item); |
3812 if (model) { | 2011 if (model) { |
3813 model[this.selectedAs] = false; | 2012 model[this.selectedAs] = false; |
3814 } | 2013 } |
3815 } | 2014 } |
3816 | |
3817 if (Array.isArray(this.selectedItems)) { | 2015 if (Array.isArray(this.selectedItems)) { |
3818 this.selectedItems.forEach(unselect, this); | 2016 this.selectedItems.forEach(unselect, this); |
3819 } else if (this.selectedItem) { | 2017 } else if (this.selectedItem) { |
3820 unselect.call(this, this.selectedItem); | 2018 unselect.call(this, this.selectedItem); |
3821 } | 2019 } |
3822 | 2020 this.$.selector.clearSelection(); |
3823 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
3824 }, | 2021 }, |
3825 | |
3826 /** | |
3827 * Add an event listener to `tap` if `selectionEnabled` is true, | |
3828 * it will remove the listener otherwise. | |
3829 */ | |
3830 _selectionEnabledChanged: function(selectionEnabled) { | 2022 _selectionEnabledChanged: function(selectionEnabled) { |
3831 var handler = selectionEnabled ? this.listen : this.unlisten; | 2023 var handler = selectionEnabled ? this.listen : this.unlisten; |
3832 handler.call(this, this, 'tap', '_selectionHandler'); | 2024 handler.call(this, this, 'tap', '_selectionHandler'); |
3833 }, | 2025 }, |
3834 | |
3835 /** | |
3836 * Select an item from an event object. | |
3837 */ | |
3838 _selectionHandler: function(e) { | 2026 _selectionHandler: function(e) { |
3839 var model = this.modelForElement(e.target); | 2027 var model = this.modelForElement(e.target); |
3840 if (!model) { | 2028 if (!model) { |
3841 return; | 2029 return; |
3842 } | 2030 } |
3843 var modelTabIndex, activeElTabIndex; | 2031 var modelTabIndex, activeElTabIndex; |
3844 var target = Polymer.dom(e).path[0]; | 2032 var target = Polymer.dom(e).path[0]; |
3845 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 2033 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
3846 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 2034 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
3847 // Safari does not focus certain form controls via mouse | 2035 if (target.localName === 'input' || target.localName === 'button' || targe
t.localName === 'select') { |
3848 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
3849 if (target.localName === 'input' || | |
3850 target.localName === 'button' || | |
3851 target.localName === 'select') { | |
3852 return; | 2036 return; |
3853 } | 2037 } |
3854 // Set a temporary tabindex | |
3855 modelTabIndex = model.tabIndex; | 2038 modelTabIndex = model.tabIndex; |
3856 model.tabIndex = SECRET_TABINDEX; | 2039 model.tabIndex = SECRET_TABINDEX; |
3857 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 2040 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
3858 model.tabIndex = modelTabIndex; | 2041 model.tabIndex = modelTabIndex; |
3859 // Only select the item if the tap wasn't on a focusable child | |
3860 // or the element bound to `tabIndex` | |
3861 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 2042 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
3862 return; | 2043 return; |
3863 } | 2044 } |
3864 this.toggleSelectionForItem(model[this.as]); | 2045 this.toggleSelectionForItem(model[this.as]); |
3865 }, | 2046 }, |
3866 | |
3867 _multiSelectionChanged: function(multiSelection) { | 2047 _multiSelectionChanged: function(multiSelection) { |
3868 this.clearSelection(); | 2048 this.clearSelection(); |
3869 this.$.selector.multi = multiSelection; | 2049 this.$.selector.multi = multiSelection; |
3870 }, | 2050 }, |
3871 | |
3872 /** | |
3873 * Updates the size of an item. | |
3874 * | |
3875 * @method updateSizeForItem | |
3876 * @param {(Object|number)} item The item object or its index | |
3877 */ | |
3878 updateSizeForItem: function(item) { | 2051 updateSizeForItem: function(item) { |
3879 item = this._getNormalizedItem(item); | 2052 item = this._getNormalizedItem(item); |
3880 var key = this._collection.getKey(item); | 2053 var key = this._collection.getKey(item); |
3881 var pidx = this._physicalIndexForKey[key]; | 2054 var pidx = this._physicalIndexForKey[key]; |
3882 | |
3883 if (pidx != null) { | 2055 if (pidx != null) { |
3884 this._updateMetrics([pidx]); | 2056 this._updateMetrics([ pidx ]); |
3885 this._positionItems(); | 2057 this._positionItems(); |
3886 } | 2058 } |
3887 }, | 2059 }, |
3888 | |
3889 /** | |
3890 * Creates a temporary backfill item in the rendered pool of physical items | |
3891 * to replace the main focused item. The focused item has tabIndex = 0 | |
3892 * and might be currently focused by the user. | |
3893 * | |
3894 * This dynamic replacement helps to preserve the focus state. | |
3895 */ | |
3896 _manageFocus: function() { | 2060 _manageFocus: function() { |
3897 var fidx = this._focusedIndex; | 2061 var fidx = this._focusedIndex; |
3898 | |
3899 if (fidx >= 0 && fidx < this._virtualCount) { | 2062 if (fidx >= 0 && fidx < this._virtualCount) { |
3900 // if it's a valid index, check if that index is rendered | |
3901 // in a physical item. | |
3902 if (this._isIndexRendered(fidx)) { | 2063 if (this._isIndexRendered(fidx)) { |
3903 this._restoreFocusedItem(); | 2064 this._restoreFocusedItem(); |
3904 } else { | 2065 } else { |
3905 this._createFocusBackfillItem(); | 2066 this._createFocusBackfillItem(); |
3906 } | 2067 } |
3907 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 2068 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
3908 // otherwise, assign the initial focused index. | |
3909 this._focusedIndex = this._virtualStart; | 2069 this._focusedIndex = this._virtualStart; |
3910 this._focusedItem = this._physicalItems[this._physicalStart]; | 2070 this._focusedItem = this._physicalItems[this._physicalStart]; |
3911 } | 2071 } |
3912 }, | 2072 }, |
3913 | |
3914 _isIndexRendered: function(idx) { | 2073 _isIndexRendered: function(idx) { |
3915 return idx >= this._virtualStart && idx <= this._virtualEnd; | 2074 return idx >= this._virtualStart && idx <= this._virtualEnd; |
3916 }, | 2075 }, |
3917 | |
3918 _isIndexVisible: function(idx) { | 2076 _isIndexVisible: function(idx) { |
3919 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 2077 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
3920 }, | 2078 }, |
3921 | |
3922 _getPhysicalIndex: function(idx) { | 2079 _getPhysicalIndex: function(idx) { |
3923 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 2080 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
3924 }, | 2081 }, |
3925 | |
3926 _focusPhysicalItem: function(idx) { | 2082 _focusPhysicalItem: function(idx) { |
3927 if (idx < 0 || idx >= this._virtualCount) { | 2083 if (idx < 0 || idx >= this._virtualCount) { |
3928 return; | 2084 return; |
3929 } | 2085 } |
3930 this._restoreFocusedItem(); | 2086 this._restoreFocusedItem(); |
3931 // scroll to index to make sure it's rendered | |
3932 if (!this._isIndexRendered(idx)) { | 2087 if (!this._isIndexRendered(idx)) { |
3933 this.scrollToIndex(idx); | 2088 this.scrollToIndex(idx); |
3934 } | 2089 } |
3935 | |
3936 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 2090 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
3937 var model = physicalItem._templateInstance; | 2091 var model = physicalItem._templateInstance; |
3938 var focusable; | 2092 var focusable; |
3939 | |
3940 // set a secret tab index | |
3941 model.tabIndex = SECRET_TABINDEX; | 2093 model.tabIndex = SECRET_TABINDEX; |
3942 // check if focusable element is the physical item | |
3943 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 2094 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
3944 focusable = physicalItem; | 2095 focusable = physicalItem; |
3945 } | 2096 } |
3946 // search for the element which tabindex is bound to the secret tab index | |
3947 if (!focusable) { | 2097 if (!focusable) { |
3948 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 2098 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
3949 } | 2099 } |
3950 // restore the tab index | |
3951 model.tabIndex = 0; | 2100 model.tabIndex = 0; |
3952 // focus the focusable element | |
3953 this._focusedIndex = idx; | 2101 this._focusedIndex = idx; |
3954 focusable && focusable.focus(); | 2102 focusable && focusable.focus(); |
3955 }, | 2103 }, |
3956 | |
3957 _removeFocusedItem: function() { | 2104 _removeFocusedItem: function() { |
3958 if (this._offscreenFocusedItem) { | 2105 if (this._offscreenFocusedItem) { |
3959 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 2106 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
3960 } | 2107 } |
3961 this._offscreenFocusedItem = null; | 2108 this._offscreenFocusedItem = null; |
3962 this._focusBackfillItem = null; | 2109 this._focusBackfillItem = null; |
3963 this._focusedItem = null; | 2110 this._focusedItem = null; |
3964 this._focusedIndex = -1; | 2111 this._focusedIndex = -1; |
3965 }, | 2112 }, |
3966 | |
3967 _createFocusBackfillItem: function() { | 2113 _createFocusBackfillItem: function() { |
3968 var pidx, fidx = this._focusedIndex; | 2114 var pidx, fidx = this._focusedIndex; |
3969 if (this._offscreenFocusedItem || fidx < 0) { | 2115 if (this._offscreenFocusedItem || fidx < 0) { |
3970 return; | 2116 return; |
3971 } | 2117 } |
3972 if (!this._focusBackfillItem) { | 2118 if (!this._focusBackfillItem) { |
3973 // create a physical item, so that it backfills the focused item. | |
3974 var stampedTemplate = this.stamp(null); | 2119 var stampedTemplate = this.stamp(null); |
3975 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 2120 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
3976 Polymer.dom(this).appendChild(stampedTemplate.root); | 2121 Polymer.dom(this).appendChild(stampedTemplate.root); |
3977 } | 2122 } |
3978 // get the physical index for the focused index | |
3979 pidx = this._getPhysicalIndex(fidx); | 2123 pidx = this._getPhysicalIndex(fidx); |
3980 | |
3981 if (pidx != null) { | 2124 if (pidx != null) { |
3982 // set the offcreen focused physical item | |
3983 this._offscreenFocusedItem = this._physicalItems[pidx]; | 2125 this._offscreenFocusedItem = this._physicalItems[pidx]; |
3984 // backfill the focused physical item | |
3985 this._physicalItems[pidx] = this._focusBackfillItem; | 2126 this._physicalItems[pidx] = this._focusBackfillItem; |
3986 // hide the focused physical | |
3987 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 2127 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
3988 } | 2128 } |
3989 }, | 2129 }, |
3990 | |
3991 _restoreFocusedItem: function() { | 2130 _restoreFocusedItem: function() { |
3992 var pidx, fidx = this._focusedIndex; | 2131 var pidx, fidx = this._focusedIndex; |
3993 | |
3994 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 2132 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
3995 return; | 2133 return; |
3996 } | 2134 } |
3997 // assign models to the focused index | |
3998 this._assignModels(); | 2135 this._assignModels(); |
3999 // get the new physical index for the focused index | |
4000 pidx = this._getPhysicalIndex(fidx); | 2136 pidx = this._getPhysicalIndex(fidx); |
4001 | |
4002 if (pidx != null) { | 2137 if (pidx != null) { |
4003 // flip the focus backfill | |
4004 this._focusBackfillItem = this._physicalItems[pidx]; | 2138 this._focusBackfillItem = this._physicalItems[pidx]; |
4005 // restore the focused physical item | |
4006 this._physicalItems[pidx] = this._offscreenFocusedItem; | 2139 this._physicalItems[pidx] = this._offscreenFocusedItem; |
4007 // reset the offscreen focused item | |
4008 this._offscreenFocusedItem = null; | 2140 this._offscreenFocusedItem = null; |
4009 // hide the physical item that backfills | |
4010 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 2141 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
4011 } | 2142 } |
4012 }, | 2143 }, |
4013 | |
4014 _didFocus: function(e) { | 2144 _didFocus: function(e) { |
4015 var targetModel = this.modelForElement(e.target); | 2145 var targetModel = this.modelForElement(e.target); |
4016 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 2146 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
4017 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 2147 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
4018 var fidx = this._focusedIndex; | 2148 var fidx = this._focusedIndex; |
4019 | |
4020 if (!targetModel || !focusedModel) { | 2149 if (!targetModel || !focusedModel) { |
4021 return; | 2150 return; |
4022 } | 2151 } |
4023 if (focusedModel === targetModel) { | 2152 if (focusedModel === targetModel) { |
4024 // if the user focused the same item, then bring it into view if it's no
t visible | |
4025 if (!this._isIndexVisible(fidx)) { | 2153 if (!this._isIndexVisible(fidx)) { |
4026 this.scrollToIndex(fidx); | 2154 this.scrollToIndex(fidx); |
4027 } | 2155 } |
4028 } else { | 2156 } else { |
4029 this._restoreFocusedItem(); | 2157 this._restoreFocusedItem(); |
4030 // restore tabIndex for the currently focused item | |
4031 focusedModel.tabIndex = -1; | 2158 focusedModel.tabIndex = -1; |
4032 // set the tabIndex for the next focused item | |
4033 targetModel.tabIndex = 0; | 2159 targetModel.tabIndex = 0; |
4034 fidx = targetModel[this.indexAs]; | 2160 fidx = targetModel[this.indexAs]; |
4035 this._focusedIndex = fidx; | 2161 this._focusedIndex = fidx; |
4036 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 2162 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
4037 | |
4038 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 2163 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
4039 this._update(); | 2164 this._update(); |
4040 } | 2165 } |
4041 } | 2166 } |
4042 }, | 2167 }, |
4043 | |
4044 _didMoveUp: function() { | 2168 _didMoveUp: function() { |
4045 this._focusPhysicalItem(this._focusedIndex - 1); | 2169 this._focusPhysicalItem(this._focusedIndex - 1); |
4046 }, | 2170 }, |
4047 | |
4048 _didMoveDown: function(e) { | 2171 _didMoveDown: function(e) { |
4049 // disable scroll when pressing the down key | |
4050 e.detail.keyboardEvent.preventDefault(); | 2172 e.detail.keyboardEvent.preventDefault(); |
4051 this._focusPhysicalItem(this._focusedIndex + 1); | 2173 this._focusPhysicalItem(this._focusedIndex + 1); |
4052 }, | 2174 }, |
4053 | |
4054 _didEnter: function(e) { | 2175 _didEnter: function(e) { |
4055 this._focusPhysicalItem(this._focusedIndex); | 2176 this._focusPhysicalItem(this._focusedIndex); |
4056 this._selectionHandler(e.detail.keyboardEvent); | 2177 this._selectionHandler(e.detail.keyboardEvent); |
4057 } | 2178 } |
4058 }); | 2179 }); |
| 2180 })(); |
4059 | 2181 |
4060 })(); | |
4061 // Copyright 2015 The Chromium Authors. All rights reserved. | 2182 // Copyright 2015 The Chromium Authors. All rights reserved. |
4062 // Use of this source code is governed by a BSD-style license that can be | 2183 // Use of this source code is governed by a BSD-style license that can be |
4063 // found in the LICENSE file. | 2184 // found in the LICENSE file. |
4064 | |
4065 cr.define('downloads', function() { | 2185 cr.define('downloads', function() { |
4066 /** | |
4067 * @param {string} chromeSendName | |
4068 * @return {function(string):void} A chrome.send() callback with curried name. | |
4069 */ | |
4070 function chromeSendWithId(chromeSendName) { | 2186 function chromeSendWithId(chromeSendName) { |
4071 return function(id) { chrome.send(chromeSendName, [id]); }; | 2187 return function(id) { |
| 2188 chrome.send(chromeSendName, [ id ]); |
| 2189 }; |
4072 } | 2190 } |
4073 | |
4074 /** @constructor */ | |
4075 function ActionService() { | 2191 function ActionService() { |
4076 /** @private {Array<string>} */ | |
4077 this.searchTerms_ = []; | 2192 this.searchTerms_ = []; |
4078 } | 2193 } |
4079 | 2194 function trim(s) { |
4080 /** | 2195 return s.trim(); |
4081 * @param {string} s | 2196 } |
4082 * @return {string} |s| without whitespace at the beginning or end. | 2197 function truthy(value) { |
4083 */ | 2198 return !!value; |
4084 function trim(s) { return s.trim(); } | 2199 } |
4085 | |
4086 /** | |
4087 * @param {string|undefined} value | |
4088 * @return {boolean} Whether |value| is truthy. | |
4089 */ | |
4090 function truthy(value) { return !!value; } | |
4091 | |
4092 /** | |
4093 * @param {string} searchText Input typed by the user into a search box. | |
4094 * @return {Array<string>} A list of terms extracted from |searchText|. | |
4095 */ | |
4096 ActionService.splitTerms = function(searchText) { | 2200 ActionService.splitTerms = function(searchText) { |
4097 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
4098 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | 2201 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); |
4099 }; | 2202 }; |
4100 | |
4101 ActionService.prototype = { | 2203 ActionService.prototype = { |
4102 /** @param {string} id ID of the download to cancel. */ | |
4103 cancel: chromeSendWithId('cancel'), | 2204 cancel: chromeSendWithId('cancel'), |
4104 | |
4105 /** Instructs the browser to clear all finished downloads. */ | |
4106 clearAll: function() { | 2205 clearAll: function() { |
4107 if (loadTimeData.getBoolean('allowDeletingHistory')) { | 2206 if (loadTimeData.getBoolean('allowDeletingHistory')) { |
4108 chrome.send('clearAll'); | 2207 chrome.send('clearAll'); |
4109 this.search(''); | 2208 this.search(''); |
4110 } | 2209 } |
4111 }, | 2210 }, |
4112 | |
4113 /** @param {string} id ID of the dangerous download to discard. */ | |
4114 discardDangerous: chromeSendWithId('discardDangerous'), | 2211 discardDangerous: chromeSendWithId('discardDangerous'), |
4115 | |
4116 /** @param {string} url URL of a file to download. */ | |
4117 download: function(url) { | 2212 download: function(url) { |
4118 var a = document.createElement('a'); | 2213 var a = document.createElement('a'); |
4119 a.href = url; | 2214 a.href = url; |
4120 a.setAttribute('download', ''); | 2215 a.setAttribute('download', ''); |
4121 a.click(); | 2216 a.click(); |
4122 }, | 2217 }, |
4123 | |
4124 /** @param {string} id ID of the download that the user started dragging. */ | |
4125 drag: chromeSendWithId('drag'), | 2218 drag: chromeSendWithId('drag'), |
4126 | |
4127 /** Loads more downloads with the current search terms. */ | |
4128 loadMore: function() { | 2219 loadMore: function() { |
4129 chrome.send('getDownloads', this.searchTerms_); | 2220 chrome.send('getDownloads', this.searchTerms_); |
4130 }, | 2221 }, |
4131 | |
4132 /** | |
4133 * @return {boolean} Whether the user is currently searching for downloads | |
4134 * (i.e. has a non-empty search term). | |
4135 */ | |
4136 isSearching: function() { | 2222 isSearching: function() { |
4137 return this.searchTerms_.length > 0; | 2223 return this.searchTerms_.length > 0; |
4138 }, | 2224 }, |
4139 | |
4140 /** Opens the current local destination for downloads. */ | |
4141 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | 2225 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), |
4142 | |
4143 /** | |
4144 * @param {string} id ID of the download to run locally on the user's box. | |
4145 */ | |
4146 openFile: chromeSendWithId('openFile'), | 2226 openFile: chromeSendWithId('openFile'), |
4147 | |
4148 /** @param {string} id ID the of the progressing download to pause. */ | |
4149 pause: chromeSendWithId('pause'), | 2227 pause: chromeSendWithId('pause'), |
4150 | |
4151 /** @param {string} id ID of the finished download to remove. */ | |
4152 remove: chromeSendWithId('remove'), | 2228 remove: chromeSendWithId('remove'), |
4153 | |
4154 /** @param {string} id ID of the paused download to resume. */ | |
4155 resume: chromeSendWithId('resume'), | 2229 resume: chromeSendWithId('resume'), |
4156 | |
4157 /** | |
4158 * @param {string} id ID of the dangerous download to save despite | |
4159 * warnings. | |
4160 */ | |
4161 saveDangerous: chromeSendWithId('saveDangerous'), | 2230 saveDangerous: chromeSendWithId('saveDangerous'), |
4162 | |
4163 /** @param {string} searchText What to search for. */ | |
4164 search: function(searchText) { | 2231 search: function(searchText) { |
4165 var searchTerms = ActionService.splitTerms(searchText); | 2232 var searchTerms = ActionService.splitTerms(searchText); |
4166 var sameTerms = searchTerms.length == this.searchTerms_.length; | 2233 var sameTerms = searchTerms.length == this.searchTerms_.length; |
4167 | |
4168 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | 2234 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { |
4169 if (searchTerms[i] != this.searchTerms_[i]) | 2235 if (searchTerms[i] != this.searchTerms_[i]) sameTerms = false; |
4170 sameTerms = false; | |
4171 } | 2236 } |
4172 | 2237 if (sameTerms) return; |
4173 if (sameTerms) | |
4174 return; | |
4175 | |
4176 this.searchTerms_ = searchTerms; | 2238 this.searchTerms_ = searchTerms; |
4177 this.loadMore(); | 2239 this.loadMore(); |
4178 }, | 2240 }, |
| 2241 show: chromeSendWithId('show'), |
| 2242 undo: chrome.send.bind(chrome, 'undo') |
| 2243 }; |
| 2244 cr.addSingletonGetter(ActionService); |
| 2245 return { |
| 2246 ActionService: ActionService |
| 2247 }; |
| 2248 }); |
4179 | 2249 |
4180 /** | |
4181 * Shows the local folder a finished download resides in. | |
4182 * @param {string} id ID of the download to show. | |
4183 */ | |
4184 show: chromeSendWithId('show'), | |
4185 | |
4186 /** Undo download removal. */ | |
4187 undo: chrome.send.bind(chrome, 'undo'), | |
4188 }; | |
4189 | |
4190 cr.addSingletonGetter(ActionService); | |
4191 | |
4192 return {ActionService: ActionService}; | |
4193 }); | |
4194 // Copyright 2015 The Chromium Authors. All rights reserved. | 2250 // Copyright 2015 The Chromium Authors. All rights reserved. |
4195 // Use of this source code is governed by a BSD-style license that can be | 2251 // Use of this source code is governed by a BSD-style license that can be |
4196 // found in the LICENSE file. | 2252 // found in the LICENSE file. |
4197 | |
4198 cr.define('downloads', function() { | 2253 cr.define('downloads', function() { |
4199 /** | |
4200 * Explains why a download is in DANGEROUS state. | |
4201 * @enum {string} | |
4202 */ | |
4203 var DangerType = { | 2254 var DangerType = { |
4204 NOT_DANGEROUS: 'NOT_DANGEROUS', | 2255 NOT_DANGEROUS: 'NOT_DANGEROUS', |
4205 DANGEROUS_FILE: 'DANGEROUS_FILE', | 2256 DANGEROUS_FILE: 'DANGEROUS_FILE', |
4206 DANGEROUS_URL: 'DANGEROUS_URL', | 2257 DANGEROUS_URL: 'DANGEROUS_URL', |
4207 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | 2258 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', |
4208 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | 2259 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', |
4209 DANGEROUS_HOST: 'DANGEROUS_HOST', | 2260 DANGEROUS_HOST: 'DANGEROUS_HOST', |
4210 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | 2261 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED' |
4211 }; | 2262 }; |
4212 | |
4213 /** | |
4214 * The states a download can be in. These correspond to states defined in | |
4215 * DownloadsDOMHandler::CreateDownloadItemValue | |
4216 * @enum {string} | |
4217 */ | |
4218 var States = { | 2263 var States = { |
4219 IN_PROGRESS: 'IN_PROGRESS', | 2264 IN_PROGRESS: 'IN_PROGRESS', |
4220 CANCELLED: 'CANCELLED', | 2265 CANCELLED: 'CANCELLED', |
4221 COMPLETE: 'COMPLETE', | 2266 COMPLETE: 'COMPLETE', |
4222 PAUSED: 'PAUSED', | 2267 PAUSED: 'PAUSED', |
4223 DANGEROUS: 'DANGEROUS', | 2268 DANGEROUS: 'DANGEROUS', |
4224 INTERRUPTED: 'INTERRUPTED', | 2269 INTERRUPTED: 'INTERRUPTED' |
4225 }; | 2270 }; |
4226 | |
4227 return { | 2271 return { |
4228 DangerType: DangerType, | 2272 DangerType: DangerType, |
4229 States: States, | 2273 States: States |
4230 }; | 2274 }; |
4231 }); | 2275 }); |
| 2276 |
4232 // Copyright 2014 The Chromium Authors. All rights reserved. | 2277 // Copyright 2014 The Chromium Authors. All rights reserved. |
4233 // Use of this source code is governed by a BSD-style license that can be | 2278 // Use of this source code is governed by a BSD-style license that can be |
4234 // found in the LICENSE file. | 2279 // found in the LICENSE file. |
4235 | |
4236 // Action links are elements that are used to perform an in-page navigation or | |
4237 // action (e.g. showing a dialog). | |
4238 // | |
4239 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
4240 // they're subtly different as they're not initially underlined (giving users a | |
4241 // clue that underlined links navigate while action links don't). | |
4242 // | |
4243 // Action links look very similar to normal links when hovered (hand cursor, | |
4244 // underlined). This gives the user an idea that clicking this link will do | |
4245 // something similar to navigation but in the same page. | |
4246 // | |
4247 // They can be created in JavaScript like this: | |
4248 // | |
4249 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
4250 // | |
4251 // or with a constructor like this: | |
4252 // | |
4253 // var link = new ActionLink(); | |
4254 // | |
4255 // They can be used easily from HTML as well, like so: | |
4256 // | |
4257 // <a is="action-link">Click me!</a> | |
4258 // | |
4259 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
4260 | |
4261 /** | |
4262 * @constructor | |
4263 * @extends {HTMLAnchorElement} | |
4264 */ | |
4265 var ActionLink = document.registerElement('action-link', { | 2280 var ActionLink = document.registerElement('action-link', { |
4266 prototype: { | 2281 prototype: { |
4267 __proto__: HTMLAnchorElement.prototype, | 2282 __proto__: HTMLAnchorElement.prototype, |
4268 | |
4269 /** @this {ActionLink} */ | |
4270 createdCallback: function() { | 2283 createdCallback: function() { |
4271 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
4272 this.tabIndex = this.disabled ? -1 : 0; | 2284 this.tabIndex = this.disabled ? -1 : 0; |
4273 | 2285 if (!this.hasAttribute('role')) this.setAttribute('role', 'link'); |
4274 if (!this.hasAttribute('role')) | |
4275 this.setAttribute('role', 'link'); | |
4276 | |
4277 this.addEventListener('keydown', function(e) { | 2286 this.addEventListener('keydown', function(e) { |
4278 if (!this.disabled && e.key == 'Enter' && !this.href) { | 2287 if (!this.disabled && e.key == 'Enter' && !this.href) { |
4279 // Schedule a click asynchronously because other 'keydown' handlers | |
4280 // may still run later (e.g. document.addEventListener('keydown')). | |
4281 // Specifically options dialogs break when this timeout isn't here. | |
4282 // NOTE: this affects the "trusted" state of the ensuing click. I | |
4283 // haven't found anything that breaks because of this (yet). | |
4284 window.setTimeout(this.click.bind(this), 0); | 2288 window.setTimeout(this.click.bind(this), 0); |
4285 } | 2289 } |
4286 }); | 2290 }); |
4287 | |
4288 function preventDefault(e) { | 2291 function preventDefault(e) { |
4289 e.preventDefault(); | 2292 e.preventDefault(); |
4290 } | 2293 } |
4291 | |
4292 function removePreventDefault() { | 2294 function removePreventDefault() { |
4293 document.removeEventListener('selectstart', preventDefault); | 2295 document.removeEventListener('selectstart', preventDefault); |
4294 document.removeEventListener('mouseup', removePreventDefault); | 2296 document.removeEventListener('mouseup', removePreventDefault); |
4295 } | 2297 } |
4296 | |
4297 this.addEventListener('mousedown', function() { | 2298 this.addEventListener('mousedown', function() { |
4298 // This handlers strives to match the behavior of <a href="...">. | |
4299 | |
4300 // While the mouse is down, prevent text selection from dragging. | |
4301 document.addEventListener('selectstart', preventDefault); | 2299 document.addEventListener('selectstart', preventDefault); |
4302 document.addEventListener('mouseup', removePreventDefault); | 2300 document.addEventListener('mouseup', removePreventDefault); |
4303 | 2301 if (document.activeElement != this) this.classList.add('no-outline'); |
4304 // If focus started via mouse press, don't show an outline. | |
4305 if (document.activeElement != this) | |
4306 this.classList.add('no-outline'); | |
4307 }); | 2302 }); |
4308 | |
4309 this.addEventListener('blur', function() { | 2303 this.addEventListener('blur', function() { |
4310 this.classList.remove('no-outline'); | 2304 this.classList.remove('no-outline'); |
4311 }); | 2305 }); |
4312 }, | 2306 }, |
4313 | |
4314 /** @type {boolean} */ | |
4315 set disabled(disabled) { | 2307 set disabled(disabled) { |
4316 if (disabled) | 2308 if (disabled) HTMLAnchorElement.prototype.setAttribute.call(this, 'disable
d', ''); else HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled')
; |
4317 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', ''); | |
4318 else | |
4319 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled'); | |
4320 this.tabIndex = disabled ? -1 : 0; | 2309 this.tabIndex = disabled ? -1 : 0; |
4321 }, | 2310 }, |
4322 get disabled() { | 2311 get disabled() { |
4323 return this.hasAttribute('disabled'); | 2312 return this.hasAttribute('disabled'); |
4324 }, | 2313 }, |
4325 | |
4326 /** @override */ | |
4327 setAttribute: function(attr, val) { | 2314 setAttribute: function(attr, val) { |
4328 if (attr.toLowerCase() == 'disabled') | 2315 if (attr.toLowerCase() == 'disabled') this.disabled = true; else HTMLAncho
rElement.prototype.setAttribute.apply(this, arguments); |
4329 this.disabled = true; | 2316 }, |
4330 else | |
4331 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments); | |
4332 }, | |
4333 | |
4334 /** @override */ | |
4335 removeAttribute: function(attr) { | 2317 removeAttribute: function(attr) { |
4336 if (attr.toLowerCase() == 'disabled') | 2318 if (attr.toLowerCase() == 'disabled') this.disabled = false; else HTMLAnch
orElement.prototype.removeAttribute.apply(this, arguments); |
4337 this.disabled = false; | 2319 } |
4338 else | 2320 }, |
4339 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | 2321 "extends": 'a' |
4340 }, | |
4341 }, | |
4342 | |
4343 extends: 'a', | |
4344 }); | 2322 }); |
| 2323 |
4345 (function() { | 2324 (function() { |
4346 | 2325 var metaDatas = {}; |
4347 // monostate data | 2326 var metaArrays = {}; |
4348 var metaDatas = {}; | 2327 var singleton = null; |
4349 var metaArrays = {}; | 2328 Polymer.IronMeta = Polymer({ |
4350 var singleton = null; | 2329 is: 'iron-meta', |
4351 | 2330 properties: { |
4352 Polymer.IronMeta = Polymer({ | 2331 type: { |
4353 | 2332 type: String, |
4354 is: 'iron-meta', | 2333 value: 'default', |
4355 | 2334 observer: '_typeChanged' |
4356 properties: { | 2335 }, |
4357 | 2336 key: { |
4358 /** | 2337 type: String, |
4359 * The type of meta-data. All meta-data of the same type is stored | 2338 observer: '_keyChanged' |
4360 * together. | 2339 }, |
4361 */ | 2340 value: { |
4362 type: { | 2341 type: Object, |
4363 type: String, | 2342 notify: true, |
4364 value: 'default', | 2343 observer: '_valueChanged' |
4365 observer: '_typeChanged' | 2344 }, |
4366 }, | 2345 self: { |
4367 | 2346 type: Boolean, |
4368 /** | 2347 observer: '_selfChanged' |
4369 * The key used to store `value` under the `type` namespace. | 2348 }, |
4370 */ | 2349 list: { |
4371 key: { | 2350 type: Array, |
4372 type: String, | 2351 notify: true |
4373 observer: '_keyChanged' | 2352 } |
4374 }, | 2353 }, |
4375 | 2354 hostAttributes: { |
4376 /** | 2355 hidden: true |
4377 * The meta-data to store or retrieve. | 2356 }, |
4378 */ | 2357 factoryImpl: function(config) { |
4379 value: { | 2358 if (config) { |
4380 type: Object, | 2359 for (var n in config) { |
4381 notify: true, | 2360 switch (n) { |
4382 observer: '_valueChanged' | 2361 case 'type': |
4383 }, | 2362 case 'key': |
4384 | 2363 case 'value': |
4385 /** | 2364 this[n] = config[n]; |
4386 * If true, `value` is set to the iron-meta instance itself. | 2365 break; |
4387 */ | |
4388 self: { | |
4389 type: Boolean, | |
4390 observer: '_selfChanged' | |
4391 }, | |
4392 | |
4393 /** | |
4394 * Array of all meta-data values for the given type. | |
4395 */ | |
4396 list: { | |
4397 type: Array, | |
4398 notify: true | |
4399 } | |
4400 | |
4401 }, | |
4402 | |
4403 hostAttributes: { | |
4404 hidden: true | |
4405 }, | |
4406 | |
4407 /** | |
4408 * Only runs if someone invokes the factory/constructor directly | |
4409 * e.g. `new Polymer.IronMeta()` | |
4410 * | |
4411 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
4412 */ | |
4413 factoryImpl: function(config) { | |
4414 if (config) { | |
4415 for (var n in config) { | |
4416 switch(n) { | |
4417 case 'type': | |
4418 case 'key': | |
4419 case 'value': | |
4420 this[n] = config[n]; | |
4421 break; | |
4422 } | |
4423 } | 2366 } |
4424 } | 2367 } |
4425 }, | 2368 } |
4426 | 2369 }, |
4427 created: function() { | 2370 created: function() { |
4428 // TODO(sjmiles): good for debugging? | 2371 this._metaDatas = metaDatas; |
4429 this._metaDatas = metaDatas; | 2372 this._metaArrays = metaArrays; |
4430 this._metaArrays = metaArrays; | 2373 }, |
4431 }, | 2374 _keyChanged: function(key, old) { |
4432 | 2375 this._resetRegistration(old); |
4433 _keyChanged: function(key, old) { | 2376 }, |
4434 this._resetRegistration(old); | 2377 _valueChanged: function(value) { |
4435 }, | 2378 this._resetRegistration(this.key); |
4436 | 2379 }, |
4437 _valueChanged: function(value) { | 2380 _selfChanged: function(self) { |
4438 this._resetRegistration(this.key); | 2381 if (self) { |
4439 }, | 2382 this.value = this; |
4440 | 2383 } |
4441 _selfChanged: function(self) { | 2384 }, |
4442 if (self) { | 2385 _typeChanged: function(type) { |
4443 this.value = this; | 2386 this._unregisterKey(this.key); |
| 2387 if (!metaDatas[type]) { |
| 2388 metaDatas[type] = {}; |
| 2389 } |
| 2390 this._metaData = metaDatas[type]; |
| 2391 if (!metaArrays[type]) { |
| 2392 metaArrays[type] = []; |
| 2393 } |
| 2394 this.list = metaArrays[type]; |
| 2395 this._registerKeyValue(this.key, this.value); |
| 2396 }, |
| 2397 byKey: function(key) { |
| 2398 return this._metaData && this._metaData[key]; |
| 2399 }, |
| 2400 _resetRegistration: function(oldKey) { |
| 2401 this._unregisterKey(oldKey); |
| 2402 this._registerKeyValue(this.key, this.value); |
| 2403 }, |
| 2404 _unregisterKey: function(key) { |
| 2405 this._unregister(key, this._metaData, this.list); |
| 2406 }, |
| 2407 _registerKeyValue: function(key, value) { |
| 2408 this._register(key, value, this._metaData, this.list); |
| 2409 }, |
| 2410 _register: function(key, value, data, list) { |
| 2411 if (key && data && value !== undefined) { |
| 2412 data[key] = value; |
| 2413 list.push(value); |
| 2414 } |
| 2415 }, |
| 2416 _unregister: function(key, data, list) { |
| 2417 if (key && data) { |
| 2418 if (key in data) { |
| 2419 var value = data[key]; |
| 2420 delete data[key]; |
| 2421 this.arrayDelete(list, value); |
4444 } | 2422 } |
4445 }, | 2423 } |
4446 | 2424 } |
4447 _typeChanged: function(type) { | 2425 }); |
4448 this._unregisterKey(this.key); | 2426 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
4449 if (!metaDatas[type]) { | 2427 if (singleton === null) { |
4450 metaDatas[type] = {}; | 2428 singleton = new Polymer.IronMeta(); |
4451 } | 2429 } |
4452 this._metaData = metaDatas[type]; | 2430 return singleton; |
4453 if (!metaArrays[type]) { | 2431 }; |
4454 metaArrays[type] = []; | 2432 Polymer.IronMetaQuery = Polymer({ |
4455 } | 2433 is: 'iron-meta-query', |
4456 this.list = metaArrays[type]; | 2434 properties: { |
4457 this._registerKeyValue(this.key, this.value); | 2435 type: { |
4458 }, | 2436 type: String, |
4459 | 2437 value: 'default', |
4460 /** | 2438 observer: '_typeChanged' |
4461 * Retrieves meta data value by key. | 2439 }, |
4462 * | 2440 key: { |
4463 * @method byKey | 2441 type: String, |
4464 * @param {string} key The key of the meta-data to be returned. | 2442 observer: '_keyChanged' |
4465 * @return {*} | 2443 }, |
4466 */ | 2444 value: { |
4467 byKey: function(key) { | 2445 type: Object, |
4468 return this._metaData && this._metaData[key]; | 2446 notify: true, |
4469 }, | 2447 readOnly: true |
4470 | 2448 }, |
4471 _resetRegistration: function(oldKey) { | 2449 list: { |
4472 this._unregisterKey(oldKey); | 2450 type: Array, |
4473 this._registerKeyValue(this.key, this.value); | 2451 notify: true |
4474 }, | 2452 } |
4475 | 2453 }, |
4476 _unregisterKey: function(key) { | 2454 factoryImpl: function(config) { |
4477 this._unregister(key, this._metaData, this.list); | 2455 if (config) { |
4478 }, | 2456 for (var n in config) { |
4479 | 2457 switch (n) { |
4480 _registerKeyValue: function(key, value) { | 2458 case 'type': |
4481 this._register(key, value, this._metaData, this.list); | 2459 case 'key': |
4482 }, | 2460 this[n] = config[n]; |
4483 | 2461 break; |
4484 _register: function(key, value, data, list) { | |
4485 if (key && data && value !== undefined) { | |
4486 data[key] = value; | |
4487 list.push(value); | |
4488 } | |
4489 }, | |
4490 | |
4491 _unregister: function(key, data, list) { | |
4492 if (key && data) { | |
4493 if (key in data) { | |
4494 var value = data[key]; | |
4495 delete data[key]; | |
4496 this.arrayDelete(list, value); | |
4497 } | 2462 } |
4498 } | 2463 } |
4499 } | 2464 } |
4500 | 2465 }, |
4501 }); | 2466 created: function() { |
4502 | 2467 this._metaDatas = metaDatas; |
4503 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 2468 this._metaArrays = metaArrays; |
4504 if (singleton === null) { | 2469 }, |
4505 singleton = new Polymer.IronMeta(); | 2470 _keyChanged: function(key) { |
4506 } | 2471 this._setValue(this._metaData && this._metaData[key]); |
4507 return singleton; | 2472 }, |
4508 }; | 2473 _typeChanged: function(type) { |
4509 | 2474 this._metaData = metaDatas[type]; |
4510 /** | 2475 this.list = metaArrays[type]; |
4511 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | 2476 if (this.key) { |
4512 | 2477 this._keyChanged(this.key); |
4513 Examples: | 2478 } |
4514 | 2479 }, |
4515 If I create an instance like this: | 2480 byKey: function(key) { |
4516 | 2481 return this._metaData && this._metaData[key]; |
4517 <iron-meta key="info" value="foo/bar"></iron-meta> | 2482 } |
4518 | 2483 }); |
4519 Note that value="foo/bar" is the metadata I've defined. I could define more | 2484 })(); |
4520 attributes or use child nodes to define additional metadata. | 2485 |
4521 | 2486 Polymer({ |
4522 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | 2487 is: 'iron-icon', |
4523 | 2488 properties: { |
4524 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | 2489 icon: { |
4525 | 2490 type: String, |
4526 @group Polymer Iron Elements | 2491 observer: '_iconChanged' |
4527 @element iron-meta-query | 2492 }, |
4528 */ | 2493 theme: { |
4529 Polymer.IronMetaQuery = Polymer({ | 2494 type: String, |
4530 | 2495 observer: '_updateIcon' |
4531 is: 'iron-meta-query', | 2496 }, |
4532 | 2497 src: { |
4533 properties: { | 2498 type: String, |
4534 | 2499 observer: '_srcChanged' |
4535 /** | 2500 }, |
4536 * The type of meta-data. All meta-data of the same type is stored | 2501 _meta: { |
4537 * together. | 2502 value: Polymer.Base.create('iron-meta', { |
4538 */ | 2503 type: 'iconset' |
4539 type: { | 2504 }), |
4540 type: String, | 2505 observer: '_updateIcon' |
4541 value: 'default', | 2506 } |
4542 observer: '_typeChanged' | 2507 }, |
4543 }, | 2508 _DEFAULT_ICONSET: 'icons', |
4544 | 2509 _iconChanged: function(icon) { |
4545 /** | 2510 var parts = (icon || '').split(':'); |
4546 * Specifies a key to use for retrieving `value` from the `type` | 2511 this._iconName = parts.pop(); |
4547 * namespace. | 2512 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
4548 */ | 2513 this._updateIcon(); |
4549 key: { | 2514 }, |
4550 type: String, | 2515 _srcChanged: function(src) { |
4551 observer: '_keyChanged' | 2516 this._updateIcon(); |
4552 }, | 2517 }, |
4553 | 2518 _usesIconset: function() { |
4554 /** | 2519 return this.icon || !this.src; |
4555 * The meta-data to store or retrieve. | 2520 }, |
4556 */ | 2521 _updateIcon: function() { |
4557 value: { | 2522 if (this._usesIconset()) { |
4558 type: Object, | 2523 if (this._img && this._img.parentNode) { |
4559 notify: true, | 2524 Polymer.dom(this.root).removeChild(this._img); |
4560 readOnly: true | 2525 } |
4561 }, | 2526 if (this._iconName === "") { |
4562 | 2527 if (this._iconset) { |
4563 /** | 2528 this._iconset.removeIcon(this); |
4564 * Array of all meta-data values for the given type. | |
4565 */ | |
4566 list: { | |
4567 type: Array, | |
4568 notify: true | |
4569 } | 2529 } |
4570 | 2530 } else if (this._iconsetName && this._meta) { |
4571 }, | 2531 this._iconset = this._meta.byKey(this._iconsetName); |
4572 | 2532 if (this._iconset) { |
4573 /** | 2533 this._iconset.applyIcon(this, this._iconName, this.theme); |
4574 * Actually a factory method, not a true constructor. Only runs if | 2534 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
4575 * someone invokes it directly (via `new Polymer.IronMeta()`); | 2535 } else { |
4576 * | 2536 this.listen(window, 'iron-iconset-added', '_updateIcon'); |
4577 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
4578 */ | |
4579 factoryImpl: function(config) { | |
4580 if (config) { | |
4581 for (var n in config) { | |
4582 switch(n) { | |
4583 case 'type': | |
4584 case 'key': | |
4585 this[n] = config[n]; | |
4586 break; | |
4587 } | |
4588 } | |
4589 } | 2537 } |
4590 }, | 2538 } |
4591 | 2539 } else { |
4592 created: function() { | 2540 if (this._iconset) { |
4593 // TODO(sjmiles): good for debugging? | 2541 this._iconset.removeIcon(this); |
4594 this._metaDatas = metaDatas; | 2542 } |
4595 this._metaArrays = metaArrays; | 2543 if (!this._img) { |
4596 }, | 2544 this._img = document.createElement('img'); |
4597 | 2545 this._img.style.width = '100%'; |
4598 _keyChanged: function(key) { | 2546 this._img.style.height = '100%'; |
4599 this._setValue(this._metaData && this._metaData[key]); | 2547 this._img.draggable = false; |
4600 }, | 2548 } |
4601 | 2549 this._img.src = this.src; |
4602 _typeChanged: function(type) { | 2550 Polymer.dom(this.root).appendChild(this._img); |
4603 this._metaData = metaDatas[type]; | 2551 } |
4604 this.list = metaArrays[type]; | 2552 } |
4605 if (this.key) { | 2553 }); |
4606 this._keyChanged(this.key); | 2554 |
| 2555 Polymer.IronControlState = { |
| 2556 properties: { |
| 2557 focused: { |
| 2558 type: Boolean, |
| 2559 value: false, |
| 2560 notify: true, |
| 2561 readOnly: true, |
| 2562 reflectToAttribute: true |
| 2563 }, |
| 2564 disabled: { |
| 2565 type: Boolean, |
| 2566 value: false, |
| 2567 notify: true, |
| 2568 observer: '_disabledChanged', |
| 2569 reflectToAttribute: true |
| 2570 }, |
| 2571 _oldTabIndex: { |
| 2572 type: Number |
| 2573 }, |
| 2574 _boundFocusBlurHandler: { |
| 2575 type: Function, |
| 2576 value: function() { |
| 2577 return this._focusBlurHandler.bind(this); |
| 2578 } |
| 2579 } |
| 2580 }, |
| 2581 observers: [ '_changedControlState(focused, disabled)' ], |
| 2582 ready: function() { |
| 2583 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2584 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2585 }, |
| 2586 _focusBlurHandler: function(event) { |
| 2587 if (event.target === this) { |
| 2588 this._setFocused(event.type === 'focus'); |
| 2589 } else if (!this.shadowRoot) { |
| 2590 var target = Polymer.dom(event).localTarget; |
| 2591 if (!this.isLightDescendant(target)) { |
| 2592 this.fire(event.type, { |
| 2593 sourceEvent: event |
| 2594 }, { |
| 2595 node: this, |
| 2596 bubbles: event.bubbles, |
| 2597 cancelable: event.cancelable |
| 2598 }); |
| 2599 } |
| 2600 } |
| 2601 }, |
| 2602 _disabledChanged: function(disabled, old) { |
| 2603 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2604 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2605 if (disabled) { |
| 2606 this._oldTabIndex = this.tabIndex; |
| 2607 this._setFocused(false); |
| 2608 this.tabIndex = -1; |
| 2609 this.blur(); |
| 2610 } else if (this._oldTabIndex !== undefined) { |
| 2611 this.tabIndex = this._oldTabIndex; |
| 2612 } |
| 2613 }, |
| 2614 _changedControlState: function() { |
| 2615 if (this._controlStateChanged) { |
| 2616 this._controlStateChanged(); |
| 2617 } |
| 2618 } |
| 2619 }; |
| 2620 |
| 2621 Polymer.IronButtonStateImpl = { |
| 2622 properties: { |
| 2623 pressed: { |
| 2624 type: Boolean, |
| 2625 readOnly: true, |
| 2626 value: false, |
| 2627 reflectToAttribute: true, |
| 2628 observer: '_pressedChanged' |
| 2629 }, |
| 2630 toggles: { |
| 2631 type: Boolean, |
| 2632 value: false, |
| 2633 reflectToAttribute: true |
| 2634 }, |
| 2635 active: { |
| 2636 type: Boolean, |
| 2637 value: false, |
| 2638 notify: true, |
| 2639 reflectToAttribute: true |
| 2640 }, |
| 2641 pointerDown: { |
| 2642 type: Boolean, |
| 2643 readOnly: true, |
| 2644 value: false |
| 2645 }, |
| 2646 receivedFocusFromKeyboard: { |
| 2647 type: Boolean, |
| 2648 readOnly: true |
| 2649 }, |
| 2650 ariaActiveAttribute: { |
| 2651 type: String, |
| 2652 value: 'aria-pressed', |
| 2653 observer: '_ariaActiveAttributeChanged' |
| 2654 } |
| 2655 }, |
| 2656 listeners: { |
| 2657 down: '_downHandler', |
| 2658 up: '_upHandler', |
| 2659 tap: '_tapHandler' |
| 2660 }, |
| 2661 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi
veAttribute)' ], |
| 2662 keyBindings: { |
| 2663 'enter:keydown': '_asyncClick', |
| 2664 'space:keydown': '_spaceKeyDownHandler', |
| 2665 'space:keyup': '_spaceKeyUpHandler' |
| 2666 }, |
| 2667 _mouseEventRe: /^mouse/, |
| 2668 _tapHandler: function() { |
| 2669 if (this.toggles) { |
| 2670 this._userActivate(!this.active); |
| 2671 } else { |
| 2672 this.active = false; |
| 2673 } |
| 2674 }, |
| 2675 _detectKeyboardFocus: function(focused) { |
| 2676 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 2677 }, |
| 2678 _userActivate: function(active) { |
| 2679 if (this.active !== active) { |
| 2680 this.active = active; |
| 2681 this.fire('change'); |
| 2682 } |
| 2683 }, |
| 2684 _downHandler: function(event) { |
| 2685 this._setPointerDown(true); |
| 2686 this._setPressed(true); |
| 2687 this._setReceivedFocusFromKeyboard(false); |
| 2688 }, |
| 2689 _upHandler: function() { |
| 2690 this._setPointerDown(false); |
| 2691 this._setPressed(false); |
| 2692 }, |
| 2693 _spaceKeyDownHandler: function(event) { |
| 2694 var keyboardEvent = event.detail.keyboardEvent; |
| 2695 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2696 if (this.isLightDescendant(target)) return; |
| 2697 keyboardEvent.preventDefault(); |
| 2698 keyboardEvent.stopImmediatePropagation(); |
| 2699 this._setPressed(true); |
| 2700 }, |
| 2701 _spaceKeyUpHandler: function(event) { |
| 2702 var keyboardEvent = event.detail.keyboardEvent; |
| 2703 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2704 if (this.isLightDescendant(target)) return; |
| 2705 if (this.pressed) { |
| 2706 this._asyncClick(); |
| 2707 } |
| 2708 this._setPressed(false); |
| 2709 }, |
| 2710 _asyncClick: function() { |
| 2711 this.async(function() { |
| 2712 this.click(); |
| 2713 }, 1); |
| 2714 }, |
| 2715 _pressedChanged: function(pressed) { |
| 2716 this._changedButtonState(); |
| 2717 }, |
| 2718 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 2719 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 2720 this.removeAttribute(oldValue); |
| 2721 } |
| 2722 }, |
| 2723 _activeChanged: function(active, ariaActiveAttribute) { |
| 2724 if (this.toggles) { |
| 2725 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false'); |
| 2726 } else { |
| 2727 this.removeAttribute(this.ariaActiveAttribute); |
| 2728 } |
| 2729 this._changedButtonState(); |
| 2730 }, |
| 2731 _controlStateChanged: function() { |
| 2732 if (this.disabled) { |
| 2733 this._setPressed(false); |
| 2734 } else { |
| 2735 this._changedButtonState(); |
| 2736 } |
| 2737 }, |
| 2738 _changedButtonState: function() { |
| 2739 if (this._buttonStateChanged) { |
| 2740 this._buttonStateChanged(); |
| 2741 } |
| 2742 } |
| 2743 }; |
| 2744 |
| 2745 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat
eImpl ]; |
| 2746 |
| 2747 (function() { |
| 2748 var Utility = { |
| 2749 distance: function(x1, y1, x2, y2) { |
| 2750 var xDelta = x1 - x2; |
| 2751 var yDelta = y1 - y2; |
| 2752 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 2753 }, |
| 2754 now: window.performance && window.performance.now ? window.performance.now.b
ind(window.performance) : Date.now |
| 2755 }; |
| 2756 function ElementMetrics(element) { |
| 2757 this.element = element; |
| 2758 this.width = this.boundingRect.width; |
| 2759 this.height = this.boundingRect.height; |
| 2760 this.size = Math.max(this.width, this.height); |
| 2761 } |
| 2762 ElementMetrics.prototype = { |
| 2763 get boundingRect() { |
| 2764 return this.element.getBoundingClientRect(); |
| 2765 }, |
| 2766 furthestCornerDistanceFrom: function(x, y) { |
| 2767 var topLeft = Utility.distance(x, y, 0, 0); |
| 2768 var topRight = Utility.distance(x, y, this.width, 0); |
| 2769 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 2770 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 2771 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 2772 } |
| 2773 }; |
| 2774 function Ripple(element) { |
| 2775 this.element = element; |
| 2776 this.color = window.getComputedStyle(element).color; |
| 2777 this.wave = document.createElement('div'); |
| 2778 this.waveContainer = document.createElement('div'); |
| 2779 this.wave.style.backgroundColor = this.color; |
| 2780 this.wave.classList.add('wave'); |
| 2781 this.waveContainer.classList.add('wave-container'); |
| 2782 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 2783 this.resetInteractionState(); |
| 2784 } |
| 2785 Ripple.MAX_RADIUS = 300; |
| 2786 Ripple.prototype = { |
| 2787 get recenters() { |
| 2788 return this.element.recenters; |
| 2789 }, |
| 2790 get center() { |
| 2791 return this.element.center; |
| 2792 }, |
| 2793 get mouseDownElapsed() { |
| 2794 var elapsed; |
| 2795 if (!this.mouseDownStart) { |
| 2796 return 0; |
| 2797 } |
| 2798 elapsed = Utility.now() - this.mouseDownStart; |
| 2799 if (this.mouseUpStart) { |
| 2800 elapsed -= this.mouseUpElapsed; |
| 2801 } |
| 2802 return elapsed; |
| 2803 }, |
| 2804 get mouseUpElapsed() { |
| 2805 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0; |
| 2806 }, |
| 2807 get mouseDownElapsedSeconds() { |
| 2808 return this.mouseDownElapsed / 1e3; |
| 2809 }, |
| 2810 get mouseUpElapsedSeconds() { |
| 2811 return this.mouseUpElapsed / 1e3; |
| 2812 }, |
| 2813 get mouseInteractionSeconds() { |
| 2814 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 2815 }, |
| 2816 get initialOpacity() { |
| 2817 return this.element.initialOpacity; |
| 2818 }, |
| 2819 get opacityDecayVelocity() { |
| 2820 return this.element.opacityDecayVelocity; |
| 2821 }, |
| 2822 get radius() { |
| 2823 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 2824 var height2 = this.containerMetrics.height * this.containerMetrics.height; |
| 2825 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS)
* 1.1 + 5; |
| 2826 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS); |
| 2827 var timeNow = this.mouseInteractionSeconds / duration; |
| 2828 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 2829 return Math.abs(size); |
| 2830 }, |
| 2831 get opacity() { |
| 2832 if (!this.mouseUpStart) { |
| 2833 return this.initialOpacity; |
| 2834 } |
| 2835 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this
.opacityDecayVelocity); |
| 2836 }, |
| 2837 get outerOpacity() { |
| 2838 var outerOpacity = this.mouseUpElapsedSeconds * .3; |
| 2839 var waveOpacity = this.opacity; |
| 2840 return Math.max(0, Math.min(outerOpacity, waveOpacity)); |
| 2841 }, |
| 2842 get isOpacityFullyDecayed() { |
| 2843 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl
e.MAX_RADIUS); |
| 2844 }, |
| 2845 get isRestingAtMaxRadius() { |
| 2846 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this
.maxRadius, Ripple.MAX_RADIUS); |
| 2847 }, |
| 2848 get isAnimationComplete() { |
| 2849 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa
xRadius; |
| 2850 }, |
| 2851 get translationFraction() { |
| 2852 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr
t(2)); |
| 2853 }, |
| 2854 get xNow() { |
| 2855 if (this.xEnd) { |
| 2856 return this.xStart + this.translationFraction * (this.xEnd - this.xStart
); |
| 2857 } |
| 2858 return this.xStart; |
| 2859 }, |
| 2860 get yNow() { |
| 2861 if (this.yEnd) { |
| 2862 return this.yStart + this.translationFraction * (this.yEnd - this.yStart
); |
| 2863 } |
| 2864 return this.yStart; |
| 2865 }, |
| 2866 get isMouseDown() { |
| 2867 return this.mouseDownStart && !this.mouseUpStart; |
| 2868 }, |
| 2869 resetInteractionState: function() { |
| 2870 this.maxRadius = 0; |
| 2871 this.mouseDownStart = 0; |
| 2872 this.mouseUpStart = 0; |
| 2873 this.xStart = 0; |
| 2874 this.yStart = 0; |
| 2875 this.xEnd = 0; |
| 2876 this.yEnd = 0; |
| 2877 this.slideDistance = 0; |
| 2878 this.containerMetrics = new ElementMetrics(this.element); |
| 2879 }, |
| 2880 draw: function() { |
| 2881 var scale; |
| 2882 var translateString; |
| 2883 var dx; |
| 2884 var dy; |
| 2885 this.wave.style.opacity = this.opacity; |
| 2886 scale = this.radius / (this.containerMetrics.size / 2); |
| 2887 dx = this.xNow - this.containerMetrics.width / 2; |
| 2888 dy = this.yNow - this.containerMetrics.height / 2; |
| 2889 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy
+ 'px)'; |
| 2890 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + '
px, 0)'; |
| 2891 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 2892 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 2893 }, |
| 2894 downAction: function(event) { |
| 2895 var xCenter = this.containerMetrics.width / 2; |
| 2896 var yCenter = this.containerMetrics.height / 2; |
| 2897 this.resetInteractionState(); |
| 2898 this.mouseDownStart = Utility.now(); |
| 2899 if (this.center) { |
| 2900 this.xStart = xCenter; |
| 2901 this.yStart = yCenter; |
| 2902 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2903 } else { |
| 2904 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec
t.left : this.containerMetrics.width / 2; |
| 2905 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec
t.top : this.containerMetrics.height / 2; |
| 2906 } |
| 2907 if (this.recenters) { |
| 2908 this.xEnd = xCenter; |
| 2909 this.yEnd = yCenter; |
| 2910 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2911 } |
| 2912 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt
art, this.yStart); |
| 2913 this.waveContainer.style.top = (this.containerMetrics.height - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2914 this.waveContainer.style.left = (this.containerMetrics.width - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2915 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 2916 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 2917 }, |
| 2918 upAction: function(event) { |
| 2919 if (!this.isMouseDown) { |
| 2920 return; |
| 2921 } |
| 2922 this.mouseUpStart = Utility.now(); |
| 2923 }, |
| 2924 remove: function() { |
| 2925 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer)
; |
| 2926 } |
| 2927 }; |
| 2928 Polymer({ |
| 2929 is: 'paper-ripple', |
| 2930 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 2931 properties: { |
| 2932 initialOpacity: { |
| 2933 type: Number, |
| 2934 value: .25 |
| 2935 }, |
| 2936 opacityDecayVelocity: { |
| 2937 type: Number, |
| 2938 value: .8 |
| 2939 }, |
| 2940 recenters: { |
| 2941 type: Boolean, |
| 2942 value: false |
| 2943 }, |
| 2944 center: { |
| 2945 type: Boolean, |
| 2946 value: false |
| 2947 }, |
| 2948 ripples: { |
| 2949 type: Array, |
| 2950 value: function() { |
| 2951 return []; |
4607 } | 2952 } |
4608 }, | 2953 }, |
4609 | 2954 animating: { |
4610 /** | 2955 type: Boolean, |
4611 * Retrieves meta data value by key. | 2956 readOnly: true, |
4612 * @param {string} key The key of the meta-data to be returned. | 2957 reflectToAttribute: true, |
4613 * @return {*} | 2958 value: false |
4614 */ | 2959 }, |
4615 byKey: function(key) { | 2960 holdDown: { |
4616 return this._metaData && this._metaData[key]; | |
4617 } | |
4618 | |
4619 }); | |
4620 | |
4621 })(); | |
4622 Polymer({ | |
4623 | |
4624 is: 'iron-icon', | |
4625 | |
4626 properties: { | |
4627 | |
4628 /** | |
4629 * The name of the icon to use. The name should be of the form: | |
4630 * `iconset_name:icon_name`. | |
4631 */ | |
4632 icon: { | |
4633 type: String, | |
4634 observer: '_iconChanged' | |
4635 }, | |
4636 | |
4637 /** | |
4638 * The name of the theme to used, if one is specified by the | |
4639 * iconset. | |
4640 */ | |
4641 theme: { | |
4642 type: String, | |
4643 observer: '_updateIcon' | |
4644 }, | |
4645 | |
4646 /** | |
4647 * If using iron-icon without an iconset, you can set the src to be | |
4648 * the URL of an individual icon image file. Note that this will take | |
4649 * precedence over a given icon attribute. | |
4650 */ | |
4651 src: { | |
4652 type: String, | |
4653 observer: '_srcChanged' | |
4654 }, | |
4655 | |
4656 /** | |
4657 * @type {!Polymer.IronMeta} | |
4658 */ | |
4659 _meta: { | |
4660 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | |
4661 observer: '_updateIcon' | |
4662 } | |
4663 | |
4664 }, | |
4665 | |
4666 _DEFAULT_ICONSET: 'icons', | |
4667 | |
4668 _iconChanged: function(icon) { | |
4669 var parts = (icon || '').split(':'); | |
4670 this._iconName = parts.pop(); | |
4671 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; | |
4672 this._updateIcon(); | |
4673 }, | |
4674 | |
4675 _srcChanged: function(src) { | |
4676 this._updateIcon(); | |
4677 }, | |
4678 | |
4679 _usesIconset: function() { | |
4680 return this.icon || !this.src; | |
4681 }, | |
4682 | |
4683 /** @suppress {visibility} */ | |
4684 _updateIcon: function() { | |
4685 if (this._usesIconset()) { | |
4686 if (this._img && this._img.parentNode) { | |
4687 Polymer.dom(this.root).removeChild(this._img); | |
4688 } | |
4689 if (this._iconName === "") { | |
4690 if (this._iconset) { | |
4691 this._iconset.removeIcon(this); | |
4692 } | |
4693 } else if (this._iconsetName && this._meta) { | |
4694 this._iconset = /** @type {?Polymer.Iconset} */ ( | |
4695 this._meta.byKey(this._iconsetName)); | |
4696 if (this._iconset) { | |
4697 this._iconset.applyIcon(this, this._iconName, this.theme); | |
4698 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); | |
4699 } else { | |
4700 this.listen(window, 'iron-iconset-added', '_updateIcon'); | |
4701 } | |
4702 } | |
4703 } else { | |
4704 if (this._iconset) { | |
4705 this._iconset.removeIcon(this); | |
4706 } | |
4707 if (!this._img) { | |
4708 this._img = document.createElement('img'); | |
4709 this._img.style.width = '100%'; | |
4710 this._img.style.height = '100%'; | |
4711 this._img.draggable = false; | |
4712 } | |
4713 this._img.src = this.src; | |
4714 Polymer.dom(this.root).appendChild(this._img); | |
4715 } | |
4716 } | |
4717 | |
4718 }); | |
4719 /** | |
4720 * @demo demo/index.html | |
4721 * @polymerBehavior | |
4722 */ | |
4723 Polymer.IronControlState = { | |
4724 | |
4725 properties: { | |
4726 | |
4727 /** | |
4728 * If true, the element currently has focus. | |
4729 */ | |
4730 focused: { | |
4731 type: Boolean, | 2961 type: Boolean, |
4732 value: false, | 2962 value: false, |
4733 notify: true, | 2963 observer: '_holdDownChanged' |
4734 readOnly: true, | 2964 }, |
4735 reflectToAttribute: true | 2965 noink: { |
4736 }, | |
4737 | |
4738 /** | |
4739 * If true, the user cannot interact with this element. | |
4740 */ | |
4741 disabled: { | |
4742 type: Boolean, | 2966 type: Boolean, |
4743 value: false, | 2967 value: false |
4744 notify: true, | 2968 }, |
4745 observer: '_disabledChanged', | 2969 _animating: { |
4746 reflectToAttribute: true | 2970 type: Boolean |
4747 }, | 2971 }, |
4748 | 2972 _boundAnimate: { |
4749 _oldTabIndex: { | |
4750 type: Number | |
4751 }, | |
4752 | |
4753 _boundFocusBlurHandler: { | |
4754 type: Function, | 2973 type: Function, |
4755 value: function() { | 2974 value: function() { |
4756 return this._focusBlurHandler.bind(this); | 2975 return this.animate.bind(this); |
4757 } | 2976 } |
4758 } | 2977 } |
4759 | 2978 }, |
4760 }, | 2979 get target() { |
4761 | 2980 return this.keyEventTarget; |
4762 observers: [ | 2981 }, |
4763 '_changedControlState(focused, disabled)' | 2982 keyBindings: { |
4764 ], | 2983 'enter:keydown': '_onEnterKeydown', |
4765 | 2984 'space:keydown': '_onSpaceKeydown', |
4766 ready: function() { | 2985 'space:keyup': '_onSpaceKeyup' |
4767 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 2986 }, |
4768 this.addEventListener('blur', this._boundFocusBlurHandler, true); | 2987 attached: function() { |
4769 }, | 2988 if (this.parentNode.nodeType == 11) { |
4770 | 2989 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
4771 _focusBlurHandler: function(event) { | 2990 } else { |
4772 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | 2991 this.keyEventTarget = this.parentNode; |
4773 // eventually become `this` due to retargeting; if we are not in | 2992 } |
4774 // ShadowDOM land, `event.target` will eventually become `this` due | 2993 var keyEventTarget = this.keyEventTarget; |
4775 // to the second conditional which fires a synthetic event (that is also | 2994 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
4776 // handled). In either case, we can disregard `event.path`. | 2995 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
4777 | 2996 }, |
4778 if (event.target === this) { | 2997 detached: function() { |
4779 this._setFocused(event.type === 'focus'); | 2998 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
4780 } else if (!this.shadowRoot) { | 2999 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
4781 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | 3000 this.keyEventTarget = null; |
4782 if (!this.isLightDescendant(target)) { | 3001 }, |
4783 this.fire(event.type, {sourceEvent: event}, { | 3002 get shouldKeepAnimating() { |
4784 node: this, | 3003 for (var index = 0; index < this.ripples.length; ++index) { |
4785 bubbles: event.bubbles, | 3004 if (!this.ripples[index].isAnimationComplete) { |
4786 cancelable: event.cancelable | 3005 return true; |
4787 }); | |
4788 } | 3006 } |
4789 } | 3007 } |
4790 }, | 3008 return false; |
4791 | 3009 }, |
4792 _disabledChanged: function(disabled, old) { | 3010 simulatedRipple: function() { |
4793 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3011 this.downAction(null); |
4794 this.style.pointerEvents = disabled ? 'none' : ''; | 3012 this.async(function() { |
4795 if (disabled) { | 3013 this.upAction(); |
4796 this._oldTabIndex = this.tabIndex; | 3014 }, 1); |
4797 this._setFocused(false); | 3015 }, |
4798 this.tabIndex = -1; | 3016 uiDownAction: function(event) { |
4799 this.blur(); | 3017 if (!this.noink) { |
4800 } else if (this._oldTabIndex !== undefined) { | 3018 this.downAction(event); |
4801 this.tabIndex = this._oldTabIndex; | 3019 } |
4802 } | 3020 }, |
4803 }, | 3021 downAction: function(event) { |
4804 | 3022 if (this.holdDown && this.ripples.length > 0) { |
4805 _changedControlState: function() { | |
4806 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
4807 if (this._controlStateChanged) { | |
4808 this._controlStateChanged(); | |
4809 } | |
4810 } | |
4811 | |
4812 }; | |
4813 /** | |
4814 * @demo demo/index.html | |
4815 * @polymerBehavior Polymer.IronButtonState | |
4816 */ | |
4817 Polymer.IronButtonStateImpl = { | |
4818 | |
4819 properties: { | |
4820 | |
4821 /** | |
4822 * If true, the user is currently holding down the button. | |
4823 */ | |
4824 pressed: { | |
4825 type: Boolean, | |
4826 readOnly: true, | |
4827 value: false, | |
4828 reflectToAttribute: true, | |
4829 observer: '_pressedChanged' | |
4830 }, | |
4831 | |
4832 /** | |
4833 * If true, the button toggles the active state with each tap or press | |
4834 * of the spacebar. | |
4835 */ | |
4836 toggles: { | |
4837 type: Boolean, | |
4838 value: false, | |
4839 reflectToAttribute: true | |
4840 }, | |
4841 | |
4842 /** | |
4843 * If true, the button is a toggle and is currently in the active state. | |
4844 */ | |
4845 active: { | |
4846 type: Boolean, | |
4847 value: false, | |
4848 notify: true, | |
4849 reflectToAttribute: true | |
4850 }, | |
4851 | |
4852 /** | |
4853 * True if the element is currently being pressed by a "pointer," which | |
4854 * is loosely defined as mouse or touch input (but specifically excluding | |
4855 * keyboard input). | |
4856 */ | |
4857 pointerDown: { | |
4858 type: Boolean, | |
4859 readOnly: true, | |
4860 value: false | |
4861 }, | |
4862 | |
4863 /** | |
4864 * True if the input device that caused the element to receive focus | |
4865 * was a keyboard. | |
4866 */ | |
4867 receivedFocusFromKeyboard: { | |
4868 type: Boolean, | |
4869 readOnly: true | |
4870 }, | |
4871 | |
4872 /** | |
4873 * The aria attribute to be set if the button is a toggle and in the | |
4874 * active state. | |
4875 */ | |
4876 ariaActiveAttribute: { | |
4877 type: String, | |
4878 value: 'aria-pressed', | |
4879 observer: '_ariaActiveAttributeChanged' | |
4880 } | |
4881 }, | |
4882 | |
4883 listeners: { | |
4884 down: '_downHandler', | |
4885 up: '_upHandler', | |
4886 tap: '_tapHandler' | |
4887 }, | |
4888 | |
4889 observers: [ | |
4890 '_detectKeyboardFocus(focused)', | |
4891 '_activeChanged(active, ariaActiveAttribute)' | |
4892 ], | |
4893 | |
4894 keyBindings: { | |
4895 'enter:keydown': '_asyncClick', | |
4896 'space:keydown': '_spaceKeyDownHandler', | |
4897 'space:keyup': '_spaceKeyUpHandler', | |
4898 }, | |
4899 | |
4900 _mouseEventRe: /^mouse/, | |
4901 | |
4902 _tapHandler: function() { | |
4903 if (this.toggles) { | |
4904 // a tap is needed to toggle the active state | |
4905 this._userActivate(!this.active); | |
4906 } else { | |
4907 this.active = false; | |
4908 } | |
4909 }, | |
4910 | |
4911 _detectKeyboardFocus: function(focused) { | |
4912 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
4913 }, | |
4914 | |
4915 // to emulate native checkbox, (de-)activations from a user interaction fire | |
4916 // 'change' events | |
4917 _userActivate: function(active) { | |
4918 if (this.active !== active) { | |
4919 this.active = active; | |
4920 this.fire('change'); | |
4921 } | |
4922 }, | |
4923 | |
4924 _downHandler: function(event) { | |
4925 this._setPointerDown(true); | |
4926 this._setPressed(true); | |
4927 this._setReceivedFocusFromKeyboard(false); | |
4928 }, | |
4929 | |
4930 _upHandler: function() { | |
4931 this._setPointerDown(false); | |
4932 this._setPressed(false); | |
4933 }, | |
4934 | |
4935 /** | |
4936 * @param {!KeyboardEvent} event . | |
4937 */ | |
4938 _spaceKeyDownHandler: function(event) { | |
4939 var keyboardEvent = event.detail.keyboardEvent; | |
4940 var target = Polymer.dom(keyboardEvent).localTarget; | |
4941 | |
4942 // Ignore the event if this is coming from a focused light child, since th
at | |
4943 // element will deal with it. | |
4944 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4945 return; | 3023 return; |
4946 | 3024 } |
4947 keyboardEvent.preventDefault(); | 3025 var ripple = this.addRipple(); |
4948 keyboardEvent.stopImmediatePropagation(); | 3026 ripple.downAction(event); |
4949 this._setPressed(true); | 3027 if (!this._animating) { |
4950 }, | |
4951 | |
4952 /** | |
4953 * @param {!KeyboardEvent} event . | |
4954 */ | |
4955 _spaceKeyUpHandler: function(event) { | |
4956 var keyboardEvent = event.detail.keyboardEvent; | |
4957 var target = Polymer.dom(keyboardEvent).localTarget; | |
4958 | |
4959 // Ignore the event if this is coming from a focused light child, since th
at | |
4960 // element will deal with it. | |
4961 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4962 return; | |
4963 | |
4964 if (this.pressed) { | |
4965 this._asyncClick(); | |
4966 } | |
4967 this._setPressed(false); | |
4968 }, | |
4969 | |
4970 // trigger click asynchronously, the asynchrony is useful to allow one | |
4971 // event handler to unwind before triggering another event | |
4972 _asyncClick: function() { | |
4973 this.async(function() { | |
4974 this.click(); | |
4975 }, 1); | |
4976 }, | |
4977 | |
4978 // any of these changes are considered a change to button state | |
4979 | |
4980 _pressedChanged: function(pressed) { | |
4981 this._changedButtonState(); | |
4982 }, | |
4983 | |
4984 _ariaActiveAttributeChanged: function(value, oldValue) { | |
4985 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | |
4986 this.removeAttribute(oldValue); | |
4987 } | |
4988 }, | |
4989 | |
4990 _activeChanged: function(active, ariaActiveAttribute) { | |
4991 if (this.toggles) { | |
4992 this.setAttribute(this.ariaActiveAttribute, | |
4993 active ? 'true' : 'false'); | |
4994 } else { | |
4995 this.removeAttribute(this.ariaActiveAttribute); | |
4996 } | |
4997 this._changedButtonState(); | |
4998 }, | |
4999 | |
5000 _controlStateChanged: function() { | |
5001 if (this.disabled) { | |
5002 this._setPressed(false); | |
5003 } else { | |
5004 this._changedButtonState(); | |
5005 } | |
5006 }, | |
5007 | |
5008 // provide hook for follow-on behaviors to react to button-state | |
5009 | |
5010 _changedButtonState: function() { | |
5011 if (this._buttonStateChanged) { | |
5012 this._buttonStateChanged(); // abstract | |
5013 } | |
5014 } | |
5015 | |
5016 }; | |
5017 | |
5018 /** @polymerBehavior */ | |
5019 Polymer.IronButtonState = [ | |
5020 Polymer.IronA11yKeysBehavior, | |
5021 Polymer.IronButtonStateImpl | |
5022 ]; | |
5023 (function() { | |
5024 var Utility = { | |
5025 distance: function(x1, y1, x2, y2) { | |
5026 var xDelta = (x1 - x2); | |
5027 var yDelta = (y1 - y2); | |
5028 | |
5029 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
5030 }, | |
5031 | |
5032 now: window.performance && window.performance.now ? | |
5033 window.performance.now.bind(window.performance) : Date.now | |
5034 }; | |
5035 | |
5036 /** | |
5037 * @param {HTMLElement} element | |
5038 * @constructor | |
5039 */ | |
5040 function ElementMetrics(element) { | |
5041 this.element = element; | |
5042 this.width = this.boundingRect.width; | |
5043 this.height = this.boundingRect.height; | |
5044 | |
5045 this.size = Math.max(this.width, this.height); | |
5046 } | |
5047 | |
5048 ElementMetrics.prototype = { | |
5049 get boundingRect () { | |
5050 return this.element.getBoundingClientRect(); | |
5051 }, | |
5052 | |
5053 furthestCornerDistanceFrom: function(x, y) { | |
5054 var topLeft = Utility.distance(x, y, 0, 0); | |
5055 var topRight = Utility.distance(x, y, this.width, 0); | |
5056 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
5057 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
5058 | |
5059 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
5060 } | |
5061 }; | |
5062 | |
5063 /** | |
5064 * @param {HTMLElement} element | |
5065 * @constructor | |
5066 */ | |
5067 function Ripple(element) { | |
5068 this.element = element; | |
5069 this.color = window.getComputedStyle(element).color; | |
5070 | |
5071 this.wave = document.createElement('div'); | |
5072 this.waveContainer = document.createElement('div'); | |
5073 this.wave.style.backgroundColor = this.color; | |
5074 this.wave.classList.add('wave'); | |
5075 this.waveContainer.classList.add('wave-container'); | |
5076 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
5077 | |
5078 this.resetInteractionState(); | |
5079 } | |
5080 | |
5081 Ripple.MAX_RADIUS = 300; | |
5082 | |
5083 Ripple.prototype = { | |
5084 get recenters() { | |
5085 return this.element.recenters; | |
5086 }, | |
5087 | |
5088 get center() { | |
5089 return this.element.center; | |
5090 }, | |
5091 | |
5092 get mouseDownElapsed() { | |
5093 var elapsed; | |
5094 | |
5095 if (!this.mouseDownStart) { | |
5096 return 0; | |
5097 } | |
5098 | |
5099 elapsed = Utility.now() - this.mouseDownStart; | |
5100 | |
5101 if (this.mouseUpStart) { | |
5102 elapsed -= this.mouseUpElapsed; | |
5103 } | |
5104 | |
5105 return elapsed; | |
5106 }, | |
5107 | |
5108 get mouseUpElapsed() { | |
5109 return this.mouseUpStart ? | |
5110 Utility.now () - this.mouseUpStart : 0; | |
5111 }, | |
5112 | |
5113 get mouseDownElapsedSeconds() { | |
5114 return this.mouseDownElapsed / 1000; | |
5115 }, | |
5116 | |
5117 get mouseUpElapsedSeconds() { | |
5118 return this.mouseUpElapsed / 1000; | |
5119 }, | |
5120 | |
5121 get mouseInteractionSeconds() { | |
5122 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
5123 }, | |
5124 | |
5125 get initialOpacity() { | |
5126 return this.element.initialOpacity; | |
5127 }, | |
5128 | |
5129 get opacityDecayVelocity() { | |
5130 return this.element.opacityDecayVelocity; | |
5131 }, | |
5132 | |
5133 get radius() { | |
5134 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
5135 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
5136 var waveRadius = Math.min( | |
5137 Math.sqrt(width2 + height2), | |
5138 Ripple.MAX_RADIUS | |
5139 ) * 1.1 + 5; | |
5140 | |
5141 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
5142 var timeNow = this.mouseInteractionSeconds / duration; | |
5143 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
5144 | |
5145 return Math.abs(size); | |
5146 }, | |
5147 | |
5148 get opacity() { | |
5149 if (!this.mouseUpStart) { | |
5150 return this.initialOpacity; | |
5151 } | |
5152 | |
5153 return Math.max( | |
5154 0, | |
5155 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
5156 ); | |
5157 }, | |
5158 | |
5159 get outerOpacity() { | |
5160 // Linear increase in background opacity, capped at the opacity | |
5161 // of the wavefront (waveOpacity). | |
5162 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
5163 var waveOpacity = this.opacity; | |
5164 | |
5165 return Math.max( | |
5166 0, | |
5167 Math.min(outerOpacity, waveOpacity) | |
5168 ); | |
5169 }, | |
5170 | |
5171 get isOpacityFullyDecayed() { | |
5172 return this.opacity < 0.01 && | |
5173 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5174 }, | |
5175 | |
5176 get isRestingAtMaxRadius() { | |
5177 return this.opacity >= this.initialOpacity && | |
5178 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5179 }, | |
5180 | |
5181 get isAnimationComplete() { | |
5182 return this.mouseUpStart ? | |
5183 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
5184 }, | |
5185 | |
5186 get translationFraction() { | |
5187 return Math.min( | |
5188 1, | |
5189 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
5190 ); | |
5191 }, | |
5192 | |
5193 get xNow() { | |
5194 if (this.xEnd) { | |
5195 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
5196 } | |
5197 | |
5198 return this.xStart; | |
5199 }, | |
5200 | |
5201 get yNow() { | |
5202 if (this.yEnd) { | |
5203 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
5204 } | |
5205 | |
5206 return this.yStart; | |
5207 }, | |
5208 | |
5209 get isMouseDown() { | |
5210 return this.mouseDownStart && !this.mouseUpStart; | |
5211 }, | |
5212 | |
5213 resetInteractionState: function() { | |
5214 this.maxRadius = 0; | |
5215 this.mouseDownStart = 0; | |
5216 this.mouseUpStart = 0; | |
5217 | |
5218 this.xStart = 0; | |
5219 this.yStart = 0; | |
5220 this.xEnd = 0; | |
5221 this.yEnd = 0; | |
5222 this.slideDistance = 0; | |
5223 | |
5224 this.containerMetrics = new ElementMetrics(this.element); | |
5225 }, | |
5226 | |
5227 draw: function() { | |
5228 var scale; | |
5229 var translateString; | |
5230 var dx; | |
5231 var dy; | |
5232 | |
5233 this.wave.style.opacity = this.opacity; | |
5234 | |
5235 scale = this.radius / (this.containerMetrics.size / 2); | |
5236 dx = this.xNow - (this.containerMetrics.width / 2); | |
5237 dy = this.yNow - (this.containerMetrics.height / 2); | |
5238 | |
5239 | |
5240 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
5241 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
5242 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
5243 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
5244 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
5245 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
5246 }, | |
5247 | |
5248 /** @param {Event=} event */ | |
5249 downAction: function(event) { | |
5250 var xCenter = this.containerMetrics.width / 2; | |
5251 var yCenter = this.containerMetrics.height / 2; | |
5252 | |
5253 this.resetInteractionState(); | |
5254 this.mouseDownStart = Utility.now(); | |
5255 | |
5256 if (this.center) { | |
5257 this.xStart = xCenter; | |
5258 this.yStart = yCenter; | |
5259 this.slideDistance = Utility.distance( | |
5260 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5261 ); | |
5262 } else { | |
5263 this.xStart = event ? | |
5264 event.detail.x - this.containerMetrics.boundingRect.left : | |
5265 this.containerMetrics.width / 2; | |
5266 this.yStart = event ? | |
5267 event.detail.y - this.containerMetrics.boundingRect.top : | |
5268 this.containerMetrics.height / 2; | |
5269 } | |
5270 | |
5271 if (this.recenters) { | |
5272 this.xEnd = xCenter; | |
5273 this.yEnd = yCenter; | |
5274 this.slideDistance = Utility.distance( | |
5275 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5276 ); | |
5277 } | |
5278 | |
5279 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
5280 this.xStart, | |
5281 this.yStart | |
5282 ); | |
5283 | |
5284 this.waveContainer.style.top = | |
5285 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
5286 this.waveContainer.style.left = | |
5287 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
5288 | |
5289 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
5290 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
5291 }, | |
5292 | |
5293 /** @param {Event=} event */ | |
5294 upAction: function(event) { | |
5295 if (!this.isMouseDown) { | |
5296 return; | |
5297 } | |
5298 | |
5299 this.mouseUpStart = Utility.now(); | |
5300 }, | |
5301 | |
5302 remove: function() { | |
5303 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
5304 this.waveContainer | |
5305 ); | |
5306 } | |
5307 }; | |
5308 | |
5309 Polymer({ | |
5310 is: 'paper-ripple', | |
5311 | |
5312 behaviors: [ | |
5313 Polymer.IronA11yKeysBehavior | |
5314 ], | |
5315 | |
5316 properties: { | |
5317 /** | |
5318 * The initial opacity set on the wave. | |
5319 * | |
5320 * @attribute initialOpacity | |
5321 * @type number | |
5322 * @default 0.25 | |
5323 */ | |
5324 initialOpacity: { | |
5325 type: Number, | |
5326 value: 0.25 | |
5327 }, | |
5328 | |
5329 /** | |
5330 * How fast (opacity per second) the wave fades out. | |
5331 * | |
5332 * @attribute opacityDecayVelocity | |
5333 * @type number | |
5334 * @default 0.8 | |
5335 */ | |
5336 opacityDecayVelocity: { | |
5337 type: Number, | |
5338 value: 0.8 | |
5339 }, | |
5340 | |
5341 /** | |
5342 * If true, ripples will exhibit a gravitational pull towards | |
5343 * the center of their container as they fade away. | |
5344 * | |
5345 * @attribute recenters | |
5346 * @type boolean | |
5347 * @default false | |
5348 */ | |
5349 recenters: { | |
5350 type: Boolean, | |
5351 value: false | |
5352 }, | |
5353 | |
5354 /** | |
5355 * If true, ripples will center inside its container | |
5356 * | |
5357 * @attribute recenters | |
5358 * @type boolean | |
5359 * @default false | |
5360 */ | |
5361 center: { | |
5362 type: Boolean, | |
5363 value: false | |
5364 }, | |
5365 | |
5366 /** | |
5367 * A list of the visual ripples. | |
5368 * | |
5369 * @attribute ripples | |
5370 * @type Array | |
5371 * @default [] | |
5372 */ | |
5373 ripples: { | |
5374 type: Array, | |
5375 value: function() { | |
5376 return []; | |
5377 } | |
5378 }, | |
5379 | |
5380 /** | |
5381 * True when there are visible ripples animating within the | |
5382 * element. | |
5383 */ | |
5384 animating: { | |
5385 type: Boolean, | |
5386 readOnly: true, | |
5387 reflectToAttribute: true, | |
5388 value: false | |
5389 }, | |
5390 | |
5391 /** | |
5392 * If true, the ripple will remain in the "down" state until `holdDown` | |
5393 * is set to false again. | |
5394 */ | |
5395 holdDown: { | |
5396 type: Boolean, | |
5397 value: false, | |
5398 observer: '_holdDownChanged' | |
5399 }, | |
5400 | |
5401 /** | |
5402 * If true, the ripple will not generate a ripple effect | |
5403 * via pointer interaction. | |
5404 * Calling ripple's imperative api like `simulatedRipple` will | |
5405 * still generate the ripple effect. | |
5406 */ | |
5407 noink: { | |
5408 type: Boolean, | |
5409 value: false | |
5410 }, | |
5411 | |
5412 _animating: { | |
5413 type: Boolean | |
5414 }, | |
5415 | |
5416 _boundAnimate: { | |
5417 type: Function, | |
5418 value: function() { | |
5419 return this.animate.bind(this); | |
5420 } | |
5421 } | |
5422 }, | |
5423 | |
5424 get target () { | |
5425 return this.keyEventTarget; | |
5426 }, | |
5427 | |
5428 keyBindings: { | |
5429 'enter:keydown': '_onEnterKeydown', | |
5430 'space:keydown': '_onSpaceKeydown', | |
5431 'space:keyup': '_onSpaceKeyup' | |
5432 }, | |
5433 | |
5434 attached: function() { | |
5435 // Set up a11yKeysBehavior to listen to key events on the target, | |
5436 // so that space and enter activate the ripple even if the target doesn'
t | |
5437 // handle key events. The key handlers deal with `noink` themselves. | |
5438 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
5439 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
5440 } else { | |
5441 this.keyEventTarget = this.parentNode; | |
5442 } | |
5443 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
5444 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
5445 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
5446 }, | |
5447 | |
5448 detached: function() { | |
5449 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
5450 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
5451 this.keyEventTarget = null; | |
5452 }, | |
5453 | |
5454 get shouldKeepAnimating () { | |
5455 for (var index = 0; index < this.ripples.length; ++index) { | |
5456 if (!this.ripples[index].isAnimationComplete) { | |
5457 return true; | |
5458 } | |
5459 } | |
5460 | |
5461 return false; | |
5462 }, | |
5463 | |
5464 simulatedRipple: function() { | |
5465 this.downAction(null); | |
5466 | |
5467 // Please see polymer/polymer#1305 | |
5468 this.async(function() { | |
5469 this.upAction(); | |
5470 }, 1); | |
5471 }, | |
5472 | |
5473 /** | |
5474 * Provokes a ripple down effect via a UI event, | |
5475 * respecting the `noink` property. | |
5476 * @param {Event=} event | |
5477 */ | |
5478 uiDownAction: function(event) { | |
5479 if (!this.noink) { | |
5480 this.downAction(event); | |
5481 } | |
5482 }, | |
5483 | |
5484 /** | |
5485 * Provokes a ripple down effect via a UI event, | |
5486 * *not* respecting the `noink` property. | |
5487 * @param {Event=} event | |
5488 */ | |
5489 downAction: function(event) { | |
5490 if (this.holdDown && this.ripples.length > 0) { | |
5491 return; | |
5492 } | |
5493 | |
5494 var ripple = this.addRipple(); | |
5495 | |
5496 ripple.downAction(event); | |
5497 | |
5498 if (!this._animating) { | |
5499 this._animating = true; | |
5500 this.animate(); | |
5501 } | |
5502 }, | |
5503 | |
5504 /** | |
5505 * Provokes a ripple up effect via a UI event, | |
5506 * respecting the `noink` property. | |
5507 * @param {Event=} event | |
5508 */ | |
5509 uiUpAction: function(event) { | |
5510 if (!this.noink) { | |
5511 this.upAction(event); | |
5512 } | |
5513 }, | |
5514 | |
5515 /** | |
5516 * Provokes a ripple up effect via a UI event, | |
5517 * *not* respecting the `noink` property. | |
5518 * @param {Event=} event | |
5519 */ | |
5520 upAction: function(event) { | |
5521 if (this.holdDown) { | |
5522 return; | |
5523 } | |
5524 | |
5525 this.ripples.forEach(function(ripple) { | |
5526 ripple.upAction(event); | |
5527 }); | |
5528 | |
5529 this._animating = true; | 3028 this._animating = true; |
5530 this.animate(); | 3029 this.animate(); |
5531 }, | 3030 } |
5532 | 3031 }, |
5533 onAnimationComplete: function() { | 3032 uiUpAction: function(event) { |
5534 this._animating = false; | 3033 if (!this.noink) { |
5535 this.$.background.style.backgroundColor = null; | 3034 this.upAction(event); |
5536 this.fire('transitionend'); | 3035 } |
5537 }, | 3036 }, |
5538 | 3037 upAction: function(event) { |
5539 addRipple: function() { | 3038 if (this.holdDown) { |
5540 var ripple = new Ripple(this); | 3039 return; |
5541 | 3040 } |
5542 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | 3041 this.ripples.forEach(function(ripple) { |
5543 this.$.background.style.backgroundColor = ripple.color; | 3042 ripple.upAction(event); |
5544 this.ripples.push(ripple); | 3043 }); |
5545 | 3044 this._animating = true; |
5546 this._setAnimating(true); | 3045 this.animate(); |
5547 | 3046 }, |
5548 return ripple; | 3047 onAnimationComplete: function() { |
5549 }, | 3048 this._animating = false; |
5550 | 3049 this.$.background.style.backgroundColor = null; |
5551 removeRipple: function(ripple) { | 3050 this.fire('transitionend'); |
5552 var rippleIndex = this.ripples.indexOf(ripple); | 3051 }, |
5553 | 3052 addRipple: function() { |
5554 if (rippleIndex < 0) { | 3053 var ripple = new Ripple(this); |
5555 return; | 3054 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 3055 this.$.background.style.backgroundColor = ripple.color; |
| 3056 this.ripples.push(ripple); |
| 3057 this._setAnimating(true); |
| 3058 return ripple; |
| 3059 }, |
| 3060 removeRipple: function(ripple) { |
| 3061 var rippleIndex = this.ripples.indexOf(ripple); |
| 3062 if (rippleIndex < 0) { |
| 3063 return; |
| 3064 } |
| 3065 this.ripples.splice(rippleIndex, 1); |
| 3066 ripple.remove(); |
| 3067 if (!this.ripples.length) { |
| 3068 this._setAnimating(false); |
| 3069 } |
| 3070 }, |
| 3071 animate: function() { |
| 3072 if (!this._animating) { |
| 3073 return; |
| 3074 } |
| 3075 var index; |
| 3076 var ripple; |
| 3077 for (index = 0; index < this.ripples.length; ++index) { |
| 3078 ripple = this.ripples[index]; |
| 3079 ripple.draw(); |
| 3080 this.$.background.style.opacity = ripple.outerOpacity; |
| 3081 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 3082 this.removeRipple(ripple); |
5556 } | 3083 } |
5557 | 3084 } |
5558 this.ripples.splice(rippleIndex, 1); | 3085 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
5559 | 3086 this.onAnimationComplete(); |
5560 ripple.remove(); | 3087 } else { |
5561 | 3088 window.requestAnimationFrame(this._boundAnimate); |
5562 if (!this.ripples.length) { | 3089 } |
5563 this._setAnimating(false); | 3090 }, |
| 3091 _onEnterKeydown: function() { |
| 3092 this.uiDownAction(); |
| 3093 this.async(this.uiUpAction, 1); |
| 3094 }, |
| 3095 _onSpaceKeydown: function() { |
| 3096 this.uiDownAction(); |
| 3097 }, |
| 3098 _onSpaceKeyup: function() { |
| 3099 this.uiUpAction(); |
| 3100 }, |
| 3101 _holdDownChanged: function(newVal, oldVal) { |
| 3102 if (oldVal === undefined) { |
| 3103 return; |
| 3104 } |
| 3105 if (newVal) { |
| 3106 this.downAction(); |
| 3107 } else { |
| 3108 this.upAction(); |
| 3109 } |
| 3110 } |
| 3111 }); |
| 3112 })(); |
| 3113 |
| 3114 Polymer.PaperRippleBehavior = { |
| 3115 properties: { |
| 3116 noink: { |
| 3117 type: Boolean, |
| 3118 observer: '_noinkChanged' |
| 3119 }, |
| 3120 _rippleContainer: { |
| 3121 type: Object |
| 3122 } |
| 3123 }, |
| 3124 _buttonStateChanged: function() { |
| 3125 if (this.focused) { |
| 3126 this.ensureRipple(); |
| 3127 } |
| 3128 }, |
| 3129 _downHandler: function(event) { |
| 3130 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 3131 if (this.pressed) { |
| 3132 this.ensureRipple(event); |
| 3133 } |
| 3134 }, |
| 3135 ensureRipple: function(optTriggeringEvent) { |
| 3136 if (!this.hasRipple()) { |
| 3137 this._ripple = this._createRipple(); |
| 3138 this._ripple.noink = this.noink; |
| 3139 var rippleContainer = this._rippleContainer || this.root; |
| 3140 if (rippleContainer) { |
| 3141 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 3142 } |
| 3143 if (optTriggeringEvent) { |
| 3144 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 3145 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 3146 if (domContainer.deepContains(target)) { |
| 3147 this._ripple.uiDownAction(optTriggeringEvent); |
5564 } | 3148 } |
5565 }, | 3149 } |
5566 | 3150 } |
5567 animate: function() { | 3151 }, |
5568 if (!this._animating) { | 3152 getRipple: function() { |
5569 return; | 3153 this.ensureRipple(); |
5570 } | 3154 return this._ripple; |
5571 var index; | 3155 }, |
5572 var ripple; | 3156 hasRipple: function() { |
5573 | 3157 return Boolean(this._ripple); |
5574 for (index = 0; index < this.ripples.length; ++index) { | 3158 }, |
5575 ripple = this.ripples[index]; | 3159 _createRipple: function() { |
5576 | 3160 return document.createElement('paper-ripple'); |
5577 ripple.draw(); | 3161 }, |
5578 | 3162 _noinkChanged: function(noink) { |
5579 this.$.background.style.opacity = ripple.outerOpacity; | 3163 if (this.hasRipple()) { |
5580 | 3164 this._ripple.noink = noink; |
5581 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | 3165 } |
5582 this.removeRipple(ripple); | 3166 } |
5583 } | 3167 }; |
5584 } | 3168 |
5585 | 3169 Polymer.PaperButtonBehaviorImpl = { |
5586 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | 3170 properties: { |
5587 this.onAnimationComplete(); | 3171 elevation: { |
5588 } else { | 3172 type: Number, |
5589 window.requestAnimationFrame(this._boundAnimate); | 3173 reflectToAttribute: true, |
5590 } | 3174 readOnly: true |
5591 }, | 3175 } |
5592 | 3176 }, |
5593 _onEnterKeydown: function() { | 3177 observers: [ '_calculateElevation(focused, disabled, active, pressed, received
FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ], |
5594 this.uiDownAction(); | 3178 hostAttributes: { |
5595 this.async(this.uiUpAction, 1); | 3179 role: 'button', |
5596 }, | 3180 tabindex: '0', |
5597 | 3181 animated: true |
5598 _onSpaceKeydown: function() { | 3182 }, |
5599 this.uiDownAction(); | 3183 _calculateElevation: function() { |
5600 }, | 3184 var e = 1; |
5601 | 3185 if (this.disabled) { |
5602 _onSpaceKeyup: function() { | 3186 e = 0; |
5603 this.uiUpAction(); | 3187 } else if (this.active || this.pressed) { |
5604 }, | 3188 e = 4; |
5605 | 3189 } else if (this.receivedFocusFromKeyboard) { |
5606 // note: holdDown does not respect noink since it can be a focus based | 3190 e = 3; |
5607 // effect. | 3191 } |
5608 _holdDownChanged: function(newVal, oldVal) { | 3192 this._setElevation(e); |
5609 if (oldVal === undefined) { | 3193 }, |
5610 return; | 3194 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
5611 } | 3195 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
5612 if (newVal) { | 3196 }, |
5613 this.downAction(); | 3197 _spaceKeyDownHandler: function(event) { |
5614 } else { | 3198 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
5615 this.upAction(); | 3199 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
5616 } | 3200 this._ripple.uiDownAction(); |
5617 } | 3201 } |
5618 | 3202 }, |
5619 /** | 3203 _spaceKeyUpHandler: function(event) { |
5620 Fired when the animation finishes. | 3204 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
5621 This is useful if you want to wait until | 3205 if (this.hasRipple()) { |
5622 the ripple animation finishes to perform some action. | 3206 this._ripple.uiUpAction(); |
5623 | 3207 } |
5624 @event transitionend | 3208 } |
5625 @param {{node: Object}} detail Contains the animated node. | 3209 }; |
5626 */ | 3210 |
5627 }); | 3211 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat
e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ]; |
5628 })(); | 3212 |
5629 /** | |
5630 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
5631 * when the element has focus via pointer or keyboard. | |
5632 * | |
5633 * NOTE: This behavior is intended to be used in conjunction with and after | |
5634 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
5635 * | |
5636 * @polymerBehavior Polymer.PaperRippleBehavior | |
5637 */ | |
5638 Polymer.PaperRippleBehavior = { | |
5639 properties: { | |
5640 /** | |
5641 * If true, the element will not produce a ripple effect when interacted | |
5642 * with via the pointer. | |
5643 */ | |
5644 noink: { | |
5645 type: Boolean, | |
5646 observer: '_noinkChanged' | |
5647 }, | |
5648 | |
5649 /** | |
5650 * @type {Element|undefined} | |
5651 */ | |
5652 _rippleContainer: { | |
5653 type: Object, | |
5654 } | |
5655 }, | |
5656 | |
5657 /** | |
5658 * Ensures a `<paper-ripple>` element is available when the element is | |
5659 * focused. | |
5660 */ | |
5661 _buttonStateChanged: function() { | |
5662 if (this.focused) { | |
5663 this.ensureRipple(); | |
5664 } | |
5665 }, | |
5666 | |
5667 /** | |
5668 * In addition to the functionality provided in `IronButtonState`, ensures | |
5669 * a ripple effect is created when the element is in a `pressed` state. | |
5670 */ | |
5671 _downHandler: function(event) { | |
5672 Polymer.IronButtonStateImpl._downHandler.call(this, event); | |
5673 if (this.pressed) { | |
5674 this.ensureRipple(event); | |
5675 } | |
5676 }, | |
5677 | |
5678 /** | |
5679 * Ensures this element contains a ripple effect. For startup efficiency | |
5680 * the ripple effect is dynamically on demand when needed. | |
5681 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
5682 * ripple. | |
5683 */ | |
5684 ensureRipple: function(optTriggeringEvent) { | |
5685 if (!this.hasRipple()) { | |
5686 this._ripple = this._createRipple(); | |
5687 this._ripple.noink = this.noink; | |
5688 var rippleContainer = this._rippleContainer || this.root; | |
5689 if (rippleContainer) { | |
5690 Polymer.dom(rippleContainer).appendChild(this._ripple); | |
5691 } | |
5692 if (optTriggeringEvent) { | |
5693 // Check if the event happened inside of the ripple container | |
5694 // Fall back to host instead of the root because distributed text | |
5695 // nodes are not valid event targets | |
5696 var domContainer = Polymer.dom(this._rippleContainer || this); | |
5697 var target = Polymer.dom(optTriggeringEvent).rootTarget; | |
5698 if (domContainer.deepContains( /** @type {Node} */(target))) { | |
5699 this._ripple.uiDownAction(optTriggeringEvent); | |
5700 } | |
5701 } | |
5702 } | |
5703 }, | |
5704 | |
5705 /** | |
5706 * Returns the `<paper-ripple>` element used by this element to create | |
5707 * ripple effects. The element's ripple is created on demand, when | |
5708 * necessary, and calling this method will force the | |
5709 * ripple to be created. | |
5710 */ | |
5711 getRipple: function() { | |
5712 this.ensureRipple(); | |
5713 return this._ripple; | |
5714 }, | |
5715 | |
5716 /** | |
5717 * Returns true if this element currently contains a ripple effect. | |
5718 * @return {boolean} | |
5719 */ | |
5720 hasRipple: function() { | |
5721 return Boolean(this._ripple); | |
5722 }, | |
5723 | |
5724 /** | |
5725 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
5726 * Override this method to customize the ripple element. | |
5727 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
5728 */ | |
5729 _createRipple: function() { | |
5730 return /** @type {!PaperRippleElement} */ ( | |
5731 document.createElement('paper-ripple')); | |
5732 }, | |
5733 | |
5734 _noinkChanged: function(noink) { | |
5735 if (this.hasRipple()) { | |
5736 this._ripple.noink = noink; | |
5737 } | |
5738 } | |
5739 }; | |
5740 /** @polymerBehavior Polymer.PaperButtonBehavior */ | |
5741 Polymer.PaperButtonBehaviorImpl = { | |
5742 properties: { | |
5743 /** | |
5744 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
5745 * shadow, and each increasing number greater than 0 will be "deeper" | |
5746 * than the last. | |
5747 * | |
5748 * @attribute elevation | |
5749 * @type number | |
5750 * @default 1 | |
5751 */ | |
5752 elevation: { | |
5753 type: Number, | |
5754 reflectToAttribute: true, | |
5755 readOnly: true | |
5756 } | |
5757 }, | |
5758 | |
5759 observers: [ | |
5760 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
5761 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
5762 ], | |
5763 | |
5764 hostAttributes: { | |
5765 role: 'button', | |
5766 tabindex: '0', | |
5767 animated: true | |
5768 }, | |
5769 | |
5770 _calculateElevation: function() { | |
5771 var e = 1; | |
5772 if (this.disabled) { | |
5773 e = 0; | |
5774 } else if (this.active || this.pressed) { | |
5775 e = 4; | |
5776 } else if (this.receivedFocusFromKeyboard) { | |
5777 e = 3; | |
5778 } | |
5779 this._setElevation(e); | |
5780 }, | |
5781 | |
5782 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
5783 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
5784 }, | |
5785 | |
5786 /** | |
5787 * In addition to `IronButtonState` behavior, when space key goes down, | |
5788 * create a ripple down effect. | |
5789 * | |
5790 * @param {!KeyboardEvent} event . | |
5791 */ | |
5792 _spaceKeyDownHandler: function(event) { | |
5793 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
5794 // Ensure that there is at most one ripple when the space key is held down
. | |
5795 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
5796 this._ripple.uiDownAction(); | |
5797 } | |
5798 }, | |
5799 | |
5800 /** | |
5801 * In addition to `IronButtonState` behavior, when space key goes up, | |
5802 * create a ripple up effect. | |
5803 * | |
5804 * @param {!KeyboardEvent} event . | |
5805 */ | |
5806 _spaceKeyUpHandler: function(event) { | |
5807 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
5808 if (this.hasRipple()) { | |
5809 this._ripple.uiUpAction(); | |
5810 } | |
5811 } | |
5812 }; | |
5813 | |
5814 /** @polymerBehavior */ | |
5815 Polymer.PaperButtonBehavior = [ | |
5816 Polymer.IronButtonState, | |
5817 Polymer.IronControlState, | |
5818 Polymer.PaperRippleBehavior, | |
5819 Polymer.PaperButtonBehaviorImpl | |
5820 ]; | |
5821 Polymer({ | 3213 Polymer({ |
5822 is: 'paper-button', | 3214 is: 'paper-button', |
5823 | 3215 behaviors: [ Polymer.PaperButtonBehavior ], |
5824 behaviors: [ | 3216 properties: { |
5825 Polymer.PaperButtonBehavior | 3217 raised: { |
5826 ], | 3218 type: Boolean, |
5827 | 3219 reflectToAttribute: true, |
5828 properties: { | 3220 value: false, |
5829 /** | 3221 observer: '_calculateElevation' |
5830 * If true, the button should be styled with a shadow. | 3222 } |
5831 */ | 3223 }, |
5832 raised: { | 3224 _calculateElevation: function() { |
5833 type: Boolean, | 3225 if (!this.raised) { |
5834 reflectToAttribute: true, | 3226 this._setElevation(0); |
5835 value: false, | 3227 } else { |
5836 observer: '_calculateElevation' | 3228 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
5837 } | 3229 } |
5838 }, | 3230 } |
5839 | 3231 }); |
5840 _calculateElevation: function() { | 3232 |
5841 if (!this.raised) { | |
5842 this._setElevation(0); | |
5843 } else { | |
5844 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | |
5845 } | |
5846 } | |
5847 | |
5848 /** | |
5849 Fired when the animation finishes. | |
5850 This is useful if you want to wait until | |
5851 the ripple animation finishes to perform some action. | |
5852 | |
5853 @event transitionend | |
5854 Event param: {{node: Object}} detail Contains the animated node. | |
5855 */ | |
5856 }); | |
5857 Polymer({ | 3233 Polymer({ |
5858 is: 'paper-icon-button-light', | 3234 is: 'paper-icon-button-light', |
5859 extends: 'button', | 3235 "extends": 'button', |
5860 | 3236 behaviors: [ Polymer.PaperRippleBehavior ], |
5861 behaviors: [ | 3237 listeners: { |
5862 Polymer.PaperRippleBehavior | 3238 down: '_rippleDown', |
5863 ], | 3239 up: '_rippleUp', |
5864 | 3240 focus: '_rippleDown', |
5865 listeners: { | 3241 blur: '_rippleUp' |
5866 'down': '_rippleDown', | 3242 }, |
5867 'up': '_rippleUp', | 3243 _rippleDown: function() { |
5868 'focus': '_rippleDown', | 3244 this.getRipple().downAction(); |
5869 'blur': '_rippleUp', | 3245 }, |
5870 }, | 3246 _rippleUp: function() { |
5871 | 3247 this.getRipple().upAction(); |
5872 _rippleDown: function() { | 3248 }, |
5873 this.getRipple().downAction(); | 3249 ensureRipple: function(var_args) { |
5874 }, | 3250 var lastRipple = this._ripple; |
5875 | 3251 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
5876 _rippleUp: function() { | 3252 if (this._ripple && this._ripple !== lastRipple) { |
5877 this.getRipple().upAction(); | 3253 this._ripple.center = true; |
5878 }, | 3254 this._ripple.classList.add('circle'); |
5879 | 3255 } |
5880 /** | 3256 } |
5881 * @param {...*} var_args | 3257 }); |
5882 */ | 3258 |
5883 ensureRipple: function(var_args) { | 3259 Polymer.IronRangeBehavior = { |
5884 var lastRipple = this._ripple; | |
5885 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | |
5886 if (this._ripple && this._ripple !== lastRipple) { | |
5887 this._ripple.center = true; | |
5888 this._ripple.classList.add('circle'); | |
5889 } | |
5890 } | |
5891 }); | |
5892 /** | |
5893 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | |
5894 * | |
5895 * @demo demo/index.html | |
5896 * @polymerBehavior | |
5897 */ | |
5898 Polymer.IronRangeBehavior = { | |
5899 | |
5900 properties: { | 3260 properties: { |
5901 | |
5902 /** | |
5903 * The number that represents the current value. | |
5904 */ | |
5905 value: { | 3261 value: { |
5906 type: Number, | 3262 type: Number, |
5907 value: 0, | 3263 value: 0, |
5908 notify: true, | 3264 notify: true, |
5909 reflectToAttribute: true | 3265 reflectToAttribute: true |
5910 }, | 3266 }, |
5911 | |
5912 /** | |
5913 * The number that indicates the minimum value of the range. | |
5914 */ | |
5915 min: { | 3267 min: { |
5916 type: Number, | 3268 type: Number, |
5917 value: 0, | 3269 value: 0, |
5918 notify: true | 3270 notify: true |
5919 }, | 3271 }, |
5920 | |
5921 /** | |
5922 * The number that indicates the maximum value of the range. | |
5923 */ | |
5924 max: { | 3272 max: { |
5925 type: Number, | 3273 type: Number, |
5926 value: 100, | 3274 value: 100, |
5927 notify: true | 3275 notify: true |
5928 }, | 3276 }, |
5929 | |
5930 /** | |
5931 * Specifies the value granularity of the range's value. | |
5932 */ | |
5933 step: { | 3277 step: { |
5934 type: Number, | 3278 type: Number, |
5935 value: 1, | 3279 value: 1, |
5936 notify: true | 3280 notify: true |
5937 }, | 3281 }, |
5938 | |
5939 /** | |
5940 * Returns the ratio of the value. | |
5941 */ | |
5942 ratio: { | 3282 ratio: { |
5943 type: Number, | 3283 type: Number, |
5944 value: 0, | 3284 value: 0, |
5945 readOnly: true, | 3285 readOnly: true, |
5946 notify: true | 3286 notify: true |
5947 }, | 3287 } |
5948 }, | 3288 }, |
5949 | 3289 observers: [ '_update(value, min, max, step)' ], |
5950 observers: [ | |
5951 '_update(value, min, max, step)' | |
5952 ], | |
5953 | |
5954 _calcRatio: function(value) { | 3290 _calcRatio: function(value) { |
5955 return (this._clampValue(value) - this.min) / (this.max - this.min); | 3291 return (this._clampValue(value) - this.min) / (this.max - this.min); |
5956 }, | 3292 }, |
5957 | |
5958 _clampValue: function(value) { | 3293 _clampValue: function(value) { |
5959 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | 3294 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); |
5960 }, | 3295 }, |
5961 | |
5962 _calcStep: function(value) { | 3296 _calcStep: function(value) { |
5963 // polymer/issues/2493 | |
5964 value = parseFloat(value); | 3297 value = parseFloat(value); |
5965 | |
5966 if (!this.step) { | 3298 if (!this.step) { |
5967 return value; | 3299 return value; |
5968 } | 3300 } |
5969 | |
5970 var numSteps = Math.round((value - this.min) / this.step); | 3301 var numSteps = Math.round((value - this.min) / this.step); |
5971 if (this.step < 1) { | 3302 if (this.step < 1) { |
5972 /** | |
5973 * For small values of this.step, if we calculate the step using | |
5974 * `Math.round(value / step) * step` we may hit a precision point issue | |
5975 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
5976 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
5977 * | |
5978 * as a work around we can divide by the reciprocal of `step` | |
5979 */ | |
5980 return numSteps / (1 / this.step) + this.min; | 3303 return numSteps / (1 / this.step) + this.min; |
5981 } else { | 3304 } else { |
5982 return numSteps * this.step + this.min; | 3305 return numSteps * this.step + this.min; |
5983 } | 3306 } |
5984 }, | 3307 }, |
5985 | |
5986 _validateValue: function() { | 3308 _validateValue: function() { |
5987 var v = this._clampValue(this.value); | 3309 var v = this._clampValue(this.value); |
5988 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | 3310 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; |
5989 return this.value !== v; | 3311 return this.value !== v; |
5990 }, | 3312 }, |
5991 | |
5992 _update: function() { | 3313 _update: function() { |
5993 this._validateValue(); | 3314 this._validateValue(); |
5994 this._setRatio(this._calcRatio(this.value) * 100); | 3315 this._setRatio(this._calcRatio(this.value) * 100); |
5995 } | 3316 } |
5996 | |
5997 }; | 3317 }; |
| 3318 |
5998 Polymer({ | 3319 Polymer({ |
5999 is: 'paper-progress', | 3320 is: 'paper-progress', |
6000 | 3321 behaviors: [ Polymer.IronRangeBehavior ], |
6001 behaviors: [ | 3322 properties: { |
6002 Polymer.IronRangeBehavior | 3323 secondaryProgress: { |
6003 ], | 3324 type: Number, |
6004 | 3325 value: 0 |
6005 properties: { | 3326 }, |
6006 /** | 3327 secondaryRatio: { |
6007 * The number that represents the current secondary progress. | 3328 type: Number, |
6008 */ | 3329 value: 0, |
6009 secondaryProgress: { | 3330 readOnly: true |
6010 type: Number, | 3331 }, |
6011 value: 0 | 3332 indeterminate: { |
6012 }, | 3333 type: Boolean, |
6013 | 3334 value: false, |
6014 /** | 3335 observer: '_toggleIndeterminate' |
6015 * The secondary ratio | 3336 }, |
6016 */ | 3337 disabled: { |
6017 secondaryRatio: { | 3338 type: Boolean, |
6018 type: Number, | 3339 value: false, |
6019 value: 0, | 3340 reflectToAttribute: true, |
6020 readOnly: true | 3341 observer: '_disabledChanged' |
6021 }, | 3342 } |
6022 | 3343 }, |
6023 /** | 3344 observers: [ '_progressChanged(secondaryProgress, value, min, max)' ], |
6024 * Use an indeterminate progress indicator. | 3345 hostAttributes: { |
6025 */ | 3346 role: 'progressbar' |
6026 indeterminate: { | 3347 }, |
6027 type: Boolean, | 3348 _toggleIndeterminate: function(indeterminate) { |
6028 value: false, | 3349 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); |
6029 observer: '_toggleIndeterminate' | 3350 }, |
6030 }, | 3351 _transformProgress: function(progress, ratio) { |
6031 | 3352 var transform = 'scaleX(' + ratio / 100 + ')'; |
6032 /** | 3353 progress.style.transform = progress.style.webkitTransform = transform; |
6033 * True if the progress is disabled. | 3354 }, |
6034 */ | 3355 _mainRatioChanged: function(ratio) { |
6035 disabled: { | 3356 this._transformProgress(this.$.primaryProgress, ratio); |
6036 type: Boolean, | 3357 }, |
6037 value: false, | 3358 _progressChanged: function(secondaryProgress, value, min, max) { |
6038 reflectToAttribute: true, | 3359 secondaryProgress = this._clampValue(secondaryProgress); |
6039 observer: '_disabledChanged' | 3360 value = this._clampValue(value); |
6040 } | 3361 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; |
6041 }, | 3362 var mainRatio = this._calcRatio(value) * 100; |
6042 | 3363 this._setSecondaryRatio(secondaryRatio); |
6043 observers: [ | 3364 this._transformProgress(this.$.secondaryProgress, secondaryRatio); |
6044 '_progressChanged(secondaryProgress, value, min, max)' | 3365 this._transformProgress(this.$.primaryProgress, mainRatio); |
6045 ], | 3366 this.secondaryProgress = secondaryProgress; |
6046 | 3367 this.setAttribute('aria-valuenow', value); |
6047 hostAttributes: { | 3368 this.setAttribute('aria-valuemin', min); |
6048 role: 'progressbar' | 3369 this.setAttribute('aria-valuemax', max); |
6049 }, | 3370 }, |
6050 | 3371 _disabledChanged: function(disabled) { |
6051 _toggleIndeterminate: function(indeterminate) { | 3372 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
6052 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | 3373 }, |
6053 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | 3374 _hideSecondaryProgress: function(secondaryRatio) { |
6054 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 3375 return secondaryRatio === 0; |
6055 }, | 3376 } |
6056 | 3377 }); |
6057 _transformProgress: function(progress, ratio) { | 3378 |
6058 var transform = 'scaleX(' + (ratio / 100) + ')'; | 3379 Polymer({ |
6059 progress.style.transform = progress.style.webkitTransform = transform; | 3380 is: 'iron-iconset-svg', |
6060 }, | 3381 properties: { |
6061 | 3382 name: { |
6062 _mainRatioChanged: function(ratio) { | 3383 type: String, |
6063 this._transformProgress(this.$.primaryProgress, ratio); | 3384 observer: '_nameChanged' |
6064 }, | 3385 }, |
6065 | 3386 size: { |
6066 _progressChanged: function(secondaryProgress, value, min, max) { | 3387 type: Number, |
6067 secondaryProgress = this._clampValue(secondaryProgress); | 3388 value: 24 |
6068 value = this._clampValue(value); | 3389 } |
6069 | 3390 }, |
6070 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; | 3391 attached: function() { |
6071 var mainRatio = this._calcRatio(value) * 100; | 3392 this.style.display = 'none'; |
6072 | 3393 }, |
6073 this._setSecondaryRatio(secondaryRatio); | 3394 getIconNames: function() { |
6074 this._transformProgress(this.$.secondaryProgress, secondaryRatio); | 3395 this._icons = this._createIconMap(); |
6075 this._transformProgress(this.$.primaryProgress, mainRatio); | 3396 return Object.keys(this._icons).map(function(n) { |
6076 | 3397 return this.name + ':' + n; |
6077 this.secondaryProgress = secondaryProgress; | 3398 }, this); |
6078 | 3399 }, |
6079 this.setAttribute('aria-valuenow', value); | 3400 applyIcon: function(element, iconName) { |
6080 this.setAttribute('aria-valuemin', min); | 3401 element = element.root || element; |
6081 this.setAttribute('aria-valuemax', max); | 3402 this.removeIcon(element); |
6082 }, | 3403 var svg = this._cloneIcon(iconName); |
6083 | 3404 if (svg) { |
6084 _disabledChanged: function(disabled) { | 3405 var pde = Polymer.dom(element); |
6085 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3406 pde.insertBefore(svg, pde.childNodes[0]); |
6086 }, | 3407 return element._svgIcon = svg; |
6087 | 3408 } |
6088 _hideSecondaryProgress: function(secondaryRatio) { | 3409 return null; |
6089 return secondaryRatio === 0; | 3410 }, |
6090 } | 3411 removeIcon: function(element) { |
6091 }); | 3412 if (element._svgIcon) { |
6092 /** | 3413 Polymer.dom(element).removeChild(element._svgIcon); |
6093 * The `iron-iconset-svg` element allows users to define their own icon sets | 3414 element._svgIcon = null; |
6094 * that contain svg icons. The svg icon elements should be children of the | 3415 } |
6095 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | 3416 }, |
6096 * | 3417 _nameChanged: function() { |
6097 * Using svg elements to create icons has a few advantages over traditional | 3418 new Polymer.IronMeta({ |
6098 * bitmap graphics like jpg or png. Icons that use svg are vector based so | 3419 type: 'iconset', |
6099 * they are resolution independent and should look good on any device. They | 3420 key: this.name, |
6100 * are stylable via css. Icons can be themed, colorized, and even animated. | 3421 value: this |
6101 * | 3422 }); |
6102 * Example: | 3423 this.async(function() { |
6103 * | 3424 this.fire('iron-iconset-added', this, { |
6104 * <iron-iconset-svg name="my-svg-icons" size="24"> | 3425 node: window |
6105 * <svg> | |
6106 * <defs> | |
6107 * <g id="shape"> | |
6108 * <rect x="12" y="0" width="12" height="24" /> | |
6109 * <circle cx="12" cy="12" r="12" /> | |
6110 * </g> | |
6111 * </defs> | |
6112 * </svg> | |
6113 * </iron-iconset-svg> | |
6114 * | |
6115 * This will automatically register the icon set "my-svg-icons" to the iconset | |
6116 * database. To use these icons from within another element, make a | |
6117 * `iron-iconset` element and call the `byId` method | |
6118 * to retrieve a given iconset. To apply a particular icon inside an | |
6119 * element use the `applyIcon` method. For example: | |
6120 * | |
6121 * iconset.applyIcon(iconNode, 'car'); | |
6122 * | |
6123 * @element iron-iconset-svg | |
6124 * @demo demo/index.html | |
6125 * @implements {Polymer.Iconset} | |
6126 */ | |
6127 Polymer({ | |
6128 is: 'iron-iconset-svg', | |
6129 | |
6130 properties: { | |
6131 | |
6132 /** | |
6133 * The name of the iconset. | |
6134 */ | |
6135 name: { | |
6136 type: String, | |
6137 observer: '_nameChanged' | |
6138 }, | |
6139 | |
6140 /** | |
6141 * The size of an individual icon. Note that icons must be square. | |
6142 */ | |
6143 size: { | |
6144 type: Number, | |
6145 value: 24 | |
6146 } | |
6147 | |
6148 }, | |
6149 | |
6150 attached: function() { | |
6151 this.style.display = 'none'; | |
6152 }, | |
6153 | |
6154 /** | |
6155 * Construct an array of all icon names in this iconset. | |
6156 * | |
6157 * @return {!Array} Array of icon names. | |
6158 */ | |
6159 getIconNames: function() { | |
6160 this._icons = this._createIconMap(); | |
6161 return Object.keys(this._icons).map(function(n) { | |
6162 return this.name + ':' + n; | |
6163 }, this); | |
6164 }, | |
6165 | |
6166 /** | |
6167 * Applies an icon to the given element. | |
6168 * | |
6169 * An svg icon is prepended to the element's shadowRoot if it exists, | |
6170 * otherwise to the element itself. | |
6171 * | |
6172 * @method applyIcon | |
6173 * @param {Element} element Element to which the icon is applied. | |
6174 * @param {string} iconName Name of the icon to apply. | |
6175 * @return {?Element} The svg element which renders the icon. | |
6176 */ | |
6177 applyIcon: function(element, iconName) { | |
6178 // insert svg element into shadow root, if it exists | |
6179 element = element.root || element; | |
6180 // Remove old svg element | |
6181 this.removeIcon(element); | |
6182 // install new svg element | |
6183 var svg = this._cloneIcon(iconName); | |
6184 if (svg) { | |
6185 var pde = Polymer.dom(element); | |
6186 pde.insertBefore(svg, pde.childNodes[0]); | |
6187 return element._svgIcon = svg; | |
6188 } | |
6189 return null; | |
6190 }, | |
6191 | |
6192 /** | |
6193 * Remove an icon from the given element by undoing the changes effected | |
6194 * by `applyIcon`. | |
6195 * | |
6196 * @param {Element} element The element from which the icon is removed. | |
6197 */ | |
6198 removeIcon: function(element) { | |
6199 // Remove old svg element | |
6200 if (element._svgIcon) { | |
6201 Polymer.dom(element).removeChild(element._svgIcon); | |
6202 element._svgIcon = null; | |
6203 } | |
6204 }, | |
6205 | |
6206 /** | |
6207 * | |
6208 * When name is changed, register iconset metadata | |
6209 * | |
6210 */ | |
6211 _nameChanged: function() { | |
6212 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | |
6213 this.async(function() { | |
6214 this.fire('iron-iconset-added', this, {node: window}); | |
6215 }); | 3426 }); |
6216 }, | 3427 }); |
6217 | 3428 }, |
6218 /** | 3429 _createIconMap: function() { |
6219 * Create a map of child SVG elements by id. | 3430 var icons = Object.create(null); |
6220 * | 3431 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) { |
6221 * @return {!Object} Map of id's to SVG elements. | 3432 icons[icon.id] = icon; |
6222 */ | 3433 }); |
6223 _createIconMap: function() { | 3434 return icons; |
6224 // Objects chained to Object.prototype (`{}`) have members. Specifically, | 3435 }, |
6225 // on FF there is a `watch` method that confuses the icon map, so we | 3436 _cloneIcon: function(id) { |
6226 // need to use a null-based object here. | 3437 this._icons = this._icons || this._createIconMap(); |
6227 var icons = Object.create(null); | 3438 return this._prepareSvgClone(this._icons[id], this.size); |
6228 Polymer.dom(this).querySelectorAll('[id]') | 3439 }, |
6229 .forEach(function(icon) { | 3440 _prepareSvgClone: function(sourceSvg, size) { |
6230 icons[icon.id] = icon; | 3441 if (sourceSvg) { |
6231 }); | 3442 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h
ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') ||
'0 0 ' + size + ' ' + size; |
6232 return icons; | 3443 svg.setAttribute('viewBox', viewBox); |
6233 }, | 3444 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
6234 | 3445 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he
ight: 100%;'; |
6235 /** | 3446 svg.appendChild(content).removeAttribute('id'); |
6236 * Produce installable clone of the SVG element matching `id` in this | 3447 return svg; |
6237 * iconset, or `undefined` if there is no matching element. | 3448 } |
6238 * | 3449 return null; |
6239 * @return {Element} Returns an installable clone of the SVG element | 3450 } |
6240 * matching `id`. | 3451 }); |
6241 */ | 3452 |
6242 _cloneIcon: function(id) { | |
6243 // create the icon map on-demand, since the iconset itself has no discrete | |
6244 // signal to know when it's children are fully parsed | |
6245 this._icons = this._icons || this._createIconMap(); | |
6246 return this._prepareSvgClone(this._icons[id], this.size); | |
6247 }, | |
6248 | |
6249 /** | |
6250 * @param {Element} sourceSvg | |
6251 * @param {number} size | |
6252 * @return {Element} | |
6253 */ | |
6254 _prepareSvgClone: function(sourceSvg, size) { | |
6255 if (sourceSvg) { | |
6256 var content = sourceSvg.cloneNode(true), | |
6257 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | |
6258 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | |
6259 svg.setAttribute('viewBox', viewBox); | |
6260 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | |
6261 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
6262 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
6263 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | |
6264 svg.appendChild(content).removeAttribute('id'); | |
6265 return svg; | |
6266 } | |
6267 return null; | |
6268 } | |
6269 | |
6270 }); | |
6271 // Copyright 2015 The Chromium Authors. All rights reserved. | 3453 // Copyright 2015 The Chromium Authors. All rights reserved. |
6272 // Use of this source code is governed by a BSD-style license that can be | 3454 // Use of this source code is governed by a BSD-style license that can be |
6273 // found in the LICENSE file. | 3455 // found in the LICENSE file. |
6274 | |
6275 cr.define('downloads', function() { | 3456 cr.define('downloads', function() { |
6276 var Item = Polymer({ | 3457 var Item = Polymer({ |
6277 is: 'downloads-item', | 3458 is: 'downloads-item', |
6278 | |
6279 properties: { | 3459 properties: { |
6280 data: { | 3460 data: { |
6281 type: Object, | 3461 type: Object |
6282 }, | 3462 }, |
6283 | |
6284 completelyOnDisk_: { | 3463 completelyOnDisk_: { |
6285 computed: 'computeCompletelyOnDisk_(' + | 3464 computed: 'computeCompletelyOnDisk_(' + 'data.state, data.file_externall
y_removed)', |
6286 'data.state, data.file_externally_removed)', | |
6287 type: Boolean, | 3465 type: Boolean, |
6288 value: true, | 3466 value: true |
6289 }, | 3467 }, |
6290 | |
6291 controlledBy_: { | 3468 controlledBy_: { |
6292 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', | 3469 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', |
6293 type: String, | 3470 type: String, |
6294 value: '', | 3471 value: '' |
6295 }, | 3472 }, |
6296 | |
6297 isActive_: { | 3473 isActive_: { |
6298 computed: 'computeIsActive_(' + | 3474 computed: 'computeIsActive_(' + 'data.state, data.file_externally_remove
d)', |
6299 'data.state, data.file_externally_removed)', | |
6300 type: Boolean, | 3475 type: Boolean, |
6301 value: true, | 3476 value: true |
6302 }, | 3477 }, |
6303 | |
6304 isDangerous_: { | 3478 isDangerous_: { |
6305 computed: 'computeIsDangerous_(data.state)', | 3479 computed: 'computeIsDangerous_(data.state)', |
6306 type: Boolean, | 3480 type: Boolean, |
6307 value: false, | 3481 value: false |
6308 }, | 3482 }, |
6309 | |
6310 isMalware_: { | 3483 isMalware_: { |
6311 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', | 3484 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', |
6312 type: Boolean, | 3485 type: Boolean, |
6313 value: false, | 3486 value: false |
6314 }, | 3487 }, |
6315 | |
6316 isInProgress_: { | 3488 isInProgress_: { |
6317 computed: 'computeIsInProgress_(data.state)', | 3489 computed: 'computeIsInProgress_(data.state)', |
6318 type: Boolean, | 3490 type: Boolean, |
6319 value: false, | 3491 value: false |
6320 }, | 3492 }, |
6321 | |
6322 pauseOrResumeText_: { | 3493 pauseOrResumeText_: { |
6323 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', | 3494 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', |
6324 type: String, | 3495 type: String |
6325 }, | 3496 }, |
6326 | |
6327 showCancel_: { | 3497 showCancel_: { |
6328 computed: 'computeShowCancel_(data.state)', | 3498 computed: 'computeShowCancel_(data.state)', |
6329 type: Boolean, | 3499 type: Boolean, |
6330 value: false, | 3500 value: false |
6331 }, | 3501 }, |
6332 | |
6333 showProgress_: { | 3502 showProgress_: { |
6334 computed: 'computeShowProgress_(showCancel_, data.percent)', | 3503 computed: 'computeShowProgress_(showCancel_, data.percent)', |
6335 type: Boolean, | 3504 type: Boolean, |
6336 value: false, | 3505 value: false |
6337 }, | 3506 } |
6338 }, | 3507 }, |
6339 | 3508 observers: [ 'observeControlledBy_(controlledBy_)', 'observeIsDangerous_(isD
angerous_, data)' ], |
6340 observers: [ | |
6341 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | |
6342 // and data.by_ext_name directly. Why? | |
6343 'observeControlledBy_(controlledBy_)', | |
6344 'observeIsDangerous_(isDangerous_, data)', | |
6345 ], | |
6346 | |
6347 ready: function() { | 3509 ready: function() { |
6348 this.content = this.$.content; | 3510 this.content = this.$.content; |
6349 }, | 3511 }, |
6350 | |
6351 /** @private */ | |
6352 computeClass_: function() { | 3512 computeClass_: function() { |
6353 var classes = []; | 3513 var classes = []; |
6354 | 3514 if (this.isActive_) classes.push('is-active'); |
6355 if (this.isActive_) | 3515 if (this.isDangerous_) classes.push('dangerous'); |
6356 classes.push('is-active'); | 3516 if (this.showProgress_) classes.push('show-progress'); |
6357 | |
6358 if (this.isDangerous_) | |
6359 classes.push('dangerous'); | |
6360 | |
6361 if (this.showProgress_) | |
6362 classes.push('show-progress'); | |
6363 | |
6364 return classes.join(' '); | 3517 return classes.join(' '); |
6365 }, | 3518 }, |
6366 | |
6367 /** @private */ | |
6368 computeCompletelyOnDisk_: function() { | 3519 computeCompletelyOnDisk_: function() { |
6369 return this.data.state == downloads.States.COMPLETE && | 3520 return this.data.state == downloads.States.COMPLETE && !this.data.file_ext
ernally_removed; |
6370 !this.data.file_externally_removed; | 3521 }, |
6371 }, | |
6372 | |
6373 /** @private */ | |
6374 computeControlledBy_: function() { | 3522 computeControlledBy_: function() { |
6375 if (!this.data.by_ext_id || !this.data.by_ext_name) | 3523 if (!this.data.by_ext_id || !this.data.by_ext_name) return ''; |
6376 return ''; | |
6377 | |
6378 var url = 'chrome://extensions#' + this.data.by_ext_id; | 3524 var url = 'chrome://extensions#' + this.data.by_ext_id; |
6379 var name = this.data.by_ext_name; | 3525 var name = this.data.by_ext_name; |
6380 return loadTimeData.getStringF('controlledByUrl', url, name); | 3526 return loadTimeData.getStringF('controlledByUrl', url, name); |
6381 }, | 3527 }, |
6382 | |
6383 /** @private */ | |
6384 computeDangerIcon_: function() { | 3528 computeDangerIcon_: function() { |
6385 if (!this.isDangerous_) | 3529 if (!this.isDangerous_) return ''; |
6386 return ''; | |
6387 | |
6388 switch (this.data.danger_type) { | 3530 switch (this.data.danger_type) { |
6389 case downloads.DangerType.DANGEROUS_CONTENT: | 3531 case downloads.DangerType.DANGEROUS_CONTENT: |
6390 case downloads.DangerType.DANGEROUS_HOST: | 3532 case downloads.DangerType.DANGEROUS_HOST: |
6391 case downloads.DangerType.DANGEROUS_URL: | 3533 case downloads.DangerType.DANGEROUS_URL: |
6392 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3534 case downloads.DangerType.POTENTIALLY_UNWANTED: |
6393 case downloads.DangerType.UNCOMMON_CONTENT: | 3535 case downloads.DangerType.UNCOMMON_CONTENT: |
6394 return 'downloads:remove-circle'; | 3536 return 'downloads:remove-circle'; |
6395 default: | 3537 |
6396 return 'cr:warning'; | 3538 default: |
6397 } | 3539 return 'cr:warning'; |
6398 }, | 3540 } |
6399 | 3541 }, |
6400 /** @private */ | |
6401 computeDate_: function() { | 3542 computeDate_: function() { |
6402 assert(typeof this.data.hideDate == 'boolean'); | 3543 assert(typeof this.data.hideDate == 'boolean'); |
6403 if (this.data.hideDate) | 3544 if (this.data.hideDate) return ''; |
6404 return ''; | |
6405 return assert(this.data.since_string || this.data.date_string); | 3545 return assert(this.data.since_string || this.data.date_string); |
6406 }, | 3546 }, |
6407 | |
6408 /** @private */ | |
6409 computeDescription_: function() { | 3547 computeDescription_: function() { |
6410 var data = this.data; | 3548 var data = this.data; |
6411 | |
6412 switch (data.state) { | 3549 switch (data.state) { |
6413 case downloads.States.DANGEROUS: | 3550 case downloads.States.DANGEROUS: |
6414 var fileName = data.file_name; | 3551 var fileName = data.file_name; |
6415 switch (data.danger_type) { | 3552 switch (data.danger_type) { |
6416 case downloads.DangerType.DANGEROUS_FILE: | 3553 case downloads.DangerType.DANGEROUS_FILE: |
6417 return loadTimeData.getStringF('dangerFileDesc', fileName); | 3554 return loadTimeData.getStringF('dangerFileDesc', fileName); |
6418 case downloads.DangerType.DANGEROUS_URL: | 3555 |
6419 return loadTimeData.getString('dangerUrlDesc'); | 3556 case downloads.DangerType.DANGEROUS_URL: |
6420 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. | 3557 return loadTimeData.getString('dangerUrlDesc'); |
6421 case downloads.DangerType.DANGEROUS_HOST: | 3558 |
6422 return loadTimeData.getStringF('dangerContentDesc', fileName); | 3559 case downloads.DangerType.DANGEROUS_CONTENT: |
6423 case downloads.DangerType.UNCOMMON_CONTENT: | 3560 case downloads.DangerType.DANGEROUS_HOST: |
6424 return loadTimeData.getStringF('dangerUncommonDesc', fileName); | 3561 return loadTimeData.getStringF('dangerContentDesc', fileName); |
6425 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3562 |
6426 return loadTimeData.getStringF('dangerSettingsDesc', fileName); | 3563 case downloads.DangerType.UNCOMMON_CONTENT: |
6427 } | 3564 return loadTimeData.getStringF('dangerUncommonDesc', fileName); |
6428 break; | 3565 |
6429 | 3566 case downloads.DangerType.POTENTIALLY_UNWANTED: |
6430 case downloads.States.IN_PROGRESS: | 3567 return loadTimeData.getStringF('dangerSettingsDesc', fileName); |
6431 case downloads.States.PAUSED: // Fallthrough. | 3568 } |
6432 return data.progress_status_text; | 3569 break; |
6433 } | 3570 |
6434 | 3571 case downloads.States.IN_PROGRESS: |
| 3572 case downloads.States.PAUSED: |
| 3573 return data.progress_status_text; |
| 3574 } |
6435 return ''; | 3575 return ''; |
6436 }, | 3576 }, |
6437 | |
6438 /** @private */ | |
6439 computeIsActive_: function() { | 3577 computeIsActive_: function() { |
6440 return this.data.state != downloads.States.CANCELLED && | 3578 return this.data.state != downloads.States.CANCELLED && this.data.state !=
downloads.States.INTERRUPTED && !this.data.file_externally_removed; |
6441 this.data.state != downloads.States.INTERRUPTED && | 3579 }, |
6442 !this.data.file_externally_removed; | |
6443 }, | |
6444 | |
6445 /** @private */ | |
6446 computeIsDangerous_: function() { | 3580 computeIsDangerous_: function() { |
6447 return this.data.state == downloads.States.DANGEROUS; | 3581 return this.data.state == downloads.States.DANGEROUS; |
6448 }, | 3582 }, |
6449 | |
6450 /** @private */ | |
6451 computeIsInProgress_: function() { | 3583 computeIsInProgress_: function() { |
6452 return this.data.state == downloads.States.IN_PROGRESS; | 3584 return this.data.state == downloads.States.IN_PROGRESS; |
6453 }, | 3585 }, |
6454 | |
6455 /** @private */ | |
6456 computeIsMalware_: function() { | 3586 computeIsMalware_: function() { |
6457 return this.isDangerous_ && | 3587 return this.isDangerous_ && (this.data.danger_type == downloads.DangerType
.DANGEROUS_CONTENT || this.data.danger_type == downloads.DangerType.DANGEROUS_HO
ST || this.data.danger_type == downloads.DangerType.DANGEROUS_URL || this.data.d
anger_type == downloads.DangerType.POTENTIALLY_UNWANTED); |
6458 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || | 3588 }, |
6459 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || | |
6460 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || | |
6461 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); | |
6462 }, | |
6463 | |
6464 /** @private */ | |
6465 computePauseOrResumeText_: function() { | 3589 computePauseOrResumeText_: function() { |
6466 if (this.isInProgress_) | 3590 if (this.isInProgress_) return loadTimeData.getString('controlPause'); |
6467 return loadTimeData.getString('controlPause'); | 3591 if (this.data.resume) return loadTimeData.getString('controlResume'); |
6468 if (this.data.resume) | |
6469 return loadTimeData.getString('controlResume'); | |
6470 return ''; | 3592 return ''; |
6471 }, | 3593 }, |
6472 | |
6473 /** @private */ | |
6474 computeRemoveStyle_: function() { | 3594 computeRemoveStyle_: function() { |
6475 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); | 3595 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); |
6476 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; | 3596 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; |
6477 return hideRemove ? 'visibility: hidden' : ''; | 3597 return hideRemove ? 'visibility: hidden' : ''; |
6478 }, | 3598 }, |
6479 | |
6480 /** @private */ | |
6481 computeShowCancel_: function() { | 3599 computeShowCancel_: function() { |
6482 return this.data.state == downloads.States.IN_PROGRESS || | 3600 return this.data.state == downloads.States.IN_PROGRESS || this.data.state
== downloads.States.PAUSED; |
6483 this.data.state == downloads.States.PAUSED; | 3601 }, |
6484 }, | |
6485 | |
6486 /** @private */ | |
6487 computeShowProgress_: function() { | 3602 computeShowProgress_: function() { |
6488 return this.showCancel_ && this.data.percent >= -1; | 3603 return this.showCancel_ && this.data.percent >= -1; |
6489 }, | 3604 }, |
6490 | |
6491 /** @private */ | |
6492 computeTag_: function() { | 3605 computeTag_: function() { |
6493 switch (this.data.state) { | 3606 switch (this.data.state) { |
6494 case downloads.States.CANCELLED: | 3607 case downloads.States.CANCELLED: |
6495 return loadTimeData.getString('statusCancelled'); | 3608 return loadTimeData.getString('statusCancelled'); |
6496 | 3609 |
6497 case downloads.States.INTERRUPTED: | 3610 case downloads.States.INTERRUPTED: |
6498 return this.data.last_reason_text; | 3611 return this.data.last_reason_text; |
6499 | 3612 |
6500 case downloads.States.COMPLETE: | 3613 case downloads.States.COMPLETE: |
6501 return this.data.file_externally_removed ? | 3614 return this.data.file_externally_removed ? loadTimeData.getString('statu
sRemoved') : ''; |
6502 loadTimeData.getString('statusRemoved') : ''; | 3615 } |
6503 } | |
6504 | |
6505 return ''; | 3616 return ''; |
6506 }, | 3617 }, |
6507 | |
6508 /** @private */ | |
6509 isIndeterminate_: function() { | 3618 isIndeterminate_: function() { |
6510 return this.data.percent == -1; | 3619 return this.data.percent == -1; |
6511 }, | 3620 }, |
6512 | |
6513 /** @private */ | |
6514 observeControlledBy_: function() { | 3621 observeControlledBy_: function() { |
6515 this.$['controlled-by'].innerHTML = this.controlledBy_; | 3622 this.$['controlled-by'].innerHTML = this.controlledBy_; |
6516 }, | 3623 }, |
6517 | |
6518 /** @private */ | |
6519 observeIsDangerous_: function() { | 3624 observeIsDangerous_: function() { |
6520 if (!this.data) | 3625 if (!this.data) return; |
6521 return; | |
6522 | |
6523 if (this.isDangerous_) { | 3626 if (this.isDangerous_) { |
6524 this.$.url.removeAttribute('href'); | 3627 this.$.url.removeAttribute('href'); |
6525 } else { | 3628 } else { |
6526 this.$.url.href = assert(this.data.url); | 3629 this.$.url.href = assert(this.data.url); |
6527 var filePath = encodeURIComponent(this.data.file_path); | 3630 var filePath = encodeURIComponent(this.data.file_path); |
6528 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; | 3631 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; |
6529 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; | 3632 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; |
6530 } | 3633 } |
6531 }, | 3634 }, |
6532 | |
6533 /** @private */ | |
6534 onCancelTap_: function() { | 3635 onCancelTap_: function() { |
6535 downloads.ActionService.getInstance().cancel(this.data.id); | 3636 downloads.ActionService.getInstance().cancel(this.data.id); |
6536 }, | 3637 }, |
6537 | |
6538 /** @private */ | |
6539 onDiscardDangerousTap_: function() { | 3638 onDiscardDangerousTap_: function() { |
6540 downloads.ActionService.getInstance().discardDangerous(this.data.id); | 3639 downloads.ActionService.getInstance().discardDangerous(this.data.id); |
6541 }, | 3640 }, |
6542 | |
6543 /** | |
6544 * @private | |
6545 * @param {Event} e | |
6546 */ | |
6547 onDragStart_: function(e) { | 3641 onDragStart_: function(e) { |
6548 e.preventDefault(); | 3642 e.preventDefault(); |
6549 downloads.ActionService.getInstance().drag(this.data.id); | 3643 downloads.ActionService.getInstance().drag(this.data.id); |
6550 }, | 3644 }, |
6551 | |
6552 /** | |
6553 * @param {Event} e | |
6554 * @private | |
6555 */ | |
6556 onFileLinkTap_: function(e) { | 3645 onFileLinkTap_: function(e) { |
6557 e.preventDefault(); | 3646 e.preventDefault(); |
6558 downloads.ActionService.getInstance().openFile(this.data.id); | 3647 downloads.ActionService.getInstance().openFile(this.data.id); |
6559 }, | 3648 }, |
6560 | |
6561 /** @private */ | |
6562 onPauseOrResumeTap_: function() { | 3649 onPauseOrResumeTap_: function() { |
6563 if (this.isInProgress_) | 3650 if (this.isInProgress_) downloads.ActionService.getInstance().pause(this.d
ata.id); else downloads.ActionService.getInstance().resume(this.data.id); |
6564 downloads.ActionService.getInstance().pause(this.data.id); | 3651 }, |
6565 else | |
6566 downloads.ActionService.getInstance().resume(this.data.id); | |
6567 }, | |
6568 | |
6569 /** @private */ | |
6570 onRemoveTap_: function() { | 3652 onRemoveTap_: function() { |
6571 downloads.ActionService.getInstance().remove(this.data.id); | 3653 downloads.ActionService.getInstance().remove(this.data.id); |
6572 }, | 3654 }, |
6573 | |
6574 /** @private */ | |
6575 onRetryTap_: function() { | 3655 onRetryTap_: function() { |
6576 downloads.ActionService.getInstance().download(this.data.url); | 3656 downloads.ActionService.getInstance().download(this.data.url); |
6577 }, | 3657 }, |
6578 | |
6579 /** @private */ | |
6580 onSaveDangerousTap_: function() { | 3658 onSaveDangerousTap_: function() { |
6581 downloads.ActionService.getInstance().saveDangerous(this.data.id); | 3659 downloads.ActionService.getInstance().saveDangerous(this.data.id); |
6582 }, | 3660 }, |
6583 | |
6584 /** @private */ | |
6585 onShowTap_: function() { | 3661 onShowTap_: function() { |
6586 downloads.ActionService.getInstance().show(this.data.id); | 3662 downloads.ActionService.getInstance().show(this.data.id); |
6587 }, | 3663 } |
6588 }); | 3664 }); |
6589 | 3665 return { |
6590 return {Item: Item}; | 3666 Item: Item |
| 3667 }; |
6591 }); | 3668 }); |
6592 /** @polymerBehavior Polymer.PaperItemBehavior */ | 3669 |
6593 Polymer.PaperItemBehaviorImpl = { | 3670 Polymer.PaperItemBehaviorImpl = { |
6594 hostAttributes: { | 3671 hostAttributes: { |
6595 role: 'option', | 3672 role: 'option', |
6596 tabindex: '0' | 3673 tabindex: '0' |
6597 } | 3674 } |
6598 }; | 3675 }; |
6599 | 3676 |
6600 /** @polymerBehavior */ | 3677 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState,
Polymer.PaperItemBehaviorImpl ]; |
6601 Polymer.PaperItemBehavior = [ | 3678 |
6602 Polymer.IronButtonState, | |
6603 Polymer.IronControlState, | |
6604 Polymer.PaperItemBehaviorImpl | |
6605 ]; | |
6606 Polymer({ | 3679 Polymer({ |
6607 is: 'paper-item', | 3680 is: 'paper-item', |
6608 | 3681 behaviors: [ Polymer.PaperItemBehavior ] |
6609 behaviors: [ | 3682 }); |
6610 Polymer.PaperItemBehavior | 3683 |
6611 ] | 3684 Polymer.IronSelection = function(selectCallback) { |
6612 }); | 3685 this.selection = []; |
6613 /** | 3686 this.selectCallback = selectCallback; |
6614 * @param {!Function} selectCallback | 3687 }; |
6615 * @constructor | 3688 |
6616 */ | 3689 Polymer.IronSelection.prototype = { |
6617 Polymer.IronSelection = function(selectCallback) { | 3690 get: function() { |
6618 this.selection = []; | 3691 return this.multi ? this.selection.slice() : this.selection[0]; |
6619 this.selectCallback = selectCallback; | 3692 }, |
6620 }; | 3693 clear: function(excludes) { |
6621 | 3694 this.selection.slice().forEach(function(item) { |
6622 Polymer.IronSelection.prototype = { | 3695 if (!excludes || excludes.indexOf(item) < 0) { |
6623 | 3696 this.setItemSelected(item, false); |
6624 /** | 3697 } |
6625 * Retrieves the selected item(s). | 3698 }, this); |
6626 * | 3699 }, |
6627 * @method get | 3700 isSelected: function(item) { |
6628 * @returns Returns the selected item(s). If the multi property is true, | 3701 return this.selection.indexOf(item) >= 0; |
6629 * `get` will return an array, otherwise it will return | 3702 }, |
6630 * the selected item or undefined if there is no selection. | 3703 setItemSelected: function(item, isSelected) { |
6631 */ | 3704 if (item != null) { |
6632 get: function() { | 3705 if (isSelected !== this.isSelected(item)) { |
6633 return this.multi ? this.selection.slice() : this.selection[0]; | 3706 if (isSelected) { |
6634 }, | 3707 this.selection.push(item); |
6635 | 3708 } else { |
6636 /** | 3709 var i = this.selection.indexOf(item); |
6637 * Clears all the selection except the ones indicated. | 3710 if (i >= 0) { |
6638 * | 3711 this.selection.splice(i, 1); |
6639 * @method clear | |
6640 * @param {Array} excludes items to be excluded. | |
6641 */ | |
6642 clear: function(excludes) { | |
6643 this.selection.slice().forEach(function(item) { | |
6644 if (!excludes || excludes.indexOf(item) < 0) { | |
6645 this.setItemSelected(item, false); | |
6646 } | |
6647 }, this); | |
6648 }, | |
6649 | |
6650 /** | |
6651 * Indicates if a given item is selected. | |
6652 * | |
6653 * @method isSelected | |
6654 * @param {*} item The item whose selection state should be checked. | |
6655 * @returns Returns true if `item` is selected. | |
6656 */ | |
6657 isSelected: function(item) { | |
6658 return this.selection.indexOf(item) >= 0; | |
6659 }, | |
6660 | |
6661 /** | |
6662 * Sets the selection state for a given item to either selected or deselecte
d. | |
6663 * | |
6664 * @method setItemSelected | |
6665 * @param {*} item The item to select. | |
6666 * @param {boolean} isSelected True for selected, false for deselected. | |
6667 */ | |
6668 setItemSelected: function(item, isSelected) { | |
6669 if (item != null) { | |
6670 if (isSelected !== this.isSelected(item)) { | |
6671 // proceed to update selection only if requested state differs from cu
rrent | |
6672 if (isSelected) { | |
6673 this.selection.push(item); | |
6674 } else { | |
6675 var i = this.selection.indexOf(item); | |
6676 if (i >= 0) { | |
6677 this.selection.splice(i, 1); | |
6678 } | |
6679 } | |
6680 if (this.selectCallback) { | |
6681 this.selectCallback(item, isSelected); | |
6682 } | 3712 } |
6683 } | 3713 } |
6684 } | 3714 if (this.selectCallback) { |
6685 }, | 3715 this.selectCallback(item, isSelected); |
6686 | |
6687 /** | |
6688 * Sets the selection state for a given item. If the `multi` property | |
6689 * is true, then the selected state of `item` will be toggled; otherwise | |
6690 * the `item` will be selected. | |
6691 * | |
6692 * @method select | |
6693 * @param {*} item The item to select. | |
6694 */ | |
6695 select: function(item) { | |
6696 if (this.multi) { | |
6697 this.toggle(item); | |
6698 } else if (this.get() !== item) { | |
6699 this.setItemSelected(this.get(), false); | |
6700 this.setItemSelected(item, true); | |
6701 } | |
6702 }, | |
6703 | |
6704 /** | |
6705 * Toggles the selection state for `item`. | |
6706 * | |
6707 * @method toggle | |
6708 * @param {*} item The item to toggle. | |
6709 */ | |
6710 toggle: function(item) { | |
6711 this.setItemSelected(item, !this.isSelected(item)); | |
6712 } | |
6713 | |
6714 }; | |
6715 /** @polymerBehavior */ | |
6716 Polymer.IronSelectableBehavior = { | |
6717 | |
6718 /** | |
6719 * Fired when iron-selector is activated (selected or deselected). | |
6720 * It is fired before the selected items are changed. | |
6721 * Cancel the event to abort selection. | |
6722 * | |
6723 * @event iron-activate | |
6724 */ | |
6725 | |
6726 /** | |
6727 * Fired when an item is selected | |
6728 * | |
6729 * @event iron-select | |
6730 */ | |
6731 | |
6732 /** | |
6733 * Fired when an item is deselected | |
6734 * | |
6735 * @event iron-deselect | |
6736 */ | |
6737 | |
6738 /** | |
6739 * Fired when the list of selectable items changes (e.g., items are | |
6740 * added or removed). The detail of the event is a mutation record that | |
6741 * describes what changed. | |
6742 * | |
6743 * @event iron-items-changed | |
6744 */ | |
6745 | |
6746 properties: { | |
6747 | |
6748 /** | |
6749 * If you want to use an attribute value or property of an element for | |
6750 * `selected` instead of the index, set this to the name of the attribute | |
6751 * or property. Hyphenated values are converted to camel case when used to | |
6752 * look up the property of a selectable element. Camel cased values are | |
6753 * *not* converted to hyphenated values for attribute lookup. It's | |
6754 * recommended that you provide the hyphenated form of the name so that | |
6755 * selection works in both cases. (Use `attr-or-property-name` instead of | |
6756 * `attrOrPropertyName`.) | |
6757 */ | |
6758 attrForSelected: { | |
6759 type: String, | |
6760 value: null | |
6761 }, | |
6762 | |
6763 /** | |
6764 * Gets or sets the selected element. The default is to use the index of t
he item. | |
6765 * @type {string|number} | |
6766 */ | |
6767 selected: { | |
6768 type: String, | |
6769 notify: true | |
6770 }, | |
6771 | |
6772 /** | |
6773 * Returns the currently selected item. | |
6774 * | |
6775 * @type {?Object} | |
6776 */ | |
6777 selectedItem: { | |
6778 type: Object, | |
6779 readOnly: true, | |
6780 notify: true | |
6781 }, | |
6782 | |
6783 /** | |
6784 * The event that fires from items when they are selected. Selectable | |
6785 * will listen for this event from items and update the selection state. | |
6786 * Set to empty string to listen to no events. | |
6787 */ | |
6788 activateEvent: { | |
6789 type: String, | |
6790 value: 'tap', | |
6791 observer: '_activateEventChanged' | |
6792 }, | |
6793 | |
6794 /** | |
6795 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
6796 * are selectable. | |
6797 */ | |
6798 selectable: String, | |
6799 | |
6800 /** | |
6801 * The class to set on elements when selected. | |
6802 */ | |
6803 selectedClass: { | |
6804 type: String, | |
6805 value: 'iron-selected' | |
6806 }, | |
6807 | |
6808 /** | |
6809 * The attribute to set on elements when selected. | |
6810 */ | |
6811 selectedAttribute: { | |
6812 type: String, | |
6813 value: null | |
6814 }, | |
6815 | |
6816 /** | |
6817 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
6818 * is not found. | |
6819 */ | |
6820 fallbackSelection: { | |
6821 type: String, | |
6822 value: null | |
6823 }, | |
6824 | |
6825 /** | |
6826 * The list of items from which a selection can be made. | |
6827 */ | |
6828 items: { | |
6829 type: Array, | |
6830 readOnly: true, | |
6831 notify: true, | |
6832 value: function() { | |
6833 return []; | |
6834 } | 3716 } |
6835 }, | 3717 } |
6836 | 3718 } |
6837 /** | 3719 }, |
6838 * The set of excluded elements where the key is the `localName` | 3720 select: function(item) { |
6839 * of the element that will be ignored from the item list. | 3721 if (this.multi) { |
6840 * | 3722 this.toggle(item); |
6841 * @default {template: 1} | 3723 } else if (this.get() !== item) { |
6842 */ | 3724 this.setItemSelected(this.get(), false); |
6843 _excludedLocalNames: { | 3725 this.setItemSelected(item, true); |
6844 type: Object, | 3726 } |
6845 value: function() { | 3727 }, |
6846 return { | 3728 toggle: function(item) { |
6847 'template': 1 | 3729 this.setItemSelected(item, !this.isSelected(item)); |
6848 }; | 3730 } |
| 3731 }; |
| 3732 |
| 3733 Polymer.IronSelectableBehavior = { |
| 3734 properties: { |
| 3735 attrForSelected: { |
| 3736 type: String, |
| 3737 value: null |
| 3738 }, |
| 3739 selected: { |
| 3740 type: String, |
| 3741 notify: true |
| 3742 }, |
| 3743 selectedItem: { |
| 3744 type: Object, |
| 3745 readOnly: true, |
| 3746 notify: true |
| 3747 }, |
| 3748 activateEvent: { |
| 3749 type: String, |
| 3750 value: 'tap', |
| 3751 observer: '_activateEventChanged' |
| 3752 }, |
| 3753 selectable: String, |
| 3754 selectedClass: { |
| 3755 type: String, |
| 3756 value: 'iron-selected' |
| 3757 }, |
| 3758 selectedAttribute: { |
| 3759 type: String, |
| 3760 value: null |
| 3761 }, |
| 3762 fallbackSelection: { |
| 3763 type: String, |
| 3764 value: null |
| 3765 }, |
| 3766 items: { |
| 3767 type: Array, |
| 3768 readOnly: true, |
| 3769 notify: true, |
| 3770 value: function() { |
| 3771 return []; |
| 3772 } |
| 3773 }, |
| 3774 _excludedLocalNames: { |
| 3775 type: Object, |
| 3776 value: function() { |
| 3777 return { |
| 3778 template: 1 |
| 3779 }; |
| 3780 } |
| 3781 } |
| 3782 }, |
| 3783 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec
ted)', '_checkFallback(fallbackSelection)' ], |
| 3784 created: function() { |
| 3785 this._bindFilterItem = this._filterItem.bind(this); |
| 3786 this._selection = new Polymer.IronSelection(this._applySelection.bind(this))
; |
| 3787 }, |
| 3788 attached: function() { |
| 3789 this._observer = this._observeItems(this); |
| 3790 this._updateItems(); |
| 3791 if (!this._shouldUpdateSelection) { |
| 3792 this._updateSelected(); |
| 3793 } |
| 3794 this._addListener(this.activateEvent); |
| 3795 }, |
| 3796 detached: function() { |
| 3797 if (this._observer) { |
| 3798 Polymer.dom(this).unobserveNodes(this._observer); |
| 3799 } |
| 3800 this._removeListener(this.activateEvent); |
| 3801 }, |
| 3802 indexOf: function(item) { |
| 3803 return this.items.indexOf(item); |
| 3804 }, |
| 3805 select: function(value) { |
| 3806 this.selected = value; |
| 3807 }, |
| 3808 selectPrevious: function() { |
| 3809 var length = this.items.length; |
| 3810 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt
h; |
| 3811 this.selected = this._indexToValue(index); |
| 3812 }, |
| 3813 selectNext: function() { |
| 3814 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len
gth; |
| 3815 this.selected = this._indexToValue(index); |
| 3816 }, |
| 3817 selectIndex: function(index) { |
| 3818 this.select(this._indexToValue(index)); |
| 3819 }, |
| 3820 forceSynchronousItemUpdate: function() { |
| 3821 this._updateItems(); |
| 3822 }, |
| 3823 get _shouldUpdateSelection() { |
| 3824 return this.selected != null; |
| 3825 }, |
| 3826 _checkFallback: function() { |
| 3827 if (this._shouldUpdateSelection) { |
| 3828 this._updateSelected(); |
| 3829 } |
| 3830 }, |
| 3831 _addListener: function(eventName) { |
| 3832 this.listen(this, eventName, '_activateHandler'); |
| 3833 }, |
| 3834 _removeListener: function(eventName) { |
| 3835 this.unlisten(this, eventName, '_activateHandler'); |
| 3836 }, |
| 3837 _activateEventChanged: function(eventName, old) { |
| 3838 this._removeListener(old); |
| 3839 this._addListener(eventName); |
| 3840 }, |
| 3841 _updateItems: function() { |
| 3842 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*
'); |
| 3843 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 3844 this._setItems(nodes); |
| 3845 }, |
| 3846 _updateAttrForSelected: function() { |
| 3847 if (this._shouldUpdateSelection) { |
| 3848 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 3849 } |
| 3850 }, |
| 3851 _updateSelected: function() { |
| 3852 this._selectSelected(this.selected); |
| 3853 }, |
| 3854 _selectSelected: function(selected) { |
| 3855 this._selection.select(this._valueToItem(this.selected)); |
| 3856 if (this.fallbackSelection && this.items.length && this._selection.get() ===
undefined) { |
| 3857 this.selected = this.fallbackSelection; |
| 3858 } |
| 3859 }, |
| 3860 _filterItem: function(node) { |
| 3861 return !this._excludedLocalNames[node.localName]; |
| 3862 }, |
| 3863 _valueToItem: function(value) { |
| 3864 return value == null ? null : this.items[this._valueToIndex(value)]; |
| 3865 }, |
| 3866 _valueToIndex: function(value) { |
| 3867 if (this.attrForSelected) { |
| 3868 for (var i = 0, item; item = this.items[i]; i++) { |
| 3869 if (this._valueForItem(item) == value) { |
| 3870 return i; |
6849 } | 3871 } |
6850 } | 3872 } |
6851 }, | 3873 } else { |
6852 | 3874 return Number(value); |
6853 observers: [ | 3875 } |
6854 '_updateAttrForSelected(attrForSelected)', | 3876 }, |
6855 '_updateSelected(selected)', | 3877 _indexToValue: function(index) { |
6856 '_checkFallback(fallbackSelection)' | 3878 if (this.attrForSelected) { |
6857 ], | 3879 var item = this.items[index]; |
6858 | 3880 if (item) { |
6859 created: function() { | 3881 return this._valueForItem(item); |
6860 this._bindFilterItem = this._filterItem.bind(this); | 3882 } |
6861 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | 3883 } else { |
6862 }, | 3884 return index; |
6863 | 3885 } |
6864 attached: function() { | 3886 }, |
6865 this._observer = this._observeItems(this); | 3887 _valueForItem: function(item) { |
| 3888 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; |
| 3889 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe
lected); |
| 3890 }, |
| 3891 _applySelection: function(item, isSelected) { |
| 3892 if (this.selectedClass) { |
| 3893 this.toggleClass(this.selectedClass, isSelected, item); |
| 3894 } |
| 3895 if (this.selectedAttribute) { |
| 3896 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 3897 } |
| 3898 this._selectionChange(); |
| 3899 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), { |
| 3900 item: item |
| 3901 }); |
| 3902 }, |
| 3903 _selectionChange: function() { |
| 3904 this._setSelectedItem(this._selection.get()); |
| 3905 }, |
| 3906 _observeItems: function(node) { |
| 3907 return Polymer.dom(node).observeNodes(function(mutation) { |
6866 this._updateItems(); | 3908 this._updateItems(); |
6867 if (!this._shouldUpdateSelection) { | |
6868 this._updateSelected(); | |
6869 } | |
6870 this._addListener(this.activateEvent); | |
6871 }, | |
6872 | |
6873 detached: function() { | |
6874 if (this._observer) { | |
6875 Polymer.dom(this).unobserveNodes(this._observer); | |
6876 } | |
6877 this._removeListener(this.activateEvent); | |
6878 }, | |
6879 | |
6880 /** | |
6881 * Returns the index of the given item. | |
6882 * | |
6883 * @method indexOf | |
6884 * @param {Object} item | |
6885 * @returns Returns the index of the item | |
6886 */ | |
6887 indexOf: function(item) { | |
6888 return this.items.indexOf(item); | |
6889 }, | |
6890 | |
6891 /** | |
6892 * Selects the given value. | |
6893 * | |
6894 * @method select | |
6895 * @param {string|number} value the value to select. | |
6896 */ | |
6897 select: function(value) { | |
6898 this.selected = value; | |
6899 }, | |
6900 | |
6901 /** | |
6902 * Selects the previous item. | |
6903 * | |
6904 * @method selectPrevious | |
6905 */ | |
6906 selectPrevious: function() { | |
6907 var length = this.items.length; | |
6908 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | |
6909 this.selected = this._indexToValue(index); | |
6910 }, | |
6911 | |
6912 /** | |
6913 * Selects the next item. | |
6914 * | |
6915 * @method selectNext | |
6916 */ | |
6917 selectNext: function() { | |
6918 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | |
6919 this.selected = this._indexToValue(index); | |
6920 }, | |
6921 | |
6922 /** | |
6923 * Selects the item at the given index. | |
6924 * | |
6925 * @method selectIndex | |
6926 */ | |
6927 selectIndex: function(index) { | |
6928 this.select(this._indexToValue(index)); | |
6929 }, | |
6930 | |
6931 /** | |
6932 * Force a synchronous update of the `items` property. | |
6933 * | |
6934 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
6935 * updates to the set of selectable items after updates to the DOM list and | |
6936 * selection state have been made. | |
6937 * | |
6938 * WARNING: If you are using this method, you should probably consider an | |
6939 * alternate approach. Synchronously querying for items is potentially | |
6940 * slow for many use cases. The `items` property will update asynchronously | |
6941 * on its own to reflect selectable items in the DOM. | |
6942 */ | |
6943 forceSynchronousItemUpdate: function() { | |
6944 this._updateItems(); | |
6945 }, | |
6946 | |
6947 get _shouldUpdateSelection() { | |
6948 return this.selected != null; | |
6949 }, | |
6950 | |
6951 _checkFallback: function() { | |
6952 if (this._shouldUpdateSelection) { | 3909 if (this._shouldUpdateSelection) { |
6953 this._updateSelected(); | 3910 this._updateSelected(); |
6954 } | 3911 } |
6955 }, | 3912 this.fire('iron-items-changed', mutation, { |
6956 | 3913 bubbles: false, |
6957 _addListener: function(eventName) { | 3914 cancelable: false |
6958 this.listen(this, eventName, '_activateHandler'); | 3915 }); |
6959 }, | 3916 }); |
6960 | 3917 }, |
6961 _removeListener: function(eventName) { | 3918 _activateHandler: function(e) { |
6962 this.unlisten(this, eventName, '_activateHandler'); | 3919 var t = e.target; |
6963 }, | 3920 var items = this.items; |
6964 | 3921 while (t && t != this) { |
6965 _activateEventChanged: function(eventName, old) { | 3922 var i = items.indexOf(t); |
6966 this._removeListener(old); | 3923 if (i >= 0) { |
6967 this._addListener(eventName); | 3924 var value = this._indexToValue(i); |
6968 }, | 3925 this._itemActivate(value, t); |
6969 | 3926 return; |
6970 _updateItems: function() { | 3927 } |
6971 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | 3928 t = t.parentNode; |
6972 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | 3929 } |
6973 this._setItems(nodes); | 3930 }, |
6974 }, | 3931 _itemActivate: function(value, item) { |
6975 | 3932 if (!this.fire('iron-activate', { |
6976 _updateAttrForSelected: function() { | 3933 selected: value, |
6977 if (this._shouldUpdateSelection) { | 3934 item: item |
6978 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | 3935 }, { |
6979 } | 3936 cancelable: true |
6980 }, | 3937 }).defaultPrevented) { |
6981 | 3938 this.select(value); |
6982 _updateSelected: function() { | 3939 } |
| 3940 } |
| 3941 }; |
| 3942 |
| 3943 Polymer.IronMultiSelectableBehaviorImpl = { |
| 3944 properties: { |
| 3945 multi: { |
| 3946 type: Boolean, |
| 3947 value: false, |
| 3948 observer: 'multiChanged' |
| 3949 }, |
| 3950 selectedValues: { |
| 3951 type: Array, |
| 3952 notify: true |
| 3953 }, |
| 3954 selectedItems: { |
| 3955 type: Array, |
| 3956 readOnly: true, |
| 3957 notify: true |
| 3958 } |
| 3959 }, |
| 3960 observers: [ '_updateSelected(selectedValues.splices)' ], |
| 3961 select: function(value) { |
| 3962 if (this.multi) { |
| 3963 if (this.selectedValues) { |
| 3964 this._toggleSelected(value); |
| 3965 } else { |
| 3966 this.selectedValues = [ value ]; |
| 3967 } |
| 3968 } else { |
| 3969 this.selected = value; |
| 3970 } |
| 3971 }, |
| 3972 multiChanged: function(multi) { |
| 3973 this._selection.multi = multi; |
| 3974 }, |
| 3975 get _shouldUpdateSelection() { |
| 3976 return this.selected != null || this.selectedValues != null && this.selected
Values.length; |
| 3977 }, |
| 3978 _updateAttrForSelected: function() { |
| 3979 if (!this.multi) { |
| 3980 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 3981 } else if (this._shouldUpdateSelection) { |
| 3982 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 3983 return this._indexToValue(this.indexOf(selectedItem)); |
| 3984 }, this).filter(function(unfilteredValue) { |
| 3985 return unfilteredValue != null; |
| 3986 }, this); |
| 3987 } |
| 3988 }, |
| 3989 _updateSelected: function() { |
| 3990 if (this.multi) { |
| 3991 this._selectMulti(this.selectedValues); |
| 3992 } else { |
6983 this._selectSelected(this.selected); | 3993 this._selectSelected(this.selected); |
6984 }, | 3994 } |
6985 | 3995 }, |
6986 _selectSelected: function(selected) { | 3996 _selectMulti: function(values) { |
6987 this._selection.select(this._valueToItem(this.selected)); | 3997 if (values) { |
6988 // Check for items, since this array is populated only when attached | 3998 var selectedItems = this._valuesToItems(values); |
6989 // Since Number(0) is falsy, explicitly check for undefined | 3999 this._selection.clear(selectedItems); |
6990 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | 4000 for (var i = 0; i < selectedItems.length; i++) { |
6991 this.selected = this.fallbackSelection; | 4001 this._selection.setItemSelected(selectedItems[i], true); |
6992 } | 4002 } |
6993 }, | 4003 if (this.fallbackSelection && this.items.length && !this._selection.get().
length) { |
6994 | 4004 var fallback = this._valueToItem(this.fallbackSelection); |
6995 _filterItem: function(node) { | 4005 if (fallback) { |
6996 return !this._excludedLocalNames[node.localName]; | 4006 this.selectedValues = [ this.fallbackSelection ]; |
6997 }, | |
6998 | |
6999 _valueToItem: function(value) { | |
7000 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
7001 }, | |
7002 | |
7003 _valueToIndex: function(value) { | |
7004 if (this.attrForSelected) { | |
7005 for (var i = 0, item; item = this.items[i]; i++) { | |
7006 if (this._valueForItem(item) == value) { | |
7007 return i; | |
7008 } | |
7009 } | 4007 } |
| 4008 } |
| 4009 } else { |
| 4010 this._selection.clear(); |
| 4011 } |
| 4012 }, |
| 4013 _selectionChange: function() { |
| 4014 var s = this._selection.get(); |
| 4015 if (this.multi) { |
| 4016 this._setSelectedItems(s); |
| 4017 } else { |
| 4018 this._setSelectedItems([ s ]); |
| 4019 this._setSelectedItem(s); |
| 4020 } |
| 4021 }, |
| 4022 _toggleSelected: function(value) { |
| 4023 var i = this.selectedValues.indexOf(value); |
| 4024 var unselected = i < 0; |
| 4025 if (unselected) { |
| 4026 this.push('selectedValues', value); |
| 4027 } else { |
| 4028 this.splice('selectedValues', i, 1); |
| 4029 } |
| 4030 }, |
| 4031 _valuesToItems: function(values) { |
| 4032 return values == null ? null : values.map(function(value) { |
| 4033 return this._valueToItem(value); |
| 4034 }, this); |
| 4035 } |
| 4036 }; |
| 4037 |
| 4038 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer.
IronMultiSelectableBehaviorImpl ]; |
| 4039 |
| 4040 Polymer.IronMenuBehaviorImpl = { |
| 4041 properties: { |
| 4042 focusedItem: { |
| 4043 observer: '_focusedItemChanged', |
| 4044 readOnly: true, |
| 4045 type: Object |
| 4046 }, |
| 4047 attrForItemTitle: { |
| 4048 type: String |
| 4049 } |
| 4050 }, |
| 4051 hostAttributes: { |
| 4052 role: 'menu', |
| 4053 tabindex: '0' |
| 4054 }, |
| 4055 observers: [ '_updateMultiselectable(multi)' ], |
| 4056 listeners: { |
| 4057 focus: '_onFocus', |
| 4058 keydown: '_onKeydown', |
| 4059 'iron-items-changed': '_onIronItemsChanged' |
| 4060 }, |
| 4061 keyBindings: { |
| 4062 up: '_onUpKey', |
| 4063 down: '_onDownKey', |
| 4064 esc: '_onEscKey', |
| 4065 'shift+tab:keydown': '_onShiftTabDown' |
| 4066 }, |
| 4067 attached: function() { |
| 4068 this._resetTabindices(); |
| 4069 }, |
| 4070 select: function(value) { |
| 4071 if (this._defaultFocusAsync) { |
| 4072 this.cancelAsync(this._defaultFocusAsync); |
| 4073 this._defaultFocusAsync = null; |
| 4074 } |
| 4075 var item = this._valueToItem(value); |
| 4076 if (item && item.hasAttribute('disabled')) return; |
| 4077 this._setFocusedItem(item); |
| 4078 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| 4079 }, |
| 4080 _resetTabindices: function() { |
| 4081 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0]
: this.selectedItem; |
| 4082 this.items.forEach(function(item) { |
| 4083 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| 4084 }, this); |
| 4085 }, |
| 4086 _updateMultiselectable: function(multi) { |
| 4087 if (multi) { |
| 4088 this.setAttribute('aria-multiselectable', 'true'); |
| 4089 } else { |
| 4090 this.removeAttribute('aria-multiselectable'); |
| 4091 } |
| 4092 }, |
| 4093 _focusWithKeyboardEvent: function(event) { |
| 4094 for (var i = 0, item; item = this.items[i]; i++) { |
| 4095 var attr = this.attrForItemTitle || 'textContent'; |
| 4096 var title = item[attr] || item.getAttribute(attr); |
| 4097 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo
werCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| 4098 this._setFocusedItem(item); |
| 4099 break; |
| 4100 } |
| 4101 } |
| 4102 }, |
| 4103 _focusPrevious: function() { |
| 4104 var length = this.items.length; |
| 4105 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4106 for (var i = 1; i < length + 1; i++) { |
| 4107 var item = this.items[(curFocusIndex - i + length) % length]; |
| 4108 if (!item.hasAttribute('disabled')) { |
| 4109 this._setFocusedItem(item); |
| 4110 return; |
| 4111 } |
| 4112 } |
| 4113 }, |
| 4114 _focusNext: function() { |
| 4115 var length = this.items.length; |
| 4116 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4117 for (var i = 1; i < length + 1; i++) { |
| 4118 var item = this.items[(curFocusIndex + i) % length]; |
| 4119 if (!item.hasAttribute('disabled')) { |
| 4120 this._setFocusedItem(item); |
| 4121 return; |
| 4122 } |
| 4123 } |
| 4124 }, |
| 4125 _applySelection: function(item, isSelected) { |
| 4126 if (isSelected) { |
| 4127 item.setAttribute('aria-selected', 'true'); |
| 4128 } else { |
| 4129 item.removeAttribute('aria-selected'); |
| 4130 } |
| 4131 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| 4132 }, |
| 4133 _focusedItemChanged: function(focusedItem, old) { |
| 4134 old && old.setAttribute('tabindex', '-1'); |
| 4135 if (focusedItem) { |
| 4136 focusedItem.setAttribute('tabindex', '0'); |
| 4137 focusedItem.focus(); |
| 4138 } |
| 4139 }, |
| 4140 _onIronItemsChanged: function(event) { |
| 4141 if (event.detail.addedNodes.length) { |
| 4142 this._resetTabindices(); |
| 4143 } |
| 4144 }, |
| 4145 _onShiftTabDown: function(event) { |
| 4146 var oldTabIndex = this.getAttribute('tabindex'); |
| 4147 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| 4148 this._setFocusedItem(null); |
| 4149 this.setAttribute('tabindex', '-1'); |
| 4150 this.async(function() { |
| 4151 this.setAttribute('tabindex', oldTabIndex); |
| 4152 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4153 }, 1); |
| 4154 }, |
| 4155 _onFocus: function(event) { |
| 4156 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| 4157 return; |
| 4158 } |
| 4159 var rootTarget = Polymer.dom(event).rootTarget; |
| 4160 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th
is.isLightDescendant(rootTarget)) { |
| 4161 return; |
| 4162 } |
| 4163 this._defaultFocusAsync = this.async(function() { |
| 4164 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0
] : this.selectedItem; |
| 4165 this._setFocusedItem(null); |
| 4166 if (selectedItem) { |
| 4167 this._setFocusedItem(selectedItem); |
| 4168 } else if (this.items[0]) { |
| 4169 this._focusNext(); |
| 4170 } |
| 4171 }); |
| 4172 }, |
| 4173 _onUpKey: function(event) { |
| 4174 this._focusPrevious(); |
| 4175 event.detail.keyboardEvent.preventDefault(); |
| 4176 }, |
| 4177 _onDownKey: function(event) { |
| 4178 this._focusNext(); |
| 4179 event.detail.keyboardEvent.preventDefault(); |
| 4180 }, |
| 4181 _onEscKey: function(event) { |
| 4182 this.focusedItem.blur(); |
| 4183 }, |
| 4184 _onKeydown: function(event) { |
| 4185 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| 4186 this._focusWithKeyboardEvent(event); |
| 4187 } |
| 4188 event.stopPropagation(); |
| 4189 }, |
| 4190 _activateHandler: function(event) { |
| 4191 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| 4192 event.stopPropagation(); |
| 4193 } |
| 4194 }; |
| 4195 |
| 4196 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4197 |
| 4198 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1
1yKeysBehavior, Polymer.IronMenuBehaviorImpl ]; |
| 4199 |
| 4200 (function() { |
| 4201 Polymer({ |
| 4202 is: 'paper-menu', |
| 4203 behaviors: [ Polymer.IronMenuBehavior ] |
| 4204 }); |
| 4205 })(); |
| 4206 |
| 4207 Polymer.IronFitBehavior = { |
| 4208 properties: { |
| 4209 sizingTarget: { |
| 4210 type: Object, |
| 4211 value: function() { |
| 4212 return this; |
| 4213 } |
| 4214 }, |
| 4215 fitInto: { |
| 4216 type: Object, |
| 4217 value: window |
| 4218 }, |
| 4219 noOverlap: { |
| 4220 type: Boolean |
| 4221 }, |
| 4222 positionTarget: { |
| 4223 type: Element |
| 4224 }, |
| 4225 horizontalAlign: { |
| 4226 type: String |
| 4227 }, |
| 4228 verticalAlign: { |
| 4229 type: String |
| 4230 }, |
| 4231 dynamicAlign: { |
| 4232 type: Boolean |
| 4233 }, |
| 4234 horizontalOffset: { |
| 4235 type: Number, |
| 4236 value: 0, |
| 4237 notify: true |
| 4238 }, |
| 4239 verticalOffset: { |
| 4240 type: Number, |
| 4241 value: 0, |
| 4242 notify: true |
| 4243 }, |
| 4244 autoFitOnAttach: { |
| 4245 type: Boolean, |
| 4246 value: false |
| 4247 }, |
| 4248 _fitInfo: { |
| 4249 type: Object |
| 4250 } |
| 4251 }, |
| 4252 get _fitWidth() { |
| 4253 var fitWidth; |
| 4254 if (this.fitInto === window) { |
| 4255 fitWidth = this.fitInto.innerWidth; |
| 4256 } else { |
| 4257 fitWidth = this.fitInto.getBoundingClientRect().width; |
| 4258 } |
| 4259 return fitWidth; |
| 4260 }, |
| 4261 get _fitHeight() { |
| 4262 var fitHeight; |
| 4263 if (this.fitInto === window) { |
| 4264 fitHeight = this.fitInto.innerHeight; |
| 4265 } else { |
| 4266 fitHeight = this.fitInto.getBoundingClientRect().height; |
| 4267 } |
| 4268 return fitHeight; |
| 4269 }, |
| 4270 get _fitLeft() { |
| 4271 var fitLeft; |
| 4272 if (this.fitInto === window) { |
| 4273 fitLeft = 0; |
| 4274 } else { |
| 4275 fitLeft = this.fitInto.getBoundingClientRect().left; |
| 4276 } |
| 4277 return fitLeft; |
| 4278 }, |
| 4279 get _fitTop() { |
| 4280 var fitTop; |
| 4281 if (this.fitInto === window) { |
| 4282 fitTop = 0; |
| 4283 } else { |
| 4284 fitTop = this.fitInto.getBoundingClientRect().top; |
| 4285 } |
| 4286 return fitTop; |
| 4287 }, |
| 4288 get _defaultPositionTarget() { |
| 4289 var parent = Polymer.dom(this).parentNode; |
| 4290 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 4291 parent = parent.host; |
| 4292 } |
| 4293 return parent; |
| 4294 }, |
| 4295 get _localeHorizontalAlign() { |
| 4296 if (this._isRTL) { |
| 4297 if (this.horizontalAlign === 'right') { |
| 4298 return 'left'; |
| 4299 } |
| 4300 if (this.horizontalAlign === 'left') { |
| 4301 return 'right'; |
| 4302 } |
| 4303 } |
| 4304 return this.horizontalAlign; |
| 4305 }, |
| 4306 attached: function() { |
| 4307 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
| 4308 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
| 4309 if (this.autoFitOnAttach) { |
| 4310 if (window.getComputedStyle(this).display === 'none') { |
| 4311 setTimeout(function() { |
| 4312 this.fit(); |
| 4313 }.bind(this)); |
7010 } else { | 4314 } else { |
7011 return Number(value); | 4315 this.fit(); |
7012 } | 4316 } |
7013 }, | 4317 } |
7014 | 4318 }, |
7015 _indexToValue: function(index) { | 4319 fit: function() { |
7016 if (this.attrForSelected) { | 4320 this.position(); |
7017 var item = this.items[index]; | 4321 this.constrain(); |
7018 if (item) { | 4322 this.center(); |
7019 return this._valueForItem(item); | 4323 }, |
| 4324 _discoverInfo: function() { |
| 4325 if (this._fitInfo) { |
| 4326 return; |
| 4327 } |
| 4328 var target = window.getComputedStyle(this); |
| 4329 var sizer = window.getComputedStyle(this.sizingTarget); |
| 4330 this._fitInfo = { |
| 4331 inlineStyle: { |
| 4332 top: this.style.top || '', |
| 4333 left: this.style.left || '', |
| 4334 position: this.style.position || '' |
| 4335 }, |
| 4336 sizerInlineStyle: { |
| 4337 maxWidth: this.sizingTarget.style.maxWidth || '', |
| 4338 maxHeight: this.sizingTarget.style.maxHeight || '', |
| 4339 boxSizing: this.sizingTarget.style.boxSizing || '' |
| 4340 }, |
| 4341 positionedBy: { |
| 4342 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? '
bottom' : null, |
| 4343 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto'
? 'right' : null |
| 4344 }, |
| 4345 sizedBy: { |
| 4346 height: sizer.maxHeight !== 'none', |
| 4347 width: sizer.maxWidth !== 'none', |
| 4348 minWidth: parseInt(sizer.minWidth, 10) || 0, |
| 4349 minHeight: parseInt(sizer.minHeight, 10) || 0 |
| 4350 }, |
| 4351 margin: { |
| 4352 top: parseInt(target.marginTop, 10) || 0, |
| 4353 right: parseInt(target.marginRight, 10) || 0, |
| 4354 bottom: parseInt(target.marginBottom, 10) || 0, |
| 4355 left: parseInt(target.marginLeft, 10) || 0 |
| 4356 } |
| 4357 }; |
| 4358 if (this.verticalOffset) { |
| 4359 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs
et; |
| 4360 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
| 4361 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
| 4362 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px
'; |
| 4363 } |
| 4364 if (this.horizontalOffset) { |
| 4365 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf
fset; |
| 4366 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
| 4367 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
| 4368 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + '
px'; |
| 4369 } |
| 4370 }, |
| 4371 resetFit: function() { |
| 4372 var info = this._fitInfo || {}; |
| 4373 for (var property in info.sizerInlineStyle) { |
| 4374 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
| 4375 } |
| 4376 for (var property in info.inlineStyle) { |
| 4377 this.style[property] = info.inlineStyle[property]; |
| 4378 } |
| 4379 this._fitInfo = null; |
| 4380 }, |
| 4381 refit: function() { |
| 4382 var scrollLeft = this.sizingTarget.scrollLeft; |
| 4383 var scrollTop = this.sizingTarget.scrollTop; |
| 4384 this.resetFit(); |
| 4385 this.fit(); |
| 4386 this.sizingTarget.scrollLeft = scrollLeft; |
| 4387 this.sizingTarget.scrollTop = scrollTop; |
| 4388 }, |
| 4389 position: function() { |
| 4390 if (!this.horizontalAlign && !this.verticalAlign) { |
| 4391 return; |
| 4392 } |
| 4393 this._discoverInfo(); |
| 4394 this.style.position = 'fixed'; |
| 4395 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4396 this.style.left = '0px'; |
| 4397 this.style.top = '0px'; |
| 4398 var rect = this.getBoundingClientRect(); |
| 4399 var positionRect = this.__getNormalizedRect(this.positionTarget); |
| 4400 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4401 var margin = this._fitInfo.margin; |
| 4402 var size = { |
| 4403 width: rect.width + margin.left + margin.right, |
| 4404 height: rect.height + margin.top + margin.bottom |
| 4405 }; |
| 4406 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical
Align, size, positionRect, fitRect); |
| 4407 var left = position.left + margin.left; |
| 4408 var top = position.top + margin.top; |
| 4409 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
| 4410 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
| 4411 var minWidth = this._fitInfo.sizedBy.minWidth; |
| 4412 var minHeight = this._fitInfo.sizedBy.minHeight; |
| 4413 if (left < margin.left) { |
| 4414 left = margin.left; |
| 4415 if (right - left < minWidth) { |
| 4416 left = right - minWidth; |
| 4417 } |
| 4418 } |
| 4419 if (top < margin.top) { |
| 4420 top = margin.top; |
| 4421 if (bottom - top < minHeight) { |
| 4422 top = bottom - minHeight; |
| 4423 } |
| 4424 } |
| 4425 this.sizingTarget.style.maxWidth = right - left + 'px'; |
| 4426 this.sizingTarget.style.maxHeight = bottom - top + 'px'; |
| 4427 this.style.left = left - rect.left + 'px'; |
| 4428 this.style.top = top - rect.top + 'px'; |
| 4429 }, |
| 4430 constrain: function() { |
| 4431 if (this.horizontalAlign || this.verticalAlign) { |
| 4432 return; |
| 4433 } |
| 4434 this._discoverInfo(); |
| 4435 var info = this._fitInfo; |
| 4436 if (!info.positionedBy.vertically) { |
| 4437 this.style.position = 'fixed'; |
| 4438 this.style.top = '0px'; |
| 4439 } |
| 4440 if (!info.positionedBy.horizontally) { |
| 4441 this.style.position = 'fixed'; |
| 4442 this.style.left = '0px'; |
| 4443 } |
| 4444 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4445 var rect = this.getBoundingClientRect(); |
| 4446 if (!info.sizedBy.height) { |
| 4447 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom',
'Height'); |
| 4448 } |
| 4449 if (!info.sizedBy.width) { |
| 4450 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right'
, 'Width'); |
| 4451 } |
| 4452 }, |
| 4453 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4454 this.__sizeDimension(rect, positionedBy, start, end, extent); |
| 4455 }, |
| 4456 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4457 var info = this._fitInfo; |
| 4458 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4459 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
| 4460 var flip = positionedBy === end; |
| 4461 var offset = flip ? max - rect[end] : rect[start]; |
| 4462 var margin = info.margin[flip ? start : end]; |
| 4463 var offsetExtent = 'offset' + extent; |
| 4464 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
| 4465 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs
et + 'px'; |
| 4466 }, |
| 4467 center: function() { |
| 4468 if (this.horizontalAlign || this.verticalAlign) { |
| 4469 return; |
| 4470 } |
| 4471 this._discoverInfo(); |
| 4472 var positionedBy = this._fitInfo.positionedBy; |
| 4473 if (positionedBy.vertically && positionedBy.horizontally) { |
| 4474 return; |
| 4475 } |
| 4476 this.style.position = 'fixed'; |
| 4477 if (!positionedBy.vertically) { |
| 4478 this.style.top = '0px'; |
| 4479 } |
| 4480 if (!positionedBy.horizontally) { |
| 4481 this.style.left = '0px'; |
| 4482 } |
| 4483 var rect = this.getBoundingClientRect(); |
| 4484 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4485 if (!positionedBy.vertically) { |
| 4486 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
| 4487 this.style.top = top + 'px'; |
| 4488 } |
| 4489 if (!positionedBy.horizontally) { |
| 4490 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
| 4491 this.style.left = left + 'px'; |
| 4492 } |
| 4493 }, |
| 4494 __getNormalizedRect: function(target) { |
| 4495 if (target === document.documentElement || target === window) { |
| 4496 return { |
| 4497 top: 0, |
| 4498 left: 0, |
| 4499 width: window.innerWidth, |
| 4500 height: window.innerHeight, |
| 4501 right: window.innerWidth, |
| 4502 bottom: window.innerHeight |
| 4503 }; |
| 4504 } |
| 4505 return target.getBoundingClientRect(); |
| 4506 }, |
| 4507 __getCroppedArea: function(position, size, fitRect) { |
| 4508 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom -
(position.top + size.height)); |
| 4509 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right
- (position.left + size.width)); |
| 4510 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size
.height; |
| 4511 }, |
| 4512 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
| 4513 var positions = [ { |
| 4514 verticalAlign: 'top', |
| 4515 horizontalAlign: 'left', |
| 4516 top: positionRect.top, |
| 4517 left: positionRect.left |
| 4518 }, { |
| 4519 verticalAlign: 'top', |
| 4520 horizontalAlign: 'right', |
| 4521 top: positionRect.top, |
| 4522 left: positionRect.right - size.width |
| 4523 }, { |
| 4524 verticalAlign: 'bottom', |
| 4525 horizontalAlign: 'left', |
| 4526 top: positionRect.bottom - size.height, |
| 4527 left: positionRect.left |
| 4528 }, { |
| 4529 verticalAlign: 'bottom', |
| 4530 horizontalAlign: 'right', |
| 4531 top: positionRect.bottom - size.height, |
| 4532 left: positionRect.right - size.width |
| 4533 } ]; |
| 4534 if (this.noOverlap) { |
| 4535 for (var i = 0, l = positions.length; i < l; i++) { |
| 4536 var copy = {}; |
| 4537 for (var key in positions[i]) { |
| 4538 copy[key] = positions[i][key]; |
7020 } | 4539 } |
7021 } else { | 4540 positions.push(copy); |
7022 return index; | 4541 } |
7023 } | 4542 positions[0].top = positions[1].top += positionRect.height; |
7024 }, | 4543 positions[2].top = positions[3].top -= positionRect.height; |
7025 | 4544 positions[4].left = positions[6].left += positionRect.width; |
7026 _valueForItem: function(item) { | 4545 positions[5].left = positions[7].left -= positionRect.width; |
7027 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | 4546 } |
7028 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | 4547 vAlign = vAlign === 'auto' ? null : vAlign; |
7029 }, | 4548 hAlign = hAlign === 'auto' ? null : hAlign; |
7030 | 4549 var position; |
7031 _applySelection: function(item, isSelected) { | 4550 for (var i = 0; i < positions.length; i++) { |
7032 if (this.selectedClass) { | 4551 var pos = positions[i]; |
7033 this.toggleClass(this.selectedClass, isSelected, item); | 4552 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign
&& pos.horizontalAlign === hAlign) { |
7034 } | 4553 position = pos; |
7035 if (this.selectedAttribute) { | 4554 break; |
7036 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 4555 } |
7037 } | 4556 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos
.horizontalAlign === hAlign); |
7038 this._selectionChange(); | 4557 if (!this.dynamicAlign && !alignOk) { |
7039 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | 4558 continue; |
7040 }, | 4559 } |
7041 | 4560 position = position || pos; |
7042 _selectionChange: function() { | 4561 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
7043 this._setSelectedItem(this._selection.get()); | 4562 var diff = pos.croppedArea - position.croppedArea; |
7044 }, | 4563 if (diff < 0 || diff === 0 && alignOk) { |
7045 | 4564 position = pos; |
7046 // observe items change under the given node. | 4565 } |
7047 _observeItems: function(node) { | 4566 if (position.croppedArea === 0 && alignOk) { |
7048 return Polymer.dom(node).observeNodes(function(mutation) { | 4567 break; |
7049 this._updateItems(); | 4568 } |
7050 | 4569 } |
7051 if (this._shouldUpdateSelection) { | 4570 return position; |
7052 this._updateSelected(); | 4571 } |
7053 } | 4572 }; |
7054 | 4573 |
7055 // Let other interested parties know about the change so that | 4574 (function() { |
7056 // we don't have to recreate mutation observers everywhere. | 4575 'use strict'; |
7057 this.fire('iron-items-changed', mutation, { | 4576 Polymer({ |
7058 bubbles: false, | 4577 is: 'iron-overlay-backdrop', |
7059 cancelable: false | |
7060 }); | |
7061 }); | |
7062 }, | |
7063 | |
7064 _activateHandler: function(e) { | |
7065 var t = e.target; | |
7066 var items = this.items; | |
7067 while (t && t != this) { | |
7068 var i = items.indexOf(t); | |
7069 if (i >= 0) { | |
7070 var value = this._indexToValue(i); | |
7071 this._itemActivate(value, t); | |
7072 return; | |
7073 } | |
7074 t = t.parentNode; | |
7075 } | |
7076 }, | |
7077 | |
7078 _itemActivate: function(value, item) { | |
7079 if (!this.fire('iron-activate', | |
7080 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | |
7081 this.select(value); | |
7082 } | |
7083 } | |
7084 | |
7085 }; | |
7086 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
7087 Polymer.IronMultiSelectableBehaviorImpl = { | |
7088 properties: { | 4578 properties: { |
7089 | |
7090 /** | |
7091 * If true, multiple selections are allowed. | |
7092 */ | |
7093 multi: { | |
7094 type: Boolean, | |
7095 value: false, | |
7096 observer: 'multiChanged' | |
7097 }, | |
7098 | |
7099 /** | |
7100 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
7101 * is true. | |
7102 */ | |
7103 selectedValues: { | |
7104 type: Array, | |
7105 notify: true | |
7106 }, | |
7107 | |
7108 /** | |
7109 * Returns an array of currently selected items. | |
7110 */ | |
7111 selectedItems: { | |
7112 type: Array, | |
7113 readOnly: true, | |
7114 notify: true | |
7115 }, | |
7116 | |
7117 }, | |
7118 | |
7119 observers: [ | |
7120 '_updateSelected(selectedValues.splices)' | |
7121 ], | |
7122 | |
7123 /** | |
7124 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7125 * `value` will be toggled; otherwise the `value` will be selected. | |
7126 * | |
7127 * @method select | |
7128 * @param {string|number} value the value to select. | |
7129 */ | |
7130 select: function(value) { | |
7131 if (this.multi) { | |
7132 if (this.selectedValues) { | |
7133 this._toggleSelected(value); | |
7134 } else { | |
7135 this.selectedValues = [value]; | |
7136 } | |
7137 } else { | |
7138 this.selected = value; | |
7139 } | |
7140 }, | |
7141 | |
7142 multiChanged: function(multi) { | |
7143 this._selection.multi = multi; | |
7144 }, | |
7145 | |
7146 get _shouldUpdateSelection() { | |
7147 return this.selected != null || | |
7148 (this.selectedValues != null && this.selectedValues.length); | |
7149 }, | |
7150 | |
7151 _updateAttrForSelected: function() { | |
7152 if (!this.multi) { | |
7153 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
7154 } else if (this._shouldUpdateSelection) { | |
7155 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
7156 return this._indexToValue(this.indexOf(selectedItem)); | |
7157 }, this).filter(function(unfilteredValue) { | |
7158 return unfilteredValue != null; | |
7159 }, this); | |
7160 } | |
7161 }, | |
7162 | |
7163 _updateSelected: function() { | |
7164 if (this.multi) { | |
7165 this._selectMulti(this.selectedValues); | |
7166 } else { | |
7167 this._selectSelected(this.selected); | |
7168 } | |
7169 }, | |
7170 | |
7171 _selectMulti: function(values) { | |
7172 if (values) { | |
7173 var selectedItems = this._valuesToItems(values); | |
7174 // clear all but the current selected items | |
7175 this._selection.clear(selectedItems); | |
7176 // select only those not selected yet | |
7177 for (var i = 0; i < selectedItems.length; i++) { | |
7178 this._selection.setItemSelected(selectedItems[i], true); | |
7179 } | |
7180 // Check for items, since this array is populated only when attached | |
7181 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
7182 var fallback = this._valueToItem(this.fallbackSelection); | |
7183 if (fallback) { | |
7184 this.selectedValues = [this.fallbackSelection]; | |
7185 } | |
7186 } | |
7187 } else { | |
7188 this._selection.clear(); | |
7189 } | |
7190 }, | |
7191 | |
7192 _selectionChange: function() { | |
7193 var s = this._selection.get(); | |
7194 if (this.multi) { | |
7195 this._setSelectedItems(s); | |
7196 } else { | |
7197 this._setSelectedItems([s]); | |
7198 this._setSelectedItem(s); | |
7199 } | |
7200 }, | |
7201 | |
7202 _toggleSelected: function(value) { | |
7203 var i = this.selectedValues.indexOf(value); | |
7204 var unselected = i < 0; | |
7205 if (unselected) { | |
7206 this.push('selectedValues',value); | |
7207 } else { | |
7208 this.splice('selectedValues',i,1); | |
7209 } | |
7210 }, | |
7211 | |
7212 _valuesToItems: function(values) { | |
7213 return (values == null) ? null : values.map(function(value) { | |
7214 return this._valueToItem(value); | |
7215 }, this); | |
7216 } | |
7217 }; | |
7218 | |
7219 /** @polymerBehavior */ | |
7220 Polymer.IronMultiSelectableBehavior = [ | |
7221 Polymer.IronSelectableBehavior, | |
7222 Polymer.IronMultiSelectableBehaviorImpl | |
7223 ]; | |
7224 /** | |
7225 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
7226 * | |
7227 * @demo demo/index.html | |
7228 * @polymerBehavior Polymer.IronMenuBehavior | |
7229 */ | |
7230 Polymer.IronMenuBehaviorImpl = { | |
7231 | |
7232 properties: { | |
7233 | |
7234 /** | |
7235 * Returns the currently focused item. | |
7236 * @type {?Object} | |
7237 */ | |
7238 focusedItem: { | |
7239 observer: '_focusedItemChanged', | |
7240 readOnly: true, | |
7241 type: Object | |
7242 }, | |
7243 | |
7244 /** | |
7245 * The attribute to use on menu items to look up the item title. Typing th
e first | |
7246 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
7247 * will be used. | |
7248 */ | |
7249 attrForItemTitle: { | |
7250 type: String | |
7251 } | |
7252 }, | |
7253 | |
7254 hostAttributes: { | |
7255 'role': 'menu', | |
7256 'tabindex': '0' | |
7257 }, | |
7258 | |
7259 observers: [ | |
7260 '_updateMultiselectable(multi)' | |
7261 ], | |
7262 | |
7263 listeners: { | |
7264 'focus': '_onFocus', | |
7265 'keydown': '_onKeydown', | |
7266 'iron-items-changed': '_onIronItemsChanged' | |
7267 }, | |
7268 | |
7269 keyBindings: { | |
7270 'up': '_onUpKey', | |
7271 'down': '_onDownKey', | |
7272 'esc': '_onEscKey', | |
7273 'shift+tab:keydown': '_onShiftTabDown' | |
7274 }, | |
7275 | |
7276 attached: function() { | |
7277 this._resetTabindices(); | |
7278 }, | |
7279 | |
7280 /** | |
7281 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7282 * `value` will be toggled; otherwise the `value` will be selected. | |
7283 * | |
7284 * @param {string|number} value the value to select. | |
7285 */ | |
7286 select: function(value) { | |
7287 // Cancel automatically focusing a default item if the menu received focus | |
7288 // through a user action selecting a particular item. | |
7289 if (this._defaultFocusAsync) { | |
7290 this.cancelAsync(this._defaultFocusAsync); | |
7291 this._defaultFocusAsync = null; | |
7292 } | |
7293 var item = this._valueToItem(value); | |
7294 if (item && item.hasAttribute('disabled')) return; | |
7295 this._setFocusedItem(item); | |
7296 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | |
7297 }, | |
7298 | |
7299 /** | |
7300 * Resets all tabindex attributes to the appropriate value based on the | |
7301 * current selection state. The appropriate value is `0` (focusable) for | |
7302 * the default selected item, and `-1` (not keyboard focusable) for all | |
7303 * other items. | |
7304 */ | |
7305 _resetTabindices: function() { | |
7306 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | |
7307 | |
7308 this.items.forEach(function(item) { | |
7309 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | |
7310 }, this); | |
7311 }, | |
7312 | |
7313 /** | |
7314 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
7315 * multi-selectable. | |
7316 * | |
7317 * @param {boolean} multi True if the menu should be multi-selectable. | |
7318 */ | |
7319 _updateMultiselectable: function(multi) { | |
7320 if (multi) { | |
7321 this.setAttribute('aria-multiselectable', 'true'); | |
7322 } else { | |
7323 this.removeAttribute('aria-multiselectable'); | |
7324 } | |
7325 }, | |
7326 | |
7327 /** | |
7328 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
7329 * menu (if there is a relevant item, and it is possible to focus it). | |
7330 * | |
7331 * @param {KeyboardEvent} event A KeyboardEvent. | |
7332 */ | |
7333 _focusWithKeyboardEvent: function(event) { | |
7334 for (var i = 0, item; item = this.items[i]; i++) { | |
7335 var attr = this.attrForItemTitle || 'textContent'; | |
7336 var title = item[attr] || item.getAttribute(attr); | |
7337 | |
7338 if (!item.hasAttribute('disabled') && title && | |
7339 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | |
7340 this._setFocusedItem(item); | |
7341 break; | |
7342 } | |
7343 } | |
7344 }, | |
7345 | |
7346 /** | |
7347 * Focuses the previous item (relative to the currently focused item) in the | |
7348 * menu, disabled items will be skipped. | |
7349 * Loop until length + 1 to handle case of single item in menu. | |
7350 */ | |
7351 _focusPrevious: function() { | |
7352 var length = this.items.length; | |
7353 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7354 for (var i = 1; i < length + 1; i++) { | |
7355 var item = this.items[(curFocusIndex - i + length) % length]; | |
7356 if (!item.hasAttribute('disabled')) { | |
7357 this._setFocusedItem(item); | |
7358 return; | |
7359 } | |
7360 } | |
7361 }, | |
7362 | |
7363 /** | |
7364 * Focuses the next item (relative to the currently focused item) in the | |
7365 * menu, disabled items will be skipped. | |
7366 * Loop until length + 1 to handle case of single item in menu. | |
7367 */ | |
7368 _focusNext: function() { | |
7369 var length = this.items.length; | |
7370 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7371 for (var i = 1; i < length + 1; i++) { | |
7372 var item = this.items[(curFocusIndex + i) % length]; | |
7373 if (!item.hasAttribute('disabled')) { | |
7374 this._setFocusedItem(item); | |
7375 return; | |
7376 } | |
7377 } | |
7378 }, | |
7379 | |
7380 /** | |
7381 * Mutates items in the menu based on provided selection details, so that | |
7382 * all items correctly reflect selection state. | |
7383 * | |
7384 * @param {Element} item An item in the menu. | |
7385 * @param {boolean} isSelected True if the item should be shown in a | |
7386 * selected state, otherwise false. | |
7387 */ | |
7388 _applySelection: function(item, isSelected) { | |
7389 if (isSelected) { | |
7390 item.setAttribute('aria-selected', 'true'); | |
7391 } else { | |
7392 item.removeAttribute('aria-selected'); | |
7393 } | |
7394 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | |
7395 }, | |
7396 | |
7397 /** | |
7398 * Discretely updates tabindex values among menu items as the focused item | |
7399 * changes. | |
7400 * | |
7401 * @param {Element} focusedItem The element that is currently focused. | |
7402 * @param {?Element} old The last element that was considered focused, if | |
7403 * applicable. | |
7404 */ | |
7405 _focusedItemChanged: function(focusedItem, old) { | |
7406 old && old.setAttribute('tabindex', '-1'); | |
7407 if (focusedItem) { | |
7408 focusedItem.setAttribute('tabindex', '0'); | |
7409 focusedItem.focus(); | |
7410 } | |
7411 }, | |
7412 | |
7413 /** | |
7414 * A handler that responds to mutation changes related to the list of items | |
7415 * in the menu. | |
7416 * | |
7417 * @param {CustomEvent} event An event containing mutation records as its | |
7418 * detail. | |
7419 */ | |
7420 _onIronItemsChanged: function(event) { | |
7421 if (event.detail.addedNodes.length) { | |
7422 this._resetTabindices(); | |
7423 } | |
7424 }, | |
7425 | |
7426 /** | |
7427 * Handler that is called when a shift+tab keypress is detected by the menu. | |
7428 * | |
7429 * @param {CustomEvent} event A key combination event. | |
7430 */ | |
7431 _onShiftTabDown: function(event) { | |
7432 var oldTabIndex = this.getAttribute('tabindex'); | |
7433 | |
7434 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
7435 | |
7436 this._setFocusedItem(null); | |
7437 | |
7438 this.setAttribute('tabindex', '-1'); | |
7439 | |
7440 this.async(function() { | |
7441 this.setAttribute('tabindex', oldTabIndex); | |
7442 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7443 // NOTE(cdata): polymer/polymer#1305 | |
7444 }, 1); | |
7445 }, | |
7446 | |
7447 /** | |
7448 * Handler that is called when the menu receives focus. | |
7449 * | |
7450 * @param {FocusEvent} event A focus event. | |
7451 */ | |
7452 _onFocus: function(event) { | |
7453 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
7454 // do not focus the menu itself | |
7455 return; | |
7456 } | |
7457 | |
7458 // Do not focus the selected tab if the deepest target is part of the | |
7459 // menu element's local DOM and is focusable. | |
7460 var rootTarget = /** @type {?HTMLElement} */( | |
7461 Polymer.dom(event).rootTarget); | |
7462 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
7463 return; | |
7464 } | |
7465 | |
7466 // clear the cached focus item | |
7467 this._defaultFocusAsync = this.async(function() { | |
7468 // focus the selected item when the menu receives focus, or the first it
em | |
7469 // if no item is selected | |
7470 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
7471 | |
7472 this._setFocusedItem(null); | |
7473 | |
7474 if (selectedItem) { | |
7475 this._setFocusedItem(selectedItem); | |
7476 } else if (this.items[0]) { | |
7477 // We find the first none-disabled item (if one exists) | |
7478 this._focusNext(); | |
7479 } | |
7480 }); | |
7481 }, | |
7482 | |
7483 /** | |
7484 * Handler that is called when the up key is pressed. | |
7485 * | |
7486 * @param {CustomEvent} event A key combination event. | |
7487 */ | |
7488 _onUpKey: function(event) { | |
7489 // up and down arrows moves the focus | |
7490 this._focusPrevious(); | |
7491 event.detail.keyboardEvent.preventDefault(); | |
7492 }, | |
7493 | |
7494 /** | |
7495 * Handler that is called when the down key is pressed. | |
7496 * | |
7497 * @param {CustomEvent} event A key combination event. | |
7498 */ | |
7499 _onDownKey: function(event) { | |
7500 this._focusNext(); | |
7501 event.detail.keyboardEvent.preventDefault(); | |
7502 }, | |
7503 | |
7504 /** | |
7505 * Handler that is called when the esc key is pressed. | |
7506 * | |
7507 * @param {CustomEvent} event A key combination event. | |
7508 */ | |
7509 _onEscKey: function(event) { | |
7510 // esc blurs the control | |
7511 this.focusedItem.blur(); | |
7512 }, | |
7513 | |
7514 /** | |
7515 * Handler that is called when a keydown event is detected. | |
7516 * | |
7517 * @param {KeyboardEvent} event A keyboard event. | |
7518 */ | |
7519 _onKeydown: function(event) { | |
7520 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
7521 // all other keys focus the menu item starting with that character | |
7522 this._focusWithKeyboardEvent(event); | |
7523 } | |
7524 event.stopPropagation(); | |
7525 }, | |
7526 | |
7527 // override _activateHandler | |
7528 _activateHandler: function(event) { | |
7529 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
7530 event.stopPropagation(); | |
7531 } | |
7532 }; | |
7533 | |
7534 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7535 | |
7536 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
7537 Polymer.IronMenuBehavior = [ | |
7538 Polymer.IronMultiSelectableBehavior, | |
7539 Polymer.IronA11yKeysBehavior, | |
7540 Polymer.IronMenuBehaviorImpl | |
7541 ]; | |
7542 (function() { | |
7543 Polymer({ | |
7544 is: 'paper-menu', | |
7545 | |
7546 behaviors: [ | |
7547 Polymer.IronMenuBehavior | |
7548 ] | |
7549 }); | |
7550 })(); | |
7551 /** | |
7552 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
7553 optionally centers it in the window or another element. | |
7554 | |
7555 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | |
7556 by CSS. | |
7557 | |
7558 CSS properties | Action | |
7559 -----------------------------|------------------------------------------- | |
7560 `position` set | Element is not centered horizontally or verticall
y | |
7561 `top` or `bottom` set | Element is not vertically centered | |
7562 `left` or `right` set | Element is not horizontally centered | |
7563 `max-height` set | Element respects `max-height` | |
7564 `max-width` set | Element respects `max-width` | |
7565 | |
7566 `Polymer.IronFitBehavior` can position an element into another element using | |
7567 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
7568 | |
7569 <div class="container"> | |
7570 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
7571 Positioned into the container | |
7572 </iron-fit-impl> | |
7573 </div> | |
7574 | |
7575 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
7576 | |
7577 <div class="container"> | |
7578 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
7579 Positioned around the container | |
7580 </iron-fit-impl> | |
7581 </div> | |
7582 | |
7583 @demo demo/index.html | |
7584 @polymerBehavior | |
7585 */ | |
7586 | |
7587 Polymer.IronFitBehavior = { | |
7588 | |
7589 properties: { | |
7590 | |
7591 /** | |
7592 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
7593 * but it can be set to a child element. This is useful, for example, for
implementing a | |
7594 * scrolling region inside the element. | |
7595 * @type {!Element} | |
7596 */ | |
7597 sizingTarget: { | |
7598 type: Object, | |
7599 value: function() { | |
7600 return this; | |
7601 } | |
7602 }, | |
7603 | |
7604 /** | |
7605 * The element to fit `this` into. | |
7606 */ | |
7607 fitInto: { | |
7608 type: Object, | |
7609 value: window | |
7610 }, | |
7611 | |
7612 /** | |
7613 * Will position the element around the positionTarget without overlapping
it. | |
7614 */ | |
7615 noOverlap: { | |
7616 type: Boolean | |
7617 }, | |
7618 | |
7619 /** | |
7620 * The element that should be used to position the element. If not set, it
will | |
7621 * default to the parent node. | |
7622 * @type {!Element} | |
7623 */ | |
7624 positionTarget: { | |
7625 type: Element | |
7626 }, | |
7627 | |
7628 /** | |
7629 * The orientation against which to align the element horizontally | |
7630 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
7631 */ | |
7632 horizontalAlign: { | |
7633 type: String | |
7634 }, | |
7635 | |
7636 /** | |
7637 * The orientation against which to align the element vertically | |
7638 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
7639 */ | |
7640 verticalAlign: { | |
7641 type: String | |
7642 }, | |
7643 | |
7644 /** | |
7645 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
7646 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
7647 */ | |
7648 dynamicAlign: { | |
7649 type: Boolean | |
7650 }, | |
7651 | |
7652 /** | |
7653 * The same as setting margin-left and margin-right css properties. | |
7654 * @deprecated | |
7655 */ | |
7656 horizontalOffset: { | |
7657 type: Number, | |
7658 value: 0, | |
7659 notify: true | |
7660 }, | |
7661 | |
7662 /** | |
7663 * The same as setting margin-top and margin-bottom css properties. | |
7664 * @deprecated | |
7665 */ | |
7666 verticalOffset: { | |
7667 type: Number, | |
7668 value: 0, | |
7669 notify: true | |
7670 }, | |
7671 | |
7672 /** | |
7673 * Set to true to auto-fit on attach. | |
7674 */ | |
7675 autoFitOnAttach: { | |
7676 type: Boolean, | |
7677 value: false | |
7678 }, | |
7679 | |
7680 /** @type {?Object} */ | |
7681 _fitInfo: { | |
7682 type: Object | |
7683 } | |
7684 }, | |
7685 | |
7686 get _fitWidth() { | |
7687 var fitWidth; | |
7688 if (this.fitInto === window) { | |
7689 fitWidth = this.fitInto.innerWidth; | |
7690 } else { | |
7691 fitWidth = this.fitInto.getBoundingClientRect().width; | |
7692 } | |
7693 return fitWidth; | |
7694 }, | |
7695 | |
7696 get _fitHeight() { | |
7697 var fitHeight; | |
7698 if (this.fitInto === window) { | |
7699 fitHeight = this.fitInto.innerHeight; | |
7700 } else { | |
7701 fitHeight = this.fitInto.getBoundingClientRect().height; | |
7702 } | |
7703 return fitHeight; | |
7704 }, | |
7705 | |
7706 get _fitLeft() { | |
7707 var fitLeft; | |
7708 if (this.fitInto === window) { | |
7709 fitLeft = 0; | |
7710 } else { | |
7711 fitLeft = this.fitInto.getBoundingClientRect().left; | |
7712 } | |
7713 return fitLeft; | |
7714 }, | |
7715 | |
7716 get _fitTop() { | |
7717 var fitTop; | |
7718 if (this.fitInto === window) { | |
7719 fitTop = 0; | |
7720 } else { | |
7721 fitTop = this.fitInto.getBoundingClientRect().top; | |
7722 } | |
7723 return fitTop; | |
7724 }, | |
7725 | |
7726 /** | |
7727 * The element that should be used to position the element, | |
7728 * if no position target is configured. | |
7729 */ | |
7730 get _defaultPositionTarget() { | |
7731 var parent = Polymer.dom(this).parentNode; | |
7732 | |
7733 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
7734 parent = parent.host; | |
7735 } | |
7736 | |
7737 return parent; | |
7738 }, | |
7739 | |
7740 /** | |
7741 * The horizontal align value, accounting for the RTL/LTR text direction. | |
7742 */ | |
7743 get _localeHorizontalAlign() { | |
7744 if (this._isRTL) { | |
7745 // In RTL, "left" becomes "right". | |
7746 if (this.horizontalAlign === 'right') { | |
7747 return 'left'; | |
7748 } | |
7749 if (this.horizontalAlign === 'left') { | |
7750 return 'right'; | |
7751 } | |
7752 } | |
7753 return this.horizontalAlign; | |
7754 }, | |
7755 | |
7756 attached: function() { | |
7757 // Memoize this to avoid expensive calculations & relayouts. | |
7758 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | |
7759 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | |
7760 if (this.autoFitOnAttach) { | |
7761 if (window.getComputedStyle(this).display === 'none') { | |
7762 setTimeout(function() { | |
7763 this.fit(); | |
7764 }.bind(this)); | |
7765 } else { | |
7766 this.fit(); | |
7767 } | |
7768 } | |
7769 }, | |
7770 | |
7771 /** | |
7772 * Positions and fits the element into the `fitInto` element. | |
7773 */ | |
7774 fit: function() { | |
7775 this.position(); | |
7776 this.constrain(); | |
7777 this.center(); | |
7778 }, | |
7779 | |
7780 /** | |
7781 * Memoize information needed to position and size the target element. | |
7782 * @suppress {deprecated} | |
7783 */ | |
7784 _discoverInfo: function() { | |
7785 if (this._fitInfo) { | |
7786 return; | |
7787 } | |
7788 var target = window.getComputedStyle(this); | |
7789 var sizer = window.getComputedStyle(this.sizingTarget); | |
7790 | |
7791 this._fitInfo = { | |
7792 inlineStyle: { | |
7793 top: this.style.top || '', | |
7794 left: this.style.left || '', | |
7795 position: this.style.position || '' | |
7796 }, | |
7797 sizerInlineStyle: { | |
7798 maxWidth: this.sizingTarget.style.maxWidth || '', | |
7799 maxHeight: this.sizingTarget.style.maxHeight || '', | |
7800 boxSizing: this.sizingTarget.style.boxSizing || '' | |
7801 }, | |
7802 positionedBy: { | |
7803 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto'
? | |
7804 'bottom' : null), | |
7805 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut
o' ? | |
7806 'right' : null) | |
7807 }, | |
7808 sizedBy: { | |
7809 height: sizer.maxHeight !== 'none', | |
7810 width: sizer.maxWidth !== 'none', | |
7811 minWidth: parseInt(sizer.minWidth, 10) || 0, | |
7812 minHeight: parseInt(sizer.minHeight, 10) || 0 | |
7813 }, | |
7814 margin: { | |
7815 top: parseInt(target.marginTop, 10) || 0, | |
7816 right: parseInt(target.marginRight, 10) || 0, | |
7817 bottom: parseInt(target.marginBottom, 10) || 0, | |
7818 left: parseInt(target.marginLeft, 10) || 0 | |
7819 } | |
7820 }; | |
7821 | |
7822 // Support these properties until they are removed. | |
7823 if (this.verticalOffset) { | |
7824 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | |
7825 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | |
7826 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | |
7827 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | |
7828 } | |
7829 if (this.horizontalOffset) { | |
7830 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | |
7831 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | |
7832 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | |
7833 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | |
7834 } | |
7835 }, | |
7836 | |
7837 /** | |
7838 * Resets the target element's position and size constraints, and clear | |
7839 * the memoized data. | |
7840 */ | |
7841 resetFit: function() { | |
7842 var info = this._fitInfo || {}; | |
7843 for (var property in info.sizerInlineStyle) { | |
7844 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | |
7845 } | |
7846 for (var property in info.inlineStyle) { | |
7847 this.style[property] = info.inlineStyle[property]; | |
7848 } | |
7849 | |
7850 this._fitInfo = null; | |
7851 }, | |
7852 | |
7853 /** | |
7854 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
7855 * the element or the `fitInto` element has been resized, or if any of the | |
7856 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
7857 * It preserves the scroll position of the sizingTarget. | |
7858 */ | |
7859 refit: function() { | |
7860 var scrollLeft = this.sizingTarget.scrollLeft; | |
7861 var scrollTop = this.sizingTarget.scrollTop; | |
7862 this.resetFit(); | |
7863 this.fit(); | |
7864 this.sizingTarget.scrollLeft = scrollLeft; | |
7865 this.sizingTarget.scrollTop = scrollTop; | |
7866 }, | |
7867 | |
7868 /** | |
7869 * Positions the element according to `horizontalAlign, verticalAlign`. | |
7870 */ | |
7871 position: function() { | |
7872 if (!this.horizontalAlign && !this.verticalAlign) { | |
7873 // needs to be centered, and it is done after constrain. | |
7874 return; | |
7875 } | |
7876 this._discoverInfo(); | |
7877 | |
7878 this.style.position = 'fixed'; | |
7879 // Need border-box for margin/padding. | |
7880 this.sizingTarget.style.boxSizing = 'border-box'; | |
7881 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
7882 this.style.left = '0px'; | |
7883 this.style.top = '0px'; | |
7884 | |
7885 var rect = this.getBoundingClientRect(); | |
7886 var positionRect = this.__getNormalizedRect(this.positionTarget); | |
7887 var fitRect = this.__getNormalizedRect(this.fitInto); | |
7888 | |
7889 var margin = this._fitInfo.margin; | |
7890 | |
7891 // Consider the margin as part of the size for position calculations. | |
7892 var size = { | |
7893 width: rect.width + margin.left + margin.right, | |
7894 height: rect.height + margin.top + margin.bottom | |
7895 }; | |
7896 | |
7897 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | |
7898 | |
7899 var left = position.left + margin.left; | |
7900 var top = position.top + margin.top; | |
7901 | |
7902 // Use original size (without margin). | |
7903 var right = Math.min(fitRect.right - margin.right, left + rect.width); | |
7904 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | |
7905 | |
7906 var minWidth = this._fitInfo.sizedBy.minWidth; | |
7907 var minHeight = this._fitInfo.sizedBy.minHeight; | |
7908 if (left < margin.left) { | |
7909 left = margin.left; | |
7910 if (right - left < minWidth) { | |
7911 left = right - minWidth; | |
7912 } | |
7913 } | |
7914 if (top < margin.top) { | |
7915 top = margin.top; | |
7916 if (bottom - top < minHeight) { | |
7917 top = bottom - minHeight; | |
7918 } | |
7919 } | |
7920 | |
7921 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | |
7922 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | |
7923 | |
7924 // Remove the offset caused by any stacking context. | |
7925 this.style.left = (left - rect.left) + 'px'; | |
7926 this.style.top = (top - rect.top) + 'px'; | |
7927 }, | |
7928 | |
7929 /** | |
7930 * Constrains the size of the element to `fitInto` by setting `max-height` | |
7931 * and/or `max-width`. | |
7932 */ | |
7933 constrain: function() { | |
7934 if (this.horizontalAlign || this.verticalAlign) { | |
7935 return; | |
7936 } | |
7937 this._discoverInfo(); | |
7938 | |
7939 var info = this._fitInfo; | |
7940 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
7941 if (!info.positionedBy.vertically) { | |
7942 this.style.position = 'fixed'; | |
7943 this.style.top = '0px'; | |
7944 } | |
7945 if (!info.positionedBy.horizontally) { | |
7946 this.style.position = 'fixed'; | |
7947 this.style.left = '0px'; | |
7948 } | |
7949 | |
7950 // need border-box for margin/padding | |
7951 this.sizingTarget.style.boxSizing = 'border-box'; | |
7952 // constrain the width and height if not already set | |
7953 var rect = this.getBoundingClientRect(); | |
7954 if (!info.sizedBy.height) { | |
7955 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | |
7956 } | |
7957 if (!info.sizedBy.width) { | |
7958 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | |
7959 } | |
7960 }, | |
7961 | |
7962 /** | |
7963 * @protected | |
7964 * @deprecated | |
7965 */ | |
7966 _sizeDimension: function(rect, positionedBy, start, end, extent) { | |
7967 this.__sizeDimension(rect, positionedBy, start, end, extent); | |
7968 }, | |
7969 | |
7970 /** | |
7971 * @private | |
7972 */ | |
7973 __sizeDimension: function(rect, positionedBy, start, end, extent) { | |
7974 var info = this._fitInfo; | |
7975 var fitRect = this.__getNormalizedRect(this.fitInto); | |
7976 var max = extent === 'Width' ? fitRect.width : fitRect.height; | |
7977 var flip = (positionedBy === end); | |
7978 var offset = flip ? max - rect[end] : rect[start]; | |
7979 var margin = info.margin[flip ? start : end]; | |
7980 var offsetExtent = 'offset' + extent; | |
7981 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | |
7982 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | |
7983 }, | |
7984 | |
7985 /** | |
7986 * Centers horizontally and vertically if not already positioned. This also
sets | |
7987 * `position:fixed`. | |
7988 */ | |
7989 center: function() { | |
7990 if (this.horizontalAlign || this.verticalAlign) { | |
7991 return; | |
7992 } | |
7993 this._discoverInfo(); | |
7994 | |
7995 var positionedBy = this._fitInfo.positionedBy; | |
7996 if (positionedBy.vertically && positionedBy.horizontally) { | |
7997 // Already positioned. | |
7998 return; | |
7999 } | |
8000 // Need position:fixed to center | |
8001 this.style.position = 'fixed'; | |
8002 // Take into account the offset caused by parents that create stacking | |
8003 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
8004 // measure the bounding rect. | |
8005 if (!positionedBy.vertically) { | |
8006 this.style.top = '0px'; | |
8007 } | |
8008 if (!positionedBy.horizontally) { | |
8009 this.style.left = '0px'; | |
8010 } | |
8011 // It will take in consideration margins and transforms | |
8012 var rect = this.getBoundingClientRect(); | |
8013 var fitRect = this.__getNormalizedRect(this.fitInto); | |
8014 if (!positionedBy.vertically) { | |
8015 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | |
8016 this.style.top = top + 'px'; | |
8017 } | |
8018 if (!positionedBy.horizontally) { | |
8019 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | |
8020 this.style.left = left + 'px'; | |
8021 } | |
8022 }, | |
8023 | |
8024 __getNormalizedRect: function(target) { | |
8025 if (target === document.documentElement || target === window) { | |
8026 return { | |
8027 top: 0, | |
8028 left: 0, | |
8029 width: window.innerWidth, | |
8030 height: window.innerHeight, | |
8031 right: window.innerWidth, | |
8032 bottom: window.innerHeight | |
8033 }; | |
8034 } | |
8035 return target.getBoundingClientRect(); | |
8036 }, | |
8037 | |
8038 __getCroppedArea: function(position, size, fitRect) { | |
8039 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | |
8040 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | |
8041 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | |
8042 }, | |
8043 | |
8044 | |
8045 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | |
8046 // All the possible configurations. | |
8047 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
8048 var positions = [{ | |
8049 verticalAlign: 'top', | |
8050 horizontalAlign: 'left', | |
8051 top: positionRect.top, | |
8052 left: positionRect.left | |
8053 }, { | |
8054 verticalAlign: 'top', | |
8055 horizontalAlign: 'right', | |
8056 top: positionRect.top, | |
8057 left: positionRect.right - size.width | |
8058 }, { | |
8059 verticalAlign: 'bottom', | |
8060 horizontalAlign: 'left', | |
8061 top: positionRect.bottom - size.height, | |
8062 left: positionRect.left | |
8063 }, { | |
8064 verticalAlign: 'bottom', | |
8065 horizontalAlign: 'right', | |
8066 top: positionRect.bottom - size.height, | |
8067 left: positionRect.right - size.width | |
8068 }]; | |
8069 | |
8070 if (this.noOverlap) { | |
8071 // Duplicate. | |
8072 for (var i = 0, l = positions.length; i < l; i++) { | |
8073 var copy = {}; | |
8074 for (var key in positions[i]) { | |
8075 copy[key] = positions[i][key]; | |
8076 } | |
8077 positions.push(copy); | |
8078 } | |
8079 // Horizontal overlap only. | |
8080 positions[0].top = positions[1].top += positionRect.height; | |
8081 positions[2].top = positions[3].top -= positionRect.height; | |
8082 // Vertical overlap only. | |
8083 positions[4].left = positions[6].left += positionRect.width; | |
8084 positions[5].left = positions[7].left -= positionRect.width; | |
8085 } | |
8086 | |
8087 // Consider auto as null for coding convenience. | |
8088 vAlign = vAlign === 'auto' ? null : vAlign; | |
8089 hAlign = hAlign === 'auto' ? null : hAlign; | |
8090 | |
8091 var position; | |
8092 for (var i = 0; i < positions.length; i++) { | |
8093 var pos = positions[i]; | |
8094 | |
8095 // If both vAlign and hAlign are defined, return exact match. | |
8096 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
8097 // we'll have to check the croppedArea to make the best choice. | |
8098 if (!this.dynamicAlign && !this.noOverlap && | |
8099 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | |
8100 position = pos; | |
8101 break; | |
8102 } | |
8103 | |
8104 // Align is ok if alignment preferences are respected. If no preferences
, | |
8105 // it is considered ok. | |
8106 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | |
8107 (!hAlign || pos.horizontalAlign === hAlign); | |
8108 | |
8109 // Filter out elements that don't match the alignment (if defined). | |
8110 // With dynamicAlign, we need to consider all the positions to find the | |
8111 // one that minimizes the cropped area. | |
8112 if (!this.dynamicAlign && !alignOk) { | |
8113 continue; | |
8114 } | |
8115 | |
8116 position = position || pos; | |
8117 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | |
8118 var diff = pos.croppedArea - position.croppedArea; | |
8119 // Check which crops less. If it crops equally, check if align is ok. | |
8120 if (diff < 0 || (diff === 0 && alignOk)) { | |
8121 position = pos; | |
8122 } | |
8123 // If not cropped and respects the align requirements, keep it. | |
8124 // This allows to prefer positions overlapping horizontally over the | |
8125 // ones overlapping vertically. | |
8126 if (position.croppedArea === 0 && alignOk) { | |
8127 break; | |
8128 } | |
8129 } | |
8130 | |
8131 return position; | |
8132 } | |
8133 | |
8134 }; | |
8135 (function() { | |
8136 'use strict'; | |
8137 | |
8138 Polymer({ | |
8139 | |
8140 is: 'iron-overlay-backdrop', | |
8141 | |
8142 properties: { | |
8143 | |
8144 /** | |
8145 * Returns true if the backdrop is opened. | |
8146 */ | |
8147 opened: { | 4579 opened: { |
8148 reflectToAttribute: true, | 4580 reflectToAttribute: true, |
8149 type: Boolean, | 4581 type: Boolean, |
8150 value: false, | 4582 value: false, |
8151 observer: '_openedChanged' | 4583 observer: '_openedChanged' |
8152 } | 4584 } |
8153 | 4585 }, |
8154 }, | |
8155 | |
8156 listeners: { | 4586 listeners: { |
8157 'transitionend': '_onTransitionend' | 4587 transitionend: '_onTransitionend' |
8158 }, | 4588 }, |
8159 | |
8160 created: function() { | 4589 created: function() { |
8161 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
8162 this.__openedRaf = null; | 4590 this.__openedRaf = null; |
8163 }, | 4591 }, |
8164 | |
8165 attached: function() { | 4592 attached: function() { |
8166 this.opened && this._openedChanged(this.opened); | 4593 this.opened && this._openedChanged(this.opened); |
8167 }, | 4594 }, |
8168 | |
8169 /** | |
8170 * Appends the backdrop to document body if needed. | |
8171 */ | |
8172 prepare: function() { | 4595 prepare: function() { |
8173 if (this.opened && !this.parentNode) { | 4596 if (this.opened && !this.parentNode) { |
8174 Polymer.dom(document.body).appendChild(this); | 4597 Polymer.dom(document.body).appendChild(this); |
8175 } | 4598 } |
8176 }, | 4599 }, |
8177 | |
8178 /** | |
8179 * Shows the backdrop. | |
8180 */ | |
8181 open: function() { | 4600 open: function() { |
8182 this.opened = true; | 4601 this.opened = true; |
8183 }, | 4602 }, |
8184 | |
8185 /** | |
8186 * Hides the backdrop. | |
8187 */ | |
8188 close: function() { | 4603 close: function() { |
8189 this.opened = false; | 4604 this.opened = false; |
8190 }, | 4605 }, |
8191 | |
8192 /** | |
8193 * Removes the backdrop from document body if needed. | |
8194 */ | |
8195 complete: function() { | 4606 complete: function() { |
8196 if (!this.opened && this.parentNode === document.body) { | 4607 if (!this.opened && this.parentNode === document.body) { |
8197 Polymer.dom(this.parentNode).removeChild(this); | 4608 Polymer.dom(this.parentNode).removeChild(this); |
8198 } | 4609 } |
8199 }, | 4610 }, |
8200 | |
8201 _onTransitionend: function(event) { | 4611 _onTransitionend: function(event) { |
8202 if (event && event.target === this) { | 4612 if (event && event.target === this) { |
8203 this.complete(); | 4613 this.complete(); |
8204 } | 4614 } |
8205 }, | 4615 }, |
8206 | |
8207 /** | |
8208 * @param {boolean} opened | |
8209 * @private | |
8210 */ | |
8211 _openedChanged: function(opened) { | 4616 _openedChanged: function(opened) { |
8212 if (opened) { | 4617 if (opened) { |
8213 // Auto-attach. | |
8214 this.prepare(); | 4618 this.prepare(); |
8215 } else { | 4619 } else { |
8216 // Animation might be disabled via the mixin or opacity custom property. | |
8217 // If it is disabled in other ways, it's up to the user to call complete
. | |
8218 var cs = window.getComputedStyle(this); | 4620 var cs = window.getComputedStyle(this); |
8219 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 4621 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
8220 this.complete(); | 4622 this.complete(); |
8221 } | 4623 } |
8222 } | 4624 } |
8223 | |
8224 if (!this.isAttached) { | 4625 if (!this.isAttached) { |
8225 return; | 4626 return; |
8226 } | 4627 } |
8227 | |
8228 // Always cancel previous requestAnimationFrame. | |
8229 if (this.__openedRaf) { | 4628 if (this.__openedRaf) { |
8230 window.cancelAnimationFrame(this.__openedRaf); | 4629 window.cancelAnimationFrame(this.__openedRaf); |
8231 this.__openedRaf = null; | 4630 this.__openedRaf = null; |
8232 } | 4631 } |
8233 // Force relayout to ensure proper transitions. | |
8234 this.scrollTop = this.scrollTop; | 4632 this.scrollTop = this.scrollTop; |
8235 this.__openedRaf = window.requestAnimationFrame(function() { | 4633 this.__openedRaf = window.requestAnimationFrame(function() { |
8236 this.__openedRaf = null; | 4634 this.__openedRaf = null; |
8237 this.toggleClass('opened', this.opened); | 4635 this.toggleClass('opened', this.opened); |
8238 }.bind(this)); | 4636 }.bind(this)); |
8239 } | 4637 } |
8240 }); | 4638 }); |
8241 | |
8242 })(); | 4639 })(); |
8243 /** | 4640 |
8244 * @struct | 4641 Polymer.IronOverlayManagerClass = function() { |
8245 * @constructor | 4642 this._overlays = []; |
8246 * @private | 4643 this._minimumZ = 101; |
8247 */ | 4644 this._backdropElement = null; |
8248 Polymer.IronOverlayManagerClass = function() { | 4645 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
8249 /** | 4646 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
8250 * Used to keep track of the opened overlays. | 4647 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
8251 * @private {Array<Element>} | 4648 }; |
8252 */ | 4649 |
8253 this._overlays = []; | 4650 Polymer.IronOverlayManagerClass.prototype = { |
8254 | 4651 constructor: Polymer.IronOverlayManagerClass, |
8255 /** | 4652 get backdropElement() { |
8256 * iframes have a default z-index of 100, | 4653 if (!this._backdropElement) { |
8257 * so this default should be at least that. | 4654 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
8258 * @private {number} | 4655 } |
8259 */ | 4656 return this._backdropElement; |
8260 this._minimumZ = 101; | 4657 }, |
8261 | 4658 get deepActiveElement() { |
8262 /** | 4659 var active = document.activeElement || document.body; |
8263 * Memoized backdrop element. | 4660 while (active.root && Polymer.dom(active.root).activeElement) { |
8264 * @private {Element|null} | 4661 active = Polymer.dom(active.root).activeElement; |
8265 */ | 4662 } |
8266 this._backdropElement = null; | 4663 return active; |
8267 | 4664 }, |
8268 // Enable document-wide tap recognizer. | 4665 _bringOverlayAtIndexToFront: function(i) { |
8269 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 4666 var overlay = this._overlays[i]; |
8270 | 4667 if (!overlay) { |
8271 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 4668 return; |
8272 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 4669 } |
8273 }; | 4670 var lastI = this._overlays.length - 1; |
8274 | 4671 var currentOverlay = this._overlays[lastI]; |
8275 Polymer.IronOverlayManagerClass.prototype = { | 4672 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8276 | 4673 lastI--; |
8277 constructor: Polymer.IronOverlayManagerClass, | 4674 } |
8278 | 4675 if (i >= lastI) { |
8279 /** | 4676 return; |
8280 * The shared backdrop element. | 4677 } |
8281 * @type {!Element} backdropElement | 4678 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
8282 */ | 4679 if (this._getZ(overlay) <= minimumZ) { |
8283 get backdropElement() { | 4680 this._applyOverlayZ(overlay, minimumZ); |
8284 if (!this._backdropElement) { | 4681 } |
8285 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 4682 while (i < lastI) { |
8286 } | 4683 this._overlays[i] = this._overlays[i + 1]; |
8287 return this._backdropElement; | 4684 i++; |
8288 }, | 4685 } |
8289 | 4686 this._overlays[lastI] = overlay; |
8290 /** | 4687 }, |
8291 * The deepest active element. | 4688 addOrRemoveOverlay: function(overlay) { |
8292 * @type {!Element} activeElement the active element | 4689 if (overlay.opened) { |
8293 */ | 4690 this.addOverlay(overlay); |
8294 get deepActiveElement() { | 4691 } else { |
8295 // document.activeElement can be null | 4692 this.removeOverlay(overlay); |
8296 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | 4693 } |
8297 // In case of null, default it to document.body. | 4694 }, |
8298 var active = document.activeElement || document.body; | 4695 addOverlay: function(overlay) { |
8299 while (active.root && Polymer.dom(active.root).activeElement) { | 4696 var i = this._overlays.indexOf(overlay); |
8300 active = Polymer.dom(active.root).activeElement; | 4697 if (i >= 0) { |
8301 } | 4698 this._bringOverlayAtIndexToFront(i); |
8302 return active; | |
8303 }, | |
8304 | |
8305 /** | |
8306 * Brings the overlay at the specified index to the front. | |
8307 * @param {number} i | |
8308 * @private | |
8309 */ | |
8310 _bringOverlayAtIndexToFront: function(i) { | |
8311 var overlay = this._overlays[i]; | |
8312 if (!overlay) { | |
8313 return; | |
8314 } | |
8315 var lastI = this._overlays.length - 1; | |
8316 var currentOverlay = this._overlays[lastI]; | |
8317 // Ensure always-on-top overlay stays on top. | |
8318 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8319 lastI--; | |
8320 } | |
8321 // If already the top element, return. | |
8322 if (i >= lastI) { | |
8323 return; | |
8324 } | |
8325 // Update z-index to be on top. | |
8326 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | |
8327 if (this._getZ(overlay) <= minimumZ) { | |
8328 this._applyOverlayZ(overlay, minimumZ); | |
8329 } | |
8330 | |
8331 // Shift other overlays behind the new on top. | |
8332 while (i < lastI) { | |
8333 this._overlays[i] = this._overlays[i + 1]; | |
8334 i++; | |
8335 } | |
8336 this._overlays[lastI] = overlay; | |
8337 }, | |
8338 | |
8339 /** | |
8340 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
8341 * Also updates the backdrop z-index. | |
8342 * @param {!Element} overlay | |
8343 */ | |
8344 addOrRemoveOverlay: function(overlay) { | |
8345 if (overlay.opened) { | |
8346 this.addOverlay(overlay); | |
8347 } else { | |
8348 this.removeOverlay(overlay); | |
8349 } | |
8350 }, | |
8351 | |
8352 /** | |
8353 * Tracks overlays for z-index and focus management. | |
8354 * Ensures the last added overlay with always-on-top remains on top. | |
8355 * @param {!Element} overlay | |
8356 */ | |
8357 addOverlay: function(overlay) { | |
8358 var i = this._overlays.indexOf(overlay); | |
8359 if (i >= 0) { | |
8360 this._bringOverlayAtIndexToFront(i); | |
8361 this.trackBackdrop(); | |
8362 return; | |
8363 } | |
8364 var insertionIndex = this._overlays.length; | |
8365 var currentOverlay = this._overlays[insertionIndex - 1]; | |
8366 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | |
8367 var newZ = this._getZ(overlay); | |
8368 | |
8369 // Ensure always-on-top overlay stays on top. | |
8370 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8371 // This bumps the z-index of +2. | |
8372 this._applyOverlayZ(currentOverlay, minimumZ); | |
8373 insertionIndex--; | |
8374 // Update minimumZ to match previous overlay's z-index. | |
8375 var previousOverlay = this._overlays[insertionIndex - 1]; | |
8376 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | |
8377 } | |
8378 | |
8379 // Update z-index and insert overlay. | |
8380 if (newZ <= minimumZ) { | |
8381 this._applyOverlayZ(overlay, minimumZ); | |
8382 } | |
8383 this._overlays.splice(insertionIndex, 0, overlay); | |
8384 | |
8385 this.trackBackdrop(); | 4699 this.trackBackdrop(); |
8386 }, | 4700 return; |
8387 | 4701 } |
8388 /** | 4702 var insertionIndex = this._overlays.length; |
8389 * @param {!Element} overlay | 4703 var currentOverlay = this._overlays[insertionIndex - 1]; |
8390 */ | 4704 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
8391 removeOverlay: function(overlay) { | 4705 var newZ = this._getZ(overlay); |
8392 var i = this._overlays.indexOf(overlay); | 4706 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8393 if (i === -1) { | 4707 this._applyOverlayZ(currentOverlay, minimumZ); |
8394 return; | 4708 insertionIndex--; |
8395 } | 4709 var previousOverlay = this._overlays[insertionIndex - 1]; |
8396 this._overlays.splice(i, 1); | 4710 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
8397 | 4711 } |
8398 this.trackBackdrop(); | 4712 if (newZ <= minimumZ) { |
8399 }, | 4713 this._applyOverlayZ(overlay, minimumZ); |
8400 | 4714 } |
8401 /** | 4715 this._overlays.splice(insertionIndex, 0, overlay); |
8402 * Returns the current overlay. | 4716 this.trackBackdrop(); |
8403 * @return {Element|undefined} | 4717 }, |
8404 */ | 4718 removeOverlay: function(overlay) { |
8405 currentOverlay: function() { | 4719 var i = this._overlays.indexOf(overlay); |
8406 var i = this._overlays.length - 1; | 4720 if (i === -1) { |
8407 return this._overlays[i]; | 4721 return; |
8408 }, | 4722 } |
8409 | 4723 this._overlays.splice(i, 1); |
8410 /** | 4724 this.trackBackdrop(); |
8411 * Returns the current overlay z-index. | 4725 }, |
8412 * @return {number} | 4726 currentOverlay: function() { |
8413 */ | 4727 var i = this._overlays.length - 1; |
8414 currentOverlayZ: function() { | 4728 return this._overlays[i]; |
8415 return this._getZ(this.currentOverlay()); | 4729 }, |
8416 }, | 4730 currentOverlayZ: function() { |
8417 | 4731 return this._getZ(this.currentOverlay()); |
8418 /** | 4732 }, |
8419 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | 4733 ensureMinimumZ: function(minimumZ) { |
8420 * This does not effect the z-index of any existing overlays. | 4734 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
8421 * @param {number} minimumZ | 4735 }, |
8422 */ | 4736 focusOverlay: function() { |
8423 ensureMinimumZ: function(minimumZ) { | 4737 var current = this.currentOverlay(); |
8424 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 4738 if (current) { |
8425 }, | 4739 current._applyFocus(); |
8426 | 4740 } |
8427 focusOverlay: function() { | 4741 }, |
8428 var current = /** @type {?} */ (this.currentOverlay()); | 4742 trackBackdrop: function() { |
8429 if (current) { | 4743 var overlay = this._overlayWithBackdrop(); |
8430 current._applyFocus(); | 4744 if (!overlay && !this._backdropElement) { |
8431 } | 4745 return; |
8432 }, | 4746 } |
8433 | 4747 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
8434 /** | 4748 this.backdropElement.opened = !!overlay; |
8435 * Updates the backdrop z-index. | 4749 }, |
8436 */ | 4750 getBackdrops: function() { |
8437 trackBackdrop: function() { | 4751 var backdrops = []; |
8438 var overlay = this._overlayWithBackdrop(); | 4752 for (var i = 0; i < this._overlays.length; i++) { |
8439 // Avoid creating the backdrop if there is no overlay with backdrop. | 4753 if (this._overlays[i].withBackdrop) { |
8440 if (!overlay && !this._backdropElement) { | 4754 backdrops.push(this._overlays[i]); |
8441 return; | 4755 } |
8442 } | 4756 } |
8443 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 4757 return backdrops; |
8444 this.backdropElement.opened = !!overlay; | 4758 }, |
8445 }, | 4759 backdropZ: function() { |
8446 | 4760 return this._getZ(this._overlayWithBackdrop()) - 1; |
8447 /** | 4761 }, |
8448 * @return {Array<Element>} | 4762 _overlayWithBackdrop: function() { |
8449 */ | 4763 for (var i = 0; i < this._overlays.length; i++) { |
8450 getBackdrops: function() { | 4764 if (this._overlays[i].withBackdrop) { |
8451 var backdrops = []; | 4765 return this._overlays[i]; |
8452 for (var i = 0; i < this._overlays.length; i++) { | 4766 } |
8453 if (this._overlays[i].withBackdrop) { | 4767 } |
8454 backdrops.push(this._overlays[i]); | 4768 }, |
8455 } | 4769 _getZ: function(overlay) { |
8456 } | 4770 var z = this._minimumZ; |
8457 return backdrops; | 4771 if (overlay) { |
8458 }, | 4772 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z
Index); |
8459 | 4773 if (z1 === z1) { |
8460 /** | 4774 z = z1; |
8461 * Returns the z-index for the backdrop. | 4775 } |
8462 * @return {number} | 4776 } |
8463 */ | 4777 return z; |
8464 backdropZ: function() { | 4778 }, |
8465 return this._getZ(this._overlayWithBackdrop()) - 1; | 4779 _setZ: function(element, z) { |
8466 }, | 4780 element.style.zIndex = z; |
8467 | 4781 }, |
8468 /** | 4782 _applyOverlayZ: function(overlay, aboveZ) { |
8469 * Returns the first opened overlay that has a backdrop. | 4783 this._setZ(overlay, aboveZ + 2); |
8470 * @return {Element|undefined} | 4784 }, |
8471 * @private | 4785 _overlayInPath: function(path) { |
8472 */ | 4786 path = path || []; |
8473 _overlayWithBackdrop: function() { | 4787 for (var i = 0; i < path.length; i++) { |
8474 for (var i = 0; i < this._overlays.length; i++) { | 4788 if (path[i]._manager === this) { |
8475 if (this._overlays[i].withBackdrop) { | 4789 return path[i]; |
8476 return this._overlays[i]; | 4790 } |
8477 } | 4791 } |
8478 } | 4792 }, |
8479 }, | 4793 _onCaptureClick: function(event) { |
8480 | 4794 var overlay = this.currentOverlay(); |
8481 /** | 4795 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
8482 * Calculates the minimum z-index for the overlay. | 4796 overlay._onCaptureClick(event); |
8483 * @param {Element=} overlay | 4797 } |
8484 * @private | 4798 }, |
8485 */ | 4799 _onCaptureFocus: function(event) { |
8486 _getZ: function(overlay) { | 4800 var overlay = this.currentOverlay(); |
8487 var z = this._minimumZ; | 4801 if (overlay) { |
8488 if (overlay) { | 4802 overlay._onCaptureFocus(event); |
8489 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 4803 } |
8490 // Check if is a number | 4804 }, |
8491 // Number.isNaN not supported in IE 10+ | 4805 _onCaptureKeyDown: function(event) { |
8492 if (z1 === z1) { | 4806 var overlay = this.currentOverlay(); |
8493 z = z1; | 4807 if (overlay) { |
8494 } | 4808 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
8495 } | 4809 overlay._onCaptureEsc(event); |
8496 return z; | 4810 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't
ab')) { |
8497 }, | 4811 overlay._onCaptureTab(event); |
8498 | 4812 } |
8499 /** | 4813 } |
8500 * @param {!Element} element | 4814 }, |
8501 * @param {number|string} z | 4815 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
8502 * @private | 4816 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
8503 */ | 4817 } |
8504 _setZ: function(element, z) { | 4818 }; |
8505 element.style.zIndex = z; | 4819 |
8506 }, | 4820 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
8507 | 4821 |
8508 /** | |
8509 * @param {!Element} overlay | |
8510 * @param {number} aboveZ | |
8511 * @private | |
8512 */ | |
8513 _applyOverlayZ: function(overlay, aboveZ) { | |
8514 this._setZ(overlay, aboveZ + 2); | |
8515 }, | |
8516 | |
8517 /** | |
8518 * Returns the deepest overlay in the path. | |
8519 * @param {Array<Element>=} path | |
8520 * @return {Element|undefined} | |
8521 * @suppress {missingProperties} | |
8522 * @private | |
8523 */ | |
8524 _overlayInPath: function(path) { | |
8525 path = path || []; | |
8526 for (var i = 0; i < path.length; i++) { | |
8527 if (path[i]._manager === this) { | |
8528 return path[i]; | |
8529 } | |
8530 } | |
8531 }, | |
8532 | |
8533 /** | |
8534 * Ensures the click event is delegated to the right overlay. | |
8535 * @param {!Event} event | |
8536 * @private | |
8537 */ | |
8538 _onCaptureClick: function(event) { | |
8539 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8540 // Check if clicked outside of top overlay. | |
8541 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | |
8542 overlay._onCaptureClick(event); | |
8543 } | |
8544 }, | |
8545 | |
8546 /** | |
8547 * Ensures the focus event is delegated to the right overlay. | |
8548 * @param {!Event} event | |
8549 * @private | |
8550 */ | |
8551 _onCaptureFocus: function(event) { | |
8552 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8553 if (overlay) { | |
8554 overlay._onCaptureFocus(event); | |
8555 } | |
8556 }, | |
8557 | |
8558 /** | |
8559 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
8560 * @param {!Event} event | |
8561 * @private | |
8562 */ | |
8563 _onCaptureKeyDown: function(event) { | |
8564 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8565 if (overlay) { | |
8566 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | |
8567 overlay._onCaptureEsc(event); | |
8568 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | |
8569 overlay._onCaptureTab(event); | |
8570 } | |
8571 } | |
8572 }, | |
8573 | |
8574 /** | |
8575 * Returns if the overlay1 should be behind overlay2. | |
8576 * @param {!Element} overlay1 | |
8577 * @param {!Element} overlay2 | |
8578 * @return {boolean} | |
8579 * @suppress {missingProperties} | |
8580 * @private | |
8581 */ | |
8582 _shouldBeBehindOverlay: function(overlay1, overlay2) { | |
8583 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | |
8584 } | |
8585 }; | |
8586 | |
8587 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | |
8588 (function() { | 4822 (function() { |
8589 'use strict'; | 4823 'use strict'; |
8590 | |
8591 /** | |
8592 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
8593 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
8594 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
8595 | |
8596 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
8597 for an example. | |
8598 | |
8599 ### Closing and canceling | |
8600 | |
8601 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
8602 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
8603 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
8604 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
8605 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
8606 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
8607 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
8608 | |
8609 ### Positioning | |
8610 | |
8611 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
8612 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
8613 | |
8614 ### Backdrop | |
8615 | |
8616 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
8617 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
8618 options. | |
8619 | |
8620 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
8621 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
8622 to achieve a different behavior. | |
8623 | |
8624 ### Limitations | |
8625 | |
8626 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
8627 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
8628 context. You should place this element as a child of `<body>` whenever possible. | |
8629 | |
8630 @demo demo/index.html | |
8631 @polymerBehavior Polymer.IronOverlayBehavior | |
8632 */ | |
8633 | |
8634 Polymer.IronOverlayBehaviorImpl = { | 4824 Polymer.IronOverlayBehaviorImpl = { |
8635 | |
8636 properties: { | 4825 properties: { |
8637 | |
8638 /** | |
8639 * True if the overlay is currently displayed. | |
8640 */ | |
8641 opened: { | 4826 opened: { |
8642 observer: '_openedChanged', | 4827 observer: '_openedChanged', |
8643 type: Boolean, | 4828 type: Boolean, |
8644 value: false, | 4829 value: false, |
8645 notify: true | 4830 notify: true |
8646 }, | 4831 }, |
8647 | |
8648 /** | |
8649 * True if the overlay was canceled when it was last closed. | |
8650 */ | |
8651 canceled: { | 4832 canceled: { |
8652 observer: '_canceledChanged', | 4833 observer: '_canceledChanged', |
8653 readOnly: true, | 4834 readOnly: true, |
8654 type: Boolean, | 4835 type: Boolean, |
8655 value: false | 4836 value: false |
8656 }, | 4837 }, |
8657 | |
8658 /** | |
8659 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
8660 * within the light DOM of the overlay. | |
8661 */ | |
8662 withBackdrop: { | 4838 withBackdrop: { |
8663 observer: '_withBackdropChanged', | 4839 observer: '_withBackdropChanged', |
8664 type: Boolean | 4840 type: Boolean |
8665 }, | 4841 }, |
8666 | |
8667 /** | |
8668 * Set to true to disable auto-focusing the overlay or child nodes with | |
8669 * the `autofocus` attribute` when the overlay is opened. | |
8670 */ | |
8671 noAutoFocus: { | 4842 noAutoFocus: { |
8672 type: Boolean, | 4843 type: Boolean, |
8673 value: false | 4844 value: false |
8674 }, | 4845 }, |
8675 | |
8676 /** | |
8677 * Set to true to disable canceling the overlay with the ESC key. | |
8678 */ | |
8679 noCancelOnEscKey: { | 4846 noCancelOnEscKey: { |
8680 type: Boolean, | 4847 type: Boolean, |
8681 value: false | 4848 value: false |
8682 }, | 4849 }, |
8683 | |
8684 /** | |
8685 * Set to true to disable canceling the overlay by clicking outside it. | |
8686 */ | |
8687 noCancelOnOutsideClick: { | 4850 noCancelOnOutsideClick: { |
8688 type: Boolean, | 4851 type: Boolean, |
8689 value: false | 4852 value: false |
8690 }, | 4853 }, |
8691 | |
8692 /** | |
8693 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
8694 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
8695 * behavior can provide other reasons in addition to `canceled`. | |
8696 */ | |
8697 closingReason: { | 4854 closingReason: { |
8698 // was a getter before, but needs to be a property so other | |
8699 // behaviors can override this. | |
8700 type: Object | 4855 type: Object |
8701 }, | 4856 }, |
8702 | |
8703 /** | |
8704 * Set to true to enable restoring of focus when overlay is closed. | |
8705 */ | |
8706 restoreFocusOnClose: { | 4857 restoreFocusOnClose: { |
8707 type: Boolean, | 4858 type: Boolean, |
8708 value: false | 4859 value: false |
8709 }, | 4860 }, |
8710 | |
8711 /** | |
8712 * Set to true to keep overlay always on top. | |
8713 */ | |
8714 alwaysOnTop: { | 4861 alwaysOnTop: { |
8715 type: Boolean | 4862 type: Boolean |
8716 }, | 4863 }, |
8717 | |
8718 /** | |
8719 * Shortcut to access to the overlay manager. | |
8720 * @private | |
8721 * @type {Polymer.IronOverlayManagerClass} | |
8722 */ | |
8723 _manager: { | 4864 _manager: { |
8724 type: Object, | 4865 type: Object, |
8725 value: Polymer.IronOverlayManager | 4866 value: Polymer.IronOverlayManager |
8726 }, | 4867 }, |
8727 | |
8728 /** | |
8729 * The node being focused. | |
8730 * @type {?Node} | |
8731 */ | |
8732 _focusedChild: { | 4868 _focusedChild: { |
8733 type: Object | 4869 type: Object |
8734 } | 4870 } |
8735 | 4871 }, |
8736 }, | |
8737 | |
8738 listeners: { | 4872 listeners: { |
8739 'iron-resize': '_onIronResize' | 4873 'iron-resize': '_onIronResize' |
8740 }, | 4874 }, |
8741 | |
8742 /** | |
8743 * The backdrop element. | |
8744 * @type {Element} | |
8745 */ | |
8746 get backdropElement() { | 4875 get backdropElement() { |
8747 return this._manager.backdropElement; | 4876 return this._manager.backdropElement; |
8748 }, | 4877 }, |
8749 | |
8750 /** | |
8751 * Returns the node to give focus to. | |
8752 * @type {Node} | |
8753 */ | |
8754 get _focusNode() { | 4878 get _focusNode() { |
8755 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 4879 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
8756 }, | 4880 }, |
8757 | |
8758 /** | |
8759 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
8760 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
8761 * to wrap the focus for overlays `with-backdrop`. | |
8762 * | |
8763 * If you know what is your content (specifically the first and last focusab
le children), | |
8764 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
8765 * @type {Array<Node>} | |
8766 * @protected | |
8767 */ | |
8768 get _focusableNodes() { | 4881 get _focusableNodes() { |
8769 // Elements that can be focused even if they have [disabled] attribute. | 4882 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin
dex]', '[contentEditable=true]' ]; |
8770 var FOCUSABLE_WITH_DISABLED = [ | 4883 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button'
]; |
8771 'a[href]', | 4884 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + ':
not([tabindex="-1"]),' + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([
tabindex="-1"]),') + ':not([disabled]):not([tabindex="-1"])'; |
8772 'area[href]', | |
8773 'iframe', | |
8774 '[tabindex]', | |
8775 '[contentEditable=true]' | |
8776 ]; | |
8777 | |
8778 // Elements that cannot be focused if they have [disabled] attribute. | |
8779 var FOCUSABLE_WITHOUT_DISABLED = [ | |
8780 'input', | |
8781 'select', | |
8782 'textarea', | |
8783 'button' | |
8784 ]; | |
8785 | |
8786 // Discard elements with tabindex=-1 (makes them not focusable). | |
8787 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | |
8788 ':not([tabindex="-1"]),' + | |
8789 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | |
8790 ':not([disabled]):not([tabindex="-1"])'; | |
8791 | |
8792 var focusables = Polymer.dom(this).querySelectorAll(selector); | 4885 var focusables = Polymer.dom(this).querySelectorAll(selector); |
8793 if (this.tabIndex >= 0) { | 4886 if (this.tabIndex >= 0) { |
8794 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
8795 // and the overlay should be the first of the list. | |
8796 focusables.splice(0, 0, this); | 4887 focusables.splice(0, 0, this); |
8797 } | 4888 } |
8798 // Sort by tabindex. | 4889 return focusables.sort(function(a, b) { |
8799 return focusables.sort(function (a, b) { | |
8800 if (a.tabIndex === b.tabIndex) { | 4890 if (a.tabIndex === b.tabIndex) { |
8801 return 0; | 4891 return 0; |
8802 } | 4892 } |
8803 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 4893 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
8804 return 1; | 4894 return 1; |
8805 } | 4895 } |
8806 return -1; | 4896 return -1; |
8807 }); | 4897 }); |
8808 }, | 4898 }, |
8809 | |
8810 ready: function() { | 4899 ready: function() { |
8811 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
8812 this.__isAnimating = false; | 4900 this.__isAnimating = false; |
8813 // with-backdrop needs tabindex to be set in order to trap the focus. | |
8814 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
8815 this.__shouldRemoveTabIndex = false; | 4901 this.__shouldRemoveTabIndex = false; |
8816 // Used for wrapping the focus on TAB / Shift+TAB. | |
8817 this.__firstFocusableNode = this.__lastFocusableNode = null; | 4902 this.__firstFocusableNode = this.__lastFocusableNode = null; |
8818 // Used by __onNextAnimationFrame to cancel any previous callback. | |
8819 this.__raf = null; | 4903 this.__raf = null; |
8820 // Focused node before overlay gets opened. Can be restored on close. | |
8821 this.__restoreFocusNode = null; | 4904 this.__restoreFocusNode = null; |
8822 this._ensureSetup(); | 4905 this._ensureSetup(); |
8823 }, | 4906 }, |
8824 | |
8825 attached: function() { | 4907 attached: function() { |
8826 // Call _openedChanged here so that position can be computed correctly. | |
8827 if (this.opened) { | 4908 if (this.opened) { |
8828 this._openedChanged(this.opened); | 4909 this._openedChanged(this.opened); |
8829 } | 4910 } |
8830 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 4911 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
8831 }, | 4912 }, |
8832 | |
8833 detached: function() { | 4913 detached: function() { |
8834 Polymer.dom(this).unobserveNodes(this._observer); | 4914 Polymer.dom(this).unobserveNodes(this._observer); |
8835 this._observer = null; | 4915 this._observer = null; |
8836 if (this.__raf) { | 4916 if (this.__raf) { |
8837 window.cancelAnimationFrame(this.__raf); | 4917 window.cancelAnimationFrame(this.__raf); |
8838 this.__raf = null; | 4918 this.__raf = null; |
8839 } | 4919 } |
8840 this._manager.removeOverlay(this); | 4920 this._manager.removeOverlay(this); |
8841 }, | 4921 }, |
8842 | |
8843 /** | |
8844 * Toggle the opened state of the overlay. | |
8845 */ | |
8846 toggle: function() { | 4922 toggle: function() { |
8847 this._setCanceled(false); | 4923 this._setCanceled(false); |
8848 this.opened = !this.opened; | 4924 this.opened = !this.opened; |
8849 }, | 4925 }, |
8850 | |
8851 /** | |
8852 * Open the overlay. | |
8853 */ | |
8854 open: function() { | 4926 open: function() { |
8855 this._setCanceled(false); | 4927 this._setCanceled(false); |
8856 this.opened = true; | 4928 this.opened = true; |
8857 }, | 4929 }, |
8858 | |
8859 /** | |
8860 * Close the overlay. | |
8861 */ | |
8862 close: function() { | 4930 close: function() { |
8863 this._setCanceled(false); | 4931 this._setCanceled(false); |
8864 this.opened = false; | 4932 this.opened = false; |
8865 }, | 4933 }, |
8866 | |
8867 /** | |
8868 * Cancels the overlay. | |
8869 * @param {Event=} event The original event | |
8870 */ | |
8871 cancel: function(event) { | 4934 cancel: function(event) { |
8872 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 4935 var cancelEvent = this.fire('iron-overlay-canceled', event, { |
| 4936 cancelable: true |
| 4937 }); |
8873 if (cancelEvent.defaultPrevented) { | 4938 if (cancelEvent.defaultPrevented) { |
8874 return; | 4939 return; |
8875 } | 4940 } |
8876 | |
8877 this._setCanceled(true); | 4941 this._setCanceled(true); |
8878 this.opened = false; | 4942 this.opened = false; |
8879 }, | 4943 }, |
8880 | |
8881 _ensureSetup: function() { | 4944 _ensureSetup: function() { |
8882 if (this._overlaySetup) { | 4945 if (this._overlaySetup) { |
8883 return; | 4946 return; |
8884 } | 4947 } |
8885 this._overlaySetup = true; | 4948 this._overlaySetup = true; |
8886 this.style.outline = 'none'; | 4949 this.style.outline = 'none'; |
8887 this.style.display = 'none'; | 4950 this.style.display = 'none'; |
8888 }, | 4951 }, |
8889 | |
8890 /** | |
8891 * Called when `opened` changes. | |
8892 * @param {boolean=} opened | |
8893 * @protected | |
8894 */ | |
8895 _openedChanged: function(opened) { | 4952 _openedChanged: function(opened) { |
8896 if (opened) { | 4953 if (opened) { |
8897 this.removeAttribute('aria-hidden'); | 4954 this.removeAttribute('aria-hidden'); |
8898 } else { | 4955 } else { |
8899 this.setAttribute('aria-hidden', 'true'); | 4956 this.setAttribute('aria-hidden', 'true'); |
8900 } | 4957 } |
8901 | |
8902 // Defer any animation-related code on attached | |
8903 // (_openedChanged gets called again on attached). | |
8904 if (!this.isAttached) { | 4958 if (!this.isAttached) { |
8905 return; | 4959 return; |
8906 } | 4960 } |
8907 | |
8908 this.__isAnimating = true; | 4961 this.__isAnimating = true; |
8909 | |
8910 // Use requestAnimationFrame for non-blocking rendering. | |
8911 this.__onNextAnimationFrame(this.__openedChanged); | 4962 this.__onNextAnimationFrame(this.__openedChanged); |
8912 }, | 4963 }, |
8913 | |
8914 _canceledChanged: function() { | 4964 _canceledChanged: function() { |
8915 this.closingReason = this.closingReason || {}; | 4965 this.closingReason = this.closingReason || {}; |
8916 this.closingReason.canceled = this.canceled; | 4966 this.closingReason.canceled = this.canceled; |
8917 }, | 4967 }, |
8918 | |
8919 _withBackdropChanged: function() { | 4968 _withBackdropChanged: function() { |
8920 // If tabindex is already set, no need to override it. | |
8921 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 4969 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
8922 this.setAttribute('tabindex', '-1'); | 4970 this.setAttribute('tabindex', '-1'); |
8923 this.__shouldRemoveTabIndex = true; | 4971 this.__shouldRemoveTabIndex = true; |
8924 } else if (this.__shouldRemoveTabIndex) { | 4972 } else if (this.__shouldRemoveTabIndex) { |
8925 this.removeAttribute('tabindex'); | 4973 this.removeAttribute('tabindex'); |
8926 this.__shouldRemoveTabIndex = false; | 4974 this.__shouldRemoveTabIndex = false; |
8927 } | 4975 } |
8928 if (this.opened && this.isAttached) { | 4976 if (this.opened && this.isAttached) { |
8929 this._manager.trackBackdrop(); | 4977 this._manager.trackBackdrop(); |
8930 } | 4978 } |
8931 }, | 4979 }, |
8932 | |
8933 /** | |
8934 * tasks which must occur before opening; e.g. making the element visible. | |
8935 * @protected | |
8936 */ | |
8937 _prepareRenderOpened: function() { | 4980 _prepareRenderOpened: function() { |
8938 // Store focused node. | |
8939 this.__restoreFocusNode = this._manager.deepActiveElement; | 4981 this.__restoreFocusNode = this._manager.deepActiveElement; |
8940 | |
8941 // Needed to calculate the size of the overlay so that transitions on its
size | |
8942 // will have the correct starting points. | |
8943 this._preparePositioning(); | 4982 this._preparePositioning(); |
8944 this.refit(); | 4983 this.refit(); |
8945 this._finishPositioning(); | 4984 this._finishPositioning(); |
8946 | |
8947 // Safari will apply the focus to the autofocus element when displayed | |
8948 // for the first time, so we make sure to return the focus where it was. | |
8949 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 4985 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
8950 this._focusNode.blur(); | 4986 this._focusNode.blur(); |
8951 this.__restoreFocusNode.focus(); | 4987 this.__restoreFocusNode.focus(); |
8952 } | 4988 } |
8953 }, | 4989 }, |
8954 | |
8955 /** | |
8956 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
8957 * @protected | |
8958 */ | |
8959 _renderOpened: function() { | 4990 _renderOpened: function() { |
8960 this._finishRenderOpened(); | 4991 this._finishRenderOpened(); |
8961 }, | 4992 }, |
8962 | |
8963 /** | |
8964 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
8965 * @protected | |
8966 */ | |
8967 _renderClosed: function() { | 4993 _renderClosed: function() { |
8968 this._finishRenderClosed(); | 4994 this._finishRenderClosed(); |
8969 }, | 4995 }, |
8970 | |
8971 /** | |
8972 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
8973 * @protected | |
8974 */ | |
8975 _finishRenderOpened: function() { | 4996 _finishRenderOpened: function() { |
8976 this.notifyResize(); | 4997 this.notifyResize(); |
8977 this.__isAnimating = false; | 4998 this.__isAnimating = false; |
8978 | |
8979 // Store it so we don't query too much. | |
8980 var focusableNodes = this._focusableNodes; | 4999 var focusableNodes = this._focusableNodes; |
8981 this.__firstFocusableNode = focusableNodes[0]; | 5000 this.__firstFocusableNode = focusableNodes[0]; |
8982 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 5001 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
8983 | |
8984 this.fire('iron-overlay-opened'); | 5002 this.fire('iron-overlay-opened'); |
8985 }, | 5003 }, |
8986 | |
8987 /** | |
8988 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
8989 * @protected | |
8990 */ | |
8991 _finishRenderClosed: function() { | 5004 _finishRenderClosed: function() { |
8992 // Hide the overlay. | |
8993 this.style.display = 'none'; | 5005 this.style.display = 'none'; |
8994 // Reset z-index only at the end of the animation. | |
8995 this.style.zIndex = ''; | 5006 this.style.zIndex = ''; |
8996 this.notifyResize(); | 5007 this.notifyResize(); |
8997 this.__isAnimating = false; | 5008 this.__isAnimating = false; |
8998 this.fire('iron-overlay-closed', this.closingReason); | 5009 this.fire('iron-overlay-closed', this.closingReason); |
8999 }, | 5010 }, |
9000 | |
9001 _preparePositioning: function() { | 5011 _preparePositioning: function() { |
9002 this.style.transition = this.style.webkitTransition = 'none'; | 5012 this.style.transition = this.style.webkitTransition = 'none'; |
9003 this.style.transform = this.style.webkitTransform = 'none'; | 5013 this.style.transform = this.style.webkitTransform = 'none'; |
9004 this.style.display = ''; | 5014 this.style.display = ''; |
9005 }, | 5015 }, |
9006 | |
9007 _finishPositioning: function() { | 5016 _finishPositioning: function() { |
9008 // First, make it invisible & reactivate animations. | |
9009 this.style.display = 'none'; | 5017 this.style.display = 'none'; |
9010 // Force reflow before re-enabling animations so that they don't start. | |
9011 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9012 this.scrollTop = this.scrollTop; | 5018 this.scrollTop = this.scrollTop; |
9013 this.style.transition = this.style.webkitTransition = ''; | 5019 this.style.transition = this.style.webkitTransition = ''; |
9014 this.style.transform = this.style.webkitTransform = ''; | 5020 this.style.transform = this.style.webkitTransform = ''; |
9015 // Now that animations are enabled, make it visible again | |
9016 this.style.display = ''; | 5021 this.style.display = ''; |
9017 // Force reflow, so that following animations are properly started. | |
9018 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9019 this.scrollTop = this.scrollTop; | 5022 this.scrollTop = this.scrollTop; |
9020 }, | 5023 }, |
9021 | |
9022 /** | |
9023 * Applies focus according to the opened state. | |
9024 * @protected | |
9025 */ | |
9026 _applyFocus: function() { | 5024 _applyFocus: function() { |
9027 if (this.opened) { | 5025 if (this.opened) { |
9028 if (!this.noAutoFocus) { | 5026 if (!this.noAutoFocus) { |
9029 this._focusNode.focus(); | 5027 this._focusNode.focus(); |
9030 } | 5028 } |
9031 } | 5029 } else { |
9032 else { | |
9033 this._focusNode.blur(); | 5030 this._focusNode.blur(); |
9034 this._focusedChild = null; | 5031 this._focusedChild = null; |
9035 // Restore focus. | |
9036 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 5032 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
9037 this.__restoreFocusNode.focus(); | 5033 this.__restoreFocusNode.focus(); |
9038 } | 5034 } |
9039 this.__restoreFocusNode = null; | 5035 this.__restoreFocusNode = null; |
9040 // If many overlays get closed at the same time, one of them would still | |
9041 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
9042 // infinitely, so we check for this not to be the current overlay. | |
9043 var currentOverlay = this._manager.currentOverlay(); | 5036 var currentOverlay = this._manager.currentOverlay(); |
9044 if (currentOverlay && this !== currentOverlay) { | 5037 if (currentOverlay && this !== currentOverlay) { |
9045 currentOverlay._applyFocus(); | 5038 currentOverlay._applyFocus(); |
9046 } | 5039 } |
9047 } | 5040 } |
9048 }, | 5041 }, |
9049 | |
9050 /** | |
9051 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
9052 * @param {!Event} event | |
9053 * @protected | |
9054 */ | |
9055 _onCaptureClick: function(event) { | 5042 _onCaptureClick: function(event) { |
9056 if (!this.noCancelOnOutsideClick) { | 5043 if (!this.noCancelOnOutsideClick) { |
9057 this.cancel(event); | 5044 this.cancel(event); |
9058 } | 5045 } |
9059 }, | 5046 }, |
9060 | 5047 _onCaptureFocus: function(event) { |
9061 /** | |
9062 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
9063 * @param {!Event} event | |
9064 * @protected | |
9065 */ | |
9066 _onCaptureFocus: function (event) { | |
9067 if (!this.withBackdrop) { | 5048 if (!this.withBackdrop) { |
9068 return; | 5049 return; |
9069 } | 5050 } |
9070 var path = Polymer.dom(event).path; | 5051 var path = Polymer.dom(event).path; |
9071 if (path.indexOf(this) === -1) { | 5052 if (path.indexOf(this) === -1) { |
9072 event.stopPropagation(); | 5053 event.stopPropagation(); |
9073 this._applyFocus(); | 5054 this._applyFocus(); |
9074 } else { | 5055 } else { |
9075 this._focusedChild = path[0]; | 5056 this._focusedChild = path[0]; |
9076 } | 5057 } |
9077 }, | 5058 }, |
9078 | |
9079 /** | |
9080 * Handles the ESC key event and cancels (closes) the overlay. | |
9081 * @param {!Event} event | |
9082 * @protected | |
9083 */ | |
9084 _onCaptureEsc: function(event) { | 5059 _onCaptureEsc: function(event) { |
9085 if (!this.noCancelOnEscKey) { | 5060 if (!this.noCancelOnEscKey) { |
9086 this.cancel(event); | 5061 this.cancel(event); |
9087 } | 5062 } |
9088 }, | 5063 }, |
9089 | |
9090 /** | |
9091 * Handles TAB key events to track focus changes. | |
9092 * Will wrap focus for overlays withBackdrop. | |
9093 * @param {!Event} event | |
9094 * @protected | |
9095 */ | |
9096 _onCaptureTab: function(event) { | 5064 _onCaptureTab: function(event) { |
9097 if (!this.withBackdrop) { | 5065 if (!this.withBackdrop) { |
9098 return; | 5066 return; |
9099 } | 5067 } |
9100 // TAB wraps from last to first focusable. | |
9101 // Shift + TAB wraps from first to last focusable. | |
9102 var shift = event.shiftKey; | 5068 var shift = event.shiftKey; |
9103 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 5069 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
9104 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 5070 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
9105 var shouldWrap = false; | 5071 var shouldWrap = false; |
9106 if (nodeToCheck === nodeToSet) { | 5072 if (nodeToCheck === nodeToSet) { |
9107 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
9108 // with 0 or 1 focusables; in either case we still need to trap the | |
9109 // focus within the overlay. | |
9110 shouldWrap = true; | 5073 shouldWrap = true; |
9111 } else { | 5074 } else { |
9112 // In dom=shadow, the manager will receive focus changes on the main | |
9113 // root but not the ones within other shadow roots, so we can't rely on | |
9114 // _focusedChild, but we should check the deepest active element. | |
9115 var focusedNode = this._manager.deepActiveElement; | 5075 var focusedNode = this._manager.deepActiveElement; |
9116 // If the active element is not the nodeToCheck but the overlay itself, | 5076 shouldWrap = focusedNode === nodeToCheck || focusedNode === this; |
9117 // it means the focus is about to go outside the overlay, hence we | |
9118 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
9119 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | |
9120 } | 5077 } |
9121 | |
9122 if (shouldWrap) { | 5078 if (shouldWrap) { |
9123 // When the overlay contains the last focusable element of the document | |
9124 // and it's already focused, pressing TAB would move the focus outside | |
9125 // the document (e.g. to the browser search bar). Similarly, when the | |
9126 // overlay contains the first focusable element of the document and it's | |
9127 // already focused, pressing Shift+TAB would move the focus outside the | |
9128 // document (e.g. to the browser search bar). | |
9129 // In both cases, we would not receive a focus event, but only a blur. | |
9130 // In order to achieve focus wrapping, we prevent this TAB event and | |
9131 // force the focus. This will also prevent the focus to temporarily move | |
9132 // outside the overlay, which might cause scrolling. | |
9133 event.preventDefault(); | 5079 event.preventDefault(); |
9134 this._focusedChild = nodeToSet; | 5080 this._focusedChild = nodeToSet; |
9135 this._applyFocus(); | 5081 this._applyFocus(); |
9136 } | 5082 } |
9137 }, | 5083 }, |
9138 | |
9139 /** | |
9140 * Refits if the overlay is opened and not animating. | |
9141 * @protected | |
9142 */ | |
9143 _onIronResize: function() { | 5084 _onIronResize: function() { |
9144 if (this.opened && !this.__isAnimating) { | 5085 if (this.opened && !this.__isAnimating) { |
9145 this.__onNextAnimationFrame(this.refit); | 5086 this.__onNextAnimationFrame(this.refit); |
9146 } | 5087 } |
9147 }, | 5088 }, |
9148 | |
9149 /** | |
9150 * Will call notifyResize if overlay is opened. | |
9151 * Can be overridden in order to avoid multiple observers on the same node. | |
9152 * @protected | |
9153 */ | |
9154 _onNodesChange: function() { | 5089 _onNodesChange: function() { |
9155 if (this.opened && !this.__isAnimating) { | 5090 if (this.opened && !this.__isAnimating) { |
9156 this.notifyResize(); | 5091 this.notifyResize(); |
9157 } | 5092 } |
9158 }, | 5093 }, |
9159 | |
9160 /** | |
9161 * Tasks executed when opened changes: prepare for the opening, move the | |
9162 * focus, update the manager, render opened/closed. | |
9163 * @private | |
9164 */ | |
9165 __openedChanged: function() { | 5094 __openedChanged: function() { |
9166 if (this.opened) { | 5095 if (this.opened) { |
9167 // Make overlay visible, then add it to the manager. | |
9168 this._prepareRenderOpened(); | 5096 this._prepareRenderOpened(); |
9169 this._manager.addOverlay(this); | 5097 this._manager.addOverlay(this); |
9170 // Move the focus to the child node with [autofocus]. | |
9171 this._applyFocus(); | 5098 this._applyFocus(); |
9172 | |
9173 this._renderOpened(); | 5099 this._renderOpened(); |
9174 } else { | 5100 } else { |
9175 // Remove overlay, then restore the focus before actually closing. | |
9176 this._manager.removeOverlay(this); | 5101 this._manager.removeOverlay(this); |
9177 this._applyFocus(); | 5102 this._applyFocus(); |
9178 | |
9179 this._renderClosed(); | 5103 this._renderClosed(); |
9180 } | 5104 } |
9181 }, | 5105 }, |
9182 | |
9183 /** | |
9184 * Executes a callback on the next animation frame, overriding any previous | |
9185 * callback awaiting for the next animation frame. e.g. | |
9186 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
9187 * `callback1` will never be invoked. | |
9188 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
9189 * @private | |
9190 */ | |
9191 __onNextAnimationFrame: function(callback) { | 5106 __onNextAnimationFrame: function(callback) { |
9192 if (this.__raf) { | 5107 if (this.__raf) { |
9193 window.cancelAnimationFrame(this.__raf); | 5108 window.cancelAnimationFrame(this.__raf); |
9194 } | 5109 } |
9195 var self = this; | 5110 var self = this; |
9196 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 5111 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
9197 self.__raf = null; | 5112 self.__raf = null; |
9198 callback.call(self); | 5113 callback.call(self); |
9199 }); | 5114 }); |
9200 } | 5115 } |
9201 | |
9202 }; | 5116 }; |
9203 | 5117 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable
Behavior, Polymer.IronOverlayBehaviorImpl ]; |
9204 /** @polymerBehavior */ | |
9205 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
9206 | |
9207 /** | |
9208 * Fired after the overlay opens. | |
9209 * @event iron-overlay-opened | |
9210 */ | |
9211 | |
9212 /** | |
9213 * Fired when the overlay is canceled, but before it is closed. | |
9214 * @event iron-overlay-canceled | |
9215 * @param {Event} event The closing of the overlay can be prevented | |
9216 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
9217 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
9218 */ | |
9219 | |
9220 /** | |
9221 * Fired after the overlay closes. | |
9222 * @event iron-overlay-closed | |
9223 * @param {Event} event The `event.detail` is the `closingReason` property | |
9224 * (contains `canceled`, whether the overlay was canceled). | |
9225 */ | |
9226 | |
9227 })(); | 5118 })(); |
9228 /** | 5119 |
9229 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | 5120 Polymer.NeonAnimatableBehavior = { |
9230 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | 5121 properties: { |
9231 * @polymerBehavior | 5122 animationConfig: { |
9232 */ | 5123 type: Object |
9233 Polymer.NeonAnimatableBehavior = { | 5124 }, |
9234 | 5125 entryAnimation: { |
9235 properties: { | 5126 observer: '_entryAnimationChanged', |
9236 | 5127 type: String |
9237 /** | 5128 }, |
9238 * Animation configuration. See README for more info. | 5129 exitAnimation: { |
9239 */ | 5130 observer: '_exitAnimationChanged', |
9240 animationConfig: { | 5131 type: String |
9241 type: Object | 5132 } |
9242 }, | 5133 }, |
9243 | 5134 _entryAnimationChanged: function() { |
9244 /** | 5135 this.animationConfig = this.animationConfig || {}; |
9245 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | 5136 this.animationConfig['entry'] = [ { |
9246 * manually if using this. The animated node is set to `this` if using thi
s property. | 5137 name: this.entryAnimation, |
9247 */ | 5138 node: this |
9248 entryAnimation: { | 5139 } ]; |
9249 observer: '_entryAnimationChanged', | 5140 }, |
9250 type: String | 5141 _exitAnimationChanged: function() { |
9251 }, | 5142 this.animationConfig = this.animationConfig || {}; |
9252 | 5143 this.animationConfig['exit'] = [ { |
9253 /** | 5144 name: this.exitAnimation, |
9254 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | 5145 node: this |
9255 * manually if using this. The animated node is set to `this` if using thi
s property. | 5146 } ]; |
9256 */ | 5147 }, |
9257 exitAnimation: { | 5148 _copyProperties: function(config1, config2) { |
9258 observer: '_exitAnimationChanged', | 5149 for (var property in config2) { |
9259 type: String | 5150 config1[property] = config2[property]; |
9260 } | 5151 } |
9261 | 5152 }, |
9262 }, | 5153 _cloneConfig: function(config) { |
9263 | 5154 var clone = { |
9264 _entryAnimationChanged: function() { | 5155 isClone: true |
9265 this.animationConfig = this.animationConfig || {}; | 5156 }; |
9266 this.animationConfig['entry'] = [{ | 5157 this._copyProperties(clone, config); |
9267 name: this.entryAnimation, | 5158 return clone; |
9268 node: this | 5159 }, |
9269 }]; | 5160 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
9270 }, | 5161 if (!this.animationConfig) { |
9271 | 5162 return; |
9272 _exitAnimationChanged: function() { | 5163 } |
9273 this.animationConfig = this.animationConfig || {}; | 5164 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun
ction') { |
9274 this.animationConfig['exit'] = [{ | 5165 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid
e of your components 'properties' object instead of outside of it.")); |
9275 name: this.exitAnimation, | 5166 return; |
9276 node: this | 5167 } |
9277 }]; | 5168 var thisConfig; |
9278 }, | 5169 if (type) { |
9279 | 5170 thisConfig = this.animationConfig[type]; |
9280 _copyProperties: function(config1, config2) { | 5171 } else { |
9281 // shallowly copy properties from config2 to config1 | 5172 thisConfig = this.animationConfig; |
9282 for (var property in config2) { | 5173 } |
9283 config1[property] = config2[property]; | 5174 if (!Array.isArray(thisConfig)) { |
9284 } | 5175 thisConfig = [ thisConfig ]; |
9285 }, | 5176 } |
9286 | 5177 if (thisConfig) { |
9287 _cloneConfig: function(config) { | 5178 for (var config, index = 0; config = thisConfig[index]; index++) { |
9288 var clone = { | 5179 if (config.animatable) { |
9289 isClone: true | 5180 config.animatable._getAnimationConfigRecursive(config.type || type, ma
p, allConfigs); |
9290 }; | 5181 } else { |
9291 this._copyProperties(clone, config); | 5182 if (config.id) { |
9292 return clone; | 5183 var cachedConfig = map[config.id]; |
9293 }, | 5184 if (cachedConfig) { |
9294 | 5185 if (!cachedConfig.isClone) { |
9295 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 5186 map[config.id] = this._cloneConfig(cachedConfig); |
9296 if (!this.animationConfig) { | 5187 cachedConfig = map[config.id]; |
9297 return; | |
9298 } | |
9299 | |
9300 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | |
9301 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | |
9302 » return; | |
9303 } | |
9304 | |
9305 // type is optional | |
9306 var thisConfig; | |
9307 if (type) { | |
9308 thisConfig = this.animationConfig[type]; | |
9309 } else { | |
9310 thisConfig = this.animationConfig; | |
9311 } | |
9312 | |
9313 if (!Array.isArray(thisConfig)) { | |
9314 thisConfig = [thisConfig]; | |
9315 } | |
9316 | |
9317 // iterate animations and recurse to process configurations from child nod
es | |
9318 if (thisConfig) { | |
9319 for (var config, index = 0; config = thisConfig[index]; index++) { | |
9320 if (config.animatable) { | |
9321 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | |
9322 } else { | |
9323 if (config.id) { | |
9324 var cachedConfig = map[config.id]; | |
9325 if (cachedConfig) { | |
9326 // merge configurations with the same id, making a clone lazily | |
9327 if (!cachedConfig.isClone) { | |
9328 map[config.id] = this._cloneConfig(cachedConfig) | |
9329 cachedConfig = map[config.id]; | |
9330 } | |
9331 this._copyProperties(cachedConfig, config); | |
9332 } else { | |
9333 // put any configs with an id into a map | |
9334 map[config.id] = config; | |
9335 } | 5188 } |
| 5189 this._copyProperties(cachedConfig, config); |
9336 } else { | 5190 } else { |
9337 allConfigs.push(config); | 5191 map[config.id] = config; |
9338 } | |
9339 } | |
9340 } | |
9341 } | |
9342 }, | |
9343 | |
9344 /** | |
9345 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
9346 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
9347 * should define the property `animationConfig`, which is either a configura
tion object | |
9348 * or a map of animation type to array of configuration objects. | |
9349 */ | |
9350 getAnimationConfig: function(type) { | |
9351 var map = {}; | |
9352 var allConfigs = []; | |
9353 this._getAnimationConfigRecursive(type, map, allConfigs); | |
9354 // append the configurations saved in the map to the array | |
9355 for (var key in map) { | |
9356 allConfigs.push(map[key]); | |
9357 } | |
9358 return allConfigs; | |
9359 } | |
9360 | |
9361 }; | |
9362 /** | |
9363 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
9364 * | |
9365 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
9366 */ | |
9367 Polymer.NeonAnimationRunnerBehaviorImpl = { | |
9368 | |
9369 _configureAnimations: function(configs) { | |
9370 var results = []; | |
9371 if (configs.length > 0) { | |
9372 for (var config, index = 0; config = configs[index]; index++) { | |
9373 var neonAnimation = document.createElement(config.name); | |
9374 // is this element actually a neon animation? | |
9375 if (neonAnimation.isNeonAnimation) { | |
9376 var result = null; | |
9377 // configuration or play could fail if polyfills aren't loaded | |
9378 try { | |
9379 result = neonAnimation.configure(config); | |
9380 // Check if we have an Effect rather than an Animation | |
9381 if (typeof result.cancel != 'function') { | |
9382 result = document.timeline.play(result); | |
9383 } | |
9384 } catch (e) { | |
9385 result = null; | |
9386 console.warn('Couldnt play', '(', config.name, ').', e); | |
9387 } | |
9388 if (result) { | |
9389 results.push({ | |
9390 neonAnimation: neonAnimation, | |
9391 config: config, | |
9392 animation: result, | |
9393 }); | |
9394 } | 5192 } |
9395 } else { | 5193 } else { |
9396 console.warn(this.is + ':', config.name, 'not found!'); | 5194 allConfigs.push(config); |
9397 } | 5195 } |
9398 } | 5196 } |
9399 } | 5197 } |
9400 return results; | 5198 } |
9401 }, | 5199 }, |
9402 | 5200 getAnimationConfig: function(type) { |
9403 _shouldComplete: function(activeEntries) { | 5201 var map = {}; |
9404 var finished = true; | 5202 var allConfigs = []; |
9405 for (var i = 0; i < activeEntries.length; i++) { | 5203 this._getAnimationConfigRecursive(type, map, allConfigs); |
9406 if (activeEntries[i].animation.playState != 'finished') { | 5204 for (var key in map) { |
9407 finished = false; | 5205 allConfigs.push(map[key]); |
9408 break; | 5206 } |
9409 } | 5207 return allConfigs; |
9410 } | 5208 } |
9411 return finished; | 5209 }; |
9412 }, | 5210 |
9413 | 5211 Polymer.NeonAnimationRunnerBehaviorImpl = { |
9414 _complete: function(activeEntries) { | 5212 _configureAnimations: function(configs) { |
9415 for (var i = 0; i < activeEntries.length; i++) { | 5213 var results = []; |
9416 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | 5214 if (configs.length > 0) { |
9417 } | 5215 for (var config, index = 0; config = configs[index]; index++) { |
9418 for (var i = 0; i < activeEntries.length; i++) { | 5216 var neonAnimation = document.createElement(config.name); |
9419 activeEntries[i].animation.cancel(); | 5217 if (neonAnimation.isNeonAnimation) { |
9420 } | 5218 var result = null; |
9421 }, | 5219 try { |
9422 | 5220 result = neonAnimation.configure(config); |
9423 /** | 5221 if (typeof result.cancel != 'function') { |
9424 * Plays an animation with an optional `type`. | 5222 result = document.timeline.play(result); |
9425 * @param {string=} type | 5223 } |
9426 * @param {!Object=} cookie | 5224 } catch (e) { |
9427 */ | 5225 result = null; |
9428 playAnimation: function(type, cookie) { | 5226 console.warn('Couldnt play', '(', config.name, ').', e); |
9429 var configs = this.getAnimationConfig(type); | 5227 } |
9430 if (!configs) { | 5228 if (result) { |
| 5229 results.push({ |
| 5230 neonAnimation: neonAnimation, |
| 5231 config: config, |
| 5232 animation: result |
| 5233 }); |
| 5234 } |
| 5235 } else { |
| 5236 console.warn(this.is + ':', config.name, 'not found!'); |
| 5237 } |
| 5238 } |
| 5239 } |
| 5240 return results; |
| 5241 }, |
| 5242 _shouldComplete: function(activeEntries) { |
| 5243 var finished = true; |
| 5244 for (var i = 0; i < activeEntries.length; i++) { |
| 5245 if (activeEntries[i].animation.playState != 'finished') { |
| 5246 finished = false; |
| 5247 break; |
| 5248 } |
| 5249 } |
| 5250 return finished; |
| 5251 }, |
| 5252 _complete: function(activeEntries) { |
| 5253 for (var i = 0; i < activeEntries.length; i++) { |
| 5254 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
| 5255 } |
| 5256 for (var i = 0; i < activeEntries.length; i++) { |
| 5257 activeEntries[i].animation.cancel(); |
| 5258 } |
| 5259 }, |
| 5260 playAnimation: function(type, cookie) { |
| 5261 var configs = this.getAnimationConfig(type); |
| 5262 if (!configs) { |
| 5263 return; |
| 5264 } |
| 5265 this._active = this._active || {}; |
| 5266 if (this._active[type]) { |
| 5267 this._complete(this._active[type]); |
| 5268 delete this._active[type]; |
| 5269 } |
| 5270 var activeEntries = this._configureAnimations(configs); |
| 5271 if (activeEntries.length == 0) { |
| 5272 this.fire('neon-animation-finish', cookie, { |
| 5273 bubbles: false |
| 5274 }); |
| 5275 return; |
| 5276 } |
| 5277 this._active[type] = activeEntries; |
| 5278 for (var i = 0; i < activeEntries.length; i++) { |
| 5279 activeEntries[i].animation.onfinish = function() { |
| 5280 if (this._shouldComplete(activeEntries)) { |
| 5281 this._complete(activeEntries); |
| 5282 delete this._active[type]; |
| 5283 this.fire('neon-animation-finish', cookie, { |
| 5284 bubbles: false |
| 5285 }); |
| 5286 } |
| 5287 }.bind(this); |
| 5288 } |
| 5289 }, |
| 5290 cancelAnimation: function() { |
| 5291 for (var k in this._animations) { |
| 5292 this._animations[k].cancel(); |
| 5293 } |
| 5294 this._animations = {}; |
| 5295 } |
| 5296 }; |
| 5297 |
| 5298 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer.
NeonAnimationRunnerBehaviorImpl ]; |
| 5299 |
| 5300 Polymer.NeonAnimationBehavior = { |
| 5301 properties: { |
| 5302 animationTiming: { |
| 5303 type: Object, |
| 5304 value: function() { |
| 5305 return { |
| 5306 duration: 500, |
| 5307 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
| 5308 fill: 'both' |
| 5309 }; |
| 5310 } |
| 5311 } |
| 5312 }, |
| 5313 isNeonAnimation: true, |
| 5314 timingFromConfig: function(config) { |
| 5315 if (config.timing) { |
| 5316 for (var property in config.timing) { |
| 5317 this.animationTiming[property] = config.timing[property]; |
| 5318 } |
| 5319 } |
| 5320 return this.animationTiming; |
| 5321 }, |
| 5322 setPrefixedProperty: function(node, property, value) { |
| 5323 var map = { |
| 5324 transform: [ 'webkitTransform' ], |
| 5325 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ] |
| 5326 }; |
| 5327 var prefixes = map[property]; |
| 5328 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
| 5329 node.style[prefix] = value; |
| 5330 } |
| 5331 node.style[property] = value; |
| 5332 }, |
| 5333 complete: function() {} |
| 5334 }; |
| 5335 |
| 5336 Polymer({ |
| 5337 is: 'opaque-animation', |
| 5338 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5339 configure: function(config) { |
| 5340 var node = config.node; |
| 5341 this._effect = new KeyframeEffect(node, [ { |
| 5342 opacity: '1' |
| 5343 }, { |
| 5344 opacity: '1' |
| 5345 } ], this.timingFromConfig(config)); |
| 5346 node.style.opacity = '0'; |
| 5347 return this._effect; |
| 5348 }, |
| 5349 complete: function(config) { |
| 5350 config.node.style.opacity = ''; |
| 5351 } |
| 5352 }); |
| 5353 |
| 5354 (function() { |
| 5355 'use strict'; |
| 5356 var LAST_TOUCH_POSITION = { |
| 5357 pageX: 0, |
| 5358 pageY: 0 |
| 5359 }; |
| 5360 var ROOT_TARGET = null; |
| 5361 var SCROLLABLE_NODES = []; |
| 5362 Polymer.IronDropdownScrollManager = { |
| 5363 get currentLockingElement() { |
| 5364 return this._lockingElements[this._lockingElements.length - 1]; |
| 5365 }, |
| 5366 elementIsScrollLocked: function(element) { |
| 5367 var currentLockingElement = this.currentLockingElement; |
| 5368 if (currentLockingElement === undefined) return false; |
| 5369 var scrollLocked; |
| 5370 if (this._hasCachedLockedElement(element)) { |
| 5371 return true; |
| 5372 } |
| 5373 if (this._hasCachedUnlockedElement(element)) { |
| 5374 return false; |
| 5375 } |
| 5376 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen
t && !this._composedTreeContains(currentLockingElement, element); |
| 5377 if (scrollLocked) { |
| 5378 this._lockedElementCache.push(element); |
| 5379 } else { |
| 5380 this._unlockedElementCache.push(element); |
| 5381 } |
| 5382 return scrollLocked; |
| 5383 }, |
| 5384 pushScrollLock: function(element) { |
| 5385 if (this._lockingElements.indexOf(element) >= 0) { |
9431 return; | 5386 return; |
9432 } | 5387 } |
9433 this._active = this._active || {}; | 5388 if (this._lockingElements.length === 0) { |
9434 if (this._active[type]) { | 5389 this._lockScrollInteractions(); |
9435 this._complete(this._active[type]); | 5390 } |
9436 delete this._active[type]; | 5391 this._lockingElements.push(element); |
9437 } | 5392 this._lockedElementCache = []; |
9438 | 5393 this._unlockedElementCache = []; |
9439 var activeEntries = this._configureAnimations(configs); | 5394 }, |
9440 | 5395 removeScrollLock: function(element) { |
9441 if (activeEntries.length == 0) { | 5396 var index = this._lockingElements.indexOf(element); |
9442 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5397 if (index === -1) { |
9443 return; | 5398 return; |
9444 } | 5399 } |
9445 | 5400 this._lockingElements.splice(index, 1); |
9446 this._active[type] = activeEntries; | 5401 this._lockedElementCache = []; |
9447 | 5402 this._unlockedElementCache = []; |
9448 for (var i = 0; i < activeEntries.length; i++) { | 5403 if (this._lockingElements.length === 0) { |
9449 activeEntries[i].animation.onfinish = function() { | 5404 this._unlockScrollInteractions(); |
9450 if (this._shouldComplete(activeEntries)) { | 5405 } |
9451 this._complete(activeEntries); | 5406 }, |
9452 delete this._active[type]; | 5407 _lockingElements: [], |
9453 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5408 _lockedElementCache: null, |
| 5409 _unlockedElementCache: null, |
| 5410 _hasCachedLockedElement: function(element) { |
| 5411 return this._lockedElementCache.indexOf(element) > -1; |
| 5412 }, |
| 5413 _hasCachedUnlockedElement: function(element) { |
| 5414 return this._unlockedElementCache.indexOf(element) > -1; |
| 5415 }, |
| 5416 _composedTreeContains: function(element, child) { |
| 5417 var contentElements; |
| 5418 var distributedNodes; |
| 5419 var contentIndex; |
| 5420 var nodeIndex; |
| 5421 if (element.contains(child)) { |
| 5422 return true; |
| 5423 } |
| 5424 contentElements = Polymer.dom(element).querySelectorAll('content'); |
| 5425 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd
ex) { |
| 5426 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib
utedNodes(); |
| 5427 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { |
| 5428 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { |
| 5429 return true; |
9454 } | 5430 } |
9455 }.bind(this); | 5431 } |
9456 } | 5432 } |
9457 }, | 5433 return false; |
9458 | 5434 }, |
9459 /** | 5435 _scrollInteractionHandler: function(event) { |
9460 * Cancels the currently running animations. | 5436 if (event.cancelable && this._shouldPreventScrolling(event)) { |
9461 */ | 5437 event.preventDefault(); |
9462 cancelAnimation: function() { | 5438 } |
9463 for (var k in this._animations) { | 5439 if (event.targetTouches) { |
9464 this._animations[k].cancel(); | 5440 var touch = event.targetTouches[0]; |
9465 } | 5441 LAST_TOUCH_POSITION.pageX = touch.pageX; |
9466 this._animations = {}; | 5442 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 5443 } |
| 5444 }, |
| 5445 _lockScrollInteractions: function() { |
| 5446 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac
tionHandler.bind(this); |
| 5447 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 5448 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 5449 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true
); |
| 5450 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 5451 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 5452 }, |
| 5453 _unlockScrollInteractions: function() { |
| 5454 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 5455 document.removeEventListener('mousewheel', this._boundScrollHandler, true)
; |
| 5456 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t
rue); |
| 5457 document.removeEventListener('touchstart', this._boundScrollHandler, true)
; |
| 5458 document.removeEventListener('touchmove', this._boundScrollHandler, true); |
| 5459 }, |
| 5460 _shouldPreventScrolling: function(event) { |
| 5461 var target = Polymer.dom(event).rootTarget; |
| 5462 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 5463 ROOT_TARGET = target; |
| 5464 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 5465 } |
| 5466 if (!SCROLLABLE_NODES.length) { |
| 5467 return true; |
| 5468 } |
| 5469 if (event.type === 'touchstart') { |
| 5470 return false; |
| 5471 } |
| 5472 var info = this._getScrollInfo(event); |
| 5473 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY)
; |
| 5474 }, |
| 5475 _getScrollableNodes: function(nodes) { |
| 5476 var scrollables = []; |
| 5477 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 5478 for (var i = 0; i <= lockingIndex; i++) { |
| 5479 var node = nodes[i]; |
| 5480 if (node.nodeType === 11) { |
| 5481 continue; |
| 5482 } |
| 5483 var style = node.style; |
| 5484 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 5485 style = window.getComputedStyle(node); |
| 5486 } |
| 5487 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 5488 scrollables.push(node); |
| 5489 } |
| 5490 } |
| 5491 return scrollables; |
| 5492 }, |
| 5493 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 5494 if (!deltaX && !deltaY) { |
| 5495 return; |
| 5496 } |
| 5497 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 5498 for (var i = 0; i < nodes.length; i++) { |
| 5499 var node = nodes[i]; |
| 5500 var canScroll = false; |
| 5501 if (verticalScroll) { |
| 5502 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc
rollHeight - node.clientHeight; |
| 5503 } else { |
| 5504 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.
scrollWidth - node.clientWidth; |
| 5505 } |
| 5506 if (canScroll) { |
| 5507 return node; |
| 5508 } |
| 5509 } |
| 5510 }, |
| 5511 _getScrollInfo: function(event) { |
| 5512 var info = { |
| 5513 deltaX: event.deltaX, |
| 5514 deltaY: event.deltaY |
| 5515 }; |
| 5516 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) { |
| 5517 info.deltaX = -event.wheelDeltaX; |
| 5518 info.deltaY = -event.wheelDeltaY; |
| 5519 } else if ('axis' in event) { |
| 5520 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 5521 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 5522 } else if (event.targetTouches) { |
| 5523 var touch = event.targetTouches[0]; |
| 5524 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 5525 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 5526 } |
| 5527 return info; |
9467 } | 5528 } |
9468 }; | 5529 }; |
9469 | 5530 })(); |
9470 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | 5531 |
9471 Polymer.NeonAnimationRunnerBehavior = [ | 5532 (function() { |
9472 Polymer.NeonAnimatableBehavior, | 5533 'use strict'; |
9473 Polymer.NeonAnimationRunnerBehaviorImpl | 5534 Polymer({ |
9474 ]; | 5535 is: 'iron-dropdown', |
9475 /** | 5536 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer
.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], |
9476 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
9477 * @polymerBehavior | |
9478 */ | |
9479 Polymer.NeonAnimationBehavior = { | |
9480 | |
9481 properties: { | 5537 properties: { |
9482 | 5538 horizontalAlign: { |
9483 /** | 5539 type: String, |
9484 * Defines the animation timing. | 5540 value: 'left', |
9485 */ | 5541 reflectToAttribute: true |
9486 animationTiming: { | 5542 }, |
| 5543 verticalAlign: { |
| 5544 type: String, |
| 5545 value: 'top', |
| 5546 reflectToAttribute: true |
| 5547 }, |
| 5548 openAnimationConfig: { |
| 5549 type: Object |
| 5550 }, |
| 5551 closeAnimationConfig: { |
| 5552 type: Object |
| 5553 }, |
| 5554 focusTarget: { |
| 5555 type: Object |
| 5556 }, |
| 5557 noAnimations: { |
| 5558 type: Boolean, |
| 5559 value: false |
| 5560 }, |
| 5561 allowOutsideScroll: { |
| 5562 type: Boolean, |
| 5563 value: false |
| 5564 }, |
| 5565 _boundOnCaptureScroll: { |
| 5566 type: Function, |
| 5567 value: function() { |
| 5568 return this._onCaptureScroll.bind(this); |
| 5569 } |
| 5570 } |
| 5571 }, |
| 5572 listeners: { |
| 5573 'neon-animation-finish': '_onNeonAnimationFinish' |
| 5574 }, |
| 5575 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont
alAlign, verticalOffset, horizontalOffset)' ], |
| 5576 get containedElement() { |
| 5577 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5578 }, |
| 5579 get _focusTarget() { |
| 5580 return this.focusTarget || this.containedElement; |
| 5581 }, |
| 5582 ready: function() { |
| 5583 this._scrollTop = 0; |
| 5584 this._scrollLeft = 0; |
| 5585 this._refitOnScrollRAF = null; |
| 5586 }, |
| 5587 detached: function() { |
| 5588 this.cancelAnimation(); |
| 5589 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5590 }, |
| 5591 _openedChanged: function() { |
| 5592 if (this.opened && this.disabled) { |
| 5593 this.cancel(); |
| 5594 } else { |
| 5595 this.cancelAnimation(); |
| 5596 this.sizingTarget = this.containedElement || this.sizingTarget; |
| 5597 this._updateAnimationConfig(); |
| 5598 this._saveScrollPosition(); |
| 5599 if (this.opened) { |
| 5600 document.addEventListener('scroll', this._boundOnCaptureScroll); |
| 5601 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro
llLock(this); |
| 5602 } else { |
| 5603 document.removeEventListener('scroll', this._boundOnCaptureScroll); |
| 5604 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5605 } |
| 5606 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); |
| 5607 } |
| 5608 }, |
| 5609 _renderOpened: function() { |
| 5610 if (!this.noAnimations && this.animationConfig.open) { |
| 5611 this.$.contentWrapper.classList.add('animating'); |
| 5612 this.playAnimation('open'); |
| 5613 } else { |
| 5614 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); |
| 5615 } |
| 5616 }, |
| 5617 _renderClosed: function() { |
| 5618 if (!this.noAnimations && this.animationConfig.close) { |
| 5619 this.$.contentWrapper.classList.add('animating'); |
| 5620 this.playAnimation('close'); |
| 5621 } else { |
| 5622 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); |
| 5623 } |
| 5624 }, |
| 5625 _onNeonAnimationFinish: function() { |
| 5626 this.$.contentWrapper.classList.remove('animating'); |
| 5627 if (this.opened) { |
| 5628 this._finishRenderOpened(); |
| 5629 } else { |
| 5630 this._finishRenderClosed(); |
| 5631 } |
| 5632 }, |
| 5633 _onCaptureScroll: function() { |
| 5634 if (!this.allowOutsideScroll) { |
| 5635 this._restoreScrollPosition(); |
| 5636 } else { |
| 5637 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol
lRAF); |
| 5638 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th
is)); |
| 5639 } |
| 5640 }, |
| 5641 _saveScrollPosition: function() { |
| 5642 if (document.scrollingElement) { |
| 5643 this._scrollTop = document.scrollingElement.scrollTop; |
| 5644 this._scrollLeft = document.scrollingElement.scrollLeft; |
| 5645 } else { |
| 5646 this._scrollTop = Math.max(document.documentElement.scrollTop, document.
body.scrollTop); |
| 5647 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen
t.body.scrollLeft); |
| 5648 } |
| 5649 }, |
| 5650 _restoreScrollPosition: function() { |
| 5651 if (document.scrollingElement) { |
| 5652 document.scrollingElement.scrollTop = this._scrollTop; |
| 5653 document.scrollingElement.scrollLeft = this._scrollLeft; |
| 5654 } else { |
| 5655 document.documentElement.scrollTop = this._scrollTop; |
| 5656 document.documentElement.scrollLeft = this._scrollLeft; |
| 5657 document.body.scrollTop = this._scrollTop; |
| 5658 document.body.scrollLeft = this._scrollLeft; |
| 5659 } |
| 5660 }, |
| 5661 _updateAnimationConfig: function() { |
| 5662 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati
onConfig || []); |
| 5663 for (var i = 0; i < animations.length; i++) { |
| 5664 animations[i].node = this.containedElement; |
| 5665 } |
| 5666 this.animationConfig = { |
| 5667 open: this.openAnimationConfig, |
| 5668 close: this.closeAnimationConfig |
| 5669 }; |
| 5670 }, |
| 5671 _updateOverlayPosition: function() { |
| 5672 if (this.isAttached) { |
| 5673 this.notifyResize(); |
| 5674 } |
| 5675 }, |
| 5676 _applyFocus: function() { |
| 5677 var focusTarget = this.focusTarget || this.containedElement; |
| 5678 if (focusTarget && this.opened && !this.noAutoFocus) { |
| 5679 focusTarget.focus(); |
| 5680 } else { |
| 5681 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
| 5682 } |
| 5683 } |
| 5684 }); |
| 5685 })(); |
| 5686 |
| 5687 Polymer({ |
| 5688 is: 'fade-in-animation', |
| 5689 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5690 configure: function(config) { |
| 5691 var node = config.node; |
| 5692 this._effect = new KeyframeEffect(node, [ { |
| 5693 opacity: '0' |
| 5694 }, { |
| 5695 opacity: '1' |
| 5696 } ], this.timingFromConfig(config)); |
| 5697 return this._effect; |
| 5698 } |
| 5699 }); |
| 5700 |
| 5701 Polymer({ |
| 5702 is: 'fade-out-animation', |
| 5703 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5704 configure: function(config) { |
| 5705 var node = config.node; |
| 5706 this._effect = new KeyframeEffect(node, [ { |
| 5707 opacity: '1' |
| 5708 }, { |
| 5709 opacity: '0' |
| 5710 } ], this.timingFromConfig(config)); |
| 5711 return this._effect; |
| 5712 } |
| 5713 }); |
| 5714 |
| 5715 Polymer({ |
| 5716 is: 'paper-menu-grow-height-animation', |
| 5717 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5718 configure: function(config) { |
| 5719 var node = config.node; |
| 5720 var rect = node.getBoundingClientRect(); |
| 5721 var height = rect.height; |
| 5722 this._effect = new KeyframeEffect(node, [ { |
| 5723 height: height / 2 + 'px' |
| 5724 }, { |
| 5725 height: height + 'px' |
| 5726 } ], this.timingFromConfig(config)); |
| 5727 return this._effect; |
| 5728 } |
| 5729 }); |
| 5730 |
| 5731 Polymer({ |
| 5732 is: 'paper-menu-grow-width-animation', |
| 5733 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5734 configure: function(config) { |
| 5735 var node = config.node; |
| 5736 var rect = node.getBoundingClientRect(); |
| 5737 var width = rect.width; |
| 5738 this._effect = new KeyframeEffect(node, [ { |
| 5739 width: width / 2 + 'px' |
| 5740 }, { |
| 5741 width: width + 'px' |
| 5742 } ], this.timingFromConfig(config)); |
| 5743 return this._effect; |
| 5744 } |
| 5745 }); |
| 5746 |
| 5747 Polymer({ |
| 5748 is: 'paper-menu-shrink-width-animation', |
| 5749 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5750 configure: function(config) { |
| 5751 var node = config.node; |
| 5752 var rect = node.getBoundingClientRect(); |
| 5753 var width = rect.width; |
| 5754 this._effect = new KeyframeEffect(node, [ { |
| 5755 width: width + 'px' |
| 5756 }, { |
| 5757 width: width - width / 20 + 'px' |
| 5758 } ], this.timingFromConfig(config)); |
| 5759 return this._effect; |
| 5760 } |
| 5761 }); |
| 5762 |
| 5763 Polymer({ |
| 5764 is: 'paper-menu-shrink-height-animation', |
| 5765 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5766 configure: function(config) { |
| 5767 var node = config.node; |
| 5768 var rect = node.getBoundingClientRect(); |
| 5769 var height = rect.height; |
| 5770 var top = rect.top; |
| 5771 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
| 5772 this._effect = new KeyframeEffect(node, [ { |
| 5773 height: height + 'px', |
| 5774 transform: 'translateY(0)' |
| 5775 }, { |
| 5776 height: height / 2 + 'px', |
| 5777 transform: 'translateY(-20px)' |
| 5778 } ], this.timingFromConfig(config)); |
| 5779 return this._effect; |
| 5780 } |
| 5781 }); |
| 5782 |
| 5783 (function() { |
| 5784 'use strict'; |
| 5785 var config = { |
| 5786 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', |
| 5787 MAX_ANIMATION_TIME_MS: 400 |
| 5788 }; |
| 5789 var PaperMenuButton = Polymer({ |
| 5790 is: 'paper-menu-button', |
| 5791 behaviors: [ Polymer.IronA11yKeysBehavior, Polymer.IronControlState ], |
| 5792 properties: { |
| 5793 opened: { |
| 5794 type: Boolean, |
| 5795 value: false, |
| 5796 notify: true, |
| 5797 observer: '_openedChanged' |
| 5798 }, |
| 5799 horizontalAlign: { |
| 5800 type: String, |
| 5801 value: 'left', |
| 5802 reflectToAttribute: true |
| 5803 }, |
| 5804 verticalAlign: { |
| 5805 type: String, |
| 5806 value: 'top', |
| 5807 reflectToAttribute: true |
| 5808 }, |
| 5809 dynamicAlign: { |
| 5810 type: Boolean |
| 5811 }, |
| 5812 horizontalOffset: { |
| 5813 type: Number, |
| 5814 value: 0, |
| 5815 notify: true |
| 5816 }, |
| 5817 verticalOffset: { |
| 5818 type: Number, |
| 5819 value: 0, |
| 5820 notify: true |
| 5821 }, |
| 5822 noOverlap: { |
| 5823 type: Boolean |
| 5824 }, |
| 5825 noAnimations: { |
| 5826 type: Boolean, |
| 5827 value: false |
| 5828 }, |
| 5829 ignoreSelect: { |
| 5830 type: Boolean, |
| 5831 value: false |
| 5832 }, |
| 5833 closeOnActivate: { |
| 5834 type: Boolean, |
| 5835 value: false |
| 5836 }, |
| 5837 openAnimationConfig: { |
9487 type: Object, | 5838 type: Object, |
9488 value: function() { | 5839 value: function() { |
9489 return { | 5840 return [ { |
9490 duration: 500, | 5841 name: 'fade-in-animation', |
9491 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | 5842 timing: { |
9492 fill: 'both' | 5843 delay: 100, |
9493 } | 5844 duration: 200 |
9494 } | 5845 } |
9495 } | 5846 }, { |
9496 | 5847 name: 'paper-menu-grow-width-animation', |
9497 }, | 5848 timing: { |
9498 | 5849 delay: 100, |
9499 /** | 5850 duration: 150, |
9500 * Can be used to determine that elements implement this behavior. | 5851 easing: config.ANIMATION_CUBIC_BEZIER |
9501 */ | 5852 } |
9502 isNeonAnimation: true, | 5853 }, { |
9503 | 5854 name: 'paper-menu-grow-height-animation', |
9504 /** | 5855 timing: { |
9505 * Do any animation configuration here. | 5856 delay: 100, |
9506 */ | 5857 duration: 275, |
9507 // configure: function(config) { | 5858 easing: config.ANIMATION_CUBIC_BEZIER |
9508 // }, | 5859 } |
9509 | 5860 } ]; |
9510 /** | 5861 } |
9511 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | 5862 }, |
9512 * by the animation. | 5863 closeAnimationConfig: { |
9513 */ | 5864 type: Object, |
9514 timingFromConfig: function(config) { | 5865 value: function() { |
9515 if (config.timing) { | 5866 return [ { |
9516 for (var property in config.timing) { | 5867 name: 'fade-out-animation', |
9517 this.animationTiming[property] = config.timing[property]; | 5868 timing: { |
9518 } | 5869 duration: 150 |
9519 } | 5870 } |
9520 return this.animationTiming; | 5871 }, { |
9521 }, | 5872 name: 'paper-menu-shrink-width-animation', |
9522 | 5873 timing: { |
9523 /** | 5874 delay: 100, |
9524 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | 5875 duration: 50, |
9525 */ | 5876 easing: config.ANIMATION_CUBIC_BEZIER |
9526 setPrefixedProperty: function(node, property, value) { | 5877 } |
9527 var map = { | 5878 }, { |
9528 'transform': ['webkitTransform'], | 5879 name: 'paper-menu-shrink-height-animation', |
9529 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | 5880 timing: { |
9530 }; | 5881 duration: 200, |
9531 var prefixes = map[property]; | 5882 easing: 'ease-in' |
9532 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | 5883 } |
9533 node.style[prefix] = value; | 5884 } ]; |
9534 } | 5885 } |
9535 node.style[property] = value; | 5886 }, |
9536 }, | 5887 allowOutsideScroll: { |
9537 | 5888 type: Boolean, |
9538 /** | 5889 value: false |
9539 * Called when the animation finishes. | 5890 }, |
9540 */ | 5891 restoreFocusOnClose: { |
9541 complete: function() {} | 5892 type: Boolean, |
9542 | 5893 value: true |
9543 }; | 5894 }, |
| 5895 _dropdownContent: { |
| 5896 type: Object |
| 5897 } |
| 5898 }, |
| 5899 hostAttributes: { |
| 5900 role: 'group', |
| 5901 'aria-haspopup': 'true' |
| 5902 }, |
| 5903 listeners: { |
| 5904 'iron-activate': '_onIronActivate', |
| 5905 'iron-select': '_onIronSelect' |
| 5906 }, |
| 5907 get contentElement() { |
| 5908 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5909 }, |
| 5910 toggle: function() { |
| 5911 if (this.opened) { |
| 5912 this.close(); |
| 5913 } else { |
| 5914 this.open(); |
| 5915 } |
| 5916 }, |
| 5917 open: function() { |
| 5918 if (this.disabled) { |
| 5919 return; |
| 5920 } |
| 5921 this.$.dropdown.open(); |
| 5922 }, |
| 5923 close: function() { |
| 5924 this.$.dropdown.close(); |
| 5925 }, |
| 5926 _onIronSelect: function(event) { |
| 5927 if (!this.ignoreSelect) { |
| 5928 this.close(); |
| 5929 } |
| 5930 }, |
| 5931 _onIronActivate: function(event) { |
| 5932 if (this.closeOnActivate) { |
| 5933 this.close(); |
| 5934 } |
| 5935 }, |
| 5936 _openedChanged: function(opened, oldOpened) { |
| 5937 if (opened) { |
| 5938 this._dropdownContent = this.contentElement; |
| 5939 this.fire('paper-dropdown-open'); |
| 5940 } else if (oldOpened != null) { |
| 5941 this.fire('paper-dropdown-close'); |
| 5942 } |
| 5943 }, |
| 5944 _disabledChanged: function(disabled) { |
| 5945 Polymer.IronControlState._disabledChanged.apply(this, arguments); |
| 5946 if (disabled && this.opened) { |
| 5947 this.close(); |
| 5948 } |
| 5949 }, |
| 5950 __onIronOverlayCanceled: function(event) { |
| 5951 var uiEvent = event.detail; |
| 5952 var target = Polymer.dom(uiEvent).rootTarget; |
| 5953 var trigger = this.$.trigger; |
| 5954 var path = Polymer.dom(uiEvent).path; |
| 5955 if (path.indexOf(trigger) > -1) { |
| 5956 event.preventDefault(); |
| 5957 } |
| 5958 } |
| 5959 }); |
| 5960 Object.keys(config).forEach(function(key) { |
| 5961 PaperMenuButton[key] = config[key]; |
| 5962 }); |
| 5963 Polymer.PaperMenuButton = PaperMenuButton; |
| 5964 })(); |
| 5965 |
| 5966 Polymer.PaperInkyFocusBehaviorImpl = { |
| 5967 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ], |
| 5968 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 5969 if (receivedFocusFromKeyboard) { |
| 5970 this.ensureRipple(); |
| 5971 } |
| 5972 if (this.hasRipple()) { |
| 5973 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 5974 } |
| 5975 }, |
| 5976 _createRipple: function() { |
| 5977 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 5978 ripple.id = 'ink'; |
| 5979 ripple.setAttribute('center', ''); |
| 5980 ripple.classList.add('circle'); |
| 5981 return ripple; |
| 5982 } |
| 5983 }; |
| 5984 |
| 5985 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS
tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ]; |
| 5986 |
9544 Polymer({ | 5987 Polymer({ |
9545 | 5988 is: 'paper-icon-button', |
9546 is: 'opaque-animation', | 5989 hostAttributes: { |
9547 | 5990 role: 'button', |
9548 behaviors: [ | 5991 tabindex: '0' |
9549 Polymer.NeonAnimationBehavior | 5992 }, |
9550 ], | 5993 behaviors: [ Polymer.PaperInkyFocusBehavior ], |
9551 | 5994 properties: { |
9552 configure: function(config) { | 5995 src: { |
9553 var node = config.node; | 5996 type: String |
9554 this._effect = new KeyframeEffect(node, [ | 5997 }, |
9555 {'opacity': '1'}, | 5998 icon: { |
9556 {'opacity': '1'} | 5999 type: String |
9557 ], this.timingFromConfig(config)); | 6000 }, |
9558 node.style.opacity = '0'; | 6001 alt: { |
9559 return this._effect; | 6002 type: String, |
9560 }, | 6003 observer: "_altChanged" |
9561 | 6004 } |
9562 complete: function(config) { | 6005 }, |
9563 config.node.style.opacity = ''; | 6006 _altChanged: function(newValue, oldValue) { |
9564 } | 6007 var label = this.getAttribute('aria-label'); |
9565 | 6008 if (!label || oldValue == label) { |
9566 }); | 6009 this.setAttribute('aria-label', newValue); |
9567 (function() { | 6010 } |
9568 'use strict'; | 6011 } |
9569 // Used to calculate the scroll direction during touch events. | 6012 }); |
9570 var LAST_TOUCH_POSITION = { | 6013 |
9571 pageX: 0, | |
9572 pageY: 0 | |
9573 }; | |
9574 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
9575 var ROOT_TARGET = null; | |
9576 var SCROLLABLE_NODES = []; | |
9577 | |
9578 /** | |
9579 * The IronDropdownScrollManager is intended to provide a central source | |
9580 * of authority and control over which elements in a document are currently | |
9581 * allowed to scroll. | |
9582 */ | |
9583 | |
9584 Polymer.IronDropdownScrollManager = { | |
9585 | |
9586 /** | |
9587 * The current element that defines the DOM boundaries of the | |
9588 * scroll lock. This is always the most recently locking element. | |
9589 */ | |
9590 get currentLockingElement() { | |
9591 return this._lockingElements[this._lockingElements.length - 1]; | |
9592 }, | |
9593 | |
9594 /** | |
9595 * Returns true if the provided element is "scroll locked", which is to | |
9596 * say that it cannot be scrolled via pointer or keyboard interactions. | |
9597 * | |
9598 * @param {HTMLElement} element An HTML element instance which may or may | |
9599 * not be scroll locked. | |
9600 */ | |
9601 elementIsScrollLocked: function(element) { | |
9602 var currentLockingElement = this.currentLockingElement; | |
9603 | |
9604 if (currentLockingElement === undefined) | |
9605 return false; | |
9606 | |
9607 var scrollLocked; | |
9608 | |
9609 if (this._hasCachedLockedElement(element)) { | |
9610 return true; | |
9611 } | |
9612 | |
9613 if (this._hasCachedUnlockedElement(element)) { | |
9614 return false; | |
9615 } | |
9616 | |
9617 scrollLocked = !!currentLockingElement && | |
9618 currentLockingElement !== element && | |
9619 !this._composedTreeContains(currentLockingElement, element); | |
9620 | |
9621 if (scrollLocked) { | |
9622 this._lockedElementCache.push(element); | |
9623 } else { | |
9624 this._unlockedElementCache.push(element); | |
9625 } | |
9626 | |
9627 return scrollLocked; | |
9628 }, | |
9629 | |
9630 /** | |
9631 * Push an element onto the current scroll lock stack. The most recently | |
9632 * pushed element and its children will be considered scrollable. All | |
9633 * other elements will not be scrollable. | |
9634 * | |
9635 * Scroll locking is implemented as a stack so that cases such as | |
9636 * dropdowns within dropdowns are handled well. | |
9637 * | |
9638 * @param {HTMLElement} element The element that should lock scroll. | |
9639 */ | |
9640 pushScrollLock: function(element) { | |
9641 // Prevent pushing the same element twice | |
9642 if (this._lockingElements.indexOf(element) >= 0) { | |
9643 return; | |
9644 } | |
9645 | |
9646 if (this._lockingElements.length === 0) { | |
9647 this._lockScrollInteractions(); | |
9648 } | |
9649 | |
9650 this._lockingElements.push(element); | |
9651 | |
9652 this._lockedElementCache = []; | |
9653 this._unlockedElementCache = []; | |
9654 }, | |
9655 | |
9656 /** | |
9657 * Remove an element from the scroll lock stack. The element being | |
9658 * removed does not need to be the most recently pushed element. However, | |
9659 * the scroll lock constraints only change when the most recently pushed | |
9660 * element is removed. | |
9661 * | |
9662 * @param {HTMLElement} element The element to remove from the scroll | |
9663 * lock stack. | |
9664 */ | |
9665 removeScrollLock: function(element) { | |
9666 var index = this._lockingElements.indexOf(element); | |
9667 | |
9668 if (index === -1) { | |
9669 return; | |
9670 } | |
9671 | |
9672 this._lockingElements.splice(index, 1); | |
9673 | |
9674 this._lockedElementCache = []; | |
9675 this._unlockedElementCache = []; | |
9676 | |
9677 if (this._lockingElements.length === 0) { | |
9678 this._unlockScrollInteractions(); | |
9679 } | |
9680 }, | |
9681 | |
9682 _lockingElements: [], | |
9683 | |
9684 _lockedElementCache: null, | |
9685 | |
9686 _unlockedElementCache: null, | |
9687 | |
9688 _hasCachedLockedElement: function(element) { | |
9689 return this._lockedElementCache.indexOf(element) > -1; | |
9690 }, | |
9691 | |
9692 _hasCachedUnlockedElement: function(element) { | |
9693 return this._unlockedElementCache.indexOf(element) > -1; | |
9694 }, | |
9695 | |
9696 _composedTreeContains: function(element, child) { | |
9697 // NOTE(cdata): This method iterates over content elements and their | |
9698 // corresponding distributed nodes to implement a contains-like method | |
9699 // that pierces through the composed tree of the ShadowDOM. Results of | |
9700 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
9701 // guard against potentially expensive lookups happening repeatedly as | |
9702 // a user scrolls / touchmoves. | |
9703 var contentElements; | |
9704 var distributedNodes; | |
9705 var contentIndex; | |
9706 var nodeIndex; | |
9707 | |
9708 if (element.contains(child)) { | |
9709 return true; | |
9710 } | |
9711 | |
9712 contentElements = Polymer.dom(element).querySelectorAll('content'); | |
9713 | |
9714 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | |
9715 | |
9716 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | |
9717 | |
9718 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | |
9719 | |
9720 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | |
9721 return true; | |
9722 } | |
9723 } | |
9724 } | |
9725 | |
9726 return false; | |
9727 }, | |
9728 | |
9729 _scrollInteractionHandler: function(event) { | |
9730 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
9731 // progress and cannot be interrupted. | |
9732 if (event.cancelable && this._shouldPreventScrolling(event)) { | |
9733 event.preventDefault(); | |
9734 } | |
9735 // If event has targetTouches (touch event), update last touch position. | |
9736 if (event.targetTouches) { | |
9737 var touch = event.targetTouches[0]; | |
9738 LAST_TOUCH_POSITION.pageX = touch.pageX; | |
9739 LAST_TOUCH_POSITION.pageY = touch.pageY; | |
9740 } | |
9741 }, | |
9742 | |
9743 _lockScrollInteractions: function() { | |
9744 this._boundScrollHandler = this._boundScrollHandler || | |
9745 this._scrollInteractionHandler.bind(this); | |
9746 // Modern `wheel` event for mouse wheel scrolling: | |
9747 document.addEventListener('wheel', this._boundScrollHandler, true); | |
9748 // Older, non-standard `mousewheel` event for some FF: | |
9749 document.addEventListener('mousewheel', this._boundScrollHandler, true); | |
9750 // IE: | |
9751 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | |
9752 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
9753 document.addEventListener('touchstart', this._boundScrollHandler, true); | |
9754 // Mobile devices can scroll on touch move: | |
9755 document.addEventListener('touchmove', this._boundScrollHandler, true); | |
9756 }, | |
9757 | |
9758 _unlockScrollInteractions: function() { | |
9759 document.removeEventListener('wheel', this._boundScrollHandler, true); | |
9760 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | |
9761 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | |
9762 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | |
9763 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | |
9764 }, | |
9765 | |
9766 /** | |
9767 * Returns true if the event causes scroll outside the current locking | |
9768 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
9769 * outside the locking element when it is already at its scroll boundaries
. | |
9770 * @param {!Event} event | |
9771 * @return {boolean} | |
9772 * @private | |
9773 */ | |
9774 _shouldPreventScrolling: function(event) { | |
9775 | |
9776 // Update if root target changed. For touch events, ensure we don't | |
9777 // update during touchmove. | |
9778 var target = Polymer.dom(event).rootTarget; | |
9779 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | |
9780 ROOT_TARGET = target; | |
9781 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | |
9782 } | |
9783 | |
9784 // Prevent event if no scrollable nodes. | |
9785 if (!SCROLLABLE_NODES.length) { | |
9786 return true; | |
9787 } | |
9788 // Don't prevent touchstart event inside the locking element when it has | |
9789 // scrollable nodes. | |
9790 if (event.type === 'touchstart') { | |
9791 return false; | |
9792 } | |
9793 // Get deltaX/Y. | |
9794 var info = this._getScrollInfo(event); | |
9795 // Prevent if there is no child that can scroll. | |
9796 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | |
9797 }, | |
9798 | |
9799 /** | |
9800 * Returns an array of scrollable nodes up to the current locking element, | |
9801 * which is included too if scrollable. | |
9802 * @param {!Array<Node>} nodes | |
9803 * @return {Array<Node>} scrollables | |
9804 * @private | |
9805 */ | |
9806 _getScrollableNodes: function(nodes) { | |
9807 var scrollables = []; | |
9808 var lockingIndex = nodes.indexOf(this.currentLockingElement); | |
9809 // Loop from root target to locking element (included). | |
9810 for (var i = 0; i <= lockingIndex; i++) { | |
9811 var node = nodes[i]; | |
9812 // Skip document fragments. | |
9813 if (node.nodeType === 11) { | |
9814 continue; | |
9815 } | |
9816 // Check inline style before checking computed style. | |
9817 var style = node.style; | |
9818 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | |
9819 style = window.getComputedStyle(node); | |
9820 } | |
9821 if (style.overflow === 'scroll' || style.overflow === 'auto') { | |
9822 scrollables.push(node); | |
9823 } | |
9824 } | |
9825 return scrollables; | |
9826 }, | |
9827 | |
9828 /** | |
9829 * Returns the node that is scrolling. If there is no scrolling, | |
9830 * returns undefined. | |
9831 * @param {!Array<Node>} nodes | |
9832 * @param {number} deltaX Scroll delta on the x-axis | |
9833 * @param {number} deltaY Scroll delta on the y-axis | |
9834 * @return {Node|undefined} | |
9835 * @private | |
9836 */ | |
9837 _getScrollingNode: function(nodes, deltaX, deltaY) { | |
9838 // No scroll. | |
9839 if (!deltaX && !deltaY) { | |
9840 return; | |
9841 } | |
9842 // Check only one axis according to where there is more scroll. | |
9843 // Prefer vertical to horizontal. | |
9844 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | |
9845 for (var i = 0; i < nodes.length; i++) { | |
9846 var node = nodes[i]; | |
9847 var canScroll = false; | |
9848 if (verticalScroll) { | |
9849 // delta < 0 is scroll up, delta > 0 is scroll down. | |
9850 canScroll = deltaY < 0 ? node.scrollTop > 0 : | |
9851 node.scrollTop < node.scrollHeight - node.clientHeight; | |
9852 } else { | |
9853 // delta < 0 is scroll left, delta > 0 is scroll right. | |
9854 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | |
9855 node.scrollLeft < node.scrollWidth - node.clientWidth; | |
9856 } | |
9857 if (canScroll) { | |
9858 return node; | |
9859 } | |
9860 } | |
9861 }, | |
9862 | |
9863 /** | |
9864 * Returns scroll `deltaX` and `deltaY`. | |
9865 * @param {!Event} event The scroll event | |
9866 * @return {{ | |
9867 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
9868 * negative: scroll left, 0: no scroll), | |
9869 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
9870 * negative: scroll up, 0: no scroll) | |
9871 * }} info | |
9872 * @private | |
9873 */ | |
9874 _getScrollInfo: function(event) { | |
9875 var info = { | |
9876 deltaX: event.deltaX, | |
9877 deltaY: event.deltaY | |
9878 }; | |
9879 // Already available. | |
9880 if ('deltaX' in event) { | |
9881 // do nothing, values are already good. | |
9882 } | |
9883 // Safari has scroll info in `wheelDeltaX/Y`. | |
9884 else if ('wheelDeltaX' in event) { | |
9885 info.deltaX = -event.wheelDeltaX; | |
9886 info.deltaY = -event.wheelDeltaY; | |
9887 } | |
9888 // Firefox has scroll info in `detail` and `axis`. | |
9889 else if ('axis' in event) { | |
9890 info.deltaX = event.axis === 1 ? event.detail : 0; | |
9891 info.deltaY = event.axis === 2 ? event.detail : 0; | |
9892 } | |
9893 // On mobile devices, calculate scroll direction. | |
9894 else if (event.targetTouches) { | |
9895 var touch = event.targetTouches[0]; | |
9896 // Touch moves from right to left => scrolling goes right. | |
9897 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | |
9898 // Touch moves from down to up => scrolling goes down. | |
9899 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | |
9900 } | |
9901 return info; | |
9902 } | |
9903 }; | |
9904 })(); | |
9905 (function() { | |
9906 'use strict'; | |
9907 | |
9908 Polymer({ | |
9909 is: 'iron-dropdown', | |
9910 | |
9911 behaviors: [ | |
9912 Polymer.IronControlState, | |
9913 Polymer.IronA11yKeysBehavior, | |
9914 Polymer.IronOverlayBehavior, | |
9915 Polymer.NeonAnimationRunnerBehavior | |
9916 ], | |
9917 | |
9918 properties: { | |
9919 /** | |
9920 * The orientation against which to align the dropdown content | |
9921 * horizontally relative to the dropdown trigger. | |
9922 * Overridden from `Polymer.IronFitBehavior`. | |
9923 */ | |
9924 horizontalAlign: { | |
9925 type: String, | |
9926 value: 'left', | |
9927 reflectToAttribute: true | |
9928 }, | |
9929 | |
9930 /** | |
9931 * The orientation against which to align the dropdown content | |
9932 * vertically relative to the dropdown trigger. | |
9933 * Overridden from `Polymer.IronFitBehavior`. | |
9934 */ | |
9935 verticalAlign: { | |
9936 type: String, | |
9937 value: 'top', | |
9938 reflectToAttribute: true | |
9939 }, | |
9940 | |
9941 /** | |
9942 * An animation config. If provided, this will be used to animate the | |
9943 * opening of the dropdown. | |
9944 */ | |
9945 openAnimationConfig: { | |
9946 type: Object | |
9947 }, | |
9948 | |
9949 /** | |
9950 * An animation config. If provided, this will be used to animate the | |
9951 * closing of the dropdown. | |
9952 */ | |
9953 closeAnimationConfig: { | |
9954 type: Object | |
9955 }, | |
9956 | |
9957 /** | |
9958 * If provided, this will be the element that will be focused when | |
9959 * the dropdown opens. | |
9960 */ | |
9961 focusTarget: { | |
9962 type: Object | |
9963 }, | |
9964 | |
9965 /** | |
9966 * Set to true to disable animations when opening and closing the | |
9967 * dropdown. | |
9968 */ | |
9969 noAnimations: { | |
9970 type: Boolean, | |
9971 value: false | |
9972 }, | |
9973 | |
9974 /** | |
9975 * By default, the dropdown will constrain scrolling on the page | |
9976 * to itself when opened. | |
9977 * Set to true in order to prevent scroll from being constrained | |
9978 * to the dropdown when it opens. | |
9979 */ | |
9980 allowOutsideScroll: { | |
9981 type: Boolean, | |
9982 value: false | |
9983 }, | |
9984 | |
9985 /** | |
9986 * Callback for scroll events. | |
9987 * @type {Function} | |
9988 * @private | |
9989 */ | |
9990 _boundOnCaptureScroll: { | |
9991 type: Function, | |
9992 value: function() { | |
9993 return this._onCaptureScroll.bind(this); | |
9994 } | |
9995 } | |
9996 }, | |
9997 | |
9998 listeners: { | |
9999 'neon-animation-finish': '_onNeonAnimationFinish' | |
10000 }, | |
10001 | |
10002 observers: [ | |
10003 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | |
10004 ], | |
10005 | |
10006 /** | |
10007 * The element that is contained by the dropdown, if any. | |
10008 */ | |
10009 get containedElement() { | |
10010 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10011 }, | |
10012 | |
10013 /** | |
10014 * The element that should be focused when the dropdown opens. | |
10015 * @deprecated | |
10016 */ | |
10017 get _focusTarget() { | |
10018 return this.focusTarget || this.containedElement; | |
10019 }, | |
10020 | |
10021 ready: function() { | |
10022 // Memoized scrolling position, used to block scrolling outside. | |
10023 this._scrollTop = 0; | |
10024 this._scrollLeft = 0; | |
10025 // Used to perform a non-blocking refit on scroll. | |
10026 this._refitOnScrollRAF = null; | |
10027 }, | |
10028 | |
10029 detached: function() { | |
10030 this.cancelAnimation(); | |
10031 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10032 }, | |
10033 | |
10034 /** | |
10035 * Called when the value of `opened` changes. | |
10036 * Overridden from `IronOverlayBehavior` | |
10037 */ | |
10038 _openedChanged: function() { | |
10039 if (this.opened && this.disabled) { | |
10040 this.cancel(); | |
10041 } else { | |
10042 this.cancelAnimation(); | |
10043 this.sizingTarget = this.containedElement || this.sizingTarget; | |
10044 this._updateAnimationConfig(); | |
10045 this._saveScrollPosition(); | |
10046 if (this.opened) { | |
10047 document.addEventListener('scroll', this._boundOnCaptureScroll); | |
10048 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | |
10049 } else { | |
10050 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | |
10051 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10052 } | |
10053 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | |
10054 } | |
10055 }, | |
10056 | |
10057 /** | |
10058 * Overridden from `IronOverlayBehavior`. | |
10059 */ | |
10060 _renderOpened: function() { | |
10061 if (!this.noAnimations && this.animationConfig.open) { | |
10062 this.$.contentWrapper.classList.add('animating'); | |
10063 this.playAnimation('open'); | |
10064 } else { | |
10065 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | |
10066 } | |
10067 }, | |
10068 | |
10069 /** | |
10070 * Overridden from `IronOverlayBehavior`. | |
10071 */ | |
10072 _renderClosed: function() { | |
10073 | |
10074 if (!this.noAnimations && this.animationConfig.close) { | |
10075 this.$.contentWrapper.classList.add('animating'); | |
10076 this.playAnimation('close'); | |
10077 } else { | |
10078 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | |
10079 } | |
10080 }, | |
10081 | |
10082 /** | |
10083 * Called when animation finishes on the dropdown (when opening or | |
10084 * closing). Responsible for "completing" the process of opening or | |
10085 * closing the dropdown by positioning it or setting its display to | |
10086 * none. | |
10087 */ | |
10088 _onNeonAnimationFinish: function() { | |
10089 this.$.contentWrapper.classList.remove('animating'); | |
10090 if (this.opened) { | |
10091 this._finishRenderOpened(); | |
10092 } else { | |
10093 this._finishRenderClosed(); | |
10094 } | |
10095 }, | |
10096 | |
10097 _onCaptureScroll: function() { | |
10098 if (!this.allowOutsideScroll) { | |
10099 this._restoreScrollPosition(); | |
10100 } else { | |
10101 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | |
10102 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | |
10103 } | |
10104 }, | |
10105 | |
10106 /** | |
10107 * Memoizes the scroll position of the outside scrolling element. | |
10108 * @private | |
10109 */ | |
10110 _saveScrollPosition: function() { | |
10111 if (document.scrollingElement) { | |
10112 this._scrollTop = document.scrollingElement.scrollTop; | |
10113 this._scrollLeft = document.scrollingElement.scrollLeft; | |
10114 } else { | |
10115 // Since we don't know if is the body or html, get max. | |
10116 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | |
10117 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | |
10118 } | |
10119 }, | |
10120 | |
10121 /** | |
10122 * Resets the scroll position of the outside scrolling element. | |
10123 * @private | |
10124 */ | |
10125 _restoreScrollPosition: function() { | |
10126 if (document.scrollingElement) { | |
10127 document.scrollingElement.scrollTop = this._scrollTop; | |
10128 document.scrollingElement.scrollLeft = this._scrollLeft; | |
10129 } else { | |
10130 // Since we don't know if is the body or html, set both. | |
10131 document.documentElement.scrollTop = this._scrollTop; | |
10132 document.documentElement.scrollLeft = this._scrollLeft; | |
10133 document.body.scrollTop = this._scrollTop; | |
10134 document.body.scrollLeft = this._scrollLeft; | |
10135 } | |
10136 }, | |
10137 | |
10138 /** | |
10139 * Constructs the final animation config from different properties used | |
10140 * to configure specific parts of the opening and closing animations. | |
10141 */ | |
10142 _updateAnimationConfig: function() { | |
10143 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | |
10144 for (var i = 0; i < animations.length; i++) { | |
10145 animations[i].node = this.containedElement; | |
10146 } | |
10147 this.animationConfig = { | |
10148 open: this.openAnimationConfig, | |
10149 close: this.closeAnimationConfig | |
10150 }; | |
10151 }, | |
10152 | |
10153 /** | |
10154 * Updates the overlay position based on configured horizontal | |
10155 * and vertical alignment. | |
10156 */ | |
10157 _updateOverlayPosition: function() { | |
10158 if (this.isAttached) { | |
10159 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
10160 this.notifyResize(); | |
10161 } | |
10162 }, | |
10163 | |
10164 /** | |
10165 * Apply focus to focusTarget or containedElement | |
10166 */ | |
10167 _applyFocus: function () { | |
10168 var focusTarget = this.focusTarget || this.containedElement; | |
10169 if (focusTarget && this.opened && !this.noAutoFocus) { | |
10170 focusTarget.focus(); | |
10171 } else { | |
10172 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | |
10173 } | |
10174 } | |
10175 }); | |
10176 })(); | |
10177 Polymer({ | |
10178 | |
10179 is: 'fade-in-animation', | |
10180 | |
10181 behaviors: [ | |
10182 Polymer.NeonAnimationBehavior | |
10183 ], | |
10184 | |
10185 configure: function(config) { | |
10186 var node = config.node; | |
10187 this._effect = new KeyframeEffect(node, [ | |
10188 {'opacity': '0'}, | |
10189 {'opacity': '1'} | |
10190 ], this.timingFromConfig(config)); | |
10191 return this._effect; | |
10192 } | |
10193 | |
10194 }); | |
10195 Polymer({ | |
10196 | |
10197 is: 'fade-out-animation', | |
10198 | |
10199 behaviors: [ | |
10200 Polymer.NeonAnimationBehavior | |
10201 ], | |
10202 | |
10203 configure: function(config) { | |
10204 var node = config.node; | |
10205 this._effect = new KeyframeEffect(node, [ | |
10206 {'opacity': '1'}, | |
10207 {'opacity': '0'} | |
10208 ], this.timingFromConfig(config)); | |
10209 return this._effect; | |
10210 } | |
10211 | |
10212 }); | |
10213 Polymer({ | |
10214 is: 'paper-menu-grow-height-animation', | |
10215 | |
10216 behaviors: [ | |
10217 Polymer.NeonAnimationBehavior | |
10218 ], | |
10219 | |
10220 configure: function(config) { | |
10221 var node = config.node; | |
10222 var rect = node.getBoundingClientRect(); | |
10223 var height = rect.height; | |
10224 | |
10225 this._effect = new KeyframeEffect(node, [{ | |
10226 height: (height / 2) + 'px' | |
10227 }, { | |
10228 height: height + 'px' | |
10229 }], this.timingFromConfig(config)); | |
10230 | |
10231 return this._effect; | |
10232 } | |
10233 }); | |
10234 | |
10235 Polymer({ | |
10236 is: 'paper-menu-grow-width-animation', | |
10237 | |
10238 behaviors: [ | |
10239 Polymer.NeonAnimationBehavior | |
10240 ], | |
10241 | |
10242 configure: function(config) { | |
10243 var node = config.node; | |
10244 var rect = node.getBoundingClientRect(); | |
10245 var width = rect.width; | |
10246 | |
10247 this._effect = new KeyframeEffect(node, [{ | |
10248 width: (width / 2) + 'px' | |
10249 }, { | |
10250 width: width + 'px' | |
10251 }], this.timingFromConfig(config)); | |
10252 | |
10253 return this._effect; | |
10254 } | |
10255 }); | |
10256 | |
10257 Polymer({ | |
10258 is: 'paper-menu-shrink-width-animation', | |
10259 | |
10260 behaviors: [ | |
10261 Polymer.NeonAnimationBehavior | |
10262 ], | |
10263 | |
10264 configure: function(config) { | |
10265 var node = config.node; | |
10266 var rect = node.getBoundingClientRect(); | |
10267 var width = rect.width; | |
10268 | |
10269 this._effect = new KeyframeEffect(node, [{ | |
10270 width: width + 'px' | |
10271 }, { | |
10272 width: width - (width / 20) + 'px' | |
10273 }], this.timingFromConfig(config)); | |
10274 | |
10275 return this._effect; | |
10276 } | |
10277 }); | |
10278 | |
10279 Polymer({ | |
10280 is: 'paper-menu-shrink-height-animation', | |
10281 | |
10282 behaviors: [ | |
10283 Polymer.NeonAnimationBehavior | |
10284 ], | |
10285 | |
10286 configure: function(config) { | |
10287 var node = config.node; | |
10288 var rect = node.getBoundingClientRect(); | |
10289 var height = rect.height; | |
10290 var top = rect.top; | |
10291 | |
10292 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); | |
10293 | |
10294 this._effect = new KeyframeEffect(node, [{ | |
10295 height: height + 'px', | |
10296 transform: 'translateY(0)' | |
10297 }, { | |
10298 height: height / 2 + 'px', | |
10299 transform: 'translateY(-20px)' | |
10300 }], this.timingFromConfig(config)); | |
10301 | |
10302 return this._effect; | |
10303 } | |
10304 }); | |
10305 (function() { | |
10306 'use strict'; | |
10307 | |
10308 var config = { | |
10309 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | |
10310 MAX_ANIMATION_TIME_MS: 400 | |
10311 }; | |
10312 | |
10313 var PaperMenuButton = Polymer({ | |
10314 is: 'paper-menu-button', | |
10315 | |
10316 /** | |
10317 * Fired when the dropdown opens. | |
10318 * | |
10319 * @event paper-dropdown-open | |
10320 */ | |
10321 | |
10322 /** | |
10323 * Fired when the dropdown closes. | |
10324 * | |
10325 * @event paper-dropdown-close | |
10326 */ | |
10327 | |
10328 behaviors: [ | |
10329 Polymer.IronA11yKeysBehavior, | |
10330 Polymer.IronControlState | |
10331 ], | |
10332 | |
10333 properties: { | |
10334 /** | |
10335 * True if the content is currently displayed. | |
10336 */ | |
10337 opened: { | |
10338 type: Boolean, | |
10339 value: false, | |
10340 notify: true, | |
10341 observer: '_openedChanged' | |
10342 }, | |
10343 | |
10344 /** | |
10345 * The orientation against which to align the menu dropdown | |
10346 * horizontally relative to the dropdown trigger. | |
10347 */ | |
10348 horizontalAlign: { | |
10349 type: String, | |
10350 value: 'left', | |
10351 reflectToAttribute: true | |
10352 }, | |
10353 | |
10354 /** | |
10355 * The orientation against which to align the menu dropdown | |
10356 * vertically relative to the dropdown trigger. | |
10357 */ | |
10358 verticalAlign: { | |
10359 type: String, | |
10360 value: 'top', | |
10361 reflectToAttribute: true | |
10362 }, | |
10363 | |
10364 /** | |
10365 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
10366 * be considered preferences instead of strict requirements when | |
10367 * positioning the dropdown and may be changed if doing so reduces | |
10368 * the area of the dropdown falling outside of `fitInto`. | |
10369 */ | |
10370 dynamicAlign: { | |
10371 type: Boolean | |
10372 }, | |
10373 | |
10374 /** | |
10375 * A pixel value that will be added to the position calculated for the | |
10376 * given `horizontalAlign`. Use a negative value to offset to the | |
10377 * left, or a positive value to offset to the right. | |
10378 */ | |
10379 horizontalOffset: { | |
10380 type: Number, | |
10381 value: 0, | |
10382 notify: true | |
10383 }, | |
10384 | |
10385 /** | |
10386 * A pixel value that will be added to the position calculated for the | |
10387 * given `verticalAlign`. Use a negative value to offset towards the | |
10388 * top, or a positive value to offset towards the bottom. | |
10389 */ | |
10390 verticalOffset: { | |
10391 type: Number, | |
10392 value: 0, | |
10393 notify: true | |
10394 }, | |
10395 | |
10396 /** | |
10397 * If true, the dropdown will be positioned so that it doesn't overlap | |
10398 * the button. | |
10399 */ | |
10400 noOverlap: { | |
10401 type: Boolean | |
10402 }, | |
10403 | |
10404 /** | |
10405 * Set to true to disable animations when opening and closing the | |
10406 * dropdown. | |
10407 */ | |
10408 noAnimations: { | |
10409 type: Boolean, | |
10410 value: false | |
10411 }, | |
10412 | |
10413 /** | |
10414 * Set to true to disable automatically closing the dropdown after | |
10415 * a selection has been made. | |
10416 */ | |
10417 ignoreSelect: { | |
10418 type: Boolean, | |
10419 value: false | |
10420 }, | |
10421 | |
10422 /** | |
10423 * Set to true to enable automatically closing the dropdown after an | |
10424 * item has been activated, even if the selection did not change. | |
10425 */ | |
10426 closeOnActivate: { | |
10427 type: Boolean, | |
10428 value: false | |
10429 }, | |
10430 | |
10431 /** | |
10432 * An animation config. If provided, this will be used to animate the | |
10433 * opening of the dropdown. | |
10434 */ | |
10435 openAnimationConfig: { | |
10436 type: Object, | |
10437 value: function() { | |
10438 return [{ | |
10439 name: 'fade-in-animation', | |
10440 timing: { | |
10441 delay: 100, | |
10442 duration: 200 | |
10443 } | |
10444 }, { | |
10445 name: 'paper-menu-grow-width-animation', | |
10446 timing: { | |
10447 delay: 100, | |
10448 duration: 150, | |
10449 easing: config.ANIMATION_CUBIC_BEZIER | |
10450 } | |
10451 }, { | |
10452 name: 'paper-menu-grow-height-animation', | |
10453 timing: { | |
10454 delay: 100, | |
10455 duration: 275, | |
10456 easing: config.ANIMATION_CUBIC_BEZIER | |
10457 } | |
10458 }]; | |
10459 } | |
10460 }, | |
10461 | |
10462 /** | |
10463 * An animation config. If provided, this will be used to animate the | |
10464 * closing of the dropdown. | |
10465 */ | |
10466 closeAnimationConfig: { | |
10467 type: Object, | |
10468 value: function() { | |
10469 return [{ | |
10470 name: 'fade-out-animation', | |
10471 timing: { | |
10472 duration: 150 | |
10473 } | |
10474 }, { | |
10475 name: 'paper-menu-shrink-width-animation', | |
10476 timing: { | |
10477 delay: 100, | |
10478 duration: 50, | |
10479 easing: config.ANIMATION_CUBIC_BEZIER | |
10480 } | |
10481 }, { | |
10482 name: 'paper-menu-shrink-height-animation', | |
10483 timing: { | |
10484 duration: 200, | |
10485 easing: 'ease-in' | |
10486 } | |
10487 }]; | |
10488 } | |
10489 }, | |
10490 | |
10491 /** | |
10492 * By default, the dropdown will constrain scrolling on the page | |
10493 * to itself when opened. | |
10494 * Set to true in order to prevent scroll from being constrained | |
10495 * to the dropdown when it opens. | |
10496 */ | |
10497 allowOutsideScroll: { | |
10498 type: Boolean, | |
10499 value: false | |
10500 }, | |
10501 | |
10502 /** | |
10503 * Whether focus should be restored to the button when the menu closes
. | |
10504 */ | |
10505 restoreFocusOnClose: { | |
10506 type: Boolean, | |
10507 value: true | |
10508 }, | |
10509 | |
10510 /** | |
10511 * This is the element intended to be bound as the focus target | |
10512 * for the `iron-dropdown` contained by `paper-menu-button`. | |
10513 */ | |
10514 _dropdownContent: { | |
10515 type: Object | |
10516 } | |
10517 }, | |
10518 | |
10519 hostAttributes: { | |
10520 role: 'group', | |
10521 'aria-haspopup': 'true' | |
10522 }, | |
10523 | |
10524 listeners: { | |
10525 'iron-activate': '_onIronActivate', | |
10526 'iron-select': '_onIronSelect' | |
10527 }, | |
10528 | |
10529 /** | |
10530 * The content element that is contained by the menu button, if any. | |
10531 */ | |
10532 get contentElement() { | |
10533 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10534 }, | |
10535 | |
10536 /** | |
10537 * Toggles the drowpdown content between opened and closed. | |
10538 */ | |
10539 toggle: function() { | |
10540 if (this.opened) { | |
10541 this.close(); | |
10542 } else { | |
10543 this.open(); | |
10544 } | |
10545 }, | |
10546 | |
10547 /** | |
10548 * Make the dropdown content appear as an overlay positioned relative | |
10549 * to the dropdown trigger. | |
10550 */ | |
10551 open: function() { | |
10552 if (this.disabled) { | |
10553 return; | |
10554 } | |
10555 | |
10556 this.$.dropdown.open(); | |
10557 }, | |
10558 | |
10559 /** | |
10560 * Hide the dropdown content. | |
10561 */ | |
10562 close: function() { | |
10563 this.$.dropdown.close(); | |
10564 }, | |
10565 | |
10566 /** | |
10567 * When an `iron-select` event is received, the dropdown should | |
10568 * automatically close on the assumption that a value has been chosen. | |
10569 * | |
10570 * @param {CustomEvent} event A CustomEvent instance with type | |
10571 * set to `"iron-select"`. | |
10572 */ | |
10573 _onIronSelect: function(event) { | |
10574 if (!this.ignoreSelect) { | |
10575 this.close(); | |
10576 } | |
10577 }, | |
10578 | |
10579 /** | |
10580 * Closes the dropdown when an `iron-activate` event is received if | |
10581 * `closeOnActivate` is true. | |
10582 * | |
10583 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | |
10584 */ | |
10585 _onIronActivate: function(event) { | |
10586 if (this.closeOnActivate) { | |
10587 this.close(); | |
10588 } | |
10589 }, | |
10590 | |
10591 /** | |
10592 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | |
10593 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | |
10594 * | |
10595 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | |
10596 * @param {boolean} oldOpened The previous value of `opened`. | |
10597 */ | |
10598 _openedChanged: function(opened, oldOpened) { | |
10599 if (opened) { | |
10600 // TODO(cdata): Update this when we can measure changes in distribut
ed | |
10601 // children in an idiomatic way. | |
10602 // We poke this property in case the element has changed. This will | |
10603 // cause the focus target for the `iron-dropdown` to be updated as | |
10604 // necessary: | |
10605 this._dropdownContent = this.contentElement; | |
10606 this.fire('paper-dropdown-open'); | |
10607 } else if (oldOpened != null) { | |
10608 this.fire('paper-dropdown-close'); | |
10609 } | |
10610 }, | |
10611 | |
10612 /** | |
10613 * If the dropdown is open when disabled becomes true, close the | |
10614 * dropdown. | |
10615 * | |
10616 * @param {boolean} disabled True if disabled, otherwise false. | |
10617 */ | |
10618 _disabledChanged: function(disabled) { | |
10619 Polymer.IronControlState._disabledChanged.apply(this, arguments); | |
10620 if (disabled && this.opened) { | |
10621 this.close(); | |
10622 } | |
10623 }, | |
10624 | |
10625 __onIronOverlayCanceled: function(event) { | |
10626 var uiEvent = event.detail; | |
10627 var target = Polymer.dom(uiEvent).rootTarget; | |
10628 var trigger = this.$.trigger; | |
10629 var path = Polymer.dom(uiEvent).path; | |
10630 | |
10631 if (path.indexOf(trigger) > -1) { | |
10632 event.preventDefault(); | |
10633 } | |
10634 } | |
10635 }); | |
10636 | |
10637 Object.keys(config).forEach(function (key) { | |
10638 PaperMenuButton[key] = config[key]; | |
10639 }); | |
10640 | |
10641 Polymer.PaperMenuButton = PaperMenuButton; | |
10642 })(); | |
10643 /** | |
10644 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | |
10645 * | |
10646 * @polymerBehavior Polymer.PaperInkyFocusBehavior | |
10647 */ | |
10648 Polymer.PaperInkyFocusBehaviorImpl = { | |
10649 observers: [ | |
10650 '_focusedChanged(receivedFocusFromKeyboard)' | |
10651 ], | |
10652 | |
10653 _focusedChanged: function(receivedFocusFromKeyboard) { | |
10654 if (receivedFocusFromKeyboard) { | |
10655 this.ensureRipple(); | |
10656 } | |
10657 if (this.hasRipple()) { | |
10658 this._ripple.holdDown = receivedFocusFromKeyboard; | |
10659 } | |
10660 }, | |
10661 | |
10662 _createRipple: function() { | |
10663 var ripple = Polymer.PaperRippleBehavior._createRipple(); | |
10664 ripple.id = 'ink'; | |
10665 ripple.setAttribute('center', ''); | |
10666 ripple.classList.add('circle'); | |
10667 return ripple; | |
10668 } | |
10669 }; | |
10670 | |
10671 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | |
10672 Polymer.PaperInkyFocusBehavior = [ | |
10673 Polymer.IronButtonState, | |
10674 Polymer.IronControlState, | |
10675 Polymer.PaperRippleBehavior, | |
10676 Polymer.PaperInkyFocusBehaviorImpl | |
10677 ]; | |
10678 Polymer({ | |
10679 is: 'paper-icon-button', | |
10680 | |
10681 hostAttributes: { | |
10682 role: 'button', | |
10683 tabindex: '0' | |
10684 }, | |
10685 | |
10686 behaviors: [ | |
10687 Polymer.PaperInkyFocusBehavior | |
10688 ], | |
10689 | |
10690 properties: { | |
10691 /** | |
10692 * The URL of an image for the icon. If the src property is specified, | |
10693 * the icon property should not be. | |
10694 */ | |
10695 src: { | |
10696 type: String | |
10697 }, | |
10698 | |
10699 /** | |
10700 * Specifies the icon name or index in the set of icons available in | |
10701 * the icon's icon set. If the icon property is specified, | |
10702 * the src property should not be. | |
10703 */ | |
10704 icon: { | |
10705 type: String | |
10706 }, | |
10707 | |
10708 /** | |
10709 * Specifies the alternate text for the button, for accessibility. | |
10710 */ | |
10711 alt: { | |
10712 type: String, | |
10713 observer: "_altChanged" | |
10714 } | |
10715 }, | |
10716 | |
10717 _altChanged: function(newValue, oldValue) { | |
10718 var label = this.getAttribute('aria-label'); | |
10719 | |
10720 // Don't stomp over a user-set aria-label. | |
10721 if (!label || oldValue == label) { | |
10722 this.setAttribute('aria-label', newValue); | |
10723 } | |
10724 } | |
10725 }); | |
10726 // Copyright 2016 The Chromium Authors. All rights reserved. | 6014 // Copyright 2016 The Chromium Authors. All rights reserved. |
10727 // Use of this source code is governed by a BSD-style license that can be | 6015 // Use of this source code is governed by a BSD-style license that can be |
10728 // found in the LICENSE file. | 6016 // found in the LICENSE file. |
10729 | |
10730 /** | |
10731 * Implements an incremental search field which can be shown and hidden. | |
10732 * Canonical implementation is <cr-search-field>. | |
10733 * @polymerBehavior | |
10734 */ | |
10735 var CrSearchFieldBehavior = { | 6017 var CrSearchFieldBehavior = { |
10736 properties: { | 6018 properties: { |
10737 label: { | 6019 label: { |
10738 type: String, | 6020 type: String, |
10739 value: '', | 6021 value: '' |
10740 }, | 6022 }, |
10741 | |
10742 clearLabel: { | 6023 clearLabel: { |
10743 type: String, | 6024 type: String, |
10744 value: '', | 6025 value: '' |
10745 }, | 6026 }, |
10746 | |
10747 showingSearch: { | 6027 showingSearch: { |
10748 type: Boolean, | 6028 type: Boolean, |
10749 value: false, | 6029 value: false, |
10750 notify: true, | 6030 notify: true, |
10751 observer: 'showingSearchChanged_', | 6031 observer: 'showingSearchChanged_', |
10752 reflectToAttribute: true | 6032 reflectToAttribute: true |
10753 }, | 6033 }, |
10754 | |
10755 /** @private */ | |
10756 lastValue_: { | 6034 lastValue_: { |
10757 type: String, | 6035 type: String, |
10758 value: '', | 6036 value: '' |
10759 }, | 6037 } |
10760 }, | 6038 }, |
10761 | |
10762 /** | |
10763 * @abstract | |
10764 * @return {!HTMLInputElement} The input field element the behavior should | |
10765 * use. | |
10766 */ | |
10767 getSearchInput: function() {}, | 6039 getSearchInput: function() {}, |
10768 | |
10769 /** | |
10770 * @return {string} The value of the search field. | |
10771 */ | |
10772 getValue: function() { | 6040 getValue: function() { |
10773 return this.getSearchInput().value; | 6041 return this.getSearchInput().value; |
10774 }, | 6042 }, |
10775 | |
10776 /** | |
10777 * Sets the value of the search field. | |
10778 * @param {string} value | |
10779 */ | |
10780 setValue: function(value) { | 6043 setValue: function(value) { |
10781 // Use bindValue when setting the input value so that changes propagate | |
10782 // correctly. | |
10783 this.getSearchInput().bindValue = value; | 6044 this.getSearchInput().bindValue = value; |
10784 this.onValueChanged_(value); | 6045 this.onValueChanged_(value); |
10785 }, | 6046 }, |
10786 | |
10787 showAndFocus: function() { | 6047 showAndFocus: function() { |
10788 this.showingSearch = true; | 6048 this.showingSearch = true; |
10789 this.focus_(); | 6049 this.focus_(); |
10790 }, | 6050 }, |
10791 | |
10792 /** @private */ | |
10793 focus_: function() { | 6051 focus_: function() { |
10794 this.getSearchInput().focus(); | 6052 this.getSearchInput().focus(); |
10795 }, | 6053 }, |
10796 | |
10797 onSearchTermSearch: function() { | 6054 onSearchTermSearch: function() { |
10798 this.onValueChanged_(this.getValue()); | 6055 this.onValueChanged_(this.getValue()); |
10799 }, | 6056 }, |
10800 | |
10801 /** | |
10802 * Updates the internal state of the search field based on a change that has | |
10803 * already happened. | |
10804 * @param {string} newValue | |
10805 * @private | |
10806 */ | |
10807 onValueChanged_: function(newValue) { | 6057 onValueChanged_: function(newValue) { |
10808 if (newValue == this.lastValue_) | 6058 if (newValue == this.lastValue_) return; |
10809 return; | |
10810 | |
10811 this.fire('search-changed', newValue); | 6059 this.fire('search-changed', newValue); |
10812 this.lastValue_ = newValue; | 6060 this.lastValue_ = newValue; |
10813 }, | 6061 }, |
10814 | |
10815 onSearchTermKeydown: function(e) { | 6062 onSearchTermKeydown: function(e) { |
10816 if (e.key == 'Escape') | 6063 if (e.key == 'Escape') this.showingSearch = false; |
10817 this.showingSearch = false; | 6064 }, |
10818 }, | |
10819 | |
10820 /** @private */ | |
10821 showingSearchChanged_: function() { | 6065 showingSearchChanged_: function() { |
10822 if (this.showingSearch) { | 6066 if (this.showingSearch) { |
10823 this.focus_(); | 6067 this.focus_(); |
10824 return; | 6068 return; |
10825 } | 6069 } |
10826 | |
10827 this.setValue(''); | 6070 this.setValue(''); |
10828 this.getSearchInput().blur(); | 6071 this.getSearchInput().blur(); |
10829 } | 6072 } |
10830 }; | 6073 }; |
| 6074 |
10831 (function() { | 6075 (function() { |
10832 'use strict'; | 6076 'use strict'; |
10833 | 6077 Polymer.IronA11yAnnouncer = Polymer({ |
10834 Polymer.IronA11yAnnouncer = Polymer({ | 6078 is: 'iron-a11y-announcer', |
10835 is: 'iron-a11y-announcer', | |
10836 | |
10837 properties: { | |
10838 | |
10839 /** | |
10840 * The value of mode is used to set the `aria-live` attribute | |
10841 * for the element that will be announced. Valid values are: `off`, | |
10842 * `polite` and `assertive`. | |
10843 */ | |
10844 mode: { | |
10845 type: String, | |
10846 value: 'polite' | |
10847 }, | |
10848 | |
10849 _text: { | |
10850 type: String, | |
10851 value: '' | |
10852 } | |
10853 }, | |
10854 | |
10855 created: function() { | |
10856 if (!Polymer.IronA11yAnnouncer.instance) { | |
10857 Polymer.IronA11yAnnouncer.instance = this; | |
10858 } | |
10859 | |
10860 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
10861 }, | |
10862 | |
10863 /** | |
10864 * Cause a text string to be announced by screen readers. | |
10865 * | |
10866 * @param {string} text The text that should be announced. | |
10867 */ | |
10868 announce: function(text) { | |
10869 this._text = ''; | |
10870 this.async(function() { | |
10871 this._text = text; | |
10872 }, 100); | |
10873 }, | |
10874 | |
10875 _onIronAnnounce: function(event) { | |
10876 if (event.detail && event.detail.text) { | |
10877 this.announce(event.detail.text); | |
10878 } | |
10879 } | |
10880 }); | |
10881 | |
10882 Polymer.IronA11yAnnouncer.instance = null; | |
10883 | |
10884 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
10885 if (!Polymer.IronA11yAnnouncer.instance) { | |
10886 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
10887 } | |
10888 | |
10889 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
10890 }; | |
10891 })(); | |
10892 /** | |
10893 * Singleton IronMeta instance. | |
10894 */ | |
10895 Polymer.IronValidatableBehaviorMeta = null; | |
10896 | |
10897 /** | |
10898 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
10899 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
10900 * | |
10901 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
10902 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
10903 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
10904 * custom element will get a public `validate()`, which | |
10905 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
10906 * which can be used for styling. | |
10907 * | |
10908 * To implement the custom validation logic of your element, you must override | |
10909 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
10910 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
10911 * for an example. | |
10912 * | |
10913 * ### Accessibility | |
10914 * | |
10915 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
10916 * `aria-invalid` attribute. | |
10917 * | |
10918 * @demo demo/index.html | |
10919 * @polymerBehavior | |
10920 */ | |
10921 Polymer.IronValidatableBehavior = { | |
10922 | |
10923 properties: { | 6079 properties: { |
10924 | 6080 mode: { |
10925 /** | 6081 type: String, |
10926 * Name of the validator to use. | 6082 value: 'polite' |
10927 */ | |
10928 validator: { | |
10929 type: String | |
10930 }, | 6083 }, |
10931 | 6084 _text: { |
10932 /** | |
10933 * True if the last call to `validate` is invalid. | |
10934 */ | |
10935 invalid: { | |
10936 notify: true, | |
10937 reflectToAttribute: true, | |
10938 type: Boolean, | |
10939 value: false | |
10940 }, | |
10941 | |
10942 /** | |
10943 * This property is deprecated and should not be used. Use the global | |
10944 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
10945 */ | |
10946 _validatorMeta: { | |
10947 type: Object | |
10948 }, | |
10949 | |
10950 /** | |
10951 * Namespace for this validator. This property is deprecated and should | |
10952 * not be used. For all intents and purposes, please consider it a | |
10953 * read-only, config-time property. | |
10954 */ | |
10955 validatorType: { | |
10956 type: String, | |
10957 value: 'validator' | |
10958 }, | |
10959 | |
10960 _validator: { | |
10961 type: Object, | |
10962 computed: '__computeValidator(validator)' | |
10963 } | |
10964 }, | |
10965 | |
10966 observers: [ | |
10967 '_invalidChanged(invalid)' | |
10968 ], | |
10969 | |
10970 registered: function() { | |
10971 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
10972 }, | |
10973 | |
10974 _invalidChanged: function() { | |
10975 if (this.invalid) { | |
10976 this.setAttribute('aria-invalid', 'true'); | |
10977 } else { | |
10978 this.removeAttribute('aria-invalid'); | |
10979 } | |
10980 }, | |
10981 | |
10982 /** | |
10983 * @return {boolean} True if the validator `validator` exists. | |
10984 */ | |
10985 hasValidator: function() { | |
10986 return this._validator != null; | |
10987 }, | |
10988 | |
10989 /** | |
10990 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
10991 * your element to have custom validation logic, do not override this method
; | |
10992 * override `_getValidity(value)` instead. | |
10993 | |
10994 * @param {Object} value The value to be validated. By default, it is passed | |
10995 * to the validator's `validate()` function, if a validator is set. | |
10996 * @return {boolean} True if `value` is valid. | |
10997 */ | |
10998 validate: function(value) { | |
10999 this.invalid = !this._getValidity(value); | |
11000 return !this.invalid; | |
11001 }, | |
11002 | |
11003 /** | |
11004 * Returns true if `value` is valid. By default, it is passed | |
11005 * to the validator's `validate()` function, if a validator is set. You | |
11006 * should override this method if you want to implement custom validity | |
11007 * logic for your element. | |
11008 * | |
11009 * @param {Object} value The value to be validated. | |
11010 * @return {boolean} True if `value` is valid. | |
11011 */ | |
11012 | |
11013 _getValidity: function(value) { | |
11014 if (this.hasValidator()) { | |
11015 return this._validator.validate(value); | |
11016 } | |
11017 return true; | |
11018 }, | |
11019 | |
11020 __computeValidator: function() { | |
11021 return Polymer.IronValidatableBehaviorMeta && | |
11022 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
11023 } | |
11024 }; | |
11025 /* | |
11026 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
11027 to `<input>`. | |
11028 | |
11029 ### Two-way binding | |
11030 | |
11031 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
11032 | |
11033 <input value="{{myValue::input}}"> | |
11034 | |
11035 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
11036 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
11037 | |
11038 <input is="iron-input" bind-value="{{myValue}}"> | |
11039 | |
11040 ### Custom validators | |
11041 | |
11042 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
11043 | |
11044 <input is="iron-input" validator="my-custom-validator"> | |
11045 | |
11046 ### Stopping invalid input | |
11047 | |
11048 It may be desirable to only allow users to enter certain characters. You can use
the | |
11049 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
11050 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
11051 | |
11052 \x3c!-- only allow characters that match [0-9] --\x3e | |
11053 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
11054 | |
11055 @hero hero.svg | |
11056 @demo demo/index.html | |
11057 */ | |
11058 | |
11059 Polymer({ | |
11060 | |
11061 is: 'iron-input', | |
11062 | |
11063 extends: 'input', | |
11064 | |
11065 behaviors: [ | |
11066 Polymer.IronValidatableBehavior | |
11067 ], | |
11068 | |
11069 properties: { | |
11070 | |
11071 /** | |
11072 * Use this property instead of `value` for two-way data binding. | |
11073 */ | |
11074 bindValue: { | |
11075 observer: '_bindValueChanged', | |
11076 type: String | |
11077 }, | |
11078 | |
11079 /** | |
11080 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
11081 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
11082 * Pasted input will have each character checked individually; if any char
acter | |
11083 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
11084 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
11085 */ | |
11086 preventInvalidInput: { | |
11087 type: Boolean | |
11088 }, | |
11089 | |
11090 /** | |
11091 * Regular expression that list the characters allowed as input. | |
11092 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
11093 * each individual character will be checked against the pattern (rather t
han checking | |
11094 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
11095 * for example, `[a-zA-Z0-9.+-!;:]` | |
11096 */ | |
11097 allowedPattern: { | |
11098 type: String, | |
11099 observer: "_allowedPatternChanged" | |
11100 }, | |
11101 | |
11102 _previousValidInput: { | |
11103 type: String, | 6085 type: String, |
11104 value: '' | 6086 value: '' |
11105 }, | 6087 } |
11106 | 6088 }, |
11107 _patternAlreadyChecked: { | |
11108 type: Boolean, | |
11109 value: false | |
11110 } | |
11111 | |
11112 }, | |
11113 | |
11114 listeners: { | |
11115 'input': '_onInput', | |
11116 'keypress': '_onKeypress' | |
11117 }, | |
11118 | |
11119 /** @suppress {checkTypes} */ | |
11120 registered: function() { | |
11121 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
11122 if (!this._canDispatchEventOnDisabled()) { | |
11123 this._origDispatchEvent = this.dispatchEvent; | |
11124 this.dispatchEvent = this._dispatchEventFirefoxIE; | |
11125 } | |
11126 }, | |
11127 | |
11128 created: function() { | 6089 created: function() { |
11129 Polymer.IronA11yAnnouncer.requestAvailability(); | 6090 if (!Polymer.IronA11yAnnouncer.instance) { |
11130 }, | 6091 Polymer.IronA11yAnnouncer.instance = this; |
11131 | 6092 } |
11132 _canDispatchEventOnDisabled: function() { | 6093 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(
this)); |
11133 var input = document.createElement('input'); | 6094 }, |
11134 var canDispatch = false; | 6095 announce: function(text) { |
11135 input.disabled = true; | 6096 this._text = ''; |
11136 | 6097 this.async(function() { |
11137 input.addEventListener('feature-check-dispatch-event', function() { | 6098 this._text = text; |
11138 canDispatch = true; | 6099 }, 100); |
11139 }); | 6100 }, |
11140 | 6101 _onIronAnnounce: function(event) { |
11141 try { | 6102 if (event.detail && event.detail.text) { |
11142 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 6103 this.announce(event.detail.text); |
11143 } catch(e) {} | 6104 } |
11144 | 6105 } |
11145 return canDispatch; | 6106 }); |
11146 }, | 6107 Polymer.IronA11yAnnouncer.instance = null; |
11147 | 6108 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
11148 _dispatchEventFirefoxIE: function() { | 6109 if (!Polymer.IronA11yAnnouncer.instance) { |
11149 // Due to Firefox bug, events fired on disabled form controls can throw | 6110 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann
ouncer'); |
11150 // errors; furthermore, neither IE nor Firefox will actually dispatch | 6111 } |
11151 // events from disabled form controls; as such, we toggle disable around | 6112 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
11152 // the dispatch to allow notifying properties to notify | 6113 }; |
11153 // See issue #47 for details | 6114 })(); |
11154 var disabled = this.disabled; | 6115 |
11155 this.disabled = false; | 6116 Polymer.IronValidatableBehaviorMeta = null; |
11156 this._origDispatchEvent.apply(this, arguments); | 6117 |
11157 this.disabled = disabled; | 6118 Polymer.IronValidatableBehavior = { |
11158 }, | 6119 properties: { |
11159 | 6120 validator: { |
11160 get _patternRegExp() { | 6121 type: String |
11161 var pattern; | 6122 }, |
11162 if (this.allowedPattern) { | 6123 invalid: { |
11163 pattern = new RegExp(this.allowedPattern); | 6124 notify: true, |
| 6125 reflectToAttribute: true, |
| 6126 type: Boolean, |
| 6127 value: false |
| 6128 }, |
| 6129 _validatorMeta: { |
| 6130 type: Object |
| 6131 }, |
| 6132 validatorType: { |
| 6133 type: String, |
| 6134 value: 'validator' |
| 6135 }, |
| 6136 _validator: { |
| 6137 type: Object, |
| 6138 computed: '__computeValidator(validator)' |
| 6139 } |
| 6140 }, |
| 6141 observers: [ '_invalidChanged(invalid)' ], |
| 6142 registered: function() { |
| 6143 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({ |
| 6144 type: 'validator' |
| 6145 }); |
| 6146 }, |
| 6147 _invalidChanged: function() { |
| 6148 if (this.invalid) { |
| 6149 this.setAttribute('aria-invalid', 'true'); |
| 6150 } else { |
| 6151 this.removeAttribute('aria-invalid'); |
| 6152 } |
| 6153 }, |
| 6154 hasValidator: function() { |
| 6155 return this._validator != null; |
| 6156 }, |
| 6157 validate: function(value) { |
| 6158 this.invalid = !this._getValidity(value); |
| 6159 return !this.invalid; |
| 6160 }, |
| 6161 _getValidity: function(value) { |
| 6162 if (this.hasValidator()) { |
| 6163 return this._validator.validate(value); |
| 6164 } |
| 6165 return true; |
| 6166 }, |
| 6167 __computeValidator: function() { |
| 6168 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio
rMeta.byKey(this.validator); |
| 6169 } |
| 6170 }; |
| 6171 |
| 6172 Polymer({ |
| 6173 is: 'iron-input', |
| 6174 "extends": 'input', |
| 6175 behaviors: [ Polymer.IronValidatableBehavior ], |
| 6176 properties: { |
| 6177 bindValue: { |
| 6178 observer: '_bindValueChanged', |
| 6179 type: String |
| 6180 }, |
| 6181 preventInvalidInput: { |
| 6182 type: Boolean |
| 6183 }, |
| 6184 allowedPattern: { |
| 6185 type: String, |
| 6186 observer: "_allowedPatternChanged" |
| 6187 }, |
| 6188 _previousValidInput: { |
| 6189 type: String, |
| 6190 value: '' |
| 6191 }, |
| 6192 _patternAlreadyChecked: { |
| 6193 type: Boolean, |
| 6194 value: false |
| 6195 } |
| 6196 }, |
| 6197 listeners: { |
| 6198 input: '_onInput', |
| 6199 keypress: '_onKeypress' |
| 6200 }, |
| 6201 registered: function() { |
| 6202 if (!this._canDispatchEventOnDisabled()) { |
| 6203 this._origDispatchEvent = this.dispatchEvent; |
| 6204 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 6205 } |
| 6206 }, |
| 6207 created: function() { |
| 6208 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 6209 }, |
| 6210 _canDispatchEventOnDisabled: function() { |
| 6211 var input = document.createElement('input'); |
| 6212 var canDispatch = false; |
| 6213 input.disabled = true; |
| 6214 input.addEventListener('feature-check-dispatch-event', function() { |
| 6215 canDispatch = true; |
| 6216 }); |
| 6217 try { |
| 6218 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 6219 } catch (e) {} |
| 6220 return canDispatch; |
| 6221 }, |
| 6222 _dispatchEventFirefoxIE: function() { |
| 6223 var disabled = this.disabled; |
| 6224 this.disabled = false; |
| 6225 this._origDispatchEvent.apply(this, arguments); |
| 6226 this.disabled = disabled; |
| 6227 }, |
| 6228 get _patternRegExp() { |
| 6229 var pattern; |
| 6230 if (this.allowedPattern) { |
| 6231 pattern = new RegExp(this.allowedPattern); |
| 6232 } else { |
| 6233 switch (this.type) { |
| 6234 case 'number': |
| 6235 pattern = /[0-9.,e-]/; |
| 6236 break; |
| 6237 } |
| 6238 } |
| 6239 return pattern; |
| 6240 }, |
| 6241 ready: function() { |
| 6242 this.bindValue = this.value; |
| 6243 }, |
| 6244 _bindValueChanged: function() { |
| 6245 if (this.value !== this.bindValue) { |
| 6246 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue ==
= false) ? '' : this.bindValue; |
| 6247 } |
| 6248 this.fire('bind-value-changed', { |
| 6249 value: this.bindValue |
| 6250 }); |
| 6251 }, |
| 6252 _allowedPatternChanged: function() { |
| 6253 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 6254 }, |
| 6255 _onInput: function() { |
| 6256 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 6257 var valid = this._checkPatternValidity(); |
| 6258 if (!valid) { |
| 6259 this._announceInvalidCharacter('Invalid string of characters not entered
.'); |
| 6260 this.value = this._previousValidInput; |
| 6261 } |
| 6262 } |
| 6263 this.bindValue = this.value; |
| 6264 this._previousValidInput = this.value; |
| 6265 this._patternAlreadyChecked = false; |
| 6266 }, |
| 6267 _isPrintable: function(event) { |
| 6268 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC
ode == 13 || event.keyCode == 27; |
| 6269 var mozNonPrintable = event.keyCode == 19 || event.keyCode == 20 || event.ke
yCode == 45 || event.keyCode == 46 || event.keyCode == 144 || event.keyCode == 1
45 || event.keyCode > 32 && event.keyCode < 41 || event.keyCode > 111 && event.k
eyCode < 124; |
| 6270 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 6271 }, |
| 6272 _onKeypress: function(event) { |
| 6273 if (!this.preventInvalidInput && this.type !== 'number') { |
| 6274 return; |
| 6275 } |
| 6276 var regexp = this._patternRegExp; |
| 6277 if (!regexp) { |
| 6278 return; |
| 6279 } |
| 6280 if (event.metaKey || event.ctrlKey || event.altKey) return; |
| 6281 this._patternAlreadyChecked = true; |
| 6282 var thisChar = String.fromCharCode(event.charCode); |
| 6283 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 6284 event.preventDefault(); |
| 6285 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent
ered.'); |
| 6286 } |
| 6287 }, |
| 6288 _checkPatternValidity: function() { |
| 6289 var regexp = this._patternRegExp; |
| 6290 if (!regexp) { |
| 6291 return true; |
| 6292 } |
| 6293 for (var i = 0; i < this.value.length; i++) { |
| 6294 if (!regexp.test(this.value[i])) { |
| 6295 return false; |
| 6296 } |
| 6297 } |
| 6298 return true; |
| 6299 }, |
| 6300 validate: function() { |
| 6301 var valid = this.checkValidity(); |
| 6302 if (valid) { |
| 6303 if (this.required && this.value === '') { |
| 6304 valid = false; |
| 6305 } else if (this.hasValidator()) { |
| 6306 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); |
| 6307 } |
| 6308 } |
| 6309 this.invalid = !valid; |
| 6310 this.fire('iron-input-validate'); |
| 6311 return valid; |
| 6312 }, |
| 6313 _announceInvalidCharacter: function(message) { |
| 6314 this.fire('iron-announce', { |
| 6315 text: message |
| 6316 }); |
| 6317 } |
| 6318 }); |
| 6319 |
| 6320 Polymer({ |
| 6321 is: 'paper-input-container', |
| 6322 properties: { |
| 6323 noLabelFloat: { |
| 6324 type: Boolean, |
| 6325 value: false |
| 6326 }, |
| 6327 alwaysFloatLabel: { |
| 6328 type: Boolean, |
| 6329 value: false |
| 6330 }, |
| 6331 attrForValue: { |
| 6332 type: String, |
| 6333 value: 'bind-value' |
| 6334 }, |
| 6335 autoValidate: { |
| 6336 type: Boolean, |
| 6337 value: false |
| 6338 }, |
| 6339 invalid: { |
| 6340 observer: '_invalidChanged', |
| 6341 type: Boolean, |
| 6342 value: false |
| 6343 }, |
| 6344 focused: { |
| 6345 readOnly: true, |
| 6346 type: Boolean, |
| 6347 value: false, |
| 6348 notify: true |
| 6349 }, |
| 6350 _addons: { |
| 6351 type: Array |
| 6352 }, |
| 6353 _inputHasContent: { |
| 6354 type: Boolean, |
| 6355 value: false |
| 6356 }, |
| 6357 _inputSelector: { |
| 6358 type: String, |
| 6359 value: 'input,textarea,.paper-input-input' |
| 6360 }, |
| 6361 _boundOnFocus: { |
| 6362 type: Function, |
| 6363 value: function() { |
| 6364 return this._onFocus.bind(this); |
| 6365 } |
| 6366 }, |
| 6367 _boundOnBlur: { |
| 6368 type: Function, |
| 6369 value: function() { |
| 6370 return this._onBlur.bind(this); |
| 6371 } |
| 6372 }, |
| 6373 _boundOnInput: { |
| 6374 type: Function, |
| 6375 value: function() { |
| 6376 return this._onInput.bind(this); |
| 6377 } |
| 6378 }, |
| 6379 _boundValueChanged: { |
| 6380 type: Function, |
| 6381 value: function() { |
| 6382 return this._onValueChanged.bind(this); |
| 6383 } |
| 6384 } |
| 6385 }, |
| 6386 listeners: { |
| 6387 'addon-attached': '_onAddonAttached', |
| 6388 'iron-input-validate': '_onIronInputValidate' |
| 6389 }, |
| 6390 get _valueChangedEvent() { |
| 6391 return this.attrForValue + '-changed'; |
| 6392 }, |
| 6393 get _propertyForValue() { |
| 6394 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 6395 }, |
| 6396 get _inputElement() { |
| 6397 return Polymer.dom(this).querySelector(this._inputSelector); |
| 6398 }, |
| 6399 get _inputElementValue() { |
| 6400 return this._inputElement[this._propertyForValue] || this._inputElement.valu
e; |
| 6401 }, |
| 6402 ready: function() { |
| 6403 if (!this._addons) { |
| 6404 this._addons = []; |
| 6405 } |
| 6406 this.addEventListener('focus', this._boundOnFocus, true); |
| 6407 this.addEventListener('blur', this._boundOnBlur, true); |
| 6408 }, |
| 6409 attached: function() { |
| 6410 if (this.attrForValue) { |
| 6411 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa
lueChanged); |
| 6412 } else { |
| 6413 this.addEventListener('input', this._onInput); |
| 6414 } |
| 6415 if (this._inputElementValue != '') { |
| 6416 this._handleValueAndAutoValidate(this._inputElement); |
| 6417 } else { |
| 6418 this._handleValue(this._inputElement); |
| 6419 } |
| 6420 }, |
| 6421 _onAddonAttached: function(event) { |
| 6422 if (!this._addons) { |
| 6423 this._addons = []; |
| 6424 } |
| 6425 var target = event.target; |
| 6426 if (this._addons.indexOf(target) === -1) { |
| 6427 this._addons.push(target); |
| 6428 if (this.isAttached) { |
| 6429 this._handleValue(this._inputElement); |
| 6430 } |
| 6431 } |
| 6432 }, |
| 6433 _onFocus: function() { |
| 6434 this._setFocused(true); |
| 6435 }, |
| 6436 _onBlur: function() { |
| 6437 this._setFocused(false); |
| 6438 this._handleValueAndAutoValidate(this._inputElement); |
| 6439 }, |
| 6440 _onInput: function(event) { |
| 6441 this._handleValueAndAutoValidate(event.target); |
| 6442 }, |
| 6443 _onValueChanged: function(event) { |
| 6444 this._handleValueAndAutoValidate(event.target); |
| 6445 }, |
| 6446 _handleValue: function(inputElement) { |
| 6447 var value = this._inputElementValue; |
| 6448 if (value || value === 0 || inputElement.type === 'number' && !inputElement.
checkValidity()) { |
| 6449 this._inputHasContent = true; |
| 6450 } else { |
| 6451 this._inputHasContent = false; |
| 6452 } |
| 6453 this.updateAddons({ |
| 6454 inputElement: inputElement, |
| 6455 value: value, |
| 6456 invalid: this.invalid |
| 6457 }); |
| 6458 }, |
| 6459 _handleValueAndAutoValidate: function(inputElement) { |
| 6460 if (this.autoValidate) { |
| 6461 var valid; |
| 6462 if (inputElement.validate) { |
| 6463 valid = inputElement.validate(this._inputElementValue); |
11164 } else { | 6464 } else { |
11165 switch (this.type) { | 6465 valid = inputElement.checkValidity(); |
11166 case 'number': | 6466 } |
11167 pattern = /[0-9.,e-]/; | |
11168 break; | |
11169 } | |
11170 } | |
11171 return pattern; | |
11172 }, | |
11173 | |
11174 ready: function() { | |
11175 this.bindValue = this.value; | |
11176 }, | |
11177 | |
11178 /** | |
11179 * @suppress {checkTypes} | |
11180 */ | |
11181 _bindValueChanged: function() { | |
11182 if (this.value !== this.bindValue) { | |
11183 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | |
11184 } | |
11185 // manually notify because we don't want to notify until after setting val
ue | |
11186 this.fire('bind-value-changed', {value: this.bindValue}); | |
11187 }, | |
11188 | |
11189 _allowedPatternChanged: function() { | |
11190 // Force to prevent invalid input when an `allowed-pattern` is set | |
11191 this.preventInvalidInput = this.allowedPattern ? true : false; | |
11192 }, | |
11193 | |
11194 _onInput: function() { | |
11195 // Need to validate each of the characters pasted if they haven't | |
11196 // been validated inside `_onKeypress` already. | |
11197 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | |
11198 var valid = this._checkPatternValidity(); | |
11199 if (!valid) { | |
11200 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | |
11201 this.value = this._previousValidInput; | |
11202 } | |
11203 } | |
11204 | |
11205 this.bindValue = this.value; | |
11206 this._previousValidInput = this.value; | |
11207 this._patternAlreadyChecked = false; | |
11208 }, | |
11209 | |
11210 _isPrintable: function(event) { | |
11211 // What a control/printable character is varies wildly based on the browse
r. | |
11212 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
11213 // in Chrome, but the *do* on Firefox | |
11214 // - in Firefox, when they do send a `keypress` event, control chars have | |
11215 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
11216 // - printable characters always send a keypress event. | |
11217 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
11218 // always matches the charCode. | |
11219 // None of this makes any sense. | |
11220 | |
11221 // For these keys, ASCII code == browser keycode. | |
11222 var anyNonPrintable = | |
11223 (event.keyCode == 8) || // backspace | |
11224 (event.keyCode == 9) || // tab | |
11225 (event.keyCode == 13) || // enter | |
11226 (event.keyCode == 27); // escape | |
11227 | |
11228 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
11229 var mozNonPrintable = | |
11230 (event.keyCode == 19) || // pause | |
11231 (event.keyCode == 20) || // caps lock | |
11232 (event.keyCode == 45) || // insert | |
11233 (event.keyCode == 46) || // delete | |
11234 (event.keyCode == 144) || // num lock | |
11235 (event.keyCode == 145) || // scroll lock | |
11236 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | |
11237 (event.keyCode > 111 && event.keyCode < 124); // fn keys | |
11238 | |
11239 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | |
11240 }, | |
11241 | |
11242 _onKeypress: function(event) { | |
11243 if (!this.preventInvalidInput && this.type !== 'number') { | |
11244 return; | |
11245 } | |
11246 var regexp = this._patternRegExp; | |
11247 if (!regexp) { | |
11248 return; | |
11249 } | |
11250 | |
11251 // Handle special keys and backspace | |
11252 if (event.metaKey || event.ctrlKey || event.altKey) | |
11253 return; | |
11254 | |
11255 // Check the pattern either here or in `_onInput`, but not in both. | |
11256 this._patternAlreadyChecked = true; | |
11257 | |
11258 var thisChar = String.fromCharCode(event.charCode); | |
11259 if (this._isPrintable(event) && !regexp.test(thisChar)) { | |
11260 event.preventDefault(); | |
11261 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | |
11262 } | |
11263 }, | |
11264 | |
11265 _checkPatternValidity: function() { | |
11266 var regexp = this._patternRegExp; | |
11267 if (!regexp) { | |
11268 return true; | |
11269 } | |
11270 for (var i = 0; i < this.value.length; i++) { | |
11271 if (!regexp.test(this.value[i])) { | |
11272 return false; | |
11273 } | |
11274 } | |
11275 return true; | |
11276 }, | |
11277 | |
11278 /** | |
11279 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
11280 * then any constraints. | |
11281 * @return {boolean} True if the value is valid. | |
11282 */ | |
11283 validate: function() { | |
11284 // First, check what the browser thinks. Some inputs (like type=number) | |
11285 // behave weirdly and will set the value to "" if something invalid is | |
11286 // entered, but will set the validity correctly. | |
11287 var valid = this.checkValidity(); | |
11288 | |
11289 // Only do extra checking if the browser thought this was valid. | |
11290 if (valid) { | |
11291 // Empty, required input is invalid | |
11292 if (this.required && this.value === '') { | |
11293 valid = false; | |
11294 } else if (this.hasValidator()) { | |
11295 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | |
11296 } | |
11297 } | |
11298 | |
11299 this.invalid = !valid; | 6467 this.invalid = !valid; |
11300 this.fire('iron-input-validate'); | 6468 } |
11301 return valid; | 6469 this._handleValue(inputElement); |
11302 }, | 6470 }, |
11303 | 6471 _onIronInputValidate: function(event) { |
11304 _announceInvalidCharacter: function(message) { | 6472 this.invalid = this._inputElement.invalid; |
11305 this.fire('iron-announce', { text: message }); | 6473 }, |
11306 } | 6474 _invalidChanged: function() { |
11307 }); | 6475 if (this._addons) { |
11308 | |
11309 /* | |
11310 The `iron-input-validate` event is fired whenever `validate()` is called. | |
11311 @event iron-input-validate | |
11312 */ | |
11313 Polymer({ | |
11314 is: 'paper-input-container', | |
11315 | |
11316 properties: { | |
11317 /** | |
11318 * Set to true to disable the floating label. The label disappears when th
e input value is | |
11319 * not null. | |
11320 */ | |
11321 noLabelFloat: { | |
11322 type: Boolean, | |
11323 value: false | |
11324 }, | |
11325 | |
11326 /** | |
11327 * Set to true to always float the floating label. | |
11328 */ | |
11329 alwaysFloatLabel: { | |
11330 type: Boolean, | |
11331 value: false | |
11332 }, | |
11333 | |
11334 /** | |
11335 * The attribute to listen for value changes on. | |
11336 */ | |
11337 attrForValue: { | |
11338 type: String, | |
11339 value: 'bind-value' | |
11340 }, | |
11341 | |
11342 /** | |
11343 * Set to true to auto-validate the input value when it changes. | |
11344 */ | |
11345 autoValidate: { | |
11346 type: Boolean, | |
11347 value: false | |
11348 }, | |
11349 | |
11350 /** | |
11351 * True if the input is invalid. This property is set automatically when t
he input value | |
11352 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
11353 */ | |
11354 invalid: { | |
11355 observer: '_invalidChanged', | |
11356 type: Boolean, | |
11357 value: false | |
11358 }, | |
11359 | |
11360 /** | |
11361 * True if the input has focus. | |
11362 */ | |
11363 focused: { | |
11364 readOnly: true, | |
11365 type: Boolean, | |
11366 value: false, | |
11367 notify: true | |
11368 }, | |
11369 | |
11370 _addons: { | |
11371 type: Array | |
11372 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
11373 // distributed child is attached, which may occur before configuration f
or this element | |
11374 // in polyfill. | |
11375 }, | |
11376 | |
11377 _inputHasContent: { | |
11378 type: Boolean, | |
11379 value: false | |
11380 }, | |
11381 | |
11382 _inputSelector: { | |
11383 type: String, | |
11384 value: 'input,textarea,.paper-input-input' | |
11385 }, | |
11386 | |
11387 _boundOnFocus: { | |
11388 type: Function, | |
11389 value: function() { | |
11390 return this._onFocus.bind(this); | |
11391 } | |
11392 }, | |
11393 | |
11394 _boundOnBlur: { | |
11395 type: Function, | |
11396 value: function() { | |
11397 return this._onBlur.bind(this); | |
11398 } | |
11399 }, | |
11400 | |
11401 _boundOnInput: { | |
11402 type: Function, | |
11403 value: function() { | |
11404 return this._onInput.bind(this); | |
11405 } | |
11406 }, | |
11407 | |
11408 _boundValueChanged: { | |
11409 type: Function, | |
11410 value: function() { | |
11411 return this._onValueChanged.bind(this); | |
11412 } | |
11413 } | |
11414 }, | |
11415 | |
11416 listeners: { | |
11417 'addon-attached': '_onAddonAttached', | |
11418 'iron-input-validate': '_onIronInputValidate' | |
11419 }, | |
11420 | |
11421 get _valueChangedEvent() { | |
11422 return this.attrForValue + '-changed'; | |
11423 }, | |
11424 | |
11425 get _propertyForValue() { | |
11426 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
11427 }, | |
11428 | |
11429 get _inputElement() { | |
11430 return Polymer.dom(this).querySelector(this._inputSelector); | |
11431 }, | |
11432 | |
11433 get _inputElementValue() { | |
11434 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
11435 }, | |
11436 | |
11437 ready: function() { | |
11438 if (!this._addons) { | |
11439 this._addons = []; | |
11440 } | |
11441 this.addEventListener('focus', this._boundOnFocus, true); | |
11442 this.addEventListener('blur', this._boundOnBlur, true); | |
11443 }, | |
11444 | |
11445 attached: function() { | |
11446 if (this.attrForValue) { | |
11447 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
11448 } else { | |
11449 this.addEventListener('input', this._onInput); | |
11450 } | |
11451 | |
11452 // Only validate when attached if the input already has a value. | |
11453 if (this._inputElementValue != '') { | |
11454 this._handleValueAndAutoValidate(this._inputElement); | |
11455 } else { | |
11456 this._handleValue(this._inputElement); | |
11457 } | |
11458 }, | |
11459 | |
11460 _onAddonAttached: function(event) { | |
11461 if (!this._addons) { | |
11462 this._addons = []; | |
11463 } | |
11464 var target = event.target; | |
11465 if (this._addons.indexOf(target) === -1) { | |
11466 this._addons.push(target); | |
11467 if (this.isAttached) { | |
11468 this._handleValue(this._inputElement); | |
11469 } | |
11470 } | |
11471 }, | |
11472 | |
11473 _onFocus: function() { | |
11474 this._setFocused(true); | |
11475 }, | |
11476 | |
11477 _onBlur: function() { | |
11478 this._setFocused(false); | |
11479 this._handleValueAndAutoValidate(this._inputElement); | |
11480 }, | |
11481 | |
11482 _onInput: function(event) { | |
11483 this._handleValueAndAutoValidate(event.target); | |
11484 }, | |
11485 | |
11486 _onValueChanged: function(event) { | |
11487 this._handleValueAndAutoValidate(event.target); | |
11488 }, | |
11489 | |
11490 _handleValue: function(inputElement) { | |
11491 var value = this._inputElementValue; | |
11492 | |
11493 // type="number" hack needed because this.value is empty until it's valid | |
11494 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
11495 this._inputHasContent = true; | |
11496 } else { | |
11497 this._inputHasContent = false; | |
11498 } | |
11499 | |
11500 this.updateAddons({ | 6476 this.updateAddons({ |
11501 inputElement: inputElement, | |
11502 value: value, | |
11503 invalid: this.invalid | 6477 invalid: this.invalid |
11504 }); | 6478 }); |
11505 }, | 6479 } |
11506 | 6480 }, |
11507 _handleValueAndAutoValidate: function(inputElement) { | 6481 updateAddons: function(state) { |
11508 if (this.autoValidate) { | 6482 for (var addon, index = 0; addon = this._addons[index]; index++) { |
11509 var valid; | 6483 addon.update(state); |
11510 if (inputElement.validate) { | 6484 } |
11511 valid = inputElement.validate(this._inputElementValue); | 6485 }, |
11512 } else { | 6486 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i
nvalid, _inputHasContent) { |
11513 valid = inputElement.checkValidity(); | 6487 var cls = 'input-content'; |
11514 } | 6488 if (!noLabelFloat) { |
11515 this.invalid = !valid; | 6489 var label = this.querySelector('label'); |
11516 } | 6490 if (alwaysFloatLabel || _inputHasContent) { |
11517 | 6491 cls += ' label-is-floating'; |
11518 // Call this last to notify the add-ons. | 6492 this.$.labelAndInputContainer.style.position = 'static'; |
11519 this._handleValue(inputElement); | 6493 if (invalid) { |
11520 }, | 6494 cls += ' is-invalid'; |
11521 | 6495 } else if (focused) { |
11522 _onIronInputValidate: function(event) { | 6496 cls += " label-is-highlighted"; |
11523 this.invalid = this._inputElement.invalid; | |
11524 }, | |
11525 | |
11526 _invalidChanged: function() { | |
11527 if (this._addons) { | |
11528 this.updateAddons({invalid: this.invalid}); | |
11529 } | |
11530 }, | |
11531 | |
11532 /** | |
11533 * Call this to update the state of add-ons. | |
11534 * @param {Object} state Add-on state. | |
11535 */ | |
11536 updateAddons: function(state) { | |
11537 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
11538 addon.update(state); | |
11539 } | |
11540 }, | |
11541 | |
11542 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
11543 var cls = 'input-content'; | |
11544 if (!noLabelFloat) { | |
11545 var label = this.querySelector('label'); | |
11546 | |
11547 if (alwaysFloatLabel || _inputHasContent) { | |
11548 cls += ' label-is-floating'; | |
11549 // If the label is floating, ignore any offsets that may have been | |
11550 // applied from a prefix element. | |
11551 this.$.labelAndInputContainer.style.position = 'static'; | |
11552 | |
11553 if (invalid) { | |
11554 cls += ' is-invalid'; | |
11555 } else if (focused) { | |
11556 cls += " label-is-highlighted"; | |
11557 } | |
11558 } else { | |
11559 // When the label is not floating, it should overlap the input element
. | |
11560 if (label) { | |
11561 this.$.labelAndInputContainer.style.position = 'relative'; | |
11562 } | |
11563 } | 6497 } |
11564 } else { | 6498 } else { |
11565 if (_inputHasContent) { | 6499 if (label) { |
11566 cls += ' label-is-hidden'; | 6500 this.$.labelAndInputContainer.style.position = 'relative'; |
11567 } | 6501 } |
11568 } | 6502 } |
11569 return cls; | 6503 } else { |
11570 }, | 6504 if (_inputHasContent) { |
11571 | 6505 cls += ' label-is-hidden'; |
11572 _computeUnderlineClass: function(focused, invalid) { | 6506 } |
11573 var cls = 'underline'; | 6507 } |
11574 if (invalid) { | 6508 return cls; |
11575 cls += ' is-invalid'; | 6509 }, |
11576 } else if (focused) { | 6510 _computeUnderlineClass: function(focused, invalid) { |
11577 cls += ' is-highlighted' | 6511 var cls = 'underline'; |
11578 } | 6512 if (invalid) { |
11579 return cls; | 6513 cls += ' is-invalid'; |
11580 }, | 6514 } else if (focused) { |
11581 | 6515 cls += ' is-highlighted'; |
11582 _computeAddOnContentClass: function(focused, invalid) { | 6516 } |
11583 var cls = 'add-on-content'; | 6517 return cls; |
11584 if (invalid) { | 6518 }, |
11585 cls += ' is-invalid'; | 6519 _computeAddOnContentClass: function(focused, invalid) { |
11586 } else if (focused) { | 6520 var cls = 'add-on-content'; |
11587 cls += ' is-highlighted' | 6521 if (invalid) { |
11588 } | 6522 cls += ' is-invalid'; |
11589 return cls; | 6523 } else if (focused) { |
11590 } | 6524 cls += ' is-highlighted'; |
11591 }); | 6525 } |
| 6526 return cls; |
| 6527 } |
| 6528 }); |
| 6529 |
11592 // Copyright 2015 The Chromium Authors. All rights reserved. | 6530 // Copyright 2015 The Chromium Authors. All rights reserved. |
11593 // Use of this source code is governed by a BSD-style license that can be | 6531 // Use of this source code is governed by a BSD-style license that can be |
11594 // found in the LICENSE file. | 6532 // found in the LICENSE file. |
11595 | |
11596 var SearchField = Polymer({ | 6533 var SearchField = Polymer({ |
11597 is: 'cr-search-field', | 6534 is: 'cr-search-field', |
11598 | 6535 behaviors: [ CrSearchFieldBehavior ], |
11599 behaviors: [CrSearchFieldBehavior], | |
11600 | |
11601 properties: { | 6536 properties: { |
11602 value_: String, | 6537 value_: String |
11603 }, | 6538 }, |
11604 | |
11605 /** @return {!HTMLInputElement} */ | |
11606 getSearchInput: function() { | 6539 getSearchInput: function() { |
11607 return this.$.searchInput; | 6540 return this.$.searchInput; |
11608 }, | 6541 }, |
11609 | |
11610 /** @private */ | |
11611 clearSearch_: function() { | 6542 clearSearch_: function() { |
11612 this.setValue(''); | 6543 this.setValue(''); |
11613 this.getSearchInput().focus(); | 6544 this.getSearchInput().focus(); |
11614 }, | 6545 }, |
11615 | |
11616 /** @private */ | |
11617 toggleShowingSearch_: function() { | 6546 toggleShowingSearch_: function() { |
11618 this.showingSearch = !this.showingSearch; | 6547 this.showingSearch = !this.showingSearch; |
11619 }, | 6548 } |
11620 }); | 6549 }); |
| 6550 |
11621 // Copyright 2015 The Chromium Authors. All rights reserved. | 6551 // Copyright 2015 The Chromium Authors. All rights reserved. |
11622 // Use of this source code is governed by a BSD-style license that can be | 6552 // Use of this source code is governed by a BSD-style license that can be |
11623 // found in the LICENSE file. | 6553 // found in the LICENSE file. |
11624 | |
11625 cr.define('downloads', function() { | 6554 cr.define('downloads', function() { |
11626 var Toolbar = Polymer({ | 6555 var Toolbar = Polymer({ |
11627 is: 'downloads-toolbar', | 6556 is: 'downloads-toolbar', |
11628 | |
11629 attached: function() { | 6557 attached: function() { |
11630 // isRTL() only works after i18n_template.js runs to set <html dir>. | |
11631 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 6558 this.overflowAlign_ = isRTL() ? 'left' : 'right'; |
11632 }, | 6559 }, |
11633 | |
11634 properties: { | 6560 properties: { |
11635 downloadsShowing: { | 6561 downloadsShowing: { |
11636 reflectToAttribute: true, | 6562 reflectToAttribute: true, |
11637 type: Boolean, | 6563 type: Boolean, |
11638 value: false, | 6564 value: false, |
11639 observer: 'downloadsShowingChanged_', | 6565 observer: 'downloadsShowingChanged_' |
11640 }, | 6566 }, |
11641 | |
11642 overflowAlign_: { | 6567 overflowAlign_: { |
11643 type: String, | 6568 type: String, |
11644 value: 'right', | 6569 value: 'right' |
11645 }, | 6570 } |
11646 }, | 6571 }, |
11647 | |
11648 listeners: { | 6572 listeners: { |
11649 'paper-dropdown-close': 'onPaperDropdownClose_', | 6573 'paper-dropdown-close': 'onPaperDropdownClose_', |
11650 'paper-dropdown-open': 'onPaperDropdownOpen_', | 6574 'paper-dropdown-open': 'onPaperDropdownOpen_' |
11651 }, | 6575 }, |
11652 | |
11653 /** @return {boolean} Whether removal can be undone. */ | |
11654 canUndo: function() { | 6576 canUndo: function() { |
11655 return this.$['search-input'] != this.shadowRoot.activeElement; | 6577 return this.$['search-input'] != this.shadowRoot.activeElement; |
11656 }, | 6578 }, |
11657 | |
11658 /** @return {boolean} Whether "Clear all" should be allowed. */ | |
11659 canClearAll: function() { | 6579 canClearAll: function() { |
11660 return !this.$['search-input'].getValue() && this.downloadsShowing; | 6580 return !this.$['search-input'].getValue() && this.downloadsShowing; |
11661 }, | 6581 }, |
11662 | |
11663 onFindCommand: function() { | 6582 onFindCommand: function() { |
11664 this.$['search-input'].showAndFocus(); | 6583 this.$['search-input'].showAndFocus(); |
11665 }, | 6584 }, |
11666 | |
11667 /** @private */ | |
11668 closeMoreActions_: function() { | 6585 closeMoreActions_: function() { |
11669 this.$.more.close(); | 6586 this.$.more.close(); |
11670 }, | 6587 }, |
11671 | |
11672 /** @private */ | |
11673 downloadsShowingChanged_: function() { | 6588 downloadsShowingChanged_: function() { |
11674 this.updateClearAll_(); | 6589 this.updateClearAll_(); |
11675 }, | 6590 }, |
11676 | |
11677 /** @private */ | |
11678 onClearAllTap_: function() { | 6591 onClearAllTap_: function() { |
11679 assert(this.canClearAll()); | 6592 assert(this.canClearAll()); |
11680 downloads.ActionService.getInstance().clearAll(); | 6593 downloads.ActionService.getInstance().clearAll(); |
11681 }, | 6594 }, |
11682 | |
11683 /** @private */ | |
11684 onPaperDropdownClose_: function() { | 6595 onPaperDropdownClose_: function() { |
11685 window.removeEventListener('resize', assert(this.boundClose_)); | 6596 window.removeEventListener('resize', assert(this.boundClose_)); |
11686 }, | 6597 }, |
11687 | |
11688 /** | |
11689 * @param {!Event} e | |
11690 * @private | |
11691 */ | |
11692 onItemBlur_: function(e) { | 6598 onItemBlur_: function(e) { |
11693 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 6599 var menu = this.$$('paper-menu'); |
11694 if (menu.items.indexOf(e.relatedTarget) >= 0) | 6600 if (menu.items.indexOf(e.relatedTarget) >= 0) return; |
11695 return; | |
11696 | |
11697 this.$.more.restoreFocusOnClose = false; | 6601 this.$.more.restoreFocusOnClose = false; |
11698 this.closeMoreActions_(); | 6602 this.closeMoreActions_(); |
11699 this.$.more.restoreFocusOnClose = true; | 6603 this.$.more.restoreFocusOnClose = true; |
11700 }, | 6604 }, |
11701 | |
11702 /** @private */ | |
11703 onPaperDropdownOpen_: function() { | 6605 onPaperDropdownOpen_: function() { |
11704 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 6606 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); |
11705 window.addEventListener('resize', this.boundClose_); | 6607 window.addEventListener('resize', this.boundClose_); |
11706 }, | 6608 }, |
11707 | |
11708 /** | |
11709 * @param {!CustomEvent} event | |
11710 * @private | |
11711 */ | |
11712 onSearchChanged_: function(event) { | 6609 onSearchChanged_: function(event) { |
11713 downloads.ActionService.getInstance().search( | 6610 downloads.ActionService.getInstance().search(event.detail); |
11714 /** @type {string} */ (event.detail)); | |
11715 this.updateClearAll_(); | 6611 this.updateClearAll_(); |
11716 }, | 6612 }, |
11717 | |
11718 /** @private */ | |
11719 onOpenDownloadsFolderTap_: function() { | 6613 onOpenDownloadsFolderTap_: function() { |
11720 downloads.ActionService.getInstance().openDownloadsFolder(); | 6614 downloads.ActionService.getInstance().openDownloadsFolder(); |
11721 }, | 6615 }, |
11722 | |
11723 /** @private */ | |
11724 updateClearAll_: function() { | 6616 updateClearAll_: function() { |
11725 this.$$('#actions .clear-all').hidden = !this.canClearAll(); | 6617 this.$$('#actions .clear-all').hidden = !this.canClearAll(); |
11726 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); | 6618 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); |
11727 }, | 6619 } |
11728 }); | 6620 }); |
11729 | 6621 return { |
11730 return {Toolbar: Toolbar}; | 6622 Toolbar: Toolbar |
| 6623 }; |
11731 }); | 6624 }); |
| 6625 |
11732 // Copyright 2015 The Chromium Authors. All rights reserved. | 6626 // Copyright 2015 The Chromium Authors. All rights reserved. |
11733 // Use of this source code is governed by a BSD-style license that can be | 6627 // Use of this source code is governed by a BSD-style license that can be |
11734 // found in the LICENSE file. | 6628 // found in the LICENSE file. |
11735 | |
11736 cr.define('downloads', function() { | 6629 cr.define('downloads', function() { |
11737 var Manager = Polymer({ | 6630 var Manager = Polymer({ |
11738 is: 'downloads-manager', | 6631 is: 'downloads-manager', |
11739 | |
11740 properties: { | 6632 properties: { |
11741 hasDownloads_: { | 6633 hasDownloads_: { |
11742 observer: 'hasDownloadsChanged_', | 6634 observer: 'hasDownloadsChanged_', |
11743 type: Boolean, | 6635 type: Boolean |
11744 }, | 6636 }, |
11745 | |
11746 items_: { | 6637 items_: { |
11747 type: Array, | 6638 type: Array, |
11748 value: function() { return []; }, | 6639 value: function() { |
11749 }, | 6640 return []; |
11750 }, | 6641 } |
11751 | 6642 } |
| 6643 }, |
11752 hostAttributes: { | 6644 hostAttributes: { |
11753 loading: true, | 6645 loading: true |
11754 }, | 6646 }, |
11755 | |
11756 listeners: { | 6647 listeners: { |
11757 'downloads-list.scroll': 'onListScroll_', | 6648 'downloads-list.scroll': 'onListScroll_' |
11758 }, | 6649 }, |
11759 | 6650 observers: [ 'itemsChanged_(items_.*)' ], |
11760 observers: [ | |
11761 'itemsChanged_(items_.*)', | |
11762 ], | |
11763 | |
11764 /** @private */ | |
11765 clearAll_: function() { | 6651 clearAll_: function() { |
11766 this.set('items_', []); | 6652 this.set('items_', []); |
11767 }, | 6653 }, |
11768 | |
11769 /** @private */ | |
11770 hasDownloadsChanged_: function() { | 6654 hasDownloadsChanged_: function() { |
11771 if (loadTimeData.getBoolean('allowDeletingHistory')) | 6655 if (loadTimeData.getBoolean('allowDeletingHistory')) this.$.toolbar.downlo
adsShowing = this.hasDownloads_; |
11772 this.$.toolbar.downloadsShowing = this.hasDownloads_; | |
11773 | |
11774 if (this.hasDownloads_) { | 6656 if (this.hasDownloads_) { |
11775 this.$['downloads-list'].fire('iron-resize'); | 6657 this.$['downloads-list'].fire('iron-resize'); |
11776 } else { | 6658 } else { |
11777 var isSearching = downloads.ActionService.getInstance().isSearching(); | 6659 var isSearching = downloads.ActionService.getInstance().isSearching(); |
11778 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 6660 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; |
11779 this.$['no-downloads'].querySelector('span').textContent = | 6661 this.$['no-downloads'].querySelector('span').textContent = loadTimeData.
getString(messageToShow); |
11780 loadTimeData.getString(messageToShow); | 6662 } |
11781 } | 6663 }, |
11782 }, | |
11783 | |
11784 /** | |
11785 * @param {number} index | |
11786 * @param {!Array<!downloads.Data>} list | |
11787 * @private | |
11788 */ | |
11789 insertItems_: function(index, list) { | 6664 insertItems_: function(index, list) { |
11790 this.splice.apply(this, ['items_', index, 0].concat(list)); | 6665 this.splice.apply(this, [ 'items_', index, 0 ].concat(list)); |
11791 this.updateHideDates_(index, index + list.length); | 6666 this.updateHideDates_(index, index + list.length); |
11792 this.removeAttribute('loading'); | 6667 this.removeAttribute('loading'); |
11793 }, | 6668 }, |
11794 | |
11795 /** @private */ | |
11796 itemsChanged_: function() { | 6669 itemsChanged_: function() { |
11797 this.hasDownloads_ = this.items_.length > 0; | 6670 this.hasDownloads_ = this.items_.length > 0; |
11798 }, | 6671 }, |
11799 | |
11800 /** | |
11801 * @param {Event} e | |
11802 * @private | |
11803 */ | |
11804 onCanExecute_: function(e) { | 6672 onCanExecute_: function(e) { |
11805 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 6673 e = e; |
11806 switch (e.command.id) { | 6674 switch (e.command.id) { |
11807 case 'undo-command': | 6675 case 'undo-command': |
11808 e.canExecute = this.$.toolbar.canUndo(); | 6676 e.canExecute = this.$.toolbar.canUndo(); |
11809 break; | 6677 break; |
11810 case 'clear-all-command': | 6678 |
11811 e.canExecute = this.$.toolbar.canClearAll(); | 6679 case 'clear-all-command': |
11812 break; | 6680 e.canExecute = this.$.toolbar.canClearAll(); |
11813 case 'find-command': | 6681 break; |
11814 e.canExecute = true; | 6682 |
11815 break; | 6683 case 'find-command': |
11816 } | 6684 e.canExecute = true; |
11817 }, | 6685 break; |
11818 | 6686 } |
11819 /** | 6687 }, |
11820 * @param {Event} e | |
11821 * @private | |
11822 */ | |
11823 onCommand_: function(e) { | 6688 onCommand_: function(e) { |
11824 if (e.command.id == 'clear-all-command') | 6689 if (e.command.id == 'clear-all-command') downloads.ActionService.getInstan
ce().clearAll(); else if (e.command.id == 'undo-command') downloads.ActionServic
e.getInstance().undo(); else if (e.command.id == 'find-command') this.$.toolbar.
onFindCommand(); |
11825 downloads.ActionService.getInstance().clearAll(); | 6690 }, |
11826 else if (e.command.id == 'undo-command') | |
11827 downloads.ActionService.getInstance().undo(); | |
11828 else if (e.command.id == 'find-command') | |
11829 this.$.toolbar.onFindCommand(); | |
11830 }, | |
11831 | |
11832 /** @private */ | |
11833 onListScroll_: function() { | 6691 onListScroll_: function() { |
11834 var list = this.$['downloads-list']; | 6692 var list = this.$['downloads-list']; |
11835 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 6693 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { |
11836 // Approaching the end of the scrollback. Attempt to load more items. | |
11837 downloads.ActionService.getInstance().loadMore(); | 6694 downloads.ActionService.getInstance().loadMore(); |
11838 } | 6695 } |
11839 }, | 6696 }, |
11840 | |
11841 /** @private */ | |
11842 onLoad_: function() { | 6697 onLoad_: function() { |
11843 cr.ui.decorate('command', cr.ui.Command); | 6698 cr.ui.decorate('command', cr.ui.Command); |
11844 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 6699 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
11845 document.addEventListener('command', this.onCommand_.bind(this)); | 6700 document.addEventListener('command', this.onCommand_.bind(this)); |
11846 | |
11847 downloads.ActionService.getInstance().loadMore(); | 6701 downloads.ActionService.getInstance().loadMore(); |
11848 }, | 6702 }, |
11849 | |
11850 /** | |
11851 * @param {number} index | |
11852 * @private | |
11853 */ | |
11854 removeItem_: function(index) { | 6703 removeItem_: function(index) { |
11855 this.splice('items_', index, 1); | 6704 this.splice('items_', index, 1); |
11856 this.updateHideDates_(index, index); | 6705 this.updateHideDates_(index, index); |
11857 this.onListScroll_(); | 6706 this.onListScroll_(); |
11858 }, | 6707 }, |
11859 | |
11860 /** | |
11861 * @param {number} start | |
11862 * @param {number} end | |
11863 * @private | |
11864 */ | |
11865 updateHideDates_: function(start, end) { | 6708 updateHideDates_: function(start, end) { |
11866 for (var i = start; i <= end; ++i) { | 6709 for (var i = start; i <= end; ++i) { |
11867 var current = this.items_[i]; | 6710 var current = this.items_[i]; |
11868 if (!current) | 6711 if (!current) continue; |
11869 continue; | |
11870 var prev = this.items_[i - 1]; | 6712 var prev = this.items_[i - 1]; |
11871 current.hideDate = !!prev && prev.date_string == current.date_string; | 6713 current.hideDate = !!prev && prev.date_string == current.date_string; |
11872 } | 6714 } |
11873 }, | 6715 }, |
11874 | |
11875 /** | |
11876 * @param {number} index | |
11877 * @param {!downloads.Data} data | |
11878 * @private | |
11879 */ | |
11880 updateItem_: function(index, data) { | 6716 updateItem_: function(index, data) { |
11881 this.set('items_.' + index, data); | 6717 this.set('items_.' + index, data); |
11882 this.updateHideDates_(index, index); | 6718 this.updateHideDates_(index, index); |
11883 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | 6719 var list = this.$['downloads-list']; |
11884 list.updateSizeForItem(index); | 6720 list.updateSizeForItem(index); |
11885 }, | 6721 } |
11886 }); | 6722 }); |
11887 | |
11888 Manager.clearAll = function() { | 6723 Manager.clearAll = function() { |
11889 Manager.get().clearAll_(); | 6724 Manager.get().clearAll_(); |
11890 }; | 6725 }; |
11891 | |
11892 /** @return {!downloads.Manager} */ | |
11893 Manager.get = function() { | 6726 Manager.get = function() { |
11894 return /** @type {!downloads.Manager} */( | 6727 return queryRequiredElement('downloads-manager'); |
11895 queryRequiredElement('downloads-manager')); | 6728 }; |
11896 }; | |
11897 | |
11898 Manager.insertItems = function(index, list) { | 6729 Manager.insertItems = function(index, list) { |
11899 Manager.get().insertItems_(index, list); | 6730 Manager.get().insertItems_(index, list); |
11900 }; | 6731 }; |
11901 | |
11902 Manager.onLoad = function() { | 6732 Manager.onLoad = function() { |
11903 Manager.get().onLoad_(); | 6733 Manager.get().onLoad_(); |
11904 }; | 6734 }; |
11905 | |
11906 Manager.removeItem = function(index) { | 6735 Manager.removeItem = function(index) { |
11907 Manager.get().removeItem_(index); | 6736 Manager.get().removeItem_(index); |
11908 }; | 6737 }; |
11909 | |
11910 Manager.updateItem = function(index, data) { | 6738 Manager.updateItem = function(index, data) { |
11911 Manager.get().updateItem_(index, data); | 6739 Manager.get().updateItem_(index, data); |
11912 }; | 6740 }; |
11913 | 6741 return { |
11914 return {Manager: Manager}; | 6742 Manager: Manager |
| 6743 }; |
11915 }); | 6744 }); |
| 6745 |
11916 // Copyright 2015 The Chromium Authors. All rights reserved. | 6746 // Copyright 2015 The Chromium Authors. All rights reserved. |
11917 // Use of this source code is governed by a BSD-style license that can be | 6747 // Use of this source code is governed by a BSD-style license that can be |
11918 // found in the LICENSE file. | 6748 // found in the LICENSE file. |
11919 | |
11920 window.addEventListener('load', downloads.Manager.onLoad); | 6749 window.addEventListener('load', downloads.Manager.onLoad); |
OLD | NEW |