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 | 4 |
5 /** | |
6 * @fileoverview Assertion support. | |
7 */ | |
8 | 5 |
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) { | 6 function assert(condition, opt_message) { |
19 if (!condition) { | 7 if (!condition) { |
20 var message = 'Assertion failed'; | 8 var message = 'Assertion failed'; |
21 if (opt_message) | 9 if (opt_message) |
22 message = message + ': ' + opt_message; | 10 message = message + ': ' + opt_message; |
23 var error = new Error(message); | 11 var error = new Error(message); |
24 var global = function() { return this; }(); | 12 var global = function() { return this; }(); |
25 if (global.traceAssertionsForTesting) | 13 if (global.traceAssertionsForTesting) |
26 console.warn(error.stack); | 14 console.warn(error.stack); |
27 throw error; | 15 throw error; |
28 } | 16 } |
29 return condition; | 17 return condition; |
30 } | 18 } |
31 | 19 |
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) { | 20 function assertNotReached(opt_message) { |
54 assert(false, opt_message || 'Unreachable code hit'); | 21 assert(false, opt_message || 'Unreachable code hit'); |
55 } | 22 } |
56 | 23 |
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) { | 24 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)) { | 25 if (!(value instanceof type)) { |
68 assertNotReached(opt_message || 'Value ' + value + | 26 assertNotReached(opt_message || 'Value ' + value + |
69 ' is not a[n] ' + (type.name || typeof type)); | 27 ' is not a[n] ' + (type.name || typeof type)); |
70 } | 28 } |
71 return value; | 29 return value; |
72 }; | 30 }; |
73 // Copyright 2016 The Chromium Authors. All rights reserved. | 31 // Copyright 2016 The Chromium Authors. All rights reserved. |
74 // Use of this source code is governed by a BSD-style license that can be | 32 // Use of this source code is governed by a BSD-style license that can be |
75 // found in the LICENSE file. | 33 // found in the LICENSE file. |
76 | 34 |
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 | 35 |
91 /** | |
92 * @constructor @struct | |
93 * @template T | |
94 */ | |
95 function PromiseResolver() { | 36 function PromiseResolver() { |
96 /** @private {function(T=): void} */ | 37 /** @private {function(T=): void} */ |
97 this.resolve_; | 38 this.resolve_; |
98 | 39 |
99 /** @private {function(*=): void} */ | 40 /** @private {function(*=): void} */ |
100 this.reject_; | 41 this.reject_; |
101 | 42 |
102 /** @private {!Promise<T>} */ | 43 /** @private {!Promise<T>} */ |
103 this.promise_ = new Promise(function(resolve, reject) { | 44 this.promise_ = new Promise(function(resolve, reject) { |
104 this.resolve_ = resolve; | 45 this.resolve_ = resolve; |
(...skipping 11 matching lines...) Expand all Loading... |
116 set resolve(r) { assertNotReached(); }, | 57 set resolve(r) { assertNotReached(); }, |
117 | 58 |
118 /** @return {function(*=): void} */ | 59 /** @return {function(*=): void} */ |
119 get reject() { return this.reject_; }, | 60 get reject() { return this.reject_; }, |
120 set reject(s) { assertNotReached(); }, | 61 set reject(s) { assertNotReached(); }, |
121 }; | 62 }; |
122 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 63 // 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 | 64 // Use of this source code is governed by a BSD-style license that can be |
124 // found in the LICENSE file. | 65 // found in the LICENSE file. |
125 | 66 |
126 /** | |
127 * The global object. | |
128 * @type {!Object} | |
129 * @const | |
130 */ | |
131 var global = this; | 67 var global = this; |
132 | 68 |
133 /** @typedef {{eventName: string, uid: number}} */ | 69 /** @typedef {{eventName: string, uid: number}} */ |
134 var WebUIListener; | 70 var WebUIListener; |
135 | 71 |
136 /** Platform, package, object property, and Event support. **/ | 72 /** Platform, package, object property, and Event support. **/ |
137 var cr = cr || function() { | 73 var cr = cr || function() { |
138 'use strict'; | 74 'use strict'; |
139 | 75 |
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) { | 76 function exportPath(name, opt_object, opt_objectToExportTo) { |
154 var parts = name.split('.'); | 77 var parts = name.split('.'); |
155 var cur = opt_objectToExportTo || global; | 78 var cur = opt_objectToExportTo || global; |
156 | 79 |
157 for (var part; parts.length && (part = parts.shift());) { | 80 for (var part; parts.length && (part = parts.shift());) { |
158 if (!parts.length && opt_object !== undefined) { | 81 if (!parts.length && opt_object !== undefined) { |
159 // last part and we have an object; use it | |
160 cur[part] = opt_object; | 82 cur[part] = opt_object; |
161 } else if (part in cur) { | 83 } else if (part in cur) { |
162 cur = cur[part]; | 84 cur = cur[part]; |
163 } else { | 85 } else { |
164 cur = cur[part] = {}; | 86 cur = cur[part] = {}; |
165 } | 87 } |
166 } | 88 } |
167 return cur; | 89 return cur; |
168 } | 90 } |
169 | 91 |
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) { | 92 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
178 var e = new Event(propertyName + 'Change'); | 93 var e = new Event(propertyName + 'Change'); |
179 e.propertyName = propertyName; | 94 e.propertyName = propertyName; |
180 e.newValue = newValue; | 95 e.newValue = newValue; |
181 e.oldValue = oldValue; | 96 e.oldValue = oldValue; |
182 target.dispatchEvent(e); | 97 target.dispatchEvent(e); |
183 } | 98 } |
184 | 99 |
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) { | 100 function getAttributeName(jsName) { |
192 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); | 101 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
193 } | 102 } |
194 | 103 |
195 /** | |
196 * The kind of property to define in {@code defineProperty}. | |
197 * @enum {string} | |
198 * @const | |
199 */ | |
200 var PropertyKind = { | 104 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', | 105 JS: 'js', |
207 | 106 |
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', | 107 ATTR: 'attr', |
213 | 108 |
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' | 109 BOOL_ATTR: 'boolAttr' |
220 }; | 110 }; |
221 | 111 |
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) { | 112 function getGetter(name, kind) { |
230 switch (kind) { | 113 switch (kind) { |
231 case PropertyKind.JS: | 114 case PropertyKind.JS: |
232 var privateName = name + '_'; | 115 var privateName = name + '_'; |
233 return function() { | 116 return function() { |
234 return this[privateName]; | 117 return this[privateName]; |
235 }; | 118 }; |
236 case PropertyKind.ATTR: | 119 case PropertyKind.ATTR: |
237 var attributeName = getAttributeName(name); | 120 var attributeName = getAttributeName(name); |
238 return function() { | 121 return function() { |
239 return this.getAttribute(attributeName); | 122 return this.getAttribute(attributeName); |
240 }; | 123 }; |
241 case PropertyKind.BOOL_ATTR: | 124 case PropertyKind.BOOL_ATTR: |
242 var attributeName = getAttributeName(name); | 125 var attributeName = getAttributeName(name); |
243 return function() { | 126 return function() { |
244 return this.hasAttribute(attributeName); | 127 return this.hasAttribute(attributeName); |
245 }; | 128 }; |
246 } | 129 } |
247 | 130 |
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'; | 131 throw 'not reached'; |
251 } | 132 } |
252 | 133 |
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) { | 134 function getSetter(name, kind, opt_setHook) { |
265 switch (kind) { | 135 switch (kind) { |
266 case PropertyKind.JS: | 136 case PropertyKind.JS: |
267 var privateName = name + '_'; | 137 var privateName = name + '_'; |
268 return function(value) { | 138 return function(value) { |
269 var oldValue = this[name]; | 139 var oldValue = this[name]; |
270 if (value !== oldValue) { | 140 if (value !== oldValue) { |
271 this[privateName] = value; | 141 this[privateName] = value; |
272 if (opt_setHook) | 142 if (opt_setHook) |
273 opt_setHook.call(this, value, oldValue); | 143 opt_setHook.call(this, value, oldValue); |
(...skipping 25 matching lines...) Expand all Loading... |
299 this.setAttribute(attributeName, name); | 169 this.setAttribute(attributeName, name); |
300 else | 170 else |
301 this.removeAttribute(attributeName); | 171 this.removeAttribute(attributeName); |
302 if (opt_setHook) | 172 if (opt_setHook) |
303 opt_setHook.call(this, value, oldValue); | 173 opt_setHook.call(this, value, oldValue); |
304 dispatchPropertyChange(this, name, value, oldValue); | 174 dispatchPropertyChange(this, name, value, oldValue); |
305 } | 175 } |
306 }; | 176 }; |
307 } | 177 } |
308 | 178 |
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'; | 179 throw 'not reached'; |
312 } | 180 } |
313 | 181 |
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) { | 182 function defineProperty(obj, name, opt_kind, opt_setHook) { |
324 if (typeof obj == 'function') | 183 if (typeof obj == 'function') |
325 obj = obj.prototype; | 184 obj = obj.prototype; |
326 | 185 |
327 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); | 186 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); |
328 | 187 |
329 if (!obj.__lookupGetter__(name)) | 188 if (!obj.__lookupGetter__(name)) |
330 obj.__defineGetter__(name, getGetter(name, kind)); | 189 obj.__defineGetter__(name, getGetter(name, kind)); |
331 | 190 |
332 if (!obj.__lookupSetter__(name)) | 191 if (!obj.__lookupSetter__(name)) |
333 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); | 192 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); |
334 } | 193 } |
335 | 194 |
336 /** | |
337 * Counter for use with createUid | |
338 */ | |
339 var uidCounter = 1; | 195 var uidCounter = 1; |
340 | 196 |
341 /** | |
342 * @return {number} A new unique ID. | |
343 */ | |
344 function createUid() { | 197 function createUid() { |
345 return uidCounter++; | 198 return uidCounter++; |
346 } | 199 } |
347 | 200 |
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) { | 201 function getUid(item) { |
355 if (item.hasOwnProperty('uid')) | 202 if (item.hasOwnProperty('uid')) |
356 return item.uid; | 203 return item.uid; |
357 return item.uid = createUid(); | 204 return item.uid = createUid(); |
358 } | 205 } |
359 | 206 |
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) { | 207 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
371 var e = new Event(type, { | 208 var e = new Event(type, { |
372 bubbles: opt_bubbles, | 209 bubbles: opt_bubbles, |
373 cancelable: opt_cancelable === undefined || opt_cancelable | 210 cancelable: opt_cancelable === undefined || opt_cancelable |
374 }); | 211 }); |
375 return target.dispatchEvent(e); | 212 return target.dispatchEvent(e); |
376 } | 213 } |
377 | 214 |
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) { | 215 function define(name, fun) { |
398 var obj = exportPath(name); | 216 var obj = exportPath(name); |
399 var exports = fun(); | 217 var exports = fun(); |
400 for (var propertyName in exports) { | 218 for (var propertyName in exports) { |
401 // Maybe we should check the prototype chain here? The current usage | |
402 // pattern is always using an object literal so we only care about own | |
403 // properties. | |
404 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, | 219 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, |
405 propertyName); | 220 propertyName); |
406 if (propertyDescriptor) | 221 if (propertyDescriptor) |
407 Object.defineProperty(obj, propertyName, propertyDescriptor); | 222 Object.defineProperty(obj, propertyName, propertyDescriptor); |
408 } | 223 } |
409 } | 224 } |
410 | 225 |
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) { | 226 function addSingletonGetter(ctor) { |
418 ctor.getInstance = function() { | 227 ctor.getInstance = function() { |
419 return ctor.instance_ || (ctor.instance_ = new ctor()); | 228 return ctor.instance_ || (ctor.instance_ = new ctor()); |
420 }; | 229 }; |
421 } | 230 } |
422 | 231 |
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) { | 232 function makePublic(ctor, methods, opt_target) { |
432 methods.forEach(function(method) { | 233 methods.forEach(function(method) { |
433 ctor[method] = function() { | 234 ctor[method] = function() { |
434 var target = opt_target ? document.getElementById(opt_target) : | 235 var target = opt_target ? document.getElementById(opt_target) : |
435 ctor.getInstance(); | 236 ctor.getInstance(); |
436 return target[method + '_'].apply(target, arguments); | 237 return target[method + '_'].apply(target, arguments); |
437 }; | 238 }; |
438 }); | 239 }); |
439 } | 240 } |
440 | 241 |
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 = {}; | 242 var chromeSendResolverMap = {}; |
449 | 243 |
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) { | 244 function webUIResponse(id, isSuccess, response) { |
464 var resolver = chromeSendResolverMap[id]; | 245 var resolver = chromeSendResolverMap[id]; |
465 delete chromeSendResolverMap[id]; | 246 delete chromeSendResolverMap[id]; |
466 | 247 |
467 if (isSuccess) | 248 if (isSuccess) |
468 resolver.resolve(response); | 249 resolver.resolve(response); |
469 else | 250 else |
470 resolver.reject(response); | 251 resolver.reject(response); |
471 } | 252 } |
472 | 253 |
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) { | 254 function sendWithPromise(methodName, var_args) { |
482 var args = Array.prototype.slice.call(arguments, 1); | 255 var args = Array.prototype.slice.call(arguments, 1); |
483 var promiseResolver = new PromiseResolver(); | 256 var promiseResolver = new PromiseResolver(); |
484 var id = methodName + '_' + createUid(); | 257 var id = methodName + '_' + createUid(); |
485 chromeSendResolverMap[id] = promiseResolver; | 258 chromeSendResolverMap[id] = promiseResolver; |
486 chrome.send(methodName, [id].concat(args)); | 259 chrome.send(methodName, [id].concat(args)); |
487 return promiseResolver.promise; | 260 return promiseResolver.promise; |
488 } | 261 } |
489 | 262 |
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 = {}; | 263 var webUIListenerMap = {}; |
498 | 264 |
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) { | 265 function webUIListenerCallback(event, var_args) { |
508 var eventListenersMap = webUIListenerMap[event]; | 266 var eventListenersMap = webUIListenerMap[event]; |
509 if (!eventListenersMap) { | 267 if (!eventListenersMap) { |
510 // C++ event sent for an event that has no listeners. | |
511 // TODO(dpapad): Should a warning be displayed here? | |
512 return; | 268 return; |
513 } | 269 } |
514 | 270 |
515 var args = Array.prototype.slice.call(arguments, 1); | 271 var args = Array.prototype.slice.call(arguments, 1); |
516 for (var listenerId in eventListenersMap) { | 272 for (var listenerId in eventListenersMap) { |
517 eventListenersMap[listenerId].apply(null, args); | 273 eventListenersMap[listenerId].apply(null, args); |
518 } | 274 } |
519 } | 275 } |
520 | 276 |
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) { | 277 function addWebUIListener(eventName, callback) { |
530 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; | 278 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
531 var uid = createUid(); | 279 var uid = createUid(); |
532 webUIListenerMap[eventName][uid] = callback; | 280 webUIListenerMap[eventName][uid] = callback; |
533 return {eventName: eventName, uid: uid}; | 281 return {eventName: eventName, uid: uid}; |
534 } | 282 } |
535 | 283 |
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) { | 284 function removeWebUIListener(listener) { |
544 var listenerExists = webUIListenerMap[listener.eventName] && | 285 var listenerExists = webUIListenerMap[listener.eventName] && |
545 webUIListenerMap[listener.eventName][listener.uid]; | 286 webUIListenerMap[listener.eventName][listener.uid]; |
546 if (listenerExists) { | 287 if (listenerExists) { |
547 delete webUIListenerMap[listener.eventName][listener.uid]; | 288 delete webUIListenerMap[listener.eventName][listener.uid]; |
548 return true; | 289 return true; |
549 } | 290 } |
550 return false; | 291 return false; |
551 } | 292 } |
552 | 293 |
553 return { | 294 return { |
554 addSingletonGetter: addSingletonGetter, | 295 addSingletonGetter: addSingletonGetter, |
555 createUid: createUid, | 296 createUid: createUid, |
556 define: define, | 297 define: define, |
557 defineProperty: defineProperty, | 298 defineProperty: defineProperty, |
558 dispatchPropertyChange: dispatchPropertyChange, | 299 dispatchPropertyChange: dispatchPropertyChange, |
559 dispatchSimpleEvent: dispatchSimpleEvent, | 300 dispatchSimpleEvent: dispatchSimpleEvent, |
560 exportPath: exportPath, | 301 exportPath: exportPath, |
561 getUid: getUid, | 302 getUid: getUid, |
562 makePublic: makePublic, | 303 makePublic: makePublic, |
563 PropertyKind: PropertyKind, | 304 PropertyKind: PropertyKind, |
564 | 305 |
565 // C++ <-> JS communication related methods. | |
566 addWebUIListener: addWebUIListener, | 306 addWebUIListener: addWebUIListener, |
567 removeWebUIListener: removeWebUIListener, | 307 removeWebUIListener: removeWebUIListener, |
568 sendWithPromise: sendWithPromise, | 308 sendWithPromise: sendWithPromise, |
569 webUIListenerCallback: webUIListenerCallback, | 309 webUIListenerCallback: webUIListenerCallback, |
570 webUIResponse: webUIResponse, | 310 webUIResponse: webUIResponse, |
571 | 311 |
572 get doc() { | 312 get doc() { |
573 return document; | 313 return document; |
574 }, | 314 }, |
575 | 315 |
(...skipping 27 matching lines...) Expand all Loading... |
603 return /iPad|iPhone|iPod/.test(navigator.platform); | 343 return /iPad|iPhone|iPod/.test(navigator.platform); |
604 } | 344 } |
605 }; | 345 }; |
606 }(); | 346 }(); |
607 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 347 // 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 | 348 // Use of this source code is governed by a BSD-style license that can be |
609 // found in the LICENSE file. | 349 // found in the LICENSE file. |
610 | 350 |
611 cr.define('cr.ui', function() { | 351 cr.define('cr.ui', function() { |
612 | 352 |
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) { | 353 function decorate(source, constr) { |
622 var elements; | 354 var elements; |
623 if (typeof source == 'string') | 355 if (typeof source == 'string') |
624 elements = cr.doc.querySelectorAll(source); | 356 elements = cr.doc.querySelectorAll(source); |
625 else | 357 else |
626 elements = [source]; | 358 elements = [source]; |
627 | 359 |
628 for (var i = 0, el; el = elements[i]; i++) { | 360 for (var i = 0, el; el = elements[i]; i++) { |
629 if (!(el instanceof constr)) | 361 if (!(el instanceof constr)) |
630 constr.decorate(el); | 362 constr.decorate(el); |
631 } | 363 } |
632 } | 364 } |
633 | 365 |
634 /** | |
635 * Helper function for creating new element for define. | |
636 */ | |
637 function createElementHelper(tagName, opt_bag) { | 366 function createElementHelper(tagName, opt_bag) { |
638 // Allow passing in ownerDocument to create in a different document. | |
639 var doc; | 367 var doc; |
640 if (opt_bag && opt_bag.ownerDocument) | 368 if (opt_bag && opt_bag.ownerDocument) |
641 doc = opt_bag.ownerDocument; | 369 doc = opt_bag.ownerDocument; |
642 else | 370 else |
643 doc = cr.doc; | 371 doc = cr.doc; |
644 return doc.createElement(tagName); | 372 return doc.createElement(tagName); |
645 } | 373 } |
646 | 374 |
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) { | 375 function define(tagNameOrFunction) { |
670 var createFunction, tagName; | 376 var createFunction, tagName; |
671 if (typeof tagNameOrFunction == 'function') { | 377 if (typeof tagNameOrFunction == 'function') { |
672 createFunction = tagNameOrFunction; | 378 createFunction = tagNameOrFunction; |
673 tagName = ''; | 379 tagName = ''; |
674 } else { | 380 } else { |
675 createFunction = createElementHelper; | 381 createFunction = createElementHelper; |
676 tagName = tagNameOrFunction; | 382 tagName = tagNameOrFunction; |
677 } | 383 } |
678 | 384 |
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) { | 385 function f(opt_propertyBag) { |
688 var el = createFunction(tagName, opt_propertyBag); | 386 var el = createFunction(tagName, opt_propertyBag); |
689 f.decorate(el); | 387 f.decorate(el); |
690 for (var propertyName in opt_propertyBag) { | 388 for (var propertyName in opt_propertyBag) { |
691 el[propertyName] = opt_propertyBag[propertyName]; | 389 el[propertyName] = opt_propertyBag[propertyName]; |
692 } | 390 } |
693 return el; | 391 return el; |
694 } | 392 } |
695 | 393 |
696 /** | |
697 * Decorates an element as a UI element class. | |
698 * @param {!Element} el The element to decorate. | |
699 */ | |
700 f.decorate = function(el) { | 394 f.decorate = function(el) { |
701 el.__proto__ = f.prototype; | 395 el.__proto__ = f.prototype; |
702 el.decorate(); | 396 el.decorate(); |
703 }; | 397 }; |
704 | 398 |
705 return f; | 399 return f; |
706 } | 400 } |
707 | 401 |
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) { | 402 function limitInputWidth(el, parentEl, min, opt_scale) { |
719 // Needs a size larger than borders | |
720 el.style.width = '10px'; | 403 el.style.width = '10px'; |
721 var doc = el.ownerDocument; | 404 var doc = el.ownerDocument; |
722 var win = doc.defaultView; | 405 var win = doc.defaultView; |
723 var computedStyle = win.getComputedStyle(el); | 406 var computedStyle = win.getComputedStyle(el); |
724 var parentComputedStyle = win.getComputedStyle(parentEl); | 407 var parentComputedStyle = win.getComputedStyle(parentEl); |
725 var rtl = computedStyle.direction == 'rtl'; | 408 var rtl = computedStyle.direction == 'rtl'; |
726 | 409 |
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 | 410 var inputRect = el.getBoundingClientRect(); // box-sizing |
730 var parentRect = parentEl.getBoundingClientRect(); | 411 var parentRect = parentEl.getBoundingClientRect(); |
731 var startPos = rtl ? parentRect.right - inputRect.right : | 412 var startPos = rtl ? parentRect.right - inputRect.right : |
732 inputRect.left - parentRect.left; | 413 inputRect.left - parentRect.left; |
733 | 414 |
734 // Add up border and padding of the input. | |
735 var inner = parseInt(computedStyle.borderLeftWidth, 10) + | 415 var inner = parseInt(computedStyle.borderLeftWidth, 10) + |
736 parseInt(computedStyle.paddingLeft, 10) + | 416 parseInt(computedStyle.paddingLeft, 10) + |
737 parseInt(computedStyle.paddingRight, 10) + | 417 parseInt(computedStyle.paddingRight, 10) + |
738 parseInt(computedStyle.borderRightWidth, 10); | 418 parseInt(computedStyle.borderRightWidth, 10); |
739 | 419 |
740 // We also need to subtract the padding of parent to prevent it to overflow. | |
741 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : | 420 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : |
742 parseInt(parentComputedStyle.paddingRight, 10); | 421 parseInt(parentComputedStyle.paddingRight, 10); |
743 | 422 |
744 var max = parentEl.clientWidth - startPos - inner - parentPadding; | 423 var max = parentEl.clientWidth - startPos - inner - parentPadding; |
745 if (opt_scale) | 424 if (opt_scale) |
746 max *= opt_scale; | 425 max *= opt_scale; |
747 | 426 |
748 function limit() { | 427 function limit() { |
749 if (el.scrollWidth > max) { | 428 if (el.scrollWidth > max) { |
750 el.style.width = max + 'px'; | 429 el.style.width = max + 'px'; |
751 } else { | 430 } else { |
752 el.style.width = 0; | 431 el.style.width = 0; |
753 var sw = el.scrollWidth; | 432 var sw = el.scrollWidth; |
754 if (sw < min) { | 433 if (sw < min) { |
755 el.style.width = min + 'px'; | 434 el.style.width = min + 'px'; |
756 } else { | 435 } else { |
757 el.style.width = sw + 'px'; | 436 el.style.width = sw + 'px'; |
758 } | 437 } |
759 } | 438 } |
760 } | 439 } |
761 | 440 |
762 el.addEventListener('input', limit); | 441 el.addEventListener('input', limit); |
763 limit(); | 442 limit(); |
764 } | 443 } |
765 | 444 |
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) { | 445 function toCssPx(pixels) { |
773 if (!window.isFinite(pixels)) | 446 if (!window.isFinite(pixels)) |
774 console.error('Pixel value is not a number: ' + pixels); | 447 console.error('Pixel value is not a number: ' + pixels); |
775 return Math.round(pixels) + 'px'; | 448 return Math.round(pixels) + 'px'; |
776 } | 449 } |
777 | 450 |
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) { | 451 function swallowDoubleClick(e) { |
785 var doc = e.target.ownerDocument; | 452 var doc = e.target.ownerDocument; |
786 var counter = Math.min(1, e.detail); | 453 var counter = Math.min(1, e.detail); |
787 function swallow(e) { | 454 function swallow(e) { |
788 e.stopPropagation(); | 455 e.stopPropagation(); |
789 e.preventDefault(); | 456 e.preventDefault(); |
790 } | 457 } |
791 function onclick(e) { | 458 function onclick(e) { |
792 if (e.detail > counter) { | 459 if (e.detail > counter) { |
793 counter = e.detail; | 460 counter = e.detail; |
794 // Swallow the click since it's a click inside the doubleclick timeout. | |
795 swallow(e); | 461 swallow(e); |
796 } else { | 462 } else { |
797 // Stop tracking clicks and let regular handling. | |
798 doc.removeEventListener('dblclick', swallow, true); | 463 doc.removeEventListener('dblclick', swallow, true); |
799 doc.removeEventListener('click', onclick, true); | 464 doc.removeEventListener('click', onclick, true); |
800 } | 465 } |
801 } | 466 } |
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() { | 467 setTimeout(function() { |
806 doc.addEventListener('click', onclick, true); | 468 doc.addEventListener('click', onclick, true); |
807 doc.addEventListener('dblclick', swallow, true); | 469 doc.addEventListener('dblclick', swallow, true); |
808 }, 0); | 470 }, 0); |
809 } | 471 } |
810 | 472 |
811 return { | 473 return { |
812 decorate: decorate, | 474 decorate: decorate, |
813 define: define, | 475 define: define, |
814 limitInputWidth: limitInputWidth, | 476 limitInputWidth: limitInputWidth, |
815 toCssPx: toCssPx, | 477 toCssPx: toCssPx, |
816 swallowDoubleClick: swallowDoubleClick | 478 swallowDoubleClick: swallowDoubleClick |
817 }; | 479 }; |
818 }); | 480 }); |
819 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 481 // 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 | 482 // Use of this source code is governed by a BSD-style license that can be |
821 // found in the LICENSE file. | 483 // found in the LICENSE file. |
822 | 484 |
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 | 485 |
836 cr.define('cr.ui', function() { | 486 cr.define('cr.ui', function() { |
837 | 487 |
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) { | 488 function KeyboardShortcut(shortcut) { |
845 var mods = {}; | 489 var mods = {}; |
846 var ident = ''; | 490 var ident = ''; |
847 shortcut.split('|').forEach(function(part) { | 491 shortcut.split('|').forEach(function(part) { |
848 var partLc = part.toLowerCase(); | 492 var partLc = part.toLowerCase(); |
849 switch (partLc) { | 493 switch (partLc) { |
850 case 'alt': | 494 case 'alt': |
851 case 'ctrl': | 495 case 'ctrl': |
852 case 'meta': | 496 case 'meta': |
853 case 'shift': | 497 case 'shift': |
854 mods[partLc + 'Key'] = true; | 498 mods[partLc + 'Key'] = true; |
855 break; | 499 break; |
856 default: | 500 default: |
857 if (ident) | 501 if (ident) |
858 throw Error('Invalid shortcut'); | 502 throw Error('Invalid shortcut'); |
859 ident = part; | 503 ident = part; |
860 } | 504 } |
861 }); | 505 }); |
862 | 506 |
863 this.ident_ = ident; | 507 this.ident_ = ident; |
864 this.mods_ = mods; | 508 this.mods_ = mods; |
865 } | 509 } |
866 | 510 |
867 KeyboardShortcut.prototype = { | 511 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) { | 512 matchesEvent: function(e) { |
874 if (e.key == this.ident_) { | 513 if (e.key == this.ident_) { |
875 // All keyboard modifiers needs to match. | |
876 var mods = this.mods_; | 514 var mods = this.mods_; |
877 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { | 515 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { |
878 return e[k] == !!mods[k]; | 516 return e[k] == !!mods[k]; |
879 }); | 517 }); |
880 } | 518 } |
881 return false; | 519 return false; |
882 } | 520 } |
883 }; | 521 }; |
884 | 522 |
885 /** | |
886 * Creates a new command element. | |
887 * @constructor | |
888 * @extends {HTMLElement} | |
889 */ | |
890 var Command = cr.ui.define('command'); | 523 var Command = cr.ui.define('command'); |
891 | 524 |
892 Command.prototype = { | 525 Command.prototype = { |
893 __proto__: HTMLElement.prototype, | 526 __proto__: HTMLElement.prototype, |
894 | 527 |
895 /** | |
896 * Initializes the command. | |
897 */ | |
898 decorate: function() { | 528 decorate: function() { |
899 CommandManager.init(assert(this.ownerDocument)); | 529 CommandManager.init(assert(this.ownerDocument)); |
900 | 530 |
901 if (this.hasAttribute('shortcut')) | 531 if (this.hasAttribute('shortcut')) |
902 this.shortcut = this.getAttribute('shortcut'); | 532 this.shortcut = this.getAttribute('shortcut'); |
903 }, | 533 }, |
904 | 534 |
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) { | 535 execute: function(opt_element) { |
912 if (this.disabled) | 536 if (this.disabled) |
913 return; | 537 return; |
914 var doc = this.ownerDocument; | 538 var doc = this.ownerDocument; |
915 if (doc.activeElement) { | 539 if (doc.activeElement) { |
916 var e = new Event('command', {bubbles: true}); | 540 var e = new Event('command', {bubbles: true}); |
917 e.command = this; | 541 e.command = this; |
918 | 542 |
919 (opt_element || doc.activeElement).dispatchEvent(e); | 543 (opt_element || doc.activeElement).dispatchEvent(e); |
920 } | 544 } |
921 }, | 545 }, |
922 | 546 |
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) { | 547 canExecuteChange: function(opt_node) { |
929 dispatchCanExecuteEvent(this, | 548 dispatchCanExecuteEvent(this, |
930 opt_node || this.ownerDocument.activeElement); | 549 opt_node || this.ownerDocument.activeElement); |
931 }, | 550 }, |
932 | 551 |
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_: '', | 552 shortcut_: '', |
950 get shortcut() { | 553 get shortcut() { |
951 return this.shortcut_; | 554 return this.shortcut_; |
952 }, | 555 }, |
953 set shortcut(shortcut) { | 556 set shortcut(shortcut) { |
954 var oldShortcut = this.shortcut_; | 557 var oldShortcut = this.shortcut_; |
955 if (shortcut !== oldShortcut) { | 558 if (shortcut !== oldShortcut) { |
956 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { | 559 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { |
957 return new KeyboardShortcut(shortcut); | 560 return new KeyboardShortcut(shortcut); |
958 }); | 561 }); |
959 | 562 |
960 // Set this after the keyboardShortcuts_ since that might throw. | |
961 this.shortcut_ = shortcut; | 563 this.shortcut_ = shortcut; |
962 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, | 564 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, |
963 oldShortcut); | 565 oldShortcut); |
964 } | 566 } |
965 }, | 567 }, |
966 | 568 |
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) { | 569 matchesEvent: function(e) { |
973 if (!this.keyboardShortcuts_) | 570 if (!this.keyboardShortcuts_) |
974 return false; | 571 return false; |
975 | 572 |
976 return this.keyboardShortcuts_.some(function(keyboardShortcut) { | 573 return this.keyboardShortcuts_.some(function(keyboardShortcut) { |
977 return keyboardShortcut.matchesEvent(e); | 574 return keyboardShortcut.matchesEvent(e); |
978 }); | 575 }); |
979 }, | 576 }, |
980 }; | 577 }; |
981 | 578 |
982 /** | |
983 * The label of the command. | |
984 */ | |
985 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); | 579 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); |
986 | 580 |
987 /** | |
988 * Whether the command is disabled or not. | |
989 */ | |
990 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); | 581 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); |
991 | 582 |
992 /** | |
993 * Whether the command is hidden or not. | |
994 */ | |
995 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); | 583 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
996 | 584 |
997 /** | |
998 * Whether the command is checked or not. | |
999 */ | |
1000 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); | 585 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
1001 | 586 |
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); | 587 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); |
1010 | 588 |
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) { | 589 function dispatchCanExecuteEvent(command, target) { |
1017 var e = new CanExecuteEvent(command); | 590 var e = new CanExecuteEvent(command); |
1018 target.dispatchEvent(e); | 591 target.dispatchEvent(e); |
1019 command.disabled = !e.canExecute; | 592 command.disabled = !e.canExecute; |
1020 } | 593 } |
1021 | 594 |
1022 /** | |
1023 * The command managers for different documents. | |
1024 */ | |
1025 var commandManagers = {}; | 595 var commandManagers = {}; |
1026 | 596 |
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) { | 597 function CommandManager(doc) { |
1034 doc.addEventListener('focus', this.handleFocus_.bind(this), true); | 598 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); | 599 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); |
1038 } | 600 } |
1039 | 601 |
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) { | 602 CommandManager.init = function(doc) { |
1045 var uid = cr.getUid(doc); | 603 var uid = cr.getUid(doc); |
1046 if (!(uid in commandManagers)) { | 604 if (!(uid in commandManagers)) { |
1047 commandManagers[uid] = new CommandManager(doc); | 605 commandManagers[uid] = new CommandManager(doc); |
1048 } | 606 } |
1049 }; | 607 }; |
1050 | 608 |
1051 CommandManager.prototype = { | 609 CommandManager.prototype = { |
1052 | 610 |
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) { | 611 handleFocus_: function(e) { |
1061 var target = e.target; | 612 var target = e.target; |
1062 | 613 |
1063 // Ignore focus on a menu button or command item. | |
1064 if (target.menu || target.command) | 614 if (target.menu || target.command) |
1065 return; | 615 return; |
1066 | 616 |
1067 var commands = Array.prototype.slice.call( | 617 var commands = Array.prototype.slice.call( |
1068 target.ownerDocument.querySelectorAll('command')); | 618 target.ownerDocument.querySelectorAll('command')); |
1069 | 619 |
1070 commands.forEach(function(command) { | 620 commands.forEach(function(command) { |
1071 dispatchCanExecuteEvent(command, target); | 621 dispatchCanExecuteEvent(command, target); |
1072 }); | 622 }); |
1073 }, | 623 }, |
1074 | 624 |
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) { | 625 handleKeyDown_: function(e) { |
1080 var target = e.target; | 626 var target = e.target; |
1081 var commands = Array.prototype.slice.call( | 627 var commands = Array.prototype.slice.call( |
1082 target.ownerDocument.querySelectorAll('command')); | 628 target.ownerDocument.querySelectorAll('command')); |
1083 | 629 |
1084 for (var i = 0, command; command = commands[i]; i++) { | 630 for (var i = 0, command; command = commands[i]; i++) { |
1085 if (command.matchesEvent(e)) { | 631 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(); | 632 command.canExecuteChange(); |
1090 | 633 |
1091 if (!command.disabled) { | 634 if (!command.disabled) { |
1092 e.preventDefault(); | 635 e.preventDefault(); |
1093 // We do not want any other element to handle this. | |
1094 e.stopPropagation(); | 636 e.stopPropagation(); |
1095 command.execute(); | 637 command.execute(); |
1096 return; | 638 return; |
1097 } | 639 } |
1098 } | 640 } |
1099 } | 641 } |
1100 } | 642 } |
1101 }; | 643 }; |
1102 | 644 |
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) { | 645 function CanExecuteEvent(command) { |
1111 var e = new Event('canExecute', {bubbles: true, cancelable: true}); | 646 var e = new Event('canExecute', {bubbles: true, cancelable: true}); |
1112 e.__proto__ = CanExecuteEvent.prototype; | 647 e.__proto__ = CanExecuteEvent.prototype; |
1113 e.command = command; | 648 e.command = command; |
1114 return e; | 649 return e; |
1115 } | 650 } |
1116 | 651 |
1117 CanExecuteEvent.prototype = { | 652 CanExecuteEvent.prototype = { |
1118 __proto__: Event.prototype, | 653 __proto__: Event.prototype, |
1119 | 654 |
1120 /** | |
1121 * The current command | |
1122 * @type {cr.ui.Command} | |
1123 */ | |
1124 command: null, | 655 command: null, |
1125 | 656 |
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, | 657 canExecute_: false, |
1133 get canExecute() { | 658 get canExecute() { |
1134 return this.canExecute_; | 659 return this.canExecute_; |
1135 }, | 660 }, |
1136 set canExecute(canExecute) { | 661 set canExecute(canExecute) { |
1137 this.canExecute_ = !!canExecute; | 662 this.canExecute_ = !!canExecute; |
1138 this.stopPropagation(); | 663 this.stopPropagation(); |
1139 this.preventDefault(); | 664 this.preventDefault(); |
1140 } | 665 } |
1141 }; | 666 }; |
1142 | 667 |
1143 // Export | |
1144 return { | 668 return { |
1145 Command: Command, | 669 Command: Command, |
1146 CanExecuteEvent: CanExecuteEvent | 670 CanExecuteEvent: CanExecuteEvent |
1147 }; | 671 }; |
1148 }); | 672 }); |
1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 673 // 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 | 674 // Use of this source code is governed by a BSD-style license that can be |
1151 // found in the LICENSE file. | 675 // found in the LICENSE file. |
1152 | 676 |
1153 // <include src-disabled="assert.js"> | |
1154 | 677 |
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) { | 678 function $(id) { |
1161 var el = document.getElementById(id); | 679 var el = document.getElementById(id); |
1162 return el ? assertInstanceof(el, HTMLElement) : null; | 680 return el ? assertInstanceof(el, HTMLElement) : null; |
1163 } | 681 } |
1164 | 682 |
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) { | 683 function getSVGElement(id) { |
1173 var el = document.getElementById(id); | 684 var el = document.getElementById(id); |
1174 return el ? assertInstanceof(el, Element) : null; | 685 return el ? assertInstanceof(el, Element) : null; |
1175 } | 686 } |
1176 | 687 |
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) { | 688 function announceAccessibleMessage(msg) { |
1184 var element = document.createElement('div'); | 689 var element = document.createElement('div'); |
1185 element.setAttribute('aria-live', 'polite'); | 690 element.setAttribute('aria-live', 'polite'); |
1186 element.style.position = 'relative'; | 691 element.style.position = 'relative'; |
1187 element.style.left = '-9999px'; | 692 element.style.left = '-9999px'; |
1188 element.style.height = '0px'; | 693 element.style.height = '0px'; |
1189 element.innerText = msg; | 694 element.innerText = msg; |
1190 document.body.appendChild(element); | 695 document.body.appendChild(element); |
1191 window.setTimeout(function() { | 696 window.setTimeout(function() { |
1192 document.body.removeChild(element); | 697 document.body.removeChild(element); |
1193 }, 0); | 698 }, 0); |
1194 } | 699 } |
1195 | 700 |
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) { | 701 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'); | 702 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)) { | 703 if (/\\\\$/.test(s2)) { |
1209 // Add a space to work around the WebKit bug. | |
1210 s2 += ' '; | 704 s2 += ' '; |
1211 } | 705 } |
1212 return 'url("' + s2 + '")'; | 706 return 'url("' + s2 + '")'; |
1213 } | 707 } |
1214 | 708 |
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) { | 709 function parseQueryParams(location) { |
1221 var params = {}; | 710 var params = {}; |
1222 var query = unescape(location.search.substring(1)); | 711 var query = unescape(location.search.substring(1)); |
1223 var vars = query.split('&'); | 712 var vars = query.split('&'); |
1224 for (var i = 0; i < vars.length; i++) { | 713 for (var i = 0; i < vars.length; i++) { |
1225 var pair = vars[i].split('='); | 714 var pair = vars[i].split('='); |
1226 params[pair[0]] = pair[1]; | 715 params[pair[0]] = pair[1]; |
1227 } | 716 } |
1228 return params; | 717 return params; |
1229 } | 718 } |
1230 | 719 |
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) { | 720 function setQueryParam(location, key, value) { |
1240 var query = parseQueryParams(location); | 721 var query = parseQueryParams(location); |
1241 query[encodeURIComponent(key)] = encodeURIComponent(value); | 722 query[encodeURIComponent(key)] = encodeURIComponent(value); |
1242 | 723 |
1243 var newQuery = ''; | 724 var newQuery = ''; |
1244 for (var q in query) { | 725 for (var q in query) { |
1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; | 726 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; |
1246 } | 727 } |
1247 | 728 |
1248 return location.origin + location.pathname + newQuery + location.hash; | 729 return location.origin + location.pathname + newQuery + location.hash; |
1249 } | 730 } |
1250 | 731 |
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) { | 732 function findAncestorByClass(el, className) { |
1257 return /** @type {Element} */(findAncestor(el, function(el) { | 733 return /** @type {Element} */(findAncestor(el, function(el) { |
1258 return el.classList && el.classList.contains(className); | 734 return el.classList && el.classList.contains(className); |
1259 })); | 735 })); |
1260 } | 736 } |
1261 | 737 |
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) { | 738 function findAncestor(node, predicate) { |
1270 var last = false; | 739 var last = false; |
1271 while (node != null && !(last = predicate(node))) { | 740 while (node != null && !(last = predicate(node))) { |
1272 node = node.parentNode; | 741 node = node.parentNode; |
1273 } | 742 } |
1274 return last ? node : null; | 743 return last ? node : null; |
1275 } | 744 } |
1276 | 745 |
1277 function swapDomNodes(a, b) { | 746 function swapDomNodes(a, b) { |
1278 var afterA = a.nextSibling; | 747 var afterA = a.nextSibling; |
1279 if (afterA == b) { | 748 if (afterA == b) { |
1280 swapDomNodes(b, a); | 749 swapDomNodes(b, a); |
1281 return; | 750 return; |
1282 } | 751 } |
1283 var aParent = a.parentNode; | 752 var aParent = a.parentNode; |
1284 b.parentNode.replaceChild(a, b); | 753 b.parentNode.replaceChild(a, b); |
1285 aParent.insertBefore(b, afterA); | 754 aParent.insertBefore(b, afterA); |
1286 } | 755 } |
1287 | 756 |
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) { | 757 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { |
1297 // Disable text selection. | |
1298 document.onselectstart = function(e) { | 758 document.onselectstart = function(e) { |
1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) | 759 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) |
1300 e.preventDefault(); | 760 e.preventDefault(); |
1301 }; | 761 }; |
1302 | 762 |
1303 // Disable dragging. | |
1304 document.ondragstart = function(e) { | 763 document.ondragstart = function(e) { |
1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) | 764 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) |
1306 e.preventDefault(); | 765 e.preventDefault(); |
1307 }; | 766 }; |
1308 } | 767 } |
1309 | 768 |
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() { | 769 function preventDefaultOnPoundLinkClicks() { |
1316 document.addEventListener('click', function(e) { | 770 document.addEventListener('click', function(e) { |
1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { | 771 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { |
1318 return el.tagName == 'A'; | 772 return el.tagName == 'A'; |
1319 }); | 773 }); |
1320 // Use getAttribute() to prevent URL normalization. | |
1321 if (anchor && anchor.getAttribute('href') == '#') | 774 if (anchor && anchor.getAttribute('href') == '#') |
1322 e.preventDefault(); | 775 e.preventDefault(); |
1323 }); | 776 }); |
1324 } | 777 } |
1325 | 778 |
1326 /** | |
1327 * Check the directionality of the page. | |
1328 * @return {boolean} True if Chrome is running an RTL UI. | |
1329 */ | |
1330 function isRTL() { | 779 function isRTL() { |
1331 return document.documentElement.dir == 'rtl'; | 780 return document.documentElement.dir == 'rtl'; |
1332 } | 781 } |
1333 | 782 |
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) { | 783 function getRequiredElement(id) { |
1342 return assertInstanceof($(id), HTMLElement, | 784 return assertInstanceof($(id), HTMLElement, |
1343 'Missing required element: ' + id); | 785 'Missing required element: ' + id); |
1344 } | 786 } |
1345 | 787 |
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) { | 788 function queryRequiredElement(selectors, opt_context) { |
1356 var element = (opt_context || document).querySelector(selectors); | 789 var element = (opt_context || document).querySelector(selectors); |
1357 return assertInstanceof(element, HTMLElement, | 790 return assertInstanceof(element, HTMLElement, |
1358 'Missing required element: ' + selectors); | 791 'Missing required element: ' + selectors); |
1359 } | 792 } |
1360 | 793 |
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 ['click', 'auxclick'].forEach(function(eventName) { | 794 ['click', 'auxclick'].forEach(function(eventName) { |
1364 document.addEventListener(eventName, function(e) { | 795 document.addEventListener(eventName, function(e) { |
1365 if (e.defaultPrevented) | 796 if (e.defaultPrevented) |
1366 return; | 797 return; |
1367 | 798 |
1368 var eventPath = e.path; | 799 var eventPath = e.path; |
1369 var anchor = null; | 800 var anchor = null; |
1370 if (eventPath) { | 801 if (eventPath) { |
1371 for (var i = 0; i < eventPath.length; i++) { | 802 for (var i = 0; i < eventPath.length; i++) { |
1372 var element = eventPath[i]; | 803 var element = eventPath[i]; |
1373 if (element.tagName === 'A' && element.href) { | 804 if (element.tagName === 'A' && element.href) { |
1374 anchor = element; | 805 anchor = element; |
1375 break; | 806 break; |
1376 } | 807 } |
1377 } | 808 } |
1378 } | 809 } |
1379 | 810 |
1380 // Fallback if Event.path is not available. | |
1381 var el = e.target; | 811 var el = e.target; |
1382 if (!anchor && el.nodeType == Node.ELEMENT_NODE && | 812 if (!anchor && el.nodeType == Node.ELEMENT_NODE && |
1383 el.webkitMatchesSelector('A, A *')) { | 813 el.webkitMatchesSelector('A, A *')) { |
1384 while (el.tagName != 'A') { | 814 while (el.tagName != 'A') { |
1385 el = el.parentElement; | 815 el = el.parentElement; |
1386 } | 816 } |
1387 anchor = el; | 817 anchor = el; |
1388 } | 818 } |
1389 | 819 |
1390 if (!anchor) | 820 if (!anchor) |
1391 return; | 821 return; |
1392 | 822 |
1393 anchor = /** @type {!HTMLAnchorElement} */(anchor); | 823 anchor = /** @type {!HTMLAnchorElement} */(anchor); |
1394 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && | 824 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && |
1395 (e.button == 0 || e.button == 1)) { | 825 (e.button == 0 || e.button == 1)) { |
1396 chrome.send('navigateToUrl', [ | 826 chrome.send('navigateToUrl', [ |
1397 anchor.href, | 827 anchor.href, |
1398 anchor.target, | 828 anchor.target, |
1399 e.button, | 829 e.button, |
1400 e.altKey, | 830 e.altKey, |
1401 e.ctrlKey, | 831 e.ctrlKey, |
1402 e.metaKey, | 832 e.metaKey, |
1403 e.shiftKey | 833 e.shiftKey |
1404 ]); | 834 ]); |
1405 e.preventDefault(); | 835 e.preventDefault(); |
1406 } | 836 } |
1407 }); | 837 }); |
1408 }); | 838 }); |
1409 | 839 |
1410 /** | |
1411 * Creates a new URL which is the old URL with a GET param of key=value. | |
1412 * @param {string} url The base URL. There is not sanity checking on the URL so | |
1413 * it must be passed in a proper format. | |
1414 * @param {string} key The key of the param. | |
1415 * @param {string} value The value of the param. | |
1416 * @return {string} The new URL. | |
1417 */ | |
1418 function appendParam(url, key, value) { | 840 function appendParam(url, key, value) { |
1419 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 841 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); |
1420 | 842 |
1421 if (url.indexOf('?') == -1) | 843 if (url.indexOf('?') == -1) |
1422 return url + '?' + param; | 844 return url + '?' + param; |
1423 return url + '&' + param; | 845 return url + '&' + param; |
1424 } | 846 } |
1425 | 847 |
1426 /** | |
1427 * Creates an element of a specified type with a specified class name. | |
1428 * @param {string} type The node type. | |
1429 * @param {string} className The class name to use. | |
1430 * @return {Element} The created element. | |
1431 */ | |
1432 function createElementWithClassName(type, className) { | 848 function createElementWithClassName(type, className) { |
1433 var elm = document.createElement(type); | 849 var elm = document.createElement(type); |
1434 elm.className = className; | 850 elm.className = className; |
1435 return elm; | 851 return elm; |
1436 } | 852 } |
1437 | 853 |
1438 /** | |
1439 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | |
1440 * or when no paint happens during the animation). This function sets up | |
1441 * a timer and emulate the event if it is not fired when the timer expires. | |
1442 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | |
1443 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | |
1444 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | |
1445 * using the transitionDuration style value. | |
1446 */ | |
1447 function ensureTransitionEndEvent(el, opt_timeOut) { | 854 function ensureTransitionEndEvent(el, opt_timeOut) { |
1448 if (opt_timeOut === undefined) { | 855 if (opt_timeOut === undefined) { |
1449 var style = getComputedStyle(el); | 856 var style = getComputedStyle(el); |
1450 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 857 opt_timeOut = parseFloat(style.transitionDuration) * 1000; |
1451 | 858 |
1452 // Give an additional 50ms buffer for the animation to complete. | |
1453 opt_timeOut += 50; | 859 opt_timeOut += 50; |
1454 } | 860 } |
1455 | 861 |
1456 var fired = false; | 862 var fired = false; |
1457 el.addEventListener('webkitTransitionEnd', function f(e) { | 863 el.addEventListener('webkitTransitionEnd', function f(e) { |
1458 el.removeEventListener('webkitTransitionEnd', f); | 864 el.removeEventListener('webkitTransitionEnd', f); |
1459 fired = true; | 865 fired = true; |
1460 }); | 866 }); |
1461 window.setTimeout(function() { | 867 window.setTimeout(function() { |
1462 if (!fired) | 868 if (!fired) |
1463 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | 869 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); |
1464 }, opt_timeOut); | 870 }, opt_timeOut); |
1465 } | 871 } |
1466 | 872 |
1467 /** | |
1468 * Alias for document.scrollTop getter. | |
1469 * @param {!HTMLDocument} doc The document node where information will be | |
1470 * queried from. | |
1471 * @return {number} The Y document scroll offset. | |
1472 */ | |
1473 function scrollTopForDocument(doc) { | 873 function scrollTopForDocument(doc) { |
1474 return doc.documentElement.scrollTop || doc.body.scrollTop; | 874 return doc.documentElement.scrollTop || doc.body.scrollTop; |
1475 } | 875 } |
1476 | 876 |
1477 /** | |
1478 * Alias for document.scrollTop setter. | |
1479 * @param {!HTMLDocument} doc The document node where information will be | |
1480 * queried from. | |
1481 * @param {number} value The target Y scroll offset. | |
1482 */ | |
1483 function setScrollTopForDocument(doc, value) { | 877 function setScrollTopForDocument(doc, value) { |
1484 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 878 doc.documentElement.scrollTop = doc.body.scrollTop = value; |
1485 } | 879 } |
1486 | 880 |
1487 /** | |
1488 * Alias for document.scrollLeft getter. | |
1489 * @param {!HTMLDocument} doc The document node where information will be | |
1490 * queried from. | |
1491 * @return {number} The X document scroll offset. | |
1492 */ | |
1493 function scrollLeftForDocument(doc) { | 881 function scrollLeftForDocument(doc) { |
1494 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 882 return doc.documentElement.scrollLeft || doc.body.scrollLeft; |
1495 } | 883 } |
1496 | 884 |
1497 /** | |
1498 * Alias for document.scrollLeft setter. | |
1499 * @param {!HTMLDocument} doc The document node where information will be | |
1500 * queried from. | |
1501 * @param {number} value The target X scroll offset. | |
1502 */ | |
1503 function setScrollLeftForDocument(doc, value) { | 885 function setScrollLeftForDocument(doc, value) { |
1504 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 886 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; |
1505 } | 887 } |
1506 | 888 |
1507 /** | |
1508 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | |
1509 * @param {string} original The original string. | |
1510 * @return {string} The string with all the characters mentioned above replaced. | |
1511 */ | |
1512 function HTMLEscape(original) { | 889 function HTMLEscape(original) { |
1513 return original.replace(/&/g, '&') | 890 return original.replace(/&/g, '&') |
1514 .replace(/</g, '<') | 891 .replace(/</g, '<') |
1515 .replace(/>/g, '>') | 892 .replace(/>/g, '>') |
1516 .replace(/"/g, '"') | 893 .replace(/"/g, '"') |
1517 .replace(/'/g, '''); | 894 .replace(/'/g, '''); |
1518 } | 895 } |
1519 | 896 |
1520 /** | |
1521 * Shortens the provided string (if necessary) to a string of length at most | |
1522 * |maxLength|. | |
1523 * @param {string} original The original string. | |
1524 * @param {number} maxLength The maximum length allowed for the string. | |
1525 * @return {string} The original string if its length does not exceed | |
1526 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | |
1527 * appended. | |
1528 */ | |
1529 function elide(original, maxLength) { | 897 function elide(original, maxLength) { |
1530 if (original.length <= maxLength) | 898 if (original.length <= maxLength) |
1531 return original; | 899 return original; |
1532 return original.substring(0, maxLength - 1) + '\u2026'; | 900 return original.substring(0, maxLength - 1) + '\u2026'; |
1533 } | 901 } |
1534 | 902 |
1535 /** | |
1536 * Quote a string so it can be used in a regular expression. | |
1537 * @param {string} str The source string. | |
1538 * @return {string} The escaped string. | |
1539 */ | |
1540 function quoteString(str) { | 903 function quoteString(str) { |
1541 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 904 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); |
1542 } | 905 } |
1543 | 906 |
1544 // <if expr="is_ios"> | 907 // <if expr="is_ios"> |
1545 // Polyfill 'key' in KeyboardEvent for iOS. | |
1546 // This function is not intended to be complete but should | |
1547 // be sufficient enough to have iOS work correctly while | |
1548 // it does not support key yet. | |
1549 if (!('key' in KeyboardEvent.prototype)) { | 908 if (!('key' in KeyboardEvent.prototype)) { |
1550 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 909 Object.defineProperty(KeyboardEvent.prototype, 'key', { |
1551 /** @this {KeyboardEvent} */ | 910 /** @this {KeyboardEvent} */ |
1552 get: function () { | 911 get: function () { |
1553 // 0-9 | |
1554 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | 912 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) |
1555 return String.fromCharCode(this.keyCode); | 913 return String.fromCharCode(this.keyCode); |
1556 | 914 |
1557 // A-Z | |
1558 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | 915 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { |
1559 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 916 var result = String.fromCharCode(this.keyCode).toLowerCase(); |
1560 if (this.shiftKey) | 917 if (this.shiftKey) |
1561 result = result.toUpperCase(); | 918 result = result.toUpperCase(); |
1562 return result; | 919 return result; |
1563 } | 920 } |
1564 | 921 |
1565 // Special characters | |
1566 switch(this.keyCode) { | 922 switch(this.keyCode) { |
1567 case 0x08: return 'Backspace'; | 923 case 0x08: return 'Backspace'; |
1568 case 0x09: return 'Tab'; | 924 case 0x09: return 'Tab'; |
1569 case 0x0d: return 'Enter'; | 925 case 0x0d: return 'Enter'; |
1570 case 0x10: return 'Shift'; | 926 case 0x10: return 'Shift'; |
1571 case 0x11: return 'Control'; | 927 case 0x11: return 'Control'; |
1572 case 0x12: return 'Alt'; | 928 case 0x12: return 'Alt'; |
1573 case 0x1b: return 'Escape'; | 929 case 0x1b: return 'Escape'; |
1574 case 0x20: return ' '; | 930 case 0x20: return ' '; |
1575 case 0x21: return 'PageUp'; | 931 case 0x21: return 'PageUp'; |
(...skipping 24 matching lines...) Expand all Loading... |
1600 case 0xdb: return '['; | 956 case 0xdb: return '['; |
1601 case 0xdd: return ']'; | 957 case 0xdd: return ']'; |
1602 } | 958 } |
1603 return 'Unidentified'; | 959 return 'Unidentified'; |
1604 } | 960 } |
1605 }); | 961 }); |
1606 } else { | 962 } else { |
1607 window.console.log("KeyboardEvent.Key polyfill not required"); | 963 window.console.log("KeyboardEvent.Key polyfill not required"); |
1608 } | 964 } |
1609 // </if> /* is_ios */ | 965 // </if> /* is_ios */ |
1610 /** | |
1611 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | |
1612 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | |
1613 * size or hidden state of their children) and "resizables" (elements that nee
d to be | |
1614 * notified when they are resized or un-hidden by their parents in order to ta
ke | |
1615 * action on their new measurements). | |
1616 * | |
1617 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | |
1618 * their element definition and listen for the `iron-resize` event on themselv
es. | |
1619 * This event will be fired when they become showing after having been hidden, | |
1620 * when they are resized explicitly by another resizable, or when the window h
as been | |
1621 * resized. | |
1622 * | |
1623 * Note, the `iron-resize` event is non-bubbling. | |
1624 * | |
1625 * @polymerBehavior Polymer.IronResizableBehavior | |
1626 * @demo demo/index.html | |
1627 **/ | |
1628 Polymer.IronResizableBehavior = { | 966 Polymer.IronResizableBehavior = { |
1629 properties: { | 967 properties: { |
1630 /** | |
1631 * The closest ancestor element that implements `IronResizableBehavior`. | |
1632 */ | |
1633 _parentResizable: { | 968 _parentResizable: { |
1634 type: Object, | 969 type: Object, |
1635 observer: '_parentResizableChanged' | 970 observer: '_parentResizableChanged' |
1636 }, | 971 }, |
1637 | 972 |
1638 /** | |
1639 * True if this element is currently notifying its descedant elements of | |
1640 * resize. | |
1641 */ | |
1642 _notifyingDescendant: { | 973 _notifyingDescendant: { |
1643 type: Boolean, | 974 type: Boolean, |
1644 value: false | 975 value: false |
1645 } | 976 } |
1646 }, | 977 }, |
1647 | 978 |
1648 listeners: { | 979 listeners: { |
1649 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | 980 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
1650 }, | 981 }, |
1651 | 982 |
1652 created: function() { | 983 created: function() { |
1653 // We don't really need property effects on these, and also we want them | |
1654 // to be created before the `_parentResizable` observer fires: | |
1655 this._interestedResizables = []; | 984 this._interestedResizables = []; |
1656 this._boundNotifyResize = this.notifyResize.bind(this); | 985 this._boundNotifyResize = this.notifyResize.bind(this); |
1657 }, | 986 }, |
1658 | 987 |
1659 attached: function() { | 988 attached: function() { |
1660 this.fire('iron-request-resize-notifications', null, { | 989 this.fire('iron-request-resize-notifications', null, { |
1661 node: this, | 990 node: this, |
1662 bubbles: true, | 991 bubbles: true, |
1663 cancelable: true | 992 cancelable: true |
1664 }); | 993 }); |
1665 | 994 |
1666 if (!this._parentResizable) { | 995 if (!this._parentResizable) { |
1667 window.addEventListener('resize', this._boundNotifyResize); | 996 window.addEventListener('resize', this._boundNotifyResize); |
1668 this.notifyResize(); | 997 this.notifyResize(); |
1669 } | 998 } |
1670 }, | 999 }, |
1671 | 1000 |
1672 detached: function() { | 1001 detached: function() { |
1673 if (this._parentResizable) { | 1002 if (this._parentResizable) { |
1674 this._parentResizable.stopResizeNotificationsFor(this); | 1003 this._parentResizable.stopResizeNotificationsFor(this); |
1675 } else { | 1004 } else { |
1676 window.removeEventListener('resize', this._boundNotifyResize); | 1005 window.removeEventListener('resize', this._boundNotifyResize); |
1677 } | 1006 } |
1678 | 1007 |
1679 this._parentResizable = null; | 1008 this._parentResizable = null; |
1680 }, | 1009 }, |
1681 | 1010 |
1682 /** | |
1683 * Can be called to manually notify a resizable and its descendant | |
1684 * resizables of a resize change. | |
1685 */ | |
1686 notifyResize: function() { | 1011 notifyResize: function() { |
1687 if (!this.isAttached) { | 1012 if (!this.isAttached) { |
1688 return; | 1013 return; |
1689 } | 1014 } |
1690 | 1015 |
1691 this._interestedResizables.forEach(function(resizable) { | 1016 this._interestedResizables.forEach(function(resizable) { |
1692 if (this.resizerShouldNotify(resizable)) { | 1017 if (this.resizerShouldNotify(resizable)) { |
1693 this._notifyDescendant(resizable); | 1018 this._notifyDescendant(resizable); |
1694 } | 1019 } |
1695 }, this); | 1020 }, this); |
1696 | 1021 |
1697 this._fireResize(); | 1022 this._fireResize(); |
1698 }, | 1023 }, |
1699 | 1024 |
1700 /** | |
1701 * Used to assign the closest resizable ancestor to this resizable | |
1702 * if the ancestor detects a request for notifications. | |
1703 */ | |
1704 assignParentResizable: function(parentResizable) { | 1025 assignParentResizable: function(parentResizable) { |
1705 this._parentResizable = parentResizable; | 1026 this._parentResizable = parentResizable; |
1706 }, | 1027 }, |
1707 | 1028 |
1708 /** | |
1709 * Used to remove a resizable descendant from the list of descendants | |
1710 * that should be notified of a resize change. | |
1711 */ | |
1712 stopResizeNotificationsFor: function(target) { | 1029 stopResizeNotificationsFor: function(target) { |
1713 var index = this._interestedResizables.indexOf(target); | 1030 var index = this._interestedResizables.indexOf(target); |
1714 | 1031 |
1715 if (index > -1) { | 1032 if (index > -1) { |
1716 this._interestedResizables.splice(index, 1); | 1033 this._interestedResizables.splice(index, 1); |
1717 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | 1034 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
1718 } | 1035 } |
1719 }, | 1036 }, |
1720 | 1037 |
1721 /** | |
1722 * This method can be overridden to filter nested elements that should or | |
1723 * should not be notified by the current element. Return true if an element | |
1724 * should be notified, or false if it should not be notified. | |
1725 * | |
1726 * @param {HTMLElement} element A candidate descendant element that | |
1727 * implements `IronResizableBehavior`. | |
1728 * @return {boolean} True if the `element` should be notified of resize. | |
1729 */ | |
1730 resizerShouldNotify: function(element) { return true; }, | 1038 resizerShouldNotify: function(element) { return true; }, |
1731 | 1039 |
1732 _onDescendantIronResize: function(event) { | 1040 _onDescendantIronResize: function(event) { |
1733 if (this._notifyingDescendant) { | 1041 if (this._notifyingDescendant) { |
1734 event.stopPropagation(); | 1042 event.stopPropagation(); |
1735 return; | 1043 return; |
1736 } | 1044 } |
1737 | 1045 |
1738 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
1739 // otherwise non-bubbling event "just work." We do it manually here for | |
1740 // the case where Polymer is not using shadow roots for whatever reason: | |
1741 if (!Polymer.Settings.useShadow) { | 1046 if (!Polymer.Settings.useShadow) { |
1742 this._fireResize(); | 1047 this._fireResize(); |
1743 } | 1048 } |
1744 }, | 1049 }, |
1745 | 1050 |
1746 _fireResize: function() { | 1051 _fireResize: function() { |
1747 this.fire('iron-resize', null, { | 1052 this.fire('iron-resize', null, { |
1748 node: this, | 1053 node: this, |
1749 bubbles: false | 1054 bubbles: false |
1750 }); | 1055 }); |
(...skipping 17 matching lines...) Expand all Loading... |
1768 event.stopPropagation(); | 1073 event.stopPropagation(); |
1769 }, | 1074 }, |
1770 | 1075 |
1771 _parentResizableChanged: function(parentResizable) { | 1076 _parentResizableChanged: function(parentResizable) { |
1772 if (parentResizable) { | 1077 if (parentResizable) { |
1773 window.removeEventListener('resize', this._boundNotifyResize); | 1078 window.removeEventListener('resize', this._boundNotifyResize); |
1774 } | 1079 } |
1775 }, | 1080 }, |
1776 | 1081 |
1777 _notifyDescendant: function(descendant) { | 1082 _notifyDescendant: function(descendant) { |
1778 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
1779 // important not to notify them if the parent is not attached yet (or | |
1780 // else they will get redundantly notified when the parent attaches). | |
1781 if (!this.isAttached) { | 1083 if (!this.isAttached) { |
1782 return; | 1084 return; |
1783 } | 1085 } |
1784 | 1086 |
1785 this._notifyingDescendant = true; | 1087 this._notifyingDescendant = true; |
1786 descendant.notifyResize(); | 1088 descendant.notifyResize(); |
1787 this._notifyingDescendant = false; | 1089 this._notifyingDescendant = false; |
1788 } | 1090 } |
1789 }; | 1091 }; |
1790 (function() { | 1092 (function() { |
1791 'use strict'; | 1093 'use strict'; |
1792 | 1094 |
1793 /** | |
1794 * Chrome uses an older version of DOM Level 3 Keyboard Events | |
1795 * | |
1796 * Most keys are labeled as text, but some are Unicode codepoints. | |
1797 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | |
1798 */ | |
1799 var KEY_IDENTIFIER = { | 1095 var KEY_IDENTIFIER = { |
1800 'U+0008': 'backspace', | 1096 'U+0008': 'backspace', |
1801 'U+0009': 'tab', | 1097 'U+0009': 'tab', |
1802 'U+001B': 'esc', | 1098 'U+001B': 'esc', |
1803 'U+0020': 'space', | 1099 'U+0020': 'space', |
1804 'U+007F': 'del' | 1100 'U+007F': 'del' |
1805 }; | 1101 }; |
1806 | 1102 |
1807 /** | |
1808 * Special table for KeyboardEvent.keyCode. | |
1809 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | |
1810 * than that. | |
1811 * | |
1812 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
1813 */ | |
1814 var KEY_CODE = { | 1103 var KEY_CODE = { |
1815 8: 'backspace', | 1104 8: 'backspace', |
1816 9: 'tab', | 1105 9: 'tab', |
1817 13: 'enter', | 1106 13: 'enter', |
1818 27: 'esc', | 1107 27: 'esc', |
1819 33: 'pageup', | 1108 33: 'pageup', |
1820 34: 'pagedown', | 1109 34: 'pagedown', |
1821 35: 'end', | 1110 35: 'end', |
1822 36: 'home', | 1111 36: 'home', |
1823 32: 'space', | 1112 32: 'space', |
1824 37: 'left', | 1113 37: 'left', |
1825 38: 'up', | 1114 38: 'up', |
1826 39: 'right', | 1115 39: 'right', |
1827 40: 'down', | 1116 40: 'down', |
1828 46: 'del', | 1117 46: 'del', |
1829 106: '*' | 1118 106: '*' |
1830 }; | 1119 }; |
1831 | 1120 |
1832 /** | |
1833 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
1834 * combo string to the property name that references those same keys | |
1835 * in a KeyboardEvent instance. | |
1836 */ | |
1837 var MODIFIER_KEYS = { | 1121 var MODIFIER_KEYS = { |
1838 'shift': 'shiftKey', | 1122 'shift': 'shiftKey', |
1839 'ctrl': 'ctrlKey', | 1123 'ctrl': 'ctrlKey', |
1840 'alt': 'altKey', | 1124 'alt': 'altKey', |
1841 'meta': 'metaKey' | 1125 'meta': 'metaKey' |
1842 }; | 1126 }; |
1843 | 1127 |
1844 /** | |
1845 * KeyboardEvent.key is mostly represented by printable character made by | |
1846 * the keyboard, with unprintable keys labeled nicely. | |
1847 * | |
1848 * However, on OS X, Alt+char can make a Unicode character that follows an | |
1849 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
1850 */ | |
1851 var KEY_CHAR = /[a-z0-9*]/; | 1128 var KEY_CHAR = /[a-z0-9*]/; |
1852 | 1129 |
1853 /** | |
1854 * Matches a keyIdentifier string. | |
1855 */ | |
1856 var IDENT_CHAR = /U\+/; | 1130 var IDENT_CHAR = /U\+/; |
1857 | 1131 |
1858 /** | |
1859 * Matches arrow keys in Gecko 27.0+ | |
1860 */ | |
1861 var ARROW_KEY = /^arrow/; | 1132 var ARROW_KEY = /^arrow/; |
1862 | 1133 |
1863 /** | |
1864 * Matches space keys everywhere (notably including IE10's exceptional name | |
1865 * `spacebar`). | |
1866 */ | |
1867 var SPACE_KEY = /^space(bar)?/; | 1134 var SPACE_KEY = /^space(bar)?/; |
1868 | 1135 |
1869 /** | |
1870 * Matches ESC key. | |
1871 * | |
1872 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
1873 */ | |
1874 var ESC_KEY = /^escape$/; | 1136 var ESC_KEY = /^escape$/; |
1875 | 1137 |
1876 /** | |
1877 * Transforms the key. | |
1878 * @param {string} key The KeyBoardEvent.key | |
1879 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
1880 * alpha-numeric characters. | |
1881 */ | |
1882 function transformKey(key, noSpecialChars) { | 1138 function transformKey(key, noSpecialChars) { |
1883 var validKey = ''; | 1139 var validKey = ''; |
1884 if (key) { | 1140 if (key) { |
1885 var lKey = key.toLowerCase(); | 1141 var lKey = key.toLowerCase(); |
1886 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | 1142 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
1887 validKey = 'space'; | 1143 validKey = 'space'; |
1888 } else if (ESC_KEY.test(lKey)) { | 1144 } else if (ESC_KEY.test(lKey)) { |
1889 validKey = 'esc'; | 1145 validKey = 'esc'; |
1890 } else if (lKey.length == 1) { | 1146 } else if (lKey.length == 1) { |
1891 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | 1147 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
1892 validKey = lKey; | 1148 validKey = lKey; |
1893 } | 1149 } |
1894 } else if (ARROW_KEY.test(lKey)) { | 1150 } else if (ARROW_KEY.test(lKey)) { |
1895 validKey = lKey.replace('arrow', ''); | 1151 validKey = lKey.replace('arrow', ''); |
1896 } else if (lKey == 'multiply') { | 1152 } else if (lKey == 'multiply') { |
1897 // numpad '*' can map to Multiply on IE/Windows | |
1898 validKey = '*'; | 1153 validKey = '*'; |
1899 } else { | 1154 } else { |
1900 validKey = lKey; | 1155 validKey = lKey; |
1901 } | 1156 } |
1902 } | 1157 } |
1903 return validKey; | 1158 return validKey; |
1904 } | 1159 } |
1905 | 1160 |
1906 function transformKeyIdentifier(keyIdent) { | 1161 function transformKeyIdentifier(keyIdent) { |
1907 var validKey = ''; | 1162 var validKey = ''; |
1908 if (keyIdent) { | 1163 if (keyIdent) { |
1909 if (keyIdent in KEY_IDENTIFIER) { | 1164 if (keyIdent in KEY_IDENTIFIER) { |
1910 validKey = KEY_IDENTIFIER[keyIdent]; | 1165 validKey = KEY_IDENTIFIER[keyIdent]; |
1911 } else if (IDENT_CHAR.test(keyIdent)) { | 1166 } else if (IDENT_CHAR.test(keyIdent)) { |
1912 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | 1167 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
1913 validKey = String.fromCharCode(keyIdent).toLowerCase(); | 1168 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
1914 } else { | 1169 } else { |
1915 validKey = keyIdent.toLowerCase(); | 1170 validKey = keyIdent.toLowerCase(); |
1916 } | 1171 } |
1917 } | 1172 } |
1918 return validKey; | 1173 return validKey; |
1919 } | 1174 } |
1920 | 1175 |
1921 function transformKeyCode(keyCode) { | 1176 function transformKeyCode(keyCode) { |
1922 var validKey = ''; | 1177 var validKey = ''; |
1923 if (Number(keyCode)) { | 1178 if (Number(keyCode)) { |
1924 if (keyCode >= 65 && keyCode <= 90) { | 1179 if (keyCode >= 65 && keyCode <= 90) { |
1925 // ascii a-z | |
1926 // lowercase is 32 offset from uppercase | |
1927 validKey = String.fromCharCode(32 + keyCode); | 1180 validKey = String.fromCharCode(32 + keyCode); |
1928 } else if (keyCode >= 112 && keyCode <= 123) { | 1181 } else if (keyCode >= 112 && keyCode <= 123) { |
1929 // function keys f1-f12 | |
1930 validKey = 'f' + (keyCode - 112); | 1182 validKey = 'f' + (keyCode - 112); |
1931 } else if (keyCode >= 48 && keyCode <= 57) { | 1183 } else if (keyCode >= 48 && keyCode <= 57) { |
1932 // top 0-9 keys | |
1933 validKey = String(keyCode - 48); | 1184 validKey = String(keyCode - 48); |
1934 } else if (keyCode >= 96 && keyCode <= 105) { | 1185 } else if (keyCode >= 96 && keyCode <= 105) { |
1935 // num pad 0-9 | |
1936 validKey = String(keyCode - 96); | 1186 validKey = String(keyCode - 96); |
1937 } else { | 1187 } else { |
1938 validKey = KEY_CODE[keyCode]; | 1188 validKey = KEY_CODE[keyCode]; |
1939 } | 1189 } |
1940 } | 1190 } |
1941 return validKey; | 1191 return validKey; |
1942 } | 1192 } |
1943 | 1193 |
1944 /** | |
1945 * Calculates the normalized key for a KeyboardEvent. | |
1946 * @param {KeyboardEvent} keyEvent | |
1947 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
1948 * transformation to alpha-numeric chars. This is useful with key | |
1949 * combinations like shift + 2, which on FF for MacOS produces | |
1950 * keyEvent.key = @ | |
1951 * To get 2 returned, set noSpecialChars = true | |
1952 * To get @ returned, set noSpecialChars = false | |
1953 */ | |
1954 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | 1194 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
1955 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
1956 // .detail.key to support artificial keyboard events. | |
1957 return transformKey(keyEvent.key, noSpecialChars) || | 1195 return transformKey(keyEvent.key, noSpecialChars) || |
1958 transformKeyIdentifier(keyEvent.keyIdentifier) || | 1196 transformKeyIdentifier(keyEvent.keyIdentifier) || |
1959 transformKeyCode(keyEvent.keyCode) || | 1197 transformKeyCode(keyEvent.keyCode) || |
1960 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | 1198 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; |
1961 } | 1199 } |
1962 | 1200 |
1963 function keyComboMatchesEvent(keyCombo, event) { | 1201 function keyComboMatchesEvent(keyCombo, event) { |
1964 // For combos with modifiers we support only alpha-numeric keys | |
1965 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | 1202 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
1966 return keyEvent === keyCombo.key && | 1203 return keyEvent === keyCombo.key && |
1967 (!keyCombo.hasModifiers || ( | 1204 (!keyCombo.hasModifiers || ( |
1968 !!event.shiftKey === !!keyCombo.shiftKey && | 1205 !!event.shiftKey === !!keyCombo.shiftKey && |
1969 !!event.ctrlKey === !!keyCombo.ctrlKey && | 1206 !!event.ctrlKey === !!keyCombo.ctrlKey && |
1970 !!event.altKey === !!keyCombo.altKey && | 1207 !!event.altKey === !!keyCombo.altKey && |
1971 !!event.metaKey === !!keyCombo.metaKey) | 1208 !!event.metaKey === !!keyCombo.metaKey) |
1972 ); | 1209 ); |
1973 } | 1210 } |
1974 | 1211 |
(...skipping 23 matching lines...) Expand all Loading... |
1998 combo: keyComboString.split(':').shift() | 1235 combo: keyComboString.split(':').shift() |
1999 }); | 1236 }); |
2000 } | 1237 } |
2001 | 1238 |
2002 function parseEventString(eventString) { | 1239 function parseEventString(eventString) { |
2003 return eventString.trim().split(' ').map(function(keyComboString) { | 1240 return eventString.trim().split(' ').map(function(keyComboString) { |
2004 return parseKeyComboString(keyComboString); | 1241 return parseKeyComboString(keyComboString); |
2005 }); | 1242 }); |
2006 } | 1243 } |
2007 | 1244 |
2008 /** | |
2009 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
2010 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
2011 * The element takes care of browser differences with respect to Keyboard ev
ents | |
2012 * and uses an expressive syntax to filter key presses. | |
2013 * | |
2014 * Use the `keyBindings` prototype property to express what combination of k
eys | |
2015 * will trigger the callback. A key binding has the format | |
2016 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
2017 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
2018 * | |
2019 * keyBindings: { | |
2020 * 'space': '_onKeydown', // same as 'space:keydown' | |
2021 * 'shift+tab': '_onKeydown', | |
2022 * 'enter:keypress': '_onKeypress', | |
2023 * 'esc:keyup': '_onKeyup' | |
2024 * } | |
2025 * | |
2026 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
2027 * | |
2028 * _onKeydown: function(event) { | |
2029 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
2030 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
2031 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
2032 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
2033 * } | |
2034 * | |
2035 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
2036 * node. | |
2037 * | |
2038 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
2039 * for an example. | |
2040 * | |
2041 * @demo demo/index.html | |
2042 * @polymerBehavior | |
2043 */ | |
2044 Polymer.IronA11yKeysBehavior = { | 1245 Polymer.IronA11yKeysBehavior = { |
2045 properties: { | 1246 properties: { |
2046 /** | |
2047 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
2048 * `null` to disable the listeners. | |
2049 * @type {?EventTarget} | |
2050 */ | |
2051 keyEventTarget: { | 1247 keyEventTarget: { |
2052 type: Object, | 1248 type: Object, |
2053 value: function() { | 1249 value: function() { |
2054 return this; | 1250 return this; |
2055 } | 1251 } |
2056 }, | 1252 }, |
2057 | 1253 |
2058 /** | |
2059 * If true, this property will cause the implementing element to | |
2060 * automatically stop propagation on any handled KeyboardEvents. | |
2061 */ | |
2062 stopKeyboardEventPropagation: { | 1254 stopKeyboardEventPropagation: { |
2063 type: Boolean, | 1255 type: Boolean, |
2064 value: false | 1256 value: false |
2065 }, | 1257 }, |
2066 | 1258 |
2067 _boundKeyHandlers: { | 1259 _boundKeyHandlers: { |
2068 type: Array, | 1260 type: Array, |
2069 value: function() { | 1261 value: function() { |
2070 return []; | 1262 return []; |
2071 } | 1263 } |
2072 }, | 1264 }, |
2073 | 1265 |
2074 // We use this due to a limitation in IE10 where instances will have | |
2075 // own properties of everything on the "prototype". | |
2076 _imperativeKeyBindings: { | 1266 _imperativeKeyBindings: { |
2077 type: Object, | 1267 type: Object, |
2078 value: function() { | 1268 value: function() { |
2079 return {}; | 1269 return {}; |
2080 } | 1270 } |
2081 } | 1271 } |
2082 }, | 1272 }, |
2083 | 1273 |
2084 observers: [ | 1274 observers: [ |
2085 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | 1275 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' |
2086 ], | 1276 ], |
2087 | 1277 |
2088 | 1278 |
2089 /** | |
2090 * To be used to express what combination of keys will trigger the relati
ve | |
2091 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
2092 * @type {Object} | |
2093 */ | |
2094 keyBindings: {}, | 1279 keyBindings: {}, |
2095 | 1280 |
2096 registered: function() { | 1281 registered: function() { |
2097 this._prepKeyBindings(); | 1282 this._prepKeyBindings(); |
2098 }, | 1283 }, |
2099 | 1284 |
2100 attached: function() { | 1285 attached: function() { |
2101 this._listenKeyEventListeners(); | 1286 this._listenKeyEventListeners(); |
2102 }, | 1287 }, |
2103 | 1288 |
2104 detached: function() { | 1289 detached: function() { |
2105 this._unlistenKeyEventListeners(); | 1290 this._unlistenKeyEventListeners(); |
2106 }, | 1291 }, |
2107 | 1292 |
2108 /** | |
2109 * Can be used to imperatively add a key binding to the implementing | |
2110 * element. This is the imperative equivalent of declaring a keybinding | |
2111 * in the `keyBindings` prototype property. | |
2112 */ | |
2113 addOwnKeyBinding: function(eventString, handlerName) { | 1293 addOwnKeyBinding: function(eventString, handlerName) { |
2114 this._imperativeKeyBindings[eventString] = handlerName; | 1294 this._imperativeKeyBindings[eventString] = handlerName; |
2115 this._prepKeyBindings(); | 1295 this._prepKeyBindings(); |
2116 this._resetKeyEventListeners(); | 1296 this._resetKeyEventListeners(); |
2117 }, | 1297 }, |
2118 | 1298 |
2119 /** | |
2120 * When called, will remove all imperatively-added key bindings. | |
2121 */ | |
2122 removeOwnKeyBindings: function() { | 1299 removeOwnKeyBindings: function() { |
2123 this._imperativeKeyBindings = {}; | 1300 this._imperativeKeyBindings = {}; |
2124 this._prepKeyBindings(); | 1301 this._prepKeyBindings(); |
2125 this._resetKeyEventListeners(); | 1302 this._resetKeyEventListeners(); |
2126 }, | 1303 }, |
2127 | 1304 |
2128 /** | |
2129 * Returns true if a keyboard event matches `eventString`. | |
2130 * | |
2131 * @param {KeyboardEvent} event | |
2132 * @param {string} eventString | |
2133 * @return {boolean} | |
2134 */ | |
2135 keyboardEventMatchesKeys: function(event, eventString) { | 1305 keyboardEventMatchesKeys: function(event, eventString) { |
2136 var keyCombos = parseEventString(eventString); | 1306 var keyCombos = parseEventString(eventString); |
2137 for (var i = 0; i < keyCombos.length; ++i) { | 1307 for (var i = 0; i < keyCombos.length; ++i) { |
2138 if (keyComboMatchesEvent(keyCombos[i], event)) { | 1308 if (keyComboMatchesEvent(keyCombos[i], event)) { |
2139 return true; | 1309 return true; |
2140 } | 1310 } |
2141 } | 1311 } |
2142 return false; | 1312 return false; |
2143 }, | 1313 }, |
2144 | 1314 |
(...skipping 15 matching lines...) Expand all Loading... |
2160 this._collectKeyBindings().forEach(function(keyBindings) { | 1330 this._collectKeyBindings().forEach(function(keyBindings) { |
2161 for (var eventString in keyBindings) { | 1331 for (var eventString in keyBindings) { |
2162 this._addKeyBinding(eventString, keyBindings[eventString]); | 1332 this._addKeyBinding(eventString, keyBindings[eventString]); |
2163 } | 1333 } |
2164 }, this); | 1334 }, this); |
2165 | 1335 |
2166 for (var eventString in this._imperativeKeyBindings) { | 1336 for (var eventString in this._imperativeKeyBindings) { |
2167 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | 1337 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); |
2168 } | 1338 } |
2169 | 1339 |
2170 // Give precedence to combos with modifiers to be checked first. | |
2171 for (var eventName in this._keyBindings) { | 1340 for (var eventName in this._keyBindings) { |
2172 this._keyBindings[eventName].sort(function (kb1, kb2) { | 1341 this._keyBindings[eventName].sort(function (kb1, kb2) { |
2173 var b1 = kb1[0].hasModifiers; | 1342 var b1 = kb1[0].hasModifiers; |
2174 var b2 = kb2[0].hasModifiers; | 1343 var b2 = kb2[0].hasModifiers; |
2175 return (b1 === b2) ? 0 : b1 ? -1 : 1; | 1344 return (b1 === b2) ? 0 : b1 ? -1 : 1; |
2176 }) | 1345 }) |
2177 } | 1346 } |
2178 }, | 1347 }, |
2179 | 1348 |
2180 _addKeyBinding: function(eventString, handlerName) { | 1349 _addKeyBinding: function(eventString, handlerName) { |
(...skipping 30 matching lines...) Expand all Loading... |
2211 }, this); | 1380 }, this); |
2212 }, | 1381 }, |
2213 | 1382 |
2214 _unlistenKeyEventListeners: function() { | 1383 _unlistenKeyEventListeners: function() { |
2215 var keyHandlerTuple; | 1384 var keyHandlerTuple; |
2216 var keyEventTarget; | 1385 var keyEventTarget; |
2217 var eventName; | 1386 var eventName; |
2218 var boundKeyHandler; | 1387 var boundKeyHandler; |
2219 | 1388 |
2220 while (this._boundKeyHandlers.length) { | 1389 while (this._boundKeyHandlers.length) { |
2221 // My kingdom for block-scope binding and destructuring assignment.. | |
2222 keyHandlerTuple = this._boundKeyHandlers.pop(); | 1390 keyHandlerTuple = this._boundKeyHandlers.pop(); |
2223 keyEventTarget = keyHandlerTuple[0]; | 1391 keyEventTarget = keyHandlerTuple[0]; |
2224 eventName = keyHandlerTuple[1]; | 1392 eventName = keyHandlerTuple[1]; |
2225 boundKeyHandler = keyHandlerTuple[2]; | 1393 boundKeyHandler = keyHandlerTuple[2]; |
2226 | 1394 |
2227 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | 1395 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
2228 } | 1396 } |
2229 }, | 1397 }, |
2230 | 1398 |
2231 _onKeyBindingEvent: function(keyBindings, event) { | 1399 _onKeyBindingEvent: function(keyBindings, event) { |
2232 if (this.stopKeyboardEventPropagation) { | 1400 if (this.stopKeyboardEventPropagation) { |
2233 event.stopPropagation(); | 1401 event.stopPropagation(); |
2234 } | 1402 } |
2235 | 1403 |
2236 // if event has been already prevented, don't do anything | |
2237 if (event.defaultPrevented) { | 1404 if (event.defaultPrevented) { |
2238 return; | 1405 return; |
2239 } | 1406 } |
2240 | 1407 |
2241 for (var i = 0; i < keyBindings.length; i++) { | 1408 for (var i = 0; i < keyBindings.length; i++) { |
2242 var keyCombo = keyBindings[i][0]; | 1409 var keyCombo = keyBindings[i][0]; |
2243 var handlerName = keyBindings[i][1]; | 1410 var handlerName = keyBindings[i][1]; |
2244 if (keyComboMatchesEvent(keyCombo, event)) { | 1411 if (keyComboMatchesEvent(keyCombo, event)) { |
2245 this._triggerKeyHandler(keyCombo, handlerName, event); | 1412 this._triggerKeyHandler(keyCombo, handlerName, event); |
2246 // exit the loop if eventDefault was prevented | |
2247 if (event.defaultPrevented) { | 1413 if (event.defaultPrevented) { |
2248 return; | 1414 return; |
2249 } | 1415 } |
2250 } | 1416 } |
2251 } | 1417 } |
2252 }, | 1418 }, |
2253 | 1419 |
2254 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | 1420 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
2255 var detail = Object.create(keyCombo); | 1421 var detail = Object.create(keyCombo); |
2256 detail.keyboardEvent = keyboardEvent; | 1422 detail.keyboardEvent = keyboardEvent; |
2257 var event = new CustomEvent(keyCombo.event, { | 1423 var event = new CustomEvent(keyCombo.event, { |
2258 detail: detail, | 1424 detail: detail, |
2259 cancelable: true | 1425 cancelable: true |
2260 }); | 1426 }); |
2261 this[handlerName].call(this, event); | 1427 this[handlerName].call(this, event); |
2262 if (event.defaultPrevented) { | 1428 if (event.defaultPrevented) { |
2263 keyboardEvent.preventDefault(); | 1429 keyboardEvent.preventDefault(); |
2264 } | 1430 } |
2265 } | 1431 } |
2266 }; | 1432 }; |
2267 })(); | 1433 })(); |
2268 /** | |
2269 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
2270 * designated scroll target. | |
2271 * | |
2272 * Elements that consume this behavior can override the `_scrollHandler` | |
2273 * method to add logic on the scroll event. | |
2274 * | |
2275 * @demo demo/scrolling-region.html Scrolling Region | |
2276 * @demo demo/document.html Document Element | |
2277 * @polymerBehavior | |
2278 */ | |
2279 Polymer.IronScrollTargetBehavior = { | 1434 Polymer.IronScrollTargetBehavior = { |
2280 | 1435 |
2281 properties: { | 1436 properties: { |
2282 | 1437 |
2283 /** | |
2284 * Specifies the element that will handle the scroll event | |
2285 * on the behalf of the current element. This is typically a reference to
an element, | |
2286 * but there are a few more posibilities: | |
2287 * | |
2288 * ### Elements id | |
2289 * | |
2290 *```html | |
2291 * <div id="scrollable-element" style="overflow: auto;"> | |
2292 * <x-element scroll-target="scrollable-element"> | |
2293 * \x3c!-- Content--\x3e | |
2294 * </x-element> | |
2295 * </div> | |
2296 *``` | |
2297 * In this case, the `scrollTarget` will point to the outer div element. | |
2298 * | |
2299 * ### Document scrolling | |
2300 * | |
2301 * For document scrolling, you can use the reserved word `document`: | |
2302 * | |
2303 *```html | |
2304 * <x-element scroll-target="document"> | |
2305 * \x3c!-- Content --\x3e | |
2306 * </x-element> | |
2307 *``` | |
2308 * | |
2309 * ### Elements reference | |
2310 * | |
2311 *```js | |
2312 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
2313 *``` | |
2314 * | |
2315 * @type {HTMLElement} | |
2316 */ | |
2317 scrollTarget: { | 1438 scrollTarget: { |
2318 type: HTMLElement, | 1439 type: HTMLElement, |
2319 value: function() { | 1440 value: function() { |
2320 return this._defaultScrollTarget; | 1441 return this._defaultScrollTarget; |
2321 } | 1442 } |
2322 } | 1443 } |
2323 }, | 1444 }, |
2324 | 1445 |
2325 observers: [ | 1446 observers: [ |
2326 '_scrollTargetChanged(scrollTarget, isAttached)' | 1447 '_scrollTargetChanged(scrollTarget, isAttached)' |
2327 ], | 1448 ], |
2328 | 1449 |
2329 _scrollTargetChanged: function(scrollTarget, isAttached) { | 1450 _scrollTargetChanged: function(scrollTarget, isAttached) { |
2330 var eventTarget; | 1451 var eventTarget; |
2331 | 1452 |
2332 if (this._oldScrollTarget) { | 1453 if (this._oldScrollTarget) { |
2333 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | 1454 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; |
2334 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | 1455 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
2335 this._oldScrollTarget = null; | 1456 this._oldScrollTarget = null; |
2336 } | 1457 } |
2337 | 1458 |
2338 if (!isAttached) { | 1459 if (!isAttached) { |
2339 return; | 1460 return; |
2340 } | 1461 } |
2341 // Support element id references | |
2342 if (scrollTarget === 'document') { | 1462 if (scrollTarget === 'document') { |
2343 | 1463 |
2344 this.scrollTarget = this._doc; | 1464 this.scrollTarget = this._doc; |
2345 | 1465 |
2346 } else if (typeof scrollTarget === 'string') { | 1466 } else if (typeof scrollTarget === 'string') { |
2347 | 1467 |
2348 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | 1468 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : |
2349 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | 1469 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); |
2350 | 1470 |
2351 } else if (this._isValidScrollTarget()) { | 1471 } else if (this._isValidScrollTarget()) { |
2352 | 1472 |
2353 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | 1473 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
2354 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | 1474 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); |
2355 this._oldScrollTarget = scrollTarget; | 1475 this._oldScrollTarget = scrollTarget; |
2356 | 1476 |
2357 eventTarget.addEventListener('scroll', this._boundScrollHandler); | 1477 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
2358 } | 1478 } |
2359 }, | 1479 }, |
2360 | 1480 |
2361 /** | |
2362 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
2363 * | |
2364 * @protected | |
2365 */ | |
2366 _scrollHandler: function scrollHandler() {}, | 1481 _scrollHandler: function scrollHandler() {}, |
2367 | 1482 |
2368 /** | |
2369 * The default scroll target. Consumers of this behavior may want to customi
ze | |
2370 * the default scroll target. | |
2371 * | |
2372 * @type {Element} | |
2373 */ | |
2374 get _defaultScrollTarget() { | 1483 get _defaultScrollTarget() { |
2375 return this._doc; | 1484 return this._doc; |
2376 }, | 1485 }, |
2377 | 1486 |
2378 /** | |
2379 * Shortcut for the document element | |
2380 * | |
2381 * @type {Element} | |
2382 */ | |
2383 get _doc() { | 1487 get _doc() { |
2384 return this.ownerDocument.documentElement; | 1488 return this.ownerDocument.documentElement; |
2385 }, | 1489 }, |
2386 | 1490 |
2387 /** | |
2388 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
2389 * | |
2390 * @type {number} | |
2391 */ | |
2392 get _scrollTop() { | 1491 get _scrollTop() { |
2393 if (this._isValidScrollTarget()) { | 1492 if (this._isValidScrollTarget()) { |
2394 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | 1493 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; |
2395 } | 1494 } |
2396 return 0; | 1495 return 0; |
2397 }, | 1496 }, |
2398 | 1497 |
2399 /** | |
2400 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
2401 * | |
2402 * @type {number} | |
2403 */ | |
2404 get _scrollLeft() { | 1498 get _scrollLeft() { |
2405 if (this._isValidScrollTarget()) { | 1499 if (this._isValidScrollTarget()) { |
2406 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | 1500 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; |
2407 } | 1501 } |
2408 return 0; | 1502 return 0; |
2409 }, | 1503 }, |
2410 | 1504 |
2411 /** | |
2412 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
2413 * | |
2414 * @type {number} | |
2415 */ | |
2416 set _scrollTop(top) { | 1505 set _scrollTop(top) { |
2417 if (this.scrollTarget === this._doc) { | 1506 if (this.scrollTarget === this._doc) { |
2418 window.scrollTo(window.pageXOffset, top); | 1507 window.scrollTo(window.pageXOffset, top); |
2419 } else if (this._isValidScrollTarget()) { | 1508 } else if (this._isValidScrollTarget()) { |
2420 this.scrollTarget.scrollTop = top; | 1509 this.scrollTarget.scrollTop = top; |
2421 } | 1510 } |
2422 }, | 1511 }, |
2423 | 1512 |
2424 /** | |
2425 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
2426 * | |
2427 * @type {number} | |
2428 */ | |
2429 set _scrollLeft(left) { | 1513 set _scrollLeft(left) { |
2430 if (this.scrollTarget === this._doc) { | 1514 if (this.scrollTarget === this._doc) { |
2431 window.scrollTo(left, window.pageYOffset); | 1515 window.scrollTo(left, window.pageYOffset); |
2432 } else if (this._isValidScrollTarget()) { | 1516 } else if (this._isValidScrollTarget()) { |
2433 this.scrollTarget.scrollLeft = left; | 1517 this.scrollTarget.scrollLeft = left; |
2434 } | 1518 } |
2435 }, | 1519 }, |
2436 | 1520 |
2437 /** | |
2438 * Scrolls the content to a particular place. | |
2439 * | |
2440 * @method scroll | |
2441 * @param {number} left The left position | |
2442 * @param {number} top The top position | |
2443 */ | |
2444 scroll: function(left, top) { | 1521 scroll: function(left, top) { |
2445 if (this.scrollTarget === this._doc) { | 1522 if (this.scrollTarget === this._doc) { |
2446 window.scrollTo(left, top); | 1523 window.scrollTo(left, top); |
2447 } else if (this._isValidScrollTarget()) { | 1524 } else if (this._isValidScrollTarget()) { |
2448 this.scrollTarget.scrollLeft = left; | 1525 this.scrollTarget.scrollLeft = left; |
2449 this.scrollTarget.scrollTop = top; | 1526 this.scrollTarget.scrollTop = top; |
2450 } | 1527 } |
2451 }, | 1528 }, |
2452 | 1529 |
2453 /** | |
2454 * Gets the width of the scroll target. | |
2455 * | |
2456 * @type {number} | |
2457 */ | |
2458 get _scrollTargetWidth() { | 1530 get _scrollTargetWidth() { |
2459 if (this._isValidScrollTarget()) { | 1531 if (this._isValidScrollTarget()) { |
2460 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | 1532 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; |
2461 } | 1533 } |
2462 return 0; | 1534 return 0; |
2463 }, | 1535 }, |
2464 | 1536 |
2465 /** | |
2466 * Gets the height of the scroll target. | |
2467 * | |
2468 * @type {number} | |
2469 */ | |
2470 get _scrollTargetHeight() { | 1537 get _scrollTargetHeight() { |
2471 if (this._isValidScrollTarget()) { | 1538 if (this._isValidScrollTarget()) { |
2472 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | 1539 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; |
2473 } | 1540 } |
2474 return 0; | 1541 return 0; |
2475 }, | 1542 }, |
2476 | 1543 |
2477 /** | |
2478 * Returns true if the scroll target is a valid HTMLElement. | |
2479 * | |
2480 * @return {boolean} | |
2481 */ | |
2482 _isValidScrollTarget: function() { | 1544 _isValidScrollTarget: function() { |
2483 return this.scrollTarget instanceof HTMLElement; | 1545 return this.scrollTarget instanceof HTMLElement; |
2484 } | 1546 } |
2485 }; | 1547 }; |
2486 (function() { | 1548 (function() { |
2487 | 1549 |
2488 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 1550 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
2489 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 1551 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
2490 var DEFAULT_PHYSICAL_COUNT = 3; | 1552 var DEFAULT_PHYSICAL_COUNT = 3; |
2491 var HIDDEN_Y = '-10000px'; | 1553 var HIDDEN_Y = '-10000px'; |
2492 var DEFAULT_GRID_SIZE = 200; | 1554 var DEFAULT_GRID_SIZE = 200; |
2493 var SECRET_TABINDEX = -100; | 1555 var SECRET_TABINDEX = -100; |
2494 | 1556 |
2495 Polymer({ | 1557 Polymer({ |
2496 | 1558 |
2497 is: 'iron-list', | 1559 is: 'iron-list', |
2498 | 1560 |
2499 properties: { | 1561 properties: { |
2500 | 1562 |
2501 /** | |
2502 * An array containing items determining how many instances of the templat
e | |
2503 * to stamp and that that each template instance should bind to. | |
2504 */ | |
2505 items: { | 1563 items: { |
2506 type: Array | 1564 type: Array |
2507 }, | 1565 }, |
2508 | 1566 |
2509 /** | |
2510 * The max count of physical items the pool can extend to. | |
2511 */ | |
2512 maxPhysicalCount: { | 1567 maxPhysicalCount: { |
2513 type: Number, | 1568 type: Number, |
2514 value: 500 | 1569 value: 500 |
2515 }, | 1570 }, |
2516 | 1571 |
2517 /** | |
2518 * The name of the variable to add to the binding scope for the array | |
2519 * element associated with a given template instance. | |
2520 */ | |
2521 as: { | 1572 as: { |
2522 type: String, | 1573 type: String, |
2523 value: 'item' | 1574 value: 'item' |
2524 }, | 1575 }, |
2525 | 1576 |
2526 /** | |
2527 * The name of the variable to add to the binding scope with the index | |
2528 * for the row. | |
2529 */ | |
2530 indexAs: { | 1577 indexAs: { |
2531 type: String, | 1578 type: String, |
2532 value: 'index' | 1579 value: 'index' |
2533 }, | 1580 }, |
2534 | 1581 |
2535 /** | |
2536 * The name of the variable to add to the binding scope to indicate | |
2537 * if the row is selected. | |
2538 */ | |
2539 selectedAs: { | 1582 selectedAs: { |
2540 type: String, | 1583 type: String, |
2541 value: 'selected' | 1584 value: 'selected' |
2542 }, | 1585 }, |
2543 | 1586 |
2544 /** | |
2545 * When true, the list is rendered as a grid. Grid items must have | |
2546 * fixed width and height set via CSS. e.g. | |
2547 * | |
2548 * ```html | |
2549 * <iron-list grid> | |
2550 * <template> | |
2551 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
2552 * </template> | |
2553 * </iron-list> | |
2554 * ``` | |
2555 */ | |
2556 grid: { | 1587 grid: { |
2557 type: Boolean, | 1588 type: Boolean, |
2558 value: false, | 1589 value: false, |
2559 reflectToAttribute: true | 1590 reflectToAttribute: true |
2560 }, | 1591 }, |
2561 | 1592 |
2562 /** | |
2563 * When true, tapping a row will select the item, placing its data model | |
2564 * in the set of selected items retrievable via the selection property. | |
2565 * | |
2566 * Note that tapping focusable elements within the list item will not | |
2567 * result in selection, since they are presumed to have their * own action
. | |
2568 */ | |
2569 selectionEnabled: { | 1593 selectionEnabled: { |
2570 type: Boolean, | 1594 type: Boolean, |
2571 value: false | 1595 value: false |
2572 }, | 1596 }, |
2573 | 1597 |
2574 /** | |
2575 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
2576 * if no item is selected. | |
2577 */ | |
2578 selectedItem: { | 1598 selectedItem: { |
2579 type: Object, | 1599 type: Object, |
2580 notify: true | 1600 notify: true |
2581 }, | 1601 }, |
2582 | 1602 |
2583 /** | |
2584 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
2585 */ | |
2586 selectedItems: { | 1603 selectedItems: { |
2587 type: Object, | 1604 type: Object, |
2588 notify: true | 1605 notify: true |
2589 }, | 1606 }, |
2590 | 1607 |
2591 /** | |
2592 * When `true`, multiple items may be selected at once (in this case, | |
2593 * `selected` is an array of currently selected items). When `false`, | |
2594 * only one item may be selected at a time. | |
2595 */ | |
2596 multiSelection: { | 1608 multiSelection: { |
2597 type: Boolean, | 1609 type: Boolean, |
2598 value: false | 1610 value: false |
2599 } | 1611 } |
2600 }, | 1612 }, |
2601 | 1613 |
2602 observers: [ | 1614 observers: [ |
2603 '_itemsChanged(items.*)', | 1615 '_itemsChanged(items.*)', |
2604 '_selectionEnabledChanged(selectionEnabled)', | 1616 '_selectionEnabledChanged(selectionEnabled)', |
2605 '_multiSelectionChanged(multiSelection)', | 1617 '_multiSelectionChanged(multiSelection)', |
2606 '_setOverflow(scrollTarget)' | 1618 '_setOverflow(scrollTarget)' |
2607 ], | 1619 ], |
2608 | 1620 |
2609 behaviors: [ | 1621 behaviors: [ |
2610 Polymer.Templatizer, | 1622 Polymer.Templatizer, |
2611 Polymer.IronResizableBehavior, | 1623 Polymer.IronResizableBehavior, |
2612 Polymer.IronA11yKeysBehavior, | 1624 Polymer.IronA11yKeysBehavior, |
2613 Polymer.IronScrollTargetBehavior | 1625 Polymer.IronScrollTargetBehavior |
2614 ], | 1626 ], |
2615 | 1627 |
2616 keyBindings: { | 1628 keyBindings: { |
2617 'up': '_didMoveUp', | 1629 'up': '_didMoveUp', |
2618 'down': '_didMoveDown', | 1630 'down': '_didMoveDown', |
2619 'enter': '_didEnter' | 1631 'enter': '_didEnter' |
2620 }, | 1632 }, |
2621 | 1633 |
2622 /** | |
2623 * The ratio of hidden tiles that should remain in the scroll direction. | |
2624 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
2625 */ | |
2626 _ratio: 0.5, | 1634 _ratio: 0.5, |
2627 | 1635 |
2628 /** | |
2629 * The padding-top value for the list. | |
2630 */ | |
2631 _scrollerPaddingTop: 0, | 1636 _scrollerPaddingTop: 0, |
2632 | 1637 |
2633 /** | |
2634 * This value is the same as `scrollTop`. | |
2635 */ | |
2636 _scrollPosition: 0, | 1638 _scrollPosition: 0, |
2637 | 1639 |
2638 /** | |
2639 * The sum of the heights of all the tiles in the DOM. | |
2640 */ | |
2641 _physicalSize: 0, | 1640 _physicalSize: 0, |
2642 | 1641 |
2643 /** | |
2644 * The average `offsetHeight` of the tiles observed till now. | |
2645 */ | |
2646 _physicalAverage: 0, | 1642 _physicalAverage: 0, |
2647 | 1643 |
2648 /** | |
2649 * The number of tiles which `offsetHeight` > 0 observed until now. | |
2650 */ | |
2651 _physicalAverageCount: 0, | 1644 _physicalAverageCount: 0, |
2652 | 1645 |
2653 /** | |
2654 * The Y position of the item rendered in the `_physicalStart` | |
2655 * tile relative to the scrolling list. | |
2656 */ | |
2657 _physicalTop: 0, | 1646 _physicalTop: 0, |
2658 | 1647 |
2659 /** | |
2660 * The number of items in the list. | |
2661 */ | |
2662 _virtualCount: 0, | 1648 _virtualCount: 0, |
2663 | 1649 |
2664 /** | |
2665 * A map between an item key and its physical item index | |
2666 */ | |
2667 _physicalIndexForKey: null, | 1650 _physicalIndexForKey: null, |
2668 | 1651 |
2669 /** | |
2670 * The estimated scroll height based on `_physicalAverage` | |
2671 */ | |
2672 _estScrollHeight: 0, | 1652 _estScrollHeight: 0, |
2673 | 1653 |
2674 /** | |
2675 * The scroll height of the dom node | |
2676 */ | |
2677 _scrollHeight: 0, | 1654 _scrollHeight: 0, |
2678 | 1655 |
2679 /** | |
2680 * The height of the list. This is referred as the viewport in the context o
f list. | |
2681 */ | |
2682 _viewportHeight: 0, | 1656 _viewportHeight: 0, |
2683 | 1657 |
2684 /** | |
2685 * The width of the list. This is referred as the viewport in the context of
list. | |
2686 */ | |
2687 _viewportWidth: 0, | 1658 _viewportWidth: 0, |
2688 | 1659 |
2689 /** | |
2690 * An array of DOM nodes that are currently in the tree | |
2691 * @type {?Array<!TemplatizerNode>} | |
2692 */ | |
2693 _physicalItems: null, | 1660 _physicalItems: null, |
2694 | 1661 |
2695 /** | |
2696 * An array of heights for each item in `_physicalItems` | |
2697 * @type {?Array<number>} | |
2698 */ | |
2699 _physicalSizes: null, | 1662 _physicalSizes: null, |
2700 | 1663 |
2701 /** | |
2702 * A cached value for the first visible index. | |
2703 * See `firstVisibleIndex` | |
2704 * @type {?number} | |
2705 */ | |
2706 _firstVisibleIndexVal: null, | 1664 _firstVisibleIndexVal: null, |
2707 | 1665 |
2708 /** | |
2709 * A cached value for the last visible index. | |
2710 * See `lastVisibleIndex` | |
2711 * @type {?number} | |
2712 */ | |
2713 _lastVisibleIndexVal: null, | 1666 _lastVisibleIndexVal: null, |
2714 | 1667 |
2715 /** | |
2716 * A Polymer collection for the items. | |
2717 * @type {?Polymer.Collection} | |
2718 */ | |
2719 _collection: null, | 1668 _collection: null, |
2720 | 1669 |
2721 /** | |
2722 * True if the current item list was rendered for the first time | |
2723 * after attached. | |
2724 */ | |
2725 _itemsRendered: false, | 1670 _itemsRendered: false, |
2726 | 1671 |
2727 /** | |
2728 * The page that is currently rendered. | |
2729 */ | |
2730 _lastPage: null, | 1672 _lastPage: null, |
2731 | 1673 |
2732 /** | |
2733 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
2734 */ | |
2735 _maxPages: 3, | 1674 _maxPages: 3, |
2736 | 1675 |
2737 /** | |
2738 * The currently focused physical item. | |
2739 */ | |
2740 _focusedItem: null, | 1676 _focusedItem: null, |
2741 | 1677 |
2742 /** | |
2743 * The index of the `_focusedItem`. | |
2744 */ | |
2745 _focusedIndex: -1, | 1678 _focusedIndex: -1, |
2746 | 1679 |
2747 /** | |
2748 * The the item that is focused if it is moved offscreen. | |
2749 * @private {?TemplatizerNode} | |
2750 */ | |
2751 _offscreenFocusedItem: null, | 1680 _offscreenFocusedItem: null, |
2752 | 1681 |
2753 /** | |
2754 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
2755 * list when that item is moved offscreen. | |
2756 */ | |
2757 _focusBackfillItem: null, | 1682 _focusBackfillItem: null, |
2758 | 1683 |
2759 /** | |
2760 * The maximum items per row | |
2761 */ | |
2762 _itemsPerRow: 1, | 1684 _itemsPerRow: 1, |
2763 | 1685 |
2764 /** | |
2765 * The width of each grid item | |
2766 */ | |
2767 _itemWidth: 0, | 1686 _itemWidth: 0, |
2768 | 1687 |
2769 /** | |
2770 * The height of the row in grid layout. | |
2771 */ | |
2772 _rowHeight: 0, | 1688 _rowHeight: 0, |
2773 | 1689 |
2774 /** | |
2775 * The bottom of the physical content. | |
2776 */ | |
2777 get _physicalBottom() { | 1690 get _physicalBottom() { |
2778 return this._physicalTop + this._physicalSize; | 1691 return this._physicalTop + this._physicalSize; |
2779 }, | 1692 }, |
2780 | 1693 |
2781 /** | |
2782 * The bottom of the scroll. | |
2783 */ | |
2784 get _scrollBottom() { | 1694 get _scrollBottom() { |
2785 return this._scrollPosition + this._viewportHeight; | 1695 return this._scrollPosition + this._viewportHeight; |
2786 }, | 1696 }, |
2787 | 1697 |
2788 /** | |
2789 * The n-th item rendered in the last physical item. | |
2790 */ | |
2791 get _virtualEnd() { | 1698 get _virtualEnd() { |
2792 return this._virtualStart + this._physicalCount - 1; | 1699 return this._virtualStart + this._physicalCount - 1; |
2793 }, | 1700 }, |
2794 | 1701 |
2795 /** | |
2796 * The height of the physical content that isn't on the screen. | |
2797 */ | |
2798 get _hiddenContentSize() { | 1702 get _hiddenContentSize() { |
2799 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 1703 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
2800 return size - this._viewportHeight; | 1704 return size - this._viewportHeight; |
2801 }, | 1705 }, |
2802 | 1706 |
2803 /** | |
2804 * The maximum scroll top value. | |
2805 */ | |
2806 get _maxScrollTop() { | 1707 get _maxScrollTop() { |
2807 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 1708 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
2808 }, | 1709 }, |
2809 | 1710 |
2810 /** | |
2811 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
2812 */ | |
2813 _minVirtualStart: 0, | 1711 _minVirtualStart: 0, |
2814 | 1712 |
2815 /** | |
2816 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
2817 */ | |
2818 get _maxVirtualStart() { | 1713 get _maxVirtualStart() { |
2819 return Math.max(0, this._virtualCount - this._physicalCount); | 1714 return Math.max(0, this._virtualCount - this._physicalCount); |
2820 }, | 1715 }, |
2821 | 1716 |
2822 /** | |
2823 * The n-th item rendered in the `_physicalStart` tile. | |
2824 */ | |
2825 _virtualStartVal: 0, | 1717 _virtualStartVal: 0, |
2826 | 1718 |
2827 set _virtualStart(val) { | 1719 set _virtualStart(val) { |
2828 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 1720 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
2829 }, | 1721 }, |
2830 | 1722 |
2831 get _virtualStart() { | 1723 get _virtualStart() { |
2832 return this._virtualStartVal || 0; | 1724 return this._virtualStartVal || 0; |
2833 }, | 1725 }, |
2834 | 1726 |
2835 /** | |
2836 * The k-th tile that is at the top of the scrolling list. | |
2837 */ | |
2838 _physicalStartVal: 0, | 1727 _physicalStartVal: 0, |
2839 | 1728 |
2840 set _physicalStart(val) { | 1729 set _physicalStart(val) { |
2841 this._physicalStartVal = val % this._physicalCount; | 1730 this._physicalStartVal = val % this._physicalCount; |
2842 if (this._physicalStartVal < 0) { | 1731 if (this._physicalStartVal < 0) { |
2843 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 1732 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
2844 } | 1733 } |
2845 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1734 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2846 }, | 1735 }, |
2847 | 1736 |
2848 get _physicalStart() { | 1737 get _physicalStart() { |
2849 return this._physicalStartVal || 0; | 1738 return this._physicalStartVal || 0; |
2850 }, | 1739 }, |
2851 | 1740 |
2852 /** | |
2853 * The number of tiles in the DOM. | |
2854 */ | |
2855 _physicalCountVal: 0, | 1741 _physicalCountVal: 0, |
2856 | 1742 |
2857 set _physicalCount(val) { | 1743 set _physicalCount(val) { |
2858 this._physicalCountVal = val; | 1744 this._physicalCountVal = val; |
2859 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1745 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
2860 }, | 1746 }, |
2861 | 1747 |
2862 get _physicalCount() { | 1748 get _physicalCount() { |
2863 return this._physicalCountVal; | 1749 return this._physicalCountVal; |
2864 }, | 1750 }, |
2865 | 1751 |
2866 /** | |
2867 * The k-th tile that is at the bottom of the scrolling list. | |
2868 */ | |
2869 _physicalEnd: 0, | 1752 _physicalEnd: 0, |
2870 | 1753 |
2871 /** | |
2872 * An optimal physical size such that we will have enough physical items | |
2873 * to fill up the viewport and recycle when the user scrolls. | |
2874 * | |
2875 * This default value assumes that we will at least have the equivalent | |
2876 * to a viewport of physical items above and below the user's viewport. | |
2877 */ | |
2878 get _optPhysicalSize() { | 1754 get _optPhysicalSize() { |
2879 if (this.grid) { | 1755 if (this.grid) { |
2880 return this._estRowsInView * this._rowHeight * this._maxPages; | 1756 return this._estRowsInView * this._rowHeight * this._maxPages; |
2881 } | 1757 } |
2882 return this._viewportHeight * this._maxPages; | 1758 return this._viewportHeight * this._maxPages; |
2883 }, | 1759 }, |
2884 | 1760 |
2885 get _optPhysicalCount() { | 1761 get _optPhysicalCount() { |
2886 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 1762 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
2887 }, | 1763 }, |
2888 | 1764 |
2889 /** | |
2890 * True if the current list is visible. | |
2891 */ | |
2892 get _isVisible() { | 1765 get _isVisible() { |
2893 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 1766 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
2894 }, | 1767 }, |
2895 | 1768 |
2896 /** | |
2897 * Gets the index of the first visible item in the viewport. | |
2898 * | |
2899 * @type {number} | |
2900 */ | |
2901 get firstVisibleIndex() { | 1769 get firstVisibleIndex() { |
2902 if (this._firstVisibleIndexVal === null) { | 1770 if (this._firstVisibleIndexVal === null) { |
2903 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 1771 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
2904 | 1772 |
2905 this._firstVisibleIndexVal = this._iterateItems( | 1773 this._firstVisibleIndexVal = this._iterateItems( |
2906 function(pidx, vidx) { | 1774 function(pidx, vidx) { |
2907 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1775 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2908 | 1776 |
2909 if (physicalOffset > this._scrollPosition) { | 1777 if (physicalOffset > this._scrollPosition) { |
2910 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 1778 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; |
2911 } | 1779 } |
2912 // Handle a partially rendered final row in grid mode | |
2913 if (this.grid && this._virtualCount - 1 === vidx) { | 1780 if (this.grid && this._virtualCount - 1 === vidx) { |
2914 return vidx - (vidx % this._itemsPerRow); | 1781 return vidx - (vidx % this._itemsPerRow); |
2915 } | 1782 } |
2916 }) || 0; | 1783 }) || 0; |
2917 } | 1784 } |
2918 return this._firstVisibleIndexVal; | 1785 return this._firstVisibleIndexVal; |
2919 }, | 1786 }, |
2920 | 1787 |
2921 /** | |
2922 * Gets the index of the last visible item in the viewport. | |
2923 * | |
2924 * @type {number} | |
2925 */ | |
2926 get lastVisibleIndex() { | 1788 get lastVisibleIndex() { |
2927 if (this._lastVisibleIndexVal === null) { | 1789 if (this._lastVisibleIndexVal === null) { |
2928 if (this.grid) { | 1790 if (this.grid) { |
2929 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 1791 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
2930 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 1792 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
2931 } else { | 1793 } else { |
2932 var physicalOffset = this._physicalTop; | 1794 var physicalOffset = this._physicalTop; |
2933 this._iterateItems(function(pidx, vidx) { | 1795 this._iterateItems(function(pidx, vidx) { |
2934 if (physicalOffset < this._scrollBottom) { | 1796 if (physicalOffset < this._scrollBottom) { |
2935 this._lastVisibleIndexVal = vidx; | 1797 this._lastVisibleIndexVal = vidx; |
2936 } else { | 1798 } else { |
2937 // Break _iterateItems | |
2938 return true; | 1799 return true; |
2939 } | 1800 } |
2940 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1801 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
2941 }); | 1802 }); |
2942 } | 1803 } |
2943 } | 1804 } |
2944 return this._lastVisibleIndexVal; | 1805 return this._lastVisibleIndexVal; |
2945 }, | 1806 }, |
2946 | 1807 |
2947 get _defaultScrollTarget() { | 1808 get _defaultScrollTarget() { |
(...skipping 11 matching lines...) Expand all Loading... |
2959 return Math.ceil(this._physicalCount / this._itemsPerRow); | 1820 return Math.ceil(this._physicalCount / this._itemsPerRow); |
2960 }, | 1821 }, |
2961 | 1822 |
2962 ready: function() { | 1823 ready: function() { |
2963 this.addEventListener('focus', this._didFocus.bind(this), true); | 1824 this.addEventListener('focus', this._didFocus.bind(this), true); |
2964 }, | 1825 }, |
2965 | 1826 |
2966 attached: function() { | 1827 attached: function() { |
2967 this.updateViewportBoundaries(); | 1828 this.updateViewportBoundaries(); |
2968 this._render(); | 1829 this._render(); |
2969 // `iron-resize` is fired when the list is attached if the event is added | |
2970 // before attached causing unnecessary work. | |
2971 this.listen(this, 'iron-resize', '_resizeHandler'); | 1830 this.listen(this, 'iron-resize', '_resizeHandler'); |
2972 }, | 1831 }, |
2973 | 1832 |
2974 detached: function() { | 1833 detached: function() { |
2975 this._itemsRendered = false; | 1834 this._itemsRendered = false; |
2976 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 1835 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
2977 }, | 1836 }, |
2978 | 1837 |
2979 /** | |
2980 * Set the overflow property if this element has its own scrolling region | |
2981 */ | |
2982 _setOverflow: function(scrollTarget) { | 1838 _setOverflow: function(scrollTarget) { |
2983 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 1839 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
2984 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 1840 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
2985 }, | 1841 }, |
2986 | 1842 |
2987 /** | |
2988 * Invoke this method if you dynamically update the viewport's | |
2989 * size or CSS padding. | |
2990 * | |
2991 * @method updateViewportBoundaries | |
2992 */ | |
2993 updateViewportBoundaries: function() { | 1843 updateViewportBoundaries: function() { |
2994 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 1844 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : |
2995 parseInt(window.getComputedStyle(this)['padding-top'], 10); | 1845 parseInt(window.getComputedStyle(this)['padding-top'], 10); |
2996 | 1846 |
2997 this._viewportHeight = this._scrollTargetHeight; | 1847 this._viewportHeight = this._scrollTargetHeight; |
2998 if (this.grid) { | 1848 if (this.grid) { |
2999 this._updateGridMetrics(); | 1849 this._updateGridMetrics(); |
3000 } | 1850 } |
3001 }, | 1851 }, |
3002 | 1852 |
3003 /** | |
3004 * Update the models, the position of the | |
3005 * items in the viewport and recycle tiles as needed. | |
3006 */ | |
3007 _scrollHandler: function() { | 1853 _scrollHandler: function() { |
3008 // clamp the `scrollTop` value | |
3009 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 1854 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
3010 var delta = scrollTop - this._scrollPosition; | 1855 var delta = scrollTop - this._scrollPosition; |
3011 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 1856 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
3012 var ratio = this._ratio; | 1857 var ratio = this._ratio; |
3013 var recycledTiles = 0; | 1858 var recycledTiles = 0; |
3014 var hiddenContentSize = this._hiddenContentSize; | 1859 var hiddenContentSize = this._hiddenContentSize; |
3015 var currentRatio = ratio; | 1860 var currentRatio = ratio; |
3016 var movingUp = []; | 1861 var movingUp = []; |
3017 | 1862 |
3018 // track the last `scrollTop` | |
3019 this._scrollPosition = scrollTop; | 1863 this._scrollPosition = scrollTop; |
3020 | 1864 |
3021 // clear cached visible indexes | |
3022 this._firstVisibleIndexVal = null; | 1865 this._firstVisibleIndexVal = null; |
3023 this._lastVisibleIndexVal = null; | 1866 this._lastVisibleIndexVal = null; |
3024 | 1867 |
3025 scrollBottom = this._scrollBottom; | 1868 scrollBottom = this._scrollBottom; |
3026 physicalBottom = this._physicalBottom; | 1869 physicalBottom = this._physicalBottom; |
3027 | 1870 |
3028 // random access | |
3029 if (Math.abs(delta) > this._physicalSize) { | 1871 if (Math.abs(delta) > this._physicalSize) { |
3030 this._physicalTop += delta; | 1872 this._physicalTop += delta; |
3031 recycledTiles = Math.round(delta / this._physicalAverage); | 1873 recycledTiles = Math.round(delta / this._physicalAverage); |
3032 } | 1874 } |
3033 // scroll up | |
3034 else if (delta < 0) { | 1875 else if (delta < 0) { |
3035 var topSpace = scrollTop - this._physicalTop; | 1876 var topSpace = scrollTop - this._physicalTop; |
3036 var virtualStart = this._virtualStart; | 1877 var virtualStart = this._virtualStart; |
3037 | 1878 |
3038 recycledTileSet = []; | 1879 recycledTileSet = []; |
3039 | 1880 |
3040 kth = this._physicalEnd; | 1881 kth = this._physicalEnd; |
3041 currentRatio = topSpace / hiddenContentSize; | 1882 currentRatio = topSpace / hiddenContentSize; |
3042 | 1883 |
3043 // move tiles from bottom to top | |
3044 while ( | 1884 while ( |
3045 // approximate `currentRatio` to `ratio` | |
3046 currentRatio < ratio && | 1885 currentRatio < ratio && |
3047 // recycle less physical items than the total | |
3048 recycledTiles < this._physicalCount && | 1886 recycledTiles < this._physicalCount && |
3049 // ensure that these recycled tiles are needed | |
3050 virtualStart - recycledTiles > 0 && | 1887 virtualStart - recycledTiles > 0 && |
3051 // ensure that the tile is not visible | |
3052 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | 1888 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom |
3053 ) { | 1889 ) { |
3054 | 1890 |
3055 tileHeight = this._getPhysicalSizeIncrement(kth); | 1891 tileHeight = this._getPhysicalSizeIncrement(kth); |
3056 currentRatio += tileHeight / hiddenContentSize; | 1892 currentRatio += tileHeight / hiddenContentSize; |
3057 physicalBottom -= tileHeight; | 1893 physicalBottom -= tileHeight; |
3058 recycledTileSet.push(kth); | 1894 recycledTileSet.push(kth); |
3059 recycledTiles++; | 1895 recycledTiles++; |
3060 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 1896 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; |
3061 } | 1897 } |
3062 | 1898 |
3063 movingUp = recycledTileSet; | 1899 movingUp = recycledTileSet; |
3064 recycledTiles = -recycledTiles; | 1900 recycledTiles = -recycledTiles; |
3065 } | 1901 } |
3066 // scroll down | |
3067 else if (delta > 0) { | 1902 else if (delta > 0) { |
3068 var bottomSpace = physicalBottom - scrollBottom; | 1903 var bottomSpace = physicalBottom - scrollBottom; |
3069 var virtualEnd = this._virtualEnd; | 1904 var virtualEnd = this._virtualEnd; |
3070 var lastVirtualItemIndex = this._virtualCount-1; | 1905 var lastVirtualItemIndex = this._virtualCount-1; |
3071 | 1906 |
3072 recycledTileSet = []; | 1907 recycledTileSet = []; |
3073 | 1908 |
3074 kth = this._physicalStart; | 1909 kth = this._physicalStart; |
3075 currentRatio = bottomSpace / hiddenContentSize; | 1910 currentRatio = bottomSpace / hiddenContentSize; |
3076 | 1911 |
3077 // move tiles from top to bottom | |
3078 while ( | 1912 while ( |
3079 // approximate `currentRatio` to `ratio` | |
3080 currentRatio < ratio && | 1913 currentRatio < ratio && |
3081 // recycle less physical items than the total | |
3082 recycledTiles < this._physicalCount && | 1914 recycledTiles < this._physicalCount && |
3083 // ensure that these recycled tiles are needed | |
3084 virtualEnd + recycledTiles < lastVirtualItemIndex && | 1915 virtualEnd + recycledTiles < lastVirtualItemIndex && |
3085 // ensure that the tile is not visible | |
3086 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | 1916 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop |
3087 ) { | 1917 ) { |
3088 | 1918 |
3089 tileHeight = this._getPhysicalSizeIncrement(kth); | 1919 tileHeight = this._getPhysicalSizeIncrement(kth); |
3090 currentRatio += tileHeight / hiddenContentSize; | 1920 currentRatio += tileHeight / hiddenContentSize; |
3091 | 1921 |
3092 this._physicalTop += tileHeight; | 1922 this._physicalTop += tileHeight; |
3093 recycledTileSet.push(kth); | 1923 recycledTileSet.push(kth); |
3094 recycledTiles++; | 1924 recycledTiles++; |
3095 kth = (kth + 1) % this._physicalCount; | 1925 kth = (kth + 1) % this._physicalCount; |
3096 } | 1926 } |
3097 } | 1927 } |
3098 | 1928 |
3099 if (recycledTiles === 0) { | 1929 if (recycledTiles === 0) { |
3100 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
3101 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 1930 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
3102 this._increasePoolIfNeeded(); | 1931 this._increasePoolIfNeeded(); |
3103 } | 1932 } |
3104 } else { | 1933 } else { |
3105 this._virtualStart = this._virtualStart + recycledTiles; | 1934 this._virtualStart = this._virtualStart + recycledTiles; |
3106 this._physicalStart = this._physicalStart + recycledTiles; | 1935 this._physicalStart = this._physicalStart + recycledTiles; |
3107 this._update(recycledTileSet, movingUp); | 1936 this._update(recycledTileSet, movingUp); |
3108 } | 1937 } |
3109 }, | 1938 }, |
3110 | 1939 |
3111 /** | |
3112 * Update the list of items, starting from the `_virtualStart` item. | |
3113 * @param {!Array<number>=} itemSet | |
3114 * @param {!Array<number>=} movingUp | |
3115 */ | |
3116 _update: function(itemSet, movingUp) { | 1940 _update: function(itemSet, movingUp) { |
3117 // manage focus | |
3118 this._manageFocus(); | 1941 this._manageFocus(); |
3119 // update models | |
3120 this._assignModels(itemSet); | 1942 this._assignModels(itemSet); |
3121 // measure heights | |
3122 this._updateMetrics(itemSet); | 1943 this._updateMetrics(itemSet); |
3123 // adjust offset after measuring | |
3124 if (movingUp) { | 1944 if (movingUp) { |
3125 while (movingUp.length) { | 1945 while (movingUp.length) { |
3126 var idx = movingUp.pop(); | 1946 var idx = movingUp.pop(); |
3127 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 1947 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
3128 } | 1948 } |
3129 } | 1949 } |
3130 // update the position of the items | |
3131 this._positionItems(); | 1950 this._positionItems(); |
3132 // set the scroller size | |
3133 this._updateScrollerSize(); | 1951 this._updateScrollerSize(); |
3134 // increase the pool of physical items | |
3135 this._increasePoolIfNeeded(); | 1952 this._increasePoolIfNeeded(); |
3136 }, | 1953 }, |
3137 | 1954 |
3138 /** | |
3139 * Creates a pool of DOM elements and attaches them to the local dom. | |
3140 */ | |
3141 _createPool: function(size) { | 1955 _createPool: function(size) { |
3142 var physicalItems = new Array(size); | 1956 var physicalItems = new Array(size); |
3143 | 1957 |
3144 this._ensureTemplatized(); | 1958 this._ensureTemplatized(); |
3145 | 1959 |
3146 for (var i = 0; i < size; i++) { | 1960 for (var i = 0; i < size; i++) { |
3147 var inst = this.stamp(null); | 1961 var inst = this.stamp(null); |
3148 // First element child is item; Safari doesn't support children[0] | |
3149 // on a doc fragment | |
3150 physicalItems[i] = inst.root.querySelector('*'); | 1962 physicalItems[i] = inst.root.querySelector('*'); |
3151 Polymer.dom(this).appendChild(inst.root); | 1963 Polymer.dom(this).appendChild(inst.root); |
3152 } | 1964 } |
3153 return physicalItems; | 1965 return physicalItems; |
3154 }, | 1966 }, |
3155 | 1967 |
3156 /** | |
3157 * Increases the pool of physical items only if needed. | |
3158 * | |
3159 * @return {boolean} True if the pool was increased. | |
3160 */ | |
3161 _increasePoolIfNeeded: function() { | 1968 _increasePoolIfNeeded: function() { |
3162 // Base case 1: the list has no height. | |
3163 if (this._viewportHeight === 0) { | 1969 if (this._viewportHeight === 0) { |
3164 return false; | 1970 return false; |
3165 } | 1971 } |
3166 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
3167 // with physical items, don't increase the pool. | |
3168 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 1972 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
3169 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 1973 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
3170 return false; | 1974 return false; |
3171 } | 1975 } |
3172 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
3173 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 1976 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
3174 | 1977 |
3175 if (currentPage === 0) { | 1978 if (currentPage === 0) { |
3176 // fill the first page | |
3177 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | 1979 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); |
3178 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 1980 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
3179 // paint the page and defer the next increase | |
3180 // wait 16ms which is rough enough to get paint cycle. | |
3181 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 1981 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
3182 } else { | 1982 } else { |
3183 // fill the rest of the pages | |
3184 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 1983 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
3185 } | 1984 } |
3186 | 1985 |
3187 this._lastPage = currentPage; | 1986 this._lastPage = currentPage; |
3188 | 1987 |
3189 return true; | 1988 return true; |
3190 }, | 1989 }, |
3191 | 1990 |
3192 /** | |
3193 * Increases the pool size. | |
3194 */ | |
3195 _increasePool: function(missingItems) { | 1991 _increasePool: function(missingItems) { |
3196 var nextPhysicalCount = Math.min( | 1992 var nextPhysicalCount = Math.min( |
3197 this._physicalCount + missingItems, | 1993 this._physicalCount + missingItems, |
3198 this._virtualCount - this._virtualStart, | 1994 this._virtualCount - this._virtualStart, |
3199 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | 1995 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) |
3200 ); | 1996 ); |
3201 var prevPhysicalCount = this._physicalCount; | 1997 var prevPhysicalCount = this._physicalCount; |
3202 var delta = nextPhysicalCount - prevPhysicalCount; | 1998 var delta = nextPhysicalCount - prevPhysicalCount; |
3203 | 1999 |
3204 if (delta <= 0) { | 2000 if (delta <= 0) { |
3205 return; | 2001 return; |
3206 } | 2002 } |
3207 | 2003 |
3208 [].push.apply(this._physicalItems, this._createPool(delta)); | 2004 [].push.apply(this._physicalItems, this._createPool(delta)); |
3209 [].push.apply(this._physicalSizes, new Array(delta)); | 2005 [].push.apply(this._physicalSizes, new Array(delta)); |
3210 | 2006 |
3211 this._physicalCount = prevPhysicalCount + delta; | 2007 this._physicalCount = prevPhysicalCount + delta; |
3212 | 2008 |
3213 // update the physical start if we need to preserve the model of the focus
ed item. | |
3214 // In this situation, the focused item is currently rendered and its model
would | |
3215 // have changed after increasing the pool if the physical start remained u
nchanged. | |
3216 if (this._physicalStart > this._physicalEnd && | 2009 if (this._physicalStart > this._physicalEnd && |
3217 this._isIndexRendered(this._focusedIndex) && | 2010 this._isIndexRendered(this._focusedIndex) && |
3218 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | 2011 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { |
3219 this._physicalStart = this._physicalStart + delta; | 2012 this._physicalStart = this._physicalStart + delta; |
3220 } | 2013 } |
3221 this._update(); | 2014 this._update(); |
3222 }, | 2015 }, |
3223 | 2016 |
3224 /** | |
3225 * Render a new list of items. This method does exactly the same as `update`
, | |
3226 * but it also ensures that only one `update` cycle is created. | |
3227 */ | |
3228 _render: function() { | 2017 _render: function() { |
3229 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 2018 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
3230 | 2019 |
3231 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 2020 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
3232 this._lastPage = 0; | 2021 this._lastPage = 0; |
3233 this._update(); | 2022 this._update(); |
3234 this._itemsRendered = true; | 2023 this._itemsRendered = true; |
3235 } | 2024 } |
3236 }, | 2025 }, |
3237 | 2026 |
3238 /** | |
3239 * Templetizes the user template. | |
3240 */ | |
3241 _ensureTemplatized: function() { | 2027 _ensureTemplatized: function() { |
3242 if (!this.ctor) { | 2028 if (!this.ctor) { |
3243 // Template instance props that should be excluded from forwarding | |
3244 var props = {}; | 2029 var props = {}; |
3245 props.__key__ = true; | 2030 props.__key__ = true; |
3246 props[this.as] = true; | 2031 props[this.as] = true; |
3247 props[this.indexAs] = true; | 2032 props[this.indexAs] = true; |
3248 props[this.selectedAs] = true; | 2033 props[this.selectedAs] = true; |
3249 props.tabIndex = true; | 2034 props.tabIndex = true; |
3250 | 2035 |
3251 this._instanceProps = props; | 2036 this._instanceProps = props; |
3252 this._userTemplate = Polymer.dom(this).querySelector('template'); | 2037 this._userTemplate = Polymer.dom(this).querySelector('template'); |
3253 | 2038 |
3254 if (this._userTemplate) { | 2039 if (this._userTemplate) { |
3255 this.templatize(this._userTemplate); | 2040 this.templatize(this._userTemplate); |
3256 } else { | 2041 } else { |
3257 console.warn('iron-list requires a template to be provided in light-do
m'); | 2042 console.warn('iron-list requires a template to be provided in light-do
m'); |
3258 } | 2043 } |
3259 } | 2044 } |
3260 }, | 2045 }, |
3261 | 2046 |
3262 /** | |
3263 * Implements extension point from Templatizer mixin. | |
3264 */ | |
3265 _getStampedChildren: function() { | 2047 _getStampedChildren: function() { |
3266 return this._physicalItems; | 2048 return this._physicalItems; |
3267 }, | 2049 }, |
3268 | 2050 |
3269 /** | |
3270 * Implements extension point from Templatizer | |
3271 * Called as a side effect of a template instance path change, responsible | |
3272 * for notifying items.<key-for-instance>.<path> change up to host. | |
3273 */ | |
3274 _forwardInstancePath: function(inst, path, value) { | 2051 _forwardInstancePath: function(inst, path, value) { |
3275 if (path.indexOf(this.as + '.') === 0) { | 2052 if (path.indexOf(this.as + '.') === 0) { |
3276 this.notifyPath('items.' + inst.__key__ + '.' + | 2053 this.notifyPath('items.' + inst.__key__ + '.' + |
3277 path.slice(this.as.length + 1), value); | 2054 path.slice(this.as.length + 1), value); |
3278 } | 2055 } |
3279 }, | 2056 }, |
3280 | 2057 |
3281 /** | |
3282 * Implements extension point from Templatizer mixin | |
3283 * Called as side-effect of a host property change, responsible for | |
3284 * notifying parent path change on each row. | |
3285 */ | |
3286 _forwardParentProp: function(prop, value) { | 2058 _forwardParentProp: function(prop, value) { |
3287 if (this._physicalItems) { | 2059 if (this._physicalItems) { |
3288 this._physicalItems.forEach(function(item) { | 2060 this._physicalItems.forEach(function(item) { |
3289 item._templateInstance[prop] = value; | 2061 item._templateInstance[prop] = value; |
3290 }, this); | 2062 }, this); |
3291 } | 2063 } |
3292 }, | 2064 }, |
3293 | 2065 |
3294 /** | |
3295 * Implements extension point from Templatizer | |
3296 * Called as side-effect of a host path change, responsible for | |
3297 * notifying parent.<path> path change on each row. | |
3298 */ | |
3299 _forwardParentPath: function(path, value) { | 2066 _forwardParentPath: function(path, value) { |
3300 if (this._physicalItems) { | 2067 if (this._physicalItems) { |
3301 this._physicalItems.forEach(function(item) { | 2068 this._physicalItems.forEach(function(item) { |
3302 item._templateInstance.notifyPath(path, value, true); | 2069 item._templateInstance.notifyPath(path, value, true); |
3303 }, this); | 2070 }, this); |
3304 } | 2071 } |
3305 }, | 2072 }, |
3306 | 2073 |
3307 /** | |
3308 * Called as a side effect of a host items.<key>.<path> path change, | |
3309 * responsible for notifying item.<path> changes. | |
3310 */ | |
3311 _forwardItemPath: function(path, value) { | 2074 _forwardItemPath: function(path, value) { |
3312 if (!this._physicalIndexForKey) { | 2075 if (!this._physicalIndexForKey) { |
3313 return; | 2076 return; |
3314 } | 2077 } |
3315 var dot = path.indexOf('.'); | 2078 var dot = path.indexOf('.'); |
3316 var key = path.substring(0, dot < 0 ? path.length : dot); | 2079 var key = path.substring(0, dot < 0 ? path.length : dot); |
3317 var idx = this._physicalIndexForKey[key]; | 2080 var idx = this._physicalIndexForKey[key]; |
3318 var offscreenItem = this._offscreenFocusedItem; | 2081 var offscreenItem = this._offscreenFocusedItem; |
3319 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 2082 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? |
3320 offscreenItem : this._physicalItems[idx]; | 2083 offscreenItem : this._physicalItems[idx]; |
3321 | 2084 |
3322 if (!el || el._templateInstance.__key__ !== key) { | 2085 if (!el || el._templateInstance.__key__ !== key) { |
3323 return; | 2086 return; |
3324 } | 2087 } |
3325 if (dot >= 0) { | 2088 if (dot >= 0) { |
3326 path = this.as + '.' + path.substring(dot+1); | 2089 path = this.as + '.' + path.substring(dot+1); |
3327 el._templateInstance.notifyPath(path, value, true); | 2090 el._templateInstance.notifyPath(path, value, true); |
3328 } else { | 2091 } else { |
3329 // Update selection if needed | |
3330 var currentItem = el._templateInstance[this.as]; | 2092 var currentItem = el._templateInstance[this.as]; |
3331 if (Array.isArray(this.selectedItems)) { | 2093 if (Array.isArray(this.selectedItems)) { |
3332 for (var i = 0; i < this.selectedItems.length; i++) { | 2094 for (var i = 0; i < this.selectedItems.length; i++) { |
3333 if (this.selectedItems[i] === currentItem) { | 2095 if (this.selectedItems[i] === currentItem) { |
3334 this.set('selectedItems.' + i, value); | 2096 this.set('selectedItems.' + i, value); |
3335 break; | 2097 break; |
3336 } | 2098 } |
3337 } | 2099 } |
3338 } else if (this.selectedItem === currentItem) { | 2100 } else if (this.selectedItem === currentItem) { |
3339 this.set('selectedItem', value); | 2101 this.set('selectedItem', value); |
3340 } | 2102 } |
3341 el._templateInstance[this.as] = value; | 2103 el._templateInstance[this.as] = value; |
3342 } | 2104 } |
3343 }, | 2105 }, |
3344 | 2106 |
3345 /** | |
3346 * Called when the items have changed. That is, ressignments | |
3347 * to `items`, splices or updates to a single item. | |
3348 */ | |
3349 _itemsChanged: function(change) { | 2107 _itemsChanged: function(change) { |
3350 if (change.path === 'items') { | 2108 if (change.path === 'items') { |
3351 // reset items | |
3352 this._virtualStart = 0; | 2109 this._virtualStart = 0; |
3353 this._physicalTop = 0; | 2110 this._physicalTop = 0; |
3354 this._virtualCount = this.items ? this.items.length : 0; | 2111 this._virtualCount = this.items ? this.items.length : 0; |
3355 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 2112 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
3356 this._physicalIndexForKey = {}; | 2113 this._physicalIndexForKey = {}; |
3357 this._firstVisibleIndexVal = null; | 2114 this._firstVisibleIndexVal = null; |
3358 this._lastVisibleIndexVal = null; | 2115 this._lastVisibleIndexVal = null; |
3359 | 2116 |
3360 this._resetScrollPosition(0); | 2117 this._resetScrollPosition(0); |
3361 this._removeFocusedItem(); | 2118 this._removeFocusedItem(); |
3362 // create the initial physical items | |
3363 if (!this._physicalItems) { | 2119 if (!this._physicalItems) { |
3364 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 2120 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
3365 this._physicalItems = this._createPool(this._physicalCount); | 2121 this._physicalItems = this._createPool(this._physicalCount); |
3366 this._physicalSizes = new Array(this._physicalCount); | 2122 this._physicalSizes = new Array(this._physicalCount); |
3367 } | 2123 } |
3368 | 2124 |
3369 this._physicalStart = 0; | 2125 this._physicalStart = 0; |
3370 | 2126 |
3371 } else if (change.path === 'items.splices') { | 2127 } else if (change.path === 'items.splices') { |
3372 | 2128 |
3373 this._adjustVirtualIndex(change.value.indexSplices); | 2129 this._adjustVirtualIndex(change.value.indexSplices); |
3374 this._virtualCount = this.items ? this.items.length : 0; | 2130 this._virtualCount = this.items ? this.items.length : 0; |
3375 | 2131 |
3376 } else { | 2132 } else { |
3377 // update a single item | |
3378 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 2133 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
3379 return; | 2134 return; |
3380 } | 2135 } |
3381 | 2136 |
3382 this._itemsRendered = false; | 2137 this._itemsRendered = false; |
3383 this._debounceTemplate(this._render); | 2138 this._debounceTemplate(this._render); |
3384 }, | 2139 }, |
3385 | 2140 |
3386 /** | |
3387 * @param {!Array<!PolymerSplice>} splices | |
3388 */ | |
3389 _adjustVirtualIndex: function(splices) { | 2141 _adjustVirtualIndex: function(splices) { |
3390 splices.forEach(function(splice) { | 2142 splices.forEach(function(splice) { |
3391 // deselect removed items | |
3392 splice.removed.forEach(this._removeItem, this); | 2143 splice.removed.forEach(this._removeItem, this); |
3393 // We only need to care about changes happening above the current positi
on | |
3394 if (splice.index < this._virtualStart) { | 2144 if (splice.index < this._virtualStart) { |
3395 var delta = Math.max( | 2145 var delta = Math.max( |
3396 splice.addedCount - splice.removed.length, | 2146 splice.addedCount - splice.removed.length, |
3397 splice.index - this._virtualStart); | 2147 splice.index - this._virtualStart); |
3398 | 2148 |
3399 this._virtualStart = this._virtualStart + delta; | 2149 this._virtualStart = this._virtualStart + delta; |
3400 | 2150 |
3401 if (this._focusedIndex >= 0) { | 2151 if (this._focusedIndex >= 0) { |
3402 this._focusedIndex = this._focusedIndex + delta; | 2152 this._focusedIndex = this._focusedIndex + delta; |
3403 } | 2153 } |
3404 } | 2154 } |
3405 }, this); | 2155 }, this); |
3406 }, | 2156 }, |
3407 | 2157 |
3408 _removeItem: function(item) { | 2158 _removeItem: function(item) { |
3409 this.$.selector.deselect(item); | 2159 this.$.selector.deselect(item); |
3410 // remove the current focused item | |
3411 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 2160 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
3412 this._removeFocusedItem(); | 2161 this._removeFocusedItem(); |
3413 } | 2162 } |
3414 }, | 2163 }, |
3415 | 2164 |
3416 /** | |
3417 * Executes a provided function per every physical index in `itemSet` | |
3418 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
3419 * | |
3420 * @param {!function(number, number)} fn | |
3421 * @param {!Array<number>=} itemSet | |
3422 */ | |
3423 _iterateItems: function(fn, itemSet) { | 2165 _iterateItems: function(fn, itemSet) { |
3424 var pidx, vidx, rtn, i; | 2166 var pidx, vidx, rtn, i; |
3425 | 2167 |
3426 if (arguments.length === 2 && itemSet) { | 2168 if (arguments.length === 2 && itemSet) { |
3427 for (i = 0; i < itemSet.length; i++) { | 2169 for (i = 0; i < itemSet.length; i++) { |
3428 pidx = itemSet[i]; | 2170 pidx = itemSet[i]; |
3429 vidx = this._computeVidx(pidx); | 2171 vidx = this._computeVidx(pidx); |
3430 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 2172 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3431 return rtn; | 2173 return rtn; |
3432 } | 2174 } |
3433 } | 2175 } |
3434 } else { | 2176 } else { |
3435 pidx = this._physicalStart; | 2177 pidx = this._physicalStart; |
3436 vidx = this._virtualStart; | 2178 vidx = this._virtualStart; |
3437 | 2179 |
3438 for (; pidx < this._physicalCount; pidx++, vidx++) { | 2180 for (; pidx < this._physicalCount; pidx++, vidx++) { |
3439 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 2181 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3440 return rtn; | 2182 return rtn; |
3441 } | 2183 } |
3442 } | 2184 } |
3443 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 2185 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
3444 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 2186 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
3445 return rtn; | 2187 return rtn; |
3446 } | 2188 } |
3447 } | 2189 } |
3448 } | 2190 } |
3449 }, | 2191 }, |
3450 | 2192 |
3451 /** | |
3452 * Returns the virtual index for a given physical index | |
3453 * | |
3454 * @param {number} pidx Physical index | |
3455 * @return {number} | |
3456 */ | |
3457 _computeVidx: function(pidx) { | 2193 _computeVidx: function(pidx) { |
3458 if (pidx >= this._physicalStart) { | 2194 if (pidx >= this._physicalStart) { |
3459 return this._virtualStart + (pidx - this._physicalStart); | 2195 return this._virtualStart + (pidx - this._physicalStart); |
3460 } | 2196 } |
3461 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 2197 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
3462 }, | 2198 }, |
3463 | 2199 |
3464 /** | |
3465 * Assigns the data models to a given set of items. | |
3466 * @param {!Array<number>=} itemSet | |
3467 */ | |
3468 _assignModels: function(itemSet) { | 2200 _assignModels: function(itemSet) { |
3469 this._iterateItems(function(pidx, vidx) { | 2201 this._iterateItems(function(pidx, vidx) { |
3470 var el = this._physicalItems[pidx]; | 2202 var el = this._physicalItems[pidx]; |
3471 var inst = el._templateInstance; | 2203 var inst = el._templateInstance; |
3472 var item = this.items && this.items[vidx]; | 2204 var item = this.items && this.items[vidx]; |
3473 | 2205 |
3474 if (item != null) { | 2206 if (item != null) { |
3475 inst[this.as] = item; | 2207 inst[this.as] = item; |
3476 inst.__key__ = this._collection.getKey(item); | 2208 inst.__key__ = this._collection.getKey(item); |
3477 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 2209 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); |
3478 inst[this.indexAs] = vidx; | 2210 inst[this.indexAs] = vidx; |
3479 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 2211 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
3480 this._physicalIndexForKey[inst.__key__] = pidx; | 2212 this._physicalIndexForKey[inst.__key__] = pidx; |
3481 el.removeAttribute('hidden'); | 2213 el.removeAttribute('hidden'); |
3482 } else { | 2214 } else { |
3483 inst.__key__ = null; | 2215 inst.__key__ = null; |
3484 el.setAttribute('hidden', ''); | 2216 el.setAttribute('hidden', ''); |
3485 } | 2217 } |
3486 }, itemSet); | 2218 }, itemSet); |
3487 }, | 2219 }, |
3488 | 2220 |
3489 /** | |
3490 * Updates the height for a given set of items. | |
3491 * | |
3492 * @param {!Array<number>=} itemSet | |
3493 */ | |
3494 _updateMetrics: function(itemSet) { | 2221 _updateMetrics: function(itemSet) { |
3495 // Make sure we distributed all the physical items | |
3496 // so we can measure them | |
3497 Polymer.dom.flush(); | 2222 Polymer.dom.flush(); |
3498 | 2223 |
3499 var newPhysicalSize = 0; | 2224 var newPhysicalSize = 0; |
3500 var oldPhysicalSize = 0; | 2225 var oldPhysicalSize = 0; |
3501 var prevAvgCount = this._physicalAverageCount; | 2226 var prevAvgCount = this._physicalAverageCount; |
3502 var prevPhysicalAvg = this._physicalAverage; | 2227 var prevPhysicalAvg = this._physicalAverage; |
3503 | 2228 |
3504 this._iterateItems(function(pidx, vidx) { | 2229 this._iterateItems(function(pidx, vidx) { |
3505 | 2230 |
3506 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 2231 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
3507 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 2232 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
3508 newPhysicalSize += this._physicalSizes[pidx]; | 2233 newPhysicalSize += this._physicalSizes[pidx]; |
3509 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 2234 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
3510 | 2235 |
3511 }, itemSet); | 2236 }, itemSet); |
3512 | 2237 |
3513 this._viewportHeight = this._scrollTargetHeight; | 2238 this._viewportHeight = this._scrollTargetHeight; |
3514 if (this.grid) { | 2239 if (this.grid) { |
3515 this._updateGridMetrics(); | 2240 this._updateGridMetrics(); |
3516 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 2241 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
3517 } else { | 2242 } else { |
3518 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 2243 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
3519 } | 2244 } |
3520 | 2245 |
3521 // update the average if we measured something | |
3522 if (this._physicalAverageCount !== prevAvgCount) { | 2246 if (this._physicalAverageCount !== prevAvgCount) { |
3523 this._physicalAverage = Math.round( | 2247 this._physicalAverage = Math.round( |
3524 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | 2248 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
3525 this._physicalAverageCount); | 2249 this._physicalAverageCount); |
3526 } | 2250 } |
3527 }, | 2251 }, |
3528 | 2252 |
3529 _updateGridMetrics: function() { | 2253 _updateGridMetrics: function() { |
3530 this._viewportWidth = this.$.items.offsetWidth; | 2254 this._viewportWidth = this.$.items.offsetWidth; |
3531 // Set item width to the value of the _physicalItems offsetWidth | |
3532 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 2255 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
3533 // Set row height to the value of the _physicalItems offsetHeight | |
3534 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 2256 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
3535 // If in grid mode compute how many items with exist in each row | |
3536 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 2257 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
3537 }, | 2258 }, |
3538 | 2259 |
3539 /** | |
3540 * Updates the position of the physical items. | |
3541 */ | |
3542 _positionItems: function() { | 2260 _positionItems: function() { |
3543 this._adjustScrollPosition(); | 2261 this._adjustScrollPosition(); |
3544 | 2262 |
3545 var y = this._physicalTop; | 2263 var y = this._physicalTop; |
3546 | 2264 |
3547 if (this.grid) { | 2265 if (this.grid) { |
3548 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 2266 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
3549 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 2267 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
3550 | 2268 |
3551 this._iterateItems(function(pidx, vidx) { | 2269 this._iterateItems(function(pidx, vidx) { |
(...skipping 21 matching lines...) Expand all Loading... |
3573 _getPhysicalSizeIncrement: function(pidx) { | 2291 _getPhysicalSizeIncrement: function(pidx) { |
3574 if (!this.grid) { | 2292 if (!this.grid) { |
3575 return this._physicalSizes[pidx]; | 2293 return this._physicalSizes[pidx]; |
3576 } | 2294 } |
3577 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 2295 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
3578 return 0; | 2296 return 0; |
3579 } | 2297 } |
3580 return this._rowHeight; | 2298 return this._rowHeight; |
3581 }, | 2299 }, |
3582 | 2300 |
3583 /** | |
3584 * Returns, based on the current index, | |
3585 * whether or not the next index will need | |
3586 * to be rendered on a new row. | |
3587 * | |
3588 * @param {number} vidx Virtual index | |
3589 * @return {boolean} | |
3590 */ | |
3591 _shouldRenderNextRow: function(vidx) { | 2301 _shouldRenderNextRow: function(vidx) { |
3592 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 2302 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
3593 }, | 2303 }, |
3594 | 2304 |
3595 /** | |
3596 * Adjusts the scroll position when it was overestimated. | |
3597 */ | |
3598 _adjustScrollPosition: function() { | 2305 _adjustScrollPosition: function() { |
3599 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 2306 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
3600 Math.min(this._scrollPosition + this._physicalTop, 0); | 2307 Math.min(this._scrollPosition + this._physicalTop, 0); |
3601 | 2308 |
3602 if (deltaHeight) { | 2309 if (deltaHeight) { |
3603 this._physicalTop = this._physicalTop - deltaHeight; | 2310 this._physicalTop = this._physicalTop - deltaHeight; |
3604 // juking scroll position during interial scrolling on iOS is no bueno | |
3605 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 2311 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
3606 this._resetScrollPosition(this._scrollTop - deltaHeight); | 2312 this._resetScrollPosition(this._scrollTop - deltaHeight); |
3607 } | 2313 } |
3608 } | 2314 } |
3609 }, | 2315 }, |
3610 | 2316 |
3611 /** | |
3612 * Sets the position of the scroll. | |
3613 */ | |
3614 _resetScrollPosition: function(pos) { | 2317 _resetScrollPosition: function(pos) { |
3615 if (this.scrollTarget) { | 2318 if (this.scrollTarget) { |
3616 this._scrollTop = pos; | 2319 this._scrollTop = pos; |
3617 this._scrollPosition = this._scrollTop; | 2320 this._scrollPosition = this._scrollTop; |
3618 } | 2321 } |
3619 }, | 2322 }, |
3620 | 2323 |
3621 /** | |
3622 * Sets the scroll height, that's the height of the content, | |
3623 * | |
3624 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
3625 */ | |
3626 _updateScrollerSize: function(forceUpdate) { | 2324 _updateScrollerSize: function(forceUpdate) { |
3627 if (this.grid) { | 2325 if (this.grid) { |
3628 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 2326 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
3629 } else { | 2327 } else { |
3630 this._estScrollHeight = (this._physicalBottom + | 2328 this._estScrollHeight = (this._physicalBottom + |
3631 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | 2329 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); |
3632 } | 2330 } |
3633 | 2331 |
3634 forceUpdate = forceUpdate || this._scrollHeight === 0; | 2332 forceUpdate = forceUpdate || this._scrollHeight === 0; |
3635 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 2333 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
3636 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 2334 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
3637 | 2335 |
3638 // amortize height adjustment, so it won't trigger repaints very often | |
3639 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 2336 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
3640 this.$.items.style.height = this._estScrollHeight + 'px'; | 2337 this.$.items.style.height = this._estScrollHeight + 'px'; |
3641 this._scrollHeight = this._estScrollHeight; | 2338 this._scrollHeight = this._estScrollHeight; |
3642 } | 2339 } |
3643 }, | 2340 }, |
3644 | 2341 |
3645 /** | |
3646 * Scroll to a specific item in the virtual list regardless | |
3647 * of the physical items in the DOM tree. | |
3648 * | |
3649 * @method scrollToItem | |
3650 * @param {(Object)} item The item to be scrolled to | |
3651 */ | |
3652 scrollToItem: function(item){ | 2342 scrollToItem: function(item){ |
3653 return this.scrollToIndex(this.items.indexOf(item)); | 2343 return this.scrollToIndex(this.items.indexOf(item)); |
3654 }, | 2344 }, |
3655 | 2345 |
3656 /** | |
3657 * Scroll to a specific index in the virtual list regardless | |
3658 * of the physical items in the DOM tree. | |
3659 * | |
3660 * @method scrollToIndex | |
3661 * @param {number} idx The index of the item | |
3662 */ | |
3663 scrollToIndex: function(idx) { | 2346 scrollToIndex: function(idx) { |
3664 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 2347 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
3665 return; | 2348 return; |
3666 } | 2349 } |
3667 | 2350 |
3668 Polymer.dom.flush(); | 2351 Polymer.dom.flush(); |
3669 | 2352 |
3670 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | 2353 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
3671 // update the virtual start only when needed | |
3672 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 2354 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
3673 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 2355 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); |
3674 } | 2356 } |
3675 // manage focus | |
3676 this._manageFocus(); | 2357 this._manageFocus(); |
3677 // assign new models | |
3678 this._assignModels(); | 2358 this._assignModels(); |
3679 // measure the new sizes | |
3680 this._updateMetrics(); | 2359 this._updateMetrics(); |
3681 | 2360 |
3682 // estimate new physical offset | |
3683 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | 2361 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
3684 this._physicalTop = estPhysicalTop; | 2362 this._physicalTop = estPhysicalTop; |
3685 | 2363 |
3686 var currentTopItem = this._physicalStart; | 2364 var currentTopItem = this._physicalStart; |
3687 var currentVirtualItem = this._virtualStart; | 2365 var currentVirtualItem = this._virtualStart; |
3688 var targetOffsetTop = 0; | 2366 var targetOffsetTop = 0; |
3689 var hiddenContentSize = this._hiddenContentSize; | 2367 var hiddenContentSize = this._hiddenContentSize; |
3690 | 2368 |
3691 // scroll to the item as much as we can | |
3692 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 2369 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
3693 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 2370 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
3694 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 2371 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
3695 currentVirtualItem++; | 2372 currentVirtualItem++; |
3696 } | 2373 } |
3697 // update the scroller size | |
3698 this._updateScrollerSize(true); | 2374 this._updateScrollerSize(true); |
3699 // update the position of the items | |
3700 this._positionItems(); | 2375 this._positionItems(); |
3701 // set the new scroll position | |
3702 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 2376 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
3703 // increase the pool of physical items if needed | |
3704 this._increasePoolIfNeeded(); | 2377 this._increasePoolIfNeeded(); |
3705 // clear cached visible index | |
3706 this._firstVisibleIndexVal = null; | 2378 this._firstVisibleIndexVal = null; |
3707 this._lastVisibleIndexVal = null; | 2379 this._lastVisibleIndexVal = null; |
3708 }, | 2380 }, |
3709 | 2381 |
3710 /** | |
3711 * Reset the physical average and the average count. | |
3712 */ | |
3713 _resetAverage: function() { | 2382 _resetAverage: function() { |
3714 this._physicalAverage = 0; | 2383 this._physicalAverage = 0; |
3715 this._physicalAverageCount = 0; | 2384 this._physicalAverageCount = 0; |
3716 }, | 2385 }, |
3717 | 2386 |
3718 /** | |
3719 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
3720 * when the element is resized. | |
3721 */ | |
3722 _resizeHandler: function() { | 2387 _resizeHandler: function() { |
3723 // iOS fires the resize event when the address bar slides up | |
3724 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 2388 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
3725 return; | 2389 return; |
3726 } | 2390 } |
3727 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
3728 // changing the scroll position from a resize handler would result in | |
3729 // the scroll position being reset. Waiting 1ms fixes the issue. | |
3730 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 2391 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
3731 this.updateViewportBoundaries(); | 2392 this.updateViewportBoundaries(); |
3732 this._render(); | 2393 this._render(); |
3733 | 2394 |
3734 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 2395 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
3735 this._resetAverage(); | 2396 this._resetAverage(); |
3736 this.scrollToIndex(this.firstVisibleIndex); | 2397 this.scrollToIndex(this.firstVisibleIndex); |
3737 } | 2398 } |
3738 }.bind(this), 1)); | 2399 }.bind(this), 1)); |
3739 }, | 2400 }, |
3740 | 2401 |
3741 _getModelFromItem: function(item) { | 2402 _getModelFromItem: function(item) { |
3742 var key = this._collection.getKey(item); | 2403 var key = this._collection.getKey(item); |
3743 var pidx = this._physicalIndexForKey[key]; | 2404 var pidx = this._physicalIndexForKey[key]; |
3744 | 2405 |
3745 if (pidx != null) { | 2406 if (pidx != null) { |
3746 return this._physicalItems[pidx]._templateInstance; | 2407 return this._physicalItems[pidx]._templateInstance; |
3747 } | 2408 } |
3748 return null; | 2409 return null; |
3749 }, | 2410 }, |
3750 | 2411 |
3751 /** | |
3752 * Gets a valid item instance from its index or the object value. | |
3753 * | |
3754 * @param {(Object|number)} item The item object or its index | |
3755 */ | |
3756 _getNormalizedItem: function(item) { | 2412 _getNormalizedItem: function(item) { |
3757 if (this._collection.getKey(item) === undefined) { | 2413 if (this._collection.getKey(item) === undefined) { |
3758 if (typeof item === 'number') { | 2414 if (typeof item === 'number') { |
3759 item = this.items[item]; | 2415 item = this.items[item]; |
3760 if (!item) { | 2416 if (!item) { |
3761 throw new RangeError('<item> not found'); | 2417 throw new RangeError('<item> not found'); |
3762 } | 2418 } |
3763 return item; | 2419 return item; |
3764 } | 2420 } |
3765 throw new TypeError('<item> should be a valid item'); | 2421 throw new TypeError('<item> should be a valid item'); |
3766 } | 2422 } |
3767 return item; | 2423 return item; |
3768 }, | 2424 }, |
3769 | 2425 |
3770 /** | |
3771 * Select the list item at the given index. | |
3772 * | |
3773 * @method selectItem | |
3774 * @param {(Object|number)} item The item object or its index | |
3775 */ | |
3776 selectItem: function(item) { | 2426 selectItem: function(item) { |
3777 item = this._getNormalizedItem(item); | 2427 item = this._getNormalizedItem(item); |
3778 var model = this._getModelFromItem(item); | 2428 var model = this._getModelFromItem(item); |
3779 | 2429 |
3780 if (!this.multiSelection && this.selectedItem) { | 2430 if (!this.multiSelection && this.selectedItem) { |
3781 this.deselectItem(this.selectedItem); | 2431 this.deselectItem(this.selectedItem); |
3782 } | 2432 } |
3783 if (model) { | 2433 if (model) { |
3784 model[this.selectedAs] = true; | 2434 model[this.selectedAs] = true; |
3785 } | 2435 } |
3786 this.$.selector.select(item); | 2436 this.$.selector.select(item); |
3787 this.updateSizeForItem(item); | 2437 this.updateSizeForItem(item); |
3788 }, | 2438 }, |
3789 | 2439 |
3790 /** | |
3791 * Deselects the given item list if it is already selected. | |
3792 * | |
3793 | |
3794 * @method deselect | |
3795 * @param {(Object|number)} item The item object or its index | |
3796 */ | |
3797 deselectItem: function(item) { | 2440 deselectItem: function(item) { |
3798 item = this._getNormalizedItem(item); | 2441 item = this._getNormalizedItem(item); |
3799 var model = this._getModelFromItem(item); | 2442 var model = this._getModelFromItem(item); |
3800 | 2443 |
3801 if (model) { | 2444 if (model) { |
3802 model[this.selectedAs] = false; | 2445 model[this.selectedAs] = false; |
3803 } | 2446 } |
3804 this.$.selector.deselect(item); | 2447 this.$.selector.deselect(item); |
3805 this.updateSizeForItem(item); | 2448 this.updateSizeForItem(item); |
3806 }, | 2449 }, |
3807 | 2450 |
3808 /** | |
3809 * Select or deselect a given item depending on whether the item | |
3810 * has already been selected. | |
3811 * | |
3812 * @method toggleSelectionForItem | |
3813 * @param {(Object|number)} item The item object or its index | |
3814 */ | |
3815 toggleSelectionForItem: function(item) { | 2451 toggleSelectionForItem: function(item) { |
3816 item = this._getNormalizedItem(item); | 2452 item = this._getNormalizedItem(item); |
3817 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 2453 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { |
3818 this.deselectItem(item); | 2454 this.deselectItem(item); |
3819 } else { | 2455 } else { |
3820 this.selectItem(item); | 2456 this.selectItem(item); |
3821 } | 2457 } |
3822 }, | 2458 }, |
3823 | 2459 |
3824 /** | |
3825 * Clears the current selection state of the list. | |
3826 * | |
3827 * @method clearSelection | |
3828 */ | |
3829 clearSelection: function() { | 2460 clearSelection: function() { |
3830 function unselect(item) { | 2461 function unselect(item) { |
3831 var model = this._getModelFromItem(item); | 2462 var model = this._getModelFromItem(item); |
3832 if (model) { | 2463 if (model) { |
3833 model[this.selectedAs] = false; | 2464 model[this.selectedAs] = false; |
3834 } | 2465 } |
3835 } | 2466 } |
3836 | 2467 |
3837 if (Array.isArray(this.selectedItems)) { | 2468 if (Array.isArray(this.selectedItems)) { |
3838 this.selectedItems.forEach(unselect, this); | 2469 this.selectedItems.forEach(unselect, this); |
3839 } else if (this.selectedItem) { | 2470 } else if (this.selectedItem) { |
3840 unselect.call(this, this.selectedItem); | 2471 unselect.call(this, this.selectedItem); |
3841 } | 2472 } |
3842 | 2473 |
3843 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | 2474 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
3844 }, | 2475 }, |
3845 | 2476 |
3846 /** | |
3847 * Add an event listener to `tap` if `selectionEnabled` is true, | |
3848 * it will remove the listener otherwise. | |
3849 */ | |
3850 _selectionEnabledChanged: function(selectionEnabled) { | 2477 _selectionEnabledChanged: function(selectionEnabled) { |
3851 var handler = selectionEnabled ? this.listen : this.unlisten; | 2478 var handler = selectionEnabled ? this.listen : this.unlisten; |
3852 handler.call(this, this, 'tap', '_selectionHandler'); | 2479 handler.call(this, this, 'tap', '_selectionHandler'); |
3853 }, | 2480 }, |
3854 | 2481 |
3855 /** | |
3856 * Select an item from an event object. | |
3857 */ | |
3858 _selectionHandler: function(e) { | 2482 _selectionHandler: function(e) { |
3859 var model = this.modelForElement(e.target); | 2483 var model = this.modelForElement(e.target); |
3860 if (!model) { | 2484 if (!model) { |
3861 return; | 2485 return; |
3862 } | 2486 } |
3863 var modelTabIndex, activeElTabIndex; | 2487 var modelTabIndex, activeElTabIndex; |
3864 var target = Polymer.dom(e).path[0]; | 2488 var target = Polymer.dom(e).path[0]; |
3865 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 2489 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
3866 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 2490 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
3867 // Safari does not focus certain form controls via mouse | |
3868 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
3869 if (target.localName === 'input' || | 2491 if (target.localName === 'input' || |
3870 target.localName === 'button' || | 2492 target.localName === 'button' || |
3871 target.localName === 'select') { | 2493 target.localName === 'select') { |
3872 return; | 2494 return; |
3873 } | 2495 } |
3874 // Set a temporary tabindex | |
3875 modelTabIndex = model.tabIndex; | 2496 modelTabIndex = model.tabIndex; |
3876 model.tabIndex = SECRET_TABINDEX; | 2497 model.tabIndex = SECRET_TABINDEX; |
3877 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 2498 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
3878 model.tabIndex = modelTabIndex; | 2499 model.tabIndex = modelTabIndex; |
3879 // Only select the item if the tap wasn't on a focusable child | |
3880 // or the element bound to `tabIndex` | |
3881 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 2500 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
3882 return; | 2501 return; |
3883 } | 2502 } |
3884 this.toggleSelectionForItem(model[this.as]); | 2503 this.toggleSelectionForItem(model[this.as]); |
3885 }, | 2504 }, |
3886 | 2505 |
3887 _multiSelectionChanged: function(multiSelection) { | 2506 _multiSelectionChanged: function(multiSelection) { |
3888 this.clearSelection(); | 2507 this.clearSelection(); |
3889 this.$.selector.multi = multiSelection; | 2508 this.$.selector.multi = multiSelection; |
3890 }, | 2509 }, |
3891 | 2510 |
3892 /** | |
3893 * Updates the size of an item. | |
3894 * | |
3895 * @method updateSizeForItem | |
3896 * @param {(Object|number)} item The item object or its index | |
3897 */ | |
3898 updateSizeForItem: function(item) { | 2511 updateSizeForItem: function(item) { |
3899 item = this._getNormalizedItem(item); | 2512 item = this._getNormalizedItem(item); |
3900 var key = this._collection.getKey(item); | 2513 var key = this._collection.getKey(item); |
3901 var pidx = this._physicalIndexForKey[key]; | 2514 var pidx = this._physicalIndexForKey[key]; |
3902 | 2515 |
3903 if (pidx != null) { | 2516 if (pidx != null) { |
3904 this._updateMetrics([pidx]); | 2517 this._updateMetrics([pidx]); |
3905 this._positionItems(); | 2518 this._positionItems(); |
3906 } | 2519 } |
3907 }, | 2520 }, |
3908 | 2521 |
3909 /** | |
3910 * Creates a temporary backfill item in the rendered pool of physical items | |
3911 * to replace the main focused item. The focused item has tabIndex = 0 | |
3912 * and might be currently focused by the user. | |
3913 * | |
3914 * This dynamic replacement helps to preserve the focus state. | |
3915 */ | |
3916 _manageFocus: function() { | 2522 _manageFocus: function() { |
3917 var fidx = this._focusedIndex; | 2523 var fidx = this._focusedIndex; |
3918 | 2524 |
3919 if (fidx >= 0 && fidx < this._virtualCount) { | 2525 if (fidx >= 0 && fidx < this._virtualCount) { |
3920 // if it's a valid index, check if that index is rendered | |
3921 // in a physical item. | |
3922 if (this._isIndexRendered(fidx)) { | 2526 if (this._isIndexRendered(fidx)) { |
3923 this._restoreFocusedItem(); | 2527 this._restoreFocusedItem(); |
3924 } else { | 2528 } else { |
3925 this._createFocusBackfillItem(); | 2529 this._createFocusBackfillItem(); |
3926 } | 2530 } |
3927 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 2531 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
3928 // otherwise, assign the initial focused index. | |
3929 this._focusedIndex = this._virtualStart; | 2532 this._focusedIndex = this._virtualStart; |
3930 this._focusedItem = this._physicalItems[this._physicalStart]; | 2533 this._focusedItem = this._physicalItems[this._physicalStart]; |
3931 } | 2534 } |
3932 }, | 2535 }, |
3933 | 2536 |
3934 _isIndexRendered: function(idx) { | 2537 _isIndexRendered: function(idx) { |
3935 return idx >= this._virtualStart && idx <= this._virtualEnd; | 2538 return idx >= this._virtualStart && idx <= this._virtualEnd; |
3936 }, | 2539 }, |
3937 | 2540 |
3938 _isIndexVisible: function(idx) { | 2541 _isIndexVisible: function(idx) { |
3939 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 2542 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
3940 }, | 2543 }, |
3941 | 2544 |
3942 _getPhysicalIndex: function(idx) { | 2545 _getPhysicalIndex: function(idx) { |
3943 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 2546 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
3944 }, | 2547 }, |
3945 | 2548 |
3946 _focusPhysicalItem: function(idx) { | 2549 _focusPhysicalItem: function(idx) { |
3947 if (idx < 0 || idx >= this._virtualCount) { | 2550 if (idx < 0 || idx >= this._virtualCount) { |
3948 return; | 2551 return; |
3949 } | 2552 } |
3950 this._restoreFocusedItem(); | 2553 this._restoreFocusedItem(); |
3951 // scroll to index to make sure it's rendered | |
3952 if (!this._isIndexRendered(idx)) { | 2554 if (!this._isIndexRendered(idx)) { |
3953 this.scrollToIndex(idx); | 2555 this.scrollToIndex(idx); |
3954 } | 2556 } |
3955 | 2557 |
3956 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 2558 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
3957 var model = physicalItem._templateInstance; | 2559 var model = physicalItem._templateInstance; |
3958 var focusable; | 2560 var focusable; |
3959 | 2561 |
3960 // set a secret tab index | |
3961 model.tabIndex = SECRET_TABINDEX; | 2562 model.tabIndex = SECRET_TABINDEX; |
3962 // check if focusable element is the physical item | |
3963 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 2563 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
3964 focusable = physicalItem; | 2564 focusable = physicalItem; |
3965 } | 2565 } |
3966 // search for the element which tabindex is bound to the secret tab index | |
3967 if (!focusable) { | 2566 if (!focusable) { |
3968 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 2567 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
3969 } | 2568 } |
3970 // restore the tab index | |
3971 model.tabIndex = 0; | 2569 model.tabIndex = 0; |
3972 // focus the focusable element | |
3973 this._focusedIndex = idx; | 2570 this._focusedIndex = idx; |
3974 focusable && focusable.focus(); | 2571 focusable && focusable.focus(); |
3975 }, | 2572 }, |
3976 | 2573 |
3977 _removeFocusedItem: function() { | 2574 _removeFocusedItem: function() { |
3978 if (this._offscreenFocusedItem) { | 2575 if (this._offscreenFocusedItem) { |
3979 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 2576 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
3980 } | 2577 } |
3981 this._offscreenFocusedItem = null; | 2578 this._offscreenFocusedItem = null; |
3982 this._focusBackfillItem = null; | 2579 this._focusBackfillItem = null; |
3983 this._focusedItem = null; | 2580 this._focusedItem = null; |
3984 this._focusedIndex = -1; | 2581 this._focusedIndex = -1; |
3985 }, | 2582 }, |
3986 | 2583 |
3987 _createFocusBackfillItem: function() { | 2584 _createFocusBackfillItem: function() { |
3988 var pidx, fidx = this._focusedIndex; | 2585 var pidx, fidx = this._focusedIndex; |
3989 if (this._offscreenFocusedItem || fidx < 0) { | 2586 if (this._offscreenFocusedItem || fidx < 0) { |
3990 return; | 2587 return; |
3991 } | 2588 } |
3992 if (!this._focusBackfillItem) { | 2589 if (!this._focusBackfillItem) { |
3993 // create a physical item, so that it backfills the focused item. | |
3994 var stampedTemplate = this.stamp(null); | 2590 var stampedTemplate = this.stamp(null); |
3995 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 2591 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
3996 Polymer.dom(this).appendChild(stampedTemplate.root); | 2592 Polymer.dom(this).appendChild(stampedTemplate.root); |
3997 } | 2593 } |
3998 // get the physical index for the focused index | |
3999 pidx = this._getPhysicalIndex(fidx); | 2594 pidx = this._getPhysicalIndex(fidx); |
4000 | 2595 |
4001 if (pidx != null) { | 2596 if (pidx != null) { |
4002 // set the offcreen focused physical item | |
4003 this._offscreenFocusedItem = this._physicalItems[pidx]; | 2597 this._offscreenFocusedItem = this._physicalItems[pidx]; |
4004 // backfill the focused physical item | |
4005 this._physicalItems[pidx] = this._focusBackfillItem; | 2598 this._physicalItems[pidx] = this._focusBackfillItem; |
4006 // hide the focused physical | |
4007 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 2599 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
4008 } | 2600 } |
4009 }, | 2601 }, |
4010 | 2602 |
4011 _restoreFocusedItem: function() { | 2603 _restoreFocusedItem: function() { |
4012 var pidx, fidx = this._focusedIndex; | 2604 var pidx, fidx = this._focusedIndex; |
4013 | 2605 |
4014 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 2606 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
4015 return; | 2607 return; |
4016 } | 2608 } |
4017 // assign models to the focused index | |
4018 this._assignModels(); | 2609 this._assignModels(); |
4019 // get the new physical index for the focused index | |
4020 pidx = this._getPhysicalIndex(fidx); | 2610 pidx = this._getPhysicalIndex(fidx); |
4021 | 2611 |
4022 if (pidx != null) { | 2612 if (pidx != null) { |
4023 // flip the focus backfill | |
4024 this._focusBackfillItem = this._physicalItems[pidx]; | 2613 this._focusBackfillItem = this._physicalItems[pidx]; |
4025 // restore the focused physical item | |
4026 this._physicalItems[pidx] = this._offscreenFocusedItem; | 2614 this._physicalItems[pidx] = this._offscreenFocusedItem; |
4027 // reset the offscreen focused item | |
4028 this._offscreenFocusedItem = null; | 2615 this._offscreenFocusedItem = null; |
4029 // hide the physical item that backfills | |
4030 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 2616 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
4031 } | 2617 } |
4032 }, | 2618 }, |
4033 | 2619 |
4034 _didFocus: function(e) { | 2620 _didFocus: function(e) { |
4035 var targetModel = this.modelForElement(e.target); | 2621 var targetModel = this.modelForElement(e.target); |
4036 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 2622 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
4037 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 2623 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
4038 var fidx = this._focusedIndex; | 2624 var fidx = this._focusedIndex; |
4039 | 2625 |
4040 if (!targetModel || !focusedModel) { | 2626 if (!targetModel || !focusedModel) { |
4041 return; | 2627 return; |
4042 } | 2628 } |
4043 if (focusedModel === targetModel) { | 2629 if (focusedModel === targetModel) { |
4044 // if the user focused the same item, then bring it into view if it's no
t visible | |
4045 if (!this._isIndexVisible(fidx)) { | 2630 if (!this._isIndexVisible(fidx)) { |
4046 this.scrollToIndex(fidx); | 2631 this.scrollToIndex(fidx); |
4047 } | 2632 } |
4048 } else { | 2633 } else { |
4049 this._restoreFocusedItem(); | 2634 this._restoreFocusedItem(); |
4050 // restore tabIndex for the currently focused item | |
4051 focusedModel.tabIndex = -1; | 2635 focusedModel.tabIndex = -1; |
4052 // set the tabIndex for the next focused item | |
4053 targetModel.tabIndex = 0; | 2636 targetModel.tabIndex = 0; |
4054 fidx = targetModel[this.indexAs]; | 2637 fidx = targetModel[this.indexAs]; |
4055 this._focusedIndex = fidx; | 2638 this._focusedIndex = fidx; |
4056 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 2639 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
4057 | 2640 |
4058 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 2641 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
4059 this._update(); | 2642 this._update(); |
4060 } | 2643 } |
4061 } | 2644 } |
4062 }, | 2645 }, |
4063 | 2646 |
4064 _didMoveUp: function() { | 2647 _didMoveUp: function() { |
4065 this._focusPhysicalItem(this._focusedIndex - 1); | 2648 this._focusPhysicalItem(this._focusedIndex - 1); |
4066 }, | 2649 }, |
4067 | 2650 |
4068 _didMoveDown: function(e) { | 2651 _didMoveDown: function(e) { |
4069 // disable scroll when pressing the down key | |
4070 e.detail.keyboardEvent.preventDefault(); | 2652 e.detail.keyboardEvent.preventDefault(); |
4071 this._focusPhysicalItem(this._focusedIndex + 1); | 2653 this._focusPhysicalItem(this._focusedIndex + 1); |
4072 }, | 2654 }, |
4073 | 2655 |
4074 _didEnter: function(e) { | 2656 _didEnter: function(e) { |
4075 this._focusPhysicalItem(this._focusedIndex); | 2657 this._focusPhysicalItem(this._focusedIndex); |
4076 this._selectionHandler(e.detail.keyboardEvent); | 2658 this._selectionHandler(e.detail.keyboardEvent); |
4077 } | 2659 } |
4078 }); | 2660 }); |
4079 | 2661 |
4080 })(); | 2662 })(); |
4081 // Copyright 2015 The Chromium Authors. All rights reserved. | 2663 // Copyright 2015 The Chromium Authors. All rights reserved. |
4082 // Use of this source code is governed by a BSD-style license that can be | 2664 // Use of this source code is governed by a BSD-style license that can be |
4083 // found in the LICENSE file. | 2665 // found in the LICENSE file. |
4084 | 2666 |
4085 cr.define('downloads', function() { | 2667 cr.define('downloads', function() { |
4086 /** | |
4087 * @param {string} chromeSendName | |
4088 * @return {function(string):void} A chrome.send() callback with curried name. | |
4089 */ | |
4090 function chromeSendWithId(chromeSendName) { | 2668 function chromeSendWithId(chromeSendName) { |
4091 return function(id) { chrome.send(chromeSendName, [id]); }; | 2669 return function(id) { chrome.send(chromeSendName, [id]); }; |
4092 } | 2670 } |
4093 | 2671 |
4094 /** @constructor */ | 2672 /** @constructor */ |
4095 function ActionService() { | 2673 function ActionService() { |
4096 /** @private {Array<string>} */ | 2674 /** @private {Array<string>} */ |
4097 this.searchTerms_ = []; | 2675 this.searchTerms_ = []; |
4098 } | 2676 } |
4099 | 2677 |
4100 /** | |
4101 * @param {string} s | |
4102 * @return {string} |s| without whitespace at the beginning or end. | |
4103 */ | |
4104 function trim(s) { return s.trim(); } | 2678 function trim(s) { return s.trim(); } |
4105 | 2679 |
4106 /** | |
4107 * @param {string|undefined} value | |
4108 * @return {boolean} Whether |value| is truthy. | |
4109 */ | |
4110 function truthy(value) { return !!value; } | 2680 function truthy(value) { return !!value; } |
4111 | 2681 |
4112 /** | |
4113 * @param {string} searchText Input typed by the user into a search box. | |
4114 * @return {Array<string>} A list of terms extracted from |searchText|. | |
4115 */ | |
4116 ActionService.splitTerms = function(searchText) { | 2682 ActionService.splitTerms = function(searchText) { |
4117 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
4118 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | 2683 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); |
4119 }; | 2684 }; |
4120 | 2685 |
4121 ActionService.prototype = { | 2686 ActionService.prototype = { |
4122 /** @param {string} id ID of the download to cancel. */ | 2687 /** @param {string} id ID of the download to cancel. */ |
4123 cancel: chromeSendWithId('cancel'), | 2688 cancel: chromeSendWithId('cancel'), |
4124 | 2689 |
4125 /** Instructs the browser to clear all finished downloads. */ | 2690 /** Instructs the browser to clear all finished downloads. */ |
4126 clearAll: function() { | 2691 clearAll: function() { |
4127 if (loadTimeData.getBoolean('allowDeletingHistory')) { | 2692 if (loadTimeData.getBoolean('allowDeletingHistory')) { |
(...skipping 14 matching lines...) Expand all Loading... |
4142 }, | 2707 }, |
4143 | 2708 |
4144 /** @param {string} id ID of the download that the user started dragging. */ | 2709 /** @param {string} id ID of the download that the user started dragging. */ |
4145 drag: chromeSendWithId('drag'), | 2710 drag: chromeSendWithId('drag'), |
4146 | 2711 |
4147 /** Loads more downloads with the current search terms. */ | 2712 /** Loads more downloads with the current search terms. */ |
4148 loadMore: function() { | 2713 loadMore: function() { |
4149 chrome.send('getDownloads', this.searchTerms_); | 2714 chrome.send('getDownloads', this.searchTerms_); |
4150 }, | 2715 }, |
4151 | 2716 |
4152 /** | |
4153 * @return {boolean} Whether the user is currently searching for downloads | |
4154 * (i.e. has a non-empty search term). | |
4155 */ | |
4156 isSearching: function() { | 2717 isSearching: function() { |
4157 return this.searchTerms_.length > 0; | 2718 return this.searchTerms_.length > 0; |
4158 }, | 2719 }, |
4159 | 2720 |
4160 /** Opens the current local destination for downloads. */ | 2721 /** Opens the current local destination for downloads. */ |
4161 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | 2722 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), |
4162 | 2723 |
4163 /** | |
4164 * @param {string} id ID of the download to run locally on the user's box. | |
4165 */ | |
4166 openFile: chromeSendWithId('openFile'), | 2724 openFile: chromeSendWithId('openFile'), |
4167 | 2725 |
4168 /** @param {string} id ID the of the progressing download to pause. */ | 2726 /** @param {string} id ID the of the progressing download to pause. */ |
4169 pause: chromeSendWithId('pause'), | 2727 pause: chromeSendWithId('pause'), |
4170 | 2728 |
4171 /** @param {string} id ID of the finished download to remove. */ | 2729 /** @param {string} id ID of the finished download to remove. */ |
4172 remove: chromeSendWithId('remove'), | 2730 remove: chromeSendWithId('remove'), |
4173 | 2731 |
4174 /** @param {string} id ID of the paused download to resume. */ | 2732 /** @param {string} id ID of the paused download to resume. */ |
4175 resume: chromeSendWithId('resume'), | 2733 resume: chromeSendWithId('resume'), |
4176 | 2734 |
4177 /** | |
4178 * @param {string} id ID of the dangerous download to save despite | |
4179 * warnings. | |
4180 */ | |
4181 saveDangerous: chromeSendWithId('saveDangerous'), | 2735 saveDangerous: chromeSendWithId('saveDangerous'), |
4182 | 2736 |
4183 /** @param {string} searchText What to search for. */ | 2737 /** @param {string} searchText What to search for. */ |
4184 search: function(searchText) { | 2738 search: function(searchText) { |
4185 var searchTerms = ActionService.splitTerms(searchText); | 2739 var searchTerms = ActionService.splitTerms(searchText); |
4186 var sameTerms = searchTerms.length == this.searchTerms_.length; | 2740 var sameTerms = searchTerms.length == this.searchTerms_.length; |
4187 | 2741 |
4188 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | 2742 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { |
4189 if (searchTerms[i] != this.searchTerms_[i]) | 2743 if (searchTerms[i] != this.searchTerms_[i]) |
4190 sameTerms = false; | 2744 sameTerms = false; |
4191 } | 2745 } |
4192 | 2746 |
4193 if (sameTerms) | 2747 if (sameTerms) |
4194 return; | 2748 return; |
4195 | 2749 |
4196 this.searchTerms_ = searchTerms; | 2750 this.searchTerms_ = searchTerms; |
4197 this.loadMore(); | 2751 this.loadMore(); |
4198 }, | 2752 }, |
4199 | 2753 |
4200 /** | |
4201 * Shows the local folder a finished download resides in. | |
4202 * @param {string} id ID of the download to show. | |
4203 */ | |
4204 show: chromeSendWithId('show'), | 2754 show: chromeSendWithId('show'), |
4205 | 2755 |
4206 /** Undo download removal. */ | 2756 /** Undo download removal. */ |
4207 undo: chrome.send.bind(chrome, 'undo'), | 2757 undo: chrome.send.bind(chrome, 'undo'), |
4208 }; | 2758 }; |
4209 | 2759 |
4210 cr.addSingletonGetter(ActionService); | 2760 cr.addSingletonGetter(ActionService); |
4211 | 2761 |
4212 return {ActionService: ActionService}; | 2762 return {ActionService: ActionService}; |
4213 }); | 2763 }); |
4214 // Copyright 2015 The Chromium Authors. All rights reserved. | 2764 // Copyright 2015 The Chromium Authors. All rights reserved. |
4215 // Use of this source code is governed by a BSD-style license that can be | 2765 // Use of this source code is governed by a BSD-style license that can be |
4216 // found in the LICENSE file. | 2766 // found in the LICENSE file. |
4217 | 2767 |
4218 cr.define('downloads', function() { | 2768 cr.define('downloads', function() { |
4219 /** | |
4220 * Explains why a download is in DANGEROUS state. | |
4221 * @enum {string} | |
4222 */ | |
4223 var DangerType = { | 2769 var DangerType = { |
4224 NOT_DANGEROUS: 'NOT_DANGEROUS', | 2770 NOT_DANGEROUS: 'NOT_DANGEROUS', |
4225 DANGEROUS_FILE: 'DANGEROUS_FILE', | 2771 DANGEROUS_FILE: 'DANGEROUS_FILE', |
4226 DANGEROUS_URL: 'DANGEROUS_URL', | 2772 DANGEROUS_URL: 'DANGEROUS_URL', |
4227 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | 2773 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', |
4228 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | 2774 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', |
4229 DANGEROUS_HOST: 'DANGEROUS_HOST', | 2775 DANGEROUS_HOST: 'DANGEROUS_HOST', |
4230 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | 2776 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', |
4231 }; | 2777 }; |
4232 | 2778 |
4233 /** | |
4234 * The states a download can be in. These correspond to states defined in | |
4235 * DownloadsDOMHandler::CreateDownloadItemValue | |
4236 * @enum {string} | |
4237 */ | |
4238 var States = { | 2779 var States = { |
4239 IN_PROGRESS: 'IN_PROGRESS', | 2780 IN_PROGRESS: 'IN_PROGRESS', |
4240 CANCELLED: 'CANCELLED', | 2781 CANCELLED: 'CANCELLED', |
4241 COMPLETE: 'COMPLETE', | 2782 COMPLETE: 'COMPLETE', |
4242 PAUSED: 'PAUSED', | 2783 PAUSED: 'PAUSED', |
4243 DANGEROUS: 'DANGEROUS', | 2784 DANGEROUS: 'DANGEROUS', |
4244 INTERRUPTED: 'INTERRUPTED', | 2785 INTERRUPTED: 'INTERRUPTED', |
4245 }; | 2786 }; |
4246 | 2787 |
4247 return { | 2788 return { |
4248 DangerType: DangerType, | 2789 DangerType: DangerType, |
4249 States: States, | 2790 States: States, |
4250 }; | 2791 }; |
4251 }); | 2792 }); |
4252 // Copyright 2014 The Chromium Authors. All rights reserved. | 2793 // Copyright 2014 The Chromium Authors. All rights reserved. |
4253 // Use of this source code is governed by a BSD-style license that can be | 2794 // Use of this source code is governed by a BSD-style license that can be |
4254 // found in the LICENSE file. | 2795 // found in the LICENSE file. |
4255 | 2796 |
4256 // Action links are elements that are used to perform an in-page navigation or | |
4257 // action (e.g. showing a dialog). | |
4258 // | |
4259 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
4260 // they're subtly different as they're not initially underlined (giving users a | |
4261 // clue that underlined links navigate while action links don't). | |
4262 // | |
4263 // Action links look very similar to normal links when hovered (hand cursor, | |
4264 // underlined). This gives the user an idea that clicking this link will do | |
4265 // something similar to navigation but in the same page. | |
4266 // | |
4267 // They can be created in JavaScript like this: | |
4268 // | |
4269 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
4270 // | |
4271 // or with a constructor like this: | |
4272 // | |
4273 // var link = new ActionLink(); | |
4274 // | |
4275 // They can be used easily from HTML as well, like so: | |
4276 // | |
4277 // <a is="action-link">Click me!</a> | |
4278 // | |
4279 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
4280 | 2797 |
4281 /** | |
4282 * @constructor | |
4283 * @extends {HTMLAnchorElement} | |
4284 */ | |
4285 var ActionLink = document.registerElement('action-link', { | 2798 var ActionLink = document.registerElement('action-link', { |
4286 prototype: { | 2799 prototype: { |
4287 __proto__: HTMLAnchorElement.prototype, | 2800 __proto__: HTMLAnchorElement.prototype, |
4288 | 2801 |
4289 /** @this {ActionLink} */ | 2802 /** @this {ActionLink} */ |
4290 createdCallback: function() { | 2803 createdCallback: function() { |
4291 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
4292 this.tabIndex = this.disabled ? -1 : 0; | 2804 this.tabIndex = this.disabled ? -1 : 0; |
4293 | 2805 |
4294 if (!this.hasAttribute('role')) | 2806 if (!this.hasAttribute('role')) |
4295 this.setAttribute('role', 'link'); | 2807 this.setAttribute('role', 'link'); |
4296 | 2808 |
4297 this.addEventListener('keydown', function(e) { | 2809 this.addEventListener('keydown', function(e) { |
4298 if (!this.disabled && e.key == 'Enter' && !this.href) { | 2810 if (!this.disabled && e.key == 'Enter' && !this.href) { |
4299 // Schedule a click asynchronously because other 'keydown' handlers | |
4300 // may still run later (e.g. document.addEventListener('keydown')). | |
4301 // Specifically options dialogs break when this timeout isn't here. | |
4302 // NOTE: this affects the "trusted" state of the ensuing click. I | |
4303 // haven't found anything that breaks because of this (yet). | |
4304 window.setTimeout(this.click.bind(this), 0); | 2811 window.setTimeout(this.click.bind(this), 0); |
4305 } | 2812 } |
4306 }); | 2813 }); |
4307 | 2814 |
4308 function preventDefault(e) { | 2815 function preventDefault(e) { |
4309 e.preventDefault(); | 2816 e.preventDefault(); |
4310 } | 2817 } |
4311 | 2818 |
4312 function removePreventDefault() { | 2819 function removePreventDefault() { |
4313 document.removeEventListener('selectstart', preventDefault); | 2820 document.removeEventListener('selectstart', preventDefault); |
4314 document.removeEventListener('mouseup', removePreventDefault); | 2821 document.removeEventListener('mouseup', removePreventDefault); |
4315 } | 2822 } |
4316 | 2823 |
4317 this.addEventListener('mousedown', function() { | 2824 this.addEventListener('mousedown', function() { |
4318 // This handlers strives to match the behavior of <a href="...">. | |
4319 | 2825 |
4320 // While the mouse is down, prevent text selection from dragging. | |
4321 document.addEventListener('selectstart', preventDefault); | 2826 document.addEventListener('selectstart', preventDefault); |
4322 document.addEventListener('mouseup', removePreventDefault); | 2827 document.addEventListener('mouseup', removePreventDefault); |
4323 | 2828 |
4324 // If focus started via mouse press, don't show an outline. | |
4325 if (document.activeElement != this) | 2829 if (document.activeElement != this) |
4326 this.classList.add('no-outline'); | 2830 this.classList.add('no-outline'); |
4327 }); | 2831 }); |
4328 | 2832 |
4329 this.addEventListener('blur', function() { | 2833 this.addEventListener('blur', function() { |
4330 this.classList.remove('no-outline'); | 2834 this.classList.remove('no-outline'); |
4331 }); | 2835 }); |
4332 }, | 2836 }, |
4333 | 2837 |
4334 /** @type {boolean} */ | 2838 /** @type {boolean} */ |
(...skipping 22 matching lines...) Expand all Loading... |
4357 this.disabled = false; | 2861 this.disabled = false; |
4358 else | 2862 else |
4359 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | 2863 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); |
4360 }, | 2864 }, |
4361 }, | 2865 }, |
4362 | 2866 |
4363 extends: 'a', | 2867 extends: 'a', |
4364 }); | 2868 }); |
4365 (function() { | 2869 (function() { |
4366 | 2870 |
4367 // monostate data | |
4368 var metaDatas = {}; | 2871 var metaDatas = {}; |
4369 var metaArrays = {}; | 2872 var metaArrays = {}; |
4370 var singleton = null; | 2873 var singleton = null; |
4371 | 2874 |
4372 Polymer.IronMeta = Polymer({ | 2875 Polymer.IronMeta = Polymer({ |
4373 | 2876 |
4374 is: 'iron-meta', | 2877 is: 'iron-meta', |
4375 | 2878 |
4376 properties: { | 2879 properties: { |
4377 | 2880 |
4378 /** | |
4379 * The type of meta-data. All meta-data of the same type is stored | |
4380 * together. | |
4381 */ | |
4382 type: { | 2881 type: { |
4383 type: String, | 2882 type: String, |
4384 value: 'default', | 2883 value: 'default', |
4385 observer: '_typeChanged' | 2884 observer: '_typeChanged' |
4386 }, | 2885 }, |
4387 | 2886 |
4388 /** | |
4389 * The key used to store `value` under the `type` namespace. | |
4390 */ | |
4391 key: { | 2887 key: { |
4392 type: String, | 2888 type: String, |
4393 observer: '_keyChanged' | 2889 observer: '_keyChanged' |
4394 }, | 2890 }, |
4395 | 2891 |
4396 /** | |
4397 * The meta-data to store or retrieve. | |
4398 */ | |
4399 value: { | 2892 value: { |
4400 type: Object, | 2893 type: Object, |
4401 notify: true, | 2894 notify: true, |
4402 observer: '_valueChanged' | 2895 observer: '_valueChanged' |
4403 }, | 2896 }, |
4404 | 2897 |
4405 /** | |
4406 * If true, `value` is set to the iron-meta instance itself. | |
4407 */ | |
4408 self: { | 2898 self: { |
4409 type: Boolean, | 2899 type: Boolean, |
4410 observer: '_selfChanged' | 2900 observer: '_selfChanged' |
4411 }, | 2901 }, |
4412 | 2902 |
4413 /** | |
4414 * Array of all meta-data values for the given type. | |
4415 */ | |
4416 list: { | 2903 list: { |
4417 type: Array, | 2904 type: Array, |
4418 notify: true | 2905 notify: true |
4419 } | 2906 } |
4420 | 2907 |
4421 }, | 2908 }, |
4422 | 2909 |
4423 hostAttributes: { | 2910 hostAttributes: { |
4424 hidden: true | 2911 hidden: true |
4425 }, | 2912 }, |
4426 | 2913 |
4427 /** | |
4428 * Only runs if someone invokes the factory/constructor directly | |
4429 * e.g. `new Polymer.IronMeta()` | |
4430 * | |
4431 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
4432 */ | |
4433 factoryImpl: function(config) { | 2914 factoryImpl: function(config) { |
4434 if (config) { | 2915 if (config) { |
4435 for (var n in config) { | 2916 for (var n in config) { |
4436 switch(n) { | 2917 switch(n) { |
4437 case 'type': | 2918 case 'type': |
4438 case 'key': | 2919 case 'key': |
4439 case 'value': | 2920 case 'value': |
4440 this[n] = config[n]; | 2921 this[n] = config[n]; |
4441 break; | 2922 break; |
4442 } | 2923 } |
4443 } | 2924 } |
4444 } | 2925 } |
4445 }, | 2926 }, |
4446 | 2927 |
4447 created: function() { | 2928 created: function() { |
4448 // TODO(sjmiles): good for debugging? | |
4449 this._metaDatas = metaDatas; | 2929 this._metaDatas = metaDatas; |
4450 this._metaArrays = metaArrays; | 2930 this._metaArrays = metaArrays; |
4451 }, | 2931 }, |
4452 | 2932 |
4453 _keyChanged: function(key, old) { | 2933 _keyChanged: function(key, old) { |
4454 this._resetRegistration(old); | 2934 this._resetRegistration(old); |
4455 }, | 2935 }, |
4456 | 2936 |
4457 _valueChanged: function(value) { | 2937 _valueChanged: function(value) { |
4458 this._resetRegistration(this.key); | 2938 this._resetRegistration(this.key); |
(...skipping 11 matching lines...) Expand all Loading... |
4470 metaDatas[type] = {}; | 2950 metaDatas[type] = {}; |
4471 } | 2951 } |
4472 this._metaData = metaDatas[type]; | 2952 this._metaData = metaDatas[type]; |
4473 if (!metaArrays[type]) { | 2953 if (!metaArrays[type]) { |
4474 metaArrays[type] = []; | 2954 metaArrays[type] = []; |
4475 } | 2955 } |
4476 this.list = metaArrays[type]; | 2956 this.list = metaArrays[type]; |
4477 this._registerKeyValue(this.key, this.value); | 2957 this._registerKeyValue(this.key, this.value); |
4478 }, | 2958 }, |
4479 | 2959 |
4480 /** | |
4481 * Retrieves meta data value by key. | |
4482 * | |
4483 * @method byKey | |
4484 * @param {string} key The key of the meta-data to be returned. | |
4485 * @return {*} | |
4486 */ | |
4487 byKey: function(key) { | 2960 byKey: function(key) { |
4488 return this._metaData && this._metaData[key]; | 2961 return this._metaData && this._metaData[key]; |
4489 }, | 2962 }, |
4490 | 2963 |
4491 _resetRegistration: function(oldKey) { | 2964 _resetRegistration: function(oldKey) { |
4492 this._unregisterKey(oldKey); | 2965 this._unregisterKey(oldKey); |
4493 this._registerKeyValue(this.key, this.value); | 2966 this._registerKeyValue(this.key, this.value); |
4494 }, | 2967 }, |
4495 | 2968 |
4496 _unregisterKey: function(key) { | 2969 _unregisterKey: function(key) { |
(...skipping 23 matching lines...) Expand all Loading... |
4520 | 2993 |
4521 }); | 2994 }); |
4522 | 2995 |
4523 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 2996 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
4524 if (singleton === null) { | 2997 if (singleton === null) { |
4525 singleton = new Polymer.IronMeta(); | 2998 singleton = new Polymer.IronMeta(); |
4526 } | 2999 } |
4527 return singleton; | 3000 return singleton; |
4528 }; | 3001 }; |
4529 | 3002 |
4530 /** | |
4531 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | |
4532 | |
4533 Examples: | |
4534 | |
4535 If I create an instance like this: | |
4536 | |
4537 <iron-meta key="info" value="foo/bar"></iron-meta> | |
4538 | |
4539 Note that value="foo/bar" is the metadata I've defined. I could define more | |
4540 attributes or use child nodes to define additional metadata. | |
4541 | |
4542 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | |
4543 | |
4544 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | |
4545 | |
4546 @group Polymer Iron Elements | |
4547 @element iron-meta-query | |
4548 */ | |
4549 Polymer.IronMetaQuery = Polymer({ | 3003 Polymer.IronMetaQuery = Polymer({ |
4550 | 3004 |
4551 is: 'iron-meta-query', | 3005 is: 'iron-meta-query', |
4552 | 3006 |
4553 properties: { | 3007 properties: { |
4554 | 3008 |
4555 /** | |
4556 * The type of meta-data. All meta-data of the same type is stored | |
4557 * together. | |
4558 */ | |
4559 type: { | 3009 type: { |
4560 type: String, | 3010 type: String, |
4561 value: 'default', | 3011 value: 'default', |
4562 observer: '_typeChanged' | 3012 observer: '_typeChanged' |
4563 }, | 3013 }, |
4564 | 3014 |
4565 /** | |
4566 * Specifies a key to use for retrieving `value` from the `type` | |
4567 * namespace. | |
4568 */ | |
4569 key: { | 3015 key: { |
4570 type: String, | 3016 type: String, |
4571 observer: '_keyChanged' | 3017 observer: '_keyChanged' |
4572 }, | 3018 }, |
4573 | 3019 |
4574 /** | |
4575 * The meta-data to store or retrieve. | |
4576 */ | |
4577 value: { | 3020 value: { |
4578 type: Object, | 3021 type: Object, |
4579 notify: true, | 3022 notify: true, |
4580 readOnly: true | 3023 readOnly: true |
4581 }, | 3024 }, |
4582 | 3025 |
4583 /** | |
4584 * Array of all meta-data values for the given type. | |
4585 */ | |
4586 list: { | 3026 list: { |
4587 type: Array, | 3027 type: Array, |
4588 notify: true | 3028 notify: true |
4589 } | 3029 } |
4590 | 3030 |
4591 }, | 3031 }, |
4592 | 3032 |
4593 /** | |
4594 * Actually a factory method, not a true constructor. Only runs if | |
4595 * someone invokes it directly (via `new Polymer.IronMeta()`); | |
4596 * | |
4597 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
4598 */ | |
4599 factoryImpl: function(config) { | 3033 factoryImpl: function(config) { |
4600 if (config) { | 3034 if (config) { |
4601 for (var n in config) { | 3035 for (var n in config) { |
4602 switch(n) { | 3036 switch(n) { |
4603 case 'type': | 3037 case 'type': |
4604 case 'key': | 3038 case 'key': |
4605 this[n] = config[n]; | 3039 this[n] = config[n]; |
4606 break; | 3040 break; |
4607 } | 3041 } |
4608 } | 3042 } |
4609 } | 3043 } |
4610 }, | 3044 }, |
4611 | 3045 |
4612 created: function() { | 3046 created: function() { |
4613 // TODO(sjmiles): good for debugging? | |
4614 this._metaDatas = metaDatas; | 3047 this._metaDatas = metaDatas; |
4615 this._metaArrays = metaArrays; | 3048 this._metaArrays = metaArrays; |
4616 }, | 3049 }, |
4617 | 3050 |
4618 _keyChanged: function(key) { | 3051 _keyChanged: function(key) { |
4619 this._setValue(this._metaData && this._metaData[key]); | 3052 this._setValue(this._metaData && this._metaData[key]); |
4620 }, | 3053 }, |
4621 | 3054 |
4622 _typeChanged: function(type) { | 3055 _typeChanged: function(type) { |
4623 this._metaData = metaDatas[type]; | 3056 this._metaData = metaDatas[type]; |
4624 this.list = metaArrays[type]; | 3057 this.list = metaArrays[type]; |
4625 if (this.key) { | 3058 if (this.key) { |
4626 this._keyChanged(this.key); | 3059 this._keyChanged(this.key); |
4627 } | 3060 } |
4628 }, | 3061 }, |
4629 | 3062 |
4630 /** | |
4631 * Retrieves meta data value by key. | |
4632 * @param {string} key The key of the meta-data to be returned. | |
4633 * @return {*} | |
4634 */ | |
4635 byKey: function(key) { | 3063 byKey: function(key) { |
4636 return this._metaData && this._metaData[key]; | 3064 return this._metaData && this._metaData[key]; |
4637 } | 3065 } |
4638 | 3066 |
4639 }); | 3067 }); |
4640 | 3068 |
4641 })(); | 3069 })(); |
4642 Polymer({ | 3070 Polymer({ |
4643 | 3071 |
4644 is: 'iron-icon', | 3072 is: 'iron-icon', |
4645 | 3073 |
4646 properties: { | 3074 properties: { |
4647 | 3075 |
4648 /** | |
4649 * The name of the icon to use. The name should be of the form: | |
4650 * `iconset_name:icon_name`. | |
4651 */ | |
4652 icon: { | 3076 icon: { |
4653 type: String, | 3077 type: String, |
4654 observer: '_iconChanged' | 3078 observer: '_iconChanged' |
4655 }, | 3079 }, |
4656 | 3080 |
4657 /** | |
4658 * The name of the theme to used, if one is specified by the | |
4659 * iconset. | |
4660 */ | |
4661 theme: { | 3081 theme: { |
4662 type: String, | 3082 type: String, |
4663 observer: '_updateIcon' | 3083 observer: '_updateIcon' |
4664 }, | 3084 }, |
4665 | 3085 |
4666 /** | |
4667 * If using iron-icon without an iconset, you can set the src to be | |
4668 * the URL of an individual icon image file. Note that this will take | |
4669 * precedence over a given icon attribute. | |
4670 */ | |
4671 src: { | 3086 src: { |
4672 type: String, | 3087 type: String, |
4673 observer: '_srcChanged' | 3088 observer: '_srcChanged' |
4674 }, | 3089 }, |
4675 | 3090 |
4676 /** | |
4677 * @type {!Polymer.IronMeta} | |
4678 */ | |
4679 _meta: { | 3091 _meta: { |
4680 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | 3092 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), |
4681 observer: '_updateIcon' | 3093 observer: '_updateIcon' |
4682 } | 3094 } |
4683 | 3095 |
4684 }, | 3096 }, |
4685 | 3097 |
4686 _DEFAULT_ICONSET: 'icons', | 3098 _DEFAULT_ICONSET: 'icons', |
4687 | 3099 |
4688 _iconChanged: function(icon) { | 3100 _iconChanged: function(icon) { |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4729 this._img.style.width = '100%'; | 3141 this._img.style.width = '100%'; |
4730 this._img.style.height = '100%'; | 3142 this._img.style.height = '100%'; |
4731 this._img.draggable = false; | 3143 this._img.draggable = false; |
4732 } | 3144 } |
4733 this._img.src = this.src; | 3145 this._img.src = this.src; |
4734 Polymer.dom(this.root).appendChild(this._img); | 3146 Polymer.dom(this.root).appendChild(this._img); |
4735 } | 3147 } |
4736 } | 3148 } |
4737 | 3149 |
4738 }); | 3150 }); |
4739 /** | |
4740 * @demo demo/index.html | |
4741 * @polymerBehavior | |
4742 */ | |
4743 Polymer.IronControlState = { | 3151 Polymer.IronControlState = { |
4744 | 3152 |
4745 properties: { | 3153 properties: { |
4746 | 3154 |
4747 /** | |
4748 * If true, the element currently has focus. | |
4749 */ | |
4750 focused: { | 3155 focused: { |
4751 type: Boolean, | 3156 type: Boolean, |
4752 value: false, | 3157 value: false, |
4753 notify: true, | 3158 notify: true, |
4754 readOnly: true, | 3159 readOnly: true, |
4755 reflectToAttribute: true | 3160 reflectToAttribute: true |
4756 }, | 3161 }, |
4757 | 3162 |
4758 /** | |
4759 * If true, the user cannot interact with this element. | |
4760 */ | |
4761 disabled: { | 3163 disabled: { |
4762 type: Boolean, | 3164 type: Boolean, |
4763 value: false, | 3165 value: false, |
4764 notify: true, | 3166 notify: true, |
4765 observer: '_disabledChanged', | 3167 observer: '_disabledChanged', |
4766 reflectToAttribute: true | 3168 reflectToAttribute: true |
4767 }, | 3169 }, |
4768 | 3170 |
4769 _oldTabIndex: { | 3171 _oldTabIndex: { |
4770 type: Number | 3172 type: Number |
(...skipping 11 matching lines...) Expand all Loading... |
4782 observers: [ | 3184 observers: [ |
4783 '_changedControlState(focused, disabled)' | 3185 '_changedControlState(focused, disabled)' |
4784 ], | 3186 ], |
4785 | 3187 |
4786 ready: function() { | 3188 ready: function() { |
4787 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 3189 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
4788 this.addEventListener('blur', this._boundFocusBlurHandler, true); | 3190 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
4789 }, | 3191 }, |
4790 | 3192 |
4791 _focusBlurHandler: function(event) { | 3193 _focusBlurHandler: function(event) { |
4792 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | |
4793 // eventually become `this` due to retargeting; if we are not in | |
4794 // ShadowDOM land, `event.target` will eventually become `this` due | |
4795 // to the second conditional which fires a synthetic event (that is also | |
4796 // handled). In either case, we can disregard `event.path`. | |
4797 | 3194 |
4798 if (event.target === this) { | 3195 if (event.target === this) { |
4799 this._setFocused(event.type === 'focus'); | 3196 this._setFocused(event.type === 'focus'); |
4800 } else if (!this.shadowRoot) { | 3197 } else if (!this.shadowRoot) { |
4801 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | 3198 var target = /** @type {Node} */(Polymer.dom(event).localTarget); |
4802 if (!this.isLightDescendant(target)) { | 3199 if (!this.isLightDescendant(target)) { |
4803 this.fire(event.type, {sourceEvent: event}, { | 3200 this.fire(event.type, {sourceEvent: event}, { |
4804 node: this, | 3201 node: this, |
4805 bubbles: event.bubbles, | 3202 bubbles: event.bubbles, |
4806 cancelable: event.cancelable | 3203 cancelable: event.cancelable |
4807 }); | 3204 }); |
4808 } | 3205 } |
4809 } | 3206 } |
4810 }, | 3207 }, |
4811 | 3208 |
4812 _disabledChanged: function(disabled, old) { | 3209 _disabledChanged: function(disabled, old) { |
4813 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3210 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
4814 this.style.pointerEvents = disabled ? 'none' : ''; | 3211 this.style.pointerEvents = disabled ? 'none' : ''; |
4815 if (disabled) { | 3212 if (disabled) { |
4816 this._oldTabIndex = this.tabIndex; | 3213 this._oldTabIndex = this.tabIndex; |
4817 this._setFocused(false); | 3214 this._setFocused(false); |
4818 this.tabIndex = -1; | 3215 this.tabIndex = -1; |
4819 this.blur(); | 3216 this.blur(); |
4820 } else if (this._oldTabIndex !== undefined) { | 3217 } else if (this._oldTabIndex !== undefined) { |
4821 this.tabIndex = this._oldTabIndex; | 3218 this.tabIndex = this._oldTabIndex; |
4822 } | 3219 } |
4823 }, | 3220 }, |
4824 | 3221 |
4825 _changedControlState: function() { | 3222 _changedControlState: function() { |
4826 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
4827 if (this._controlStateChanged) { | 3223 if (this._controlStateChanged) { |
4828 this._controlStateChanged(); | 3224 this._controlStateChanged(); |
4829 } | 3225 } |
4830 } | 3226 } |
4831 | 3227 |
4832 }; | 3228 }; |
4833 /** | |
4834 * @demo demo/index.html | |
4835 * @polymerBehavior Polymer.IronButtonState | |
4836 */ | |
4837 Polymer.IronButtonStateImpl = { | 3229 Polymer.IronButtonStateImpl = { |
4838 | 3230 |
4839 properties: { | 3231 properties: { |
4840 | 3232 |
4841 /** | |
4842 * If true, the user is currently holding down the button. | |
4843 */ | |
4844 pressed: { | 3233 pressed: { |
4845 type: Boolean, | 3234 type: Boolean, |
4846 readOnly: true, | 3235 readOnly: true, |
4847 value: false, | 3236 value: false, |
4848 reflectToAttribute: true, | 3237 reflectToAttribute: true, |
4849 observer: '_pressedChanged' | 3238 observer: '_pressedChanged' |
4850 }, | 3239 }, |
4851 | 3240 |
4852 /** | |
4853 * If true, the button toggles the active state with each tap or press | |
4854 * of the spacebar. | |
4855 */ | |
4856 toggles: { | 3241 toggles: { |
4857 type: Boolean, | 3242 type: Boolean, |
4858 value: false, | 3243 value: false, |
4859 reflectToAttribute: true | 3244 reflectToAttribute: true |
4860 }, | 3245 }, |
4861 | 3246 |
4862 /** | |
4863 * If true, the button is a toggle and is currently in the active state. | |
4864 */ | |
4865 active: { | 3247 active: { |
4866 type: Boolean, | 3248 type: Boolean, |
4867 value: false, | 3249 value: false, |
4868 notify: true, | 3250 notify: true, |
4869 reflectToAttribute: true | 3251 reflectToAttribute: true |
4870 }, | 3252 }, |
4871 | 3253 |
4872 /** | |
4873 * True if the element is currently being pressed by a "pointer," which | |
4874 * is loosely defined as mouse or touch input (but specifically excluding | |
4875 * keyboard input). | |
4876 */ | |
4877 pointerDown: { | 3254 pointerDown: { |
4878 type: Boolean, | 3255 type: Boolean, |
4879 readOnly: true, | 3256 readOnly: true, |
4880 value: false | 3257 value: false |
4881 }, | 3258 }, |
4882 | 3259 |
4883 /** | |
4884 * True if the input device that caused the element to receive focus | |
4885 * was a keyboard. | |
4886 */ | |
4887 receivedFocusFromKeyboard: { | 3260 receivedFocusFromKeyboard: { |
4888 type: Boolean, | 3261 type: Boolean, |
4889 readOnly: true | 3262 readOnly: true |
4890 }, | 3263 }, |
4891 | 3264 |
4892 /** | |
4893 * The aria attribute to be set if the button is a toggle and in the | |
4894 * active state. | |
4895 */ | |
4896 ariaActiveAttribute: { | 3265 ariaActiveAttribute: { |
4897 type: String, | 3266 type: String, |
4898 value: 'aria-pressed', | 3267 value: 'aria-pressed', |
4899 observer: '_ariaActiveAttributeChanged' | 3268 observer: '_ariaActiveAttributeChanged' |
4900 } | 3269 } |
4901 }, | 3270 }, |
4902 | 3271 |
4903 listeners: { | 3272 listeners: { |
4904 down: '_downHandler', | 3273 down: '_downHandler', |
4905 up: '_upHandler', | 3274 up: '_upHandler', |
4906 tap: '_tapHandler' | 3275 tap: '_tapHandler' |
4907 }, | 3276 }, |
4908 | 3277 |
4909 observers: [ | 3278 observers: [ |
4910 '_detectKeyboardFocus(focused)', | 3279 '_detectKeyboardFocus(focused)', |
4911 '_activeChanged(active, ariaActiveAttribute)' | 3280 '_activeChanged(active, ariaActiveAttribute)' |
4912 ], | 3281 ], |
4913 | 3282 |
4914 keyBindings: { | 3283 keyBindings: { |
4915 'enter:keydown': '_asyncClick', | 3284 'enter:keydown': '_asyncClick', |
4916 'space:keydown': '_spaceKeyDownHandler', | 3285 'space:keydown': '_spaceKeyDownHandler', |
4917 'space:keyup': '_spaceKeyUpHandler', | 3286 'space:keyup': '_spaceKeyUpHandler', |
4918 }, | 3287 }, |
4919 | 3288 |
4920 _mouseEventRe: /^mouse/, | 3289 _mouseEventRe: /^mouse/, |
4921 | 3290 |
4922 _tapHandler: function() { | 3291 _tapHandler: function() { |
4923 if (this.toggles) { | 3292 if (this.toggles) { |
4924 // a tap is needed to toggle the active state | |
4925 this._userActivate(!this.active); | 3293 this._userActivate(!this.active); |
4926 } else { | 3294 } else { |
4927 this.active = false; | 3295 this.active = false; |
4928 } | 3296 } |
4929 }, | 3297 }, |
4930 | 3298 |
4931 _detectKeyboardFocus: function(focused) { | 3299 _detectKeyboardFocus: function(focused) { |
4932 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | 3300 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
4933 }, | 3301 }, |
4934 | 3302 |
4935 // to emulate native checkbox, (de-)activations from a user interaction fire | |
4936 // 'change' events | |
4937 _userActivate: function(active) { | 3303 _userActivate: function(active) { |
4938 if (this.active !== active) { | 3304 if (this.active !== active) { |
4939 this.active = active; | 3305 this.active = active; |
4940 this.fire('change'); | 3306 this.fire('change'); |
4941 } | 3307 } |
4942 }, | 3308 }, |
4943 | 3309 |
4944 _downHandler: function(event) { | 3310 _downHandler: function(event) { |
4945 this._setPointerDown(true); | 3311 this._setPointerDown(true); |
4946 this._setPressed(true); | 3312 this._setPressed(true); |
4947 this._setReceivedFocusFromKeyboard(false); | 3313 this._setReceivedFocusFromKeyboard(false); |
4948 }, | 3314 }, |
4949 | 3315 |
4950 _upHandler: function() { | 3316 _upHandler: function() { |
4951 this._setPointerDown(false); | 3317 this._setPointerDown(false); |
4952 this._setPressed(false); | 3318 this._setPressed(false); |
4953 }, | 3319 }, |
4954 | 3320 |
4955 /** | |
4956 * @param {!KeyboardEvent} event . | |
4957 */ | |
4958 _spaceKeyDownHandler: function(event) { | 3321 _spaceKeyDownHandler: function(event) { |
4959 var keyboardEvent = event.detail.keyboardEvent; | 3322 var keyboardEvent = event.detail.keyboardEvent; |
4960 var target = Polymer.dom(keyboardEvent).localTarget; | 3323 var target = Polymer.dom(keyboardEvent).localTarget; |
4961 | 3324 |
4962 // Ignore the event if this is coming from a focused light child, since th
at | |
4963 // element will deal with it. | |
4964 if (this.isLightDescendant(/** @type {Node} */(target))) | 3325 if (this.isLightDescendant(/** @type {Node} */(target))) |
4965 return; | 3326 return; |
4966 | 3327 |
4967 keyboardEvent.preventDefault(); | 3328 keyboardEvent.preventDefault(); |
4968 keyboardEvent.stopImmediatePropagation(); | 3329 keyboardEvent.stopImmediatePropagation(); |
4969 this._setPressed(true); | 3330 this._setPressed(true); |
4970 }, | 3331 }, |
4971 | 3332 |
4972 /** | |
4973 * @param {!KeyboardEvent} event . | |
4974 */ | |
4975 _spaceKeyUpHandler: function(event) { | 3333 _spaceKeyUpHandler: function(event) { |
4976 var keyboardEvent = event.detail.keyboardEvent; | 3334 var keyboardEvent = event.detail.keyboardEvent; |
4977 var target = Polymer.dom(keyboardEvent).localTarget; | 3335 var target = Polymer.dom(keyboardEvent).localTarget; |
4978 | 3336 |
4979 // Ignore the event if this is coming from a focused light child, since th
at | |
4980 // element will deal with it. | |
4981 if (this.isLightDescendant(/** @type {Node} */(target))) | 3337 if (this.isLightDescendant(/** @type {Node} */(target))) |
4982 return; | 3338 return; |
4983 | 3339 |
4984 if (this.pressed) { | 3340 if (this.pressed) { |
4985 this._asyncClick(); | 3341 this._asyncClick(); |
4986 } | 3342 } |
4987 this._setPressed(false); | 3343 this._setPressed(false); |
4988 }, | 3344 }, |
4989 | 3345 |
4990 // trigger click asynchronously, the asynchrony is useful to allow one | |
4991 // event handler to unwind before triggering another event | |
4992 _asyncClick: function() { | 3346 _asyncClick: function() { |
4993 this.async(function() { | 3347 this.async(function() { |
4994 this.click(); | 3348 this.click(); |
4995 }, 1); | 3349 }, 1); |
4996 }, | 3350 }, |
4997 | 3351 |
4998 // any of these changes are considered a change to button state | |
4999 | 3352 |
5000 _pressedChanged: function(pressed) { | 3353 _pressedChanged: function(pressed) { |
5001 this._changedButtonState(); | 3354 this._changedButtonState(); |
5002 }, | 3355 }, |
5003 | 3356 |
5004 _ariaActiveAttributeChanged: function(value, oldValue) { | 3357 _ariaActiveAttributeChanged: function(value, oldValue) { |
5005 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | 3358 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
5006 this.removeAttribute(oldValue); | 3359 this.removeAttribute(oldValue); |
5007 } | 3360 } |
5008 }, | 3361 }, |
5009 | 3362 |
5010 _activeChanged: function(active, ariaActiveAttribute) { | 3363 _activeChanged: function(active, ariaActiveAttribute) { |
5011 if (this.toggles) { | 3364 if (this.toggles) { |
5012 this.setAttribute(this.ariaActiveAttribute, | 3365 this.setAttribute(this.ariaActiveAttribute, |
5013 active ? 'true' : 'false'); | 3366 active ? 'true' : 'false'); |
5014 } else { | 3367 } else { |
5015 this.removeAttribute(this.ariaActiveAttribute); | 3368 this.removeAttribute(this.ariaActiveAttribute); |
5016 } | 3369 } |
5017 this._changedButtonState(); | 3370 this._changedButtonState(); |
5018 }, | 3371 }, |
5019 | 3372 |
5020 _controlStateChanged: function() { | 3373 _controlStateChanged: function() { |
5021 if (this.disabled) { | 3374 if (this.disabled) { |
5022 this._setPressed(false); | 3375 this._setPressed(false); |
5023 } else { | 3376 } else { |
5024 this._changedButtonState(); | 3377 this._changedButtonState(); |
5025 } | 3378 } |
5026 }, | 3379 }, |
5027 | 3380 |
5028 // provide hook for follow-on behaviors to react to button-state | |
5029 | 3381 |
5030 _changedButtonState: function() { | 3382 _changedButtonState: function() { |
5031 if (this._buttonStateChanged) { | 3383 if (this._buttonStateChanged) { |
5032 this._buttonStateChanged(); // abstract | 3384 this._buttonStateChanged(); // abstract |
5033 } | 3385 } |
5034 } | 3386 } |
5035 | 3387 |
5036 }; | 3388 }; |
5037 | 3389 |
5038 /** @polymerBehavior */ | 3390 /** @polymerBehavior */ |
5039 Polymer.IronButtonState = [ | 3391 Polymer.IronButtonState = [ |
5040 Polymer.IronA11yKeysBehavior, | 3392 Polymer.IronA11yKeysBehavior, |
5041 Polymer.IronButtonStateImpl | 3393 Polymer.IronButtonStateImpl |
5042 ]; | 3394 ]; |
5043 (function() { | 3395 (function() { |
5044 var Utility = { | 3396 var Utility = { |
5045 distance: function(x1, y1, x2, y2) { | 3397 distance: function(x1, y1, x2, y2) { |
5046 var xDelta = (x1 - x2); | 3398 var xDelta = (x1 - x2); |
5047 var yDelta = (y1 - y2); | 3399 var yDelta = (y1 - y2); |
5048 | 3400 |
5049 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | 3401 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
5050 }, | 3402 }, |
5051 | 3403 |
5052 now: window.performance && window.performance.now ? | 3404 now: window.performance && window.performance.now ? |
5053 window.performance.now.bind(window.performance) : Date.now | 3405 window.performance.now.bind(window.performance) : Date.now |
5054 }; | 3406 }; |
5055 | 3407 |
5056 /** | |
5057 * @param {HTMLElement} element | |
5058 * @constructor | |
5059 */ | |
5060 function ElementMetrics(element) { | 3408 function ElementMetrics(element) { |
5061 this.element = element; | 3409 this.element = element; |
5062 this.width = this.boundingRect.width; | 3410 this.width = this.boundingRect.width; |
5063 this.height = this.boundingRect.height; | 3411 this.height = this.boundingRect.height; |
5064 | 3412 |
5065 this.size = Math.max(this.width, this.height); | 3413 this.size = Math.max(this.width, this.height); |
5066 } | 3414 } |
5067 | 3415 |
5068 ElementMetrics.prototype = { | 3416 ElementMetrics.prototype = { |
5069 get boundingRect () { | 3417 get boundingRect () { |
5070 return this.element.getBoundingClientRect(); | 3418 return this.element.getBoundingClientRect(); |
5071 }, | 3419 }, |
5072 | 3420 |
5073 furthestCornerDistanceFrom: function(x, y) { | 3421 furthestCornerDistanceFrom: function(x, y) { |
5074 var topLeft = Utility.distance(x, y, 0, 0); | 3422 var topLeft = Utility.distance(x, y, 0, 0); |
5075 var topRight = Utility.distance(x, y, this.width, 0); | 3423 var topRight = Utility.distance(x, y, this.width, 0); |
5076 var bottomLeft = Utility.distance(x, y, 0, this.height); | 3424 var bottomLeft = Utility.distance(x, y, 0, this.height); |
5077 var bottomRight = Utility.distance(x, y, this.width, this.height); | 3425 var bottomRight = Utility.distance(x, y, this.width, this.height); |
5078 | 3426 |
5079 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | 3427 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
5080 } | 3428 } |
5081 }; | 3429 }; |
5082 | 3430 |
5083 /** | |
5084 * @param {HTMLElement} element | |
5085 * @constructor | |
5086 */ | |
5087 function Ripple(element) { | 3431 function Ripple(element) { |
5088 this.element = element; | 3432 this.element = element; |
5089 this.color = window.getComputedStyle(element).color; | 3433 this.color = window.getComputedStyle(element).color; |
5090 | 3434 |
5091 this.wave = document.createElement('div'); | 3435 this.wave = document.createElement('div'); |
5092 this.waveContainer = document.createElement('div'); | 3436 this.waveContainer = document.createElement('div'); |
5093 this.wave.style.backgroundColor = this.color; | 3437 this.wave.style.backgroundColor = this.color; |
5094 this.wave.classList.add('wave'); | 3438 this.wave.classList.add('wave'); |
5095 this.waveContainer.classList.add('wave-container'); | 3439 this.waveContainer.classList.add('wave-container'); |
5096 Polymer.dom(this.waveContainer).appendChild(this.wave); | 3440 Polymer.dom(this.waveContainer).appendChild(this.wave); |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
5170 return this.initialOpacity; | 3514 return this.initialOpacity; |
5171 } | 3515 } |
5172 | 3516 |
5173 return Math.max( | 3517 return Math.max( |
5174 0, | 3518 0, |
5175 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | 3519 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity |
5176 ); | 3520 ); |
5177 }, | 3521 }, |
5178 | 3522 |
5179 get outerOpacity() { | 3523 get outerOpacity() { |
5180 // Linear increase in background opacity, capped at the opacity | |
5181 // of the wavefront (waveOpacity). | |
5182 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | 3524 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; |
5183 var waveOpacity = this.opacity; | 3525 var waveOpacity = this.opacity; |
5184 | 3526 |
5185 return Math.max( | 3527 return Math.max( |
5186 0, | 3528 0, |
5187 Math.min(outerOpacity, waveOpacity) | 3529 Math.min(outerOpacity, waveOpacity) |
5188 ); | 3530 ); |
5189 }, | 3531 }, |
5190 | 3532 |
5191 get isOpacityFullyDecayed() { | 3533 get isOpacityFullyDecayed() { |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
5250 var dx; | 3592 var dx; |
5251 var dy; | 3593 var dy; |
5252 | 3594 |
5253 this.wave.style.opacity = this.opacity; | 3595 this.wave.style.opacity = this.opacity; |
5254 | 3596 |
5255 scale = this.radius / (this.containerMetrics.size / 2); | 3597 scale = this.radius / (this.containerMetrics.size / 2); |
5256 dx = this.xNow - (this.containerMetrics.width / 2); | 3598 dx = this.xNow - (this.containerMetrics.width / 2); |
5257 dy = this.yNow - (this.containerMetrics.height / 2); | 3599 dy = this.yNow - (this.containerMetrics.height / 2); |
5258 | 3600 |
5259 | 3601 |
5260 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
5261 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
5262 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | 3602 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; |
5263 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | 3603 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; |
5264 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | 3604 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
5265 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | 3605 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
5266 }, | 3606 }, |
5267 | 3607 |
5268 /** @param {Event=} event */ | 3608 /** @param {Event=} event */ |
5269 downAction: function(event) { | 3609 downAction: function(event) { |
5270 var xCenter = this.containerMetrics.width / 2; | 3610 var xCenter = this.containerMetrics.width / 2; |
5271 var yCenter = this.containerMetrics.height / 2; | 3611 var yCenter = this.containerMetrics.height / 2; |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
5327 }; | 3667 }; |
5328 | 3668 |
5329 Polymer({ | 3669 Polymer({ |
5330 is: 'paper-ripple', | 3670 is: 'paper-ripple', |
5331 | 3671 |
5332 behaviors: [ | 3672 behaviors: [ |
5333 Polymer.IronA11yKeysBehavior | 3673 Polymer.IronA11yKeysBehavior |
5334 ], | 3674 ], |
5335 | 3675 |
5336 properties: { | 3676 properties: { |
5337 /** | |
5338 * The initial opacity set on the wave. | |
5339 * | |
5340 * @attribute initialOpacity | |
5341 * @type number | |
5342 * @default 0.25 | |
5343 */ | |
5344 initialOpacity: { | 3677 initialOpacity: { |
5345 type: Number, | 3678 type: Number, |
5346 value: 0.25 | 3679 value: 0.25 |
5347 }, | 3680 }, |
5348 | 3681 |
5349 /** | |
5350 * How fast (opacity per second) the wave fades out. | |
5351 * | |
5352 * @attribute opacityDecayVelocity | |
5353 * @type number | |
5354 * @default 0.8 | |
5355 */ | |
5356 opacityDecayVelocity: { | 3682 opacityDecayVelocity: { |
5357 type: Number, | 3683 type: Number, |
5358 value: 0.8 | 3684 value: 0.8 |
5359 }, | 3685 }, |
5360 | 3686 |
5361 /** | |
5362 * If true, ripples will exhibit a gravitational pull towards | |
5363 * the center of their container as they fade away. | |
5364 * | |
5365 * @attribute recenters | |
5366 * @type boolean | |
5367 * @default false | |
5368 */ | |
5369 recenters: { | 3687 recenters: { |
5370 type: Boolean, | 3688 type: Boolean, |
5371 value: false | 3689 value: false |
5372 }, | 3690 }, |
5373 | 3691 |
5374 /** | |
5375 * If true, ripples will center inside its container | |
5376 * | |
5377 * @attribute recenters | |
5378 * @type boolean | |
5379 * @default false | |
5380 */ | |
5381 center: { | 3692 center: { |
5382 type: Boolean, | 3693 type: Boolean, |
5383 value: false | 3694 value: false |
5384 }, | 3695 }, |
5385 | 3696 |
5386 /** | |
5387 * A list of the visual ripples. | |
5388 * | |
5389 * @attribute ripples | |
5390 * @type Array | |
5391 * @default [] | |
5392 */ | |
5393 ripples: { | 3697 ripples: { |
5394 type: Array, | 3698 type: Array, |
5395 value: function() { | 3699 value: function() { |
5396 return []; | 3700 return []; |
5397 } | 3701 } |
5398 }, | 3702 }, |
5399 | 3703 |
5400 /** | |
5401 * True when there are visible ripples animating within the | |
5402 * element. | |
5403 */ | |
5404 animating: { | 3704 animating: { |
5405 type: Boolean, | 3705 type: Boolean, |
5406 readOnly: true, | 3706 readOnly: true, |
5407 reflectToAttribute: true, | 3707 reflectToAttribute: true, |
5408 value: false | 3708 value: false |
5409 }, | 3709 }, |
5410 | 3710 |
5411 /** | |
5412 * If true, the ripple will remain in the "down" state until `holdDown` | |
5413 * is set to false again. | |
5414 */ | |
5415 holdDown: { | 3711 holdDown: { |
5416 type: Boolean, | 3712 type: Boolean, |
5417 value: false, | 3713 value: false, |
5418 observer: '_holdDownChanged' | 3714 observer: '_holdDownChanged' |
5419 }, | 3715 }, |
5420 | 3716 |
5421 /** | |
5422 * If true, the ripple will not generate a ripple effect | |
5423 * via pointer interaction. | |
5424 * Calling ripple's imperative api like `simulatedRipple` will | |
5425 * still generate the ripple effect. | |
5426 */ | |
5427 noink: { | 3717 noink: { |
5428 type: Boolean, | 3718 type: Boolean, |
5429 value: false | 3719 value: false |
5430 }, | 3720 }, |
5431 | 3721 |
5432 _animating: { | 3722 _animating: { |
5433 type: Boolean | 3723 type: Boolean |
5434 }, | 3724 }, |
5435 | 3725 |
5436 _boundAnimate: { | 3726 _boundAnimate: { |
5437 type: Function, | 3727 type: Function, |
5438 value: function() { | 3728 value: function() { |
5439 return this.animate.bind(this); | 3729 return this.animate.bind(this); |
5440 } | 3730 } |
5441 } | 3731 } |
5442 }, | 3732 }, |
5443 | 3733 |
5444 get target () { | 3734 get target () { |
5445 return this.keyEventTarget; | 3735 return this.keyEventTarget; |
5446 }, | 3736 }, |
5447 | 3737 |
5448 keyBindings: { | 3738 keyBindings: { |
5449 'enter:keydown': '_onEnterKeydown', | 3739 'enter:keydown': '_onEnterKeydown', |
5450 'space:keydown': '_onSpaceKeydown', | 3740 'space:keydown': '_onSpaceKeydown', |
5451 'space:keyup': '_onSpaceKeyup' | 3741 'space:keyup': '_onSpaceKeyup' |
5452 }, | 3742 }, |
5453 | 3743 |
5454 attached: function() { | 3744 attached: function() { |
5455 // Set up a11yKeysBehavior to listen to key events on the target, | |
5456 // so that space and enter activate the ripple even if the target doesn'
t | |
5457 // handle key events. The key handlers deal with `noink` themselves. | |
5458 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | 3745 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE |
5459 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | 3746 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
5460 } else { | 3747 } else { |
5461 this.keyEventTarget = this.parentNode; | 3748 this.keyEventTarget = this.parentNode; |
5462 } | 3749 } |
5463 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | 3750 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); |
5464 this.listen(keyEventTarget, 'up', 'uiUpAction'); | 3751 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
5465 this.listen(keyEventTarget, 'down', 'uiDownAction'); | 3752 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
5466 }, | 3753 }, |
5467 | 3754 |
5468 detached: function() { | 3755 detached: function() { |
5469 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | 3756 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
5470 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | 3757 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
5471 this.keyEventTarget = null; | 3758 this.keyEventTarget = null; |
5472 }, | 3759 }, |
5473 | 3760 |
5474 get shouldKeepAnimating () { | 3761 get shouldKeepAnimating () { |
5475 for (var index = 0; index < this.ripples.length; ++index) { | 3762 for (var index = 0; index < this.ripples.length; ++index) { |
5476 if (!this.ripples[index].isAnimationComplete) { | 3763 if (!this.ripples[index].isAnimationComplete) { |
5477 return true; | 3764 return true; |
5478 } | 3765 } |
5479 } | 3766 } |
5480 | 3767 |
5481 return false; | 3768 return false; |
5482 }, | 3769 }, |
5483 | 3770 |
5484 simulatedRipple: function() { | 3771 simulatedRipple: function() { |
5485 this.downAction(null); | 3772 this.downAction(null); |
5486 | 3773 |
5487 // Please see polymer/polymer#1305 | |
5488 this.async(function() { | 3774 this.async(function() { |
5489 this.upAction(); | 3775 this.upAction(); |
5490 }, 1); | 3776 }, 1); |
5491 }, | 3777 }, |
5492 | 3778 |
5493 /** | |
5494 * Provokes a ripple down effect via a UI event, | |
5495 * respecting the `noink` property. | |
5496 * @param {Event=} event | |
5497 */ | |
5498 uiDownAction: function(event) { | 3779 uiDownAction: function(event) { |
5499 if (!this.noink) { | 3780 if (!this.noink) { |
5500 this.downAction(event); | 3781 this.downAction(event); |
5501 } | 3782 } |
5502 }, | 3783 }, |
5503 | 3784 |
5504 /** | |
5505 * Provokes a ripple down effect via a UI event, | |
5506 * *not* respecting the `noink` property. | |
5507 * @param {Event=} event | |
5508 */ | |
5509 downAction: function(event) { | 3785 downAction: function(event) { |
5510 if (this.holdDown && this.ripples.length > 0) { | 3786 if (this.holdDown && this.ripples.length > 0) { |
5511 return; | 3787 return; |
5512 } | 3788 } |
5513 | 3789 |
5514 var ripple = this.addRipple(); | 3790 var ripple = this.addRipple(); |
5515 | 3791 |
5516 ripple.downAction(event); | 3792 ripple.downAction(event); |
5517 | 3793 |
5518 if (!this._animating) { | 3794 if (!this._animating) { |
5519 this._animating = true; | 3795 this._animating = true; |
5520 this.animate(); | 3796 this.animate(); |
5521 } | 3797 } |
5522 }, | 3798 }, |
5523 | 3799 |
5524 /** | |
5525 * Provokes a ripple up effect via a UI event, | |
5526 * respecting the `noink` property. | |
5527 * @param {Event=} event | |
5528 */ | |
5529 uiUpAction: function(event) { | 3800 uiUpAction: function(event) { |
5530 if (!this.noink) { | 3801 if (!this.noink) { |
5531 this.upAction(event); | 3802 this.upAction(event); |
5532 } | 3803 } |
5533 }, | 3804 }, |
5534 | 3805 |
5535 /** | |
5536 * Provokes a ripple up effect via a UI event, | |
5537 * *not* respecting the `noink` property. | |
5538 * @param {Event=} event | |
5539 */ | |
5540 upAction: function(event) { | 3806 upAction: function(event) { |
5541 if (this.holdDown) { | 3807 if (this.holdDown) { |
5542 return; | 3808 return; |
5543 } | 3809 } |
5544 | 3810 |
5545 this.ripples.forEach(function(ripple) { | 3811 this.ripples.forEach(function(ripple) { |
5546 ripple.upAction(event); | 3812 ripple.upAction(event); |
5547 }); | 3813 }); |
5548 | 3814 |
5549 this._animating = true; | 3815 this._animating = true; |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
5616 }, | 3882 }, |
5617 | 3883 |
5618 _onSpaceKeydown: function() { | 3884 _onSpaceKeydown: function() { |
5619 this.uiDownAction(); | 3885 this.uiDownAction(); |
5620 }, | 3886 }, |
5621 | 3887 |
5622 _onSpaceKeyup: function() { | 3888 _onSpaceKeyup: function() { |
5623 this.uiUpAction(); | 3889 this.uiUpAction(); |
5624 }, | 3890 }, |
5625 | 3891 |
5626 // note: holdDown does not respect noink since it can be a focus based | |
5627 // effect. | |
5628 _holdDownChanged: function(newVal, oldVal) { | 3892 _holdDownChanged: function(newVal, oldVal) { |
5629 if (oldVal === undefined) { | 3893 if (oldVal === undefined) { |
5630 return; | 3894 return; |
5631 } | 3895 } |
5632 if (newVal) { | 3896 if (newVal) { |
5633 this.downAction(); | 3897 this.downAction(); |
5634 } else { | 3898 } else { |
5635 this.upAction(); | 3899 this.upAction(); |
5636 } | 3900 } |
5637 } | 3901 } |
5638 | 3902 |
5639 /** | |
5640 Fired when the animation finishes. | |
5641 This is useful if you want to wait until | |
5642 the ripple animation finishes to perform some action. | |
5643 | |
5644 @event transitionend | |
5645 @param {{node: Object}} detail Contains the animated node. | |
5646 */ | |
5647 }); | 3903 }); |
5648 })(); | 3904 })(); |
5649 /** | |
5650 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
5651 * when the element has focus via pointer or keyboard. | |
5652 * | |
5653 * NOTE: This behavior is intended to be used in conjunction with and after | |
5654 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
5655 * | |
5656 * @polymerBehavior Polymer.PaperRippleBehavior | |
5657 */ | |
5658 Polymer.PaperRippleBehavior = { | 3905 Polymer.PaperRippleBehavior = { |
5659 properties: { | 3906 properties: { |
5660 /** | |
5661 * If true, the element will not produce a ripple effect when interacted | |
5662 * with via the pointer. | |
5663 */ | |
5664 noink: { | 3907 noink: { |
5665 type: Boolean, | 3908 type: Boolean, |
5666 observer: '_noinkChanged' | 3909 observer: '_noinkChanged' |
5667 }, | 3910 }, |
5668 | 3911 |
5669 /** | |
5670 * @type {Element|undefined} | |
5671 */ | |
5672 _rippleContainer: { | 3912 _rippleContainer: { |
5673 type: Object, | 3913 type: Object, |
5674 } | 3914 } |
5675 }, | 3915 }, |
5676 | 3916 |
5677 /** | |
5678 * Ensures a `<paper-ripple>` element is available when the element is | |
5679 * focused. | |
5680 */ | |
5681 _buttonStateChanged: function() { | 3917 _buttonStateChanged: function() { |
5682 if (this.focused) { | 3918 if (this.focused) { |
5683 this.ensureRipple(); | 3919 this.ensureRipple(); |
5684 } | 3920 } |
5685 }, | 3921 }, |
5686 | 3922 |
5687 /** | |
5688 * In addition to the functionality provided in `IronButtonState`, ensures | |
5689 * a ripple effect is created when the element is in a `pressed` state. | |
5690 */ | |
5691 _downHandler: function(event) { | 3923 _downHandler: function(event) { |
5692 Polymer.IronButtonStateImpl._downHandler.call(this, event); | 3924 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
5693 if (this.pressed) { | 3925 if (this.pressed) { |
5694 this.ensureRipple(event); | 3926 this.ensureRipple(event); |
5695 } | 3927 } |
5696 }, | 3928 }, |
5697 | 3929 |
5698 /** | |
5699 * Ensures this element contains a ripple effect. For startup efficiency | |
5700 * the ripple effect is dynamically on demand when needed. | |
5701 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
5702 * ripple. | |
5703 */ | |
5704 ensureRipple: function(optTriggeringEvent) { | 3930 ensureRipple: function(optTriggeringEvent) { |
5705 if (!this.hasRipple()) { | 3931 if (!this.hasRipple()) { |
5706 this._ripple = this._createRipple(); | 3932 this._ripple = this._createRipple(); |
5707 this._ripple.noink = this.noink; | 3933 this._ripple.noink = this.noink; |
5708 var rippleContainer = this._rippleContainer || this.root; | 3934 var rippleContainer = this._rippleContainer || this.root; |
5709 if (rippleContainer) { | 3935 if (rippleContainer) { |
5710 Polymer.dom(rippleContainer).appendChild(this._ripple); | 3936 Polymer.dom(rippleContainer).appendChild(this._ripple); |
5711 } | 3937 } |
5712 if (optTriggeringEvent) { | 3938 if (optTriggeringEvent) { |
5713 // Check if the event happened inside of the ripple container | |
5714 // Fall back to host instead of the root because distributed text | |
5715 // nodes are not valid event targets | |
5716 var domContainer = Polymer.dom(this._rippleContainer || this); | 3939 var domContainer = Polymer.dom(this._rippleContainer || this); |
5717 var target = Polymer.dom(optTriggeringEvent).rootTarget; | 3940 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
5718 if (domContainer.deepContains( /** @type {Node} */(target))) { | 3941 if (domContainer.deepContains( /** @type {Node} */(target))) { |
5719 this._ripple.uiDownAction(optTriggeringEvent); | 3942 this._ripple.uiDownAction(optTriggeringEvent); |
5720 } | 3943 } |
5721 } | 3944 } |
5722 } | 3945 } |
5723 }, | 3946 }, |
5724 | 3947 |
5725 /** | |
5726 * Returns the `<paper-ripple>` element used by this element to create | |
5727 * ripple effects. The element's ripple is created on demand, when | |
5728 * necessary, and calling this method will force the | |
5729 * ripple to be created. | |
5730 */ | |
5731 getRipple: function() { | 3948 getRipple: function() { |
5732 this.ensureRipple(); | 3949 this.ensureRipple(); |
5733 return this._ripple; | 3950 return this._ripple; |
5734 }, | 3951 }, |
5735 | 3952 |
5736 /** | |
5737 * Returns true if this element currently contains a ripple effect. | |
5738 * @return {boolean} | |
5739 */ | |
5740 hasRipple: function() { | 3953 hasRipple: function() { |
5741 return Boolean(this._ripple); | 3954 return Boolean(this._ripple); |
5742 }, | 3955 }, |
5743 | 3956 |
5744 /** | |
5745 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
5746 * Override this method to customize the ripple element. | |
5747 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
5748 */ | |
5749 _createRipple: function() { | 3957 _createRipple: function() { |
5750 return /** @type {!PaperRippleElement} */ ( | 3958 return /** @type {!PaperRippleElement} */ ( |
5751 document.createElement('paper-ripple')); | 3959 document.createElement('paper-ripple')); |
5752 }, | 3960 }, |
5753 | 3961 |
5754 _noinkChanged: function(noink) { | 3962 _noinkChanged: function(noink) { |
5755 if (this.hasRipple()) { | 3963 if (this.hasRipple()) { |
5756 this._ripple.noink = noink; | 3964 this._ripple.noink = noink; |
5757 } | 3965 } |
5758 } | 3966 } |
5759 }; | 3967 }; |
5760 /** @polymerBehavior Polymer.PaperButtonBehavior */ | 3968 /** @polymerBehavior Polymer.PaperButtonBehavior */ |
5761 Polymer.PaperButtonBehaviorImpl = { | 3969 Polymer.PaperButtonBehaviorImpl = { |
5762 properties: { | 3970 properties: { |
5763 /** | |
5764 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
5765 * shadow, and each increasing number greater than 0 will be "deeper" | |
5766 * than the last. | |
5767 * | |
5768 * @attribute elevation | |
5769 * @type number | |
5770 * @default 1 | |
5771 */ | |
5772 elevation: { | 3971 elevation: { |
5773 type: Number, | 3972 type: Number, |
5774 reflectToAttribute: true, | 3973 reflectToAttribute: true, |
5775 readOnly: true | 3974 readOnly: true |
5776 } | 3975 } |
5777 }, | 3976 }, |
5778 | 3977 |
5779 observers: [ | 3978 observers: [ |
5780 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | 3979 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', |
5781 '_computeKeyboardClass(receivedFocusFromKeyboard)' | 3980 '_computeKeyboardClass(receivedFocusFromKeyboard)' |
(...skipping 14 matching lines...) Expand all Loading... |
5796 } else if (this.receivedFocusFromKeyboard) { | 3995 } else if (this.receivedFocusFromKeyboard) { |
5797 e = 3; | 3996 e = 3; |
5798 } | 3997 } |
5799 this._setElevation(e); | 3998 this._setElevation(e); |
5800 }, | 3999 }, |
5801 | 4000 |
5802 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | 4001 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
5803 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | 4002 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
5804 }, | 4003 }, |
5805 | 4004 |
5806 /** | |
5807 * In addition to `IronButtonState` behavior, when space key goes down, | |
5808 * create a ripple down effect. | |
5809 * | |
5810 * @param {!KeyboardEvent} event . | |
5811 */ | |
5812 _spaceKeyDownHandler: function(event) { | 4005 _spaceKeyDownHandler: function(event) { |
5813 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | 4006 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
5814 // Ensure that there is at most one ripple when the space key is held down
. | |
5815 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | 4007 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
5816 this._ripple.uiDownAction(); | 4008 this._ripple.uiDownAction(); |
5817 } | 4009 } |
5818 }, | 4010 }, |
5819 | 4011 |
5820 /** | |
5821 * In addition to `IronButtonState` behavior, when space key goes up, | |
5822 * create a ripple up effect. | |
5823 * | |
5824 * @param {!KeyboardEvent} event . | |
5825 */ | |
5826 _spaceKeyUpHandler: function(event) { | 4012 _spaceKeyUpHandler: function(event) { |
5827 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | 4013 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
5828 if (this.hasRipple()) { | 4014 if (this.hasRipple()) { |
5829 this._ripple.uiUpAction(); | 4015 this._ripple.uiUpAction(); |
5830 } | 4016 } |
5831 } | 4017 } |
5832 }; | 4018 }; |
5833 | 4019 |
5834 /** @polymerBehavior */ | 4020 /** @polymerBehavior */ |
5835 Polymer.PaperButtonBehavior = [ | 4021 Polymer.PaperButtonBehavior = [ |
5836 Polymer.IronButtonState, | 4022 Polymer.IronButtonState, |
5837 Polymer.IronControlState, | 4023 Polymer.IronControlState, |
5838 Polymer.PaperRippleBehavior, | 4024 Polymer.PaperRippleBehavior, |
5839 Polymer.PaperButtonBehaviorImpl | 4025 Polymer.PaperButtonBehaviorImpl |
5840 ]; | 4026 ]; |
5841 Polymer({ | 4027 Polymer({ |
5842 is: 'paper-button', | 4028 is: 'paper-button', |
5843 | 4029 |
5844 behaviors: [ | 4030 behaviors: [ |
5845 Polymer.PaperButtonBehavior | 4031 Polymer.PaperButtonBehavior |
5846 ], | 4032 ], |
5847 | 4033 |
5848 properties: { | 4034 properties: { |
5849 /** | |
5850 * If true, the button should be styled with a shadow. | |
5851 */ | |
5852 raised: { | 4035 raised: { |
5853 type: Boolean, | 4036 type: Boolean, |
5854 reflectToAttribute: true, | 4037 reflectToAttribute: true, |
5855 value: false, | 4038 value: false, |
5856 observer: '_calculateElevation' | 4039 observer: '_calculateElevation' |
5857 } | 4040 } |
5858 }, | 4041 }, |
5859 | 4042 |
5860 _calculateElevation: function() { | 4043 _calculateElevation: function() { |
5861 if (!this.raised) { | 4044 if (!this.raised) { |
5862 this._setElevation(0); | 4045 this._setElevation(0); |
5863 } else { | 4046 } else { |
5864 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | 4047 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
5865 } | 4048 } |
5866 } | 4049 } |
5867 | 4050 |
5868 /** | |
5869 Fired when the animation finishes. | |
5870 This is useful if you want to wait until | |
5871 the ripple animation finishes to perform some action. | |
5872 | |
5873 @event transitionend | |
5874 Event param: {{node: Object}} detail Contains the animated node. | |
5875 */ | |
5876 }); | 4051 }); |
5877 Polymer({ | 4052 Polymer({ |
5878 is: 'paper-icon-button-light', | 4053 is: 'paper-icon-button-light', |
5879 extends: 'button', | 4054 extends: 'button', |
5880 | 4055 |
5881 behaviors: [ | 4056 behaviors: [ |
5882 Polymer.PaperRippleBehavior | 4057 Polymer.PaperRippleBehavior |
5883 ], | 4058 ], |
5884 | 4059 |
5885 listeners: { | 4060 listeners: { |
5886 'down': '_rippleDown', | 4061 'down': '_rippleDown', |
5887 'up': '_rippleUp', | 4062 'up': '_rippleUp', |
5888 'focus': '_rippleDown', | 4063 'focus': '_rippleDown', |
5889 'blur': '_rippleUp', | 4064 'blur': '_rippleUp', |
5890 }, | 4065 }, |
5891 | 4066 |
5892 _rippleDown: function() { | 4067 _rippleDown: function() { |
5893 this.getRipple().downAction(); | 4068 this.getRipple().downAction(); |
5894 }, | 4069 }, |
5895 | 4070 |
5896 _rippleUp: function() { | 4071 _rippleUp: function() { |
5897 this.getRipple().upAction(); | 4072 this.getRipple().upAction(); |
5898 }, | 4073 }, |
5899 | 4074 |
5900 /** | |
5901 * @param {...*} var_args | |
5902 */ | |
5903 ensureRipple: function(var_args) { | 4075 ensureRipple: function(var_args) { |
5904 var lastRipple = this._ripple; | 4076 var lastRipple = this._ripple; |
5905 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | 4077 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
5906 if (this._ripple && this._ripple !== lastRipple) { | 4078 if (this._ripple && this._ripple !== lastRipple) { |
5907 this._ripple.center = true; | 4079 this._ripple.center = true; |
5908 this._ripple.classList.add('circle'); | 4080 this._ripple.classList.add('circle'); |
5909 } | 4081 } |
5910 } | 4082 } |
5911 }); | 4083 }); |
5912 /** | |
5913 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | |
5914 * | |
5915 * @demo demo/index.html | |
5916 * @polymerBehavior | |
5917 */ | |
5918 Polymer.IronRangeBehavior = { | 4084 Polymer.IronRangeBehavior = { |
5919 | 4085 |
5920 properties: { | 4086 properties: { |
5921 | 4087 |
5922 /** | |
5923 * The number that represents the current value. | |
5924 */ | |
5925 value: { | 4088 value: { |
5926 type: Number, | 4089 type: Number, |
5927 value: 0, | 4090 value: 0, |
5928 notify: true, | 4091 notify: true, |
5929 reflectToAttribute: true | 4092 reflectToAttribute: true |
5930 }, | 4093 }, |
5931 | 4094 |
5932 /** | |
5933 * The number that indicates the minimum value of the range. | |
5934 */ | |
5935 min: { | 4095 min: { |
5936 type: Number, | 4096 type: Number, |
5937 value: 0, | 4097 value: 0, |
5938 notify: true | 4098 notify: true |
5939 }, | 4099 }, |
5940 | 4100 |
5941 /** | |
5942 * The number that indicates the maximum value of the range. | |
5943 */ | |
5944 max: { | 4101 max: { |
5945 type: Number, | 4102 type: Number, |
5946 value: 100, | 4103 value: 100, |
5947 notify: true | 4104 notify: true |
5948 }, | 4105 }, |
5949 | 4106 |
5950 /** | |
5951 * Specifies the value granularity of the range's value. | |
5952 */ | |
5953 step: { | 4107 step: { |
5954 type: Number, | 4108 type: Number, |
5955 value: 1, | 4109 value: 1, |
5956 notify: true | 4110 notify: true |
5957 }, | 4111 }, |
5958 | 4112 |
5959 /** | |
5960 * Returns the ratio of the value. | |
5961 */ | |
5962 ratio: { | 4113 ratio: { |
5963 type: Number, | 4114 type: Number, |
5964 value: 0, | 4115 value: 0, |
5965 readOnly: true, | 4116 readOnly: true, |
5966 notify: true | 4117 notify: true |
5967 }, | 4118 }, |
5968 }, | 4119 }, |
5969 | 4120 |
5970 observers: [ | 4121 observers: [ |
5971 '_update(value, min, max, step)' | 4122 '_update(value, min, max, step)' |
5972 ], | 4123 ], |
5973 | 4124 |
5974 _calcRatio: function(value) { | 4125 _calcRatio: function(value) { |
5975 return (this._clampValue(value) - this.min) / (this.max - this.min); | 4126 return (this._clampValue(value) - this.min) / (this.max - this.min); |
5976 }, | 4127 }, |
5977 | 4128 |
5978 _clampValue: function(value) { | 4129 _clampValue: function(value) { |
5979 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | 4130 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); |
5980 }, | 4131 }, |
5981 | 4132 |
5982 _calcStep: function(value) { | 4133 _calcStep: function(value) { |
5983 // polymer/issues/2493 | |
5984 value = parseFloat(value); | 4134 value = parseFloat(value); |
5985 | 4135 |
5986 if (!this.step) { | 4136 if (!this.step) { |
5987 return value; | 4137 return value; |
5988 } | 4138 } |
5989 | 4139 |
5990 var numSteps = Math.round((value - this.min) / this.step); | 4140 var numSteps = Math.round((value - this.min) / this.step); |
5991 if (this.step < 1) { | 4141 if (this.step < 1) { |
5992 /** | |
5993 * For small values of this.step, if we calculate the step using | |
5994 * `Math.round(value / step) * step` we may hit a precision point issue | |
5995 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
5996 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
5997 * | |
5998 * as a work around we can divide by the reciprocal of `step` | |
5999 */ | |
6000 return numSteps / (1 / this.step) + this.min; | 4142 return numSteps / (1 / this.step) + this.min; |
6001 } else { | 4143 } else { |
6002 return numSteps * this.step + this.min; | 4144 return numSteps * this.step + this.min; |
6003 } | 4145 } |
6004 }, | 4146 }, |
6005 | 4147 |
6006 _validateValue: function() { | 4148 _validateValue: function() { |
6007 var v = this._clampValue(this.value); | 4149 var v = this._clampValue(this.value); |
6008 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | 4150 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; |
6009 return this.value !== v; | 4151 return this.value !== v; |
6010 }, | 4152 }, |
6011 | 4153 |
6012 _update: function() { | 4154 _update: function() { |
6013 this._validateValue(); | 4155 this._validateValue(); |
6014 this._setRatio(this._calcRatio(this.value) * 100); | 4156 this._setRatio(this._calcRatio(this.value) * 100); |
6015 } | 4157 } |
6016 | 4158 |
6017 }; | 4159 }; |
6018 Polymer({ | 4160 Polymer({ |
6019 is: 'paper-progress', | 4161 is: 'paper-progress', |
6020 | 4162 |
6021 behaviors: [ | 4163 behaviors: [ |
6022 Polymer.IronRangeBehavior | 4164 Polymer.IronRangeBehavior |
6023 ], | 4165 ], |
6024 | 4166 |
6025 properties: { | 4167 properties: { |
6026 /** | |
6027 * The number that represents the current secondary progress. | |
6028 */ | |
6029 secondaryProgress: { | 4168 secondaryProgress: { |
6030 type: Number, | 4169 type: Number, |
6031 value: 0 | 4170 value: 0 |
6032 }, | 4171 }, |
6033 | 4172 |
6034 /** | |
6035 * The secondary ratio | |
6036 */ | |
6037 secondaryRatio: { | 4173 secondaryRatio: { |
6038 type: Number, | 4174 type: Number, |
6039 value: 0, | 4175 value: 0, |
6040 readOnly: true | 4176 readOnly: true |
6041 }, | 4177 }, |
6042 | 4178 |
6043 /** | |
6044 * Use an indeterminate progress indicator. | |
6045 */ | |
6046 indeterminate: { | 4179 indeterminate: { |
6047 type: Boolean, | 4180 type: Boolean, |
6048 value: false, | 4181 value: false, |
6049 observer: '_toggleIndeterminate' | 4182 observer: '_toggleIndeterminate' |
6050 }, | 4183 }, |
6051 | 4184 |
6052 /** | |
6053 * True if the progress is disabled. | |
6054 */ | |
6055 disabled: { | 4185 disabled: { |
6056 type: Boolean, | 4186 type: Boolean, |
6057 value: false, | 4187 value: false, |
6058 reflectToAttribute: true, | 4188 reflectToAttribute: true, |
6059 observer: '_disabledChanged' | 4189 observer: '_disabledChanged' |
6060 } | 4190 } |
6061 }, | 4191 }, |
6062 | 4192 |
6063 observers: [ | 4193 observers: [ |
6064 '_progressChanged(secondaryProgress, value, min, max)' | 4194 '_progressChanged(secondaryProgress, value, min, max)' |
6065 ], | 4195 ], |
6066 | 4196 |
6067 hostAttributes: { | 4197 hostAttributes: { |
6068 role: 'progressbar' | 4198 role: 'progressbar' |
6069 }, | 4199 }, |
6070 | 4200 |
6071 _toggleIndeterminate: function(indeterminate) { | 4201 _toggleIndeterminate: function(indeterminate) { |
6072 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | |
6073 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | |
6074 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 4202 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); |
6075 }, | 4203 }, |
6076 | 4204 |
6077 _transformProgress: function(progress, ratio) { | 4205 _transformProgress: function(progress, ratio) { |
6078 var transform = 'scaleX(' + (ratio / 100) + ')'; | 4206 var transform = 'scaleX(' + (ratio / 100) + ')'; |
6079 progress.style.transform = progress.style.webkitTransform = transform; | 4207 progress.style.transform = progress.style.webkitTransform = transform; |
6080 }, | 4208 }, |
6081 | 4209 |
6082 _mainRatioChanged: function(ratio) { | 4210 _mainRatioChanged: function(ratio) { |
6083 this._transformProgress(this.$.primaryProgress, ratio); | 4211 this._transformProgress(this.$.primaryProgress, ratio); |
(...skipping 18 matching lines...) Expand all Loading... |
6102 }, | 4230 }, |
6103 | 4231 |
6104 _disabledChanged: function(disabled) { | 4232 _disabledChanged: function(disabled) { |
6105 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 4233 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
6106 }, | 4234 }, |
6107 | 4235 |
6108 _hideSecondaryProgress: function(secondaryRatio) { | 4236 _hideSecondaryProgress: function(secondaryRatio) { |
6109 return secondaryRatio === 0; | 4237 return secondaryRatio === 0; |
6110 } | 4238 } |
6111 }); | 4239 }); |
6112 /** | |
6113 * The `iron-iconset-svg` element allows users to define their own icon sets | |
6114 * that contain svg icons. The svg icon elements should be children of the | |
6115 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | |
6116 * | |
6117 * Using svg elements to create icons has a few advantages over traditional | |
6118 * bitmap graphics like jpg or png. Icons that use svg are vector based so | |
6119 * they are resolution independent and should look good on any device. They | |
6120 * are stylable via css. Icons can be themed, colorized, and even animated. | |
6121 * | |
6122 * Example: | |
6123 * | |
6124 * <iron-iconset-svg name="my-svg-icons" size="24"> | |
6125 * <svg> | |
6126 * <defs> | |
6127 * <g id="shape"> | |
6128 * <rect x="12" y="0" width="12" height="24" /> | |
6129 * <circle cx="12" cy="12" r="12" /> | |
6130 * </g> | |
6131 * </defs> | |
6132 * </svg> | |
6133 * </iron-iconset-svg> | |
6134 * | |
6135 * This will automatically register the icon set "my-svg-icons" to the iconset | |
6136 * database. To use these icons from within another element, make a | |
6137 * `iron-iconset` element and call the `byId` method | |
6138 * to retrieve a given iconset. To apply a particular icon inside an | |
6139 * element use the `applyIcon` method. For example: | |
6140 * | |
6141 * iconset.applyIcon(iconNode, 'car'); | |
6142 * | |
6143 * @element iron-iconset-svg | |
6144 * @demo demo/index.html | |
6145 * @implements {Polymer.Iconset} | |
6146 */ | |
6147 Polymer({ | 4240 Polymer({ |
6148 is: 'iron-iconset-svg', | 4241 is: 'iron-iconset-svg', |
6149 | 4242 |
6150 properties: { | 4243 properties: { |
6151 | 4244 |
6152 /** | |
6153 * The name of the iconset. | |
6154 */ | |
6155 name: { | 4245 name: { |
6156 type: String, | 4246 type: String, |
6157 observer: '_nameChanged' | 4247 observer: '_nameChanged' |
6158 }, | 4248 }, |
6159 | 4249 |
6160 /** | |
6161 * The size of an individual icon. Note that icons must be square. | |
6162 */ | |
6163 size: { | 4250 size: { |
6164 type: Number, | 4251 type: Number, |
6165 value: 24 | 4252 value: 24 |
6166 } | 4253 } |
6167 | 4254 |
6168 }, | 4255 }, |
6169 | 4256 |
6170 attached: function() { | 4257 attached: function() { |
6171 this.style.display = 'none'; | 4258 this.style.display = 'none'; |
6172 }, | 4259 }, |
6173 | 4260 |
6174 /** | |
6175 * Construct an array of all icon names in this iconset. | |
6176 * | |
6177 * @return {!Array} Array of icon names. | |
6178 */ | |
6179 getIconNames: function() { | 4261 getIconNames: function() { |
6180 this._icons = this._createIconMap(); | 4262 this._icons = this._createIconMap(); |
6181 return Object.keys(this._icons).map(function(n) { | 4263 return Object.keys(this._icons).map(function(n) { |
6182 return this.name + ':' + n; | 4264 return this.name + ':' + n; |
6183 }, this); | 4265 }, this); |
6184 }, | 4266 }, |
6185 | 4267 |
6186 /** | |
6187 * Applies an icon to the given element. | |
6188 * | |
6189 * An svg icon is prepended to the element's shadowRoot if it exists, | |
6190 * otherwise to the element itself. | |
6191 * | |
6192 * @method applyIcon | |
6193 * @param {Element} element Element to which the icon is applied. | |
6194 * @param {string} iconName Name of the icon to apply. | |
6195 * @return {?Element} The svg element which renders the icon. | |
6196 */ | |
6197 applyIcon: function(element, iconName) { | 4268 applyIcon: function(element, iconName) { |
6198 // insert svg element into shadow root, if it exists | |
6199 element = element.root || element; | 4269 element = element.root || element; |
6200 // Remove old svg element | |
6201 this.removeIcon(element); | 4270 this.removeIcon(element); |
6202 // install new svg element | |
6203 var svg = this._cloneIcon(iconName); | 4271 var svg = this._cloneIcon(iconName); |
6204 if (svg) { | 4272 if (svg) { |
6205 var pde = Polymer.dom(element); | 4273 var pde = Polymer.dom(element); |
6206 pde.insertBefore(svg, pde.childNodes[0]); | 4274 pde.insertBefore(svg, pde.childNodes[0]); |
6207 return element._svgIcon = svg; | 4275 return element._svgIcon = svg; |
6208 } | 4276 } |
6209 return null; | 4277 return null; |
6210 }, | 4278 }, |
6211 | 4279 |
6212 /** | |
6213 * Remove an icon from the given element by undoing the changes effected | |
6214 * by `applyIcon`. | |
6215 * | |
6216 * @param {Element} element The element from which the icon is removed. | |
6217 */ | |
6218 removeIcon: function(element) { | 4280 removeIcon: function(element) { |
6219 // Remove old svg element | |
6220 if (element._svgIcon) { | 4281 if (element._svgIcon) { |
6221 Polymer.dom(element).removeChild(element._svgIcon); | 4282 Polymer.dom(element).removeChild(element._svgIcon); |
6222 element._svgIcon = null; | 4283 element._svgIcon = null; |
6223 } | 4284 } |
6224 }, | 4285 }, |
6225 | 4286 |
6226 /** | |
6227 * | |
6228 * When name is changed, register iconset metadata | |
6229 * | |
6230 */ | |
6231 _nameChanged: function() { | 4287 _nameChanged: function() { |
6232 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | 4288 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); |
6233 this.async(function() { | 4289 this.async(function() { |
6234 this.fire('iron-iconset-added', this, {node: window}); | 4290 this.fire('iron-iconset-added', this, {node: window}); |
6235 }); | 4291 }); |
6236 }, | 4292 }, |
6237 | 4293 |
6238 /** | |
6239 * Create a map of child SVG elements by id. | |
6240 * | |
6241 * @return {!Object} Map of id's to SVG elements. | |
6242 */ | |
6243 _createIconMap: function() { | 4294 _createIconMap: function() { |
6244 // Objects chained to Object.prototype (`{}`) have members. Specifically, | |
6245 // on FF there is a `watch` method that confuses the icon map, so we | |
6246 // need to use a null-based object here. | |
6247 var icons = Object.create(null); | 4295 var icons = Object.create(null); |
6248 Polymer.dom(this).querySelectorAll('[id]') | 4296 Polymer.dom(this).querySelectorAll('[id]') |
6249 .forEach(function(icon) { | 4297 .forEach(function(icon) { |
6250 icons[icon.id] = icon; | 4298 icons[icon.id] = icon; |
6251 }); | 4299 }); |
6252 return icons; | 4300 return icons; |
6253 }, | 4301 }, |
6254 | 4302 |
6255 /** | |
6256 * Produce installable clone of the SVG element matching `id` in this | |
6257 * iconset, or `undefined` if there is no matching element. | |
6258 * | |
6259 * @return {Element} Returns an installable clone of the SVG element | |
6260 * matching `id`. | |
6261 */ | |
6262 _cloneIcon: function(id) { | 4303 _cloneIcon: function(id) { |
6263 // create the icon map on-demand, since the iconset itself has no discrete | |
6264 // signal to know when it's children are fully parsed | |
6265 this._icons = this._icons || this._createIconMap(); | 4304 this._icons = this._icons || this._createIconMap(); |
6266 return this._prepareSvgClone(this._icons[id], this.size); | 4305 return this._prepareSvgClone(this._icons[id], this.size); |
6267 }, | 4306 }, |
6268 | 4307 |
6269 /** | |
6270 * @param {Element} sourceSvg | |
6271 * @param {number} size | |
6272 * @return {Element} | |
6273 */ | |
6274 _prepareSvgClone: function(sourceSvg, size) { | 4308 _prepareSvgClone: function(sourceSvg, size) { |
6275 if (sourceSvg) { | 4309 if (sourceSvg) { |
6276 var content = sourceSvg.cloneNode(true), | 4310 var content = sourceSvg.cloneNode(true), |
6277 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | 4311 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), |
6278 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | 4312 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; |
6279 svg.setAttribute('viewBox', viewBox); | 4313 svg.setAttribute('viewBox', viewBox); |
6280 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | 4314 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
6281 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
6282 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
6283 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | 4315 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; |
6284 svg.appendChild(content).removeAttribute('id'); | 4316 svg.appendChild(content).removeAttribute('id'); |
6285 return svg; | 4317 return svg; |
6286 } | 4318 } |
6287 return null; | 4319 return null; |
6288 } | 4320 } |
6289 | 4321 |
6290 }); | 4322 }); |
6291 // Copyright 2015 The Chromium Authors. All rights reserved. | 4323 // Copyright 2015 The Chromium Authors. All rights reserved. |
6292 // Use of this source code is governed by a BSD-style license that can be | 4324 // Use of this source code is governed by a BSD-style license that can be |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
6351 }, | 4383 }, |
6352 | 4384 |
6353 showProgress_: { | 4385 showProgress_: { |
6354 computed: 'computeShowProgress_(showCancel_, data.percent)', | 4386 computed: 'computeShowProgress_(showCancel_, data.percent)', |
6355 type: Boolean, | 4387 type: Boolean, |
6356 value: false, | 4388 value: false, |
6357 }, | 4389 }, |
6358 }, | 4390 }, |
6359 | 4391 |
6360 observers: [ | 4392 observers: [ |
6361 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | |
6362 // and data.by_ext_name directly. Why? | |
6363 'observeControlledBy_(controlledBy_)', | 4393 'observeControlledBy_(controlledBy_)', |
6364 'observeIsDangerous_(isDangerous_, data)', | 4394 'observeIsDangerous_(isDangerous_, data)', |
6365 ], | 4395 ], |
6366 | 4396 |
6367 ready: function() { | 4397 ready: function() { |
6368 this.content = this.$.content; | 4398 this.content = this.$.content; |
6369 }, | 4399 }, |
6370 | 4400 |
6371 /** @private */ | 4401 /** @private */ |
6372 computeClass_: function() { | 4402 computeClass_: function() { |
(...skipping 180 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
6553 /** @private */ | 4583 /** @private */ |
6554 onCancelTap_: function() { | 4584 onCancelTap_: function() { |
6555 downloads.ActionService.getInstance().cancel(this.data.id); | 4585 downloads.ActionService.getInstance().cancel(this.data.id); |
6556 }, | 4586 }, |
6557 | 4587 |
6558 /** @private */ | 4588 /** @private */ |
6559 onDiscardDangerousTap_: function() { | 4589 onDiscardDangerousTap_: function() { |
6560 downloads.ActionService.getInstance().discardDangerous(this.data.id); | 4590 downloads.ActionService.getInstance().discardDangerous(this.data.id); |
6561 }, | 4591 }, |
6562 | 4592 |
6563 /** | |
6564 * @private | |
6565 * @param {Event} e | |
6566 */ | |
6567 onDragStart_: function(e) { | 4593 onDragStart_: function(e) { |
6568 e.preventDefault(); | 4594 e.preventDefault(); |
6569 downloads.ActionService.getInstance().drag(this.data.id); | 4595 downloads.ActionService.getInstance().drag(this.data.id); |
6570 }, | 4596 }, |
6571 | 4597 |
6572 /** | |
6573 * @param {Event} e | |
6574 * @private | |
6575 */ | |
6576 onFileLinkTap_: function(e) { | 4598 onFileLinkTap_: function(e) { |
6577 e.preventDefault(); | 4599 e.preventDefault(); |
6578 downloads.ActionService.getInstance().openFile(this.data.id); | 4600 downloads.ActionService.getInstance().openFile(this.data.id); |
6579 }, | 4601 }, |
6580 | 4602 |
6581 /** @private */ | 4603 /** @private */ |
6582 onPauseOrResumeTap_: function() { | 4604 onPauseOrResumeTap_: function() { |
6583 if (this.isInProgress_) | 4605 if (this.isInProgress_) |
6584 downloads.ActionService.getInstance().pause(this.data.id); | 4606 downloads.ActionService.getInstance().pause(this.data.id); |
6585 else | 4607 else |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
6623 Polymer.IronControlState, | 4645 Polymer.IronControlState, |
6624 Polymer.PaperItemBehaviorImpl | 4646 Polymer.PaperItemBehaviorImpl |
6625 ]; | 4647 ]; |
6626 Polymer({ | 4648 Polymer({ |
6627 is: 'paper-item', | 4649 is: 'paper-item', |
6628 | 4650 |
6629 behaviors: [ | 4651 behaviors: [ |
6630 Polymer.PaperItemBehavior | 4652 Polymer.PaperItemBehavior |
6631 ] | 4653 ] |
6632 }); | 4654 }); |
6633 /** | |
6634 * @param {!Function} selectCallback | |
6635 * @constructor | |
6636 */ | |
6637 Polymer.IronSelection = function(selectCallback) { | 4655 Polymer.IronSelection = function(selectCallback) { |
6638 this.selection = []; | 4656 this.selection = []; |
6639 this.selectCallback = selectCallback; | 4657 this.selectCallback = selectCallback; |
6640 }; | 4658 }; |
6641 | 4659 |
6642 Polymer.IronSelection.prototype = { | 4660 Polymer.IronSelection.prototype = { |
6643 | 4661 |
6644 /** | |
6645 * Retrieves the selected item(s). | |
6646 * | |
6647 * @method get | |
6648 * @returns Returns the selected item(s). If the multi property is true, | |
6649 * `get` will return an array, otherwise it will return | |
6650 * the selected item or undefined if there is no selection. | |
6651 */ | |
6652 get: function() { | 4662 get: function() { |
6653 return this.multi ? this.selection.slice() : this.selection[0]; | 4663 return this.multi ? this.selection.slice() : this.selection[0]; |
6654 }, | 4664 }, |
6655 | 4665 |
6656 /** | |
6657 * Clears all the selection except the ones indicated. | |
6658 * | |
6659 * @method clear | |
6660 * @param {Array} excludes items to be excluded. | |
6661 */ | |
6662 clear: function(excludes) { | 4666 clear: function(excludes) { |
6663 this.selection.slice().forEach(function(item) { | 4667 this.selection.slice().forEach(function(item) { |
6664 if (!excludes || excludes.indexOf(item) < 0) { | 4668 if (!excludes || excludes.indexOf(item) < 0) { |
6665 this.setItemSelected(item, false); | 4669 this.setItemSelected(item, false); |
6666 } | 4670 } |
6667 }, this); | 4671 }, this); |
6668 }, | 4672 }, |
6669 | 4673 |
6670 /** | |
6671 * Indicates if a given item is selected. | |
6672 * | |
6673 * @method isSelected | |
6674 * @param {*} item The item whose selection state should be checked. | |
6675 * @returns Returns true if `item` is selected. | |
6676 */ | |
6677 isSelected: function(item) { | 4674 isSelected: function(item) { |
6678 return this.selection.indexOf(item) >= 0; | 4675 return this.selection.indexOf(item) >= 0; |
6679 }, | 4676 }, |
6680 | 4677 |
6681 /** | |
6682 * Sets the selection state for a given item to either selected or deselecte
d. | |
6683 * | |
6684 * @method setItemSelected | |
6685 * @param {*} item The item to select. | |
6686 * @param {boolean} isSelected True for selected, false for deselected. | |
6687 */ | |
6688 setItemSelected: function(item, isSelected) { | 4678 setItemSelected: function(item, isSelected) { |
6689 if (item != null) { | 4679 if (item != null) { |
6690 if (isSelected !== this.isSelected(item)) { | 4680 if (isSelected !== this.isSelected(item)) { |
6691 // proceed to update selection only if requested state differs from cu
rrent | |
6692 if (isSelected) { | 4681 if (isSelected) { |
6693 this.selection.push(item); | 4682 this.selection.push(item); |
6694 } else { | 4683 } else { |
6695 var i = this.selection.indexOf(item); | 4684 var i = this.selection.indexOf(item); |
6696 if (i >= 0) { | 4685 if (i >= 0) { |
6697 this.selection.splice(i, 1); | 4686 this.selection.splice(i, 1); |
6698 } | 4687 } |
6699 } | 4688 } |
6700 if (this.selectCallback) { | 4689 if (this.selectCallback) { |
6701 this.selectCallback(item, isSelected); | 4690 this.selectCallback(item, isSelected); |
6702 } | 4691 } |
6703 } | 4692 } |
6704 } | 4693 } |
6705 }, | 4694 }, |
6706 | 4695 |
6707 /** | |
6708 * Sets the selection state for a given item. If the `multi` property | |
6709 * is true, then the selected state of `item` will be toggled; otherwise | |
6710 * the `item` will be selected. | |
6711 * | |
6712 * @method select | |
6713 * @param {*} item The item to select. | |
6714 */ | |
6715 select: function(item) { | 4696 select: function(item) { |
6716 if (this.multi) { | 4697 if (this.multi) { |
6717 this.toggle(item); | 4698 this.toggle(item); |
6718 } else if (this.get() !== item) { | 4699 } else if (this.get() !== item) { |
6719 this.setItemSelected(this.get(), false); | 4700 this.setItemSelected(this.get(), false); |
6720 this.setItemSelected(item, true); | 4701 this.setItemSelected(item, true); |
6721 } | 4702 } |
6722 }, | 4703 }, |
6723 | 4704 |
6724 /** | |
6725 * Toggles the selection state for `item`. | |
6726 * | |
6727 * @method toggle | |
6728 * @param {*} item The item to toggle. | |
6729 */ | |
6730 toggle: function(item) { | 4705 toggle: function(item) { |
6731 this.setItemSelected(item, !this.isSelected(item)); | 4706 this.setItemSelected(item, !this.isSelected(item)); |
6732 } | 4707 } |
6733 | 4708 |
6734 }; | 4709 }; |
6735 /** @polymerBehavior */ | 4710 /** @polymerBehavior */ |
6736 Polymer.IronSelectableBehavior = { | 4711 Polymer.IronSelectableBehavior = { |
6737 | 4712 |
6738 /** | |
6739 * Fired when iron-selector is activated (selected or deselected). | |
6740 * It is fired before the selected items are changed. | |
6741 * Cancel the event to abort selection. | |
6742 * | |
6743 * @event iron-activate | |
6744 */ | |
6745 | 4713 |
6746 /** | |
6747 * Fired when an item is selected | |
6748 * | |
6749 * @event iron-select | |
6750 */ | |
6751 | 4714 |
6752 /** | |
6753 * Fired when an item is deselected | |
6754 * | |
6755 * @event iron-deselect | |
6756 */ | |
6757 | 4715 |
6758 /** | |
6759 * Fired when the list of selectable items changes (e.g., items are | |
6760 * added or removed). The detail of the event is a mutation record that | |
6761 * describes what changed. | |
6762 * | |
6763 * @event iron-items-changed | |
6764 */ | |
6765 | 4716 |
6766 properties: { | 4717 properties: { |
6767 | 4718 |
6768 /** | |
6769 * If you want to use an attribute value or property of an element for | |
6770 * `selected` instead of the index, set this to the name of the attribute | |
6771 * or property. Hyphenated values are converted to camel case when used to | |
6772 * look up the property of a selectable element. Camel cased values are | |
6773 * *not* converted to hyphenated values for attribute lookup. It's | |
6774 * recommended that you provide the hyphenated form of the name so that | |
6775 * selection works in both cases. (Use `attr-or-property-name` instead of | |
6776 * `attrOrPropertyName`.) | |
6777 */ | |
6778 attrForSelected: { | 4719 attrForSelected: { |
6779 type: String, | 4720 type: String, |
6780 value: null | 4721 value: null |
6781 }, | 4722 }, |
6782 | 4723 |
6783 /** | |
6784 * Gets or sets the selected element. The default is to use the index of t
he item. | |
6785 * @type {string|number} | |
6786 */ | |
6787 selected: { | 4724 selected: { |
6788 type: String, | 4725 type: String, |
6789 notify: true | 4726 notify: true |
6790 }, | 4727 }, |
6791 | 4728 |
6792 /** | |
6793 * Returns the currently selected item. | |
6794 * | |
6795 * @type {?Object} | |
6796 */ | |
6797 selectedItem: { | 4729 selectedItem: { |
6798 type: Object, | 4730 type: Object, |
6799 readOnly: true, | 4731 readOnly: true, |
6800 notify: true | 4732 notify: true |
6801 }, | 4733 }, |
6802 | 4734 |
6803 /** | |
6804 * The event that fires from items when they are selected. Selectable | |
6805 * will listen for this event from items and update the selection state. | |
6806 * Set to empty string to listen to no events. | |
6807 */ | |
6808 activateEvent: { | 4735 activateEvent: { |
6809 type: String, | 4736 type: String, |
6810 value: 'tap', | 4737 value: 'tap', |
6811 observer: '_activateEventChanged' | 4738 observer: '_activateEventChanged' |
6812 }, | 4739 }, |
6813 | 4740 |
6814 /** | |
6815 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
6816 * are selectable. | |
6817 */ | |
6818 selectable: String, | 4741 selectable: String, |
6819 | 4742 |
6820 /** | |
6821 * The class to set on elements when selected. | |
6822 */ | |
6823 selectedClass: { | 4743 selectedClass: { |
6824 type: String, | 4744 type: String, |
6825 value: 'iron-selected' | 4745 value: 'iron-selected' |
6826 }, | 4746 }, |
6827 | 4747 |
6828 /** | |
6829 * The attribute to set on elements when selected. | |
6830 */ | |
6831 selectedAttribute: { | 4748 selectedAttribute: { |
6832 type: String, | 4749 type: String, |
6833 value: null | 4750 value: null |
6834 }, | 4751 }, |
6835 | 4752 |
6836 /** | |
6837 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
6838 * is not found. | |
6839 */ | |
6840 fallbackSelection: { | 4753 fallbackSelection: { |
6841 type: String, | 4754 type: String, |
6842 value: null | 4755 value: null |
6843 }, | 4756 }, |
6844 | 4757 |
6845 /** | |
6846 * The list of items from which a selection can be made. | |
6847 */ | |
6848 items: { | 4758 items: { |
6849 type: Array, | 4759 type: Array, |
6850 readOnly: true, | 4760 readOnly: true, |
6851 notify: true, | 4761 notify: true, |
6852 value: function() { | 4762 value: function() { |
6853 return []; | 4763 return []; |
6854 } | 4764 } |
6855 }, | 4765 }, |
6856 | 4766 |
6857 /** | |
6858 * The set of excluded elements where the key is the `localName` | |
6859 * of the element that will be ignored from the item list. | |
6860 * | |
6861 * @default {template: 1} | |
6862 */ | |
6863 _excludedLocalNames: { | 4767 _excludedLocalNames: { |
6864 type: Object, | 4768 type: Object, |
6865 value: function() { | 4769 value: function() { |
6866 return { | 4770 return { |
6867 'template': 1 | 4771 'template': 1 |
6868 }; | 4772 }; |
6869 } | 4773 } |
6870 } | 4774 } |
6871 }, | 4775 }, |
6872 | 4776 |
(...skipping 17 matching lines...) Expand all Loading... |
6890 this._addListener(this.activateEvent); | 4794 this._addListener(this.activateEvent); |
6891 }, | 4795 }, |
6892 | 4796 |
6893 detached: function() { | 4797 detached: function() { |
6894 if (this._observer) { | 4798 if (this._observer) { |
6895 Polymer.dom(this).unobserveNodes(this._observer); | 4799 Polymer.dom(this).unobserveNodes(this._observer); |
6896 } | 4800 } |
6897 this._removeListener(this.activateEvent); | 4801 this._removeListener(this.activateEvent); |
6898 }, | 4802 }, |
6899 | 4803 |
6900 /** | |
6901 * Returns the index of the given item. | |
6902 * | |
6903 * @method indexOf | |
6904 * @param {Object} item | |
6905 * @returns Returns the index of the item | |
6906 */ | |
6907 indexOf: function(item) { | 4804 indexOf: function(item) { |
6908 return this.items.indexOf(item); | 4805 return this.items.indexOf(item); |
6909 }, | 4806 }, |
6910 | 4807 |
6911 /** | |
6912 * Selects the given value. | |
6913 * | |
6914 * @method select | |
6915 * @param {string|number} value the value to select. | |
6916 */ | |
6917 select: function(value) { | 4808 select: function(value) { |
6918 this.selected = value; | 4809 this.selected = value; |
6919 }, | 4810 }, |
6920 | 4811 |
6921 /** | |
6922 * Selects the previous item. | |
6923 * | |
6924 * @method selectPrevious | |
6925 */ | |
6926 selectPrevious: function() { | 4812 selectPrevious: function() { |
6927 var length = this.items.length; | 4813 var length = this.items.length; |
6928 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | 4814 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; |
6929 this.selected = this._indexToValue(index); | 4815 this.selected = this._indexToValue(index); |
6930 }, | 4816 }, |
6931 | 4817 |
6932 /** | |
6933 * Selects the next item. | |
6934 * | |
6935 * @method selectNext | |
6936 */ | |
6937 selectNext: function() { | 4818 selectNext: function() { |
6938 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | 4819 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; |
6939 this.selected = this._indexToValue(index); | 4820 this.selected = this._indexToValue(index); |
6940 }, | 4821 }, |
6941 | 4822 |
6942 /** | |
6943 * Selects the item at the given index. | |
6944 * | |
6945 * @method selectIndex | |
6946 */ | |
6947 selectIndex: function(index) { | 4823 selectIndex: function(index) { |
6948 this.select(this._indexToValue(index)); | 4824 this.select(this._indexToValue(index)); |
6949 }, | 4825 }, |
6950 | 4826 |
6951 /** | |
6952 * Force a synchronous update of the `items` property. | |
6953 * | |
6954 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
6955 * updates to the set of selectable items after updates to the DOM list and | |
6956 * selection state have been made. | |
6957 * | |
6958 * WARNING: If you are using this method, you should probably consider an | |
6959 * alternate approach. Synchronously querying for items is potentially | |
6960 * slow for many use cases. The `items` property will update asynchronously | |
6961 * on its own to reflect selectable items in the DOM. | |
6962 */ | |
6963 forceSynchronousItemUpdate: function() { | 4827 forceSynchronousItemUpdate: function() { |
6964 this._updateItems(); | 4828 this._updateItems(); |
6965 }, | 4829 }, |
6966 | 4830 |
6967 get _shouldUpdateSelection() { | 4831 get _shouldUpdateSelection() { |
6968 return this.selected != null; | 4832 return this.selected != null; |
6969 }, | 4833 }, |
6970 | 4834 |
6971 _checkFallback: function() { | 4835 _checkFallback: function() { |
6972 if (this._shouldUpdateSelection) { | 4836 if (this._shouldUpdateSelection) { |
(...skipping 25 matching lines...) Expand all Loading... |
6998 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | 4862 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
6999 } | 4863 } |
7000 }, | 4864 }, |
7001 | 4865 |
7002 _updateSelected: function() { | 4866 _updateSelected: function() { |
7003 this._selectSelected(this.selected); | 4867 this._selectSelected(this.selected); |
7004 }, | 4868 }, |
7005 | 4869 |
7006 _selectSelected: function(selected) { | 4870 _selectSelected: function(selected) { |
7007 this._selection.select(this._valueToItem(this.selected)); | 4871 this._selection.select(this._valueToItem(this.selected)); |
7008 // Check for items, since this array is populated only when attached | |
7009 // Since Number(0) is falsy, explicitly check for undefined | |
7010 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | 4872 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { |
7011 this.selected = this.fallbackSelection; | 4873 this.selected = this.fallbackSelection; |
7012 } | 4874 } |
7013 }, | 4875 }, |
7014 | 4876 |
7015 _filterItem: function(node) { | 4877 _filterItem: function(node) { |
7016 return !this._excludedLocalNames[node.localName]; | 4878 return !this._excludedLocalNames[node.localName]; |
7017 }, | 4879 }, |
7018 | 4880 |
7019 _valueToItem: function(value) { | 4881 _valueToItem: function(value) { |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
7056 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 4918 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
7057 } | 4919 } |
7058 this._selectionChange(); | 4920 this._selectionChange(); |
7059 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | 4921 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); |
7060 }, | 4922 }, |
7061 | 4923 |
7062 _selectionChange: function() { | 4924 _selectionChange: function() { |
7063 this._setSelectedItem(this._selection.get()); | 4925 this._setSelectedItem(this._selection.get()); |
7064 }, | 4926 }, |
7065 | 4927 |
7066 // observe items change under the given node. | |
7067 _observeItems: function(node) { | 4928 _observeItems: function(node) { |
7068 return Polymer.dom(node).observeNodes(function(mutation) { | 4929 return Polymer.dom(node).observeNodes(function(mutation) { |
7069 this._updateItems(); | 4930 this._updateItems(); |
7070 | 4931 |
7071 if (this._shouldUpdateSelection) { | 4932 if (this._shouldUpdateSelection) { |
7072 this._updateSelected(); | 4933 this._updateSelected(); |
7073 } | 4934 } |
7074 | 4935 |
7075 // Let other interested parties know about the change so that | |
7076 // we don't have to recreate mutation observers everywhere. | |
7077 this.fire('iron-items-changed', mutation, { | 4936 this.fire('iron-items-changed', mutation, { |
7078 bubbles: false, | 4937 bubbles: false, |
7079 cancelable: false | 4938 cancelable: false |
7080 }); | 4939 }); |
7081 }); | 4940 }); |
7082 }, | 4941 }, |
7083 | 4942 |
7084 _activateHandler: function(e) { | 4943 _activateHandler: function(e) { |
7085 var t = e.target; | 4944 var t = e.target; |
7086 var items = this.items; | 4945 var items = this.items; |
(...skipping 13 matching lines...) Expand all Loading... |
7100 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | 4959 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { |
7101 this.select(value); | 4960 this.select(value); |
7102 } | 4961 } |
7103 } | 4962 } |
7104 | 4963 |
7105 }; | 4964 }; |
7106 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | 4965 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ |
7107 Polymer.IronMultiSelectableBehaviorImpl = { | 4966 Polymer.IronMultiSelectableBehaviorImpl = { |
7108 properties: { | 4967 properties: { |
7109 | 4968 |
7110 /** | |
7111 * If true, multiple selections are allowed. | |
7112 */ | |
7113 multi: { | 4969 multi: { |
7114 type: Boolean, | 4970 type: Boolean, |
7115 value: false, | 4971 value: false, |
7116 observer: 'multiChanged' | 4972 observer: 'multiChanged' |
7117 }, | 4973 }, |
7118 | 4974 |
7119 /** | |
7120 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
7121 * is true. | |
7122 */ | |
7123 selectedValues: { | 4975 selectedValues: { |
7124 type: Array, | 4976 type: Array, |
7125 notify: true | 4977 notify: true |
7126 }, | 4978 }, |
7127 | 4979 |
7128 /** | |
7129 * Returns an array of currently selected items. | |
7130 */ | |
7131 selectedItems: { | 4980 selectedItems: { |
7132 type: Array, | 4981 type: Array, |
7133 readOnly: true, | 4982 readOnly: true, |
7134 notify: true | 4983 notify: true |
7135 }, | 4984 }, |
7136 | 4985 |
7137 }, | 4986 }, |
7138 | 4987 |
7139 observers: [ | 4988 observers: [ |
7140 '_updateSelected(selectedValues.splices)' | 4989 '_updateSelected(selectedValues.splices)' |
7141 ], | 4990 ], |
7142 | 4991 |
7143 /** | |
7144 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7145 * `value` will be toggled; otherwise the `value` will be selected. | |
7146 * | |
7147 * @method select | |
7148 * @param {string|number} value the value to select. | |
7149 */ | |
7150 select: function(value) { | 4992 select: function(value) { |
7151 if (this.multi) { | 4993 if (this.multi) { |
7152 if (this.selectedValues) { | 4994 if (this.selectedValues) { |
7153 this._toggleSelected(value); | 4995 this._toggleSelected(value); |
7154 } else { | 4996 } else { |
7155 this.selectedValues = [value]; | 4997 this.selectedValues = [value]; |
7156 } | 4998 } |
7157 } else { | 4999 } else { |
7158 this.selected = value; | 5000 this.selected = value; |
7159 } | 5001 } |
(...skipping 24 matching lines...) Expand all Loading... |
7184 if (this.multi) { | 5026 if (this.multi) { |
7185 this._selectMulti(this.selectedValues); | 5027 this._selectMulti(this.selectedValues); |
7186 } else { | 5028 } else { |
7187 this._selectSelected(this.selected); | 5029 this._selectSelected(this.selected); |
7188 } | 5030 } |
7189 }, | 5031 }, |
7190 | 5032 |
7191 _selectMulti: function(values) { | 5033 _selectMulti: function(values) { |
7192 if (values) { | 5034 if (values) { |
7193 var selectedItems = this._valuesToItems(values); | 5035 var selectedItems = this._valuesToItems(values); |
7194 // clear all but the current selected items | |
7195 this._selection.clear(selectedItems); | 5036 this._selection.clear(selectedItems); |
7196 // select only those not selected yet | |
7197 for (var i = 0; i < selectedItems.length; i++) { | 5037 for (var i = 0; i < selectedItems.length; i++) { |
7198 this._selection.setItemSelected(selectedItems[i], true); | 5038 this._selection.setItemSelected(selectedItems[i], true); |
7199 } | 5039 } |
7200 // Check for items, since this array is populated only when attached | |
7201 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | 5040 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { |
7202 var fallback = this._valueToItem(this.fallbackSelection); | 5041 var fallback = this._valueToItem(this.fallbackSelection); |
7203 if (fallback) { | 5042 if (fallback) { |
7204 this.selectedValues = [this.fallbackSelection]; | 5043 this.selectedValues = [this.fallbackSelection]; |
7205 } | 5044 } |
7206 } | 5045 } |
7207 } else { | 5046 } else { |
7208 this._selection.clear(); | 5047 this._selection.clear(); |
7209 } | 5048 } |
7210 }, | 5049 }, |
(...skipping 23 matching lines...) Expand all Loading... |
7234 return this._valueToItem(value); | 5073 return this._valueToItem(value); |
7235 }, this); | 5074 }, this); |
7236 } | 5075 } |
7237 }; | 5076 }; |
7238 | 5077 |
7239 /** @polymerBehavior */ | 5078 /** @polymerBehavior */ |
7240 Polymer.IronMultiSelectableBehavior = [ | 5079 Polymer.IronMultiSelectableBehavior = [ |
7241 Polymer.IronSelectableBehavior, | 5080 Polymer.IronSelectableBehavior, |
7242 Polymer.IronMultiSelectableBehaviorImpl | 5081 Polymer.IronMultiSelectableBehaviorImpl |
7243 ]; | 5082 ]; |
7244 /** | |
7245 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
7246 * | |
7247 * @demo demo/index.html | |
7248 * @polymerBehavior Polymer.IronMenuBehavior | |
7249 */ | |
7250 Polymer.IronMenuBehaviorImpl = { | 5083 Polymer.IronMenuBehaviorImpl = { |
7251 | 5084 |
7252 properties: { | 5085 properties: { |
7253 | 5086 |
7254 /** | |
7255 * Returns the currently focused item. | |
7256 * @type {?Object} | |
7257 */ | |
7258 focusedItem: { | 5087 focusedItem: { |
7259 observer: '_focusedItemChanged', | 5088 observer: '_focusedItemChanged', |
7260 readOnly: true, | 5089 readOnly: true, |
7261 type: Object | 5090 type: Object |
7262 }, | 5091 }, |
7263 | 5092 |
7264 /** | |
7265 * The attribute to use on menu items to look up the item title. Typing th
e first | |
7266 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
7267 * will be used. | |
7268 */ | |
7269 attrForItemTitle: { | 5093 attrForItemTitle: { |
7270 type: String | 5094 type: String |
7271 } | 5095 } |
7272 }, | 5096 }, |
7273 | 5097 |
7274 hostAttributes: { | 5098 hostAttributes: { |
7275 'role': 'menu', | 5099 'role': 'menu', |
7276 'tabindex': '0' | 5100 'tabindex': '0' |
7277 }, | 5101 }, |
7278 | 5102 |
(...skipping 11 matching lines...) Expand all Loading... |
7290 'up': '_onUpKey', | 5114 'up': '_onUpKey', |
7291 'down': '_onDownKey', | 5115 'down': '_onDownKey', |
7292 'esc': '_onEscKey', | 5116 'esc': '_onEscKey', |
7293 'shift+tab:keydown': '_onShiftTabDown' | 5117 'shift+tab:keydown': '_onShiftTabDown' |
7294 }, | 5118 }, |
7295 | 5119 |
7296 attached: function() { | 5120 attached: function() { |
7297 this._resetTabindices(); | 5121 this._resetTabindices(); |
7298 }, | 5122 }, |
7299 | 5123 |
7300 /** | |
7301 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7302 * `value` will be toggled; otherwise the `value` will be selected. | |
7303 * | |
7304 * @param {string|number} value the value to select. | |
7305 */ | |
7306 select: function(value) { | 5124 select: function(value) { |
7307 // Cancel automatically focusing a default item if the menu received focus | |
7308 // through a user action selecting a particular item. | |
7309 if (this._defaultFocusAsync) { | 5125 if (this._defaultFocusAsync) { |
7310 this.cancelAsync(this._defaultFocusAsync); | 5126 this.cancelAsync(this._defaultFocusAsync); |
7311 this._defaultFocusAsync = null; | 5127 this._defaultFocusAsync = null; |
7312 } | 5128 } |
7313 var item = this._valueToItem(value); | 5129 var item = this._valueToItem(value); |
7314 if (item && item.hasAttribute('disabled')) return; | 5130 if (item && item.hasAttribute('disabled')) return; |
7315 this._setFocusedItem(item); | 5131 this._setFocusedItem(item); |
7316 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | 5132 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
7317 }, | 5133 }, |
7318 | 5134 |
7319 /** | |
7320 * Resets all tabindex attributes to the appropriate value based on the | |
7321 * current selection state. The appropriate value is `0` (focusable) for | |
7322 * the default selected item, and `-1` (not keyboard focusable) for all | |
7323 * other items. | |
7324 */ | |
7325 _resetTabindices: function() { | 5135 _resetTabindices: function() { |
7326 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | 5136 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; |
7327 | 5137 |
7328 this.items.forEach(function(item) { | 5138 this.items.forEach(function(item) { |
7329 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | 5139 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
7330 }, this); | 5140 }, this); |
7331 }, | 5141 }, |
7332 | 5142 |
7333 /** | |
7334 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
7335 * multi-selectable. | |
7336 * | |
7337 * @param {boolean} multi True if the menu should be multi-selectable. | |
7338 */ | |
7339 _updateMultiselectable: function(multi) { | 5143 _updateMultiselectable: function(multi) { |
7340 if (multi) { | 5144 if (multi) { |
7341 this.setAttribute('aria-multiselectable', 'true'); | 5145 this.setAttribute('aria-multiselectable', 'true'); |
7342 } else { | 5146 } else { |
7343 this.removeAttribute('aria-multiselectable'); | 5147 this.removeAttribute('aria-multiselectable'); |
7344 } | 5148 } |
7345 }, | 5149 }, |
7346 | 5150 |
7347 /** | |
7348 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
7349 * menu (if there is a relevant item, and it is possible to focus it). | |
7350 * | |
7351 * @param {KeyboardEvent} event A KeyboardEvent. | |
7352 */ | |
7353 _focusWithKeyboardEvent: function(event) { | 5151 _focusWithKeyboardEvent: function(event) { |
7354 for (var i = 0, item; item = this.items[i]; i++) { | 5152 for (var i = 0, item; item = this.items[i]; i++) { |
7355 var attr = this.attrForItemTitle || 'textContent'; | 5153 var attr = this.attrForItemTitle || 'textContent'; |
7356 var title = item[attr] || item.getAttribute(attr); | 5154 var title = item[attr] || item.getAttribute(attr); |
7357 | 5155 |
7358 if (!item.hasAttribute('disabled') && title && | 5156 if (!item.hasAttribute('disabled') && title && |
7359 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | 5157 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { |
7360 this._setFocusedItem(item); | 5158 this._setFocusedItem(item); |
7361 break; | 5159 break; |
7362 } | 5160 } |
7363 } | 5161 } |
7364 }, | 5162 }, |
7365 | 5163 |
7366 /** | |
7367 * Focuses the previous item (relative to the currently focused item) in the | |
7368 * menu, disabled items will be skipped. | |
7369 * Loop until length + 1 to handle case of single item in menu. | |
7370 */ | |
7371 _focusPrevious: function() { | 5164 _focusPrevious: function() { |
7372 var length = this.items.length; | 5165 var length = this.items.length; |
7373 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 5166 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
7374 for (var i = 1; i < length + 1; i++) { | 5167 for (var i = 1; i < length + 1; i++) { |
7375 var item = this.items[(curFocusIndex - i + length) % length]; | 5168 var item = this.items[(curFocusIndex - i + length) % length]; |
7376 if (!item.hasAttribute('disabled')) { | 5169 if (!item.hasAttribute('disabled')) { |
7377 this._setFocusedItem(item); | 5170 this._setFocusedItem(item); |
7378 return; | 5171 return; |
7379 } | 5172 } |
7380 } | 5173 } |
7381 }, | 5174 }, |
7382 | 5175 |
7383 /** | |
7384 * Focuses the next item (relative to the currently focused item) in the | |
7385 * menu, disabled items will be skipped. | |
7386 * Loop until length + 1 to handle case of single item in menu. | |
7387 */ | |
7388 _focusNext: function() { | 5176 _focusNext: function() { |
7389 var length = this.items.length; | 5177 var length = this.items.length; |
7390 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 5178 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
7391 for (var i = 1; i < length + 1; i++) { | 5179 for (var i = 1; i < length + 1; i++) { |
7392 var item = this.items[(curFocusIndex + i) % length]; | 5180 var item = this.items[(curFocusIndex + i) % length]; |
7393 if (!item.hasAttribute('disabled')) { | 5181 if (!item.hasAttribute('disabled')) { |
7394 this._setFocusedItem(item); | 5182 this._setFocusedItem(item); |
7395 return; | 5183 return; |
7396 } | 5184 } |
7397 } | 5185 } |
7398 }, | 5186 }, |
7399 | 5187 |
7400 /** | |
7401 * Mutates items in the menu based on provided selection details, so that | |
7402 * all items correctly reflect selection state. | |
7403 * | |
7404 * @param {Element} item An item in the menu. | |
7405 * @param {boolean} isSelected True if the item should be shown in a | |
7406 * selected state, otherwise false. | |
7407 */ | |
7408 _applySelection: function(item, isSelected) { | 5188 _applySelection: function(item, isSelected) { |
7409 if (isSelected) { | 5189 if (isSelected) { |
7410 item.setAttribute('aria-selected', 'true'); | 5190 item.setAttribute('aria-selected', 'true'); |
7411 } else { | 5191 } else { |
7412 item.removeAttribute('aria-selected'); | 5192 item.removeAttribute('aria-selected'); |
7413 } | 5193 } |
7414 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | 5194 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
7415 }, | 5195 }, |
7416 | 5196 |
7417 /** | |
7418 * Discretely updates tabindex values among menu items as the focused item | |
7419 * changes. | |
7420 * | |
7421 * @param {Element} focusedItem The element that is currently focused. | |
7422 * @param {?Element} old The last element that was considered focused, if | |
7423 * applicable. | |
7424 */ | |
7425 _focusedItemChanged: function(focusedItem, old) { | 5197 _focusedItemChanged: function(focusedItem, old) { |
7426 old && old.setAttribute('tabindex', '-1'); | 5198 old && old.setAttribute('tabindex', '-1'); |
7427 if (focusedItem) { | 5199 if (focusedItem) { |
7428 focusedItem.setAttribute('tabindex', '0'); | 5200 focusedItem.setAttribute('tabindex', '0'); |
7429 focusedItem.focus(); | 5201 focusedItem.focus(); |
7430 } | 5202 } |
7431 }, | 5203 }, |
7432 | 5204 |
7433 /** | |
7434 * A handler that responds to mutation changes related to the list of items | |
7435 * in the menu. | |
7436 * | |
7437 * @param {CustomEvent} event An event containing mutation records as its | |
7438 * detail. | |
7439 */ | |
7440 _onIronItemsChanged: function(event) { | 5205 _onIronItemsChanged: function(event) { |
7441 if (event.detail.addedNodes.length) { | 5206 if (event.detail.addedNodes.length) { |
7442 this._resetTabindices(); | 5207 this._resetTabindices(); |
7443 } | 5208 } |
7444 }, | 5209 }, |
7445 | 5210 |
7446 /** | |
7447 * Handler that is called when a shift+tab keypress is detected by the menu. | |
7448 * | |
7449 * @param {CustomEvent} event A key combination event. | |
7450 */ | |
7451 _onShiftTabDown: function(event) { | 5211 _onShiftTabDown: function(event) { |
7452 var oldTabIndex = this.getAttribute('tabindex'); | 5212 var oldTabIndex = this.getAttribute('tabindex'); |
7453 | 5213 |
7454 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | 5214 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
7455 | 5215 |
7456 this._setFocusedItem(null); | 5216 this._setFocusedItem(null); |
7457 | 5217 |
7458 this.setAttribute('tabindex', '-1'); | 5218 this.setAttribute('tabindex', '-1'); |
7459 | 5219 |
7460 this.async(function() { | 5220 this.async(function() { |
7461 this.setAttribute('tabindex', oldTabIndex); | 5221 this.setAttribute('tabindex', oldTabIndex); |
7462 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | 5222 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
7463 // NOTE(cdata): polymer/polymer#1305 | |
7464 }, 1); | 5223 }, 1); |
7465 }, | 5224 }, |
7466 | 5225 |
7467 /** | |
7468 * Handler that is called when the menu receives focus. | |
7469 * | |
7470 * @param {FocusEvent} event A focus event. | |
7471 */ | |
7472 _onFocus: function(event) { | 5226 _onFocus: function(event) { |
7473 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | 5227 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
7474 // do not focus the menu itself | |
7475 return; | 5228 return; |
7476 } | 5229 } |
7477 | 5230 |
7478 // Do not focus the selected tab if the deepest target is part of the | |
7479 // menu element's local DOM and is focusable. | |
7480 var rootTarget = /** @type {?HTMLElement} */( | 5231 var rootTarget = /** @type {?HTMLElement} */( |
7481 Polymer.dom(event).rootTarget); | 5232 Polymer.dom(event).rootTarget); |
7482 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | 5233 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { |
7483 return; | 5234 return; |
7484 } | 5235 } |
7485 | 5236 |
7486 // clear the cached focus item | |
7487 this._defaultFocusAsync = this.async(function() { | 5237 this._defaultFocusAsync = this.async(function() { |
7488 // focus the selected item when the menu receives focus, or the first it
em | |
7489 // if no item is selected | |
7490 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | 5238 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; |
7491 | 5239 |
7492 this._setFocusedItem(null); | 5240 this._setFocusedItem(null); |
7493 | 5241 |
7494 if (selectedItem) { | 5242 if (selectedItem) { |
7495 this._setFocusedItem(selectedItem); | 5243 this._setFocusedItem(selectedItem); |
7496 } else if (this.items[0]) { | 5244 } else if (this.items[0]) { |
7497 // We find the first none-disabled item (if one exists) | |
7498 this._focusNext(); | 5245 this._focusNext(); |
7499 } | 5246 } |
7500 }); | 5247 }); |
7501 }, | 5248 }, |
7502 | 5249 |
7503 /** | |
7504 * Handler that is called when the up key is pressed. | |
7505 * | |
7506 * @param {CustomEvent} event A key combination event. | |
7507 */ | |
7508 _onUpKey: function(event) { | 5250 _onUpKey: function(event) { |
7509 // up and down arrows moves the focus | |
7510 this._focusPrevious(); | 5251 this._focusPrevious(); |
7511 event.detail.keyboardEvent.preventDefault(); | 5252 event.detail.keyboardEvent.preventDefault(); |
7512 }, | 5253 }, |
7513 | 5254 |
7514 /** | |
7515 * Handler that is called when the down key is pressed. | |
7516 * | |
7517 * @param {CustomEvent} event A key combination event. | |
7518 */ | |
7519 _onDownKey: function(event) { | 5255 _onDownKey: function(event) { |
7520 this._focusNext(); | 5256 this._focusNext(); |
7521 event.detail.keyboardEvent.preventDefault(); | 5257 event.detail.keyboardEvent.preventDefault(); |
7522 }, | 5258 }, |
7523 | 5259 |
7524 /** | |
7525 * Handler that is called when the esc key is pressed. | |
7526 * | |
7527 * @param {CustomEvent} event A key combination event. | |
7528 */ | |
7529 _onEscKey: function(event) { | 5260 _onEscKey: function(event) { |
7530 // esc blurs the control | |
7531 this.focusedItem.blur(); | 5261 this.focusedItem.blur(); |
7532 }, | 5262 }, |
7533 | 5263 |
7534 /** | |
7535 * Handler that is called when a keydown event is detected. | |
7536 * | |
7537 * @param {KeyboardEvent} event A keyboard event. | |
7538 */ | |
7539 _onKeydown: function(event) { | 5264 _onKeydown: function(event) { |
7540 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | 5265 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
7541 // all other keys focus the menu item starting with that character | |
7542 this._focusWithKeyboardEvent(event); | 5266 this._focusWithKeyboardEvent(event); |
7543 } | 5267 } |
7544 event.stopPropagation(); | 5268 event.stopPropagation(); |
7545 }, | 5269 }, |
7546 | 5270 |
7547 // override _activateHandler | |
7548 _activateHandler: function(event) { | 5271 _activateHandler: function(event) { |
7549 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | 5272 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
7550 event.stopPropagation(); | 5273 event.stopPropagation(); |
7551 } | 5274 } |
7552 }; | 5275 }; |
7553 | 5276 |
7554 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | 5277 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
7555 | 5278 |
7556 /** @polymerBehavior Polymer.IronMenuBehavior */ | 5279 /** @polymerBehavior Polymer.IronMenuBehavior */ |
7557 Polymer.IronMenuBehavior = [ | 5280 Polymer.IronMenuBehavior = [ |
7558 Polymer.IronMultiSelectableBehavior, | 5281 Polymer.IronMultiSelectableBehavior, |
7559 Polymer.IronA11yKeysBehavior, | 5282 Polymer.IronA11yKeysBehavior, |
7560 Polymer.IronMenuBehaviorImpl | 5283 Polymer.IronMenuBehaviorImpl |
7561 ]; | 5284 ]; |
7562 (function() { | 5285 (function() { |
7563 Polymer({ | 5286 Polymer({ |
7564 is: 'paper-menu', | 5287 is: 'paper-menu', |
7565 | 5288 |
7566 behaviors: [ | 5289 behaviors: [ |
7567 Polymer.IronMenuBehavior | 5290 Polymer.IronMenuBehavior |
7568 ] | 5291 ] |
7569 }); | 5292 }); |
7570 })(); | 5293 })(); |
7571 /** | |
7572 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
7573 optionally centers it in the window or another element. | |
7574 | |
7575 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | |
7576 by CSS. | |
7577 | |
7578 CSS properties | Action | |
7579 -----------------------------|------------------------------------------- | |
7580 `position` set | Element is not centered horizontally or verticall
y | |
7581 `top` or `bottom` set | Element is not vertically centered | |
7582 `left` or `right` set | Element is not horizontally centered | |
7583 `max-height` set | Element respects `max-height` | |
7584 `max-width` set | Element respects `max-width` | |
7585 | |
7586 `Polymer.IronFitBehavior` can position an element into another element using | |
7587 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
7588 | |
7589 <div class="container"> | |
7590 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
7591 Positioned into the container | |
7592 </iron-fit-impl> | |
7593 </div> | |
7594 | |
7595 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
7596 | |
7597 <div class="container"> | |
7598 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
7599 Positioned around the container | |
7600 </iron-fit-impl> | |
7601 </div> | |
7602 | |
7603 @demo demo/index.html | |
7604 @polymerBehavior | |
7605 */ | |
7606 | 5294 |
7607 Polymer.IronFitBehavior = { | 5295 Polymer.IronFitBehavior = { |
7608 | 5296 |
7609 properties: { | 5297 properties: { |
7610 | 5298 |
7611 /** | |
7612 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
7613 * but it can be set to a child element. This is useful, for example, for
implementing a | |
7614 * scrolling region inside the element. | |
7615 * @type {!Element} | |
7616 */ | |
7617 sizingTarget: { | 5299 sizingTarget: { |
7618 type: Object, | 5300 type: Object, |
7619 value: function() { | 5301 value: function() { |
7620 return this; | 5302 return this; |
7621 } | 5303 } |
7622 }, | 5304 }, |
7623 | 5305 |
7624 /** | |
7625 * The element to fit `this` into. | |
7626 */ | |
7627 fitInto: { | 5306 fitInto: { |
7628 type: Object, | 5307 type: Object, |
7629 value: window | 5308 value: window |
7630 }, | 5309 }, |
7631 | 5310 |
7632 /** | |
7633 * Will position the element around the positionTarget without overlapping
it. | |
7634 */ | |
7635 noOverlap: { | 5311 noOverlap: { |
7636 type: Boolean | 5312 type: Boolean |
7637 }, | 5313 }, |
7638 | 5314 |
7639 /** | |
7640 * The element that should be used to position the element. If not set, it
will | |
7641 * default to the parent node. | |
7642 * @type {!Element} | |
7643 */ | |
7644 positionTarget: { | 5315 positionTarget: { |
7645 type: Element | 5316 type: Element |
7646 }, | 5317 }, |
7647 | 5318 |
7648 /** | |
7649 * The orientation against which to align the element horizontally | |
7650 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
7651 */ | |
7652 horizontalAlign: { | 5319 horizontalAlign: { |
7653 type: String | 5320 type: String |
7654 }, | 5321 }, |
7655 | 5322 |
7656 /** | |
7657 * The orientation against which to align the element vertically | |
7658 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
7659 */ | |
7660 verticalAlign: { | 5323 verticalAlign: { |
7661 type: String | 5324 type: String |
7662 }, | 5325 }, |
7663 | 5326 |
7664 /** | |
7665 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
7666 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
7667 */ | |
7668 dynamicAlign: { | 5327 dynamicAlign: { |
7669 type: Boolean | 5328 type: Boolean |
7670 }, | 5329 }, |
7671 | 5330 |
7672 /** | |
7673 * The same as setting margin-left and margin-right css properties. | |
7674 * @deprecated | |
7675 */ | |
7676 horizontalOffset: { | 5331 horizontalOffset: { |
7677 type: Number, | 5332 type: Number, |
7678 value: 0, | 5333 value: 0, |
7679 notify: true | 5334 notify: true |
7680 }, | 5335 }, |
7681 | 5336 |
7682 /** | |
7683 * The same as setting margin-top and margin-bottom css properties. | |
7684 * @deprecated | |
7685 */ | |
7686 verticalOffset: { | 5337 verticalOffset: { |
7687 type: Number, | 5338 type: Number, |
7688 value: 0, | 5339 value: 0, |
7689 notify: true | 5340 notify: true |
7690 }, | 5341 }, |
7691 | 5342 |
7692 /** | |
7693 * Set to true to auto-fit on attach. | |
7694 */ | |
7695 autoFitOnAttach: { | 5343 autoFitOnAttach: { |
7696 type: Boolean, | 5344 type: Boolean, |
7697 value: false | 5345 value: false |
7698 }, | 5346 }, |
7699 | 5347 |
7700 /** @type {?Object} */ | 5348 /** @type {?Object} */ |
7701 _fitInfo: { | 5349 _fitInfo: { |
7702 type: Object | 5350 type: Object |
7703 } | 5351 } |
7704 }, | 5352 }, |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
7736 get _fitTop() { | 5384 get _fitTop() { |
7737 var fitTop; | 5385 var fitTop; |
7738 if (this.fitInto === window) { | 5386 if (this.fitInto === window) { |
7739 fitTop = 0; | 5387 fitTop = 0; |
7740 } else { | 5388 } else { |
7741 fitTop = this.fitInto.getBoundingClientRect().top; | 5389 fitTop = this.fitInto.getBoundingClientRect().top; |
7742 } | 5390 } |
7743 return fitTop; | 5391 return fitTop; |
7744 }, | 5392 }, |
7745 | 5393 |
7746 /** | |
7747 * The element that should be used to position the element, | |
7748 * if no position target is configured. | |
7749 */ | |
7750 get _defaultPositionTarget() { | 5394 get _defaultPositionTarget() { |
7751 var parent = Polymer.dom(this).parentNode; | 5395 var parent = Polymer.dom(this).parentNode; |
7752 | 5396 |
7753 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | 5397 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
7754 parent = parent.host; | 5398 parent = parent.host; |
7755 } | 5399 } |
7756 | 5400 |
7757 return parent; | 5401 return parent; |
7758 }, | 5402 }, |
7759 | 5403 |
7760 /** | |
7761 * The horizontal align value, accounting for the RTL/LTR text direction. | |
7762 */ | |
7763 get _localeHorizontalAlign() { | 5404 get _localeHorizontalAlign() { |
7764 if (this._isRTL) { | 5405 if (this._isRTL) { |
7765 // In RTL, "left" becomes "right". | |
7766 if (this.horizontalAlign === 'right') { | 5406 if (this.horizontalAlign === 'right') { |
7767 return 'left'; | 5407 return 'left'; |
7768 } | 5408 } |
7769 if (this.horizontalAlign === 'left') { | 5409 if (this.horizontalAlign === 'left') { |
7770 return 'right'; | 5410 return 'right'; |
7771 } | 5411 } |
7772 } | 5412 } |
7773 return this.horizontalAlign; | 5413 return this.horizontalAlign; |
7774 }, | 5414 }, |
7775 | 5415 |
7776 attached: function() { | 5416 attached: function() { |
7777 // Memoize this to avoid expensive calculations & relayouts. | |
7778 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | 5417 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
7779 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | 5418 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
7780 if (this.autoFitOnAttach) { | 5419 if (this.autoFitOnAttach) { |
7781 if (window.getComputedStyle(this).display === 'none') { | 5420 if (window.getComputedStyle(this).display === 'none') { |
7782 setTimeout(function() { | 5421 setTimeout(function() { |
7783 this.fit(); | 5422 this.fit(); |
7784 }.bind(this)); | 5423 }.bind(this)); |
7785 } else { | 5424 } else { |
7786 this.fit(); | 5425 this.fit(); |
7787 } | 5426 } |
7788 } | 5427 } |
7789 }, | 5428 }, |
7790 | 5429 |
7791 /** | |
7792 * Positions and fits the element into the `fitInto` element. | |
7793 */ | |
7794 fit: function() { | 5430 fit: function() { |
7795 this.position(); | 5431 this.position(); |
7796 this.constrain(); | 5432 this.constrain(); |
7797 this.center(); | 5433 this.center(); |
7798 }, | 5434 }, |
7799 | 5435 |
7800 /** | |
7801 * Memoize information needed to position and size the target element. | |
7802 * @suppress {deprecated} | |
7803 */ | |
7804 _discoverInfo: function() { | 5436 _discoverInfo: function() { |
7805 if (this._fitInfo) { | 5437 if (this._fitInfo) { |
7806 return; | 5438 return; |
7807 } | 5439 } |
7808 var target = window.getComputedStyle(this); | 5440 var target = window.getComputedStyle(this); |
7809 var sizer = window.getComputedStyle(this.sizingTarget); | 5441 var sizer = window.getComputedStyle(this.sizingTarget); |
7810 | 5442 |
7811 this._fitInfo = { | 5443 this._fitInfo = { |
7812 inlineStyle: { | 5444 inlineStyle: { |
7813 top: this.style.top || '', | 5445 top: this.style.top || '', |
(...skipping 18 matching lines...) Expand all Loading... |
7832 minHeight: parseInt(sizer.minHeight, 10) || 0 | 5464 minHeight: parseInt(sizer.minHeight, 10) || 0 |
7833 }, | 5465 }, |
7834 margin: { | 5466 margin: { |
7835 top: parseInt(target.marginTop, 10) || 0, | 5467 top: parseInt(target.marginTop, 10) || 0, |
7836 right: parseInt(target.marginRight, 10) || 0, | 5468 right: parseInt(target.marginRight, 10) || 0, |
7837 bottom: parseInt(target.marginBottom, 10) || 0, | 5469 bottom: parseInt(target.marginBottom, 10) || 0, |
7838 left: parseInt(target.marginLeft, 10) || 0 | 5470 left: parseInt(target.marginLeft, 10) || 0 |
7839 } | 5471 } |
7840 }; | 5472 }; |
7841 | 5473 |
7842 // Support these properties until they are removed. | |
7843 if (this.verticalOffset) { | 5474 if (this.verticalOffset) { |
7844 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | 5475 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; |
7845 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | 5476 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
7846 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | 5477 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
7847 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | 5478 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; |
7848 } | 5479 } |
7849 if (this.horizontalOffset) { | 5480 if (this.horizontalOffset) { |
7850 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | 5481 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; |
7851 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | 5482 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
7852 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | 5483 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
7853 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | 5484 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; |
7854 } | 5485 } |
7855 }, | 5486 }, |
7856 | 5487 |
7857 /** | |
7858 * Resets the target element's position and size constraints, and clear | |
7859 * the memoized data. | |
7860 */ | |
7861 resetFit: function() { | 5488 resetFit: function() { |
7862 var info = this._fitInfo || {}; | 5489 var info = this._fitInfo || {}; |
7863 for (var property in info.sizerInlineStyle) { | 5490 for (var property in info.sizerInlineStyle) { |
7864 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | 5491 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
7865 } | 5492 } |
7866 for (var property in info.inlineStyle) { | 5493 for (var property in info.inlineStyle) { |
7867 this.style[property] = info.inlineStyle[property]; | 5494 this.style[property] = info.inlineStyle[property]; |
7868 } | 5495 } |
7869 | 5496 |
7870 this._fitInfo = null; | 5497 this._fitInfo = null; |
7871 }, | 5498 }, |
7872 | 5499 |
7873 /** | |
7874 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
7875 * the element or the `fitInto` element has been resized, or if any of the | |
7876 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
7877 * It preserves the scroll position of the sizingTarget. | |
7878 */ | |
7879 refit: function() { | 5500 refit: function() { |
7880 var scrollLeft = this.sizingTarget.scrollLeft; | 5501 var scrollLeft = this.sizingTarget.scrollLeft; |
7881 var scrollTop = this.sizingTarget.scrollTop; | 5502 var scrollTop = this.sizingTarget.scrollTop; |
7882 this.resetFit(); | 5503 this.resetFit(); |
7883 this.fit(); | 5504 this.fit(); |
7884 this.sizingTarget.scrollLeft = scrollLeft; | 5505 this.sizingTarget.scrollLeft = scrollLeft; |
7885 this.sizingTarget.scrollTop = scrollTop; | 5506 this.sizingTarget.scrollTop = scrollTop; |
7886 }, | 5507 }, |
7887 | 5508 |
7888 /** | |
7889 * Positions the element according to `horizontalAlign, verticalAlign`. | |
7890 */ | |
7891 position: function() { | 5509 position: function() { |
7892 if (!this.horizontalAlign && !this.verticalAlign) { | 5510 if (!this.horizontalAlign && !this.verticalAlign) { |
7893 // needs to be centered, and it is done after constrain. | |
7894 return; | 5511 return; |
7895 } | 5512 } |
7896 this._discoverInfo(); | 5513 this._discoverInfo(); |
7897 | 5514 |
7898 this.style.position = 'fixed'; | 5515 this.style.position = 'fixed'; |
7899 // Need border-box for margin/padding. | |
7900 this.sizingTarget.style.boxSizing = 'border-box'; | 5516 this.sizingTarget.style.boxSizing = 'border-box'; |
7901 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
7902 this.style.left = '0px'; | 5517 this.style.left = '0px'; |
7903 this.style.top = '0px'; | 5518 this.style.top = '0px'; |
7904 | 5519 |
7905 var rect = this.getBoundingClientRect(); | 5520 var rect = this.getBoundingClientRect(); |
7906 var positionRect = this.__getNormalizedRect(this.positionTarget); | 5521 var positionRect = this.__getNormalizedRect(this.positionTarget); |
7907 var fitRect = this.__getNormalizedRect(this.fitInto); | 5522 var fitRect = this.__getNormalizedRect(this.fitInto); |
7908 | 5523 |
7909 var margin = this._fitInfo.margin; | 5524 var margin = this._fitInfo.margin; |
7910 | 5525 |
7911 // Consider the margin as part of the size for position calculations. | |
7912 var size = { | 5526 var size = { |
7913 width: rect.width + margin.left + margin.right, | 5527 width: rect.width + margin.left + margin.right, |
7914 height: rect.height + margin.top + margin.bottom | 5528 height: rect.height + margin.top + margin.bottom |
7915 }; | 5529 }; |
7916 | 5530 |
7917 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | 5531 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); |
7918 | 5532 |
7919 var left = position.left + margin.left; | 5533 var left = position.left + margin.left; |
7920 var top = position.top + margin.top; | 5534 var top = position.top + margin.top; |
7921 | 5535 |
7922 // Use original size (without margin). | |
7923 var right = Math.min(fitRect.right - margin.right, left + rect.width); | 5536 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
7924 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | 5537 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
7925 | 5538 |
7926 var minWidth = this._fitInfo.sizedBy.minWidth; | 5539 var minWidth = this._fitInfo.sizedBy.minWidth; |
7927 var minHeight = this._fitInfo.sizedBy.minHeight; | 5540 var minHeight = this._fitInfo.sizedBy.minHeight; |
7928 if (left < margin.left) { | 5541 if (left < margin.left) { |
7929 left = margin.left; | 5542 left = margin.left; |
7930 if (right - left < minWidth) { | 5543 if (right - left < minWidth) { |
7931 left = right - minWidth; | 5544 left = right - minWidth; |
7932 } | 5545 } |
7933 } | 5546 } |
7934 if (top < margin.top) { | 5547 if (top < margin.top) { |
7935 top = margin.top; | 5548 top = margin.top; |
7936 if (bottom - top < minHeight) { | 5549 if (bottom - top < minHeight) { |
7937 top = bottom - minHeight; | 5550 top = bottom - minHeight; |
7938 } | 5551 } |
7939 } | 5552 } |
7940 | 5553 |
7941 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | 5554 this.sizingTarget.style.maxWidth = (right - left) + 'px'; |
7942 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | 5555 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; |
7943 | 5556 |
7944 // Remove the offset caused by any stacking context. | |
7945 this.style.left = (left - rect.left) + 'px'; | 5557 this.style.left = (left - rect.left) + 'px'; |
7946 this.style.top = (top - rect.top) + 'px'; | 5558 this.style.top = (top - rect.top) + 'px'; |
7947 }, | 5559 }, |
7948 | 5560 |
7949 /** | |
7950 * Constrains the size of the element to `fitInto` by setting `max-height` | |
7951 * and/or `max-width`. | |
7952 */ | |
7953 constrain: function() { | 5561 constrain: function() { |
7954 if (this.horizontalAlign || this.verticalAlign) { | 5562 if (this.horizontalAlign || this.verticalAlign) { |
7955 return; | 5563 return; |
7956 } | 5564 } |
7957 this._discoverInfo(); | 5565 this._discoverInfo(); |
7958 | 5566 |
7959 var info = this._fitInfo; | 5567 var info = this._fitInfo; |
7960 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
7961 if (!info.positionedBy.vertically) { | 5568 if (!info.positionedBy.vertically) { |
7962 this.style.position = 'fixed'; | 5569 this.style.position = 'fixed'; |
7963 this.style.top = '0px'; | 5570 this.style.top = '0px'; |
7964 } | 5571 } |
7965 if (!info.positionedBy.horizontally) { | 5572 if (!info.positionedBy.horizontally) { |
7966 this.style.position = 'fixed'; | 5573 this.style.position = 'fixed'; |
7967 this.style.left = '0px'; | 5574 this.style.left = '0px'; |
7968 } | 5575 } |
7969 | 5576 |
7970 // need border-box for margin/padding | |
7971 this.sizingTarget.style.boxSizing = 'border-box'; | 5577 this.sizingTarget.style.boxSizing = 'border-box'; |
7972 // constrain the width and height if not already set | |
7973 var rect = this.getBoundingClientRect(); | 5578 var rect = this.getBoundingClientRect(); |
7974 if (!info.sizedBy.height) { | 5579 if (!info.sizedBy.height) { |
7975 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | 5580 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); |
7976 } | 5581 } |
7977 if (!info.sizedBy.width) { | 5582 if (!info.sizedBy.width) { |
7978 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | 5583 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); |
7979 } | 5584 } |
7980 }, | 5585 }, |
7981 | 5586 |
7982 /** | |
7983 * @protected | |
7984 * @deprecated | |
7985 */ | |
7986 _sizeDimension: function(rect, positionedBy, start, end, extent) { | 5587 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
7987 this.__sizeDimension(rect, positionedBy, start, end, extent); | 5588 this.__sizeDimension(rect, positionedBy, start, end, extent); |
7988 }, | 5589 }, |
7989 | 5590 |
7990 /** | |
7991 * @private | |
7992 */ | |
7993 __sizeDimension: function(rect, positionedBy, start, end, extent) { | 5591 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
7994 var info = this._fitInfo; | 5592 var info = this._fitInfo; |
7995 var fitRect = this.__getNormalizedRect(this.fitInto); | 5593 var fitRect = this.__getNormalizedRect(this.fitInto); |
7996 var max = extent === 'Width' ? fitRect.width : fitRect.height; | 5594 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
7997 var flip = (positionedBy === end); | 5595 var flip = (positionedBy === end); |
7998 var offset = flip ? max - rect[end] : rect[start]; | 5596 var offset = flip ? max - rect[end] : rect[start]; |
7999 var margin = info.margin[flip ? start : end]; | 5597 var margin = info.margin[flip ? start : end]; |
8000 var offsetExtent = 'offset' + extent; | 5598 var offsetExtent = 'offset' + extent; |
8001 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | 5599 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
8002 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | 5600 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; |
8003 }, | 5601 }, |
8004 | 5602 |
8005 /** | |
8006 * Centers horizontally and vertically if not already positioned. This also
sets | |
8007 * `position:fixed`. | |
8008 */ | |
8009 center: function() { | 5603 center: function() { |
8010 if (this.horizontalAlign || this.verticalAlign) { | 5604 if (this.horizontalAlign || this.verticalAlign) { |
8011 return; | 5605 return; |
8012 } | 5606 } |
8013 this._discoverInfo(); | 5607 this._discoverInfo(); |
8014 | 5608 |
8015 var positionedBy = this._fitInfo.positionedBy; | 5609 var positionedBy = this._fitInfo.positionedBy; |
8016 if (positionedBy.vertically && positionedBy.horizontally) { | 5610 if (positionedBy.vertically && positionedBy.horizontally) { |
8017 // Already positioned. | |
8018 return; | 5611 return; |
8019 } | 5612 } |
8020 // Need position:fixed to center | |
8021 this.style.position = 'fixed'; | 5613 this.style.position = 'fixed'; |
8022 // Take into account the offset caused by parents that create stacking | |
8023 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
8024 // measure the bounding rect. | |
8025 if (!positionedBy.vertically) { | 5614 if (!positionedBy.vertically) { |
8026 this.style.top = '0px'; | 5615 this.style.top = '0px'; |
8027 } | 5616 } |
8028 if (!positionedBy.horizontally) { | 5617 if (!positionedBy.horizontally) { |
8029 this.style.left = '0px'; | 5618 this.style.left = '0px'; |
8030 } | 5619 } |
8031 // It will take in consideration margins and transforms | |
8032 var rect = this.getBoundingClientRect(); | 5620 var rect = this.getBoundingClientRect(); |
8033 var fitRect = this.__getNormalizedRect(this.fitInto); | 5621 var fitRect = this.__getNormalizedRect(this.fitInto); |
8034 if (!positionedBy.vertically) { | 5622 if (!positionedBy.vertically) { |
8035 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | 5623 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
8036 this.style.top = top + 'px'; | 5624 this.style.top = top + 'px'; |
8037 } | 5625 } |
8038 if (!positionedBy.horizontally) { | 5626 if (!positionedBy.horizontally) { |
8039 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | 5627 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
8040 this.style.left = left + 'px'; | 5628 this.style.left = left + 'px'; |
8041 } | 5629 } |
(...skipping 14 matching lines...) Expand all Loading... |
8056 }, | 5644 }, |
8057 | 5645 |
8058 __getCroppedArea: function(position, size, fitRect) { | 5646 __getCroppedArea: function(position, size, fitRect) { |
8059 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | 5647 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); |
8060 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | 5648 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); |
8061 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | 5649 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; |
8062 }, | 5650 }, |
8063 | 5651 |
8064 | 5652 |
8065 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | 5653 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
8066 // All the possible configurations. | |
8067 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
8068 var positions = [{ | 5654 var positions = [{ |
8069 verticalAlign: 'top', | 5655 verticalAlign: 'top', |
8070 horizontalAlign: 'left', | 5656 horizontalAlign: 'left', |
8071 top: positionRect.top, | 5657 top: positionRect.top, |
8072 left: positionRect.left | 5658 left: positionRect.left |
8073 }, { | 5659 }, { |
8074 verticalAlign: 'top', | 5660 verticalAlign: 'top', |
8075 horizontalAlign: 'right', | 5661 horizontalAlign: 'right', |
8076 top: positionRect.top, | 5662 top: positionRect.top, |
8077 left: positionRect.right - size.width | 5663 left: positionRect.right - size.width |
8078 }, { | 5664 }, { |
8079 verticalAlign: 'bottom', | 5665 verticalAlign: 'bottom', |
8080 horizontalAlign: 'left', | 5666 horizontalAlign: 'left', |
8081 top: positionRect.bottom - size.height, | 5667 top: positionRect.bottom - size.height, |
8082 left: positionRect.left | 5668 left: positionRect.left |
8083 }, { | 5669 }, { |
8084 verticalAlign: 'bottom', | 5670 verticalAlign: 'bottom', |
8085 horizontalAlign: 'right', | 5671 horizontalAlign: 'right', |
8086 top: positionRect.bottom - size.height, | 5672 top: positionRect.bottom - size.height, |
8087 left: positionRect.right - size.width | 5673 left: positionRect.right - size.width |
8088 }]; | 5674 }]; |
8089 | 5675 |
8090 if (this.noOverlap) { | 5676 if (this.noOverlap) { |
8091 // Duplicate. | |
8092 for (var i = 0, l = positions.length; i < l; i++) { | 5677 for (var i = 0, l = positions.length; i < l; i++) { |
8093 var copy = {}; | 5678 var copy = {}; |
8094 for (var key in positions[i]) { | 5679 for (var key in positions[i]) { |
8095 copy[key] = positions[i][key]; | 5680 copy[key] = positions[i][key]; |
8096 } | 5681 } |
8097 positions.push(copy); | 5682 positions.push(copy); |
8098 } | 5683 } |
8099 // Horizontal overlap only. | |
8100 positions[0].top = positions[1].top += positionRect.height; | 5684 positions[0].top = positions[1].top += positionRect.height; |
8101 positions[2].top = positions[3].top -= positionRect.height; | 5685 positions[2].top = positions[3].top -= positionRect.height; |
8102 // Vertical overlap only. | |
8103 positions[4].left = positions[6].left += positionRect.width; | 5686 positions[4].left = positions[6].left += positionRect.width; |
8104 positions[5].left = positions[7].left -= positionRect.width; | 5687 positions[5].left = positions[7].left -= positionRect.width; |
8105 } | 5688 } |
8106 | 5689 |
8107 // Consider auto as null for coding convenience. | |
8108 vAlign = vAlign === 'auto' ? null : vAlign; | 5690 vAlign = vAlign === 'auto' ? null : vAlign; |
8109 hAlign = hAlign === 'auto' ? null : hAlign; | 5691 hAlign = hAlign === 'auto' ? null : hAlign; |
8110 | 5692 |
8111 var position; | 5693 var position; |
8112 for (var i = 0; i < positions.length; i++) { | 5694 for (var i = 0; i < positions.length; i++) { |
8113 var pos = positions[i]; | 5695 var pos = positions[i]; |
8114 | 5696 |
8115 // If both vAlign and hAlign are defined, return exact match. | |
8116 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
8117 // we'll have to check the croppedArea to make the best choice. | |
8118 if (!this.dynamicAlign && !this.noOverlap && | 5697 if (!this.dynamicAlign && !this.noOverlap && |
8119 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | 5698 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { |
8120 position = pos; | 5699 position = pos; |
8121 break; | 5700 break; |
8122 } | 5701 } |
8123 | 5702 |
8124 // Align is ok if alignment preferences are respected. If no preferences
, | |
8125 // it is considered ok. | |
8126 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | 5703 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && |
8127 (!hAlign || pos.horizontalAlign === hAlign); | 5704 (!hAlign || pos.horizontalAlign === hAlign); |
8128 | 5705 |
8129 // Filter out elements that don't match the alignment (if defined). | |
8130 // With dynamicAlign, we need to consider all the positions to find the | |
8131 // one that minimizes the cropped area. | |
8132 if (!this.dynamicAlign && !alignOk) { | 5706 if (!this.dynamicAlign && !alignOk) { |
8133 continue; | 5707 continue; |
8134 } | 5708 } |
8135 | 5709 |
8136 position = position || pos; | 5710 position = position || pos; |
8137 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | 5711 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
8138 var diff = pos.croppedArea - position.croppedArea; | 5712 var diff = pos.croppedArea - position.croppedArea; |
8139 // Check which crops less. If it crops equally, check if align is ok. | |
8140 if (diff < 0 || (diff === 0 && alignOk)) { | 5713 if (diff < 0 || (diff === 0 && alignOk)) { |
8141 position = pos; | 5714 position = pos; |
8142 } | 5715 } |
8143 // If not cropped and respects the align requirements, keep it. | |
8144 // This allows to prefer positions overlapping horizontally over the | |
8145 // ones overlapping vertically. | |
8146 if (position.croppedArea === 0 && alignOk) { | 5716 if (position.croppedArea === 0 && alignOk) { |
8147 break; | 5717 break; |
8148 } | 5718 } |
8149 } | 5719 } |
8150 | 5720 |
8151 return position; | 5721 return position; |
8152 } | 5722 } |
8153 | 5723 |
8154 }; | 5724 }; |
8155 (function() { | 5725 (function() { |
8156 'use strict'; | 5726 'use strict'; |
8157 | 5727 |
8158 Polymer({ | 5728 Polymer({ |
8159 | 5729 |
8160 is: 'iron-overlay-backdrop', | 5730 is: 'iron-overlay-backdrop', |
8161 | 5731 |
8162 properties: { | 5732 properties: { |
8163 | 5733 |
8164 /** | |
8165 * Returns true if the backdrop is opened. | |
8166 */ | |
8167 opened: { | 5734 opened: { |
8168 reflectToAttribute: true, | 5735 reflectToAttribute: true, |
8169 type: Boolean, | 5736 type: Boolean, |
8170 value: false, | 5737 value: false, |
8171 observer: '_openedChanged' | 5738 observer: '_openedChanged' |
8172 } | 5739 } |
8173 | 5740 |
8174 }, | 5741 }, |
8175 | 5742 |
8176 listeners: { | 5743 listeners: { |
8177 'transitionend': '_onTransitionend' | 5744 'transitionend': '_onTransitionend' |
8178 }, | 5745 }, |
8179 | 5746 |
8180 created: function() { | 5747 created: function() { |
8181 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
8182 this.__openedRaf = null; | 5748 this.__openedRaf = null; |
8183 }, | 5749 }, |
8184 | 5750 |
8185 attached: function() { | 5751 attached: function() { |
8186 this.opened && this._openedChanged(this.opened); | 5752 this.opened && this._openedChanged(this.opened); |
8187 }, | 5753 }, |
8188 | 5754 |
8189 /** | |
8190 * Appends the backdrop to document body if needed. | |
8191 */ | |
8192 prepare: function() { | 5755 prepare: function() { |
8193 if (this.opened && !this.parentNode) { | 5756 if (this.opened && !this.parentNode) { |
8194 Polymer.dom(document.body).appendChild(this); | 5757 Polymer.dom(document.body).appendChild(this); |
8195 } | 5758 } |
8196 }, | 5759 }, |
8197 | 5760 |
8198 /** | |
8199 * Shows the backdrop. | |
8200 */ | |
8201 open: function() { | 5761 open: function() { |
8202 this.opened = true; | 5762 this.opened = true; |
8203 }, | 5763 }, |
8204 | 5764 |
8205 /** | |
8206 * Hides the backdrop. | |
8207 */ | |
8208 close: function() { | 5765 close: function() { |
8209 this.opened = false; | 5766 this.opened = false; |
8210 }, | 5767 }, |
8211 | 5768 |
8212 /** | |
8213 * Removes the backdrop from document body if needed. | |
8214 */ | |
8215 complete: function() { | 5769 complete: function() { |
8216 if (!this.opened && this.parentNode === document.body) { | 5770 if (!this.opened && this.parentNode === document.body) { |
8217 Polymer.dom(this.parentNode).removeChild(this); | 5771 Polymer.dom(this.parentNode).removeChild(this); |
8218 } | 5772 } |
8219 }, | 5773 }, |
8220 | 5774 |
8221 _onTransitionend: function(event) { | 5775 _onTransitionend: function(event) { |
8222 if (event && event.target === this) { | 5776 if (event && event.target === this) { |
8223 this.complete(); | 5777 this.complete(); |
8224 } | 5778 } |
8225 }, | 5779 }, |
8226 | 5780 |
8227 /** | |
8228 * @param {boolean} opened | |
8229 * @private | |
8230 */ | |
8231 _openedChanged: function(opened) { | 5781 _openedChanged: function(opened) { |
8232 if (opened) { | 5782 if (opened) { |
8233 // Auto-attach. | |
8234 this.prepare(); | 5783 this.prepare(); |
8235 } else { | 5784 } else { |
8236 // Animation might be disabled via the mixin or opacity custom property. | |
8237 // If it is disabled in other ways, it's up to the user to call complete
. | |
8238 var cs = window.getComputedStyle(this); | 5785 var cs = window.getComputedStyle(this); |
8239 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 5786 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
8240 this.complete(); | 5787 this.complete(); |
8241 } | 5788 } |
8242 } | 5789 } |
8243 | 5790 |
8244 if (!this.isAttached) { | 5791 if (!this.isAttached) { |
8245 return; | 5792 return; |
8246 } | 5793 } |
8247 | 5794 |
8248 // Always cancel previous requestAnimationFrame. | |
8249 if (this.__openedRaf) { | 5795 if (this.__openedRaf) { |
8250 window.cancelAnimationFrame(this.__openedRaf); | 5796 window.cancelAnimationFrame(this.__openedRaf); |
8251 this.__openedRaf = null; | 5797 this.__openedRaf = null; |
8252 } | 5798 } |
8253 // Force relayout to ensure proper transitions. | |
8254 this.scrollTop = this.scrollTop; | 5799 this.scrollTop = this.scrollTop; |
8255 this.__openedRaf = window.requestAnimationFrame(function() { | 5800 this.__openedRaf = window.requestAnimationFrame(function() { |
8256 this.__openedRaf = null; | 5801 this.__openedRaf = null; |
8257 this.toggleClass('opened', this.opened); | 5802 this.toggleClass('opened', this.opened); |
8258 }.bind(this)); | 5803 }.bind(this)); |
8259 } | 5804 } |
8260 }); | 5805 }); |
8261 | 5806 |
8262 })(); | 5807 })(); |
8263 /** | |
8264 * @struct | |
8265 * @constructor | |
8266 * @private | |
8267 */ | |
8268 Polymer.IronOverlayManagerClass = function() { | 5808 Polymer.IronOverlayManagerClass = function() { |
8269 /** | |
8270 * Used to keep track of the opened overlays. | |
8271 * @private {Array<Element>} | |
8272 */ | |
8273 this._overlays = []; | 5809 this._overlays = []; |
8274 | 5810 |
8275 /** | |
8276 * iframes have a default z-index of 100, | |
8277 * so this default should be at least that. | |
8278 * @private {number} | |
8279 */ | |
8280 this._minimumZ = 101; | 5811 this._minimumZ = 101; |
8281 | 5812 |
8282 /** | |
8283 * Memoized backdrop element. | |
8284 * @private {Element|null} | |
8285 */ | |
8286 this._backdropElement = null; | 5813 this._backdropElement = null; |
8287 | 5814 |
8288 // Enable document-wide tap recognizer. | |
8289 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 5815 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
8290 | 5816 |
8291 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 5817 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
8292 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 5818 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); |
8293 }; | 5819 }; |
8294 | 5820 |
8295 Polymer.IronOverlayManagerClass.prototype = { | 5821 Polymer.IronOverlayManagerClass.prototype = { |
8296 | 5822 |
8297 constructor: Polymer.IronOverlayManagerClass, | 5823 constructor: Polymer.IronOverlayManagerClass, |
8298 | 5824 |
8299 /** | |
8300 * The shared backdrop element. | |
8301 * @type {!Element} backdropElement | |
8302 */ | |
8303 get backdropElement() { | 5825 get backdropElement() { |
8304 if (!this._backdropElement) { | 5826 if (!this._backdropElement) { |
8305 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 5827 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
8306 } | 5828 } |
8307 return this._backdropElement; | 5829 return this._backdropElement; |
8308 }, | 5830 }, |
8309 | 5831 |
8310 /** | |
8311 * The deepest active element. | |
8312 * @type {!Element} activeElement the active element | |
8313 */ | |
8314 get deepActiveElement() { | 5832 get deepActiveElement() { |
8315 // document.activeElement can be null | |
8316 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | |
8317 // In case of null, default it to document.body. | |
8318 var active = document.activeElement || document.body; | 5833 var active = document.activeElement || document.body; |
8319 while (active.root && Polymer.dom(active.root).activeElement) { | 5834 while (active.root && Polymer.dom(active.root).activeElement) { |
8320 active = Polymer.dom(active.root).activeElement; | 5835 active = Polymer.dom(active.root).activeElement; |
8321 } | 5836 } |
8322 return active; | 5837 return active; |
8323 }, | 5838 }, |
8324 | 5839 |
8325 /** | |
8326 * Brings the overlay at the specified index to the front. | |
8327 * @param {number} i | |
8328 * @private | |
8329 */ | |
8330 _bringOverlayAtIndexToFront: function(i) { | 5840 _bringOverlayAtIndexToFront: function(i) { |
8331 var overlay = this._overlays[i]; | 5841 var overlay = this._overlays[i]; |
8332 if (!overlay) { | 5842 if (!overlay) { |
8333 return; | 5843 return; |
8334 } | 5844 } |
8335 var lastI = this._overlays.length - 1; | 5845 var lastI = this._overlays.length - 1; |
8336 var currentOverlay = this._overlays[lastI]; | 5846 var currentOverlay = this._overlays[lastI]; |
8337 // Ensure always-on-top overlay stays on top. | |
8338 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | 5847 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { |
8339 lastI--; | 5848 lastI--; |
8340 } | 5849 } |
8341 // If already the top element, return. | |
8342 if (i >= lastI) { | 5850 if (i >= lastI) { |
8343 return; | 5851 return; |
8344 } | 5852 } |
8345 // Update z-index to be on top. | |
8346 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | 5853 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
8347 if (this._getZ(overlay) <= minimumZ) { | 5854 if (this._getZ(overlay) <= minimumZ) { |
8348 this._applyOverlayZ(overlay, minimumZ); | 5855 this._applyOverlayZ(overlay, minimumZ); |
8349 } | 5856 } |
8350 | 5857 |
8351 // Shift other overlays behind the new on top. | |
8352 while (i < lastI) { | 5858 while (i < lastI) { |
8353 this._overlays[i] = this._overlays[i + 1]; | 5859 this._overlays[i] = this._overlays[i + 1]; |
8354 i++; | 5860 i++; |
8355 } | 5861 } |
8356 this._overlays[lastI] = overlay; | 5862 this._overlays[lastI] = overlay; |
8357 }, | 5863 }, |
8358 | 5864 |
8359 /** | |
8360 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
8361 * Also updates the backdrop z-index. | |
8362 * @param {!Element} overlay | |
8363 */ | |
8364 addOrRemoveOverlay: function(overlay) { | 5865 addOrRemoveOverlay: function(overlay) { |
8365 if (overlay.opened) { | 5866 if (overlay.opened) { |
8366 this.addOverlay(overlay); | 5867 this.addOverlay(overlay); |
8367 } else { | 5868 } else { |
8368 this.removeOverlay(overlay); | 5869 this.removeOverlay(overlay); |
8369 } | 5870 } |
8370 }, | 5871 }, |
8371 | 5872 |
8372 /** | |
8373 * Tracks overlays for z-index and focus management. | |
8374 * Ensures the last added overlay with always-on-top remains on top. | |
8375 * @param {!Element} overlay | |
8376 */ | |
8377 addOverlay: function(overlay) { | 5873 addOverlay: function(overlay) { |
8378 var i = this._overlays.indexOf(overlay); | 5874 var i = this._overlays.indexOf(overlay); |
8379 if (i >= 0) { | 5875 if (i >= 0) { |
8380 this._bringOverlayAtIndexToFront(i); | 5876 this._bringOverlayAtIndexToFront(i); |
8381 this.trackBackdrop(); | 5877 this.trackBackdrop(); |
8382 return; | 5878 return; |
8383 } | 5879 } |
8384 var insertionIndex = this._overlays.length; | 5880 var insertionIndex = this._overlays.length; |
8385 var currentOverlay = this._overlays[insertionIndex - 1]; | 5881 var currentOverlay = this._overlays[insertionIndex - 1]; |
8386 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | 5882 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
8387 var newZ = this._getZ(overlay); | 5883 var newZ = this._getZ(overlay); |
8388 | 5884 |
8389 // Ensure always-on-top overlay stays on top. | |
8390 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | 5885 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { |
8391 // This bumps the z-index of +2. | |
8392 this._applyOverlayZ(currentOverlay, minimumZ); | 5886 this._applyOverlayZ(currentOverlay, minimumZ); |
8393 insertionIndex--; | 5887 insertionIndex--; |
8394 // Update minimumZ to match previous overlay's z-index. | |
8395 var previousOverlay = this._overlays[insertionIndex - 1]; | 5888 var previousOverlay = this._overlays[insertionIndex - 1]; |
8396 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | 5889 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
8397 } | 5890 } |
8398 | 5891 |
8399 // Update z-index and insert overlay. | |
8400 if (newZ <= minimumZ) { | 5892 if (newZ <= minimumZ) { |
8401 this._applyOverlayZ(overlay, minimumZ); | 5893 this._applyOverlayZ(overlay, minimumZ); |
8402 } | 5894 } |
8403 this._overlays.splice(insertionIndex, 0, overlay); | 5895 this._overlays.splice(insertionIndex, 0, overlay); |
8404 | 5896 |
8405 this.trackBackdrop(); | 5897 this.trackBackdrop(); |
8406 }, | 5898 }, |
8407 | 5899 |
8408 /** | |
8409 * @param {!Element} overlay | |
8410 */ | |
8411 removeOverlay: function(overlay) { | 5900 removeOverlay: function(overlay) { |
8412 var i = this._overlays.indexOf(overlay); | 5901 var i = this._overlays.indexOf(overlay); |
8413 if (i === -1) { | 5902 if (i === -1) { |
8414 return; | 5903 return; |
8415 } | 5904 } |
8416 this._overlays.splice(i, 1); | 5905 this._overlays.splice(i, 1); |
8417 | 5906 |
8418 this.trackBackdrop(); | 5907 this.trackBackdrop(); |
8419 }, | 5908 }, |
8420 | 5909 |
8421 /** | |
8422 * Returns the current overlay. | |
8423 * @return {Element|undefined} | |
8424 */ | |
8425 currentOverlay: function() { | 5910 currentOverlay: function() { |
8426 var i = this._overlays.length - 1; | 5911 var i = this._overlays.length - 1; |
8427 return this._overlays[i]; | 5912 return this._overlays[i]; |
8428 }, | 5913 }, |
8429 | 5914 |
8430 /** | |
8431 * Returns the current overlay z-index. | |
8432 * @return {number} | |
8433 */ | |
8434 currentOverlayZ: function() { | 5915 currentOverlayZ: function() { |
8435 return this._getZ(this.currentOverlay()); | 5916 return this._getZ(this.currentOverlay()); |
8436 }, | 5917 }, |
8437 | 5918 |
8438 /** | |
8439 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | |
8440 * This does not effect the z-index of any existing overlays. | |
8441 * @param {number} minimumZ | |
8442 */ | |
8443 ensureMinimumZ: function(minimumZ) { | 5919 ensureMinimumZ: function(minimumZ) { |
8444 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 5920 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
8445 }, | 5921 }, |
8446 | 5922 |
8447 focusOverlay: function() { | 5923 focusOverlay: function() { |
8448 var current = /** @type {?} */ (this.currentOverlay()); | 5924 var current = /** @type {?} */ (this.currentOverlay()); |
8449 if (current) { | 5925 if (current) { |
8450 current._applyFocus(); | 5926 current._applyFocus(); |
8451 } | 5927 } |
8452 }, | 5928 }, |
8453 | 5929 |
8454 /** | |
8455 * Updates the backdrop z-index. | |
8456 */ | |
8457 trackBackdrop: function() { | 5930 trackBackdrop: function() { |
8458 var overlay = this._overlayWithBackdrop(); | 5931 var overlay = this._overlayWithBackdrop(); |
8459 // Avoid creating the backdrop if there is no overlay with backdrop. | |
8460 if (!overlay && !this._backdropElement) { | 5932 if (!overlay && !this._backdropElement) { |
8461 return; | 5933 return; |
8462 } | 5934 } |
8463 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 5935 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
8464 this.backdropElement.opened = !!overlay; | 5936 this.backdropElement.opened = !!overlay; |
8465 }, | 5937 }, |
8466 | 5938 |
8467 /** | |
8468 * @return {Array<Element>} | |
8469 */ | |
8470 getBackdrops: function() { | 5939 getBackdrops: function() { |
8471 var backdrops = []; | 5940 var backdrops = []; |
8472 for (var i = 0; i < this._overlays.length; i++) { | 5941 for (var i = 0; i < this._overlays.length; i++) { |
8473 if (this._overlays[i].withBackdrop) { | 5942 if (this._overlays[i].withBackdrop) { |
8474 backdrops.push(this._overlays[i]); | 5943 backdrops.push(this._overlays[i]); |
8475 } | 5944 } |
8476 } | 5945 } |
8477 return backdrops; | 5946 return backdrops; |
8478 }, | 5947 }, |
8479 | 5948 |
8480 /** | |
8481 * Returns the z-index for the backdrop. | |
8482 * @return {number} | |
8483 */ | |
8484 backdropZ: function() { | 5949 backdropZ: function() { |
8485 return this._getZ(this._overlayWithBackdrop()) - 1; | 5950 return this._getZ(this._overlayWithBackdrop()) - 1; |
8486 }, | 5951 }, |
8487 | 5952 |
8488 /** | |
8489 * Returns the first opened overlay that has a backdrop. | |
8490 * @return {Element|undefined} | |
8491 * @private | |
8492 */ | |
8493 _overlayWithBackdrop: function() { | 5953 _overlayWithBackdrop: function() { |
8494 for (var i = 0; i < this._overlays.length; i++) { | 5954 for (var i = 0; i < this._overlays.length; i++) { |
8495 if (this._overlays[i].withBackdrop) { | 5955 if (this._overlays[i].withBackdrop) { |
8496 return this._overlays[i]; | 5956 return this._overlays[i]; |
8497 } | 5957 } |
8498 } | 5958 } |
8499 }, | 5959 }, |
8500 | 5960 |
8501 /** | |
8502 * Calculates the minimum z-index for the overlay. | |
8503 * @param {Element=} overlay | |
8504 * @private | |
8505 */ | |
8506 _getZ: function(overlay) { | 5961 _getZ: function(overlay) { |
8507 var z = this._minimumZ; | 5962 var z = this._minimumZ; |
8508 if (overlay) { | 5963 if (overlay) { |
8509 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 5964 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); |
8510 // Check if is a number | |
8511 // Number.isNaN not supported in IE 10+ | |
8512 if (z1 === z1) { | 5965 if (z1 === z1) { |
8513 z = z1; | 5966 z = z1; |
8514 } | 5967 } |
8515 } | 5968 } |
8516 return z; | 5969 return z; |
8517 }, | 5970 }, |
8518 | 5971 |
8519 /** | |
8520 * @param {!Element} element | |
8521 * @param {number|string} z | |
8522 * @private | |
8523 */ | |
8524 _setZ: function(element, z) { | 5972 _setZ: function(element, z) { |
8525 element.style.zIndex = z; | 5973 element.style.zIndex = z; |
8526 }, | 5974 }, |
8527 | 5975 |
8528 /** | |
8529 * @param {!Element} overlay | |
8530 * @param {number} aboveZ | |
8531 * @private | |
8532 */ | |
8533 _applyOverlayZ: function(overlay, aboveZ) { | 5976 _applyOverlayZ: function(overlay, aboveZ) { |
8534 this._setZ(overlay, aboveZ + 2); | 5977 this._setZ(overlay, aboveZ + 2); |
8535 }, | 5978 }, |
8536 | 5979 |
8537 /** | |
8538 * Returns the deepest overlay in the path. | |
8539 * @param {Array<Element>=} path | |
8540 * @return {Element|undefined} | |
8541 * @suppress {missingProperties} | |
8542 * @private | |
8543 */ | |
8544 _overlayInPath: function(path) { | 5980 _overlayInPath: function(path) { |
8545 path = path || []; | 5981 path = path || []; |
8546 for (var i = 0; i < path.length; i++) { | 5982 for (var i = 0; i < path.length; i++) { |
8547 if (path[i]._manager === this) { | 5983 if (path[i]._manager === this) { |
8548 return path[i]; | 5984 return path[i]; |
8549 } | 5985 } |
8550 } | 5986 } |
8551 }, | 5987 }, |
8552 | 5988 |
8553 /** | |
8554 * Ensures the click event is delegated to the right overlay. | |
8555 * @param {!Event} event | |
8556 * @private | |
8557 */ | |
8558 _onCaptureClick: function(event) { | 5989 _onCaptureClick: function(event) { |
8559 var overlay = /** @type {?} */ (this.currentOverlay()); | 5990 var overlay = /** @type {?} */ (this.currentOverlay()); |
8560 // Check if clicked outside of top overlay. | |
8561 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | 5991 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
8562 overlay._onCaptureClick(event); | 5992 overlay._onCaptureClick(event); |
8563 } | 5993 } |
8564 }, | 5994 }, |
8565 | 5995 |
8566 /** | |
8567 * Ensures the focus event is delegated to the right overlay. | |
8568 * @param {!Event} event | |
8569 * @private | |
8570 */ | |
8571 _onCaptureFocus: function(event) { | 5996 _onCaptureFocus: function(event) { |
8572 var overlay = /** @type {?} */ (this.currentOverlay()); | 5997 var overlay = /** @type {?} */ (this.currentOverlay()); |
8573 if (overlay) { | 5998 if (overlay) { |
8574 overlay._onCaptureFocus(event); | 5999 overlay._onCaptureFocus(event); |
8575 } | 6000 } |
8576 }, | 6001 }, |
8577 | 6002 |
8578 /** | |
8579 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
8580 * @param {!Event} event | |
8581 * @private | |
8582 */ | |
8583 _onCaptureKeyDown: function(event) { | 6003 _onCaptureKeyDown: function(event) { |
8584 var overlay = /** @type {?} */ (this.currentOverlay()); | 6004 var overlay = /** @type {?} */ (this.currentOverlay()); |
8585 if (overlay) { | 6005 if (overlay) { |
8586 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | 6006 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ |
8587 overlay._onCaptureEsc(event); | 6007 overlay._onCaptureEsc(event); |
8588 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | 6008 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { |
8589 overlay._onCaptureTab(event); | 6009 overlay._onCaptureTab(event); |
8590 } | 6010 } |
8591 } | 6011 } |
8592 }, | 6012 }, |
8593 | 6013 |
8594 /** | |
8595 * Returns if the overlay1 should be behind overlay2. | |
8596 * @param {!Element} overlay1 | |
8597 * @param {!Element} overlay2 | |
8598 * @return {boolean} | |
8599 * @suppress {missingProperties} | |
8600 * @private | |
8601 */ | |
8602 _shouldBeBehindOverlay: function(overlay1, overlay2) { | 6014 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
8603 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | 6015 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
8604 } | 6016 } |
8605 }; | 6017 }; |
8606 | 6018 |
8607 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | 6019 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
8608 (function() { | 6020 (function() { |
8609 'use strict'; | 6021 'use strict'; |
8610 | 6022 |
8611 /** | |
8612 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
8613 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
8614 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
8615 | |
8616 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
8617 for an example. | |
8618 | |
8619 ### Closing and canceling | |
8620 | |
8621 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
8622 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
8623 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
8624 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
8625 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
8626 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
8627 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
8628 | |
8629 ### Positioning | |
8630 | |
8631 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
8632 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
8633 | |
8634 ### Backdrop | |
8635 | |
8636 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
8637 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
8638 options. | |
8639 | |
8640 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
8641 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
8642 to achieve a different behavior. | |
8643 | |
8644 ### Limitations | |
8645 | |
8646 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
8647 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
8648 context. You should place this element as a child of `<body>` whenever possible. | |
8649 | |
8650 @demo demo/index.html | |
8651 @polymerBehavior Polymer.IronOverlayBehavior | |
8652 */ | |
8653 | 6023 |
8654 Polymer.IronOverlayBehaviorImpl = { | 6024 Polymer.IronOverlayBehaviorImpl = { |
8655 | 6025 |
8656 properties: { | 6026 properties: { |
8657 | 6027 |
8658 /** | |
8659 * True if the overlay is currently displayed. | |
8660 */ | |
8661 opened: { | 6028 opened: { |
8662 observer: '_openedChanged', | 6029 observer: '_openedChanged', |
8663 type: Boolean, | 6030 type: Boolean, |
8664 value: false, | 6031 value: false, |
8665 notify: true | 6032 notify: true |
8666 }, | 6033 }, |
8667 | 6034 |
8668 /** | |
8669 * True if the overlay was canceled when it was last closed. | |
8670 */ | |
8671 canceled: { | 6035 canceled: { |
8672 observer: '_canceledChanged', | 6036 observer: '_canceledChanged', |
8673 readOnly: true, | 6037 readOnly: true, |
8674 type: Boolean, | 6038 type: Boolean, |
8675 value: false | 6039 value: false |
8676 }, | 6040 }, |
8677 | 6041 |
8678 /** | |
8679 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
8680 * within the light DOM of the overlay. | |
8681 */ | |
8682 withBackdrop: { | 6042 withBackdrop: { |
8683 observer: '_withBackdropChanged', | 6043 observer: '_withBackdropChanged', |
8684 type: Boolean | 6044 type: Boolean |
8685 }, | 6045 }, |
8686 | 6046 |
8687 /** | |
8688 * Set to true to disable auto-focusing the overlay or child nodes with | |
8689 * the `autofocus` attribute` when the overlay is opened. | |
8690 */ | |
8691 noAutoFocus: { | 6047 noAutoFocus: { |
8692 type: Boolean, | 6048 type: Boolean, |
8693 value: false | 6049 value: false |
8694 }, | 6050 }, |
8695 | 6051 |
8696 /** | |
8697 * Set to true to disable canceling the overlay with the ESC key. | |
8698 */ | |
8699 noCancelOnEscKey: { | 6052 noCancelOnEscKey: { |
8700 type: Boolean, | 6053 type: Boolean, |
8701 value: false | 6054 value: false |
8702 }, | 6055 }, |
8703 | 6056 |
8704 /** | |
8705 * Set to true to disable canceling the overlay by clicking outside it. | |
8706 */ | |
8707 noCancelOnOutsideClick: { | 6057 noCancelOnOutsideClick: { |
8708 type: Boolean, | 6058 type: Boolean, |
8709 value: false | 6059 value: false |
8710 }, | 6060 }, |
8711 | 6061 |
8712 /** | |
8713 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
8714 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
8715 * behavior can provide other reasons in addition to `canceled`. | |
8716 */ | |
8717 closingReason: { | 6062 closingReason: { |
8718 // was a getter before, but needs to be a property so other | |
8719 // behaviors can override this. | |
8720 type: Object | 6063 type: Object |
8721 }, | 6064 }, |
8722 | 6065 |
8723 /** | |
8724 * Set to true to enable restoring of focus when overlay is closed. | |
8725 */ | |
8726 restoreFocusOnClose: { | 6066 restoreFocusOnClose: { |
8727 type: Boolean, | 6067 type: Boolean, |
8728 value: false | 6068 value: false |
8729 }, | 6069 }, |
8730 | 6070 |
8731 /** | |
8732 * Set to true to keep overlay always on top. | |
8733 */ | |
8734 alwaysOnTop: { | 6071 alwaysOnTop: { |
8735 type: Boolean | 6072 type: Boolean |
8736 }, | 6073 }, |
8737 | 6074 |
8738 /** | |
8739 * Shortcut to access to the overlay manager. | |
8740 * @private | |
8741 * @type {Polymer.IronOverlayManagerClass} | |
8742 */ | |
8743 _manager: { | 6075 _manager: { |
8744 type: Object, | 6076 type: Object, |
8745 value: Polymer.IronOverlayManager | 6077 value: Polymer.IronOverlayManager |
8746 }, | 6078 }, |
8747 | 6079 |
8748 /** | |
8749 * The node being focused. | |
8750 * @type {?Node} | |
8751 */ | |
8752 _focusedChild: { | 6080 _focusedChild: { |
8753 type: Object | 6081 type: Object |
8754 } | 6082 } |
8755 | 6083 |
8756 }, | 6084 }, |
8757 | 6085 |
8758 listeners: { | 6086 listeners: { |
8759 'iron-resize': '_onIronResize' | 6087 'iron-resize': '_onIronResize' |
8760 }, | 6088 }, |
8761 | 6089 |
8762 /** | |
8763 * The backdrop element. | |
8764 * @type {Element} | |
8765 */ | |
8766 get backdropElement() { | 6090 get backdropElement() { |
8767 return this._manager.backdropElement; | 6091 return this._manager.backdropElement; |
8768 }, | 6092 }, |
8769 | 6093 |
8770 /** | |
8771 * Returns the node to give focus to. | |
8772 * @type {Node} | |
8773 */ | |
8774 get _focusNode() { | 6094 get _focusNode() { |
8775 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 6095 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
8776 }, | 6096 }, |
8777 | 6097 |
8778 /** | |
8779 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
8780 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
8781 * to wrap the focus for overlays `with-backdrop`. | |
8782 * | |
8783 * If you know what is your content (specifically the first and last focusab
le children), | |
8784 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
8785 * @type {Array<Node>} | |
8786 * @protected | |
8787 */ | |
8788 get _focusableNodes() { | 6098 get _focusableNodes() { |
8789 // Elements that can be focused even if they have [disabled] attribute. | |
8790 var FOCUSABLE_WITH_DISABLED = [ | 6099 var FOCUSABLE_WITH_DISABLED = [ |
8791 'a[href]', | 6100 'a[href]', |
8792 'area[href]', | 6101 'area[href]', |
8793 'iframe', | 6102 'iframe', |
8794 '[tabindex]', | 6103 '[tabindex]', |
8795 '[contentEditable=true]' | 6104 '[contentEditable=true]' |
8796 ]; | 6105 ]; |
8797 | 6106 |
8798 // Elements that cannot be focused if they have [disabled] attribute. | |
8799 var FOCUSABLE_WITHOUT_DISABLED = [ | 6107 var FOCUSABLE_WITHOUT_DISABLED = [ |
8800 'input', | 6108 'input', |
8801 'select', | 6109 'select', |
8802 'textarea', | 6110 'textarea', |
8803 'button' | 6111 'button' |
8804 ]; | 6112 ]; |
8805 | 6113 |
8806 // Discard elements with tabindex=-1 (makes them not focusable). | |
8807 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | 6114 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + |
8808 ':not([tabindex="-1"]),' + | 6115 ':not([tabindex="-1"]),' + |
8809 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | 6116 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + |
8810 ':not([disabled]):not([tabindex="-1"])'; | 6117 ':not([disabled]):not([tabindex="-1"])'; |
8811 | 6118 |
8812 var focusables = Polymer.dom(this).querySelectorAll(selector); | 6119 var focusables = Polymer.dom(this).querySelectorAll(selector); |
8813 if (this.tabIndex >= 0) { | 6120 if (this.tabIndex >= 0) { |
8814 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
8815 // and the overlay should be the first of the list. | |
8816 focusables.splice(0, 0, this); | 6121 focusables.splice(0, 0, this); |
8817 } | 6122 } |
8818 // Sort by tabindex. | |
8819 return focusables.sort(function (a, b) { | 6123 return focusables.sort(function (a, b) { |
8820 if (a.tabIndex === b.tabIndex) { | 6124 if (a.tabIndex === b.tabIndex) { |
8821 return 0; | 6125 return 0; |
8822 } | 6126 } |
8823 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 6127 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
8824 return 1; | 6128 return 1; |
8825 } | 6129 } |
8826 return -1; | 6130 return -1; |
8827 }); | 6131 }); |
8828 }, | 6132 }, |
8829 | 6133 |
8830 ready: function() { | 6134 ready: function() { |
8831 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
8832 this.__isAnimating = false; | 6135 this.__isAnimating = false; |
8833 // with-backdrop needs tabindex to be set in order to trap the focus. | |
8834 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
8835 this.__shouldRemoveTabIndex = false; | 6136 this.__shouldRemoveTabIndex = false; |
8836 // Used for wrapping the focus on TAB / Shift+TAB. | |
8837 this.__firstFocusableNode = this.__lastFocusableNode = null; | 6137 this.__firstFocusableNode = this.__lastFocusableNode = null; |
8838 // Used by __onNextAnimationFrame to cancel any previous callback. | |
8839 this.__raf = null; | 6138 this.__raf = null; |
8840 // Focused node before overlay gets opened. Can be restored on close. | |
8841 this.__restoreFocusNode = null; | 6139 this.__restoreFocusNode = null; |
8842 this._ensureSetup(); | 6140 this._ensureSetup(); |
8843 }, | 6141 }, |
8844 | 6142 |
8845 attached: function() { | 6143 attached: function() { |
8846 // Call _openedChanged here so that position can be computed correctly. | |
8847 if (this.opened) { | 6144 if (this.opened) { |
8848 this._openedChanged(this.opened); | 6145 this._openedChanged(this.opened); |
8849 } | 6146 } |
8850 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 6147 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
8851 }, | 6148 }, |
8852 | 6149 |
8853 detached: function() { | 6150 detached: function() { |
8854 Polymer.dom(this).unobserveNodes(this._observer); | 6151 Polymer.dom(this).unobserveNodes(this._observer); |
8855 this._observer = null; | 6152 this._observer = null; |
8856 if (this.__raf) { | 6153 if (this.__raf) { |
8857 window.cancelAnimationFrame(this.__raf); | 6154 window.cancelAnimationFrame(this.__raf); |
8858 this.__raf = null; | 6155 this.__raf = null; |
8859 } | 6156 } |
8860 this._manager.removeOverlay(this); | 6157 this._manager.removeOverlay(this); |
8861 }, | 6158 }, |
8862 | 6159 |
8863 /** | |
8864 * Toggle the opened state of the overlay. | |
8865 */ | |
8866 toggle: function() { | 6160 toggle: function() { |
8867 this._setCanceled(false); | 6161 this._setCanceled(false); |
8868 this.opened = !this.opened; | 6162 this.opened = !this.opened; |
8869 }, | 6163 }, |
8870 | 6164 |
8871 /** | |
8872 * Open the overlay. | |
8873 */ | |
8874 open: function() { | 6165 open: function() { |
8875 this._setCanceled(false); | 6166 this._setCanceled(false); |
8876 this.opened = true; | 6167 this.opened = true; |
8877 }, | 6168 }, |
8878 | 6169 |
8879 /** | |
8880 * Close the overlay. | |
8881 */ | |
8882 close: function() { | 6170 close: function() { |
8883 this._setCanceled(false); | 6171 this._setCanceled(false); |
8884 this.opened = false; | 6172 this.opened = false; |
8885 }, | 6173 }, |
8886 | 6174 |
8887 /** | |
8888 * Cancels the overlay. | |
8889 * @param {Event=} event The original event | |
8890 */ | |
8891 cancel: function(event) { | 6175 cancel: function(event) { |
8892 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 6176 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); |
8893 if (cancelEvent.defaultPrevented) { | 6177 if (cancelEvent.defaultPrevented) { |
8894 return; | 6178 return; |
8895 } | 6179 } |
8896 | 6180 |
8897 this._setCanceled(true); | 6181 this._setCanceled(true); |
8898 this.opened = false; | 6182 this.opened = false; |
8899 }, | 6183 }, |
8900 | 6184 |
8901 _ensureSetup: function() { | 6185 _ensureSetup: function() { |
8902 if (this._overlaySetup) { | 6186 if (this._overlaySetup) { |
8903 return; | 6187 return; |
8904 } | 6188 } |
8905 this._overlaySetup = true; | 6189 this._overlaySetup = true; |
8906 this.style.outline = 'none'; | 6190 this.style.outline = 'none'; |
8907 this.style.display = 'none'; | 6191 this.style.display = 'none'; |
8908 }, | 6192 }, |
8909 | 6193 |
8910 /** | |
8911 * Called when `opened` changes. | |
8912 * @param {boolean=} opened | |
8913 * @protected | |
8914 */ | |
8915 _openedChanged: function(opened) { | 6194 _openedChanged: function(opened) { |
8916 if (opened) { | 6195 if (opened) { |
8917 this.removeAttribute('aria-hidden'); | 6196 this.removeAttribute('aria-hidden'); |
8918 } else { | 6197 } else { |
8919 this.setAttribute('aria-hidden', 'true'); | 6198 this.setAttribute('aria-hidden', 'true'); |
8920 } | 6199 } |
8921 | 6200 |
8922 // Defer any animation-related code on attached | |
8923 // (_openedChanged gets called again on attached). | |
8924 if (!this.isAttached) { | 6201 if (!this.isAttached) { |
8925 return; | 6202 return; |
8926 } | 6203 } |
8927 | 6204 |
8928 this.__isAnimating = true; | 6205 this.__isAnimating = true; |
8929 | 6206 |
8930 // Use requestAnimationFrame for non-blocking rendering. | |
8931 this.__onNextAnimationFrame(this.__openedChanged); | 6207 this.__onNextAnimationFrame(this.__openedChanged); |
8932 }, | 6208 }, |
8933 | 6209 |
8934 _canceledChanged: function() { | 6210 _canceledChanged: function() { |
8935 this.closingReason = this.closingReason || {}; | 6211 this.closingReason = this.closingReason || {}; |
8936 this.closingReason.canceled = this.canceled; | 6212 this.closingReason.canceled = this.canceled; |
8937 }, | 6213 }, |
8938 | 6214 |
8939 _withBackdropChanged: function() { | 6215 _withBackdropChanged: function() { |
8940 // If tabindex is already set, no need to override it. | |
8941 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 6216 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
8942 this.setAttribute('tabindex', '-1'); | 6217 this.setAttribute('tabindex', '-1'); |
8943 this.__shouldRemoveTabIndex = true; | 6218 this.__shouldRemoveTabIndex = true; |
8944 } else if (this.__shouldRemoveTabIndex) { | 6219 } else if (this.__shouldRemoveTabIndex) { |
8945 this.removeAttribute('tabindex'); | 6220 this.removeAttribute('tabindex'); |
8946 this.__shouldRemoveTabIndex = false; | 6221 this.__shouldRemoveTabIndex = false; |
8947 } | 6222 } |
8948 if (this.opened && this.isAttached) { | 6223 if (this.opened && this.isAttached) { |
8949 this._manager.trackBackdrop(); | 6224 this._manager.trackBackdrop(); |
8950 } | 6225 } |
8951 }, | 6226 }, |
8952 | 6227 |
8953 /** | |
8954 * tasks which must occur before opening; e.g. making the element visible. | |
8955 * @protected | |
8956 */ | |
8957 _prepareRenderOpened: function() { | 6228 _prepareRenderOpened: function() { |
8958 // Store focused node. | |
8959 this.__restoreFocusNode = this._manager.deepActiveElement; | 6229 this.__restoreFocusNode = this._manager.deepActiveElement; |
8960 | 6230 |
8961 // Needed to calculate the size of the overlay so that transitions on its
size | |
8962 // will have the correct starting points. | |
8963 this._preparePositioning(); | 6231 this._preparePositioning(); |
8964 this.refit(); | 6232 this.refit(); |
8965 this._finishPositioning(); | 6233 this._finishPositioning(); |
8966 | 6234 |
8967 // Safari will apply the focus to the autofocus element when displayed | |
8968 // for the first time, so we make sure to return the focus where it was. | |
8969 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 6235 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
8970 this._focusNode.blur(); | 6236 this._focusNode.blur(); |
8971 this.__restoreFocusNode.focus(); | 6237 this.__restoreFocusNode.focus(); |
8972 } | 6238 } |
8973 }, | 6239 }, |
8974 | 6240 |
8975 /** | |
8976 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
8977 * @protected | |
8978 */ | |
8979 _renderOpened: function() { | 6241 _renderOpened: function() { |
8980 this._finishRenderOpened(); | 6242 this._finishRenderOpened(); |
8981 }, | 6243 }, |
8982 | 6244 |
8983 /** | |
8984 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
8985 * @protected | |
8986 */ | |
8987 _renderClosed: function() { | 6245 _renderClosed: function() { |
8988 this._finishRenderClosed(); | 6246 this._finishRenderClosed(); |
8989 }, | 6247 }, |
8990 | 6248 |
8991 /** | |
8992 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
8993 * @protected | |
8994 */ | |
8995 _finishRenderOpened: function() { | 6249 _finishRenderOpened: function() { |
8996 this.notifyResize(); | 6250 this.notifyResize(); |
8997 this.__isAnimating = false; | 6251 this.__isAnimating = false; |
8998 | 6252 |
8999 // Store it so we don't query too much. | |
9000 var focusableNodes = this._focusableNodes; | 6253 var focusableNodes = this._focusableNodes; |
9001 this.__firstFocusableNode = focusableNodes[0]; | 6254 this.__firstFocusableNode = focusableNodes[0]; |
9002 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 6255 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
9003 | 6256 |
9004 this.fire('iron-overlay-opened'); | 6257 this.fire('iron-overlay-opened'); |
9005 }, | 6258 }, |
9006 | 6259 |
9007 /** | |
9008 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
9009 * @protected | |
9010 */ | |
9011 _finishRenderClosed: function() { | 6260 _finishRenderClosed: function() { |
9012 // Hide the overlay. | |
9013 this.style.display = 'none'; | 6261 this.style.display = 'none'; |
9014 // Reset z-index only at the end of the animation. | |
9015 this.style.zIndex = ''; | 6262 this.style.zIndex = ''; |
9016 this.notifyResize(); | 6263 this.notifyResize(); |
9017 this.__isAnimating = false; | 6264 this.__isAnimating = false; |
9018 this.fire('iron-overlay-closed', this.closingReason); | 6265 this.fire('iron-overlay-closed', this.closingReason); |
9019 }, | 6266 }, |
9020 | 6267 |
9021 _preparePositioning: function() { | 6268 _preparePositioning: function() { |
9022 this.style.transition = this.style.webkitTransition = 'none'; | 6269 this.style.transition = this.style.webkitTransition = 'none'; |
9023 this.style.transform = this.style.webkitTransform = 'none'; | 6270 this.style.transform = this.style.webkitTransform = 'none'; |
9024 this.style.display = ''; | 6271 this.style.display = ''; |
9025 }, | 6272 }, |
9026 | 6273 |
9027 _finishPositioning: function() { | 6274 _finishPositioning: function() { |
9028 // First, make it invisible & reactivate animations. | |
9029 this.style.display = 'none'; | 6275 this.style.display = 'none'; |
9030 // Force reflow before re-enabling animations so that they don't start. | |
9031 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9032 this.scrollTop = this.scrollTop; | 6276 this.scrollTop = this.scrollTop; |
9033 this.style.transition = this.style.webkitTransition = ''; | 6277 this.style.transition = this.style.webkitTransition = ''; |
9034 this.style.transform = this.style.webkitTransform = ''; | 6278 this.style.transform = this.style.webkitTransform = ''; |
9035 // Now that animations are enabled, make it visible again | |
9036 this.style.display = ''; | 6279 this.style.display = ''; |
9037 // Force reflow, so that following animations are properly started. | |
9038 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9039 this.scrollTop = this.scrollTop; | 6280 this.scrollTop = this.scrollTop; |
9040 }, | 6281 }, |
9041 | 6282 |
9042 /** | |
9043 * Applies focus according to the opened state. | |
9044 * @protected | |
9045 */ | |
9046 _applyFocus: function() { | 6283 _applyFocus: function() { |
9047 if (this.opened) { | 6284 if (this.opened) { |
9048 if (!this.noAutoFocus) { | 6285 if (!this.noAutoFocus) { |
9049 this._focusNode.focus(); | 6286 this._focusNode.focus(); |
9050 } | 6287 } |
9051 } | 6288 } |
9052 else { | 6289 else { |
9053 this._focusNode.blur(); | 6290 this._focusNode.blur(); |
9054 this._focusedChild = null; | 6291 this._focusedChild = null; |
9055 // Restore focus. | |
9056 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 6292 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
9057 this.__restoreFocusNode.focus(); | 6293 this.__restoreFocusNode.focus(); |
9058 } | 6294 } |
9059 this.__restoreFocusNode = null; | 6295 this.__restoreFocusNode = null; |
9060 // If many overlays get closed at the same time, one of them would still | |
9061 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
9062 // infinitely, so we check for this not to be the current overlay. | |
9063 var currentOverlay = this._manager.currentOverlay(); | 6296 var currentOverlay = this._manager.currentOverlay(); |
9064 if (currentOverlay && this !== currentOverlay) { | 6297 if (currentOverlay && this !== currentOverlay) { |
9065 currentOverlay._applyFocus(); | 6298 currentOverlay._applyFocus(); |
9066 } | 6299 } |
9067 } | 6300 } |
9068 }, | 6301 }, |
9069 | 6302 |
9070 /** | |
9071 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
9072 * @param {!Event} event | |
9073 * @protected | |
9074 */ | |
9075 _onCaptureClick: function(event) { | 6303 _onCaptureClick: function(event) { |
9076 if (!this.noCancelOnOutsideClick) { | 6304 if (!this.noCancelOnOutsideClick) { |
9077 this.cancel(event); | 6305 this.cancel(event); |
9078 } | 6306 } |
9079 }, | 6307 }, |
9080 | 6308 |
9081 /** | |
9082 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
9083 * @param {!Event} event | |
9084 * @protected | |
9085 */ | |
9086 _onCaptureFocus: function (event) { | 6309 _onCaptureFocus: function (event) { |
9087 if (!this.withBackdrop) { | 6310 if (!this.withBackdrop) { |
9088 return; | 6311 return; |
9089 } | 6312 } |
9090 var path = Polymer.dom(event).path; | 6313 var path = Polymer.dom(event).path; |
9091 if (path.indexOf(this) === -1) { | 6314 if (path.indexOf(this) === -1) { |
9092 event.stopPropagation(); | 6315 event.stopPropagation(); |
9093 this._applyFocus(); | 6316 this._applyFocus(); |
9094 } else { | 6317 } else { |
9095 this._focusedChild = path[0]; | 6318 this._focusedChild = path[0]; |
9096 } | 6319 } |
9097 }, | 6320 }, |
9098 | 6321 |
9099 /** | |
9100 * Handles the ESC key event and cancels (closes) the overlay. | |
9101 * @param {!Event} event | |
9102 * @protected | |
9103 */ | |
9104 _onCaptureEsc: function(event) { | 6322 _onCaptureEsc: function(event) { |
9105 if (!this.noCancelOnEscKey) { | 6323 if (!this.noCancelOnEscKey) { |
9106 this.cancel(event); | 6324 this.cancel(event); |
9107 } | 6325 } |
9108 }, | 6326 }, |
9109 | 6327 |
9110 /** | |
9111 * Handles TAB key events to track focus changes. | |
9112 * Will wrap focus for overlays withBackdrop. | |
9113 * @param {!Event} event | |
9114 * @protected | |
9115 */ | |
9116 _onCaptureTab: function(event) { | 6328 _onCaptureTab: function(event) { |
9117 if (!this.withBackdrop) { | 6329 if (!this.withBackdrop) { |
9118 return; | 6330 return; |
9119 } | 6331 } |
9120 // TAB wraps from last to first focusable. | |
9121 // Shift + TAB wraps from first to last focusable. | |
9122 var shift = event.shiftKey; | 6332 var shift = event.shiftKey; |
9123 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 6333 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
9124 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 6334 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
9125 var shouldWrap = false; | 6335 var shouldWrap = false; |
9126 if (nodeToCheck === nodeToSet) { | 6336 if (nodeToCheck === nodeToSet) { |
9127 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
9128 // with 0 or 1 focusables; in either case we still need to trap the | |
9129 // focus within the overlay. | |
9130 shouldWrap = true; | 6337 shouldWrap = true; |
9131 } else { | 6338 } else { |
9132 // In dom=shadow, the manager will receive focus changes on the main | |
9133 // root but not the ones within other shadow roots, so we can't rely on | |
9134 // _focusedChild, but we should check the deepest active element. | |
9135 var focusedNode = this._manager.deepActiveElement; | 6339 var focusedNode = this._manager.deepActiveElement; |
9136 // If the active element is not the nodeToCheck but the overlay itself, | |
9137 // it means the focus is about to go outside the overlay, hence we | |
9138 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
9139 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | 6340 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); |
9140 } | 6341 } |
9141 | 6342 |
9142 if (shouldWrap) { | 6343 if (shouldWrap) { |
9143 // When the overlay contains the last focusable element of the document | |
9144 // and it's already focused, pressing TAB would move the focus outside | |
9145 // the document (e.g. to the browser search bar). Similarly, when the | |
9146 // overlay contains the first focusable element of the document and it's | |
9147 // already focused, pressing Shift+TAB would move the focus outside the | |
9148 // document (e.g. to the browser search bar). | |
9149 // In both cases, we would not receive a focus event, but only a blur. | |
9150 // In order to achieve focus wrapping, we prevent this TAB event and | |
9151 // force the focus. This will also prevent the focus to temporarily move | |
9152 // outside the overlay, which might cause scrolling. | |
9153 event.preventDefault(); | 6344 event.preventDefault(); |
9154 this._focusedChild = nodeToSet; | 6345 this._focusedChild = nodeToSet; |
9155 this._applyFocus(); | 6346 this._applyFocus(); |
9156 } | 6347 } |
9157 }, | 6348 }, |
9158 | 6349 |
9159 /** | |
9160 * Refits if the overlay is opened and not animating. | |
9161 * @protected | |
9162 */ | |
9163 _onIronResize: function() { | 6350 _onIronResize: function() { |
9164 if (this.opened && !this.__isAnimating) { | 6351 if (this.opened && !this.__isAnimating) { |
9165 this.__onNextAnimationFrame(this.refit); | 6352 this.__onNextAnimationFrame(this.refit); |
9166 } | 6353 } |
9167 }, | 6354 }, |
9168 | 6355 |
9169 /** | |
9170 * Will call notifyResize if overlay is opened. | |
9171 * Can be overridden in order to avoid multiple observers on the same node. | |
9172 * @protected | |
9173 */ | |
9174 _onNodesChange: function() { | 6356 _onNodesChange: function() { |
9175 if (this.opened && !this.__isAnimating) { | 6357 if (this.opened && !this.__isAnimating) { |
9176 this.notifyResize(); | 6358 this.notifyResize(); |
9177 } | 6359 } |
9178 }, | 6360 }, |
9179 | 6361 |
9180 /** | |
9181 * Tasks executed when opened changes: prepare for the opening, move the | |
9182 * focus, update the manager, render opened/closed. | |
9183 * @private | |
9184 */ | |
9185 __openedChanged: function() { | 6362 __openedChanged: function() { |
9186 if (this.opened) { | 6363 if (this.opened) { |
9187 // Make overlay visible, then add it to the manager. | |
9188 this._prepareRenderOpened(); | 6364 this._prepareRenderOpened(); |
9189 this._manager.addOverlay(this); | 6365 this._manager.addOverlay(this); |
9190 // Move the focus to the child node with [autofocus]. | |
9191 this._applyFocus(); | 6366 this._applyFocus(); |
9192 | 6367 |
9193 this._renderOpened(); | 6368 this._renderOpened(); |
9194 } else { | 6369 } else { |
9195 // Remove overlay, then restore the focus before actually closing. | |
9196 this._manager.removeOverlay(this); | 6370 this._manager.removeOverlay(this); |
9197 this._applyFocus(); | 6371 this._applyFocus(); |
9198 | 6372 |
9199 this._renderClosed(); | 6373 this._renderClosed(); |
9200 } | 6374 } |
9201 }, | 6375 }, |
9202 | 6376 |
9203 /** | |
9204 * Executes a callback on the next animation frame, overriding any previous | |
9205 * callback awaiting for the next animation frame. e.g. | |
9206 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
9207 * `callback1` will never be invoked. | |
9208 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
9209 * @private | |
9210 */ | |
9211 __onNextAnimationFrame: function(callback) { | 6377 __onNextAnimationFrame: function(callback) { |
9212 if (this.__raf) { | 6378 if (this.__raf) { |
9213 window.cancelAnimationFrame(this.__raf); | 6379 window.cancelAnimationFrame(this.__raf); |
9214 } | 6380 } |
9215 var self = this; | 6381 var self = this; |
9216 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 6382 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
9217 self.__raf = null; | 6383 self.__raf = null; |
9218 callback.call(self); | 6384 callback.call(self); |
9219 }); | 6385 }); |
9220 } | 6386 } |
9221 | 6387 |
9222 }; | 6388 }; |
9223 | 6389 |
9224 /** @polymerBehavior */ | 6390 /** @polymerBehavior */ |
9225 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | 6391 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; |
9226 | 6392 |
9227 /** | |
9228 * Fired after the overlay opens. | |
9229 * @event iron-overlay-opened | |
9230 */ | |
9231 | 6393 |
9232 /** | |
9233 * Fired when the overlay is canceled, but before it is closed. | |
9234 * @event iron-overlay-canceled | |
9235 * @param {Event} event The closing of the overlay can be prevented | |
9236 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
9237 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
9238 */ | |
9239 | 6394 |
9240 /** | |
9241 * Fired after the overlay closes. | |
9242 * @event iron-overlay-closed | |
9243 * @param {Event} event The `event.detail` is the `closingReason` property | |
9244 * (contains `canceled`, whether the overlay was canceled). | |
9245 */ | |
9246 | 6395 |
9247 })(); | 6396 })(); |
9248 /** | |
9249 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | |
9250 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | |
9251 * @polymerBehavior | |
9252 */ | |
9253 Polymer.NeonAnimatableBehavior = { | 6397 Polymer.NeonAnimatableBehavior = { |
9254 | 6398 |
9255 properties: { | 6399 properties: { |
9256 | 6400 |
9257 /** | |
9258 * Animation configuration. See README for more info. | |
9259 */ | |
9260 animationConfig: { | 6401 animationConfig: { |
9261 type: Object | 6402 type: Object |
9262 }, | 6403 }, |
9263 | 6404 |
9264 /** | |
9265 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | |
9266 * manually if using this. The animated node is set to `this` if using thi
s property. | |
9267 */ | |
9268 entryAnimation: { | 6405 entryAnimation: { |
9269 observer: '_entryAnimationChanged', | 6406 observer: '_entryAnimationChanged', |
9270 type: String | 6407 type: String |
9271 }, | 6408 }, |
9272 | 6409 |
9273 /** | |
9274 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | |
9275 * manually if using this. The animated node is set to `this` if using thi
s property. | |
9276 */ | |
9277 exitAnimation: { | 6410 exitAnimation: { |
9278 observer: '_exitAnimationChanged', | 6411 observer: '_exitAnimationChanged', |
9279 type: String | 6412 type: String |
9280 } | 6413 } |
9281 | 6414 |
9282 }, | 6415 }, |
9283 | 6416 |
9284 _entryAnimationChanged: function() { | 6417 _entryAnimationChanged: function() { |
9285 this.animationConfig = this.animationConfig || {}; | 6418 this.animationConfig = this.animationConfig || {}; |
9286 this.animationConfig['entry'] = [{ | 6419 this.animationConfig['entry'] = [{ |
9287 name: this.entryAnimation, | 6420 name: this.entryAnimation, |
9288 node: this | 6421 node: this |
9289 }]; | 6422 }]; |
9290 }, | 6423 }, |
9291 | 6424 |
9292 _exitAnimationChanged: function() { | 6425 _exitAnimationChanged: function() { |
9293 this.animationConfig = this.animationConfig || {}; | 6426 this.animationConfig = this.animationConfig || {}; |
9294 this.animationConfig['exit'] = [{ | 6427 this.animationConfig['exit'] = [{ |
9295 name: this.exitAnimation, | 6428 name: this.exitAnimation, |
9296 node: this | 6429 node: this |
9297 }]; | 6430 }]; |
9298 }, | 6431 }, |
9299 | 6432 |
9300 _copyProperties: function(config1, config2) { | 6433 _copyProperties: function(config1, config2) { |
9301 // shallowly copy properties from config2 to config1 | |
9302 for (var property in config2) { | 6434 for (var property in config2) { |
9303 config1[property] = config2[property]; | 6435 config1[property] = config2[property]; |
9304 } | 6436 } |
9305 }, | 6437 }, |
9306 | 6438 |
9307 _cloneConfig: function(config) { | 6439 _cloneConfig: function(config) { |
9308 var clone = { | 6440 var clone = { |
9309 isClone: true | 6441 isClone: true |
9310 }; | 6442 }; |
9311 this._copyProperties(clone, config); | 6443 this._copyProperties(clone, config); |
9312 return clone; | 6444 return clone; |
9313 }, | 6445 }, |
9314 | 6446 |
9315 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 6447 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
9316 if (!this.animationConfig) { | 6448 if (!this.animationConfig) { |
9317 return; | 6449 return; |
9318 } | 6450 } |
9319 | 6451 |
9320 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | 6452 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { |
9321 this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | 6453 this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); |
9322 return; | 6454 return; |
9323 } | 6455 } |
9324 | 6456 |
9325 // type is optional | |
9326 var thisConfig; | 6457 var thisConfig; |
9327 if (type) { | 6458 if (type) { |
9328 thisConfig = this.animationConfig[type]; | 6459 thisConfig = this.animationConfig[type]; |
9329 } else { | 6460 } else { |
9330 thisConfig = this.animationConfig; | 6461 thisConfig = this.animationConfig; |
9331 } | 6462 } |
9332 | 6463 |
9333 if (!Array.isArray(thisConfig)) { | 6464 if (!Array.isArray(thisConfig)) { |
9334 thisConfig = [thisConfig]; | 6465 thisConfig = [thisConfig]; |
9335 } | 6466 } |
9336 | 6467 |
9337 // iterate animations and recurse to process configurations from child nod
es | |
9338 if (thisConfig) { | 6468 if (thisConfig) { |
9339 for (var config, index = 0; config = thisConfig[index]; index++) { | 6469 for (var config, index = 0; config = thisConfig[index]; index++) { |
9340 if (config.animatable) { | 6470 if (config.animatable) { |
9341 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | 6471 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); |
9342 } else { | 6472 } else { |
9343 if (config.id) { | 6473 if (config.id) { |
9344 var cachedConfig = map[config.id]; | 6474 var cachedConfig = map[config.id]; |
9345 if (cachedConfig) { | 6475 if (cachedConfig) { |
9346 // merge configurations with the same id, making a clone lazily | |
9347 if (!cachedConfig.isClone) { | 6476 if (!cachedConfig.isClone) { |
9348 map[config.id] = this._cloneConfig(cachedConfig) | 6477 map[config.id] = this._cloneConfig(cachedConfig) |
9349 cachedConfig = map[config.id]; | 6478 cachedConfig = map[config.id]; |
9350 } | 6479 } |
9351 this._copyProperties(cachedConfig, config); | 6480 this._copyProperties(cachedConfig, config); |
9352 } else { | 6481 } else { |
9353 // put any configs with an id into a map | |
9354 map[config.id] = config; | 6482 map[config.id] = config; |
9355 } | 6483 } |
9356 } else { | 6484 } else { |
9357 allConfigs.push(config); | 6485 allConfigs.push(config); |
9358 } | 6486 } |
9359 } | 6487 } |
9360 } | 6488 } |
9361 } | 6489 } |
9362 }, | 6490 }, |
9363 | 6491 |
9364 /** | |
9365 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
9366 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
9367 * should define the property `animationConfig`, which is either a configura
tion object | |
9368 * or a map of animation type to array of configuration objects. | |
9369 */ | |
9370 getAnimationConfig: function(type) { | 6492 getAnimationConfig: function(type) { |
9371 var map = {}; | 6493 var map = {}; |
9372 var allConfigs = []; | 6494 var allConfigs = []; |
9373 this._getAnimationConfigRecursive(type, map, allConfigs); | 6495 this._getAnimationConfigRecursive(type, map, allConfigs); |
9374 // append the configurations saved in the map to the array | |
9375 for (var key in map) { | 6496 for (var key in map) { |
9376 allConfigs.push(map[key]); | 6497 allConfigs.push(map[key]); |
9377 } | 6498 } |
9378 return allConfigs; | 6499 return allConfigs; |
9379 } | 6500 } |
9380 | 6501 |
9381 }; | 6502 }; |
9382 /** | |
9383 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
9384 * | |
9385 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
9386 */ | |
9387 Polymer.NeonAnimationRunnerBehaviorImpl = { | 6503 Polymer.NeonAnimationRunnerBehaviorImpl = { |
9388 | 6504 |
9389 _configureAnimations: function(configs) { | 6505 _configureAnimations: function(configs) { |
9390 var results = []; | 6506 var results = []; |
9391 if (configs.length > 0) { | 6507 if (configs.length > 0) { |
9392 for (var config, index = 0; config = configs[index]; index++) { | 6508 for (var config, index = 0; config = configs[index]; index++) { |
9393 var neonAnimation = document.createElement(config.name); | 6509 var neonAnimation = document.createElement(config.name); |
9394 // is this element actually a neon animation? | |
9395 if (neonAnimation.isNeonAnimation) { | 6510 if (neonAnimation.isNeonAnimation) { |
9396 var result = null; | 6511 var result = null; |
9397 // configuration or play could fail if polyfills aren't loaded | |
9398 try { | 6512 try { |
9399 result = neonAnimation.configure(config); | 6513 result = neonAnimation.configure(config); |
9400 // Check if we have an Effect rather than an Animation | |
9401 if (typeof result.cancel != 'function') { | 6514 if (typeof result.cancel != 'function') { |
9402 result = document.timeline.play(result); | 6515 result = document.timeline.play(result); |
9403 } | 6516 } |
9404 } catch (e) { | 6517 } catch (e) { |
9405 result = null; | 6518 result = null; |
9406 console.warn('Couldnt play', '(', config.name, ').', e); | 6519 console.warn('Couldnt play', '(', config.name, ').', e); |
9407 } | 6520 } |
9408 if (result) { | 6521 if (result) { |
9409 results.push({ | 6522 results.push({ |
9410 neonAnimation: neonAnimation, | 6523 neonAnimation: neonAnimation, |
(...skipping 22 matching lines...) Expand all Loading... |
9433 | 6546 |
9434 _complete: function(activeEntries) { | 6547 _complete: function(activeEntries) { |
9435 for (var i = 0; i < activeEntries.length; i++) { | 6548 for (var i = 0; i < activeEntries.length; i++) { |
9436 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | 6549 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
9437 } | 6550 } |
9438 for (var i = 0; i < activeEntries.length; i++) { | 6551 for (var i = 0; i < activeEntries.length; i++) { |
9439 activeEntries[i].animation.cancel(); | 6552 activeEntries[i].animation.cancel(); |
9440 } | 6553 } |
9441 }, | 6554 }, |
9442 | 6555 |
9443 /** | |
9444 * Plays an animation with an optional `type`. | |
9445 * @param {string=} type | |
9446 * @param {!Object=} cookie | |
9447 */ | |
9448 playAnimation: function(type, cookie) { | 6556 playAnimation: function(type, cookie) { |
9449 var configs = this.getAnimationConfig(type); | 6557 var configs = this.getAnimationConfig(type); |
9450 if (!configs) { | 6558 if (!configs) { |
9451 return; | 6559 return; |
9452 } | 6560 } |
9453 this._active = this._active || {}; | 6561 this._active = this._active || {}; |
9454 if (this._active[type]) { | 6562 if (this._active[type]) { |
9455 this._complete(this._active[type]); | 6563 this._complete(this._active[type]); |
9456 delete this._active[type]; | 6564 delete this._active[type]; |
9457 } | 6565 } |
(...skipping 11 matching lines...) Expand all Loading... |
9469 activeEntries[i].animation.onfinish = function() { | 6577 activeEntries[i].animation.onfinish = function() { |
9470 if (this._shouldComplete(activeEntries)) { | 6578 if (this._shouldComplete(activeEntries)) { |
9471 this._complete(activeEntries); | 6579 this._complete(activeEntries); |
9472 delete this._active[type]; | 6580 delete this._active[type]; |
9473 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 6581 this.fire('neon-animation-finish', cookie, {bubbles: false}); |
9474 } | 6582 } |
9475 }.bind(this); | 6583 }.bind(this); |
9476 } | 6584 } |
9477 }, | 6585 }, |
9478 | 6586 |
9479 /** | |
9480 * Cancels the currently running animations. | |
9481 */ | |
9482 cancelAnimation: function() { | 6587 cancelAnimation: function() { |
9483 for (var k in this._animations) { | 6588 for (var k in this._animations) { |
9484 this._animations[k].cancel(); | 6589 this._animations[k].cancel(); |
9485 } | 6590 } |
9486 this._animations = {}; | 6591 this._animations = {}; |
9487 } | 6592 } |
9488 }; | 6593 }; |
9489 | 6594 |
9490 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | 6595 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ |
9491 Polymer.NeonAnimationRunnerBehavior = [ | 6596 Polymer.NeonAnimationRunnerBehavior = [ |
9492 Polymer.NeonAnimatableBehavior, | 6597 Polymer.NeonAnimatableBehavior, |
9493 Polymer.NeonAnimationRunnerBehaviorImpl | 6598 Polymer.NeonAnimationRunnerBehaviorImpl |
9494 ]; | 6599 ]; |
9495 /** | |
9496 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
9497 * @polymerBehavior | |
9498 */ | |
9499 Polymer.NeonAnimationBehavior = { | 6600 Polymer.NeonAnimationBehavior = { |
9500 | 6601 |
9501 properties: { | 6602 properties: { |
9502 | 6603 |
9503 /** | |
9504 * Defines the animation timing. | |
9505 */ | |
9506 animationTiming: { | 6604 animationTiming: { |
9507 type: Object, | 6605 type: Object, |
9508 value: function() { | 6606 value: function() { |
9509 return { | 6607 return { |
9510 duration: 500, | 6608 duration: 500, |
9511 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | 6609 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
9512 fill: 'both' | 6610 fill: 'both' |
9513 } | 6611 } |
9514 } | 6612 } |
9515 } | 6613 } |
9516 | 6614 |
9517 }, | 6615 }, |
9518 | 6616 |
9519 /** | |
9520 * Can be used to determine that elements implement this behavior. | |
9521 */ | |
9522 isNeonAnimation: true, | 6617 isNeonAnimation: true, |
9523 | 6618 |
9524 /** | |
9525 * Do any animation configuration here. | |
9526 */ | |
9527 // configure: function(config) { | |
9528 // }, | |
9529 | 6619 |
9530 /** | |
9531 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | |
9532 * by the animation. | |
9533 */ | |
9534 timingFromConfig: function(config) { | 6620 timingFromConfig: function(config) { |
9535 if (config.timing) { | 6621 if (config.timing) { |
9536 for (var property in config.timing) { | 6622 for (var property in config.timing) { |
9537 this.animationTiming[property] = config.timing[property]; | 6623 this.animationTiming[property] = config.timing[property]; |
9538 } | 6624 } |
9539 } | 6625 } |
9540 return this.animationTiming; | 6626 return this.animationTiming; |
9541 }, | 6627 }, |
9542 | 6628 |
9543 /** | |
9544 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | |
9545 */ | |
9546 setPrefixedProperty: function(node, property, value) { | 6629 setPrefixedProperty: function(node, property, value) { |
9547 var map = { | 6630 var map = { |
9548 'transform': ['webkitTransform'], | 6631 'transform': ['webkitTransform'], |
9549 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | 6632 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] |
9550 }; | 6633 }; |
9551 var prefixes = map[property]; | 6634 var prefixes = map[property]; |
9552 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | 6635 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
9553 node.style[prefix] = value; | 6636 node.style[prefix] = value; |
9554 } | 6637 } |
9555 node.style[property] = value; | 6638 node.style[property] = value; |
9556 }, | 6639 }, |
9557 | 6640 |
9558 /** | |
9559 * Called when the animation finishes. | |
9560 */ | |
9561 complete: function() {} | 6641 complete: function() {} |
9562 | 6642 |
9563 }; | 6643 }; |
9564 Polymer({ | 6644 Polymer({ |
9565 | 6645 |
9566 is: 'opaque-animation', | 6646 is: 'opaque-animation', |
9567 | 6647 |
9568 behaviors: [ | 6648 behaviors: [ |
9569 Polymer.NeonAnimationBehavior | 6649 Polymer.NeonAnimationBehavior |
9570 ], | 6650 ], |
9571 | 6651 |
9572 configure: function(config) { | 6652 configure: function(config) { |
9573 var node = config.node; | 6653 var node = config.node; |
9574 this._effect = new KeyframeEffect(node, [ | 6654 this._effect = new KeyframeEffect(node, [ |
9575 {'opacity': '1'}, | 6655 {'opacity': '1'}, |
9576 {'opacity': '1'} | 6656 {'opacity': '1'} |
9577 ], this.timingFromConfig(config)); | 6657 ], this.timingFromConfig(config)); |
9578 node.style.opacity = '0'; | 6658 node.style.opacity = '0'; |
9579 return this._effect; | 6659 return this._effect; |
9580 }, | 6660 }, |
9581 | 6661 |
9582 complete: function(config) { | 6662 complete: function(config) { |
9583 config.node.style.opacity = ''; | 6663 config.node.style.opacity = ''; |
9584 } | 6664 } |
9585 | 6665 |
9586 }); | 6666 }); |
9587 (function() { | 6667 (function() { |
9588 'use strict'; | 6668 'use strict'; |
9589 // Used to calculate the scroll direction during touch events. | |
9590 var LAST_TOUCH_POSITION = { | 6669 var LAST_TOUCH_POSITION = { |
9591 pageX: 0, | 6670 pageX: 0, |
9592 pageY: 0 | 6671 pageY: 0 |
9593 }; | 6672 }; |
9594 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
9595 var ROOT_TARGET = null; | 6673 var ROOT_TARGET = null; |
9596 var SCROLLABLE_NODES = []; | 6674 var SCROLLABLE_NODES = []; |
9597 | 6675 |
9598 /** | |
9599 * The IronDropdownScrollManager is intended to provide a central source | |
9600 * of authority and control over which elements in a document are currently | |
9601 * allowed to scroll. | |
9602 */ | |
9603 | 6676 |
9604 Polymer.IronDropdownScrollManager = { | 6677 Polymer.IronDropdownScrollManager = { |
9605 | 6678 |
9606 /** | |
9607 * The current element that defines the DOM boundaries of the | |
9608 * scroll lock. This is always the most recently locking element. | |
9609 */ | |
9610 get currentLockingElement() { | 6679 get currentLockingElement() { |
9611 return this._lockingElements[this._lockingElements.length - 1]; | 6680 return this._lockingElements[this._lockingElements.length - 1]; |
9612 }, | 6681 }, |
9613 | 6682 |
9614 /** | |
9615 * Returns true if the provided element is "scroll locked", which is to | |
9616 * say that it cannot be scrolled via pointer or keyboard interactions. | |
9617 * | |
9618 * @param {HTMLElement} element An HTML element instance which may or may | |
9619 * not be scroll locked. | |
9620 */ | |
9621 elementIsScrollLocked: function(element) { | 6683 elementIsScrollLocked: function(element) { |
9622 var currentLockingElement = this.currentLockingElement; | 6684 var currentLockingElement = this.currentLockingElement; |
9623 | 6685 |
9624 if (currentLockingElement === undefined) | 6686 if (currentLockingElement === undefined) |
9625 return false; | 6687 return false; |
9626 | 6688 |
9627 var scrollLocked; | 6689 var scrollLocked; |
9628 | 6690 |
9629 if (this._hasCachedLockedElement(element)) { | 6691 if (this._hasCachedLockedElement(element)) { |
9630 return true; | 6692 return true; |
9631 } | 6693 } |
9632 | 6694 |
9633 if (this._hasCachedUnlockedElement(element)) { | 6695 if (this._hasCachedUnlockedElement(element)) { |
9634 return false; | 6696 return false; |
9635 } | 6697 } |
9636 | 6698 |
9637 scrollLocked = !!currentLockingElement && | 6699 scrollLocked = !!currentLockingElement && |
9638 currentLockingElement !== element && | 6700 currentLockingElement !== element && |
9639 !this._composedTreeContains(currentLockingElement, element); | 6701 !this._composedTreeContains(currentLockingElement, element); |
9640 | 6702 |
9641 if (scrollLocked) { | 6703 if (scrollLocked) { |
9642 this._lockedElementCache.push(element); | 6704 this._lockedElementCache.push(element); |
9643 } else { | 6705 } else { |
9644 this._unlockedElementCache.push(element); | 6706 this._unlockedElementCache.push(element); |
9645 } | 6707 } |
9646 | 6708 |
9647 return scrollLocked; | 6709 return scrollLocked; |
9648 }, | 6710 }, |
9649 | 6711 |
9650 /** | |
9651 * Push an element onto the current scroll lock stack. The most recently | |
9652 * pushed element and its children will be considered scrollable. All | |
9653 * other elements will not be scrollable. | |
9654 * | |
9655 * Scroll locking is implemented as a stack so that cases such as | |
9656 * dropdowns within dropdowns are handled well. | |
9657 * | |
9658 * @param {HTMLElement} element The element that should lock scroll. | |
9659 */ | |
9660 pushScrollLock: function(element) { | 6712 pushScrollLock: function(element) { |
9661 // Prevent pushing the same element twice | |
9662 if (this._lockingElements.indexOf(element) >= 0) { | 6713 if (this._lockingElements.indexOf(element) >= 0) { |
9663 return; | 6714 return; |
9664 } | 6715 } |
9665 | 6716 |
9666 if (this._lockingElements.length === 0) { | 6717 if (this._lockingElements.length === 0) { |
9667 this._lockScrollInteractions(); | 6718 this._lockScrollInteractions(); |
9668 } | 6719 } |
9669 | 6720 |
9670 this._lockingElements.push(element); | 6721 this._lockingElements.push(element); |
9671 | 6722 |
9672 this._lockedElementCache = []; | 6723 this._lockedElementCache = []; |
9673 this._unlockedElementCache = []; | 6724 this._unlockedElementCache = []; |
9674 }, | 6725 }, |
9675 | 6726 |
9676 /** | |
9677 * Remove an element from the scroll lock stack. The element being | |
9678 * removed does not need to be the most recently pushed element. However, | |
9679 * the scroll lock constraints only change when the most recently pushed | |
9680 * element is removed. | |
9681 * | |
9682 * @param {HTMLElement} element The element to remove from the scroll | |
9683 * lock stack. | |
9684 */ | |
9685 removeScrollLock: function(element) { | 6727 removeScrollLock: function(element) { |
9686 var index = this._lockingElements.indexOf(element); | 6728 var index = this._lockingElements.indexOf(element); |
9687 | 6729 |
9688 if (index === -1) { | 6730 if (index === -1) { |
9689 return; | 6731 return; |
9690 } | 6732 } |
9691 | 6733 |
9692 this._lockingElements.splice(index, 1); | 6734 this._lockingElements.splice(index, 1); |
9693 | 6735 |
9694 this._lockedElementCache = []; | 6736 this._lockedElementCache = []; |
(...skipping 12 matching lines...) Expand all Loading... |
9707 | 6749 |
9708 _hasCachedLockedElement: function(element) { | 6750 _hasCachedLockedElement: function(element) { |
9709 return this._lockedElementCache.indexOf(element) > -1; | 6751 return this._lockedElementCache.indexOf(element) > -1; |
9710 }, | 6752 }, |
9711 | 6753 |
9712 _hasCachedUnlockedElement: function(element) { | 6754 _hasCachedUnlockedElement: function(element) { |
9713 return this._unlockedElementCache.indexOf(element) > -1; | 6755 return this._unlockedElementCache.indexOf(element) > -1; |
9714 }, | 6756 }, |
9715 | 6757 |
9716 _composedTreeContains: function(element, child) { | 6758 _composedTreeContains: function(element, child) { |
9717 // NOTE(cdata): This method iterates over content elements and their | |
9718 // corresponding distributed nodes to implement a contains-like method | |
9719 // that pierces through the composed tree of the ShadowDOM. Results of | |
9720 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
9721 // guard against potentially expensive lookups happening repeatedly as | |
9722 // a user scrolls / touchmoves. | |
9723 var contentElements; | 6759 var contentElements; |
9724 var distributedNodes; | 6760 var distributedNodes; |
9725 var contentIndex; | 6761 var contentIndex; |
9726 var nodeIndex; | 6762 var nodeIndex; |
9727 | 6763 |
9728 if (element.contains(child)) { | 6764 if (element.contains(child)) { |
9729 return true; | 6765 return true; |
9730 } | 6766 } |
9731 | 6767 |
9732 contentElements = Polymer.dom(element).querySelectorAll('content'); | 6768 contentElements = Polymer.dom(element).querySelectorAll('content'); |
9733 | 6769 |
9734 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | 6770 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { |
9735 | 6771 |
9736 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | 6772 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); |
9737 | 6773 |
9738 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | 6774 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ |
9739 | 6775 |
9740 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | 6776 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ |
9741 return true; | 6777 return true; |
9742 } | 6778 } |
9743 } | 6779 } |
9744 } | 6780 } |
9745 | 6781 |
9746 return false; | 6782 return false; |
9747 }, | 6783 }, |
9748 | 6784 |
9749 _scrollInteractionHandler: function(event) { | 6785 _scrollInteractionHandler: function(event) { |
9750 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
9751 // progress and cannot be interrupted. | |
9752 if (event.cancelable && this._shouldPreventScrolling(event)) { | 6786 if (event.cancelable && this._shouldPreventScrolling(event)) { |
9753 event.preventDefault(); | 6787 event.preventDefault(); |
9754 } | 6788 } |
9755 // If event has targetTouches (touch event), update last touch position. | |
9756 if (event.targetTouches) { | 6789 if (event.targetTouches) { |
9757 var touch = event.targetTouches[0]; | 6790 var touch = event.targetTouches[0]; |
9758 LAST_TOUCH_POSITION.pageX = touch.pageX; | 6791 LAST_TOUCH_POSITION.pageX = touch.pageX; |
9759 LAST_TOUCH_POSITION.pageY = touch.pageY; | 6792 LAST_TOUCH_POSITION.pageY = touch.pageY; |
9760 } | 6793 } |
9761 }, | 6794 }, |
9762 | 6795 |
9763 _lockScrollInteractions: function() { | 6796 _lockScrollInteractions: function() { |
9764 this._boundScrollHandler = this._boundScrollHandler || | 6797 this._boundScrollHandler = this._boundScrollHandler || |
9765 this._scrollInteractionHandler.bind(this); | 6798 this._scrollInteractionHandler.bind(this); |
9766 // Modern `wheel` event for mouse wheel scrolling: | |
9767 document.addEventListener('wheel', this._boundScrollHandler, true); | 6799 document.addEventListener('wheel', this._boundScrollHandler, true); |
9768 // Older, non-standard `mousewheel` event for some FF: | |
9769 document.addEventListener('mousewheel', this._boundScrollHandler, true); | 6800 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
9770 // IE: | |
9771 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | 6801 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); |
9772 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
9773 document.addEventListener('touchstart', this._boundScrollHandler, true); | 6802 document.addEventListener('touchstart', this._boundScrollHandler, true); |
9774 // Mobile devices can scroll on touch move: | |
9775 document.addEventListener('touchmove', this._boundScrollHandler, true); | 6803 document.addEventListener('touchmove', this._boundScrollHandler, true); |
9776 }, | 6804 }, |
9777 | 6805 |
9778 _unlockScrollInteractions: function() { | 6806 _unlockScrollInteractions: function() { |
9779 document.removeEventListener('wheel', this._boundScrollHandler, true); | 6807 document.removeEventListener('wheel', this._boundScrollHandler, true); |
9780 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | 6808 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); |
9781 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | 6809 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); |
9782 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | 6810 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); |
9783 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | 6811 document.removeEventListener('touchmove', this._boundScrollHandler, true
); |
9784 }, | 6812 }, |
9785 | 6813 |
9786 /** | |
9787 * Returns true if the event causes scroll outside the current locking | |
9788 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
9789 * outside the locking element when it is already at its scroll boundaries
. | |
9790 * @param {!Event} event | |
9791 * @return {boolean} | |
9792 * @private | |
9793 */ | |
9794 _shouldPreventScrolling: function(event) { | 6814 _shouldPreventScrolling: function(event) { |
9795 | 6815 |
9796 // Update if root target changed. For touch events, ensure we don't | |
9797 // update during touchmove. | |
9798 var target = Polymer.dom(event).rootTarget; | 6816 var target = Polymer.dom(event).rootTarget; |
9799 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | 6817 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
9800 ROOT_TARGET = target; | 6818 ROOT_TARGET = target; |
9801 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | 6819 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
9802 } | 6820 } |
9803 | 6821 |
9804 // Prevent event if no scrollable nodes. | |
9805 if (!SCROLLABLE_NODES.length) { | 6822 if (!SCROLLABLE_NODES.length) { |
9806 return true; | 6823 return true; |
9807 } | 6824 } |
9808 // Don't prevent touchstart event inside the locking element when it has | |
9809 // scrollable nodes. | |
9810 if (event.type === 'touchstart') { | 6825 if (event.type === 'touchstart') { |
9811 return false; | 6826 return false; |
9812 } | 6827 } |
9813 // Get deltaX/Y. | |
9814 var info = this._getScrollInfo(event); | 6828 var info = this._getScrollInfo(event); |
9815 // Prevent if there is no child that can scroll. | |
9816 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | 6829 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); |
9817 }, | 6830 }, |
9818 | 6831 |
9819 /** | |
9820 * Returns an array of scrollable nodes up to the current locking element, | |
9821 * which is included too if scrollable. | |
9822 * @param {!Array<Node>} nodes | |
9823 * @return {Array<Node>} scrollables | |
9824 * @private | |
9825 */ | |
9826 _getScrollableNodes: function(nodes) { | 6832 _getScrollableNodes: function(nodes) { |
9827 var scrollables = []; | 6833 var scrollables = []; |
9828 var lockingIndex = nodes.indexOf(this.currentLockingElement); | 6834 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
9829 // Loop from root target to locking element (included). | |
9830 for (var i = 0; i <= lockingIndex; i++) { | 6835 for (var i = 0; i <= lockingIndex; i++) { |
9831 var node = nodes[i]; | 6836 var node = nodes[i]; |
9832 // Skip document fragments. | |
9833 if (node.nodeType === 11) { | 6837 if (node.nodeType === 11) { |
9834 continue; | 6838 continue; |
9835 } | 6839 } |
9836 // Check inline style before checking computed style. | |
9837 var style = node.style; | 6840 var style = node.style; |
9838 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | 6841 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
9839 style = window.getComputedStyle(node); | 6842 style = window.getComputedStyle(node); |
9840 } | 6843 } |
9841 if (style.overflow === 'scroll' || style.overflow === 'auto') { | 6844 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
9842 scrollables.push(node); | 6845 scrollables.push(node); |
9843 } | 6846 } |
9844 } | 6847 } |
9845 return scrollables; | 6848 return scrollables; |
9846 }, | 6849 }, |
9847 | 6850 |
9848 /** | |
9849 * Returns the node that is scrolling. If there is no scrolling, | |
9850 * returns undefined. | |
9851 * @param {!Array<Node>} nodes | |
9852 * @param {number} deltaX Scroll delta on the x-axis | |
9853 * @param {number} deltaY Scroll delta on the y-axis | |
9854 * @return {Node|undefined} | |
9855 * @private | |
9856 */ | |
9857 _getScrollingNode: function(nodes, deltaX, deltaY) { | 6851 _getScrollingNode: function(nodes, deltaX, deltaY) { |
9858 // No scroll. | |
9859 if (!deltaX && !deltaY) { | 6852 if (!deltaX && !deltaY) { |
9860 return; | 6853 return; |
9861 } | 6854 } |
9862 // Check only one axis according to where there is more scroll. | |
9863 // Prefer vertical to horizontal. | |
9864 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | 6855 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
9865 for (var i = 0; i < nodes.length; i++) { | 6856 for (var i = 0; i < nodes.length; i++) { |
9866 var node = nodes[i]; | 6857 var node = nodes[i]; |
9867 var canScroll = false; | 6858 var canScroll = false; |
9868 if (verticalScroll) { | 6859 if (verticalScroll) { |
9869 // delta < 0 is scroll up, delta > 0 is scroll down. | |
9870 canScroll = deltaY < 0 ? node.scrollTop > 0 : | 6860 canScroll = deltaY < 0 ? node.scrollTop > 0 : |
9871 node.scrollTop < node.scrollHeight - node.clientHeight; | 6861 node.scrollTop < node.scrollHeight - node.clientHeight; |
9872 } else { | 6862 } else { |
9873 // delta < 0 is scroll left, delta > 0 is scroll right. | |
9874 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | 6863 canScroll = deltaX < 0 ? node.scrollLeft > 0 : |
9875 node.scrollLeft < node.scrollWidth - node.clientWidth; | 6864 node.scrollLeft < node.scrollWidth - node.clientWidth; |
9876 } | 6865 } |
9877 if (canScroll) { | 6866 if (canScroll) { |
9878 return node; | 6867 return node; |
9879 } | 6868 } |
9880 } | 6869 } |
9881 }, | 6870 }, |
9882 | 6871 |
9883 /** | |
9884 * Returns scroll `deltaX` and `deltaY`. | |
9885 * @param {!Event} event The scroll event | |
9886 * @return {{ | |
9887 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
9888 * negative: scroll left, 0: no scroll), | |
9889 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
9890 * negative: scroll up, 0: no scroll) | |
9891 * }} info | |
9892 * @private | |
9893 */ | |
9894 _getScrollInfo: function(event) { | 6872 _getScrollInfo: function(event) { |
9895 var info = { | 6873 var info = { |
9896 deltaX: event.deltaX, | 6874 deltaX: event.deltaX, |
9897 deltaY: event.deltaY | 6875 deltaY: event.deltaY |
9898 }; | 6876 }; |
9899 // Already available. | |
9900 if ('deltaX' in event) { | 6877 if ('deltaX' in event) { |
9901 // do nothing, values are already good. | |
9902 } | 6878 } |
9903 // Safari has scroll info in `wheelDeltaX/Y`. | |
9904 else if ('wheelDeltaX' in event) { | 6879 else if ('wheelDeltaX' in event) { |
9905 info.deltaX = -event.wheelDeltaX; | 6880 info.deltaX = -event.wheelDeltaX; |
9906 info.deltaY = -event.wheelDeltaY; | 6881 info.deltaY = -event.wheelDeltaY; |
9907 } | 6882 } |
9908 // Firefox has scroll info in `detail` and `axis`. | |
9909 else if ('axis' in event) { | 6883 else if ('axis' in event) { |
9910 info.deltaX = event.axis === 1 ? event.detail : 0; | 6884 info.deltaX = event.axis === 1 ? event.detail : 0; |
9911 info.deltaY = event.axis === 2 ? event.detail : 0; | 6885 info.deltaY = event.axis === 2 ? event.detail : 0; |
9912 } | 6886 } |
9913 // On mobile devices, calculate scroll direction. | |
9914 else if (event.targetTouches) { | 6887 else if (event.targetTouches) { |
9915 var touch = event.targetTouches[0]; | 6888 var touch = event.targetTouches[0]; |
9916 // Touch moves from right to left => scrolling goes right. | |
9917 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | 6889 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
9918 // Touch moves from down to up => scrolling goes down. | |
9919 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | 6890 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
9920 } | 6891 } |
9921 return info; | 6892 return info; |
9922 } | 6893 } |
9923 }; | 6894 }; |
9924 })(); | 6895 })(); |
9925 (function() { | 6896 (function() { |
9926 'use strict'; | 6897 'use strict'; |
9927 | 6898 |
9928 Polymer({ | 6899 Polymer({ |
9929 is: 'iron-dropdown', | 6900 is: 'iron-dropdown', |
9930 | 6901 |
9931 behaviors: [ | 6902 behaviors: [ |
9932 Polymer.IronControlState, | 6903 Polymer.IronControlState, |
9933 Polymer.IronA11yKeysBehavior, | 6904 Polymer.IronA11yKeysBehavior, |
9934 Polymer.IronOverlayBehavior, | 6905 Polymer.IronOverlayBehavior, |
9935 Polymer.NeonAnimationRunnerBehavior | 6906 Polymer.NeonAnimationRunnerBehavior |
9936 ], | 6907 ], |
9937 | 6908 |
9938 properties: { | 6909 properties: { |
9939 /** | |
9940 * The orientation against which to align the dropdown content | |
9941 * horizontally relative to the dropdown trigger. | |
9942 * Overridden from `Polymer.IronFitBehavior`. | |
9943 */ | |
9944 horizontalAlign: { | 6910 horizontalAlign: { |
9945 type: String, | 6911 type: String, |
9946 value: 'left', | 6912 value: 'left', |
9947 reflectToAttribute: true | 6913 reflectToAttribute: true |
9948 }, | 6914 }, |
9949 | 6915 |
9950 /** | |
9951 * The orientation against which to align the dropdown content | |
9952 * vertically relative to the dropdown trigger. | |
9953 * Overridden from `Polymer.IronFitBehavior`. | |
9954 */ | |
9955 verticalAlign: { | 6916 verticalAlign: { |
9956 type: String, | 6917 type: String, |
9957 value: 'top', | 6918 value: 'top', |
9958 reflectToAttribute: true | 6919 reflectToAttribute: true |
9959 }, | 6920 }, |
9960 | 6921 |
9961 /** | |
9962 * An animation config. If provided, this will be used to animate the | |
9963 * opening of the dropdown. | |
9964 */ | |
9965 openAnimationConfig: { | 6922 openAnimationConfig: { |
9966 type: Object | 6923 type: Object |
9967 }, | 6924 }, |
9968 | 6925 |
9969 /** | |
9970 * An animation config. If provided, this will be used to animate the | |
9971 * closing of the dropdown. | |
9972 */ | |
9973 closeAnimationConfig: { | 6926 closeAnimationConfig: { |
9974 type: Object | 6927 type: Object |
9975 }, | 6928 }, |
9976 | 6929 |
9977 /** | |
9978 * If provided, this will be the element that will be focused when | |
9979 * the dropdown opens. | |
9980 */ | |
9981 focusTarget: { | 6930 focusTarget: { |
9982 type: Object | 6931 type: Object |
9983 }, | 6932 }, |
9984 | 6933 |
9985 /** | |
9986 * Set to true to disable animations when opening and closing the | |
9987 * dropdown. | |
9988 */ | |
9989 noAnimations: { | 6934 noAnimations: { |
9990 type: Boolean, | 6935 type: Boolean, |
9991 value: false | 6936 value: false |
9992 }, | 6937 }, |
9993 | 6938 |
9994 /** | |
9995 * By default, the dropdown will constrain scrolling on the page | |
9996 * to itself when opened. | |
9997 * Set to true in order to prevent scroll from being constrained | |
9998 * to the dropdown when it opens. | |
9999 */ | |
10000 allowOutsideScroll: { | 6939 allowOutsideScroll: { |
10001 type: Boolean, | 6940 type: Boolean, |
10002 value: false | 6941 value: false |
10003 }, | 6942 }, |
10004 | 6943 |
10005 /** | |
10006 * Callback for scroll events. | |
10007 * @type {Function} | |
10008 * @private | |
10009 */ | |
10010 _boundOnCaptureScroll: { | 6944 _boundOnCaptureScroll: { |
10011 type: Function, | 6945 type: Function, |
10012 value: function() { | 6946 value: function() { |
10013 return this._onCaptureScroll.bind(this); | 6947 return this._onCaptureScroll.bind(this); |
10014 } | 6948 } |
10015 } | 6949 } |
10016 }, | 6950 }, |
10017 | 6951 |
10018 listeners: { | 6952 listeners: { |
10019 'neon-animation-finish': '_onNeonAnimationFinish' | 6953 'neon-animation-finish': '_onNeonAnimationFinish' |
10020 }, | 6954 }, |
10021 | 6955 |
10022 observers: [ | 6956 observers: [ |
10023 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | 6957 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' |
10024 ], | 6958 ], |
10025 | 6959 |
10026 /** | |
10027 * The element that is contained by the dropdown, if any. | |
10028 */ | |
10029 get containedElement() { | 6960 get containedElement() { |
10030 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | 6961 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
10031 }, | 6962 }, |
10032 | 6963 |
10033 /** | |
10034 * The element that should be focused when the dropdown opens. | |
10035 * @deprecated | |
10036 */ | |
10037 get _focusTarget() { | 6964 get _focusTarget() { |
10038 return this.focusTarget || this.containedElement; | 6965 return this.focusTarget || this.containedElement; |
10039 }, | 6966 }, |
10040 | 6967 |
10041 ready: function() { | 6968 ready: function() { |
10042 // Memoized scrolling position, used to block scrolling outside. | |
10043 this._scrollTop = 0; | 6969 this._scrollTop = 0; |
10044 this._scrollLeft = 0; | 6970 this._scrollLeft = 0; |
10045 // Used to perform a non-blocking refit on scroll. | |
10046 this._refitOnScrollRAF = null; | 6971 this._refitOnScrollRAF = null; |
10047 }, | 6972 }, |
10048 | 6973 |
10049 detached: function() { | 6974 detached: function() { |
10050 this.cancelAnimation(); | 6975 this.cancelAnimation(); |
10051 Polymer.IronDropdownScrollManager.removeScrollLock(this); | 6976 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
10052 }, | 6977 }, |
10053 | 6978 |
10054 /** | |
10055 * Called when the value of `opened` changes. | |
10056 * Overridden from `IronOverlayBehavior` | |
10057 */ | |
10058 _openedChanged: function() { | 6979 _openedChanged: function() { |
10059 if (this.opened && this.disabled) { | 6980 if (this.opened && this.disabled) { |
10060 this.cancel(); | 6981 this.cancel(); |
10061 } else { | 6982 } else { |
10062 this.cancelAnimation(); | 6983 this.cancelAnimation(); |
10063 this.sizingTarget = this.containedElement || this.sizingTarget; | 6984 this.sizingTarget = this.containedElement || this.sizingTarget; |
10064 this._updateAnimationConfig(); | 6985 this._updateAnimationConfig(); |
10065 this._saveScrollPosition(); | 6986 this._saveScrollPosition(); |
10066 if (this.opened) { | 6987 if (this.opened) { |
10067 document.addEventListener('scroll', this._boundOnCaptureScroll); | 6988 document.addEventListener('scroll', this._boundOnCaptureScroll); |
10068 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | 6989 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); |
10069 } else { | 6990 } else { |
10070 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | 6991 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; |
10071 Polymer.IronDropdownScrollManager.removeScrollLock(this); | 6992 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
10072 } | 6993 } |
10073 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | 6994 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); |
10074 } | 6995 } |
10075 }, | 6996 }, |
10076 | 6997 |
10077 /** | |
10078 * Overridden from `IronOverlayBehavior`. | |
10079 */ | |
10080 _renderOpened: function() { | 6998 _renderOpened: function() { |
10081 if (!this.noAnimations && this.animationConfig.open) { | 6999 if (!this.noAnimations && this.animationConfig.open) { |
10082 this.$.contentWrapper.classList.add('animating'); | 7000 this.$.contentWrapper.classList.add('animating'); |
10083 this.playAnimation('open'); | 7001 this.playAnimation('open'); |
10084 } else { | 7002 } else { |
10085 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | 7003 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; |
10086 } | 7004 } |
10087 }, | 7005 }, |
10088 | 7006 |
10089 /** | |
10090 * Overridden from `IronOverlayBehavior`. | |
10091 */ | |
10092 _renderClosed: function() { | 7007 _renderClosed: function() { |
10093 | 7008 |
10094 if (!this.noAnimations && this.animationConfig.close) { | 7009 if (!this.noAnimations && this.animationConfig.close) { |
10095 this.$.contentWrapper.classList.add('animating'); | 7010 this.$.contentWrapper.classList.add('animating'); |
10096 this.playAnimation('close'); | 7011 this.playAnimation('close'); |
10097 } else { | 7012 } else { |
10098 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | 7013 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; |
10099 } | 7014 } |
10100 }, | 7015 }, |
10101 | 7016 |
10102 /** | |
10103 * Called when animation finishes on the dropdown (when opening or | |
10104 * closing). Responsible for "completing" the process of opening or | |
10105 * closing the dropdown by positioning it or setting its display to | |
10106 * none. | |
10107 */ | |
10108 _onNeonAnimationFinish: function() { | 7017 _onNeonAnimationFinish: function() { |
10109 this.$.contentWrapper.classList.remove('animating'); | 7018 this.$.contentWrapper.classList.remove('animating'); |
10110 if (this.opened) { | 7019 if (this.opened) { |
10111 this._finishRenderOpened(); | 7020 this._finishRenderOpened(); |
10112 } else { | 7021 } else { |
10113 this._finishRenderClosed(); | 7022 this._finishRenderClosed(); |
10114 } | 7023 } |
10115 }, | 7024 }, |
10116 | 7025 |
10117 _onCaptureScroll: function() { | 7026 _onCaptureScroll: function() { |
10118 if (!this.allowOutsideScroll) { | 7027 if (!this.allowOutsideScroll) { |
10119 this._restoreScrollPosition(); | 7028 this._restoreScrollPosition(); |
10120 } else { | 7029 } else { |
10121 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | 7030 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); |
10122 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | 7031 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); |
10123 } | 7032 } |
10124 }, | 7033 }, |
10125 | 7034 |
10126 /** | |
10127 * Memoizes the scroll position of the outside scrolling element. | |
10128 * @private | |
10129 */ | |
10130 _saveScrollPosition: function() { | 7035 _saveScrollPosition: function() { |
10131 if (document.scrollingElement) { | 7036 if (document.scrollingElement) { |
10132 this._scrollTop = document.scrollingElement.scrollTop; | 7037 this._scrollTop = document.scrollingElement.scrollTop; |
10133 this._scrollLeft = document.scrollingElement.scrollLeft; | 7038 this._scrollLeft = document.scrollingElement.scrollLeft; |
10134 } else { | 7039 } else { |
10135 // Since we don't know if is the body or html, get max. | |
10136 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | 7040 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); |
10137 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | 7041 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); |
10138 } | 7042 } |
10139 }, | 7043 }, |
10140 | 7044 |
10141 /** | |
10142 * Resets the scroll position of the outside scrolling element. | |
10143 * @private | |
10144 */ | |
10145 _restoreScrollPosition: function() { | 7045 _restoreScrollPosition: function() { |
10146 if (document.scrollingElement) { | 7046 if (document.scrollingElement) { |
10147 document.scrollingElement.scrollTop = this._scrollTop; | 7047 document.scrollingElement.scrollTop = this._scrollTop; |
10148 document.scrollingElement.scrollLeft = this._scrollLeft; | 7048 document.scrollingElement.scrollLeft = this._scrollLeft; |
10149 } else { | 7049 } else { |
10150 // Since we don't know if is the body or html, set both. | |
10151 document.documentElement.scrollTop = this._scrollTop; | 7050 document.documentElement.scrollTop = this._scrollTop; |
10152 document.documentElement.scrollLeft = this._scrollLeft; | 7051 document.documentElement.scrollLeft = this._scrollLeft; |
10153 document.body.scrollTop = this._scrollTop; | 7052 document.body.scrollTop = this._scrollTop; |
10154 document.body.scrollLeft = this._scrollLeft; | 7053 document.body.scrollLeft = this._scrollLeft; |
10155 } | 7054 } |
10156 }, | 7055 }, |
10157 | 7056 |
10158 /** | |
10159 * Constructs the final animation config from different properties used | |
10160 * to configure specific parts of the opening and closing animations. | |
10161 */ | |
10162 _updateAnimationConfig: function() { | 7057 _updateAnimationConfig: function() { |
10163 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | 7058 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); |
10164 for (var i = 0; i < animations.length; i++) { | 7059 for (var i = 0; i < animations.length; i++) { |
10165 animations[i].node = this.containedElement; | 7060 animations[i].node = this.containedElement; |
10166 } | 7061 } |
10167 this.animationConfig = { | 7062 this.animationConfig = { |
10168 open: this.openAnimationConfig, | 7063 open: this.openAnimationConfig, |
10169 close: this.closeAnimationConfig | 7064 close: this.closeAnimationConfig |
10170 }; | 7065 }; |
10171 }, | 7066 }, |
10172 | 7067 |
10173 /** | |
10174 * Updates the overlay position based on configured horizontal | |
10175 * and vertical alignment. | |
10176 */ | |
10177 _updateOverlayPosition: function() { | 7068 _updateOverlayPosition: function() { |
10178 if (this.isAttached) { | 7069 if (this.isAttached) { |
10179 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
10180 this.notifyResize(); | 7070 this.notifyResize(); |
10181 } | 7071 } |
10182 }, | 7072 }, |
10183 | 7073 |
10184 /** | |
10185 * Apply focus to focusTarget or containedElement | |
10186 */ | |
10187 _applyFocus: function () { | 7074 _applyFocus: function () { |
10188 var focusTarget = this.focusTarget || this.containedElement; | 7075 var focusTarget = this.focusTarget || this.containedElement; |
10189 if (focusTarget && this.opened && !this.noAutoFocus) { | 7076 if (focusTarget && this.opened && !this.noAutoFocus) { |
10190 focusTarget.focus(); | 7077 focusTarget.focus(); |
10191 } else { | 7078 } else { |
10192 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | 7079 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
10193 } | 7080 } |
10194 } | 7081 } |
10195 }); | 7082 }); |
10196 })(); | 7083 })(); |
(...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
10326 'use strict'; | 7213 'use strict'; |
10327 | 7214 |
10328 var config = { | 7215 var config = { |
10329 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | 7216 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', |
10330 MAX_ANIMATION_TIME_MS: 400 | 7217 MAX_ANIMATION_TIME_MS: 400 |
10331 }; | 7218 }; |
10332 | 7219 |
10333 var PaperMenuButton = Polymer({ | 7220 var PaperMenuButton = Polymer({ |
10334 is: 'paper-menu-button', | 7221 is: 'paper-menu-button', |
10335 | 7222 |
10336 /** | |
10337 * Fired when the dropdown opens. | |
10338 * | |
10339 * @event paper-dropdown-open | |
10340 */ | |
10341 | 7223 |
10342 /** | |
10343 * Fired when the dropdown closes. | |
10344 * | |
10345 * @event paper-dropdown-close | |
10346 */ | |
10347 | 7224 |
10348 behaviors: [ | 7225 behaviors: [ |
10349 Polymer.IronA11yKeysBehavior, | 7226 Polymer.IronA11yKeysBehavior, |
10350 Polymer.IronControlState | 7227 Polymer.IronControlState |
10351 ], | 7228 ], |
10352 | 7229 |
10353 properties: { | 7230 properties: { |
10354 /** | |
10355 * True if the content is currently displayed. | |
10356 */ | |
10357 opened: { | 7231 opened: { |
10358 type: Boolean, | 7232 type: Boolean, |
10359 value: false, | 7233 value: false, |
10360 notify: true, | 7234 notify: true, |
10361 observer: '_openedChanged' | 7235 observer: '_openedChanged' |
10362 }, | 7236 }, |
10363 | 7237 |
10364 /** | |
10365 * The orientation against which to align the menu dropdown | |
10366 * horizontally relative to the dropdown trigger. | |
10367 */ | |
10368 horizontalAlign: { | 7238 horizontalAlign: { |
10369 type: String, | 7239 type: String, |
10370 value: 'left', | 7240 value: 'left', |
10371 reflectToAttribute: true | 7241 reflectToAttribute: true |
10372 }, | 7242 }, |
10373 | 7243 |
10374 /** | |
10375 * The orientation against which to align the menu dropdown | |
10376 * vertically relative to the dropdown trigger. | |
10377 */ | |
10378 verticalAlign: { | 7244 verticalAlign: { |
10379 type: String, | 7245 type: String, |
10380 value: 'top', | 7246 value: 'top', |
10381 reflectToAttribute: true | 7247 reflectToAttribute: true |
10382 }, | 7248 }, |
10383 | 7249 |
10384 /** | |
10385 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
10386 * be considered preferences instead of strict requirements when | |
10387 * positioning the dropdown and may be changed if doing so reduces | |
10388 * the area of the dropdown falling outside of `fitInto`. | |
10389 */ | |
10390 dynamicAlign: { | 7250 dynamicAlign: { |
10391 type: Boolean | 7251 type: Boolean |
10392 }, | 7252 }, |
10393 | 7253 |
10394 /** | |
10395 * A pixel value that will be added to the position calculated for the | |
10396 * given `horizontalAlign`. Use a negative value to offset to the | |
10397 * left, or a positive value to offset to the right. | |
10398 */ | |
10399 horizontalOffset: { | 7254 horizontalOffset: { |
10400 type: Number, | 7255 type: Number, |
10401 value: 0, | 7256 value: 0, |
10402 notify: true | 7257 notify: true |
10403 }, | 7258 }, |
10404 | 7259 |
10405 /** | |
10406 * A pixel value that will be added to the position calculated for the | |
10407 * given `verticalAlign`. Use a negative value to offset towards the | |
10408 * top, or a positive value to offset towards the bottom. | |
10409 */ | |
10410 verticalOffset: { | 7260 verticalOffset: { |
10411 type: Number, | 7261 type: Number, |
10412 value: 0, | 7262 value: 0, |
10413 notify: true | 7263 notify: true |
10414 }, | 7264 }, |
10415 | 7265 |
10416 /** | |
10417 * If true, the dropdown will be positioned so that it doesn't overlap | |
10418 * the button. | |
10419 */ | |
10420 noOverlap: { | 7266 noOverlap: { |
10421 type: Boolean | 7267 type: Boolean |
10422 }, | 7268 }, |
10423 | 7269 |
10424 /** | |
10425 * Set to true to disable animations when opening and closing the | |
10426 * dropdown. | |
10427 */ | |
10428 noAnimations: { | 7270 noAnimations: { |
10429 type: Boolean, | 7271 type: Boolean, |
10430 value: false | 7272 value: false |
10431 }, | 7273 }, |
10432 | 7274 |
10433 /** | |
10434 * Set to true to disable automatically closing the dropdown after | |
10435 * a selection has been made. | |
10436 */ | |
10437 ignoreSelect: { | 7275 ignoreSelect: { |
10438 type: Boolean, | 7276 type: Boolean, |
10439 value: false | 7277 value: false |
10440 }, | 7278 }, |
10441 | 7279 |
10442 /** | |
10443 * Set to true to enable automatically closing the dropdown after an | |
10444 * item has been activated, even if the selection did not change. | |
10445 */ | |
10446 closeOnActivate: { | 7280 closeOnActivate: { |
10447 type: Boolean, | 7281 type: Boolean, |
10448 value: false | 7282 value: false |
10449 }, | 7283 }, |
10450 | 7284 |
10451 /** | |
10452 * An animation config. If provided, this will be used to animate the | |
10453 * opening of the dropdown. | |
10454 */ | |
10455 openAnimationConfig: { | 7285 openAnimationConfig: { |
10456 type: Object, | 7286 type: Object, |
10457 value: function() { | 7287 value: function() { |
10458 return [{ | 7288 return [{ |
10459 name: 'fade-in-animation', | 7289 name: 'fade-in-animation', |
10460 timing: { | 7290 timing: { |
10461 delay: 100, | 7291 delay: 100, |
10462 duration: 200 | 7292 duration: 200 |
10463 } | 7293 } |
10464 }, { | 7294 }, { |
10465 name: 'paper-menu-grow-width-animation', | 7295 name: 'paper-menu-grow-width-animation', |
10466 timing: { | 7296 timing: { |
10467 delay: 100, | 7297 delay: 100, |
10468 duration: 150, | 7298 duration: 150, |
10469 easing: config.ANIMATION_CUBIC_BEZIER | 7299 easing: config.ANIMATION_CUBIC_BEZIER |
10470 } | 7300 } |
10471 }, { | 7301 }, { |
10472 name: 'paper-menu-grow-height-animation', | 7302 name: 'paper-menu-grow-height-animation', |
10473 timing: { | 7303 timing: { |
10474 delay: 100, | 7304 delay: 100, |
10475 duration: 275, | 7305 duration: 275, |
10476 easing: config.ANIMATION_CUBIC_BEZIER | 7306 easing: config.ANIMATION_CUBIC_BEZIER |
10477 } | 7307 } |
10478 }]; | 7308 }]; |
10479 } | 7309 } |
10480 }, | 7310 }, |
10481 | 7311 |
10482 /** | |
10483 * An animation config. If provided, this will be used to animate the | |
10484 * closing of the dropdown. | |
10485 */ | |
10486 closeAnimationConfig: { | 7312 closeAnimationConfig: { |
10487 type: Object, | 7313 type: Object, |
10488 value: function() { | 7314 value: function() { |
10489 return [{ | 7315 return [{ |
10490 name: 'fade-out-animation', | 7316 name: 'fade-out-animation', |
10491 timing: { | 7317 timing: { |
10492 duration: 150 | 7318 duration: 150 |
10493 } | 7319 } |
10494 }, { | 7320 }, { |
10495 name: 'paper-menu-shrink-width-animation', | 7321 name: 'paper-menu-shrink-width-animation', |
10496 timing: { | 7322 timing: { |
10497 delay: 100, | 7323 delay: 100, |
10498 duration: 50, | 7324 duration: 50, |
10499 easing: config.ANIMATION_CUBIC_BEZIER | 7325 easing: config.ANIMATION_CUBIC_BEZIER |
10500 } | 7326 } |
10501 }, { | 7327 }, { |
10502 name: 'paper-menu-shrink-height-animation', | 7328 name: 'paper-menu-shrink-height-animation', |
10503 timing: { | 7329 timing: { |
10504 duration: 200, | 7330 duration: 200, |
10505 easing: 'ease-in' | 7331 easing: 'ease-in' |
10506 } | 7332 } |
10507 }]; | 7333 }]; |
10508 } | 7334 } |
10509 }, | 7335 }, |
10510 | 7336 |
10511 /** | |
10512 * By default, the dropdown will constrain scrolling on the page | |
10513 * to itself when opened. | |
10514 * Set to true in order to prevent scroll from being constrained | |
10515 * to the dropdown when it opens. | |
10516 */ | |
10517 allowOutsideScroll: { | 7337 allowOutsideScroll: { |
10518 type: Boolean, | 7338 type: Boolean, |
10519 value: false | 7339 value: false |
10520 }, | 7340 }, |
10521 | 7341 |
10522 /** | |
10523 * Whether focus should be restored to the button when the menu closes
. | |
10524 */ | |
10525 restoreFocusOnClose: { | 7342 restoreFocusOnClose: { |
10526 type: Boolean, | 7343 type: Boolean, |
10527 value: true | 7344 value: true |
10528 }, | 7345 }, |
10529 | 7346 |
10530 /** | |
10531 * This is the element intended to be bound as the focus target | |
10532 * for the `iron-dropdown` contained by `paper-menu-button`. | |
10533 */ | |
10534 _dropdownContent: { | 7347 _dropdownContent: { |
10535 type: Object | 7348 type: Object |
10536 } | 7349 } |
10537 }, | 7350 }, |
10538 | 7351 |
10539 hostAttributes: { | 7352 hostAttributes: { |
10540 role: 'group', | 7353 role: 'group', |
10541 'aria-haspopup': 'true' | 7354 'aria-haspopup': 'true' |
10542 }, | 7355 }, |
10543 | 7356 |
10544 listeners: { | 7357 listeners: { |
10545 'iron-activate': '_onIronActivate', | 7358 'iron-activate': '_onIronActivate', |
10546 'iron-select': '_onIronSelect' | 7359 'iron-select': '_onIronSelect' |
10547 }, | 7360 }, |
10548 | 7361 |
10549 /** | |
10550 * The content element that is contained by the menu button, if any. | |
10551 */ | |
10552 get contentElement() { | 7362 get contentElement() { |
10553 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | 7363 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
10554 }, | 7364 }, |
10555 | 7365 |
10556 /** | |
10557 * Toggles the drowpdown content between opened and closed. | |
10558 */ | |
10559 toggle: function() { | 7366 toggle: function() { |
10560 if (this.opened) { | 7367 if (this.opened) { |
10561 this.close(); | 7368 this.close(); |
10562 } else { | 7369 } else { |
10563 this.open(); | 7370 this.open(); |
10564 } | 7371 } |
10565 }, | 7372 }, |
10566 | 7373 |
10567 /** | |
10568 * Make the dropdown content appear as an overlay positioned relative | |
10569 * to the dropdown trigger. | |
10570 */ | |
10571 open: function() { | 7374 open: function() { |
10572 if (this.disabled) { | 7375 if (this.disabled) { |
10573 return; | 7376 return; |
10574 } | 7377 } |
10575 | 7378 |
10576 this.$.dropdown.open(); | 7379 this.$.dropdown.open(); |
10577 }, | 7380 }, |
10578 | 7381 |
10579 /** | |
10580 * Hide the dropdown content. | |
10581 */ | |
10582 close: function() { | 7382 close: function() { |
10583 this.$.dropdown.close(); | 7383 this.$.dropdown.close(); |
10584 }, | 7384 }, |
10585 | 7385 |
10586 /** | |
10587 * When an `iron-select` event is received, the dropdown should | |
10588 * automatically close on the assumption that a value has been chosen. | |
10589 * | |
10590 * @param {CustomEvent} event A CustomEvent instance with type | |
10591 * set to `"iron-select"`. | |
10592 */ | |
10593 _onIronSelect: function(event) { | 7386 _onIronSelect: function(event) { |
10594 if (!this.ignoreSelect) { | 7387 if (!this.ignoreSelect) { |
10595 this.close(); | 7388 this.close(); |
10596 } | 7389 } |
10597 }, | 7390 }, |
10598 | 7391 |
10599 /** | |
10600 * Closes the dropdown when an `iron-activate` event is received if | |
10601 * `closeOnActivate` is true. | |
10602 * | |
10603 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | |
10604 */ | |
10605 _onIronActivate: function(event) { | 7392 _onIronActivate: function(event) { |
10606 if (this.closeOnActivate) { | 7393 if (this.closeOnActivate) { |
10607 this.close(); | 7394 this.close(); |
10608 } | 7395 } |
10609 }, | 7396 }, |
10610 | 7397 |
10611 /** | |
10612 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | |
10613 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | |
10614 * | |
10615 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | |
10616 * @param {boolean} oldOpened The previous value of `opened`. | |
10617 */ | |
10618 _openedChanged: function(opened, oldOpened) { | 7398 _openedChanged: function(opened, oldOpened) { |
10619 if (opened) { | 7399 if (opened) { |
10620 // TODO(cdata): Update this when we can measure changes in distribut
ed | |
10621 // children in an idiomatic way. | |
10622 // We poke this property in case the element has changed. This will | |
10623 // cause the focus target for the `iron-dropdown` to be updated as | |
10624 // necessary: | |
10625 this._dropdownContent = this.contentElement; | 7400 this._dropdownContent = this.contentElement; |
10626 this.fire('paper-dropdown-open'); | 7401 this.fire('paper-dropdown-open'); |
10627 } else if (oldOpened != null) { | 7402 } else if (oldOpened != null) { |
10628 this.fire('paper-dropdown-close'); | 7403 this.fire('paper-dropdown-close'); |
10629 } | 7404 } |
10630 }, | 7405 }, |
10631 | 7406 |
10632 /** | |
10633 * If the dropdown is open when disabled becomes true, close the | |
10634 * dropdown. | |
10635 * | |
10636 * @param {boolean} disabled True if disabled, otherwise false. | |
10637 */ | |
10638 _disabledChanged: function(disabled) { | 7407 _disabledChanged: function(disabled) { |
10639 Polymer.IronControlState._disabledChanged.apply(this, arguments); | 7408 Polymer.IronControlState._disabledChanged.apply(this, arguments); |
10640 if (disabled && this.opened) { | 7409 if (disabled && this.opened) { |
10641 this.close(); | 7410 this.close(); |
10642 } | 7411 } |
10643 }, | 7412 }, |
10644 | 7413 |
10645 __onIronOverlayCanceled: function(event) { | 7414 __onIronOverlayCanceled: function(event) { |
10646 var uiEvent = event.detail; | 7415 var uiEvent = event.detail; |
10647 var target = Polymer.dom(uiEvent).rootTarget; | 7416 var target = Polymer.dom(uiEvent).rootTarget; |
10648 var trigger = this.$.trigger; | 7417 var trigger = this.$.trigger; |
10649 var path = Polymer.dom(uiEvent).path; | 7418 var path = Polymer.dom(uiEvent).path; |
10650 | 7419 |
10651 if (path.indexOf(trigger) > -1) { | 7420 if (path.indexOf(trigger) > -1) { |
10652 event.preventDefault(); | 7421 event.preventDefault(); |
10653 } | 7422 } |
10654 } | 7423 } |
10655 }); | 7424 }); |
10656 | 7425 |
10657 Object.keys(config).forEach(function (key) { | 7426 Object.keys(config).forEach(function (key) { |
10658 PaperMenuButton[key] = config[key]; | 7427 PaperMenuButton[key] = config[key]; |
10659 }); | 7428 }); |
10660 | 7429 |
10661 Polymer.PaperMenuButton = PaperMenuButton; | 7430 Polymer.PaperMenuButton = PaperMenuButton; |
10662 })(); | 7431 })(); |
10663 /** | |
10664 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | |
10665 * | |
10666 * @polymerBehavior Polymer.PaperInkyFocusBehavior | |
10667 */ | |
10668 Polymer.PaperInkyFocusBehaviorImpl = { | 7432 Polymer.PaperInkyFocusBehaviorImpl = { |
10669 observers: [ | 7433 observers: [ |
10670 '_focusedChanged(receivedFocusFromKeyboard)' | 7434 '_focusedChanged(receivedFocusFromKeyboard)' |
10671 ], | 7435 ], |
10672 | 7436 |
10673 _focusedChanged: function(receivedFocusFromKeyboard) { | 7437 _focusedChanged: function(receivedFocusFromKeyboard) { |
10674 if (receivedFocusFromKeyboard) { | 7438 if (receivedFocusFromKeyboard) { |
10675 this.ensureRipple(); | 7439 this.ensureRipple(); |
10676 } | 7440 } |
10677 if (this.hasRipple()) { | 7441 if (this.hasRipple()) { |
(...skipping 23 matching lines...) Expand all Loading... |
10701 hostAttributes: { | 7465 hostAttributes: { |
10702 role: 'button', | 7466 role: 'button', |
10703 tabindex: '0' | 7467 tabindex: '0' |
10704 }, | 7468 }, |
10705 | 7469 |
10706 behaviors: [ | 7470 behaviors: [ |
10707 Polymer.PaperInkyFocusBehavior | 7471 Polymer.PaperInkyFocusBehavior |
10708 ], | 7472 ], |
10709 | 7473 |
10710 properties: { | 7474 properties: { |
10711 /** | |
10712 * The URL of an image for the icon. If the src property is specified, | |
10713 * the icon property should not be. | |
10714 */ | |
10715 src: { | 7475 src: { |
10716 type: String | 7476 type: String |
10717 }, | 7477 }, |
10718 | 7478 |
10719 /** | |
10720 * Specifies the icon name or index in the set of icons available in | |
10721 * the icon's icon set. If the icon property is specified, | |
10722 * the src property should not be. | |
10723 */ | |
10724 icon: { | 7479 icon: { |
10725 type: String | 7480 type: String |
10726 }, | 7481 }, |
10727 | 7482 |
10728 /** | |
10729 * Specifies the alternate text for the button, for accessibility. | |
10730 */ | |
10731 alt: { | 7483 alt: { |
10732 type: String, | 7484 type: String, |
10733 observer: "_altChanged" | 7485 observer: "_altChanged" |
10734 } | 7486 } |
10735 }, | 7487 }, |
10736 | 7488 |
10737 _altChanged: function(newValue, oldValue) { | 7489 _altChanged: function(newValue, oldValue) { |
10738 var label = this.getAttribute('aria-label'); | 7490 var label = this.getAttribute('aria-label'); |
10739 | 7491 |
10740 // Don't stomp over a user-set aria-label. | |
10741 if (!label || oldValue == label) { | 7492 if (!label || oldValue == label) { |
10742 this.setAttribute('aria-label', newValue); | 7493 this.setAttribute('aria-label', newValue); |
10743 } | 7494 } |
10744 } | 7495 } |
10745 }); | 7496 }); |
10746 // Copyright 2016 The Chromium Authors. All rights reserved. | 7497 // Copyright 2016 The Chromium Authors. All rights reserved. |
10747 // Use of this source code is governed by a BSD-style license that can be | 7498 // Use of this source code is governed by a BSD-style license that can be |
10748 // found in the LICENSE file. | 7499 // found in the LICENSE file. |
10749 | 7500 |
10750 /** | |
10751 * Implements an incremental search field which can be shown and hidden. | |
10752 * Canonical implementation is <cr-search-field>. | |
10753 * @polymerBehavior | |
10754 */ | |
10755 var CrSearchFieldBehavior = { | 7501 var CrSearchFieldBehavior = { |
10756 properties: { | 7502 properties: { |
10757 label: { | 7503 label: { |
10758 type: String, | 7504 type: String, |
10759 value: '', | 7505 value: '', |
10760 }, | 7506 }, |
10761 | 7507 |
10762 clearLabel: { | 7508 clearLabel: { |
10763 type: String, | 7509 type: String, |
10764 value: '', | 7510 value: '', |
10765 }, | 7511 }, |
10766 | 7512 |
10767 showingSearch: { | 7513 showingSearch: { |
10768 type: Boolean, | 7514 type: Boolean, |
10769 value: false, | 7515 value: false, |
10770 notify: true, | 7516 notify: true, |
10771 observer: 'showingSearchChanged_', | 7517 observer: 'showingSearchChanged_', |
10772 reflectToAttribute: true | 7518 reflectToAttribute: true |
10773 }, | 7519 }, |
10774 | 7520 |
10775 /** @private */ | 7521 /** @private */ |
10776 lastValue_: { | 7522 lastValue_: { |
10777 type: String, | 7523 type: String, |
10778 value: '', | 7524 value: '', |
10779 }, | 7525 }, |
10780 }, | 7526 }, |
10781 | 7527 |
10782 /** | |
10783 * @abstract | |
10784 * @return {!HTMLInputElement} The input field element the behavior should | |
10785 * use. | |
10786 */ | |
10787 getSearchInput: function() {}, | 7528 getSearchInput: function() {}, |
10788 | 7529 |
10789 /** | |
10790 * @return {string} The value of the search field. | |
10791 */ | |
10792 getValue: function() { | 7530 getValue: function() { |
10793 return this.getSearchInput().value; | 7531 return this.getSearchInput().value; |
10794 }, | 7532 }, |
10795 | 7533 |
10796 /** | |
10797 * Sets the value of the search field. | |
10798 * @param {string} value | |
10799 */ | |
10800 setValue: function(value) { | 7534 setValue: function(value) { |
10801 // Use bindValue when setting the input value so that changes propagate | |
10802 // correctly. | |
10803 this.getSearchInput().bindValue = value; | 7535 this.getSearchInput().bindValue = value; |
10804 this.onValueChanged_(value); | 7536 this.onValueChanged_(value); |
10805 }, | 7537 }, |
10806 | 7538 |
10807 showAndFocus: function() { | 7539 showAndFocus: function() { |
10808 this.showingSearch = true; | 7540 this.showingSearch = true; |
10809 this.focus_(); | 7541 this.focus_(); |
10810 }, | 7542 }, |
10811 | 7543 |
10812 /** @private */ | 7544 /** @private */ |
10813 focus_: function() { | 7545 focus_: function() { |
10814 this.getSearchInput().focus(); | 7546 this.getSearchInput().focus(); |
10815 }, | 7547 }, |
10816 | 7548 |
10817 onSearchTermSearch: function() { | 7549 onSearchTermSearch: function() { |
10818 this.onValueChanged_(this.getValue()); | 7550 this.onValueChanged_(this.getValue()); |
10819 }, | 7551 }, |
10820 | 7552 |
10821 /** | |
10822 * Updates the internal state of the search field based on a change that has | |
10823 * already happened. | |
10824 * @param {string} newValue | |
10825 * @private | |
10826 */ | |
10827 onValueChanged_: function(newValue) { | 7553 onValueChanged_: function(newValue) { |
10828 if (newValue == this.lastValue_) | 7554 if (newValue == this.lastValue_) |
10829 return; | 7555 return; |
10830 | 7556 |
10831 this.fire('search-changed', newValue); | 7557 this.fire('search-changed', newValue); |
10832 this.lastValue_ = newValue; | 7558 this.lastValue_ = newValue; |
10833 }, | 7559 }, |
10834 | 7560 |
10835 onSearchTermKeydown: function(e) { | 7561 onSearchTermKeydown: function(e) { |
10836 if (e.key == 'Escape') | 7562 if (e.key == 'Escape') |
(...skipping 12 matching lines...) Expand all Loading... |
10849 } | 7575 } |
10850 }; | 7576 }; |
10851 (function() { | 7577 (function() { |
10852 'use strict'; | 7578 'use strict'; |
10853 | 7579 |
10854 Polymer.IronA11yAnnouncer = Polymer({ | 7580 Polymer.IronA11yAnnouncer = Polymer({ |
10855 is: 'iron-a11y-announcer', | 7581 is: 'iron-a11y-announcer', |
10856 | 7582 |
10857 properties: { | 7583 properties: { |
10858 | 7584 |
10859 /** | |
10860 * The value of mode is used to set the `aria-live` attribute | |
10861 * for the element that will be announced. Valid values are: `off`, | |
10862 * `polite` and `assertive`. | |
10863 */ | |
10864 mode: { | 7585 mode: { |
10865 type: String, | 7586 type: String, |
10866 value: 'polite' | 7587 value: 'polite' |
10867 }, | 7588 }, |
10868 | 7589 |
10869 _text: { | 7590 _text: { |
10870 type: String, | 7591 type: String, |
10871 value: '' | 7592 value: '' |
10872 } | 7593 } |
10873 }, | 7594 }, |
10874 | 7595 |
10875 created: function() { | 7596 created: function() { |
10876 if (!Polymer.IronA11yAnnouncer.instance) { | 7597 if (!Polymer.IronA11yAnnouncer.instance) { |
10877 Polymer.IronA11yAnnouncer.instance = this; | 7598 Polymer.IronA11yAnnouncer.instance = this; |
10878 } | 7599 } |
10879 | 7600 |
10880 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | 7601 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); |
10881 }, | 7602 }, |
10882 | 7603 |
10883 /** | |
10884 * Cause a text string to be announced by screen readers. | |
10885 * | |
10886 * @param {string} text The text that should be announced. | |
10887 */ | |
10888 announce: function(text) { | 7604 announce: function(text) { |
10889 this._text = ''; | 7605 this._text = ''; |
10890 this.async(function() { | 7606 this.async(function() { |
10891 this._text = text; | 7607 this._text = text; |
10892 }, 100); | 7608 }, 100); |
10893 }, | 7609 }, |
10894 | 7610 |
10895 _onIronAnnounce: function(event) { | 7611 _onIronAnnounce: function(event) { |
10896 if (event.detail && event.detail.text) { | 7612 if (event.detail && event.detail.text) { |
10897 this.announce(event.detail.text); | 7613 this.announce(event.detail.text); |
10898 } | 7614 } |
10899 } | 7615 } |
10900 }); | 7616 }); |
10901 | 7617 |
10902 Polymer.IronA11yAnnouncer.instance = null; | 7618 Polymer.IronA11yAnnouncer.instance = null; |
10903 | 7619 |
10904 Polymer.IronA11yAnnouncer.requestAvailability = function() { | 7620 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
10905 if (!Polymer.IronA11yAnnouncer.instance) { | 7621 if (!Polymer.IronA11yAnnouncer.instance) { |
10906 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | 7622 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); |
10907 } | 7623 } |
10908 | 7624 |
10909 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | 7625 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
10910 }; | 7626 }; |
10911 })(); | 7627 })(); |
10912 /** | |
10913 * Singleton IronMeta instance. | |
10914 */ | |
10915 Polymer.IronValidatableBehaviorMeta = null; | 7628 Polymer.IronValidatableBehaviorMeta = null; |
10916 | 7629 |
10917 /** | |
10918 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
10919 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
10920 * | |
10921 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
10922 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
10923 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
10924 * custom element will get a public `validate()`, which | |
10925 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
10926 * which can be used for styling. | |
10927 * | |
10928 * To implement the custom validation logic of your element, you must override | |
10929 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
10930 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
10931 * for an example. | |
10932 * | |
10933 * ### Accessibility | |
10934 * | |
10935 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
10936 * `aria-invalid` attribute. | |
10937 * | |
10938 * @demo demo/index.html | |
10939 * @polymerBehavior | |
10940 */ | |
10941 Polymer.IronValidatableBehavior = { | 7630 Polymer.IronValidatableBehavior = { |
10942 | 7631 |
10943 properties: { | 7632 properties: { |
10944 | 7633 |
10945 /** | |
10946 * Name of the validator to use. | |
10947 */ | |
10948 validator: { | 7634 validator: { |
10949 type: String | 7635 type: String |
10950 }, | 7636 }, |
10951 | 7637 |
10952 /** | |
10953 * True if the last call to `validate` is invalid. | |
10954 */ | |
10955 invalid: { | 7638 invalid: { |
10956 notify: true, | 7639 notify: true, |
10957 reflectToAttribute: true, | 7640 reflectToAttribute: true, |
10958 type: Boolean, | 7641 type: Boolean, |
10959 value: false | 7642 value: false |
10960 }, | 7643 }, |
10961 | 7644 |
10962 /** | |
10963 * This property is deprecated and should not be used. Use the global | |
10964 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
10965 */ | |
10966 _validatorMeta: { | 7645 _validatorMeta: { |
10967 type: Object | 7646 type: Object |
10968 }, | 7647 }, |
10969 | 7648 |
10970 /** | |
10971 * Namespace for this validator. This property is deprecated and should | |
10972 * not be used. For all intents and purposes, please consider it a | |
10973 * read-only, config-time property. | |
10974 */ | |
10975 validatorType: { | 7649 validatorType: { |
10976 type: String, | 7650 type: String, |
10977 value: 'validator' | 7651 value: 'validator' |
10978 }, | 7652 }, |
10979 | 7653 |
10980 _validator: { | 7654 _validator: { |
10981 type: Object, | 7655 type: Object, |
10982 computed: '__computeValidator(validator)' | 7656 computed: '__computeValidator(validator)' |
10983 } | 7657 } |
10984 }, | 7658 }, |
10985 | 7659 |
10986 observers: [ | 7660 observers: [ |
10987 '_invalidChanged(invalid)' | 7661 '_invalidChanged(invalid)' |
10988 ], | 7662 ], |
10989 | 7663 |
10990 registered: function() { | 7664 registered: function() { |
10991 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | 7665 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); |
10992 }, | 7666 }, |
10993 | 7667 |
10994 _invalidChanged: function() { | 7668 _invalidChanged: function() { |
10995 if (this.invalid) { | 7669 if (this.invalid) { |
10996 this.setAttribute('aria-invalid', 'true'); | 7670 this.setAttribute('aria-invalid', 'true'); |
10997 } else { | 7671 } else { |
10998 this.removeAttribute('aria-invalid'); | 7672 this.removeAttribute('aria-invalid'); |
10999 } | 7673 } |
11000 }, | 7674 }, |
11001 | 7675 |
11002 /** | |
11003 * @return {boolean} True if the validator `validator` exists. | |
11004 */ | |
11005 hasValidator: function() { | 7676 hasValidator: function() { |
11006 return this._validator != null; | 7677 return this._validator != null; |
11007 }, | 7678 }, |
11008 | 7679 |
11009 /** | |
11010 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
11011 * your element to have custom validation logic, do not override this method
; | |
11012 * override `_getValidity(value)` instead. | |
11013 | |
11014 * @param {Object} value The value to be validated. By default, it is passed | |
11015 * to the validator's `validate()` function, if a validator is set. | |
11016 * @return {boolean} True if `value` is valid. | |
11017 */ | |
11018 validate: function(value) { | 7680 validate: function(value) { |
11019 this.invalid = !this._getValidity(value); | 7681 this.invalid = !this._getValidity(value); |
11020 return !this.invalid; | 7682 return !this.invalid; |
11021 }, | 7683 }, |
11022 | 7684 |
11023 /** | |
11024 * Returns true if `value` is valid. By default, it is passed | |
11025 * to the validator's `validate()` function, if a validator is set. You | |
11026 * should override this method if you want to implement custom validity | |
11027 * logic for your element. | |
11028 * | |
11029 * @param {Object} value The value to be validated. | |
11030 * @return {boolean} True if `value` is valid. | |
11031 */ | |
11032 | 7685 |
11033 _getValidity: function(value) { | 7686 _getValidity: function(value) { |
11034 if (this.hasValidator()) { | 7687 if (this.hasValidator()) { |
11035 return this._validator.validate(value); | 7688 return this._validator.validate(value); |
11036 } | 7689 } |
11037 return true; | 7690 return true; |
11038 }, | 7691 }, |
11039 | 7692 |
11040 __computeValidator: function() { | 7693 __computeValidator: function() { |
11041 return Polymer.IronValidatableBehaviorMeta && | 7694 return Polymer.IronValidatableBehaviorMeta && |
11042 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | 7695 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); |
11043 } | 7696 } |
11044 }; | 7697 }; |
11045 /* | |
11046 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
11047 to `<input>`. | |
11048 | |
11049 ### Two-way binding | |
11050 | |
11051 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
11052 | |
11053 <input value="{{myValue::input}}"> | |
11054 | |
11055 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
11056 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
11057 | |
11058 <input is="iron-input" bind-value="{{myValue}}"> | |
11059 | |
11060 ### Custom validators | |
11061 | |
11062 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
11063 | |
11064 <input is="iron-input" validator="my-custom-validator"> | |
11065 | |
11066 ### Stopping invalid input | |
11067 | |
11068 It may be desirable to only allow users to enter certain characters. You can use
the | |
11069 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
11070 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
11071 | |
11072 \x3c!-- only allow characters that match [0-9] --\x3e | |
11073 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
11074 | |
11075 @hero hero.svg | |
11076 @demo demo/index.html | |
11077 */ | |
11078 | 7698 |
11079 Polymer({ | 7699 Polymer({ |
11080 | 7700 |
11081 is: 'iron-input', | 7701 is: 'iron-input', |
11082 | 7702 |
11083 extends: 'input', | 7703 extends: 'input', |
11084 | 7704 |
11085 behaviors: [ | 7705 behaviors: [ |
11086 Polymer.IronValidatableBehavior | 7706 Polymer.IronValidatableBehavior |
11087 ], | 7707 ], |
11088 | 7708 |
11089 properties: { | 7709 properties: { |
11090 | 7710 |
11091 /** | |
11092 * Use this property instead of `value` for two-way data binding. | |
11093 */ | |
11094 bindValue: { | 7711 bindValue: { |
11095 observer: '_bindValueChanged', | 7712 observer: '_bindValueChanged', |
11096 type: String | 7713 type: String |
11097 }, | 7714 }, |
11098 | 7715 |
11099 /** | |
11100 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
11101 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
11102 * Pasted input will have each character checked individually; if any char
acter | |
11103 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
11104 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
11105 */ | |
11106 preventInvalidInput: { | 7716 preventInvalidInput: { |
11107 type: Boolean | 7717 type: Boolean |
11108 }, | 7718 }, |
11109 | 7719 |
11110 /** | |
11111 * Regular expression that list the characters allowed as input. | |
11112 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
11113 * each individual character will be checked against the pattern (rather t
han checking | |
11114 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
11115 * for example, `[a-zA-Z0-9.+-!;:]` | |
11116 */ | |
11117 allowedPattern: { | 7720 allowedPattern: { |
11118 type: String, | 7721 type: String, |
11119 observer: "_allowedPatternChanged" | 7722 observer: "_allowedPatternChanged" |
11120 }, | 7723 }, |
11121 | 7724 |
11122 _previousValidInput: { | 7725 _previousValidInput: { |
11123 type: String, | 7726 type: String, |
11124 value: '' | 7727 value: '' |
11125 }, | 7728 }, |
11126 | 7729 |
11127 _patternAlreadyChecked: { | 7730 _patternAlreadyChecked: { |
11128 type: Boolean, | 7731 type: Boolean, |
11129 value: false | 7732 value: false |
11130 } | 7733 } |
11131 | 7734 |
11132 }, | 7735 }, |
11133 | 7736 |
11134 listeners: { | 7737 listeners: { |
11135 'input': '_onInput', | 7738 'input': '_onInput', |
11136 'keypress': '_onKeypress' | 7739 'keypress': '_onKeypress' |
11137 }, | 7740 }, |
11138 | 7741 |
11139 /** @suppress {checkTypes} */ | 7742 /** @suppress {checkTypes} */ |
11140 registered: function() { | 7743 registered: function() { |
11141 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
11142 if (!this._canDispatchEventOnDisabled()) { | 7744 if (!this._canDispatchEventOnDisabled()) { |
11143 this._origDispatchEvent = this.dispatchEvent; | 7745 this._origDispatchEvent = this.dispatchEvent; |
11144 this.dispatchEvent = this._dispatchEventFirefoxIE; | 7746 this.dispatchEvent = this._dispatchEventFirefoxIE; |
11145 } | 7747 } |
11146 }, | 7748 }, |
11147 | 7749 |
11148 created: function() { | 7750 created: function() { |
11149 Polymer.IronA11yAnnouncer.requestAvailability(); | 7751 Polymer.IronA11yAnnouncer.requestAvailability(); |
11150 }, | 7752 }, |
11151 | 7753 |
11152 _canDispatchEventOnDisabled: function() { | 7754 _canDispatchEventOnDisabled: function() { |
11153 var input = document.createElement('input'); | 7755 var input = document.createElement('input'); |
11154 var canDispatch = false; | 7756 var canDispatch = false; |
11155 input.disabled = true; | 7757 input.disabled = true; |
11156 | 7758 |
11157 input.addEventListener('feature-check-dispatch-event', function() { | 7759 input.addEventListener('feature-check-dispatch-event', function() { |
11158 canDispatch = true; | 7760 canDispatch = true; |
11159 }); | 7761 }); |
11160 | 7762 |
11161 try { | 7763 try { |
11162 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 7764 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
11163 } catch(e) {} | 7765 } catch(e) {} |
11164 | 7766 |
11165 return canDispatch; | 7767 return canDispatch; |
11166 }, | 7768 }, |
11167 | 7769 |
11168 _dispatchEventFirefoxIE: function() { | 7770 _dispatchEventFirefoxIE: function() { |
11169 // Due to Firefox bug, events fired on disabled form controls can throw | |
11170 // errors; furthermore, neither IE nor Firefox will actually dispatch | |
11171 // events from disabled form controls; as such, we toggle disable around | |
11172 // the dispatch to allow notifying properties to notify | |
11173 // See issue #47 for details | |
11174 var disabled = this.disabled; | 7771 var disabled = this.disabled; |
11175 this.disabled = false; | 7772 this.disabled = false; |
11176 this._origDispatchEvent.apply(this, arguments); | 7773 this._origDispatchEvent.apply(this, arguments); |
11177 this.disabled = disabled; | 7774 this.disabled = disabled; |
11178 }, | 7775 }, |
11179 | 7776 |
11180 get _patternRegExp() { | 7777 get _patternRegExp() { |
11181 var pattern; | 7778 var pattern; |
11182 if (this.allowedPattern) { | 7779 if (this.allowedPattern) { |
11183 pattern = new RegExp(this.allowedPattern); | 7780 pattern = new RegExp(this.allowedPattern); |
11184 } else { | 7781 } else { |
11185 switch (this.type) { | 7782 switch (this.type) { |
11186 case 'number': | 7783 case 'number': |
11187 pattern = /[0-9.,e-]/; | 7784 pattern = /[0-9.,e-]/; |
11188 break; | 7785 break; |
11189 } | 7786 } |
11190 } | 7787 } |
11191 return pattern; | 7788 return pattern; |
11192 }, | 7789 }, |
11193 | 7790 |
11194 ready: function() { | 7791 ready: function() { |
11195 this.bindValue = this.value; | 7792 this.bindValue = this.value; |
11196 }, | 7793 }, |
11197 | 7794 |
11198 /** | |
11199 * @suppress {checkTypes} | |
11200 */ | |
11201 _bindValueChanged: function() { | 7795 _bindValueChanged: function() { |
11202 if (this.value !== this.bindValue) { | 7796 if (this.value !== this.bindValue) { |
11203 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | 7797 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; |
11204 } | 7798 } |
11205 // manually notify because we don't want to notify until after setting val
ue | |
11206 this.fire('bind-value-changed', {value: this.bindValue}); | 7799 this.fire('bind-value-changed', {value: this.bindValue}); |
11207 }, | 7800 }, |
11208 | 7801 |
11209 _allowedPatternChanged: function() { | 7802 _allowedPatternChanged: function() { |
11210 // Force to prevent invalid input when an `allowed-pattern` is set | |
11211 this.preventInvalidInput = this.allowedPattern ? true : false; | 7803 this.preventInvalidInput = this.allowedPattern ? true : false; |
11212 }, | 7804 }, |
11213 | 7805 |
11214 _onInput: function() { | 7806 _onInput: function() { |
11215 // Need to validate each of the characters pasted if they haven't | |
11216 // been validated inside `_onKeypress` already. | |
11217 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | 7807 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
11218 var valid = this._checkPatternValidity(); | 7808 var valid = this._checkPatternValidity(); |
11219 if (!valid) { | 7809 if (!valid) { |
11220 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | 7810 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); |
11221 this.value = this._previousValidInput; | 7811 this.value = this._previousValidInput; |
11222 } | 7812 } |
11223 } | 7813 } |
11224 | 7814 |
11225 this.bindValue = this.value; | 7815 this.bindValue = this.value; |
11226 this._previousValidInput = this.value; | 7816 this._previousValidInput = this.value; |
11227 this._patternAlreadyChecked = false; | 7817 this._patternAlreadyChecked = false; |
11228 }, | 7818 }, |
11229 | 7819 |
11230 _isPrintable: function(event) { | 7820 _isPrintable: function(event) { |
11231 // What a control/printable character is varies wildly based on the browse
r. | |
11232 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
11233 // in Chrome, but the *do* on Firefox | |
11234 // - in Firefox, when they do send a `keypress` event, control chars have | |
11235 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
11236 // - printable characters always send a keypress event. | |
11237 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
11238 // always matches the charCode. | |
11239 // None of this makes any sense. | |
11240 | 7821 |
11241 // For these keys, ASCII code == browser keycode. | |
11242 var anyNonPrintable = | 7822 var anyNonPrintable = |
11243 (event.keyCode == 8) || // backspace | 7823 (event.keyCode == 8) || // backspace |
11244 (event.keyCode == 9) || // tab | 7824 (event.keyCode == 9) || // tab |
11245 (event.keyCode == 13) || // enter | 7825 (event.keyCode == 13) || // enter |
11246 (event.keyCode == 27); // escape | 7826 (event.keyCode == 27); // escape |
11247 | 7827 |
11248 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
11249 var mozNonPrintable = | 7828 var mozNonPrintable = |
11250 (event.keyCode == 19) || // pause | 7829 (event.keyCode == 19) || // pause |
11251 (event.keyCode == 20) || // caps lock | 7830 (event.keyCode == 20) || // caps lock |
11252 (event.keyCode == 45) || // insert | 7831 (event.keyCode == 45) || // insert |
11253 (event.keyCode == 46) || // delete | 7832 (event.keyCode == 46) || // delete |
11254 (event.keyCode == 144) || // num lock | 7833 (event.keyCode == 144) || // num lock |
11255 (event.keyCode == 145) || // scroll lock | 7834 (event.keyCode == 145) || // scroll lock |
11256 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | 7835 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows |
11257 (event.keyCode > 111 && event.keyCode < 124); // fn keys | 7836 (event.keyCode > 111 && event.keyCode < 124); // fn keys |
11258 | 7837 |
11259 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | 7838 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
11260 }, | 7839 }, |
11261 | 7840 |
11262 _onKeypress: function(event) { | 7841 _onKeypress: function(event) { |
11263 if (!this.preventInvalidInput && this.type !== 'number') { | 7842 if (!this.preventInvalidInput && this.type !== 'number') { |
11264 return; | 7843 return; |
11265 } | 7844 } |
11266 var regexp = this._patternRegExp; | 7845 var regexp = this._patternRegExp; |
11267 if (!regexp) { | 7846 if (!regexp) { |
11268 return; | 7847 return; |
11269 } | 7848 } |
11270 | 7849 |
11271 // Handle special keys and backspace | |
11272 if (event.metaKey || event.ctrlKey || event.altKey) | 7850 if (event.metaKey || event.ctrlKey || event.altKey) |
11273 return; | 7851 return; |
11274 | 7852 |
11275 // Check the pattern either here or in `_onInput`, but not in both. | |
11276 this._patternAlreadyChecked = true; | 7853 this._patternAlreadyChecked = true; |
11277 | 7854 |
11278 var thisChar = String.fromCharCode(event.charCode); | 7855 var thisChar = String.fromCharCode(event.charCode); |
11279 if (this._isPrintable(event) && !regexp.test(thisChar)) { | 7856 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
11280 event.preventDefault(); | 7857 event.preventDefault(); |
11281 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | 7858 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); |
11282 } | 7859 } |
11283 }, | 7860 }, |
11284 | 7861 |
11285 _checkPatternValidity: function() { | 7862 _checkPatternValidity: function() { |
11286 var regexp = this._patternRegExp; | 7863 var regexp = this._patternRegExp; |
11287 if (!regexp) { | 7864 if (!regexp) { |
11288 return true; | 7865 return true; |
11289 } | 7866 } |
11290 for (var i = 0; i < this.value.length; i++) { | 7867 for (var i = 0; i < this.value.length; i++) { |
11291 if (!regexp.test(this.value[i])) { | 7868 if (!regexp.test(this.value[i])) { |
11292 return false; | 7869 return false; |
11293 } | 7870 } |
11294 } | 7871 } |
11295 return true; | 7872 return true; |
11296 }, | 7873 }, |
11297 | 7874 |
11298 /** | |
11299 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
11300 * then any constraints. | |
11301 * @return {boolean} True if the value is valid. | |
11302 */ | |
11303 validate: function() { | 7875 validate: function() { |
11304 // First, check what the browser thinks. Some inputs (like type=number) | |
11305 // behave weirdly and will set the value to "" if something invalid is | |
11306 // entered, but will set the validity correctly. | |
11307 var valid = this.checkValidity(); | 7876 var valid = this.checkValidity(); |
11308 | 7877 |
11309 // Only do extra checking if the browser thought this was valid. | |
11310 if (valid) { | 7878 if (valid) { |
11311 // Empty, required input is invalid | |
11312 if (this.required && this.value === '') { | 7879 if (this.required && this.value === '') { |
11313 valid = false; | 7880 valid = false; |
11314 } else if (this.hasValidator()) { | 7881 } else if (this.hasValidator()) { |
11315 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | 7882 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); |
11316 } | 7883 } |
11317 } | 7884 } |
11318 | 7885 |
11319 this.invalid = !valid; | 7886 this.invalid = !valid; |
11320 this.fire('iron-input-validate'); | 7887 this.fire('iron-input-validate'); |
11321 return valid; | 7888 return valid; |
11322 }, | 7889 }, |
11323 | 7890 |
11324 _announceInvalidCharacter: function(message) { | 7891 _announceInvalidCharacter: function(message) { |
11325 this.fire('iron-announce', { text: message }); | 7892 this.fire('iron-announce', { text: message }); |
11326 } | 7893 } |
11327 }); | 7894 }); |
11328 | 7895 |
11329 /* | |
11330 The `iron-input-validate` event is fired whenever `validate()` is called. | |
11331 @event iron-input-validate | |
11332 */ | |
11333 Polymer({ | 7896 Polymer({ |
11334 is: 'paper-input-container', | 7897 is: 'paper-input-container', |
11335 | 7898 |
11336 properties: { | 7899 properties: { |
11337 /** | |
11338 * Set to true to disable the floating label. The label disappears when th
e input value is | |
11339 * not null. | |
11340 */ | |
11341 noLabelFloat: { | 7900 noLabelFloat: { |
11342 type: Boolean, | 7901 type: Boolean, |
11343 value: false | 7902 value: false |
11344 }, | 7903 }, |
11345 | 7904 |
11346 /** | |
11347 * Set to true to always float the floating label. | |
11348 */ | |
11349 alwaysFloatLabel: { | 7905 alwaysFloatLabel: { |
11350 type: Boolean, | 7906 type: Boolean, |
11351 value: false | 7907 value: false |
11352 }, | 7908 }, |
11353 | 7909 |
11354 /** | |
11355 * The attribute to listen for value changes on. | |
11356 */ | |
11357 attrForValue: { | 7910 attrForValue: { |
11358 type: String, | 7911 type: String, |
11359 value: 'bind-value' | 7912 value: 'bind-value' |
11360 }, | 7913 }, |
11361 | 7914 |
11362 /** | |
11363 * Set to true to auto-validate the input value when it changes. | |
11364 */ | |
11365 autoValidate: { | 7915 autoValidate: { |
11366 type: Boolean, | 7916 type: Boolean, |
11367 value: false | 7917 value: false |
11368 }, | 7918 }, |
11369 | 7919 |
11370 /** | |
11371 * True if the input is invalid. This property is set automatically when t
he input value | |
11372 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
11373 */ | |
11374 invalid: { | 7920 invalid: { |
11375 observer: '_invalidChanged', | 7921 observer: '_invalidChanged', |
11376 type: Boolean, | 7922 type: Boolean, |
11377 value: false | 7923 value: false |
11378 }, | 7924 }, |
11379 | 7925 |
11380 /** | |
11381 * True if the input has focus. | |
11382 */ | |
11383 focused: { | 7926 focused: { |
11384 readOnly: true, | 7927 readOnly: true, |
11385 type: Boolean, | 7928 type: Boolean, |
11386 value: false, | 7929 value: false, |
11387 notify: true | 7930 notify: true |
11388 }, | 7931 }, |
11389 | 7932 |
11390 _addons: { | 7933 _addons: { |
11391 type: Array | 7934 type: Array |
11392 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
11393 // distributed child is attached, which may occur before configuration f
or this element | |
11394 // in polyfill. | |
11395 }, | 7935 }, |
11396 | 7936 |
11397 _inputHasContent: { | 7937 _inputHasContent: { |
11398 type: Boolean, | 7938 type: Boolean, |
11399 value: false | 7939 value: false |
11400 }, | 7940 }, |
11401 | 7941 |
11402 _inputSelector: { | 7942 _inputSelector: { |
11403 type: String, | 7943 type: String, |
11404 value: 'input,textarea,.paper-input-input' | 7944 value: 'input,textarea,.paper-input-input' |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
11462 this.addEventListener('blur', this._boundOnBlur, true); | 8002 this.addEventListener('blur', this._boundOnBlur, true); |
11463 }, | 8003 }, |
11464 | 8004 |
11465 attached: function() { | 8005 attached: function() { |
11466 if (this.attrForValue) { | 8006 if (this.attrForValue) { |
11467 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | 8007 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); |
11468 } else { | 8008 } else { |
11469 this.addEventListener('input', this._onInput); | 8009 this.addEventListener('input', this._onInput); |
11470 } | 8010 } |
11471 | 8011 |
11472 // Only validate when attached if the input already has a value. | |
11473 if (this._inputElementValue != '') { | 8012 if (this._inputElementValue != '') { |
11474 this._handleValueAndAutoValidate(this._inputElement); | 8013 this._handleValueAndAutoValidate(this._inputElement); |
11475 } else { | 8014 } else { |
11476 this._handleValue(this._inputElement); | 8015 this._handleValue(this._inputElement); |
11477 } | 8016 } |
11478 }, | 8017 }, |
11479 | 8018 |
11480 _onAddonAttached: function(event) { | 8019 _onAddonAttached: function(event) { |
11481 if (!this._addons) { | 8020 if (!this._addons) { |
11482 this._addons = []; | 8021 this._addons = []; |
(...skipping 20 matching lines...) Expand all Loading... |
11503 this._handleValueAndAutoValidate(event.target); | 8042 this._handleValueAndAutoValidate(event.target); |
11504 }, | 8043 }, |
11505 | 8044 |
11506 _onValueChanged: function(event) { | 8045 _onValueChanged: function(event) { |
11507 this._handleValueAndAutoValidate(event.target); | 8046 this._handleValueAndAutoValidate(event.target); |
11508 }, | 8047 }, |
11509 | 8048 |
11510 _handleValue: function(inputElement) { | 8049 _handleValue: function(inputElement) { |
11511 var value = this._inputElementValue; | 8050 var value = this._inputElementValue; |
11512 | 8051 |
11513 // type="number" hack needed because this.value is empty until it's valid | |
11514 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | 8052 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { |
11515 this._inputHasContent = true; | 8053 this._inputHasContent = true; |
11516 } else { | 8054 } else { |
11517 this._inputHasContent = false; | 8055 this._inputHasContent = false; |
11518 } | 8056 } |
11519 | 8057 |
11520 this.updateAddons({ | 8058 this.updateAddons({ |
11521 inputElement: inputElement, | 8059 inputElement: inputElement, |
11522 value: value, | 8060 value: value, |
11523 invalid: this.invalid | 8061 invalid: this.invalid |
11524 }); | 8062 }); |
11525 }, | 8063 }, |
11526 | 8064 |
11527 _handleValueAndAutoValidate: function(inputElement) { | 8065 _handleValueAndAutoValidate: function(inputElement) { |
11528 if (this.autoValidate) { | 8066 if (this.autoValidate) { |
11529 var valid; | 8067 var valid; |
11530 if (inputElement.validate) { | 8068 if (inputElement.validate) { |
11531 valid = inputElement.validate(this._inputElementValue); | 8069 valid = inputElement.validate(this._inputElementValue); |
11532 } else { | 8070 } else { |
11533 valid = inputElement.checkValidity(); | 8071 valid = inputElement.checkValidity(); |
11534 } | 8072 } |
11535 this.invalid = !valid; | 8073 this.invalid = !valid; |
11536 } | 8074 } |
11537 | 8075 |
11538 // Call this last to notify the add-ons. | |
11539 this._handleValue(inputElement); | 8076 this._handleValue(inputElement); |
11540 }, | 8077 }, |
11541 | 8078 |
11542 _onIronInputValidate: function(event) { | 8079 _onIronInputValidate: function(event) { |
11543 this.invalid = this._inputElement.invalid; | 8080 this.invalid = this._inputElement.invalid; |
11544 }, | 8081 }, |
11545 | 8082 |
11546 _invalidChanged: function() { | 8083 _invalidChanged: function() { |
11547 if (this._addons) { | 8084 if (this._addons) { |
11548 this.updateAddons({invalid: this.invalid}); | 8085 this.updateAddons({invalid: this.invalid}); |
11549 } | 8086 } |
11550 }, | 8087 }, |
11551 | 8088 |
11552 /** | |
11553 * Call this to update the state of add-ons. | |
11554 * @param {Object} state Add-on state. | |
11555 */ | |
11556 updateAddons: function(state) { | 8089 updateAddons: function(state) { |
11557 for (var addon, index = 0; addon = this._addons[index]; index++) { | 8090 for (var addon, index = 0; addon = this._addons[index]; index++) { |
11558 addon.update(state); | 8091 addon.update(state); |
11559 } | 8092 } |
11560 }, | 8093 }, |
11561 | 8094 |
11562 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | 8095 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { |
11563 var cls = 'input-content'; | 8096 var cls = 'input-content'; |
11564 if (!noLabelFloat) { | 8097 if (!noLabelFloat) { |
11565 var label = this.querySelector('label'); | 8098 var label = this.querySelector('label'); |
11566 | 8099 |
11567 if (alwaysFloatLabel || _inputHasContent) { | 8100 if (alwaysFloatLabel || _inputHasContent) { |
11568 cls += ' label-is-floating'; | 8101 cls += ' label-is-floating'; |
11569 // If the label is floating, ignore any offsets that may have been | |
11570 // applied from a prefix element. | |
11571 this.$.labelAndInputContainer.style.position = 'static'; | 8102 this.$.labelAndInputContainer.style.position = 'static'; |
11572 | 8103 |
11573 if (invalid) { | 8104 if (invalid) { |
11574 cls += ' is-invalid'; | 8105 cls += ' is-invalid'; |
11575 } else if (focused) { | 8106 } else if (focused) { |
11576 cls += " label-is-highlighted"; | 8107 cls += " label-is-highlighted"; |
11577 } | 8108 } |
11578 } else { | 8109 } else { |
11579 // When the label is not floating, it should overlap the input element
. | |
11580 if (label) { | 8110 if (label) { |
11581 this.$.labelAndInputContainer.style.position = 'relative'; | 8111 this.$.labelAndInputContainer.style.position = 'relative'; |
11582 } | 8112 } |
11583 } | 8113 } |
11584 } else { | 8114 } else { |
11585 if (_inputHasContent) { | 8115 if (_inputHasContent) { |
11586 cls += ' label-is-hidden'; | 8116 cls += ' label-is-hidden'; |
11587 } | 8117 } |
11588 } | 8118 } |
11589 return cls; | 8119 return cls; |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
11640 }); | 8170 }); |
11641 // Copyright 2015 The Chromium Authors. All rights reserved. | 8171 // Copyright 2015 The Chromium Authors. All rights reserved. |
11642 // Use of this source code is governed by a BSD-style license that can be | 8172 // Use of this source code is governed by a BSD-style license that can be |
11643 // found in the LICENSE file. | 8173 // found in the LICENSE file. |
11644 | 8174 |
11645 cr.define('downloads', function() { | 8175 cr.define('downloads', function() { |
11646 var Toolbar = Polymer({ | 8176 var Toolbar = Polymer({ |
11647 is: 'downloads-toolbar', | 8177 is: 'downloads-toolbar', |
11648 | 8178 |
11649 attached: function() { | 8179 attached: function() { |
11650 // isRTL() only works after i18n_template.js runs to set <html dir>. | |
11651 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 8180 this.overflowAlign_ = isRTL() ? 'left' : 'right'; |
11652 }, | 8181 }, |
11653 | 8182 |
11654 properties: { | 8183 properties: { |
11655 downloadsShowing: { | 8184 downloadsShowing: { |
11656 reflectToAttribute: true, | 8185 reflectToAttribute: true, |
11657 type: Boolean, | 8186 type: Boolean, |
11658 value: false, | 8187 value: false, |
11659 observer: 'downloadsShowingChanged_', | 8188 observer: 'downloadsShowingChanged_', |
11660 }, | 8189 }, |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
11698 onClearAllTap_: function() { | 8227 onClearAllTap_: function() { |
11699 assert(this.canClearAll()); | 8228 assert(this.canClearAll()); |
11700 downloads.ActionService.getInstance().clearAll(); | 8229 downloads.ActionService.getInstance().clearAll(); |
11701 }, | 8230 }, |
11702 | 8231 |
11703 /** @private */ | 8232 /** @private */ |
11704 onPaperDropdownClose_: function() { | 8233 onPaperDropdownClose_: function() { |
11705 window.removeEventListener('resize', assert(this.boundClose_)); | 8234 window.removeEventListener('resize', assert(this.boundClose_)); |
11706 }, | 8235 }, |
11707 | 8236 |
11708 /** | |
11709 * @param {!Event} e | |
11710 * @private | |
11711 */ | |
11712 onItemBlur_: function(e) { | 8237 onItemBlur_: function(e) { |
11713 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 8238 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); |
11714 if (menu.items.indexOf(e.relatedTarget) >= 0) | 8239 if (menu.items.indexOf(e.relatedTarget) >= 0) |
11715 return; | 8240 return; |
11716 | 8241 |
11717 this.$.more.restoreFocusOnClose = false; | 8242 this.$.more.restoreFocusOnClose = false; |
11718 this.closeMoreActions_(); | 8243 this.closeMoreActions_(); |
11719 this.$.more.restoreFocusOnClose = true; | 8244 this.$.more.restoreFocusOnClose = true; |
11720 }, | 8245 }, |
11721 | 8246 |
11722 /** @private */ | 8247 /** @private */ |
11723 onPaperDropdownOpen_: function() { | 8248 onPaperDropdownOpen_: function() { |
11724 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 8249 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); |
11725 window.addEventListener('resize', this.boundClose_); | 8250 window.addEventListener('resize', this.boundClose_); |
11726 }, | 8251 }, |
11727 | 8252 |
11728 /** | |
11729 * @param {!CustomEvent} event | |
11730 * @private | |
11731 */ | |
11732 onSearchChanged_: function(event) { | 8253 onSearchChanged_: function(event) { |
11733 downloads.ActionService.getInstance().search( | 8254 downloads.ActionService.getInstance().search( |
11734 /** @type {string} */ (event.detail)); | 8255 /** @type {string} */ (event.detail)); |
11735 this.updateClearAll_(); | 8256 this.updateClearAll_(); |
11736 }, | 8257 }, |
11737 | 8258 |
11738 /** @private */ | 8259 /** @private */ |
11739 onOpenDownloadsFolderTap_: function() { | 8260 onOpenDownloadsFolderTap_: function() { |
11740 downloads.ActionService.getInstance().openDownloadsFolder(); | 8261 downloads.ActionService.getInstance().openDownloadsFolder(); |
11741 }, | 8262 }, |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
11794 if (this.hasDownloads_) { | 8315 if (this.hasDownloads_) { |
11795 this.$['downloads-list'].fire('iron-resize'); | 8316 this.$['downloads-list'].fire('iron-resize'); |
11796 } else { | 8317 } else { |
11797 var isSearching = downloads.ActionService.getInstance().isSearching(); | 8318 var isSearching = downloads.ActionService.getInstance().isSearching(); |
11798 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 8319 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; |
11799 this.$['no-downloads'].querySelector('span').textContent = | 8320 this.$['no-downloads'].querySelector('span').textContent = |
11800 loadTimeData.getString(messageToShow); | 8321 loadTimeData.getString(messageToShow); |
11801 } | 8322 } |
11802 }, | 8323 }, |
11803 | 8324 |
11804 /** | |
11805 * @param {number} index | |
11806 * @param {!Array<!downloads.Data>} list | |
11807 * @private | |
11808 */ | |
11809 insertItems_: function(index, list) { | 8325 insertItems_: function(index, list) { |
11810 this.splice.apply(this, ['items_', index, 0].concat(list)); | 8326 this.splice.apply(this, ['items_', index, 0].concat(list)); |
11811 this.updateHideDates_(index, index + list.length); | 8327 this.updateHideDates_(index, index + list.length); |
11812 this.removeAttribute('loading'); | 8328 this.removeAttribute('loading'); |
11813 }, | 8329 }, |
11814 | 8330 |
11815 /** @private */ | 8331 /** @private */ |
11816 itemsChanged_: function() { | 8332 itemsChanged_: function() { |
11817 this.hasDownloads_ = this.items_.length > 0; | 8333 this.hasDownloads_ = this.items_.length > 0; |
11818 }, | 8334 }, |
11819 | 8335 |
11820 /** | |
11821 * @param {Event} e | |
11822 * @private | |
11823 */ | |
11824 onCanExecute_: function(e) { | 8336 onCanExecute_: function(e) { |
11825 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 8337 e = /** @type {cr.ui.CanExecuteEvent} */(e); |
11826 switch (e.command.id) { | 8338 switch (e.command.id) { |
11827 case 'undo-command': | 8339 case 'undo-command': |
11828 e.canExecute = this.$.toolbar.canUndo(); | 8340 e.canExecute = this.$.toolbar.canUndo(); |
11829 break; | 8341 break; |
11830 case 'clear-all-command': | 8342 case 'clear-all-command': |
11831 e.canExecute = this.$.toolbar.canClearAll(); | 8343 e.canExecute = this.$.toolbar.canClearAll(); |
11832 break; | 8344 break; |
11833 case 'find-command': | 8345 case 'find-command': |
11834 e.canExecute = true; | 8346 e.canExecute = true; |
11835 break; | 8347 break; |
11836 } | 8348 } |
11837 }, | 8349 }, |
11838 | 8350 |
11839 /** | |
11840 * @param {Event} e | |
11841 * @private | |
11842 */ | |
11843 onCommand_: function(e) { | 8351 onCommand_: function(e) { |
11844 if (e.command.id == 'clear-all-command') | 8352 if (e.command.id == 'clear-all-command') |
11845 downloads.ActionService.getInstance().clearAll(); | 8353 downloads.ActionService.getInstance().clearAll(); |
11846 else if (e.command.id == 'undo-command') | 8354 else if (e.command.id == 'undo-command') |
11847 downloads.ActionService.getInstance().undo(); | 8355 downloads.ActionService.getInstance().undo(); |
11848 else if (e.command.id == 'find-command') | 8356 else if (e.command.id == 'find-command') |
11849 this.$.toolbar.onFindCommand(); | 8357 this.$.toolbar.onFindCommand(); |
11850 }, | 8358 }, |
11851 | 8359 |
11852 /** @private */ | 8360 /** @private */ |
11853 onListScroll_: function() { | 8361 onListScroll_: function() { |
11854 var list = this.$['downloads-list']; | 8362 var list = this.$['downloads-list']; |
11855 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 8363 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { |
11856 // Approaching the end of the scrollback. Attempt to load more items. | |
11857 downloads.ActionService.getInstance().loadMore(); | 8364 downloads.ActionService.getInstance().loadMore(); |
11858 } | 8365 } |
11859 }, | 8366 }, |
11860 | 8367 |
11861 /** @private */ | 8368 /** @private */ |
11862 onLoad_: function() { | 8369 onLoad_: function() { |
11863 cr.ui.decorate('command', cr.ui.Command); | 8370 cr.ui.decorate('command', cr.ui.Command); |
11864 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 8371 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
11865 document.addEventListener('command', this.onCommand_.bind(this)); | 8372 document.addEventListener('command', this.onCommand_.bind(this)); |
11866 | 8373 |
11867 downloads.ActionService.getInstance().loadMore(); | 8374 downloads.ActionService.getInstance().loadMore(); |
11868 }, | 8375 }, |
11869 | 8376 |
11870 /** | |
11871 * @param {number} index | |
11872 * @private | |
11873 */ | |
11874 removeItem_: function(index) { | 8377 removeItem_: function(index) { |
11875 this.splice('items_', index, 1); | 8378 this.splice('items_', index, 1); |
11876 this.updateHideDates_(index, index); | 8379 this.updateHideDates_(index, index); |
11877 this.onListScroll_(); | 8380 this.onListScroll_(); |
11878 }, | 8381 }, |
11879 | 8382 |
11880 /** | |
11881 * @param {number} start | |
11882 * @param {number} end | |
11883 * @private | |
11884 */ | |
11885 updateHideDates_: function(start, end) { | 8383 updateHideDates_: function(start, end) { |
11886 for (var i = start; i <= end; ++i) { | 8384 for (var i = start; i <= end; ++i) { |
11887 var current = this.items_[i]; | 8385 var current = this.items_[i]; |
11888 if (!current) | 8386 if (!current) |
11889 continue; | 8387 continue; |
11890 var prev = this.items_[i - 1]; | 8388 var prev = this.items_[i - 1]; |
11891 current.hideDate = !!prev && prev.date_string == current.date_string; | 8389 current.hideDate = !!prev && prev.date_string == current.date_string; |
11892 } | 8390 } |
11893 }, | 8391 }, |
11894 | 8392 |
11895 /** | |
11896 * @param {number} index | |
11897 * @param {!downloads.Data} data | |
11898 * @private | |
11899 */ | |
11900 updateItem_: function(index, data) { | 8393 updateItem_: function(index, data) { |
11901 this.set('items_.' + index, data); | 8394 this.set('items_.' + index, data); |
11902 this.updateHideDates_(index, index); | 8395 this.updateHideDates_(index, index); |
11903 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | 8396 var list = /** @type {!IronListElement} */(this.$['downloads-list']); |
11904 list.updateSizeForItem(index); | 8397 list.updateSizeForItem(index); |
11905 }, | 8398 }, |
11906 }); | 8399 }); |
11907 | 8400 |
11908 Manager.clearAll = function() { | 8401 Manager.clearAll = function() { |
11909 Manager.get().clearAll_(); | 8402 Manager.get().clearAll_(); |
(...skipping 21 matching lines...) Expand all Loading... |
11931 Manager.get().updateItem_(index, data); | 8424 Manager.get().updateItem_(index, data); |
11932 }; | 8425 }; |
11933 | 8426 |
11934 return {Manager: Manager}; | 8427 return {Manager: Manager}; |
11935 }); | 8428 }); |
11936 // Copyright 2015 The Chromium Authors. All rights reserved. | 8429 // Copyright 2015 The Chromium Authors. All rights reserved. |
11937 // Use of this source code is governed by a BSD-style license that can be | 8430 // Use of this source code is governed by a BSD-style license that can be |
11938 // found in the LICENSE file. | 8431 // found in the LICENSE file. |
11939 | 8432 |
11940 window.addEventListener('load', downloads.Manager.onLoad); | 8433 window.addEventListener('load', downloads.Manager.onLoad); |
OLD | NEW |