| OLD | NEW |
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | |
| 5 /** | |
| 6 * @fileoverview Assertion support. | |
| 7 */ | |
| 8 | |
| 9 /** | |
| 10 * Verify |condition| is truthy and return |condition| if so. | |
| 11 * @template T | |
| 12 * @param {T} condition A condition to check for truthiness. Note that this | |
| 13 * may be used to test whether a value is defined or not, and we don't want | |
| 14 * to force a cast to Boolean. | |
| 15 * @param {string=} opt_message A message to show on failure. | |
| 16 * @return {T} A non-null |condition|. | |
| 17 */ | |
| 18 function assert(condition, opt_message) { | 4 function assert(condition, opt_message) { |
| 19 if (!condition) { | 5 if (!condition) { |
| 20 var message = 'Assertion failed'; | 6 var message = 'Assertion failed'; |
| 21 if (opt_message) | 7 if (opt_message) message = message + ': ' + opt_message; |
| 22 message = message + ': ' + opt_message; | |
| 23 var error = new Error(message); | 8 var error = new Error(message); |
| 24 var global = function() { return this; }(); | 9 var global = function() { |
| 25 if (global.traceAssertionsForTesting) | 10 return this; |
| 26 console.warn(error.stack); | 11 }(); |
| 12 if (global.traceAssertionsForTesting) console.warn(error.stack); |
| 27 throw error; | 13 throw error; |
| 28 } | 14 } |
| 29 return condition; | 15 return condition; |
| 30 } | 16 } |
| 31 | 17 |
| 32 /** | |
| 33 * Call this from places in the code that should never be reached. | |
| 34 * | |
| 35 * For example, handling all the values of enum with a switch() like this: | |
| 36 * | |
| 37 * function getValueFromEnum(enum) { | |
| 38 * switch (enum) { | |
| 39 * case ENUM_FIRST_OF_TWO: | |
| 40 * return first | |
| 41 * case ENUM_LAST_OF_TWO: | |
| 42 * return last; | |
| 43 * } | |
| 44 * assertNotReached(); | |
| 45 * return document; | |
| 46 * } | |
| 47 * | |
| 48 * This code should only be hit in the case of serious programmer error or | |
| 49 * unexpected input. | |
| 50 * | |
| 51 * @param {string=} opt_message A message to show when this is hit. | |
| 52 */ | |
| 53 function assertNotReached(opt_message) { | 18 function assertNotReached(opt_message) { |
| 54 assert(false, opt_message || 'Unreachable code hit'); | 19 assert(false, opt_message || 'Unreachable code hit'); |
| 55 } | 20 } |
| 56 | 21 |
| 57 /** | |
| 58 * @param {*} value The value to check. | |
| 59 * @param {function(new: T, ...)} type A user-defined constructor. | |
| 60 * @param {string=} opt_message A message to show when this is hit. | |
| 61 * @return {T} | |
| 62 * @template T | |
| 63 */ | |
| 64 function assertInstanceof(value, type, opt_message) { | 22 function assertInstanceof(value, type, opt_message) { |
| 65 // We don't use assert immediately here so that we avoid constructing an error | |
| 66 // message if we don't have to. | |
| 67 if (!(value instanceof type)) { | 23 if (!(value instanceof type)) { |
| 68 assertNotReached(opt_message || 'Value ' + value + | 24 assertNotReached(opt_message || 'Value ' + value + ' is not a[n] ' + (type.n
ame || typeof type)); |
| 69 ' is not a[n] ' + (type.name || typeof type)); | |
| 70 } | 25 } |
| 71 return value; | 26 return value; |
| 72 }; | 27 } |
| 28 |
| 73 // Copyright 2016 The Chromium Authors. All rights reserved. | 29 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 74 // Use of this source code is governed by a BSD-style license that can be | 30 // Use of this source code is governed by a BSD-style license that can be |
| 75 // found in the LICENSE file. | 31 // found in the LICENSE file. |
| 76 | |
| 77 /** | |
| 78 * @fileoverview PromiseResolver is a helper class that allows creating a | |
| 79 * Promise that will be fulfilled (resolved or rejected) some time later. | |
| 80 * | |
| 81 * Example: | |
| 82 * var resolver = new PromiseResolver(); | |
| 83 * resolver.promise.then(function(result) { | |
| 84 * console.log('resolved with', result); | |
| 85 * }); | |
| 86 * ... | |
| 87 * ... | |
| 88 * resolver.resolve({hello: 'world'}); | |
| 89 */ | |
| 90 | |
| 91 /** | |
| 92 * @constructor @struct | |
| 93 * @template T | |
| 94 */ | |
| 95 function PromiseResolver() { | 32 function PromiseResolver() { |
| 96 /** @private {function(T=): void} */ | |
| 97 this.resolve_; | 33 this.resolve_; |
| 98 | |
| 99 /** @private {function(*=): void} */ | |
| 100 this.reject_; | 34 this.reject_; |
| 101 | |
| 102 /** @private {!Promise<T>} */ | |
| 103 this.promise_ = new Promise(function(resolve, reject) { | 35 this.promise_ = new Promise(function(resolve, reject) { |
| 104 this.resolve_ = resolve; | 36 this.resolve_ = resolve; |
| 105 this.reject_ = reject; | 37 this.reject_ = reject; |
| 106 }.bind(this)); | 38 }.bind(this)); |
| 107 } | 39 } |
| 108 | 40 |
| 109 PromiseResolver.prototype = { | 41 PromiseResolver.prototype = { |
| 110 /** @return {!Promise<T>} */ | 42 get promise() { |
| 111 get promise() { return this.promise_; }, | 43 return this.promise_; |
| 112 set promise(p) { assertNotReached(); }, | 44 }, |
| 45 set promise(p) { |
| 46 assertNotReached(); |
| 47 }, |
| 48 get resolve() { |
| 49 return this.resolve_; |
| 50 }, |
| 51 set resolve(r) { |
| 52 assertNotReached(); |
| 53 }, |
| 54 get reject() { |
| 55 return this.reject_; |
| 56 }, |
| 57 set reject(s) { |
| 58 assertNotReached(); |
| 59 } |
| 60 }; |
| 113 | 61 |
| 114 /** @return {function(T=): void} */ | |
| 115 get resolve() { return this.resolve_; }, | |
| 116 set resolve(r) { assertNotReached(); }, | |
| 117 | |
| 118 /** @return {function(*=): void} */ | |
| 119 get reject() { return this.reject_; }, | |
| 120 set reject(s) { assertNotReached(); }, | |
| 121 }; | |
| 122 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 62 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 123 // Use of this source code is governed by a BSD-style license that can be | 63 // Use of this source code is governed by a BSD-style license that can be |
| 124 // found in the LICENSE file. | 64 // found in the LICENSE file. |
| 125 | |
| 126 /** | |
| 127 * The global object. | |
| 128 * @type {!Object} | |
| 129 * @const | |
| 130 */ | |
| 131 var global = this; | 65 var global = this; |
| 132 | 66 |
| 133 /** @typedef {{eventName: string, uid: number}} */ | |
| 134 var WebUIListener; | 67 var WebUIListener; |
| 135 | 68 |
| 136 /** Platform, package, object property, and Event support. **/ | |
| 137 var cr = cr || function() { | 69 var cr = cr || function() { |
| 138 'use strict'; | 70 'use strict'; |
| 139 | |
| 140 /** | |
| 141 * Builds an object structure for the provided namespace path, | |
| 142 * ensuring that names that already exist are not overwritten. For | |
| 143 * example: | |
| 144 * "a.b.c" -> a = {};a.b={};a.b.c={}; | |
| 145 * @param {string} name Name of the object that this file defines. | |
| 146 * @param {*=} opt_object The object to expose at the end of the path. | |
| 147 * @param {Object=} opt_objectToExportTo The object to add the path to; | |
| 148 * default is {@code global}. | |
| 149 * @return {!Object} The last object exported (i.e. exportPath('cr.ui') | |
| 150 * returns a reference to the ui property of window.cr). | |
| 151 * @private | |
| 152 */ | |
| 153 function exportPath(name, opt_object, opt_objectToExportTo) { | 71 function exportPath(name, opt_object, opt_objectToExportTo) { |
| 154 var parts = name.split('.'); | 72 var parts = name.split('.'); |
| 155 var cur = opt_objectToExportTo || global; | 73 var cur = opt_objectToExportTo || global; |
| 156 | 74 for (var part; parts.length && (part = parts.shift()); ) { |
| 157 for (var part; parts.length && (part = parts.shift());) { | |
| 158 if (!parts.length && opt_object !== undefined) { | 75 if (!parts.length && opt_object !== undefined) { |
| 159 // last part and we have an object; use it | |
| 160 cur[part] = opt_object; | 76 cur[part] = opt_object; |
| 161 } else if (part in cur) { | 77 } else if (part in cur) { |
| 162 cur = cur[part]; | 78 cur = cur[part]; |
| 163 } else { | 79 } else { |
| 164 cur = cur[part] = {}; | 80 cur = cur[part] = {}; |
| 165 } | 81 } |
| 166 } | 82 } |
| 167 return cur; | 83 return cur; |
| 168 } | 84 } |
| 169 | |
| 170 /** | |
| 171 * Fires a property change event on the target. | |
| 172 * @param {EventTarget} target The target to dispatch the event on. | |
| 173 * @param {string} propertyName The name of the property that changed. | |
| 174 * @param {*} newValue The new value for the property. | |
| 175 * @param {*} oldValue The old value for the property. | |
| 176 */ | |
| 177 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { | 85 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
| 178 var e = new Event(propertyName + 'Change'); | 86 var e = new Event(propertyName + 'Change'); |
| 179 e.propertyName = propertyName; | 87 e.propertyName = propertyName; |
| 180 e.newValue = newValue; | 88 e.newValue = newValue; |
| 181 e.oldValue = oldValue; | 89 e.oldValue = oldValue; |
| 182 target.dispatchEvent(e); | 90 target.dispatchEvent(e); |
| 183 } | 91 } |
| 184 | |
| 185 /** | |
| 186 * Converts a camelCase javascript property name to a hyphenated-lower-case | |
| 187 * attribute name. | |
| 188 * @param {string} jsName The javascript camelCase property name. | |
| 189 * @return {string} The equivalent hyphenated-lower-case attribute name. | |
| 190 */ | |
| 191 function getAttributeName(jsName) { | 92 function getAttributeName(jsName) { |
| 192 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); | 93 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
| 193 } | 94 } |
| 194 | |
| 195 /** | |
| 196 * The kind of property to define in {@code defineProperty}. | |
| 197 * @enum {string} | |
| 198 * @const | |
| 199 */ | |
| 200 var PropertyKind = { | 95 var PropertyKind = { |
| 201 /** | |
| 202 * Plain old JS property where the backing data is stored as a "private" | |
| 203 * field on the object. | |
| 204 * Use for properties of any type. Type will not be checked. | |
| 205 */ | |
| 206 JS: 'js', | 96 JS: 'js', |
| 207 | |
| 208 /** | |
| 209 * The property backing data is stored as an attribute on an element. | |
| 210 * Use only for properties of type {string}. | |
| 211 */ | |
| 212 ATTR: 'attr', | 97 ATTR: 'attr', |
| 213 | |
| 214 /** | |
| 215 * The property backing data is stored as an attribute on an element. If the | |
| 216 * element has the attribute then the value is true. | |
| 217 * Use only for properties of type {boolean}. | |
| 218 */ | |
| 219 BOOL_ATTR: 'boolAttr' | 98 BOOL_ATTR: 'boolAttr' |
| 220 }; | 99 }; |
| 221 | |
| 222 /** | |
| 223 * Helper function for defineProperty that returns the getter to use for the | |
| 224 * property. | |
| 225 * @param {string} name The name of the property. | |
| 226 * @param {PropertyKind} kind The kind of the property. | |
| 227 * @return {function():*} The getter for the property. | |
| 228 */ | |
| 229 function getGetter(name, kind) { | 100 function getGetter(name, kind) { |
| 230 switch (kind) { | 101 switch (kind) { |
| 231 case PropertyKind.JS: | 102 case PropertyKind.JS: |
| 232 var privateName = name + '_'; | 103 var privateName = name + '_'; |
| 233 return function() { | 104 return function() { |
| 234 return this[privateName]; | 105 return this[privateName]; |
| 235 }; | 106 }; |
| 236 case PropertyKind.ATTR: | 107 |
| 237 var attributeName = getAttributeName(name); | 108 case PropertyKind.ATTR: |
| 238 return function() { | 109 var attributeName = getAttributeName(name); |
| 239 return this.getAttribute(attributeName); | 110 return function() { |
| 240 }; | 111 return this.getAttribute(attributeName); |
| 241 case PropertyKind.BOOL_ATTR: | 112 }; |
| 242 var attributeName = getAttributeName(name); | 113 |
| 243 return function() { | 114 case PropertyKind.BOOL_ATTR: |
| 244 return this.hasAttribute(attributeName); | 115 var attributeName = getAttributeName(name); |
| 245 }; | 116 return function() { |
| 117 return this.hasAttribute(attributeName); |
| 118 }; |
| 246 } | 119 } |
| 247 | |
| 248 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
| 249 // the browser/unit tests to preprocess this file through grit. | |
| 250 throw 'not reached'; | 120 throw 'not reached'; |
| 251 } | 121 } |
| 252 | |
| 253 /** | |
| 254 * Helper function for defineProperty that returns the setter of the right | |
| 255 * kind. | |
| 256 * @param {string} name The name of the property we are defining the setter | |
| 257 * for. | |
| 258 * @param {PropertyKind} kind The kind of property we are getting the | |
| 259 * setter for. | |
| 260 * @param {function(*, *):void=} opt_setHook A function to run after the | |
| 261 * property is set, but before the propertyChange event is fired. | |
| 262 * @return {function(*):void} The function to use as a setter. | |
| 263 */ | |
| 264 function getSetter(name, kind, opt_setHook) { | 122 function getSetter(name, kind, opt_setHook) { |
| 265 switch (kind) { | 123 switch (kind) { |
| 266 case PropertyKind.JS: | 124 case PropertyKind.JS: |
| 267 var privateName = name + '_'; | 125 var privateName = name + '_'; |
| 268 return function(value) { | 126 return function(value) { |
| 269 var oldValue = this[name]; | 127 var oldValue = this[name]; |
| 270 if (value !== oldValue) { | 128 if (value !== oldValue) { |
| 271 this[privateName] = value; | 129 this[privateName] = value; |
| 272 if (opt_setHook) | 130 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 273 opt_setHook.call(this, value, oldValue); | 131 dispatchPropertyChange(this, name, value, oldValue); |
| 274 dispatchPropertyChange(this, name, value, oldValue); | 132 } |
| 275 } | 133 }; |
| 276 }; | |
| 277 | 134 |
| 278 case PropertyKind.ATTR: | 135 case PropertyKind.ATTR: |
| 279 var attributeName = getAttributeName(name); | 136 var attributeName = getAttributeName(name); |
| 280 return function(value) { | 137 return function(value) { |
| 281 var oldValue = this[name]; | 138 var oldValue = this[name]; |
| 282 if (value !== oldValue) { | 139 if (value !== oldValue) { |
| 283 if (value == undefined) | 140 if (value == undefined) this.removeAttribute(attributeName); else this
.setAttribute(attributeName, value); |
| 284 this.removeAttribute(attributeName); | 141 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 285 else | 142 dispatchPropertyChange(this, name, value, oldValue); |
| 286 this.setAttribute(attributeName, value); | 143 } |
| 287 if (opt_setHook) | 144 }; |
| 288 opt_setHook.call(this, value, oldValue); | |
| 289 dispatchPropertyChange(this, name, value, oldValue); | |
| 290 } | |
| 291 }; | |
| 292 | 145 |
| 293 case PropertyKind.BOOL_ATTR: | 146 case PropertyKind.BOOL_ATTR: |
| 294 var attributeName = getAttributeName(name); | 147 var attributeName = getAttributeName(name); |
| 295 return function(value) { | 148 return function(value) { |
| 296 var oldValue = this[name]; | 149 var oldValue = this[name]; |
| 297 if (value !== oldValue) { | 150 if (value !== oldValue) { |
| 298 if (value) | 151 if (value) this.setAttribute(attributeName, name); else this.removeAtt
ribute(attributeName); |
| 299 this.setAttribute(attributeName, name); | 152 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 300 else | 153 dispatchPropertyChange(this, name, value, oldValue); |
| 301 this.removeAttribute(attributeName); | 154 } |
| 302 if (opt_setHook) | 155 }; |
| 303 opt_setHook.call(this, value, oldValue); | |
| 304 dispatchPropertyChange(this, name, value, oldValue); | |
| 305 } | |
| 306 }; | |
| 307 } | 156 } |
| 308 | |
| 309 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
| 310 // the browser/unit tests to preprocess this file through grit. | |
| 311 throw 'not reached'; | 157 throw 'not reached'; |
| 312 } | 158 } |
| 313 | |
| 314 /** | |
| 315 * Defines a property on an object. When the setter changes the value a | |
| 316 * property change event with the type {@code name + 'Change'} is fired. | |
| 317 * @param {!Object} obj The object to define the property for. | |
| 318 * @param {string} name The name of the property. | |
| 319 * @param {PropertyKind=} opt_kind What kind of underlying storage to use. | |
| 320 * @param {function(*, *):void=} opt_setHook A function to run after the | |
| 321 * property is set, but before the propertyChange event is fired. | |
| 322 */ | |
| 323 function defineProperty(obj, name, opt_kind, opt_setHook) { | 159 function defineProperty(obj, name, opt_kind, opt_setHook) { |
| 324 if (typeof obj == 'function') | 160 if (typeof obj == 'function') obj = obj.prototype; |
| 325 obj = obj.prototype; | 161 var kind = opt_kind || PropertyKind.JS; |
| 326 | 162 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name,
kind)); |
| 327 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); | 163 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name,
kind, opt_setHook)); |
| 328 | |
| 329 if (!obj.__lookupGetter__(name)) | |
| 330 obj.__defineGetter__(name, getGetter(name, kind)); | |
| 331 | |
| 332 if (!obj.__lookupSetter__(name)) | |
| 333 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); | |
| 334 } | 164 } |
| 335 | |
| 336 /** | |
| 337 * Counter for use with createUid | |
| 338 */ | |
| 339 var uidCounter = 1; | 165 var uidCounter = 1; |
| 340 | |
| 341 /** | |
| 342 * @return {number} A new unique ID. | |
| 343 */ | |
| 344 function createUid() { | 166 function createUid() { |
| 345 return uidCounter++; | 167 return uidCounter++; |
| 346 } | 168 } |
| 347 | |
| 348 /** | |
| 349 * Returns a unique ID for the item. This mutates the item so it needs to be | |
| 350 * an object | |
| 351 * @param {!Object} item The item to get the unique ID for. | |
| 352 * @return {number} The unique ID for the item. | |
| 353 */ | |
| 354 function getUid(item) { | 169 function getUid(item) { |
| 355 if (item.hasOwnProperty('uid')) | 170 if (item.hasOwnProperty('uid')) return item.uid; |
| 356 return item.uid; | |
| 357 return item.uid = createUid(); | 171 return item.uid = createUid(); |
| 358 } | 172 } |
| 359 | |
| 360 /** | |
| 361 * Dispatches a simple event on an event target. | |
| 362 * @param {!EventTarget} target The event target to dispatch the event on. | |
| 363 * @param {string} type The type of the event. | |
| 364 * @param {boolean=} opt_bubbles Whether the event bubbles or not. | |
| 365 * @param {boolean=} opt_cancelable Whether the default action of the event | |
| 366 * can be prevented. Default is true. | |
| 367 * @return {boolean} If any of the listeners called {@code preventDefault} | |
| 368 * during the dispatch this will return false. | |
| 369 */ | |
| 370 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { | 173 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
| 371 var e = new Event(type, { | 174 var e = new Event(type, { |
| 372 bubbles: opt_bubbles, | 175 bubbles: opt_bubbles, |
| 373 cancelable: opt_cancelable === undefined || opt_cancelable | 176 cancelable: opt_cancelable === undefined || opt_cancelable |
| 374 }); | 177 }); |
| 375 return target.dispatchEvent(e); | 178 return target.dispatchEvent(e); |
| 376 } | 179 } |
| 377 | |
| 378 /** | |
| 379 * Calls |fun| and adds all the fields of the returned object to the object | |
| 380 * named by |name|. For example, cr.define('cr.ui', function() { | |
| 381 * function List() { | |
| 382 * ... | |
| 383 * } | |
| 384 * function ListItem() { | |
| 385 * ... | |
| 386 * } | |
| 387 * return { | |
| 388 * List: List, | |
| 389 * ListItem: ListItem, | |
| 390 * }; | |
| 391 * }); | |
| 392 * defines the functions cr.ui.List and cr.ui.ListItem. | |
| 393 * @param {string} name The name of the object that we are adding fields to. | |
| 394 * @param {!Function} fun The function that will return an object containing | |
| 395 * the names and values of the new fields. | |
| 396 */ | |
| 397 function define(name, fun) { | 180 function define(name, fun) { |
| 398 var obj = exportPath(name); | 181 var obj = exportPath(name); |
| 399 var exports = fun(); | 182 var exports = fun(); |
| 400 for (var propertyName in exports) { | 183 for (var propertyName in exports) { |
| 401 // Maybe we should check the prototype chain here? The current usage | 184 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property
Name); |
| 402 // pattern is always using an object literal so we only care about own | 185 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD
escriptor); |
| 403 // properties. | |
| 404 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, | |
| 405 propertyName); | |
| 406 if (propertyDescriptor) | |
| 407 Object.defineProperty(obj, propertyName, propertyDescriptor); | |
| 408 } | 186 } |
| 409 } | 187 } |
| 410 | |
| 411 /** | |
| 412 * Adds a {@code getInstance} static method that always return the same | |
| 413 * instance object. | |
| 414 * @param {!Function} ctor The constructor for the class to add the static | |
| 415 * method to. | |
| 416 */ | |
| 417 function addSingletonGetter(ctor) { | 188 function addSingletonGetter(ctor) { |
| 418 ctor.getInstance = function() { | 189 ctor.getInstance = function() { |
| 419 return ctor.instance_ || (ctor.instance_ = new ctor()); | 190 return ctor.instance_ || (ctor.instance_ = new ctor()); |
| 420 }; | 191 }; |
| 421 } | 192 } |
| 422 | |
| 423 /** | |
| 424 * Forwards public APIs to private implementations. | |
| 425 * @param {Function} ctor Constructor that have private implementations in its | |
| 426 * prototype. | |
| 427 * @param {Array<string>} methods List of public method names that have their | |
| 428 * underscored counterparts in constructor's prototype. | |
| 429 * @param {string=} opt_target Selector for target node. | |
| 430 */ | |
| 431 function makePublic(ctor, methods, opt_target) { | 193 function makePublic(ctor, methods, opt_target) { |
| 432 methods.forEach(function(method) { | 194 methods.forEach(function(method) { |
| 433 ctor[method] = function() { | 195 ctor[method] = function() { |
| 434 var target = opt_target ? document.getElementById(opt_target) : | 196 var target = opt_target ? document.getElementById(opt_target) : ctor.get
Instance(); |
| 435 ctor.getInstance(); | |
| 436 return target[method + '_'].apply(target, arguments); | 197 return target[method + '_'].apply(target, arguments); |
| 437 }; | 198 }; |
| 438 }); | 199 }); |
| 439 } | 200 } |
| 440 | |
| 441 /** | |
| 442 * The mapping used by the sendWithPromise mechanism to tie the Promise | |
| 443 * returned to callers with the corresponding WebUI response. The mapping is | |
| 444 * from ID to the PromiseResolver helper; the ID is generated by | |
| 445 * sendWithPromise and is unique across all invocations of said method. | |
| 446 * @type {!Object<!PromiseResolver>} | |
| 447 */ | |
| 448 var chromeSendResolverMap = {}; | 201 var chromeSendResolverMap = {}; |
| 449 | |
| 450 /** | |
| 451 * The named method the WebUI handler calls directly in response to a | |
| 452 * chrome.send call that expects a response. The handler requires no knowledge | |
| 453 * of the specific name of this method, as the name is passed to the handler | |
| 454 * as the first argument in the arguments list of chrome.send. The handler | |
| 455 * must pass the ID, also sent via the chrome.send arguments list, as the | |
| 456 * first argument of the JS invocation; additionally, the handler may | |
| 457 * supply any number of other arguments that will be included in the response. | |
| 458 * @param {string} id The unique ID identifying the Promise this response is | |
| 459 * tied to. | |
| 460 * @param {boolean} isSuccess Whether the request was successful. | |
| 461 * @param {*} response The response as sent from C++. | |
| 462 */ | |
| 463 function webUIResponse(id, isSuccess, response) { | 202 function webUIResponse(id, isSuccess, response) { |
| 464 var resolver = chromeSendResolverMap[id]; | 203 var resolver = chromeSendResolverMap[id]; |
| 465 delete chromeSendResolverMap[id]; | 204 delete chromeSendResolverMap[id]; |
| 466 | 205 if (isSuccess) resolver.resolve(response); else resolver.reject(response); |
| 467 if (isSuccess) | |
| 468 resolver.resolve(response); | |
| 469 else | |
| 470 resolver.reject(response); | |
| 471 } | 206 } |
| 472 | |
| 473 /** | |
| 474 * A variation of chrome.send, suitable for messages that expect a single | |
| 475 * response from C++. | |
| 476 * @param {string} methodName The name of the WebUI handler API. | |
| 477 * @param {...*} var_args Varibale number of arguments to be forwarded to the | |
| 478 * C++ call. | |
| 479 * @return {!Promise} | |
| 480 */ | |
| 481 function sendWithPromise(methodName, var_args) { | 207 function sendWithPromise(methodName, var_args) { |
| 482 var args = Array.prototype.slice.call(arguments, 1); | 208 var args = Array.prototype.slice.call(arguments, 1); |
| 483 var promiseResolver = new PromiseResolver(); | 209 var promiseResolver = new PromiseResolver(); |
| 484 var id = methodName + '_' + createUid(); | 210 var id = methodName + '_' + createUid(); |
| 485 chromeSendResolverMap[id] = promiseResolver; | 211 chromeSendResolverMap[id] = promiseResolver; |
| 486 chrome.send(methodName, [id].concat(args)); | 212 chrome.send(methodName, [ id ].concat(args)); |
| 487 return promiseResolver.promise; | 213 return promiseResolver.promise; |
| 488 } | 214 } |
| 489 | |
| 490 /** | |
| 491 * A map of maps associating event names with listeners. The 2nd level map | |
| 492 * associates a listener ID with the callback function, such that individual | |
| 493 * listeners can be removed from an event without affecting other listeners of | |
| 494 * the same event. | |
| 495 * @type {!Object<!Object<!Function>>} | |
| 496 */ | |
| 497 var webUIListenerMap = {}; | 215 var webUIListenerMap = {}; |
| 498 | |
| 499 /** | |
| 500 * The named method the WebUI handler calls directly when an event occurs. | |
| 501 * The WebUI handler must supply the name of the event as the first argument | |
| 502 * of the JS invocation; additionally, the handler may supply any number of | |
| 503 * other arguments that will be forwarded to the listener callbacks. | |
| 504 * @param {string} event The name of the event that has occurred. | |
| 505 * @param {...*} var_args Additional arguments passed from C++. | |
| 506 */ | |
| 507 function webUIListenerCallback(event, var_args) { | 216 function webUIListenerCallback(event, var_args) { |
| 508 var eventListenersMap = webUIListenerMap[event]; | 217 var eventListenersMap = webUIListenerMap[event]; |
| 509 if (!eventListenersMap) { | 218 if (!eventListenersMap) { |
| 510 // C++ event sent for an event that has no listeners. | |
| 511 // TODO(dpapad): Should a warning be displayed here? | |
| 512 return; | 219 return; |
| 513 } | 220 } |
| 514 | |
| 515 var args = Array.prototype.slice.call(arguments, 1); | 221 var args = Array.prototype.slice.call(arguments, 1); |
| 516 for (var listenerId in eventListenersMap) { | 222 for (var listenerId in eventListenersMap) { |
| 517 eventListenersMap[listenerId].apply(null, args); | 223 eventListenersMap[listenerId].apply(null, args); |
| 518 } | 224 } |
| 519 } | 225 } |
| 520 | |
| 521 /** | |
| 522 * Registers a listener for an event fired from WebUI handlers. Any number of | |
| 523 * listeners may register for a single event. | |
| 524 * @param {string} eventName The event to listen to. | |
| 525 * @param {!Function} callback The callback run when the event is fired. | |
| 526 * @return {!WebUIListener} An object to be used for removing a listener via | |
| 527 * cr.removeWebUIListener. Should be treated as read-only. | |
| 528 */ | |
| 529 function addWebUIListener(eventName, callback) { | 226 function addWebUIListener(eventName, callback) { |
| 530 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; | 227 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
| 531 var uid = createUid(); | 228 var uid = createUid(); |
| 532 webUIListenerMap[eventName][uid] = callback; | 229 webUIListenerMap[eventName][uid] = callback; |
| 533 return {eventName: eventName, uid: uid}; | 230 return { |
| 231 eventName: eventName, |
| 232 uid: uid |
| 233 }; |
| 534 } | 234 } |
| 535 | |
| 536 /** | |
| 537 * Removes a listener. Does nothing if the specified listener is not found. | |
| 538 * @param {!WebUIListener} listener The listener to be removed (as returned by | |
| 539 * addWebUIListener). | |
| 540 * @return {boolean} Whether the given listener was found and actually | |
| 541 * removed. | |
| 542 */ | |
| 543 function removeWebUIListener(listener) { | 235 function removeWebUIListener(listener) { |
| 544 var listenerExists = webUIListenerMap[listener.eventName] && | 236 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa
p[listener.eventName][listener.uid]; |
| 545 webUIListenerMap[listener.eventName][listener.uid]; | |
| 546 if (listenerExists) { | 237 if (listenerExists) { |
| 547 delete webUIListenerMap[listener.eventName][listener.uid]; | 238 delete webUIListenerMap[listener.eventName][listener.uid]; |
| 548 return true; | 239 return true; |
| 549 } | 240 } |
| 550 return false; | 241 return false; |
| 551 } | 242 } |
| 552 | |
| 553 return { | 243 return { |
| 554 addSingletonGetter: addSingletonGetter, | 244 addSingletonGetter: addSingletonGetter, |
| 555 createUid: createUid, | 245 createUid: createUid, |
| 556 define: define, | 246 define: define, |
| 557 defineProperty: defineProperty, | 247 defineProperty: defineProperty, |
| 558 dispatchPropertyChange: dispatchPropertyChange, | 248 dispatchPropertyChange: dispatchPropertyChange, |
| 559 dispatchSimpleEvent: dispatchSimpleEvent, | 249 dispatchSimpleEvent: dispatchSimpleEvent, |
| 560 exportPath: exportPath, | 250 exportPath: exportPath, |
| 561 getUid: getUid, | 251 getUid: getUid, |
| 562 makePublic: makePublic, | 252 makePublic: makePublic, |
| 563 PropertyKind: PropertyKind, | 253 PropertyKind: PropertyKind, |
| 564 | |
| 565 // C++ <-> JS communication related methods. | |
| 566 addWebUIListener: addWebUIListener, | 254 addWebUIListener: addWebUIListener, |
| 567 removeWebUIListener: removeWebUIListener, | 255 removeWebUIListener: removeWebUIListener, |
| 568 sendWithPromise: sendWithPromise, | 256 sendWithPromise: sendWithPromise, |
| 569 webUIListenerCallback: webUIListenerCallback, | 257 webUIListenerCallback: webUIListenerCallback, |
| 570 webUIResponse: webUIResponse, | 258 webUIResponse: webUIResponse, |
| 571 | |
| 572 get doc() { | 259 get doc() { |
| 573 return document; | 260 return document; |
| 574 }, | 261 }, |
| 575 | |
| 576 /** Whether we are using a Mac or not. */ | |
| 577 get isMac() { | 262 get isMac() { |
| 578 return /Mac/.test(navigator.platform); | 263 return /Mac/.test(navigator.platform); |
| 579 }, | 264 }, |
| 580 | |
| 581 /** Whether this is on the Windows platform or not. */ | |
| 582 get isWindows() { | 265 get isWindows() { |
| 583 return /Win/.test(navigator.platform); | 266 return /Win/.test(navigator.platform); |
| 584 }, | 267 }, |
| 585 | |
| 586 /** Whether this is on chromeOS or not. */ | |
| 587 get isChromeOS() { | 268 get isChromeOS() { |
| 588 return /CrOS/.test(navigator.userAgent); | 269 return /CrOS/.test(navigator.userAgent); |
| 589 }, | 270 }, |
| 590 | |
| 591 /** Whether this is on vanilla Linux (not chromeOS). */ | |
| 592 get isLinux() { | 271 get isLinux() { |
| 593 return /Linux/.test(navigator.userAgent); | 272 return /Linux/.test(navigator.userAgent); |
| 594 }, | 273 }, |
| 595 | |
| 596 /** Whether this is on Android. */ | |
| 597 get isAndroid() { | 274 get isAndroid() { |
| 598 return /Android/.test(navigator.userAgent); | 275 return /Android/.test(navigator.userAgent); |
| 599 }, | 276 }, |
| 600 | |
| 601 /** Whether this is on iOS. */ | |
| 602 get isIOS() { | 277 get isIOS() { |
| 603 return /iPad|iPhone|iPod/.test(navigator.platform); | 278 return /iPad|iPhone|iPod/.test(navigator.platform); |
| 604 } | 279 } |
| 605 }; | 280 }; |
| 606 }(); | 281 }(); |
| 282 |
| 607 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 283 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 608 // Use of this source code is governed by a BSD-style license that can be | 284 // Use of this source code is governed by a BSD-style license that can be |
| 609 // found in the LICENSE file. | 285 // found in the LICENSE file. |
| 610 | |
| 611 cr.define('cr.ui', function() { | 286 cr.define('cr.ui', function() { |
| 612 | |
| 613 /** | |
| 614 * Decorates elements as an instance of a class. | |
| 615 * @param {string|!Element} source The way to find the element(s) to decorate. | |
| 616 * If this is a string then {@code querySeletorAll} is used to find the | |
| 617 * elements to decorate. | |
| 618 * @param {!Function} constr The constructor to decorate with. The constr | |
| 619 * needs to have a {@code decorate} function. | |
| 620 */ | |
| 621 function decorate(source, constr) { | 287 function decorate(source, constr) { |
| 622 var elements; | 288 var elements; |
| 623 if (typeof source == 'string') | 289 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e
lse elements = [ source ]; |
| 624 elements = cr.doc.querySelectorAll(source); | |
| 625 else | |
| 626 elements = [source]; | |
| 627 | |
| 628 for (var i = 0, el; el = elements[i]; i++) { | 290 for (var i = 0, el; el = elements[i]; i++) { |
| 629 if (!(el instanceof constr)) | 291 if (!(el instanceof constr)) constr.decorate(el); |
| 630 constr.decorate(el); | |
| 631 } | 292 } |
| 632 } | 293 } |
| 633 | |
| 634 /** | |
| 635 * Helper function for creating new element for define. | |
| 636 */ | |
| 637 function createElementHelper(tagName, opt_bag) { | 294 function createElementHelper(tagName, opt_bag) { |
| 638 // Allow passing in ownerDocument to create in a different document. | |
| 639 var doc; | 295 var doc; |
| 640 if (opt_bag && opt_bag.ownerDocument) | 296 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc
= cr.doc; |
| 641 doc = opt_bag.ownerDocument; | |
| 642 else | |
| 643 doc = cr.doc; | |
| 644 return doc.createElement(tagName); | 297 return doc.createElement(tagName); |
| 645 } | 298 } |
| 646 | |
| 647 /** | |
| 648 * Creates the constructor for a UI element class. | |
| 649 * | |
| 650 * Usage: | |
| 651 * <pre> | |
| 652 * var List = cr.ui.define('list'); | |
| 653 * List.prototype = { | |
| 654 * __proto__: HTMLUListElement.prototype, | |
| 655 * decorate: function() { | |
| 656 * ... | |
| 657 * }, | |
| 658 * ... | |
| 659 * }; | |
| 660 * </pre> | |
| 661 * | |
| 662 * @param {string|Function} tagNameOrFunction The tagName or | |
| 663 * function to use for newly created elements. If this is a function it | |
| 664 * needs to return a new element when called. | |
| 665 * @return {function(Object=):Element} The constructor function which takes | |
| 666 * an optional property bag. The function also has a static | |
| 667 * {@code decorate} method added to it. | |
| 668 */ | |
| 669 function define(tagNameOrFunction) { | 299 function define(tagNameOrFunction) { |
| 670 var createFunction, tagName; | 300 var createFunction, tagName; |
| 671 if (typeof tagNameOrFunction == 'function') { | 301 if (typeof tagNameOrFunction == 'function') { |
| 672 createFunction = tagNameOrFunction; | 302 createFunction = tagNameOrFunction; |
| 673 tagName = ''; | 303 tagName = ''; |
| 674 } else { | 304 } else { |
| 675 createFunction = createElementHelper; | 305 createFunction = createElementHelper; |
| 676 tagName = tagNameOrFunction; | 306 tagName = tagNameOrFunction; |
| 677 } | 307 } |
| 678 | |
| 679 /** | |
| 680 * Creates a new UI element constructor. | |
| 681 * @param {Object=} opt_propertyBag Optional bag of properties to set on the | |
| 682 * object after created. The property {@code ownerDocument} is special | |
| 683 * cased and it allows you to create the element in a different | |
| 684 * document than the default. | |
| 685 * @constructor | |
| 686 */ | |
| 687 function f(opt_propertyBag) { | 308 function f(opt_propertyBag) { |
| 688 var el = createFunction(tagName, opt_propertyBag); | 309 var el = createFunction(tagName, opt_propertyBag); |
| 689 f.decorate(el); | 310 f.decorate(el); |
| 690 for (var propertyName in opt_propertyBag) { | 311 for (var propertyName in opt_propertyBag) { |
| 691 el[propertyName] = opt_propertyBag[propertyName]; | 312 el[propertyName] = opt_propertyBag[propertyName]; |
| 692 } | 313 } |
| 693 return el; | 314 return el; |
| 694 } | 315 } |
| 695 | |
| 696 /** | |
| 697 * Decorates an element as a UI element class. | |
| 698 * @param {!Element} el The element to decorate. | |
| 699 */ | |
| 700 f.decorate = function(el) { | 316 f.decorate = function(el) { |
| 701 el.__proto__ = f.prototype; | 317 el.__proto__ = f.prototype; |
| 702 el.decorate(); | 318 el.decorate(); |
| 703 }; | 319 }; |
| 704 | |
| 705 return f; | 320 return f; |
| 706 } | 321 } |
| 707 | |
| 708 /** | |
| 709 * Input elements do not grow and shrink with their content. This is a simple | |
| 710 * (and not very efficient) way of handling shrinking to content with support | |
| 711 * for min width and limited by the width of the parent element. | |
| 712 * @param {!HTMLElement} el The element to limit the width for. | |
| 713 * @param {!HTMLElement} parentEl The parent element that should limit the | |
| 714 * size. | |
| 715 * @param {number} min The minimum width. | |
| 716 * @param {number=} opt_scale Optional scale factor to apply to the width. | |
| 717 */ | |
| 718 function limitInputWidth(el, parentEl, min, opt_scale) { | 322 function limitInputWidth(el, parentEl, min, opt_scale) { |
| 719 // Needs a size larger than borders | |
| 720 el.style.width = '10px'; | 323 el.style.width = '10px'; |
| 721 var doc = el.ownerDocument; | 324 var doc = el.ownerDocument; |
| 722 var win = doc.defaultView; | 325 var win = doc.defaultView; |
| 723 var computedStyle = win.getComputedStyle(el); | 326 var computedStyle = win.getComputedStyle(el); |
| 724 var parentComputedStyle = win.getComputedStyle(parentEl); | 327 var parentComputedStyle = win.getComputedStyle(parentEl); |
| 725 var rtl = computedStyle.direction == 'rtl'; | 328 var rtl = computedStyle.direction == 'rtl'; |
| 726 | 329 var inputRect = el.getBoundingClientRect(); |
| 727 // To get the max width we get the width of the treeItem minus the position | |
| 728 // of the input. | |
| 729 var inputRect = el.getBoundingClientRect(); // box-sizing | |
| 730 var parentRect = parentEl.getBoundingClientRect(); | 330 var parentRect = parentEl.getBoundingClientRect(); |
| 731 var startPos = rtl ? parentRect.right - inputRect.right : | 331 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p
arentRect.left; |
| 732 inputRect.left - parentRect.left; | 332 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS
tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp
utedStyle.borderRightWidth, 10); |
| 733 | 333 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa
rseInt(parentComputedStyle.paddingRight, 10); |
| 734 // Add up border and padding of the input. | |
| 735 var inner = parseInt(computedStyle.borderLeftWidth, 10) + | |
| 736 parseInt(computedStyle.paddingLeft, 10) + | |
| 737 parseInt(computedStyle.paddingRight, 10) + | |
| 738 parseInt(computedStyle.borderRightWidth, 10); | |
| 739 | |
| 740 // We also need to subtract the padding of parent to prevent it to overflow. | |
| 741 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : | |
| 742 parseInt(parentComputedStyle.paddingRight, 10); | |
| 743 | |
| 744 var max = parentEl.clientWidth - startPos - inner - parentPadding; | 334 var max = parentEl.clientWidth - startPos - inner - parentPadding; |
| 745 if (opt_scale) | 335 if (opt_scale) max *= opt_scale; |
| 746 max *= opt_scale; | |
| 747 | |
| 748 function limit() { | 336 function limit() { |
| 749 if (el.scrollWidth > max) { | 337 if (el.scrollWidth > max) { |
| 750 el.style.width = max + 'px'; | 338 el.style.width = max + 'px'; |
| 751 } else { | 339 } else { |
| 752 el.style.width = 0; | 340 el.style.width = 0; |
| 753 var sw = el.scrollWidth; | 341 var sw = el.scrollWidth; |
| 754 if (sw < min) { | 342 if (sw < min) { |
| 755 el.style.width = min + 'px'; | 343 el.style.width = min + 'px'; |
| 756 } else { | 344 } else { |
| 757 el.style.width = sw + 'px'; | 345 el.style.width = sw + 'px'; |
| 758 } | 346 } |
| 759 } | 347 } |
| 760 } | 348 } |
| 761 | |
| 762 el.addEventListener('input', limit); | 349 el.addEventListener('input', limit); |
| 763 limit(); | 350 limit(); |
| 764 } | 351 } |
| 765 | |
| 766 /** | |
| 767 * Takes a number and spits out a value CSS will be happy with. To avoid | |
| 768 * subpixel layout issues, the value is rounded to the nearest integral value. | |
| 769 * @param {number} pixels The number of pixels. | |
| 770 * @return {string} e.g. '16px'. | |
| 771 */ | |
| 772 function toCssPx(pixels) { | 352 function toCssPx(pixels) { |
| 773 if (!window.isFinite(pixels)) | 353 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: '
+ pixels); |
| 774 console.error('Pixel value is not a number: ' + pixels); | |
| 775 return Math.round(pixels) + 'px'; | 354 return Math.round(pixels) + 'px'; |
| 776 } | 355 } |
| 777 | |
| 778 /** | |
| 779 * Users complain they occasionaly use doubleclicks instead of clicks | |
| 780 * (http://crbug.com/140364). To fix it we freeze click handling for | |
| 781 * the doubleclick time interval. | |
| 782 * @param {MouseEvent} e Initial click event. | |
| 783 */ | |
| 784 function swallowDoubleClick(e) { | 356 function swallowDoubleClick(e) { |
| 785 var doc = e.target.ownerDocument; | 357 var doc = e.target.ownerDocument; |
| 786 var counter = Math.min(1, e.detail); | 358 var counter = Math.min(1, e.detail); |
| 787 function swallow(e) { | 359 function swallow(e) { |
| 788 e.stopPropagation(); | 360 e.stopPropagation(); |
| 789 e.preventDefault(); | 361 e.preventDefault(); |
| 790 } | 362 } |
| 791 function onclick(e) { | 363 function onclick(e) { |
| 792 if (e.detail > counter) { | 364 if (e.detail > counter) { |
| 793 counter = e.detail; | 365 counter = e.detail; |
| 794 // Swallow the click since it's a click inside the doubleclick timeout. | |
| 795 swallow(e); | 366 swallow(e); |
| 796 } else { | 367 } else { |
| 797 // Stop tracking clicks and let regular handling. | |
| 798 doc.removeEventListener('dblclick', swallow, true); | 368 doc.removeEventListener('dblclick', swallow, true); |
| 799 doc.removeEventListener('click', onclick, true); | 369 doc.removeEventListener('click', onclick, true); |
| 800 } | 370 } |
| 801 } | 371 } |
| 802 // The following 'click' event (if e.type == 'mouseup') mustn't be taken | |
| 803 // into account (it mustn't stop tracking clicks). Start event listening | |
| 804 // after zero timeout. | |
| 805 setTimeout(function() { | 372 setTimeout(function() { |
| 806 doc.addEventListener('click', onclick, true); | 373 doc.addEventListener('click', onclick, true); |
| 807 doc.addEventListener('dblclick', swallow, true); | 374 doc.addEventListener('dblclick', swallow, true); |
| 808 }, 0); | 375 }, 0); |
| 809 } | 376 } |
| 810 | |
| 811 return { | 377 return { |
| 812 decorate: decorate, | 378 decorate: decorate, |
| 813 define: define, | 379 define: define, |
| 814 limitInputWidth: limitInputWidth, | 380 limitInputWidth: limitInputWidth, |
| 815 toCssPx: toCssPx, | 381 toCssPx: toCssPx, |
| 816 swallowDoubleClick: swallowDoubleClick | 382 swallowDoubleClick: swallowDoubleClick |
| 817 }; | 383 }; |
| 818 }); | 384 }); |
| 385 |
| 819 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 386 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 820 // Use of this source code is governed by a BSD-style license that can be | 387 // Use of this source code is governed by a BSD-style license that can be |
| 821 // found in the LICENSE file. | 388 // found in the LICENSE file. |
| 822 | |
| 823 /** | |
| 824 * @fileoverview A command is an abstraction of an action a user can do in the | |
| 825 * UI. | |
| 826 * | |
| 827 * When the focus changes in the document for each command a canExecute event | |
| 828 * is dispatched on the active element. By listening to this event you can | |
| 829 * enable and disable the command by setting the event.canExecute property. | |
| 830 * | |
| 831 * When a command is executed a command event is dispatched on the active | |
| 832 * element. Note that you should stop the propagation after you have handled the | |
| 833 * command if there might be other command listeners higher up in the DOM tree. | |
| 834 */ | |
| 835 | |
| 836 cr.define('cr.ui', function() { | 389 cr.define('cr.ui', function() { |
| 837 | |
| 838 /** | |
| 839 * This is used to identify keyboard shortcuts. | |
| 840 * @param {string} shortcut The text used to describe the keys for this | |
| 841 * keyboard shortcut. | |
| 842 * @constructor | |
| 843 */ | |
| 844 function KeyboardShortcut(shortcut) { | 390 function KeyboardShortcut(shortcut) { |
| 845 var mods = {}; | 391 var mods = {}; |
| 846 var ident = ''; | 392 var ident = ''; |
| 847 shortcut.split('|').forEach(function(part) { | 393 shortcut.split('|').forEach(function(part) { |
| 848 var partLc = part.toLowerCase(); | 394 var partLc = part.toLowerCase(); |
| 849 switch (partLc) { | 395 switch (partLc) { |
| 850 case 'alt': | 396 case 'alt': |
| 851 case 'ctrl': | 397 case 'ctrl': |
| 852 case 'meta': | 398 case 'meta': |
| 853 case 'shift': | 399 case 'shift': |
| 854 mods[partLc + 'Key'] = true; | 400 mods[partLc + 'Key'] = true; |
| 855 break; | 401 break; |
| 856 default: | 402 |
| 857 if (ident) | 403 default: |
| 858 throw Error('Invalid shortcut'); | 404 if (ident) throw Error('Invalid shortcut'); |
| 859 ident = part; | 405 ident = part; |
| 860 } | 406 } |
| 861 }); | 407 }); |
| 862 | |
| 863 this.ident_ = ident; | 408 this.ident_ = ident; |
| 864 this.mods_ = mods; | 409 this.mods_ = mods; |
| 865 } | 410 } |
| 866 | |
| 867 KeyboardShortcut.prototype = { | 411 KeyboardShortcut.prototype = { |
| 868 /** | |
| 869 * Whether the keyboard shortcut object matches a keyboard event. | |
| 870 * @param {!Event} e The keyboard event object. | |
| 871 * @return {boolean} Whether we found a match or not. | |
| 872 */ | |
| 873 matchesEvent: function(e) { | 412 matchesEvent: function(e) { |
| 874 if (e.key == this.ident_) { | 413 if (e.key == this.ident_) { |
| 875 // All keyboard modifiers needs to match. | |
| 876 var mods = this.mods_; | 414 var mods = this.mods_; |
| 877 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { | 415 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k)
{ |
| 878 return e[k] == !!mods[k]; | 416 return e[k] == !!mods[k]; |
| 879 }); | 417 }); |
| 880 } | 418 } |
| 881 return false; | 419 return false; |
| 882 } | 420 } |
| 883 }; | 421 }; |
| 884 | |
| 885 /** | |
| 886 * Creates a new command element. | |
| 887 * @constructor | |
| 888 * @extends {HTMLElement} | |
| 889 */ | |
| 890 var Command = cr.ui.define('command'); | 422 var Command = cr.ui.define('command'); |
| 891 | |
| 892 Command.prototype = { | 423 Command.prototype = { |
| 893 __proto__: HTMLElement.prototype, | 424 __proto__: HTMLElement.prototype, |
| 894 | |
| 895 /** | |
| 896 * Initializes the command. | |
| 897 */ | |
| 898 decorate: function() { | 425 decorate: function() { |
| 899 CommandManager.init(assert(this.ownerDocument)); | 426 CommandManager.init(assert(this.ownerDocument)); |
| 900 | 427 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor
tcut'); |
| 901 if (this.hasAttribute('shortcut')) | |
| 902 this.shortcut = this.getAttribute('shortcut'); | |
| 903 }, | 428 }, |
| 904 | |
| 905 /** | |
| 906 * Executes the command by dispatching a command event on the given element. | |
| 907 * If |element| isn't given, the active element is used instead. | |
| 908 * If the command is {@code disabled} this does nothing. | |
| 909 * @param {HTMLElement=} opt_element Optional element to dispatch event on. | |
| 910 */ | |
| 911 execute: function(opt_element) { | 429 execute: function(opt_element) { |
| 912 if (this.disabled) | 430 if (this.disabled) return; |
| 913 return; | |
| 914 var doc = this.ownerDocument; | 431 var doc = this.ownerDocument; |
| 915 if (doc.activeElement) { | 432 if (doc.activeElement) { |
| 916 var e = new Event('command', {bubbles: true}); | 433 var e = new Event('command', { |
| 434 bubbles: true |
| 435 }); |
| 917 e.command = this; | 436 e.command = this; |
| 918 | |
| 919 (opt_element || doc.activeElement).dispatchEvent(e); | 437 (opt_element || doc.activeElement).dispatchEvent(e); |
| 920 } | 438 } |
| 921 }, | 439 }, |
| 922 | |
| 923 /** | |
| 924 * Call this when there have been changes that might change whether the | |
| 925 * command can be executed or not. | |
| 926 * @param {Node=} opt_node Node for which to actuate command state. | |
| 927 */ | |
| 928 canExecuteChange: function(opt_node) { | 440 canExecuteChange: function(opt_node) { |
| 929 dispatchCanExecuteEvent(this, | 441 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement
); |
| 930 opt_node || this.ownerDocument.activeElement); | |
| 931 }, | 442 }, |
| 932 | |
| 933 /** | |
| 934 * The keyboard shortcut that triggers the command. This is a string | |
| 935 * consisting of a key (as reported by WebKit in keydown) as | |
| 936 * well as optional key modifiers joinded with a '|'. | |
| 937 * | |
| 938 * Multiple keyboard shortcuts can be provided by separating them by | |
| 939 * whitespace. | |
| 940 * | |
| 941 * For example: | |
| 942 * "F1" | |
| 943 * "Backspace|Meta" for Apple command backspace. | |
| 944 * "a|Ctrl" for Control A | |
| 945 * "Delete Backspace|Meta" for Delete and Command Backspace | |
| 946 * | |
| 947 * @type {string} | |
| 948 */ | |
| 949 shortcut_: '', | 443 shortcut_: '', |
| 950 get shortcut() { | 444 get shortcut() { |
| 951 return this.shortcut_; | 445 return this.shortcut_; |
| 952 }, | 446 }, |
| 953 set shortcut(shortcut) { | 447 set shortcut(shortcut) { |
| 954 var oldShortcut = this.shortcut_; | 448 var oldShortcut = this.shortcut_; |
| 955 if (shortcut !== oldShortcut) { | 449 if (shortcut !== oldShortcut) { |
| 956 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { | 450 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { |
| 957 return new KeyboardShortcut(shortcut); | 451 return new KeyboardShortcut(shortcut); |
| 958 }); | 452 }); |
| 959 | |
| 960 // Set this after the keyboardShortcuts_ since that might throw. | |
| 961 this.shortcut_ = shortcut; | 453 this.shortcut_ = shortcut; |
| 962 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, | 454 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut)
; |
| 963 oldShortcut); | |
| 964 } | 455 } |
| 965 }, | 456 }, |
| 966 | |
| 967 /** | |
| 968 * Whether the event object matches the shortcut for this command. | |
| 969 * @param {!Event} e The key event object. | |
| 970 * @return {boolean} Whether it matched or not. | |
| 971 */ | |
| 972 matchesEvent: function(e) { | 457 matchesEvent: function(e) { |
| 973 if (!this.keyboardShortcuts_) | 458 if (!this.keyboardShortcuts_) return false; |
| 974 return false; | |
| 975 | |
| 976 return this.keyboardShortcuts_.some(function(keyboardShortcut) { | 459 return this.keyboardShortcuts_.some(function(keyboardShortcut) { |
| 977 return keyboardShortcut.matchesEvent(e); | 460 return keyboardShortcut.matchesEvent(e); |
| 978 }); | 461 }); |
| 979 }, | 462 } |
| 980 }; | 463 }; |
| 981 | |
| 982 /** | |
| 983 * The label of the command. | |
| 984 */ | |
| 985 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); | 464 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); |
| 986 | |
| 987 /** | |
| 988 * Whether the command is disabled or not. | |
| 989 */ | |
| 990 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); | 465 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); |
| 991 | |
| 992 /** | |
| 993 * Whether the command is hidden or not. | |
| 994 */ | |
| 995 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); | 466 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
| 996 | |
| 997 /** | |
| 998 * Whether the command is checked or not. | |
| 999 */ | |
| 1000 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); | 467 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
| 1001 | |
| 1002 /** | |
| 1003 * The flag that prevents the shortcut text from being displayed on menu. | |
| 1004 * | |
| 1005 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command) | |
| 1006 * is displayed in menu when the command is assosiated with a menu item. | |
| 1007 * Otherwise, no text is displayed. | |
| 1008 */ | |
| 1009 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); | 468 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); |
| 1010 | |
| 1011 /** | |
| 1012 * Dispatches a canExecute event on the target. | |
| 1013 * @param {!cr.ui.Command} command The command that we are testing for. | |
| 1014 * @param {EventTarget} target The target element to dispatch the event on. | |
| 1015 */ | |
| 1016 function dispatchCanExecuteEvent(command, target) { | 469 function dispatchCanExecuteEvent(command, target) { |
| 1017 var e = new CanExecuteEvent(command); | 470 var e = new CanExecuteEvent(command); |
| 1018 target.dispatchEvent(e); | 471 target.dispatchEvent(e); |
| 1019 command.disabled = !e.canExecute; | 472 command.disabled = !e.canExecute; |
| 1020 } | 473 } |
| 1021 | |
| 1022 /** | |
| 1023 * The command managers for different documents. | |
| 1024 */ | |
| 1025 var commandManagers = {}; | 474 var commandManagers = {}; |
| 1026 | |
| 1027 /** | |
| 1028 * Keeps track of the focused element and updates the commands when the focus | |
| 1029 * changes. | |
| 1030 * @param {!Document} doc The document that we are managing the commands for. | |
| 1031 * @constructor | |
| 1032 */ | |
| 1033 function CommandManager(doc) { | 475 function CommandManager(doc) { |
| 1034 doc.addEventListener('focus', this.handleFocus_.bind(this), true); | 476 doc.addEventListener('focus', this.handleFocus_.bind(this), true); |
| 1035 // Make sure we add the listener to the bubbling phase so that elements can | |
| 1036 // prevent the command. | |
| 1037 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); | 477 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); |
| 1038 } | 478 } |
| 1039 | |
| 1040 /** | |
| 1041 * Initializes a command manager for the document as needed. | |
| 1042 * @param {!Document} doc The document to manage the commands for. | |
| 1043 */ | |
| 1044 CommandManager.init = function(doc) { | 479 CommandManager.init = function(doc) { |
| 1045 var uid = cr.getUid(doc); | 480 var uid = cr.getUid(doc); |
| 1046 if (!(uid in commandManagers)) { | 481 if (!(uid in commandManagers)) { |
| 1047 commandManagers[uid] = new CommandManager(doc); | 482 commandManagers[uid] = new CommandManager(doc); |
| 1048 } | 483 } |
| 1049 }; | 484 }; |
| 1050 | |
| 1051 CommandManager.prototype = { | 485 CommandManager.prototype = { |
| 1052 | |
| 1053 /** | |
| 1054 * Handles focus changes on the document. | |
| 1055 * @param {Event} e The focus event object. | |
| 1056 * @private | |
| 1057 * @suppress {checkTypes} | |
| 1058 * TODO(vitalyp): remove the suppression. | |
| 1059 */ | |
| 1060 handleFocus_: function(e) { | 486 handleFocus_: function(e) { |
| 1061 var target = e.target; | 487 var target = e.target; |
| 1062 | 488 if (target.menu || target.command) return; |
| 1063 // Ignore focus on a menu button or command item. | 489 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
| 1064 if (target.menu || target.command) | |
| 1065 return; | |
| 1066 | |
| 1067 var commands = Array.prototype.slice.call( | |
| 1068 target.ownerDocument.querySelectorAll('command')); | |
| 1069 | |
| 1070 commands.forEach(function(command) { | 490 commands.forEach(function(command) { |
| 1071 dispatchCanExecuteEvent(command, target); | 491 dispatchCanExecuteEvent(command, target); |
| 1072 }); | 492 }); |
| 1073 }, | 493 }, |
| 1074 | |
| 1075 /** | |
| 1076 * Handles the keydown event and routes it to the right command. | |
| 1077 * @param {!Event} e The keydown event. | |
| 1078 */ | |
| 1079 handleKeyDown_: function(e) { | 494 handleKeyDown_: function(e) { |
| 1080 var target = e.target; | 495 var target = e.target; |
| 1081 var commands = Array.prototype.slice.call( | 496 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
| 1082 target.ownerDocument.querySelectorAll('command')); | |
| 1083 | |
| 1084 for (var i = 0, command; command = commands[i]; i++) { | 497 for (var i = 0, command; command = commands[i]; i++) { |
| 1085 if (command.matchesEvent(e)) { | 498 if (command.matchesEvent(e)) { |
| 1086 // When invoking a command via a shortcut, we have to manually check | |
| 1087 // if it can be executed, since focus might not have been changed | |
| 1088 // what would have updated the command's state. | |
| 1089 command.canExecuteChange(); | 499 command.canExecuteChange(); |
| 1090 | |
| 1091 if (!command.disabled) { | 500 if (!command.disabled) { |
| 1092 e.preventDefault(); | 501 e.preventDefault(); |
| 1093 // We do not want any other element to handle this. | |
| 1094 e.stopPropagation(); | 502 e.stopPropagation(); |
| 1095 command.execute(); | 503 command.execute(); |
| 1096 return; | 504 return; |
| 1097 } | 505 } |
| 1098 } | 506 } |
| 1099 } | 507 } |
| 1100 } | 508 } |
| 1101 }; | 509 }; |
| 1102 | |
| 1103 /** | |
| 1104 * The event type used for canExecute events. | |
| 1105 * @param {!cr.ui.Command} command The command that we are evaluating. | |
| 1106 * @extends {Event} | |
| 1107 * @constructor | |
| 1108 * @class | |
| 1109 */ | |
| 1110 function CanExecuteEvent(command) { | 510 function CanExecuteEvent(command) { |
| 1111 var e = new Event('canExecute', {bubbles: true, cancelable: true}); | 511 var e = new Event('canExecute', { |
| 512 bubbles: true, |
| 513 cancelable: true |
| 514 }); |
| 1112 e.__proto__ = CanExecuteEvent.prototype; | 515 e.__proto__ = CanExecuteEvent.prototype; |
| 1113 e.command = command; | 516 e.command = command; |
| 1114 return e; | 517 return e; |
| 1115 } | 518 } |
| 1116 | |
| 1117 CanExecuteEvent.prototype = { | 519 CanExecuteEvent.prototype = { |
| 1118 __proto__: Event.prototype, | 520 __proto__: Event.prototype, |
| 1119 | |
| 1120 /** | |
| 1121 * The current command | |
| 1122 * @type {cr.ui.Command} | |
| 1123 */ | |
| 1124 command: null, | 521 command: null, |
| 1125 | |
| 1126 /** | |
| 1127 * Whether the target can execute the command. Setting this also stops the | |
| 1128 * propagation and prevents the default. Callers can tell if an event has | |
| 1129 * been handled via |this.defaultPrevented|. | |
| 1130 * @type {boolean} | |
| 1131 */ | |
| 1132 canExecute_: false, | 522 canExecute_: false, |
| 1133 get canExecute() { | 523 get canExecute() { |
| 1134 return this.canExecute_; | 524 return this.canExecute_; |
| 1135 }, | 525 }, |
| 1136 set canExecute(canExecute) { | 526 set canExecute(canExecute) { |
| 1137 this.canExecute_ = !!canExecute; | 527 this.canExecute_ = !!canExecute; |
| 1138 this.stopPropagation(); | 528 this.stopPropagation(); |
| 1139 this.preventDefault(); | 529 this.preventDefault(); |
| 1140 } | 530 } |
| 1141 }; | 531 }; |
| 1142 | |
| 1143 // Export | |
| 1144 return { | 532 return { |
| 1145 Command: Command, | 533 Command: Command, |
| 1146 CanExecuteEvent: CanExecuteEvent | 534 CanExecuteEvent: CanExecuteEvent |
| 1147 }; | 535 }; |
| 1148 }); | 536 }); |
| 537 |
| 1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 538 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 1150 // Use of this source code is governed by a BSD-style license that can be | 539 // Use of this source code is governed by a BSD-style license that can be |
| 1151 // found in the LICENSE file. | 540 // found in the LICENSE file. |
| 1152 | |
| 1153 // <include src-disabled="assert.js"> | |
| 1154 | |
| 1155 /** | |
| 1156 * Alias for document.getElementById. Found elements must be HTMLElements. | |
| 1157 * @param {string} id The ID of the element to find. | |
| 1158 * @return {HTMLElement} The found element or null if not found. | |
| 1159 */ | |
| 1160 function $(id) { | 541 function $(id) { |
| 1161 var el = document.getElementById(id); | 542 var el = document.getElementById(id); |
| 1162 return el ? assertInstanceof(el, HTMLElement) : null; | 543 return el ? assertInstanceof(el, HTMLElement) : null; |
| 1163 } | 544 } |
| 1164 | 545 |
| 1165 // TODO(devlin): This should return SVGElement, but closure compiler is missing | |
| 1166 // those externs. | |
| 1167 /** | |
| 1168 * Alias for document.getElementById. Found elements must be SVGElements. | |
| 1169 * @param {string} id The ID of the element to find. | |
| 1170 * @return {Element} The found element or null if not found. | |
| 1171 */ | |
| 1172 function getSVGElement(id) { | 546 function getSVGElement(id) { |
| 1173 var el = document.getElementById(id); | 547 var el = document.getElementById(id); |
| 1174 return el ? assertInstanceof(el, Element) : null; | 548 return el ? assertInstanceof(el, Element) : null; |
| 1175 } | 549 } |
| 1176 | 550 |
| 1177 /** | |
| 1178 * Add an accessible message to the page that will be announced to | |
| 1179 * users who have spoken feedback on, but will be invisible to all | |
| 1180 * other users. It's removed right away so it doesn't clutter the DOM. | |
| 1181 * @param {string} msg The text to be pronounced. | |
| 1182 */ | |
| 1183 function announceAccessibleMessage(msg) { | 551 function announceAccessibleMessage(msg) { |
| 1184 var element = document.createElement('div'); | 552 var element = document.createElement('div'); |
| 1185 element.setAttribute('aria-live', 'polite'); | 553 element.setAttribute('aria-live', 'polite'); |
| 1186 element.style.position = 'relative'; | 554 element.style.position = 'relative'; |
| 1187 element.style.left = '-9999px'; | 555 element.style.left = '-9999px'; |
| 1188 element.style.height = '0px'; | 556 element.style.height = '0px'; |
| 1189 element.innerText = msg; | 557 element.innerText = msg; |
| 1190 document.body.appendChild(element); | 558 document.body.appendChild(element); |
| 1191 window.setTimeout(function() { | 559 window.setTimeout(function() { |
| 1192 document.body.removeChild(element); | 560 document.body.removeChild(element); |
| 1193 }, 0); | 561 }, 0); |
| 1194 } | 562 } |
| 1195 | 563 |
| 1196 /** | |
| 1197 * Generates a CSS url string. | |
| 1198 * @param {string} s The URL to generate the CSS url for. | |
| 1199 * @return {string} The CSS url string. | |
| 1200 */ | |
| 1201 function url(s) { | 564 function url(s) { |
| 1202 // http://www.w3.org/TR/css3-values/#uris | |
| 1203 // Parentheses, commas, whitespace characters, single quotes (') and double | |
| 1204 // quotes (") appearing in a URI must be escaped with a backslash | |
| 1205 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | 565 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); |
| 1206 // WebKit has a bug when it comes to URLs that end with \ | |
| 1207 // https://bugs.webkit.org/show_bug.cgi?id=28885 | |
| 1208 if (/\\\\$/.test(s2)) { | 566 if (/\\\\$/.test(s2)) { |
| 1209 // Add a space to work around the WebKit bug. | |
| 1210 s2 += ' '; | 567 s2 += ' '; |
| 1211 } | 568 } |
| 1212 return 'url("' + s2 + '")'; | 569 return 'url("' + s2 + '")'; |
| 1213 } | 570 } |
| 1214 | 571 |
| 1215 /** | |
| 1216 * Parses query parameters from Location. | |
| 1217 * @param {Location} location The URL to generate the CSS url for. | |
| 1218 * @return {Object} Dictionary containing name value pairs for URL | |
| 1219 */ | |
| 1220 function parseQueryParams(location) { | 572 function parseQueryParams(location) { |
| 1221 var params = {}; | 573 var params = {}; |
| 1222 var query = unescape(location.search.substring(1)); | 574 var query = unescape(location.search.substring(1)); |
| 1223 var vars = query.split('&'); | 575 var vars = query.split('&'); |
| 1224 for (var i = 0; i < vars.length; i++) { | 576 for (var i = 0; i < vars.length; i++) { |
| 1225 var pair = vars[i].split('='); | 577 var pair = vars[i].split('='); |
| 1226 params[pair[0]] = pair[1]; | 578 params[pair[0]] = pair[1]; |
| 1227 } | 579 } |
| 1228 return params; | 580 return params; |
| 1229 } | 581 } |
| 1230 | 582 |
| 1231 /** | |
| 1232 * Creates a new URL by appending or replacing the given query key and value. | |
| 1233 * Not supporting URL with username and password. | |
| 1234 * @param {Location} location The original URL. | |
| 1235 * @param {string} key The query parameter name. | |
| 1236 * @param {string} value The query parameter value. | |
| 1237 * @return {string} The constructed new URL. | |
| 1238 */ | |
| 1239 function setQueryParam(location, key, value) { | 583 function setQueryParam(location, key, value) { |
| 1240 var query = parseQueryParams(location); | 584 var query = parseQueryParams(location); |
| 1241 query[encodeURIComponent(key)] = encodeURIComponent(value); | 585 query[encodeURIComponent(key)] = encodeURIComponent(value); |
| 1242 | |
| 1243 var newQuery = ''; | 586 var newQuery = ''; |
| 1244 for (var q in query) { | 587 for (var q in query) { |
| 1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; | 588 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; |
| 1246 } | 589 } |
| 1247 | |
| 1248 return location.origin + location.pathname + newQuery + location.hash; | 590 return location.origin + location.pathname + newQuery + location.hash; |
| 1249 } | 591 } |
| 1250 | 592 |
| 1251 /** | |
| 1252 * @param {Node} el A node to search for ancestors with |className|. | |
| 1253 * @param {string} className A class to search for. | |
| 1254 * @return {Element} A node with class of |className| or null if none is found. | |
| 1255 */ | |
| 1256 function findAncestorByClass(el, className) { | 593 function findAncestorByClass(el, className) { |
| 1257 return /** @type {Element} */(findAncestor(el, function(el) { | 594 return findAncestor(el, function(el) { |
| 1258 return el.classList && el.classList.contains(className); | 595 return el.classList && el.classList.contains(className); |
| 1259 })); | 596 }); |
| 1260 } | 597 } |
| 1261 | 598 |
| 1262 /** | |
| 1263 * Return the first ancestor for which the {@code predicate} returns true. | |
| 1264 * @param {Node} node The node to check. | |
| 1265 * @param {function(Node):boolean} predicate The function that tests the | |
| 1266 * nodes. | |
| 1267 * @return {Node} The found ancestor or null if not found. | |
| 1268 */ | |
| 1269 function findAncestor(node, predicate) { | 599 function findAncestor(node, predicate) { |
| 1270 var last = false; | 600 var last = false; |
| 1271 while (node != null && !(last = predicate(node))) { | 601 while (node != null && !(last = predicate(node))) { |
| 1272 node = node.parentNode; | 602 node = node.parentNode; |
| 1273 } | 603 } |
| 1274 return last ? node : null; | 604 return last ? node : null; |
| 1275 } | 605 } |
| 1276 | 606 |
| 1277 function swapDomNodes(a, b) { | 607 function swapDomNodes(a, b) { |
| 1278 var afterA = a.nextSibling; | 608 var afterA = a.nextSibling; |
| 1279 if (afterA == b) { | 609 if (afterA == b) { |
| 1280 swapDomNodes(b, a); | 610 swapDomNodes(b, a); |
| 1281 return; | 611 return; |
| 1282 } | 612 } |
| 1283 var aParent = a.parentNode; | 613 var aParent = a.parentNode; |
| 1284 b.parentNode.replaceChild(a, b); | 614 b.parentNode.replaceChild(a, b); |
| 1285 aParent.insertBefore(b, afterA); | 615 aParent.insertBefore(b, afterA); |
| 1286 } | 616 } |
| 1287 | 617 |
| 1288 /** | |
| 1289 * Disables text selection and dragging, with optional whitelist callbacks. | |
| 1290 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function | |
| 1291 * is defined and returns true, the onselectionstart event will be | |
| 1292 * surpressed. | |
| 1293 * @param {function(Event):boolean=} opt_allowDragStart Unless this function | |
| 1294 * is defined and returns true, the ondragstart event will be surpressed. | |
| 1295 */ | |
| 1296 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { | 618 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { |
| 1297 // Disable text selection. | |
| 1298 document.onselectstart = function(e) { | 619 document.onselectstart = function(e) { |
| 1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) | 620 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) e.prevent
Default(); |
| 1300 e.preventDefault(); | |
| 1301 }; | 621 }; |
| 1302 | |
| 1303 // Disable dragging. | |
| 1304 document.ondragstart = function(e) { | 622 document.ondragstart = function(e) { |
| 1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) | 623 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) e.preventDefa
ult(); |
| 1306 e.preventDefault(); | |
| 1307 }; | 624 }; |
| 1308 } | 625 } |
| 1309 | 626 |
| 1310 /** | |
| 1311 * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead. | |
| 1312 * Call this to stop clicks on <a href="#"> links from scrolling to the top of | |
| 1313 * the page (and possibly showing a # in the link). | |
| 1314 */ | |
| 1315 function preventDefaultOnPoundLinkClicks() { | 627 function preventDefaultOnPoundLinkClicks() { |
| 1316 document.addEventListener('click', function(e) { | 628 document.addEventListener('click', function(e) { |
| 1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { | 629 var anchor = findAncestor(e.target, function(el) { |
| 1318 return el.tagName == 'A'; | 630 return el.tagName == 'A'; |
| 1319 }); | 631 }); |
| 1320 // Use getAttribute() to prevent URL normalization. | 632 if (anchor && anchor.getAttribute('href') == '#') e.preventDefault(); |
| 1321 if (anchor && anchor.getAttribute('href') == '#') | |
| 1322 e.preventDefault(); | |
| 1323 }); | 633 }); |
| 1324 } | 634 } |
| 1325 | 635 |
| 1326 /** | |
| 1327 * Check the directionality of the page. | |
| 1328 * @return {boolean} True if Chrome is running an RTL UI. | |
| 1329 */ | |
| 1330 function isRTL() { | 636 function isRTL() { |
| 1331 return document.documentElement.dir == 'rtl'; | 637 return document.documentElement.dir == 'rtl'; |
| 1332 } | 638 } |
| 1333 | 639 |
| 1334 /** | |
| 1335 * Get an element that's known to exist by its ID. We use this instead of just | |
| 1336 * calling getElementById and not checking the result because this lets us | |
| 1337 * satisfy the JSCompiler type system. | |
| 1338 * @param {string} id The identifier name. | |
| 1339 * @return {!HTMLElement} the Element. | |
| 1340 */ | |
| 1341 function getRequiredElement(id) { | 640 function getRequiredElement(id) { |
| 1342 return assertInstanceof($(id), HTMLElement, | 641 return assertInstanceof($(id), HTMLElement, 'Missing required element: ' + id)
; |
| 1343 'Missing required element: ' + id); | |
| 1344 } | 642 } |
| 1345 | 643 |
| 1346 /** | |
| 1347 * Query an element that's known to exist by a selector. We use this instead of | |
| 1348 * just calling querySelector and not checking the result because this lets us | |
| 1349 * satisfy the JSCompiler type system. | |
| 1350 * @param {string} selectors CSS selectors to query the element. | |
| 1351 * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional | |
| 1352 * context object for querySelector. | |
| 1353 * @return {!HTMLElement} the Element. | |
| 1354 */ | |
| 1355 function queryRequiredElement(selectors, opt_context) { | 644 function queryRequiredElement(selectors, opt_context) { |
| 1356 var element = (opt_context || document).querySelector(selectors); | 645 var element = (opt_context || document).querySelector(selectors); |
| 1357 return assertInstanceof(element, HTMLElement, | 646 return assertInstanceof(element, HTMLElement, 'Missing required element: ' + s
electors); |
| 1358 'Missing required element: ' + selectors); | |
| 1359 } | 647 } |
| 1360 | 648 |
| 1361 // Handle click on a link. If the link points to a chrome: or file: url, then | 649 [ 'click', 'auxclick' ].forEach(function(eventName) { |
| 1362 // call into the browser to do the navigation. | |
| 1363 ['click', 'auxclick'].forEach(function(eventName) { | |
| 1364 document.addEventListener(eventName, function(e) { | 650 document.addEventListener(eventName, function(e) { |
| 1365 if (e.defaultPrevented) | 651 if (e.defaultPrevented) return; |
| 1366 return; | |
| 1367 | |
| 1368 var eventPath = e.path; | 652 var eventPath = e.path; |
| 1369 var anchor = null; | 653 var anchor = null; |
| 1370 if (eventPath) { | 654 if (eventPath) { |
| 1371 for (var i = 0; i < eventPath.length; i++) { | 655 for (var i = 0; i < eventPath.length; i++) { |
| 1372 var element = eventPath[i]; | 656 var element = eventPath[i]; |
| 1373 if (element.tagName === 'A' && element.href) { | 657 if (element.tagName === 'A' && element.href) { |
| 1374 anchor = element; | 658 anchor = element; |
| 1375 break; | 659 break; |
| 1376 } | 660 } |
| 1377 } | 661 } |
| 1378 } | 662 } |
| 1379 | |
| 1380 // Fallback if Event.path is not available. | |
| 1381 var el = e.target; | 663 var el = e.target; |
| 1382 if (!anchor && el.nodeType == Node.ELEMENT_NODE && | 664 if (!anchor && el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector(
'A, A *')) { |
| 1383 el.webkitMatchesSelector('A, A *')) { | |
| 1384 while (el.tagName != 'A') { | 665 while (el.tagName != 'A') { |
| 1385 el = el.parentElement; | 666 el = el.parentElement; |
| 1386 } | 667 } |
| 1387 anchor = el; | 668 anchor = el; |
| 1388 } | 669 } |
| 1389 | 670 if (!anchor) return; |
| 1390 if (!anchor) | 671 anchor = anchor; |
| 1391 return; | 672 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && (e.button
== 0 || e.button == 1)) { |
| 1392 | 673 chrome.send('navigateToUrl', [ anchor.href, anchor.target, e.button, e.alt
Key, e.ctrlKey, e.metaKey, e.shiftKey ]); |
| 1393 anchor = /** @type {!HTMLAnchorElement} */(anchor); | |
| 1394 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && | |
| 1395 (e.button == 0 || e.button == 1)) { | |
| 1396 chrome.send('navigateToUrl', [ | |
| 1397 anchor.href, | |
| 1398 anchor.target, | |
| 1399 e.button, | |
| 1400 e.altKey, | |
| 1401 e.ctrlKey, | |
| 1402 e.metaKey, | |
| 1403 e.shiftKey | |
| 1404 ]); | |
| 1405 e.preventDefault(); | 674 e.preventDefault(); |
| 1406 } | 675 } |
| 1407 }); | 676 }); |
| 1408 }); | 677 }); |
| 1409 | 678 |
| 1410 /** | |
| 1411 * Creates a new URL which is the old URL with a GET param of key=value. | |
| 1412 * @param {string} url The base URL. There is not sanity checking on the URL so | |
| 1413 * it must be passed in a proper format. | |
| 1414 * @param {string} key The key of the param. | |
| 1415 * @param {string} value The value of the param. | |
| 1416 * @return {string} The new URL. | |
| 1417 */ | |
| 1418 function appendParam(url, key, value) { | 679 function appendParam(url, key, value) { |
| 1419 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 680 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); |
| 1420 | 681 if (url.indexOf('?') == -1) return url + '?' + param; |
| 1421 if (url.indexOf('?') == -1) | |
| 1422 return url + '?' + param; | |
| 1423 return url + '&' + param; | 682 return url + '&' + param; |
| 1424 } | 683 } |
| 1425 | 684 |
| 1426 /** | |
| 1427 * Creates an element of a specified type with a specified class name. | |
| 1428 * @param {string} type The node type. | |
| 1429 * @param {string} className The class name to use. | |
| 1430 * @return {Element} The created element. | |
| 1431 */ | |
| 1432 function createElementWithClassName(type, className) { | 685 function createElementWithClassName(type, className) { |
| 1433 var elm = document.createElement(type); | 686 var elm = document.createElement(type); |
| 1434 elm.className = className; | 687 elm.className = className; |
| 1435 return elm; | 688 return elm; |
| 1436 } | 689 } |
| 1437 | 690 |
| 1438 /** | |
| 1439 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | |
| 1440 * or when no paint happens during the animation). This function sets up | |
| 1441 * a timer and emulate the event if it is not fired when the timer expires. | |
| 1442 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | |
| 1443 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | |
| 1444 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | |
| 1445 * using the transitionDuration style value. | |
| 1446 */ | |
| 1447 function ensureTransitionEndEvent(el, opt_timeOut) { | 691 function ensureTransitionEndEvent(el, opt_timeOut) { |
| 1448 if (opt_timeOut === undefined) { | 692 if (opt_timeOut === undefined) { |
| 1449 var style = getComputedStyle(el); | 693 var style = getComputedStyle(el); |
| 1450 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 694 opt_timeOut = parseFloat(style.transitionDuration) * 1e3; |
| 1451 | |
| 1452 // Give an additional 50ms buffer for the animation to complete. | |
| 1453 opt_timeOut += 50; | 695 opt_timeOut += 50; |
| 1454 } | 696 } |
| 1455 | |
| 1456 var fired = false; | 697 var fired = false; |
| 1457 el.addEventListener('webkitTransitionEnd', function f(e) { | 698 el.addEventListener('webkitTransitionEnd', function f(e) { |
| 1458 el.removeEventListener('webkitTransitionEnd', f); | 699 el.removeEventListener('webkitTransitionEnd', f); |
| 1459 fired = true; | 700 fired = true; |
| 1460 }); | 701 }); |
| 1461 window.setTimeout(function() { | 702 window.setTimeout(function() { |
| 1462 if (!fired) | 703 if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); |
| 1463 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | |
| 1464 }, opt_timeOut); | 704 }, opt_timeOut); |
| 1465 } | 705 } |
| 1466 | 706 |
| 1467 /** | |
| 1468 * Alias for document.scrollTop getter. | |
| 1469 * @param {!HTMLDocument} doc The document node where information will be | |
| 1470 * queried from. | |
| 1471 * @return {number} The Y document scroll offset. | |
| 1472 */ | |
| 1473 function scrollTopForDocument(doc) { | 707 function scrollTopForDocument(doc) { |
| 1474 return doc.documentElement.scrollTop || doc.body.scrollTop; | 708 return doc.documentElement.scrollTop || doc.body.scrollTop; |
| 1475 } | 709 } |
| 1476 | 710 |
| 1477 /** | |
| 1478 * Alias for document.scrollTop setter. | |
| 1479 * @param {!HTMLDocument} doc The document node where information will be | |
| 1480 * queried from. | |
| 1481 * @param {number} value The target Y scroll offset. | |
| 1482 */ | |
| 1483 function setScrollTopForDocument(doc, value) { | 711 function setScrollTopForDocument(doc, value) { |
| 1484 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 712 doc.documentElement.scrollTop = doc.body.scrollTop = value; |
| 1485 } | 713 } |
| 1486 | 714 |
| 1487 /** | |
| 1488 * Alias for document.scrollLeft getter. | |
| 1489 * @param {!HTMLDocument} doc The document node where information will be | |
| 1490 * queried from. | |
| 1491 * @return {number} The X document scroll offset. | |
| 1492 */ | |
| 1493 function scrollLeftForDocument(doc) { | 715 function scrollLeftForDocument(doc) { |
| 1494 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 716 return doc.documentElement.scrollLeft || doc.body.scrollLeft; |
| 1495 } | 717 } |
| 1496 | 718 |
| 1497 /** | |
| 1498 * Alias for document.scrollLeft setter. | |
| 1499 * @param {!HTMLDocument} doc The document node where information will be | |
| 1500 * queried from. | |
| 1501 * @param {number} value The target X scroll offset. | |
| 1502 */ | |
| 1503 function setScrollLeftForDocument(doc, value) { | 719 function setScrollLeftForDocument(doc, value) { |
| 1504 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 720 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; |
| 1505 } | 721 } |
| 1506 | 722 |
| 1507 /** | |
| 1508 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | |
| 1509 * @param {string} original The original string. | |
| 1510 * @return {string} The string with all the characters mentioned above replaced. | |
| 1511 */ | |
| 1512 function HTMLEscape(original) { | 723 function HTMLEscape(original) { |
| 1513 return original.replace(/&/g, '&') | 724 return original.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '&g
t;').replace(/"/g, '"').replace(/'/g, '''); |
| 1514 .replace(/</g, '<') | 725 } |
| 1515 .replace(/>/g, '>') | 726 |
| 1516 .replace(/"/g, '"') | |
| 1517 .replace(/'/g, '''); | |
| 1518 } | |
| 1519 | |
| 1520 /** | |
| 1521 * Shortens the provided string (if necessary) to a string of length at most | |
| 1522 * |maxLength|. | |
| 1523 * @param {string} original The original string. | |
| 1524 * @param {number} maxLength The maximum length allowed for the string. | |
| 1525 * @return {string} The original string if its length does not exceed | |
| 1526 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | |
| 1527 * appended. | |
| 1528 */ | |
| 1529 function elide(original, maxLength) { | 727 function elide(original, maxLength) { |
| 1530 if (original.length <= maxLength) | 728 if (original.length <= maxLength) return original; |
| 1531 return original; | 729 return original.substring(0, maxLength - 1) + '…'; |
| 1532 return original.substring(0, maxLength - 1) + '\u2026'; | 730 } |
| 1533 } | 731 |
| 1534 | |
| 1535 /** | |
| 1536 * Quote a string so it can be used in a regular expression. | |
| 1537 * @param {string} str The source string. | |
| 1538 * @return {string} The escaped string. | |
| 1539 */ | |
| 1540 function quoteString(str) { | 732 function quoteString(str) { |
| 1541 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 733 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); |
| 1542 } | 734 } |
| 1543 | 735 |
| 1544 // <if expr="is_ios"> | 736 // <if expr="is_ios"> |
| 1545 // Polyfill 'key' in KeyboardEvent for iOS. | |
| 1546 // This function is not intended to be complete but should | |
| 1547 // be sufficient enough to have iOS work correctly while | |
| 1548 // it does not support key yet. | |
| 1549 if (!('key' in KeyboardEvent.prototype)) { | 737 if (!('key' in KeyboardEvent.prototype)) { |
| 1550 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 738 Object.defineProperty(KeyboardEvent.prototype, 'key', { |
| 1551 /** @this {KeyboardEvent} */ | 739 get: function() { |
| 1552 get: function () { | 740 if (this.keyCode >= 48 && this.keyCode <= 57) return String.fromCharCode(t
his.keyCode); |
| 1553 // 0-9 | 741 if (this.keyCode >= 65 && this.keyCode <= 90) { |
| 1554 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | |
| 1555 return String.fromCharCode(this.keyCode); | |
| 1556 | |
| 1557 // A-Z | |
| 1558 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | |
| 1559 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 742 var result = String.fromCharCode(this.keyCode).toLowerCase(); |
| 1560 if (this.shiftKey) | 743 if (this.shiftKey) result = result.toUpperCase(); |
| 1561 result = result.toUpperCase(); | |
| 1562 return result; | 744 return result; |
| 1563 } | 745 } |
| 1564 | 746 switch (this.keyCode) { |
| 1565 // Special characters | 747 case 8: |
| 1566 switch(this.keyCode) { | 748 return 'Backspace'; |
| 1567 case 0x08: return 'Backspace'; | 749 |
| 1568 case 0x09: return 'Tab'; | 750 case 9: |
| 1569 case 0x0d: return 'Enter'; | 751 return 'Tab'; |
| 1570 case 0x10: return 'Shift'; | 752 |
| 1571 case 0x11: return 'Control'; | 753 case 13: |
| 1572 case 0x12: return 'Alt'; | 754 return 'Enter'; |
| 1573 case 0x1b: return 'Escape'; | 755 |
| 1574 case 0x20: return ' '; | 756 case 16: |
| 1575 case 0x21: return 'PageUp'; | 757 return 'Shift'; |
| 1576 case 0x22: return 'PageDown'; | 758 |
| 1577 case 0x23: return 'End'; | 759 case 17: |
| 1578 case 0x24: return 'Home'; | 760 return 'Control'; |
| 1579 case 0x25: return 'ArrowLeft'; | 761 |
| 1580 case 0x26: return 'ArrowUp'; | 762 case 18: |
| 1581 case 0x27: return 'ArrowRight'; | 763 return 'Alt'; |
| 1582 case 0x28: return 'ArrowDown'; | 764 |
| 1583 case 0x2d: return 'Insert'; | 765 case 27: |
| 1584 case 0x2e: return 'Delete'; | 766 return 'Escape'; |
| 1585 case 0x5b: return 'Meta'; | 767 |
| 1586 case 0x70: return 'F1'; | 768 case 32: |
| 1587 case 0x71: return 'F2'; | 769 return ' '; |
| 1588 case 0x72: return 'F3'; | 770 |
| 1589 case 0x73: return 'F4'; | 771 case 33: |
| 1590 case 0x74: return 'F5'; | 772 return 'PageUp'; |
| 1591 case 0x75: return 'F6'; | 773 |
| 1592 case 0x76: return 'F7'; | 774 case 34: |
| 1593 case 0x77: return 'F8'; | 775 return 'PageDown'; |
| 1594 case 0x78: return 'F9'; | 776 |
| 1595 case 0x79: return 'F10'; | 777 case 35: |
| 1596 case 0x7a: return 'F11'; | 778 return 'End'; |
| 1597 case 0x7b: return 'F12'; | 779 |
| 1598 case 0xbb: return '='; | 780 case 36: |
| 1599 case 0xbd: return '-'; | 781 return 'Home'; |
| 1600 case 0xdb: return '['; | 782 |
| 1601 case 0xdd: return ']'; | 783 case 37: |
| 784 return 'ArrowLeft'; |
| 785 |
| 786 case 38: |
| 787 return 'ArrowUp'; |
| 788 |
| 789 case 39: |
| 790 return 'ArrowRight'; |
| 791 |
| 792 case 40: |
| 793 return 'ArrowDown'; |
| 794 |
| 795 case 45: |
| 796 return 'Insert'; |
| 797 |
| 798 case 46: |
| 799 return 'Delete'; |
| 800 |
| 801 case 91: |
| 802 return 'Meta'; |
| 803 |
| 804 case 112: |
| 805 return 'F1'; |
| 806 |
| 807 case 113: |
| 808 return 'F2'; |
| 809 |
| 810 case 114: |
| 811 return 'F3'; |
| 812 |
| 813 case 115: |
| 814 return 'F4'; |
| 815 |
| 816 case 116: |
| 817 return 'F5'; |
| 818 |
| 819 case 117: |
| 820 return 'F6'; |
| 821 |
| 822 case 118: |
| 823 return 'F7'; |
| 824 |
| 825 case 119: |
| 826 return 'F8'; |
| 827 |
| 828 case 120: |
| 829 return 'F9'; |
| 830 |
| 831 case 121: |
| 832 return 'F10'; |
| 833 |
| 834 case 122: |
| 835 return 'F11'; |
| 836 |
| 837 case 123: |
| 838 return 'F12'; |
| 839 |
| 840 case 187: |
| 841 return '='; |
| 842 |
| 843 case 189: |
| 844 return '-'; |
| 845 |
| 846 case 219: |
| 847 return '['; |
| 848 |
| 849 case 221: |
| 850 return ']'; |
| 1602 } | 851 } |
| 1603 return 'Unidentified'; | 852 return 'Unidentified'; |
| 1604 } | 853 } |
| 1605 }); | 854 }); |
| 1606 } else { | 855 } else { |
| 1607 window.console.log("KeyboardEvent.Key polyfill not required"); | 856 window.console.log("KeyboardEvent.Key polyfill not required"); |
| 1608 } | 857 } |
| 858 |
| 1609 // </if> /* is_ios */ | 859 // </if> /* is_ios */ |
| 1610 /** | 860 Polymer.IronResizableBehavior = { |
| 1611 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | 861 properties: { |
| 1612 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | 862 _parentResizable: { |
| 1613 * size or hidden state of their children) and "resizables" (elements that nee
d to be | 863 type: Object, |
| 1614 * notified when they are resized or un-hidden by their parents in order to ta
ke | 864 observer: '_parentResizableChanged' |
| 1615 * action on their new measurements). | 865 }, |
| 1616 * | 866 _notifyingDescendant: { |
| 1617 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | 867 type: Boolean, |
| 1618 * their element definition and listen for the `iron-resize` event on themselv
es. | 868 value: false |
| 1619 * This event will be fired when they become showing after having been hidden, | 869 } |
| 1620 * when they are resized explicitly by another resizable, or when the window h
as been | 870 }, |
| 1621 * resized. | 871 listeners: { |
| 1622 * | 872 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
| 1623 * Note, the `iron-resize` event is non-bubbling. | 873 }, |
| 1624 * | 874 created: function() { |
| 1625 * @polymerBehavior Polymer.IronResizableBehavior | 875 this._interestedResizables = []; |
| 1626 * @demo demo/index.html | 876 this._boundNotifyResize = this.notifyResize.bind(this); |
| 1627 **/ | 877 }, |
| 1628 Polymer.IronResizableBehavior = { | 878 attached: function() { |
| 879 this.fire('iron-request-resize-notifications', null, { |
| 880 node: this, |
| 881 bubbles: true, |
| 882 cancelable: true |
| 883 }); |
| 884 if (!this._parentResizable) { |
| 885 window.addEventListener('resize', this._boundNotifyResize); |
| 886 this.notifyResize(); |
| 887 } |
| 888 }, |
| 889 detached: function() { |
| 890 if (this._parentResizable) { |
| 891 this._parentResizable.stopResizeNotificationsFor(this); |
| 892 } else { |
| 893 window.removeEventListener('resize', this._boundNotifyResize); |
| 894 } |
| 895 this._parentResizable = null; |
| 896 }, |
| 897 notifyResize: function() { |
| 898 if (!this.isAttached) { |
| 899 return; |
| 900 } |
| 901 this._interestedResizables.forEach(function(resizable) { |
| 902 if (this.resizerShouldNotify(resizable)) { |
| 903 this._notifyDescendant(resizable); |
| 904 } |
| 905 }, this); |
| 906 this._fireResize(); |
| 907 }, |
| 908 assignParentResizable: function(parentResizable) { |
| 909 this._parentResizable = parentResizable; |
| 910 }, |
| 911 stopResizeNotificationsFor: function(target) { |
| 912 var index = this._interestedResizables.indexOf(target); |
| 913 if (index > -1) { |
| 914 this._interestedResizables.splice(index, 1); |
| 915 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
| 916 } |
| 917 }, |
| 918 resizerShouldNotify: function(element) { |
| 919 return true; |
| 920 }, |
| 921 _onDescendantIronResize: function(event) { |
| 922 if (this._notifyingDescendant) { |
| 923 event.stopPropagation(); |
| 924 return; |
| 925 } |
| 926 if (!Polymer.Settings.useShadow) { |
| 927 this._fireResize(); |
| 928 } |
| 929 }, |
| 930 _fireResize: function() { |
| 931 this.fire('iron-resize', null, { |
| 932 node: this, |
| 933 bubbles: false |
| 934 }); |
| 935 }, |
| 936 _onIronRequestResizeNotifications: function(event) { |
| 937 var target = event.path ? event.path[0] : event.target; |
| 938 if (target === this) { |
| 939 return; |
| 940 } |
| 941 if (this._interestedResizables.indexOf(target) === -1) { |
| 942 this._interestedResizables.push(target); |
| 943 this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| 944 } |
| 945 target.assignParentResizable(this); |
| 946 this._notifyDescendant(target); |
| 947 event.stopPropagation(); |
| 948 }, |
| 949 _parentResizableChanged: function(parentResizable) { |
| 950 if (parentResizable) { |
| 951 window.removeEventListener('resize', this._boundNotifyResize); |
| 952 } |
| 953 }, |
| 954 _notifyDescendant: function(descendant) { |
| 955 if (!this.isAttached) { |
| 956 return; |
| 957 } |
| 958 this._notifyingDescendant = true; |
| 959 descendant.notifyResize(); |
| 960 this._notifyingDescendant = false; |
| 961 } |
| 962 }; |
| 963 |
| 964 (function() { |
| 965 'use strict'; |
| 966 var KEY_IDENTIFIER = { |
| 967 'U+0008': 'backspace', |
| 968 'U+0009': 'tab', |
| 969 'U+001B': 'esc', |
| 970 'U+0020': 'space', |
| 971 'U+007F': 'del' |
| 972 }; |
| 973 var KEY_CODE = { |
| 974 8: 'backspace', |
| 975 9: 'tab', |
| 976 13: 'enter', |
| 977 27: 'esc', |
| 978 33: 'pageup', |
| 979 34: 'pagedown', |
| 980 35: 'end', |
| 981 36: 'home', |
| 982 32: 'space', |
| 983 37: 'left', |
| 984 38: 'up', |
| 985 39: 'right', |
| 986 40: 'down', |
| 987 46: 'del', |
| 988 106: '*' |
| 989 }; |
| 990 var MODIFIER_KEYS = { |
| 991 shift: 'shiftKey', |
| 992 ctrl: 'ctrlKey', |
| 993 alt: 'altKey', |
| 994 meta: 'metaKey' |
| 995 }; |
| 996 var KEY_CHAR = /[a-z0-9*]/; |
| 997 var IDENT_CHAR = /U\+/; |
| 998 var ARROW_KEY = /^arrow/; |
| 999 var SPACE_KEY = /^space(bar)?/; |
| 1000 var ESC_KEY = /^escape$/; |
| 1001 function transformKey(key, noSpecialChars) { |
| 1002 var validKey = ''; |
| 1003 if (key) { |
| 1004 var lKey = key.toLowerCase(); |
| 1005 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 1006 validKey = 'space'; |
| 1007 } else if (ESC_KEY.test(lKey)) { |
| 1008 validKey = 'esc'; |
| 1009 } else if (lKey.length == 1) { |
| 1010 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 1011 validKey = lKey; |
| 1012 } |
| 1013 } else if (ARROW_KEY.test(lKey)) { |
| 1014 validKey = lKey.replace('arrow', ''); |
| 1015 } else if (lKey == 'multiply') { |
| 1016 validKey = '*'; |
| 1017 } else { |
| 1018 validKey = lKey; |
| 1019 } |
| 1020 } |
| 1021 return validKey; |
| 1022 } |
| 1023 function transformKeyIdentifier(keyIdent) { |
| 1024 var validKey = ''; |
| 1025 if (keyIdent) { |
| 1026 if (keyIdent in KEY_IDENTIFIER) { |
| 1027 validKey = KEY_IDENTIFIER[keyIdent]; |
| 1028 } else if (IDENT_CHAR.test(keyIdent)) { |
| 1029 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 1030 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 1031 } else { |
| 1032 validKey = keyIdent.toLowerCase(); |
| 1033 } |
| 1034 } |
| 1035 return validKey; |
| 1036 } |
| 1037 function transformKeyCode(keyCode) { |
| 1038 var validKey = ''; |
| 1039 if (Number(keyCode)) { |
| 1040 if (keyCode >= 65 && keyCode <= 90) { |
| 1041 validKey = String.fromCharCode(32 + keyCode); |
| 1042 } else if (keyCode >= 112 && keyCode <= 123) { |
| 1043 validKey = 'f' + (keyCode - 112); |
| 1044 } else if (keyCode >= 48 && keyCode <= 57) { |
| 1045 validKey = String(keyCode - 48); |
| 1046 } else if (keyCode >= 96 && keyCode <= 105) { |
| 1047 validKey = String(keyCode - 96); |
| 1048 } else { |
| 1049 validKey = KEY_CODE[keyCode]; |
| 1050 } |
| 1051 } |
| 1052 return validKey; |
| 1053 } |
| 1054 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 1055 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier(
keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke
yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| 1056 } |
| 1057 function keyComboMatchesEvent(keyCombo, event) { |
| 1058 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 1059 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift
Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event
.altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey); |
| 1060 } |
| 1061 function parseKeyComboString(keyComboString) { |
| 1062 if (keyComboString.length === 1) { |
| 1063 return { |
| 1064 combo: keyComboString, |
| 1065 key: keyComboString, |
| 1066 event: 'keydown' |
| 1067 }; |
| 1068 } |
| 1069 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar
t) { |
| 1070 var eventParts = keyComboPart.split(':'); |
| 1071 var keyName = eventParts[0]; |
| 1072 var event = eventParts[1]; |
| 1073 if (keyName in MODIFIER_KEYS) { |
| 1074 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 1075 parsedKeyCombo.hasModifiers = true; |
| 1076 } else { |
| 1077 parsedKeyCombo.key = keyName; |
| 1078 parsedKeyCombo.event = event || 'keydown'; |
| 1079 } |
| 1080 return parsedKeyCombo; |
| 1081 }, { |
| 1082 combo: keyComboString.split(':').shift() |
| 1083 }); |
| 1084 } |
| 1085 function parseEventString(eventString) { |
| 1086 return eventString.trim().split(' ').map(function(keyComboString) { |
| 1087 return parseKeyComboString(keyComboString); |
| 1088 }); |
| 1089 } |
| 1090 Polymer.IronA11yKeysBehavior = { |
| 1629 properties: { | 1091 properties: { |
| 1630 /** | 1092 keyEventTarget: { |
| 1631 * The closest ancestor element that implements `IronResizableBehavior`. | |
| 1632 */ | |
| 1633 _parentResizable: { | |
| 1634 type: Object, | 1093 type: Object, |
| 1635 observer: '_parentResizableChanged' | 1094 value: function() { |
| 1636 }, | 1095 return this; |
| 1637 | 1096 } |
| 1638 /** | 1097 }, |
| 1639 * True if this element is currently notifying its descedant elements of | 1098 stopKeyboardEventPropagation: { |
| 1640 * resize. | |
| 1641 */ | |
| 1642 _notifyingDescendant: { | |
| 1643 type: Boolean, | 1099 type: Boolean, |
| 1644 value: false | 1100 value: false |
| 1645 } | 1101 }, |
| 1646 }, | 1102 _boundKeyHandlers: { |
| 1647 | 1103 type: Array, |
| 1648 listeners: { | 1104 value: function() { |
| 1649 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | 1105 return []; |
| 1650 }, | 1106 } |
| 1651 | 1107 }, |
| 1652 created: function() { | 1108 _imperativeKeyBindings: { |
| 1653 // We don't really need property effects on these, and also we want them | 1109 type: Object, |
| 1654 // to be created before the `_parentResizable` observer fires: | 1110 value: function() { |
| 1655 this._interestedResizables = []; | 1111 return {}; |
| 1656 this._boundNotifyResize = this.notifyResize.bind(this); | 1112 } |
| 1657 }, | 1113 } |
| 1658 | 1114 }, |
| 1115 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], |
| 1116 keyBindings: {}, |
| 1117 registered: function() { |
| 1118 this._prepKeyBindings(); |
| 1119 }, |
| 1659 attached: function() { | 1120 attached: function() { |
| 1660 this.fire('iron-request-resize-notifications', null, { | 1121 this._listenKeyEventListeners(); |
| 1661 node: this, | 1122 }, |
| 1662 bubbles: true, | 1123 detached: function() { |
| 1124 this._unlistenKeyEventListeners(); |
| 1125 }, |
| 1126 addOwnKeyBinding: function(eventString, handlerName) { |
| 1127 this._imperativeKeyBindings[eventString] = handlerName; |
| 1128 this._prepKeyBindings(); |
| 1129 this._resetKeyEventListeners(); |
| 1130 }, |
| 1131 removeOwnKeyBindings: function() { |
| 1132 this._imperativeKeyBindings = {}; |
| 1133 this._prepKeyBindings(); |
| 1134 this._resetKeyEventListeners(); |
| 1135 }, |
| 1136 keyboardEventMatchesKeys: function(event, eventString) { |
| 1137 var keyCombos = parseEventString(eventString); |
| 1138 for (var i = 0; i < keyCombos.length; ++i) { |
| 1139 if (keyComboMatchesEvent(keyCombos[i], event)) { |
| 1140 return true; |
| 1141 } |
| 1142 } |
| 1143 return false; |
| 1144 }, |
| 1145 _collectKeyBindings: function() { |
| 1146 var keyBindings = this.behaviors.map(function(behavior) { |
| 1147 return behavior.keyBindings; |
| 1148 }); |
| 1149 if (keyBindings.indexOf(this.keyBindings) === -1) { |
| 1150 keyBindings.push(this.keyBindings); |
| 1151 } |
| 1152 return keyBindings; |
| 1153 }, |
| 1154 _prepKeyBindings: function() { |
| 1155 this._keyBindings = {}; |
| 1156 this._collectKeyBindings().forEach(function(keyBindings) { |
| 1157 for (var eventString in keyBindings) { |
| 1158 this._addKeyBinding(eventString, keyBindings[eventString]); |
| 1159 } |
| 1160 }, this); |
| 1161 for (var eventString in this._imperativeKeyBindings) { |
| 1162 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString
]); |
| 1163 } |
| 1164 for (var eventName in this._keyBindings) { |
| 1165 this._keyBindings[eventName].sort(function(kb1, kb2) { |
| 1166 var b1 = kb1[0].hasModifiers; |
| 1167 var b2 = kb2[0].hasModifiers; |
| 1168 return b1 === b2 ? 0 : b1 ? -1 : 1; |
| 1169 }); |
| 1170 } |
| 1171 }, |
| 1172 _addKeyBinding: function(eventString, handlerName) { |
| 1173 parseEventString(eventString).forEach(function(keyCombo) { |
| 1174 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] ||
[]; |
| 1175 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); |
| 1176 }, this); |
| 1177 }, |
| 1178 _resetKeyEventListeners: function() { |
| 1179 this._unlistenKeyEventListeners(); |
| 1180 if (this.isAttached) { |
| 1181 this._listenKeyEventListeners(); |
| 1182 } |
| 1183 }, |
| 1184 _listenKeyEventListeners: function() { |
| 1185 if (!this.keyEventTarget) { |
| 1186 return; |
| 1187 } |
| 1188 Object.keys(this._keyBindings).forEach(function(eventName) { |
| 1189 var keyBindings = this._keyBindings[eventName]; |
| 1190 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
| 1191 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa
ndler ]); |
| 1192 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| 1193 }, this); |
| 1194 }, |
| 1195 _unlistenKeyEventListeners: function() { |
| 1196 var keyHandlerTuple; |
| 1197 var keyEventTarget; |
| 1198 var eventName; |
| 1199 var boundKeyHandler; |
| 1200 while (this._boundKeyHandlers.length) { |
| 1201 keyHandlerTuple = this._boundKeyHandlers.pop(); |
| 1202 keyEventTarget = keyHandlerTuple[0]; |
| 1203 eventName = keyHandlerTuple[1]; |
| 1204 boundKeyHandler = keyHandlerTuple[2]; |
| 1205 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| 1206 } |
| 1207 }, |
| 1208 _onKeyBindingEvent: function(keyBindings, event) { |
| 1209 if (this.stopKeyboardEventPropagation) { |
| 1210 event.stopPropagation(); |
| 1211 } |
| 1212 if (event.defaultPrevented) { |
| 1213 return; |
| 1214 } |
| 1215 for (var i = 0; i < keyBindings.length; i++) { |
| 1216 var keyCombo = keyBindings[i][0]; |
| 1217 var handlerName = keyBindings[i][1]; |
| 1218 if (keyComboMatchesEvent(keyCombo, event)) { |
| 1219 this._triggerKeyHandler(keyCombo, handlerName, event); |
| 1220 if (event.defaultPrevented) { |
| 1221 return; |
| 1222 } |
| 1223 } |
| 1224 } |
| 1225 }, |
| 1226 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 1227 var detail = Object.create(keyCombo); |
| 1228 detail.keyboardEvent = keyboardEvent; |
| 1229 var event = new CustomEvent(keyCombo.event, { |
| 1230 detail: detail, |
| 1663 cancelable: true | 1231 cancelable: true |
| 1664 }); | 1232 }); |
| 1665 | 1233 this[handlerName].call(this, event); |
| 1666 if (!this._parentResizable) { | 1234 if (event.defaultPrevented) { |
| 1667 window.addEventListener('resize', this._boundNotifyResize); | 1235 keyboardEvent.preventDefault(); |
| 1668 this.notifyResize(); | 1236 } |
| 1669 } | |
| 1670 }, | |
| 1671 | |
| 1672 detached: function() { | |
| 1673 if (this._parentResizable) { | |
| 1674 this._parentResizable.stopResizeNotificationsFor(this); | |
| 1675 } else { | |
| 1676 window.removeEventListener('resize', this._boundNotifyResize); | |
| 1677 } | |
| 1678 | |
| 1679 this._parentResizable = null; | |
| 1680 }, | |
| 1681 | |
| 1682 /** | |
| 1683 * Can be called to manually notify a resizable and its descendant | |
| 1684 * resizables of a resize change. | |
| 1685 */ | |
| 1686 notifyResize: function() { | |
| 1687 if (!this.isAttached) { | |
| 1688 return; | |
| 1689 } | |
| 1690 | |
| 1691 this._interestedResizables.forEach(function(resizable) { | |
| 1692 if (this.resizerShouldNotify(resizable)) { | |
| 1693 this._notifyDescendant(resizable); | |
| 1694 } | |
| 1695 }, this); | |
| 1696 | |
| 1697 this._fireResize(); | |
| 1698 }, | |
| 1699 | |
| 1700 /** | |
| 1701 * Used to assign the closest resizable ancestor to this resizable | |
| 1702 * if the ancestor detects a request for notifications. | |
| 1703 */ | |
| 1704 assignParentResizable: function(parentResizable) { | |
| 1705 this._parentResizable = parentResizable; | |
| 1706 }, | |
| 1707 | |
| 1708 /** | |
| 1709 * Used to remove a resizable descendant from the list of descendants | |
| 1710 * that should be notified of a resize change. | |
| 1711 */ | |
| 1712 stopResizeNotificationsFor: function(target) { | |
| 1713 var index = this._interestedResizables.indexOf(target); | |
| 1714 | |
| 1715 if (index > -1) { | |
| 1716 this._interestedResizables.splice(index, 1); | |
| 1717 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | |
| 1718 } | |
| 1719 }, | |
| 1720 | |
| 1721 /** | |
| 1722 * This method can be overridden to filter nested elements that should or | |
| 1723 * should not be notified by the current element. Return true if an element | |
| 1724 * should be notified, or false if it should not be notified. | |
| 1725 * | |
| 1726 * @param {HTMLElement} element A candidate descendant element that | |
| 1727 * implements `IronResizableBehavior`. | |
| 1728 * @return {boolean} True if the `element` should be notified of resize. | |
| 1729 */ | |
| 1730 resizerShouldNotify: function(element) { return true; }, | |
| 1731 | |
| 1732 _onDescendantIronResize: function(event) { | |
| 1733 if (this._notifyingDescendant) { | |
| 1734 event.stopPropagation(); | |
| 1735 return; | |
| 1736 } | |
| 1737 | |
| 1738 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
| 1739 // otherwise non-bubbling event "just work." We do it manually here for | |
| 1740 // the case where Polymer is not using shadow roots for whatever reason: | |
| 1741 if (!Polymer.Settings.useShadow) { | |
| 1742 this._fireResize(); | |
| 1743 } | |
| 1744 }, | |
| 1745 | |
| 1746 _fireResize: function() { | |
| 1747 this.fire('iron-resize', null, { | |
| 1748 node: this, | |
| 1749 bubbles: false | |
| 1750 }); | |
| 1751 }, | |
| 1752 | |
| 1753 _onIronRequestResizeNotifications: function(event) { | |
| 1754 var target = event.path ? event.path[0] : event.target; | |
| 1755 | |
| 1756 if (target === this) { | |
| 1757 return; | |
| 1758 } | |
| 1759 | |
| 1760 if (this._interestedResizables.indexOf(target) === -1) { | |
| 1761 this._interestedResizables.push(target); | |
| 1762 this.listen(target, 'iron-resize', '_onDescendantIronResize'); | |
| 1763 } | |
| 1764 | |
| 1765 target.assignParentResizable(this); | |
| 1766 this._notifyDescendant(target); | |
| 1767 | |
| 1768 event.stopPropagation(); | |
| 1769 }, | |
| 1770 | |
| 1771 _parentResizableChanged: function(parentResizable) { | |
| 1772 if (parentResizable) { | |
| 1773 window.removeEventListener('resize', this._boundNotifyResize); | |
| 1774 } | |
| 1775 }, | |
| 1776 | |
| 1777 _notifyDescendant: function(descendant) { | |
| 1778 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
| 1779 // important not to notify them if the parent is not attached yet (or | |
| 1780 // else they will get redundantly notified when the parent attaches). | |
| 1781 if (!this.isAttached) { | |
| 1782 return; | |
| 1783 } | |
| 1784 | |
| 1785 this._notifyingDescendant = true; | |
| 1786 descendant.notifyResize(); | |
| 1787 this._notifyingDescendant = false; | |
| 1788 } | 1237 } |
| 1789 }; | 1238 }; |
| 1239 })(); |
| 1240 |
| 1241 Polymer.IronScrollTargetBehavior = { |
| 1242 properties: { |
| 1243 scrollTarget: { |
| 1244 type: HTMLElement, |
| 1245 value: function() { |
| 1246 return this._defaultScrollTarget; |
| 1247 } |
| 1248 } |
| 1249 }, |
| 1250 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ], |
| 1251 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 1252 var eventTarget; |
| 1253 if (this._oldScrollTarget) { |
| 1254 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro
llTarget; |
| 1255 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 1256 this._oldScrollTarget = null; |
| 1257 } |
| 1258 if (!isAttached) { |
| 1259 return; |
| 1260 } |
| 1261 if (scrollTarget === 'document') { |
| 1262 this.scrollTarget = this._doc; |
| 1263 } else if (typeof scrollTarget === 'string') { |
| 1264 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer.
dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 1265 } else if (this._isValidScrollTarget()) { |
| 1266 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 1267 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler
.bind(this); |
| 1268 this._oldScrollTarget = scrollTarget; |
| 1269 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 1270 } |
| 1271 }, |
| 1272 _scrollHandler: function scrollHandler() {}, |
| 1273 get _defaultScrollTarget() { |
| 1274 return this._doc; |
| 1275 }, |
| 1276 get _doc() { |
| 1277 return this.ownerDocument.documentElement; |
| 1278 }, |
| 1279 get _scrollTop() { |
| 1280 if (this._isValidScrollTarget()) { |
| 1281 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT
arget.scrollTop; |
| 1282 } |
| 1283 return 0; |
| 1284 }, |
| 1285 get _scrollLeft() { |
| 1286 if (this._isValidScrollTarget()) { |
| 1287 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT
arget.scrollLeft; |
| 1288 } |
| 1289 return 0; |
| 1290 }, |
| 1291 set _scrollTop(top) { |
| 1292 if (this.scrollTarget === this._doc) { |
| 1293 window.scrollTo(window.pageXOffset, top); |
| 1294 } else if (this._isValidScrollTarget()) { |
| 1295 this.scrollTarget.scrollTop = top; |
| 1296 } |
| 1297 }, |
| 1298 set _scrollLeft(left) { |
| 1299 if (this.scrollTarget === this._doc) { |
| 1300 window.scrollTo(left, window.pageYOffset); |
| 1301 } else if (this._isValidScrollTarget()) { |
| 1302 this.scrollTarget.scrollLeft = left; |
| 1303 } |
| 1304 }, |
| 1305 scroll: function(left, top) { |
| 1306 if (this.scrollTarget === this._doc) { |
| 1307 window.scrollTo(left, top); |
| 1308 } else if (this._isValidScrollTarget()) { |
| 1309 this.scrollTarget.scrollLeft = left; |
| 1310 this.scrollTarget.scrollTop = top; |
| 1311 } |
| 1312 }, |
| 1313 get _scrollTargetWidth() { |
| 1314 if (this._isValidScrollTarget()) { |
| 1315 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa
rget.offsetWidth; |
| 1316 } |
| 1317 return 0; |
| 1318 }, |
| 1319 get _scrollTargetHeight() { |
| 1320 if (this._isValidScrollTarget()) { |
| 1321 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT
arget.offsetHeight; |
| 1322 } |
| 1323 return 0; |
| 1324 }, |
| 1325 _isValidScrollTarget: function() { |
| 1326 return this.scrollTarget instanceof HTMLElement; |
| 1327 } |
| 1328 }; |
| 1329 |
| 1790 (function() { | 1330 (function() { |
| 1791 'use strict'; | |
| 1792 | |
| 1793 /** | |
| 1794 * Chrome uses an older version of DOM Level 3 Keyboard Events | |
| 1795 * | |
| 1796 * Most keys are labeled as text, but some are Unicode codepoints. | |
| 1797 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | |
| 1798 */ | |
| 1799 var KEY_IDENTIFIER = { | |
| 1800 'U+0008': 'backspace', | |
| 1801 'U+0009': 'tab', | |
| 1802 'U+001B': 'esc', | |
| 1803 'U+0020': 'space', | |
| 1804 'U+007F': 'del' | |
| 1805 }; | |
| 1806 | |
| 1807 /** | |
| 1808 * Special table for KeyboardEvent.keyCode. | |
| 1809 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | |
| 1810 * than that. | |
| 1811 * | |
| 1812 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
| 1813 */ | |
| 1814 var KEY_CODE = { | |
| 1815 8: 'backspace', | |
| 1816 9: 'tab', | |
| 1817 13: 'enter', | |
| 1818 27: 'esc', | |
| 1819 33: 'pageup', | |
| 1820 34: 'pagedown', | |
| 1821 35: 'end', | |
| 1822 36: 'home', | |
| 1823 32: 'space', | |
| 1824 37: 'left', | |
| 1825 38: 'up', | |
| 1826 39: 'right', | |
| 1827 40: 'down', | |
| 1828 46: 'del', | |
| 1829 106: '*' | |
| 1830 }; | |
| 1831 | |
| 1832 /** | |
| 1833 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
| 1834 * combo string to the property name that references those same keys | |
| 1835 * in a KeyboardEvent instance. | |
| 1836 */ | |
| 1837 var MODIFIER_KEYS = { | |
| 1838 'shift': 'shiftKey', | |
| 1839 'ctrl': 'ctrlKey', | |
| 1840 'alt': 'altKey', | |
| 1841 'meta': 'metaKey' | |
| 1842 }; | |
| 1843 | |
| 1844 /** | |
| 1845 * KeyboardEvent.key is mostly represented by printable character made by | |
| 1846 * the keyboard, with unprintable keys labeled nicely. | |
| 1847 * | |
| 1848 * However, on OS X, Alt+char can make a Unicode character that follows an | |
| 1849 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
| 1850 */ | |
| 1851 var KEY_CHAR = /[a-z0-9*]/; | |
| 1852 | |
| 1853 /** | |
| 1854 * Matches a keyIdentifier string. | |
| 1855 */ | |
| 1856 var IDENT_CHAR = /U\+/; | |
| 1857 | |
| 1858 /** | |
| 1859 * Matches arrow keys in Gecko 27.0+ | |
| 1860 */ | |
| 1861 var ARROW_KEY = /^arrow/; | |
| 1862 | |
| 1863 /** | |
| 1864 * Matches space keys everywhere (notably including IE10's exceptional name | |
| 1865 * `spacebar`). | |
| 1866 */ | |
| 1867 var SPACE_KEY = /^space(bar)?/; | |
| 1868 | |
| 1869 /** | |
| 1870 * Matches ESC key. | |
| 1871 * | |
| 1872 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
| 1873 */ | |
| 1874 var ESC_KEY = /^escape$/; | |
| 1875 | |
| 1876 /** | |
| 1877 * Transforms the key. | |
| 1878 * @param {string} key The KeyBoardEvent.key | |
| 1879 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
| 1880 * alpha-numeric characters. | |
| 1881 */ | |
| 1882 function transformKey(key, noSpecialChars) { | |
| 1883 var validKey = ''; | |
| 1884 if (key) { | |
| 1885 var lKey = key.toLowerCase(); | |
| 1886 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
| 1887 validKey = 'space'; | |
| 1888 } else if (ESC_KEY.test(lKey)) { | |
| 1889 validKey = 'esc'; | |
| 1890 } else if (lKey.length == 1) { | |
| 1891 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
| 1892 validKey = lKey; | |
| 1893 } | |
| 1894 } else if (ARROW_KEY.test(lKey)) { | |
| 1895 validKey = lKey.replace('arrow', ''); | |
| 1896 } else if (lKey == 'multiply') { | |
| 1897 // numpad '*' can map to Multiply on IE/Windows | |
| 1898 validKey = '*'; | |
| 1899 } else { | |
| 1900 validKey = lKey; | |
| 1901 } | |
| 1902 } | |
| 1903 return validKey; | |
| 1904 } | |
| 1905 | |
| 1906 function transformKeyIdentifier(keyIdent) { | |
| 1907 var validKey = ''; | |
| 1908 if (keyIdent) { | |
| 1909 if (keyIdent in KEY_IDENTIFIER) { | |
| 1910 validKey = KEY_IDENTIFIER[keyIdent]; | |
| 1911 } else if (IDENT_CHAR.test(keyIdent)) { | |
| 1912 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
| 1913 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
| 1914 } else { | |
| 1915 validKey = keyIdent.toLowerCase(); | |
| 1916 } | |
| 1917 } | |
| 1918 return validKey; | |
| 1919 } | |
| 1920 | |
| 1921 function transformKeyCode(keyCode) { | |
| 1922 var validKey = ''; | |
| 1923 if (Number(keyCode)) { | |
| 1924 if (keyCode >= 65 && keyCode <= 90) { | |
| 1925 // ascii a-z | |
| 1926 // lowercase is 32 offset from uppercase | |
| 1927 validKey = String.fromCharCode(32 + keyCode); | |
| 1928 } else if (keyCode >= 112 && keyCode <= 123) { | |
| 1929 // function keys f1-f12 | |
| 1930 validKey = 'f' + (keyCode - 112); | |
| 1931 } else if (keyCode >= 48 && keyCode <= 57) { | |
| 1932 // top 0-9 keys | |
| 1933 validKey = String(keyCode - 48); | |
| 1934 } else if (keyCode >= 96 && keyCode <= 105) { | |
| 1935 // num pad 0-9 | |
| 1936 validKey = String(keyCode - 96); | |
| 1937 } else { | |
| 1938 validKey = KEY_CODE[keyCode]; | |
| 1939 } | |
| 1940 } | |
| 1941 return validKey; | |
| 1942 } | |
| 1943 | |
| 1944 /** | |
| 1945 * Calculates the normalized key for a KeyboardEvent. | |
| 1946 * @param {KeyboardEvent} keyEvent | |
| 1947 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
| 1948 * transformation to alpha-numeric chars. This is useful with key | |
| 1949 * combinations like shift + 2, which on FF for MacOS produces | |
| 1950 * keyEvent.key = @ | |
| 1951 * To get 2 returned, set noSpecialChars = true | |
| 1952 * To get @ returned, set noSpecialChars = false | |
| 1953 */ | |
| 1954 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
| 1955 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
| 1956 // .detail.key to support artificial keyboard events. | |
| 1957 return transformKey(keyEvent.key, noSpecialChars) || | |
| 1958 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
| 1959 transformKeyCode(keyEvent.keyCode) || | |
| 1960 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | |
| 1961 } | |
| 1962 | |
| 1963 function keyComboMatchesEvent(keyCombo, event) { | |
| 1964 // For combos with modifiers we support only alpha-numeric keys | |
| 1965 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
| 1966 return keyEvent === keyCombo.key && | |
| 1967 (!keyCombo.hasModifiers || ( | |
| 1968 !!event.shiftKey === !!keyCombo.shiftKey && | |
| 1969 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
| 1970 !!event.altKey === !!keyCombo.altKey && | |
| 1971 !!event.metaKey === !!keyCombo.metaKey) | |
| 1972 ); | |
| 1973 } | |
| 1974 | |
| 1975 function parseKeyComboString(keyComboString) { | |
| 1976 if (keyComboString.length === 1) { | |
| 1977 return { | |
| 1978 combo: keyComboString, | |
| 1979 key: keyComboString, | |
| 1980 event: 'keydown' | |
| 1981 }; | |
| 1982 } | |
| 1983 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
| 1984 var eventParts = keyComboPart.split(':'); | |
| 1985 var keyName = eventParts[0]; | |
| 1986 var event = eventParts[1]; | |
| 1987 | |
| 1988 if (keyName in MODIFIER_KEYS) { | |
| 1989 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
| 1990 parsedKeyCombo.hasModifiers = true; | |
| 1991 } else { | |
| 1992 parsedKeyCombo.key = keyName; | |
| 1993 parsedKeyCombo.event = event || 'keydown'; | |
| 1994 } | |
| 1995 | |
| 1996 return parsedKeyCombo; | |
| 1997 }, { | |
| 1998 combo: keyComboString.split(':').shift() | |
| 1999 }); | |
| 2000 } | |
| 2001 | |
| 2002 function parseEventString(eventString) { | |
| 2003 return eventString.trim().split(' ').map(function(keyComboString) { | |
| 2004 return parseKeyComboString(keyComboString); | |
| 2005 }); | |
| 2006 } | |
| 2007 | |
| 2008 /** | |
| 2009 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
| 2010 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
| 2011 * The element takes care of browser differences with respect to Keyboard ev
ents | |
| 2012 * and uses an expressive syntax to filter key presses. | |
| 2013 * | |
| 2014 * Use the `keyBindings` prototype property to express what combination of k
eys | |
| 2015 * will trigger the callback. A key binding has the format | |
| 2016 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
| 2017 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
| 2018 * | |
| 2019 * keyBindings: { | |
| 2020 * 'space': '_onKeydown', // same as 'space:keydown' | |
| 2021 * 'shift+tab': '_onKeydown', | |
| 2022 * 'enter:keypress': '_onKeypress', | |
| 2023 * 'esc:keyup': '_onKeyup' | |
| 2024 * } | |
| 2025 * | |
| 2026 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
| 2027 * | |
| 2028 * _onKeydown: function(event) { | |
| 2029 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
| 2030 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
| 2031 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
| 2032 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
| 2033 * } | |
| 2034 * | |
| 2035 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
| 2036 * node. | |
| 2037 * | |
| 2038 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
| 2039 * for an example. | |
| 2040 * | |
| 2041 * @demo demo/index.html | |
| 2042 * @polymerBehavior | |
| 2043 */ | |
| 2044 Polymer.IronA11yKeysBehavior = { | |
| 2045 properties: { | |
| 2046 /** | |
| 2047 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
| 2048 * `null` to disable the listeners. | |
| 2049 * @type {?EventTarget} | |
| 2050 */ | |
| 2051 keyEventTarget: { | |
| 2052 type: Object, | |
| 2053 value: function() { | |
| 2054 return this; | |
| 2055 } | |
| 2056 }, | |
| 2057 | |
| 2058 /** | |
| 2059 * If true, this property will cause the implementing element to | |
| 2060 * automatically stop propagation on any handled KeyboardEvents. | |
| 2061 */ | |
| 2062 stopKeyboardEventPropagation: { | |
| 2063 type: Boolean, | |
| 2064 value: false | |
| 2065 }, | |
| 2066 | |
| 2067 _boundKeyHandlers: { | |
| 2068 type: Array, | |
| 2069 value: function() { | |
| 2070 return []; | |
| 2071 } | |
| 2072 }, | |
| 2073 | |
| 2074 // We use this due to a limitation in IE10 where instances will have | |
| 2075 // own properties of everything on the "prototype". | |
| 2076 _imperativeKeyBindings: { | |
| 2077 type: Object, | |
| 2078 value: function() { | |
| 2079 return {}; | |
| 2080 } | |
| 2081 } | |
| 2082 }, | |
| 2083 | |
| 2084 observers: [ | |
| 2085 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | |
| 2086 ], | |
| 2087 | |
| 2088 | |
| 2089 /** | |
| 2090 * To be used to express what combination of keys will trigger the relati
ve | |
| 2091 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
| 2092 * @type {Object} | |
| 2093 */ | |
| 2094 keyBindings: {}, | |
| 2095 | |
| 2096 registered: function() { | |
| 2097 this._prepKeyBindings(); | |
| 2098 }, | |
| 2099 | |
| 2100 attached: function() { | |
| 2101 this._listenKeyEventListeners(); | |
| 2102 }, | |
| 2103 | |
| 2104 detached: function() { | |
| 2105 this._unlistenKeyEventListeners(); | |
| 2106 }, | |
| 2107 | |
| 2108 /** | |
| 2109 * Can be used to imperatively add a key binding to the implementing | |
| 2110 * element. This is the imperative equivalent of declaring a keybinding | |
| 2111 * in the `keyBindings` prototype property. | |
| 2112 */ | |
| 2113 addOwnKeyBinding: function(eventString, handlerName) { | |
| 2114 this._imperativeKeyBindings[eventString] = handlerName; | |
| 2115 this._prepKeyBindings(); | |
| 2116 this._resetKeyEventListeners(); | |
| 2117 }, | |
| 2118 | |
| 2119 /** | |
| 2120 * When called, will remove all imperatively-added key bindings. | |
| 2121 */ | |
| 2122 removeOwnKeyBindings: function() { | |
| 2123 this._imperativeKeyBindings = {}; | |
| 2124 this._prepKeyBindings(); | |
| 2125 this._resetKeyEventListeners(); | |
| 2126 }, | |
| 2127 | |
| 2128 /** | |
| 2129 * Returns true if a keyboard event matches `eventString`. | |
| 2130 * | |
| 2131 * @param {KeyboardEvent} event | |
| 2132 * @param {string} eventString | |
| 2133 * @return {boolean} | |
| 2134 */ | |
| 2135 keyboardEventMatchesKeys: function(event, eventString) { | |
| 2136 var keyCombos = parseEventString(eventString); | |
| 2137 for (var i = 0; i < keyCombos.length; ++i) { | |
| 2138 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
| 2139 return true; | |
| 2140 } | |
| 2141 } | |
| 2142 return false; | |
| 2143 }, | |
| 2144 | |
| 2145 _collectKeyBindings: function() { | |
| 2146 var keyBindings = this.behaviors.map(function(behavior) { | |
| 2147 return behavior.keyBindings; | |
| 2148 }); | |
| 2149 | |
| 2150 if (keyBindings.indexOf(this.keyBindings) === -1) { | |
| 2151 keyBindings.push(this.keyBindings); | |
| 2152 } | |
| 2153 | |
| 2154 return keyBindings; | |
| 2155 }, | |
| 2156 | |
| 2157 _prepKeyBindings: function() { | |
| 2158 this._keyBindings = {}; | |
| 2159 | |
| 2160 this._collectKeyBindings().forEach(function(keyBindings) { | |
| 2161 for (var eventString in keyBindings) { | |
| 2162 this._addKeyBinding(eventString, keyBindings[eventString]); | |
| 2163 } | |
| 2164 }, this); | |
| 2165 | |
| 2166 for (var eventString in this._imperativeKeyBindings) { | |
| 2167 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
| 2168 } | |
| 2169 | |
| 2170 // Give precedence to combos with modifiers to be checked first. | |
| 2171 for (var eventName in this._keyBindings) { | |
| 2172 this._keyBindings[eventName].sort(function (kb1, kb2) { | |
| 2173 var b1 = kb1[0].hasModifiers; | |
| 2174 var b2 = kb2[0].hasModifiers; | |
| 2175 return (b1 === b2) ? 0 : b1 ? -1 : 1; | |
| 2176 }) | |
| 2177 } | |
| 2178 }, | |
| 2179 | |
| 2180 _addKeyBinding: function(eventString, handlerName) { | |
| 2181 parseEventString(eventString).forEach(function(keyCombo) { | |
| 2182 this._keyBindings[keyCombo.event] = | |
| 2183 this._keyBindings[keyCombo.event] || []; | |
| 2184 | |
| 2185 this._keyBindings[keyCombo.event].push([ | |
| 2186 keyCombo, | |
| 2187 handlerName | |
| 2188 ]); | |
| 2189 }, this); | |
| 2190 }, | |
| 2191 | |
| 2192 _resetKeyEventListeners: function() { | |
| 2193 this._unlistenKeyEventListeners(); | |
| 2194 | |
| 2195 if (this.isAttached) { | |
| 2196 this._listenKeyEventListeners(); | |
| 2197 } | |
| 2198 }, | |
| 2199 | |
| 2200 _listenKeyEventListeners: function() { | |
| 2201 if (!this.keyEventTarget) { | |
| 2202 return; | |
| 2203 } | |
| 2204 Object.keys(this._keyBindings).forEach(function(eventName) { | |
| 2205 var keyBindings = this._keyBindings[eventName]; | |
| 2206 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | |
| 2207 | |
| 2208 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | |
| 2209 | |
| 2210 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | |
| 2211 }, this); | |
| 2212 }, | |
| 2213 | |
| 2214 _unlistenKeyEventListeners: function() { | |
| 2215 var keyHandlerTuple; | |
| 2216 var keyEventTarget; | |
| 2217 var eventName; | |
| 2218 var boundKeyHandler; | |
| 2219 | |
| 2220 while (this._boundKeyHandlers.length) { | |
| 2221 // My kingdom for block-scope binding and destructuring assignment.. | |
| 2222 keyHandlerTuple = this._boundKeyHandlers.pop(); | |
| 2223 keyEventTarget = keyHandlerTuple[0]; | |
| 2224 eventName = keyHandlerTuple[1]; | |
| 2225 boundKeyHandler = keyHandlerTuple[2]; | |
| 2226 | |
| 2227 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | |
| 2228 } | |
| 2229 }, | |
| 2230 | |
| 2231 _onKeyBindingEvent: function(keyBindings, event) { | |
| 2232 if (this.stopKeyboardEventPropagation) { | |
| 2233 event.stopPropagation(); | |
| 2234 } | |
| 2235 | |
| 2236 // if event has been already prevented, don't do anything | |
| 2237 if (event.defaultPrevented) { | |
| 2238 return; | |
| 2239 } | |
| 2240 | |
| 2241 for (var i = 0; i < keyBindings.length; i++) { | |
| 2242 var keyCombo = keyBindings[i][0]; | |
| 2243 var handlerName = keyBindings[i][1]; | |
| 2244 if (keyComboMatchesEvent(keyCombo, event)) { | |
| 2245 this._triggerKeyHandler(keyCombo, handlerName, event); | |
| 2246 // exit the loop if eventDefault was prevented | |
| 2247 if (event.defaultPrevented) { | |
| 2248 return; | |
| 2249 } | |
| 2250 } | |
| 2251 } | |
| 2252 }, | |
| 2253 | |
| 2254 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | |
| 2255 var detail = Object.create(keyCombo); | |
| 2256 detail.keyboardEvent = keyboardEvent; | |
| 2257 var event = new CustomEvent(keyCombo.event, { | |
| 2258 detail: detail, | |
| 2259 cancelable: true | |
| 2260 }); | |
| 2261 this[handlerName].call(this, event); | |
| 2262 if (event.defaultPrevented) { | |
| 2263 keyboardEvent.preventDefault(); | |
| 2264 } | |
| 2265 } | |
| 2266 }; | |
| 2267 })(); | |
| 2268 /** | |
| 2269 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
| 2270 * designated scroll target. | |
| 2271 * | |
| 2272 * Elements that consume this behavior can override the `_scrollHandler` | |
| 2273 * method to add logic on the scroll event. | |
| 2274 * | |
| 2275 * @demo demo/scrolling-region.html Scrolling Region | |
| 2276 * @demo demo/document.html Document Element | |
| 2277 * @polymerBehavior | |
| 2278 */ | |
| 2279 Polymer.IronScrollTargetBehavior = { | |
| 2280 | |
| 2281 properties: { | |
| 2282 | |
| 2283 /** | |
| 2284 * Specifies the element that will handle the scroll event | |
| 2285 * on the behalf of the current element. This is typically a reference to
an element, | |
| 2286 * but there are a few more posibilities: | |
| 2287 * | |
| 2288 * ### Elements id | |
| 2289 * | |
| 2290 *```html | |
| 2291 * <div id="scrollable-element" style="overflow: auto;"> | |
| 2292 * <x-element scroll-target="scrollable-element"> | |
| 2293 * \x3c!-- Content--\x3e | |
| 2294 * </x-element> | |
| 2295 * </div> | |
| 2296 *``` | |
| 2297 * In this case, the `scrollTarget` will point to the outer div element. | |
| 2298 * | |
| 2299 * ### Document scrolling | |
| 2300 * | |
| 2301 * For document scrolling, you can use the reserved word `document`: | |
| 2302 * | |
| 2303 *```html | |
| 2304 * <x-element scroll-target="document"> | |
| 2305 * \x3c!-- Content --\x3e | |
| 2306 * </x-element> | |
| 2307 *``` | |
| 2308 * | |
| 2309 * ### Elements reference | |
| 2310 * | |
| 2311 *```js | |
| 2312 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
| 2313 *``` | |
| 2314 * | |
| 2315 * @type {HTMLElement} | |
| 2316 */ | |
| 2317 scrollTarget: { | |
| 2318 type: HTMLElement, | |
| 2319 value: function() { | |
| 2320 return this._defaultScrollTarget; | |
| 2321 } | |
| 2322 } | |
| 2323 }, | |
| 2324 | |
| 2325 observers: [ | |
| 2326 '_scrollTargetChanged(scrollTarget, isAttached)' | |
| 2327 ], | |
| 2328 | |
| 2329 _scrollTargetChanged: function(scrollTarget, isAttached) { | |
| 2330 var eventTarget; | |
| 2331 | |
| 2332 if (this._oldScrollTarget) { | |
| 2333 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | |
| 2334 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | |
| 2335 this._oldScrollTarget = null; | |
| 2336 } | |
| 2337 | |
| 2338 if (!isAttached) { | |
| 2339 return; | |
| 2340 } | |
| 2341 // Support element id references | |
| 2342 if (scrollTarget === 'document') { | |
| 2343 | |
| 2344 this.scrollTarget = this._doc; | |
| 2345 | |
| 2346 } else if (typeof scrollTarget === 'string') { | |
| 2347 | |
| 2348 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | |
| 2349 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | |
| 2350 | |
| 2351 } else if (this._isValidScrollTarget()) { | |
| 2352 | |
| 2353 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | |
| 2354 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | |
| 2355 this._oldScrollTarget = scrollTarget; | |
| 2356 | |
| 2357 eventTarget.addEventListener('scroll', this._boundScrollHandler); | |
| 2358 } | |
| 2359 }, | |
| 2360 | |
| 2361 /** | |
| 2362 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
| 2363 * | |
| 2364 * @protected | |
| 2365 */ | |
| 2366 _scrollHandler: function scrollHandler() {}, | |
| 2367 | |
| 2368 /** | |
| 2369 * The default scroll target. Consumers of this behavior may want to customi
ze | |
| 2370 * the default scroll target. | |
| 2371 * | |
| 2372 * @type {Element} | |
| 2373 */ | |
| 2374 get _defaultScrollTarget() { | |
| 2375 return this._doc; | |
| 2376 }, | |
| 2377 | |
| 2378 /** | |
| 2379 * Shortcut for the document element | |
| 2380 * | |
| 2381 * @type {Element} | |
| 2382 */ | |
| 2383 get _doc() { | |
| 2384 return this.ownerDocument.documentElement; | |
| 2385 }, | |
| 2386 | |
| 2387 /** | |
| 2388 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 2389 * | |
| 2390 * @type {number} | |
| 2391 */ | |
| 2392 get _scrollTop() { | |
| 2393 if (this._isValidScrollTarget()) { | |
| 2394 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
| 2395 } | |
| 2396 return 0; | |
| 2397 }, | |
| 2398 | |
| 2399 /** | |
| 2400 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
| 2401 * | |
| 2402 * @type {number} | |
| 2403 */ | |
| 2404 get _scrollLeft() { | |
| 2405 if (this._isValidScrollTarget()) { | |
| 2406 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
| 2407 } | |
| 2408 return 0; | |
| 2409 }, | |
| 2410 | |
| 2411 /** | |
| 2412 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 2413 * | |
| 2414 * @type {number} | |
| 2415 */ | |
| 2416 set _scrollTop(top) { | |
| 2417 if (this.scrollTarget === this._doc) { | |
| 2418 window.scrollTo(window.pageXOffset, top); | |
| 2419 } else if (this._isValidScrollTarget()) { | |
| 2420 this.scrollTarget.scrollTop = top; | |
| 2421 } | |
| 2422 }, | |
| 2423 | |
| 2424 /** | |
| 2425 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
| 2426 * | |
| 2427 * @type {number} | |
| 2428 */ | |
| 2429 set _scrollLeft(left) { | |
| 2430 if (this.scrollTarget === this._doc) { | |
| 2431 window.scrollTo(left, window.pageYOffset); | |
| 2432 } else if (this._isValidScrollTarget()) { | |
| 2433 this.scrollTarget.scrollLeft = left; | |
| 2434 } | |
| 2435 }, | |
| 2436 | |
| 2437 /** | |
| 2438 * Scrolls the content to a particular place. | |
| 2439 * | |
| 2440 * @method scroll | |
| 2441 * @param {number} left The left position | |
| 2442 * @param {number} top The top position | |
| 2443 */ | |
| 2444 scroll: function(left, top) { | |
| 2445 if (this.scrollTarget === this._doc) { | |
| 2446 window.scrollTo(left, top); | |
| 2447 } else if (this._isValidScrollTarget()) { | |
| 2448 this.scrollTarget.scrollLeft = left; | |
| 2449 this.scrollTarget.scrollTop = top; | |
| 2450 } | |
| 2451 }, | |
| 2452 | |
| 2453 /** | |
| 2454 * Gets the width of the scroll target. | |
| 2455 * | |
| 2456 * @type {number} | |
| 2457 */ | |
| 2458 get _scrollTargetWidth() { | |
| 2459 if (this._isValidScrollTarget()) { | |
| 2460 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
| 2461 } | |
| 2462 return 0; | |
| 2463 }, | |
| 2464 | |
| 2465 /** | |
| 2466 * Gets the height of the scroll target. | |
| 2467 * | |
| 2468 * @type {number} | |
| 2469 */ | |
| 2470 get _scrollTargetHeight() { | |
| 2471 if (this._isValidScrollTarget()) { | |
| 2472 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
| 2473 } | |
| 2474 return 0; | |
| 2475 }, | |
| 2476 | |
| 2477 /** | |
| 2478 * Returns true if the scroll target is a valid HTMLElement. | |
| 2479 * | |
| 2480 * @return {boolean} | |
| 2481 */ | |
| 2482 _isValidScrollTarget: function() { | |
| 2483 return this.scrollTarget instanceof HTMLElement; | |
| 2484 } | |
| 2485 }; | |
| 2486 (function() { | |
| 2487 | |
| 2488 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 1331 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
| 2489 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 1332 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
| 2490 var DEFAULT_PHYSICAL_COUNT = 3; | 1333 var DEFAULT_PHYSICAL_COUNT = 3; |
| 2491 var HIDDEN_Y = '-10000px'; | 1334 var HIDDEN_Y = '-10000px'; |
| 2492 var DEFAULT_GRID_SIZE = 200; | 1335 var DEFAULT_GRID_SIZE = 200; |
| 2493 var SECRET_TABINDEX = -100; | 1336 var SECRET_TABINDEX = -100; |
| 2494 | |
| 2495 Polymer({ | 1337 Polymer({ |
| 2496 | |
| 2497 is: 'iron-list', | 1338 is: 'iron-list', |
| 2498 | |
| 2499 properties: { | 1339 properties: { |
| 2500 | |
| 2501 /** | |
| 2502 * An array containing items determining how many instances of the templat
e | |
| 2503 * to stamp and that that each template instance should bind to. | |
| 2504 */ | |
| 2505 items: { | 1340 items: { |
| 2506 type: Array | 1341 type: Array |
| 2507 }, | 1342 }, |
| 2508 | |
| 2509 /** | |
| 2510 * The max count of physical items the pool can extend to. | |
| 2511 */ | |
| 2512 maxPhysicalCount: { | 1343 maxPhysicalCount: { |
| 2513 type: Number, | 1344 type: Number, |
| 2514 value: 500 | 1345 value: 500 |
| 2515 }, | 1346 }, |
| 2516 | |
| 2517 /** | |
| 2518 * The name of the variable to add to the binding scope for the array | |
| 2519 * element associated with a given template instance. | |
| 2520 */ | |
| 2521 as: { | 1347 as: { |
| 2522 type: String, | 1348 type: String, |
| 2523 value: 'item' | 1349 value: 'item' |
| 2524 }, | 1350 }, |
| 2525 | |
| 2526 /** | |
| 2527 * The name of the variable to add to the binding scope with the index | |
| 2528 * for the row. | |
| 2529 */ | |
| 2530 indexAs: { | 1351 indexAs: { |
| 2531 type: String, | 1352 type: String, |
| 2532 value: 'index' | 1353 value: 'index' |
| 2533 }, | 1354 }, |
| 2534 | |
| 2535 /** | |
| 2536 * The name of the variable to add to the binding scope to indicate | |
| 2537 * if the row is selected. | |
| 2538 */ | |
| 2539 selectedAs: { | 1355 selectedAs: { |
| 2540 type: String, | 1356 type: String, |
| 2541 value: 'selected' | 1357 value: 'selected' |
| 2542 }, | 1358 }, |
| 2543 | |
| 2544 /** | |
| 2545 * When true, the list is rendered as a grid. Grid items must have | |
| 2546 * fixed width and height set via CSS. e.g. | |
| 2547 * | |
| 2548 * ```html | |
| 2549 * <iron-list grid> | |
| 2550 * <template> | |
| 2551 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
| 2552 * </template> | |
| 2553 * </iron-list> | |
| 2554 * ``` | |
| 2555 */ | |
| 2556 grid: { | 1359 grid: { |
| 2557 type: Boolean, | 1360 type: Boolean, |
| 2558 value: false, | 1361 value: false, |
| 2559 reflectToAttribute: true | 1362 reflectToAttribute: true |
| 2560 }, | 1363 }, |
| 2561 | |
| 2562 /** | |
| 2563 * When true, tapping a row will select the item, placing its data model | |
| 2564 * in the set of selected items retrievable via the selection property. | |
| 2565 * | |
| 2566 * Note that tapping focusable elements within the list item will not | |
| 2567 * result in selection, since they are presumed to have their * own action
. | |
| 2568 */ | |
| 2569 selectionEnabled: { | 1364 selectionEnabled: { |
| 2570 type: Boolean, | 1365 type: Boolean, |
| 2571 value: false | 1366 value: false |
| 2572 }, | 1367 }, |
| 2573 | |
| 2574 /** | |
| 2575 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
| 2576 * if no item is selected. | |
| 2577 */ | |
| 2578 selectedItem: { | 1368 selectedItem: { |
| 2579 type: Object, | 1369 type: Object, |
| 2580 notify: true | 1370 notify: true |
| 2581 }, | 1371 }, |
| 2582 | |
| 2583 /** | |
| 2584 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
| 2585 */ | |
| 2586 selectedItems: { | 1372 selectedItems: { |
| 2587 type: Object, | 1373 type: Object, |
| 2588 notify: true | 1374 notify: true |
| 2589 }, | 1375 }, |
| 2590 | |
| 2591 /** | |
| 2592 * When `true`, multiple items may be selected at once (in this case, | |
| 2593 * `selected` is an array of currently selected items). When `false`, | |
| 2594 * only one item may be selected at a time. | |
| 2595 */ | |
| 2596 multiSelection: { | 1376 multiSelection: { |
| 2597 type: Boolean, | 1377 type: Boolean, |
| 2598 value: false | 1378 value: false |
| 2599 } | 1379 } |
| 2600 }, | 1380 }, |
| 2601 | 1381 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn
abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)'
], |
| 2602 observers: [ | 1382 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro
nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ], |
| 2603 '_itemsChanged(items.*)', | |
| 2604 '_selectionEnabledChanged(selectionEnabled)', | |
| 2605 '_multiSelectionChanged(multiSelection)', | |
| 2606 '_setOverflow(scrollTarget)' | |
| 2607 ], | |
| 2608 | |
| 2609 behaviors: [ | |
| 2610 Polymer.Templatizer, | |
| 2611 Polymer.IronResizableBehavior, | |
| 2612 Polymer.IronA11yKeysBehavior, | |
| 2613 Polymer.IronScrollTargetBehavior | |
| 2614 ], | |
| 2615 | |
| 2616 keyBindings: { | 1383 keyBindings: { |
| 2617 'up': '_didMoveUp', | 1384 up: '_didMoveUp', |
| 2618 'down': '_didMoveDown', | 1385 down: '_didMoveDown', |
| 2619 'enter': '_didEnter' | 1386 enter: '_didEnter' |
| 2620 }, | 1387 }, |
| 2621 | 1388 _ratio: .5, |
| 2622 /** | |
| 2623 * The ratio of hidden tiles that should remain in the scroll direction. | |
| 2624 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
| 2625 */ | |
| 2626 _ratio: 0.5, | |
| 2627 | |
| 2628 /** | |
| 2629 * The padding-top value for the list. | |
| 2630 */ | |
| 2631 _scrollerPaddingTop: 0, | 1389 _scrollerPaddingTop: 0, |
| 2632 | |
| 2633 /** | |
| 2634 * This value is the same as `scrollTop`. | |
| 2635 */ | |
| 2636 _scrollPosition: 0, | 1390 _scrollPosition: 0, |
| 2637 | |
| 2638 /** | |
| 2639 * The sum of the heights of all the tiles in the DOM. | |
| 2640 */ | |
| 2641 _physicalSize: 0, | 1391 _physicalSize: 0, |
| 2642 | |
| 2643 /** | |
| 2644 * The average `offsetHeight` of the tiles observed till now. | |
| 2645 */ | |
| 2646 _physicalAverage: 0, | 1392 _physicalAverage: 0, |
| 2647 | |
| 2648 /** | |
| 2649 * The number of tiles which `offsetHeight` > 0 observed until now. | |
| 2650 */ | |
| 2651 _physicalAverageCount: 0, | 1393 _physicalAverageCount: 0, |
| 2652 | |
| 2653 /** | |
| 2654 * The Y position of the item rendered in the `_physicalStart` | |
| 2655 * tile relative to the scrolling list. | |
| 2656 */ | |
| 2657 _physicalTop: 0, | 1394 _physicalTop: 0, |
| 2658 | |
| 2659 /** | |
| 2660 * The number of items in the list. | |
| 2661 */ | |
| 2662 _virtualCount: 0, | 1395 _virtualCount: 0, |
| 2663 | |
| 2664 /** | |
| 2665 * A map between an item key and its physical item index | |
| 2666 */ | |
| 2667 _physicalIndexForKey: null, | 1396 _physicalIndexForKey: null, |
| 2668 | |
| 2669 /** | |
| 2670 * The estimated scroll height based on `_physicalAverage` | |
| 2671 */ | |
| 2672 _estScrollHeight: 0, | 1397 _estScrollHeight: 0, |
| 2673 | |
| 2674 /** | |
| 2675 * The scroll height of the dom node | |
| 2676 */ | |
| 2677 _scrollHeight: 0, | 1398 _scrollHeight: 0, |
| 2678 | |
| 2679 /** | |
| 2680 * The height of the list. This is referred as the viewport in the context o
f list. | |
| 2681 */ | |
| 2682 _viewportHeight: 0, | 1399 _viewportHeight: 0, |
| 2683 | |
| 2684 /** | |
| 2685 * The width of the list. This is referred as the viewport in the context of
list. | |
| 2686 */ | |
| 2687 _viewportWidth: 0, | 1400 _viewportWidth: 0, |
| 2688 | |
| 2689 /** | |
| 2690 * An array of DOM nodes that are currently in the tree | |
| 2691 * @type {?Array<!TemplatizerNode>} | |
| 2692 */ | |
| 2693 _physicalItems: null, | 1401 _physicalItems: null, |
| 2694 | |
| 2695 /** | |
| 2696 * An array of heights for each item in `_physicalItems` | |
| 2697 * @type {?Array<number>} | |
| 2698 */ | |
| 2699 _physicalSizes: null, | 1402 _physicalSizes: null, |
| 2700 | |
| 2701 /** | |
| 2702 * A cached value for the first visible index. | |
| 2703 * See `firstVisibleIndex` | |
| 2704 * @type {?number} | |
| 2705 */ | |
| 2706 _firstVisibleIndexVal: null, | 1403 _firstVisibleIndexVal: null, |
| 2707 | |
| 2708 /** | |
| 2709 * A cached value for the last visible index. | |
| 2710 * See `lastVisibleIndex` | |
| 2711 * @type {?number} | |
| 2712 */ | |
| 2713 _lastVisibleIndexVal: null, | 1404 _lastVisibleIndexVal: null, |
| 2714 | |
| 2715 /** | |
| 2716 * A Polymer collection for the items. | |
| 2717 * @type {?Polymer.Collection} | |
| 2718 */ | |
| 2719 _collection: null, | 1405 _collection: null, |
| 2720 | |
| 2721 /** | |
| 2722 * True if the current item list was rendered for the first time | |
| 2723 * after attached. | |
| 2724 */ | |
| 2725 _itemsRendered: false, | 1406 _itemsRendered: false, |
| 2726 | |
| 2727 /** | |
| 2728 * The page that is currently rendered. | |
| 2729 */ | |
| 2730 _lastPage: null, | 1407 _lastPage: null, |
| 2731 | |
| 2732 /** | |
| 2733 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
| 2734 */ | |
| 2735 _maxPages: 3, | 1408 _maxPages: 3, |
| 2736 | |
| 2737 /** | |
| 2738 * The currently focused physical item. | |
| 2739 */ | |
| 2740 _focusedItem: null, | 1409 _focusedItem: null, |
| 2741 | |
| 2742 /** | |
| 2743 * The index of the `_focusedItem`. | |
| 2744 */ | |
| 2745 _focusedIndex: -1, | 1410 _focusedIndex: -1, |
| 2746 | |
| 2747 /** | |
| 2748 * The the item that is focused if it is moved offscreen. | |
| 2749 * @private {?TemplatizerNode} | |
| 2750 */ | |
| 2751 _offscreenFocusedItem: null, | 1411 _offscreenFocusedItem: null, |
| 2752 | |
| 2753 /** | |
| 2754 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
| 2755 * list when that item is moved offscreen. | |
| 2756 */ | |
| 2757 _focusBackfillItem: null, | 1412 _focusBackfillItem: null, |
| 2758 | |
| 2759 /** | |
| 2760 * The maximum items per row | |
| 2761 */ | |
| 2762 _itemsPerRow: 1, | 1413 _itemsPerRow: 1, |
| 2763 | |
| 2764 /** | |
| 2765 * The width of each grid item | |
| 2766 */ | |
| 2767 _itemWidth: 0, | 1414 _itemWidth: 0, |
| 2768 | |
| 2769 /** | |
| 2770 * The height of the row in grid layout. | |
| 2771 */ | |
| 2772 _rowHeight: 0, | 1415 _rowHeight: 0, |
| 2773 | |
| 2774 /** | |
| 2775 * The bottom of the physical content. | |
| 2776 */ | |
| 2777 get _physicalBottom() { | 1416 get _physicalBottom() { |
| 2778 return this._physicalTop + this._physicalSize; | 1417 return this._physicalTop + this._physicalSize; |
| 2779 }, | 1418 }, |
| 2780 | |
| 2781 /** | |
| 2782 * The bottom of the scroll. | |
| 2783 */ | |
| 2784 get _scrollBottom() { | 1419 get _scrollBottom() { |
| 2785 return this._scrollPosition + this._viewportHeight; | 1420 return this._scrollPosition + this._viewportHeight; |
| 2786 }, | 1421 }, |
| 2787 | |
| 2788 /** | |
| 2789 * The n-th item rendered in the last physical item. | |
| 2790 */ | |
| 2791 get _virtualEnd() { | 1422 get _virtualEnd() { |
| 2792 return this._virtualStart + this._physicalCount - 1; | 1423 return this._virtualStart + this._physicalCount - 1; |
| 2793 }, | 1424 }, |
| 2794 | |
| 2795 /** | |
| 2796 * The height of the physical content that isn't on the screen. | |
| 2797 */ | |
| 2798 get _hiddenContentSize() { | 1425 get _hiddenContentSize() { |
| 2799 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 1426 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
| 2800 return size - this._viewportHeight; | 1427 return size - this._viewportHeight; |
| 2801 }, | 1428 }, |
| 2802 | |
| 2803 /** | |
| 2804 * The maximum scroll top value. | |
| 2805 */ | |
| 2806 get _maxScrollTop() { | 1429 get _maxScrollTop() { |
| 2807 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 1430 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
| 2808 }, | 1431 }, |
| 2809 | |
| 2810 /** | |
| 2811 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
| 2812 */ | |
| 2813 _minVirtualStart: 0, | 1432 _minVirtualStart: 0, |
| 2814 | |
| 2815 /** | |
| 2816 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
| 2817 */ | |
| 2818 get _maxVirtualStart() { | 1433 get _maxVirtualStart() { |
| 2819 return Math.max(0, this._virtualCount - this._physicalCount); | 1434 return Math.max(0, this._virtualCount - this._physicalCount); |
| 2820 }, | 1435 }, |
| 2821 | |
| 2822 /** | |
| 2823 * The n-th item rendered in the `_physicalStart` tile. | |
| 2824 */ | |
| 2825 _virtualStartVal: 0, | 1436 _virtualStartVal: 0, |
| 2826 | |
| 2827 set _virtualStart(val) { | 1437 set _virtualStart(val) { |
| 2828 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 1438 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
| 2829 }, | 1439 }, |
| 2830 | |
| 2831 get _virtualStart() { | 1440 get _virtualStart() { |
| 2832 return this._virtualStartVal || 0; | 1441 return this._virtualStartVal || 0; |
| 2833 }, | 1442 }, |
| 2834 | |
| 2835 /** | |
| 2836 * The k-th tile that is at the top of the scrolling list. | |
| 2837 */ | |
| 2838 _physicalStartVal: 0, | 1443 _physicalStartVal: 0, |
| 2839 | |
| 2840 set _physicalStart(val) { | 1444 set _physicalStart(val) { |
| 2841 this._physicalStartVal = val % this._physicalCount; | 1445 this._physicalStartVal = val % this._physicalCount; |
| 2842 if (this._physicalStartVal < 0) { | 1446 if (this._physicalStartVal < 0) { |
| 2843 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 1447 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
| 2844 } | 1448 } |
| 2845 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1449 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 2846 }, | 1450 }, |
| 2847 | |
| 2848 get _physicalStart() { | 1451 get _physicalStart() { |
| 2849 return this._physicalStartVal || 0; | 1452 return this._physicalStartVal || 0; |
| 2850 }, | 1453 }, |
| 2851 | |
| 2852 /** | |
| 2853 * The number of tiles in the DOM. | |
| 2854 */ | |
| 2855 _physicalCountVal: 0, | 1454 _physicalCountVal: 0, |
| 2856 | |
| 2857 set _physicalCount(val) { | 1455 set _physicalCount(val) { |
| 2858 this._physicalCountVal = val; | 1456 this._physicalCountVal = val; |
| 2859 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 1457 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 2860 }, | 1458 }, |
| 2861 | |
| 2862 get _physicalCount() { | 1459 get _physicalCount() { |
| 2863 return this._physicalCountVal; | 1460 return this._physicalCountVal; |
| 2864 }, | 1461 }, |
| 2865 | |
| 2866 /** | |
| 2867 * The k-th tile that is at the bottom of the scrolling list. | |
| 2868 */ | |
| 2869 _physicalEnd: 0, | 1462 _physicalEnd: 0, |
| 2870 | |
| 2871 /** | |
| 2872 * An optimal physical size such that we will have enough physical items | |
| 2873 * to fill up the viewport and recycle when the user scrolls. | |
| 2874 * | |
| 2875 * This default value assumes that we will at least have the equivalent | |
| 2876 * to a viewport of physical items above and below the user's viewport. | |
| 2877 */ | |
| 2878 get _optPhysicalSize() { | 1463 get _optPhysicalSize() { |
| 2879 if (this.grid) { | 1464 if (this.grid) { |
| 2880 return this._estRowsInView * this._rowHeight * this._maxPages; | 1465 return this._estRowsInView * this._rowHeight * this._maxPages; |
| 2881 } | 1466 } |
| 2882 return this._viewportHeight * this._maxPages; | 1467 return this._viewportHeight * this._maxPages; |
| 2883 }, | 1468 }, |
| 2884 | |
| 2885 get _optPhysicalCount() { | 1469 get _optPhysicalCount() { |
| 2886 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 1470 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
| 2887 }, | 1471 }, |
| 2888 | |
| 2889 /** | |
| 2890 * True if the current list is visible. | |
| 2891 */ | |
| 2892 get _isVisible() { | 1472 get _isVisible() { |
| 2893 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 1473 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
| 2894 }, | 1474 }, |
| 2895 | |
| 2896 /** | |
| 2897 * Gets the index of the first visible item in the viewport. | |
| 2898 * | |
| 2899 * @type {number} | |
| 2900 */ | |
| 2901 get firstVisibleIndex() { | 1475 get firstVisibleIndex() { |
| 2902 if (this._firstVisibleIndexVal === null) { | 1476 if (this._firstVisibleIndexVal === null) { |
| 2903 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 1477 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
| 2904 | 1478 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) { |
| 2905 this._firstVisibleIndexVal = this._iterateItems( | 1479 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 2906 function(pidx, vidx) { | 1480 if (physicalOffset > this._scrollPosition) { |
| 2907 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1481 return this.grid ? vidx - vidx % this._itemsPerRow : vidx; |
| 2908 | 1482 } |
| 2909 if (physicalOffset > this._scrollPosition) { | 1483 if (this.grid && this._virtualCount - 1 === vidx) { |
| 2910 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 1484 return vidx - vidx % this._itemsPerRow; |
| 2911 } | 1485 } |
| 2912 // Handle a partially rendered final row in grid mode | 1486 }) || 0; |
| 2913 if (this.grid && this._virtualCount - 1 === vidx) { | |
| 2914 return vidx - (vidx % this._itemsPerRow); | |
| 2915 } | |
| 2916 }) || 0; | |
| 2917 } | 1487 } |
| 2918 return this._firstVisibleIndexVal; | 1488 return this._firstVisibleIndexVal; |
| 2919 }, | 1489 }, |
| 2920 | |
| 2921 /** | |
| 2922 * Gets the index of the last visible item in the viewport. | |
| 2923 * | |
| 2924 * @type {number} | |
| 2925 */ | |
| 2926 get lastVisibleIndex() { | 1490 get lastVisibleIndex() { |
| 2927 if (this._lastVisibleIndexVal === null) { | 1491 if (this._lastVisibleIndexVal === null) { |
| 2928 if (this.grid) { | 1492 if (this.grid) { |
| 2929 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 1493 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
| 2930 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 1494 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
| 2931 } else { | 1495 } else { |
| 2932 var physicalOffset = this._physicalTop; | 1496 var physicalOffset = this._physicalTop; |
| 2933 this._iterateItems(function(pidx, vidx) { | 1497 this._iterateItems(function(pidx, vidx) { |
| 2934 if (physicalOffset < this._scrollBottom) { | 1498 if (physicalOffset < this._scrollBottom) { |
| 2935 this._lastVisibleIndexVal = vidx; | 1499 this._lastVisibleIndexVal = vidx; |
| 2936 } else { | 1500 } else { |
| 2937 // Break _iterateItems | |
| 2938 return true; | 1501 return true; |
| 2939 } | 1502 } |
| 2940 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 1503 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 2941 }); | 1504 }); |
| 2942 } | 1505 } |
| 2943 } | 1506 } |
| 2944 return this._lastVisibleIndexVal; | 1507 return this._lastVisibleIndexVal; |
| 2945 }, | 1508 }, |
| 2946 | |
| 2947 get _defaultScrollTarget() { | 1509 get _defaultScrollTarget() { |
| 2948 return this; | 1510 return this; |
| 2949 }, | 1511 }, |
| 2950 get _virtualRowCount() { | 1512 get _virtualRowCount() { |
| 2951 return Math.ceil(this._virtualCount / this._itemsPerRow); | 1513 return Math.ceil(this._virtualCount / this._itemsPerRow); |
| 2952 }, | 1514 }, |
| 2953 | |
| 2954 get _estRowsInView() { | 1515 get _estRowsInView() { |
| 2955 return Math.ceil(this._viewportHeight / this._rowHeight); | 1516 return Math.ceil(this._viewportHeight / this._rowHeight); |
| 2956 }, | 1517 }, |
| 2957 | |
| 2958 get _physicalRows() { | 1518 get _physicalRows() { |
| 2959 return Math.ceil(this._physicalCount / this._itemsPerRow); | 1519 return Math.ceil(this._physicalCount / this._itemsPerRow); |
| 2960 }, | 1520 }, |
| 2961 | |
| 2962 ready: function() { | 1521 ready: function() { |
| 2963 this.addEventListener('focus', this._didFocus.bind(this), true); | 1522 this.addEventListener('focus', this._didFocus.bind(this), true); |
| 2964 }, | 1523 }, |
| 2965 | |
| 2966 attached: function() { | 1524 attached: function() { |
| 2967 this.updateViewportBoundaries(); | 1525 this.updateViewportBoundaries(); |
| 2968 this._render(); | 1526 this._render(); |
| 2969 // `iron-resize` is fired when the list is attached if the event is added | |
| 2970 // before attached causing unnecessary work. | |
| 2971 this.listen(this, 'iron-resize', '_resizeHandler'); | 1527 this.listen(this, 'iron-resize', '_resizeHandler'); |
| 2972 }, | 1528 }, |
| 2973 | |
| 2974 detached: function() { | 1529 detached: function() { |
| 2975 this._itemsRendered = false; | 1530 this._itemsRendered = false; |
| 2976 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 1531 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
| 2977 }, | 1532 }, |
| 2978 | |
| 2979 /** | |
| 2980 * Set the overflow property if this element has its own scrolling region | |
| 2981 */ | |
| 2982 _setOverflow: function(scrollTarget) { | 1533 _setOverflow: function(scrollTarget) { |
| 2983 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 1534 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
| 2984 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 1535 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 2985 }, | 1536 }, |
| 2986 | |
| 2987 /** | |
| 2988 * Invoke this method if you dynamically update the viewport's | |
| 2989 * size or CSS padding. | |
| 2990 * | |
| 2991 * @method updateViewportBoundaries | |
| 2992 */ | |
| 2993 updateViewportBoundaries: function() { | 1537 updateViewportBoundaries: function() { |
| 2994 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 1538 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo
w.getComputedStyle(this)['padding-top'], 10); |
| 2995 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
| 2996 | |
| 2997 this._viewportHeight = this._scrollTargetHeight; | 1539 this._viewportHeight = this._scrollTargetHeight; |
| 2998 if (this.grid) { | 1540 if (this.grid) { |
| 2999 this._updateGridMetrics(); | 1541 this._updateGridMetrics(); |
| 3000 } | 1542 } |
| 3001 }, | 1543 }, |
| 3002 | |
| 3003 /** | |
| 3004 * Update the models, the position of the | |
| 3005 * items in the viewport and recycle tiles as needed. | |
| 3006 */ | |
| 3007 _scrollHandler: function() { | 1544 _scrollHandler: function() { |
| 3008 // clamp the `scrollTop` value | |
| 3009 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 1545 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
| 3010 var delta = scrollTop - this._scrollPosition; | 1546 var delta = scrollTop - this._scrollPosition; |
| 3011 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 1547 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
| 3012 var ratio = this._ratio; | 1548 var ratio = this._ratio; |
| 3013 var recycledTiles = 0; | 1549 var recycledTiles = 0; |
| 3014 var hiddenContentSize = this._hiddenContentSize; | 1550 var hiddenContentSize = this._hiddenContentSize; |
| 3015 var currentRatio = ratio; | 1551 var currentRatio = ratio; |
| 3016 var movingUp = []; | 1552 var movingUp = []; |
| 3017 | |
| 3018 // track the last `scrollTop` | |
| 3019 this._scrollPosition = scrollTop; | 1553 this._scrollPosition = scrollTop; |
| 3020 | |
| 3021 // clear cached visible indexes | |
| 3022 this._firstVisibleIndexVal = null; | 1554 this._firstVisibleIndexVal = null; |
| 3023 this._lastVisibleIndexVal = null; | 1555 this._lastVisibleIndexVal = null; |
| 3024 | |
| 3025 scrollBottom = this._scrollBottom; | 1556 scrollBottom = this._scrollBottom; |
| 3026 physicalBottom = this._physicalBottom; | 1557 physicalBottom = this._physicalBottom; |
| 3027 | |
| 3028 // random access | |
| 3029 if (Math.abs(delta) > this._physicalSize) { | 1558 if (Math.abs(delta) > this._physicalSize) { |
| 3030 this._physicalTop += delta; | 1559 this._physicalTop += delta; |
| 3031 recycledTiles = Math.round(delta / this._physicalAverage); | 1560 recycledTiles = Math.round(delta / this._physicalAverage); |
| 3032 } | 1561 } else if (delta < 0) { |
| 3033 // scroll up | |
| 3034 else if (delta < 0) { | |
| 3035 var topSpace = scrollTop - this._physicalTop; | 1562 var topSpace = scrollTop - this._physicalTop; |
| 3036 var virtualStart = this._virtualStart; | 1563 var virtualStart = this._virtualStart; |
| 3037 | |
| 3038 recycledTileSet = []; | 1564 recycledTileSet = []; |
| 3039 | |
| 3040 kth = this._physicalEnd; | 1565 kth = this._physicalEnd; |
| 3041 currentRatio = topSpace / hiddenContentSize; | 1566 currentRatio = topSpace / hiddenContentSize; |
| 3042 | 1567 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen
t(kth) > scrollBottom) { |
| 3043 // move tiles from bottom to top | |
| 3044 while ( | |
| 3045 // approximate `currentRatio` to `ratio` | |
| 3046 currentRatio < ratio && | |
| 3047 // recycle less physical items than the total | |
| 3048 recycledTiles < this._physicalCount && | |
| 3049 // ensure that these recycled tiles are needed | |
| 3050 virtualStart - recycledTiles > 0 && | |
| 3051 // ensure that the tile is not visible | |
| 3052 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
| 3053 ) { | |
| 3054 | |
| 3055 tileHeight = this._getPhysicalSizeIncrement(kth); | 1568 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 3056 currentRatio += tileHeight / hiddenContentSize; | 1569 currentRatio += tileHeight / hiddenContentSize; |
| 3057 physicalBottom -= tileHeight; | 1570 physicalBottom -= tileHeight; |
| 3058 recycledTileSet.push(kth); | 1571 recycledTileSet.push(kth); |
| 3059 recycledTiles++; | 1572 recycledTiles++; |
| 3060 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 1573 kth = kth === 0 ? this._physicalCount - 1 : kth - 1; |
| 3061 } | 1574 } |
| 3062 | |
| 3063 movingUp = recycledTileSet; | 1575 movingUp = recycledTileSet; |
| 3064 recycledTiles = -recycledTiles; | 1576 recycledTiles = -recycledTiles; |
| 3065 } | 1577 } else if (delta > 0) { |
| 3066 // scroll down | |
| 3067 else if (delta > 0) { | |
| 3068 var bottomSpace = physicalBottom - scrollBottom; | 1578 var bottomSpace = physicalBottom - scrollBottom; |
| 3069 var virtualEnd = this._virtualEnd; | 1579 var virtualEnd = this._virtualEnd; |
| 3070 var lastVirtualItemIndex = this._virtualCount-1; | 1580 var lastVirtualItemIndex = this._virtualCount - 1; |
| 3071 | |
| 3072 recycledTileSet = []; | 1581 recycledTileSet = []; |
| 3073 | |
| 3074 kth = this._physicalStart; | 1582 kth = this._physicalStart; |
| 3075 currentRatio = bottomSpace / hiddenContentSize; | 1583 currentRatio = bottomSpace / hiddenContentSize; |
| 3076 | 1584 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get
PhysicalSizeIncrement(kth) < scrollTop) { |
| 3077 // move tiles from top to bottom | |
| 3078 while ( | |
| 3079 // approximate `currentRatio` to `ratio` | |
| 3080 currentRatio < ratio && | |
| 3081 // recycle less physical items than the total | |
| 3082 recycledTiles < this._physicalCount && | |
| 3083 // ensure that these recycled tiles are needed | |
| 3084 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
| 3085 // ensure that the tile is not visible | |
| 3086 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
| 3087 ) { | |
| 3088 | |
| 3089 tileHeight = this._getPhysicalSizeIncrement(kth); | 1585 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 3090 currentRatio += tileHeight / hiddenContentSize; | 1586 currentRatio += tileHeight / hiddenContentSize; |
| 3091 | |
| 3092 this._physicalTop += tileHeight; | 1587 this._physicalTop += tileHeight; |
| 3093 recycledTileSet.push(kth); | 1588 recycledTileSet.push(kth); |
| 3094 recycledTiles++; | 1589 recycledTiles++; |
| 3095 kth = (kth + 1) % this._physicalCount; | 1590 kth = (kth + 1) % this._physicalCount; |
| 3096 } | 1591 } |
| 3097 } | 1592 } |
| 3098 | |
| 3099 if (recycledTiles === 0) { | 1593 if (recycledTiles === 0) { |
| 3100 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
| 3101 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 1594 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
| 3102 this._increasePoolIfNeeded(); | 1595 this._increasePoolIfNeeded(); |
| 3103 } | 1596 } |
| 3104 } else { | 1597 } else { |
| 3105 this._virtualStart = this._virtualStart + recycledTiles; | 1598 this._virtualStart = this._virtualStart + recycledTiles; |
| 3106 this._physicalStart = this._physicalStart + recycledTiles; | 1599 this._physicalStart = this._physicalStart + recycledTiles; |
| 3107 this._update(recycledTileSet, movingUp); | 1600 this._update(recycledTileSet, movingUp); |
| 3108 } | 1601 } |
| 3109 }, | 1602 }, |
| 3110 | |
| 3111 /** | |
| 3112 * Update the list of items, starting from the `_virtualStart` item. | |
| 3113 * @param {!Array<number>=} itemSet | |
| 3114 * @param {!Array<number>=} movingUp | |
| 3115 */ | |
| 3116 _update: function(itemSet, movingUp) { | 1603 _update: function(itemSet, movingUp) { |
| 3117 // manage focus | |
| 3118 this._manageFocus(); | 1604 this._manageFocus(); |
| 3119 // update models | |
| 3120 this._assignModels(itemSet); | 1605 this._assignModels(itemSet); |
| 3121 // measure heights | |
| 3122 this._updateMetrics(itemSet); | 1606 this._updateMetrics(itemSet); |
| 3123 // adjust offset after measuring | |
| 3124 if (movingUp) { | 1607 if (movingUp) { |
| 3125 while (movingUp.length) { | 1608 while (movingUp.length) { |
| 3126 var idx = movingUp.pop(); | 1609 var idx = movingUp.pop(); |
| 3127 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 1610 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
| 3128 } | 1611 } |
| 3129 } | 1612 } |
| 3130 // update the position of the items | |
| 3131 this._positionItems(); | 1613 this._positionItems(); |
| 3132 // set the scroller size | |
| 3133 this._updateScrollerSize(); | 1614 this._updateScrollerSize(); |
| 3134 // increase the pool of physical items | |
| 3135 this._increasePoolIfNeeded(); | 1615 this._increasePoolIfNeeded(); |
| 3136 }, | 1616 }, |
| 3137 | |
| 3138 /** | |
| 3139 * Creates a pool of DOM elements and attaches them to the local dom. | |
| 3140 */ | |
| 3141 _createPool: function(size) { | 1617 _createPool: function(size) { |
| 3142 var physicalItems = new Array(size); | 1618 var physicalItems = new Array(size); |
| 3143 | |
| 3144 this._ensureTemplatized(); | 1619 this._ensureTemplatized(); |
| 3145 | |
| 3146 for (var i = 0; i < size; i++) { | 1620 for (var i = 0; i < size; i++) { |
| 3147 var inst = this.stamp(null); | 1621 var inst = this.stamp(null); |
| 3148 // First element child is item; Safari doesn't support children[0] | |
| 3149 // on a doc fragment | |
| 3150 physicalItems[i] = inst.root.querySelector('*'); | 1622 physicalItems[i] = inst.root.querySelector('*'); |
| 3151 Polymer.dom(this).appendChild(inst.root); | 1623 Polymer.dom(this).appendChild(inst.root); |
| 3152 } | 1624 } |
| 3153 return physicalItems; | 1625 return physicalItems; |
| 3154 }, | 1626 }, |
| 3155 | |
| 3156 /** | |
| 3157 * Increases the pool of physical items only if needed. | |
| 3158 * | |
| 3159 * @return {boolean} True if the pool was increased. | |
| 3160 */ | |
| 3161 _increasePoolIfNeeded: function() { | 1627 _increasePoolIfNeeded: function() { |
| 3162 // Base case 1: the list has no height. | |
| 3163 if (this._viewportHeight === 0) { | 1628 if (this._viewportHeight === 0) { |
| 3164 return false; | 1629 return false; |
| 3165 } | 1630 } |
| 3166 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
| 3167 // with physical items, don't increase the pool. | |
| 3168 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 1631 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
| 3169 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 1632 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
| 3170 return false; | 1633 return false; |
| 3171 } | 1634 } |
| 3172 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
| 3173 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 1635 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
| 3174 | |
| 3175 if (currentPage === 0) { | 1636 if (currentPage === 0) { |
| 3176 // fill the first page | 1637 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * .5))); |
| 3177 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
| 3178 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 1638 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
| 3179 // paint the page and defer the next increase | |
| 3180 // wait 16ms which is rough enough to get paint cycle. | |
| 3181 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 1639 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
| 3182 } else { | 1640 } else { |
| 3183 // fill the rest of the pages | |
| 3184 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 1641 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
| 3185 } | 1642 } |
| 3186 | |
| 3187 this._lastPage = currentPage; | 1643 this._lastPage = currentPage; |
| 3188 | |
| 3189 return true; | 1644 return true; |
| 3190 }, | 1645 }, |
| 3191 | |
| 3192 /** | |
| 3193 * Increases the pool size. | |
| 3194 */ | |
| 3195 _increasePool: function(missingItems) { | 1646 _increasePool: function(missingItems) { |
| 3196 var nextPhysicalCount = Math.min( | 1647 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this.
_virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS
ICAL_COUNT)); |
| 3197 this._physicalCount + missingItems, | |
| 3198 this._virtualCount - this._virtualStart, | |
| 3199 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
| 3200 ); | |
| 3201 var prevPhysicalCount = this._physicalCount; | 1648 var prevPhysicalCount = this._physicalCount; |
| 3202 var delta = nextPhysicalCount - prevPhysicalCount; | 1649 var delta = nextPhysicalCount - prevPhysicalCount; |
| 3203 | |
| 3204 if (delta <= 0) { | 1650 if (delta <= 0) { |
| 3205 return; | 1651 return; |
| 3206 } | 1652 } |
| 3207 | |
| 3208 [].push.apply(this._physicalItems, this._createPool(delta)); | 1653 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 3209 [].push.apply(this._physicalSizes, new Array(delta)); | 1654 [].push.apply(this._physicalSizes, new Array(delta)); |
| 3210 | |
| 3211 this._physicalCount = prevPhysicalCount + delta; | 1655 this._physicalCount = prevPhysicalCount + delta; |
| 3212 | 1656 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this.
_focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd
) { |
| 3213 // update the physical start if we need to preserve the model of the focus
ed item. | |
| 3214 // In this situation, the focused item is currently rendered and its model
would | |
| 3215 // have changed after increasing the pool if the physical start remained u
nchanged. | |
| 3216 if (this._physicalStart > this._physicalEnd && | |
| 3217 this._isIndexRendered(this._focusedIndex) && | |
| 3218 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
| 3219 this._physicalStart = this._physicalStart + delta; | 1657 this._physicalStart = this._physicalStart + delta; |
| 3220 } | 1658 } |
| 3221 this._update(); | 1659 this._update(); |
| 3222 }, | 1660 }, |
| 3223 | |
| 3224 /** | |
| 3225 * Render a new list of items. This method does exactly the same as `update`
, | |
| 3226 * but it also ensures that only one `update` cycle is created. | |
| 3227 */ | |
| 3228 _render: function() { | 1661 _render: function() { |
| 3229 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 1662 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
| 3230 | |
| 3231 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 1663 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 3232 this._lastPage = 0; | 1664 this._lastPage = 0; |
| 3233 this._update(); | 1665 this._update(); |
| 3234 this._itemsRendered = true; | 1666 this._itemsRendered = true; |
| 3235 } | 1667 } |
| 3236 }, | 1668 }, |
| 3237 | |
| 3238 /** | |
| 3239 * Templetizes the user template. | |
| 3240 */ | |
| 3241 _ensureTemplatized: function() { | 1669 _ensureTemplatized: function() { |
| 3242 if (!this.ctor) { | 1670 if (!this.ctor) { |
| 3243 // Template instance props that should be excluded from forwarding | |
| 3244 var props = {}; | 1671 var props = {}; |
| 3245 props.__key__ = true; | 1672 props.__key__ = true; |
| 3246 props[this.as] = true; | 1673 props[this.as] = true; |
| 3247 props[this.indexAs] = true; | 1674 props[this.indexAs] = true; |
| 3248 props[this.selectedAs] = true; | 1675 props[this.selectedAs] = true; |
| 3249 props.tabIndex = true; | 1676 props.tabIndex = true; |
| 3250 | |
| 3251 this._instanceProps = props; | 1677 this._instanceProps = props; |
| 3252 this._userTemplate = Polymer.dom(this).querySelector('template'); | 1678 this._userTemplate = Polymer.dom(this).querySelector('template'); |
| 3253 | |
| 3254 if (this._userTemplate) { | 1679 if (this._userTemplate) { |
| 3255 this.templatize(this._userTemplate); | 1680 this.templatize(this._userTemplate); |
| 3256 } else { | 1681 } else { |
| 3257 console.warn('iron-list requires a template to be provided in light-do
m'); | 1682 console.warn('iron-list requires a template to be provided in light-do
m'); |
| 3258 } | 1683 } |
| 3259 } | 1684 } |
| 3260 }, | 1685 }, |
| 3261 | |
| 3262 /** | |
| 3263 * Implements extension point from Templatizer mixin. | |
| 3264 */ | |
| 3265 _getStampedChildren: function() { | 1686 _getStampedChildren: function() { |
| 3266 return this._physicalItems; | 1687 return this._physicalItems; |
| 3267 }, | 1688 }, |
| 3268 | |
| 3269 /** | |
| 3270 * Implements extension point from Templatizer | |
| 3271 * Called as a side effect of a template instance path change, responsible | |
| 3272 * for notifying items.<key-for-instance>.<path> change up to host. | |
| 3273 */ | |
| 3274 _forwardInstancePath: function(inst, path, value) { | 1689 _forwardInstancePath: function(inst, path, value) { |
| 3275 if (path.indexOf(this.as + '.') === 0) { | 1690 if (path.indexOf(this.as + '.') === 0) { |
| 3276 this.notifyPath('items.' + inst.__key__ + '.' + | 1691 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt
h + 1), value); |
| 3277 path.slice(this.as.length + 1), value); | |
| 3278 } | 1692 } |
| 3279 }, | 1693 }, |
| 3280 | |
| 3281 /** | |
| 3282 * Implements extension point from Templatizer mixin | |
| 3283 * Called as side-effect of a host property change, responsible for | |
| 3284 * notifying parent path change on each row. | |
| 3285 */ | |
| 3286 _forwardParentProp: function(prop, value) { | 1694 _forwardParentProp: function(prop, value) { |
| 3287 if (this._physicalItems) { | 1695 if (this._physicalItems) { |
| 3288 this._physicalItems.forEach(function(item) { | 1696 this._physicalItems.forEach(function(item) { |
| 3289 item._templateInstance[prop] = value; | 1697 item._templateInstance[prop] = value; |
| 3290 }, this); | 1698 }, this); |
| 3291 } | 1699 } |
| 3292 }, | 1700 }, |
| 3293 | |
| 3294 /** | |
| 3295 * Implements extension point from Templatizer | |
| 3296 * Called as side-effect of a host path change, responsible for | |
| 3297 * notifying parent.<path> path change on each row. | |
| 3298 */ | |
| 3299 _forwardParentPath: function(path, value) { | 1701 _forwardParentPath: function(path, value) { |
| 3300 if (this._physicalItems) { | 1702 if (this._physicalItems) { |
| 3301 this._physicalItems.forEach(function(item) { | 1703 this._physicalItems.forEach(function(item) { |
| 3302 item._templateInstance.notifyPath(path, value, true); | 1704 item._templateInstance.notifyPath(path, value, true); |
| 3303 }, this); | 1705 }, this); |
| 3304 } | 1706 } |
| 3305 }, | 1707 }, |
| 3306 | |
| 3307 /** | |
| 3308 * Called as a side effect of a host items.<key>.<path> path change, | |
| 3309 * responsible for notifying item.<path> changes. | |
| 3310 */ | |
| 3311 _forwardItemPath: function(path, value) { | 1708 _forwardItemPath: function(path, value) { |
| 3312 if (!this._physicalIndexForKey) { | 1709 if (!this._physicalIndexForKey) { |
| 3313 return; | 1710 return; |
| 3314 } | 1711 } |
| 3315 var dot = path.indexOf('.'); | 1712 var dot = path.indexOf('.'); |
| 3316 var key = path.substring(0, dot < 0 ? path.length : dot); | 1713 var key = path.substring(0, dot < 0 ? path.length : dot); |
| 3317 var idx = this._physicalIndexForKey[key]; | 1714 var idx = this._physicalIndexForKey[key]; |
| 3318 var offscreenItem = this._offscreenFocusedItem; | 1715 var offscreenItem = this._offscreenFocusedItem; |
| 3319 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 1716 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? offscreenItem : this._physicalItems[idx]; |
| 3320 offscreenItem : this._physicalItems[idx]; | |
| 3321 | |
| 3322 if (!el || el._templateInstance.__key__ !== key) { | 1717 if (!el || el._templateInstance.__key__ !== key) { |
| 3323 return; | 1718 return; |
| 3324 } | 1719 } |
| 3325 if (dot >= 0) { | 1720 if (dot >= 0) { |
| 3326 path = this.as + '.' + path.substring(dot+1); | 1721 path = this.as + '.' + path.substring(dot + 1); |
| 3327 el._templateInstance.notifyPath(path, value, true); | 1722 el._templateInstance.notifyPath(path, value, true); |
| 3328 } else { | 1723 } else { |
| 3329 // Update selection if needed | |
| 3330 var currentItem = el._templateInstance[this.as]; | 1724 var currentItem = el._templateInstance[this.as]; |
| 3331 if (Array.isArray(this.selectedItems)) { | 1725 if (Array.isArray(this.selectedItems)) { |
| 3332 for (var i = 0; i < this.selectedItems.length; i++) { | 1726 for (var i = 0; i < this.selectedItems.length; i++) { |
| 3333 if (this.selectedItems[i] === currentItem) { | 1727 if (this.selectedItems[i] === currentItem) { |
| 3334 this.set('selectedItems.' + i, value); | 1728 this.set('selectedItems.' + i, value); |
| 3335 break; | 1729 break; |
| 3336 } | 1730 } |
| 3337 } | 1731 } |
| 3338 } else if (this.selectedItem === currentItem) { | 1732 } else if (this.selectedItem === currentItem) { |
| 3339 this.set('selectedItem', value); | 1733 this.set('selectedItem', value); |
| 3340 } | 1734 } |
| 3341 el._templateInstance[this.as] = value; | 1735 el._templateInstance[this.as] = value; |
| 3342 } | 1736 } |
| 3343 }, | 1737 }, |
| 3344 | |
| 3345 /** | |
| 3346 * Called when the items have changed. That is, ressignments | |
| 3347 * to `items`, splices or updates to a single item. | |
| 3348 */ | |
| 3349 _itemsChanged: function(change) { | 1738 _itemsChanged: function(change) { |
| 3350 if (change.path === 'items') { | 1739 if (change.path === 'items') { |
| 3351 // reset items | |
| 3352 this._virtualStart = 0; | 1740 this._virtualStart = 0; |
| 3353 this._physicalTop = 0; | 1741 this._physicalTop = 0; |
| 3354 this._virtualCount = this.items ? this.items.length : 0; | 1742 this._virtualCount = this.items ? this.items.length : 0; |
| 3355 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 1743 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
| 3356 this._physicalIndexForKey = {}; | 1744 this._physicalIndexForKey = {}; |
| 3357 this._firstVisibleIndexVal = null; | 1745 this._firstVisibleIndexVal = null; |
| 3358 this._lastVisibleIndexVal = null; | 1746 this._lastVisibleIndexVal = null; |
| 3359 | |
| 3360 this._resetScrollPosition(0); | 1747 this._resetScrollPosition(0); |
| 3361 this._removeFocusedItem(); | 1748 this._removeFocusedItem(); |
| 3362 // create the initial physical items | |
| 3363 if (!this._physicalItems) { | 1749 if (!this._physicalItems) { |
| 3364 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 1750 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
| 3365 this._physicalItems = this._createPool(this._physicalCount); | 1751 this._physicalItems = this._createPool(this._physicalCount); |
| 3366 this._physicalSizes = new Array(this._physicalCount); | 1752 this._physicalSizes = new Array(this._physicalCount); |
| 3367 } | 1753 } |
| 3368 | |
| 3369 this._physicalStart = 0; | 1754 this._physicalStart = 0; |
| 3370 | |
| 3371 } else if (change.path === 'items.splices') { | 1755 } else if (change.path === 'items.splices') { |
| 3372 | |
| 3373 this._adjustVirtualIndex(change.value.indexSplices); | 1756 this._adjustVirtualIndex(change.value.indexSplices); |
| 3374 this._virtualCount = this.items ? this.items.length : 0; | 1757 this._virtualCount = this.items ? this.items.length : 0; |
| 3375 | |
| 3376 } else { | 1758 } else { |
| 3377 // update a single item | |
| 3378 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 1759 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
| 3379 return; | 1760 return; |
| 3380 } | 1761 } |
| 3381 | |
| 3382 this._itemsRendered = false; | 1762 this._itemsRendered = false; |
| 3383 this._debounceTemplate(this._render); | 1763 this._debounceTemplate(this._render); |
| 3384 }, | 1764 }, |
| 3385 | |
| 3386 /** | |
| 3387 * @param {!Array<!PolymerSplice>} splices | |
| 3388 */ | |
| 3389 _adjustVirtualIndex: function(splices) { | 1765 _adjustVirtualIndex: function(splices) { |
| 3390 splices.forEach(function(splice) { | 1766 splices.forEach(function(splice) { |
| 3391 // deselect removed items | |
| 3392 splice.removed.forEach(this._removeItem, this); | 1767 splice.removed.forEach(this._removeItem, this); |
| 3393 // We only need to care about changes happening above the current positi
on | |
| 3394 if (splice.index < this._virtualStart) { | 1768 if (splice.index < this._virtualStart) { |
| 3395 var delta = Math.max( | 1769 var delta = Math.max(splice.addedCount - splice.removed.length, splice
.index - this._virtualStart); |
| 3396 splice.addedCount - splice.removed.length, | |
| 3397 splice.index - this._virtualStart); | |
| 3398 | |
| 3399 this._virtualStart = this._virtualStart + delta; | 1770 this._virtualStart = this._virtualStart + delta; |
| 3400 | |
| 3401 if (this._focusedIndex >= 0) { | 1771 if (this._focusedIndex >= 0) { |
| 3402 this._focusedIndex = this._focusedIndex + delta; | 1772 this._focusedIndex = this._focusedIndex + delta; |
| 3403 } | 1773 } |
| 3404 } | 1774 } |
| 3405 }, this); | 1775 }, this); |
| 3406 }, | 1776 }, |
| 3407 | |
| 3408 _removeItem: function(item) { | 1777 _removeItem: function(item) { |
| 3409 this.$.selector.deselect(item); | 1778 this.$.selector.deselect(item); |
| 3410 // remove the current focused item | |
| 3411 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 1779 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
| 3412 this._removeFocusedItem(); | 1780 this._removeFocusedItem(); |
| 3413 } | 1781 } |
| 3414 }, | 1782 }, |
| 3415 | |
| 3416 /** | |
| 3417 * Executes a provided function per every physical index in `itemSet` | |
| 3418 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
| 3419 * | |
| 3420 * @param {!function(number, number)} fn | |
| 3421 * @param {!Array<number>=} itemSet | |
| 3422 */ | |
| 3423 _iterateItems: function(fn, itemSet) { | 1783 _iterateItems: function(fn, itemSet) { |
| 3424 var pidx, vidx, rtn, i; | 1784 var pidx, vidx, rtn, i; |
| 3425 | |
| 3426 if (arguments.length === 2 && itemSet) { | 1785 if (arguments.length === 2 && itemSet) { |
| 3427 for (i = 0; i < itemSet.length; i++) { | 1786 for (i = 0; i < itemSet.length; i++) { |
| 3428 pidx = itemSet[i]; | 1787 pidx = itemSet[i]; |
| 3429 vidx = this._computeVidx(pidx); | 1788 vidx = this._computeVidx(pidx); |
| 3430 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1789 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 3431 return rtn; | 1790 return rtn; |
| 3432 } | 1791 } |
| 3433 } | 1792 } |
| 3434 } else { | 1793 } else { |
| 3435 pidx = this._physicalStart; | 1794 pidx = this._physicalStart; |
| 3436 vidx = this._virtualStart; | 1795 vidx = this._virtualStart; |
| 3437 | 1796 for (;pidx < this._physicalCount; pidx++, vidx++) { |
| 3438 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
| 3439 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1797 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 3440 return rtn; | 1798 return rtn; |
| 3441 } | 1799 } |
| 3442 } | 1800 } |
| 3443 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 1801 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
| 3444 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 1802 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 3445 return rtn; | 1803 return rtn; |
| 3446 } | 1804 } |
| 3447 } | 1805 } |
| 3448 } | 1806 } |
| 3449 }, | 1807 }, |
| 3450 | |
| 3451 /** | |
| 3452 * Returns the virtual index for a given physical index | |
| 3453 * | |
| 3454 * @param {number} pidx Physical index | |
| 3455 * @return {number} | |
| 3456 */ | |
| 3457 _computeVidx: function(pidx) { | 1808 _computeVidx: function(pidx) { |
| 3458 if (pidx >= this._physicalStart) { | 1809 if (pidx >= this._physicalStart) { |
| 3459 return this._virtualStart + (pidx - this._physicalStart); | 1810 return this._virtualStart + (pidx - this._physicalStart); |
| 3460 } | 1811 } |
| 3461 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 1812 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
| 3462 }, | 1813 }, |
| 3463 | |
| 3464 /** | |
| 3465 * Assigns the data models to a given set of items. | |
| 3466 * @param {!Array<number>=} itemSet | |
| 3467 */ | |
| 3468 _assignModels: function(itemSet) { | 1814 _assignModels: function(itemSet) { |
| 3469 this._iterateItems(function(pidx, vidx) { | 1815 this._iterateItems(function(pidx, vidx) { |
| 3470 var el = this._physicalItems[pidx]; | 1816 var el = this._physicalItems[pidx]; |
| 3471 var inst = el._templateInstance; | 1817 var inst = el._templateInstance; |
| 3472 var item = this.items && this.items[vidx]; | 1818 var item = this.items && this.items[vidx]; |
| 3473 | |
| 3474 if (item != null) { | 1819 if (item != null) { |
| 3475 inst[this.as] = item; | 1820 inst[this.as] = item; |
| 3476 inst.__key__ = this._collection.getKey(item); | 1821 inst.__key__ = this._collection.getKey(item); |
| 3477 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 1822 inst[this.selectedAs] = this.$.selector.isSelected(item); |
| 3478 inst[this.indexAs] = vidx; | 1823 inst[this.indexAs] = vidx; |
| 3479 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 1824 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
| 3480 this._physicalIndexForKey[inst.__key__] = pidx; | 1825 this._physicalIndexForKey[inst.__key__] = pidx; |
| 3481 el.removeAttribute('hidden'); | 1826 el.removeAttribute('hidden'); |
| 3482 } else { | 1827 } else { |
| 3483 inst.__key__ = null; | 1828 inst.__key__ = null; |
| 3484 el.setAttribute('hidden', ''); | 1829 el.setAttribute('hidden', ''); |
| 3485 } | 1830 } |
| 3486 }, itemSet); | 1831 }, itemSet); |
| 3487 }, | 1832 }, |
| 3488 | 1833 _updateMetrics: function(itemSet) { |
| 3489 /** | |
| 3490 * Updates the height for a given set of items. | |
| 3491 * | |
| 3492 * @param {!Array<number>=} itemSet | |
| 3493 */ | |
| 3494 _updateMetrics: function(itemSet) { | |
| 3495 // Make sure we distributed all the physical items | |
| 3496 // so we can measure them | |
| 3497 Polymer.dom.flush(); | 1834 Polymer.dom.flush(); |
| 3498 | |
| 3499 var newPhysicalSize = 0; | 1835 var newPhysicalSize = 0; |
| 3500 var oldPhysicalSize = 0; | 1836 var oldPhysicalSize = 0; |
| 3501 var prevAvgCount = this._physicalAverageCount; | 1837 var prevAvgCount = this._physicalAverageCount; |
| 3502 var prevPhysicalAvg = this._physicalAverage; | 1838 var prevPhysicalAvg = this._physicalAverage; |
| 3503 | |
| 3504 this._iterateItems(function(pidx, vidx) { | 1839 this._iterateItems(function(pidx, vidx) { |
| 3505 | |
| 3506 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 1840 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
| 3507 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 1841 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
| 3508 newPhysicalSize += this._physicalSizes[pidx]; | 1842 newPhysicalSize += this._physicalSizes[pidx]; |
| 3509 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 1843 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| 3510 | |
| 3511 }, itemSet); | 1844 }, itemSet); |
| 3512 | |
| 3513 this._viewportHeight = this._scrollTargetHeight; | 1845 this._viewportHeight = this._scrollTargetHeight; |
| 3514 if (this.grid) { | 1846 if (this.grid) { |
| 3515 this._updateGridMetrics(); | 1847 this._updateGridMetrics(); |
| 3516 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 1848 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
| 3517 } else { | 1849 } else { |
| 3518 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 1850 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
| 3519 } | 1851 } |
| 3520 | |
| 3521 // update the average if we measured something | |
| 3522 if (this._physicalAverageCount !== prevAvgCount) { | 1852 if (this._physicalAverageCount !== prevAvgCount) { |
| 3523 this._physicalAverage = Math.round( | 1853 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new
PhysicalSize) / this._physicalAverageCount); |
| 3524 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
| 3525 this._physicalAverageCount); | |
| 3526 } | 1854 } |
| 3527 }, | 1855 }, |
| 3528 | |
| 3529 _updateGridMetrics: function() { | 1856 _updateGridMetrics: function() { |
| 3530 this._viewportWidth = this.$.items.offsetWidth; | 1857 this._viewportWidth = this.$.items.offsetWidth; |
| 3531 // Set item width to the value of the _physicalItems offsetWidth | |
| 3532 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 1858 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
| 3533 // Set row height to the value of the _physicalItems offsetHeight | |
| 3534 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 1859 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
| 3535 // If in grid mode compute how many items with exist in each row | |
| 3536 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 1860 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
| 3537 }, | 1861 }, |
| 3538 | |
| 3539 /** | |
| 3540 * Updates the position of the physical items. | |
| 3541 */ | |
| 3542 _positionItems: function() { | 1862 _positionItems: function() { |
| 3543 this._adjustScrollPosition(); | 1863 this._adjustScrollPosition(); |
| 3544 | |
| 3545 var y = this._physicalTop; | 1864 var y = this._physicalTop; |
| 3546 | |
| 3547 if (this.grid) { | 1865 if (this.grid) { |
| 3548 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 1866 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
| 3549 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 1867 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
| 3550 | |
| 3551 this._iterateItems(function(pidx, vidx) { | 1868 this._iterateItems(function(pidx, vidx) { |
| 3552 | |
| 3553 var modulus = vidx % this._itemsPerRow; | 1869 var modulus = vidx % this._itemsPerRow; |
| 3554 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | 1870 var x = Math.floor(modulus * this._itemWidth + rowOffset); |
| 3555 | |
| 3556 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | 1871 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
| 3557 | |
| 3558 if (this._shouldRenderNextRow(vidx)) { | 1872 if (this._shouldRenderNextRow(vidx)) { |
| 3559 y += this._rowHeight; | 1873 y += this._rowHeight; |
| 3560 } | 1874 } |
| 3561 | |
| 3562 }); | 1875 }); |
| 3563 } else { | 1876 } else { |
| 3564 this._iterateItems(function(pidx, vidx) { | 1877 this._iterateItems(function(pidx, vidx) { |
| 3565 | |
| 3566 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 1878 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
| 3567 y += this._physicalSizes[pidx]; | 1879 y += this._physicalSizes[pidx]; |
| 3568 | |
| 3569 }); | 1880 }); |
| 3570 } | 1881 } |
| 3571 }, | 1882 }, |
| 3572 | |
| 3573 _getPhysicalSizeIncrement: function(pidx) { | 1883 _getPhysicalSizeIncrement: function(pidx) { |
| 3574 if (!this.grid) { | 1884 if (!this.grid) { |
| 3575 return this._physicalSizes[pidx]; | 1885 return this._physicalSizes[pidx]; |
| 3576 } | 1886 } |
| 3577 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 1887 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
| 3578 return 0; | 1888 return 0; |
| 3579 } | 1889 } |
| 3580 return this._rowHeight; | 1890 return this._rowHeight; |
| 3581 }, | 1891 }, |
| 3582 | |
| 3583 /** | |
| 3584 * Returns, based on the current index, | |
| 3585 * whether or not the next index will need | |
| 3586 * to be rendered on a new row. | |
| 3587 * | |
| 3588 * @param {number} vidx Virtual index | |
| 3589 * @return {boolean} | |
| 3590 */ | |
| 3591 _shouldRenderNextRow: function(vidx) { | 1892 _shouldRenderNextRow: function(vidx) { |
| 3592 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 1893 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
| 3593 }, | 1894 }, |
| 3594 | |
| 3595 /** | |
| 3596 * Adjusts the scroll position when it was overestimated. | |
| 3597 */ | |
| 3598 _adjustScrollPosition: function() { | 1895 _adjustScrollPosition: function() { |
| 3599 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 1896 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(
this._scrollPosition + this._physicalTop, 0); |
| 3600 Math.min(this._scrollPosition + this._physicalTop, 0); | |
| 3601 | |
| 3602 if (deltaHeight) { | 1897 if (deltaHeight) { |
| 3603 this._physicalTop = this._physicalTop - deltaHeight; | 1898 this._physicalTop = this._physicalTop - deltaHeight; |
| 3604 // juking scroll position during interial scrolling on iOS is no bueno | |
| 3605 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 1899 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
| 3606 this._resetScrollPosition(this._scrollTop - deltaHeight); | 1900 this._resetScrollPosition(this._scrollTop - deltaHeight); |
| 3607 } | 1901 } |
| 3608 } | 1902 } |
| 3609 }, | 1903 }, |
| 3610 | |
| 3611 /** | |
| 3612 * Sets the position of the scroll. | |
| 3613 */ | |
| 3614 _resetScrollPosition: function(pos) { | 1904 _resetScrollPosition: function(pos) { |
| 3615 if (this.scrollTarget) { | 1905 if (this.scrollTarget) { |
| 3616 this._scrollTop = pos; | 1906 this._scrollTop = pos; |
| 3617 this._scrollPosition = this._scrollTop; | 1907 this._scrollPosition = this._scrollTop; |
| 3618 } | 1908 } |
| 3619 }, | 1909 }, |
| 3620 | |
| 3621 /** | |
| 3622 * Sets the scroll height, that's the height of the content, | |
| 3623 * | |
| 3624 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
| 3625 */ | |
| 3626 _updateScrollerSize: function(forceUpdate) { | 1910 _updateScrollerSize: function(forceUpdate) { |
| 3627 if (this.grid) { | 1911 if (this.grid) { |
| 3628 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 1912 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
| 3629 } else { | 1913 } else { |
| 3630 this._estScrollHeight = (this._physicalBottom + | 1914 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou
nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage; |
| 3631 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
| 3632 } | 1915 } |
| 3633 | |
| 3634 forceUpdate = forceUpdate || this._scrollHeight === 0; | 1916 forceUpdate = forceUpdate || this._scrollHeight === 0; |
| 3635 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 1917 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
| 3636 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 1918 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
| 3637 | |
| 3638 // amortize height adjustment, so it won't trigger repaints very often | |
| 3639 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 1919 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
| 3640 this.$.items.style.height = this._estScrollHeight + 'px'; | 1920 this.$.items.style.height = this._estScrollHeight + 'px'; |
| 3641 this._scrollHeight = this._estScrollHeight; | 1921 this._scrollHeight = this._estScrollHeight; |
| 3642 } | 1922 } |
| 3643 }, | 1923 }, |
| 3644 | 1924 scrollToItem: function(item) { |
| 3645 /** | |
| 3646 * Scroll to a specific item in the virtual list regardless | |
| 3647 * of the physical items in the DOM tree. | |
| 3648 * | |
| 3649 * @method scrollToItem | |
| 3650 * @param {(Object)} item The item to be scrolled to | |
| 3651 */ | |
| 3652 scrollToItem: function(item){ | |
| 3653 return this.scrollToIndex(this.items.indexOf(item)); | 1925 return this.scrollToIndex(this.items.indexOf(item)); |
| 3654 }, | 1926 }, |
| 3655 | |
| 3656 /** | |
| 3657 * Scroll to a specific index in the virtual list regardless | |
| 3658 * of the physical items in the DOM tree. | |
| 3659 * | |
| 3660 * @method scrollToIndex | |
| 3661 * @param {number} idx The index of the item | |
| 3662 */ | |
| 3663 scrollToIndex: function(idx) { | 1927 scrollToIndex: function(idx) { |
| 3664 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 1928 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
| 3665 return; | 1929 return; |
| 3666 } | 1930 } |
| 3667 | |
| 3668 Polymer.dom.flush(); | 1931 Polymer.dom.flush(); |
| 3669 | 1932 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1); |
| 3670 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
| 3671 // update the virtual start only when needed | |
| 3672 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 1933 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
| 3673 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 1934 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1; |
| 3674 } | 1935 } |
| 3675 // manage focus | |
| 3676 this._manageFocus(); | 1936 this._manageFocus(); |
| 3677 // assign new models | |
| 3678 this._assignModels(); | 1937 this._assignModels(); |
| 3679 // measure the new sizes | |
| 3680 this._updateMetrics(); | 1938 this._updateMetrics(); |
| 3681 | 1939 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
| 3682 // estimate new physical offset | |
| 3683 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
| 3684 this._physicalTop = estPhysicalTop; | 1940 this._physicalTop = estPhysicalTop; |
| 3685 | |
| 3686 var currentTopItem = this._physicalStart; | 1941 var currentTopItem = this._physicalStart; |
| 3687 var currentVirtualItem = this._virtualStart; | 1942 var currentVirtualItem = this._virtualStart; |
| 3688 var targetOffsetTop = 0; | 1943 var targetOffsetTop = 0; |
| 3689 var hiddenContentSize = this._hiddenContentSize; | 1944 var hiddenContentSize = this._hiddenContentSize; |
| 3690 | |
| 3691 // scroll to the item as much as we can | |
| 3692 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 1945 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
| 3693 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 1946 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
| 3694 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 1947 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
| 3695 currentVirtualItem++; | 1948 currentVirtualItem++; |
| 3696 } | 1949 } |
| 3697 // update the scroller size | |
| 3698 this._updateScrollerSize(true); | 1950 this._updateScrollerSize(true); |
| 3699 // update the position of the items | |
| 3700 this._positionItems(); | 1951 this._positionItems(); |
| 3701 // set the new scroll position | |
| 3702 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 1952 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
| 3703 // increase the pool of physical items if needed | |
| 3704 this._increasePoolIfNeeded(); | 1953 this._increasePoolIfNeeded(); |
| 3705 // clear cached visible index | |
| 3706 this._firstVisibleIndexVal = null; | 1954 this._firstVisibleIndexVal = null; |
| 3707 this._lastVisibleIndexVal = null; | 1955 this._lastVisibleIndexVal = null; |
| 3708 }, | 1956 }, |
| 3709 | |
| 3710 /** | |
| 3711 * Reset the physical average and the average count. | |
| 3712 */ | |
| 3713 _resetAverage: function() { | 1957 _resetAverage: function() { |
| 3714 this._physicalAverage = 0; | 1958 this._physicalAverage = 0; |
| 3715 this._physicalAverageCount = 0; | 1959 this._physicalAverageCount = 0; |
| 3716 }, | 1960 }, |
| 3717 | |
| 3718 /** | |
| 3719 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
| 3720 * when the element is resized. | |
| 3721 */ | |
| 3722 _resizeHandler: function() { | 1961 _resizeHandler: function() { |
| 3723 // iOS fires the resize event when the address bar slides up | |
| 3724 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 1962 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
| 3725 return; | 1963 return; |
| 3726 } | 1964 } |
| 3727 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
| 3728 // changing the scroll position from a resize handler would result in | |
| 3729 // the scroll position being reset. Waiting 1ms fixes the issue. | |
| 3730 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 1965 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
| 3731 this.updateViewportBoundaries(); | 1966 this.updateViewportBoundaries(); |
| 3732 this._render(); | 1967 this._render(); |
| 3733 | |
| 3734 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 1968 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
| 3735 this._resetAverage(); | 1969 this._resetAverage(); |
| 3736 this.scrollToIndex(this.firstVisibleIndex); | 1970 this.scrollToIndex(this.firstVisibleIndex); |
| 3737 } | 1971 } |
| 3738 }.bind(this), 1)); | 1972 }.bind(this), 1)); |
| 3739 }, | 1973 }, |
| 3740 | |
| 3741 _getModelFromItem: function(item) { | 1974 _getModelFromItem: function(item) { |
| 3742 var key = this._collection.getKey(item); | 1975 var key = this._collection.getKey(item); |
| 3743 var pidx = this._physicalIndexForKey[key]; | 1976 var pidx = this._physicalIndexForKey[key]; |
| 3744 | |
| 3745 if (pidx != null) { | 1977 if (pidx != null) { |
| 3746 return this._physicalItems[pidx]._templateInstance; | 1978 return this._physicalItems[pidx]._templateInstance; |
| 3747 } | 1979 } |
| 3748 return null; | 1980 return null; |
| 3749 }, | 1981 }, |
| 3750 | |
| 3751 /** | |
| 3752 * Gets a valid item instance from its index or the object value. | |
| 3753 * | |
| 3754 * @param {(Object|number)} item The item object or its index | |
| 3755 */ | |
| 3756 _getNormalizedItem: function(item) { | 1982 _getNormalizedItem: function(item) { |
| 3757 if (this._collection.getKey(item) === undefined) { | 1983 if (this._collection.getKey(item) === undefined) { |
| 3758 if (typeof item === 'number') { | 1984 if (typeof item === 'number') { |
| 3759 item = this.items[item]; | 1985 item = this.items[item]; |
| 3760 if (!item) { | 1986 if (!item) { |
| 3761 throw new RangeError('<item> not found'); | 1987 throw new RangeError('<item> not found'); |
| 3762 } | 1988 } |
| 3763 return item; | 1989 return item; |
| 3764 } | 1990 } |
| 3765 throw new TypeError('<item> should be a valid item'); | 1991 throw new TypeError('<item> should be a valid item'); |
| 3766 } | 1992 } |
| 3767 return item; | 1993 return item; |
| 3768 }, | 1994 }, |
| 3769 | |
| 3770 /** | |
| 3771 * Select the list item at the given index. | |
| 3772 * | |
| 3773 * @method selectItem | |
| 3774 * @param {(Object|number)} item The item object or its index | |
| 3775 */ | |
| 3776 selectItem: function(item) { | 1995 selectItem: function(item) { |
| 3777 item = this._getNormalizedItem(item); | 1996 item = this._getNormalizedItem(item); |
| 3778 var model = this._getModelFromItem(item); | 1997 var model = this._getModelFromItem(item); |
| 3779 | |
| 3780 if (!this.multiSelection && this.selectedItem) { | 1998 if (!this.multiSelection && this.selectedItem) { |
| 3781 this.deselectItem(this.selectedItem); | 1999 this.deselectItem(this.selectedItem); |
| 3782 } | 2000 } |
| 3783 if (model) { | 2001 if (model) { |
| 3784 model[this.selectedAs] = true; | 2002 model[this.selectedAs] = true; |
| 3785 } | 2003 } |
| 3786 this.$.selector.select(item); | 2004 this.$.selector.select(item); |
| 3787 this.updateSizeForItem(item); | 2005 this.updateSizeForItem(item); |
| 3788 }, | 2006 }, |
| 3789 | |
| 3790 /** | |
| 3791 * Deselects the given item list if it is already selected. | |
| 3792 * | |
| 3793 | |
| 3794 * @method deselect | |
| 3795 * @param {(Object|number)} item The item object or its index | |
| 3796 */ | |
| 3797 deselectItem: function(item) { | 2007 deselectItem: function(item) { |
| 3798 item = this._getNormalizedItem(item); | 2008 item = this._getNormalizedItem(item); |
| 3799 var model = this._getModelFromItem(item); | 2009 var model = this._getModelFromItem(item); |
| 3800 | |
| 3801 if (model) { | 2010 if (model) { |
| 3802 model[this.selectedAs] = false; | 2011 model[this.selectedAs] = false; |
| 3803 } | 2012 } |
| 3804 this.$.selector.deselect(item); | 2013 this.$.selector.deselect(item); |
| 3805 this.updateSizeForItem(item); | 2014 this.updateSizeForItem(item); |
| 3806 }, | 2015 }, |
| 3807 | |
| 3808 /** | |
| 3809 * Select or deselect a given item depending on whether the item | |
| 3810 * has already been selected. | |
| 3811 * | |
| 3812 * @method toggleSelectionForItem | |
| 3813 * @param {(Object|number)} item The item object or its index | |
| 3814 */ | |
| 3815 toggleSelectionForItem: function(item) { | 2016 toggleSelectionForItem: function(item) { |
| 3816 item = this._getNormalizedItem(item); | 2017 item = this._getNormalizedItem(item); |
| 3817 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 2018 if (this.$.selector.isSelected(item)) { |
| 3818 this.deselectItem(item); | 2019 this.deselectItem(item); |
| 3819 } else { | 2020 } else { |
| 3820 this.selectItem(item); | 2021 this.selectItem(item); |
| 3821 } | 2022 } |
| 3822 }, | 2023 }, |
| 3823 | |
| 3824 /** | |
| 3825 * Clears the current selection state of the list. | |
| 3826 * | |
| 3827 * @method clearSelection | |
| 3828 */ | |
| 3829 clearSelection: function() { | 2024 clearSelection: function() { |
| 3830 function unselect(item) { | 2025 function unselect(item) { |
| 3831 var model = this._getModelFromItem(item); | 2026 var model = this._getModelFromItem(item); |
| 3832 if (model) { | 2027 if (model) { |
| 3833 model[this.selectedAs] = false; | 2028 model[this.selectedAs] = false; |
| 3834 } | 2029 } |
| 3835 } | 2030 } |
| 3836 | |
| 3837 if (Array.isArray(this.selectedItems)) { | 2031 if (Array.isArray(this.selectedItems)) { |
| 3838 this.selectedItems.forEach(unselect, this); | 2032 this.selectedItems.forEach(unselect, this); |
| 3839 } else if (this.selectedItem) { | 2033 } else if (this.selectedItem) { |
| 3840 unselect.call(this, this.selectedItem); | 2034 unselect.call(this, this.selectedItem); |
| 3841 } | 2035 } |
| 3842 | 2036 this.$.selector.clearSelection(); |
| 3843 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
| 3844 }, | 2037 }, |
| 3845 | |
| 3846 /** | |
| 3847 * Add an event listener to `tap` if `selectionEnabled` is true, | |
| 3848 * it will remove the listener otherwise. | |
| 3849 */ | |
| 3850 _selectionEnabledChanged: function(selectionEnabled) { | 2038 _selectionEnabledChanged: function(selectionEnabled) { |
| 3851 var handler = selectionEnabled ? this.listen : this.unlisten; | 2039 var handler = selectionEnabled ? this.listen : this.unlisten; |
| 3852 handler.call(this, this, 'tap', '_selectionHandler'); | 2040 handler.call(this, this, 'tap', '_selectionHandler'); |
| 3853 }, | 2041 }, |
| 3854 | |
| 3855 /** | |
| 3856 * Select an item from an event object. | |
| 3857 */ | |
| 3858 _selectionHandler: function(e) { | 2042 _selectionHandler: function(e) { |
| 3859 var model = this.modelForElement(e.target); | 2043 var model = this.modelForElement(e.target); |
| 3860 if (!model) { | 2044 if (!model) { |
| 3861 return; | 2045 return; |
| 3862 } | 2046 } |
| 3863 var modelTabIndex, activeElTabIndex; | 2047 var modelTabIndex, activeElTabIndex; |
| 3864 var target = Polymer.dom(e).path[0]; | 2048 var target = Polymer.dom(e).path[0]; |
| 3865 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 2049 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
| 3866 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 2050 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
| 3867 // Safari does not focus certain form controls via mouse | 2051 if (target.localName === 'input' || target.localName === 'button' || targe
t.localName === 'select') { |
| 3868 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
| 3869 if (target.localName === 'input' || | |
| 3870 target.localName === 'button' || | |
| 3871 target.localName === 'select') { | |
| 3872 return; | 2052 return; |
| 3873 } | 2053 } |
| 3874 // Set a temporary tabindex | |
| 3875 modelTabIndex = model.tabIndex; | 2054 modelTabIndex = model.tabIndex; |
| 3876 model.tabIndex = SECRET_TABINDEX; | 2055 model.tabIndex = SECRET_TABINDEX; |
| 3877 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 2056 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
| 3878 model.tabIndex = modelTabIndex; | 2057 model.tabIndex = modelTabIndex; |
| 3879 // Only select the item if the tap wasn't on a focusable child | |
| 3880 // or the element bound to `tabIndex` | |
| 3881 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 2058 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
| 3882 return; | 2059 return; |
| 3883 } | 2060 } |
| 3884 this.toggleSelectionForItem(model[this.as]); | 2061 this.toggleSelectionForItem(model[this.as]); |
| 3885 }, | 2062 }, |
| 3886 | |
| 3887 _multiSelectionChanged: function(multiSelection) { | 2063 _multiSelectionChanged: function(multiSelection) { |
| 3888 this.clearSelection(); | 2064 this.clearSelection(); |
| 3889 this.$.selector.multi = multiSelection; | 2065 this.$.selector.multi = multiSelection; |
| 3890 }, | 2066 }, |
| 3891 | |
| 3892 /** | |
| 3893 * Updates the size of an item. | |
| 3894 * | |
| 3895 * @method updateSizeForItem | |
| 3896 * @param {(Object|number)} item The item object or its index | |
| 3897 */ | |
| 3898 updateSizeForItem: function(item) { | 2067 updateSizeForItem: function(item) { |
| 3899 item = this._getNormalizedItem(item); | 2068 item = this._getNormalizedItem(item); |
| 3900 var key = this._collection.getKey(item); | 2069 var key = this._collection.getKey(item); |
| 3901 var pidx = this._physicalIndexForKey[key]; | 2070 var pidx = this._physicalIndexForKey[key]; |
| 3902 | |
| 3903 if (pidx != null) { | 2071 if (pidx != null) { |
| 3904 this._updateMetrics([pidx]); | 2072 this._updateMetrics([ pidx ]); |
| 3905 this._positionItems(); | 2073 this._positionItems(); |
| 3906 } | 2074 } |
| 3907 }, | 2075 }, |
| 3908 | |
| 3909 /** | |
| 3910 * Creates a temporary backfill item in the rendered pool of physical items | |
| 3911 * to replace the main focused item. The focused item has tabIndex = 0 | |
| 3912 * and might be currently focused by the user. | |
| 3913 * | |
| 3914 * This dynamic replacement helps to preserve the focus state. | |
| 3915 */ | |
| 3916 _manageFocus: function() { | 2076 _manageFocus: function() { |
| 3917 var fidx = this._focusedIndex; | 2077 var fidx = this._focusedIndex; |
| 3918 | |
| 3919 if (fidx >= 0 && fidx < this._virtualCount) { | 2078 if (fidx >= 0 && fidx < this._virtualCount) { |
| 3920 // if it's a valid index, check if that index is rendered | |
| 3921 // in a physical item. | |
| 3922 if (this._isIndexRendered(fidx)) { | 2079 if (this._isIndexRendered(fidx)) { |
| 3923 this._restoreFocusedItem(); | 2080 this._restoreFocusedItem(); |
| 3924 } else { | 2081 } else { |
| 3925 this._createFocusBackfillItem(); | 2082 this._createFocusBackfillItem(); |
| 3926 } | 2083 } |
| 3927 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 2084 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
| 3928 // otherwise, assign the initial focused index. | |
| 3929 this._focusedIndex = this._virtualStart; | 2085 this._focusedIndex = this._virtualStart; |
| 3930 this._focusedItem = this._physicalItems[this._physicalStart]; | 2086 this._focusedItem = this._physicalItems[this._physicalStart]; |
| 3931 } | 2087 } |
| 3932 }, | 2088 }, |
| 3933 | |
| 3934 _isIndexRendered: function(idx) { | 2089 _isIndexRendered: function(idx) { |
| 3935 return idx >= this._virtualStart && idx <= this._virtualEnd; | 2090 return idx >= this._virtualStart && idx <= this._virtualEnd; |
| 3936 }, | 2091 }, |
| 3937 | |
| 3938 _isIndexVisible: function(idx) { | 2092 _isIndexVisible: function(idx) { |
| 3939 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 2093 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
| 3940 }, | 2094 }, |
| 3941 | |
| 3942 _getPhysicalIndex: function(idx) { | 2095 _getPhysicalIndex: function(idx) { |
| 3943 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 2096 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
| 3944 }, | 2097 }, |
| 3945 | |
| 3946 _focusPhysicalItem: function(idx) { | 2098 _focusPhysicalItem: function(idx) { |
| 3947 if (idx < 0 || idx >= this._virtualCount) { | 2099 if (idx < 0 || idx >= this._virtualCount) { |
| 3948 return; | 2100 return; |
| 3949 } | 2101 } |
| 3950 this._restoreFocusedItem(); | 2102 this._restoreFocusedItem(); |
| 3951 // scroll to index to make sure it's rendered | |
| 3952 if (!this._isIndexRendered(idx)) { | 2103 if (!this._isIndexRendered(idx)) { |
| 3953 this.scrollToIndex(idx); | 2104 this.scrollToIndex(idx); |
| 3954 } | 2105 } |
| 3955 | |
| 3956 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 2106 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
| 3957 var model = physicalItem._templateInstance; | 2107 var model = physicalItem._templateInstance; |
| 3958 var focusable; | 2108 var focusable; |
| 3959 | |
| 3960 // set a secret tab index | |
| 3961 model.tabIndex = SECRET_TABINDEX; | 2109 model.tabIndex = SECRET_TABINDEX; |
| 3962 // check if focusable element is the physical item | |
| 3963 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 2110 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
| 3964 focusable = physicalItem; | 2111 focusable = physicalItem; |
| 3965 } | 2112 } |
| 3966 // search for the element which tabindex is bound to the secret tab index | |
| 3967 if (!focusable) { | 2113 if (!focusable) { |
| 3968 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 2114 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
| 3969 } | 2115 } |
| 3970 // restore the tab index | |
| 3971 model.tabIndex = 0; | 2116 model.tabIndex = 0; |
| 3972 // focus the focusable element | |
| 3973 this._focusedIndex = idx; | 2117 this._focusedIndex = idx; |
| 3974 focusable && focusable.focus(); | 2118 focusable && focusable.focus(); |
| 3975 }, | 2119 }, |
| 3976 | |
| 3977 _removeFocusedItem: function() { | 2120 _removeFocusedItem: function() { |
| 3978 if (this._offscreenFocusedItem) { | 2121 if (this._offscreenFocusedItem) { |
| 3979 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 2122 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
| 3980 } | 2123 } |
| 3981 this._offscreenFocusedItem = null; | 2124 this._offscreenFocusedItem = null; |
| 3982 this._focusBackfillItem = null; | 2125 this._focusBackfillItem = null; |
| 3983 this._focusedItem = null; | 2126 this._focusedItem = null; |
| 3984 this._focusedIndex = -1; | 2127 this._focusedIndex = -1; |
| 3985 }, | 2128 }, |
| 3986 | |
| 3987 _createFocusBackfillItem: function() { | 2129 _createFocusBackfillItem: function() { |
| 3988 var pidx, fidx = this._focusedIndex; | 2130 var pidx, fidx = this._focusedIndex; |
| 3989 if (this._offscreenFocusedItem || fidx < 0) { | 2131 if (this._offscreenFocusedItem || fidx < 0) { |
| 3990 return; | 2132 return; |
| 3991 } | 2133 } |
| 3992 if (!this._focusBackfillItem) { | 2134 if (!this._focusBackfillItem) { |
| 3993 // create a physical item, so that it backfills the focused item. | |
| 3994 var stampedTemplate = this.stamp(null); | 2135 var stampedTemplate = this.stamp(null); |
| 3995 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 2136 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
| 3996 Polymer.dom(this).appendChild(stampedTemplate.root); | 2137 Polymer.dom(this).appendChild(stampedTemplate.root); |
| 3997 } | 2138 } |
| 3998 // get the physical index for the focused index | |
| 3999 pidx = this._getPhysicalIndex(fidx); | 2139 pidx = this._getPhysicalIndex(fidx); |
| 4000 | |
| 4001 if (pidx != null) { | 2140 if (pidx != null) { |
| 4002 // set the offcreen focused physical item | |
| 4003 this._offscreenFocusedItem = this._physicalItems[pidx]; | 2141 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 4004 // backfill the focused physical item | |
| 4005 this._physicalItems[pidx] = this._focusBackfillItem; | 2142 this._physicalItems[pidx] = this._focusBackfillItem; |
| 4006 // hide the focused physical | |
| 4007 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 2143 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 4008 } | 2144 } |
| 4009 }, | 2145 }, |
| 4010 | |
| 4011 _restoreFocusedItem: function() { | 2146 _restoreFocusedItem: function() { |
| 4012 var pidx, fidx = this._focusedIndex; | 2147 var pidx, fidx = this._focusedIndex; |
| 4013 | |
| 4014 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 2148 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
| 4015 return; | 2149 return; |
| 4016 } | 2150 } |
| 4017 // assign models to the focused index | |
| 4018 this._assignModels(); | 2151 this._assignModels(); |
| 4019 // get the new physical index for the focused index | |
| 4020 pidx = this._getPhysicalIndex(fidx); | 2152 pidx = this._getPhysicalIndex(fidx); |
| 4021 | |
| 4022 if (pidx != null) { | 2153 if (pidx != null) { |
| 4023 // flip the focus backfill | |
| 4024 this._focusBackfillItem = this._physicalItems[pidx]; | 2154 this._focusBackfillItem = this._physicalItems[pidx]; |
| 4025 // restore the focused physical item | |
| 4026 this._physicalItems[pidx] = this._offscreenFocusedItem; | 2155 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 4027 // reset the offscreen focused item | |
| 4028 this._offscreenFocusedItem = null; | 2156 this._offscreenFocusedItem = null; |
| 4029 // hide the physical item that backfills | |
| 4030 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 2157 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
| 4031 } | 2158 } |
| 4032 }, | 2159 }, |
| 4033 | |
| 4034 _didFocus: function(e) { | 2160 _didFocus: function(e) { |
| 4035 var targetModel = this.modelForElement(e.target); | 2161 var targetModel = this.modelForElement(e.target); |
| 4036 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 2162 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
| 4037 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 2163 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
| 4038 var fidx = this._focusedIndex; | 2164 var fidx = this._focusedIndex; |
| 4039 | |
| 4040 if (!targetModel || !focusedModel) { | 2165 if (!targetModel || !focusedModel) { |
| 4041 return; | 2166 return; |
| 4042 } | 2167 } |
| 4043 if (focusedModel === targetModel) { | 2168 if (focusedModel === targetModel) { |
| 4044 // if the user focused the same item, then bring it into view if it's no
t visible | |
| 4045 if (!this._isIndexVisible(fidx)) { | 2169 if (!this._isIndexVisible(fidx)) { |
| 4046 this.scrollToIndex(fidx); | 2170 this.scrollToIndex(fidx); |
| 4047 } | 2171 } |
| 4048 } else { | 2172 } else { |
| 4049 this._restoreFocusedItem(); | 2173 this._restoreFocusedItem(); |
| 4050 // restore tabIndex for the currently focused item | |
| 4051 focusedModel.tabIndex = -1; | 2174 focusedModel.tabIndex = -1; |
| 4052 // set the tabIndex for the next focused item | |
| 4053 targetModel.tabIndex = 0; | 2175 targetModel.tabIndex = 0; |
| 4054 fidx = targetModel[this.indexAs]; | 2176 fidx = targetModel[this.indexAs]; |
| 4055 this._focusedIndex = fidx; | 2177 this._focusedIndex = fidx; |
| 4056 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 2178 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
| 4057 | |
| 4058 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 2179 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
| 4059 this._update(); | 2180 this._update(); |
| 4060 } | 2181 } |
| 4061 } | 2182 } |
| 4062 }, | 2183 }, |
| 4063 | |
| 4064 _didMoveUp: function() { | 2184 _didMoveUp: function() { |
| 4065 this._focusPhysicalItem(this._focusedIndex - 1); | 2185 this._focusPhysicalItem(this._focusedIndex - 1); |
| 4066 }, | 2186 }, |
| 4067 | |
| 4068 _didMoveDown: function(e) { | 2187 _didMoveDown: function(e) { |
| 4069 // disable scroll when pressing the down key | |
| 4070 e.detail.keyboardEvent.preventDefault(); | 2188 e.detail.keyboardEvent.preventDefault(); |
| 4071 this._focusPhysicalItem(this._focusedIndex + 1); | 2189 this._focusPhysicalItem(this._focusedIndex + 1); |
| 4072 }, | 2190 }, |
| 4073 | |
| 4074 _didEnter: function(e) { | 2191 _didEnter: function(e) { |
| 4075 this._focusPhysicalItem(this._focusedIndex); | 2192 this._focusPhysicalItem(this._focusedIndex); |
| 4076 this._selectionHandler(e.detail.keyboardEvent); | 2193 this._selectionHandler(e.detail.keyboardEvent); |
| 4077 } | 2194 } |
| 4078 }); | 2195 }); |
| 2196 })(); |
| 4079 | 2197 |
| 4080 })(); | |
| 4081 // Copyright 2015 The Chromium Authors. All rights reserved. | 2198 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 4082 // Use of this source code is governed by a BSD-style license that can be | 2199 // Use of this source code is governed by a BSD-style license that can be |
| 4083 // found in the LICENSE file. | 2200 // found in the LICENSE file. |
| 4084 | |
| 4085 cr.define('downloads', function() { | 2201 cr.define('downloads', function() { |
| 4086 /** | |
| 4087 * @param {string} chromeSendName | |
| 4088 * @return {function(string):void} A chrome.send() callback with curried name. | |
| 4089 */ | |
| 4090 function chromeSendWithId(chromeSendName) { | 2202 function chromeSendWithId(chromeSendName) { |
| 4091 return function(id) { chrome.send(chromeSendName, [id]); }; | 2203 return function(id) { |
| 2204 chrome.send(chromeSendName, [ id ]); |
| 2205 }; |
| 4092 } | 2206 } |
| 4093 | |
| 4094 /** @constructor */ | |
| 4095 function ActionService() { | 2207 function ActionService() { |
| 4096 /** @private {Array<string>} */ | |
| 4097 this.searchTerms_ = []; | 2208 this.searchTerms_ = []; |
| 4098 } | 2209 } |
| 4099 | 2210 function trim(s) { |
| 4100 /** | 2211 return s.trim(); |
| 4101 * @param {string} s | 2212 } |
| 4102 * @return {string} |s| without whitespace at the beginning or end. | 2213 function truthy(value) { |
| 4103 */ | 2214 return !!value; |
| 4104 function trim(s) { return s.trim(); } | 2215 } |
| 4105 | |
| 4106 /** | |
| 4107 * @param {string|undefined} value | |
| 4108 * @return {boolean} Whether |value| is truthy. | |
| 4109 */ | |
| 4110 function truthy(value) { return !!value; } | |
| 4111 | |
| 4112 /** | |
| 4113 * @param {string} searchText Input typed by the user into a search box. | |
| 4114 * @return {Array<string>} A list of terms extracted from |searchText|. | |
| 4115 */ | |
| 4116 ActionService.splitTerms = function(searchText) { | 2216 ActionService.splitTerms = function(searchText) { |
| 4117 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
| 4118 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | 2217 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); |
| 4119 }; | 2218 }; |
| 4120 | |
| 4121 ActionService.prototype = { | 2219 ActionService.prototype = { |
| 4122 /** @param {string} id ID of the download to cancel. */ | |
| 4123 cancel: chromeSendWithId('cancel'), | 2220 cancel: chromeSendWithId('cancel'), |
| 4124 | |
| 4125 /** Instructs the browser to clear all finished downloads. */ | |
| 4126 clearAll: function() { | 2221 clearAll: function() { |
| 4127 if (loadTimeData.getBoolean('allowDeletingHistory')) { | 2222 if (loadTimeData.getBoolean('allowDeletingHistory')) { |
| 4128 chrome.send('clearAll'); | 2223 chrome.send('clearAll'); |
| 4129 this.search(''); | 2224 this.search(''); |
| 4130 } | 2225 } |
| 4131 }, | 2226 }, |
| 4132 | |
| 4133 /** @param {string} id ID of the dangerous download to discard. */ | |
| 4134 discardDangerous: chromeSendWithId('discardDangerous'), | 2227 discardDangerous: chromeSendWithId('discardDangerous'), |
| 4135 | |
| 4136 /** @param {string} url URL of a file to download. */ | |
| 4137 download: function(url) { | 2228 download: function(url) { |
| 4138 var a = document.createElement('a'); | 2229 var a = document.createElement('a'); |
| 4139 a.href = url; | 2230 a.href = url; |
| 4140 a.setAttribute('download', ''); | 2231 a.setAttribute('download', ''); |
| 4141 a.click(); | 2232 a.click(); |
| 4142 }, | 2233 }, |
| 4143 | |
| 4144 /** @param {string} id ID of the download that the user started dragging. */ | |
| 4145 drag: chromeSendWithId('drag'), | 2234 drag: chromeSendWithId('drag'), |
| 4146 | |
| 4147 /** Loads more downloads with the current search terms. */ | |
| 4148 loadMore: function() { | 2235 loadMore: function() { |
| 4149 chrome.send('getDownloads', this.searchTerms_); | 2236 chrome.send('getDownloads', this.searchTerms_); |
| 4150 }, | 2237 }, |
| 4151 | |
| 4152 /** | |
| 4153 * @return {boolean} Whether the user is currently searching for downloads | |
| 4154 * (i.e. has a non-empty search term). | |
| 4155 */ | |
| 4156 isSearching: function() { | 2238 isSearching: function() { |
| 4157 return this.searchTerms_.length > 0; | 2239 return this.searchTerms_.length > 0; |
| 4158 }, | 2240 }, |
| 4159 | |
| 4160 /** Opens the current local destination for downloads. */ | |
| 4161 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | 2241 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), |
| 4162 | |
| 4163 /** | |
| 4164 * @param {string} id ID of the download to run locally on the user's box. | |
| 4165 */ | |
| 4166 openFile: chromeSendWithId('openFile'), | 2242 openFile: chromeSendWithId('openFile'), |
| 4167 | |
| 4168 /** @param {string} id ID the of the progressing download to pause. */ | |
| 4169 pause: chromeSendWithId('pause'), | 2243 pause: chromeSendWithId('pause'), |
| 4170 | |
| 4171 /** @param {string} id ID of the finished download to remove. */ | |
| 4172 remove: chromeSendWithId('remove'), | 2244 remove: chromeSendWithId('remove'), |
| 4173 | |
| 4174 /** @param {string} id ID of the paused download to resume. */ | |
| 4175 resume: chromeSendWithId('resume'), | 2245 resume: chromeSendWithId('resume'), |
| 4176 | |
| 4177 /** | |
| 4178 * @param {string} id ID of the dangerous download to save despite | |
| 4179 * warnings. | |
| 4180 */ | |
| 4181 saveDangerous: chromeSendWithId('saveDangerous'), | 2246 saveDangerous: chromeSendWithId('saveDangerous'), |
| 4182 | |
| 4183 /** @param {string} searchText What to search for. */ | |
| 4184 search: function(searchText) { | 2247 search: function(searchText) { |
| 4185 var searchTerms = ActionService.splitTerms(searchText); | 2248 var searchTerms = ActionService.splitTerms(searchText); |
| 4186 var sameTerms = searchTerms.length == this.searchTerms_.length; | 2249 var sameTerms = searchTerms.length == this.searchTerms_.length; |
| 4187 | |
| 4188 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | 2250 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { |
| 4189 if (searchTerms[i] != this.searchTerms_[i]) | 2251 if (searchTerms[i] != this.searchTerms_[i]) sameTerms = false; |
| 4190 sameTerms = false; | |
| 4191 } | 2252 } |
| 4192 | 2253 if (sameTerms) return; |
| 4193 if (sameTerms) | |
| 4194 return; | |
| 4195 | |
| 4196 this.searchTerms_ = searchTerms; | 2254 this.searchTerms_ = searchTerms; |
| 4197 this.loadMore(); | 2255 this.loadMore(); |
| 4198 }, | 2256 }, |
| 2257 show: chromeSendWithId('show'), |
| 2258 undo: chrome.send.bind(chrome, 'undo') |
| 2259 }; |
| 2260 cr.addSingletonGetter(ActionService); |
| 2261 return { |
| 2262 ActionService: ActionService |
| 2263 }; |
| 2264 }); |
| 4199 | 2265 |
| 4200 /** | |
| 4201 * Shows the local folder a finished download resides in. | |
| 4202 * @param {string} id ID of the download to show. | |
| 4203 */ | |
| 4204 show: chromeSendWithId('show'), | |
| 4205 | |
| 4206 /** Undo download removal. */ | |
| 4207 undo: chrome.send.bind(chrome, 'undo'), | |
| 4208 }; | |
| 4209 | |
| 4210 cr.addSingletonGetter(ActionService); | |
| 4211 | |
| 4212 return {ActionService: ActionService}; | |
| 4213 }); | |
| 4214 // Copyright 2015 The Chromium Authors. All rights reserved. | 2266 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 4215 // Use of this source code is governed by a BSD-style license that can be | 2267 // Use of this source code is governed by a BSD-style license that can be |
| 4216 // found in the LICENSE file. | 2268 // found in the LICENSE file. |
| 4217 | |
| 4218 cr.define('downloads', function() { | 2269 cr.define('downloads', function() { |
| 4219 /** | |
| 4220 * Explains why a download is in DANGEROUS state. | |
| 4221 * @enum {string} | |
| 4222 */ | |
| 4223 var DangerType = { | 2270 var DangerType = { |
| 4224 NOT_DANGEROUS: 'NOT_DANGEROUS', | 2271 NOT_DANGEROUS: 'NOT_DANGEROUS', |
| 4225 DANGEROUS_FILE: 'DANGEROUS_FILE', | 2272 DANGEROUS_FILE: 'DANGEROUS_FILE', |
| 4226 DANGEROUS_URL: 'DANGEROUS_URL', | 2273 DANGEROUS_URL: 'DANGEROUS_URL', |
| 4227 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | 2274 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', |
| 4228 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | 2275 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', |
| 4229 DANGEROUS_HOST: 'DANGEROUS_HOST', | 2276 DANGEROUS_HOST: 'DANGEROUS_HOST', |
| 4230 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | 2277 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED' |
| 4231 }; | 2278 }; |
| 4232 | |
| 4233 /** | |
| 4234 * The states a download can be in. These correspond to states defined in | |
| 4235 * DownloadsDOMHandler::CreateDownloadItemValue | |
| 4236 * @enum {string} | |
| 4237 */ | |
| 4238 var States = { | 2279 var States = { |
| 4239 IN_PROGRESS: 'IN_PROGRESS', | 2280 IN_PROGRESS: 'IN_PROGRESS', |
| 4240 CANCELLED: 'CANCELLED', | 2281 CANCELLED: 'CANCELLED', |
| 4241 COMPLETE: 'COMPLETE', | 2282 COMPLETE: 'COMPLETE', |
| 4242 PAUSED: 'PAUSED', | 2283 PAUSED: 'PAUSED', |
| 4243 DANGEROUS: 'DANGEROUS', | 2284 DANGEROUS: 'DANGEROUS', |
| 4244 INTERRUPTED: 'INTERRUPTED', | 2285 INTERRUPTED: 'INTERRUPTED' |
| 4245 }; | 2286 }; |
| 4246 | |
| 4247 return { | 2287 return { |
| 4248 DangerType: DangerType, | 2288 DangerType: DangerType, |
| 4249 States: States, | 2289 States: States |
| 4250 }; | 2290 }; |
| 4251 }); | 2291 }); |
| 2292 |
| 4252 // Copyright 2014 The Chromium Authors. All rights reserved. | 2293 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 4253 // Use of this source code is governed by a BSD-style license that can be | 2294 // Use of this source code is governed by a BSD-style license that can be |
| 4254 // found in the LICENSE file. | 2295 // found in the LICENSE file. |
| 4255 | |
| 4256 // Action links are elements that are used to perform an in-page navigation or | |
| 4257 // action (e.g. showing a dialog). | |
| 4258 // | |
| 4259 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
| 4260 // they're subtly different as they're not initially underlined (giving users a | |
| 4261 // clue that underlined links navigate while action links don't). | |
| 4262 // | |
| 4263 // Action links look very similar to normal links when hovered (hand cursor, | |
| 4264 // underlined). This gives the user an idea that clicking this link will do | |
| 4265 // something similar to navigation but in the same page. | |
| 4266 // | |
| 4267 // They can be created in JavaScript like this: | |
| 4268 // | |
| 4269 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
| 4270 // | |
| 4271 // or with a constructor like this: | |
| 4272 // | |
| 4273 // var link = new ActionLink(); | |
| 4274 // | |
| 4275 // They can be used easily from HTML as well, like so: | |
| 4276 // | |
| 4277 // <a is="action-link">Click me!</a> | |
| 4278 // | |
| 4279 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
| 4280 | |
| 4281 /** | |
| 4282 * @constructor | |
| 4283 * @extends {HTMLAnchorElement} | |
| 4284 */ | |
| 4285 var ActionLink = document.registerElement('action-link', { | 2296 var ActionLink = document.registerElement('action-link', { |
| 4286 prototype: { | 2297 prototype: { |
| 4287 __proto__: HTMLAnchorElement.prototype, | 2298 __proto__: HTMLAnchorElement.prototype, |
| 4288 | |
| 4289 /** @this {ActionLink} */ | |
| 4290 createdCallback: function() { | 2299 createdCallback: function() { |
| 4291 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
| 4292 this.tabIndex = this.disabled ? -1 : 0; | 2300 this.tabIndex = this.disabled ? -1 : 0; |
| 4293 | 2301 if (!this.hasAttribute('role')) this.setAttribute('role', 'link'); |
| 4294 if (!this.hasAttribute('role')) | |
| 4295 this.setAttribute('role', 'link'); | |
| 4296 | |
| 4297 this.addEventListener('keydown', function(e) { | 2302 this.addEventListener('keydown', function(e) { |
| 4298 if (!this.disabled && e.key == 'Enter' && !this.href) { | 2303 if (!this.disabled && e.key == 'Enter' && !this.href) { |
| 4299 // Schedule a click asynchronously because other 'keydown' handlers | |
| 4300 // may still run later (e.g. document.addEventListener('keydown')). | |
| 4301 // Specifically options dialogs break when this timeout isn't here. | |
| 4302 // NOTE: this affects the "trusted" state of the ensuing click. I | |
| 4303 // haven't found anything that breaks because of this (yet). | |
| 4304 window.setTimeout(this.click.bind(this), 0); | 2304 window.setTimeout(this.click.bind(this), 0); |
| 4305 } | 2305 } |
| 4306 }); | 2306 }); |
| 4307 | |
| 4308 function preventDefault(e) { | 2307 function preventDefault(e) { |
| 4309 e.preventDefault(); | 2308 e.preventDefault(); |
| 4310 } | 2309 } |
| 4311 | |
| 4312 function removePreventDefault() { | 2310 function removePreventDefault() { |
| 4313 document.removeEventListener('selectstart', preventDefault); | 2311 document.removeEventListener('selectstart', preventDefault); |
| 4314 document.removeEventListener('mouseup', removePreventDefault); | 2312 document.removeEventListener('mouseup', removePreventDefault); |
| 4315 } | 2313 } |
| 4316 | |
| 4317 this.addEventListener('mousedown', function() { | 2314 this.addEventListener('mousedown', function() { |
| 4318 // This handlers strives to match the behavior of <a href="...">. | |
| 4319 | |
| 4320 // While the mouse is down, prevent text selection from dragging. | |
| 4321 document.addEventListener('selectstart', preventDefault); | 2315 document.addEventListener('selectstart', preventDefault); |
| 4322 document.addEventListener('mouseup', removePreventDefault); | 2316 document.addEventListener('mouseup', removePreventDefault); |
| 4323 | 2317 if (document.activeElement != this) this.classList.add('no-outline'); |
| 4324 // If focus started via mouse press, don't show an outline. | |
| 4325 if (document.activeElement != this) | |
| 4326 this.classList.add('no-outline'); | |
| 4327 }); | 2318 }); |
| 4328 | |
| 4329 this.addEventListener('blur', function() { | 2319 this.addEventListener('blur', function() { |
| 4330 this.classList.remove('no-outline'); | 2320 this.classList.remove('no-outline'); |
| 4331 }); | 2321 }); |
| 4332 }, | 2322 }, |
| 4333 | |
| 4334 /** @type {boolean} */ | |
| 4335 set disabled(disabled) { | 2323 set disabled(disabled) { |
| 4336 if (disabled) | 2324 if (disabled) HTMLAnchorElement.prototype.setAttribute.call(this, 'disable
d', ''); else HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled')
; |
| 4337 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', ''); | |
| 4338 else | |
| 4339 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled'); | |
| 4340 this.tabIndex = disabled ? -1 : 0; | 2325 this.tabIndex = disabled ? -1 : 0; |
| 4341 }, | 2326 }, |
| 4342 get disabled() { | 2327 get disabled() { |
| 4343 return this.hasAttribute('disabled'); | 2328 return this.hasAttribute('disabled'); |
| 4344 }, | 2329 }, |
| 4345 | |
| 4346 /** @override */ | |
| 4347 setAttribute: function(attr, val) { | 2330 setAttribute: function(attr, val) { |
| 4348 if (attr.toLowerCase() == 'disabled') | 2331 if (attr.toLowerCase() == 'disabled') this.disabled = true; else HTMLAncho
rElement.prototype.setAttribute.apply(this, arguments); |
| 4349 this.disabled = true; | 2332 }, |
| 4350 else | |
| 4351 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments); | |
| 4352 }, | |
| 4353 | |
| 4354 /** @override */ | |
| 4355 removeAttribute: function(attr) { | 2333 removeAttribute: function(attr) { |
| 4356 if (attr.toLowerCase() == 'disabled') | 2334 if (attr.toLowerCase() == 'disabled') this.disabled = false; else HTMLAnch
orElement.prototype.removeAttribute.apply(this, arguments); |
| 4357 this.disabled = false; | 2335 } |
| 4358 else | 2336 }, |
| 4359 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | 2337 "extends": 'a' |
| 4360 }, | |
| 4361 }, | |
| 4362 | |
| 4363 extends: 'a', | |
| 4364 }); | 2338 }); |
| 2339 |
| 4365 (function() { | 2340 (function() { |
| 4366 | 2341 var metaDatas = {}; |
| 4367 // monostate data | 2342 var metaArrays = {}; |
| 4368 var metaDatas = {}; | 2343 var singleton = null; |
| 4369 var metaArrays = {}; | 2344 Polymer.IronMeta = Polymer({ |
| 4370 var singleton = null; | 2345 is: 'iron-meta', |
| 4371 | 2346 properties: { |
| 4372 Polymer.IronMeta = Polymer({ | 2347 type: { |
| 4373 | 2348 type: String, |
| 4374 is: 'iron-meta', | 2349 value: 'default', |
| 4375 | 2350 observer: '_typeChanged' |
| 4376 properties: { | 2351 }, |
| 4377 | 2352 key: { |
| 4378 /** | 2353 type: String, |
| 4379 * The type of meta-data. All meta-data of the same type is stored | 2354 observer: '_keyChanged' |
| 4380 * together. | 2355 }, |
| 4381 */ | 2356 value: { |
| 4382 type: { | 2357 type: Object, |
| 4383 type: String, | 2358 notify: true, |
| 4384 value: 'default', | 2359 observer: '_valueChanged' |
| 4385 observer: '_typeChanged' | 2360 }, |
| 4386 }, | 2361 self: { |
| 4387 | 2362 type: Boolean, |
| 4388 /** | 2363 observer: '_selfChanged' |
| 4389 * The key used to store `value` under the `type` namespace. | 2364 }, |
| 4390 */ | 2365 list: { |
| 4391 key: { | 2366 type: Array, |
| 4392 type: String, | 2367 notify: true |
| 4393 observer: '_keyChanged' | 2368 } |
| 4394 }, | 2369 }, |
| 4395 | 2370 hostAttributes: { |
| 4396 /** | 2371 hidden: true |
| 4397 * The meta-data to store or retrieve. | 2372 }, |
| 4398 */ | 2373 factoryImpl: function(config) { |
| 4399 value: { | 2374 if (config) { |
| 4400 type: Object, | 2375 for (var n in config) { |
| 4401 notify: true, | 2376 switch (n) { |
| 4402 observer: '_valueChanged' | 2377 case 'type': |
| 4403 }, | 2378 case 'key': |
| 4404 | 2379 case 'value': |
| 4405 /** | 2380 this[n] = config[n]; |
| 4406 * If true, `value` is set to the iron-meta instance itself. | 2381 break; |
| 4407 */ | |
| 4408 self: { | |
| 4409 type: Boolean, | |
| 4410 observer: '_selfChanged' | |
| 4411 }, | |
| 4412 | |
| 4413 /** | |
| 4414 * Array of all meta-data values for the given type. | |
| 4415 */ | |
| 4416 list: { | |
| 4417 type: Array, | |
| 4418 notify: true | |
| 4419 } | |
| 4420 | |
| 4421 }, | |
| 4422 | |
| 4423 hostAttributes: { | |
| 4424 hidden: true | |
| 4425 }, | |
| 4426 | |
| 4427 /** | |
| 4428 * Only runs if someone invokes the factory/constructor directly | |
| 4429 * e.g. `new Polymer.IronMeta()` | |
| 4430 * | |
| 4431 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
| 4432 */ | |
| 4433 factoryImpl: function(config) { | |
| 4434 if (config) { | |
| 4435 for (var n in config) { | |
| 4436 switch(n) { | |
| 4437 case 'type': | |
| 4438 case 'key': | |
| 4439 case 'value': | |
| 4440 this[n] = config[n]; | |
| 4441 break; | |
| 4442 } | |
| 4443 } | 2382 } |
| 4444 } | 2383 } |
| 4445 }, | 2384 } |
| 4446 | 2385 }, |
| 4447 created: function() { | 2386 created: function() { |
| 4448 // TODO(sjmiles): good for debugging? | 2387 this._metaDatas = metaDatas; |
| 4449 this._metaDatas = metaDatas; | 2388 this._metaArrays = metaArrays; |
| 4450 this._metaArrays = metaArrays; | 2389 }, |
| 4451 }, | 2390 _keyChanged: function(key, old) { |
| 4452 | 2391 this._resetRegistration(old); |
| 4453 _keyChanged: function(key, old) { | 2392 }, |
| 4454 this._resetRegistration(old); | 2393 _valueChanged: function(value) { |
| 4455 }, | 2394 this._resetRegistration(this.key); |
| 4456 | 2395 }, |
| 4457 _valueChanged: function(value) { | 2396 _selfChanged: function(self) { |
| 4458 this._resetRegistration(this.key); | 2397 if (self) { |
| 4459 }, | 2398 this.value = this; |
| 4460 | 2399 } |
| 4461 _selfChanged: function(self) { | 2400 }, |
| 4462 if (self) { | 2401 _typeChanged: function(type) { |
| 4463 this.value = this; | 2402 this._unregisterKey(this.key); |
| 2403 if (!metaDatas[type]) { |
| 2404 metaDatas[type] = {}; |
| 2405 } |
| 2406 this._metaData = metaDatas[type]; |
| 2407 if (!metaArrays[type]) { |
| 2408 metaArrays[type] = []; |
| 2409 } |
| 2410 this.list = metaArrays[type]; |
| 2411 this._registerKeyValue(this.key, this.value); |
| 2412 }, |
| 2413 byKey: function(key) { |
| 2414 return this._metaData && this._metaData[key]; |
| 2415 }, |
| 2416 _resetRegistration: function(oldKey) { |
| 2417 this._unregisterKey(oldKey); |
| 2418 this._registerKeyValue(this.key, this.value); |
| 2419 }, |
| 2420 _unregisterKey: function(key) { |
| 2421 this._unregister(key, this._metaData, this.list); |
| 2422 }, |
| 2423 _registerKeyValue: function(key, value) { |
| 2424 this._register(key, value, this._metaData, this.list); |
| 2425 }, |
| 2426 _register: function(key, value, data, list) { |
| 2427 if (key && data && value !== undefined) { |
| 2428 data[key] = value; |
| 2429 list.push(value); |
| 2430 } |
| 2431 }, |
| 2432 _unregister: function(key, data, list) { |
| 2433 if (key && data) { |
| 2434 if (key in data) { |
| 2435 var value = data[key]; |
| 2436 delete data[key]; |
| 2437 this.arrayDelete(list, value); |
| 4464 } | 2438 } |
| 4465 }, | 2439 } |
| 4466 | 2440 } |
| 4467 _typeChanged: function(type) { | 2441 }); |
| 4468 this._unregisterKey(this.key); | 2442 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
| 4469 if (!metaDatas[type]) { | 2443 if (singleton === null) { |
| 4470 metaDatas[type] = {}; | 2444 singleton = new Polymer.IronMeta(); |
| 4471 } | 2445 } |
| 4472 this._metaData = metaDatas[type]; | 2446 return singleton; |
| 4473 if (!metaArrays[type]) { | 2447 }; |
| 4474 metaArrays[type] = []; | 2448 Polymer.IronMetaQuery = Polymer({ |
| 4475 } | 2449 is: 'iron-meta-query', |
| 4476 this.list = metaArrays[type]; | 2450 properties: { |
| 4477 this._registerKeyValue(this.key, this.value); | 2451 type: { |
| 4478 }, | 2452 type: String, |
| 4479 | 2453 value: 'default', |
| 4480 /** | 2454 observer: '_typeChanged' |
| 4481 * Retrieves meta data value by key. | 2455 }, |
| 4482 * | 2456 key: { |
| 4483 * @method byKey | 2457 type: String, |
| 4484 * @param {string} key The key of the meta-data to be returned. | 2458 observer: '_keyChanged' |
| 4485 * @return {*} | 2459 }, |
| 4486 */ | 2460 value: { |
| 4487 byKey: function(key) { | 2461 type: Object, |
| 4488 return this._metaData && this._metaData[key]; | 2462 notify: true, |
| 4489 }, | 2463 readOnly: true |
| 4490 | 2464 }, |
| 4491 _resetRegistration: function(oldKey) { | 2465 list: { |
| 4492 this._unregisterKey(oldKey); | 2466 type: Array, |
| 4493 this._registerKeyValue(this.key, this.value); | 2467 notify: true |
| 4494 }, | 2468 } |
| 4495 | 2469 }, |
| 4496 _unregisterKey: function(key) { | 2470 factoryImpl: function(config) { |
| 4497 this._unregister(key, this._metaData, this.list); | 2471 if (config) { |
| 4498 }, | 2472 for (var n in config) { |
| 4499 | 2473 switch (n) { |
| 4500 _registerKeyValue: function(key, value) { | 2474 case 'type': |
| 4501 this._register(key, value, this._metaData, this.list); | 2475 case 'key': |
| 4502 }, | 2476 this[n] = config[n]; |
| 4503 | 2477 break; |
| 4504 _register: function(key, value, data, list) { | |
| 4505 if (key && data && value !== undefined) { | |
| 4506 data[key] = value; | |
| 4507 list.push(value); | |
| 4508 } | |
| 4509 }, | |
| 4510 | |
| 4511 _unregister: function(key, data, list) { | |
| 4512 if (key && data) { | |
| 4513 if (key in data) { | |
| 4514 var value = data[key]; | |
| 4515 delete data[key]; | |
| 4516 this.arrayDelete(list, value); | |
| 4517 } | 2478 } |
| 4518 } | 2479 } |
| 4519 } | 2480 } |
| 4520 | 2481 }, |
| 4521 }); | 2482 created: function() { |
| 4522 | 2483 this._metaDatas = metaDatas; |
| 4523 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 2484 this._metaArrays = metaArrays; |
| 4524 if (singleton === null) { | 2485 }, |
| 4525 singleton = new Polymer.IronMeta(); | 2486 _keyChanged: function(key) { |
| 4526 } | 2487 this._setValue(this._metaData && this._metaData[key]); |
| 4527 return singleton; | 2488 }, |
| 4528 }; | 2489 _typeChanged: function(type) { |
| 4529 | 2490 this._metaData = metaDatas[type]; |
| 4530 /** | 2491 this.list = metaArrays[type]; |
| 4531 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | 2492 if (this.key) { |
| 4532 | 2493 this._keyChanged(this.key); |
| 4533 Examples: | 2494 } |
| 4534 | 2495 }, |
| 4535 If I create an instance like this: | 2496 byKey: function(key) { |
| 4536 | 2497 return this._metaData && this._metaData[key]; |
| 4537 <iron-meta key="info" value="foo/bar"></iron-meta> | 2498 } |
| 4538 | 2499 }); |
| 4539 Note that value="foo/bar" is the metadata I've defined. I could define more | 2500 })(); |
| 4540 attributes or use child nodes to define additional metadata. | 2501 |
| 4541 | 2502 Polymer({ |
| 4542 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | 2503 is: 'iron-icon', |
| 4543 | 2504 properties: { |
| 4544 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | 2505 icon: { |
| 4545 | 2506 type: String, |
| 4546 @group Polymer Iron Elements | 2507 observer: '_iconChanged' |
| 4547 @element iron-meta-query | 2508 }, |
| 4548 */ | 2509 theme: { |
| 4549 Polymer.IronMetaQuery = Polymer({ | 2510 type: String, |
| 4550 | 2511 observer: '_updateIcon' |
| 4551 is: 'iron-meta-query', | 2512 }, |
| 4552 | 2513 src: { |
| 4553 properties: { | 2514 type: String, |
| 4554 | 2515 observer: '_srcChanged' |
| 4555 /** | 2516 }, |
| 4556 * The type of meta-data. All meta-data of the same type is stored | 2517 _meta: { |
| 4557 * together. | 2518 value: Polymer.Base.create('iron-meta', { |
| 4558 */ | 2519 type: 'iconset' |
| 4559 type: { | 2520 }), |
| 4560 type: String, | 2521 observer: '_updateIcon' |
| 4561 value: 'default', | 2522 } |
| 4562 observer: '_typeChanged' | 2523 }, |
| 4563 }, | 2524 _DEFAULT_ICONSET: 'icons', |
| 4564 | 2525 _iconChanged: function(icon) { |
| 4565 /** | 2526 var parts = (icon || '').split(':'); |
| 4566 * Specifies a key to use for retrieving `value` from the `type` | 2527 this._iconName = parts.pop(); |
| 4567 * namespace. | 2528 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
| 4568 */ | 2529 this._updateIcon(); |
| 4569 key: { | 2530 }, |
| 4570 type: String, | 2531 _srcChanged: function(src) { |
| 4571 observer: '_keyChanged' | 2532 this._updateIcon(); |
| 4572 }, | 2533 }, |
| 4573 | 2534 _usesIconset: function() { |
| 4574 /** | 2535 return this.icon || !this.src; |
| 4575 * The meta-data to store or retrieve. | 2536 }, |
| 4576 */ | 2537 _updateIcon: function() { |
| 4577 value: { | 2538 if (this._usesIconset()) { |
| 4578 type: Object, | 2539 if (this._img && this._img.parentNode) { |
| 4579 notify: true, | 2540 Polymer.dom(this.root).removeChild(this._img); |
| 4580 readOnly: true | 2541 } |
| 4581 }, | 2542 if (this._iconName === "") { |
| 4582 | 2543 if (this._iconset) { |
| 4583 /** | 2544 this._iconset.removeIcon(this); |
| 4584 * Array of all meta-data values for the given type. | |
| 4585 */ | |
| 4586 list: { | |
| 4587 type: Array, | |
| 4588 notify: true | |
| 4589 } | 2545 } |
| 4590 | 2546 } else if (this._iconsetName && this._meta) { |
| 4591 }, | 2547 this._iconset = this._meta.byKey(this._iconsetName); |
| 4592 | 2548 if (this._iconset) { |
| 4593 /** | 2549 this._iconset.applyIcon(this, this._iconName, this.theme); |
| 4594 * Actually a factory method, not a true constructor. Only runs if | 2550 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
| 4595 * someone invokes it directly (via `new Polymer.IronMeta()`); | 2551 } else { |
| 4596 * | 2552 this.listen(window, 'iron-iconset-added', '_updateIcon'); |
| 4597 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
| 4598 */ | |
| 4599 factoryImpl: function(config) { | |
| 4600 if (config) { | |
| 4601 for (var n in config) { | |
| 4602 switch(n) { | |
| 4603 case 'type': | |
| 4604 case 'key': | |
| 4605 this[n] = config[n]; | |
| 4606 break; | |
| 4607 } | |
| 4608 } | |
| 4609 } | 2553 } |
| 4610 }, | 2554 } |
| 4611 | 2555 } else { |
| 4612 created: function() { | 2556 if (this._iconset) { |
| 4613 // TODO(sjmiles): good for debugging? | 2557 this._iconset.removeIcon(this); |
| 4614 this._metaDatas = metaDatas; | 2558 } |
| 4615 this._metaArrays = metaArrays; | 2559 if (!this._img) { |
| 4616 }, | 2560 this._img = document.createElement('img'); |
| 4617 | 2561 this._img.style.width = '100%'; |
| 4618 _keyChanged: function(key) { | 2562 this._img.style.height = '100%'; |
| 4619 this._setValue(this._metaData && this._metaData[key]); | 2563 this._img.draggable = false; |
| 4620 }, | 2564 } |
| 4621 | 2565 this._img.src = this.src; |
| 4622 _typeChanged: function(type) { | 2566 Polymer.dom(this.root).appendChild(this._img); |
| 4623 this._metaData = metaDatas[type]; | 2567 } |
| 4624 this.list = metaArrays[type]; | 2568 } |
| 4625 if (this.key) { | 2569 }); |
| 4626 this._keyChanged(this.key); | 2570 |
| 2571 Polymer.IronControlState = { |
| 2572 properties: { |
| 2573 focused: { |
| 2574 type: Boolean, |
| 2575 value: false, |
| 2576 notify: true, |
| 2577 readOnly: true, |
| 2578 reflectToAttribute: true |
| 2579 }, |
| 2580 disabled: { |
| 2581 type: Boolean, |
| 2582 value: false, |
| 2583 notify: true, |
| 2584 observer: '_disabledChanged', |
| 2585 reflectToAttribute: true |
| 2586 }, |
| 2587 _oldTabIndex: { |
| 2588 type: Number |
| 2589 }, |
| 2590 _boundFocusBlurHandler: { |
| 2591 type: Function, |
| 2592 value: function() { |
| 2593 return this._focusBlurHandler.bind(this); |
| 2594 } |
| 2595 } |
| 2596 }, |
| 2597 observers: [ '_changedControlState(focused, disabled)' ], |
| 2598 ready: function() { |
| 2599 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2600 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2601 }, |
| 2602 _focusBlurHandler: function(event) { |
| 2603 if (event.target === this) { |
| 2604 this._setFocused(event.type === 'focus'); |
| 2605 } else if (!this.shadowRoot) { |
| 2606 var target = Polymer.dom(event).localTarget; |
| 2607 if (!this.isLightDescendant(target)) { |
| 2608 this.fire(event.type, { |
| 2609 sourceEvent: event |
| 2610 }, { |
| 2611 node: this, |
| 2612 bubbles: event.bubbles, |
| 2613 cancelable: event.cancelable |
| 2614 }); |
| 2615 } |
| 2616 } |
| 2617 }, |
| 2618 _disabledChanged: function(disabled, old) { |
| 2619 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2620 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2621 if (disabled) { |
| 2622 this._oldTabIndex = this.tabIndex; |
| 2623 this._setFocused(false); |
| 2624 this.tabIndex = -1; |
| 2625 this.blur(); |
| 2626 } else if (this._oldTabIndex !== undefined) { |
| 2627 this.tabIndex = this._oldTabIndex; |
| 2628 } |
| 2629 }, |
| 2630 _changedControlState: function() { |
| 2631 if (this._controlStateChanged) { |
| 2632 this._controlStateChanged(); |
| 2633 } |
| 2634 } |
| 2635 }; |
| 2636 |
| 2637 Polymer.IronButtonStateImpl = { |
| 2638 properties: { |
| 2639 pressed: { |
| 2640 type: Boolean, |
| 2641 readOnly: true, |
| 2642 value: false, |
| 2643 reflectToAttribute: true, |
| 2644 observer: '_pressedChanged' |
| 2645 }, |
| 2646 toggles: { |
| 2647 type: Boolean, |
| 2648 value: false, |
| 2649 reflectToAttribute: true |
| 2650 }, |
| 2651 active: { |
| 2652 type: Boolean, |
| 2653 value: false, |
| 2654 notify: true, |
| 2655 reflectToAttribute: true |
| 2656 }, |
| 2657 pointerDown: { |
| 2658 type: Boolean, |
| 2659 readOnly: true, |
| 2660 value: false |
| 2661 }, |
| 2662 receivedFocusFromKeyboard: { |
| 2663 type: Boolean, |
| 2664 readOnly: true |
| 2665 }, |
| 2666 ariaActiveAttribute: { |
| 2667 type: String, |
| 2668 value: 'aria-pressed', |
| 2669 observer: '_ariaActiveAttributeChanged' |
| 2670 } |
| 2671 }, |
| 2672 listeners: { |
| 2673 down: '_downHandler', |
| 2674 up: '_upHandler', |
| 2675 tap: '_tapHandler' |
| 2676 }, |
| 2677 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi
veAttribute)' ], |
| 2678 keyBindings: { |
| 2679 'enter:keydown': '_asyncClick', |
| 2680 'space:keydown': '_spaceKeyDownHandler', |
| 2681 'space:keyup': '_spaceKeyUpHandler' |
| 2682 }, |
| 2683 _mouseEventRe: /^mouse/, |
| 2684 _tapHandler: function() { |
| 2685 if (this.toggles) { |
| 2686 this._userActivate(!this.active); |
| 2687 } else { |
| 2688 this.active = false; |
| 2689 } |
| 2690 }, |
| 2691 _detectKeyboardFocus: function(focused) { |
| 2692 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 2693 }, |
| 2694 _userActivate: function(active) { |
| 2695 if (this.active !== active) { |
| 2696 this.active = active; |
| 2697 this.fire('change'); |
| 2698 } |
| 2699 }, |
| 2700 _downHandler: function(event) { |
| 2701 this._setPointerDown(true); |
| 2702 this._setPressed(true); |
| 2703 this._setReceivedFocusFromKeyboard(false); |
| 2704 }, |
| 2705 _upHandler: function() { |
| 2706 this._setPointerDown(false); |
| 2707 this._setPressed(false); |
| 2708 }, |
| 2709 _spaceKeyDownHandler: function(event) { |
| 2710 var keyboardEvent = event.detail.keyboardEvent; |
| 2711 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2712 if (this.isLightDescendant(target)) return; |
| 2713 keyboardEvent.preventDefault(); |
| 2714 keyboardEvent.stopImmediatePropagation(); |
| 2715 this._setPressed(true); |
| 2716 }, |
| 2717 _spaceKeyUpHandler: function(event) { |
| 2718 var keyboardEvent = event.detail.keyboardEvent; |
| 2719 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2720 if (this.isLightDescendant(target)) return; |
| 2721 if (this.pressed) { |
| 2722 this._asyncClick(); |
| 2723 } |
| 2724 this._setPressed(false); |
| 2725 }, |
| 2726 _asyncClick: function() { |
| 2727 this.async(function() { |
| 2728 this.click(); |
| 2729 }, 1); |
| 2730 }, |
| 2731 _pressedChanged: function(pressed) { |
| 2732 this._changedButtonState(); |
| 2733 }, |
| 2734 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 2735 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 2736 this.removeAttribute(oldValue); |
| 2737 } |
| 2738 }, |
| 2739 _activeChanged: function(active, ariaActiveAttribute) { |
| 2740 if (this.toggles) { |
| 2741 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false'); |
| 2742 } else { |
| 2743 this.removeAttribute(this.ariaActiveAttribute); |
| 2744 } |
| 2745 this._changedButtonState(); |
| 2746 }, |
| 2747 _controlStateChanged: function() { |
| 2748 if (this.disabled) { |
| 2749 this._setPressed(false); |
| 2750 } else { |
| 2751 this._changedButtonState(); |
| 2752 } |
| 2753 }, |
| 2754 _changedButtonState: function() { |
| 2755 if (this._buttonStateChanged) { |
| 2756 this._buttonStateChanged(); |
| 2757 } |
| 2758 } |
| 2759 }; |
| 2760 |
| 2761 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat
eImpl ]; |
| 2762 |
| 2763 (function() { |
| 2764 var Utility = { |
| 2765 distance: function(x1, y1, x2, y2) { |
| 2766 var xDelta = x1 - x2; |
| 2767 var yDelta = y1 - y2; |
| 2768 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 2769 }, |
| 2770 now: window.performance && window.performance.now ? window.performance.now.b
ind(window.performance) : Date.now |
| 2771 }; |
| 2772 function ElementMetrics(element) { |
| 2773 this.element = element; |
| 2774 this.width = this.boundingRect.width; |
| 2775 this.height = this.boundingRect.height; |
| 2776 this.size = Math.max(this.width, this.height); |
| 2777 } |
| 2778 ElementMetrics.prototype = { |
| 2779 get boundingRect() { |
| 2780 return this.element.getBoundingClientRect(); |
| 2781 }, |
| 2782 furthestCornerDistanceFrom: function(x, y) { |
| 2783 var topLeft = Utility.distance(x, y, 0, 0); |
| 2784 var topRight = Utility.distance(x, y, this.width, 0); |
| 2785 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 2786 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 2787 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 2788 } |
| 2789 }; |
| 2790 function Ripple(element) { |
| 2791 this.element = element; |
| 2792 this.color = window.getComputedStyle(element).color; |
| 2793 this.wave = document.createElement('div'); |
| 2794 this.waveContainer = document.createElement('div'); |
| 2795 this.wave.style.backgroundColor = this.color; |
| 2796 this.wave.classList.add('wave'); |
| 2797 this.waveContainer.classList.add('wave-container'); |
| 2798 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 2799 this.resetInteractionState(); |
| 2800 } |
| 2801 Ripple.MAX_RADIUS = 300; |
| 2802 Ripple.prototype = { |
| 2803 get recenters() { |
| 2804 return this.element.recenters; |
| 2805 }, |
| 2806 get center() { |
| 2807 return this.element.center; |
| 2808 }, |
| 2809 get mouseDownElapsed() { |
| 2810 var elapsed; |
| 2811 if (!this.mouseDownStart) { |
| 2812 return 0; |
| 2813 } |
| 2814 elapsed = Utility.now() - this.mouseDownStart; |
| 2815 if (this.mouseUpStart) { |
| 2816 elapsed -= this.mouseUpElapsed; |
| 2817 } |
| 2818 return elapsed; |
| 2819 }, |
| 2820 get mouseUpElapsed() { |
| 2821 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0; |
| 2822 }, |
| 2823 get mouseDownElapsedSeconds() { |
| 2824 return this.mouseDownElapsed / 1e3; |
| 2825 }, |
| 2826 get mouseUpElapsedSeconds() { |
| 2827 return this.mouseUpElapsed / 1e3; |
| 2828 }, |
| 2829 get mouseInteractionSeconds() { |
| 2830 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 2831 }, |
| 2832 get initialOpacity() { |
| 2833 return this.element.initialOpacity; |
| 2834 }, |
| 2835 get opacityDecayVelocity() { |
| 2836 return this.element.opacityDecayVelocity; |
| 2837 }, |
| 2838 get radius() { |
| 2839 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 2840 var height2 = this.containerMetrics.height * this.containerMetrics.height; |
| 2841 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS)
* 1.1 + 5; |
| 2842 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS); |
| 2843 var timeNow = this.mouseInteractionSeconds / duration; |
| 2844 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 2845 return Math.abs(size); |
| 2846 }, |
| 2847 get opacity() { |
| 2848 if (!this.mouseUpStart) { |
| 2849 return this.initialOpacity; |
| 2850 } |
| 2851 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this
.opacityDecayVelocity); |
| 2852 }, |
| 2853 get outerOpacity() { |
| 2854 var outerOpacity = this.mouseUpElapsedSeconds * .3; |
| 2855 var waveOpacity = this.opacity; |
| 2856 return Math.max(0, Math.min(outerOpacity, waveOpacity)); |
| 2857 }, |
| 2858 get isOpacityFullyDecayed() { |
| 2859 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl
e.MAX_RADIUS); |
| 2860 }, |
| 2861 get isRestingAtMaxRadius() { |
| 2862 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this
.maxRadius, Ripple.MAX_RADIUS); |
| 2863 }, |
| 2864 get isAnimationComplete() { |
| 2865 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa
xRadius; |
| 2866 }, |
| 2867 get translationFraction() { |
| 2868 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr
t(2)); |
| 2869 }, |
| 2870 get xNow() { |
| 2871 if (this.xEnd) { |
| 2872 return this.xStart + this.translationFraction * (this.xEnd - this.xStart
); |
| 2873 } |
| 2874 return this.xStart; |
| 2875 }, |
| 2876 get yNow() { |
| 2877 if (this.yEnd) { |
| 2878 return this.yStart + this.translationFraction * (this.yEnd - this.yStart
); |
| 2879 } |
| 2880 return this.yStart; |
| 2881 }, |
| 2882 get isMouseDown() { |
| 2883 return this.mouseDownStart && !this.mouseUpStart; |
| 2884 }, |
| 2885 resetInteractionState: function() { |
| 2886 this.maxRadius = 0; |
| 2887 this.mouseDownStart = 0; |
| 2888 this.mouseUpStart = 0; |
| 2889 this.xStart = 0; |
| 2890 this.yStart = 0; |
| 2891 this.xEnd = 0; |
| 2892 this.yEnd = 0; |
| 2893 this.slideDistance = 0; |
| 2894 this.containerMetrics = new ElementMetrics(this.element); |
| 2895 }, |
| 2896 draw: function() { |
| 2897 var scale; |
| 2898 var translateString; |
| 2899 var dx; |
| 2900 var dy; |
| 2901 this.wave.style.opacity = this.opacity; |
| 2902 scale = this.radius / (this.containerMetrics.size / 2); |
| 2903 dx = this.xNow - this.containerMetrics.width / 2; |
| 2904 dy = this.yNow - this.containerMetrics.height / 2; |
| 2905 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy
+ 'px)'; |
| 2906 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + '
px, 0)'; |
| 2907 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 2908 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 2909 }, |
| 2910 downAction: function(event) { |
| 2911 var xCenter = this.containerMetrics.width / 2; |
| 2912 var yCenter = this.containerMetrics.height / 2; |
| 2913 this.resetInteractionState(); |
| 2914 this.mouseDownStart = Utility.now(); |
| 2915 if (this.center) { |
| 2916 this.xStart = xCenter; |
| 2917 this.yStart = yCenter; |
| 2918 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2919 } else { |
| 2920 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec
t.left : this.containerMetrics.width / 2; |
| 2921 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec
t.top : this.containerMetrics.height / 2; |
| 2922 } |
| 2923 if (this.recenters) { |
| 2924 this.xEnd = xCenter; |
| 2925 this.yEnd = yCenter; |
| 2926 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 2927 } |
| 2928 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt
art, this.yStart); |
| 2929 this.waveContainer.style.top = (this.containerMetrics.height - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2930 this.waveContainer.style.left = (this.containerMetrics.width - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2931 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 2932 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 2933 }, |
| 2934 upAction: function(event) { |
| 2935 if (!this.isMouseDown) { |
| 2936 return; |
| 2937 } |
| 2938 this.mouseUpStart = Utility.now(); |
| 2939 }, |
| 2940 remove: function() { |
| 2941 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer)
; |
| 2942 } |
| 2943 }; |
| 2944 Polymer({ |
| 2945 is: 'paper-ripple', |
| 2946 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 2947 properties: { |
| 2948 initialOpacity: { |
| 2949 type: Number, |
| 2950 value: .25 |
| 2951 }, |
| 2952 opacityDecayVelocity: { |
| 2953 type: Number, |
| 2954 value: .8 |
| 2955 }, |
| 2956 recenters: { |
| 2957 type: Boolean, |
| 2958 value: false |
| 2959 }, |
| 2960 center: { |
| 2961 type: Boolean, |
| 2962 value: false |
| 2963 }, |
| 2964 ripples: { |
| 2965 type: Array, |
| 2966 value: function() { |
| 2967 return []; |
| 4627 } | 2968 } |
| 4628 }, | 2969 }, |
| 4629 | 2970 animating: { |
| 4630 /** | 2971 type: Boolean, |
| 4631 * Retrieves meta data value by key. | 2972 readOnly: true, |
| 4632 * @param {string} key The key of the meta-data to be returned. | 2973 reflectToAttribute: true, |
| 4633 * @return {*} | 2974 value: false |
| 4634 */ | 2975 }, |
| 4635 byKey: function(key) { | 2976 holdDown: { |
| 4636 return this._metaData && this._metaData[key]; | |
| 4637 } | |
| 4638 | |
| 4639 }); | |
| 4640 | |
| 4641 })(); | |
| 4642 Polymer({ | |
| 4643 | |
| 4644 is: 'iron-icon', | |
| 4645 | |
| 4646 properties: { | |
| 4647 | |
| 4648 /** | |
| 4649 * The name of the icon to use. The name should be of the form: | |
| 4650 * `iconset_name:icon_name`. | |
| 4651 */ | |
| 4652 icon: { | |
| 4653 type: String, | |
| 4654 observer: '_iconChanged' | |
| 4655 }, | |
| 4656 | |
| 4657 /** | |
| 4658 * The name of the theme to used, if one is specified by the | |
| 4659 * iconset. | |
| 4660 */ | |
| 4661 theme: { | |
| 4662 type: String, | |
| 4663 observer: '_updateIcon' | |
| 4664 }, | |
| 4665 | |
| 4666 /** | |
| 4667 * If using iron-icon without an iconset, you can set the src to be | |
| 4668 * the URL of an individual icon image file. Note that this will take | |
| 4669 * precedence over a given icon attribute. | |
| 4670 */ | |
| 4671 src: { | |
| 4672 type: String, | |
| 4673 observer: '_srcChanged' | |
| 4674 }, | |
| 4675 | |
| 4676 /** | |
| 4677 * @type {!Polymer.IronMeta} | |
| 4678 */ | |
| 4679 _meta: { | |
| 4680 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | |
| 4681 observer: '_updateIcon' | |
| 4682 } | |
| 4683 | |
| 4684 }, | |
| 4685 | |
| 4686 _DEFAULT_ICONSET: 'icons', | |
| 4687 | |
| 4688 _iconChanged: function(icon) { | |
| 4689 var parts = (icon || '').split(':'); | |
| 4690 this._iconName = parts.pop(); | |
| 4691 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; | |
| 4692 this._updateIcon(); | |
| 4693 }, | |
| 4694 | |
| 4695 _srcChanged: function(src) { | |
| 4696 this._updateIcon(); | |
| 4697 }, | |
| 4698 | |
| 4699 _usesIconset: function() { | |
| 4700 return this.icon || !this.src; | |
| 4701 }, | |
| 4702 | |
| 4703 /** @suppress {visibility} */ | |
| 4704 _updateIcon: function() { | |
| 4705 if (this._usesIconset()) { | |
| 4706 if (this._img && this._img.parentNode) { | |
| 4707 Polymer.dom(this.root).removeChild(this._img); | |
| 4708 } | |
| 4709 if (this._iconName === "") { | |
| 4710 if (this._iconset) { | |
| 4711 this._iconset.removeIcon(this); | |
| 4712 } | |
| 4713 } else if (this._iconsetName && this._meta) { | |
| 4714 this._iconset = /** @type {?Polymer.Iconset} */ ( | |
| 4715 this._meta.byKey(this._iconsetName)); | |
| 4716 if (this._iconset) { | |
| 4717 this._iconset.applyIcon(this, this._iconName, this.theme); | |
| 4718 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); | |
| 4719 } else { | |
| 4720 this.listen(window, 'iron-iconset-added', '_updateIcon'); | |
| 4721 } | |
| 4722 } | |
| 4723 } else { | |
| 4724 if (this._iconset) { | |
| 4725 this._iconset.removeIcon(this); | |
| 4726 } | |
| 4727 if (!this._img) { | |
| 4728 this._img = document.createElement('img'); | |
| 4729 this._img.style.width = '100%'; | |
| 4730 this._img.style.height = '100%'; | |
| 4731 this._img.draggable = false; | |
| 4732 } | |
| 4733 this._img.src = this.src; | |
| 4734 Polymer.dom(this.root).appendChild(this._img); | |
| 4735 } | |
| 4736 } | |
| 4737 | |
| 4738 }); | |
| 4739 /** | |
| 4740 * @demo demo/index.html | |
| 4741 * @polymerBehavior | |
| 4742 */ | |
| 4743 Polymer.IronControlState = { | |
| 4744 | |
| 4745 properties: { | |
| 4746 | |
| 4747 /** | |
| 4748 * If true, the element currently has focus. | |
| 4749 */ | |
| 4750 focused: { | |
| 4751 type: Boolean, | 2977 type: Boolean, |
| 4752 value: false, | 2978 value: false, |
| 4753 notify: true, | 2979 observer: '_holdDownChanged' |
| 4754 readOnly: true, | 2980 }, |
| 4755 reflectToAttribute: true | 2981 noink: { |
| 4756 }, | |
| 4757 | |
| 4758 /** | |
| 4759 * If true, the user cannot interact with this element. | |
| 4760 */ | |
| 4761 disabled: { | |
| 4762 type: Boolean, | 2982 type: Boolean, |
| 4763 value: false, | 2983 value: false |
| 4764 notify: true, | 2984 }, |
| 4765 observer: '_disabledChanged', | 2985 _animating: { |
| 4766 reflectToAttribute: true | 2986 type: Boolean |
| 4767 }, | 2987 }, |
| 4768 | 2988 _boundAnimate: { |
| 4769 _oldTabIndex: { | |
| 4770 type: Number | |
| 4771 }, | |
| 4772 | |
| 4773 _boundFocusBlurHandler: { | |
| 4774 type: Function, | 2989 type: Function, |
| 4775 value: function() { | 2990 value: function() { |
| 4776 return this._focusBlurHandler.bind(this); | 2991 return this.animate.bind(this); |
| 4777 } | 2992 } |
| 4778 } | 2993 } |
| 4779 | 2994 }, |
| 4780 }, | 2995 get target() { |
| 4781 | 2996 return this.keyEventTarget; |
| 4782 observers: [ | 2997 }, |
| 4783 '_changedControlState(focused, disabled)' | 2998 keyBindings: { |
| 4784 ], | 2999 'enter:keydown': '_onEnterKeydown', |
| 4785 | 3000 'space:keydown': '_onSpaceKeydown', |
| 4786 ready: function() { | 3001 'space:keyup': '_onSpaceKeyup' |
| 4787 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 3002 }, |
| 4788 this.addEventListener('blur', this._boundFocusBlurHandler, true); | 3003 attached: function() { |
| 4789 }, | 3004 if (this.parentNode.nodeType == 11) { |
| 4790 | 3005 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
| 4791 _focusBlurHandler: function(event) { | 3006 } else { |
| 4792 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | 3007 this.keyEventTarget = this.parentNode; |
| 4793 // eventually become `this` due to retargeting; if we are not in | 3008 } |
| 4794 // ShadowDOM land, `event.target` will eventually become `this` due | 3009 var keyEventTarget = this.keyEventTarget; |
| 4795 // to the second conditional which fires a synthetic event (that is also | 3010 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
| 4796 // handled). In either case, we can disregard `event.path`. | 3011 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
| 4797 | 3012 }, |
| 4798 if (event.target === this) { | 3013 detached: function() { |
| 4799 this._setFocused(event.type === 'focus'); | 3014 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
| 4800 } else if (!this.shadowRoot) { | 3015 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
| 4801 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | 3016 this.keyEventTarget = null; |
| 4802 if (!this.isLightDescendant(target)) { | 3017 }, |
| 4803 this.fire(event.type, {sourceEvent: event}, { | 3018 get shouldKeepAnimating() { |
| 4804 node: this, | 3019 for (var index = 0; index < this.ripples.length; ++index) { |
| 4805 bubbles: event.bubbles, | 3020 if (!this.ripples[index].isAnimationComplete) { |
| 4806 cancelable: event.cancelable | 3021 return true; |
| 4807 }); | |
| 4808 } | 3022 } |
| 4809 } | 3023 } |
| 4810 }, | 3024 return false; |
| 4811 | 3025 }, |
| 4812 _disabledChanged: function(disabled, old) { | 3026 simulatedRipple: function() { |
| 4813 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3027 this.downAction(null); |
| 4814 this.style.pointerEvents = disabled ? 'none' : ''; | 3028 this.async(function() { |
| 4815 if (disabled) { | 3029 this.upAction(); |
| 4816 this._oldTabIndex = this.tabIndex; | 3030 }, 1); |
| 4817 this._setFocused(false); | 3031 }, |
| 4818 this.tabIndex = -1; | 3032 uiDownAction: function(event) { |
| 4819 this.blur(); | 3033 if (!this.noink) { |
| 4820 } else if (this._oldTabIndex !== undefined) { | 3034 this.downAction(event); |
| 4821 this.tabIndex = this._oldTabIndex; | 3035 } |
| 4822 } | 3036 }, |
| 4823 }, | 3037 downAction: function(event) { |
| 4824 | 3038 if (this.holdDown && this.ripples.length > 0) { |
| 4825 _changedControlState: function() { | |
| 4826 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
| 4827 if (this._controlStateChanged) { | |
| 4828 this._controlStateChanged(); | |
| 4829 } | |
| 4830 } | |
| 4831 | |
| 4832 }; | |
| 4833 /** | |
| 4834 * @demo demo/index.html | |
| 4835 * @polymerBehavior Polymer.IronButtonState | |
| 4836 */ | |
| 4837 Polymer.IronButtonStateImpl = { | |
| 4838 | |
| 4839 properties: { | |
| 4840 | |
| 4841 /** | |
| 4842 * If true, the user is currently holding down the button. | |
| 4843 */ | |
| 4844 pressed: { | |
| 4845 type: Boolean, | |
| 4846 readOnly: true, | |
| 4847 value: false, | |
| 4848 reflectToAttribute: true, | |
| 4849 observer: '_pressedChanged' | |
| 4850 }, | |
| 4851 | |
| 4852 /** | |
| 4853 * If true, the button toggles the active state with each tap or press | |
| 4854 * of the spacebar. | |
| 4855 */ | |
| 4856 toggles: { | |
| 4857 type: Boolean, | |
| 4858 value: false, | |
| 4859 reflectToAttribute: true | |
| 4860 }, | |
| 4861 | |
| 4862 /** | |
| 4863 * If true, the button is a toggle and is currently in the active state. | |
| 4864 */ | |
| 4865 active: { | |
| 4866 type: Boolean, | |
| 4867 value: false, | |
| 4868 notify: true, | |
| 4869 reflectToAttribute: true | |
| 4870 }, | |
| 4871 | |
| 4872 /** | |
| 4873 * True if the element is currently being pressed by a "pointer," which | |
| 4874 * is loosely defined as mouse or touch input (but specifically excluding | |
| 4875 * keyboard input). | |
| 4876 */ | |
| 4877 pointerDown: { | |
| 4878 type: Boolean, | |
| 4879 readOnly: true, | |
| 4880 value: false | |
| 4881 }, | |
| 4882 | |
| 4883 /** | |
| 4884 * True if the input device that caused the element to receive focus | |
| 4885 * was a keyboard. | |
| 4886 */ | |
| 4887 receivedFocusFromKeyboard: { | |
| 4888 type: Boolean, | |
| 4889 readOnly: true | |
| 4890 }, | |
| 4891 | |
| 4892 /** | |
| 4893 * The aria attribute to be set if the button is a toggle and in the | |
| 4894 * active state. | |
| 4895 */ | |
| 4896 ariaActiveAttribute: { | |
| 4897 type: String, | |
| 4898 value: 'aria-pressed', | |
| 4899 observer: '_ariaActiveAttributeChanged' | |
| 4900 } | |
| 4901 }, | |
| 4902 | |
| 4903 listeners: { | |
| 4904 down: '_downHandler', | |
| 4905 up: '_upHandler', | |
| 4906 tap: '_tapHandler' | |
| 4907 }, | |
| 4908 | |
| 4909 observers: [ | |
| 4910 '_detectKeyboardFocus(focused)', | |
| 4911 '_activeChanged(active, ariaActiveAttribute)' | |
| 4912 ], | |
| 4913 | |
| 4914 keyBindings: { | |
| 4915 'enter:keydown': '_asyncClick', | |
| 4916 'space:keydown': '_spaceKeyDownHandler', | |
| 4917 'space:keyup': '_spaceKeyUpHandler', | |
| 4918 }, | |
| 4919 | |
| 4920 _mouseEventRe: /^mouse/, | |
| 4921 | |
| 4922 _tapHandler: function() { | |
| 4923 if (this.toggles) { | |
| 4924 // a tap is needed to toggle the active state | |
| 4925 this._userActivate(!this.active); | |
| 4926 } else { | |
| 4927 this.active = false; | |
| 4928 } | |
| 4929 }, | |
| 4930 | |
| 4931 _detectKeyboardFocus: function(focused) { | |
| 4932 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
| 4933 }, | |
| 4934 | |
| 4935 // to emulate native checkbox, (de-)activations from a user interaction fire | |
| 4936 // 'change' events | |
| 4937 _userActivate: function(active) { | |
| 4938 if (this.active !== active) { | |
| 4939 this.active = active; | |
| 4940 this.fire('change'); | |
| 4941 } | |
| 4942 }, | |
| 4943 | |
| 4944 _downHandler: function(event) { | |
| 4945 this._setPointerDown(true); | |
| 4946 this._setPressed(true); | |
| 4947 this._setReceivedFocusFromKeyboard(false); | |
| 4948 }, | |
| 4949 | |
| 4950 _upHandler: function() { | |
| 4951 this._setPointerDown(false); | |
| 4952 this._setPressed(false); | |
| 4953 }, | |
| 4954 | |
| 4955 /** | |
| 4956 * @param {!KeyboardEvent} event . | |
| 4957 */ | |
| 4958 _spaceKeyDownHandler: function(event) { | |
| 4959 var keyboardEvent = event.detail.keyboardEvent; | |
| 4960 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 4961 | |
| 4962 // Ignore the event if this is coming from a focused light child, since th
at | |
| 4963 // element will deal with it. | |
| 4964 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 4965 return; | 3039 return; |
| 4966 | 3040 } |
| 4967 keyboardEvent.preventDefault(); | 3041 var ripple = this.addRipple(); |
| 4968 keyboardEvent.stopImmediatePropagation(); | 3042 ripple.downAction(event); |
| 4969 this._setPressed(true); | 3043 if (!this._animating) { |
| 4970 }, | |
| 4971 | |
| 4972 /** | |
| 4973 * @param {!KeyboardEvent} event . | |
| 4974 */ | |
| 4975 _spaceKeyUpHandler: function(event) { | |
| 4976 var keyboardEvent = event.detail.keyboardEvent; | |
| 4977 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 4978 | |
| 4979 // Ignore the event if this is coming from a focused light child, since th
at | |
| 4980 // element will deal with it. | |
| 4981 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 4982 return; | |
| 4983 | |
| 4984 if (this.pressed) { | |
| 4985 this._asyncClick(); | |
| 4986 } | |
| 4987 this._setPressed(false); | |
| 4988 }, | |
| 4989 | |
| 4990 // trigger click asynchronously, the asynchrony is useful to allow one | |
| 4991 // event handler to unwind before triggering another event | |
| 4992 _asyncClick: function() { | |
| 4993 this.async(function() { | |
| 4994 this.click(); | |
| 4995 }, 1); | |
| 4996 }, | |
| 4997 | |
| 4998 // any of these changes are considered a change to button state | |
| 4999 | |
| 5000 _pressedChanged: function(pressed) { | |
| 5001 this._changedButtonState(); | |
| 5002 }, | |
| 5003 | |
| 5004 _ariaActiveAttributeChanged: function(value, oldValue) { | |
| 5005 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | |
| 5006 this.removeAttribute(oldValue); | |
| 5007 } | |
| 5008 }, | |
| 5009 | |
| 5010 _activeChanged: function(active, ariaActiveAttribute) { | |
| 5011 if (this.toggles) { | |
| 5012 this.setAttribute(this.ariaActiveAttribute, | |
| 5013 active ? 'true' : 'false'); | |
| 5014 } else { | |
| 5015 this.removeAttribute(this.ariaActiveAttribute); | |
| 5016 } | |
| 5017 this._changedButtonState(); | |
| 5018 }, | |
| 5019 | |
| 5020 _controlStateChanged: function() { | |
| 5021 if (this.disabled) { | |
| 5022 this._setPressed(false); | |
| 5023 } else { | |
| 5024 this._changedButtonState(); | |
| 5025 } | |
| 5026 }, | |
| 5027 | |
| 5028 // provide hook for follow-on behaviors to react to button-state | |
| 5029 | |
| 5030 _changedButtonState: function() { | |
| 5031 if (this._buttonStateChanged) { | |
| 5032 this._buttonStateChanged(); // abstract | |
| 5033 } | |
| 5034 } | |
| 5035 | |
| 5036 }; | |
| 5037 | |
| 5038 /** @polymerBehavior */ | |
| 5039 Polymer.IronButtonState = [ | |
| 5040 Polymer.IronA11yKeysBehavior, | |
| 5041 Polymer.IronButtonStateImpl | |
| 5042 ]; | |
| 5043 (function() { | |
| 5044 var Utility = { | |
| 5045 distance: function(x1, y1, x2, y2) { | |
| 5046 var xDelta = (x1 - x2); | |
| 5047 var yDelta = (y1 - y2); | |
| 5048 | |
| 5049 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
| 5050 }, | |
| 5051 | |
| 5052 now: window.performance && window.performance.now ? | |
| 5053 window.performance.now.bind(window.performance) : Date.now | |
| 5054 }; | |
| 5055 | |
| 5056 /** | |
| 5057 * @param {HTMLElement} element | |
| 5058 * @constructor | |
| 5059 */ | |
| 5060 function ElementMetrics(element) { | |
| 5061 this.element = element; | |
| 5062 this.width = this.boundingRect.width; | |
| 5063 this.height = this.boundingRect.height; | |
| 5064 | |
| 5065 this.size = Math.max(this.width, this.height); | |
| 5066 } | |
| 5067 | |
| 5068 ElementMetrics.prototype = { | |
| 5069 get boundingRect () { | |
| 5070 return this.element.getBoundingClientRect(); | |
| 5071 }, | |
| 5072 | |
| 5073 furthestCornerDistanceFrom: function(x, y) { | |
| 5074 var topLeft = Utility.distance(x, y, 0, 0); | |
| 5075 var topRight = Utility.distance(x, y, this.width, 0); | |
| 5076 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
| 5077 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
| 5078 | |
| 5079 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
| 5080 } | |
| 5081 }; | |
| 5082 | |
| 5083 /** | |
| 5084 * @param {HTMLElement} element | |
| 5085 * @constructor | |
| 5086 */ | |
| 5087 function Ripple(element) { | |
| 5088 this.element = element; | |
| 5089 this.color = window.getComputedStyle(element).color; | |
| 5090 | |
| 5091 this.wave = document.createElement('div'); | |
| 5092 this.waveContainer = document.createElement('div'); | |
| 5093 this.wave.style.backgroundColor = this.color; | |
| 5094 this.wave.classList.add('wave'); | |
| 5095 this.waveContainer.classList.add('wave-container'); | |
| 5096 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
| 5097 | |
| 5098 this.resetInteractionState(); | |
| 5099 } | |
| 5100 | |
| 5101 Ripple.MAX_RADIUS = 300; | |
| 5102 | |
| 5103 Ripple.prototype = { | |
| 5104 get recenters() { | |
| 5105 return this.element.recenters; | |
| 5106 }, | |
| 5107 | |
| 5108 get center() { | |
| 5109 return this.element.center; | |
| 5110 }, | |
| 5111 | |
| 5112 get mouseDownElapsed() { | |
| 5113 var elapsed; | |
| 5114 | |
| 5115 if (!this.mouseDownStart) { | |
| 5116 return 0; | |
| 5117 } | |
| 5118 | |
| 5119 elapsed = Utility.now() - this.mouseDownStart; | |
| 5120 | |
| 5121 if (this.mouseUpStart) { | |
| 5122 elapsed -= this.mouseUpElapsed; | |
| 5123 } | |
| 5124 | |
| 5125 return elapsed; | |
| 5126 }, | |
| 5127 | |
| 5128 get mouseUpElapsed() { | |
| 5129 return this.mouseUpStart ? | |
| 5130 Utility.now () - this.mouseUpStart : 0; | |
| 5131 }, | |
| 5132 | |
| 5133 get mouseDownElapsedSeconds() { | |
| 5134 return this.mouseDownElapsed / 1000; | |
| 5135 }, | |
| 5136 | |
| 5137 get mouseUpElapsedSeconds() { | |
| 5138 return this.mouseUpElapsed / 1000; | |
| 5139 }, | |
| 5140 | |
| 5141 get mouseInteractionSeconds() { | |
| 5142 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
| 5143 }, | |
| 5144 | |
| 5145 get initialOpacity() { | |
| 5146 return this.element.initialOpacity; | |
| 5147 }, | |
| 5148 | |
| 5149 get opacityDecayVelocity() { | |
| 5150 return this.element.opacityDecayVelocity; | |
| 5151 }, | |
| 5152 | |
| 5153 get radius() { | |
| 5154 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
| 5155 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
| 5156 var waveRadius = Math.min( | |
| 5157 Math.sqrt(width2 + height2), | |
| 5158 Ripple.MAX_RADIUS | |
| 5159 ) * 1.1 + 5; | |
| 5160 | |
| 5161 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
| 5162 var timeNow = this.mouseInteractionSeconds / duration; | |
| 5163 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
| 5164 | |
| 5165 return Math.abs(size); | |
| 5166 }, | |
| 5167 | |
| 5168 get opacity() { | |
| 5169 if (!this.mouseUpStart) { | |
| 5170 return this.initialOpacity; | |
| 5171 } | |
| 5172 | |
| 5173 return Math.max( | |
| 5174 0, | |
| 5175 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
| 5176 ); | |
| 5177 }, | |
| 5178 | |
| 5179 get outerOpacity() { | |
| 5180 // Linear increase in background opacity, capped at the opacity | |
| 5181 // of the wavefront (waveOpacity). | |
| 5182 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
| 5183 var waveOpacity = this.opacity; | |
| 5184 | |
| 5185 return Math.max( | |
| 5186 0, | |
| 5187 Math.min(outerOpacity, waveOpacity) | |
| 5188 ); | |
| 5189 }, | |
| 5190 | |
| 5191 get isOpacityFullyDecayed() { | |
| 5192 return this.opacity < 0.01 && | |
| 5193 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 5194 }, | |
| 5195 | |
| 5196 get isRestingAtMaxRadius() { | |
| 5197 return this.opacity >= this.initialOpacity && | |
| 5198 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 5199 }, | |
| 5200 | |
| 5201 get isAnimationComplete() { | |
| 5202 return this.mouseUpStart ? | |
| 5203 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
| 5204 }, | |
| 5205 | |
| 5206 get translationFraction() { | |
| 5207 return Math.min( | |
| 5208 1, | |
| 5209 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
| 5210 ); | |
| 5211 }, | |
| 5212 | |
| 5213 get xNow() { | |
| 5214 if (this.xEnd) { | |
| 5215 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
| 5216 } | |
| 5217 | |
| 5218 return this.xStart; | |
| 5219 }, | |
| 5220 | |
| 5221 get yNow() { | |
| 5222 if (this.yEnd) { | |
| 5223 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
| 5224 } | |
| 5225 | |
| 5226 return this.yStart; | |
| 5227 }, | |
| 5228 | |
| 5229 get isMouseDown() { | |
| 5230 return this.mouseDownStart && !this.mouseUpStart; | |
| 5231 }, | |
| 5232 | |
| 5233 resetInteractionState: function() { | |
| 5234 this.maxRadius = 0; | |
| 5235 this.mouseDownStart = 0; | |
| 5236 this.mouseUpStart = 0; | |
| 5237 | |
| 5238 this.xStart = 0; | |
| 5239 this.yStart = 0; | |
| 5240 this.xEnd = 0; | |
| 5241 this.yEnd = 0; | |
| 5242 this.slideDistance = 0; | |
| 5243 | |
| 5244 this.containerMetrics = new ElementMetrics(this.element); | |
| 5245 }, | |
| 5246 | |
| 5247 draw: function() { | |
| 5248 var scale; | |
| 5249 var translateString; | |
| 5250 var dx; | |
| 5251 var dy; | |
| 5252 | |
| 5253 this.wave.style.opacity = this.opacity; | |
| 5254 | |
| 5255 scale = this.radius / (this.containerMetrics.size / 2); | |
| 5256 dx = this.xNow - (this.containerMetrics.width / 2); | |
| 5257 dy = this.yNow - (this.containerMetrics.height / 2); | |
| 5258 | |
| 5259 | |
| 5260 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
| 5261 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
| 5262 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
| 5263 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
| 5264 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
| 5265 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
| 5266 }, | |
| 5267 | |
| 5268 /** @param {Event=} event */ | |
| 5269 downAction: function(event) { | |
| 5270 var xCenter = this.containerMetrics.width / 2; | |
| 5271 var yCenter = this.containerMetrics.height / 2; | |
| 5272 | |
| 5273 this.resetInteractionState(); | |
| 5274 this.mouseDownStart = Utility.now(); | |
| 5275 | |
| 5276 if (this.center) { | |
| 5277 this.xStart = xCenter; | |
| 5278 this.yStart = yCenter; | |
| 5279 this.slideDistance = Utility.distance( | |
| 5280 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 5281 ); | |
| 5282 } else { | |
| 5283 this.xStart = event ? | |
| 5284 event.detail.x - this.containerMetrics.boundingRect.left : | |
| 5285 this.containerMetrics.width / 2; | |
| 5286 this.yStart = event ? | |
| 5287 event.detail.y - this.containerMetrics.boundingRect.top : | |
| 5288 this.containerMetrics.height / 2; | |
| 5289 } | |
| 5290 | |
| 5291 if (this.recenters) { | |
| 5292 this.xEnd = xCenter; | |
| 5293 this.yEnd = yCenter; | |
| 5294 this.slideDistance = Utility.distance( | |
| 5295 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 5296 ); | |
| 5297 } | |
| 5298 | |
| 5299 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
| 5300 this.xStart, | |
| 5301 this.yStart | |
| 5302 ); | |
| 5303 | |
| 5304 this.waveContainer.style.top = | |
| 5305 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
| 5306 this.waveContainer.style.left = | |
| 5307 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
| 5308 | |
| 5309 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
| 5310 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
| 5311 }, | |
| 5312 | |
| 5313 /** @param {Event=} event */ | |
| 5314 upAction: function(event) { | |
| 5315 if (!this.isMouseDown) { | |
| 5316 return; | |
| 5317 } | |
| 5318 | |
| 5319 this.mouseUpStart = Utility.now(); | |
| 5320 }, | |
| 5321 | |
| 5322 remove: function() { | |
| 5323 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
| 5324 this.waveContainer | |
| 5325 ); | |
| 5326 } | |
| 5327 }; | |
| 5328 | |
| 5329 Polymer({ | |
| 5330 is: 'paper-ripple', | |
| 5331 | |
| 5332 behaviors: [ | |
| 5333 Polymer.IronA11yKeysBehavior | |
| 5334 ], | |
| 5335 | |
| 5336 properties: { | |
| 5337 /** | |
| 5338 * The initial opacity set on the wave. | |
| 5339 * | |
| 5340 * @attribute initialOpacity | |
| 5341 * @type number | |
| 5342 * @default 0.25 | |
| 5343 */ | |
| 5344 initialOpacity: { | |
| 5345 type: Number, | |
| 5346 value: 0.25 | |
| 5347 }, | |
| 5348 | |
| 5349 /** | |
| 5350 * How fast (opacity per second) the wave fades out. | |
| 5351 * | |
| 5352 * @attribute opacityDecayVelocity | |
| 5353 * @type number | |
| 5354 * @default 0.8 | |
| 5355 */ | |
| 5356 opacityDecayVelocity: { | |
| 5357 type: Number, | |
| 5358 value: 0.8 | |
| 5359 }, | |
| 5360 | |
| 5361 /** | |
| 5362 * If true, ripples will exhibit a gravitational pull towards | |
| 5363 * the center of their container as they fade away. | |
| 5364 * | |
| 5365 * @attribute recenters | |
| 5366 * @type boolean | |
| 5367 * @default false | |
| 5368 */ | |
| 5369 recenters: { | |
| 5370 type: Boolean, | |
| 5371 value: false | |
| 5372 }, | |
| 5373 | |
| 5374 /** | |
| 5375 * If true, ripples will center inside its container | |
| 5376 * | |
| 5377 * @attribute recenters | |
| 5378 * @type boolean | |
| 5379 * @default false | |
| 5380 */ | |
| 5381 center: { | |
| 5382 type: Boolean, | |
| 5383 value: false | |
| 5384 }, | |
| 5385 | |
| 5386 /** | |
| 5387 * A list of the visual ripples. | |
| 5388 * | |
| 5389 * @attribute ripples | |
| 5390 * @type Array | |
| 5391 * @default [] | |
| 5392 */ | |
| 5393 ripples: { | |
| 5394 type: Array, | |
| 5395 value: function() { | |
| 5396 return []; | |
| 5397 } | |
| 5398 }, | |
| 5399 | |
| 5400 /** | |
| 5401 * True when there are visible ripples animating within the | |
| 5402 * element. | |
| 5403 */ | |
| 5404 animating: { | |
| 5405 type: Boolean, | |
| 5406 readOnly: true, | |
| 5407 reflectToAttribute: true, | |
| 5408 value: false | |
| 5409 }, | |
| 5410 | |
| 5411 /** | |
| 5412 * If true, the ripple will remain in the "down" state until `holdDown` | |
| 5413 * is set to false again. | |
| 5414 */ | |
| 5415 holdDown: { | |
| 5416 type: Boolean, | |
| 5417 value: false, | |
| 5418 observer: '_holdDownChanged' | |
| 5419 }, | |
| 5420 | |
| 5421 /** | |
| 5422 * If true, the ripple will not generate a ripple effect | |
| 5423 * via pointer interaction. | |
| 5424 * Calling ripple's imperative api like `simulatedRipple` will | |
| 5425 * still generate the ripple effect. | |
| 5426 */ | |
| 5427 noink: { | |
| 5428 type: Boolean, | |
| 5429 value: false | |
| 5430 }, | |
| 5431 | |
| 5432 _animating: { | |
| 5433 type: Boolean | |
| 5434 }, | |
| 5435 | |
| 5436 _boundAnimate: { | |
| 5437 type: Function, | |
| 5438 value: function() { | |
| 5439 return this.animate.bind(this); | |
| 5440 } | |
| 5441 } | |
| 5442 }, | |
| 5443 | |
| 5444 get target () { | |
| 5445 return this.keyEventTarget; | |
| 5446 }, | |
| 5447 | |
| 5448 keyBindings: { | |
| 5449 'enter:keydown': '_onEnterKeydown', | |
| 5450 'space:keydown': '_onSpaceKeydown', | |
| 5451 'space:keyup': '_onSpaceKeyup' | |
| 5452 }, | |
| 5453 | |
| 5454 attached: function() { | |
| 5455 // Set up a11yKeysBehavior to listen to key events on the target, | |
| 5456 // so that space and enter activate the ripple even if the target doesn'
t | |
| 5457 // handle key events. The key handlers deal with `noink` themselves. | |
| 5458 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
| 5459 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
| 5460 } else { | |
| 5461 this.keyEventTarget = this.parentNode; | |
| 5462 } | |
| 5463 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
| 5464 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
| 5465 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
| 5466 }, | |
| 5467 | |
| 5468 detached: function() { | |
| 5469 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
| 5470 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
| 5471 this.keyEventTarget = null; | |
| 5472 }, | |
| 5473 | |
| 5474 get shouldKeepAnimating () { | |
| 5475 for (var index = 0; index < this.ripples.length; ++index) { | |
| 5476 if (!this.ripples[index].isAnimationComplete) { | |
| 5477 return true; | |
| 5478 } | |
| 5479 } | |
| 5480 | |
| 5481 return false; | |
| 5482 }, | |
| 5483 | |
| 5484 simulatedRipple: function() { | |
| 5485 this.downAction(null); | |
| 5486 | |
| 5487 // Please see polymer/polymer#1305 | |
| 5488 this.async(function() { | |
| 5489 this.upAction(); | |
| 5490 }, 1); | |
| 5491 }, | |
| 5492 | |
| 5493 /** | |
| 5494 * Provokes a ripple down effect via a UI event, | |
| 5495 * respecting the `noink` property. | |
| 5496 * @param {Event=} event | |
| 5497 */ | |
| 5498 uiDownAction: function(event) { | |
| 5499 if (!this.noink) { | |
| 5500 this.downAction(event); | |
| 5501 } | |
| 5502 }, | |
| 5503 | |
| 5504 /** | |
| 5505 * Provokes a ripple down effect via a UI event, | |
| 5506 * *not* respecting the `noink` property. | |
| 5507 * @param {Event=} event | |
| 5508 */ | |
| 5509 downAction: function(event) { | |
| 5510 if (this.holdDown && this.ripples.length > 0) { | |
| 5511 return; | |
| 5512 } | |
| 5513 | |
| 5514 var ripple = this.addRipple(); | |
| 5515 | |
| 5516 ripple.downAction(event); | |
| 5517 | |
| 5518 if (!this._animating) { | |
| 5519 this._animating = true; | |
| 5520 this.animate(); | |
| 5521 } | |
| 5522 }, | |
| 5523 | |
| 5524 /** | |
| 5525 * Provokes a ripple up effect via a UI event, | |
| 5526 * respecting the `noink` property. | |
| 5527 * @param {Event=} event | |
| 5528 */ | |
| 5529 uiUpAction: function(event) { | |
| 5530 if (!this.noink) { | |
| 5531 this.upAction(event); | |
| 5532 } | |
| 5533 }, | |
| 5534 | |
| 5535 /** | |
| 5536 * Provokes a ripple up effect via a UI event, | |
| 5537 * *not* respecting the `noink` property. | |
| 5538 * @param {Event=} event | |
| 5539 */ | |
| 5540 upAction: function(event) { | |
| 5541 if (this.holdDown) { | |
| 5542 return; | |
| 5543 } | |
| 5544 | |
| 5545 this.ripples.forEach(function(ripple) { | |
| 5546 ripple.upAction(event); | |
| 5547 }); | |
| 5548 | |
| 5549 this._animating = true; | 3044 this._animating = true; |
| 5550 this.animate(); | 3045 this.animate(); |
| 5551 }, | 3046 } |
| 5552 | 3047 }, |
| 5553 onAnimationComplete: function() { | 3048 uiUpAction: function(event) { |
| 5554 this._animating = false; | 3049 if (!this.noink) { |
| 5555 this.$.background.style.backgroundColor = null; | 3050 this.upAction(event); |
| 5556 this.fire('transitionend'); | 3051 } |
| 5557 }, | 3052 }, |
| 5558 | 3053 upAction: function(event) { |
| 5559 addRipple: function() { | 3054 if (this.holdDown) { |
| 5560 var ripple = new Ripple(this); | 3055 return; |
| 5561 | 3056 } |
| 5562 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | 3057 this.ripples.forEach(function(ripple) { |
| 5563 this.$.background.style.backgroundColor = ripple.color; | 3058 ripple.upAction(event); |
| 5564 this.ripples.push(ripple); | 3059 }); |
| 5565 | 3060 this._animating = true; |
| 5566 this._setAnimating(true); | 3061 this.animate(); |
| 5567 | 3062 }, |
| 5568 return ripple; | 3063 onAnimationComplete: function() { |
| 5569 }, | 3064 this._animating = false; |
| 5570 | 3065 this.$.background.style.backgroundColor = null; |
| 5571 removeRipple: function(ripple) { | 3066 this.fire('transitionend'); |
| 5572 var rippleIndex = this.ripples.indexOf(ripple); | 3067 }, |
| 5573 | 3068 addRipple: function() { |
| 5574 if (rippleIndex < 0) { | 3069 var ripple = new Ripple(this); |
| 5575 return; | 3070 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 3071 this.$.background.style.backgroundColor = ripple.color; |
| 3072 this.ripples.push(ripple); |
| 3073 this._setAnimating(true); |
| 3074 return ripple; |
| 3075 }, |
| 3076 removeRipple: function(ripple) { |
| 3077 var rippleIndex = this.ripples.indexOf(ripple); |
| 3078 if (rippleIndex < 0) { |
| 3079 return; |
| 3080 } |
| 3081 this.ripples.splice(rippleIndex, 1); |
| 3082 ripple.remove(); |
| 3083 if (!this.ripples.length) { |
| 3084 this._setAnimating(false); |
| 3085 } |
| 3086 }, |
| 3087 animate: function() { |
| 3088 if (!this._animating) { |
| 3089 return; |
| 3090 } |
| 3091 var index; |
| 3092 var ripple; |
| 3093 for (index = 0; index < this.ripples.length; ++index) { |
| 3094 ripple = this.ripples[index]; |
| 3095 ripple.draw(); |
| 3096 this.$.background.style.opacity = ripple.outerOpacity; |
| 3097 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 3098 this.removeRipple(ripple); |
| 5576 } | 3099 } |
| 5577 | 3100 } |
| 5578 this.ripples.splice(rippleIndex, 1); | 3101 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
| 5579 | 3102 this.onAnimationComplete(); |
| 5580 ripple.remove(); | 3103 } else { |
| 5581 | 3104 window.requestAnimationFrame(this._boundAnimate); |
| 5582 if (!this.ripples.length) { | 3105 } |
| 5583 this._setAnimating(false); | 3106 }, |
| 3107 _onEnterKeydown: function() { |
| 3108 this.uiDownAction(); |
| 3109 this.async(this.uiUpAction, 1); |
| 3110 }, |
| 3111 _onSpaceKeydown: function() { |
| 3112 this.uiDownAction(); |
| 3113 }, |
| 3114 _onSpaceKeyup: function() { |
| 3115 this.uiUpAction(); |
| 3116 }, |
| 3117 _holdDownChanged: function(newVal, oldVal) { |
| 3118 if (oldVal === undefined) { |
| 3119 return; |
| 3120 } |
| 3121 if (newVal) { |
| 3122 this.downAction(); |
| 3123 } else { |
| 3124 this.upAction(); |
| 3125 } |
| 3126 } |
| 3127 }); |
| 3128 })(); |
| 3129 |
| 3130 Polymer.PaperRippleBehavior = { |
| 3131 properties: { |
| 3132 noink: { |
| 3133 type: Boolean, |
| 3134 observer: '_noinkChanged' |
| 3135 }, |
| 3136 _rippleContainer: { |
| 3137 type: Object |
| 3138 } |
| 3139 }, |
| 3140 _buttonStateChanged: function() { |
| 3141 if (this.focused) { |
| 3142 this.ensureRipple(); |
| 3143 } |
| 3144 }, |
| 3145 _downHandler: function(event) { |
| 3146 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 3147 if (this.pressed) { |
| 3148 this.ensureRipple(event); |
| 3149 } |
| 3150 }, |
| 3151 ensureRipple: function(optTriggeringEvent) { |
| 3152 if (!this.hasRipple()) { |
| 3153 this._ripple = this._createRipple(); |
| 3154 this._ripple.noink = this.noink; |
| 3155 var rippleContainer = this._rippleContainer || this.root; |
| 3156 if (rippleContainer) { |
| 3157 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 3158 } |
| 3159 if (optTriggeringEvent) { |
| 3160 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 3161 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 3162 if (domContainer.deepContains(target)) { |
| 3163 this._ripple.uiDownAction(optTriggeringEvent); |
| 5584 } | 3164 } |
| 5585 }, | 3165 } |
| 5586 | 3166 } |
| 5587 animate: function() { | 3167 }, |
| 5588 if (!this._animating) { | 3168 getRipple: function() { |
| 5589 return; | 3169 this.ensureRipple(); |
| 5590 } | 3170 return this._ripple; |
| 5591 var index; | 3171 }, |
| 5592 var ripple; | 3172 hasRipple: function() { |
| 5593 | 3173 return Boolean(this._ripple); |
| 5594 for (index = 0; index < this.ripples.length; ++index) { | 3174 }, |
| 5595 ripple = this.ripples[index]; | 3175 _createRipple: function() { |
| 5596 | 3176 return document.createElement('paper-ripple'); |
| 5597 ripple.draw(); | 3177 }, |
| 5598 | 3178 _noinkChanged: function(noink) { |
| 5599 this.$.background.style.opacity = ripple.outerOpacity; | 3179 if (this.hasRipple()) { |
| 5600 | 3180 this._ripple.noink = noink; |
| 5601 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | 3181 } |
| 5602 this.removeRipple(ripple); | 3182 } |
| 5603 } | 3183 }; |
| 5604 } | 3184 |
| 5605 | 3185 Polymer.PaperButtonBehaviorImpl = { |
| 5606 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | 3186 properties: { |
| 5607 this.onAnimationComplete(); | 3187 elevation: { |
| 5608 } else { | 3188 type: Number, |
| 5609 window.requestAnimationFrame(this._boundAnimate); | 3189 reflectToAttribute: true, |
| 5610 } | 3190 readOnly: true |
| 5611 }, | 3191 } |
| 5612 | 3192 }, |
| 5613 _onEnterKeydown: function() { | 3193 observers: [ '_calculateElevation(focused, disabled, active, pressed, received
FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ], |
| 5614 this.uiDownAction(); | 3194 hostAttributes: { |
| 5615 this.async(this.uiUpAction, 1); | 3195 role: 'button', |
| 5616 }, | 3196 tabindex: '0', |
| 5617 | 3197 animated: true |
| 5618 _onSpaceKeydown: function() { | 3198 }, |
| 5619 this.uiDownAction(); | 3199 _calculateElevation: function() { |
| 5620 }, | 3200 var e = 1; |
| 5621 | 3201 if (this.disabled) { |
| 5622 _onSpaceKeyup: function() { | 3202 e = 0; |
| 5623 this.uiUpAction(); | 3203 } else if (this.active || this.pressed) { |
| 5624 }, | 3204 e = 4; |
| 5625 | 3205 } else if (this.receivedFocusFromKeyboard) { |
| 5626 // note: holdDown does not respect noink since it can be a focus based | 3206 e = 3; |
| 5627 // effect. | 3207 } |
| 5628 _holdDownChanged: function(newVal, oldVal) { | 3208 this._setElevation(e); |
| 5629 if (oldVal === undefined) { | 3209 }, |
| 5630 return; | 3210 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
| 5631 } | 3211 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
| 5632 if (newVal) { | 3212 }, |
| 5633 this.downAction(); | 3213 _spaceKeyDownHandler: function(event) { |
| 5634 } else { | 3214 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
| 5635 this.upAction(); | 3215 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
| 5636 } | 3216 this._ripple.uiDownAction(); |
| 5637 } | 3217 } |
| 5638 | 3218 }, |
| 5639 /** | 3219 _spaceKeyUpHandler: function(event) { |
| 5640 Fired when the animation finishes. | 3220 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
| 5641 This is useful if you want to wait until | 3221 if (this.hasRipple()) { |
| 5642 the ripple animation finishes to perform some action. | 3222 this._ripple.uiUpAction(); |
| 5643 | 3223 } |
| 5644 @event transitionend | 3224 } |
| 5645 @param {{node: Object}} detail Contains the animated node. | 3225 }; |
| 5646 */ | 3226 |
| 5647 }); | 3227 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat
e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ]; |
| 5648 })(); | 3228 |
| 5649 /** | |
| 5650 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
| 5651 * when the element has focus via pointer or keyboard. | |
| 5652 * | |
| 5653 * NOTE: This behavior is intended to be used in conjunction with and after | |
| 5654 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
| 5655 * | |
| 5656 * @polymerBehavior Polymer.PaperRippleBehavior | |
| 5657 */ | |
| 5658 Polymer.PaperRippleBehavior = { | |
| 5659 properties: { | |
| 5660 /** | |
| 5661 * If true, the element will not produce a ripple effect when interacted | |
| 5662 * with via the pointer. | |
| 5663 */ | |
| 5664 noink: { | |
| 5665 type: Boolean, | |
| 5666 observer: '_noinkChanged' | |
| 5667 }, | |
| 5668 | |
| 5669 /** | |
| 5670 * @type {Element|undefined} | |
| 5671 */ | |
| 5672 _rippleContainer: { | |
| 5673 type: Object, | |
| 5674 } | |
| 5675 }, | |
| 5676 | |
| 5677 /** | |
| 5678 * Ensures a `<paper-ripple>` element is available when the element is | |
| 5679 * focused. | |
| 5680 */ | |
| 5681 _buttonStateChanged: function() { | |
| 5682 if (this.focused) { | |
| 5683 this.ensureRipple(); | |
| 5684 } | |
| 5685 }, | |
| 5686 | |
| 5687 /** | |
| 5688 * In addition to the functionality provided in `IronButtonState`, ensures | |
| 5689 * a ripple effect is created when the element is in a `pressed` state. | |
| 5690 */ | |
| 5691 _downHandler: function(event) { | |
| 5692 Polymer.IronButtonStateImpl._downHandler.call(this, event); | |
| 5693 if (this.pressed) { | |
| 5694 this.ensureRipple(event); | |
| 5695 } | |
| 5696 }, | |
| 5697 | |
| 5698 /** | |
| 5699 * Ensures this element contains a ripple effect. For startup efficiency | |
| 5700 * the ripple effect is dynamically on demand when needed. | |
| 5701 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
| 5702 * ripple. | |
| 5703 */ | |
| 5704 ensureRipple: function(optTriggeringEvent) { | |
| 5705 if (!this.hasRipple()) { | |
| 5706 this._ripple = this._createRipple(); | |
| 5707 this._ripple.noink = this.noink; | |
| 5708 var rippleContainer = this._rippleContainer || this.root; | |
| 5709 if (rippleContainer) { | |
| 5710 Polymer.dom(rippleContainer).appendChild(this._ripple); | |
| 5711 } | |
| 5712 if (optTriggeringEvent) { | |
| 5713 // Check if the event happened inside of the ripple container | |
| 5714 // Fall back to host instead of the root because distributed text | |
| 5715 // nodes are not valid event targets | |
| 5716 var domContainer = Polymer.dom(this._rippleContainer || this); | |
| 5717 var target = Polymer.dom(optTriggeringEvent).rootTarget; | |
| 5718 if (domContainer.deepContains( /** @type {Node} */(target))) { | |
| 5719 this._ripple.uiDownAction(optTriggeringEvent); | |
| 5720 } | |
| 5721 } | |
| 5722 } | |
| 5723 }, | |
| 5724 | |
| 5725 /** | |
| 5726 * Returns the `<paper-ripple>` element used by this element to create | |
| 5727 * ripple effects. The element's ripple is created on demand, when | |
| 5728 * necessary, and calling this method will force the | |
| 5729 * ripple to be created. | |
| 5730 */ | |
| 5731 getRipple: function() { | |
| 5732 this.ensureRipple(); | |
| 5733 return this._ripple; | |
| 5734 }, | |
| 5735 | |
| 5736 /** | |
| 5737 * Returns true if this element currently contains a ripple effect. | |
| 5738 * @return {boolean} | |
| 5739 */ | |
| 5740 hasRipple: function() { | |
| 5741 return Boolean(this._ripple); | |
| 5742 }, | |
| 5743 | |
| 5744 /** | |
| 5745 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
| 5746 * Override this method to customize the ripple element. | |
| 5747 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
| 5748 */ | |
| 5749 _createRipple: function() { | |
| 5750 return /** @type {!PaperRippleElement} */ ( | |
| 5751 document.createElement('paper-ripple')); | |
| 5752 }, | |
| 5753 | |
| 5754 _noinkChanged: function(noink) { | |
| 5755 if (this.hasRipple()) { | |
| 5756 this._ripple.noink = noink; | |
| 5757 } | |
| 5758 } | |
| 5759 }; | |
| 5760 /** @polymerBehavior Polymer.PaperButtonBehavior */ | |
| 5761 Polymer.PaperButtonBehaviorImpl = { | |
| 5762 properties: { | |
| 5763 /** | |
| 5764 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
| 5765 * shadow, and each increasing number greater than 0 will be "deeper" | |
| 5766 * than the last. | |
| 5767 * | |
| 5768 * @attribute elevation | |
| 5769 * @type number | |
| 5770 * @default 1 | |
| 5771 */ | |
| 5772 elevation: { | |
| 5773 type: Number, | |
| 5774 reflectToAttribute: true, | |
| 5775 readOnly: true | |
| 5776 } | |
| 5777 }, | |
| 5778 | |
| 5779 observers: [ | |
| 5780 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
| 5781 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
| 5782 ], | |
| 5783 | |
| 5784 hostAttributes: { | |
| 5785 role: 'button', | |
| 5786 tabindex: '0', | |
| 5787 animated: true | |
| 5788 }, | |
| 5789 | |
| 5790 _calculateElevation: function() { | |
| 5791 var e = 1; | |
| 5792 if (this.disabled) { | |
| 5793 e = 0; | |
| 5794 } else if (this.active || this.pressed) { | |
| 5795 e = 4; | |
| 5796 } else if (this.receivedFocusFromKeyboard) { | |
| 5797 e = 3; | |
| 5798 } | |
| 5799 this._setElevation(e); | |
| 5800 }, | |
| 5801 | |
| 5802 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
| 5803 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
| 5804 }, | |
| 5805 | |
| 5806 /** | |
| 5807 * In addition to `IronButtonState` behavior, when space key goes down, | |
| 5808 * create a ripple down effect. | |
| 5809 * | |
| 5810 * @param {!KeyboardEvent} event . | |
| 5811 */ | |
| 5812 _spaceKeyDownHandler: function(event) { | |
| 5813 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
| 5814 // Ensure that there is at most one ripple when the space key is held down
. | |
| 5815 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
| 5816 this._ripple.uiDownAction(); | |
| 5817 } | |
| 5818 }, | |
| 5819 | |
| 5820 /** | |
| 5821 * In addition to `IronButtonState` behavior, when space key goes up, | |
| 5822 * create a ripple up effect. | |
| 5823 * | |
| 5824 * @param {!KeyboardEvent} event . | |
| 5825 */ | |
| 5826 _spaceKeyUpHandler: function(event) { | |
| 5827 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
| 5828 if (this.hasRipple()) { | |
| 5829 this._ripple.uiUpAction(); | |
| 5830 } | |
| 5831 } | |
| 5832 }; | |
| 5833 | |
| 5834 /** @polymerBehavior */ | |
| 5835 Polymer.PaperButtonBehavior = [ | |
| 5836 Polymer.IronButtonState, | |
| 5837 Polymer.IronControlState, | |
| 5838 Polymer.PaperRippleBehavior, | |
| 5839 Polymer.PaperButtonBehaviorImpl | |
| 5840 ]; | |
| 5841 Polymer({ | 3229 Polymer({ |
| 5842 is: 'paper-button', | 3230 is: 'paper-button', |
| 5843 | 3231 behaviors: [ Polymer.PaperButtonBehavior ], |
| 5844 behaviors: [ | 3232 properties: { |
| 5845 Polymer.PaperButtonBehavior | 3233 raised: { |
| 5846 ], | 3234 type: Boolean, |
| 5847 | 3235 reflectToAttribute: true, |
| 5848 properties: { | 3236 value: false, |
| 5849 /** | 3237 observer: '_calculateElevation' |
| 5850 * If true, the button should be styled with a shadow. | 3238 } |
| 5851 */ | 3239 }, |
| 5852 raised: { | 3240 _calculateElevation: function() { |
| 5853 type: Boolean, | 3241 if (!this.raised) { |
| 5854 reflectToAttribute: true, | 3242 this._setElevation(0); |
| 5855 value: false, | 3243 } else { |
| 5856 observer: '_calculateElevation' | 3244 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
| 5857 } | 3245 } |
| 5858 }, | 3246 } |
| 5859 | 3247 }); |
| 5860 _calculateElevation: function() { | 3248 |
| 5861 if (!this.raised) { | |
| 5862 this._setElevation(0); | |
| 5863 } else { | |
| 5864 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | |
| 5865 } | |
| 5866 } | |
| 5867 | |
| 5868 /** | |
| 5869 Fired when the animation finishes. | |
| 5870 This is useful if you want to wait until | |
| 5871 the ripple animation finishes to perform some action. | |
| 5872 | |
| 5873 @event transitionend | |
| 5874 Event param: {{node: Object}} detail Contains the animated node. | |
| 5875 */ | |
| 5876 }); | |
| 5877 Polymer({ | 3249 Polymer({ |
| 5878 is: 'paper-icon-button-light', | 3250 is: 'paper-icon-button-light', |
| 5879 extends: 'button', | 3251 "extends": 'button', |
| 5880 | 3252 behaviors: [ Polymer.PaperRippleBehavior ], |
| 5881 behaviors: [ | 3253 listeners: { |
| 5882 Polymer.PaperRippleBehavior | 3254 down: '_rippleDown', |
| 5883 ], | 3255 up: '_rippleUp', |
| 5884 | 3256 focus: '_rippleDown', |
| 5885 listeners: { | 3257 blur: '_rippleUp' |
| 5886 'down': '_rippleDown', | 3258 }, |
| 5887 'up': '_rippleUp', | 3259 _rippleDown: function() { |
| 5888 'focus': '_rippleDown', | 3260 this.getRipple().downAction(); |
| 5889 'blur': '_rippleUp', | 3261 }, |
| 5890 }, | 3262 _rippleUp: function() { |
| 5891 | 3263 this.getRipple().upAction(); |
| 5892 _rippleDown: function() { | 3264 }, |
| 5893 this.getRipple().downAction(); | 3265 ensureRipple: function(var_args) { |
| 5894 }, | 3266 var lastRipple = this._ripple; |
| 5895 | 3267 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
| 5896 _rippleUp: function() { | 3268 if (this._ripple && this._ripple !== lastRipple) { |
| 5897 this.getRipple().upAction(); | 3269 this._ripple.center = true; |
| 5898 }, | 3270 this._ripple.classList.add('circle'); |
| 5899 | 3271 } |
| 5900 /** | 3272 } |
| 5901 * @param {...*} var_args | 3273 }); |
| 5902 */ | 3274 |
| 5903 ensureRipple: function(var_args) { | 3275 Polymer.IronRangeBehavior = { |
| 5904 var lastRipple = this._ripple; | |
| 5905 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | |
| 5906 if (this._ripple && this._ripple !== lastRipple) { | |
| 5907 this._ripple.center = true; | |
| 5908 this._ripple.classList.add('circle'); | |
| 5909 } | |
| 5910 } | |
| 5911 }); | |
| 5912 /** | |
| 5913 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | |
| 5914 * | |
| 5915 * @demo demo/index.html | |
| 5916 * @polymerBehavior | |
| 5917 */ | |
| 5918 Polymer.IronRangeBehavior = { | |
| 5919 | |
| 5920 properties: { | 3276 properties: { |
| 5921 | |
| 5922 /** | |
| 5923 * The number that represents the current value. | |
| 5924 */ | |
| 5925 value: { | 3277 value: { |
| 5926 type: Number, | 3278 type: Number, |
| 5927 value: 0, | 3279 value: 0, |
| 5928 notify: true, | 3280 notify: true, |
| 5929 reflectToAttribute: true | 3281 reflectToAttribute: true |
| 5930 }, | 3282 }, |
| 5931 | |
| 5932 /** | |
| 5933 * The number that indicates the minimum value of the range. | |
| 5934 */ | |
| 5935 min: { | 3283 min: { |
| 5936 type: Number, | 3284 type: Number, |
| 5937 value: 0, | 3285 value: 0, |
| 5938 notify: true | 3286 notify: true |
| 5939 }, | 3287 }, |
| 5940 | |
| 5941 /** | |
| 5942 * The number that indicates the maximum value of the range. | |
| 5943 */ | |
| 5944 max: { | 3288 max: { |
| 5945 type: Number, | 3289 type: Number, |
| 5946 value: 100, | 3290 value: 100, |
| 5947 notify: true | 3291 notify: true |
| 5948 }, | 3292 }, |
| 5949 | |
| 5950 /** | |
| 5951 * Specifies the value granularity of the range's value. | |
| 5952 */ | |
| 5953 step: { | 3293 step: { |
| 5954 type: Number, | 3294 type: Number, |
| 5955 value: 1, | 3295 value: 1, |
| 5956 notify: true | 3296 notify: true |
| 5957 }, | 3297 }, |
| 5958 | |
| 5959 /** | |
| 5960 * Returns the ratio of the value. | |
| 5961 */ | |
| 5962 ratio: { | 3298 ratio: { |
| 5963 type: Number, | 3299 type: Number, |
| 5964 value: 0, | 3300 value: 0, |
| 5965 readOnly: true, | 3301 readOnly: true, |
| 5966 notify: true | 3302 notify: true |
| 5967 }, | 3303 } |
| 5968 }, | 3304 }, |
| 5969 | 3305 observers: [ '_update(value, min, max, step)' ], |
| 5970 observers: [ | |
| 5971 '_update(value, min, max, step)' | |
| 5972 ], | |
| 5973 | |
| 5974 _calcRatio: function(value) { | 3306 _calcRatio: function(value) { |
| 5975 return (this._clampValue(value) - this.min) / (this.max - this.min); | 3307 return (this._clampValue(value) - this.min) / (this.max - this.min); |
| 5976 }, | 3308 }, |
| 5977 | |
| 5978 _clampValue: function(value) { | 3309 _clampValue: function(value) { |
| 5979 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | 3310 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); |
| 5980 }, | 3311 }, |
| 5981 | |
| 5982 _calcStep: function(value) { | 3312 _calcStep: function(value) { |
| 5983 // polymer/issues/2493 | |
| 5984 value = parseFloat(value); | 3313 value = parseFloat(value); |
| 5985 | |
| 5986 if (!this.step) { | 3314 if (!this.step) { |
| 5987 return value; | 3315 return value; |
| 5988 } | 3316 } |
| 5989 | |
| 5990 var numSteps = Math.round((value - this.min) / this.step); | 3317 var numSteps = Math.round((value - this.min) / this.step); |
| 5991 if (this.step < 1) { | 3318 if (this.step < 1) { |
| 5992 /** | |
| 5993 * For small values of this.step, if we calculate the step using | |
| 5994 * `Math.round(value / step) * step` we may hit a precision point issue | |
| 5995 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
| 5996 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
| 5997 * | |
| 5998 * as a work around we can divide by the reciprocal of `step` | |
| 5999 */ | |
| 6000 return numSteps / (1 / this.step) + this.min; | 3319 return numSteps / (1 / this.step) + this.min; |
| 6001 } else { | 3320 } else { |
| 6002 return numSteps * this.step + this.min; | 3321 return numSteps * this.step + this.min; |
| 6003 } | 3322 } |
| 6004 }, | 3323 }, |
| 6005 | |
| 6006 _validateValue: function() { | 3324 _validateValue: function() { |
| 6007 var v = this._clampValue(this.value); | 3325 var v = this._clampValue(this.value); |
| 6008 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | 3326 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; |
| 6009 return this.value !== v; | 3327 return this.value !== v; |
| 6010 }, | 3328 }, |
| 6011 | |
| 6012 _update: function() { | 3329 _update: function() { |
| 6013 this._validateValue(); | 3330 this._validateValue(); |
| 6014 this._setRatio(this._calcRatio(this.value) * 100); | 3331 this._setRatio(this._calcRatio(this.value) * 100); |
| 6015 } | 3332 } |
| 6016 | |
| 6017 }; | 3333 }; |
| 3334 |
| 6018 Polymer({ | 3335 Polymer({ |
| 6019 is: 'paper-progress', | 3336 is: 'paper-progress', |
| 6020 | 3337 behaviors: [ Polymer.IronRangeBehavior ], |
| 6021 behaviors: [ | 3338 properties: { |
| 6022 Polymer.IronRangeBehavior | 3339 secondaryProgress: { |
| 6023 ], | 3340 type: Number, |
| 6024 | 3341 value: 0 |
| 6025 properties: { | 3342 }, |
| 6026 /** | 3343 secondaryRatio: { |
| 6027 * The number that represents the current secondary progress. | 3344 type: Number, |
| 6028 */ | 3345 value: 0, |
| 6029 secondaryProgress: { | 3346 readOnly: true |
| 6030 type: Number, | 3347 }, |
| 6031 value: 0 | 3348 indeterminate: { |
| 6032 }, | 3349 type: Boolean, |
| 6033 | 3350 value: false, |
| 6034 /** | 3351 observer: '_toggleIndeterminate' |
| 6035 * The secondary ratio | 3352 }, |
| 6036 */ | 3353 disabled: { |
| 6037 secondaryRatio: { | 3354 type: Boolean, |
| 6038 type: Number, | 3355 value: false, |
| 6039 value: 0, | 3356 reflectToAttribute: true, |
| 6040 readOnly: true | 3357 observer: '_disabledChanged' |
| 6041 }, | 3358 } |
| 6042 | 3359 }, |
| 6043 /** | 3360 observers: [ '_progressChanged(secondaryProgress, value, min, max)' ], |
| 6044 * Use an indeterminate progress indicator. | 3361 hostAttributes: { |
| 6045 */ | 3362 role: 'progressbar' |
| 6046 indeterminate: { | 3363 }, |
| 6047 type: Boolean, | 3364 _toggleIndeterminate: function(indeterminate) { |
| 6048 value: false, | 3365 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); |
| 6049 observer: '_toggleIndeterminate' | 3366 }, |
| 6050 }, | 3367 _transformProgress: function(progress, ratio) { |
| 6051 | 3368 var transform = 'scaleX(' + ratio / 100 + ')'; |
| 6052 /** | 3369 progress.style.transform = progress.style.webkitTransform = transform; |
| 6053 * True if the progress is disabled. | 3370 }, |
| 6054 */ | 3371 _mainRatioChanged: function(ratio) { |
| 6055 disabled: { | 3372 this._transformProgress(this.$.primaryProgress, ratio); |
| 6056 type: Boolean, | 3373 }, |
| 6057 value: false, | 3374 _progressChanged: function(secondaryProgress, value, min, max) { |
| 6058 reflectToAttribute: true, | 3375 secondaryProgress = this._clampValue(secondaryProgress); |
| 6059 observer: '_disabledChanged' | 3376 value = this._clampValue(value); |
| 6060 } | 3377 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; |
| 6061 }, | 3378 var mainRatio = this._calcRatio(value) * 100; |
| 6062 | 3379 this._setSecondaryRatio(secondaryRatio); |
| 6063 observers: [ | 3380 this._transformProgress(this.$.secondaryProgress, secondaryRatio); |
| 6064 '_progressChanged(secondaryProgress, value, min, max)' | 3381 this._transformProgress(this.$.primaryProgress, mainRatio); |
| 6065 ], | 3382 this.secondaryProgress = secondaryProgress; |
| 6066 | 3383 this.setAttribute('aria-valuenow', value); |
| 6067 hostAttributes: { | 3384 this.setAttribute('aria-valuemin', min); |
| 6068 role: 'progressbar' | 3385 this.setAttribute('aria-valuemax', max); |
| 6069 }, | 3386 }, |
| 6070 | 3387 _disabledChanged: function(disabled) { |
| 6071 _toggleIndeterminate: function(indeterminate) { | 3388 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 6072 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | 3389 }, |
| 6073 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | 3390 _hideSecondaryProgress: function(secondaryRatio) { |
| 6074 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 3391 return secondaryRatio === 0; |
| 6075 }, | 3392 } |
| 6076 | 3393 }); |
| 6077 _transformProgress: function(progress, ratio) { | 3394 |
| 6078 var transform = 'scaleX(' + (ratio / 100) + ')'; | 3395 Polymer({ |
| 6079 progress.style.transform = progress.style.webkitTransform = transform; | 3396 is: 'iron-iconset-svg', |
| 6080 }, | 3397 properties: { |
| 6081 | 3398 name: { |
| 6082 _mainRatioChanged: function(ratio) { | 3399 type: String, |
| 6083 this._transformProgress(this.$.primaryProgress, ratio); | 3400 observer: '_nameChanged' |
| 6084 }, | 3401 }, |
| 6085 | 3402 size: { |
| 6086 _progressChanged: function(secondaryProgress, value, min, max) { | 3403 type: Number, |
| 6087 secondaryProgress = this._clampValue(secondaryProgress); | 3404 value: 24 |
| 6088 value = this._clampValue(value); | 3405 } |
| 6089 | 3406 }, |
| 6090 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; | 3407 attached: function() { |
| 6091 var mainRatio = this._calcRatio(value) * 100; | 3408 this.style.display = 'none'; |
| 6092 | 3409 }, |
| 6093 this._setSecondaryRatio(secondaryRatio); | 3410 getIconNames: function() { |
| 6094 this._transformProgress(this.$.secondaryProgress, secondaryRatio); | 3411 this._icons = this._createIconMap(); |
| 6095 this._transformProgress(this.$.primaryProgress, mainRatio); | 3412 return Object.keys(this._icons).map(function(n) { |
| 6096 | 3413 return this.name + ':' + n; |
| 6097 this.secondaryProgress = secondaryProgress; | 3414 }, this); |
| 6098 | 3415 }, |
| 6099 this.setAttribute('aria-valuenow', value); | 3416 applyIcon: function(element, iconName) { |
| 6100 this.setAttribute('aria-valuemin', min); | 3417 element = element.root || element; |
| 6101 this.setAttribute('aria-valuemax', max); | 3418 this.removeIcon(element); |
| 6102 }, | 3419 var svg = this._cloneIcon(iconName); |
| 6103 | 3420 if (svg) { |
| 6104 _disabledChanged: function(disabled) { | 3421 var pde = Polymer.dom(element); |
| 6105 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 3422 pde.insertBefore(svg, pde.childNodes[0]); |
| 6106 }, | 3423 return element._svgIcon = svg; |
| 6107 | 3424 } |
| 6108 _hideSecondaryProgress: function(secondaryRatio) { | 3425 return null; |
| 6109 return secondaryRatio === 0; | 3426 }, |
| 6110 } | 3427 removeIcon: function(element) { |
| 6111 }); | 3428 if (element._svgIcon) { |
| 6112 /** | 3429 Polymer.dom(element).removeChild(element._svgIcon); |
| 6113 * The `iron-iconset-svg` element allows users to define their own icon sets | 3430 element._svgIcon = null; |
| 6114 * that contain svg icons. The svg icon elements should be children of the | 3431 } |
| 6115 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | 3432 }, |
| 6116 * | 3433 _nameChanged: function() { |
| 6117 * Using svg elements to create icons has a few advantages over traditional | 3434 new Polymer.IronMeta({ |
| 6118 * bitmap graphics like jpg or png. Icons that use svg are vector based so | 3435 type: 'iconset', |
| 6119 * they are resolution independent and should look good on any device. They | 3436 key: this.name, |
| 6120 * are stylable via css. Icons can be themed, colorized, and even animated. | 3437 value: this |
| 6121 * | 3438 }); |
| 6122 * Example: | 3439 this.async(function() { |
| 6123 * | 3440 this.fire('iron-iconset-added', this, { |
| 6124 * <iron-iconset-svg name="my-svg-icons" size="24"> | 3441 node: window |
| 6125 * <svg> | |
| 6126 * <defs> | |
| 6127 * <g id="shape"> | |
| 6128 * <rect x="12" y="0" width="12" height="24" /> | |
| 6129 * <circle cx="12" cy="12" r="12" /> | |
| 6130 * </g> | |
| 6131 * </defs> | |
| 6132 * </svg> | |
| 6133 * </iron-iconset-svg> | |
| 6134 * | |
| 6135 * This will automatically register the icon set "my-svg-icons" to the iconset | |
| 6136 * database. To use these icons from within another element, make a | |
| 6137 * `iron-iconset` element and call the `byId` method | |
| 6138 * to retrieve a given iconset. To apply a particular icon inside an | |
| 6139 * element use the `applyIcon` method. For example: | |
| 6140 * | |
| 6141 * iconset.applyIcon(iconNode, 'car'); | |
| 6142 * | |
| 6143 * @element iron-iconset-svg | |
| 6144 * @demo demo/index.html | |
| 6145 * @implements {Polymer.Iconset} | |
| 6146 */ | |
| 6147 Polymer({ | |
| 6148 is: 'iron-iconset-svg', | |
| 6149 | |
| 6150 properties: { | |
| 6151 | |
| 6152 /** | |
| 6153 * The name of the iconset. | |
| 6154 */ | |
| 6155 name: { | |
| 6156 type: String, | |
| 6157 observer: '_nameChanged' | |
| 6158 }, | |
| 6159 | |
| 6160 /** | |
| 6161 * The size of an individual icon. Note that icons must be square. | |
| 6162 */ | |
| 6163 size: { | |
| 6164 type: Number, | |
| 6165 value: 24 | |
| 6166 } | |
| 6167 | |
| 6168 }, | |
| 6169 | |
| 6170 attached: function() { | |
| 6171 this.style.display = 'none'; | |
| 6172 }, | |
| 6173 | |
| 6174 /** | |
| 6175 * Construct an array of all icon names in this iconset. | |
| 6176 * | |
| 6177 * @return {!Array} Array of icon names. | |
| 6178 */ | |
| 6179 getIconNames: function() { | |
| 6180 this._icons = this._createIconMap(); | |
| 6181 return Object.keys(this._icons).map(function(n) { | |
| 6182 return this.name + ':' + n; | |
| 6183 }, this); | |
| 6184 }, | |
| 6185 | |
| 6186 /** | |
| 6187 * Applies an icon to the given element. | |
| 6188 * | |
| 6189 * An svg icon is prepended to the element's shadowRoot if it exists, | |
| 6190 * otherwise to the element itself. | |
| 6191 * | |
| 6192 * @method applyIcon | |
| 6193 * @param {Element} element Element to which the icon is applied. | |
| 6194 * @param {string} iconName Name of the icon to apply. | |
| 6195 * @return {?Element} The svg element which renders the icon. | |
| 6196 */ | |
| 6197 applyIcon: function(element, iconName) { | |
| 6198 // insert svg element into shadow root, if it exists | |
| 6199 element = element.root || element; | |
| 6200 // Remove old svg element | |
| 6201 this.removeIcon(element); | |
| 6202 // install new svg element | |
| 6203 var svg = this._cloneIcon(iconName); | |
| 6204 if (svg) { | |
| 6205 var pde = Polymer.dom(element); | |
| 6206 pde.insertBefore(svg, pde.childNodes[0]); | |
| 6207 return element._svgIcon = svg; | |
| 6208 } | |
| 6209 return null; | |
| 6210 }, | |
| 6211 | |
| 6212 /** | |
| 6213 * Remove an icon from the given element by undoing the changes effected | |
| 6214 * by `applyIcon`. | |
| 6215 * | |
| 6216 * @param {Element} element The element from which the icon is removed. | |
| 6217 */ | |
| 6218 removeIcon: function(element) { | |
| 6219 // Remove old svg element | |
| 6220 if (element._svgIcon) { | |
| 6221 Polymer.dom(element).removeChild(element._svgIcon); | |
| 6222 element._svgIcon = null; | |
| 6223 } | |
| 6224 }, | |
| 6225 | |
| 6226 /** | |
| 6227 * | |
| 6228 * When name is changed, register iconset metadata | |
| 6229 * | |
| 6230 */ | |
| 6231 _nameChanged: function() { | |
| 6232 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | |
| 6233 this.async(function() { | |
| 6234 this.fire('iron-iconset-added', this, {node: window}); | |
| 6235 }); | 3442 }); |
| 6236 }, | 3443 }); |
| 6237 | 3444 }, |
| 6238 /** | 3445 _createIconMap: function() { |
| 6239 * Create a map of child SVG elements by id. | 3446 var icons = Object.create(null); |
| 6240 * | 3447 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) { |
| 6241 * @return {!Object} Map of id's to SVG elements. | 3448 icons[icon.id] = icon; |
| 6242 */ | 3449 }); |
| 6243 _createIconMap: function() { | 3450 return icons; |
| 6244 // Objects chained to Object.prototype (`{}`) have members. Specifically, | 3451 }, |
| 6245 // on FF there is a `watch` method that confuses the icon map, so we | 3452 _cloneIcon: function(id) { |
| 6246 // need to use a null-based object here. | 3453 this._icons = this._icons || this._createIconMap(); |
| 6247 var icons = Object.create(null); | 3454 return this._prepareSvgClone(this._icons[id], this.size); |
| 6248 Polymer.dom(this).querySelectorAll('[id]') | 3455 }, |
| 6249 .forEach(function(icon) { | 3456 _prepareSvgClone: function(sourceSvg, size) { |
| 6250 icons[icon.id] = icon; | 3457 if (sourceSvg) { |
| 6251 }); | 3458 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h
ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') ||
'0 0 ' + size + ' ' + size; |
| 6252 return icons; | 3459 svg.setAttribute('viewBox', viewBox); |
| 6253 }, | 3460 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
| 6254 | 3461 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he
ight: 100%;'; |
| 6255 /** | 3462 svg.appendChild(content).removeAttribute('id'); |
| 6256 * Produce installable clone of the SVG element matching `id` in this | 3463 return svg; |
| 6257 * iconset, or `undefined` if there is no matching element. | 3464 } |
| 6258 * | 3465 return null; |
| 6259 * @return {Element} Returns an installable clone of the SVG element | 3466 } |
| 6260 * matching `id`. | 3467 }); |
| 6261 */ | 3468 |
| 6262 _cloneIcon: function(id) { | |
| 6263 // create the icon map on-demand, since the iconset itself has no discrete | |
| 6264 // signal to know when it's children are fully parsed | |
| 6265 this._icons = this._icons || this._createIconMap(); | |
| 6266 return this._prepareSvgClone(this._icons[id], this.size); | |
| 6267 }, | |
| 6268 | |
| 6269 /** | |
| 6270 * @param {Element} sourceSvg | |
| 6271 * @param {number} size | |
| 6272 * @return {Element} | |
| 6273 */ | |
| 6274 _prepareSvgClone: function(sourceSvg, size) { | |
| 6275 if (sourceSvg) { | |
| 6276 var content = sourceSvg.cloneNode(true), | |
| 6277 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | |
| 6278 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | |
| 6279 svg.setAttribute('viewBox', viewBox); | |
| 6280 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | |
| 6281 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
| 6282 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
| 6283 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | |
| 6284 svg.appendChild(content).removeAttribute('id'); | |
| 6285 return svg; | |
| 6286 } | |
| 6287 return null; | |
| 6288 } | |
| 6289 | |
| 6290 }); | |
| 6291 // Copyright 2015 The Chromium Authors. All rights reserved. | 3469 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 6292 // Use of this source code is governed by a BSD-style license that can be | 3470 // Use of this source code is governed by a BSD-style license that can be |
| 6293 // found in the LICENSE file. | 3471 // found in the LICENSE file. |
| 6294 | |
| 6295 cr.define('downloads', function() { | 3472 cr.define('downloads', function() { |
| 6296 var Item = Polymer({ | 3473 var Item = Polymer({ |
| 6297 is: 'downloads-item', | 3474 is: 'downloads-item', |
| 6298 | |
| 6299 properties: { | 3475 properties: { |
| 6300 data: { | 3476 data: { |
| 6301 type: Object, | 3477 type: Object |
| 6302 }, | 3478 }, |
| 6303 | |
| 6304 completelyOnDisk_: { | 3479 completelyOnDisk_: { |
| 6305 computed: 'computeCompletelyOnDisk_(' + | 3480 computed: 'computeCompletelyOnDisk_(' + 'data.state, data.file_externall
y_removed)', |
| 6306 'data.state, data.file_externally_removed)', | |
| 6307 type: Boolean, | 3481 type: Boolean, |
| 6308 value: true, | 3482 value: true |
| 6309 }, | 3483 }, |
| 6310 | |
| 6311 controlledBy_: { | 3484 controlledBy_: { |
| 6312 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', | 3485 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', |
| 6313 type: String, | 3486 type: String, |
| 6314 value: '', | 3487 value: '' |
| 6315 }, | 3488 }, |
| 6316 | |
| 6317 isActive_: { | 3489 isActive_: { |
| 6318 computed: 'computeIsActive_(' + | 3490 computed: 'computeIsActive_(' + 'data.state, data.file_externally_remove
d)', |
| 6319 'data.state, data.file_externally_removed)', | |
| 6320 type: Boolean, | 3491 type: Boolean, |
| 6321 value: true, | 3492 value: true |
| 6322 }, | 3493 }, |
| 6323 | |
| 6324 isDangerous_: { | 3494 isDangerous_: { |
| 6325 computed: 'computeIsDangerous_(data.state)', | 3495 computed: 'computeIsDangerous_(data.state)', |
| 6326 type: Boolean, | 3496 type: Boolean, |
| 6327 value: false, | 3497 value: false |
| 6328 }, | 3498 }, |
| 6329 | |
| 6330 isMalware_: { | 3499 isMalware_: { |
| 6331 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', | 3500 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', |
| 6332 type: Boolean, | 3501 type: Boolean, |
| 6333 value: false, | 3502 value: false |
| 6334 }, | 3503 }, |
| 6335 | |
| 6336 isInProgress_: { | 3504 isInProgress_: { |
| 6337 computed: 'computeIsInProgress_(data.state)', | 3505 computed: 'computeIsInProgress_(data.state)', |
| 6338 type: Boolean, | 3506 type: Boolean, |
| 6339 value: false, | 3507 value: false |
| 6340 }, | 3508 }, |
| 6341 | |
| 6342 pauseOrResumeText_: { | 3509 pauseOrResumeText_: { |
| 6343 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', | 3510 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', |
| 6344 type: String, | 3511 type: String |
| 6345 }, | 3512 }, |
| 6346 | |
| 6347 showCancel_: { | 3513 showCancel_: { |
| 6348 computed: 'computeShowCancel_(data.state)', | 3514 computed: 'computeShowCancel_(data.state)', |
| 6349 type: Boolean, | 3515 type: Boolean, |
| 6350 value: false, | 3516 value: false |
| 6351 }, | 3517 }, |
| 6352 | |
| 6353 showProgress_: { | 3518 showProgress_: { |
| 6354 computed: 'computeShowProgress_(showCancel_, data.percent)', | 3519 computed: 'computeShowProgress_(showCancel_, data.percent)', |
| 6355 type: Boolean, | 3520 type: Boolean, |
| 6356 value: false, | 3521 value: false |
| 6357 }, | 3522 } |
| 6358 }, | 3523 }, |
| 6359 | 3524 observers: [ 'observeControlledBy_(controlledBy_)', 'observeIsDangerous_(isD
angerous_, data)' ], |
| 6360 observers: [ | |
| 6361 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | |
| 6362 // and data.by_ext_name directly. Why? | |
| 6363 'observeControlledBy_(controlledBy_)', | |
| 6364 'observeIsDangerous_(isDangerous_, data)', | |
| 6365 ], | |
| 6366 | |
| 6367 ready: function() { | 3525 ready: function() { |
| 6368 this.content = this.$.content; | 3526 this.content = this.$.content; |
| 6369 }, | 3527 }, |
| 6370 | |
| 6371 /** @private */ | |
| 6372 computeClass_: function() { | 3528 computeClass_: function() { |
| 6373 var classes = []; | 3529 var classes = []; |
| 6374 | 3530 if (this.isActive_) classes.push('is-active'); |
| 6375 if (this.isActive_) | 3531 if (this.isDangerous_) classes.push('dangerous'); |
| 6376 classes.push('is-active'); | 3532 if (this.showProgress_) classes.push('show-progress'); |
| 6377 | |
| 6378 if (this.isDangerous_) | |
| 6379 classes.push('dangerous'); | |
| 6380 | |
| 6381 if (this.showProgress_) | |
| 6382 classes.push('show-progress'); | |
| 6383 | |
| 6384 return classes.join(' '); | 3533 return classes.join(' '); |
| 6385 }, | 3534 }, |
| 6386 | |
| 6387 /** @private */ | |
| 6388 computeCompletelyOnDisk_: function() { | 3535 computeCompletelyOnDisk_: function() { |
| 6389 return this.data.state == downloads.States.COMPLETE && | 3536 return this.data.state == downloads.States.COMPLETE && !this.data.file_ext
ernally_removed; |
| 6390 !this.data.file_externally_removed; | 3537 }, |
| 6391 }, | |
| 6392 | |
| 6393 /** @private */ | |
| 6394 computeControlledBy_: function() { | 3538 computeControlledBy_: function() { |
| 6395 if (!this.data.by_ext_id || !this.data.by_ext_name) | 3539 if (!this.data.by_ext_id || !this.data.by_ext_name) return ''; |
| 6396 return ''; | |
| 6397 | |
| 6398 var url = 'chrome://extensions#' + this.data.by_ext_id; | 3540 var url = 'chrome://extensions#' + this.data.by_ext_id; |
| 6399 var name = this.data.by_ext_name; | 3541 var name = this.data.by_ext_name; |
| 6400 return loadTimeData.getStringF('controlledByUrl', url, name); | 3542 return loadTimeData.getStringF('controlledByUrl', url, name); |
| 6401 }, | 3543 }, |
| 6402 | |
| 6403 /** @private */ | |
| 6404 computeDangerIcon_: function() { | 3544 computeDangerIcon_: function() { |
| 6405 if (!this.isDangerous_) | 3545 if (!this.isDangerous_) return ''; |
| 6406 return ''; | |
| 6407 | |
| 6408 switch (this.data.danger_type) { | 3546 switch (this.data.danger_type) { |
| 6409 case downloads.DangerType.DANGEROUS_CONTENT: | 3547 case downloads.DangerType.DANGEROUS_CONTENT: |
| 6410 case downloads.DangerType.DANGEROUS_HOST: | 3548 case downloads.DangerType.DANGEROUS_HOST: |
| 6411 case downloads.DangerType.DANGEROUS_URL: | 3549 case downloads.DangerType.DANGEROUS_URL: |
| 6412 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3550 case downloads.DangerType.POTENTIALLY_UNWANTED: |
| 6413 case downloads.DangerType.UNCOMMON_CONTENT: | 3551 case downloads.DangerType.UNCOMMON_CONTENT: |
| 6414 return 'downloads:remove-circle'; | 3552 return 'downloads:remove-circle'; |
| 6415 default: | 3553 |
| 6416 return 'cr:warning'; | 3554 default: |
| 6417 } | 3555 return 'cr:warning'; |
| 6418 }, | 3556 } |
| 6419 | 3557 }, |
| 6420 /** @private */ | |
| 6421 computeDate_: function() { | 3558 computeDate_: function() { |
| 6422 assert(typeof this.data.hideDate == 'boolean'); | 3559 assert(typeof this.data.hideDate == 'boolean'); |
| 6423 if (this.data.hideDate) | 3560 if (this.data.hideDate) return ''; |
| 6424 return ''; | |
| 6425 return assert(this.data.since_string || this.data.date_string); | 3561 return assert(this.data.since_string || this.data.date_string); |
| 6426 }, | 3562 }, |
| 6427 | |
| 6428 /** @private */ | |
| 6429 computeDescription_: function() { | 3563 computeDescription_: function() { |
| 6430 var data = this.data; | 3564 var data = this.data; |
| 6431 | |
| 6432 switch (data.state) { | 3565 switch (data.state) { |
| 6433 case downloads.States.DANGEROUS: | 3566 case downloads.States.DANGEROUS: |
| 6434 var fileName = data.file_name; | 3567 var fileName = data.file_name; |
| 6435 switch (data.danger_type) { | 3568 switch (data.danger_type) { |
| 6436 case downloads.DangerType.DANGEROUS_FILE: | 3569 case downloads.DangerType.DANGEROUS_FILE: |
| 6437 return loadTimeData.getStringF('dangerFileDesc', fileName); | 3570 return loadTimeData.getStringF('dangerFileDesc', fileName); |
| 6438 case downloads.DangerType.DANGEROUS_URL: | 3571 |
| 6439 return loadTimeData.getString('dangerUrlDesc'); | 3572 case downloads.DangerType.DANGEROUS_URL: |
| 6440 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. | 3573 return loadTimeData.getString('dangerUrlDesc'); |
| 6441 case downloads.DangerType.DANGEROUS_HOST: | 3574 |
| 6442 return loadTimeData.getStringF('dangerContentDesc', fileName); | 3575 case downloads.DangerType.DANGEROUS_CONTENT: |
| 6443 case downloads.DangerType.UNCOMMON_CONTENT: | 3576 case downloads.DangerType.DANGEROUS_HOST: |
| 6444 return loadTimeData.getStringF('dangerUncommonDesc', fileName); | 3577 return loadTimeData.getStringF('dangerContentDesc', fileName); |
| 6445 case downloads.DangerType.POTENTIALLY_UNWANTED: | 3578 |
| 6446 return loadTimeData.getStringF('dangerSettingsDesc', fileName); | 3579 case downloads.DangerType.UNCOMMON_CONTENT: |
| 6447 } | 3580 return loadTimeData.getStringF('dangerUncommonDesc', fileName); |
| 6448 break; | 3581 |
| 6449 | 3582 case downloads.DangerType.POTENTIALLY_UNWANTED: |
| 6450 case downloads.States.IN_PROGRESS: | 3583 return loadTimeData.getStringF('dangerSettingsDesc', fileName); |
| 6451 case downloads.States.PAUSED: // Fallthrough. | 3584 } |
| 6452 return data.progress_status_text; | 3585 break; |
| 6453 } | 3586 |
| 6454 | 3587 case downloads.States.IN_PROGRESS: |
| 3588 case downloads.States.PAUSED: |
| 3589 return data.progress_status_text; |
| 3590 } |
| 6455 return ''; | 3591 return ''; |
| 6456 }, | 3592 }, |
| 6457 | |
| 6458 /** @private */ | |
| 6459 computeIsActive_: function() { | 3593 computeIsActive_: function() { |
| 6460 return this.data.state != downloads.States.CANCELLED && | 3594 return this.data.state != downloads.States.CANCELLED && this.data.state !=
downloads.States.INTERRUPTED && !this.data.file_externally_removed; |
| 6461 this.data.state != downloads.States.INTERRUPTED && | 3595 }, |
| 6462 !this.data.file_externally_removed; | |
| 6463 }, | |
| 6464 | |
| 6465 /** @private */ | |
| 6466 computeIsDangerous_: function() { | 3596 computeIsDangerous_: function() { |
| 6467 return this.data.state == downloads.States.DANGEROUS; | 3597 return this.data.state == downloads.States.DANGEROUS; |
| 6468 }, | 3598 }, |
| 6469 | |
| 6470 /** @private */ | |
| 6471 computeIsInProgress_: function() { | 3599 computeIsInProgress_: function() { |
| 6472 return this.data.state == downloads.States.IN_PROGRESS; | 3600 return this.data.state == downloads.States.IN_PROGRESS; |
| 6473 }, | 3601 }, |
| 6474 | |
| 6475 /** @private */ | |
| 6476 computeIsMalware_: function() { | 3602 computeIsMalware_: function() { |
| 6477 return this.isDangerous_ && | 3603 return this.isDangerous_ && (this.data.danger_type == downloads.DangerType
.DANGEROUS_CONTENT || this.data.danger_type == downloads.DangerType.DANGEROUS_HO
ST || this.data.danger_type == downloads.DangerType.DANGEROUS_URL || this.data.d
anger_type == downloads.DangerType.POTENTIALLY_UNWANTED); |
| 6478 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || | 3604 }, |
| 6479 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || | |
| 6480 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || | |
| 6481 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); | |
| 6482 }, | |
| 6483 | |
| 6484 /** @private */ | |
| 6485 computePauseOrResumeText_: function() { | 3605 computePauseOrResumeText_: function() { |
| 6486 if (this.isInProgress_) | 3606 if (this.isInProgress_) return loadTimeData.getString('controlPause'); |
| 6487 return loadTimeData.getString('controlPause'); | 3607 if (this.data.resume) return loadTimeData.getString('controlResume'); |
| 6488 if (this.data.resume) | |
| 6489 return loadTimeData.getString('controlResume'); | |
| 6490 return ''; | 3608 return ''; |
| 6491 }, | 3609 }, |
| 6492 | |
| 6493 /** @private */ | |
| 6494 computeRemoveStyle_: function() { | 3610 computeRemoveStyle_: function() { |
| 6495 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); | 3611 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); |
| 6496 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; | 3612 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; |
| 6497 return hideRemove ? 'visibility: hidden' : ''; | 3613 return hideRemove ? 'visibility: hidden' : ''; |
| 6498 }, | 3614 }, |
| 6499 | |
| 6500 /** @private */ | |
| 6501 computeShowCancel_: function() { | 3615 computeShowCancel_: function() { |
| 6502 return this.data.state == downloads.States.IN_PROGRESS || | 3616 return this.data.state == downloads.States.IN_PROGRESS || this.data.state
== downloads.States.PAUSED; |
| 6503 this.data.state == downloads.States.PAUSED; | 3617 }, |
| 6504 }, | |
| 6505 | |
| 6506 /** @private */ | |
| 6507 computeShowProgress_: function() { | 3618 computeShowProgress_: function() { |
| 6508 return this.showCancel_ && this.data.percent >= -1; | 3619 return this.showCancel_ && this.data.percent >= -1; |
| 6509 }, | 3620 }, |
| 6510 | |
| 6511 /** @private */ | |
| 6512 computeTag_: function() { | 3621 computeTag_: function() { |
| 6513 switch (this.data.state) { | 3622 switch (this.data.state) { |
| 6514 case downloads.States.CANCELLED: | 3623 case downloads.States.CANCELLED: |
| 6515 return loadTimeData.getString('statusCancelled'); | 3624 return loadTimeData.getString('statusCancelled'); |
| 6516 | 3625 |
| 6517 case downloads.States.INTERRUPTED: | 3626 case downloads.States.INTERRUPTED: |
| 6518 return this.data.last_reason_text; | 3627 return this.data.last_reason_text; |
| 6519 | 3628 |
| 6520 case downloads.States.COMPLETE: | 3629 case downloads.States.COMPLETE: |
| 6521 return this.data.file_externally_removed ? | 3630 return this.data.file_externally_removed ? loadTimeData.getString('statu
sRemoved') : ''; |
| 6522 loadTimeData.getString('statusRemoved') : ''; | 3631 } |
| 6523 } | |
| 6524 | |
| 6525 return ''; | 3632 return ''; |
| 6526 }, | 3633 }, |
| 6527 | |
| 6528 /** @private */ | |
| 6529 isIndeterminate_: function() { | 3634 isIndeterminate_: function() { |
| 6530 return this.data.percent == -1; | 3635 return this.data.percent == -1; |
| 6531 }, | 3636 }, |
| 6532 | |
| 6533 /** @private */ | |
| 6534 observeControlledBy_: function() { | 3637 observeControlledBy_: function() { |
| 6535 this.$['controlled-by'].innerHTML = this.controlledBy_; | 3638 this.$['controlled-by'].innerHTML = this.controlledBy_; |
| 6536 }, | 3639 }, |
| 6537 | |
| 6538 /** @private */ | |
| 6539 observeIsDangerous_: function() { | 3640 observeIsDangerous_: function() { |
| 6540 if (!this.data) | 3641 if (!this.data) return; |
| 6541 return; | |
| 6542 | |
| 6543 if (this.isDangerous_) { | 3642 if (this.isDangerous_) { |
| 6544 this.$.url.removeAttribute('href'); | 3643 this.$.url.removeAttribute('href'); |
| 6545 } else { | 3644 } else { |
| 6546 this.$.url.href = assert(this.data.url); | 3645 this.$.url.href = assert(this.data.url); |
| 6547 var filePath = encodeURIComponent(this.data.file_path); | 3646 var filePath = encodeURIComponent(this.data.file_path); |
| 6548 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; | 3647 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; |
| 6549 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; | 3648 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; |
| 6550 } | 3649 } |
| 6551 }, | 3650 }, |
| 6552 | |
| 6553 /** @private */ | |
| 6554 onCancelTap_: function() { | 3651 onCancelTap_: function() { |
| 6555 downloads.ActionService.getInstance().cancel(this.data.id); | 3652 downloads.ActionService.getInstance().cancel(this.data.id); |
| 6556 }, | 3653 }, |
| 6557 | |
| 6558 /** @private */ | |
| 6559 onDiscardDangerousTap_: function() { | 3654 onDiscardDangerousTap_: function() { |
| 6560 downloads.ActionService.getInstance().discardDangerous(this.data.id); | 3655 downloads.ActionService.getInstance().discardDangerous(this.data.id); |
| 6561 }, | 3656 }, |
| 6562 | |
| 6563 /** | |
| 6564 * @private | |
| 6565 * @param {Event} e | |
| 6566 */ | |
| 6567 onDragStart_: function(e) { | 3657 onDragStart_: function(e) { |
| 6568 e.preventDefault(); | 3658 e.preventDefault(); |
| 6569 downloads.ActionService.getInstance().drag(this.data.id); | 3659 downloads.ActionService.getInstance().drag(this.data.id); |
| 6570 }, | 3660 }, |
| 6571 | |
| 6572 /** | |
| 6573 * @param {Event} e | |
| 6574 * @private | |
| 6575 */ | |
| 6576 onFileLinkTap_: function(e) { | 3661 onFileLinkTap_: function(e) { |
| 6577 e.preventDefault(); | 3662 e.preventDefault(); |
| 6578 downloads.ActionService.getInstance().openFile(this.data.id); | 3663 downloads.ActionService.getInstance().openFile(this.data.id); |
| 6579 }, | 3664 }, |
| 6580 | |
| 6581 /** @private */ | |
| 6582 onPauseOrResumeTap_: function() { | 3665 onPauseOrResumeTap_: function() { |
| 6583 if (this.isInProgress_) | 3666 if (this.isInProgress_) downloads.ActionService.getInstance().pause(this.d
ata.id); else downloads.ActionService.getInstance().resume(this.data.id); |
| 6584 downloads.ActionService.getInstance().pause(this.data.id); | 3667 }, |
| 6585 else | |
| 6586 downloads.ActionService.getInstance().resume(this.data.id); | |
| 6587 }, | |
| 6588 | |
| 6589 /** @private */ | |
| 6590 onRemoveTap_: function() { | 3668 onRemoveTap_: function() { |
| 6591 downloads.ActionService.getInstance().remove(this.data.id); | 3669 downloads.ActionService.getInstance().remove(this.data.id); |
| 6592 }, | 3670 }, |
| 6593 | |
| 6594 /** @private */ | |
| 6595 onRetryTap_: function() { | 3671 onRetryTap_: function() { |
| 6596 downloads.ActionService.getInstance().download(this.data.url); | 3672 downloads.ActionService.getInstance().download(this.data.url); |
| 6597 }, | 3673 }, |
| 6598 | |
| 6599 /** @private */ | |
| 6600 onSaveDangerousTap_: function() { | 3674 onSaveDangerousTap_: function() { |
| 6601 downloads.ActionService.getInstance().saveDangerous(this.data.id); | 3675 downloads.ActionService.getInstance().saveDangerous(this.data.id); |
| 6602 }, | 3676 }, |
| 6603 | |
| 6604 /** @private */ | |
| 6605 onShowTap_: function() { | 3677 onShowTap_: function() { |
| 6606 downloads.ActionService.getInstance().show(this.data.id); | 3678 downloads.ActionService.getInstance().show(this.data.id); |
| 6607 }, | 3679 } |
| 6608 }); | 3680 }); |
| 6609 | 3681 return { |
| 6610 return {Item: Item}; | 3682 Item: Item |
| 3683 }; |
| 6611 }); | 3684 }); |
| 6612 /** @polymerBehavior Polymer.PaperItemBehavior */ | 3685 |
| 6613 Polymer.PaperItemBehaviorImpl = { | 3686 Polymer.PaperItemBehaviorImpl = { |
| 6614 hostAttributes: { | 3687 hostAttributes: { |
| 6615 role: 'option', | 3688 role: 'option', |
| 6616 tabindex: '0' | 3689 tabindex: '0' |
| 6617 } | 3690 } |
| 6618 }; | 3691 }; |
| 6619 | 3692 |
| 6620 /** @polymerBehavior */ | 3693 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState,
Polymer.PaperItemBehaviorImpl ]; |
| 6621 Polymer.PaperItemBehavior = [ | 3694 |
| 6622 Polymer.IronButtonState, | |
| 6623 Polymer.IronControlState, | |
| 6624 Polymer.PaperItemBehaviorImpl | |
| 6625 ]; | |
| 6626 Polymer({ | 3695 Polymer({ |
| 6627 is: 'paper-item', | 3696 is: 'paper-item', |
| 6628 | 3697 behaviors: [ Polymer.PaperItemBehavior ] |
| 6629 behaviors: [ | 3698 }); |
| 6630 Polymer.PaperItemBehavior | 3699 |
| 6631 ] | 3700 Polymer.IronSelection = function(selectCallback) { |
| 6632 }); | 3701 this.selection = []; |
| 6633 /** | 3702 this.selectCallback = selectCallback; |
| 6634 * @param {!Function} selectCallback | 3703 }; |
| 6635 * @constructor | 3704 |
| 6636 */ | 3705 Polymer.IronSelection.prototype = { |
| 6637 Polymer.IronSelection = function(selectCallback) { | 3706 get: function() { |
| 6638 this.selection = []; | 3707 return this.multi ? this.selection.slice() : this.selection[0]; |
| 6639 this.selectCallback = selectCallback; | 3708 }, |
| 6640 }; | 3709 clear: function(excludes) { |
| 6641 | 3710 this.selection.slice().forEach(function(item) { |
| 6642 Polymer.IronSelection.prototype = { | 3711 if (!excludes || excludes.indexOf(item) < 0) { |
| 6643 | 3712 this.setItemSelected(item, false); |
| 6644 /** | 3713 } |
| 6645 * Retrieves the selected item(s). | 3714 }, this); |
| 6646 * | 3715 }, |
| 6647 * @method get | 3716 isSelected: function(item) { |
| 6648 * @returns Returns the selected item(s). If the multi property is true, | 3717 return this.selection.indexOf(item) >= 0; |
| 6649 * `get` will return an array, otherwise it will return | 3718 }, |
| 6650 * the selected item or undefined if there is no selection. | 3719 setItemSelected: function(item, isSelected) { |
| 6651 */ | 3720 if (item != null) { |
| 6652 get: function() { | 3721 if (isSelected !== this.isSelected(item)) { |
| 6653 return this.multi ? this.selection.slice() : this.selection[0]; | 3722 if (isSelected) { |
| 6654 }, | 3723 this.selection.push(item); |
| 6655 | 3724 } else { |
| 6656 /** | 3725 var i = this.selection.indexOf(item); |
| 6657 * Clears all the selection except the ones indicated. | 3726 if (i >= 0) { |
| 6658 * | 3727 this.selection.splice(i, 1); |
| 6659 * @method clear | |
| 6660 * @param {Array} excludes items to be excluded. | |
| 6661 */ | |
| 6662 clear: function(excludes) { | |
| 6663 this.selection.slice().forEach(function(item) { | |
| 6664 if (!excludes || excludes.indexOf(item) < 0) { | |
| 6665 this.setItemSelected(item, false); | |
| 6666 } | |
| 6667 }, this); | |
| 6668 }, | |
| 6669 | |
| 6670 /** | |
| 6671 * Indicates if a given item is selected. | |
| 6672 * | |
| 6673 * @method isSelected | |
| 6674 * @param {*} item The item whose selection state should be checked. | |
| 6675 * @returns Returns true if `item` is selected. | |
| 6676 */ | |
| 6677 isSelected: function(item) { | |
| 6678 return this.selection.indexOf(item) >= 0; | |
| 6679 }, | |
| 6680 | |
| 6681 /** | |
| 6682 * Sets the selection state for a given item to either selected or deselecte
d. | |
| 6683 * | |
| 6684 * @method setItemSelected | |
| 6685 * @param {*} item The item to select. | |
| 6686 * @param {boolean} isSelected True for selected, false for deselected. | |
| 6687 */ | |
| 6688 setItemSelected: function(item, isSelected) { | |
| 6689 if (item != null) { | |
| 6690 if (isSelected !== this.isSelected(item)) { | |
| 6691 // proceed to update selection only if requested state differs from cu
rrent | |
| 6692 if (isSelected) { | |
| 6693 this.selection.push(item); | |
| 6694 } else { | |
| 6695 var i = this.selection.indexOf(item); | |
| 6696 if (i >= 0) { | |
| 6697 this.selection.splice(i, 1); | |
| 6698 } | |
| 6699 } | |
| 6700 if (this.selectCallback) { | |
| 6701 this.selectCallback(item, isSelected); | |
| 6702 } | 3728 } |
| 6703 } | 3729 } |
| 6704 } | 3730 if (this.selectCallback) { |
| 6705 }, | 3731 this.selectCallback(item, isSelected); |
| 6706 | |
| 6707 /** | |
| 6708 * Sets the selection state for a given item. If the `multi` property | |
| 6709 * is true, then the selected state of `item` will be toggled; otherwise | |
| 6710 * the `item` will be selected. | |
| 6711 * | |
| 6712 * @method select | |
| 6713 * @param {*} item The item to select. | |
| 6714 */ | |
| 6715 select: function(item) { | |
| 6716 if (this.multi) { | |
| 6717 this.toggle(item); | |
| 6718 } else if (this.get() !== item) { | |
| 6719 this.setItemSelected(this.get(), false); | |
| 6720 this.setItemSelected(item, true); | |
| 6721 } | |
| 6722 }, | |
| 6723 | |
| 6724 /** | |
| 6725 * Toggles the selection state for `item`. | |
| 6726 * | |
| 6727 * @method toggle | |
| 6728 * @param {*} item The item to toggle. | |
| 6729 */ | |
| 6730 toggle: function(item) { | |
| 6731 this.setItemSelected(item, !this.isSelected(item)); | |
| 6732 } | |
| 6733 | |
| 6734 }; | |
| 6735 /** @polymerBehavior */ | |
| 6736 Polymer.IronSelectableBehavior = { | |
| 6737 | |
| 6738 /** | |
| 6739 * Fired when iron-selector is activated (selected or deselected). | |
| 6740 * It is fired before the selected items are changed. | |
| 6741 * Cancel the event to abort selection. | |
| 6742 * | |
| 6743 * @event iron-activate | |
| 6744 */ | |
| 6745 | |
| 6746 /** | |
| 6747 * Fired when an item is selected | |
| 6748 * | |
| 6749 * @event iron-select | |
| 6750 */ | |
| 6751 | |
| 6752 /** | |
| 6753 * Fired when an item is deselected | |
| 6754 * | |
| 6755 * @event iron-deselect | |
| 6756 */ | |
| 6757 | |
| 6758 /** | |
| 6759 * Fired when the list of selectable items changes (e.g., items are | |
| 6760 * added or removed). The detail of the event is a mutation record that | |
| 6761 * describes what changed. | |
| 6762 * | |
| 6763 * @event iron-items-changed | |
| 6764 */ | |
| 6765 | |
| 6766 properties: { | |
| 6767 | |
| 6768 /** | |
| 6769 * If you want to use an attribute value or property of an element for | |
| 6770 * `selected` instead of the index, set this to the name of the attribute | |
| 6771 * or property. Hyphenated values are converted to camel case when used to | |
| 6772 * look up the property of a selectable element. Camel cased values are | |
| 6773 * *not* converted to hyphenated values for attribute lookup. It's | |
| 6774 * recommended that you provide the hyphenated form of the name so that | |
| 6775 * selection works in both cases. (Use `attr-or-property-name` instead of | |
| 6776 * `attrOrPropertyName`.) | |
| 6777 */ | |
| 6778 attrForSelected: { | |
| 6779 type: String, | |
| 6780 value: null | |
| 6781 }, | |
| 6782 | |
| 6783 /** | |
| 6784 * Gets or sets the selected element. The default is to use the index of t
he item. | |
| 6785 * @type {string|number} | |
| 6786 */ | |
| 6787 selected: { | |
| 6788 type: String, | |
| 6789 notify: true | |
| 6790 }, | |
| 6791 | |
| 6792 /** | |
| 6793 * Returns the currently selected item. | |
| 6794 * | |
| 6795 * @type {?Object} | |
| 6796 */ | |
| 6797 selectedItem: { | |
| 6798 type: Object, | |
| 6799 readOnly: true, | |
| 6800 notify: true | |
| 6801 }, | |
| 6802 | |
| 6803 /** | |
| 6804 * The event that fires from items when they are selected. Selectable | |
| 6805 * will listen for this event from items and update the selection state. | |
| 6806 * Set to empty string to listen to no events. | |
| 6807 */ | |
| 6808 activateEvent: { | |
| 6809 type: String, | |
| 6810 value: 'tap', | |
| 6811 observer: '_activateEventChanged' | |
| 6812 }, | |
| 6813 | |
| 6814 /** | |
| 6815 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
| 6816 * are selectable. | |
| 6817 */ | |
| 6818 selectable: String, | |
| 6819 | |
| 6820 /** | |
| 6821 * The class to set on elements when selected. | |
| 6822 */ | |
| 6823 selectedClass: { | |
| 6824 type: String, | |
| 6825 value: 'iron-selected' | |
| 6826 }, | |
| 6827 | |
| 6828 /** | |
| 6829 * The attribute to set on elements when selected. | |
| 6830 */ | |
| 6831 selectedAttribute: { | |
| 6832 type: String, | |
| 6833 value: null | |
| 6834 }, | |
| 6835 | |
| 6836 /** | |
| 6837 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
| 6838 * is not found. | |
| 6839 */ | |
| 6840 fallbackSelection: { | |
| 6841 type: String, | |
| 6842 value: null | |
| 6843 }, | |
| 6844 | |
| 6845 /** | |
| 6846 * The list of items from which a selection can be made. | |
| 6847 */ | |
| 6848 items: { | |
| 6849 type: Array, | |
| 6850 readOnly: true, | |
| 6851 notify: true, | |
| 6852 value: function() { | |
| 6853 return []; | |
| 6854 } | 3732 } |
| 6855 }, | 3733 } |
| 6856 | 3734 } |
| 6857 /** | 3735 }, |
| 6858 * The set of excluded elements where the key is the `localName` | 3736 select: function(item) { |
| 6859 * of the element that will be ignored from the item list. | 3737 if (this.multi) { |
| 6860 * | 3738 this.toggle(item); |
| 6861 * @default {template: 1} | 3739 } else if (this.get() !== item) { |
| 6862 */ | 3740 this.setItemSelected(this.get(), false); |
| 6863 _excludedLocalNames: { | 3741 this.setItemSelected(item, true); |
| 6864 type: Object, | 3742 } |
| 6865 value: function() { | 3743 }, |
| 6866 return { | 3744 toggle: function(item) { |
| 6867 'template': 1 | 3745 this.setItemSelected(item, !this.isSelected(item)); |
| 6868 }; | 3746 } |
| 3747 }; |
| 3748 |
| 3749 Polymer.IronSelectableBehavior = { |
| 3750 properties: { |
| 3751 attrForSelected: { |
| 3752 type: String, |
| 3753 value: null |
| 3754 }, |
| 3755 selected: { |
| 3756 type: String, |
| 3757 notify: true |
| 3758 }, |
| 3759 selectedItem: { |
| 3760 type: Object, |
| 3761 readOnly: true, |
| 3762 notify: true |
| 3763 }, |
| 3764 activateEvent: { |
| 3765 type: String, |
| 3766 value: 'tap', |
| 3767 observer: '_activateEventChanged' |
| 3768 }, |
| 3769 selectable: String, |
| 3770 selectedClass: { |
| 3771 type: String, |
| 3772 value: 'iron-selected' |
| 3773 }, |
| 3774 selectedAttribute: { |
| 3775 type: String, |
| 3776 value: null |
| 3777 }, |
| 3778 fallbackSelection: { |
| 3779 type: String, |
| 3780 value: null |
| 3781 }, |
| 3782 items: { |
| 3783 type: Array, |
| 3784 readOnly: true, |
| 3785 notify: true, |
| 3786 value: function() { |
| 3787 return []; |
| 3788 } |
| 3789 }, |
| 3790 _excludedLocalNames: { |
| 3791 type: Object, |
| 3792 value: function() { |
| 3793 return { |
| 3794 template: 1 |
| 3795 }; |
| 3796 } |
| 3797 } |
| 3798 }, |
| 3799 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec
ted)', '_checkFallback(fallbackSelection)' ], |
| 3800 created: function() { |
| 3801 this._bindFilterItem = this._filterItem.bind(this); |
| 3802 this._selection = new Polymer.IronSelection(this._applySelection.bind(this))
; |
| 3803 }, |
| 3804 attached: function() { |
| 3805 this._observer = this._observeItems(this); |
| 3806 this._updateItems(); |
| 3807 if (!this._shouldUpdateSelection) { |
| 3808 this._updateSelected(); |
| 3809 } |
| 3810 this._addListener(this.activateEvent); |
| 3811 }, |
| 3812 detached: function() { |
| 3813 if (this._observer) { |
| 3814 Polymer.dom(this).unobserveNodes(this._observer); |
| 3815 } |
| 3816 this._removeListener(this.activateEvent); |
| 3817 }, |
| 3818 indexOf: function(item) { |
| 3819 return this.items.indexOf(item); |
| 3820 }, |
| 3821 select: function(value) { |
| 3822 this.selected = value; |
| 3823 }, |
| 3824 selectPrevious: function() { |
| 3825 var length = this.items.length; |
| 3826 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt
h; |
| 3827 this.selected = this._indexToValue(index); |
| 3828 }, |
| 3829 selectNext: function() { |
| 3830 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len
gth; |
| 3831 this.selected = this._indexToValue(index); |
| 3832 }, |
| 3833 selectIndex: function(index) { |
| 3834 this.select(this._indexToValue(index)); |
| 3835 }, |
| 3836 forceSynchronousItemUpdate: function() { |
| 3837 this._updateItems(); |
| 3838 }, |
| 3839 get _shouldUpdateSelection() { |
| 3840 return this.selected != null; |
| 3841 }, |
| 3842 _checkFallback: function() { |
| 3843 if (this._shouldUpdateSelection) { |
| 3844 this._updateSelected(); |
| 3845 } |
| 3846 }, |
| 3847 _addListener: function(eventName) { |
| 3848 this.listen(this, eventName, '_activateHandler'); |
| 3849 }, |
| 3850 _removeListener: function(eventName) { |
| 3851 this.unlisten(this, eventName, '_activateHandler'); |
| 3852 }, |
| 3853 _activateEventChanged: function(eventName, old) { |
| 3854 this._removeListener(old); |
| 3855 this._addListener(eventName); |
| 3856 }, |
| 3857 _updateItems: function() { |
| 3858 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*
'); |
| 3859 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 3860 this._setItems(nodes); |
| 3861 }, |
| 3862 _updateAttrForSelected: function() { |
| 3863 if (this._shouldUpdateSelection) { |
| 3864 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 3865 } |
| 3866 }, |
| 3867 _updateSelected: function() { |
| 3868 this._selectSelected(this.selected); |
| 3869 }, |
| 3870 _selectSelected: function(selected) { |
| 3871 this._selection.select(this._valueToItem(this.selected)); |
| 3872 if (this.fallbackSelection && this.items.length && this._selection.get() ===
undefined) { |
| 3873 this.selected = this.fallbackSelection; |
| 3874 } |
| 3875 }, |
| 3876 _filterItem: function(node) { |
| 3877 return !this._excludedLocalNames[node.localName]; |
| 3878 }, |
| 3879 _valueToItem: function(value) { |
| 3880 return value == null ? null : this.items[this._valueToIndex(value)]; |
| 3881 }, |
| 3882 _valueToIndex: function(value) { |
| 3883 if (this.attrForSelected) { |
| 3884 for (var i = 0, item; item = this.items[i]; i++) { |
| 3885 if (this._valueForItem(item) == value) { |
| 3886 return i; |
| 6869 } | 3887 } |
| 6870 } | 3888 } |
| 6871 }, | 3889 } else { |
| 6872 | 3890 return Number(value); |
| 6873 observers: [ | 3891 } |
| 6874 '_updateAttrForSelected(attrForSelected)', | 3892 }, |
| 6875 '_updateSelected(selected)', | 3893 _indexToValue: function(index) { |
| 6876 '_checkFallback(fallbackSelection)' | 3894 if (this.attrForSelected) { |
| 6877 ], | 3895 var item = this.items[index]; |
| 6878 | 3896 if (item) { |
| 6879 created: function() { | 3897 return this._valueForItem(item); |
| 6880 this._bindFilterItem = this._filterItem.bind(this); | 3898 } |
| 6881 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | 3899 } else { |
| 6882 }, | 3900 return index; |
| 6883 | 3901 } |
| 6884 attached: function() { | 3902 }, |
| 6885 this._observer = this._observeItems(this); | 3903 _valueForItem: function(item) { |
| 3904 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; |
| 3905 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe
lected); |
| 3906 }, |
| 3907 _applySelection: function(item, isSelected) { |
| 3908 if (this.selectedClass) { |
| 3909 this.toggleClass(this.selectedClass, isSelected, item); |
| 3910 } |
| 3911 if (this.selectedAttribute) { |
| 3912 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 3913 } |
| 3914 this._selectionChange(); |
| 3915 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), { |
| 3916 item: item |
| 3917 }); |
| 3918 }, |
| 3919 _selectionChange: function() { |
| 3920 this._setSelectedItem(this._selection.get()); |
| 3921 }, |
| 3922 _observeItems: function(node) { |
| 3923 return Polymer.dom(node).observeNodes(function(mutation) { |
| 6886 this._updateItems(); | 3924 this._updateItems(); |
| 6887 if (!this._shouldUpdateSelection) { | |
| 6888 this._updateSelected(); | |
| 6889 } | |
| 6890 this._addListener(this.activateEvent); | |
| 6891 }, | |
| 6892 | |
| 6893 detached: function() { | |
| 6894 if (this._observer) { | |
| 6895 Polymer.dom(this).unobserveNodes(this._observer); | |
| 6896 } | |
| 6897 this._removeListener(this.activateEvent); | |
| 6898 }, | |
| 6899 | |
| 6900 /** | |
| 6901 * Returns the index of the given item. | |
| 6902 * | |
| 6903 * @method indexOf | |
| 6904 * @param {Object} item | |
| 6905 * @returns Returns the index of the item | |
| 6906 */ | |
| 6907 indexOf: function(item) { | |
| 6908 return this.items.indexOf(item); | |
| 6909 }, | |
| 6910 | |
| 6911 /** | |
| 6912 * Selects the given value. | |
| 6913 * | |
| 6914 * @method select | |
| 6915 * @param {string|number} value the value to select. | |
| 6916 */ | |
| 6917 select: function(value) { | |
| 6918 this.selected = value; | |
| 6919 }, | |
| 6920 | |
| 6921 /** | |
| 6922 * Selects the previous item. | |
| 6923 * | |
| 6924 * @method selectPrevious | |
| 6925 */ | |
| 6926 selectPrevious: function() { | |
| 6927 var length = this.items.length; | |
| 6928 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | |
| 6929 this.selected = this._indexToValue(index); | |
| 6930 }, | |
| 6931 | |
| 6932 /** | |
| 6933 * Selects the next item. | |
| 6934 * | |
| 6935 * @method selectNext | |
| 6936 */ | |
| 6937 selectNext: function() { | |
| 6938 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | |
| 6939 this.selected = this._indexToValue(index); | |
| 6940 }, | |
| 6941 | |
| 6942 /** | |
| 6943 * Selects the item at the given index. | |
| 6944 * | |
| 6945 * @method selectIndex | |
| 6946 */ | |
| 6947 selectIndex: function(index) { | |
| 6948 this.select(this._indexToValue(index)); | |
| 6949 }, | |
| 6950 | |
| 6951 /** | |
| 6952 * Force a synchronous update of the `items` property. | |
| 6953 * | |
| 6954 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
| 6955 * updates to the set of selectable items after updates to the DOM list and | |
| 6956 * selection state have been made. | |
| 6957 * | |
| 6958 * WARNING: If you are using this method, you should probably consider an | |
| 6959 * alternate approach. Synchronously querying for items is potentially | |
| 6960 * slow for many use cases. The `items` property will update asynchronously | |
| 6961 * on its own to reflect selectable items in the DOM. | |
| 6962 */ | |
| 6963 forceSynchronousItemUpdate: function() { | |
| 6964 this._updateItems(); | |
| 6965 }, | |
| 6966 | |
| 6967 get _shouldUpdateSelection() { | |
| 6968 return this.selected != null; | |
| 6969 }, | |
| 6970 | |
| 6971 _checkFallback: function() { | |
| 6972 if (this._shouldUpdateSelection) { | 3925 if (this._shouldUpdateSelection) { |
| 6973 this._updateSelected(); | 3926 this._updateSelected(); |
| 6974 } | 3927 } |
| 6975 }, | 3928 this.fire('iron-items-changed', mutation, { |
| 6976 | 3929 bubbles: false, |
| 6977 _addListener: function(eventName) { | 3930 cancelable: false |
| 6978 this.listen(this, eventName, '_activateHandler'); | 3931 }); |
| 6979 }, | 3932 }); |
| 6980 | 3933 }, |
| 6981 _removeListener: function(eventName) { | 3934 _activateHandler: function(e) { |
| 6982 this.unlisten(this, eventName, '_activateHandler'); | 3935 var t = e.target; |
| 6983 }, | 3936 var items = this.items; |
| 6984 | 3937 while (t && t != this) { |
| 6985 _activateEventChanged: function(eventName, old) { | 3938 var i = items.indexOf(t); |
| 6986 this._removeListener(old); | 3939 if (i >= 0) { |
| 6987 this._addListener(eventName); | 3940 var value = this._indexToValue(i); |
| 6988 }, | 3941 this._itemActivate(value, t); |
| 6989 | 3942 return; |
| 6990 _updateItems: function() { | 3943 } |
| 6991 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | 3944 t = t.parentNode; |
| 6992 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | 3945 } |
| 6993 this._setItems(nodes); | 3946 }, |
| 6994 }, | 3947 _itemActivate: function(value, item) { |
| 6995 | 3948 if (!this.fire('iron-activate', { |
| 6996 _updateAttrForSelected: function() { | 3949 selected: value, |
| 6997 if (this._shouldUpdateSelection) { | 3950 item: item |
| 6998 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | 3951 }, { |
| 6999 } | 3952 cancelable: true |
| 7000 }, | 3953 }).defaultPrevented) { |
| 7001 | 3954 this.select(value); |
| 7002 _updateSelected: function() { | 3955 } |
| 3956 } |
| 3957 }; |
| 3958 |
| 3959 Polymer.IronMultiSelectableBehaviorImpl = { |
| 3960 properties: { |
| 3961 multi: { |
| 3962 type: Boolean, |
| 3963 value: false, |
| 3964 observer: 'multiChanged' |
| 3965 }, |
| 3966 selectedValues: { |
| 3967 type: Array, |
| 3968 notify: true |
| 3969 }, |
| 3970 selectedItems: { |
| 3971 type: Array, |
| 3972 readOnly: true, |
| 3973 notify: true |
| 3974 } |
| 3975 }, |
| 3976 observers: [ '_updateSelected(selectedValues.splices)' ], |
| 3977 select: function(value) { |
| 3978 if (this.multi) { |
| 3979 if (this.selectedValues) { |
| 3980 this._toggleSelected(value); |
| 3981 } else { |
| 3982 this.selectedValues = [ value ]; |
| 3983 } |
| 3984 } else { |
| 3985 this.selected = value; |
| 3986 } |
| 3987 }, |
| 3988 multiChanged: function(multi) { |
| 3989 this._selection.multi = multi; |
| 3990 }, |
| 3991 get _shouldUpdateSelection() { |
| 3992 return this.selected != null || this.selectedValues != null && this.selected
Values.length; |
| 3993 }, |
| 3994 _updateAttrForSelected: function() { |
| 3995 if (!this.multi) { |
| 3996 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 3997 } else if (this._shouldUpdateSelection) { |
| 3998 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 3999 return this._indexToValue(this.indexOf(selectedItem)); |
| 4000 }, this).filter(function(unfilteredValue) { |
| 4001 return unfilteredValue != null; |
| 4002 }, this); |
| 4003 } |
| 4004 }, |
| 4005 _updateSelected: function() { |
| 4006 if (this.multi) { |
| 4007 this._selectMulti(this.selectedValues); |
| 4008 } else { |
| 7003 this._selectSelected(this.selected); | 4009 this._selectSelected(this.selected); |
| 7004 }, | 4010 } |
| 7005 | 4011 }, |
| 7006 _selectSelected: function(selected) { | 4012 _selectMulti: function(values) { |
| 7007 this._selection.select(this._valueToItem(this.selected)); | 4013 if (values) { |
| 7008 // Check for items, since this array is populated only when attached | 4014 var selectedItems = this._valuesToItems(values); |
| 7009 // Since Number(0) is falsy, explicitly check for undefined | 4015 this._selection.clear(selectedItems); |
| 7010 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | 4016 for (var i = 0; i < selectedItems.length; i++) { |
| 7011 this.selected = this.fallbackSelection; | 4017 this._selection.setItemSelected(selectedItems[i], true); |
| 7012 } | 4018 } |
| 7013 }, | 4019 if (this.fallbackSelection && this.items.length && !this._selection.get().
length) { |
| 7014 | 4020 var fallback = this._valueToItem(this.fallbackSelection); |
| 7015 _filterItem: function(node) { | 4021 if (fallback) { |
| 7016 return !this._excludedLocalNames[node.localName]; | 4022 this.selectedValues = [ this.fallbackSelection ]; |
| 7017 }, | |
| 7018 | |
| 7019 _valueToItem: function(value) { | |
| 7020 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
| 7021 }, | |
| 7022 | |
| 7023 _valueToIndex: function(value) { | |
| 7024 if (this.attrForSelected) { | |
| 7025 for (var i = 0, item; item = this.items[i]; i++) { | |
| 7026 if (this._valueForItem(item) == value) { | |
| 7027 return i; | |
| 7028 } | |
| 7029 } | 4023 } |
| 4024 } |
| 4025 } else { |
| 4026 this._selection.clear(); |
| 4027 } |
| 4028 }, |
| 4029 _selectionChange: function() { |
| 4030 var s = this._selection.get(); |
| 4031 if (this.multi) { |
| 4032 this._setSelectedItems(s); |
| 4033 } else { |
| 4034 this._setSelectedItems([ s ]); |
| 4035 this._setSelectedItem(s); |
| 4036 } |
| 4037 }, |
| 4038 _toggleSelected: function(value) { |
| 4039 var i = this.selectedValues.indexOf(value); |
| 4040 var unselected = i < 0; |
| 4041 if (unselected) { |
| 4042 this.push('selectedValues', value); |
| 4043 } else { |
| 4044 this.splice('selectedValues', i, 1); |
| 4045 } |
| 4046 }, |
| 4047 _valuesToItems: function(values) { |
| 4048 return values == null ? null : values.map(function(value) { |
| 4049 return this._valueToItem(value); |
| 4050 }, this); |
| 4051 } |
| 4052 }; |
| 4053 |
| 4054 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer.
IronMultiSelectableBehaviorImpl ]; |
| 4055 |
| 4056 Polymer.IronMenuBehaviorImpl = { |
| 4057 properties: { |
| 4058 focusedItem: { |
| 4059 observer: '_focusedItemChanged', |
| 4060 readOnly: true, |
| 4061 type: Object |
| 4062 }, |
| 4063 attrForItemTitle: { |
| 4064 type: String |
| 4065 } |
| 4066 }, |
| 4067 hostAttributes: { |
| 4068 role: 'menu', |
| 4069 tabindex: '0' |
| 4070 }, |
| 4071 observers: [ '_updateMultiselectable(multi)' ], |
| 4072 listeners: { |
| 4073 focus: '_onFocus', |
| 4074 keydown: '_onKeydown', |
| 4075 'iron-items-changed': '_onIronItemsChanged' |
| 4076 }, |
| 4077 keyBindings: { |
| 4078 up: '_onUpKey', |
| 4079 down: '_onDownKey', |
| 4080 esc: '_onEscKey', |
| 4081 'shift+tab:keydown': '_onShiftTabDown' |
| 4082 }, |
| 4083 attached: function() { |
| 4084 this._resetTabindices(); |
| 4085 }, |
| 4086 select: function(value) { |
| 4087 if (this._defaultFocusAsync) { |
| 4088 this.cancelAsync(this._defaultFocusAsync); |
| 4089 this._defaultFocusAsync = null; |
| 4090 } |
| 4091 var item = this._valueToItem(value); |
| 4092 if (item && item.hasAttribute('disabled')) return; |
| 4093 this._setFocusedItem(item); |
| 4094 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| 4095 }, |
| 4096 _resetTabindices: function() { |
| 4097 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0]
: this.selectedItem; |
| 4098 this.items.forEach(function(item) { |
| 4099 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| 4100 }, this); |
| 4101 }, |
| 4102 _updateMultiselectable: function(multi) { |
| 4103 if (multi) { |
| 4104 this.setAttribute('aria-multiselectable', 'true'); |
| 4105 } else { |
| 4106 this.removeAttribute('aria-multiselectable'); |
| 4107 } |
| 4108 }, |
| 4109 _focusWithKeyboardEvent: function(event) { |
| 4110 for (var i = 0, item; item = this.items[i]; i++) { |
| 4111 var attr = this.attrForItemTitle || 'textContent'; |
| 4112 var title = item[attr] || item.getAttribute(attr); |
| 4113 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo
werCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| 4114 this._setFocusedItem(item); |
| 4115 break; |
| 4116 } |
| 4117 } |
| 4118 }, |
| 4119 _focusPrevious: function() { |
| 4120 var length = this.items.length; |
| 4121 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4122 for (var i = 1; i < length + 1; i++) { |
| 4123 var item = this.items[(curFocusIndex - i + length) % length]; |
| 4124 if (!item.hasAttribute('disabled')) { |
| 4125 this._setFocusedItem(item); |
| 4126 return; |
| 4127 } |
| 4128 } |
| 4129 }, |
| 4130 _focusNext: function() { |
| 4131 var length = this.items.length; |
| 4132 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 4133 for (var i = 1; i < length + 1; i++) { |
| 4134 var item = this.items[(curFocusIndex + i) % length]; |
| 4135 if (!item.hasAttribute('disabled')) { |
| 4136 this._setFocusedItem(item); |
| 4137 return; |
| 4138 } |
| 4139 } |
| 4140 }, |
| 4141 _applySelection: function(item, isSelected) { |
| 4142 if (isSelected) { |
| 4143 item.setAttribute('aria-selected', 'true'); |
| 4144 } else { |
| 4145 item.removeAttribute('aria-selected'); |
| 4146 } |
| 4147 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| 4148 }, |
| 4149 _focusedItemChanged: function(focusedItem, old) { |
| 4150 old && old.setAttribute('tabindex', '-1'); |
| 4151 if (focusedItem) { |
| 4152 focusedItem.setAttribute('tabindex', '0'); |
| 4153 focusedItem.focus(); |
| 4154 } |
| 4155 }, |
| 4156 _onIronItemsChanged: function(event) { |
| 4157 if (event.detail.addedNodes.length) { |
| 4158 this._resetTabindices(); |
| 4159 } |
| 4160 }, |
| 4161 _onShiftTabDown: function(event) { |
| 4162 var oldTabIndex = this.getAttribute('tabindex'); |
| 4163 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| 4164 this._setFocusedItem(null); |
| 4165 this.setAttribute('tabindex', '-1'); |
| 4166 this.async(function() { |
| 4167 this.setAttribute('tabindex', oldTabIndex); |
| 4168 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4169 }, 1); |
| 4170 }, |
| 4171 _onFocus: function(event) { |
| 4172 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| 4173 return; |
| 4174 } |
| 4175 var rootTarget = Polymer.dom(event).rootTarget; |
| 4176 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th
is.isLightDescendant(rootTarget)) { |
| 4177 return; |
| 4178 } |
| 4179 this._defaultFocusAsync = this.async(function() { |
| 4180 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0
] : this.selectedItem; |
| 4181 this._setFocusedItem(null); |
| 4182 if (selectedItem) { |
| 4183 this._setFocusedItem(selectedItem); |
| 4184 } else if (this.items[0]) { |
| 4185 this._focusNext(); |
| 4186 } |
| 4187 }); |
| 4188 }, |
| 4189 _onUpKey: function(event) { |
| 4190 this._focusPrevious(); |
| 4191 event.detail.keyboardEvent.preventDefault(); |
| 4192 }, |
| 4193 _onDownKey: function(event) { |
| 4194 this._focusNext(); |
| 4195 event.detail.keyboardEvent.preventDefault(); |
| 4196 }, |
| 4197 _onEscKey: function(event) { |
| 4198 this.focusedItem.blur(); |
| 4199 }, |
| 4200 _onKeydown: function(event) { |
| 4201 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| 4202 this._focusWithKeyboardEvent(event); |
| 4203 } |
| 4204 event.stopPropagation(); |
| 4205 }, |
| 4206 _activateHandler: function(event) { |
| 4207 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| 4208 event.stopPropagation(); |
| 4209 } |
| 4210 }; |
| 4211 |
| 4212 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4213 |
| 4214 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1
1yKeysBehavior, Polymer.IronMenuBehaviorImpl ]; |
| 4215 |
| 4216 (function() { |
| 4217 Polymer({ |
| 4218 is: 'paper-menu', |
| 4219 behaviors: [ Polymer.IronMenuBehavior ] |
| 4220 }); |
| 4221 })(); |
| 4222 |
| 4223 Polymer.IronFitBehavior = { |
| 4224 properties: { |
| 4225 sizingTarget: { |
| 4226 type: Object, |
| 4227 value: function() { |
| 4228 return this; |
| 4229 } |
| 4230 }, |
| 4231 fitInto: { |
| 4232 type: Object, |
| 4233 value: window |
| 4234 }, |
| 4235 noOverlap: { |
| 4236 type: Boolean |
| 4237 }, |
| 4238 positionTarget: { |
| 4239 type: Element |
| 4240 }, |
| 4241 horizontalAlign: { |
| 4242 type: String |
| 4243 }, |
| 4244 verticalAlign: { |
| 4245 type: String |
| 4246 }, |
| 4247 dynamicAlign: { |
| 4248 type: Boolean |
| 4249 }, |
| 4250 horizontalOffset: { |
| 4251 type: Number, |
| 4252 value: 0, |
| 4253 notify: true |
| 4254 }, |
| 4255 verticalOffset: { |
| 4256 type: Number, |
| 4257 value: 0, |
| 4258 notify: true |
| 4259 }, |
| 4260 autoFitOnAttach: { |
| 4261 type: Boolean, |
| 4262 value: false |
| 4263 }, |
| 4264 _fitInfo: { |
| 4265 type: Object |
| 4266 } |
| 4267 }, |
| 4268 get _fitWidth() { |
| 4269 var fitWidth; |
| 4270 if (this.fitInto === window) { |
| 4271 fitWidth = this.fitInto.innerWidth; |
| 4272 } else { |
| 4273 fitWidth = this.fitInto.getBoundingClientRect().width; |
| 4274 } |
| 4275 return fitWidth; |
| 4276 }, |
| 4277 get _fitHeight() { |
| 4278 var fitHeight; |
| 4279 if (this.fitInto === window) { |
| 4280 fitHeight = this.fitInto.innerHeight; |
| 4281 } else { |
| 4282 fitHeight = this.fitInto.getBoundingClientRect().height; |
| 4283 } |
| 4284 return fitHeight; |
| 4285 }, |
| 4286 get _fitLeft() { |
| 4287 var fitLeft; |
| 4288 if (this.fitInto === window) { |
| 4289 fitLeft = 0; |
| 4290 } else { |
| 4291 fitLeft = this.fitInto.getBoundingClientRect().left; |
| 4292 } |
| 4293 return fitLeft; |
| 4294 }, |
| 4295 get _fitTop() { |
| 4296 var fitTop; |
| 4297 if (this.fitInto === window) { |
| 4298 fitTop = 0; |
| 4299 } else { |
| 4300 fitTop = this.fitInto.getBoundingClientRect().top; |
| 4301 } |
| 4302 return fitTop; |
| 4303 }, |
| 4304 get _defaultPositionTarget() { |
| 4305 var parent = Polymer.dom(this).parentNode; |
| 4306 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 4307 parent = parent.host; |
| 4308 } |
| 4309 return parent; |
| 4310 }, |
| 4311 get _localeHorizontalAlign() { |
| 4312 if (this._isRTL) { |
| 4313 if (this.horizontalAlign === 'right') { |
| 4314 return 'left'; |
| 4315 } |
| 4316 if (this.horizontalAlign === 'left') { |
| 4317 return 'right'; |
| 4318 } |
| 4319 } |
| 4320 return this.horizontalAlign; |
| 4321 }, |
| 4322 attached: function() { |
| 4323 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
| 4324 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
| 4325 if (this.autoFitOnAttach) { |
| 4326 if (window.getComputedStyle(this).display === 'none') { |
| 4327 setTimeout(function() { |
| 4328 this.fit(); |
| 4329 }.bind(this)); |
| 7030 } else { | 4330 } else { |
| 7031 return Number(value); | 4331 this.fit(); |
| 7032 } | 4332 } |
| 7033 }, | 4333 } |
| 7034 | 4334 }, |
| 7035 _indexToValue: function(index) { | 4335 fit: function() { |
| 7036 if (this.attrForSelected) { | 4336 this.position(); |
| 7037 var item = this.items[index]; | 4337 this.constrain(); |
| 7038 if (item) { | 4338 this.center(); |
| 7039 return this._valueForItem(item); | 4339 }, |
| 4340 _discoverInfo: function() { |
| 4341 if (this._fitInfo) { |
| 4342 return; |
| 4343 } |
| 4344 var target = window.getComputedStyle(this); |
| 4345 var sizer = window.getComputedStyle(this.sizingTarget); |
| 4346 this._fitInfo = { |
| 4347 inlineStyle: { |
| 4348 top: this.style.top || '', |
| 4349 left: this.style.left || '', |
| 4350 position: this.style.position || '' |
| 4351 }, |
| 4352 sizerInlineStyle: { |
| 4353 maxWidth: this.sizingTarget.style.maxWidth || '', |
| 4354 maxHeight: this.sizingTarget.style.maxHeight || '', |
| 4355 boxSizing: this.sizingTarget.style.boxSizing || '' |
| 4356 }, |
| 4357 positionedBy: { |
| 4358 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? '
bottom' : null, |
| 4359 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto'
? 'right' : null |
| 4360 }, |
| 4361 sizedBy: { |
| 4362 height: sizer.maxHeight !== 'none', |
| 4363 width: sizer.maxWidth !== 'none', |
| 4364 minWidth: parseInt(sizer.minWidth, 10) || 0, |
| 4365 minHeight: parseInt(sizer.minHeight, 10) || 0 |
| 4366 }, |
| 4367 margin: { |
| 4368 top: parseInt(target.marginTop, 10) || 0, |
| 4369 right: parseInt(target.marginRight, 10) || 0, |
| 4370 bottom: parseInt(target.marginBottom, 10) || 0, |
| 4371 left: parseInt(target.marginLeft, 10) || 0 |
| 4372 } |
| 4373 }; |
| 4374 if (this.verticalOffset) { |
| 4375 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs
et; |
| 4376 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
| 4377 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
| 4378 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px
'; |
| 4379 } |
| 4380 if (this.horizontalOffset) { |
| 4381 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf
fset; |
| 4382 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
| 4383 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
| 4384 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + '
px'; |
| 4385 } |
| 4386 }, |
| 4387 resetFit: function() { |
| 4388 var info = this._fitInfo || {}; |
| 4389 for (var property in info.sizerInlineStyle) { |
| 4390 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
| 4391 } |
| 4392 for (var property in info.inlineStyle) { |
| 4393 this.style[property] = info.inlineStyle[property]; |
| 4394 } |
| 4395 this._fitInfo = null; |
| 4396 }, |
| 4397 refit: function() { |
| 4398 var scrollLeft = this.sizingTarget.scrollLeft; |
| 4399 var scrollTop = this.sizingTarget.scrollTop; |
| 4400 this.resetFit(); |
| 4401 this.fit(); |
| 4402 this.sizingTarget.scrollLeft = scrollLeft; |
| 4403 this.sizingTarget.scrollTop = scrollTop; |
| 4404 }, |
| 4405 position: function() { |
| 4406 if (!this.horizontalAlign && !this.verticalAlign) { |
| 4407 return; |
| 4408 } |
| 4409 this._discoverInfo(); |
| 4410 this.style.position = 'fixed'; |
| 4411 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4412 this.style.left = '0px'; |
| 4413 this.style.top = '0px'; |
| 4414 var rect = this.getBoundingClientRect(); |
| 4415 var positionRect = this.__getNormalizedRect(this.positionTarget); |
| 4416 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4417 var margin = this._fitInfo.margin; |
| 4418 var size = { |
| 4419 width: rect.width + margin.left + margin.right, |
| 4420 height: rect.height + margin.top + margin.bottom |
| 4421 }; |
| 4422 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical
Align, size, positionRect, fitRect); |
| 4423 var left = position.left + margin.left; |
| 4424 var top = position.top + margin.top; |
| 4425 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
| 4426 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
| 4427 var minWidth = this._fitInfo.sizedBy.minWidth; |
| 4428 var minHeight = this._fitInfo.sizedBy.minHeight; |
| 4429 if (left < margin.left) { |
| 4430 left = margin.left; |
| 4431 if (right - left < minWidth) { |
| 4432 left = right - minWidth; |
| 4433 } |
| 4434 } |
| 4435 if (top < margin.top) { |
| 4436 top = margin.top; |
| 4437 if (bottom - top < minHeight) { |
| 4438 top = bottom - minHeight; |
| 4439 } |
| 4440 } |
| 4441 this.sizingTarget.style.maxWidth = right - left + 'px'; |
| 4442 this.sizingTarget.style.maxHeight = bottom - top + 'px'; |
| 4443 this.style.left = left - rect.left + 'px'; |
| 4444 this.style.top = top - rect.top + 'px'; |
| 4445 }, |
| 4446 constrain: function() { |
| 4447 if (this.horizontalAlign || this.verticalAlign) { |
| 4448 return; |
| 4449 } |
| 4450 this._discoverInfo(); |
| 4451 var info = this._fitInfo; |
| 4452 if (!info.positionedBy.vertically) { |
| 4453 this.style.position = 'fixed'; |
| 4454 this.style.top = '0px'; |
| 4455 } |
| 4456 if (!info.positionedBy.horizontally) { |
| 4457 this.style.position = 'fixed'; |
| 4458 this.style.left = '0px'; |
| 4459 } |
| 4460 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4461 var rect = this.getBoundingClientRect(); |
| 4462 if (!info.sizedBy.height) { |
| 4463 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom',
'Height'); |
| 4464 } |
| 4465 if (!info.sizedBy.width) { |
| 4466 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right'
, 'Width'); |
| 4467 } |
| 4468 }, |
| 4469 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4470 this.__sizeDimension(rect, positionedBy, start, end, extent); |
| 4471 }, |
| 4472 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4473 var info = this._fitInfo; |
| 4474 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4475 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
| 4476 var flip = positionedBy === end; |
| 4477 var offset = flip ? max - rect[end] : rect[start]; |
| 4478 var margin = info.margin[flip ? start : end]; |
| 4479 var offsetExtent = 'offset' + extent; |
| 4480 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
| 4481 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs
et + 'px'; |
| 4482 }, |
| 4483 center: function() { |
| 4484 if (this.horizontalAlign || this.verticalAlign) { |
| 4485 return; |
| 4486 } |
| 4487 this._discoverInfo(); |
| 4488 var positionedBy = this._fitInfo.positionedBy; |
| 4489 if (positionedBy.vertically && positionedBy.horizontally) { |
| 4490 return; |
| 4491 } |
| 4492 this.style.position = 'fixed'; |
| 4493 if (!positionedBy.vertically) { |
| 4494 this.style.top = '0px'; |
| 4495 } |
| 4496 if (!positionedBy.horizontally) { |
| 4497 this.style.left = '0px'; |
| 4498 } |
| 4499 var rect = this.getBoundingClientRect(); |
| 4500 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4501 if (!positionedBy.vertically) { |
| 4502 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
| 4503 this.style.top = top + 'px'; |
| 4504 } |
| 4505 if (!positionedBy.horizontally) { |
| 4506 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
| 4507 this.style.left = left + 'px'; |
| 4508 } |
| 4509 }, |
| 4510 __getNormalizedRect: function(target) { |
| 4511 if (target === document.documentElement || target === window) { |
| 4512 return { |
| 4513 top: 0, |
| 4514 left: 0, |
| 4515 width: window.innerWidth, |
| 4516 height: window.innerHeight, |
| 4517 right: window.innerWidth, |
| 4518 bottom: window.innerHeight |
| 4519 }; |
| 4520 } |
| 4521 return target.getBoundingClientRect(); |
| 4522 }, |
| 4523 __getCroppedArea: function(position, size, fitRect) { |
| 4524 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom -
(position.top + size.height)); |
| 4525 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right
- (position.left + size.width)); |
| 4526 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size
.height; |
| 4527 }, |
| 4528 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
| 4529 var positions = [ { |
| 4530 verticalAlign: 'top', |
| 4531 horizontalAlign: 'left', |
| 4532 top: positionRect.top, |
| 4533 left: positionRect.left |
| 4534 }, { |
| 4535 verticalAlign: 'top', |
| 4536 horizontalAlign: 'right', |
| 4537 top: positionRect.top, |
| 4538 left: positionRect.right - size.width |
| 4539 }, { |
| 4540 verticalAlign: 'bottom', |
| 4541 horizontalAlign: 'left', |
| 4542 top: positionRect.bottom - size.height, |
| 4543 left: positionRect.left |
| 4544 }, { |
| 4545 verticalAlign: 'bottom', |
| 4546 horizontalAlign: 'right', |
| 4547 top: positionRect.bottom - size.height, |
| 4548 left: positionRect.right - size.width |
| 4549 } ]; |
| 4550 if (this.noOverlap) { |
| 4551 for (var i = 0, l = positions.length; i < l; i++) { |
| 4552 var copy = {}; |
| 4553 for (var key in positions[i]) { |
| 4554 copy[key] = positions[i][key]; |
| 7040 } | 4555 } |
| 7041 } else { | 4556 positions.push(copy); |
| 7042 return index; | 4557 } |
| 7043 } | 4558 positions[0].top = positions[1].top += positionRect.height; |
| 7044 }, | 4559 positions[2].top = positions[3].top -= positionRect.height; |
| 7045 | 4560 positions[4].left = positions[6].left += positionRect.width; |
| 7046 _valueForItem: function(item) { | 4561 positions[5].left = positions[7].left -= positionRect.width; |
| 7047 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | 4562 } |
| 7048 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | 4563 vAlign = vAlign === 'auto' ? null : vAlign; |
| 7049 }, | 4564 hAlign = hAlign === 'auto' ? null : hAlign; |
| 7050 | 4565 var position; |
| 7051 _applySelection: function(item, isSelected) { | 4566 for (var i = 0; i < positions.length; i++) { |
| 7052 if (this.selectedClass) { | 4567 var pos = positions[i]; |
| 7053 this.toggleClass(this.selectedClass, isSelected, item); | 4568 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign
&& pos.horizontalAlign === hAlign) { |
| 7054 } | 4569 position = pos; |
| 7055 if (this.selectedAttribute) { | 4570 break; |
| 7056 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 4571 } |
| 7057 } | 4572 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos
.horizontalAlign === hAlign); |
| 7058 this._selectionChange(); | 4573 if (!this.dynamicAlign && !alignOk) { |
| 7059 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | 4574 continue; |
| 7060 }, | 4575 } |
| 7061 | 4576 position = position || pos; |
| 7062 _selectionChange: function() { | 4577 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
| 7063 this._setSelectedItem(this._selection.get()); | 4578 var diff = pos.croppedArea - position.croppedArea; |
| 7064 }, | 4579 if (diff < 0 || diff === 0 && alignOk) { |
| 7065 | 4580 position = pos; |
| 7066 // observe items change under the given node. | 4581 } |
| 7067 _observeItems: function(node) { | 4582 if (position.croppedArea === 0 && alignOk) { |
| 7068 return Polymer.dom(node).observeNodes(function(mutation) { | 4583 break; |
| 7069 this._updateItems(); | 4584 } |
| 7070 | 4585 } |
| 7071 if (this._shouldUpdateSelection) { | 4586 return position; |
| 7072 this._updateSelected(); | 4587 } |
| 7073 } | 4588 }; |
| 7074 | 4589 |
| 7075 // Let other interested parties know about the change so that | 4590 (function() { |
| 7076 // we don't have to recreate mutation observers everywhere. | 4591 'use strict'; |
| 7077 this.fire('iron-items-changed', mutation, { | 4592 Polymer({ |
| 7078 bubbles: false, | 4593 is: 'iron-overlay-backdrop', |
| 7079 cancelable: false | |
| 7080 }); | |
| 7081 }); | |
| 7082 }, | |
| 7083 | |
| 7084 _activateHandler: function(e) { | |
| 7085 var t = e.target; | |
| 7086 var items = this.items; | |
| 7087 while (t && t != this) { | |
| 7088 var i = items.indexOf(t); | |
| 7089 if (i >= 0) { | |
| 7090 var value = this._indexToValue(i); | |
| 7091 this._itemActivate(value, t); | |
| 7092 return; | |
| 7093 } | |
| 7094 t = t.parentNode; | |
| 7095 } | |
| 7096 }, | |
| 7097 | |
| 7098 _itemActivate: function(value, item) { | |
| 7099 if (!this.fire('iron-activate', | |
| 7100 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | |
| 7101 this.select(value); | |
| 7102 } | |
| 7103 } | |
| 7104 | |
| 7105 }; | |
| 7106 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
| 7107 Polymer.IronMultiSelectableBehaviorImpl = { | |
| 7108 properties: { | 4594 properties: { |
| 7109 | |
| 7110 /** | |
| 7111 * If true, multiple selections are allowed. | |
| 7112 */ | |
| 7113 multi: { | |
| 7114 type: Boolean, | |
| 7115 value: false, | |
| 7116 observer: 'multiChanged' | |
| 7117 }, | |
| 7118 | |
| 7119 /** | |
| 7120 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
| 7121 * is true. | |
| 7122 */ | |
| 7123 selectedValues: { | |
| 7124 type: Array, | |
| 7125 notify: true | |
| 7126 }, | |
| 7127 | |
| 7128 /** | |
| 7129 * Returns an array of currently selected items. | |
| 7130 */ | |
| 7131 selectedItems: { | |
| 7132 type: Array, | |
| 7133 readOnly: true, | |
| 7134 notify: true | |
| 7135 }, | |
| 7136 | |
| 7137 }, | |
| 7138 | |
| 7139 observers: [ | |
| 7140 '_updateSelected(selectedValues.splices)' | |
| 7141 ], | |
| 7142 | |
| 7143 /** | |
| 7144 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 7145 * `value` will be toggled; otherwise the `value` will be selected. | |
| 7146 * | |
| 7147 * @method select | |
| 7148 * @param {string|number} value the value to select. | |
| 7149 */ | |
| 7150 select: function(value) { | |
| 7151 if (this.multi) { | |
| 7152 if (this.selectedValues) { | |
| 7153 this._toggleSelected(value); | |
| 7154 } else { | |
| 7155 this.selectedValues = [value]; | |
| 7156 } | |
| 7157 } else { | |
| 7158 this.selected = value; | |
| 7159 } | |
| 7160 }, | |
| 7161 | |
| 7162 multiChanged: function(multi) { | |
| 7163 this._selection.multi = multi; | |
| 7164 }, | |
| 7165 | |
| 7166 get _shouldUpdateSelection() { | |
| 7167 return this.selected != null || | |
| 7168 (this.selectedValues != null && this.selectedValues.length); | |
| 7169 }, | |
| 7170 | |
| 7171 _updateAttrForSelected: function() { | |
| 7172 if (!this.multi) { | |
| 7173 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
| 7174 } else if (this._shouldUpdateSelection) { | |
| 7175 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
| 7176 return this._indexToValue(this.indexOf(selectedItem)); | |
| 7177 }, this).filter(function(unfilteredValue) { | |
| 7178 return unfilteredValue != null; | |
| 7179 }, this); | |
| 7180 } | |
| 7181 }, | |
| 7182 | |
| 7183 _updateSelected: function() { | |
| 7184 if (this.multi) { | |
| 7185 this._selectMulti(this.selectedValues); | |
| 7186 } else { | |
| 7187 this._selectSelected(this.selected); | |
| 7188 } | |
| 7189 }, | |
| 7190 | |
| 7191 _selectMulti: function(values) { | |
| 7192 if (values) { | |
| 7193 var selectedItems = this._valuesToItems(values); | |
| 7194 // clear all but the current selected items | |
| 7195 this._selection.clear(selectedItems); | |
| 7196 // select only those not selected yet | |
| 7197 for (var i = 0; i < selectedItems.length; i++) { | |
| 7198 this._selection.setItemSelected(selectedItems[i], true); | |
| 7199 } | |
| 7200 // Check for items, since this array is populated only when attached | |
| 7201 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
| 7202 var fallback = this._valueToItem(this.fallbackSelection); | |
| 7203 if (fallback) { | |
| 7204 this.selectedValues = [this.fallbackSelection]; | |
| 7205 } | |
| 7206 } | |
| 7207 } else { | |
| 7208 this._selection.clear(); | |
| 7209 } | |
| 7210 }, | |
| 7211 | |
| 7212 _selectionChange: function() { | |
| 7213 var s = this._selection.get(); | |
| 7214 if (this.multi) { | |
| 7215 this._setSelectedItems(s); | |
| 7216 } else { | |
| 7217 this._setSelectedItems([s]); | |
| 7218 this._setSelectedItem(s); | |
| 7219 } | |
| 7220 }, | |
| 7221 | |
| 7222 _toggleSelected: function(value) { | |
| 7223 var i = this.selectedValues.indexOf(value); | |
| 7224 var unselected = i < 0; | |
| 7225 if (unselected) { | |
| 7226 this.push('selectedValues',value); | |
| 7227 } else { | |
| 7228 this.splice('selectedValues',i,1); | |
| 7229 } | |
| 7230 }, | |
| 7231 | |
| 7232 _valuesToItems: function(values) { | |
| 7233 return (values == null) ? null : values.map(function(value) { | |
| 7234 return this._valueToItem(value); | |
| 7235 }, this); | |
| 7236 } | |
| 7237 }; | |
| 7238 | |
| 7239 /** @polymerBehavior */ | |
| 7240 Polymer.IronMultiSelectableBehavior = [ | |
| 7241 Polymer.IronSelectableBehavior, | |
| 7242 Polymer.IronMultiSelectableBehaviorImpl | |
| 7243 ]; | |
| 7244 /** | |
| 7245 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
| 7246 * | |
| 7247 * @demo demo/index.html | |
| 7248 * @polymerBehavior Polymer.IronMenuBehavior | |
| 7249 */ | |
| 7250 Polymer.IronMenuBehaviorImpl = { | |
| 7251 | |
| 7252 properties: { | |
| 7253 | |
| 7254 /** | |
| 7255 * Returns the currently focused item. | |
| 7256 * @type {?Object} | |
| 7257 */ | |
| 7258 focusedItem: { | |
| 7259 observer: '_focusedItemChanged', | |
| 7260 readOnly: true, | |
| 7261 type: Object | |
| 7262 }, | |
| 7263 | |
| 7264 /** | |
| 7265 * The attribute to use on menu items to look up the item title. Typing th
e first | |
| 7266 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
| 7267 * will be used. | |
| 7268 */ | |
| 7269 attrForItemTitle: { | |
| 7270 type: String | |
| 7271 } | |
| 7272 }, | |
| 7273 | |
| 7274 hostAttributes: { | |
| 7275 'role': 'menu', | |
| 7276 'tabindex': '0' | |
| 7277 }, | |
| 7278 | |
| 7279 observers: [ | |
| 7280 '_updateMultiselectable(multi)' | |
| 7281 ], | |
| 7282 | |
| 7283 listeners: { | |
| 7284 'focus': '_onFocus', | |
| 7285 'keydown': '_onKeydown', | |
| 7286 'iron-items-changed': '_onIronItemsChanged' | |
| 7287 }, | |
| 7288 | |
| 7289 keyBindings: { | |
| 7290 'up': '_onUpKey', | |
| 7291 'down': '_onDownKey', | |
| 7292 'esc': '_onEscKey', | |
| 7293 'shift+tab:keydown': '_onShiftTabDown' | |
| 7294 }, | |
| 7295 | |
| 7296 attached: function() { | |
| 7297 this._resetTabindices(); | |
| 7298 }, | |
| 7299 | |
| 7300 /** | |
| 7301 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 7302 * `value` will be toggled; otherwise the `value` will be selected. | |
| 7303 * | |
| 7304 * @param {string|number} value the value to select. | |
| 7305 */ | |
| 7306 select: function(value) { | |
| 7307 // Cancel automatically focusing a default item if the menu received focus | |
| 7308 // through a user action selecting a particular item. | |
| 7309 if (this._defaultFocusAsync) { | |
| 7310 this.cancelAsync(this._defaultFocusAsync); | |
| 7311 this._defaultFocusAsync = null; | |
| 7312 } | |
| 7313 var item = this._valueToItem(value); | |
| 7314 if (item && item.hasAttribute('disabled')) return; | |
| 7315 this._setFocusedItem(item); | |
| 7316 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | |
| 7317 }, | |
| 7318 | |
| 7319 /** | |
| 7320 * Resets all tabindex attributes to the appropriate value based on the | |
| 7321 * current selection state. The appropriate value is `0` (focusable) for | |
| 7322 * the default selected item, and `-1` (not keyboard focusable) for all | |
| 7323 * other items. | |
| 7324 */ | |
| 7325 _resetTabindices: function() { | |
| 7326 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | |
| 7327 | |
| 7328 this.items.forEach(function(item) { | |
| 7329 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | |
| 7330 }, this); | |
| 7331 }, | |
| 7332 | |
| 7333 /** | |
| 7334 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
| 7335 * multi-selectable. | |
| 7336 * | |
| 7337 * @param {boolean} multi True if the menu should be multi-selectable. | |
| 7338 */ | |
| 7339 _updateMultiselectable: function(multi) { | |
| 7340 if (multi) { | |
| 7341 this.setAttribute('aria-multiselectable', 'true'); | |
| 7342 } else { | |
| 7343 this.removeAttribute('aria-multiselectable'); | |
| 7344 } | |
| 7345 }, | |
| 7346 | |
| 7347 /** | |
| 7348 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
| 7349 * menu (if there is a relevant item, and it is possible to focus it). | |
| 7350 * | |
| 7351 * @param {KeyboardEvent} event A KeyboardEvent. | |
| 7352 */ | |
| 7353 _focusWithKeyboardEvent: function(event) { | |
| 7354 for (var i = 0, item; item = this.items[i]; i++) { | |
| 7355 var attr = this.attrForItemTitle || 'textContent'; | |
| 7356 var title = item[attr] || item.getAttribute(attr); | |
| 7357 | |
| 7358 if (!item.hasAttribute('disabled') && title && | |
| 7359 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | |
| 7360 this._setFocusedItem(item); | |
| 7361 break; | |
| 7362 } | |
| 7363 } | |
| 7364 }, | |
| 7365 | |
| 7366 /** | |
| 7367 * Focuses the previous item (relative to the currently focused item) in the | |
| 7368 * menu, disabled items will be skipped. | |
| 7369 * Loop until length + 1 to handle case of single item in menu. | |
| 7370 */ | |
| 7371 _focusPrevious: function() { | |
| 7372 var length = this.items.length; | |
| 7373 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
| 7374 for (var i = 1; i < length + 1; i++) { | |
| 7375 var item = this.items[(curFocusIndex - i + length) % length]; | |
| 7376 if (!item.hasAttribute('disabled')) { | |
| 7377 this._setFocusedItem(item); | |
| 7378 return; | |
| 7379 } | |
| 7380 } | |
| 7381 }, | |
| 7382 | |
| 7383 /** | |
| 7384 * Focuses the next item (relative to the currently focused item) in the | |
| 7385 * menu, disabled items will be skipped. | |
| 7386 * Loop until length + 1 to handle case of single item in menu. | |
| 7387 */ | |
| 7388 _focusNext: function() { | |
| 7389 var length = this.items.length; | |
| 7390 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
| 7391 for (var i = 1; i < length + 1; i++) { | |
| 7392 var item = this.items[(curFocusIndex + i) % length]; | |
| 7393 if (!item.hasAttribute('disabled')) { | |
| 7394 this._setFocusedItem(item); | |
| 7395 return; | |
| 7396 } | |
| 7397 } | |
| 7398 }, | |
| 7399 | |
| 7400 /** | |
| 7401 * Mutates items in the menu based on provided selection details, so that | |
| 7402 * all items correctly reflect selection state. | |
| 7403 * | |
| 7404 * @param {Element} item An item in the menu. | |
| 7405 * @param {boolean} isSelected True if the item should be shown in a | |
| 7406 * selected state, otherwise false. | |
| 7407 */ | |
| 7408 _applySelection: function(item, isSelected) { | |
| 7409 if (isSelected) { | |
| 7410 item.setAttribute('aria-selected', 'true'); | |
| 7411 } else { | |
| 7412 item.removeAttribute('aria-selected'); | |
| 7413 } | |
| 7414 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | |
| 7415 }, | |
| 7416 | |
| 7417 /** | |
| 7418 * Discretely updates tabindex values among menu items as the focused item | |
| 7419 * changes. | |
| 7420 * | |
| 7421 * @param {Element} focusedItem The element that is currently focused. | |
| 7422 * @param {?Element} old The last element that was considered focused, if | |
| 7423 * applicable. | |
| 7424 */ | |
| 7425 _focusedItemChanged: function(focusedItem, old) { | |
| 7426 old && old.setAttribute('tabindex', '-1'); | |
| 7427 if (focusedItem) { | |
| 7428 focusedItem.setAttribute('tabindex', '0'); | |
| 7429 focusedItem.focus(); | |
| 7430 } | |
| 7431 }, | |
| 7432 | |
| 7433 /** | |
| 7434 * A handler that responds to mutation changes related to the list of items | |
| 7435 * in the menu. | |
| 7436 * | |
| 7437 * @param {CustomEvent} event An event containing mutation records as its | |
| 7438 * detail. | |
| 7439 */ | |
| 7440 _onIronItemsChanged: function(event) { | |
| 7441 if (event.detail.addedNodes.length) { | |
| 7442 this._resetTabindices(); | |
| 7443 } | |
| 7444 }, | |
| 7445 | |
| 7446 /** | |
| 7447 * Handler that is called when a shift+tab keypress is detected by the menu. | |
| 7448 * | |
| 7449 * @param {CustomEvent} event A key combination event. | |
| 7450 */ | |
| 7451 _onShiftTabDown: function(event) { | |
| 7452 var oldTabIndex = this.getAttribute('tabindex'); | |
| 7453 | |
| 7454 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
| 7455 | |
| 7456 this._setFocusedItem(null); | |
| 7457 | |
| 7458 this.setAttribute('tabindex', '-1'); | |
| 7459 | |
| 7460 this.async(function() { | |
| 7461 this.setAttribute('tabindex', oldTabIndex); | |
| 7462 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 7463 // NOTE(cdata): polymer/polymer#1305 | |
| 7464 }, 1); | |
| 7465 }, | |
| 7466 | |
| 7467 /** | |
| 7468 * Handler that is called when the menu receives focus. | |
| 7469 * | |
| 7470 * @param {FocusEvent} event A focus event. | |
| 7471 */ | |
| 7472 _onFocus: function(event) { | |
| 7473 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
| 7474 // do not focus the menu itself | |
| 7475 return; | |
| 7476 } | |
| 7477 | |
| 7478 // Do not focus the selected tab if the deepest target is part of the | |
| 7479 // menu element's local DOM and is focusable. | |
| 7480 var rootTarget = /** @type {?HTMLElement} */( | |
| 7481 Polymer.dom(event).rootTarget); | |
| 7482 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
| 7483 return; | |
| 7484 } | |
| 7485 | |
| 7486 // clear the cached focus item | |
| 7487 this._defaultFocusAsync = this.async(function() { | |
| 7488 // focus the selected item when the menu receives focus, or the first it
em | |
| 7489 // if no item is selected | |
| 7490 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
| 7491 | |
| 7492 this._setFocusedItem(null); | |
| 7493 | |
| 7494 if (selectedItem) { | |
| 7495 this._setFocusedItem(selectedItem); | |
| 7496 } else if (this.items[0]) { | |
| 7497 // We find the first none-disabled item (if one exists) | |
| 7498 this._focusNext(); | |
| 7499 } | |
| 7500 }); | |
| 7501 }, | |
| 7502 | |
| 7503 /** | |
| 7504 * Handler that is called when the up key is pressed. | |
| 7505 * | |
| 7506 * @param {CustomEvent} event A key combination event. | |
| 7507 */ | |
| 7508 _onUpKey: function(event) { | |
| 7509 // up and down arrows moves the focus | |
| 7510 this._focusPrevious(); | |
| 7511 event.detail.keyboardEvent.preventDefault(); | |
| 7512 }, | |
| 7513 | |
| 7514 /** | |
| 7515 * Handler that is called when the down key is pressed. | |
| 7516 * | |
| 7517 * @param {CustomEvent} event A key combination event. | |
| 7518 */ | |
| 7519 _onDownKey: function(event) { | |
| 7520 this._focusNext(); | |
| 7521 event.detail.keyboardEvent.preventDefault(); | |
| 7522 }, | |
| 7523 | |
| 7524 /** | |
| 7525 * Handler that is called when the esc key is pressed. | |
| 7526 * | |
| 7527 * @param {CustomEvent} event A key combination event. | |
| 7528 */ | |
| 7529 _onEscKey: function(event) { | |
| 7530 // esc blurs the control | |
| 7531 this.focusedItem.blur(); | |
| 7532 }, | |
| 7533 | |
| 7534 /** | |
| 7535 * Handler that is called when a keydown event is detected. | |
| 7536 * | |
| 7537 * @param {KeyboardEvent} event A keyboard event. | |
| 7538 */ | |
| 7539 _onKeydown: function(event) { | |
| 7540 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
| 7541 // all other keys focus the menu item starting with that character | |
| 7542 this._focusWithKeyboardEvent(event); | |
| 7543 } | |
| 7544 event.stopPropagation(); | |
| 7545 }, | |
| 7546 | |
| 7547 // override _activateHandler | |
| 7548 _activateHandler: function(event) { | |
| 7549 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
| 7550 event.stopPropagation(); | |
| 7551 } | |
| 7552 }; | |
| 7553 | |
| 7554 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 7555 | |
| 7556 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
| 7557 Polymer.IronMenuBehavior = [ | |
| 7558 Polymer.IronMultiSelectableBehavior, | |
| 7559 Polymer.IronA11yKeysBehavior, | |
| 7560 Polymer.IronMenuBehaviorImpl | |
| 7561 ]; | |
| 7562 (function() { | |
| 7563 Polymer({ | |
| 7564 is: 'paper-menu', | |
| 7565 | |
| 7566 behaviors: [ | |
| 7567 Polymer.IronMenuBehavior | |
| 7568 ] | |
| 7569 }); | |
| 7570 })(); | |
| 7571 /** | |
| 7572 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
| 7573 optionally centers it in the window or another element. | |
| 7574 | |
| 7575 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | |
| 7576 by CSS. | |
| 7577 | |
| 7578 CSS properties | Action | |
| 7579 -----------------------------|------------------------------------------- | |
| 7580 `position` set | Element is not centered horizontally or verticall
y | |
| 7581 `top` or `bottom` set | Element is not vertically centered | |
| 7582 `left` or `right` set | Element is not horizontally centered | |
| 7583 `max-height` set | Element respects `max-height` | |
| 7584 `max-width` set | Element respects `max-width` | |
| 7585 | |
| 7586 `Polymer.IronFitBehavior` can position an element into another element using | |
| 7587 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
| 7588 | |
| 7589 <div class="container"> | |
| 7590 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
| 7591 Positioned into the container | |
| 7592 </iron-fit-impl> | |
| 7593 </div> | |
| 7594 | |
| 7595 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
| 7596 | |
| 7597 <div class="container"> | |
| 7598 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
| 7599 Positioned around the container | |
| 7600 </iron-fit-impl> | |
| 7601 </div> | |
| 7602 | |
| 7603 @demo demo/index.html | |
| 7604 @polymerBehavior | |
| 7605 */ | |
| 7606 | |
| 7607 Polymer.IronFitBehavior = { | |
| 7608 | |
| 7609 properties: { | |
| 7610 | |
| 7611 /** | |
| 7612 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
| 7613 * but it can be set to a child element. This is useful, for example, for
implementing a | |
| 7614 * scrolling region inside the element. | |
| 7615 * @type {!Element} | |
| 7616 */ | |
| 7617 sizingTarget: { | |
| 7618 type: Object, | |
| 7619 value: function() { | |
| 7620 return this; | |
| 7621 } | |
| 7622 }, | |
| 7623 | |
| 7624 /** | |
| 7625 * The element to fit `this` into. | |
| 7626 */ | |
| 7627 fitInto: { | |
| 7628 type: Object, | |
| 7629 value: window | |
| 7630 }, | |
| 7631 | |
| 7632 /** | |
| 7633 * Will position the element around the positionTarget without overlapping
it. | |
| 7634 */ | |
| 7635 noOverlap: { | |
| 7636 type: Boolean | |
| 7637 }, | |
| 7638 | |
| 7639 /** | |
| 7640 * The element that should be used to position the element. If not set, it
will | |
| 7641 * default to the parent node. | |
| 7642 * @type {!Element} | |
| 7643 */ | |
| 7644 positionTarget: { | |
| 7645 type: Element | |
| 7646 }, | |
| 7647 | |
| 7648 /** | |
| 7649 * The orientation against which to align the element horizontally | |
| 7650 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
| 7651 */ | |
| 7652 horizontalAlign: { | |
| 7653 type: String | |
| 7654 }, | |
| 7655 | |
| 7656 /** | |
| 7657 * The orientation against which to align the element vertically | |
| 7658 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
| 7659 */ | |
| 7660 verticalAlign: { | |
| 7661 type: String | |
| 7662 }, | |
| 7663 | |
| 7664 /** | |
| 7665 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
| 7666 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
| 7667 */ | |
| 7668 dynamicAlign: { | |
| 7669 type: Boolean | |
| 7670 }, | |
| 7671 | |
| 7672 /** | |
| 7673 * The same as setting margin-left and margin-right css properties. | |
| 7674 * @deprecated | |
| 7675 */ | |
| 7676 horizontalOffset: { | |
| 7677 type: Number, | |
| 7678 value: 0, | |
| 7679 notify: true | |
| 7680 }, | |
| 7681 | |
| 7682 /** | |
| 7683 * The same as setting margin-top and margin-bottom css properties. | |
| 7684 * @deprecated | |
| 7685 */ | |
| 7686 verticalOffset: { | |
| 7687 type: Number, | |
| 7688 value: 0, | |
| 7689 notify: true | |
| 7690 }, | |
| 7691 | |
| 7692 /** | |
| 7693 * Set to true to auto-fit on attach. | |
| 7694 */ | |
| 7695 autoFitOnAttach: { | |
| 7696 type: Boolean, | |
| 7697 value: false | |
| 7698 }, | |
| 7699 | |
| 7700 /** @type {?Object} */ | |
| 7701 _fitInfo: { | |
| 7702 type: Object | |
| 7703 } | |
| 7704 }, | |
| 7705 | |
| 7706 get _fitWidth() { | |
| 7707 var fitWidth; | |
| 7708 if (this.fitInto === window) { | |
| 7709 fitWidth = this.fitInto.innerWidth; | |
| 7710 } else { | |
| 7711 fitWidth = this.fitInto.getBoundingClientRect().width; | |
| 7712 } | |
| 7713 return fitWidth; | |
| 7714 }, | |
| 7715 | |
| 7716 get _fitHeight() { | |
| 7717 var fitHeight; | |
| 7718 if (this.fitInto === window) { | |
| 7719 fitHeight = this.fitInto.innerHeight; | |
| 7720 } else { | |
| 7721 fitHeight = this.fitInto.getBoundingClientRect().height; | |
| 7722 } | |
| 7723 return fitHeight; | |
| 7724 }, | |
| 7725 | |
| 7726 get _fitLeft() { | |
| 7727 var fitLeft; | |
| 7728 if (this.fitInto === window) { | |
| 7729 fitLeft = 0; | |
| 7730 } else { | |
| 7731 fitLeft = this.fitInto.getBoundingClientRect().left; | |
| 7732 } | |
| 7733 return fitLeft; | |
| 7734 }, | |
| 7735 | |
| 7736 get _fitTop() { | |
| 7737 var fitTop; | |
| 7738 if (this.fitInto === window) { | |
| 7739 fitTop = 0; | |
| 7740 } else { | |
| 7741 fitTop = this.fitInto.getBoundingClientRect().top; | |
| 7742 } | |
| 7743 return fitTop; | |
| 7744 }, | |
| 7745 | |
| 7746 /** | |
| 7747 * The element that should be used to position the element, | |
| 7748 * if no position target is configured. | |
| 7749 */ | |
| 7750 get _defaultPositionTarget() { | |
| 7751 var parent = Polymer.dom(this).parentNode; | |
| 7752 | |
| 7753 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
| 7754 parent = parent.host; | |
| 7755 } | |
| 7756 | |
| 7757 return parent; | |
| 7758 }, | |
| 7759 | |
| 7760 /** | |
| 7761 * The horizontal align value, accounting for the RTL/LTR text direction. | |
| 7762 */ | |
| 7763 get _localeHorizontalAlign() { | |
| 7764 if (this._isRTL) { | |
| 7765 // In RTL, "left" becomes "right". | |
| 7766 if (this.horizontalAlign === 'right') { | |
| 7767 return 'left'; | |
| 7768 } | |
| 7769 if (this.horizontalAlign === 'left') { | |
| 7770 return 'right'; | |
| 7771 } | |
| 7772 } | |
| 7773 return this.horizontalAlign; | |
| 7774 }, | |
| 7775 | |
| 7776 attached: function() { | |
| 7777 // Memoize this to avoid expensive calculations & relayouts. | |
| 7778 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | |
| 7779 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | |
| 7780 if (this.autoFitOnAttach) { | |
| 7781 if (window.getComputedStyle(this).display === 'none') { | |
| 7782 setTimeout(function() { | |
| 7783 this.fit(); | |
| 7784 }.bind(this)); | |
| 7785 } else { | |
| 7786 this.fit(); | |
| 7787 } | |
| 7788 } | |
| 7789 }, | |
| 7790 | |
| 7791 /** | |
| 7792 * Positions and fits the element into the `fitInto` element. | |
| 7793 */ | |
| 7794 fit: function() { | |
| 7795 this.position(); | |
| 7796 this.constrain(); | |
| 7797 this.center(); | |
| 7798 }, | |
| 7799 | |
| 7800 /** | |
| 7801 * Memoize information needed to position and size the target element. | |
| 7802 * @suppress {deprecated} | |
| 7803 */ | |
| 7804 _discoverInfo: function() { | |
| 7805 if (this._fitInfo) { | |
| 7806 return; | |
| 7807 } | |
| 7808 var target = window.getComputedStyle(this); | |
| 7809 var sizer = window.getComputedStyle(this.sizingTarget); | |
| 7810 | |
| 7811 this._fitInfo = { | |
| 7812 inlineStyle: { | |
| 7813 top: this.style.top || '', | |
| 7814 left: this.style.left || '', | |
| 7815 position: this.style.position || '' | |
| 7816 }, | |
| 7817 sizerInlineStyle: { | |
| 7818 maxWidth: this.sizingTarget.style.maxWidth || '', | |
| 7819 maxHeight: this.sizingTarget.style.maxHeight || '', | |
| 7820 boxSizing: this.sizingTarget.style.boxSizing || '' | |
| 7821 }, | |
| 7822 positionedBy: { | |
| 7823 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto'
? | |
| 7824 'bottom' : null), | |
| 7825 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut
o' ? | |
| 7826 'right' : null) | |
| 7827 }, | |
| 7828 sizedBy: { | |
| 7829 height: sizer.maxHeight !== 'none', | |
| 7830 width: sizer.maxWidth !== 'none', | |
| 7831 minWidth: parseInt(sizer.minWidth, 10) || 0, | |
| 7832 minHeight: parseInt(sizer.minHeight, 10) || 0 | |
| 7833 }, | |
| 7834 margin: { | |
| 7835 top: parseInt(target.marginTop, 10) || 0, | |
| 7836 right: parseInt(target.marginRight, 10) || 0, | |
| 7837 bottom: parseInt(target.marginBottom, 10) || 0, | |
| 7838 left: parseInt(target.marginLeft, 10) || 0 | |
| 7839 } | |
| 7840 }; | |
| 7841 | |
| 7842 // Support these properties until they are removed. | |
| 7843 if (this.verticalOffset) { | |
| 7844 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | |
| 7845 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | |
| 7846 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | |
| 7847 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | |
| 7848 } | |
| 7849 if (this.horizontalOffset) { | |
| 7850 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | |
| 7851 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | |
| 7852 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | |
| 7853 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | |
| 7854 } | |
| 7855 }, | |
| 7856 | |
| 7857 /** | |
| 7858 * Resets the target element's position and size constraints, and clear | |
| 7859 * the memoized data. | |
| 7860 */ | |
| 7861 resetFit: function() { | |
| 7862 var info = this._fitInfo || {}; | |
| 7863 for (var property in info.sizerInlineStyle) { | |
| 7864 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | |
| 7865 } | |
| 7866 for (var property in info.inlineStyle) { | |
| 7867 this.style[property] = info.inlineStyle[property]; | |
| 7868 } | |
| 7869 | |
| 7870 this._fitInfo = null; | |
| 7871 }, | |
| 7872 | |
| 7873 /** | |
| 7874 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
| 7875 * the element or the `fitInto` element has been resized, or if any of the | |
| 7876 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
| 7877 * It preserves the scroll position of the sizingTarget. | |
| 7878 */ | |
| 7879 refit: function() { | |
| 7880 var scrollLeft = this.sizingTarget.scrollLeft; | |
| 7881 var scrollTop = this.sizingTarget.scrollTop; | |
| 7882 this.resetFit(); | |
| 7883 this.fit(); | |
| 7884 this.sizingTarget.scrollLeft = scrollLeft; | |
| 7885 this.sizingTarget.scrollTop = scrollTop; | |
| 7886 }, | |
| 7887 | |
| 7888 /** | |
| 7889 * Positions the element according to `horizontalAlign, verticalAlign`. | |
| 7890 */ | |
| 7891 position: function() { | |
| 7892 if (!this.horizontalAlign && !this.verticalAlign) { | |
| 7893 // needs to be centered, and it is done after constrain. | |
| 7894 return; | |
| 7895 } | |
| 7896 this._discoverInfo(); | |
| 7897 | |
| 7898 this.style.position = 'fixed'; | |
| 7899 // Need border-box for margin/padding. | |
| 7900 this.sizingTarget.style.boxSizing = 'border-box'; | |
| 7901 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
| 7902 this.style.left = '0px'; | |
| 7903 this.style.top = '0px'; | |
| 7904 | |
| 7905 var rect = this.getBoundingClientRect(); | |
| 7906 var positionRect = this.__getNormalizedRect(this.positionTarget); | |
| 7907 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 7908 | |
| 7909 var margin = this._fitInfo.margin; | |
| 7910 | |
| 7911 // Consider the margin as part of the size for position calculations. | |
| 7912 var size = { | |
| 7913 width: rect.width + margin.left + margin.right, | |
| 7914 height: rect.height + margin.top + margin.bottom | |
| 7915 }; | |
| 7916 | |
| 7917 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | |
| 7918 | |
| 7919 var left = position.left + margin.left; | |
| 7920 var top = position.top + margin.top; | |
| 7921 | |
| 7922 // Use original size (without margin). | |
| 7923 var right = Math.min(fitRect.right - margin.right, left + rect.width); | |
| 7924 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | |
| 7925 | |
| 7926 var minWidth = this._fitInfo.sizedBy.minWidth; | |
| 7927 var minHeight = this._fitInfo.sizedBy.minHeight; | |
| 7928 if (left < margin.left) { | |
| 7929 left = margin.left; | |
| 7930 if (right - left < minWidth) { | |
| 7931 left = right - minWidth; | |
| 7932 } | |
| 7933 } | |
| 7934 if (top < margin.top) { | |
| 7935 top = margin.top; | |
| 7936 if (bottom - top < minHeight) { | |
| 7937 top = bottom - minHeight; | |
| 7938 } | |
| 7939 } | |
| 7940 | |
| 7941 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | |
| 7942 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | |
| 7943 | |
| 7944 // Remove the offset caused by any stacking context. | |
| 7945 this.style.left = (left - rect.left) + 'px'; | |
| 7946 this.style.top = (top - rect.top) + 'px'; | |
| 7947 }, | |
| 7948 | |
| 7949 /** | |
| 7950 * Constrains the size of the element to `fitInto` by setting `max-height` | |
| 7951 * and/or `max-width`. | |
| 7952 */ | |
| 7953 constrain: function() { | |
| 7954 if (this.horizontalAlign || this.verticalAlign) { | |
| 7955 return; | |
| 7956 } | |
| 7957 this._discoverInfo(); | |
| 7958 | |
| 7959 var info = this._fitInfo; | |
| 7960 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
| 7961 if (!info.positionedBy.vertically) { | |
| 7962 this.style.position = 'fixed'; | |
| 7963 this.style.top = '0px'; | |
| 7964 } | |
| 7965 if (!info.positionedBy.horizontally) { | |
| 7966 this.style.position = 'fixed'; | |
| 7967 this.style.left = '0px'; | |
| 7968 } | |
| 7969 | |
| 7970 // need border-box for margin/padding | |
| 7971 this.sizingTarget.style.boxSizing = 'border-box'; | |
| 7972 // constrain the width and height if not already set | |
| 7973 var rect = this.getBoundingClientRect(); | |
| 7974 if (!info.sizedBy.height) { | |
| 7975 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | |
| 7976 } | |
| 7977 if (!info.sizedBy.width) { | |
| 7978 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | |
| 7979 } | |
| 7980 }, | |
| 7981 | |
| 7982 /** | |
| 7983 * @protected | |
| 7984 * @deprecated | |
| 7985 */ | |
| 7986 _sizeDimension: function(rect, positionedBy, start, end, extent) { | |
| 7987 this.__sizeDimension(rect, positionedBy, start, end, extent); | |
| 7988 }, | |
| 7989 | |
| 7990 /** | |
| 7991 * @private | |
| 7992 */ | |
| 7993 __sizeDimension: function(rect, positionedBy, start, end, extent) { | |
| 7994 var info = this._fitInfo; | |
| 7995 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 7996 var max = extent === 'Width' ? fitRect.width : fitRect.height; | |
| 7997 var flip = (positionedBy === end); | |
| 7998 var offset = flip ? max - rect[end] : rect[start]; | |
| 7999 var margin = info.margin[flip ? start : end]; | |
| 8000 var offsetExtent = 'offset' + extent; | |
| 8001 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | |
| 8002 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | |
| 8003 }, | |
| 8004 | |
| 8005 /** | |
| 8006 * Centers horizontally and vertically if not already positioned. This also
sets | |
| 8007 * `position:fixed`. | |
| 8008 */ | |
| 8009 center: function() { | |
| 8010 if (this.horizontalAlign || this.verticalAlign) { | |
| 8011 return; | |
| 8012 } | |
| 8013 this._discoverInfo(); | |
| 8014 | |
| 8015 var positionedBy = this._fitInfo.positionedBy; | |
| 8016 if (positionedBy.vertically && positionedBy.horizontally) { | |
| 8017 // Already positioned. | |
| 8018 return; | |
| 8019 } | |
| 8020 // Need position:fixed to center | |
| 8021 this.style.position = 'fixed'; | |
| 8022 // Take into account the offset caused by parents that create stacking | |
| 8023 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
| 8024 // measure the bounding rect. | |
| 8025 if (!positionedBy.vertically) { | |
| 8026 this.style.top = '0px'; | |
| 8027 } | |
| 8028 if (!positionedBy.horizontally) { | |
| 8029 this.style.left = '0px'; | |
| 8030 } | |
| 8031 // It will take in consideration margins and transforms | |
| 8032 var rect = this.getBoundingClientRect(); | |
| 8033 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 8034 if (!positionedBy.vertically) { | |
| 8035 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | |
| 8036 this.style.top = top + 'px'; | |
| 8037 } | |
| 8038 if (!positionedBy.horizontally) { | |
| 8039 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | |
| 8040 this.style.left = left + 'px'; | |
| 8041 } | |
| 8042 }, | |
| 8043 | |
| 8044 __getNormalizedRect: function(target) { | |
| 8045 if (target === document.documentElement || target === window) { | |
| 8046 return { | |
| 8047 top: 0, | |
| 8048 left: 0, | |
| 8049 width: window.innerWidth, | |
| 8050 height: window.innerHeight, | |
| 8051 right: window.innerWidth, | |
| 8052 bottom: window.innerHeight | |
| 8053 }; | |
| 8054 } | |
| 8055 return target.getBoundingClientRect(); | |
| 8056 }, | |
| 8057 | |
| 8058 __getCroppedArea: function(position, size, fitRect) { | |
| 8059 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | |
| 8060 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | |
| 8061 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | |
| 8062 }, | |
| 8063 | |
| 8064 | |
| 8065 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | |
| 8066 // All the possible configurations. | |
| 8067 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
| 8068 var positions = [{ | |
| 8069 verticalAlign: 'top', | |
| 8070 horizontalAlign: 'left', | |
| 8071 top: positionRect.top, | |
| 8072 left: positionRect.left | |
| 8073 }, { | |
| 8074 verticalAlign: 'top', | |
| 8075 horizontalAlign: 'right', | |
| 8076 top: positionRect.top, | |
| 8077 left: positionRect.right - size.width | |
| 8078 }, { | |
| 8079 verticalAlign: 'bottom', | |
| 8080 horizontalAlign: 'left', | |
| 8081 top: positionRect.bottom - size.height, | |
| 8082 left: positionRect.left | |
| 8083 }, { | |
| 8084 verticalAlign: 'bottom', | |
| 8085 horizontalAlign: 'right', | |
| 8086 top: positionRect.bottom - size.height, | |
| 8087 left: positionRect.right - size.width | |
| 8088 }]; | |
| 8089 | |
| 8090 if (this.noOverlap) { | |
| 8091 // Duplicate. | |
| 8092 for (var i = 0, l = positions.length; i < l; i++) { | |
| 8093 var copy = {}; | |
| 8094 for (var key in positions[i]) { | |
| 8095 copy[key] = positions[i][key]; | |
| 8096 } | |
| 8097 positions.push(copy); | |
| 8098 } | |
| 8099 // Horizontal overlap only. | |
| 8100 positions[0].top = positions[1].top += positionRect.height; | |
| 8101 positions[2].top = positions[3].top -= positionRect.height; | |
| 8102 // Vertical overlap only. | |
| 8103 positions[4].left = positions[6].left += positionRect.width; | |
| 8104 positions[5].left = positions[7].left -= positionRect.width; | |
| 8105 } | |
| 8106 | |
| 8107 // Consider auto as null for coding convenience. | |
| 8108 vAlign = vAlign === 'auto' ? null : vAlign; | |
| 8109 hAlign = hAlign === 'auto' ? null : hAlign; | |
| 8110 | |
| 8111 var position; | |
| 8112 for (var i = 0; i < positions.length; i++) { | |
| 8113 var pos = positions[i]; | |
| 8114 | |
| 8115 // If both vAlign and hAlign are defined, return exact match. | |
| 8116 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
| 8117 // we'll have to check the croppedArea to make the best choice. | |
| 8118 if (!this.dynamicAlign && !this.noOverlap && | |
| 8119 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | |
| 8120 position = pos; | |
| 8121 break; | |
| 8122 } | |
| 8123 | |
| 8124 // Align is ok if alignment preferences are respected. If no preferences
, | |
| 8125 // it is considered ok. | |
| 8126 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | |
| 8127 (!hAlign || pos.horizontalAlign === hAlign); | |
| 8128 | |
| 8129 // Filter out elements that don't match the alignment (if defined). | |
| 8130 // With dynamicAlign, we need to consider all the positions to find the | |
| 8131 // one that minimizes the cropped area. | |
| 8132 if (!this.dynamicAlign && !alignOk) { | |
| 8133 continue; | |
| 8134 } | |
| 8135 | |
| 8136 position = position || pos; | |
| 8137 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | |
| 8138 var diff = pos.croppedArea - position.croppedArea; | |
| 8139 // Check which crops less. If it crops equally, check if align is ok. | |
| 8140 if (diff < 0 || (diff === 0 && alignOk)) { | |
| 8141 position = pos; | |
| 8142 } | |
| 8143 // If not cropped and respects the align requirements, keep it. | |
| 8144 // This allows to prefer positions overlapping horizontally over the | |
| 8145 // ones overlapping vertically. | |
| 8146 if (position.croppedArea === 0 && alignOk) { | |
| 8147 break; | |
| 8148 } | |
| 8149 } | |
| 8150 | |
| 8151 return position; | |
| 8152 } | |
| 8153 | |
| 8154 }; | |
| 8155 (function() { | |
| 8156 'use strict'; | |
| 8157 | |
| 8158 Polymer({ | |
| 8159 | |
| 8160 is: 'iron-overlay-backdrop', | |
| 8161 | |
| 8162 properties: { | |
| 8163 | |
| 8164 /** | |
| 8165 * Returns true if the backdrop is opened. | |
| 8166 */ | |
| 8167 opened: { | 4595 opened: { |
| 8168 reflectToAttribute: true, | 4596 reflectToAttribute: true, |
| 8169 type: Boolean, | 4597 type: Boolean, |
| 8170 value: false, | 4598 value: false, |
| 8171 observer: '_openedChanged' | 4599 observer: '_openedChanged' |
| 8172 } | 4600 } |
| 8173 | 4601 }, |
| 8174 }, | |
| 8175 | |
| 8176 listeners: { | 4602 listeners: { |
| 8177 'transitionend': '_onTransitionend' | 4603 transitionend: '_onTransitionend' |
| 8178 }, | 4604 }, |
| 8179 | |
| 8180 created: function() { | 4605 created: function() { |
| 8181 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
| 8182 this.__openedRaf = null; | 4606 this.__openedRaf = null; |
| 8183 }, | 4607 }, |
| 8184 | |
| 8185 attached: function() { | 4608 attached: function() { |
| 8186 this.opened && this._openedChanged(this.opened); | 4609 this.opened && this._openedChanged(this.opened); |
| 8187 }, | 4610 }, |
| 8188 | |
| 8189 /** | |
| 8190 * Appends the backdrop to document body if needed. | |
| 8191 */ | |
| 8192 prepare: function() { | 4611 prepare: function() { |
| 8193 if (this.opened && !this.parentNode) { | 4612 if (this.opened && !this.parentNode) { |
| 8194 Polymer.dom(document.body).appendChild(this); | 4613 Polymer.dom(document.body).appendChild(this); |
| 8195 } | 4614 } |
| 8196 }, | 4615 }, |
| 8197 | |
| 8198 /** | |
| 8199 * Shows the backdrop. | |
| 8200 */ | |
| 8201 open: function() { | 4616 open: function() { |
| 8202 this.opened = true; | 4617 this.opened = true; |
| 8203 }, | 4618 }, |
| 8204 | |
| 8205 /** | |
| 8206 * Hides the backdrop. | |
| 8207 */ | |
| 8208 close: function() { | 4619 close: function() { |
| 8209 this.opened = false; | 4620 this.opened = false; |
| 8210 }, | 4621 }, |
| 8211 | |
| 8212 /** | |
| 8213 * Removes the backdrop from document body if needed. | |
| 8214 */ | |
| 8215 complete: function() { | 4622 complete: function() { |
| 8216 if (!this.opened && this.parentNode === document.body) { | 4623 if (!this.opened && this.parentNode === document.body) { |
| 8217 Polymer.dom(this.parentNode).removeChild(this); | 4624 Polymer.dom(this.parentNode).removeChild(this); |
| 8218 } | 4625 } |
| 8219 }, | 4626 }, |
| 8220 | |
| 8221 _onTransitionend: function(event) { | 4627 _onTransitionend: function(event) { |
| 8222 if (event && event.target === this) { | 4628 if (event && event.target === this) { |
| 8223 this.complete(); | 4629 this.complete(); |
| 8224 } | 4630 } |
| 8225 }, | 4631 }, |
| 8226 | |
| 8227 /** | |
| 8228 * @param {boolean} opened | |
| 8229 * @private | |
| 8230 */ | |
| 8231 _openedChanged: function(opened) { | 4632 _openedChanged: function(opened) { |
| 8232 if (opened) { | 4633 if (opened) { |
| 8233 // Auto-attach. | |
| 8234 this.prepare(); | 4634 this.prepare(); |
| 8235 } else { | 4635 } else { |
| 8236 // Animation might be disabled via the mixin or opacity custom property. | |
| 8237 // If it is disabled in other ways, it's up to the user to call complete
. | |
| 8238 var cs = window.getComputedStyle(this); | 4636 var cs = window.getComputedStyle(this); |
| 8239 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 4637 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
| 8240 this.complete(); | 4638 this.complete(); |
| 8241 } | 4639 } |
| 8242 } | 4640 } |
| 8243 | |
| 8244 if (!this.isAttached) { | 4641 if (!this.isAttached) { |
| 8245 return; | 4642 return; |
| 8246 } | 4643 } |
| 8247 | |
| 8248 // Always cancel previous requestAnimationFrame. | |
| 8249 if (this.__openedRaf) { | 4644 if (this.__openedRaf) { |
| 8250 window.cancelAnimationFrame(this.__openedRaf); | 4645 window.cancelAnimationFrame(this.__openedRaf); |
| 8251 this.__openedRaf = null; | 4646 this.__openedRaf = null; |
| 8252 } | 4647 } |
| 8253 // Force relayout to ensure proper transitions. | |
| 8254 this.scrollTop = this.scrollTop; | 4648 this.scrollTop = this.scrollTop; |
| 8255 this.__openedRaf = window.requestAnimationFrame(function() { | 4649 this.__openedRaf = window.requestAnimationFrame(function() { |
| 8256 this.__openedRaf = null; | 4650 this.__openedRaf = null; |
| 8257 this.toggleClass('opened', this.opened); | 4651 this.toggleClass('opened', this.opened); |
| 8258 }.bind(this)); | 4652 }.bind(this)); |
| 8259 } | 4653 } |
| 8260 }); | 4654 }); |
| 8261 | |
| 8262 })(); | 4655 })(); |
| 8263 /** | 4656 |
| 8264 * @struct | 4657 Polymer.IronOverlayManagerClass = function() { |
| 8265 * @constructor | 4658 this._overlays = []; |
| 8266 * @private | 4659 this._minimumZ = 101; |
| 8267 */ | 4660 this._backdropElement = null; |
| 8268 Polymer.IronOverlayManagerClass = function() { | 4661 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
| 8269 /** | 4662 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
| 8270 * Used to keep track of the opened overlays. | 4663 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
| 8271 * @private {Array<Element>} | 4664 }; |
| 8272 */ | 4665 |
| 8273 this._overlays = []; | 4666 Polymer.IronOverlayManagerClass.prototype = { |
| 8274 | 4667 constructor: Polymer.IronOverlayManagerClass, |
| 8275 /** | 4668 get backdropElement() { |
| 8276 * iframes have a default z-index of 100, | 4669 if (!this._backdropElement) { |
| 8277 * so this default should be at least that. | 4670 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
| 8278 * @private {number} | 4671 } |
| 8279 */ | 4672 return this._backdropElement; |
| 8280 this._minimumZ = 101; | 4673 }, |
| 8281 | 4674 get deepActiveElement() { |
| 8282 /** | 4675 var active = document.activeElement || document.body; |
| 8283 * Memoized backdrop element. | 4676 while (active.root && Polymer.dom(active.root).activeElement) { |
| 8284 * @private {Element|null} | 4677 active = Polymer.dom(active.root).activeElement; |
| 8285 */ | 4678 } |
| 8286 this._backdropElement = null; | 4679 return active; |
| 8287 | 4680 }, |
| 8288 // Enable document-wide tap recognizer. | 4681 _bringOverlayAtIndexToFront: function(i) { |
| 8289 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 4682 var overlay = this._overlays[i]; |
| 8290 | 4683 if (!overlay) { |
| 8291 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 4684 return; |
| 8292 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 4685 } |
| 8293 }; | 4686 var lastI = this._overlays.length - 1; |
| 8294 | 4687 var currentOverlay = this._overlays[lastI]; |
| 8295 Polymer.IronOverlayManagerClass.prototype = { | 4688 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
| 8296 | 4689 lastI--; |
| 8297 constructor: Polymer.IronOverlayManagerClass, | 4690 } |
| 8298 | 4691 if (i >= lastI) { |
| 8299 /** | 4692 return; |
| 8300 * The shared backdrop element. | 4693 } |
| 8301 * @type {!Element} backdropElement | 4694 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
| 8302 */ | 4695 if (this._getZ(overlay) <= minimumZ) { |
| 8303 get backdropElement() { | 4696 this._applyOverlayZ(overlay, minimumZ); |
| 8304 if (!this._backdropElement) { | 4697 } |
| 8305 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 4698 while (i < lastI) { |
| 8306 } | 4699 this._overlays[i] = this._overlays[i + 1]; |
| 8307 return this._backdropElement; | 4700 i++; |
| 8308 }, | 4701 } |
| 8309 | 4702 this._overlays[lastI] = overlay; |
| 8310 /** | 4703 }, |
| 8311 * The deepest active element. | 4704 addOrRemoveOverlay: function(overlay) { |
| 8312 * @type {!Element} activeElement the active element | 4705 if (overlay.opened) { |
| 8313 */ | 4706 this.addOverlay(overlay); |
| 8314 get deepActiveElement() { | 4707 } else { |
| 8315 // document.activeElement can be null | 4708 this.removeOverlay(overlay); |
| 8316 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | 4709 } |
| 8317 // In case of null, default it to document.body. | 4710 }, |
| 8318 var active = document.activeElement || document.body; | 4711 addOverlay: function(overlay) { |
| 8319 while (active.root && Polymer.dom(active.root).activeElement) { | 4712 var i = this._overlays.indexOf(overlay); |
| 8320 active = Polymer.dom(active.root).activeElement; | 4713 if (i >= 0) { |
| 8321 } | 4714 this._bringOverlayAtIndexToFront(i); |
| 8322 return active; | |
| 8323 }, | |
| 8324 | |
| 8325 /** | |
| 8326 * Brings the overlay at the specified index to the front. | |
| 8327 * @param {number} i | |
| 8328 * @private | |
| 8329 */ | |
| 8330 _bringOverlayAtIndexToFront: function(i) { | |
| 8331 var overlay = this._overlays[i]; | |
| 8332 if (!overlay) { | |
| 8333 return; | |
| 8334 } | |
| 8335 var lastI = this._overlays.length - 1; | |
| 8336 var currentOverlay = this._overlays[lastI]; | |
| 8337 // Ensure always-on-top overlay stays on top. | |
| 8338 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
| 8339 lastI--; | |
| 8340 } | |
| 8341 // If already the top element, return. | |
| 8342 if (i >= lastI) { | |
| 8343 return; | |
| 8344 } | |
| 8345 // Update z-index to be on top. | |
| 8346 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | |
| 8347 if (this._getZ(overlay) <= minimumZ) { | |
| 8348 this._applyOverlayZ(overlay, minimumZ); | |
| 8349 } | |
| 8350 | |
| 8351 // Shift other overlays behind the new on top. | |
| 8352 while (i < lastI) { | |
| 8353 this._overlays[i] = this._overlays[i + 1]; | |
| 8354 i++; | |
| 8355 } | |
| 8356 this._overlays[lastI] = overlay; | |
| 8357 }, | |
| 8358 | |
| 8359 /** | |
| 8360 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
| 8361 * Also updates the backdrop z-index. | |
| 8362 * @param {!Element} overlay | |
| 8363 */ | |
| 8364 addOrRemoveOverlay: function(overlay) { | |
| 8365 if (overlay.opened) { | |
| 8366 this.addOverlay(overlay); | |
| 8367 } else { | |
| 8368 this.removeOverlay(overlay); | |
| 8369 } | |
| 8370 }, | |
| 8371 | |
| 8372 /** | |
| 8373 * Tracks overlays for z-index and focus management. | |
| 8374 * Ensures the last added overlay with always-on-top remains on top. | |
| 8375 * @param {!Element} overlay | |
| 8376 */ | |
| 8377 addOverlay: function(overlay) { | |
| 8378 var i = this._overlays.indexOf(overlay); | |
| 8379 if (i >= 0) { | |
| 8380 this._bringOverlayAtIndexToFront(i); | |
| 8381 this.trackBackdrop(); | |
| 8382 return; | |
| 8383 } | |
| 8384 var insertionIndex = this._overlays.length; | |
| 8385 var currentOverlay = this._overlays[insertionIndex - 1]; | |
| 8386 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | |
| 8387 var newZ = this._getZ(overlay); | |
| 8388 | |
| 8389 // Ensure always-on-top overlay stays on top. | |
| 8390 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
| 8391 // This bumps the z-index of +2. | |
| 8392 this._applyOverlayZ(currentOverlay, minimumZ); | |
| 8393 insertionIndex--; | |
| 8394 // Update minimumZ to match previous overlay's z-index. | |
| 8395 var previousOverlay = this._overlays[insertionIndex - 1]; | |
| 8396 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | |
| 8397 } | |
| 8398 | |
| 8399 // Update z-index and insert overlay. | |
| 8400 if (newZ <= minimumZ) { | |
| 8401 this._applyOverlayZ(overlay, minimumZ); | |
| 8402 } | |
| 8403 this._overlays.splice(insertionIndex, 0, overlay); | |
| 8404 | |
| 8405 this.trackBackdrop(); | 4715 this.trackBackdrop(); |
| 8406 }, | 4716 return; |
| 8407 | 4717 } |
| 8408 /** | 4718 var insertionIndex = this._overlays.length; |
| 8409 * @param {!Element} overlay | 4719 var currentOverlay = this._overlays[insertionIndex - 1]; |
| 8410 */ | 4720 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
| 8411 removeOverlay: function(overlay) { | 4721 var newZ = this._getZ(overlay); |
| 8412 var i = this._overlays.indexOf(overlay); | 4722 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
| 8413 if (i === -1) { | 4723 this._applyOverlayZ(currentOverlay, minimumZ); |
| 8414 return; | 4724 insertionIndex--; |
| 8415 } | 4725 var previousOverlay = this._overlays[insertionIndex - 1]; |
| 8416 this._overlays.splice(i, 1); | 4726 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
| 8417 | 4727 } |
| 8418 this.trackBackdrop(); | 4728 if (newZ <= minimumZ) { |
| 8419 }, | 4729 this._applyOverlayZ(overlay, minimumZ); |
| 8420 | 4730 } |
| 8421 /** | 4731 this._overlays.splice(insertionIndex, 0, overlay); |
| 8422 * Returns the current overlay. | 4732 this.trackBackdrop(); |
| 8423 * @return {Element|undefined} | 4733 }, |
| 8424 */ | 4734 removeOverlay: function(overlay) { |
| 8425 currentOverlay: function() { | 4735 var i = this._overlays.indexOf(overlay); |
| 8426 var i = this._overlays.length - 1; | 4736 if (i === -1) { |
| 8427 return this._overlays[i]; | 4737 return; |
| 8428 }, | 4738 } |
| 8429 | 4739 this._overlays.splice(i, 1); |
| 8430 /** | 4740 this.trackBackdrop(); |
| 8431 * Returns the current overlay z-index. | 4741 }, |
| 8432 * @return {number} | 4742 currentOverlay: function() { |
| 8433 */ | 4743 var i = this._overlays.length - 1; |
| 8434 currentOverlayZ: function() { | 4744 return this._overlays[i]; |
| 8435 return this._getZ(this.currentOverlay()); | 4745 }, |
| 8436 }, | 4746 currentOverlayZ: function() { |
| 8437 | 4747 return this._getZ(this.currentOverlay()); |
| 8438 /** | 4748 }, |
| 8439 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | 4749 ensureMinimumZ: function(minimumZ) { |
| 8440 * This does not effect the z-index of any existing overlays. | 4750 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
| 8441 * @param {number} minimumZ | 4751 }, |
| 8442 */ | 4752 focusOverlay: function() { |
| 8443 ensureMinimumZ: function(minimumZ) { | 4753 var current = this.currentOverlay(); |
| 8444 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 4754 if (current) { |
| 8445 }, | 4755 current._applyFocus(); |
| 8446 | 4756 } |
| 8447 focusOverlay: function() { | 4757 }, |
| 8448 var current = /** @type {?} */ (this.currentOverlay()); | 4758 trackBackdrop: function() { |
| 8449 if (current) { | 4759 var overlay = this._overlayWithBackdrop(); |
| 8450 current._applyFocus(); | 4760 if (!overlay && !this._backdropElement) { |
| 8451 } | 4761 return; |
| 8452 }, | 4762 } |
| 8453 | 4763 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
| 8454 /** | 4764 this.backdropElement.opened = !!overlay; |
| 8455 * Updates the backdrop z-index. | 4765 }, |
| 8456 */ | 4766 getBackdrops: function() { |
| 8457 trackBackdrop: function() { | 4767 var backdrops = []; |
| 8458 var overlay = this._overlayWithBackdrop(); | 4768 for (var i = 0; i < this._overlays.length; i++) { |
| 8459 // Avoid creating the backdrop if there is no overlay with backdrop. | 4769 if (this._overlays[i].withBackdrop) { |
| 8460 if (!overlay && !this._backdropElement) { | 4770 backdrops.push(this._overlays[i]); |
| 8461 return; | 4771 } |
| 8462 } | 4772 } |
| 8463 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 4773 return backdrops; |
| 8464 this.backdropElement.opened = !!overlay; | 4774 }, |
| 8465 }, | 4775 backdropZ: function() { |
| 8466 | 4776 return this._getZ(this._overlayWithBackdrop()) - 1; |
| 8467 /** | 4777 }, |
| 8468 * @return {Array<Element>} | 4778 _overlayWithBackdrop: function() { |
| 8469 */ | 4779 for (var i = 0; i < this._overlays.length; i++) { |
| 8470 getBackdrops: function() { | 4780 if (this._overlays[i].withBackdrop) { |
| 8471 var backdrops = []; | 4781 return this._overlays[i]; |
| 8472 for (var i = 0; i < this._overlays.length; i++) { | 4782 } |
| 8473 if (this._overlays[i].withBackdrop) { | 4783 } |
| 8474 backdrops.push(this._overlays[i]); | 4784 }, |
| 8475 } | 4785 _getZ: function(overlay) { |
| 8476 } | 4786 var z = this._minimumZ; |
| 8477 return backdrops; | 4787 if (overlay) { |
| 8478 }, | 4788 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z
Index); |
| 8479 | 4789 if (z1 === z1) { |
| 8480 /** | 4790 z = z1; |
| 8481 * Returns the z-index for the backdrop. | 4791 } |
| 8482 * @return {number} | 4792 } |
| 8483 */ | 4793 return z; |
| 8484 backdropZ: function() { | 4794 }, |
| 8485 return this._getZ(this._overlayWithBackdrop()) - 1; | 4795 _setZ: function(element, z) { |
| 8486 }, | 4796 element.style.zIndex = z; |
| 8487 | 4797 }, |
| 8488 /** | 4798 _applyOverlayZ: function(overlay, aboveZ) { |
| 8489 * Returns the first opened overlay that has a backdrop. | 4799 this._setZ(overlay, aboveZ + 2); |
| 8490 * @return {Element|undefined} | 4800 }, |
| 8491 * @private | 4801 _overlayInPath: function(path) { |
| 8492 */ | 4802 path = path || []; |
| 8493 _overlayWithBackdrop: function() { | 4803 for (var i = 0; i < path.length; i++) { |
| 8494 for (var i = 0; i < this._overlays.length; i++) { | 4804 if (path[i]._manager === this) { |
| 8495 if (this._overlays[i].withBackdrop) { | 4805 return path[i]; |
| 8496 return this._overlays[i]; | 4806 } |
| 8497 } | 4807 } |
| 8498 } | 4808 }, |
| 8499 }, | 4809 _onCaptureClick: function(event) { |
| 8500 | 4810 var overlay = this.currentOverlay(); |
| 8501 /** | 4811 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
| 8502 * Calculates the minimum z-index for the overlay. | 4812 overlay._onCaptureClick(event); |
| 8503 * @param {Element=} overlay | 4813 } |
| 8504 * @private | 4814 }, |
| 8505 */ | 4815 _onCaptureFocus: function(event) { |
| 8506 _getZ: function(overlay) { | 4816 var overlay = this.currentOverlay(); |
| 8507 var z = this._minimumZ; | 4817 if (overlay) { |
| 8508 if (overlay) { | 4818 overlay._onCaptureFocus(event); |
| 8509 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 4819 } |
| 8510 // Check if is a number | 4820 }, |
| 8511 // Number.isNaN not supported in IE 10+ | 4821 _onCaptureKeyDown: function(event) { |
| 8512 if (z1 === z1) { | 4822 var overlay = this.currentOverlay(); |
| 8513 z = z1; | 4823 if (overlay) { |
| 8514 } | 4824 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
| 8515 } | 4825 overlay._onCaptureEsc(event); |
| 8516 return z; | 4826 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't
ab')) { |
| 8517 }, | 4827 overlay._onCaptureTab(event); |
| 8518 | 4828 } |
| 8519 /** | 4829 } |
| 8520 * @param {!Element} element | 4830 }, |
| 8521 * @param {number|string} z | 4831 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
| 8522 * @private | 4832 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
| 8523 */ | 4833 } |
| 8524 _setZ: function(element, z) { | 4834 }; |
| 8525 element.style.zIndex = z; | 4835 |
| 8526 }, | 4836 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
| 8527 | 4837 |
| 8528 /** | |
| 8529 * @param {!Element} overlay | |
| 8530 * @param {number} aboveZ | |
| 8531 * @private | |
| 8532 */ | |
| 8533 _applyOverlayZ: function(overlay, aboveZ) { | |
| 8534 this._setZ(overlay, aboveZ + 2); | |
| 8535 }, | |
| 8536 | |
| 8537 /** | |
| 8538 * Returns the deepest overlay in the path. | |
| 8539 * @param {Array<Element>=} path | |
| 8540 * @return {Element|undefined} | |
| 8541 * @suppress {missingProperties} | |
| 8542 * @private | |
| 8543 */ | |
| 8544 _overlayInPath: function(path) { | |
| 8545 path = path || []; | |
| 8546 for (var i = 0; i < path.length; i++) { | |
| 8547 if (path[i]._manager === this) { | |
| 8548 return path[i]; | |
| 8549 } | |
| 8550 } | |
| 8551 }, | |
| 8552 | |
| 8553 /** | |
| 8554 * Ensures the click event is delegated to the right overlay. | |
| 8555 * @param {!Event} event | |
| 8556 * @private | |
| 8557 */ | |
| 8558 _onCaptureClick: function(event) { | |
| 8559 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8560 // Check if clicked outside of top overlay. | |
| 8561 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | |
| 8562 overlay._onCaptureClick(event); | |
| 8563 } | |
| 8564 }, | |
| 8565 | |
| 8566 /** | |
| 8567 * Ensures the focus event is delegated to the right overlay. | |
| 8568 * @param {!Event} event | |
| 8569 * @private | |
| 8570 */ | |
| 8571 _onCaptureFocus: function(event) { | |
| 8572 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8573 if (overlay) { | |
| 8574 overlay._onCaptureFocus(event); | |
| 8575 } | |
| 8576 }, | |
| 8577 | |
| 8578 /** | |
| 8579 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
| 8580 * @param {!Event} event | |
| 8581 * @private | |
| 8582 */ | |
| 8583 _onCaptureKeyDown: function(event) { | |
| 8584 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8585 if (overlay) { | |
| 8586 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | |
| 8587 overlay._onCaptureEsc(event); | |
| 8588 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | |
| 8589 overlay._onCaptureTab(event); | |
| 8590 } | |
| 8591 } | |
| 8592 }, | |
| 8593 | |
| 8594 /** | |
| 8595 * Returns if the overlay1 should be behind overlay2. | |
| 8596 * @param {!Element} overlay1 | |
| 8597 * @param {!Element} overlay2 | |
| 8598 * @return {boolean} | |
| 8599 * @suppress {missingProperties} | |
| 8600 * @private | |
| 8601 */ | |
| 8602 _shouldBeBehindOverlay: function(overlay1, overlay2) { | |
| 8603 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | |
| 8604 } | |
| 8605 }; | |
| 8606 | |
| 8607 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | |
| 8608 (function() { | 4838 (function() { |
| 8609 'use strict'; | 4839 'use strict'; |
| 8610 | |
| 8611 /** | |
| 8612 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
| 8613 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
| 8614 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
| 8615 | |
| 8616 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
| 8617 for an example. | |
| 8618 | |
| 8619 ### Closing and canceling | |
| 8620 | |
| 8621 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
| 8622 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
| 8623 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
| 8624 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
| 8625 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
| 8626 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
| 8627 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
| 8628 | |
| 8629 ### Positioning | |
| 8630 | |
| 8631 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
| 8632 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
| 8633 | |
| 8634 ### Backdrop | |
| 8635 | |
| 8636 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
| 8637 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
| 8638 options. | |
| 8639 | |
| 8640 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
| 8641 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
| 8642 to achieve a different behavior. | |
| 8643 | |
| 8644 ### Limitations | |
| 8645 | |
| 8646 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
| 8647 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
| 8648 context. You should place this element as a child of `<body>` whenever possible. | |
| 8649 | |
| 8650 @demo demo/index.html | |
| 8651 @polymerBehavior Polymer.IronOverlayBehavior | |
| 8652 */ | |
| 8653 | |
| 8654 Polymer.IronOverlayBehaviorImpl = { | 4840 Polymer.IronOverlayBehaviorImpl = { |
| 8655 | |
| 8656 properties: { | 4841 properties: { |
| 8657 | |
| 8658 /** | |
| 8659 * True if the overlay is currently displayed. | |
| 8660 */ | |
| 8661 opened: { | 4842 opened: { |
| 8662 observer: '_openedChanged', | 4843 observer: '_openedChanged', |
| 8663 type: Boolean, | 4844 type: Boolean, |
| 8664 value: false, | 4845 value: false, |
| 8665 notify: true | 4846 notify: true |
| 8666 }, | 4847 }, |
| 8667 | |
| 8668 /** | |
| 8669 * True if the overlay was canceled when it was last closed. | |
| 8670 */ | |
| 8671 canceled: { | 4848 canceled: { |
| 8672 observer: '_canceledChanged', | 4849 observer: '_canceledChanged', |
| 8673 readOnly: true, | 4850 readOnly: true, |
| 8674 type: Boolean, | 4851 type: Boolean, |
| 8675 value: false | 4852 value: false |
| 8676 }, | 4853 }, |
| 8677 | |
| 8678 /** | |
| 8679 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
| 8680 * within the light DOM of the overlay. | |
| 8681 */ | |
| 8682 withBackdrop: { | 4854 withBackdrop: { |
| 8683 observer: '_withBackdropChanged', | 4855 observer: '_withBackdropChanged', |
| 8684 type: Boolean | 4856 type: Boolean |
| 8685 }, | 4857 }, |
| 8686 | |
| 8687 /** | |
| 8688 * Set to true to disable auto-focusing the overlay or child nodes with | |
| 8689 * the `autofocus` attribute` when the overlay is opened. | |
| 8690 */ | |
| 8691 noAutoFocus: { | 4858 noAutoFocus: { |
| 8692 type: Boolean, | 4859 type: Boolean, |
| 8693 value: false | 4860 value: false |
| 8694 }, | 4861 }, |
| 8695 | |
| 8696 /** | |
| 8697 * Set to true to disable canceling the overlay with the ESC key. | |
| 8698 */ | |
| 8699 noCancelOnEscKey: { | 4862 noCancelOnEscKey: { |
| 8700 type: Boolean, | 4863 type: Boolean, |
| 8701 value: false | 4864 value: false |
| 8702 }, | 4865 }, |
| 8703 | |
| 8704 /** | |
| 8705 * Set to true to disable canceling the overlay by clicking outside it. | |
| 8706 */ | |
| 8707 noCancelOnOutsideClick: { | 4866 noCancelOnOutsideClick: { |
| 8708 type: Boolean, | 4867 type: Boolean, |
| 8709 value: false | 4868 value: false |
| 8710 }, | 4869 }, |
| 8711 | |
| 8712 /** | |
| 8713 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
| 8714 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
| 8715 * behavior can provide other reasons in addition to `canceled`. | |
| 8716 */ | |
| 8717 closingReason: { | 4870 closingReason: { |
| 8718 // was a getter before, but needs to be a property so other | |
| 8719 // behaviors can override this. | |
| 8720 type: Object | 4871 type: Object |
| 8721 }, | 4872 }, |
| 8722 | |
| 8723 /** | |
| 8724 * Set to true to enable restoring of focus when overlay is closed. | |
| 8725 */ | |
| 8726 restoreFocusOnClose: { | 4873 restoreFocusOnClose: { |
| 8727 type: Boolean, | 4874 type: Boolean, |
| 8728 value: false | 4875 value: false |
| 8729 }, | 4876 }, |
| 8730 | |
| 8731 /** | |
| 8732 * Set to true to keep overlay always on top. | |
| 8733 */ | |
| 8734 alwaysOnTop: { | 4877 alwaysOnTop: { |
| 8735 type: Boolean | 4878 type: Boolean |
| 8736 }, | 4879 }, |
| 8737 | |
| 8738 /** | |
| 8739 * Shortcut to access to the overlay manager. | |
| 8740 * @private | |
| 8741 * @type {Polymer.IronOverlayManagerClass} | |
| 8742 */ | |
| 8743 _manager: { | 4880 _manager: { |
| 8744 type: Object, | 4881 type: Object, |
| 8745 value: Polymer.IronOverlayManager | 4882 value: Polymer.IronOverlayManager |
| 8746 }, | 4883 }, |
| 8747 | |
| 8748 /** | |
| 8749 * The node being focused. | |
| 8750 * @type {?Node} | |
| 8751 */ | |
| 8752 _focusedChild: { | 4884 _focusedChild: { |
| 8753 type: Object | 4885 type: Object |
| 8754 } | 4886 } |
| 8755 | 4887 }, |
| 8756 }, | |
| 8757 | |
| 8758 listeners: { | 4888 listeners: { |
| 8759 'iron-resize': '_onIronResize' | 4889 'iron-resize': '_onIronResize' |
| 8760 }, | 4890 }, |
| 8761 | |
| 8762 /** | |
| 8763 * The backdrop element. | |
| 8764 * @type {Element} | |
| 8765 */ | |
| 8766 get backdropElement() { | 4891 get backdropElement() { |
| 8767 return this._manager.backdropElement; | 4892 return this._manager.backdropElement; |
| 8768 }, | 4893 }, |
| 8769 | |
| 8770 /** | |
| 8771 * Returns the node to give focus to. | |
| 8772 * @type {Node} | |
| 8773 */ | |
| 8774 get _focusNode() { | 4894 get _focusNode() { |
| 8775 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 4895 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
| 8776 }, | 4896 }, |
| 8777 | |
| 8778 /** | |
| 8779 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
| 8780 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
| 8781 * to wrap the focus for overlays `with-backdrop`. | |
| 8782 * | |
| 8783 * If you know what is your content (specifically the first and last focusab
le children), | |
| 8784 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
| 8785 * @type {Array<Node>} | |
| 8786 * @protected | |
| 8787 */ | |
| 8788 get _focusableNodes() { | 4897 get _focusableNodes() { |
| 8789 // Elements that can be focused even if they have [disabled] attribute. | 4898 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin
dex]', '[contentEditable=true]' ]; |
| 8790 var FOCUSABLE_WITH_DISABLED = [ | 4899 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button'
]; |
| 8791 'a[href]', | 4900 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + ':
not([tabindex="-1"]),' + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([
tabindex="-1"]),') + ':not([disabled]):not([tabindex="-1"])'; |
| 8792 'area[href]', | |
| 8793 'iframe', | |
| 8794 '[tabindex]', | |
| 8795 '[contentEditable=true]' | |
| 8796 ]; | |
| 8797 | |
| 8798 // Elements that cannot be focused if they have [disabled] attribute. | |
| 8799 var FOCUSABLE_WITHOUT_DISABLED = [ | |
| 8800 'input', | |
| 8801 'select', | |
| 8802 'textarea', | |
| 8803 'button' | |
| 8804 ]; | |
| 8805 | |
| 8806 // Discard elements with tabindex=-1 (makes them not focusable). | |
| 8807 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | |
| 8808 ':not([tabindex="-1"]),' + | |
| 8809 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | |
| 8810 ':not([disabled]):not([tabindex="-1"])'; | |
| 8811 | |
| 8812 var focusables = Polymer.dom(this).querySelectorAll(selector); | 4901 var focusables = Polymer.dom(this).querySelectorAll(selector); |
| 8813 if (this.tabIndex >= 0) { | 4902 if (this.tabIndex >= 0) { |
| 8814 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
| 8815 // and the overlay should be the first of the list. | |
| 8816 focusables.splice(0, 0, this); | 4903 focusables.splice(0, 0, this); |
| 8817 } | 4904 } |
| 8818 // Sort by tabindex. | 4905 return focusables.sort(function(a, b) { |
| 8819 return focusables.sort(function (a, b) { | |
| 8820 if (a.tabIndex === b.tabIndex) { | 4906 if (a.tabIndex === b.tabIndex) { |
| 8821 return 0; | 4907 return 0; |
| 8822 } | 4908 } |
| 8823 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 4909 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
| 8824 return 1; | 4910 return 1; |
| 8825 } | 4911 } |
| 8826 return -1; | 4912 return -1; |
| 8827 }); | 4913 }); |
| 8828 }, | 4914 }, |
| 8829 | |
| 8830 ready: function() { | 4915 ready: function() { |
| 8831 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
| 8832 this.__isAnimating = false; | 4916 this.__isAnimating = false; |
| 8833 // with-backdrop needs tabindex to be set in order to trap the focus. | |
| 8834 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
| 8835 this.__shouldRemoveTabIndex = false; | 4917 this.__shouldRemoveTabIndex = false; |
| 8836 // Used for wrapping the focus on TAB / Shift+TAB. | |
| 8837 this.__firstFocusableNode = this.__lastFocusableNode = null; | 4918 this.__firstFocusableNode = this.__lastFocusableNode = null; |
| 8838 // Used by __onNextAnimationFrame to cancel any previous callback. | |
| 8839 this.__raf = null; | 4919 this.__raf = null; |
| 8840 // Focused node before overlay gets opened. Can be restored on close. | |
| 8841 this.__restoreFocusNode = null; | 4920 this.__restoreFocusNode = null; |
| 8842 this._ensureSetup(); | 4921 this._ensureSetup(); |
| 8843 }, | 4922 }, |
| 8844 | |
| 8845 attached: function() { | 4923 attached: function() { |
| 8846 // Call _openedChanged here so that position can be computed correctly. | |
| 8847 if (this.opened) { | 4924 if (this.opened) { |
| 8848 this._openedChanged(this.opened); | 4925 this._openedChanged(this.opened); |
| 8849 } | 4926 } |
| 8850 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 4927 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
| 8851 }, | 4928 }, |
| 8852 | |
| 8853 detached: function() { | 4929 detached: function() { |
| 8854 Polymer.dom(this).unobserveNodes(this._observer); | 4930 Polymer.dom(this).unobserveNodes(this._observer); |
| 8855 this._observer = null; | 4931 this._observer = null; |
| 8856 if (this.__raf) { | 4932 if (this.__raf) { |
| 8857 window.cancelAnimationFrame(this.__raf); | 4933 window.cancelAnimationFrame(this.__raf); |
| 8858 this.__raf = null; | 4934 this.__raf = null; |
| 8859 } | 4935 } |
| 8860 this._manager.removeOverlay(this); | 4936 this._manager.removeOverlay(this); |
| 8861 }, | 4937 }, |
| 8862 | |
| 8863 /** | |
| 8864 * Toggle the opened state of the overlay. | |
| 8865 */ | |
| 8866 toggle: function() { | 4938 toggle: function() { |
| 8867 this._setCanceled(false); | 4939 this._setCanceled(false); |
| 8868 this.opened = !this.opened; | 4940 this.opened = !this.opened; |
| 8869 }, | 4941 }, |
| 8870 | |
| 8871 /** | |
| 8872 * Open the overlay. | |
| 8873 */ | |
| 8874 open: function() { | 4942 open: function() { |
| 8875 this._setCanceled(false); | 4943 this._setCanceled(false); |
| 8876 this.opened = true; | 4944 this.opened = true; |
| 8877 }, | 4945 }, |
| 8878 | |
| 8879 /** | |
| 8880 * Close the overlay. | |
| 8881 */ | |
| 8882 close: function() { | 4946 close: function() { |
| 8883 this._setCanceled(false); | 4947 this._setCanceled(false); |
| 8884 this.opened = false; | 4948 this.opened = false; |
| 8885 }, | 4949 }, |
| 8886 | |
| 8887 /** | |
| 8888 * Cancels the overlay. | |
| 8889 * @param {Event=} event The original event | |
| 8890 */ | |
| 8891 cancel: function(event) { | 4950 cancel: function(event) { |
| 8892 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 4951 var cancelEvent = this.fire('iron-overlay-canceled', event, { |
| 4952 cancelable: true |
| 4953 }); |
| 8893 if (cancelEvent.defaultPrevented) { | 4954 if (cancelEvent.defaultPrevented) { |
| 8894 return; | 4955 return; |
| 8895 } | 4956 } |
| 8896 | |
| 8897 this._setCanceled(true); | 4957 this._setCanceled(true); |
| 8898 this.opened = false; | 4958 this.opened = false; |
| 8899 }, | 4959 }, |
| 8900 | |
| 8901 _ensureSetup: function() { | 4960 _ensureSetup: function() { |
| 8902 if (this._overlaySetup) { | 4961 if (this._overlaySetup) { |
| 8903 return; | 4962 return; |
| 8904 } | 4963 } |
| 8905 this._overlaySetup = true; | 4964 this._overlaySetup = true; |
| 8906 this.style.outline = 'none'; | 4965 this.style.outline = 'none'; |
| 8907 this.style.display = 'none'; | 4966 this.style.display = 'none'; |
| 8908 }, | 4967 }, |
| 8909 | |
| 8910 /** | |
| 8911 * Called when `opened` changes. | |
| 8912 * @param {boolean=} opened | |
| 8913 * @protected | |
| 8914 */ | |
| 8915 _openedChanged: function(opened) { | 4968 _openedChanged: function(opened) { |
| 8916 if (opened) { | 4969 if (opened) { |
| 8917 this.removeAttribute('aria-hidden'); | 4970 this.removeAttribute('aria-hidden'); |
| 8918 } else { | 4971 } else { |
| 8919 this.setAttribute('aria-hidden', 'true'); | 4972 this.setAttribute('aria-hidden', 'true'); |
| 8920 } | 4973 } |
| 8921 | |
| 8922 // Defer any animation-related code on attached | |
| 8923 // (_openedChanged gets called again on attached). | |
| 8924 if (!this.isAttached) { | 4974 if (!this.isAttached) { |
| 8925 return; | 4975 return; |
| 8926 } | 4976 } |
| 8927 | |
| 8928 this.__isAnimating = true; | 4977 this.__isAnimating = true; |
| 8929 | |
| 8930 // Use requestAnimationFrame for non-blocking rendering. | |
| 8931 this.__onNextAnimationFrame(this.__openedChanged); | 4978 this.__onNextAnimationFrame(this.__openedChanged); |
| 8932 }, | 4979 }, |
| 8933 | |
| 8934 _canceledChanged: function() { | 4980 _canceledChanged: function() { |
| 8935 this.closingReason = this.closingReason || {}; | 4981 this.closingReason = this.closingReason || {}; |
| 8936 this.closingReason.canceled = this.canceled; | 4982 this.closingReason.canceled = this.canceled; |
| 8937 }, | 4983 }, |
| 8938 | |
| 8939 _withBackdropChanged: function() { | 4984 _withBackdropChanged: function() { |
| 8940 // If tabindex is already set, no need to override it. | |
| 8941 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 4985 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
| 8942 this.setAttribute('tabindex', '-1'); | 4986 this.setAttribute('tabindex', '-1'); |
| 8943 this.__shouldRemoveTabIndex = true; | 4987 this.__shouldRemoveTabIndex = true; |
| 8944 } else if (this.__shouldRemoveTabIndex) { | 4988 } else if (this.__shouldRemoveTabIndex) { |
| 8945 this.removeAttribute('tabindex'); | 4989 this.removeAttribute('tabindex'); |
| 8946 this.__shouldRemoveTabIndex = false; | 4990 this.__shouldRemoveTabIndex = false; |
| 8947 } | 4991 } |
| 8948 if (this.opened && this.isAttached) { | 4992 if (this.opened && this.isAttached) { |
| 8949 this._manager.trackBackdrop(); | 4993 this._manager.trackBackdrop(); |
| 8950 } | 4994 } |
| 8951 }, | 4995 }, |
| 8952 | |
| 8953 /** | |
| 8954 * tasks which must occur before opening; e.g. making the element visible. | |
| 8955 * @protected | |
| 8956 */ | |
| 8957 _prepareRenderOpened: function() { | 4996 _prepareRenderOpened: function() { |
| 8958 // Store focused node. | |
| 8959 this.__restoreFocusNode = this._manager.deepActiveElement; | 4997 this.__restoreFocusNode = this._manager.deepActiveElement; |
| 8960 | |
| 8961 // Needed to calculate the size of the overlay so that transitions on its
size | |
| 8962 // will have the correct starting points. | |
| 8963 this._preparePositioning(); | 4998 this._preparePositioning(); |
| 8964 this.refit(); | 4999 this.refit(); |
| 8965 this._finishPositioning(); | 5000 this._finishPositioning(); |
| 8966 | |
| 8967 // Safari will apply the focus to the autofocus element when displayed | |
| 8968 // for the first time, so we make sure to return the focus where it was. | |
| 8969 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 5001 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
| 8970 this._focusNode.blur(); | 5002 this._focusNode.blur(); |
| 8971 this.__restoreFocusNode.focus(); | 5003 this.__restoreFocusNode.focus(); |
| 8972 } | 5004 } |
| 8973 }, | 5005 }, |
| 8974 | |
| 8975 /** | |
| 8976 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
| 8977 * @protected | |
| 8978 */ | |
| 8979 _renderOpened: function() { | 5006 _renderOpened: function() { |
| 8980 this._finishRenderOpened(); | 5007 this._finishRenderOpened(); |
| 8981 }, | 5008 }, |
| 8982 | |
| 8983 /** | |
| 8984 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
| 8985 * @protected | |
| 8986 */ | |
| 8987 _renderClosed: function() { | 5009 _renderClosed: function() { |
| 8988 this._finishRenderClosed(); | 5010 this._finishRenderClosed(); |
| 8989 }, | 5011 }, |
| 8990 | |
| 8991 /** | |
| 8992 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
| 8993 * @protected | |
| 8994 */ | |
| 8995 _finishRenderOpened: function() { | 5012 _finishRenderOpened: function() { |
| 8996 this.notifyResize(); | 5013 this.notifyResize(); |
| 8997 this.__isAnimating = false; | 5014 this.__isAnimating = false; |
| 8998 | |
| 8999 // Store it so we don't query too much. | |
| 9000 var focusableNodes = this._focusableNodes; | 5015 var focusableNodes = this._focusableNodes; |
| 9001 this.__firstFocusableNode = focusableNodes[0]; | 5016 this.__firstFocusableNode = focusableNodes[0]; |
| 9002 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 5017 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
| 9003 | |
| 9004 this.fire('iron-overlay-opened'); | 5018 this.fire('iron-overlay-opened'); |
| 9005 }, | 5019 }, |
| 9006 | |
| 9007 /** | |
| 9008 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
| 9009 * @protected | |
| 9010 */ | |
| 9011 _finishRenderClosed: function() { | 5020 _finishRenderClosed: function() { |
| 9012 // Hide the overlay. | |
| 9013 this.style.display = 'none'; | 5021 this.style.display = 'none'; |
| 9014 // Reset z-index only at the end of the animation. | |
| 9015 this.style.zIndex = ''; | 5022 this.style.zIndex = ''; |
| 9016 this.notifyResize(); | 5023 this.notifyResize(); |
| 9017 this.__isAnimating = false; | 5024 this.__isAnimating = false; |
| 9018 this.fire('iron-overlay-closed', this.closingReason); | 5025 this.fire('iron-overlay-closed', this.closingReason); |
| 9019 }, | 5026 }, |
| 9020 | |
| 9021 _preparePositioning: function() { | 5027 _preparePositioning: function() { |
| 9022 this.style.transition = this.style.webkitTransition = 'none'; | 5028 this.style.transition = this.style.webkitTransition = 'none'; |
| 9023 this.style.transform = this.style.webkitTransform = 'none'; | 5029 this.style.transform = this.style.webkitTransform = 'none'; |
| 9024 this.style.display = ''; | 5030 this.style.display = ''; |
| 9025 }, | 5031 }, |
| 9026 | |
| 9027 _finishPositioning: function() { | 5032 _finishPositioning: function() { |
| 9028 // First, make it invisible & reactivate animations. | |
| 9029 this.style.display = 'none'; | 5033 this.style.display = 'none'; |
| 9030 // Force reflow before re-enabling animations so that they don't start. | |
| 9031 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
| 9032 this.scrollTop = this.scrollTop; | 5034 this.scrollTop = this.scrollTop; |
| 9033 this.style.transition = this.style.webkitTransition = ''; | 5035 this.style.transition = this.style.webkitTransition = ''; |
| 9034 this.style.transform = this.style.webkitTransform = ''; | 5036 this.style.transform = this.style.webkitTransform = ''; |
| 9035 // Now that animations are enabled, make it visible again | |
| 9036 this.style.display = ''; | 5037 this.style.display = ''; |
| 9037 // Force reflow, so that following animations are properly started. | |
| 9038 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
| 9039 this.scrollTop = this.scrollTop; | 5038 this.scrollTop = this.scrollTop; |
| 9040 }, | 5039 }, |
| 9041 | |
| 9042 /** | |
| 9043 * Applies focus according to the opened state. | |
| 9044 * @protected | |
| 9045 */ | |
| 9046 _applyFocus: function() { | 5040 _applyFocus: function() { |
| 9047 if (this.opened) { | 5041 if (this.opened) { |
| 9048 if (!this.noAutoFocus) { | 5042 if (!this.noAutoFocus) { |
| 9049 this._focusNode.focus(); | 5043 this._focusNode.focus(); |
| 9050 } | 5044 } |
| 9051 } | 5045 } else { |
| 9052 else { | |
| 9053 this._focusNode.blur(); | 5046 this._focusNode.blur(); |
| 9054 this._focusedChild = null; | 5047 this._focusedChild = null; |
| 9055 // Restore focus. | |
| 9056 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 5048 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
| 9057 this.__restoreFocusNode.focus(); | 5049 this.__restoreFocusNode.focus(); |
| 9058 } | 5050 } |
| 9059 this.__restoreFocusNode = null; | 5051 this.__restoreFocusNode = null; |
| 9060 // If many overlays get closed at the same time, one of them would still | |
| 9061 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
| 9062 // infinitely, so we check for this not to be the current overlay. | |
| 9063 var currentOverlay = this._manager.currentOverlay(); | 5052 var currentOverlay = this._manager.currentOverlay(); |
| 9064 if (currentOverlay && this !== currentOverlay) { | 5053 if (currentOverlay && this !== currentOverlay) { |
| 9065 currentOverlay._applyFocus(); | 5054 currentOverlay._applyFocus(); |
| 9066 } | 5055 } |
| 9067 } | 5056 } |
| 9068 }, | 5057 }, |
| 9069 | |
| 9070 /** | |
| 9071 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
| 9072 * @param {!Event} event | |
| 9073 * @protected | |
| 9074 */ | |
| 9075 _onCaptureClick: function(event) { | 5058 _onCaptureClick: function(event) { |
| 9076 if (!this.noCancelOnOutsideClick) { | 5059 if (!this.noCancelOnOutsideClick) { |
| 9077 this.cancel(event); | 5060 this.cancel(event); |
| 9078 } | 5061 } |
| 9079 }, | 5062 }, |
| 9080 | 5063 _onCaptureFocus: function(event) { |
| 9081 /** | |
| 9082 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
| 9083 * @param {!Event} event | |
| 9084 * @protected | |
| 9085 */ | |
| 9086 _onCaptureFocus: function (event) { | |
| 9087 if (!this.withBackdrop) { | 5064 if (!this.withBackdrop) { |
| 9088 return; | 5065 return; |
| 9089 } | 5066 } |
| 9090 var path = Polymer.dom(event).path; | 5067 var path = Polymer.dom(event).path; |
| 9091 if (path.indexOf(this) === -1) { | 5068 if (path.indexOf(this) === -1) { |
| 9092 event.stopPropagation(); | 5069 event.stopPropagation(); |
| 9093 this._applyFocus(); | 5070 this._applyFocus(); |
| 9094 } else { | 5071 } else { |
| 9095 this._focusedChild = path[0]; | 5072 this._focusedChild = path[0]; |
| 9096 } | 5073 } |
| 9097 }, | 5074 }, |
| 9098 | |
| 9099 /** | |
| 9100 * Handles the ESC key event and cancels (closes) the overlay. | |
| 9101 * @param {!Event} event | |
| 9102 * @protected | |
| 9103 */ | |
| 9104 _onCaptureEsc: function(event) { | 5075 _onCaptureEsc: function(event) { |
| 9105 if (!this.noCancelOnEscKey) { | 5076 if (!this.noCancelOnEscKey) { |
| 9106 this.cancel(event); | 5077 this.cancel(event); |
| 9107 } | 5078 } |
| 9108 }, | 5079 }, |
| 9109 | |
| 9110 /** | |
| 9111 * Handles TAB key events to track focus changes. | |
| 9112 * Will wrap focus for overlays withBackdrop. | |
| 9113 * @param {!Event} event | |
| 9114 * @protected | |
| 9115 */ | |
| 9116 _onCaptureTab: function(event) { | 5080 _onCaptureTab: function(event) { |
| 9117 if (!this.withBackdrop) { | 5081 if (!this.withBackdrop) { |
| 9118 return; | 5082 return; |
| 9119 } | 5083 } |
| 9120 // TAB wraps from last to first focusable. | |
| 9121 // Shift + TAB wraps from first to last focusable. | |
| 9122 var shift = event.shiftKey; | 5084 var shift = event.shiftKey; |
| 9123 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 5085 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
| 9124 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 5086 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
| 9125 var shouldWrap = false; | 5087 var shouldWrap = false; |
| 9126 if (nodeToCheck === nodeToSet) { | 5088 if (nodeToCheck === nodeToSet) { |
| 9127 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
| 9128 // with 0 or 1 focusables; in either case we still need to trap the | |
| 9129 // focus within the overlay. | |
| 9130 shouldWrap = true; | 5089 shouldWrap = true; |
| 9131 } else { | 5090 } else { |
| 9132 // In dom=shadow, the manager will receive focus changes on the main | |
| 9133 // root but not the ones within other shadow roots, so we can't rely on | |
| 9134 // _focusedChild, but we should check the deepest active element. | |
| 9135 var focusedNode = this._manager.deepActiveElement; | 5091 var focusedNode = this._manager.deepActiveElement; |
| 9136 // If the active element is not the nodeToCheck but the overlay itself, | 5092 shouldWrap = focusedNode === nodeToCheck || focusedNode === this; |
| 9137 // it means the focus is about to go outside the overlay, hence we | |
| 9138 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
| 9139 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | |
| 9140 } | 5093 } |
| 9141 | |
| 9142 if (shouldWrap) { | 5094 if (shouldWrap) { |
| 9143 // When the overlay contains the last focusable element of the document | |
| 9144 // and it's already focused, pressing TAB would move the focus outside | |
| 9145 // the document (e.g. to the browser search bar). Similarly, when the | |
| 9146 // overlay contains the first focusable element of the document and it's | |
| 9147 // already focused, pressing Shift+TAB would move the focus outside the | |
| 9148 // document (e.g. to the browser search bar). | |
| 9149 // In both cases, we would not receive a focus event, but only a blur. | |
| 9150 // In order to achieve focus wrapping, we prevent this TAB event and | |
| 9151 // force the focus. This will also prevent the focus to temporarily move | |
| 9152 // outside the overlay, which might cause scrolling. | |
| 9153 event.preventDefault(); | 5095 event.preventDefault(); |
| 9154 this._focusedChild = nodeToSet; | 5096 this._focusedChild = nodeToSet; |
| 9155 this._applyFocus(); | 5097 this._applyFocus(); |
| 9156 } | 5098 } |
| 9157 }, | 5099 }, |
| 9158 | |
| 9159 /** | |
| 9160 * Refits if the overlay is opened and not animating. | |
| 9161 * @protected | |
| 9162 */ | |
| 9163 _onIronResize: function() { | 5100 _onIronResize: function() { |
| 9164 if (this.opened && !this.__isAnimating) { | 5101 if (this.opened && !this.__isAnimating) { |
| 9165 this.__onNextAnimationFrame(this.refit); | 5102 this.__onNextAnimationFrame(this.refit); |
| 9166 } | 5103 } |
| 9167 }, | 5104 }, |
| 9168 | |
| 9169 /** | |
| 9170 * Will call notifyResize if overlay is opened. | |
| 9171 * Can be overridden in order to avoid multiple observers on the same node. | |
| 9172 * @protected | |
| 9173 */ | |
| 9174 _onNodesChange: function() { | 5105 _onNodesChange: function() { |
| 9175 if (this.opened && !this.__isAnimating) { | 5106 if (this.opened && !this.__isAnimating) { |
| 9176 this.notifyResize(); | 5107 this.notifyResize(); |
| 9177 } | 5108 } |
| 9178 }, | 5109 }, |
| 9179 | |
| 9180 /** | |
| 9181 * Tasks executed when opened changes: prepare for the opening, move the | |
| 9182 * focus, update the manager, render opened/closed. | |
| 9183 * @private | |
| 9184 */ | |
| 9185 __openedChanged: function() { | 5110 __openedChanged: function() { |
| 9186 if (this.opened) { | 5111 if (this.opened) { |
| 9187 // Make overlay visible, then add it to the manager. | |
| 9188 this._prepareRenderOpened(); | 5112 this._prepareRenderOpened(); |
| 9189 this._manager.addOverlay(this); | 5113 this._manager.addOverlay(this); |
| 9190 // Move the focus to the child node with [autofocus]. | |
| 9191 this._applyFocus(); | 5114 this._applyFocus(); |
| 9192 | |
| 9193 this._renderOpened(); | 5115 this._renderOpened(); |
| 9194 } else { | 5116 } else { |
| 9195 // Remove overlay, then restore the focus before actually closing. | |
| 9196 this._manager.removeOverlay(this); | 5117 this._manager.removeOverlay(this); |
| 9197 this._applyFocus(); | 5118 this._applyFocus(); |
| 9198 | |
| 9199 this._renderClosed(); | 5119 this._renderClosed(); |
| 9200 } | 5120 } |
| 9201 }, | 5121 }, |
| 9202 | |
| 9203 /** | |
| 9204 * Executes a callback on the next animation frame, overriding any previous | |
| 9205 * callback awaiting for the next animation frame. e.g. | |
| 9206 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
| 9207 * `callback1` will never be invoked. | |
| 9208 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
| 9209 * @private | |
| 9210 */ | |
| 9211 __onNextAnimationFrame: function(callback) { | 5122 __onNextAnimationFrame: function(callback) { |
| 9212 if (this.__raf) { | 5123 if (this.__raf) { |
| 9213 window.cancelAnimationFrame(this.__raf); | 5124 window.cancelAnimationFrame(this.__raf); |
| 9214 } | 5125 } |
| 9215 var self = this; | 5126 var self = this; |
| 9216 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 5127 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
| 9217 self.__raf = null; | 5128 self.__raf = null; |
| 9218 callback.call(self); | 5129 callback.call(self); |
| 9219 }); | 5130 }); |
| 9220 } | 5131 } |
| 9221 | |
| 9222 }; | 5132 }; |
| 9223 | 5133 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable
Behavior, Polymer.IronOverlayBehaviorImpl ]; |
| 9224 /** @polymerBehavior */ | |
| 9225 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
| 9226 | |
| 9227 /** | |
| 9228 * Fired after the overlay opens. | |
| 9229 * @event iron-overlay-opened | |
| 9230 */ | |
| 9231 | |
| 9232 /** | |
| 9233 * Fired when the overlay is canceled, but before it is closed. | |
| 9234 * @event iron-overlay-canceled | |
| 9235 * @param {Event} event The closing of the overlay can be prevented | |
| 9236 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
| 9237 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
| 9238 */ | |
| 9239 | |
| 9240 /** | |
| 9241 * Fired after the overlay closes. | |
| 9242 * @event iron-overlay-closed | |
| 9243 * @param {Event} event The `event.detail` is the `closingReason` property | |
| 9244 * (contains `canceled`, whether the overlay was canceled). | |
| 9245 */ | |
| 9246 | |
| 9247 })(); | 5134 })(); |
| 9248 /** | 5135 |
| 9249 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | 5136 Polymer.NeonAnimatableBehavior = { |
| 9250 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | 5137 properties: { |
| 9251 * @polymerBehavior | 5138 animationConfig: { |
| 9252 */ | 5139 type: Object |
| 9253 Polymer.NeonAnimatableBehavior = { | 5140 }, |
| 9254 | 5141 entryAnimation: { |
| 9255 properties: { | 5142 observer: '_entryAnimationChanged', |
| 9256 | 5143 type: String |
| 9257 /** | 5144 }, |
| 9258 * Animation configuration. See README for more info. | 5145 exitAnimation: { |
| 9259 */ | 5146 observer: '_exitAnimationChanged', |
| 9260 animationConfig: { | 5147 type: String |
| 9261 type: Object | 5148 } |
| 9262 }, | 5149 }, |
| 9263 | 5150 _entryAnimationChanged: function() { |
| 9264 /** | 5151 this.animationConfig = this.animationConfig || {}; |
| 9265 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | 5152 this.animationConfig['entry'] = [ { |
| 9266 * manually if using this. The animated node is set to `this` if using thi
s property. | 5153 name: this.entryAnimation, |
| 9267 */ | 5154 node: this |
| 9268 entryAnimation: { | 5155 } ]; |
| 9269 observer: '_entryAnimationChanged', | 5156 }, |
| 9270 type: String | 5157 _exitAnimationChanged: function() { |
| 9271 }, | 5158 this.animationConfig = this.animationConfig || {}; |
| 9272 | 5159 this.animationConfig['exit'] = [ { |
| 9273 /** | 5160 name: this.exitAnimation, |
| 9274 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | 5161 node: this |
| 9275 * manually if using this. The animated node is set to `this` if using thi
s property. | 5162 } ]; |
| 9276 */ | 5163 }, |
| 9277 exitAnimation: { | 5164 _copyProperties: function(config1, config2) { |
| 9278 observer: '_exitAnimationChanged', | 5165 for (var property in config2) { |
| 9279 type: String | 5166 config1[property] = config2[property]; |
| 9280 } | 5167 } |
| 9281 | 5168 }, |
| 9282 }, | 5169 _cloneConfig: function(config) { |
| 9283 | 5170 var clone = { |
| 9284 _entryAnimationChanged: function() { | 5171 isClone: true |
| 9285 this.animationConfig = this.animationConfig || {}; | 5172 }; |
| 9286 this.animationConfig['entry'] = [{ | 5173 this._copyProperties(clone, config); |
| 9287 name: this.entryAnimation, | 5174 return clone; |
| 9288 node: this | 5175 }, |
| 9289 }]; | 5176 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
| 9290 }, | 5177 if (!this.animationConfig) { |
| 9291 | 5178 return; |
| 9292 _exitAnimationChanged: function() { | 5179 } |
| 9293 this.animationConfig = this.animationConfig || {}; | 5180 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun
ction') { |
| 9294 this.animationConfig['exit'] = [{ | 5181 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid
e of your components 'properties' object instead of outside of it.")); |
| 9295 name: this.exitAnimation, | 5182 return; |
| 9296 node: this | 5183 } |
| 9297 }]; | 5184 var thisConfig; |
| 9298 }, | 5185 if (type) { |
| 9299 | 5186 thisConfig = this.animationConfig[type]; |
| 9300 _copyProperties: function(config1, config2) { | 5187 } else { |
| 9301 // shallowly copy properties from config2 to config1 | 5188 thisConfig = this.animationConfig; |
| 9302 for (var property in config2) { | 5189 } |
| 9303 config1[property] = config2[property]; | 5190 if (!Array.isArray(thisConfig)) { |
| 9304 } | 5191 thisConfig = [ thisConfig ]; |
| 9305 }, | 5192 } |
| 9306 | 5193 if (thisConfig) { |
| 9307 _cloneConfig: function(config) { | 5194 for (var config, index = 0; config = thisConfig[index]; index++) { |
| 9308 var clone = { | 5195 if (config.animatable) { |
| 9309 isClone: true | 5196 config.animatable._getAnimationConfigRecursive(config.type || type, ma
p, allConfigs); |
| 9310 }; | 5197 } else { |
| 9311 this._copyProperties(clone, config); | 5198 if (config.id) { |
| 9312 return clone; | 5199 var cachedConfig = map[config.id]; |
| 9313 }, | 5200 if (cachedConfig) { |
| 9314 | 5201 if (!cachedConfig.isClone) { |
| 9315 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 5202 map[config.id] = this._cloneConfig(cachedConfig); |
| 9316 if (!this.animationConfig) { | 5203 cachedConfig = map[config.id]; |
| 9317 return; | |
| 9318 } | |
| 9319 | |
| 9320 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | |
| 9321 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | |
| 9322 » return; | |
| 9323 } | |
| 9324 | |
| 9325 // type is optional | |
| 9326 var thisConfig; | |
| 9327 if (type) { | |
| 9328 thisConfig = this.animationConfig[type]; | |
| 9329 } else { | |
| 9330 thisConfig = this.animationConfig; | |
| 9331 } | |
| 9332 | |
| 9333 if (!Array.isArray(thisConfig)) { | |
| 9334 thisConfig = [thisConfig]; | |
| 9335 } | |
| 9336 | |
| 9337 // iterate animations and recurse to process configurations from child nod
es | |
| 9338 if (thisConfig) { | |
| 9339 for (var config, index = 0; config = thisConfig[index]; index++) { | |
| 9340 if (config.animatable) { | |
| 9341 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | |
| 9342 } else { | |
| 9343 if (config.id) { | |
| 9344 var cachedConfig = map[config.id]; | |
| 9345 if (cachedConfig) { | |
| 9346 // merge configurations with the same id, making a clone lazily | |
| 9347 if (!cachedConfig.isClone) { | |
| 9348 map[config.id] = this._cloneConfig(cachedConfig) | |
| 9349 cachedConfig = map[config.id]; | |
| 9350 } | |
| 9351 this._copyProperties(cachedConfig, config); | |
| 9352 } else { | |
| 9353 // put any configs with an id into a map | |
| 9354 map[config.id] = config; | |
| 9355 } | 5204 } |
| 5205 this._copyProperties(cachedConfig, config); |
| 9356 } else { | 5206 } else { |
| 9357 allConfigs.push(config); | 5207 map[config.id] = config; |
| 9358 } | |
| 9359 } | |
| 9360 } | |
| 9361 } | |
| 9362 }, | |
| 9363 | |
| 9364 /** | |
| 9365 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
| 9366 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
| 9367 * should define the property `animationConfig`, which is either a configura
tion object | |
| 9368 * or a map of animation type to array of configuration objects. | |
| 9369 */ | |
| 9370 getAnimationConfig: function(type) { | |
| 9371 var map = {}; | |
| 9372 var allConfigs = []; | |
| 9373 this._getAnimationConfigRecursive(type, map, allConfigs); | |
| 9374 // append the configurations saved in the map to the array | |
| 9375 for (var key in map) { | |
| 9376 allConfigs.push(map[key]); | |
| 9377 } | |
| 9378 return allConfigs; | |
| 9379 } | |
| 9380 | |
| 9381 }; | |
| 9382 /** | |
| 9383 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
| 9384 * | |
| 9385 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
| 9386 */ | |
| 9387 Polymer.NeonAnimationRunnerBehaviorImpl = { | |
| 9388 | |
| 9389 _configureAnimations: function(configs) { | |
| 9390 var results = []; | |
| 9391 if (configs.length > 0) { | |
| 9392 for (var config, index = 0; config = configs[index]; index++) { | |
| 9393 var neonAnimation = document.createElement(config.name); | |
| 9394 // is this element actually a neon animation? | |
| 9395 if (neonAnimation.isNeonAnimation) { | |
| 9396 var result = null; | |
| 9397 // configuration or play could fail if polyfills aren't loaded | |
| 9398 try { | |
| 9399 result = neonAnimation.configure(config); | |
| 9400 // Check if we have an Effect rather than an Animation | |
| 9401 if (typeof result.cancel != 'function') { | |
| 9402 result = document.timeline.play(result); | |
| 9403 } | |
| 9404 } catch (e) { | |
| 9405 result = null; | |
| 9406 console.warn('Couldnt play', '(', config.name, ').', e); | |
| 9407 } | |
| 9408 if (result) { | |
| 9409 results.push({ | |
| 9410 neonAnimation: neonAnimation, | |
| 9411 config: config, | |
| 9412 animation: result, | |
| 9413 }); | |
| 9414 } | 5208 } |
| 9415 } else { | 5209 } else { |
| 9416 console.warn(this.is + ':', config.name, 'not found!'); | 5210 allConfigs.push(config); |
| 9417 } | 5211 } |
| 9418 } | 5212 } |
| 9419 } | 5213 } |
| 9420 return results; | 5214 } |
| 9421 }, | 5215 }, |
| 9422 | 5216 getAnimationConfig: function(type) { |
| 9423 _shouldComplete: function(activeEntries) { | 5217 var map = {}; |
| 9424 var finished = true; | 5218 var allConfigs = []; |
| 9425 for (var i = 0; i < activeEntries.length; i++) { | 5219 this._getAnimationConfigRecursive(type, map, allConfigs); |
| 9426 if (activeEntries[i].animation.playState != 'finished') { | 5220 for (var key in map) { |
| 9427 finished = false; | 5221 allConfigs.push(map[key]); |
| 9428 break; | 5222 } |
| 9429 } | 5223 return allConfigs; |
| 9430 } | 5224 } |
| 9431 return finished; | 5225 }; |
| 9432 }, | 5226 |
| 9433 | 5227 Polymer.NeonAnimationRunnerBehaviorImpl = { |
| 9434 _complete: function(activeEntries) { | 5228 _configureAnimations: function(configs) { |
| 9435 for (var i = 0; i < activeEntries.length; i++) { | 5229 var results = []; |
| 9436 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | 5230 if (configs.length > 0) { |
| 9437 } | 5231 for (var config, index = 0; config = configs[index]; index++) { |
| 9438 for (var i = 0; i < activeEntries.length; i++) { | 5232 var neonAnimation = document.createElement(config.name); |
| 9439 activeEntries[i].animation.cancel(); | 5233 if (neonAnimation.isNeonAnimation) { |
| 9440 } | 5234 var result = null; |
| 9441 }, | 5235 try { |
| 9442 | 5236 result = neonAnimation.configure(config); |
| 9443 /** | 5237 if (typeof result.cancel != 'function') { |
| 9444 * Plays an animation with an optional `type`. | 5238 result = document.timeline.play(result); |
| 9445 * @param {string=} type | 5239 } |
| 9446 * @param {!Object=} cookie | 5240 } catch (e) { |
| 9447 */ | 5241 result = null; |
| 9448 playAnimation: function(type, cookie) { | 5242 console.warn('Couldnt play', '(', config.name, ').', e); |
| 9449 var configs = this.getAnimationConfig(type); | 5243 } |
| 9450 if (!configs) { | 5244 if (result) { |
| 5245 results.push({ |
| 5246 neonAnimation: neonAnimation, |
| 5247 config: config, |
| 5248 animation: result |
| 5249 }); |
| 5250 } |
| 5251 } else { |
| 5252 console.warn(this.is + ':', config.name, 'not found!'); |
| 5253 } |
| 5254 } |
| 5255 } |
| 5256 return results; |
| 5257 }, |
| 5258 _shouldComplete: function(activeEntries) { |
| 5259 var finished = true; |
| 5260 for (var i = 0; i < activeEntries.length; i++) { |
| 5261 if (activeEntries[i].animation.playState != 'finished') { |
| 5262 finished = false; |
| 5263 break; |
| 5264 } |
| 5265 } |
| 5266 return finished; |
| 5267 }, |
| 5268 _complete: function(activeEntries) { |
| 5269 for (var i = 0; i < activeEntries.length; i++) { |
| 5270 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
| 5271 } |
| 5272 for (var i = 0; i < activeEntries.length; i++) { |
| 5273 activeEntries[i].animation.cancel(); |
| 5274 } |
| 5275 }, |
| 5276 playAnimation: function(type, cookie) { |
| 5277 var configs = this.getAnimationConfig(type); |
| 5278 if (!configs) { |
| 5279 return; |
| 5280 } |
| 5281 this._active = this._active || {}; |
| 5282 if (this._active[type]) { |
| 5283 this._complete(this._active[type]); |
| 5284 delete this._active[type]; |
| 5285 } |
| 5286 var activeEntries = this._configureAnimations(configs); |
| 5287 if (activeEntries.length == 0) { |
| 5288 this.fire('neon-animation-finish', cookie, { |
| 5289 bubbles: false |
| 5290 }); |
| 5291 return; |
| 5292 } |
| 5293 this._active[type] = activeEntries; |
| 5294 for (var i = 0; i < activeEntries.length; i++) { |
| 5295 activeEntries[i].animation.onfinish = function() { |
| 5296 if (this._shouldComplete(activeEntries)) { |
| 5297 this._complete(activeEntries); |
| 5298 delete this._active[type]; |
| 5299 this.fire('neon-animation-finish', cookie, { |
| 5300 bubbles: false |
| 5301 }); |
| 5302 } |
| 5303 }.bind(this); |
| 5304 } |
| 5305 }, |
| 5306 cancelAnimation: function() { |
| 5307 for (var k in this._animations) { |
| 5308 this._animations[k].cancel(); |
| 5309 } |
| 5310 this._animations = {}; |
| 5311 } |
| 5312 }; |
| 5313 |
| 5314 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer.
NeonAnimationRunnerBehaviorImpl ]; |
| 5315 |
| 5316 Polymer.NeonAnimationBehavior = { |
| 5317 properties: { |
| 5318 animationTiming: { |
| 5319 type: Object, |
| 5320 value: function() { |
| 5321 return { |
| 5322 duration: 500, |
| 5323 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
| 5324 fill: 'both' |
| 5325 }; |
| 5326 } |
| 5327 } |
| 5328 }, |
| 5329 isNeonAnimation: true, |
| 5330 timingFromConfig: function(config) { |
| 5331 if (config.timing) { |
| 5332 for (var property in config.timing) { |
| 5333 this.animationTiming[property] = config.timing[property]; |
| 5334 } |
| 5335 } |
| 5336 return this.animationTiming; |
| 5337 }, |
| 5338 setPrefixedProperty: function(node, property, value) { |
| 5339 var map = { |
| 5340 transform: [ 'webkitTransform' ], |
| 5341 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ] |
| 5342 }; |
| 5343 var prefixes = map[property]; |
| 5344 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
| 5345 node.style[prefix] = value; |
| 5346 } |
| 5347 node.style[property] = value; |
| 5348 }, |
| 5349 complete: function() {} |
| 5350 }; |
| 5351 |
| 5352 Polymer({ |
| 5353 is: 'opaque-animation', |
| 5354 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5355 configure: function(config) { |
| 5356 var node = config.node; |
| 5357 this._effect = new KeyframeEffect(node, [ { |
| 5358 opacity: '1' |
| 5359 }, { |
| 5360 opacity: '1' |
| 5361 } ], this.timingFromConfig(config)); |
| 5362 node.style.opacity = '0'; |
| 5363 return this._effect; |
| 5364 }, |
| 5365 complete: function(config) { |
| 5366 config.node.style.opacity = ''; |
| 5367 } |
| 5368 }); |
| 5369 |
| 5370 (function() { |
| 5371 'use strict'; |
| 5372 var LAST_TOUCH_POSITION = { |
| 5373 pageX: 0, |
| 5374 pageY: 0 |
| 5375 }; |
| 5376 var ROOT_TARGET = null; |
| 5377 var SCROLLABLE_NODES = []; |
| 5378 Polymer.IronDropdownScrollManager = { |
| 5379 get currentLockingElement() { |
| 5380 return this._lockingElements[this._lockingElements.length - 1]; |
| 5381 }, |
| 5382 elementIsScrollLocked: function(element) { |
| 5383 var currentLockingElement = this.currentLockingElement; |
| 5384 if (currentLockingElement === undefined) return false; |
| 5385 var scrollLocked; |
| 5386 if (this._hasCachedLockedElement(element)) { |
| 5387 return true; |
| 5388 } |
| 5389 if (this._hasCachedUnlockedElement(element)) { |
| 5390 return false; |
| 5391 } |
| 5392 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen
t && !this._composedTreeContains(currentLockingElement, element); |
| 5393 if (scrollLocked) { |
| 5394 this._lockedElementCache.push(element); |
| 5395 } else { |
| 5396 this._unlockedElementCache.push(element); |
| 5397 } |
| 5398 return scrollLocked; |
| 5399 }, |
| 5400 pushScrollLock: function(element) { |
| 5401 if (this._lockingElements.indexOf(element) >= 0) { |
| 9451 return; | 5402 return; |
| 9452 } | 5403 } |
| 9453 this._active = this._active || {}; | 5404 if (this._lockingElements.length === 0) { |
| 9454 if (this._active[type]) { | 5405 this._lockScrollInteractions(); |
| 9455 this._complete(this._active[type]); | 5406 } |
| 9456 delete this._active[type]; | 5407 this._lockingElements.push(element); |
| 9457 } | 5408 this._lockedElementCache = []; |
| 9458 | 5409 this._unlockedElementCache = []; |
| 9459 var activeEntries = this._configureAnimations(configs); | 5410 }, |
| 9460 | 5411 removeScrollLock: function(element) { |
| 9461 if (activeEntries.length == 0) { | 5412 var index = this._lockingElements.indexOf(element); |
| 9462 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5413 if (index === -1) { |
| 9463 return; | 5414 return; |
| 9464 } | 5415 } |
| 9465 | 5416 this._lockingElements.splice(index, 1); |
| 9466 this._active[type] = activeEntries; | 5417 this._lockedElementCache = []; |
| 9467 | 5418 this._unlockedElementCache = []; |
| 9468 for (var i = 0; i < activeEntries.length; i++) { | 5419 if (this._lockingElements.length === 0) { |
| 9469 activeEntries[i].animation.onfinish = function() { | 5420 this._unlockScrollInteractions(); |
| 9470 if (this._shouldComplete(activeEntries)) { | 5421 } |
| 9471 this._complete(activeEntries); | 5422 }, |
| 9472 delete this._active[type]; | 5423 _lockingElements: [], |
| 9473 this.fire('neon-animation-finish', cookie, {bubbles: false}); | 5424 _lockedElementCache: null, |
| 5425 _unlockedElementCache: null, |
| 5426 _hasCachedLockedElement: function(element) { |
| 5427 return this._lockedElementCache.indexOf(element) > -1; |
| 5428 }, |
| 5429 _hasCachedUnlockedElement: function(element) { |
| 5430 return this._unlockedElementCache.indexOf(element) > -1; |
| 5431 }, |
| 5432 _composedTreeContains: function(element, child) { |
| 5433 var contentElements; |
| 5434 var distributedNodes; |
| 5435 var contentIndex; |
| 5436 var nodeIndex; |
| 5437 if (element.contains(child)) { |
| 5438 return true; |
| 5439 } |
| 5440 contentElements = Polymer.dom(element).querySelectorAll('content'); |
| 5441 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd
ex) { |
| 5442 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib
utedNodes(); |
| 5443 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { |
| 5444 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { |
| 5445 return true; |
| 9474 } | 5446 } |
| 9475 }.bind(this); | 5447 } |
| 9476 } | 5448 } |
| 9477 }, | 5449 return false; |
| 9478 | 5450 }, |
| 9479 /** | 5451 _scrollInteractionHandler: function(event) { |
| 9480 * Cancels the currently running animations. | 5452 if (event.cancelable && this._shouldPreventScrolling(event)) { |
| 9481 */ | 5453 event.preventDefault(); |
| 9482 cancelAnimation: function() { | 5454 } |
| 9483 for (var k in this._animations) { | 5455 if (event.targetTouches) { |
| 9484 this._animations[k].cancel(); | 5456 var touch = event.targetTouches[0]; |
| 9485 } | 5457 LAST_TOUCH_POSITION.pageX = touch.pageX; |
| 9486 this._animations = {}; | 5458 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 5459 } |
| 5460 }, |
| 5461 _lockScrollInteractions: function() { |
| 5462 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac
tionHandler.bind(this); |
| 5463 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 5464 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 5465 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true
); |
| 5466 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 5467 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 5468 }, |
| 5469 _unlockScrollInteractions: function() { |
| 5470 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 5471 document.removeEventListener('mousewheel', this._boundScrollHandler, true)
; |
| 5472 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t
rue); |
| 5473 document.removeEventListener('touchstart', this._boundScrollHandler, true)
; |
| 5474 document.removeEventListener('touchmove', this._boundScrollHandler, true); |
| 5475 }, |
| 5476 _shouldPreventScrolling: function(event) { |
| 5477 var target = Polymer.dom(event).rootTarget; |
| 5478 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 5479 ROOT_TARGET = target; |
| 5480 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 5481 } |
| 5482 if (!SCROLLABLE_NODES.length) { |
| 5483 return true; |
| 5484 } |
| 5485 if (event.type === 'touchstart') { |
| 5486 return false; |
| 5487 } |
| 5488 var info = this._getScrollInfo(event); |
| 5489 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY)
; |
| 5490 }, |
| 5491 _getScrollableNodes: function(nodes) { |
| 5492 var scrollables = []; |
| 5493 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 5494 for (var i = 0; i <= lockingIndex; i++) { |
| 5495 var node = nodes[i]; |
| 5496 if (node.nodeType === 11) { |
| 5497 continue; |
| 5498 } |
| 5499 var style = node.style; |
| 5500 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 5501 style = window.getComputedStyle(node); |
| 5502 } |
| 5503 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 5504 scrollables.push(node); |
| 5505 } |
| 5506 } |
| 5507 return scrollables; |
| 5508 }, |
| 5509 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 5510 if (!deltaX && !deltaY) { |
| 5511 return; |
| 5512 } |
| 5513 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 5514 for (var i = 0; i < nodes.length; i++) { |
| 5515 var node = nodes[i]; |
| 5516 var canScroll = false; |
| 5517 if (verticalScroll) { |
| 5518 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc
rollHeight - node.clientHeight; |
| 5519 } else { |
| 5520 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.
scrollWidth - node.clientWidth; |
| 5521 } |
| 5522 if (canScroll) { |
| 5523 return node; |
| 5524 } |
| 5525 } |
| 5526 }, |
| 5527 _getScrollInfo: function(event) { |
| 5528 var info = { |
| 5529 deltaX: event.deltaX, |
| 5530 deltaY: event.deltaY |
| 5531 }; |
| 5532 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) { |
| 5533 info.deltaX = -event.wheelDeltaX; |
| 5534 info.deltaY = -event.wheelDeltaY; |
| 5535 } else if ('axis' in event) { |
| 5536 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 5537 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 5538 } else if (event.targetTouches) { |
| 5539 var touch = event.targetTouches[0]; |
| 5540 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 5541 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 5542 } |
| 5543 return info; |
| 9487 } | 5544 } |
| 9488 }; | 5545 }; |
| 9489 | 5546 })(); |
| 9490 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | 5547 |
| 9491 Polymer.NeonAnimationRunnerBehavior = [ | 5548 (function() { |
| 9492 Polymer.NeonAnimatableBehavior, | 5549 'use strict'; |
| 9493 Polymer.NeonAnimationRunnerBehaviorImpl | 5550 Polymer({ |
| 9494 ]; | 5551 is: 'iron-dropdown', |
| 9495 /** | 5552 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer
.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], |
| 9496 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
| 9497 * @polymerBehavior | |
| 9498 */ | |
| 9499 Polymer.NeonAnimationBehavior = { | |
| 9500 | |
| 9501 properties: { | 5553 properties: { |
| 9502 | 5554 horizontalAlign: { |
| 9503 /** | 5555 type: String, |
| 9504 * Defines the animation timing. | 5556 value: 'left', |
| 9505 */ | 5557 reflectToAttribute: true |
| 9506 animationTiming: { | 5558 }, |
| 5559 verticalAlign: { |
| 5560 type: String, |
| 5561 value: 'top', |
| 5562 reflectToAttribute: true |
| 5563 }, |
| 5564 openAnimationConfig: { |
| 5565 type: Object |
| 5566 }, |
| 5567 closeAnimationConfig: { |
| 5568 type: Object |
| 5569 }, |
| 5570 focusTarget: { |
| 5571 type: Object |
| 5572 }, |
| 5573 noAnimations: { |
| 5574 type: Boolean, |
| 5575 value: false |
| 5576 }, |
| 5577 allowOutsideScroll: { |
| 5578 type: Boolean, |
| 5579 value: false |
| 5580 }, |
| 5581 _boundOnCaptureScroll: { |
| 5582 type: Function, |
| 5583 value: function() { |
| 5584 return this._onCaptureScroll.bind(this); |
| 5585 } |
| 5586 } |
| 5587 }, |
| 5588 listeners: { |
| 5589 'neon-animation-finish': '_onNeonAnimationFinish' |
| 5590 }, |
| 5591 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont
alAlign, verticalOffset, horizontalOffset)' ], |
| 5592 get containedElement() { |
| 5593 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5594 }, |
| 5595 get _focusTarget() { |
| 5596 return this.focusTarget || this.containedElement; |
| 5597 }, |
| 5598 ready: function() { |
| 5599 this._scrollTop = 0; |
| 5600 this._scrollLeft = 0; |
| 5601 this._refitOnScrollRAF = null; |
| 5602 }, |
| 5603 detached: function() { |
| 5604 this.cancelAnimation(); |
| 5605 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5606 }, |
| 5607 _openedChanged: function() { |
| 5608 if (this.opened && this.disabled) { |
| 5609 this.cancel(); |
| 5610 } else { |
| 5611 this.cancelAnimation(); |
| 5612 this.sizingTarget = this.containedElement || this.sizingTarget; |
| 5613 this._updateAnimationConfig(); |
| 5614 this._saveScrollPosition(); |
| 5615 if (this.opened) { |
| 5616 document.addEventListener('scroll', this._boundOnCaptureScroll); |
| 5617 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro
llLock(this); |
| 5618 } else { |
| 5619 document.removeEventListener('scroll', this._boundOnCaptureScroll); |
| 5620 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5621 } |
| 5622 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); |
| 5623 } |
| 5624 }, |
| 5625 _renderOpened: function() { |
| 5626 if (!this.noAnimations && this.animationConfig.open) { |
| 5627 this.$.contentWrapper.classList.add('animating'); |
| 5628 this.playAnimation('open'); |
| 5629 } else { |
| 5630 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); |
| 5631 } |
| 5632 }, |
| 5633 _renderClosed: function() { |
| 5634 if (!this.noAnimations && this.animationConfig.close) { |
| 5635 this.$.contentWrapper.classList.add('animating'); |
| 5636 this.playAnimation('close'); |
| 5637 } else { |
| 5638 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); |
| 5639 } |
| 5640 }, |
| 5641 _onNeonAnimationFinish: function() { |
| 5642 this.$.contentWrapper.classList.remove('animating'); |
| 5643 if (this.opened) { |
| 5644 this._finishRenderOpened(); |
| 5645 } else { |
| 5646 this._finishRenderClosed(); |
| 5647 } |
| 5648 }, |
| 5649 _onCaptureScroll: function() { |
| 5650 if (!this.allowOutsideScroll) { |
| 5651 this._restoreScrollPosition(); |
| 5652 } else { |
| 5653 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol
lRAF); |
| 5654 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th
is)); |
| 5655 } |
| 5656 }, |
| 5657 _saveScrollPosition: function() { |
| 5658 if (document.scrollingElement) { |
| 5659 this._scrollTop = document.scrollingElement.scrollTop; |
| 5660 this._scrollLeft = document.scrollingElement.scrollLeft; |
| 5661 } else { |
| 5662 this._scrollTop = Math.max(document.documentElement.scrollTop, document.
body.scrollTop); |
| 5663 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen
t.body.scrollLeft); |
| 5664 } |
| 5665 }, |
| 5666 _restoreScrollPosition: function() { |
| 5667 if (document.scrollingElement) { |
| 5668 document.scrollingElement.scrollTop = this._scrollTop; |
| 5669 document.scrollingElement.scrollLeft = this._scrollLeft; |
| 5670 } else { |
| 5671 document.documentElement.scrollTop = this._scrollTop; |
| 5672 document.documentElement.scrollLeft = this._scrollLeft; |
| 5673 document.body.scrollTop = this._scrollTop; |
| 5674 document.body.scrollLeft = this._scrollLeft; |
| 5675 } |
| 5676 }, |
| 5677 _updateAnimationConfig: function() { |
| 5678 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati
onConfig || []); |
| 5679 for (var i = 0; i < animations.length; i++) { |
| 5680 animations[i].node = this.containedElement; |
| 5681 } |
| 5682 this.animationConfig = { |
| 5683 open: this.openAnimationConfig, |
| 5684 close: this.closeAnimationConfig |
| 5685 }; |
| 5686 }, |
| 5687 _updateOverlayPosition: function() { |
| 5688 if (this.isAttached) { |
| 5689 this.notifyResize(); |
| 5690 } |
| 5691 }, |
| 5692 _applyFocus: function() { |
| 5693 var focusTarget = this.focusTarget || this.containedElement; |
| 5694 if (focusTarget && this.opened && !this.noAutoFocus) { |
| 5695 focusTarget.focus(); |
| 5696 } else { |
| 5697 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
| 5698 } |
| 5699 } |
| 5700 }); |
| 5701 })(); |
| 5702 |
| 5703 Polymer({ |
| 5704 is: 'fade-in-animation', |
| 5705 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5706 configure: function(config) { |
| 5707 var node = config.node; |
| 5708 this._effect = new KeyframeEffect(node, [ { |
| 5709 opacity: '0' |
| 5710 }, { |
| 5711 opacity: '1' |
| 5712 } ], this.timingFromConfig(config)); |
| 5713 return this._effect; |
| 5714 } |
| 5715 }); |
| 5716 |
| 5717 Polymer({ |
| 5718 is: 'fade-out-animation', |
| 5719 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5720 configure: function(config) { |
| 5721 var node = config.node; |
| 5722 this._effect = new KeyframeEffect(node, [ { |
| 5723 opacity: '1' |
| 5724 }, { |
| 5725 opacity: '0' |
| 5726 } ], this.timingFromConfig(config)); |
| 5727 return this._effect; |
| 5728 } |
| 5729 }); |
| 5730 |
| 5731 Polymer({ |
| 5732 is: 'paper-menu-grow-height-animation', |
| 5733 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5734 configure: function(config) { |
| 5735 var node = config.node; |
| 5736 var rect = node.getBoundingClientRect(); |
| 5737 var height = rect.height; |
| 5738 this._effect = new KeyframeEffect(node, [ { |
| 5739 height: height / 2 + 'px' |
| 5740 }, { |
| 5741 height: height + 'px' |
| 5742 } ], this.timingFromConfig(config)); |
| 5743 return this._effect; |
| 5744 } |
| 5745 }); |
| 5746 |
| 5747 Polymer({ |
| 5748 is: 'paper-menu-grow-width-animation', |
| 5749 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5750 configure: function(config) { |
| 5751 var node = config.node; |
| 5752 var rect = node.getBoundingClientRect(); |
| 5753 var width = rect.width; |
| 5754 this._effect = new KeyframeEffect(node, [ { |
| 5755 width: width / 2 + 'px' |
| 5756 }, { |
| 5757 width: width + 'px' |
| 5758 } ], this.timingFromConfig(config)); |
| 5759 return this._effect; |
| 5760 } |
| 5761 }); |
| 5762 |
| 5763 Polymer({ |
| 5764 is: 'paper-menu-shrink-width-animation', |
| 5765 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5766 configure: function(config) { |
| 5767 var node = config.node; |
| 5768 var rect = node.getBoundingClientRect(); |
| 5769 var width = rect.width; |
| 5770 this._effect = new KeyframeEffect(node, [ { |
| 5771 width: width + 'px' |
| 5772 }, { |
| 5773 width: width - width / 20 + 'px' |
| 5774 } ], this.timingFromConfig(config)); |
| 5775 return this._effect; |
| 5776 } |
| 5777 }); |
| 5778 |
| 5779 Polymer({ |
| 5780 is: 'paper-menu-shrink-height-animation', |
| 5781 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5782 configure: function(config) { |
| 5783 var node = config.node; |
| 5784 var rect = node.getBoundingClientRect(); |
| 5785 var height = rect.height; |
| 5786 var top = rect.top; |
| 5787 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
| 5788 this._effect = new KeyframeEffect(node, [ { |
| 5789 height: height + 'px', |
| 5790 transform: 'translateY(0)' |
| 5791 }, { |
| 5792 height: height / 2 + 'px', |
| 5793 transform: 'translateY(-20px)' |
| 5794 } ], this.timingFromConfig(config)); |
| 5795 return this._effect; |
| 5796 } |
| 5797 }); |
| 5798 |
| 5799 (function() { |
| 5800 'use strict'; |
| 5801 var config = { |
| 5802 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', |
| 5803 MAX_ANIMATION_TIME_MS: 400 |
| 5804 }; |
| 5805 var PaperMenuButton = Polymer({ |
| 5806 is: 'paper-menu-button', |
| 5807 behaviors: [ Polymer.IronA11yKeysBehavior, Polymer.IronControlState ], |
| 5808 properties: { |
| 5809 opened: { |
| 5810 type: Boolean, |
| 5811 value: false, |
| 5812 notify: true, |
| 5813 observer: '_openedChanged' |
| 5814 }, |
| 5815 horizontalAlign: { |
| 5816 type: String, |
| 5817 value: 'left', |
| 5818 reflectToAttribute: true |
| 5819 }, |
| 5820 verticalAlign: { |
| 5821 type: String, |
| 5822 value: 'top', |
| 5823 reflectToAttribute: true |
| 5824 }, |
| 5825 dynamicAlign: { |
| 5826 type: Boolean |
| 5827 }, |
| 5828 horizontalOffset: { |
| 5829 type: Number, |
| 5830 value: 0, |
| 5831 notify: true |
| 5832 }, |
| 5833 verticalOffset: { |
| 5834 type: Number, |
| 5835 value: 0, |
| 5836 notify: true |
| 5837 }, |
| 5838 noOverlap: { |
| 5839 type: Boolean |
| 5840 }, |
| 5841 noAnimations: { |
| 5842 type: Boolean, |
| 5843 value: false |
| 5844 }, |
| 5845 ignoreSelect: { |
| 5846 type: Boolean, |
| 5847 value: false |
| 5848 }, |
| 5849 closeOnActivate: { |
| 5850 type: Boolean, |
| 5851 value: false |
| 5852 }, |
| 5853 openAnimationConfig: { |
| 9507 type: Object, | 5854 type: Object, |
| 9508 value: function() { | 5855 value: function() { |
| 9509 return { | 5856 return [ { |
| 9510 duration: 500, | 5857 name: 'fade-in-animation', |
| 9511 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | 5858 timing: { |
| 9512 fill: 'both' | 5859 delay: 100, |
| 9513 } | 5860 duration: 200 |
| 9514 } | 5861 } |
| 9515 } | 5862 }, { |
| 9516 | 5863 name: 'paper-menu-grow-width-animation', |
| 9517 }, | 5864 timing: { |
| 9518 | 5865 delay: 100, |
| 9519 /** | 5866 duration: 150, |
| 9520 * Can be used to determine that elements implement this behavior. | 5867 easing: config.ANIMATION_CUBIC_BEZIER |
| 9521 */ | 5868 } |
| 9522 isNeonAnimation: true, | 5869 }, { |
| 9523 | 5870 name: 'paper-menu-grow-height-animation', |
| 9524 /** | 5871 timing: { |
| 9525 * Do any animation configuration here. | 5872 delay: 100, |
| 9526 */ | 5873 duration: 275, |
| 9527 // configure: function(config) { | 5874 easing: config.ANIMATION_CUBIC_BEZIER |
| 9528 // }, | 5875 } |
| 9529 | 5876 } ]; |
| 9530 /** | 5877 } |
| 9531 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | 5878 }, |
| 9532 * by the animation. | 5879 closeAnimationConfig: { |
| 9533 */ | 5880 type: Object, |
| 9534 timingFromConfig: function(config) { | 5881 value: function() { |
| 9535 if (config.timing) { | 5882 return [ { |
| 9536 for (var property in config.timing) { | 5883 name: 'fade-out-animation', |
| 9537 this.animationTiming[property] = config.timing[property]; | 5884 timing: { |
| 9538 } | 5885 duration: 150 |
| 9539 } | 5886 } |
| 9540 return this.animationTiming; | 5887 }, { |
| 9541 }, | 5888 name: 'paper-menu-shrink-width-animation', |
| 9542 | 5889 timing: { |
| 9543 /** | 5890 delay: 100, |
| 9544 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | 5891 duration: 50, |
| 9545 */ | 5892 easing: config.ANIMATION_CUBIC_BEZIER |
| 9546 setPrefixedProperty: function(node, property, value) { | 5893 } |
| 9547 var map = { | 5894 }, { |
| 9548 'transform': ['webkitTransform'], | 5895 name: 'paper-menu-shrink-height-animation', |
| 9549 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | 5896 timing: { |
| 9550 }; | 5897 duration: 200, |
| 9551 var prefixes = map[property]; | 5898 easing: 'ease-in' |
| 9552 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | 5899 } |
| 9553 node.style[prefix] = value; | 5900 } ]; |
| 9554 } | 5901 } |
| 9555 node.style[property] = value; | 5902 }, |
| 9556 }, | 5903 allowOutsideScroll: { |
| 9557 | 5904 type: Boolean, |
| 9558 /** | 5905 value: false |
| 9559 * Called when the animation finishes. | 5906 }, |
| 9560 */ | 5907 restoreFocusOnClose: { |
| 9561 complete: function() {} | 5908 type: Boolean, |
| 9562 | 5909 value: true |
| 9563 }; | 5910 }, |
| 5911 _dropdownContent: { |
| 5912 type: Object |
| 5913 } |
| 5914 }, |
| 5915 hostAttributes: { |
| 5916 role: 'group', |
| 5917 'aria-haspopup': 'true' |
| 5918 }, |
| 5919 listeners: { |
| 5920 'iron-activate': '_onIronActivate', |
| 5921 'iron-select': '_onIronSelect' |
| 5922 }, |
| 5923 get contentElement() { |
| 5924 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 5925 }, |
| 5926 toggle: function() { |
| 5927 if (this.opened) { |
| 5928 this.close(); |
| 5929 } else { |
| 5930 this.open(); |
| 5931 } |
| 5932 }, |
| 5933 open: function() { |
| 5934 if (this.disabled) { |
| 5935 return; |
| 5936 } |
| 5937 this.$.dropdown.open(); |
| 5938 }, |
| 5939 close: function() { |
| 5940 this.$.dropdown.close(); |
| 5941 }, |
| 5942 _onIronSelect: function(event) { |
| 5943 if (!this.ignoreSelect) { |
| 5944 this.close(); |
| 5945 } |
| 5946 }, |
| 5947 _onIronActivate: function(event) { |
| 5948 if (this.closeOnActivate) { |
| 5949 this.close(); |
| 5950 } |
| 5951 }, |
| 5952 _openedChanged: function(opened, oldOpened) { |
| 5953 if (opened) { |
| 5954 this._dropdownContent = this.contentElement; |
| 5955 this.fire('paper-dropdown-open'); |
| 5956 } else if (oldOpened != null) { |
| 5957 this.fire('paper-dropdown-close'); |
| 5958 } |
| 5959 }, |
| 5960 _disabledChanged: function(disabled) { |
| 5961 Polymer.IronControlState._disabledChanged.apply(this, arguments); |
| 5962 if (disabled && this.opened) { |
| 5963 this.close(); |
| 5964 } |
| 5965 }, |
| 5966 __onIronOverlayCanceled: function(event) { |
| 5967 var uiEvent = event.detail; |
| 5968 var target = Polymer.dom(uiEvent).rootTarget; |
| 5969 var trigger = this.$.trigger; |
| 5970 var path = Polymer.dom(uiEvent).path; |
| 5971 if (path.indexOf(trigger) > -1) { |
| 5972 event.preventDefault(); |
| 5973 } |
| 5974 } |
| 5975 }); |
| 5976 Object.keys(config).forEach(function(key) { |
| 5977 PaperMenuButton[key] = config[key]; |
| 5978 }); |
| 5979 Polymer.PaperMenuButton = PaperMenuButton; |
| 5980 })(); |
| 5981 |
| 5982 Polymer.PaperInkyFocusBehaviorImpl = { |
| 5983 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ], |
| 5984 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 5985 if (receivedFocusFromKeyboard) { |
| 5986 this.ensureRipple(); |
| 5987 } |
| 5988 if (this.hasRipple()) { |
| 5989 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 5990 } |
| 5991 }, |
| 5992 _createRipple: function() { |
| 5993 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 5994 ripple.id = 'ink'; |
| 5995 ripple.setAttribute('center', ''); |
| 5996 ripple.classList.add('circle'); |
| 5997 return ripple; |
| 5998 } |
| 5999 }; |
| 6000 |
| 6001 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS
tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ]; |
| 6002 |
| 9564 Polymer({ | 6003 Polymer({ |
| 9565 | 6004 is: 'paper-icon-button', |
| 9566 is: 'opaque-animation', | 6005 hostAttributes: { |
| 9567 | 6006 role: 'button', |
| 9568 behaviors: [ | 6007 tabindex: '0' |
| 9569 Polymer.NeonAnimationBehavior | 6008 }, |
| 9570 ], | 6009 behaviors: [ Polymer.PaperInkyFocusBehavior ], |
| 9571 | 6010 properties: { |
| 9572 configure: function(config) { | 6011 src: { |
| 9573 var node = config.node; | 6012 type: String |
| 9574 this._effect = new KeyframeEffect(node, [ | 6013 }, |
| 9575 {'opacity': '1'}, | 6014 icon: { |
| 9576 {'opacity': '1'} | 6015 type: String |
| 9577 ], this.timingFromConfig(config)); | 6016 }, |
| 9578 node.style.opacity = '0'; | 6017 alt: { |
| 9579 return this._effect; | 6018 type: String, |
| 9580 }, | 6019 observer: "_altChanged" |
| 9581 | 6020 } |
| 9582 complete: function(config) { | 6021 }, |
| 9583 config.node.style.opacity = ''; | 6022 _altChanged: function(newValue, oldValue) { |
| 9584 } | 6023 var label = this.getAttribute('aria-label'); |
| 9585 | 6024 if (!label || oldValue == label) { |
| 9586 }); | 6025 this.setAttribute('aria-label', newValue); |
| 9587 (function() { | 6026 } |
| 9588 'use strict'; | 6027 } |
| 9589 // Used to calculate the scroll direction during touch events. | 6028 }); |
| 9590 var LAST_TOUCH_POSITION = { | 6029 |
| 9591 pageX: 0, | |
| 9592 pageY: 0 | |
| 9593 }; | |
| 9594 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
| 9595 var ROOT_TARGET = null; | |
| 9596 var SCROLLABLE_NODES = []; | |
| 9597 | |
| 9598 /** | |
| 9599 * The IronDropdownScrollManager is intended to provide a central source | |
| 9600 * of authority and control over which elements in a document are currently | |
| 9601 * allowed to scroll. | |
| 9602 */ | |
| 9603 | |
| 9604 Polymer.IronDropdownScrollManager = { | |
| 9605 | |
| 9606 /** | |
| 9607 * The current element that defines the DOM boundaries of the | |
| 9608 * scroll lock. This is always the most recently locking element. | |
| 9609 */ | |
| 9610 get currentLockingElement() { | |
| 9611 return this._lockingElements[this._lockingElements.length - 1]; | |
| 9612 }, | |
| 9613 | |
| 9614 /** | |
| 9615 * Returns true if the provided element is "scroll locked", which is to | |
| 9616 * say that it cannot be scrolled via pointer or keyboard interactions. | |
| 9617 * | |
| 9618 * @param {HTMLElement} element An HTML element instance which may or may | |
| 9619 * not be scroll locked. | |
| 9620 */ | |
| 9621 elementIsScrollLocked: function(element) { | |
| 9622 var currentLockingElement = this.currentLockingElement; | |
| 9623 | |
| 9624 if (currentLockingElement === undefined) | |
| 9625 return false; | |
| 9626 | |
| 9627 var scrollLocked; | |
| 9628 | |
| 9629 if (this._hasCachedLockedElement(element)) { | |
| 9630 return true; | |
| 9631 } | |
| 9632 | |
| 9633 if (this._hasCachedUnlockedElement(element)) { | |
| 9634 return false; | |
| 9635 } | |
| 9636 | |
| 9637 scrollLocked = !!currentLockingElement && | |
| 9638 currentLockingElement !== element && | |
| 9639 !this._composedTreeContains(currentLockingElement, element); | |
| 9640 | |
| 9641 if (scrollLocked) { | |
| 9642 this._lockedElementCache.push(element); | |
| 9643 } else { | |
| 9644 this._unlockedElementCache.push(element); | |
| 9645 } | |
| 9646 | |
| 9647 return scrollLocked; | |
| 9648 }, | |
| 9649 | |
| 9650 /** | |
| 9651 * Push an element onto the current scroll lock stack. The most recently | |
| 9652 * pushed element and its children will be considered scrollable. All | |
| 9653 * other elements will not be scrollable. | |
| 9654 * | |
| 9655 * Scroll locking is implemented as a stack so that cases such as | |
| 9656 * dropdowns within dropdowns are handled well. | |
| 9657 * | |
| 9658 * @param {HTMLElement} element The element that should lock scroll. | |
| 9659 */ | |
| 9660 pushScrollLock: function(element) { | |
| 9661 // Prevent pushing the same element twice | |
| 9662 if (this._lockingElements.indexOf(element) >= 0) { | |
| 9663 return; | |
| 9664 } | |
| 9665 | |
| 9666 if (this._lockingElements.length === 0) { | |
| 9667 this._lockScrollInteractions(); | |
| 9668 } | |
| 9669 | |
| 9670 this._lockingElements.push(element); | |
| 9671 | |
| 9672 this._lockedElementCache = []; | |
| 9673 this._unlockedElementCache = []; | |
| 9674 }, | |
| 9675 | |
| 9676 /** | |
| 9677 * Remove an element from the scroll lock stack. The element being | |
| 9678 * removed does not need to be the most recently pushed element. However, | |
| 9679 * the scroll lock constraints only change when the most recently pushed | |
| 9680 * element is removed. | |
| 9681 * | |
| 9682 * @param {HTMLElement} element The element to remove from the scroll | |
| 9683 * lock stack. | |
| 9684 */ | |
| 9685 removeScrollLock: function(element) { | |
| 9686 var index = this._lockingElements.indexOf(element); | |
| 9687 | |
| 9688 if (index === -1) { | |
| 9689 return; | |
| 9690 } | |
| 9691 | |
| 9692 this._lockingElements.splice(index, 1); | |
| 9693 | |
| 9694 this._lockedElementCache = []; | |
| 9695 this._unlockedElementCache = []; | |
| 9696 | |
| 9697 if (this._lockingElements.length === 0) { | |
| 9698 this._unlockScrollInteractions(); | |
| 9699 } | |
| 9700 }, | |
| 9701 | |
| 9702 _lockingElements: [], | |
| 9703 | |
| 9704 _lockedElementCache: null, | |
| 9705 | |
| 9706 _unlockedElementCache: null, | |
| 9707 | |
| 9708 _hasCachedLockedElement: function(element) { | |
| 9709 return this._lockedElementCache.indexOf(element) > -1; | |
| 9710 }, | |
| 9711 | |
| 9712 _hasCachedUnlockedElement: function(element) { | |
| 9713 return this._unlockedElementCache.indexOf(element) > -1; | |
| 9714 }, | |
| 9715 | |
| 9716 _composedTreeContains: function(element, child) { | |
| 9717 // NOTE(cdata): This method iterates over content elements and their | |
| 9718 // corresponding distributed nodes to implement a contains-like method | |
| 9719 // that pierces through the composed tree of the ShadowDOM. Results of | |
| 9720 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
| 9721 // guard against potentially expensive lookups happening repeatedly as | |
| 9722 // a user scrolls / touchmoves. | |
| 9723 var contentElements; | |
| 9724 var distributedNodes; | |
| 9725 var contentIndex; | |
| 9726 var nodeIndex; | |
| 9727 | |
| 9728 if (element.contains(child)) { | |
| 9729 return true; | |
| 9730 } | |
| 9731 | |
| 9732 contentElements = Polymer.dom(element).querySelectorAll('content'); | |
| 9733 | |
| 9734 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | |
| 9735 | |
| 9736 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | |
| 9737 | |
| 9738 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | |
| 9739 | |
| 9740 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | |
| 9741 return true; | |
| 9742 } | |
| 9743 } | |
| 9744 } | |
| 9745 | |
| 9746 return false; | |
| 9747 }, | |
| 9748 | |
| 9749 _scrollInteractionHandler: function(event) { | |
| 9750 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
| 9751 // progress and cannot be interrupted. | |
| 9752 if (event.cancelable && this._shouldPreventScrolling(event)) { | |
| 9753 event.preventDefault(); | |
| 9754 } | |
| 9755 // If event has targetTouches (touch event), update last touch position. | |
| 9756 if (event.targetTouches) { | |
| 9757 var touch = event.targetTouches[0]; | |
| 9758 LAST_TOUCH_POSITION.pageX = touch.pageX; | |
| 9759 LAST_TOUCH_POSITION.pageY = touch.pageY; | |
| 9760 } | |
| 9761 }, | |
| 9762 | |
| 9763 _lockScrollInteractions: function() { | |
| 9764 this._boundScrollHandler = this._boundScrollHandler || | |
| 9765 this._scrollInteractionHandler.bind(this); | |
| 9766 // Modern `wheel` event for mouse wheel scrolling: | |
| 9767 document.addEventListener('wheel', this._boundScrollHandler, true); | |
| 9768 // Older, non-standard `mousewheel` event for some FF: | |
| 9769 document.addEventListener('mousewheel', this._boundScrollHandler, true); | |
| 9770 // IE: | |
| 9771 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | |
| 9772 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
| 9773 document.addEventListener('touchstart', this._boundScrollHandler, true); | |
| 9774 // Mobile devices can scroll on touch move: | |
| 9775 document.addEventListener('touchmove', this._boundScrollHandler, true); | |
| 9776 }, | |
| 9777 | |
| 9778 _unlockScrollInteractions: function() { | |
| 9779 document.removeEventListener('wheel', this._boundScrollHandler, true); | |
| 9780 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | |
| 9781 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | |
| 9782 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | |
| 9783 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | |
| 9784 }, | |
| 9785 | |
| 9786 /** | |
| 9787 * Returns true if the event causes scroll outside the current locking | |
| 9788 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
| 9789 * outside the locking element when it is already at its scroll boundaries
. | |
| 9790 * @param {!Event} event | |
| 9791 * @return {boolean} | |
| 9792 * @private | |
| 9793 */ | |
| 9794 _shouldPreventScrolling: function(event) { | |
| 9795 | |
| 9796 // Update if root target changed. For touch events, ensure we don't | |
| 9797 // update during touchmove. | |
| 9798 var target = Polymer.dom(event).rootTarget; | |
| 9799 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | |
| 9800 ROOT_TARGET = target; | |
| 9801 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | |
| 9802 } | |
| 9803 | |
| 9804 // Prevent event if no scrollable nodes. | |
| 9805 if (!SCROLLABLE_NODES.length) { | |
| 9806 return true; | |
| 9807 } | |
| 9808 // Don't prevent touchstart event inside the locking element when it has | |
| 9809 // scrollable nodes. | |
| 9810 if (event.type === 'touchstart') { | |
| 9811 return false; | |
| 9812 } | |
| 9813 // Get deltaX/Y. | |
| 9814 var info = this._getScrollInfo(event); | |
| 9815 // Prevent if there is no child that can scroll. | |
| 9816 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | |
| 9817 }, | |
| 9818 | |
| 9819 /** | |
| 9820 * Returns an array of scrollable nodes up to the current locking element, | |
| 9821 * which is included too if scrollable. | |
| 9822 * @param {!Array<Node>} nodes | |
| 9823 * @return {Array<Node>} scrollables | |
| 9824 * @private | |
| 9825 */ | |
| 9826 _getScrollableNodes: function(nodes) { | |
| 9827 var scrollables = []; | |
| 9828 var lockingIndex = nodes.indexOf(this.currentLockingElement); | |
| 9829 // Loop from root target to locking element (included). | |
| 9830 for (var i = 0; i <= lockingIndex; i++) { | |
| 9831 var node = nodes[i]; | |
| 9832 // Skip document fragments. | |
| 9833 if (node.nodeType === 11) { | |
| 9834 continue; | |
| 9835 } | |
| 9836 // Check inline style before checking computed style. | |
| 9837 var style = node.style; | |
| 9838 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | |
| 9839 style = window.getComputedStyle(node); | |
| 9840 } | |
| 9841 if (style.overflow === 'scroll' || style.overflow === 'auto') { | |
| 9842 scrollables.push(node); | |
| 9843 } | |
| 9844 } | |
| 9845 return scrollables; | |
| 9846 }, | |
| 9847 | |
| 9848 /** | |
| 9849 * Returns the node that is scrolling. If there is no scrolling, | |
| 9850 * returns undefined. | |
| 9851 * @param {!Array<Node>} nodes | |
| 9852 * @param {number} deltaX Scroll delta on the x-axis | |
| 9853 * @param {number} deltaY Scroll delta on the y-axis | |
| 9854 * @return {Node|undefined} | |
| 9855 * @private | |
| 9856 */ | |
| 9857 _getScrollingNode: function(nodes, deltaX, deltaY) { | |
| 9858 // No scroll. | |
| 9859 if (!deltaX && !deltaY) { | |
| 9860 return; | |
| 9861 } | |
| 9862 // Check only one axis according to where there is more scroll. | |
| 9863 // Prefer vertical to horizontal. | |
| 9864 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | |
| 9865 for (var i = 0; i < nodes.length; i++) { | |
| 9866 var node = nodes[i]; | |
| 9867 var canScroll = false; | |
| 9868 if (verticalScroll) { | |
| 9869 // delta < 0 is scroll up, delta > 0 is scroll down. | |
| 9870 canScroll = deltaY < 0 ? node.scrollTop > 0 : | |
| 9871 node.scrollTop < node.scrollHeight - node.clientHeight; | |
| 9872 } else { | |
| 9873 // delta < 0 is scroll left, delta > 0 is scroll right. | |
| 9874 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | |
| 9875 node.scrollLeft < node.scrollWidth - node.clientWidth; | |
| 9876 } | |
| 9877 if (canScroll) { | |
| 9878 return node; | |
| 9879 } | |
| 9880 } | |
| 9881 }, | |
| 9882 | |
| 9883 /** | |
| 9884 * Returns scroll `deltaX` and `deltaY`. | |
| 9885 * @param {!Event} event The scroll event | |
| 9886 * @return {{ | |
| 9887 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
| 9888 * negative: scroll left, 0: no scroll), | |
| 9889 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
| 9890 * negative: scroll up, 0: no scroll) | |
| 9891 * }} info | |
| 9892 * @private | |
| 9893 */ | |
| 9894 _getScrollInfo: function(event) { | |
| 9895 var info = { | |
| 9896 deltaX: event.deltaX, | |
| 9897 deltaY: event.deltaY | |
| 9898 }; | |
| 9899 // Already available. | |
| 9900 if ('deltaX' in event) { | |
| 9901 // do nothing, values are already good. | |
| 9902 } | |
| 9903 // Safari has scroll info in `wheelDeltaX/Y`. | |
| 9904 else if ('wheelDeltaX' in event) { | |
| 9905 info.deltaX = -event.wheelDeltaX; | |
| 9906 info.deltaY = -event.wheelDeltaY; | |
| 9907 } | |
| 9908 // Firefox has scroll info in `detail` and `axis`. | |
| 9909 else if ('axis' in event) { | |
| 9910 info.deltaX = event.axis === 1 ? event.detail : 0; | |
| 9911 info.deltaY = event.axis === 2 ? event.detail : 0; | |
| 9912 } | |
| 9913 // On mobile devices, calculate scroll direction. | |
| 9914 else if (event.targetTouches) { | |
| 9915 var touch = event.targetTouches[0]; | |
| 9916 // Touch moves from right to left => scrolling goes right. | |
| 9917 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | |
| 9918 // Touch moves from down to up => scrolling goes down. | |
| 9919 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | |
| 9920 } | |
| 9921 return info; | |
| 9922 } | |
| 9923 }; | |
| 9924 })(); | |
| 9925 (function() { | |
| 9926 'use strict'; | |
| 9927 | |
| 9928 Polymer({ | |
| 9929 is: 'iron-dropdown', | |
| 9930 | |
| 9931 behaviors: [ | |
| 9932 Polymer.IronControlState, | |
| 9933 Polymer.IronA11yKeysBehavior, | |
| 9934 Polymer.IronOverlayBehavior, | |
| 9935 Polymer.NeonAnimationRunnerBehavior | |
| 9936 ], | |
| 9937 | |
| 9938 properties: { | |
| 9939 /** | |
| 9940 * The orientation against which to align the dropdown content | |
| 9941 * horizontally relative to the dropdown trigger. | |
| 9942 * Overridden from `Polymer.IronFitBehavior`. | |
| 9943 */ | |
| 9944 horizontalAlign: { | |
| 9945 type: String, | |
| 9946 value: 'left', | |
| 9947 reflectToAttribute: true | |
| 9948 }, | |
| 9949 | |
| 9950 /** | |
| 9951 * The orientation against which to align the dropdown content | |
| 9952 * vertically relative to the dropdown trigger. | |
| 9953 * Overridden from `Polymer.IronFitBehavior`. | |
| 9954 */ | |
| 9955 verticalAlign: { | |
| 9956 type: String, | |
| 9957 value: 'top', | |
| 9958 reflectToAttribute: true | |
| 9959 }, | |
| 9960 | |
| 9961 /** | |
| 9962 * An animation config. If provided, this will be used to animate the | |
| 9963 * opening of the dropdown. | |
| 9964 */ | |
| 9965 openAnimationConfig: { | |
| 9966 type: Object | |
| 9967 }, | |
| 9968 | |
| 9969 /** | |
| 9970 * An animation config. If provided, this will be used to animate the | |
| 9971 * closing of the dropdown. | |
| 9972 */ | |
| 9973 closeAnimationConfig: { | |
| 9974 type: Object | |
| 9975 }, | |
| 9976 | |
| 9977 /** | |
| 9978 * If provided, this will be the element that will be focused when | |
| 9979 * the dropdown opens. | |
| 9980 */ | |
| 9981 focusTarget: { | |
| 9982 type: Object | |
| 9983 }, | |
| 9984 | |
| 9985 /** | |
| 9986 * Set to true to disable animations when opening and closing the | |
| 9987 * dropdown. | |
| 9988 */ | |
| 9989 noAnimations: { | |
| 9990 type: Boolean, | |
| 9991 value: false | |
| 9992 }, | |
| 9993 | |
| 9994 /** | |
| 9995 * By default, the dropdown will constrain scrolling on the page | |
| 9996 * to itself when opened. | |
| 9997 * Set to true in order to prevent scroll from being constrained | |
| 9998 * to the dropdown when it opens. | |
| 9999 */ | |
| 10000 allowOutsideScroll: { | |
| 10001 type: Boolean, | |
| 10002 value: false | |
| 10003 }, | |
| 10004 | |
| 10005 /** | |
| 10006 * Callback for scroll events. | |
| 10007 * @type {Function} | |
| 10008 * @private | |
| 10009 */ | |
| 10010 _boundOnCaptureScroll: { | |
| 10011 type: Function, | |
| 10012 value: function() { | |
| 10013 return this._onCaptureScroll.bind(this); | |
| 10014 } | |
| 10015 } | |
| 10016 }, | |
| 10017 | |
| 10018 listeners: { | |
| 10019 'neon-animation-finish': '_onNeonAnimationFinish' | |
| 10020 }, | |
| 10021 | |
| 10022 observers: [ | |
| 10023 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | |
| 10024 ], | |
| 10025 | |
| 10026 /** | |
| 10027 * The element that is contained by the dropdown, if any. | |
| 10028 */ | |
| 10029 get containedElement() { | |
| 10030 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
| 10031 }, | |
| 10032 | |
| 10033 /** | |
| 10034 * The element that should be focused when the dropdown opens. | |
| 10035 * @deprecated | |
| 10036 */ | |
| 10037 get _focusTarget() { | |
| 10038 return this.focusTarget || this.containedElement; | |
| 10039 }, | |
| 10040 | |
| 10041 ready: function() { | |
| 10042 // Memoized scrolling position, used to block scrolling outside. | |
| 10043 this._scrollTop = 0; | |
| 10044 this._scrollLeft = 0; | |
| 10045 // Used to perform a non-blocking refit on scroll. | |
| 10046 this._refitOnScrollRAF = null; | |
| 10047 }, | |
| 10048 | |
| 10049 detached: function() { | |
| 10050 this.cancelAnimation(); | |
| 10051 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
| 10052 }, | |
| 10053 | |
| 10054 /** | |
| 10055 * Called when the value of `opened` changes. | |
| 10056 * Overridden from `IronOverlayBehavior` | |
| 10057 */ | |
| 10058 _openedChanged: function() { | |
| 10059 if (this.opened && this.disabled) { | |
| 10060 this.cancel(); | |
| 10061 } else { | |
| 10062 this.cancelAnimation(); | |
| 10063 this.sizingTarget = this.containedElement || this.sizingTarget; | |
| 10064 this._updateAnimationConfig(); | |
| 10065 this._saveScrollPosition(); | |
| 10066 if (this.opened) { | |
| 10067 document.addEventListener('scroll', this._boundOnCaptureScroll); | |
| 10068 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | |
| 10069 } else { | |
| 10070 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | |
| 10071 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
| 10072 } | |
| 10073 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | |
| 10074 } | |
| 10075 }, | |
| 10076 | |
| 10077 /** | |
| 10078 * Overridden from `IronOverlayBehavior`. | |
| 10079 */ | |
| 10080 _renderOpened: function() { | |
| 10081 if (!this.noAnimations && this.animationConfig.open) { | |
| 10082 this.$.contentWrapper.classList.add('animating'); | |
| 10083 this.playAnimation('open'); | |
| 10084 } else { | |
| 10085 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | |
| 10086 } | |
| 10087 }, | |
| 10088 | |
| 10089 /** | |
| 10090 * Overridden from `IronOverlayBehavior`. | |
| 10091 */ | |
| 10092 _renderClosed: function() { | |
| 10093 | |
| 10094 if (!this.noAnimations && this.animationConfig.close) { | |
| 10095 this.$.contentWrapper.classList.add('animating'); | |
| 10096 this.playAnimation('close'); | |
| 10097 } else { | |
| 10098 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | |
| 10099 } | |
| 10100 }, | |
| 10101 | |
| 10102 /** | |
| 10103 * Called when animation finishes on the dropdown (when opening or | |
| 10104 * closing). Responsible for "completing" the process of opening or | |
| 10105 * closing the dropdown by positioning it or setting its display to | |
| 10106 * none. | |
| 10107 */ | |
| 10108 _onNeonAnimationFinish: function() { | |
| 10109 this.$.contentWrapper.classList.remove('animating'); | |
| 10110 if (this.opened) { | |
| 10111 this._finishRenderOpened(); | |
| 10112 } else { | |
| 10113 this._finishRenderClosed(); | |
| 10114 } | |
| 10115 }, | |
| 10116 | |
| 10117 _onCaptureScroll: function() { | |
| 10118 if (!this.allowOutsideScroll) { | |
| 10119 this._restoreScrollPosition(); | |
| 10120 } else { | |
| 10121 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | |
| 10122 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | |
| 10123 } | |
| 10124 }, | |
| 10125 | |
| 10126 /** | |
| 10127 * Memoizes the scroll position of the outside scrolling element. | |
| 10128 * @private | |
| 10129 */ | |
| 10130 _saveScrollPosition: function() { | |
| 10131 if (document.scrollingElement) { | |
| 10132 this._scrollTop = document.scrollingElement.scrollTop; | |
| 10133 this._scrollLeft = document.scrollingElement.scrollLeft; | |
| 10134 } else { | |
| 10135 // Since we don't know if is the body or html, get max. | |
| 10136 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | |
| 10137 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | |
| 10138 } | |
| 10139 }, | |
| 10140 | |
| 10141 /** | |
| 10142 * Resets the scroll position of the outside scrolling element. | |
| 10143 * @private | |
| 10144 */ | |
| 10145 _restoreScrollPosition: function() { | |
| 10146 if (document.scrollingElement) { | |
| 10147 document.scrollingElement.scrollTop = this._scrollTop; | |
| 10148 document.scrollingElement.scrollLeft = this._scrollLeft; | |
| 10149 } else { | |
| 10150 // Since we don't know if is the body or html, set both. | |
| 10151 document.documentElement.scrollTop = this._scrollTop; | |
| 10152 document.documentElement.scrollLeft = this._scrollLeft; | |
| 10153 document.body.scrollTop = this._scrollTop; | |
| 10154 document.body.scrollLeft = this._scrollLeft; | |
| 10155 } | |
| 10156 }, | |
| 10157 | |
| 10158 /** | |
| 10159 * Constructs the final animation config from different properties used | |
| 10160 * to configure specific parts of the opening and closing animations. | |
| 10161 */ | |
| 10162 _updateAnimationConfig: function() { | |
| 10163 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | |
| 10164 for (var i = 0; i < animations.length; i++) { | |
| 10165 animations[i].node = this.containedElement; | |
| 10166 } | |
| 10167 this.animationConfig = { | |
| 10168 open: this.openAnimationConfig, | |
| 10169 close: this.closeAnimationConfig | |
| 10170 }; | |
| 10171 }, | |
| 10172 | |
| 10173 /** | |
| 10174 * Updates the overlay position based on configured horizontal | |
| 10175 * and vertical alignment. | |
| 10176 */ | |
| 10177 _updateOverlayPosition: function() { | |
| 10178 if (this.isAttached) { | |
| 10179 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
| 10180 this.notifyResize(); | |
| 10181 } | |
| 10182 }, | |
| 10183 | |
| 10184 /** | |
| 10185 * Apply focus to focusTarget or containedElement | |
| 10186 */ | |
| 10187 _applyFocus: function () { | |
| 10188 var focusTarget = this.focusTarget || this.containedElement; | |
| 10189 if (focusTarget && this.opened && !this.noAutoFocus) { | |
| 10190 focusTarget.focus(); | |
| 10191 } else { | |
| 10192 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | |
| 10193 } | |
| 10194 } | |
| 10195 }); | |
| 10196 })(); | |
| 10197 Polymer({ | |
| 10198 | |
| 10199 is: 'fade-in-animation', | |
| 10200 | |
| 10201 behaviors: [ | |
| 10202 Polymer.NeonAnimationBehavior | |
| 10203 ], | |
| 10204 | |
| 10205 configure: function(config) { | |
| 10206 var node = config.node; | |
| 10207 this._effect = new KeyframeEffect(node, [ | |
| 10208 {'opacity': '0'}, | |
| 10209 {'opacity': '1'} | |
| 10210 ], this.timingFromConfig(config)); | |
| 10211 return this._effect; | |
| 10212 } | |
| 10213 | |
| 10214 }); | |
| 10215 Polymer({ | |
| 10216 | |
| 10217 is: 'fade-out-animation', | |
| 10218 | |
| 10219 behaviors: [ | |
| 10220 Polymer.NeonAnimationBehavior | |
| 10221 ], | |
| 10222 | |
| 10223 configure: function(config) { | |
| 10224 var node = config.node; | |
| 10225 this._effect = new KeyframeEffect(node, [ | |
| 10226 {'opacity': '1'}, | |
| 10227 {'opacity': '0'} | |
| 10228 ], this.timingFromConfig(config)); | |
| 10229 return this._effect; | |
| 10230 } | |
| 10231 | |
| 10232 }); | |
| 10233 Polymer({ | |
| 10234 is: 'paper-menu-grow-height-animation', | |
| 10235 | |
| 10236 behaviors: [ | |
| 10237 Polymer.NeonAnimationBehavior | |
| 10238 ], | |
| 10239 | |
| 10240 configure: function(config) { | |
| 10241 var node = config.node; | |
| 10242 var rect = node.getBoundingClientRect(); | |
| 10243 var height = rect.height; | |
| 10244 | |
| 10245 this._effect = new KeyframeEffect(node, [{ | |
| 10246 height: (height / 2) + 'px' | |
| 10247 }, { | |
| 10248 height: height + 'px' | |
| 10249 }], this.timingFromConfig(config)); | |
| 10250 | |
| 10251 return this._effect; | |
| 10252 } | |
| 10253 }); | |
| 10254 | |
| 10255 Polymer({ | |
| 10256 is: 'paper-menu-grow-width-animation', | |
| 10257 | |
| 10258 behaviors: [ | |
| 10259 Polymer.NeonAnimationBehavior | |
| 10260 ], | |
| 10261 | |
| 10262 configure: function(config) { | |
| 10263 var node = config.node; | |
| 10264 var rect = node.getBoundingClientRect(); | |
| 10265 var width = rect.width; | |
| 10266 | |
| 10267 this._effect = new KeyframeEffect(node, [{ | |
| 10268 width: (width / 2) + 'px' | |
| 10269 }, { | |
| 10270 width: width + 'px' | |
| 10271 }], this.timingFromConfig(config)); | |
| 10272 | |
| 10273 return this._effect; | |
| 10274 } | |
| 10275 }); | |
| 10276 | |
| 10277 Polymer({ | |
| 10278 is: 'paper-menu-shrink-width-animation', | |
| 10279 | |
| 10280 behaviors: [ | |
| 10281 Polymer.NeonAnimationBehavior | |
| 10282 ], | |
| 10283 | |
| 10284 configure: function(config) { | |
| 10285 var node = config.node; | |
| 10286 var rect = node.getBoundingClientRect(); | |
| 10287 var width = rect.width; | |
| 10288 | |
| 10289 this._effect = new KeyframeEffect(node, [{ | |
| 10290 width: width + 'px' | |
| 10291 }, { | |
| 10292 width: width - (width / 20) + 'px' | |
| 10293 }], this.timingFromConfig(config)); | |
| 10294 | |
| 10295 return this._effect; | |
| 10296 } | |
| 10297 }); | |
| 10298 | |
| 10299 Polymer({ | |
| 10300 is: 'paper-menu-shrink-height-animation', | |
| 10301 | |
| 10302 behaviors: [ | |
| 10303 Polymer.NeonAnimationBehavior | |
| 10304 ], | |
| 10305 | |
| 10306 configure: function(config) { | |
| 10307 var node = config.node; | |
| 10308 var rect = node.getBoundingClientRect(); | |
| 10309 var height = rect.height; | |
| 10310 var top = rect.top; | |
| 10311 | |
| 10312 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); | |
| 10313 | |
| 10314 this._effect = new KeyframeEffect(node, [{ | |
| 10315 height: height + 'px', | |
| 10316 transform: 'translateY(0)' | |
| 10317 }, { | |
| 10318 height: height / 2 + 'px', | |
| 10319 transform: 'translateY(-20px)' | |
| 10320 }], this.timingFromConfig(config)); | |
| 10321 | |
| 10322 return this._effect; | |
| 10323 } | |
| 10324 }); | |
| 10325 (function() { | |
| 10326 'use strict'; | |
| 10327 | |
| 10328 var config = { | |
| 10329 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | |
| 10330 MAX_ANIMATION_TIME_MS: 400 | |
| 10331 }; | |
| 10332 | |
| 10333 var PaperMenuButton = Polymer({ | |
| 10334 is: 'paper-menu-button', | |
| 10335 | |
| 10336 /** | |
| 10337 * Fired when the dropdown opens. | |
| 10338 * | |
| 10339 * @event paper-dropdown-open | |
| 10340 */ | |
| 10341 | |
| 10342 /** | |
| 10343 * Fired when the dropdown closes. | |
| 10344 * | |
| 10345 * @event paper-dropdown-close | |
| 10346 */ | |
| 10347 | |
| 10348 behaviors: [ | |
| 10349 Polymer.IronA11yKeysBehavior, | |
| 10350 Polymer.IronControlState | |
| 10351 ], | |
| 10352 | |
| 10353 properties: { | |
| 10354 /** | |
| 10355 * True if the content is currently displayed. | |
| 10356 */ | |
| 10357 opened: { | |
| 10358 type: Boolean, | |
| 10359 value: false, | |
| 10360 notify: true, | |
| 10361 observer: '_openedChanged' | |
| 10362 }, | |
| 10363 | |
| 10364 /** | |
| 10365 * The orientation against which to align the menu dropdown | |
| 10366 * horizontally relative to the dropdown trigger. | |
| 10367 */ | |
| 10368 horizontalAlign: { | |
| 10369 type: String, | |
| 10370 value: 'left', | |
| 10371 reflectToAttribute: true | |
| 10372 }, | |
| 10373 | |
| 10374 /** | |
| 10375 * The orientation against which to align the menu dropdown | |
| 10376 * vertically relative to the dropdown trigger. | |
| 10377 */ | |
| 10378 verticalAlign: { | |
| 10379 type: String, | |
| 10380 value: 'top', | |
| 10381 reflectToAttribute: true | |
| 10382 }, | |
| 10383 | |
| 10384 /** | |
| 10385 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
| 10386 * be considered preferences instead of strict requirements when | |
| 10387 * positioning the dropdown and may be changed if doing so reduces | |
| 10388 * the area of the dropdown falling outside of `fitInto`. | |
| 10389 */ | |
| 10390 dynamicAlign: { | |
| 10391 type: Boolean | |
| 10392 }, | |
| 10393 | |
| 10394 /** | |
| 10395 * A pixel value that will be added to the position calculated for the | |
| 10396 * given `horizontalAlign`. Use a negative value to offset to the | |
| 10397 * left, or a positive value to offset to the right. | |
| 10398 */ | |
| 10399 horizontalOffset: { | |
| 10400 type: Number, | |
| 10401 value: 0, | |
| 10402 notify: true | |
| 10403 }, | |
| 10404 | |
| 10405 /** | |
| 10406 * A pixel value that will be added to the position calculated for the | |
| 10407 * given `verticalAlign`. Use a negative value to offset towards the | |
| 10408 * top, or a positive value to offset towards the bottom. | |
| 10409 */ | |
| 10410 verticalOffset: { | |
| 10411 type: Number, | |
| 10412 value: 0, | |
| 10413 notify: true | |
| 10414 }, | |
| 10415 | |
| 10416 /** | |
| 10417 * If true, the dropdown will be positioned so that it doesn't overlap | |
| 10418 * the button. | |
| 10419 */ | |
| 10420 noOverlap: { | |
| 10421 type: Boolean | |
| 10422 }, | |
| 10423 | |
| 10424 /** | |
| 10425 * Set to true to disable animations when opening and closing the | |
| 10426 * dropdown. | |
| 10427 */ | |
| 10428 noAnimations: { | |
| 10429 type: Boolean, | |
| 10430 value: false | |
| 10431 }, | |
| 10432 | |
| 10433 /** | |
| 10434 * Set to true to disable automatically closing the dropdown after | |
| 10435 * a selection has been made. | |
| 10436 */ | |
| 10437 ignoreSelect: { | |
| 10438 type: Boolean, | |
| 10439 value: false | |
| 10440 }, | |
| 10441 | |
| 10442 /** | |
| 10443 * Set to true to enable automatically closing the dropdown after an | |
| 10444 * item has been activated, even if the selection did not change. | |
| 10445 */ | |
| 10446 closeOnActivate: { | |
| 10447 type: Boolean, | |
| 10448 value: false | |
| 10449 }, | |
| 10450 | |
| 10451 /** | |
| 10452 * An animation config. If provided, this will be used to animate the | |
| 10453 * opening of the dropdown. | |
| 10454 */ | |
| 10455 openAnimationConfig: { | |
| 10456 type: Object, | |
| 10457 value: function() { | |
| 10458 return [{ | |
| 10459 name: 'fade-in-animation', | |
| 10460 timing: { | |
| 10461 delay: 100, | |
| 10462 duration: 200 | |
| 10463 } | |
| 10464 }, { | |
| 10465 name: 'paper-menu-grow-width-animation', | |
| 10466 timing: { | |
| 10467 delay: 100, | |
| 10468 duration: 150, | |
| 10469 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10470 } | |
| 10471 }, { | |
| 10472 name: 'paper-menu-grow-height-animation', | |
| 10473 timing: { | |
| 10474 delay: 100, | |
| 10475 duration: 275, | |
| 10476 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10477 } | |
| 10478 }]; | |
| 10479 } | |
| 10480 }, | |
| 10481 | |
| 10482 /** | |
| 10483 * An animation config. If provided, this will be used to animate the | |
| 10484 * closing of the dropdown. | |
| 10485 */ | |
| 10486 closeAnimationConfig: { | |
| 10487 type: Object, | |
| 10488 value: function() { | |
| 10489 return [{ | |
| 10490 name: 'fade-out-animation', | |
| 10491 timing: { | |
| 10492 duration: 150 | |
| 10493 } | |
| 10494 }, { | |
| 10495 name: 'paper-menu-shrink-width-animation', | |
| 10496 timing: { | |
| 10497 delay: 100, | |
| 10498 duration: 50, | |
| 10499 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10500 } | |
| 10501 }, { | |
| 10502 name: 'paper-menu-shrink-height-animation', | |
| 10503 timing: { | |
| 10504 duration: 200, | |
| 10505 easing: 'ease-in' | |
| 10506 } | |
| 10507 }]; | |
| 10508 } | |
| 10509 }, | |
| 10510 | |
| 10511 /** | |
| 10512 * By default, the dropdown will constrain scrolling on the page | |
| 10513 * to itself when opened. | |
| 10514 * Set to true in order to prevent scroll from being constrained | |
| 10515 * to the dropdown when it opens. | |
| 10516 */ | |
| 10517 allowOutsideScroll: { | |
| 10518 type: Boolean, | |
| 10519 value: false | |
| 10520 }, | |
| 10521 | |
| 10522 /** | |
| 10523 * Whether focus should be restored to the button when the menu closes
. | |
| 10524 */ | |
| 10525 restoreFocusOnClose: { | |
| 10526 type: Boolean, | |
| 10527 value: true | |
| 10528 }, | |
| 10529 | |
| 10530 /** | |
| 10531 * This is the element intended to be bound as the focus target | |
| 10532 * for the `iron-dropdown` contained by `paper-menu-button`. | |
| 10533 */ | |
| 10534 _dropdownContent: { | |
| 10535 type: Object | |
| 10536 } | |
| 10537 }, | |
| 10538 | |
| 10539 hostAttributes: { | |
| 10540 role: 'group', | |
| 10541 'aria-haspopup': 'true' | |
| 10542 }, | |
| 10543 | |
| 10544 listeners: { | |
| 10545 'iron-activate': '_onIronActivate', | |
| 10546 'iron-select': '_onIronSelect' | |
| 10547 }, | |
| 10548 | |
| 10549 /** | |
| 10550 * The content element that is contained by the menu button, if any. | |
| 10551 */ | |
| 10552 get contentElement() { | |
| 10553 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
| 10554 }, | |
| 10555 | |
| 10556 /** | |
| 10557 * Toggles the drowpdown content between opened and closed. | |
| 10558 */ | |
| 10559 toggle: function() { | |
| 10560 if (this.opened) { | |
| 10561 this.close(); | |
| 10562 } else { | |
| 10563 this.open(); | |
| 10564 } | |
| 10565 }, | |
| 10566 | |
| 10567 /** | |
| 10568 * Make the dropdown content appear as an overlay positioned relative | |
| 10569 * to the dropdown trigger. | |
| 10570 */ | |
| 10571 open: function() { | |
| 10572 if (this.disabled) { | |
| 10573 return; | |
| 10574 } | |
| 10575 | |
| 10576 this.$.dropdown.open(); | |
| 10577 }, | |
| 10578 | |
| 10579 /** | |
| 10580 * Hide the dropdown content. | |
| 10581 */ | |
| 10582 close: function() { | |
| 10583 this.$.dropdown.close(); | |
| 10584 }, | |
| 10585 | |
| 10586 /** | |
| 10587 * When an `iron-select` event is received, the dropdown should | |
| 10588 * automatically close on the assumption that a value has been chosen. | |
| 10589 * | |
| 10590 * @param {CustomEvent} event A CustomEvent instance with type | |
| 10591 * set to `"iron-select"`. | |
| 10592 */ | |
| 10593 _onIronSelect: function(event) { | |
| 10594 if (!this.ignoreSelect) { | |
| 10595 this.close(); | |
| 10596 } | |
| 10597 }, | |
| 10598 | |
| 10599 /** | |
| 10600 * Closes the dropdown when an `iron-activate` event is received if | |
| 10601 * `closeOnActivate` is true. | |
| 10602 * | |
| 10603 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | |
| 10604 */ | |
| 10605 _onIronActivate: function(event) { | |
| 10606 if (this.closeOnActivate) { | |
| 10607 this.close(); | |
| 10608 } | |
| 10609 }, | |
| 10610 | |
| 10611 /** | |
| 10612 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | |
| 10613 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | |
| 10614 * | |
| 10615 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | |
| 10616 * @param {boolean} oldOpened The previous value of `opened`. | |
| 10617 */ | |
| 10618 _openedChanged: function(opened, oldOpened) { | |
| 10619 if (opened) { | |
| 10620 // TODO(cdata): Update this when we can measure changes in distribut
ed | |
| 10621 // children in an idiomatic way. | |
| 10622 // We poke this property in case the element has changed. This will | |
| 10623 // cause the focus target for the `iron-dropdown` to be updated as | |
| 10624 // necessary: | |
| 10625 this._dropdownContent = this.contentElement; | |
| 10626 this.fire('paper-dropdown-open'); | |
| 10627 } else if (oldOpened != null) { | |
| 10628 this.fire('paper-dropdown-close'); | |
| 10629 } | |
| 10630 }, | |
| 10631 | |
| 10632 /** | |
| 10633 * If the dropdown is open when disabled becomes true, close the | |
| 10634 * dropdown. | |
| 10635 * | |
| 10636 * @param {boolean} disabled True if disabled, otherwise false. | |
| 10637 */ | |
| 10638 _disabledChanged: function(disabled) { | |
| 10639 Polymer.IronControlState._disabledChanged.apply(this, arguments); | |
| 10640 if (disabled && this.opened) { | |
| 10641 this.close(); | |
| 10642 } | |
| 10643 }, | |
| 10644 | |
| 10645 __onIronOverlayCanceled: function(event) { | |
| 10646 var uiEvent = event.detail; | |
| 10647 var target = Polymer.dom(uiEvent).rootTarget; | |
| 10648 var trigger = this.$.trigger; | |
| 10649 var path = Polymer.dom(uiEvent).path; | |
| 10650 | |
| 10651 if (path.indexOf(trigger) > -1) { | |
| 10652 event.preventDefault(); | |
| 10653 } | |
| 10654 } | |
| 10655 }); | |
| 10656 | |
| 10657 Object.keys(config).forEach(function (key) { | |
| 10658 PaperMenuButton[key] = config[key]; | |
| 10659 }); | |
| 10660 | |
| 10661 Polymer.PaperMenuButton = PaperMenuButton; | |
| 10662 })(); | |
| 10663 /** | |
| 10664 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | |
| 10665 * | |
| 10666 * @polymerBehavior Polymer.PaperInkyFocusBehavior | |
| 10667 */ | |
| 10668 Polymer.PaperInkyFocusBehaviorImpl = { | |
| 10669 observers: [ | |
| 10670 '_focusedChanged(receivedFocusFromKeyboard)' | |
| 10671 ], | |
| 10672 | |
| 10673 _focusedChanged: function(receivedFocusFromKeyboard) { | |
| 10674 if (receivedFocusFromKeyboard) { | |
| 10675 this.ensureRipple(); | |
| 10676 } | |
| 10677 if (this.hasRipple()) { | |
| 10678 this._ripple.holdDown = receivedFocusFromKeyboard; | |
| 10679 } | |
| 10680 }, | |
| 10681 | |
| 10682 _createRipple: function() { | |
| 10683 var ripple = Polymer.PaperRippleBehavior._createRipple(); | |
| 10684 ripple.id = 'ink'; | |
| 10685 ripple.setAttribute('center', ''); | |
| 10686 ripple.classList.add('circle'); | |
| 10687 return ripple; | |
| 10688 } | |
| 10689 }; | |
| 10690 | |
| 10691 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | |
| 10692 Polymer.PaperInkyFocusBehavior = [ | |
| 10693 Polymer.IronButtonState, | |
| 10694 Polymer.IronControlState, | |
| 10695 Polymer.PaperRippleBehavior, | |
| 10696 Polymer.PaperInkyFocusBehaviorImpl | |
| 10697 ]; | |
| 10698 Polymer({ | |
| 10699 is: 'paper-icon-button', | |
| 10700 | |
| 10701 hostAttributes: { | |
| 10702 role: 'button', | |
| 10703 tabindex: '0' | |
| 10704 }, | |
| 10705 | |
| 10706 behaviors: [ | |
| 10707 Polymer.PaperInkyFocusBehavior | |
| 10708 ], | |
| 10709 | |
| 10710 properties: { | |
| 10711 /** | |
| 10712 * The URL of an image for the icon. If the src property is specified, | |
| 10713 * the icon property should not be. | |
| 10714 */ | |
| 10715 src: { | |
| 10716 type: String | |
| 10717 }, | |
| 10718 | |
| 10719 /** | |
| 10720 * Specifies the icon name or index in the set of icons available in | |
| 10721 * the icon's icon set. If the icon property is specified, | |
| 10722 * the src property should not be. | |
| 10723 */ | |
| 10724 icon: { | |
| 10725 type: String | |
| 10726 }, | |
| 10727 | |
| 10728 /** | |
| 10729 * Specifies the alternate text for the button, for accessibility. | |
| 10730 */ | |
| 10731 alt: { | |
| 10732 type: String, | |
| 10733 observer: "_altChanged" | |
| 10734 } | |
| 10735 }, | |
| 10736 | |
| 10737 _altChanged: function(newValue, oldValue) { | |
| 10738 var label = this.getAttribute('aria-label'); | |
| 10739 | |
| 10740 // Don't stomp over a user-set aria-label. | |
| 10741 if (!label || oldValue == label) { | |
| 10742 this.setAttribute('aria-label', newValue); | |
| 10743 } | |
| 10744 } | |
| 10745 }); | |
| 10746 // Copyright 2016 The Chromium Authors. All rights reserved. | 6030 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 10747 // Use of this source code is governed by a BSD-style license that can be | 6031 // Use of this source code is governed by a BSD-style license that can be |
| 10748 // found in the LICENSE file. | 6032 // found in the LICENSE file. |
| 10749 | |
| 10750 /** | |
| 10751 * Implements an incremental search field which can be shown and hidden. | |
| 10752 * Canonical implementation is <cr-search-field>. | |
| 10753 * @polymerBehavior | |
| 10754 */ | |
| 10755 var CrSearchFieldBehavior = { | 6033 var CrSearchFieldBehavior = { |
| 10756 properties: { | 6034 properties: { |
| 10757 label: { | 6035 label: { |
| 10758 type: String, | 6036 type: String, |
| 10759 value: '', | 6037 value: '' |
| 10760 }, | 6038 }, |
| 10761 | |
| 10762 clearLabel: { | 6039 clearLabel: { |
| 10763 type: String, | 6040 type: String, |
| 10764 value: '', | 6041 value: '' |
| 10765 }, | 6042 }, |
| 10766 | |
| 10767 showingSearch: { | 6043 showingSearch: { |
| 10768 type: Boolean, | 6044 type: Boolean, |
| 10769 value: false, | 6045 value: false, |
| 10770 notify: true, | 6046 notify: true, |
| 10771 observer: 'showingSearchChanged_', | 6047 observer: 'showingSearchChanged_', |
| 10772 reflectToAttribute: true | 6048 reflectToAttribute: true |
| 10773 }, | 6049 }, |
| 10774 | |
| 10775 /** @private */ | |
| 10776 lastValue_: { | 6050 lastValue_: { |
| 10777 type: String, | 6051 type: String, |
| 10778 value: '', | 6052 value: '' |
| 10779 }, | 6053 } |
| 10780 }, | 6054 }, |
| 10781 | |
| 10782 /** | |
| 10783 * @abstract | |
| 10784 * @return {!HTMLInputElement} The input field element the behavior should | |
| 10785 * use. | |
| 10786 */ | |
| 10787 getSearchInput: function() {}, | 6055 getSearchInput: function() {}, |
| 10788 | |
| 10789 /** | |
| 10790 * @return {string} The value of the search field. | |
| 10791 */ | |
| 10792 getValue: function() { | 6056 getValue: function() { |
| 10793 return this.getSearchInput().value; | 6057 return this.getSearchInput().value; |
| 10794 }, | 6058 }, |
| 10795 | |
| 10796 /** | |
| 10797 * Sets the value of the search field. | |
| 10798 * @param {string} value | |
| 10799 */ | |
| 10800 setValue: function(value) { | 6059 setValue: function(value) { |
| 10801 // Use bindValue when setting the input value so that changes propagate | |
| 10802 // correctly. | |
| 10803 this.getSearchInput().bindValue = value; | 6060 this.getSearchInput().bindValue = value; |
| 10804 this.onValueChanged_(value); | 6061 this.onValueChanged_(value); |
| 10805 }, | 6062 }, |
| 10806 | |
| 10807 showAndFocus: function() { | 6063 showAndFocus: function() { |
| 10808 this.showingSearch = true; | 6064 this.showingSearch = true; |
| 10809 this.focus_(); | 6065 this.focus_(); |
| 10810 }, | 6066 }, |
| 10811 | |
| 10812 /** @private */ | |
| 10813 focus_: function() { | 6067 focus_: function() { |
| 10814 this.getSearchInput().focus(); | 6068 this.getSearchInput().focus(); |
| 10815 }, | 6069 }, |
| 10816 | |
| 10817 onSearchTermSearch: function() { | 6070 onSearchTermSearch: function() { |
| 10818 this.onValueChanged_(this.getValue()); | 6071 this.onValueChanged_(this.getValue()); |
| 10819 }, | 6072 }, |
| 10820 | |
| 10821 /** | |
| 10822 * Updates the internal state of the search field based on a change that has | |
| 10823 * already happened. | |
| 10824 * @param {string} newValue | |
| 10825 * @private | |
| 10826 */ | |
| 10827 onValueChanged_: function(newValue) { | 6073 onValueChanged_: function(newValue) { |
| 10828 if (newValue == this.lastValue_) | 6074 if (newValue == this.lastValue_) return; |
| 10829 return; | |
| 10830 | |
| 10831 this.fire('search-changed', newValue); | 6075 this.fire('search-changed', newValue); |
| 10832 this.lastValue_ = newValue; | 6076 this.lastValue_ = newValue; |
| 10833 }, | 6077 }, |
| 10834 | |
| 10835 onSearchTermKeydown: function(e) { | 6078 onSearchTermKeydown: function(e) { |
| 10836 if (e.key == 'Escape') | 6079 if (e.key == 'Escape') this.showingSearch = false; |
| 10837 this.showingSearch = false; | 6080 }, |
| 10838 }, | |
| 10839 | |
| 10840 /** @private */ | |
| 10841 showingSearchChanged_: function() { | 6081 showingSearchChanged_: function() { |
| 10842 if (this.showingSearch) { | 6082 if (this.showingSearch) { |
| 10843 this.focus_(); | 6083 this.focus_(); |
| 10844 return; | 6084 return; |
| 10845 } | 6085 } |
| 10846 | |
| 10847 this.setValue(''); | 6086 this.setValue(''); |
| 10848 this.getSearchInput().blur(); | 6087 this.getSearchInput().blur(); |
| 10849 } | 6088 } |
| 10850 }; | 6089 }; |
| 6090 |
| 10851 (function() { | 6091 (function() { |
| 10852 'use strict'; | 6092 'use strict'; |
| 10853 | 6093 Polymer.IronA11yAnnouncer = Polymer({ |
| 10854 Polymer.IronA11yAnnouncer = Polymer({ | 6094 is: 'iron-a11y-announcer', |
| 10855 is: 'iron-a11y-announcer', | |
| 10856 | |
| 10857 properties: { | |
| 10858 | |
| 10859 /** | |
| 10860 * The value of mode is used to set the `aria-live` attribute | |
| 10861 * for the element that will be announced. Valid values are: `off`, | |
| 10862 * `polite` and `assertive`. | |
| 10863 */ | |
| 10864 mode: { | |
| 10865 type: String, | |
| 10866 value: 'polite' | |
| 10867 }, | |
| 10868 | |
| 10869 _text: { | |
| 10870 type: String, | |
| 10871 value: '' | |
| 10872 } | |
| 10873 }, | |
| 10874 | |
| 10875 created: function() { | |
| 10876 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 10877 Polymer.IronA11yAnnouncer.instance = this; | |
| 10878 } | |
| 10879 | |
| 10880 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
| 10881 }, | |
| 10882 | |
| 10883 /** | |
| 10884 * Cause a text string to be announced by screen readers. | |
| 10885 * | |
| 10886 * @param {string} text The text that should be announced. | |
| 10887 */ | |
| 10888 announce: function(text) { | |
| 10889 this._text = ''; | |
| 10890 this.async(function() { | |
| 10891 this._text = text; | |
| 10892 }, 100); | |
| 10893 }, | |
| 10894 | |
| 10895 _onIronAnnounce: function(event) { | |
| 10896 if (event.detail && event.detail.text) { | |
| 10897 this.announce(event.detail.text); | |
| 10898 } | |
| 10899 } | |
| 10900 }); | |
| 10901 | |
| 10902 Polymer.IronA11yAnnouncer.instance = null; | |
| 10903 | |
| 10904 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
| 10905 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 10906 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
| 10907 } | |
| 10908 | |
| 10909 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
| 10910 }; | |
| 10911 })(); | |
| 10912 /** | |
| 10913 * Singleton IronMeta instance. | |
| 10914 */ | |
| 10915 Polymer.IronValidatableBehaviorMeta = null; | |
| 10916 | |
| 10917 /** | |
| 10918 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
| 10919 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
| 10920 * | |
| 10921 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
| 10922 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
| 10923 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
| 10924 * custom element will get a public `validate()`, which | |
| 10925 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
| 10926 * which can be used for styling. | |
| 10927 * | |
| 10928 * To implement the custom validation logic of your element, you must override | |
| 10929 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
| 10930 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
| 10931 * for an example. | |
| 10932 * | |
| 10933 * ### Accessibility | |
| 10934 * | |
| 10935 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
| 10936 * `aria-invalid` attribute. | |
| 10937 * | |
| 10938 * @demo demo/index.html | |
| 10939 * @polymerBehavior | |
| 10940 */ | |
| 10941 Polymer.IronValidatableBehavior = { | |
| 10942 | |
| 10943 properties: { | 6095 properties: { |
| 10944 | 6096 mode: { |
| 10945 /** | 6097 type: String, |
| 10946 * Name of the validator to use. | 6098 value: 'polite' |
| 10947 */ | |
| 10948 validator: { | |
| 10949 type: String | |
| 10950 }, | 6099 }, |
| 10951 | 6100 _text: { |
| 10952 /** | |
| 10953 * True if the last call to `validate` is invalid. | |
| 10954 */ | |
| 10955 invalid: { | |
| 10956 notify: true, | |
| 10957 reflectToAttribute: true, | |
| 10958 type: Boolean, | |
| 10959 value: false | |
| 10960 }, | |
| 10961 | |
| 10962 /** | |
| 10963 * This property is deprecated and should not be used. Use the global | |
| 10964 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
| 10965 */ | |
| 10966 _validatorMeta: { | |
| 10967 type: Object | |
| 10968 }, | |
| 10969 | |
| 10970 /** | |
| 10971 * Namespace for this validator. This property is deprecated and should | |
| 10972 * not be used. For all intents and purposes, please consider it a | |
| 10973 * read-only, config-time property. | |
| 10974 */ | |
| 10975 validatorType: { | |
| 10976 type: String, | |
| 10977 value: 'validator' | |
| 10978 }, | |
| 10979 | |
| 10980 _validator: { | |
| 10981 type: Object, | |
| 10982 computed: '__computeValidator(validator)' | |
| 10983 } | |
| 10984 }, | |
| 10985 | |
| 10986 observers: [ | |
| 10987 '_invalidChanged(invalid)' | |
| 10988 ], | |
| 10989 | |
| 10990 registered: function() { | |
| 10991 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
| 10992 }, | |
| 10993 | |
| 10994 _invalidChanged: function() { | |
| 10995 if (this.invalid) { | |
| 10996 this.setAttribute('aria-invalid', 'true'); | |
| 10997 } else { | |
| 10998 this.removeAttribute('aria-invalid'); | |
| 10999 } | |
| 11000 }, | |
| 11001 | |
| 11002 /** | |
| 11003 * @return {boolean} True if the validator `validator` exists. | |
| 11004 */ | |
| 11005 hasValidator: function() { | |
| 11006 return this._validator != null; | |
| 11007 }, | |
| 11008 | |
| 11009 /** | |
| 11010 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
| 11011 * your element to have custom validation logic, do not override this method
; | |
| 11012 * override `_getValidity(value)` instead. | |
| 11013 | |
| 11014 * @param {Object} value The value to be validated. By default, it is passed | |
| 11015 * to the validator's `validate()` function, if a validator is set. | |
| 11016 * @return {boolean} True if `value` is valid. | |
| 11017 */ | |
| 11018 validate: function(value) { | |
| 11019 this.invalid = !this._getValidity(value); | |
| 11020 return !this.invalid; | |
| 11021 }, | |
| 11022 | |
| 11023 /** | |
| 11024 * Returns true if `value` is valid. By default, it is passed | |
| 11025 * to the validator's `validate()` function, if a validator is set. You | |
| 11026 * should override this method if you want to implement custom validity | |
| 11027 * logic for your element. | |
| 11028 * | |
| 11029 * @param {Object} value The value to be validated. | |
| 11030 * @return {boolean} True if `value` is valid. | |
| 11031 */ | |
| 11032 | |
| 11033 _getValidity: function(value) { | |
| 11034 if (this.hasValidator()) { | |
| 11035 return this._validator.validate(value); | |
| 11036 } | |
| 11037 return true; | |
| 11038 }, | |
| 11039 | |
| 11040 __computeValidator: function() { | |
| 11041 return Polymer.IronValidatableBehaviorMeta && | |
| 11042 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
| 11043 } | |
| 11044 }; | |
| 11045 /* | |
| 11046 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
| 11047 to `<input>`. | |
| 11048 | |
| 11049 ### Two-way binding | |
| 11050 | |
| 11051 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
| 11052 | |
| 11053 <input value="{{myValue::input}}"> | |
| 11054 | |
| 11055 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
| 11056 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
| 11057 | |
| 11058 <input is="iron-input" bind-value="{{myValue}}"> | |
| 11059 | |
| 11060 ### Custom validators | |
| 11061 | |
| 11062 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
| 11063 | |
| 11064 <input is="iron-input" validator="my-custom-validator"> | |
| 11065 | |
| 11066 ### Stopping invalid input | |
| 11067 | |
| 11068 It may be desirable to only allow users to enter certain characters. You can use
the | |
| 11069 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
| 11070 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
| 11071 | |
| 11072 \x3c!-- only allow characters that match [0-9] --\x3e | |
| 11073 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
| 11074 | |
| 11075 @hero hero.svg | |
| 11076 @demo demo/index.html | |
| 11077 */ | |
| 11078 | |
| 11079 Polymer({ | |
| 11080 | |
| 11081 is: 'iron-input', | |
| 11082 | |
| 11083 extends: 'input', | |
| 11084 | |
| 11085 behaviors: [ | |
| 11086 Polymer.IronValidatableBehavior | |
| 11087 ], | |
| 11088 | |
| 11089 properties: { | |
| 11090 | |
| 11091 /** | |
| 11092 * Use this property instead of `value` for two-way data binding. | |
| 11093 */ | |
| 11094 bindValue: { | |
| 11095 observer: '_bindValueChanged', | |
| 11096 type: String | |
| 11097 }, | |
| 11098 | |
| 11099 /** | |
| 11100 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
| 11101 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
| 11102 * Pasted input will have each character checked individually; if any char
acter | |
| 11103 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
| 11104 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
| 11105 */ | |
| 11106 preventInvalidInput: { | |
| 11107 type: Boolean | |
| 11108 }, | |
| 11109 | |
| 11110 /** | |
| 11111 * Regular expression that list the characters allowed as input. | |
| 11112 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
| 11113 * each individual character will be checked against the pattern (rather t
han checking | |
| 11114 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
| 11115 * for example, `[a-zA-Z0-9.+-!;:]` | |
| 11116 */ | |
| 11117 allowedPattern: { | |
| 11118 type: String, | |
| 11119 observer: "_allowedPatternChanged" | |
| 11120 }, | |
| 11121 | |
| 11122 _previousValidInput: { | |
| 11123 type: String, | 6101 type: String, |
| 11124 value: '' | 6102 value: '' |
| 11125 }, | 6103 } |
| 11126 | 6104 }, |
| 11127 _patternAlreadyChecked: { | |
| 11128 type: Boolean, | |
| 11129 value: false | |
| 11130 } | |
| 11131 | |
| 11132 }, | |
| 11133 | |
| 11134 listeners: { | |
| 11135 'input': '_onInput', | |
| 11136 'keypress': '_onKeypress' | |
| 11137 }, | |
| 11138 | |
| 11139 /** @suppress {checkTypes} */ | |
| 11140 registered: function() { | |
| 11141 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
| 11142 if (!this._canDispatchEventOnDisabled()) { | |
| 11143 this._origDispatchEvent = this.dispatchEvent; | |
| 11144 this.dispatchEvent = this._dispatchEventFirefoxIE; | |
| 11145 } | |
| 11146 }, | |
| 11147 | |
| 11148 created: function() { | 6105 created: function() { |
| 11149 Polymer.IronA11yAnnouncer.requestAvailability(); | 6106 if (!Polymer.IronA11yAnnouncer.instance) { |
| 11150 }, | 6107 Polymer.IronA11yAnnouncer.instance = this; |
| 11151 | 6108 } |
| 11152 _canDispatchEventOnDisabled: function() { | 6109 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(
this)); |
| 11153 var input = document.createElement('input'); | 6110 }, |
| 11154 var canDispatch = false; | 6111 announce: function(text) { |
| 11155 input.disabled = true; | 6112 this._text = ''; |
| 11156 | 6113 this.async(function() { |
| 11157 input.addEventListener('feature-check-dispatch-event', function() { | 6114 this._text = text; |
| 11158 canDispatch = true; | 6115 }, 100); |
| 11159 }); | 6116 }, |
| 11160 | 6117 _onIronAnnounce: function(event) { |
| 11161 try { | 6118 if (event.detail && event.detail.text) { |
| 11162 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 6119 this.announce(event.detail.text); |
| 11163 } catch(e) {} | 6120 } |
| 11164 | 6121 } |
| 11165 return canDispatch; | 6122 }); |
| 11166 }, | 6123 Polymer.IronA11yAnnouncer.instance = null; |
| 11167 | 6124 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
| 11168 _dispatchEventFirefoxIE: function() { | 6125 if (!Polymer.IronA11yAnnouncer.instance) { |
| 11169 // Due to Firefox bug, events fired on disabled form controls can throw | 6126 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann
ouncer'); |
| 11170 // errors; furthermore, neither IE nor Firefox will actually dispatch | 6127 } |
| 11171 // events from disabled form controls; as such, we toggle disable around | 6128 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
| 11172 // the dispatch to allow notifying properties to notify | 6129 }; |
| 11173 // See issue #47 for details | 6130 })(); |
| 11174 var disabled = this.disabled; | 6131 |
| 11175 this.disabled = false; | 6132 Polymer.IronValidatableBehaviorMeta = null; |
| 11176 this._origDispatchEvent.apply(this, arguments); | 6133 |
| 11177 this.disabled = disabled; | 6134 Polymer.IronValidatableBehavior = { |
| 11178 }, | 6135 properties: { |
| 11179 | 6136 validator: { |
| 11180 get _patternRegExp() { | 6137 type: String |
| 11181 var pattern; | 6138 }, |
| 11182 if (this.allowedPattern) { | 6139 invalid: { |
| 11183 pattern = new RegExp(this.allowedPattern); | 6140 notify: true, |
| 6141 reflectToAttribute: true, |
| 6142 type: Boolean, |
| 6143 value: false |
| 6144 }, |
| 6145 _validatorMeta: { |
| 6146 type: Object |
| 6147 }, |
| 6148 validatorType: { |
| 6149 type: String, |
| 6150 value: 'validator' |
| 6151 }, |
| 6152 _validator: { |
| 6153 type: Object, |
| 6154 computed: '__computeValidator(validator)' |
| 6155 } |
| 6156 }, |
| 6157 observers: [ '_invalidChanged(invalid)' ], |
| 6158 registered: function() { |
| 6159 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({ |
| 6160 type: 'validator' |
| 6161 }); |
| 6162 }, |
| 6163 _invalidChanged: function() { |
| 6164 if (this.invalid) { |
| 6165 this.setAttribute('aria-invalid', 'true'); |
| 6166 } else { |
| 6167 this.removeAttribute('aria-invalid'); |
| 6168 } |
| 6169 }, |
| 6170 hasValidator: function() { |
| 6171 return this._validator != null; |
| 6172 }, |
| 6173 validate: function(value) { |
| 6174 this.invalid = !this._getValidity(value); |
| 6175 return !this.invalid; |
| 6176 }, |
| 6177 _getValidity: function(value) { |
| 6178 if (this.hasValidator()) { |
| 6179 return this._validator.validate(value); |
| 6180 } |
| 6181 return true; |
| 6182 }, |
| 6183 __computeValidator: function() { |
| 6184 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio
rMeta.byKey(this.validator); |
| 6185 } |
| 6186 }; |
| 6187 |
| 6188 Polymer({ |
| 6189 is: 'iron-input', |
| 6190 "extends": 'input', |
| 6191 behaviors: [ Polymer.IronValidatableBehavior ], |
| 6192 properties: { |
| 6193 bindValue: { |
| 6194 observer: '_bindValueChanged', |
| 6195 type: String |
| 6196 }, |
| 6197 preventInvalidInput: { |
| 6198 type: Boolean |
| 6199 }, |
| 6200 allowedPattern: { |
| 6201 type: String, |
| 6202 observer: "_allowedPatternChanged" |
| 6203 }, |
| 6204 _previousValidInput: { |
| 6205 type: String, |
| 6206 value: '' |
| 6207 }, |
| 6208 _patternAlreadyChecked: { |
| 6209 type: Boolean, |
| 6210 value: false |
| 6211 } |
| 6212 }, |
| 6213 listeners: { |
| 6214 input: '_onInput', |
| 6215 keypress: '_onKeypress' |
| 6216 }, |
| 6217 registered: function() { |
| 6218 if (!this._canDispatchEventOnDisabled()) { |
| 6219 this._origDispatchEvent = this.dispatchEvent; |
| 6220 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 6221 } |
| 6222 }, |
| 6223 created: function() { |
| 6224 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 6225 }, |
| 6226 _canDispatchEventOnDisabled: function() { |
| 6227 var input = document.createElement('input'); |
| 6228 var canDispatch = false; |
| 6229 input.disabled = true; |
| 6230 input.addEventListener('feature-check-dispatch-event', function() { |
| 6231 canDispatch = true; |
| 6232 }); |
| 6233 try { |
| 6234 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 6235 } catch (e) {} |
| 6236 return canDispatch; |
| 6237 }, |
| 6238 _dispatchEventFirefoxIE: function() { |
| 6239 var disabled = this.disabled; |
| 6240 this.disabled = false; |
| 6241 this._origDispatchEvent.apply(this, arguments); |
| 6242 this.disabled = disabled; |
| 6243 }, |
| 6244 get _patternRegExp() { |
| 6245 var pattern; |
| 6246 if (this.allowedPattern) { |
| 6247 pattern = new RegExp(this.allowedPattern); |
| 6248 } else { |
| 6249 switch (this.type) { |
| 6250 case 'number': |
| 6251 pattern = /[0-9.,e-]/; |
| 6252 break; |
| 6253 } |
| 6254 } |
| 6255 return pattern; |
| 6256 }, |
| 6257 ready: function() { |
| 6258 this.bindValue = this.value; |
| 6259 }, |
| 6260 _bindValueChanged: function() { |
| 6261 if (this.value !== this.bindValue) { |
| 6262 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue ==
= false) ? '' : this.bindValue; |
| 6263 } |
| 6264 this.fire('bind-value-changed', { |
| 6265 value: this.bindValue |
| 6266 }); |
| 6267 }, |
| 6268 _allowedPatternChanged: function() { |
| 6269 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 6270 }, |
| 6271 _onInput: function() { |
| 6272 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 6273 var valid = this._checkPatternValidity(); |
| 6274 if (!valid) { |
| 6275 this._announceInvalidCharacter('Invalid string of characters not entered
.'); |
| 6276 this.value = this._previousValidInput; |
| 6277 } |
| 6278 } |
| 6279 this.bindValue = this.value; |
| 6280 this._previousValidInput = this.value; |
| 6281 this._patternAlreadyChecked = false; |
| 6282 }, |
| 6283 _isPrintable: function(event) { |
| 6284 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC
ode == 13 || event.keyCode == 27; |
| 6285 var mozNonPrintable = event.keyCode == 19 || event.keyCode == 20 || event.ke
yCode == 45 || event.keyCode == 46 || event.keyCode == 144 || event.keyCode == 1
45 || event.keyCode > 32 && event.keyCode < 41 || event.keyCode > 111 && event.k
eyCode < 124; |
| 6286 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 6287 }, |
| 6288 _onKeypress: function(event) { |
| 6289 if (!this.preventInvalidInput && this.type !== 'number') { |
| 6290 return; |
| 6291 } |
| 6292 var regexp = this._patternRegExp; |
| 6293 if (!regexp) { |
| 6294 return; |
| 6295 } |
| 6296 if (event.metaKey || event.ctrlKey || event.altKey) return; |
| 6297 this._patternAlreadyChecked = true; |
| 6298 var thisChar = String.fromCharCode(event.charCode); |
| 6299 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 6300 event.preventDefault(); |
| 6301 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent
ered.'); |
| 6302 } |
| 6303 }, |
| 6304 _checkPatternValidity: function() { |
| 6305 var regexp = this._patternRegExp; |
| 6306 if (!regexp) { |
| 6307 return true; |
| 6308 } |
| 6309 for (var i = 0; i < this.value.length; i++) { |
| 6310 if (!regexp.test(this.value[i])) { |
| 6311 return false; |
| 6312 } |
| 6313 } |
| 6314 return true; |
| 6315 }, |
| 6316 validate: function() { |
| 6317 var valid = this.checkValidity(); |
| 6318 if (valid) { |
| 6319 if (this.required && this.value === '') { |
| 6320 valid = false; |
| 6321 } else if (this.hasValidator()) { |
| 6322 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); |
| 6323 } |
| 6324 } |
| 6325 this.invalid = !valid; |
| 6326 this.fire('iron-input-validate'); |
| 6327 return valid; |
| 6328 }, |
| 6329 _announceInvalidCharacter: function(message) { |
| 6330 this.fire('iron-announce', { |
| 6331 text: message |
| 6332 }); |
| 6333 } |
| 6334 }); |
| 6335 |
| 6336 Polymer({ |
| 6337 is: 'paper-input-container', |
| 6338 properties: { |
| 6339 noLabelFloat: { |
| 6340 type: Boolean, |
| 6341 value: false |
| 6342 }, |
| 6343 alwaysFloatLabel: { |
| 6344 type: Boolean, |
| 6345 value: false |
| 6346 }, |
| 6347 attrForValue: { |
| 6348 type: String, |
| 6349 value: 'bind-value' |
| 6350 }, |
| 6351 autoValidate: { |
| 6352 type: Boolean, |
| 6353 value: false |
| 6354 }, |
| 6355 invalid: { |
| 6356 observer: '_invalidChanged', |
| 6357 type: Boolean, |
| 6358 value: false |
| 6359 }, |
| 6360 focused: { |
| 6361 readOnly: true, |
| 6362 type: Boolean, |
| 6363 value: false, |
| 6364 notify: true |
| 6365 }, |
| 6366 _addons: { |
| 6367 type: Array |
| 6368 }, |
| 6369 _inputHasContent: { |
| 6370 type: Boolean, |
| 6371 value: false |
| 6372 }, |
| 6373 _inputSelector: { |
| 6374 type: String, |
| 6375 value: 'input,textarea,.paper-input-input' |
| 6376 }, |
| 6377 _boundOnFocus: { |
| 6378 type: Function, |
| 6379 value: function() { |
| 6380 return this._onFocus.bind(this); |
| 6381 } |
| 6382 }, |
| 6383 _boundOnBlur: { |
| 6384 type: Function, |
| 6385 value: function() { |
| 6386 return this._onBlur.bind(this); |
| 6387 } |
| 6388 }, |
| 6389 _boundOnInput: { |
| 6390 type: Function, |
| 6391 value: function() { |
| 6392 return this._onInput.bind(this); |
| 6393 } |
| 6394 }, |
| 6395 _boundValueChanged: { |
| 6396 type: Function, |
| 6397 value: function() { |
| 6398 return this._onValueChanged.bind(this); |
| 6399 } |
| 6400 } |
| 6401 }, |
| 6402 listeners: { |
| 6403 'addon-attached': '_onAddonAttached', |
| 6404 'iron-input-validate': '_onIronInputValidate' |
| 6405 }, |
| 6406 get _valueChangedEvent() { |
| 6407 return this.attrForValue + '-changed'; |
| 6408 }, |
| 6409 get _propertyForValue() { |
| 6410 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 6411 }, |
| 6412 get _inputElement() { |
| 6413 return Polymer.dom(this).querySelector(this._inputSelector); |
| 6414 }, |
| 6415 get _inputElementValue() { |
| 6416 return this._inputElement[this._propertyForValue] || this._inputElement.valu
e; |
| 6417 }, |
| 6418 ready: function() { |
| 6419 if (!this._addons) { |
| 6420 this._addons = []; |
| 6421 } |
| 6422 this.addEventListener('focus', this._boundOnFocus, true); |
| 6423 this.addEventListener('blur', this._boundOnBlur, true); |
| 6424 }, |
| 6425 attached: function() { |
| 6426 if (this.attrForValue) { |
| 6427 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa
lueChanged); |
| 6428 } else { |
| 6429 this.addEventListener('input', this._onInput); |
| 6430 } |
| 6431 if (this._inputElementValue != '') { |
| 6432 this._handleValueAndAutoValidate(this._inputElement); |
| 6433 } else { |
| 6434 this._handleValue(this._inputElement); |
| 6435 } |
| 6436 }, |
| 6437 _onAddonAttached: function(event) { |
| 6438 if (!this._addons) { |
| 6439 this._addons = []; |
| 6440 } |
| 6441 var target = event.target; |
| 6442 if (this._addons.indexOf(target) === -1) { |
| 6443 this._addons.push(target); |
| 6444 if (this.isAttached) { |
| 6445 this._handleValue(this._inputElement); |
| 6446 } |
| 6447 } |
| 6448 }, |
| 6449 _onFocus: function() { |
| 6450 this._setFocused(true); |
| 6451 }, |
| 6452 _onBlur: function() { |
| 6453 this._setFocused(false); |
| 6454 this._handleValueAndAutoValidate(this._inputElement); |
| 6455 }, |
| 6456 _onInput: function(event) { |
| 6457 this._handleValueAndAutoValidate(event.target); |
| 6458 }, |
| 6459 _onValueChanged: function(event) { |
| 6460 this._handleValueAndAutoValidate(event.target); |
| 6461 }, |
| 6462 _handleValue: function(inputElement) { |
| 6463 var value = this._inputElementValue; |
| 6464 if (value || value === 0 || inputElement.type === 'number' && !inputElement.
checkValidity()) { |
| 6465 this._inputHasContent = true; |
| 6466 } else { |
| 6467 this._inputHasContent = false; |
| 6468 } |
| 6469 this.updateAddons({ |
| 6470 inputElement: inputElement, |
| 6471 value: value, |
| 6472 invalid: this.invalid |
| 6473 }); |
| 6474 }, |
| 6475 _handleValueAndAutoValidate: function(inputElement) { |
| 6476 if (this.autoValidate) { |
| 6477 var valid; |
| 6478 if (inputElement.validate) { |
| 6479 valid = inputElement.validate(this._inputElementValue); |
| 11184 } else { | 6480 } else { |
| 11185 switch (this.type) { | 6481 valid = inputElement.checkValidity(); |
| 11186 case 'number': | 6482 } |
| 11187 pattern = /[0-9.,e-]/; | |
| 11188 break; | |
| 11189 } | |
| 11190 } | |
| 11191 return pattern; | |
| 11192 }, | |
| 11193 | |
| 11194 ready: function() { | |
| 11195 this.bindValue = this.value; | |
| 11196 }, | |
| 11197 | |
| 11198 /** | |
| 11199 * @suppress {checkTypes} | |
| 11200 */ | |
| 11201 _bindValueChanged: function() { | |
| 11202 if (this.value !== this.bindValue) { | |
| 11203 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | |
| 11204 } | |
| 11205 // manually notify because we don't want to notify until after setting val
ue | |
| 11206 this.fire('bind-value-changed', {value: this.bindValue}); | |
| 11207 }, | |
| 11208 | |
| 11209 _allowedPatternChanged: function() { | |
| 11210 // Force to prevent invalid input when an `allowed-pattern` is set | |
| 11211 this.preventInvalidInput = this.allowedPattern ? true : false; | |
| 11212 }, | |
| 11213 | |
| 11214 _onInput: function() { | |
| 11215 // Need to validate each of the characters pasted if they haven't | |
| 11216 // been validated inside `_onKeypress` already. | |
| 11217 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | |
| 11218 var valid = this._checkPatternValidity(); | |
| 11219 if (!valid) { | |
| 11220 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | |
| 11221 this.value = this._previousValidInput; | |
| 11222 } | |
| 11223 } | |
| 11224 | |
| 11225 this.bindValue = this.value; | |
| 11226 this._previousValidInput = this.value; | |
| 11227 this._patternAlreadyChecked = false; | |
| 11228 }, | |
| 11229 | |
| 11230 _isPrintable: function(event) { | |
| 11231 // What a control/printable character is varies wildly based on the browse
r. | |
| 11232 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
| 11233 // in Chrome, but the *do* on Firefox | |
| 11234 // - in Firefox, when they do send a `keypress` event, control chars have | |
| 11235 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
| 11236 // - printable characters always send a keypress event. | |
| 11237 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
| 11238 // always matches the charCode. | |
| 11239 // None of this makes any sense. | |
| 11240 | |
| 11241 // For these keys, ASCII code == browser keycode. | |
| 11242 var anyNonPrintable = | |
| 11243 (event.keyCode == 8) || // backspace | |
| 11244 (event.keyCode == 9) || // tab | |
| 11245 (event.keyCode == 13) || // enter | |
| 11246 (event.keyCode == 27); // escape | |
| 11247 | |
| 11248 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
| 11249 var mozNonPrintable = | |
| 11250 (event.keyCode == 19) || // pause | |
| 11251 (event.keyCode == 20) || // caps lock | |
| 11252 (event.keyCode == 45) || // insert | |
| 11253 (event.keyCode == 46) || // delete | |
| 11254 (event.keyCode == 144) || // num lock | |
| 11255 (event.keyCode == 145) || // scroll lock | |
| 11256 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | |
| 11257 (event.keyCode > 111 && event.keyCode < 124); // fn keys | |
| 11258 | |
| 11259 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | |
| 11260 }, | |
| 11261 | |
| 11262 _onKeypress: function(event) { | |
| 11263 if (!this.preventInvalidInput && this.type !== 'number') { | |
| 11264 return; | |
| 11265 } | |
| 11266 var regexp = this._patternRegExp; | |
| 11267 if (!regexp) { | |
| 11268 return; | |
| 11269 } | |
| 11270 | |
| 11271 // Handle special keys and backspace | |
| 11272 if (event.metaKey || event.ctrlKey || event.altKey) | |
| 11273 return; | |
| 11274 | |
| 11275 // Check the pattern either here or in `_onInput`, but not in both. | |
| 11276 this._patternAlreadyChecked = true; | |
| 11277 | |
| 11278 var thisChar = String.fromCharCode(event.charCode); | |
| 11279 if (this._isPrintable(event) && !regexp.test(thisChar)) { | |
| 11280 event.preventDefault(); | |
| 11281 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | |
| 11282 } | |
| 11283 }, | |
| 11284 | |
| 11285 _checkPatternValidity: function() { | |
| 11286 var regexp = this._patternRegExp; | |
| 11287 if (!regexp) { | |
| 11288 return true; | |
| 11289 } | |
| 11290 for (var i = 0; i < this.value.length; i++) { | |
| 11291 if (!regexp.test(this.value[i])) { | |
| 11292 return false; | |
| 11293 } | |
| 11294 } | |
| 11295 return true; | |
| 11296 }, | |
| 11297 | |
| 11298 /** | |
| 11299 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
| 11300 * then any constraints. | |
| 11301 * @return {boolean} True if the value is valid. | |
| 11302 */ | |
| 11303 validate: function() { | |
| 11304 // First, check what the browser thinks. Some inputs (like type=number) | |
| 11305 // behave weirdly and will set the value to "" if something invalid is | |
| 11306 // entered, but will set the validity correctly. | |
| 11307 var valid = this.checkValidity(); | |
| 11308 | |
| 11309 // Only do extra checking if the browser thought this was valid. | |
| 11310 if (valid) { | |
| 11311 // Empty, required input is invalid | |
| 11312 if (this.required && this.value === '') { | |
| 11313 valid = false; | |
| 11314 } else if (this.hasValidator()) { | |
| 11315 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | |
| 11316 } | |
| 11317 } | |
| 11318 | |
| 11319 this.invalid = !valid; | 6483 this.invalid = !valid; |
| 11320 this.fire('iron-input-validate'); | 6484 } |
| 11321 return valid; | 6485 this._handleValue(inputElement); |
| 11322 }, | 6486 }, |
| 11323 | 6487 _onIronInputValidate: function(event) { |
| 11324 _announceInvalidCharacter: function(message) { | 6488 this.invalid = this._inputElement.invalid; |
| 11325 this.fire('iron-announce', { text: message }); | 6489 }, |
| 11326 } | 6490 _invalidChanged: function() { |
| 11327 }); | 6491 if (this._addons) { |
| 11328 | |
| 11329 /* | |
| 11330 The `iron-input-validate` event is fired whenever `validate()` is called. | |
| 11331 @event iron-input-validate | |
| 11332 */ | |
| 11333 Polymer({ | |
| 11334 is: 'paper-input-container', | |
| 11335 | |
| 11336 properties: { | |
| 11337 /** | |
| 11338 * Set to true to disable the floating label. The label disappears when th
e input value is | |
| 11339 * not null. | |
| 11340 */ | |
| 11341 noLabelFloat: { | |
| 11342 type: Boolean, | |
| 11343 value: false | |
| 11344 }, | |
| 11345 | |
| 11346 /** | |
| 11347 * Set to true to always float the floating label. | |
| 11348 */ | |
| 11349 alwaysFloatLabel: { | |
| 11350 type: Boolean, | |
| 11351 value: false | |
| 11352 }, | |
| 11353 | |
| 11354 /** | |
| 11355 * The attribute to listen for value changes on. | |
| 11356 */ | |
| 11357 attrForValue: { | |
| 11358 type: String, | |
| 11359 value: 'bind-value' | |
| 11360 }, | |
| 11361 | |
| 11362 /** | |
| 11363 * Set to true to auto-validate the input value when it changes. | |
| 11364 */ | |
| 11365 autoValidate: { | |
| 11366 type: Boolean, | |
| 11367 value: false | |
| 11368 }, | |
| 11369 | |
| 11370 /** | |
| 11371 * True if the input is invalid. This property is set automatically when t
he input value | |
| 11372 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
| 11373 */ | |
| 11374 invalid: { | |
| 11375 observer: '_invalidChanged', | |
| 11376 type: Boolean, | |
| 11377 value: false | |
| 11378 }, | |
| 11379 | |
| 11380 /** | |
| 11381 * True if the input has focus. | |
| 11382 */ | |
| 11383 focused: { | |
| 11384 readOnly: true, | |
| 11385 type: Boolean, | |
| 11386 value: false, | |
| 11387 notify: true | |
| 11388 }, | |
| 11389 | |
| 11390 _addons: { | |
| 11391 type: Array | |
| 11392 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
| 11393 // distributed child is attached, which may occur before configuration f
or this element | |
| 11394 // in polyfill. | |
| 11395 }, | |
| 11396 | |
| 11397 _inputHasContent: { | |
| 11398 type: Boolean, | |
| 11399 value: false | |
| 11400 }, | |
| 11401 | |
| 11402 _inputSelector: { | |
| 11403 type: String, | |
| 11404 value: 'input,textarea,.paper-input-input' | |
| 11405 }, | |
| 11406 | |
| 11407 _boundOnFocus: { | |
| 11408 type: Function, | |
| 11409 value: function() { | |
| 11410 return this._onFocus.bind(this); | |
| 11411 } | |
| 11412 }, | |
| 11413 | |
| 11414 _boundOnBlur: { | |
| 11415 type: Function, | |
| 11416 value: function() { | |
| 11417 return this._onBlur.bind(this); | |
| 11418 } | |
| 11419 }, | |
| 11420 | |
| 11421 _boundOnInput: { | |
| 11422 type: Function, | |
| 11423 value: function() { | |
| 11424 return this._onInput.bind(this); | |
| 11425 } | |
| 11426 }, | |
| 11427 | |
| 11428 _boundValueChanged: { | |
| 11429 type: Function, | |
| 11430 value: function() { | |
| 11431 return this._onValueChanged.bind(this); | |
| 11432 } | |
| 11433 } | |
| 11434 }, | |
| 11435 | |
| 11436 listeners: { | |
| 11437 'addon-attached': '_onAddonAttached', | |
| 11438 'iron-input-validate': '_onIronInputValidate' | |
| 11439 }, | |
| 11440 | |
| 11441 get _valueChangedEvent() { | |
| 11442 return this.attrForValue + '-changed'; | |
| 11443 }, | |
| 11444 | |
| 11445 get _propertyForValue() { | |
| 11446 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
| 11447 }, | |
| 11448 | |
| 11449 get _inputElement() { | |
| 11450 return Polymer.dom(this).querySelector(this._inputSelector); | |
| 11451 }, | |
| 11452 | |
| 11453 get _inputElementValue() { | |
| 11454 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
| 11455 }, | |
| 11456 | |
| 11457 ready: function() { | |
| 11458 if (!this._addons) { | |
| 11459 this._addons = []; | |
| 11460 } | |
| 11461 this.addEventListener('focus', this._boundOnFocus, true); | |
| 11462 this.addEventListener('blur', this._boundOnBlur, true); | |
| 11463 }, | |
| 11464 | |
| 11465 attached: function() { | |
| 11466 if (this.attrForValue) { | |
| 11467 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
| 11468 } else { | |
| 11469 this.addEventListener('input', this._onInput); | |
| 11470 } | |
| 11471 | |
| 11472 // Only validate when attached if the input already has a value. | |
| 11473 if (this._inputElementValue != '') { | |
| 11474 this._handleValueAndAutoValidate(this._inputElement); | |
| 11475 } else { | |
| 11476 this._handleValue(this._inputElement); | |
| 11477 } | |
| 11478 }, | |
| 11479 | |
| 11480 _onAddonAttached: function(event) { | |
| 11481 if (!this._addons) { | |
| 11482 this._addons = []; | |
| 11483 } | |
| 11484 var target = event.target; | |
| 11485 if (this._addons.indexOf(target) === -1) { | |
| 11486 this._addons.push(target); | |
| 11487 if (this.isAttached) { | |
| 11488 this._handleValue(this._inputElement); | |
| 11489 } | |
| 11490 } | |
| 11491 }, | |
| 11492 | |
| 11493 _onFocus: function() { | |
| 11494 this._setFocused(true); | |
| 11495 }, | |
| 11496 | |
| 11497 _onBlur: function() { | |
| 11498 this._setFocused(false); | |
| 11499 this._handleValueAndAutoValidate(this._inputElement); | |
| 11500 }, | |
| 11501 | |
| 11502 _onInput: function(event) { | |
| 11503 this._handleValueAndAutoValidate(event.target); | |
| 11504 }, | |
| 11505 | |
| 11506 _onValueChanged: function(event) { | |
| 11507 this._handleValueAndAutoValidate(event.target); | |
| 11508 }, | |
| 11509 | |
| 11510 _handleValue: function(inputElement) { | |
| 11511 var value = this._inputElementValue; | |
| 11512 | |
| 11513 // type="number" hack needed because this.value is empty until it's valid | |
| 11514 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
| 11515 this._inputHasContent = true; | |
| 11516 } else { | |
| 11517 this._inputHasContent = false; | |
| 11518 } | |
| 11519 | |
| 11520 this.updateAddons({ | 6492 this.updateAddons({ |
| 11521 inputElement: inputElement, | |
| 11522 value: value, | |
| 11523 invalid: this.invalid | 6493 invalid: this.invalid |
| 11524 }); | 6494 }); |
| 11525 }, | 6495 } |
| 11526 | 6496 }, |
| 11527 _handleValueAndAutoValidate: function(inputElement) { | 6497 updateAddons: function(state) { |
| 11528 if (this.autoValidate) { | 6498 for (var addon, index = 0; addon = this._addons[index]; index++) { |
| 11529 var valid; | 6499 addon.update(state); |
| 11530 if (inputElement.validate) { | 6500 } |
| 11531 valid = inputElement.validate(this._inputElementValue); | 6501 }, |
| 11532 } else { | 6502 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i
nvalid, _inputHasContent) { |
| 11533 valid = inputElement.checkValidity(); | 6503 var cls = 'input-content'; |
| 11534 } | 6504 if (!noLabelFloat) { |
| 11535 this.invalid = !valid; | 6505 var label = this.querySelector('label'); |
| 11536 } | 6506 if (alwaysFloatLabel || _inputHasContent) { |
| 11537 | 6507 cls += ' label-is-floating'; |
| 11538 // Call this last to notify the add-ons. | 6508 this.$.labelAndInputContainer.style.position = 'static'; |
| 11539 this._handleValue(inputElement); | 6509 if (invalid) { |
| 11540 }, | 6510 cls += ' is-invalid'; |
| 11541 | 6511 } else if (focused) { |
| 11542 _onIronInputValidate: function(event) { | 6512 cls += " label-is-highlighted"; |
| 11543 this.invalid = this._inputElement.invalid; | |
| 11544 }, | |
| 11545 | |
| 11546 _invalidChanged: function() { | |
| 11547 if (this._addons) { | |
| 11548 this.updateAddons({invalid: this.invalid}); | |
| 11549 } | |
| 11550 }, | |
| 11551 | |
| 11552 /** | |
| 11553 * Call this to update the state of add-ons. | |
| 11554 * @param {Object} state Add-on state. | |
| 11555 */ | |
| 11556 updateAddons: function(state) { | |
| 11557 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
| 11558 addon.update(state); | |
| 11559 } | |
| 11560 }, | |
| 11561 | |
| 11562 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
| 11563 var cls = 'input-content'; | |
| 11564 if (!noLabelFloat) { | |
| 11565 var label = this.querySelector('label'); | |
| 11566 | |
| 11567 if (alwaysFloatLabel || _inputHasContent) { | |
| 11568 cls += ' label-is-floating'; | |
| 11569 // If the label is floating, ignore any offsets that may have been | |
| 11570 // applied from a prefix element. | |
| 11571 this.$.labelAndInputContainer.style.position = 'static'; | |
| 11572 | |
| 11573 if (invalid) { | |
| 11574 cls += ' is-invalid'; | |
| 11575 } else if (focused) { | |
| 11576 cls += " label-is-highlighted"; | |
| 11577 } | |
| 11578 } else { | |
| 11579 // When the label is not floating, it should overlap the input element
. | |
| 11580 if (label) { | |
| 11581 this.$.labelAndInputContainer.style.position = 'relative'; | |
| 11582 } | |
| 11583 } | 6513 } |
| 11584 } else { | 6514 } else { |
| 11585 if (_inputHasContent) { | 6515 if (label) { |
| 11586 cls += ' label-is-hidden'; | 6516 this.$.labelAndInputContainer.style.position = 'relative'; |
| 11587 } | 6517 } |
| 11588 } | 6518 } |
| 11589 return cls; | 6519 } else { |
| 11590 }, | 6520 if (_inputHasContent) { |
| 11591 | 6521 cls += ' label-is-hidden'; |
| 11592 _computeUnderlineClass: function(focused, invalid) { | 6522 } |
| 11593 var cls = 'underline'; | 6523 } |
| 11594 if (invalid) { | 6524 return cls; |
| 11595 cls += ' is-invalid'; | 6525 }, |
| 11596 } else if (focused) { | 6526 _computeUnderlineClass: function(focused, invalid) { |
| 11597 cls += ' is-highlighted' | 6527 var cls = 'underline'; |
| 11598 } | 6528 if (invalid) { |
| 11599 return cls; | 6529 cls += ' is-invalid'; |
| 11600 }, | 6530 } else if (focused) { |
| 11601 | 6531 cls += ' is-highlighted'; |
| 11602 _computeAddOnContentClass: function(focused, invalid) { | 6532 } |
| 11603 var cls = 'add-on-content'; | 6533 return cls; |
| 11604 if (invalid) { | 6534 }, |
| 11605 cls += ' is-invalid'; | 6535 _computeAddOnContentClass: function(focused, invalid) { |
| 11606 } else if (focused) { | 6536 var cls = 'add-on-content'; |
| 11607 cls += ' is-highlighted' | 6537 if (invalid) { |
| 11608 } | 6538 cls += ' is-invalid'; |
| 11609 return cls; | 6539 } else if (focused) { |
| 11610 } | 6540 cls += ' is-highlighted'; |
| 11611 }); | 6541 } |
| 6542 return cls; |
| 6543 } |
| 6544 }); |
| 6545 |
| 11612 // Copyright 2015 The Chromium Authors. All rights reserved. | 6546 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11613 // Use of this source code is governed by a BSD-style license that can be | 6547 // Use of this source code is governed by a BSD-style license that can be |
| 11614 // found in the LICENSE file. | 6548 // found in the LICENSE file. |
| 11615 | |
| 11616 var SearchField = Polymer({ | 6549 var SearchField = Polymer({ |
| 11617 is: 'cr-search-field', | 6550 is: 'cr-search-field', |
| 11618 | 6551 behaviors: [ CrSearchFieldBehavior ], |
| 11619 behaviors: [CrSearchFieldBehavior], | |
| 11620 | |
| 11621 properties: { | 6552 properties: { |
| 11622 value_: String, | 6553 value_: String |
| 11623 }, | 6554 }, |
| 11624 | |
| 11625 /** @return {!HTMLInputElement} */ | |
| 11626 getSearchInput: function() { | 6555 getSearchInput: function() { |
| 11627 return this.$.searchInput; | 6556 return this.$.searchInput; |
| 11628 }, | 6557 }, |
| 11629 | |
| 11630 /** @private */ | |
| 11631 clearSearch_: function() { | 6558 clearSearch_: function() { |
| 11632 this.setValue(''); | 6559 this.setValue(''); |
| 11633 this.getSearchInput().focus(); | 6560 this.getSearchInput().focus(); |
| 11634 }, | 6561 }, |
| 11635 | |
| 11636 /** @private */ | |
| 11637 toggleShowingSearch_: function() { | 6562 toggleShowingSearch_: function() { |
| 11638 this.showingSearch = !this.showingSearch; | 6563 this.showingSearch = !this.showingSearch; |
| 11639 }, | 6564 } |
| 11640 }); | 6565 }); |
| 6566 |
| 11641 // Copyright 2015 The Chromium Authors. All rights reserved. | 6567 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11642 // Use of this source code is governed by a BSD-style license that can be | 6568 // Use of this source code is governed by a BSD-style license that can be |
| 11643 // found in the LICENSE file. | 6569 // found in the LICENSE file. |
| 11644 | |
| 11645 cr.define('downloads', function() { | 6570 cr.define('downloads', function() { |
| 11646 var Toolbar = Polymer({ | 6571 var Toolbar = Polymer({ |
| 11647 is: 'downloads-toolbar', | 6572 is: 'downloads-toolbar', |
| 11648 | |
| 11649 attached: function() { | 6573 attached: function() { |
| 11650 // isRTL() only works after i18n_template.js runs to set <html dir>. | |
| 11651 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 6574 this.overflowAlign_ = isRTL() ? 'left' : 'right'; |
| 11652 }, | 6575 }, |
| 11653 | |
| 11654 properties: { | 6576 properties: { |
| 11655 downloadsShowing: { | 6577 downloadsShowing: { |
| 11656 reflectToAttribute: true, | 6578 reflectToAttribute: true, |
| 11657 type: Boolean, | 6579 type: Boolean, |
| 11658 value: false, | 6580 value: false, |
| 11659 observer: 'downloadsShowingChanged_', | 6581 observer: 'downloadsShowingChanged_' |
| 11660 }, | 6582 }, |
| 11661 | |
| 11662 overflowAlign_: { | 6583 overflowAlign_: { |
| 11663 type: String, | 6584 type: String, |
| 11664 value: 'right', | 6585 value: 'right' |
| 11665 }, | 6586 } |
| 11666 }, | 6587 }, |
| 11667 | |
| 11668 listeners: { | 6588 listeners: { |
| 11669 'paper-dropdown-close': 'onPaperDropdownClose_', | 6589 'paper-dropdown-close': 'onPaperDropdownClose_', |
| 11670 'paper-dropdown-open': 'onPaperDropdownOpen_', | 6590 'paper-dropdown-open': 'onPaperDropdownOpen_' |
| 11671 }, | 6591 }, |
| 11672 | |
| 11673 /** @return {boolean} Whether removal can be undone. */ | |
| 11674 canUndo: function() { | 6592 canUndo: function() { |
| 11675 return this.$['search-input'] != this.shadowRoot.activeElement; | 6593 return this.$['search-input'] != this.shadowRoot.activeElement; |
| 11676 }, | 6594 }, |
| 11677 | |
| 11678 /** @return {boolean} Whether "Clear all" should be allowed. */ | |
| 11679 canClearAll: function() { | 6595 canClearAll: function() { |
| 11680 return !this.$['search-input'].getValue() && this.downloadsShowing; | 6596 return !this.$['search-input'].getValue() && this.downloadsShowing; |
| 11681 }, | 6597 }, |
| 11682 | |
| 11683 onFindCommand: function() { | 6598 onFindCommand: function() { |
| 11684 this.$['search-input'].showAndFocus(); | 6599 this.$['search-input'].showAndFocus(); |
| 11685 }, | 6600 }, |
| 11686 | |
| 11687 /** @private */ | |
| 11688 closeMoreActions_: function() { | 6601 closeMoreActions_: function() { |
| 11689 this.$.more.close(); | 6602 this.$.more.close(); |
| 11690 }, | 6603 }, |
| 11691 | |
| 11692 /** @private */ | |
| 11693 downloadsShowingChanged_: function() { | 6604 downloadsShowingChanged_: function() { |
| 11694 this.updateClearAll_(); | 6605 this.updateClearAll_(); |
| 11695 }, | 6606 }, |
| 11696 | |
| 11697 /** @private */ | |
| 11698 onClearAllTap_: function() { | 6607 onClearAllTap_: function() { |
| 11699 assert(this.canClearAll()); | 6608 assert(this.canClearAll()); |
| 11700 downloads.ActionService.getInstance().clearAll(); | 6609 downloads.ActionService.getInstance().clearAll(); |
| 11701 }, | 6610 }, |
| 11702 | |
| 11703 /** @private */ | |
| 11704 onPaperDropdownClose_: function() { | 6611 onPaperDropdownClose_: function() { |
| 11705 window.removeEventListener('resize', assert(this.boundClose_)); | 6612 window.removeEventListener('resize', assert(this.boundClose_)); |
| 11706 }, | 6613 }, |
| 11707 | |
| 11708 /** | |
| 11709 * @param {!Event} e | |
| 11710 * @private | |
| 11711 */ | |
| 11712 onItemBlur_: function(e) { | 6614 onItemBlur_: function(e) { |
| 11713 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 6615 var menu = this.$$('paper-menu'); |
| 11714 if (menu.items.indexOf(e.relatedTarget) >= 0) | 6616 if (menu.items.indexOf(e.relatedTarget) >= 0) return; |
| 11715 return; | |
| 11716 | |
| 11717 this.$.more.restoreFocusOnClose = false; | 6617 this.$.more.restoreFocusOnClose = false; |
| 11718 this.closeMoreActions_(); | 6618 this.closeMoreActions_(); |
| 11719 this.$.more.restoreFocusOnClose = true; | 6619 this.$.more.restoreFocusOnClose = true; |
| 11720 }, | 6620 }, |
| 11721 | |
| 11722 /** @private */ | |
| 11723 onPaperDropdownOpen_: function() { | 6621 onPaperDropdownOpen_: function() { |
| 11724 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 6622 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); |
| 11725 window.addEventListener('resize', this.boundClose_); | 6623 window.addEventListener('resize', this.boundClose_); |
| 11726 }, | 6624 }, |
| 11727 | |
| 11728 /** | |
| 11729 * @param {!CustomEvent} event | |
| 11730 * @private | |
| 11731 */ | |
| 11732 onSearchChanged_: function(event) { | 6625 onSearchChanged_: function(event) { |
| 11733 downloads.ActionService.getInstance().search( | 6626 downloads.ActionService.getInstance().search(event.detail); |
| 11734 /** @type {string} */ (event.detail)); | |
| 11735 this.updateClearAll_(); | 6627 this.updateClearAll_(); |
| 11736 }, | 6628 }, |
| 11737 | |
| 11738 /** @private */ | |
| 11739 onOpenDownloadsFolderTap_: function() { | 6629 onOpenDownloadsFolderTap_: function() { |
| 11740 downloads.ActionService.getInstance().openDownloadsFolder(); | 6630 downloads.ActionService.getInstance().openDownloadsFolder(); |
| 11741 }, | 6631 }, |
| 11742 | |
| 11743 /** @private */ | |
| 11744 updateClearAll_: function() { | 6632 updateClearAll_: function() { |
| 11745 this.$$('#actions .clear-all').hidden = !this.canClearAll(); | 6633 this.$$('#actions .clear-all').hidden = !this.canClearAll(); |
| 11746 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); | 6634 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); |
| 11747 }, | 6635 } |
| 11748 }); | 6636 }); |
| 11749 | 6637 return { |
| 11750 return {Toolbar: Toolbar}; | 6638 Toolbar: Toolbar |
| 6639 }; |
| 11751 }); | 6640 }); |
| 6641 |
| 11752 // Copyright 2015 The Chromium Authors. All rights reserved. | 6642 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11753 // Use of this source code is governed by a BSD-style license that can be | 6643 // Use of this source code is governed by a BSD-style license that can be |
| 11754 // found in the LICENSE file. | 6644 // found in the LICENSE file. |
| 11755 | |
| 11756 cr.define('downloads', function() { | 6645 cr.define('downloads', function() { |
| 11757 var Manager = Polymer({ | 6646 var Manager = Polymer({ |
| 11758 is: 'downloads-manager', | 6647 is: 'downloads-manager', |
| 11759 | |
| 11760 properties: { | 6648 properties: { |
| 11761 hasDownloads_: { | 6649 hasDownloads_: { |
| 11762 observer: 'hasDownloadsChanged_', | 6650 observer: 'hasDownloadsChanged_', |
| 11763 type: Boolean, | 6651 type: Boolean |
| 11764 }, | 6652 }, |
| 11765 | |
| 11766 items_: { | 6653 items_: { |
| 11767 type: Array, | 6654 type: Array, |
| 11768 value: function() { return []; }, | 6655 value: function() { |
| 11769 }, | 6656 return []; |
| 11770 }, | 6657 } |
| 11771 | 6658 } |
| 6659 }, |
| 11772 hostAttributes: { | 6660 hostAttributes: { |
| 11773 loading: true, | 6661 loading: true |
| 11774 }, | 6662 }, |
| 11775 | |
| 11776 listeners: { | 6663 listeners: { |
| 11777 'downloads-list.scroll': 'onListScroll_', | 6664 'downloads-list.scroll': 'onListScroll_' |
| 11778 }, | 6665 }, |
| 11779 | 6666 observers: [ 'itemsChanged_(items_.*)' ], |
| 11780 observers: [ | |
| 11781 'itemsChanged_(items_.*)', | |
| 11782 ], | |
| 11783 | |
| 11784 /** @private */ | |
| 11785 clearAll_: function() { | 6667 clearAll_: function() { |
| 11786 this.set('items_', []); | 6668 this.set('items_', []); |
| 11787 }, | 6669 }, |
| 11788 | |
| 11789 /** @private */ | |
| 11790 hasDownloadsChanged_: function() { | 6670 hasDownloadsChanged_: function() { |
| 11791 if (loadTimeData.getBoolean('allowDeletingHistory')) | 6671 if (loadTimeData.getBoolean('allowDeletingHistory')) this.$.toolbar.downlo
adsShowing = this.hasDownloads_; |
| 11792 this.$.toolbar.downloadsShowing = this.hasDownloads_; | |
| 11793 | |
| 11794 if (this.hasDownloads_) { | 6672 if (this.hasDownloads_) { |
| 11795 this.$['downloads-list'].fire('iron-resize'); | 6673 this.$['downloads-list'].fire('iron-resize'); |
| 11796 } else { | 6674 } else { |
| 11797 var isSearching = downloads.ActionService.getInstance().isSearching(); | 6675 var isSearching = downloads.ActionService.getInstance().isSearching(); |
| 11798 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 6676 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; |
| 11799 this.$['no-downloads'].querySelector('span').textContent = | 6677 this.$['no-downloads'].querySelector('span').textContent = loadTimeData.
getString(messageToShow); |
| 11800 loadTimeData.getString(messageToShow); | 6678 } |
| 11801 } | 6679 }, |
| 11802 }, | |
| 11803 | |
| 11804 /** | |
| 11805 * @param {number} index | |
| 11806 * @param {!Array<!downloads.Data>} list | |
| 11807 * @private | |
| 11808 */ | |
| 11809 insertItems_: function(index, list) { | 6680 insertItems_: function(index, list) { |
| 11810 this.splice.apply(this, ['items_', index, 0].concat(list)); | 6681 this.splice.apply(this, [ 'items_', index, 0 ].concat(list)); |
| 11811 this.updateHideDates_(index, index + list.length); | 6682 this.updateHideDates_(index, index + list.length); |
| 11812 this.removeAttribute('loading'); | 6683 this.removeAttribute('loading'); |
| 11813 }, | 6684 }, |
| 11814 | |
| 11815 /** @private */ | |
| 11816 itemsChanged_: function() { | 6685 itemsChanged_: function() { |
| 11817 this.hasDownloads_ = this.items_.length > 0; | 6686 this.hasDownloads_ = this.items_.length > 0; |
| 11818 }, | 6687 }, |
| 11819 | |
| 11820 /** | |
| 11821 * @param {Event} e | |
| 11822 * @private | |
| 11823 */ | |
| 11824 onCanExecute_: function(e) { | 6688 onCanExecute_: function(e) { |
| 11825 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 6689 e = e; |
| 11826 switch (e.command.id) { | 6690 switch (e.command.id) { |
| 11827 case 'undo-command': | 6691 case 'undo-command': |
| 11828 e.canExecute = this.$.toolbar.canUndo(); | 6692 e.canExecute = this.$.toolbar.canUndo(); |
| 11829 break; | 6693 break; |
| 11830 case 'clear-all-command': | 6694 |
| 11831 e.canExecute = this.$.toolbar.canClearAll(); | 6695 case 'clear-all-command': |
| 11832 break; | 6696 e.canExecute = this.$.toolbar.canClearAll(); |
| 11833 case 'find-command': | 6697 break; |
| 11834 e.canExecute = true; | 6698 |
| 11835 break; | 6699 case 'find-command': |
| 11836 } | 6700 e.canExecute = true; |
| 11837 }, | 6701 break; |
| 11838 | 6702 } |
| 11839 /** | 6703 }, |
| 11840 * @param {Event} e | |
| 11841 * @private | |
| 11842 */ | |
| 11843 onCommand_: function(e) { | 6704 onCommand_: function(e) { |
| 11844 if (e.command.id == 'clear-all-command') | 6705 if (e.command.id == 'clear-all-command') downloads.ActionService.getInstan
ce().clearAll(); else if (e.command.id == 'undo-command') downloads.ActionServic
e.getInstance().undo(); else if (e.command.id == 'find-command') this.$.toolbar.
onFindCommand(); |
| 11845 downloads.ActionService.getInstance().clearAll(); | 6706 }, |
| 11846 else if (e.command.id == 'undo-command') | |
| 11847 downloads.ActionService.getInstance().undo(); | |
| 11848 else if (e.command.id == 'find-command') | |
| 11849 this.$.toolbar.onFindCommand(); | |
| 11850 }, | |
| 11851 | |
| 11852 /** @private */ | |
| 11853 onListScroll_: function() { | 6707 onListScroll_: function() { |
| 11854 var list = this.$['downloads-list']; | 6708 var list = this.$['downloads-list']; |
| 11855 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 6709 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { |
| 11856 // Approaching the end of the scrollback. Attempt to load more items. | |
| 11857 downloads.ActionService.getInstance().loadMore(); | 6710 downloads.ActionService.getInstance().loadMore(); |
| 11858 } | 6711 } |
| 11859 }, | 6712 }, |
| 11860 | |
| 11861 /** @private */ | |
| 11862 onLoad_: function() { | 6713 onLoad_: function() { |
| 11863 cr.ui.decorate('command', cr.ui.Command); | 6714 cr.ui.decorate('command', cr.ui.Command); |
| 11864 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 6715 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
| 11865 document.addEventListener('command', this.onCommand_.bind(this)); | 6716 document.addEventListener('command', this.onCommand_.bind(this)); |
| 11866 | |
| 11867 downloads.ActionService.getInstance().loadMore(); | 6717 downloads.ActionService.getInstance().loadMore(); |
| 11868 }, | 6718 }, |
| 11869 | |
| 11870 /** | |
| 11871 * @param {number} index | |
| 11872 * @private | |
| 11873 */ | |
| 11874 removeItem_: function(index) { | 6719 removeItem_: function(index) { |
| 11875 this.splice('items_', index, 1); | 6720 this.splice('items_', index, 1); |
| 11876 this.updateHideDates_(index, index); | 6721 this.updateHideDates_(index, index); |
| 11877 this.onListScroll_(); | 6722 this.onListScroll_(); |
| 11878 }, | 6723 }, |
| 11879 | |
| 11880 /** | |
| 11881 * @param {number} start | |
| 11882 * @param {number} end | |
| 11883 * @private | |
| 11884 */ | |
| 11885 updateHideDates_: function(start, end) { | 6724 updateHideDates_: function(start, end) { |
| 11886 for (var i = start; i <= end; ++i) { | 6725 for (var i = start; i <= end; ++i) { |
| 11887 var current = this.items_[i]; | 6726 var current = this.items_[i]; |
| 11888 if (!current) | 6727 if (!current) continue; |
| 11889 continue; | |
| 11890 var prev = this.items_[i - 1]; | 6728 var prev = this.items_[i - 1]; |
| 11891 current.hideDate = !!prev && prev.date_string == current.date_string; | 6729 current.hideDate = !!prev && prev.date_string == current.date_string; |
| 11892 } | 6730 } |
| 11893 }, | 6731 }, |
| 11894 | |
| 11895 /** | |
| 11896 * @param {number} index | |
| 11897 * @param {!downloads.Data} data | |
| 11898 * @private | |
| 11899 */ | |
| 11900 updateItem_: function(index, data) { | 6732 updateItem_: function(index, data) { |
| 11901 this.set('items_.' + index, data); | 6733 this.set('items_.' + index, data); |
| 11902 this.updateHideDates_(index, index); | 6734 this.updateHideDates_(index, index); |
| 11903 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | 6735 var list = this.$['downloads-list']; |
| 11904 list.updateSizeForItem(index); | 6736 list.updateSizeForItem(index); |
| 11905 }, | 6737 } |
| 11906 }); | 6738 }); |
| 11907 | |
| 11908 Manager.clearAll = function() { | 6739 Manager.clearAll = function() { |
| 11909 Manager.get().clearAll_(); | 6740 Manager.get().clearAll_(); |
| 11910 }; | 6741 }; |
| 11911 | |
| 11912 /** @return {!downloads.Manager} */ | |
| 11913 Manager.get = function() { | 6742 Manager.get = function() { |
| 11914 return /** @type {!downloads.Manager} */( | 6743 return queryRequiredElement('downloads-manager'); |
| 11915 queryRequiredElement('downloads-manager')); | 6744 }; |
| 11916 }; | |
| 11917 | |
| 11918 Manager.insertItems = function(index, list) { | 6745 Manager.insertItems = function(index, list) { |
| 11919 Manager.get().insertItems_(index, list); | 6746 Manager.get().insertItems_(index, list); |
| 11920 }; | 6747 }; |
| 11921 | |
| 11922 Manager.onLoad = function() { | 6748 Manager.onLoad = function() { |
| 11923 Manager.get().onLoad_(); | 6749 Manager.get().onLoad_(); |
| 11924 }; | 6750 }; |
| 11925 | |
| 11926 Manager.removeItem = function(index) { | 6751 Manager.removeItem = function(index) { |
| 11927 Manager.get().removeItem_(index); | 6752 Manager.get().removeItem_(index); |
| 11928 }; | 6753 }; |
| 11929 | |
| 11930 Manager.updateItem = function(index, data) { | 6754 Manager.updateItem = function(index, data) { |
| 11931 Manager.get().updateItem_(index, data); | 6755 Manager.get().updateItem_(index, data); |
| 11932 }; | 6756 }; |
| 11933 | 6757 return { |
| 11934 return {Manager: Manager}; | 6758 Manager: Manager |
| 6759 }; |
| 11935 }); | 6760 }); |
| 6761 |
| 11936 // Copyright 2015 The Chromium Authors. All rights reserved. | 6762 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11937 // Use of this source code is governed by a BSD-style license that can be | 6763 // Use of this source code is governed by a BSD-style license that can be |
| 11938 // found in the LICENSE file. | 6764 // found in the LICENSE file. |
| 11939 | |
| 11940 window.addEventListener('load', downloads.Manager.onLoad); | 6765 window.addEventListener('load', downloads.Manager.onLoad); |
| OLD | NEW |