| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | |
| 5 /** | |
| 6 * @fileoverview PromiseResolver is a helper class that allows creating a | |
| 7 * Promise that will be fulfilled (resolved or rejected) some time later. | |
| 8 * | |
| 9 * Example: | |
| 10 * var resolver = new PromiseResolver(); | |
| 11 * resolver.promise.then(function(result) { | |
| 12 * console.log('resolved with', result); | |
| 13 * }); | |
| 14 * ... | |
| 15 * ... | |
| 16 * resolver.resolve({hello: 'world'}); | |
| 17 */ | |
| 18 | |
| 19 /** | |
| 20 * @constructor @struct | |
| 21 * @template T | |
| 22 */ | |
| 23 function PromiseResolver() { | 4 function PromiseResolver() { |
| 24 /** @private {function(T=): void} */ | |
| 25 this.resolve_; | 5 this.resolve_; |
| 26 | |
| 27 /** @private {function(*=): void} */ | |
| 28 this.reject_; | 6 this.reject_; |
| 29 | |
| 30 /** @private {!Promise<T>} */ | |
| 31 this.promise_ = new Promise(function(resolve, reject) { | 7 this.promise_ = new Promise(function(resolve, reject) { |
| 32 this.resolve_ = resolve; | 8 this.resolve_ = resolve; |
| 33 this.reject_ = reject; | 9 this.reject_ = reject; |
| 34 }.bind(this)); | 10 }.bind(this)); |
| 35 } | 11 } |
| 36 | 12 |
| 37 PromiseResolver.prototype = { | 13 PromiseResolver.prototype = { |
| 38 /** @return {!Promise<T>} */ | 14 get promise() { |
| 39 get promise() { return this.promise_; }, | 15 return this.promise_; |
| 40 set promise(p) { assertNotReached(); }, | 16 }, |
| 17 set promise(p) { |
| 18 assertNotReached(); |
| 19 }, |
| 20 get resolve() { |
| 21 return this.resolve_; |
| 22 }, |
| 23 set resolve(r) { |
| 24 assertNotReached(); |
| 25 }, |
| 26 get reject() { |
| 27 return this.reject_; |
| 28 }, |
| 29 set reject(s) { |
| 30 assertNotReached(); |
| 31 } |
| 32 }; |
| 41 | 33 |
| 42 /** @return {function(T=): void} */ | |
| 43 get resolve() { return this.resolve_; }, | |
| 44 set resolve(r) { assertNotReached(); }, | |
| 45 | |
| 46 /** @return {function(*=): void} */ | |
| 47 get reject() { return this.reject_; }, | |
| 48 set reject(s) { assertNotReached(); }, | |
| 49 }; | |
| 50 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 34 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 51 // Use of this source code is governed by a BSD-style license that can be | 35 // Use of this source code is governed by a BSD-style license that can be |
| 52 // found in the LICENSE file. | 36 // found in the LICENSE file. |
| 53 | |
| 54 /** | |
| 55 * The global object. | |
| 56 * @type {!Object} | |
| 57 * @const | |
| 58 */ | |
| 59 var global = this; | 37 var global = this; |
| 60 | 38 |
| 61 /** @typedef {{eventName: string, uid: number}} */ | |
| 62 var WebUIListener; | 39 var WebUIListener; |
| 63 | 40 |
| 64 /** Platform, package, object property, and Event support. **/ | |
| 65 var cr = cr || function() { | 41 var cr = cr || function() { |
| 66 'use strict'; | 42 'use strict'; |
| 67 | |
| 68 /** | |
| 69 * Builds an object structure for the provided namespace path, | |
| 70 * ensuring that names that already exist are not overwritten. For | |
| 71 * example: | |
| 72 * "a.b.c" -> a = {};a.b={};a.b.c={}; | |
| 73 * @param {string} name Name of the object that this file defines. | |
| 74 * @param {*=} opt_object The object to expose at the end of the path. | |
| 75 * @param {Object=} opt_objectToExportTo The object to add the path to; | |
| 76 * default is {@code global}. | |
| 77 * @return {!Object} The last object exported (i.e. exportPath('cr.ui') | |
| 78 * returns a reference to the ui property of window.cr). | |
| 79 * @private | |
| 80 */ | |
| 81 function exportPath(name, opt_object, opt_objectToExportTo) { | 43 function exportPath(name, opt_object, opt_objectToExportTo) { |
| 82 var parts = name.split('.'); | 44 var parts = name.split('.'); |
| 83 var cur = opt_objectToExportTo || global; | 45 var cur = opt_objectToExportTo || global; |
| 84 | 46 for (var part; parts.length && (part = parts.shift()); ) { |
| 85 for (var part; parts.length && (part = parts.shift());) { | |
| 86 if (!parts.length && opt_object !== undefined) { | 47 if (!parts.length && opt_object !== undefined) { |
| 87 // last part and we have an object; use it | |
| 88 cur[part] = opt_object; | 48 cur[part] = opt_object; |
| 89 } else if (part in cur) { | 49 } else if (part in cur) { |
| 90 cur = cur[part]; | 50 cur = cur[part]; |
| 91 } else { | 51 } else { |
| 92 cur = cur[part] = {}; | 52 cur = cur[part] = {}; |
| 93 } | 53 } |
| 94 } | 54 } |
| 95 return cur; | 55 return cur; |
| 96 } | 56 } |
| 97 | |
| 98 /** | |
| 99 * Fires a property change event on the target. | |
| 100 * @param {EventTarget} target The target to dispatch the event on. | |
| 101 * @param {string} propertyName The name of the property that changed. | |
| 102 * @param {*} newValue The new value for the property. | |
| 103 * @param {*} oldValue The old value for the property. | |
| 104 */ | |
| 105 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { | 57 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
| 106 var e = new Event(propertyName + 'Change'); | 58 var e = new Event(propertyName + 'Change'); |
| 107 e.propertyName = propertyName; | 59 e.propertyName = propertyName; |
| 108 e.newValue = newValue; | 60 e.newValue = newValue; |
| 109 e.oldValue = oldValue; | 61 e.oldValue = oldValue; |
| 110 target.dispatchEvent(e); | 62 target.dispatchEvent(e); |
| 111 } | 63 } |
| 112 | |
| 113 /** | |
| 114 * Converts a camelCase javascript property name to a hyphenated-lower-case | |
| 115 * attribute name. | |
| 116 * @param {string} jsName The javascript camelCase property name. | |
| 117 * @return {string} The equivalent hyphenated-lower-case attribute name. | |
| 118 */ | |
| 119 function getAttributeName(jsName) { | 64 function getAttributeName(jsName) { |
| 120 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); | 65 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
| 121 } | 66 } |
| 122 | |
| 123 /** | |
| 124 * The kind of property to define in {@code defineProperty}. | |
| 125 * @enum {string} | |
| 126 * @const | |
| 127 */ | |
| 128 var PropertyKind = { | 67 var PropertyKind = { |
| 129 /** | |
| 130 * Plain old JS property where the backing data is stored as a "private" | |
| 131 * field on the object. | |
| 132 * Use for properties of any type. Type will not be checked. | |
| 133 */ | |
| 134 JS: 'js', | 68 JS: 'js', |
| 135 | |
| 136 /** | |
| 137 * The property backing data is stored as an attribute on an element. | |
| 138 * Use only for properties of type {string}. | |
| 139 */ | |
| 140 ATTR: 'attr', | 69 ATTR: 'attr', |
| 141 | |
| 142 /** | |
| 143 * The property backing data is stored as an attribute on an element. If the | |
| 144 * element has the attribute then the value is true. | |
| 145 * Use only for properties of type {boolean}. | |
| 146 */ | |
| 147 BOOL_ATTR: 'boolAttr' | 70 BOOL_ATTR: 'boolAttr' |
| 148 }; | 71 }; |
| 149 | |
| 150 /** | |
| 151 * Helper function for defineProperty that returns the getter to use for the | |
| 152 * property. | |
| 153 * @param {string} name The name of the property. | |
| 154 * @param {PropertyKind} kind The kind of the property. | |
| 155 * @return {function():*} The getter for the property. | |
| 156 */ | |
| 157 function getGetter(name, kind) { | 72 function getGetter(name, kind) { |
| 158 switch (kind) { | 73 switch (kind) { |
| 159 case PropertyKind.JS: | 74 case PropertyKind.JS: |
| 160 var privateName = name + '_'; | 75 var privateName = name + '_'; |
| 161 return function() { | 76 return function() { |
| 162 return this[privateName]; | 77 return this[privateName]; |
| 163 }; | 78 }; |
| 164 case PropertyKind.ATTR: | 79 |
| 165 var attributeName = getAttributeName(name); | 80 case PropertyKind.ATTR: |
| 166 return function() { | 81 var attributeName = getAttributeName(name); |
| 167 return this.getAttribute(attributeName); | 82 return function() { |
| 168 }; | 83 return this.getAttribute(attributeName); |
| 169 case PropertyKind.BOOL_ATTR: | 84 }; |
| 170 var attributeName = getAttributeName(name); | 85 |
| 171 return function() { | 86 case PropertyKind.BOOL_ATTR: |
| 172 return this.hasAttribute(attributeName); | 87 var attributeName = getAttributeName(name); |
| 173 }; | 88 return function() { |
| 89 return this.hasAttribute(attributeName); |
| 90 }; |
| 174 } | 91 } |
| 175 | |
| 176 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
| 177 // the browser/unit tests to preprocess this file through grit. | |
| 178 throw 'not reached'; | 92 throw 'not reached'; |
| 179 } | 93 } |
| 180 | |
| 181 /** | |
| 182 * Helper function for defineProperty that returns the setter of the right | |
| 183 * kind. | |
| 184 * @param {string} name The name of the property we are defining the setter | |
| 185 * for. | |
| 186 * @param {PropertyKind} kind The kind of property we are getting the | |
| 187 * setter for. | |
| 188 * @param {function(*, *):void=} opt_setHook A function to run after the | |
| 189 * property is set, but before the propertyChange event is fired. | |
| 190 * @return {function(*):void} The function to use as a setter. | |
| 191 */ | |
| 192 function getSetter(name, kind, opt_setHook) { | 94 function getSetter(name, kind, opt_setHook) { |
| 193 switch (kind) { | 95 switch (kind) { |
| 194 case PropertyKind.JS: | 96 case PropertyKind.JS: |
| 195 var privateName = name + '_'; | 97 var privateName = name + '_'; |
| 196 return function(value) { | 98 return function(value) { |
| 197 var oldValue = this[name]; | 99 var oldValue = this[name]; |
| 198 if (value !== oldValue) { | 100 if (value !== oldValue) { |
| 199 this[privateName] = value; | 101 this[privateName] = value; |
| 200 if (opt_setHook) | 102 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 201 opt_setHook.call(this, value, oldValue); | 103 dispatchPropertyChange(this, name, value, oldValue); |
| 202 dispatchPropertyChange(this, name, value, oldValue); | 104 } |
| 203 } | 105 }; |
| 204 }; | |
| 205 | 106 |
| 206 case PropertyKind.ATTR: | 107 case PropertyKind.ATTR: |
| 207 var attributeName = getAttributeName(name); | 108 var attributeName = getAttributeName(name); |
| 208 return function(value) { | 109 return function(value) { |
| 209 var oldValue = this[name]; | 110 var oldValue = this[name]; |
| 210 if (value !== oldValue) { | 111 if (value !== oldValue) { |
| 211 if (value == undefined) | 112 if (value == undefined) this.removeAttribute(attributeName); else this
.setAttribute(attributeName, value); |
| 212 this.removeAttribute(attributeName); | 113 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 213 else | 114 dispatchPropertyChange(this, name, value, oldValue); |
| 214 this.setAttribute(attributeName, value); | 115 } |
| 215 if (opt_setHook) | 116 }; |
| 216 opt_setHook.call(this, value, oldValue); | |
| 217 dispatchPropertyChange(this, name, value, oldValue); | |
| 218 } | |
| 219 }; | |
| 220 | 117 |
| 221 case PropertyKind.BOOL_ATTR: | 118 case PropertyKind.BOOL_ATTR: |
| 222 var attributeName = getAttributeName(name); | 119 var attributeName = getAttributeName(name); |
| 223 return function(value) { | 120 return function(value) { |
| 224 var oldValue = this[name]; | 121 var oldValue = this[name]; |
| 225 if (value !== oldValue) { | 122 if (value !== oldValue) { |
| 226 if (value) | 123 if (value) this.setAttribute(attributeName, name); else this.removeAtt
ribute(attributeName); |
| 227 this.setAttribute(attributeName, name); | 124 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
| 228 else | 125 dispatchPropertyChange(this, name, value, oldValue); |
| 229 this.removeAttribute(attributeName); | 126 } |
| 230 if (opt_setHook) | 127 }; |
| 231 opt_setHook.call(this, value, oldValue); | |
| 232 dispatchPropertyChange(this, name, value, oldValue); | |
| 233 } | |
| 234 }; | |
| 235 } | 128 } |
| 236 | |
| 237 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
| 238 // the browser/unit tests to preprocess this file through grit. | |
| 239 throw 'not reached'; | 129 throw 'not reached'; |
| 240 } | 130 } |
| 241 | |
| 242 /** | |
| 243 * Defines a property on an object. When the setter changes the value a | |
| 244 * property change event with the type {@code name + 'Change'} is fired. | |
| 245 * @param {!Object} obj The object to define the property for. | |
| 246 * @param {string} name The name of the property. | |
| 247 * @param {PropertyKind=} opt_kind What kind of underlying storage to use. | |
| 248 * @param {function(*, *):void=} opt_setHook A function to run after the | |
| 249 * property is set, but before the propertyChange event is fired. | |
| 250 */ | |
| 251 function defineProperty(obj, name, opt_kind, opt_setHook) { | 131 function defineProperty(obj, name, opt_kind, opt_setHook) { |
| 252 if (typeof obj == 'function') | 132 if (typeof obj == 'function') obj = obj.prototype; |
| 253 obj = obj.prototype; | 133 var kind = opt_kind || PropertyKind.JS; |
| 254 | 134 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name,
kind)); |
| 255 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); | 135 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name,
kind, opt_setHook)); |
| 256 | |
| 257 if (!obj.__lookupGetter__(name)) | |
| 258 obj.__defineGetter__(name, getGetter(name, kind)); | |
| 259 | |
| 260 if (!obj.__lookupSetter__(name)) | |
| 261 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); | |
| 262 } | 136 } |
| 263 | |
| 264 /** | |
| 265 * Counter for use with createUid | |
| 266 */ | |
| 267 var uidCounter = 1; | 137 var uidCounter = 1; |
| 268 | |
| 269 /** | |
| 270 * @return {number} A new unique ID. | |
| 271 */ | |
| 272 function createUid() { | 138 function createUid() { |
| 273 return uidCounter++; | 139 return uidCounter++; |
| 274 } | 140 } |
| 275 | |
| 276 /** | |
| 277 * Returns a unique ID for the item. This mutates the item so it needs to be | |
| 278 * an object | |
| 279 * @param {!Object} item The item to get the unique ID for. | |
| 280 * @return {number} The unique ID for the item. | |
| 281 */ | |
| 282 function getUid(item) { | 141 function getUid(item) { |
| 283 if (item.hasOwnProperty('uid')) | 142 if (item.hasOwnProperty('uid')) return item.uid; |
| 284 return item.uid; | |
| 285 return item.uid = createUid(); | 143 return item.uid = createUid(); |
| 286 } | 144 } |
| 287 | |
| 288 /** | |
| 289 * Dispatches a simple event on an event target. | |
| 290 * @param {!EventTarget} target The event target to dispatch the event on. | |
| 291 * @param {string} type The type of the event. | |
| 292 * @param {boolean=} opt_bubbles Whether the event bubbles or not. | |
| 293 * @param {boolean=} opt_cancelable Whether the default action of the event | |
| 294 * can be prevented. Default is true. | |
| 295 * @return {boolean} If any of the listeners called {@code preventDefault} | |
| 296 * during the dispatch this will return false. | |
| 297 */ | |
| 298 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { | 145 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
| 299 var e = new Event(type, { | 146 var e = new Event(type, { |
| 300 bubbles: opt_bubbles, | 147 bubbles: opt_bubbles, |
| 301 cancelable: opt_cancelable === undefined || opt_cancelable | 148 cancelable: opt_cancelable === undefined || opt_cancelable |
| 302 }); | 149 }); |
| 303 return target.dispatchEvent(e); | 150 return target.dispatchEvent(e); |
| 304 } | 151 } |
| 305 | |
| 306 /** | |
| 307 * Calls |fun| and adds all the fields of the returned object to the object | |
| 308 * named by |name|. For example, cr.define('cr.ui', function() { | |
| 309 * function List() { | |
| 310 * ... | |
| 311 * } | |
| 312 * function ListItem() { | |
| 313 * ... | |
| 314 * } | |
| 315 * return { | |
| 316 * List: List, | |
| 317 * ListItem: ListItem, | |
| 318 * }; | |
| 319 * }); | |
| 320 * defines the functions cr.ui.List and cr.ui.ListItem. | |
| 321 * @param {string} name The name of the object that we are adding fields to. | |
| 322 * @param {!Function} fun The function that will return an object containing | |
| 323 * the names and values of the new fields. | |
| 324 */ | |
| 325 function define(name, fun) { | 152 function define(name, fun) { |
| 326 var obj = exportPath(name); | 153 var obj = exportPath(name); |
| 327 var exports = fun(); | 154 var exports = fun(); |
| 328 for (var propertyName in exports) { | 155 for (var propertyName in exports) { |
| 329 // Maybe we should check the prototype chain here? The current usage | 156 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property
Name); |
| 330 // pattern is always using an object literal so we only care about own | 157 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD
escriptor); |
| 331 // properties. | |
| 332 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, | |
| 333 propertyName); | |
| 334 if (propertyDescriptor) | |
| 335 Object.defineProperty(obj, propertyName, propertyDescriptor); | |
| 336 } | 158 } |
| 337 } | 159 } |
| 338 | |
| 339 /** | |
| 340 * Adds a {@code getInstance} static method that always return the same | |
| 341 * instance object. | |
| 342 * @param {!Function} ctor The constructor for the class to add the static | |
| 343 * method to. | |
| 344 */ | |
| 345 function addSingletonGetter(ctor) { | 160 function addSingletonGetter(ctor) { |
| 346 ctor.getInstance = function() { | 161 ctor.getInstance = function() { |
| 347 return ctor.instance_ || (ctor.instance_ = new ctor()); | 162 return ctor.instance_ || (ctor.instance_ = new ctor()); |
| 348 }; | 163 }; |
| 349 } | 164 } |
| 350 | |
| 351 /** | |
| 352 * Forwards public APIs to private implementations. | |
| 353 * @param {Function} ctor Constructor that have private implementations in its | |
| 354 * prototype. | |
| 355 * @param {Array<string>} methods List of public method names that have their | |
| 356 * underscored counterparts in constructor's prototype. | |
| 357 * @param {string=} opt_target Selector for target node. | |
| 358 */ | |
| 359 function makePublic(ctor, methods, opt_target) { | 165 function makePublic(ctor, methods, opt_target) { |
| 360 methods.forEach(function(method) { | 166 methods.forEach(function(method) { |
| 361 ctor[method] = function() { | 167 ctor[method] = function() { |
| 362 var target = opt_target ? document.getElementById(opt_target) : | 168 var target = opt_target ? document.getElementById(opt_target) : ctor.get
Instance(); |
| 363 ctor.getInstance(); | |
| 364 return target[method + '_'].apply(target, arguments); | 169 return target[method + '_'].apply(target, arguments); |
| 365 }; | 170 }; |
| 366 }); | 171 }); |
| 367 } | 172 } |
| 368 | |
| 369 /** | |
| 370 * The mapping used by the sendWithPromise mechanism to tie the Promise | |
| 371 * returned to callers with the corresponding WebUI response. The mapping is | |
| 372 * from ID to the PromiseResolver helper; the ID is generated by | |
| 373 * sendWithPromise and is unique across all invocations of said method. | |
| 374 * @type {!Object<!PromiseResolver>} | |
| 375 */ | |
| 376 var chromeSendResolverMap = {}; | 173 var chromeSendResolverMap = {}; |
| 377 | |
| 378 /** | |
| 379 * The named method the WebUI handler calls directly in response to a | |
| 380 * chrome.send call that expects a response. The handler requires no knowledge | |
| 381 * of the specific name of this method, as the name is passed to the handler | |
| 382 * as the first argument in the arguments list of chrome.send. The handler | |
| 383 * must pass the ID, also sent via the chrome.send arguments list, as the | |
| 384 * first argument of the JS invocation; additionally, the handler may | |
| 385 * supply any number of other arguments that will be included in the response. | |
| 386 * @param {string} id The unique ID identifying the Promise this response is | |
| 387 * tied to. | |
| 388 * @param {boolean} isSuccess Whether the request was successful. | |
| 389 * @param {*} response The response as sent from C++. | |
| 390 */ | |
| 391 function webUIResponse(id, isSuccess, response) { | 174 function webUIResponse(id, isSuccess, response) { |
| 392 var resolver = chromeSendResolverMap[id]; | 175 var resolver = chromeSendResolverMap[id]; |
| 393 delete chromeSendResolverMap[id]; | 176 delete chromeSendResolverMap[id]; |
| 394 | 177 if (isSuccess) resolver.resolve(response); else resolver.reject(response); |
| 395 if (isSuccess) | |
| 396 resolver.resolve(response); | |
| 397 else | |
| 398 resolver.reject(response); | |
| 399 } | 178 } |
| 400 | |
| 401 /** | |
| 402 * A variation of chrome.send, suitable for messages that expect a single | |
| 403 * response from C++. | |
| 404 * @param {string} methodName The name of the WebUI handler API. | |
| 405 * @param {...*} var_args Varibale number of arguments to be forwarded to the | |
| 406 * C++ call. | |
| 407 * @return {!Promise} | |
| 408 */ | |
| 409 function sendWithPromise(methodName, var_args) { | 179 function sendWithPromise(methodName, var_args) { |
| 410 var args = Array.prototype.slice.call(arguments, 1); | 180 var args = Array.prototype.slice.call(arguments, 1); |
| 411 var promiseResolver = new PromiseResolver(); | 181 var promiseResolver = new PromiseResolver(); |
| 412 var id = methodName + '_' + createUid(); | 182 var id = methodName + '_' + createUid(); |
| 413 chromeSendResolverMap[id] = promiseResolver; | 183 chromeSendResolverMap[id] = promiseResolver; |
| 414 chrome.send(methodName, [id].concat(args)); | 184 chrome.send(methodName, [ id ].concat(args)); |
| 415 return promiseResolver.promise; | 185 return promiseResolver.promise; |
| 416 } | 186 } |
| 417 | |
| 418 /** | |
| 419 * A map of maps associating event names with listeners. The 2nd level map | |
| 420 * associates a listener ID with the callback function, such that individual | |
| 421 * listeners can be removed from an event without affecting other listeners of | |
| 422 * the same event. | |
| 423 * @type {!Object<!Object<!Function>>} | |
| 424 */ | |
| 425 var webUIListenerMap = {}; | 187 var webUIListenerMap = {}; |
| 426 | |
| 427 /** | |
| 428 * The named method the WebUI handler calls directly when an event occurs. | |
| 429 * The WebUI handler must supply the name of the event as the first argument | |
| 430 * of the JS invocation; additionally, the handler may supply any number of | |
| 431 * other arguments that will be forwarded to the listener callbacks. | |
| 432 * @param {string} event The name of the event that has occurred. | |
| 433 * @param {...*} var_args Additional arguments passed from C++. | |
| 434 */ | |
| 435 function webUIListenerCallback(event, var_args) { | 188 function webUIListenerCallback(event, var_args) { |
| 436 var eventListenersMap = webUIListenerMap[event]; | 189 var eventListenersMap = webUIListenerMap[event]; |
| 437 if (!eventListenersMap) { | 190 if (!eventListenersMap) { |
| 438 // C++ event sent for an event that has no listeners. | |
| 439 // TODO(dpapad): Should a warning be displayed here? | |
| 440 return; | 191 return; |
| 441 } | 192 } |
| 442 | |
| 443 var args = Array.prototype.slice.call(arguments, 1); | 193 var args = Array.prototype.slice.call(arguments, 1); |
| 444 for (var listenerId in eventListenersMap) { | 194 for (var listenerId in eventListenersMap) { |
| 445 eventListenersMap[listenerId].apply(null, args); | 195 eventListenersMap[listenerId].apply(null, args); |
| 446 } | 196 } |
| 447 } | 197 } |
| 448 | |
| 449 /** | |
| 450 * Registers a listener for an event fired from WebUI handlers. Any number of | |
| 451 * listeners may register for a single event. | |
| 452 * @param {string} eventName The event to listen to. | |
| 453 * @param {!Function} callback The callback run when the event is fired. | |
| 454 * @return {!WebUIListener} An object to be used for removing a listener via | |
| 455 * cr.removeWebUIListener. Should be treated as read-only. | |
| 456 */ | |
| 457 function addWebUIListener(eventName, callback) { | 198 function addWebUIListener(eventName, callback) { |
| 458 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; | 199 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
| 459 var uid = createUid(); | 200 var uid = createUid(); |
| 460 webUIListenerMap[eventName][uid] = callback; | 201 webUIListenerMap[eventName][uid] = callback; |
| 461 return {eventName: eventName, uid: uid}; | 202 return { |
| 203 eventName: eventName, |
| 204 uid: uid |
| 205 }; |
| 462 } | 206 } |
| 463 | |
| 464 /** | |
| 465 * Removes a listener. Does nothing if the specified listener is not found. | |
| 466 * @param {!WebUIListener} listener The listener to be removed (as returned by | |
| 467 * addWebUIListener). | |
| 468 * @return {boolean} Whether the given listener was found and actually | |
| 469 * removed. | |
| 470 */ | |
| 471 function removeWebUIListener(listener) { | 207 function removeWebUIListener(listener) { |
| 472 var listenerExists = webUIListenerMap[listener.eventName] && | 208 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa
p[listener.eventName][listener.uid]; |
| 473 webUIListenerMap[listener.eventName][listener.uid]; | |
| 474 if (listenerExists) { | 209 if (listenerExists) { |
| 475 delete webUIListenerMap[listener.eventName][listener.uid]; | 210 delete webUIListenerMap[listener.eventName][listener.uid]; |
| 476 return true; | 211 return true; |
| 477 } | 212 } |
| 478 return false; | 213 return false; |
| 479 } | 214 } |
| 480 | |
| 481 return { | 215 return { |
| 482 addSingletonGetter: addSingletonGetter, | 216 addSingletonGetter: addSingletonGetter, |
| 483 createUid: createUid, | 217 createUid: createUid, |
| 484 define: define, | 218 define: define, |
| 485 defineProperty: defineProperty, | 219 defineProperty: defineProperty, |
| 486 dispatchPropertyChange: dispatchPropertyChange, | 220 dispatchPropertyChange: dispatchPropertyChange, |
| 487 dispatchSimpleEvent: dispatchSimpleEvent, | 221 dispatchSimpleEvent: dispatchSimpleEvent, |
| 488 exportPath: exportPath, | 222 exportPath: exportPath, |
| 489 getUid: getUid, | 223 getUid: getUid, |
| 490 makePublic: makePublic, | 224 makePublic: makePublic, |
| 491 PropertyKind: PropertyKind, | 225 PropertyKind: PropertyKind, |
| 492 | |
| 493 // C++ <-> JS communication related methods. | |
| 494 addWebUIListener: addWebUIListener, | 226 addWebUIListener: addWebUIListener, |
| 495 removeWebUIListener: removeWebUIListener, | 227 removeWebUIListener: removeWebUIListener, |
| 496 sendWithPromise: sendWithPromise, | 228 sendWithPromise: sendWithPromise, |
| 497 webUIListenerCallback: webUIListenerCallback, | 229 webUIListenerCallback: webUIListenerCallback, |
| 498 webUIResponse: webUIResponse, | 230 webUIResponse: webUIResponse, |
| 499 | |
| 500 get doc() { | 231 get doc() { |
| 501 return document; | 232 return document; |
| 502 }, | 233 }, |
| 503 | |
| 504 /** Whether we are using a Mac or not. */ | |
| 505 get isMac() { | 234 get isMac() { |
| 506 return /Mac/.test(navigator.platform); | 235 return /Mac/.test(navigator.platform); |
| 507 }, | 236 }, |
| 508 | |
| 509 /** Whether this is on the Windows platform or not. */ | |
| 510 get isWindows() { | 237 get isWindows() { |
| 511 return /Win/.test(navigator.platform); | 238 return /Win/.test(navigator.platform); |
| 512 }, | 239 }, |
| 513 | |
| 514 /** Whether this is on chromeOS or not. */ | |
| 515 get isChromeOS() { | 240 get isChromeOS() { |
| 516 return /CrOS/.test(navigator.userAgent); | 241 return /CrOS/.test(navigator.userAgent); |
| 517 }, | 242 }, |
| 518 | |
| 519 /** Whether this is on vanilla Linux (not chromeOS). */ | |
| 520 get isLinux() { | 243 get isLinux() { |
| 521 return /Linux/.test(navigator.userAgent); | 244 return /Linux/.test(navigator.userAgent); |
| 522 }, | 245 }, |
| 523 | |
| 524 /** Whether this is on Android. */ | |
| 525 get isAndroid() { | 246 get isAndroid() { |
| 526 return /Android/.test(navigator.userAgent); | 247 return /Android/.test(navigator.userAgent); |
| 527 }, | 248 }, |
| 528 | |
| 529 /** Whether this is on iOS. */ | |
| 530 get isIOS() { | 249 get isIOS() { |
| 531 return /iPad|iPhone|iPod/.test(navigator.platform); | 250 return /iPad|iPhone|iPod/.test(navigator.platform); |
| 532 } | 251 } |
| 533 }; | 252 }; |
| 534 }(); | 253 }(); |
| 254 |
| 535 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 255 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 536 // Use of this source code is governed by a BSD-style license that can be | 256 // Use of this source code is governed by a BSD-style license that can be |
| 537 // found in the LICENSE file. | 257 // found in the LICENSE file. |
| 538 | |
| 539 cr.define('cr.ui', function() { | 258 cr.define('cr.ui', function() { |
| 540 | |
| 541 /** | |
| 542 * Decorates elements as an instance of a class. | |
| 543 * @param {string|!Element} source The way to find the element(s) to decorate. | |
| 544 * If this is a string then {@code querySeletorAll} is used to find the | |
| 545 * elements to decorate. | |
| 546 * @param {!Function} constr The constructor to decorate with. The constr | |
| 547 * needs to have a {@code decorate} function. | |
| 548 */ | |
| 549 function decorate(source, constr) { | 259 function decorate(source, constr) { |
| 550 var elements; | 260 var elements; |
| 551 if (typeof source == 'string') | 261 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e
lse elements = [ source ]; |
| 552 elements = cr.doc.querySelectorAll(source); | |
| 553 else | |
| 554 elements = [source]; | |
| 555 | |
| 556 for (var i = 0, el; el = elements[i]; i++) { | 262 for (var i = 0, el; el = elements[i]; i++) { |
| 557 if (!(el instanceof constr)) | 263 if (!(el instanceof constr)) constr.decorate(el); |
| 558 constr.decorate(el); | |
| 559 } | 264 } |
| 560 } | 265 } |
| 561 | |
| 562 /** | |
| 563 * Helper function for creating new element for define. | |
| 564 */ | |
| 565 function createElementHelper(tagName, opt_bag) { | 266 function createElementHelper(tagName, opt_bag) { |
| 566 // Allow passing in ownerDocument to create in a different document. | |
| 567 var doc; | 267 var doc; |
| 568 if (opt_bag && opt_bag.ownerDocument) | 268 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc
= cr.doc; |
| 569 doc = opt_bag.ownerDocument; | |
| 570 else | |
| 571 doc = cr.doc; | |
| 572 return doc.createElement(tagName); | 269 return doc.createElement(tagName); |
| 573 } | 270 } |
| 574 | |
| 575 /** | |
| 576 * Creates the constructor for a UI element class. | |
| 577 * | |
| 578 * Usage: | |
| 579 * <pre> | |
| 580 * var List = cr.ui.define('list'); | |
| 581 * List.prototype = { | |
| 582 * __proto__: HTMLUListElement.prototype, | |
| 583 * decorate: function() { | |
| 584 * ... | |
| 585 * }, | |
| 586 * ... | |
| 587 * }; | |
| 588 * </pre> | |
| 589 * | |
| 590 * @param {string|Function} tagNameOrFunction The tagName or | |
| 591 * function to use for newly created elements. If this is a function it | |
| 592 * needs to return a new element when called. | |
| 593 * @return {function(Object=):Element} The constructor function which takes | |
| 594 * an optional property bag. The function also has a static | |
| 595 * {@code decorate} method added to it. | |
| 596 */ | |
| 597 function define(tagNameOrFunction) { | 271 function define(tagNameOrFunction) { |
| 598 var createFunction, tagName; | 272 var createFunction, tagName; |
| 599 if (typeof tagNameOrFunction == 'function') { | 273 if (typeof tagNameOrFunction == 'function') { |
| 600 createFunction = tagNameOrFunction; | 274 createFunction = tagNameOrFunction; |
| 601 tagName = ''; | 275 tagName = ''; |
| 602 } else { | 276 } else { |
| 603 createFunction = createElementHelper; | 277 createFunction = createElementHelper; |
| 604 tagName = tagNameOrFunction; | 278 tagName = tagNameOrFunction; |
| 605 } | 279 } |
| 606 | |
| 607 /** | |
| 608 * Creates a new UI element constructor. | |
| 609 * @param {Object=} opt_propertyBag Optional bag of properties to set on the | |
| 610 * object after created. The property {@code ownerDocument} is special | |
| 611 * cased and it allows you to create the element in a different | |
| 612 * document than the default. | |
| 613 * @constructor | |
| 614 */ | |
| 615 function f(opt_propertyBag) { | 280 function f(opt_propertyBag) { |
| 616 var el = createFunction(tagName, opt_propertyBag); | 281 var el = createFunction(tagName, opt_propertyBag); |
| 617 f.decorate(el); | 282 f.decorate(el); |
| 618 for (var propertyName in opt_propertyBag) { | 283 for (var propertyName in opt_propertyBag) { |
| 619 el[propertyName] = opt_propertyBag[propertyName]; | 284 el[propertyName] = opt_propertyBag[propertyName]; |
| 620 } | 285 } |
| 621 return el; | 286 return el; |
| 622 } | 287 } |
| 623 | |
| 624 /** | |
| 625 * Decorates an element as a UI element class. | |
| 626 * @param {!Element} el The element to decorate. | |
| 627 */ | |
| 628 f.decorate = function(el) { | 288 f.decorate = function(el) { |
| 629 el.__proto__ = f.prototype; | 289 el.__proto__ = f.prototype; |
| 630 el.decorate(); | 290 el.decorate(); |
| 631 }; | 291 }; |
| 632 | |
| 633 return f; | 292 return f; |
| 634 } | 293 } |
| 635 | |
| 636 /** | |
| 637 * Input elements do not grow and shrink with their content. This is a simple | |
| 638 * (and not very efficient) way of handling shrinking to content with support | |
| 639 * for min width and limited by the width of the parent element. | |
| 640 * @param {!HTMLElement} el The element to limit the width for. | |
| 641 * @param {!HTMLElement} parentEl The parent element that should limit the | |
| 642 * size. | |
| 643 * @param {number} min The minimum width. | |
| 644 * @param {number=} opt_scale Optional scale factor to apply to the width. | |
| 645 */ | |
| 646 function limitInputWidth(el, parentEl, min, opt_scale) { | 294 function limitInputWidth(el, parentEl, min, opt_scale) { |
| 647 // Needs a size larger than borders | |
| 648 el.style.width = '10px'; | 295 el.style.width = '10px'; |
| 649 var doc = el.ownerDocument; | 296 var doc = el.ownerDocument; |
| 650 var win = doc.defaultView; | 297 var win = doc.defaultView; |
| 651 var computedStyle = win.getComputedStyle(el); | 298 var computedStyle = win.getComputedStyle(el); |
| 652 var parentComputedStyle = win.getComputedStyle(parentEl); | 299 var parentComputedStyle = win.getComputedStyle(parentEl); |
| 653 var rtl = computedStyle.direction == 'rtl'; | 300 var rtl = computedStyle.direction == 'rtl'; |
| 654 | 301 var inputRect = el.getBoundingClientRect(); |
| 655 // To get the max width we get the width of the treeItem minus the position | |
| 656 // of the input. | |
| 657 var inputRect = el.getBoundingClientRect(); // box-sizing | |
| 658 var parentRect = parentEl.getBoundingClientRect(); | 302 var parentRect = parentEl.getBoundingClientRect(); |
| 659 var startPos = rtl ? parentRect.right - inputRect.right : | 303 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p
arentRect.left; |
| 660 inputRect.left - parentRect.left; | 304 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS
tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp
utedStyle.borderRightWidth, 10); |
| 661 | 305 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa
rseInt(parentComputedStyle.paddingRight, 10); |
| 662 // Add up border and padding of the input. | |
| 663 var inner = parseInt(computedStyle.borderLeftWidth, 10) + | |
| 664 parseInt(computedStyle.paddingLeft, 10) + | |
| 665 parseInt(computedStyle.paddingRight, 10) + | |
| 666 parseInt(computedStyle.borderRightWidth, 10); | |
| 667 | |
| 668 // We also need to subtract the padding of parent to prevent it to overflow. | |
| 669 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : | |
| 670 parseInt(parentComputedStyle.paddingRight, 10); | |
| 671 | |
| 672 var max = parentEl.clientWidth - startPos - inner - parentPadding; | 306 var max = parentEl.clientWidth - startPos - inner - parentPadding; |
| 673 if (opt_scale) | 307 if (opt_scale) max *= opt_scale; |
| 674 max *= opt_scale; | |
| 675 | |
| 676 function limit() { | 308 function limit() { |
| 677 if (el.scrollWidth > max) { | 309 if (el.scrollWidth > max) { |
| 678 el.style.width = max + 'px'; | 310 el.style.width = max + 'px'; |
| 679 } else { | 311 } else { |
| 680 el.style.width = 0; | 312 el.style.width = 0; |
| 681 var sw = el.scrollWidth; | 313 var sw = el.scrollWidth; |
| 682 if (sw < min) { | 314 if (sw < min) { |
| 683 el.style.width = min + 'px'; | 315 el.style.width = min + 'px'; |
| 684 } else { | 316 } else { |
| 685 el.style.width = sw + 'px'; | 317 el.style.width = sw + 'px'; |
| 686 } | 318 } |
| 687 } | 319 } |
| 688 } | 320 } |
| 689 | |
| 690 el.addEventListener('input', limit); | 321 el.addEventListener('input', limit); |
| 691 limit(); | 322 limit(); |
| 692 } | 323 } |
| 693 | |
| 694 /** | |
| 695 * Takes a number and spits out a value CSS will be happy with. To avoid | |
| 696 * subpixel layout issues, the value is rounded to the nearest integral value. | |
| 697 * @param {number} pixels The number of pixels. | |
| 698 * @return {string} e.g. '16px'. | |
| 699 */ | |
| 700 function toCssPx(pixels) { | 324 function toCssPx(pixels) { |
| 701 if (!window.isFinite(pixels)) | 325 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: '
+ pixels); |
| 702 console.error('Pixel value is not a number: ' + pixels); | |
| 703 return Math.round(pixels) + 'px'; | 326 return Math.round(pixels) + 'px'; |
| 704 } | 327 } |
| 705 | |
| 706 /** | |
| 707 * Users complain they occasionaly use doubleclicks instead of clicks | |
| 708 * (http://crbug.com/140364). To fix it we freeze click handling for | |
| 709 * the doubleclick time interval. | |
| 710 * @param {MouseEvent} e Initial click event. | |
| 711 */ | |
| 712 function swallowDoubleClick(e) { | 328 function swallowDoubleClick(e) { |
| 713 var doc = e.target.ownerDocument; | 329 var doc = e.target.ownerDocument; |
| 714 var counter = Math.min(1, e.detail); | 330 var counter = Math.min(1, e.detail); |
| 715 function swallow(e) { | 331 function swallow(e) { |
| 716 e.stopPropagation(); | 332 e.stopPropagation(); |
| 717 e.preventDefault(); | 333 e.preventDefault(); |
| 718 } | 334 } |
| 719 function onclick(e) { | 335 function onclick(e) { |
| 720 if (e.detail > counter) { | 336 if (e.detail > counter) { |
| 721 counter = e.detail; | 337 counter = e.detail; |
| 722 // Swallow the click since it's a click inside the doubleclick timeout. | |
| 723 swallow(e); | 338 swallow(e); |
| 724 } else { | 339 } else { |
| 725 // Stop tracking clicks and let regular handling. | |
| 726 doc.removeEventListener('dblclick', swallow, true); | 340 doc.removeEventListener('dblclick', swallow, true); |
| 727 doc.removeEventListener('click', onclick, true); | 341 doc.removeEventListener('click', onclick, true); |
| 728 } | 342 } |
| 729 } | 343 } |
| 730 // The following 'click' event (if e.type == 'mouseup') mustn't be taken | |
| 731 // into account (it mustn't stop tracking clicks). Start event listening | |
| 732 // after zero timeout. | |
| 733 setTimeout(function() { | 344 setTimeout(function() { |
| 734 doc.addEventListener('click', onclick, true); | 345 doc.addEventListener('click', onclick, true); |
| 735 doc.addEventListener('dblclick', swallow, true); | 346 doc.addEventListener('dblclick', swallow, true); |
| 736 }, 0); | 347 }, 0); |
| 737 } | 348 } |
| 738 | |
| 739 return { | 349 return { |
| 740 decorate: decorate, | 350 decorate: decorate, |
| 741 define: define, | 351 define: define, |
| 742 limitInputWidth: limitInputWidth, | 352 limitInputWidth: limitInputWidth, |
| 743 toCssPx: toCssPx, | 353 toCssPx: toCssPx, |
| 744 swallowDoubleClick: swallowDoubleClick | 354 swallowDoubleClick: swallowDoubleClick |
| 745 }; | 355 }; |
| 746 }); | 356 }); |
| 357 |
| 747 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 358 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 748 // Use of this source code is governed by a BSD-style license that can be | 359 // Use of this source code is governed by a BSD-style license that can be |
| 749 // found in the LICENSE file. | 360 // found in the LICENSE file. |
| 750 | |
| 751 /** | |
| 752 * @fileoverview A command is an abstraction of an action a user can do in the | |
| 753 * UI. | |
| 754 * | |
| 755 * When the focus changes in the document for each command a canExecute event | |
| 756 * is dispatched on the active element. By listening to this event you can | |
| 757 * enable and disable the command by setting the event.canExecute property. | |
| 758 * | |
| 759 * When a command is executed a command event is dispatched on the active | |
| 760 * element. Note that you should stop the propagation after you have handled the | |
| 761 * command if there might be other command listeners higher up in the DOM tree. | |
| 762 */ | |
| 763 | |
| 764 cr.define('cr.ui', function() { | 361 cr.define('cr.ui', function() { |
| 765 | |
| 766 /** | |
| 767 * This is used to identify keyboard shortcuts. | |
| 768 * @param {string} shortcut The text used to describe the keys for this | |
| 769 * keyboard shortcut. | |
| 770 * @constructor | |
| 771 */ | |
| 772 function KeyboardShortcut(shortcut) { | 362 function KeyboardShortcut(shortcut) { |
| 773 var mods = {}; | 363 var mods = {}; |
| 774 var ident = ''; | 364 var ident = ''; |
| 775 shortcut.split('|').forEach(function(part) { | 365 shortcut.split('|').forEach(function(part) { |
| 776 var partLc = part.toLowerCase(); | 366 var partLc = part.toLowerCase(); |
| 777 switch (partLc) { | 367 switch (partLc) { |
| 778 case 'alt': | 368 case 'alt': |
| 779 case 'ctrl': | 369 case 'ctrl': |
| 780 case 'meta': | 370 case 'meta': |
| 781 case 'shift': | 371 case 'shift': |
| 782 mods[partLc + 'Key'] = true; | 372 mods[partLc + 'Key'] = true; |
| 783 break; | 373 break; |
| 784 default: | 374 |
| 785 if (ident) | 375 default: |
| 786 throw Error('Invalid shortcut'); | 376 if (ident) throw Error('Invalid shortcut'); |
| 787 ident = part; | 377 ident = part; |
| 788 } | 378 } |
| 789 }); | 379 }); |
| 790 | |
| 791 this.ident_ = ident; | 380 this.ident_ = ident; |
| 792 this.mods_ = mods; | 381 this.mods_ = mods; |
| 793 } | 382 } |
| 794 | |
| 795 KeyboardShortcut.prototype = { | 383 KeyboardShortcut.prototype = { |
| 796 /** | |
| 797 * Whether the keyboard shortcut object matches a keyboard event. | |
| 798 * @param {!Event} e The keyboard event object. | |
| 799 * @return {boolean} Whether we found a match or not. | |
| 800 */ | |
| 801 matchesEvent: function(e) { | 384 matchesEvent: function(e) { |
| 802 if (e.key == this.ident_) { | 385 if (e.key == this.ident_) { |
| 803 // All keyboard modifiers needs to match. | |
| 804 var mods = this.mods_; | 386 var mods = this.mods_; |
| 805 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { | 387 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k)
{ |
| 806 return e[k] == !!mods[k]; | 388 return e[k] == !!mods[k]; |
| 807 }); | 389 }); |
| 808 } | 390 } |
| 809 return false; | 391 return false; |
| 810 } | 392 } |
| 811 }; | 393 }; |
| 812 | |
| 813 /** | |
| 814 * Creates a new command element. | |
| 815 * @constructor | |
| 816 * @extends {HTMLElement} | |
| 817 */ | |
| 818 var Command = cr.ui.define('command'); | 394 var Command = cr.ui.define('command'); |
| 819 | |
| 820 Command.prototype = { | 395 Command.prototype = { |
| 821 __proto__: HTMLElement.prototype, | 396 __proto__: HTMLElement.prototype, |
| 822 | |
| 823 /** | |
| 824 * Initializes the command. | |
| 825 */ | |
| 826 decorate: function() { | 397 decorate: function() { |
| 827 CommandManager.init(assert(this.ownerDocument)); | 398 CommandManager.init(assert(this.ownerDocument)); |
| 828 | 399 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor
tcut'); |
| 829 if (this.hasAttribute('shortcut')) | |
| 830 this.shortcut = this.getAttribute('shortcut'); | |
| 831 }, | 400 }, |
| 832 | |
| 833 /** | |
| 834 * Executes the command by dispatching a command event on the given element. | |
| 835 * If |element| isn't given, the active element is used instead. | |
| 836 * If the command is {@code disabled} this does nothing. | |
| 837 * @param {HTMLElement=} opt_element Optional element to dispatch event on. | |
| 838 */ | |
| 839 execute: function(opt_element) { | 401 execute: function(opt_element) { |
| 840 if (this.disabled) | 402 if (this.disabled) return; |
| 841 return; | |
| 842 var doc = this.ownerDocument; | 403 var doc = this.ownerDocument; |
| 843 if (doc.activeElement) { | 404 if (doc.activeElement) { |
| 844 var e = new Event('command', {bubbles: true}); | 405 var e = new Event('command', { |
| 406 bubbles: true |
| 407 }); |
| 845 e.command = this; | 408 e.command = this; |
| 846 | |
| 847 (opt_element || doc.activeElement).dispatchEvent(e); | 409 (opt_element || doc.activeElement).dispatchEvent(e); |
| 848 } | 410 } |
| 849 }, | 411 }, |
| 850 | |
| 851 /** | |
| 852 * Call this when there have been changes that might change whether the | |
| 853 * command can be executed or not. | |
| 854 * @param {Node=} opt_node Node for which to actuate command state. | |
| 855 */ | |
| 856 canExecuteChange: function(opt_node) { | 412 canExecuteChange: function(opt_node) { |
| 857 dispatchCanExecuteEvent(this, | 413 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement
); |
| 858 opt_node || this.ownerDocument.activeElement); | |
| 859 }, | 414 }, |
| 860 | |
| 861 /** | |
| 862 * The keyboard shortcut that triggers the command. This is a string | |
| 863 * consisting of a key (as reported by WebKit in keydown) as | |
| 864 * well as optional key modifiers joinded with a '|'. | |
| 865 * | |
| 866 * Multiple keyboard shortcuts can be provided by separating them by | |
| 867 * whitespace. | |
| 868 * | |
| 869 * For example: | |
| 870 * "F1" | |
| 871 * "Backspace|Meta" for Apple command backspace. | |
| 872 * "a|Ctrl" for Control A | |
| 873 * "Delete Backspace|Meta" for Delete and Command Backspace | |
| 874 * | |
| 875 * @type {string} | |
| 876 */ | |
| 877 shortcut_: '', | 415 shortcut_: '', |
| 878 get shortcut() { | 416 get shortcut() { |
| 879 return this.shortcut_; | 417 return this.shortcut_; |
| 880 }, | 418 }, |
| 881 set shortcut(shortcut) { | 419 set shortcut(shortcut) { |
| 882 var oldShortcut = this.shortcut_; | 420 var oldShortcut = this.shortcut_; |
| 883 if (shortcut !== oldShortcut) { | 421 if (shortcut !== oldShortcut) { |
| 884 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { | 422 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { |
| 885 return new KeyboardShortcut(shortcut); | 423 return new KeyboardShortcut(shortcut); |
| 886 }); | 424 }); |
| 887 | |
| 888 // Set this after the keyboardShortcuts_ since that might throw. | |
| 889 this.shortcut_ = shortcut; | 425 this.shortcut_ = shortcut; |
| 890 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, | 426 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut)
; |
| 891 oldShortcut); | |
| 892 } | 427 } |
| 893 }, | 428 }, |
| 894 | |
| 895 /** | |
| 896 * Whether the event object matches the shortcut for this command. | |
| 897 * @param {!Event} e The key event object. | |
| 898 * @return {boolean} Whether it matched or not. | |
| 899 */ | |
| 900 matchesEvent: function(e) { | 429 matchesEvent: function(e) { |
| 901 if (!this.keyboardShortcuts_) | 430 if (!this.keyboardShortcuts_) return false; |
| 902 return false; | |
| 903 | |
| 904 return this.keyboardShortcuts_.some(function(keyboardShortcut) { | 431 return this.keyboardShortcuts_.some(function(keyboardShortcut) { |
| 905 return keyboardShortcut.matchesEvent(e); | 432 return keyboardShortcut.matchesEvent(e); |
| 906 }); | 433 }); |
| 907 }, | 434 } |
| 908 }; | 435 }; |
| 909 | |
| 910 /** | |
| 911 * The label of the command. | |
| 912 */ | |
| 913 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); | 436 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); |
| 914 | |
| 915 /** | |
| 916 * Whether the command is disabled or not. | |
| 917 */ | |
| 918 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); | 437 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); |
| 919 | |
| 920 /** | |
| 921 * Whether the command is hidden or not. | |
| 922 */ | |
| 923 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); | 438 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
| 924 | |
| 925 /** | |
| 926 * Whether the command is checked or not. | |
| 927 */ | |
| 928 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); | 439 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
| 929 | |
| 930 /** | |
| 931 * The flag that prevents the shortcut text from being displayed on menu. | |
| 932 * | |
| 933 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command) | |
| 934 * is displayed in menu when the command is assosiated with a menu item. | |
| 935 * Otherwise, no text is displayed. | |
| 936 */ | |
| 937 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); | 440 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); |
| 938 | |
| 939 /** | |
| 940 * Dispatches a canExecute event on the target. | |
| 941 * @param {!cr.ui.Command} command The command that we are testing for. | |
| 942 * @param {EventTarget} target The target element to dispatch the event on. | |
| 943 */ | |
| 944 function dispatchCanExecuteEvent(command, target) { | 441 function dispatchCanExecuteEvent(command, target) { |
| 945 var e = new CanExecuteEvent(command); | 442 var e = new CanExecuteEvent(command); |
| 946 target.dispatchEvent(e); | 443 target.dispatchEvent(e); |
| 947 command.disabled = !e.canExecute; | 444 command.disabled = !e.canExecute; |
| 948 } | 445 } |
| 949 | |
| 950 /** | |
| 951 * The command managers for different documents. | |
| 952 */ | |
| 953 var commandManagers = {}; | 446 var commandManagers = {}; |
| 954 | |
| 955 /** | |
| 956 * Keeps track of the focused element and updates the commands when the focus | |
| 957 * changes. | |
| 958 * @param {!Document} doc The document that we are managing the commands for. | |
| 959 * @constructor | |
| 960 */ | |
| 961 function CommandManager(doc) { | 447 function CommandManager(doc) { |
| 962 doc.addEventListener('focus', this.handleFocus_.bind(this), true); | 448 doc.addEventListener('focus', this.handleFocus_.bind(this), true); |
| 963 // Make sure we add the listener to the bubbling phase so that elements can | |
| 964 // prevent the command. | |
| 965 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); | 449 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); |
| 966 } | 450 } |
| 967 | |
| 968 /** | |
| 969 * Initializes a command manager for the document as needed. | |
| 970 * @param {!Document} doc The document to manage the commands for. | |
| 971 */ | |
| 972 CommandManager.init = function(doc) { | 451 CommandManager.init = function(doc) { |
| 973 var uid = cr.getUid(doc); | 452 var uid = cr.getUid(doc); |
| 974 if (!(uid in commandManagers)) { | 453 if (!(uid in commandManagers)) { |
| 975 commandManagers[uid] = new CommandManager(doc); | 454 commandManagers[uid] = new CommandManager(doc); |
| 976 } | 455 } |
| 977 }; | 456 }; |
| 978 | |
| 979 CommandManager.prototype = { | 457 CommandManager.prototype = { |
| 980 | |
| 981 /** | |
| 982 * Handles focus changes on the document. | |
| 983 * @param {Event} e The focus event object. | |
| 984 * @private | |
| 985 * @suppress {checkTypes} | |
| 986 * TODO(vitalyp): remove the suppression. | |
| 987 */ | |
| 988 handleFocus_: function(e) { | 458 handleFocus_: function(e) { |
| 989 var target = e.target; | 459 var target = e.target; |
| 990 | 460 if (target.menu || target.command) return; |
| 991 // Ignore focus on a menu button or command item. | 461 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
| 992 if (target.menu || target.command) | |
| 993 return; | |
| 994 | |
| 995 var commands = Array.prototype.slice.call( | |
| 996 target.ownerDocument.querySelectorAll('command')); | |
| 997 | |
| 998 commands.forEach(function(command) { | 462 commands.forEach(function(command) { |
| 999 dispatchCanExecuteEvent(command, target); | 463 dispatchCanExecuteEvent(command, target); |
| 1000 }); | 464 }); |
| 1001 }, | 465 }, |
| 1002 | |
| 1003 /** | |
| 1004 * Handles the keydown event and routes it to the right command. | |
| 1005 * @param {!Event} e The keydown event. | |
| 1006 */ | |
| 1007 handleKeyDown_: function(e) { | 466 handleKeyDown_: function(e) { |
| 1008 var target = e.target; | 467 var target = e.target; |
| 1009 var commands = Array.prototype.slice.call( | 468 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
| 1010 target.ownerDocument.querySelectorAll('command')); | |
| 1011 | |
| 1012 for (var i = 0, command; command = commands[i]; i++) { | 469 for (var i = 0, command; command = commands[i]; i++) { |
| 1013 if (command.matchesEvent(e)) { | 470 if (command.matchesEvent(e)) { |
| 1014 // When invoking a command via a shortcut, we have to manually check | |
| 1015 // if it can be executed, since focus might not have been changed | |
| 1016 // what would have updated the command's state. | |
| 1017 command.canExecuteChange(); | 471 command.canExecuteChange(); |
| 1018 | |
| 1019 if (!command.disabled) { | 472 if (!command.disabled) { |
| 1020 e.preventDefault(); | 473 e.preventDefault(); |
| 1021 // We do not want any other element to handle this. | |
| 1022 e.stopPropagation(); | 474 e.stopPropagation(); |
| 1023 command.execute(); | 475 command.execute(); |
| 1024 return; | 476 return; |
| 1025 } | 477 } |
| 1026 } | 478 } |
| 1027 } | 479 } |
| 1028 } | 480 } |
| 1029 }; | 481 }; |
| 1030 | |
| 1031 /** | |
| 1032 * The event type used for canExecute events. | |
| 1033 * @param {!cr.ui.Command} command The command that we are evaluating. | |
| 1034 * @extends {Event} | |
| 1035 * @constructor | |
| 1036 * @class | |
| 1037 */ | |
| 1038 function CanExecuteEvent(command) { | 482 function CanExecuteEvent(command) { |
| 1039 var e = new Event('canExecute', {bubbles: true, cancelable: true}); | 483 var e = new Event('canExecute', { |
| 484 bubbles: true, |
| 485 cancelable: true |
| 486 }); |
| 1040 e.__proto__ = CanExecuteEvent.prototype; | 487 e.__proto__ = CanExecuteEvent.prototype; |
| 1041 e.command = command; | 488 e.command = command; |
| 1042 return e; | 489 return e; |
| 1043 } | 490 } |
| 1044 | |
| 1045 CanExecuteEvent.prototype = { | 491 CanExecuteEvent.prototype = { |
| 1046 __proto__: Event.prototype, | 492 __proto__: Event.prototype, |
| 1047 | |
| 1048 /** | |
| 1049 * The current command | |
| 1050 * @type {cr.ui.Command} | |
| 1051 */ | |
| 1052 command: null, | 493 command: null, |
| 1053 | |
| 1054 /** | |
| 1055 * Whether the target can execute the command. Setting this also stops the | |
| 1056 * propagation and prevents the default. Callers can tell if an event has | |
| 1057 * been handled via |this.defaultPrevented|. | |
| 1058 * @type {boolean} | |
| 1059 */ | |
| 1060 canExecute_: false, | 494 canExecute_: false, |
| 1061 get canExecute() { | 495 get canExecute() { |
| 1062 return this.canExecute_; | 496 return this.canExecute_; |
| 1063 }, | 497 }, |
| 1064 set canExecute(canExecute) { | 498 set canExecute(canExecute) { |
| 1065 this.canExecute_ = !!canExecute; | 499 this.canExecute_ = !!canExecute; |
| 1066 this.stopPropagation(); | 500 this.stopPropagation(); |
| 1067 this.preventDefault(); | 501 this.preventDefault(); |
| 1068 } | 502 } |
| 1069 }; | 503 }; |
| 1070 | |
| 1071 // Export | |
| 1072 return { | 504 return { |
| 1073 Command: Command, | 505 Command: Command, |
| 1074 CanExecuteEvent: CanExecuteEvent | 506 CanExecuteEvent: CanExecuteEvent |
| 1075 }; | 507 }; |
| 1076 }); | 508 }); |
| 509 |
| 1077 Polymer({ | 510 Polymer({ |
| 1078 is: 'app-drawer', | 511 is: 'app-drawer', |
| 1079 | 512 properties: { |
| 1080 properties: { | 513 opened: { |
| 1081 /** | 514 type: Boolean, |
| 1082 * The opened state of the drawer. | 515 value: false, |
| 1083 */ | 516 notify: true, |
| 1084 opened: { | 517 reflectToAttribute: true |
| 1085 type: Boolean, | 518 }, |
| 1086 value: false, | 519 persistent: { |
| 1087 notify: true, | 520 type: Boolean, |
| 1088 reflectToAttribute: true | 521 value: false, |
| 1089 }, | 522 reflectToAttribute: true |
| 1090 | 523 }, |
| 1091 /** | 524 align: { |
| 1092 * The drawer does not have a scrim and cannot be swiped close. | 525 type: String, |
| 1093 */ | 526 value: 'left' |
| 1094 persistent: { | 527 }, |
| 1095 type: Boolean, | 528 position: { |
| 1096 value: false, | 529 type: String, |
| 1097 reflectToAttribute: true | 530 readOnly: true, |
| 1098 }, | 531 value: 'left', |
| 1099 | 532 reflectToAttribute: true |
| 1100 /** | 533 }, |
| 1101 * The alignment of the drawer on the screen ('left', 'right', 'start' o
r 'end'). | 534 swipeOpen: { |
| 1102 * 'start' computes to left and 'end' to right in LTR layout and vice ve
rsa in RTL | 535 type: Boolean, |
| 1103 * layout. | 536 value: false, |
| 1104 */ | 537 reflectToAttribute: true |
| 1105 align: { | 538 }, |
| 1106 type: String, | 539 noFocusTrap: { |
| 1107 value: 'left' | 540 type: Boolean, |
| 1108 }, | 541 value: false |
| 1109 | 542 } |
| 1110 /** | 543 }, |
| 1111 * The computed, read-only position of the drawer on the screen ('left'
or 'right'). | 544 observers: [ 'resetLayout(position)', '_resetPosition(align, isAttached)' ], |
| 1112 */ | 545 _translateOffset: 0, |
| 1113 position: { | 546 _trackDetails: null, |
| 1114 type: String, | 547 _drawerState: 0, |
| 1115 readOnly: true, | 548 _boundEscKeydownHandler: null, |
| 1116 value: 'left', | 549 _firstTabStop: null, |
| 1117 reflectToAttribute: true | 550 _lastTabStop: null, |
| 1118 }, | 551 ready: function() { |
| 1119 | 552 this.setScrollDirection('y'); |
| 1120 /** | 553 this._setTransitionDuration('0s'); |
| 1121 * Create an area at the edge of the screen to swipe open the drawer. | 554 }, |
| 1122 */ | 555 attached: function() { |
| 1123 swipeOpen: { | 556 Polymer.RenderStatus.afterNextRender(this, function() { |
| 1124 type: Boolean, | 557 this._setTransitionDuration(''); |
| 1125 value: false, | 558 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); |
| 1126 reflectToAttribute: true | 559 this._resetDrawerState(); |
| 1127 }, | 560 this.listen(this, 'track', '_track'); |
| 1128 | 561 this.addEventListener('transitionend', this._transitionend.bind(this)); |
| 1129 /** | 562 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)); |
| 1130 * Trap keyboard focus when the drawer is opened and not persistent. | 563 }); |
| 1131 */ | 564 }, |
| 1132 noFocusTrap: { | 565 detached: function() { |
| 1133 type: Boolean, | 566 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
| 1134 value: false | 567 }, |
| 1135 } | 568 open: function() { |
| 1136 }, | 569 this.opened = true; |
| 1137 | 570 }, |
| 1138 observers: [ | 571 close: function() { |
| 1139 'resetLayout(position)', | 572 this.opened = false; |
| 1140 '_resetPosition(align, isAttached)' | 573 }, |
| 1141 ], | 574 toggle: function() { |
| 1142 | 575 this.opened = !this.opened; |
| 1143 _translateOffset: 0, | 576 }, |
| 1144 | 577 getWidth: function() { |
| 1145 _trackDetails: null, | 578 return this.$.contentContainer.offsetWidth; |
| 1146 | 579 }, |
| 1147 _drawerState: 0, | 580 resetLayout: function() { |
| 1148 | 581 this.debounce('_resetLayout', function() { |
| 1149 _boundEscKeydownHandler: null, | 582 this.fire('app-drawer-reset-layout'); |
| 1150 | 583 }, 1); |
| 1151 _firstTabStop: null, | 584 }, |
| 1152 | 585 _isRTL: function() { |
| 1153 _lastTabStop: null, | 586 return window.getComputedStyle(this).direction === 'rtl'; |
| 1154 | 587 }, |
| 1155 ready: function() { | 588 _resetPosition: function() { |
| 1156 // Set the scroll direction so you can vertically scroll inside the draw
er. | 589 switch (this.align) { |
| 1157 this.setScrollDirection('y'); | 590 case 'start': |
| 1158 | 591 this._setPosition(this._isRTL() ? 'right' : 'left'); |
| 1159 // Only transition the drawer after its first render (e.g. app-drawer-la
yout | 592 return; |
| 1160 // may need to set the initial opened state which should not be transiti
oned). | 593 |
| 1161 this._setTransitionDuration('0s'); | 594 case 'end': |
| 1162 }, | 595 this._setPosition(this._isRTL() ? 'left' : 'right'); |
| 1163 | 596 return; |
| 1164 attached: function() { | 597 } |
| 1165 // Only transition the drawer after its first render (e.g. app-drawer-la
yout | 598 this._setPosition(this.align); |
| 1166 // may need to set the initial opened state which should not be transiti
oned). | 599 }, |
| 1167 Polymer.RenderStatus.afterNextRender(this, function() { | 600 _escKeydownHandler: function(event) { |
| 1168 this._setTransitionDuration(''); | 601 var ESC_KEYCODE = 27; |
| 1169 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); | 602 if (event.keyCode === ESC_KEYCODE) { |
| 1170 this._resetDrawerState(); | 603 event.preventDefault(); |
| 1171 | 604 this.close(); |
| 1172 this.listen(this, 'track', '_track'); | 605 } |
| 1173 this.addEventListener('transitionend', this._transitionend.bind(this))
; | 606 }, |
| 1174 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) | 607 _track: function(event) { |
| 1175 }); | 608 if (this.persistent) { |
| 1176 }, | 609 return; |
| 1177 | 610 } |
| 1178 detached: function() { | 611 event.preventDefault(); |
| 612 switch (event.detail.state) { |
| 613 case 'start': |
| 614 this._trackStart(event); |
| 615 break; |
| 616 |
| 617 case 'track': |
| 618 this._trackMove(event); |
| 619 break; |
| 620 |
| 621 case 'end': |
| 622 this._trackEnd(event); |
| 623 break; |
| 624 } |
| 625 }, |
| 626 _trackStart: function(event) { |
| 627 this._drawerState = this._DRAWER_STATE.TRACKING; |
| 628 this._setTransitionDuration('0s'); |
| 629 this.style.visibility = 'visible'; |
| 630 var rect = this.$.contentContainer.getBoundingClientRect(); |
| 631 if (this.position === 'left') { |
| 632 this._translateOffset = rect.left; |
| 633 } else { |
| 634 this._translateOffset = rect.right - window.innerWidth; |
| 635 } |
| 636 this._trackDetails = []; |
| 637 }, |
| 638 _trackMove: function(event) { |
| 639 this._translateDrawer(event.detail.dx + this._translateOffset); |
| 640 this._trackDetails.push({ |
| 641 dx: event.detail.dx, |
| 642 timeStamp: Date.now() |
| 643 }); |
| 644 }, |
| 645 _trackEnd: function(event) { |
| 646 var x = event.detail.dx + this._translateOffset; |
| 647 var drawerWidth = this.getWidth(); |
| 648 var isPositionLeft = this.position === 'left'; |
| 649 var isInEndState = isPositionLeft ? x >= 0 || x <= -drawerWidth : x <= 0 ||
x >= drawerWidth; |
| 650 if (!isInEndState) { |
| 651 var trackDetails = this._trackDetails; |
| 652 this._trackDetails = null; |
| 653 this._flingDrawer(event, trackDetails); |
| 654 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 655 return; |
| 656 } |
| 657 } |
| 658 var halfWidth = drawerWidth / 2; |
| 659 if (event.detail.dx < -halfWidth) { |
| 660 this.opened = this.position === 'right'; |
| 661 } else if (event.detail.dx > halfWidth) { |
| 662 this.opened = this.position === 'left'; |
| 663 } |
| 664 if (isInEndState) { |
| 665 this._resetDrawerState(); |
| 666 } |
| 667 this._setTransitionDuration(''); |
| 668 this._resetDrawerTranslate(); |
| 669 this.style.visibility = ''; |
| 670 }, |
| 671 _calculateVelocity: function(event, trackDetails) { |
| 672 var now = Date.now(); |
| 673 var timeLowerBound = now - 100; |
| 674 var trackDetail; |
| 675 var min = 0; |
| 676 var max = trackDetails.length - 1; |
| 677 while (min <= max) { |
| 678 var mid = min + max >> 1; |
| 679 var d = trackDetails[mid]; |
| 680 if (d.timeStamp >= timeLowerBound) { |
| 681 trackDetail = d; |
| 682 max = mid - 1; |
| 683 } else { |
| 684 min = mid + 1; |
| 685 } |
| 686 } |
| 687 if (trackDetail) { |
| 688 var dx = event.detail.dx - trackDetail.dx; |
| 689 var dt = now - trackDetail.timeStamp || 1; |
| 690 return dx / dt; |
| 691 } |
| 692 return 0; |
| 693 }, |
| 694 _flingDrawer: function(event, trackDetails) { |
| 695 var velocity = this._calculateVelocity(event, trackDetails); |
| 696 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { |
| 697 return; |
| 698 } |
| 699 this._drawerState = this._DRAWER_STATE.FLINGING; |
| 700 var x = event.detail.dx + this._translateOffset; |
| 701 var drawerWidth = this.getWidth(); |
| 702 var isPositionLeft = this.position === 'left'; |
| 703 var isVelocityPositive = velocity > 0; |
| 704 var isClosingLeft = !isVelocityPositive && isPositionLeft; |
| 705 var isClosingRight = isVelocityPositive && !isPositionLeft; |
| 706 var dx; |
| 707 if (isClosingLeft) { |
| 708 dx = -(x + drawerWidth); |
| 709 } else if (isClosingRight) { |
| 710 dx = drawerWidth - x; |
| 711 } else { |
| 712 dx = -x; |
| 713 } |
| 714 if (isVelocityPositive) { |
| 715 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); |
| 716 this.opened = this.position === 'left'; |
| 717 } else { |
| 718 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); |
| 719 this.opened = this.position === 'right'; |
| 720 } |
| 721 this._setTransitionDuration(this._FLING_INITIAL_SLOPE * dx / velocity + 'ms'
); |
| 722 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); |
| 723 this._resetDrawerTranslate(); |
| 724 }, |
| 725 _transitionend: function(event) { |
| 726 var target = Polymer.dom(event).rootTarget; |
| 727 if (target === this.$.contentContainer || target === this.$.scrim) { |
| 728 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 729 this._setTransitionDuration(''); |
| 730 this._setTransitionTimingFunction(''); |
| 731 this.style.visibility = ''; |
| 732 } |
| 733 this._resetDrawerState(); |
| 734 } |
| 735 }, |
| 736 _setTransitionDuration: function(duration) { |
| 737 this.$.contentContainer.style.transitionDuration = duration; |
| 738 this.$.scrim.style.transitionDuration = duration; |
| 739 }, |
| 740 _setTransitionTimingFunction: function(timingFunction) { |
| 741 this.$.contentContainer.style.transitionTimingFunction = timingFunction; |
| 742 this.$.scrim.style.transitionTimingFunction = timingFunction; |
| 743 }, |
| 744 _translateDrawer: function(x) { |
| 745 var drawerWidth = this.getWidth(); |
| 746 if (this.position === 'left') { |
| 747 x = Math.max(-drawerWidth, Math.min(x, 0)); |
| 748 this.$.scrim.style.opacity = 1 + x / drawerWidth; |
| 749 } else { |
| 750 x = Math.max(0, Math.min(x, drawerWidth)); |
| 751 this.$.scrim.style.opacity = 1 - x / drawerWidth; |
| 752 } |
| 753 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); |
| 754 }, |
| 755 _resetDrawerTranslate: function() { |
| 756 this.$.scrim.style.opacity = ''; |
| 757 this.transform('', this.$.contentContainer); |
| 758 }, |
| 759 _resetDrawerState: function() { |
| 760 var oldState = this._drawerState; |
| 761 if (this.opened) { |
| 762 this._drawerState = this.persistent ? this._DRAWER_STATE.OPENED_PERSISTENT
: this._DRAWER_STATE.OPENED; |
| 763 } else { |
| 764 this._drawerState = this._DRAWER_STATE.CLOSED; |
| 765 } |
| 766 if (oldState !== this._drawerState) { |
| 767 if (this._drawerState === this._DRAWER_STATE.OPENED) { |
| 768 this._setKeyboardFocusTrap(); |
| 769 document.addEventListener('keydown', this._boundEscKeydownHandler); |
| 770 document.body.style.overflow = 'hidden'; |
| 771 } else { |
| 1179 document.removeEventListener('keydown', this._boundEscKeydownHandler); | 772 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
| 1180 }, | 773 document.body.style.overflow = ''; |
| 1181 | 774 } |
| 1182 /** | 775 if (oldState !== this._DRAWER_STATE.INIT) { |
| 1183 * Opens the drawer. | 776 this.fire('app-drawer-transitioned'); |
| 1184 */ | 777 } |
| 1185 open: function() { | 778 } |
| 1186 this.opened = true; | 779 }, |
| 1187 }, | 780 _setKeyboardFocusTrap: function() { |
| 1188 | 781 if (this.noFocusTrap) { |
| 1189 /** | 782 return; |
| 1190 * Closes the drawer. | 783 } |
| 1191 */ | 784 var focusableElementsSelector = [ 'a[href]:not([tabindex="-1"])', 'area[href
]:not([tabindex="-1"])', 'input:not([disabled]):not([tabindex="-1"])', 'select:n
ot([disabled]):not([tabindex="-1"])', 'textarea:not([disabled]):not([tabindex="-
1"])', 'button:not([disabled]):not([tabindex="-1"])', 'iframe:not([tabindex="-1"
])', '[tabindex]:not([tabindex="-1"])', '[contentEditable=true]:not([tabindex="-
1"])' ].join(','); |
| 1192 close: function() { | 785 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElements
Selector); |
| 1193 this.opened = false; | 786 if (focusableElements.length > 0) { |
| 1194 }, | 787 this._firstTabStop = focusableElements[0]; |
| 1195 | 788 this._lastTabStop = focusableElements[focusableElements.length - 1]; |
| 1196 /** | 789 } else { |
| 1197 * Toggles the drawer open and close. | 790 this._firstTabStop = null; |
| 1198 */ | 791 this._lastTabStop = null; |
| 1199 toggle: function() { | 792 } |
| 1200 this.opened = !this.opened; | 793 var tabindex = this.getAttribute('tabindex'); |
| 1201 }, | 794 if (tabindex && parseInt(tabindex, 10) > -1) { |
| 1202 | 795 this.focus(); |
| 1203 /** | 796 } else if (this._firstTabStop) { |
| 1204 * Gets the width of the drawer. | 797 this._firstTabStop.focus(); |
| 1205 * | 798 } |
| 1206 * @return {number} The width of the drawer in pixels. | 799 }, |
| 1207 */ | 800 _tabKeydownHandler: function(event) { |
| 1208 getWidth: function() { | 801 if (this.noFocusTrap) { |
| 1209 return this.$.contentContainer.offsetWidth; | 802 return; |
| 1210 }, | 803 } |
| 1211 | 804 var TAB_KEYCODE = 9; |
| 1212 /** | 805 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB
_KEYCODE) { |
| 1213 * Resets the layout. If you changed the size of app-header via CSS | 806 if (event.shiftKey) { |
| 1214 * you can notify the changes by either firing the `iron-resize` event | 807 if (this._firstTabStop && Polymer.dom(event).localTarget === this._first
TabStop) { |
| 1215 * or calling `resetLayout` directly. | |
| 1216 * | |
| 1217 * @method resetLayout | |
| 1218 */ | |
| 1219 resetLayout: function() { | |
| 1220 this.debounce('_resetLayout', function() { | |
| 1221 this.fire('app-drawer-reset-layout'); | |
| 1222 }, 1); | |
| 1223 }, | |
| 1224 | |
| 1225 _isRTL: function() { | |
| 1226 return window.getComputedStyle(this).direction === 'rtl'; | |
| 1227 }, | |
| 1228 | |
| 1229 _resetPosition: function() { | |
| 1230 switch (this.align) { | |
| 1231 case 'start': | |
| 1232 this._setPosition(this._isRTL() ? 'right' : 'left'); | |
| 1233 return; | |
| 1234 case 'end': | |
| 1235 this._setPosition(this._isRTL() ? 'left' : 'right'); | |
| 1236 return; | |
| 1237 } | |
| 1238 this._setPosition(this.align); | |
| 1239 }, | |
| 1240 | |
| 1241 _escKeydownHandler: function(event) { | |
| 1242 var ESC_KEYCODE = 27; | |
| 1243 if (event.keyCode === ESC_KEYCODE) { | |
| 1244 // Prevent any side effects if app-drawer closes. | |
| 1245 event.preventDefault(); | 808 event.preventDefault(); |
| 1246 this.close(); | 809 this._lastTabStop.focus(); |
| 1247 } | 810 } |
| 1248 }, | 811 } else { |
| 1249 | 812 if (this._lastTabStop && Polymer.dom(event).localTarget === this._lastTa
bStop) { |
| 1250 _track: function(event) { | 813 event.preventDefault(); |
| 1251 if (this.persistent) { | |
| 1252 return; | |
| 1253 } | |
| 1254 | |
| 1255 // Disable user selection on desktop. | |
| 1256 event.preventDefault(); | |
| 1257 | |
| 1258 switch (event.detail.state) { | |
| 1259 case 'start': | |
| 1260 this._trackStart(event); | |
| 1261 break; | |
| 1262 case 'track': | |
| 1263 this._trackMove(event); | |
| 1264 break; | |
| 1265 case 'end': | |
| 1266 this._trackEnd(event); | |
| 1267 break; | |
| 1268 } | |
| 1269 }, | |
| 1270 | |
| 1271 _trackStart: function(event) { | |
| 1272 this._drawerState = this._DRAWER_STATE.TRACKING; | |
| 1273 | |
| 1274 // Disable transitions since style attributes will reflect user track ev
ents. | |
| 1275 this._setTransitionDuration('0s'); | |
| 1276 this.style.visibility = 'visible'; | |
| 1277 | |
| 1278 var rect = this.$.contentContainer.getBoundingClientRect(); | |
| 1279 if (this.position === 'left') { | |
| 1280 this._translateOffset = rect.left; | |
| 1281 } else { | |
| 1282 this._translateOffset = rect.right - window.innerWidth; | |
| 1283 } | |
| 1284 | |
| 1285 this._trackDetails = []; | |
| 1286 }, | |
| 1287 | |
| 1288 _trackMove: function(event) { | |
| 1289 this._translateDrawer(event.detail.dx + this._translateOffset); | |
| 1290 | |
| 1291 // Use Date.now() since event.timeStamp is inconsistent across browsers
(e.g. most | |
| 1292 // browsers use milliseconds but FF 44 uses microseconds). | |
| 1293 this._trackDetails.push({ | |
| 1294 dx: event.detail.dx, | |
| 1295 timeStamp: Date.now() | |
| 1296 }); | |
| 1297 }, | |
| 1298 | |
| 1299 _trackEnd: function(event) { | |
| 1300 var x = event.detail.dx + this._translateOffset; | |
| 1301 var drawerWidth = this.getWidth(); | |
| 1302 var isPositionLeft = this.position === 'left'; | |
| 1303 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : | |
| 1304 (x <= 0 || x >= drawerWidth); | |
| 1305 | |
| 1306 if (!isInEndState) { | |
| 1307 // No longer need the track events after this method returns - allow t
hem to be GC'd. | |
| 1308 var trackDetails = this._trackDetails; | |
| 1309 this._trackDetails = null; | |
| 1310 | |
| 1311 this._flingDrawer(event, trackDetails); | |
| 1312 if (this._drawerState === this._DRAWER_STATE.FLINGING) { | |
| 1313 return; | |
| 1314 } | |
| 1315 } | |
| 1316 | |
| 1317 // If the drawer is not flinging, toggle the opened state based on the p
osition of | |
| 1318 // the drawer. | |
| 1319 var halfWidth = drawerWidth / 2; | |
| 1320 if (event.detail.dx < -halfWidth) { | |
| 1321 this.opened = this.position === 'right'; | |
| 1322 } else if (event.detail.dx > halfWidth) { | |
| 1323 this.opened = this.position === 'left'; | |
| 1324 } | |
| 1325 | |
| 1326 // Trigger app-drawer-transitioned now since there will be no transition
end event. | |
| 1327 if (isInEndState) { | |
| 1328 this._resetDrawerState(); | |
| 1329 } | |
| 1330 | |
| 1331 this._setTransitionDuration(''); | |
| 1332 this._resetDrawerTranslate(); | |
| 1333 this.style.visibility = ''; | |
| 1334 }, | |
| 1335 | |
| 1336 _calculateVelocity: function(event, trackDetails) { | |
| 1337 // Find the oldest track event that is within 100ms using binary search. | |
| 1338 var now = Date.now(); | |
| 1339 var timeLowerBound = now - 100; | |
| 1340 var trackDetail; | |
| 1341 var min = 0; | |
| 1342 var max = trackDetails.length - 1; | |
| 1343 | |
| 1344 while (min <= max) { | |
| 1345 // Floor of average of min and max. | |
| 1346 var mid = (min + max) >> 1; | |
| 1347 var d = trackDetails[mid]; | |
| 1348 if (d.timeStamp >= timeLowerBound) { | |
| 1349 trackDetail = d; | |
| 1350 max = mid - 1; | |
| 1351 } else { | |
| 1352 min = mid + 1; | |
| 1353 } | |
| 1354 } | |
| 1355 | |
| 1356 if (trackDetail) { | |
| 1357 var dx = event.detail.dx - trackDetail.dx; | |
| 1358 var dt = (now - trackDetail.timeStamp) || 1; | |
| 1359 return dx / dt; | |
| 1360 } | |
| 1361 return 0; | |
| 1362 }, | |
| 1363 | |
| 1364 _flingDrawer: function(event, trackDetails) { | |
| 1365 var velocity = this._calculateVelocity(event, trackDetails); | |
| 1366 | |
| 1367 // Do not fling if velocity is not above a threshold. | |
| 1368 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { | |
| 1369 return; | |
| 1370 } | |
| 1371 | |
| 1372 this._drawerState = this._DRAWER_STATE.FLINGING; | |
| 1373 | |
| 1374 var x = event.detail.dx + this._translateOffset; | |
| 1375 var drawerWidth = this.getWidth(); | |
| 1376 var isPositionLeft = this.position === 'left'; | |
| 1377 var isVelocityPositive = velocity > 0; | |
| 1378 var isClosingLeft = !isVelocityPositive && isPositionLeft; | |
| 1379 var isClosingRight = isVelocityPositive && !isPositionLeft; | |
| 1380 var dx; | |
| 1381 if (isClosingLeft) { | |
| 1382 dx = -(x + drawerWidth); | |
| 1383 } else if (isClosingRight) { | |
| 1384 dx = (drawerWidth - x); | |
| 1385 } else { | |
| 1386 dx = -x; | |
| 1387 } | |
| 1388 | |
| 1389 // Enforce a minimum transition velocity to make the drawer feel snappy. | |
| 1390 if (isVelocityPositive) { | |
| 1391 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); | |
| 1392 this.opened = this.position === 'left'; | |
| 1393 } else { | |
| 1394 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); | |
| 1395 this.opened = this.position === 'right'; | |
| 1396 } | |
| 1397 | |
| 1398 // Calculate the amount of time needed to finish the transition based on
the | |
| 1399 // initial slope of the timing function. | |
| 1400 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity)
+ 'ms'); | |
| 1401 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); | |
| 1402 | |
| 1403 this._resetDrawerTranslate(); | |
| 1404 }, | |
| 1405 | |
| 1406 _transitionend: function(event) { | |
| 1407 // contentContainer will transition on opened state changed, and scrim w
ill | |
| 1408 // transition on persistent state changed when opened - these are the | |
| 1409 // transitions we are interested in. | |
| 1410 var target = Polymer.dom(event).rootTarget; | |
| 1411 if (target === this.$.contentContainer || target === this.$.scrim) { | |
| 1412 | |
| 1413 // If the drawer was flinging, we need to reset the style attributes. | |
| 1414 if (this._drawerState === this._DRAWER_STATE.FLINGING) { | |
| 1415 this._setTransitionDuration(''); | |
| 1416 this._setTransitionTimingFunction(''); | |
| 1417 this.style.visibility = ''; | |
| 1418 } | |
| 1419 | |
| 1420 this._resetDrawerState(); | |
| 1421 } | |
| 1422 }, | |
| 1423 | |
| 1424 _setTransitionDuration: function(duration) { | |
| 1425 this.$.contentContainer.style.transitionDuration = duration; | |
| 1426 this.$.scrim.style.transitionDuration = duration; | |
| 1427 }, | |
| 1428 | |
| 1429 _setTransitionTimingFunction: function(timingFunction) { | |
| 1430 this.$.contentContainer.style.transitionTimingFunction = timingFunction; | |
| 1431 this.$.scrim.style.transitionTimingFunction = timingFunction; | |
| 1432 }, | |
| 1433 | |
| 1434 _translateDrawer: function(x) { | |
| 1435 var drawerWidth = this.getWidth(); | |
| 1436 | |
| 1437 if (this.position === 'left') { | |
| 1438 x = Math.max(-drawerWidth, Math.min(x, 0)); | |
| 1439 this.$.scrim.style.opacity = 1 + x / drawerWidth; | |
| 1440 } else { | |
| 1441 x = Math.max(0, Math.min(x, drawerWidth)); | |
| 1442 this.$.scrim.style.opacity = 1 - x / drawerWidth; | |
| 1443 } | |
| 1444 | |
| 1445 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); | |
| 1446 }, | |
| 1447 | |
| 1448 _resetDrawerTranslate: function() { | |
| 1449 this.$.scrim.style.opacity = ''; | |
| 1450 this.transform('', this.$.contentContainer); | |
| 1451 }, | |
| 1452 | |
| 1453 _resetDrawerState: function() { | |
| 1454 var oldState = this._drawerState; | |
| 1455 if (this.opened) { | |
| 1456 this._drawerState = this.persistent ? | |
| 1457 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED; | |
| 1458 } else { | |
| 1459 this._drawerState = this._DRAWER_STATE.CLOSED; | |
| 1460 } | |
| 1461 | |
| 1462 if (oldState !== this._drawerState) { | |
| 1463 if (this._drawerState === this._DRAWER_STATE.OPENED) { | |
| 1464 this._setKeyboardFocusTrap(); | |
| 1465 document.addEventListener('keydown', this._boundEscKeydownHandler); | |
| 1466 document.body.style.overflow = 'hidden'; | |
| 1467 } else { | |
| 1468 document.removeEventListener('keydown', this._boundEscKeydownHandler
); | |
| 1469 document.body.style.overflow = ''; | |
| 1470 } | |
| 1471 | |
| 1472 // Don't fire the event on initial load. | |
| 1473 if (oldState !== this._DRAWER_STATE.INIT) { | |
| 1474 this.fire('app-drawer-transitioned'); | |
| 1475 } | |
| 1476 } | |
| 1477 }, | |
| 1478 | |
| 1479 _setKeyboardFocusTrap: function() { | |
| 1480 if (this.noFocusTrap) { | |
| 1481 return; | |
| 1482 } | |
| 1483 | |
| 1484 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated)
, this will | |
| 1485 // not select focusable elements inside shadow roots. | |
| 1486 var focusableElementsSelector = [ | |
| 1487 'a[href]:not([tabindex="-1"])', | |
| 1488 'area[href]:not([tabindex="-1"])', | |
| 1489 'input:not([disabled]):not([tabindex="-1"])', | |
| 1490 'select:not([disabled]):not([tabindex="-1"])', | |
| 1491 'textarea:not([disabled]):not([tabindex="-1"])', | |
| 1492 'button:not([disabled]):not([tabindex="-1"])', | |
| 1493 'iframe:not([tabindex="-1"])', | |
| 1494 '[tabindex]:not([tabindex="-1"])', | |
| 1495 '[contentEditable=true]:not([tabindex="-1"])' | |
| 1496 ].join(','); | |
| 1497 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem
entsSelector); | |
| 1498 | |
| 1499 if (focusableElements.length > 0) { | |
| 1500 this._firstTabStop = focusableElements[0]; | |
| 1501 this._lastTabStop = focusableElements[focusableElements.length - 1]; | |
| 1502 } else { | |
| 1503 // Reset saved tab stops when there are no focusable elements in the d
rawer. | |
| 1504 this._firstTabStop = null; | |
| 1505 this._lastTabStop = null; | |
| 1506 } | |
| 1507 | |
| 1508 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the
first focusable | |
| 1509 // element in the drawer, if it exists. Use the tabindex attribute since
the this.tabIndex | |
| 1510 // property in IE/Edge returns 0 (instead of -1) when the attribute is n
ot set. | |
| 1511 var tabindex = this.getAttribute('tabindex'); | |
| 1512 if (tabindex && parseInt(tabindex, 10) > -1) { | |
| 1513 this.focus(); | |
| 1514 } else if (this._firstTabStop) { | |
| 1515 this._firstTabStop.focus(); | 814 this._firstTabStop.focus(); |
| 1516 } | 815 } |
| 1517 }, | 816 } |
| 1518 | 817 } |
| 1519 _tabKeydownHandler: function(event) { | 818 }, |
| 1520 if (this.noFocusTrap) { | 819 _MIN_FLING_THRESHOLD: .2, |
| 1521 return; | 820 _MIN_TRANSITION_VELOCITY: 1.2, |
| 1522 } | 821 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', |
| 1523 | 822 _FLING_INITIAL_SLOPE: 1.5, |
| 1524 var TAB_KEYCODE = 9; | 823 _DRAWER_STATE: { |
| 1525 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode ===
TAB_KEYCODE) { | 824 INIT: 0, |
| 1526 if (event.shiftKey) { | 825 OPENED: 1, |
| 1527 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f
irstTabStop) { | 826 OPENED_PERSISTENT: 2, |
| 1528 event.preventDefault(); | 827 CLOSED: 3, |
| 1529 this._lastTabStop.focus(); | 828 TRACKING: 4, |
| 1530 } | 829 FLINGING: 5 |
| 1531 } else { | 830 } |
| 1532 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la
stTabStop) { | 831 }); |
| 1533 event.preventDefault(); | 832 |
| 1534 this._firstTabStop.focus(); | |
| 1535 } | |
| 1536 } | |
| 1537 } | |
| 1538 }, | |
| 1539 | |
| 1540 _MIN_FLING_THRESHOLD: 0.2, | |
| 1541 | |
| 1542 _MIN_TRANSITION_VELOCITY: 1.2, | |
| 1543 | |
| 1544 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', | |
| 1545 | |
| 1546 _FLING_INITIAL_SLOPE: 1.5, | |
| 1547 | |
| 1548 _DRAWER_STATE: { | |
| 1549 INIT: 0, | |
| 1550 OPENED: 1, | |
| 1551 OPENED_PERSISTENT: 2, | |
| 1552 CLOSED: 3, | |
| 1553 TRACKING: 4, | |
| 1554 FLINGING: 5 | |
| 1555 } | |
| 1556 | |
| 1557 /** | |
| 1558 * Fired when the layout of app-drawer has changed. | |
| 1559 * | |
| 1560 * @event app-drawer-reset-layout | |
| 1561 */ | |
| 1562 | |
| 1563 /** | |
| 1564 * Fired when app-drawer has finished transitioning. | |
| 1565 * | |
| 1566 * @event app-drawer-transitioned | |
| 1567 */ | |
| 1568 }); | |
| 1569 (function() { | 833 (function() { |
| 1570 'use strict'; | 834 'use strict'; |
| 1571 | |
| 1572 Polymer({ | |
| 1573 is: 'iron-location', | |
| 1574 properties: { | |
| 1575 /** | |
| 1576 * The pathname component of the URL. | |
| 1577 */ | |
| 1578 path: { | |
| 1579 type: String, | |
| 1580 notify: true, | |
| 1581 value: function() { | |
| 1582 return window.decodeURIComponent(window.location.pathname); | |
| 1583 } | |
| 1584 }, | |
| 1585 /** | |
| 1586 * The query string portion of the URL. | |
| 1587 */ | |
| 1588 query: { | |
| 1589 type: String, | |
| 1590 notify: true, | |
| 1591 value: function() { | |
| 1592 return window.decodeURIComponent(window.location.search.slice(1)); | |
| 1593 } | |
| 1594 }, | |
| 1595 /** | |
| 1596 * The hash component of the URL. | |
| 1597 */ | |
| 1598 hash: { | |
| 1599 type: String, | |
| 1600 notify: true, | |
| 1601 value: function() { | |
| 1602 return window.decodeURIComponent(window.location.hash.slice(1)); | |
| 1603 } | |
| 1604 }, | |
| 1605 /** | |
| 1606 * If the user was on a URL for less than `dwellTime` milliseconds, it | |
| 1607 * won't be added to the browser's history, but instead will be replaced | |
| 1608 * by the next entry. | |
| 1609 * | |
| 1610 * This is to prevent large numbers of entries from clogging up the user
's | |
| 1611 * browser history. Disable by setting to a negative number. | |
| 1612 */ | |
| 1613 dwellTime: { | |
| 1614 type: Number, | |
| 1615 value: 2000 | |
| 1616 }, | |
| 1617 | |
| 1618 /** | |
| 1619 * A regexp that defines the set of URLs that should be considered part | |
| 1620 * of this web app. | |
| 1621 * | |
| 1622 * Clicking on a link that matches this regex won't result in a full pag
e | |
| 1623 * navigation, but will instead just update the URL state in place. | |
| 1624 * | |
| 1625 * This regexp is given everything after the origin in an absolute | |
| 1626 * URL. So to match just URLs that start with /search/ do: | |
| 1627 * url-space-regex="^/search/" | |
| 1628 * | |
| 1629 * @type {string|RegExp} | |
| 1630 */ | |
| 1631 urlSpaceRegex: { | |
| 1632 type: String, | |
| 1633 value: '' | |
| 1634 }, | |
| 1635 | |
| 1636 /** | |
| 1637 * urlSpaceRegex, but coerced into a regexp. | |
| 1638 * | |
| 1639 * @type {RegExp} | |
| 1640 */ | |
| 1641 _urlSpaceRegExp: { | |
| 1642 computed: '_makeRegExp(urlSpaceRegex)' | |
| 1643 }, | |
| 1644 | |
| 1645 _lastChangedAt: { | |
| 1646 type: Number | |
| 1647 }, | |
| 1648 | |
| 1649 _initialized: { | |
| 1650 type: Boolean, | |
| 1651 value: false | |
| 1652 } | |
| 1653 }, | |
| 1654 hostAttributes: { | |
| 1655 hidden: true | |
| 1656 }, | |
| 1657 observers: [ | |
| 1658 '_updateUrl(path, query, hash)' | |
| 1659 ], | |
| 1660 attached: function() { | |
| 1661 this.listen(window, 'hashchange', '_hashChanged'); | |
| 1662 this.listen(window, 'location-changed', '_urlChanged'); | |
| 1663 this.listen(window, 'popstate', '_urlChanged'); | |
| 1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g
lobalOnClick'); | |
| 1665 // Give a 200ms grace period to make initial redirects without any | |
| 1666 // additions to the user's history. | |
| 1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); | |
| 1668 | |
| 1669 this._initialized = true; | |
| 1670 this._urlChanged(); | |
| 1671 }, | |
| 1672 detached: function() { | |
| 1673 this.unlisten(window, 'hashchange', '_hashChanged'); | |
| 1674 this.unlisten(window, 'location-changed', '_urlChanged'); | |
| 1675 this.unlisten(window, 'popstate', '_urlChanged'); | |
| 1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '
_globalOnClick'); | |
| 1677 this._initialized = false; | |
| 1678 }, | |
| 1679 _hashChanged: function() { | |
| 1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1))
; | |
| 1681 }, | |
| 1682 _urlChanged: function() { | |
| 1683 // We want to extract all info out of the updated URL before we | |
| 1684 // try to write anything back into it. | |
| 1685 // | |
| 1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old | |
| 1687 // one when we set this.hash. Likewise for query. | |
| 1688 this._dontUpdateUrl = true; | |
| 1689 this._hashChanged(); | |
| 1690 this.path = window.decodeURIComponent(window.location.pathname); | |
| 1691 this.query = window.decodeURIComponent( | |
| 1692 window.location.search.substring(1)); | |
| 1693 this._dontUpdateUrl = false; | |
| 1694 this._updateUrl(); | |
| 1695 }, | |
| 1696 _getUrl: function() { | |
| 1697 var partiallyEncodedPath = window.encodeURI( | |
| 1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F'); | |
| 1699 var partiallyEncodedQuery = ''; | |
| 1700 if (this.query) { | |
| 1701 partiallyEncodedQuery = '?' + window.encodeURI( | |
| 1702 this.query).replace(/\#/g, '%23'); | |
| 1703 } | |
| 1704 var partiallyEncodedHash = ''; | |
| 1705 if (this.hash) { | |
| 1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash); | |
| 1707 } | |
| 1708 return ( | |
| 1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash)
; | |
| 1710 }, | |
| 1711 _updateUrl: function() { | |
| 1712 if (this._dontUpdateUrl || !this._initialized) { | |
| 1713 return; | |
| 1714 } | |
| 1715 if (this.path === window.decodeURIComponent(window.location.pathname) && | |
| 1716 this.query === window.decodeURIComponent( | |
| 1717 window.location.search.substring(1)) && | |
| 1718 this.hash === window.decodeURIComponent( | |
| 1719 window.location.hash.substring(1))) { | |
| 1720 // Nothing to do, the current URL is a representation of our propertie
s. | |
| 1721 return; | |
| 1722 } | |
| 1723 var newUrl = this._getUrl(); | |
| 1724 // Need to use a full URL in case the containing page has a base URI. | |
| 1725 var fullNewUrl = new URL( | |
| 1726 newUrl, window.location.protocol + '//' + window.location.host).href
; | |
| 1727 var now = window.performance.now(); | |
| 1728 var shouldReplace = | |
| 1729 this._lastChangedAt + this.dwellTime > now; | |
| 1730 this._lastChangedAt = now; | |
| 1731 if (shouldReplace) { | |
| 1732 window.history.replaceState({}, '', fullNewUrl); | |
| 1733 } else { | |
| 1734 window.history.pushState({}, '', fullNewUrl); | |
| 1735 } | |
| 1736 this.fire('location-changed', {}, {node: window}); | |
| 1737 }, | |
| 1738 /** | |
| 1739 * A necessary evil so that links work as expected. Does its best to | |
| 1740 * bail out early if possible. | |
| 1741 * | |
| 1742 * @param {MouseEvent} event . | |
| 1743 */ | |
| 1744 _globalOnClick: function(event) { | |
| 1745 // If another event handler has stopped this event then there's nothing | |
| 1746 // for us to do. This can happen e.g. when there are multiple | |
| 1747 // iron-location elements in a page. | |
| 1748 if (event.defaultPrevented) { | |
| 1749 return; | |
| 1750 } | |
| 1751 var href = this._getSameOriginLinkHref(event); | |
| 1752 if (!href) { | |
| 1753 return; | |
| 1754 } | |
| 1755 event.preventDefault(); | |
| 1756 // If the navigation is to the current page we shouldn't add a history | |
| 1757 // entry or fire a change event. | |
| 1758 if (href === window.location.href) { | |
| 1759 return; | |
| 1760 } | |
| 1761 window.history.pushState({}, '', href); | |
| 1762 this.fire('location-changed', {}, {node: window}); | |
| 1763 }, | |
| 1764 /** | |
| 1765 * Returns the absolute URL of the link (if any) that this click event | |
| 1766 * is clicking on, if we can and should override the resulting full | |
| 1767 * page navigation. Returns null otherwise. | |
| 1768 * | |
| 1769 * @param {MouseEvent} event . | |
| 1770 * @return {string?} . | |
| 1771 */ | |
| 1772 _getSameOriginLinkHref: function(event) { | |
| 1773 // We only care about left-clicks. | |
| 1774 if (event.button !== 0) { | |
| 1775 return null; | |
| 1776 } | |
| 1777 // We don't want modified clicks, where the intent is to open the page | |
| 1778 // in a new tab. | |
| 1779 if (event.metaKey || event.ctrlKey) { | |
| 1780 return null; | |
| 1781 } | |
| 1782 var eventPath = Polymer.dom(event).path; | |
| 1783 var anchor = null; | |
| 1784 for (var i = 0; i < eventPath.length; i++) { | |
| 1785 var element = eventPath[i]; | |
| 1786 if (element.tagName === 'A' && element.href) { | |
| 1787 anchor = element; | |
| 1788 break; | |
| 1789 } | |
| 1790 } | |
| 1791 | |
| 1792 // If there's no link there's nothing to do. | |
| 1793 if (!anchor) { | |
| 1794 return null; | |
| 1795 } | |
| 1796 | |
| 1797 // Target blank is a new tab, don't intercept. | |
| 1798 if (anchor.target === '_blank') { | |
| 1799 return null; | |
| 1800 } | |
| 1801 // If the link is for an existing parent frame, don't intercept. | |
| 1802 if ((anchor.target === '_top' || | |
| 1803 anchor.target === '_parent') && | |
| 1804 window.top !== window) { | |
| 1805 return null; | |
| 1806 } | |
| 1807 | |
| 1808 var href = anchor.href; | |
| 1809 | |
| 1810 // It only makes sense for us to intercept same-origin navigations. | |
| 1811 // pushState/replaceState don't work with cross-origin links. | |
| 1812 var url; | |
| 1813 if (document.baseURI != null) { | |
| 1814 url = new URL(href, /** @type {string} */(document.baseURI)); | |
| 1815 } else { | |
| 1816 url = new URL(href); | |
| 1817 } | |
| 1818 | |
| 1819 var origin; | |
| 1820 | |
| 1821 // IE Polyfill | |
| 1822 if (window.location.origin) { | |
| 1823 origin = window.location.origin; | |
| 1824 } else { | |
| 1825 origin = window.location.protocol + '//' + window.location.hostname; | |
| 1826 | |
| 1827 if (window.location.port) { | |
| 1828 origin += ':' + window.location.port; | |
| 1829 } | |
| 1830 } | |
| 1831 | |
| 1832 if (url.origin !== origin) { | |
| 1833 return null; | |
| 1834 } | |
| 1835 var normalizedHref = url.pathname + url.search + url.hash; | |
| 1836 | |
| 1837 // If we've been configured not to handle this url... don't handle it! | |
| 1838 if (this._urlSpaceRegExp && | |
| 1839 !this._urlSpaceRegExp.test(normalizedHref)) { | |
| 1840 return null; | |
| 1841 } | |
| 1842 // Need to use a full URL in case the containing page has a base URI. | |
| 1843 var fullNormalizedHref = new URL( | |
| 1844 normalizedHref, window.location.href).href; | |
| 1845 return fullNormalizedHref; | |
| 1846 }, | |
| 1847 _makeRegExp: function(urlSpaceRegex) { | |
| 1848 return RegExp(urlSpaceRegex); | |
| 1849 } | |
| 1850 }); | |
| 1851 })(); | |
| 1852 'use strict'; | |
| 1853 | |
| 1854 Polymer({ | 835 Polymer({ |
| 1855 is: 'iron-query-params', | 836 is: 'iron-location', |
| 1856 properties: { | 837 properties: { |
| 1857 paramsString: { | 838 path: { |
| 1858 type: String, | 839 type: String, |
| 1859 notify: true, | 840 notify: true, |
| 1860 observer: 'paramsStringChanged', | 841 value: function() { |
| 1861 }, | 842 return window.decodeURIComponent(window.location.pathname); |
| 1862 paramsObject: { | 843 } |
| 1863 type: Object, | 844 }, |
| 845 query: { |
| 846 type: String, |
| 1864 notify: true, | 847 notify: true, |
| 1865 value: function() { | 848 value: function() { |
| 1866 return {}; | 849 return window.decodeURIComponent(window.location.search.slice(1)); |
| 1867 } | 850 } |
| 1868 }, | 851 }, |
| 1869 _dontReact: { | 852 hash: { |
| 853 type: String, |
| 854 notify: true, |
| 855 value: function() { |
| 856 return window.decodeURIComponent(window.location.hash.slice(1)); |
| 857 } |
| 858 }, |
| 859 dwellTime: { |
| 860 type: Number, |
| 861 value: 2e3 |
| 862 }, |
| 863 urlSpaceRegex: { |
| 864 type: String, |
| 865 value: '' |
| 866 }, |
| 867 _urlSpaceRegExp: { |
| 868 computed: '_makeRegExp(urlSpaceRegex)' |
| 869 }, |
| 870 _lastChangedAt: { |
| 871 type: Number |
| 872 }, |
| 873 _initialized: { |
| 1870 type: Boolean, | 874 type: Boolean, |
| 1871 value: false | 875 value: false |
| 1872 } | 876 } |
| 1873 }, | 877 }, |
| 1874 hostAttributes: { | 878 hostAttributes: { |
| 1875 hidden: true | 879 hidden: true |
| 1876 }, | 880 }, |
| 1877 observers: [ | 881 observers: [ '_updateUrl(path, query, hash)' ], |
| 1878 'paramsObjectChanged(paramsObject.*)' | 882 attached: function() { |
| 1879 ], | 883 this.listen(window, 'hashchange', '_hashChanged'); |
| 1880 paramsStringChanged: function() { | 884 this.listen(window, 'location-changed', '_urlChanged'); |
| 1881 this._dontReact = true; | 885 this.listen(window, 'popstate', '_urlChanged'); |
| 1882 this.paramsObject = this._decodeParams(this.paramsString); | 886 this.listen(document.body, 'click', '_globalOnClick'); |
| 1883 this._dontReact = false; | 887 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); |
| 1884 }, | 888 this._initialized = true; |
| 1885 paramsObjectChanged: function() { | 889 this._urlChanged(); |
| 1886 if (this._dontReact) { | 890 }, |
| 891 detached: function() { |
| 892 this.unlisten(window, 'hashchange', '_hashChanged'); |
| 893 this.unlisten(window, 'location-changed', '_urlChanged'); |
| 894 this.unlisten(window, 'popstate', '_urlChanged'); |
| 895 this.unlisten(document.body, 'click', '_globalOnClick'); |
| 896 this._initialized = false; |
| 897 }, |
| 898 _hashChanged: function() { |
| 899 this.hash = window.decodeURIComponent(window.location.hash.substring(1)); |
| 900 }, |
| 901 _urlChanged: function() { |
| 902 this._dontUpdateUrl = true; |
| 903 this._hashChanged(); |
| 904 this.path = window.decodeURIComponent(window.location.pathname); |
| 905 this.query = window.decodeURIComponent(window.location.search.substring(1)
); |
| 906 this._dontUpdateUrl = false; |
| 907 this._updateUrl(); |
| 908 }, |
| 909 _getUrl: function() { |
| 910 var partiallyEncodedPath = window.encodeURI(this.path).replace(/\#/g, '%23
').replace(/\?/g, '%3F'); |
| 911 var partiallyEncodedQuery = ''; |
| 912 if (this.query) { |
| 913 partiallyEncodedQuery = '?' + window.encodeURI(this.query).replace(/\#/g
, '%23'); |
| 914 } |
| 915 var partiallyEncodedHash = ''; |
| 916 if (this.hash) { |
| 917 partiallyEncodedHash = '#' + window.encodeURI(this.hash); |
| 918 } |
| 919 return partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash
; |
| 920 }, |
| 921 _updateUrl: function() { |
| 922 if (this._dontUpdateUrl || !this._initialized) { |
| 1887 return; | 923 return; |
| 1888 } | 924 } |
| 1889 this.paramsString = this._encodeParams(this.paramsObject); | 925 if (this.path === window.decodeURIComponent(window.location.pathname) && t
his.query === window.decodeURIComponent(window.location.search.substring(1)) &&
this.hash === window.decodeURIComponent(window.location.hash.substring(1))) { |
| 1890 }, | 926 return; |
| 1891 _encodeParams: function(params) { | 927 } |
| 1892 var encodedParams = []; | 928 var newUrl = this._getUrl(); |
| 1893 for (var key in params) { | 929 var fullNewUrl = new URL(newUrl, window.location.protocol + '//' + window.
location.host).href; |
| 1894 var value = params[key]; | 930 var now = window.performance.now(); |
| 1895 if (value === '') { | 931 var shouldReplace = this._lastChangedAt + this.dwellTime > now; |
| 1896 encodedParams.push(encodeURIComponent(key)); | 932 this._lastChangedAt = now; |
| 1897 } else if (value) { | 933 if (shouldReplace) { |
| 1898 encodedParams.push( | 934 window.history.replaceState({}, '', fullNewUrl); |
| 1899 encodeURIComponent(key) + | 935 } else { |
| 1900 '=' + | 936 window.history.pushState({}, '', fullNewUrl); |
| 1901 encodeURIComponent(value.toString()) | 937 } |
| 1902 ); | 938 this.fire('location-changed', {}, { |
| 939 node: window |
| 940 }); |
| 941 }, |
| 942 _globalOnClick: function(event) { |
| 943 if (event.defaultPrevented) { |
| 944 return; |
| 945 } |
| 946 var href = this._getSameOriginLinkHref(event); |
| 947 if (!href) { |
| 948 return; |
| 949 } |
| 950 event.preventDefault(); |
| 951 if (href === window.location.href) { |
| 952 return; |
| 953 } |
| 954 window.history.pushState({}, '', href); |
| 955 this.fire('location-changed', {}, { |
| 956 node: window |
| 957 }); |
| 958 }, |
| 959 _getSameOriginLinkHref: function(event) { |
| 960 if (event.button !== 0) { |
| 961 return null; |
| 962 } |
| 963 if (event.metaKey || event.ctrlKey) { |
| 964 return null; |
| 965 } |
| 966 var eventPath = Polymer.dom(event).path; |
| 967 var anchor = null; |
| 968 for (var i = 0; i < eventPath.length; i++) { |
| 969 var element = eventPath[i]; |
| 970 if (element.tagName === 'A' && element.href) { |
| 971 anchor = element; |
| 972 break; |
| 1903 } | 973 } |
| 1904 } | 974 } |
| 1905 return encodedParams.join('&'); | 975 if (!anchor) { |
| 1906 }, | 976 return null; |
| 1907 _decodeParams: function(paramString) { | 977 } |
| 1908 var params = {}; | 978 if (anchor.target === '_blank') { |
| 1909 | 979 return null; |
| 1910 // Work around a bug in decodeURIComponent where + is not | 980 } |
| 1911 // converted to spaces: | 981 if ((anchor.target === '_top' || anchor.target === '_parent') && window.to
p !== window) { |
| 1912 paramString = (paramString || '').replace(/\+/g, '%20'); | 982 return null; |
| 1913 | 983 } |
| 1914 var paramList = paramString.split('&'); | 984 var href = anchor.href; |
| 1915 for (var i = 0; i < paramList.length; i++) { | 985 var url; |
| 1916 var param = paramList[i].split('='); | 986 if (document.baseURI != null) { |
| 1917 if (param[0]) { | 987 url = new URL(href, document.baseURI); |
| 1918 params[decodeURIComponent(param[0])] = | 988 } else { |
| 1919 decodeURIComponent(param[1] || ''); | 989 url = new URL(href); |
| 990 } |
| 991 var origin; |
| 992 if (window.location.origin) { |
| 993 origin = window.location.origin; |
| 994 } else { |
| 995 origin = window.location.protocol + '//' + window.location.hostname; |
| 996 if (window.location.port) { |
| 997 origin += ':' + window.location.port; |
| 1920 } | 998 } |
| 1921 } | 999 } |
| 1922 return params; | 1000 if (url.origin !== origin) { |
| 1001 return null; |
| 1002 } |
| 1003 var normalizedHref = url.pathname + url.search + url.hash; |
| 1004 if (this._urlSpaceRegExp && !this._urlSpaceRegExp.test(normalizedHref)) { |
| 1005 return null; |
| 1006 } |
| 1007 var fullNormalizedHref = new URL(normalizedHref, window.location.href).hre
f; |
| 1008 return fullNormalizedHref; |
| 1009 }, |
| 1010 _makeRegExp: function(urlSpaceRegex) { |
| 1011 return RegExp(urlSpaceRegex); |
| 1923 } | 1012 } |
| 1924 }); | 1013 }); |
| 1014 })(); |
| 1015 |
| 1925 'use strict'; | 1016 'use strict'; |
| 1926 | 1017 |
| 1927 /** | 1018 Polymer({ |
| 1928 * Provides bidirectional mapping between `path` and `queryParams` and a | 1019 is: 'iron-query-params', |
| 1929 * app-route compatible `route` object. | 1020 properties: { |
| 1930 * | 1021 paramsString: { |
| 1931 * For more information, see the docs for `app-route-converter`. | 1022 type: String, |
| 1932 * | 1023 notify: true, |
| 1933 * @polymerBehavior | 1024 observer: 'paramsStringChanged' |
| 1934 */ | 1025 }, |
| 1935 Polymer.AppRouteConverterBehavior = { | 1026 paramsObject: { |
| 1027 type: Object, |
| 1028 notify: true, |
| 1029 value: function() { |
| 1030 return {}; |
| 1031 } |
| 1032 }, |
| 1033 _dontReact: { |
| 1034 type: Boolean, |
| 1035 value: false |
| 1036 } |
| 1037 }, |
| 1038 hostAttributes: { |
| 1039 hidden: true |
| 1040 }, |
| 1041 observers: [ 'paramsObjectChanged(paramsObject.*)' ], |
| 1042 paramsStringChanged: function() { |
| 1043 this._dontReact = true; |
| 1044 this.paramsObject = this._decodeParams(this.paramsString); |
| 1045 this._dontReact = false; |
| 1046 }, |
| 1047 paramsObjectChanged: function() { |
| 1048 if (this._dontReact) { |
| 1049 return; |
| 1050 } |
| 1051 this.paramsString = this._encodeParams(this.paramsObject); |
| 1052 }, |
| 1053 _encodeParams: function(params) { |
| 1054 var encodedParams = []; |
| 1055 for (var key in params) { |
| 1056 var value = params[key]; |
| 1057 if (value === '') { |
| 1058 encodedParams.push(encodeURIComponent(key)); |
| 1059 } else if (value) { |
| 1060 encodedParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(va
lue.toString())); |
| 1061 } |
| 1062 } |
| 1063 return encodedParams.join('&'); |
| 1064 }, |
| 1065 _decodeParams: function(paramString) { |
| 1066 var params = {}; |
| 1067 paramString = (paramString || '').replace(/\+/g, '%20'); |
| 1068 var paramList = paramString.split('&'); |
| 1069 for (var i = 0; i < paramList.length; i++) { |
| 1070 var param = paramList[i].split('='); |
| 1071 if (param[0]) { |
| 1072 params[decodeURIComponent(param[0])] = decodeURIComponent(param[1] || ''
); |
| 1073 } |
| 1074 } |
| 1075 return params; |
| 1076 } |
| 1077 }); |
| 1078 |
| 1079 'use strict'; |
| 1080 |
| 1081 Polymer.AppRouteConverterBehavior = { |
| 1082 properties: { |
| 1083 route: { |
| 1084 type: Object, |
| 1085 notify: true |
| 1086 }, |
| 1087 queryParams: { |
| 1088 type: Object, |
| 1089 notify: true |
| 1090 }, |
| 1091 path: { |
| 1092 type: String, |
| 1093 notify: true |
| 1094 } |
| 1095 }, |
| 1096 observers: [ '_locationChanged(path, queryParams)', '_routeChanged(route.prefi
x, route.path)', '_routeQueryParamsChanged(route.__queryParams)' ], |
| 1097 created: function() { |
| 1098 this.linkPaths('route.__queryParams', 'queryParams'); |
| 1099 this.linkPaths('queryParams', 'route.__queryParams'); |
| 1100 }, |
| 1101 _locationChanged: function() { |
| 1102 if (this.route && this.route.path === this.path && this.queryParams === this
.route.__queryParams) { |
| 1103 return; |
| 1104 } |
| 1105 this.route = { |
| 1106 prefix: '', |
| 1107 path: this.path, |
| 1108 __queryParams: this.queryParams |
| 1109 }; |
| 1110 }, |
| 1111 _routeChanged: function() { |
| 1112 if (!this.route) { |
| 1113 return; |
| 1114 } |
| 1115 this.path = this.route.prefix + this.route.path; |
| 1116 }, |
| 1117 _routeQueryParamsChanged: function(queryParams) { |
| 1118 if (!this.route) { |
| 1119 return; |
| 1120 } |
| 1121 this.queryParams = queryParams; |
| 1122 } |
| 1123 }; |
| 1124 |
| 1125 'use strict'; |
| 1126 |
| 1127 Polymer({ |
| 1128 is: 'app-location', |
| 1129 properties: { |
| 1130 route: { |
| 1131 type: Object, |
| 1132 notify: true |
| 1133 }, |
| 1134 useHashAsPath: { |
| 1135 type: Boolean, |
| 1136 value: false |
| 1137 }, |
| 1138 urlSpaceRegex: { |
| 1139 type: String, |
| 1140 notify: true |
| 1141 }, |
| 1142 __queryParams: { |
| 1143 type: Object |
| 1144 }, |
| 1145 __path: { |
| 1146 type: String |
| 1147 }, |
| 1148 __query: { |
| 1149 type: String |
| 1150 }, |
| 1151 __hash: { |
| 1152 type: String |
| 1153 }, |
| 1154 path: { |
| 1155 type: String, |
| 1156 observer: '__onPathChanged' |
| 1157 } |
| 1158 }, |
| 1159 behaviors: [ Polymer.AppRouteConverterBehavior ], |
| 1160 observers: [ '__computeRoutePath(useHashAsPath, __hash, __path)' ], |
| 1161 __computeRoutePath: function() { |
| 1162 this.path = this.useHashAsPath ? this.__hash : this.__path; |
| 1163 }, |
| 1164 __onPathChanged: function() { |
| 1165 if (!this._readied) { |
| 1166 return; |
| 1167 } |
| 1168 if (this.useHashAsPath) { |
| 1169 this.__hash = this.path; |
| 1170 } else { |
| 1171 this.__path = this.path; |
| 1172 } |
| 1173 } |
| 1174 }); |
| 1175 |
| 1176 'use strict'; |
| 1177 |
| 1178 Polymer({ |
| 1179 is: 'app-route', |
| 1180 properties: { |
| 1181 route: { |
| 1182 type: Object, |
| 1183 notify: true |
| 1184 }, |
| 1185 pattern: { |
| 1186 type: String |
| 1187 }, |
| 1188 data: { |
| 1189 type: Object, |
| 1190 value: function() { |
| 1191 return {}; |
| 1192 }, |
| 1193 notify: true |
| 1194 }, |
| 1195 queryParams: { |
| 1196 type: Object, |
| 1197 value: function() { |
| 1198 return {}; |
| 1199 }, |
| 1200 notify: true |
| 1201 }, |
| 1202 tail: { |
| 1203 type: Object, |
| 1204 value: function() { |
| 1205 return { |
| 1206 path: null, |
| 1207 prefix: null, |
| 1208 __queryParams: null |
| 1209 }; |
| 1210 }, |
| 1211 notify: true |
| 1212 }, |
| 1213 active: { |
| 1214 type: Boolean, |
| 1215 notify: true, |
| 1216 readOnly: true |
| 1217 }, |
| 1218 _queryParamsUpdating: { |
| 1219 type: Boolean, |
| 1220 value: false |
| 1221 }, |
| 1222 _matched: { |
| 1223 type: String, |
| 1224 value: '' |
| 1225 } |
| 1226 }, |
| 1227 observers: [ '__tryToMatch(route.path, pattern)', '__updatePathOnDataChange(da
ta.*)', '__tailPathChanged(tail.path)', '__routeQueryParamsChanged(route.__query
Params)', '__tailQueryParamsChanged(tail.__queryParams)', '__queryParamsChanged(
queryParams.*)' ], |
| 1228 created: function() { |
| 1229 this.linkPaths('route.__queryParams', 'tail.__queryParams'); |
| 1230 this.linkPaths('tail.__queryParams', 'route.__queryParams'); |
| 1231 }, |
| 1232 __routeQueryParamsChanged: function(queryParams) { |
| 1233 if (queryParams && this.tail) { |
| 1234 this.set('tail.__queryParams', queryParams); |
| 1235 if (!this.active || this._queryParamsUpdating) { |
| 1236 return; |
| 1237 } |
| 1238 var copyOfQueryParams = {}; |
| 1239 var anythingChanged = false; |
| 1240 for (var key in queryParams) { |
| 1241 copyOfQueryParams[key] = queryParams[key]; |
| 1242 if (anythingChanged || !this.queryParams || queryParams[key] !== this.qu
eryParams[key]) { |
| 1243 anythingChanged = true; |
| 1244 } |
| 1245 } |
| 1246 for (var key in this.queryParams) { |
| 1247 if (anythingChanged || !(key in queryParams)) { |
| 1248 anythingChanged = true; |
| 1249 break; |
| 1250 } |
| 1251 } |
| 1252 if (!anythingChanged) { |
| 1253 return; |
| 1254 } |
| 1255 this._queryParamsUpdating = true; |
| 1256 this.set('queryParams', copyOfQueryParams); |
| 1257 this._queryParamsUpdating = false; |
| 1258 } |
| 1259 }, |
| 1260 __tailQueryParamsChanged: function(queryParams) { |
| 1261 if (queryParams && this.route) { |
| 1262 this.set('route.__queryParams', queryParams); |
| 1263 } |
| 1264 }, |
| 1265 __queryParamsChanged: function(changes) { |
| 1266 if (!this.active || this._queryParamsUpdating) { |
| 1267 return; |
| 1268 } |
| 1269 this.set('route.__' + changes.path, changes.value); |
| 1270 }, |
| 1271 __resetProperties: function() { |
| 1272 this._setActive(false); |
| 1273 this._matched = null; |
| 1274 }, |
| 1275 __tryToMatch: function() { |
| 1276 if (!this.route) { |
| 1277 return; |
| 1278 } |
| 1279 var path = this.route.path; |
| 1280 var pattern = this.pattern; |
| 1281 if (!pattern) { |
| 1282 return; |
| 1283 } |
| 1284 if (!path) { |
| 1285 this.__resetProperties(); |
| 1286 return; |
| 1287 } |
| 1288 var remainingPieces = path.split('/'); |
| 1289 var patternPieces = pattern.split('/'); |
| 1290 var matched = []; |
| 1291 var namedMatches = {}; |
| 1292 for (var i = 0; i < patternPieces.length; i++) { |
| 1293 var patternPiece = patternPieces[i]; |
| 1294 if (!patternPiece && patternPiece !== '') { |
| 1295 break; |
| 1296 } |
| 1297 var pathPiece = remainingPieces.shift(); |
| 1298 if (!pathPiece && pathPiece !== '') { |
| 1299 this.__resetProperties(); |
| 1300 return; |
| 1301 } |
| 1302 matched.push(pathPiece); |
| 1303 if (patternPiece.charAt(0) == ':') { |
| 1304 namedMatches[patternPiece.slice(1)] = pathPiece; |
| 1305 } else if (patternPiece !== pathPiece) { |
| 1306 this.__resetProperties(); |
| 1307 return; |
| 1308 } |
| 1309 } |
| 1310 this._matched = matched.join('/'); |
| 1311 var propertyUpdates = {}; |
| 1312 if (!this.active) { |
| 1313 propertyUpdates.active = true; |
| 1314 } |
| 1315 var tailPrefix = this.route.prefix + this._matched; |
| 1316 var tailPath = remainingPieces.join('/'); |
| 1317 if (remainingPieces.length > 0) { |
| 1318 tailPath = '/' + tailPath; |
| 1319 } |
| 1320 if (!this.tail || this.tail.prefix !== tailPrefix || this.tail.path !== tail
Path) { |
| 1321 propertyUpdates.tail = { |
| 1322 prefix: tailPrefix, |
| 1323 path: tailPath, |
| 1324 __queryParams: this.route.__queryParams |
| 1325 }; |
| 1326 } |
| 1327 propertyUpdates.data = namedMatches; |
| 1328 this._dataInUrl = {}; |
| 1329 for (var key in namedMatches) { |
| 1330 this._dataInUrl[key] = namedMatches[key]; |
| 1331 } |
| 1332 this.__setMulti(propertyUpdates); |
| 1333 }, |
| 1334 __tailPathChanged: function() { |
| 1335 if (!this.active) { |
| 1336 return; |
| 1337 } |
| 1338 var tailPath = this.tail.path; |
| 1339 var newPath = this._matched; |
| 1340 if (tailPath) { |
| 1341 if (tailPath.charAt(0) !== '/') { |
| 1342 tailPath = '/' + tailPath; |
| 1343 } |
| 1344 newPath += tailPath; |
| 1345 } |
| 1346 this.set('route.path', newPath); |
| 1347 }, |
| 1348 __updatePathOnDataChange: function() { |
| 1349 if (!this.route || !this.active) { |
| 1350 return; |
| 1351 } |
| 1352 var newPath = this.__getLink({}); |
| 1353 var oldPath = this.__getLink(this._dataInUrl); |
| 1354 if (newPath === oldPath) { |
| 1355 return; |
| 1356 } |
| 1357 this.set('route.path', newPath); |
| 1358 }, |
| 1359 __getLink: function(overrideValues) { |
| 1360 var values = { |
| 1361 tail: null |
| 1362 }; |
| 1363 for (var key in this.data) { |
| 1364 values[key] = this.data[key]; |
| 1365 } |
| 1366 for (var key in overrideValues) { |
| 1367 values[key] = overrideValues[key]; |
| 1368 } |
| 1369 var patternPieces = this.pattern.split('/'); |
| 1370 var interp = patternPieces.map(function(value) { |
| 1371 if (value[0] == ':') { |
| 1372 value = values[value.slice(1)]; |
| 1373 } |
| 1374 return value; |
| 1375 }, this); |
| 1376 if (values.tail && values.tail.path) { |
| 1377 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { |
| 1378 interp.push(values.tail.path.slice(1)); |
| 1379 } else { |
| 1380 interp.push(values.tail.path); |
| 1381 } |
| 1382 } |
| 1383 return interp.join('/'); |
| 1384 }, |
| 1385 __setMulti: function(setObj) { |
| 1386 for (var property in setObj) { |
| 1387 this._propertySetter(property, setObj[property]); |
| 1388 } |
| 1389 for (var property in setObj) { |
| 1390 this._pathEffector(property, this[property]); |
| 1391 this._notifyPathUp(property, this[property]); |
| 1392 } |
| 1393 } |
| 1394 }); |
| 1395 |
| 1396 Polymer({ |
| 1397 is: 'iron-media-query', |
| 1398 properties: { |
| 1399 queryMatches: { |
| 1400 type: Boolean, |
| 1401 value: false, |
| 1402 readOnly: true, |
| 1403 notify: true |
| 1404 }, |
| 1405 query: { |
| 1406 type: String, |
| 1407 observer: 'queryChanged' |
| 1408 }, |
| 1409 full: { |
| 1410 type: Boolean, |
| 1411 value: false |
| 1412 }, |
| 1413 _boundMQHandler: { |
| 1414 value: function() { |
| 1415 return this.queryHandler.bind(this); |
| 1416 } |
| 1417 }, |
| 1418 _mq: { |
| 1419 value: null |
| 1420 } |
| 1421 }, |
| 1422 attached: function() { |
| 1423 this.style.display = 'none'; |
| 1424 this.queryChanged(); |
| 1425 }, |
| 1426 detached: function() { |
| 1427 this._remove(); |
| 1428 }, |
| 1429 _add: function() { |
| 1430 if (this._mq) { |
| 1431 this._mq.addListener(this._boundMQHandler); |
| 1432 } |
| 1433 }, |
| 1434 _remove: function() { |
| 1435 if (this._mq) { |
| 1436 this._mq.removeListener(this._boundMQHandler); |
| 1437 } |
| 1438 this._mq = null; |
| 1439 }, |
| 1440 queryChanged: function() { |
| 1441 this._remove(); |
| 1442 var query = this.query; |
| 1443 if (!query) { |
| 1444 return; |
| 1445 } |
| 1446 if (!this.full && query[0] !== '(') { |
| 1447 query = '(' + query + ')'; |
| 1448 } |
| 1449 this._mq = window.matchMedia(query); |
| 1450 this._add(); |
| 1451 this.queryHandler(this._mq); |
| 1452 }, |
| 1453 queryHandler: function(mq) { |
| 1454 this._setQueryMatches(mq.matches); |
| 1455 } |
| 1456 }); |
| 1457 |
| 1458 Polymer.IronResizableBehavior = { |
| 1459 properties: { |
| 1460 _parentResizable: { |
| 1461 type: Object, |
| 1462 observer: '_parentResizableChanged' |
| 1463 }, |
| 1464 _notifyingDescendant: { |
| 1465 type: Boolean, |
| 1466 value: false |
| 1467 } |
| 1468 }, |
| 1469 listeners: { |
| 1470 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
| 1471 }, |
| 1472 created: function() { |
| 1473 this._interestedResizables = []; |
| 1474 this._boundNotifyResize = this.notifyResize.bind(this); |
| 1475 }, |
| 1476 attached: function() { |
| 1477 this.fire('iron-request-resize-notifications', null, { |
| 1478 node: this, |
| 1479 bubbles: true, |
| 1480 cancelable: true |
| 1481 }); |
| 1482 if (!this._parentResizable) { |
| 1483 window.addEventListener('resize', this._boundNotifyResize); |
| 1484 this.notifyResize(); |
| 1485 } |
| 1486 }, |
| 1487 detached: function() { |
| 1488 if (this._parentResizable) { |
| 1489 this._parentResizable.stopResizeNotificationsFor(this); |
| 1490 } else { |
| 1491 window.removeEventListener('resize', this._boundNotifyResize); |
| 1492 } |
| 1493 this._parentResizable = null; |
| 1494 }, |
| 1495 notifyResize: function() { |
| 1496 if (!this.isAttached) { |
| 1497 return; |
| 1498 } |
| 1499 this._interestedResizables.forEach(function(resizable) { |
| 1500 if (this.resizerShouldNotify(resizable)) { |
| 1501 this._notifyDescendant(resizable); |
| 1502 } |
| 1503 }, this); |
| 1504 this._fireResize(); |
| 1505 }, |
| 1506 assignParentResizable: function(parentResizable) { |
| 1507 this._parentResizable = parentResizable; |
| 1508 }, |
| 1509 stopResizeNotificationsFor: function(target) { |
| 1510 var index = this._interestedResizables.indexOf(target); |
| 1511 if (index > -1) { |
| 1512 this._interestedResizables.splice(index, 1); |
| 1513 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
| 1514 } |
| 1515 }, |
| 1516 resizerShouldNotify: function(element) { |
| 1517 return true; |
| 1518 }, |
| 1519 _onDescendantIronResize: function(event) { |
| 1520 if (this._notifyingDescendant) { |
| 1521 event.stopPropagation(); |
| 1522 return; |
| 1523 } |
| 1524 if (!Polymer.Settings.useShadow) { |
| 1525 this._fireResize(); |
| 1526 } |
| 1527 }, |
| 1528 _fireResize: function() { |
| 1529 this.fire('iron-resize', null, { |
| 1530 node: this, |
| 1531 bubbles: false |
| 1532 }); |
| 1533 }, |
| 1534 _onIronRequestResizeNotifications: function(event) { |
| 1535 var target = event.path ? event.path[0] : event.target; |
| 1536 if (target === this) { |
| 1537 return; |
| 1538 } |
| 1539 if (this._interestedResizables.indexOf(target) === -1) { |
| 1540 this._interestedResizables.push(target); |
| 1541 this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| 1542 } |
| 1543 target.assignParentResizable(this); |
| 1544 this._notifyDescendant(target); |
| 1545 event.stopPropagation(); |
| 1546 }, |
| 1547 _parentResizableChanged: function(parentResizable) { |
| 1548 if (parentResizable) { |
| 1549 window.removeEventListener('resize', this._boundNotifyResize); |
| 1550 } |
| 1551 }, |
| 1552 _notifyDescendant: function(descendant) { |
| 1553 if (!this.isAttached) { |
| 1554 return; |
| 1555 } |
| 1556 this._notifyingDescendant = true; |
| 1557 descendant.notifyResize(); |
| 1558 this._notifyingDescendant = false; |
| 1559 } |
| 1560 }; |
| 1561 |
| 1562 Polymer.IronSelection = function(selectCallback) { |
| 1563 this.selection = []; |
| 1564 this.selectCallback = selectCallback; |
| 1565 }; |
| 1566 |
| 1567 Polymer.IronSelection.prototype = { |
| 1568 get: function() { |
| 1569 return this.multi ? this.selection.slice() : this.selection[0]; |
| 1570 }, |
| 1571 clear: function(excludes) { |
| 1572 this.selection.slice().forEach(function(item) { |
| 1573 if (!excludes || excludes.indexOf(item) < 0) { |
| 1574 this.setItemSelected(item, false); |
| 1575 } |
| 1576 }, this); |
| 1577 }, |
| 1578 isSelected: function(item) { |
| 1579 return this.selection.indexOf(item) >= 0; |
| 1580 }, |
| 1581 setItemSelected: function(item, isSelected) { |
| 1582 if (item != null) { |
| 1583 if (isSelected !== this.isSelected(item)) { |
| 1584 if (isSelected) { |
| 1585 this.selection.push(item); |
| 1586 } else { |
| 1587 var i = this.selection.indexOf(item); |
| 1588 if (i >= 0) { |
| 1589 this.selection.splice(i, 1); |
| 1590 } |
| 1591 } |
| 1592 if (this.selectCallback) { |
| 1593 this.selectCallback(item, isSelected); |
| 1594 } |
| 1595 } |
| 1596 } |
| 1597 }, |
| 1598 select: function(item) { |
| 1599 if (this.multi) { |
| 1600 this.toggle(item); |
| 1601 } else if (this.get() !== item) { |
| 1602 this.setItemSelected(this.get(), false); |
| 1603 this.setItemSelected(item, true); |
| 1604 } |
| 1605 }, |
| 1606 toggle: function(item) { |
| 1607 this.setItemSelected(item, !this.isSelected(item)); |
| 1608 } |
| 1609 }; |
| 1610 |
| 1611 Polymer.IronSelectableBehavior = { |
| 1612 properties: { |
| 1613 attrForSelected: { |
| 1614 type: String, |
| 1615 value: null |
| 1616 }, |
| 1617 selected: { |
| 1618 type: String, |
| 1619 notify: true |
| 1620 }, |
| 1621 selectedItem: { |
| 1622 type: Object, |
| 1623 readOnly: true, |
| 1624 notify: true |
| 1625 }, |
| 1626 activateEvent: { |
| 1627 type: String, |
| 1628 value: 'tap', |
| 1629 observer: '_activateEventChanged' |
| 1630 }, |
| 1631 selectable: String, |
| 1632 selectedClass: { |
| 1633 type: String, |
| 1634 value: 'iron-selected' |
| 1635 }, |
| 1636 selectedAttribute: { |
| 1637 type: String, |
| 1638 value: null |
| 1639 }, |
| 1640 fallbackSelection: { |
| 1641 type: String, |
| 1642 value: null |
| 1643 }, |
| 1644 items: { |
| 1645 type: Array, |
| 1646 readOnly: true, |
| 1647 notify: true, |
| 1648 value: function() { |
| 1649 return []; |
| 1650 } |
| 1651 }, |
| 1652 _excludedLocalNames: { |
| 1653 type: Object, |
| 1654 value: function() { |
| 1655 return { |
| 1656 template: 1 |
| 1657 }; |
| 1658 } |
| 1659 } |
| 1660 }, |
| 1661 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec
ted)', '_checkFallback(fallbackSelection)' ], |
| 1662 created: function() { |
| 1663 this._bindFilterItem = this._filterItem.bind(this); |
| 1664 this._selection = new Polymer.IronSelection(this._applySelection.bind(this))
; |
| 1665 }, |
| 1666 attached: function() { |
| 1667 this._observer = this._observeItems(this); |
| 1668 this._updateItems(); |
| 1669 if (!this._shouldUpdateSelection) { |
| 1670 this._updateSelected(); |
| 1671 } |
| 1672 this._addListener(this.activateEvent); |
| 1673 }, |
| 1674 detached: function() { |
| 1675 if (this._observer) { |
| 1676 Polymer.dom(this).unobserveNodes(this._observer); |
| 1677 } |
| 1678 this._removeListener(this.activateEvent); |
| 1679 }, |
| 1680 indexOf: function(item) { |
| 1681 return this.items.indexOf(item); |
| 1682 }, |
| 1683 select: function(value) { |
| 1684 this.selected = value; |
| 1685 }, |
| 1686 selectPrevious: function() { |
| 1687 var length = this.items.length; |
| 1688 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt
h; |
| 1689 this.selected = this._indexToValue(index); |
| 1690 }, |
| 1691 selectNext: function() { |
| 1692 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len
gth; |
| 1693 this.selected = this._indexToValue(index); |
| 1694 }, |
| 1695 selectIndex: function(index) { |
| 1696 this.select(this._indexToValue(index)); |
| 1697 }, |
| 1698 forceSynchronousItemUpdate: function() { |
| 1699 this._updateItems(); |
| 1700 }, |
| 1701 get _shouldUpdateSelection() { |
| 1702 return this.selected != null; |
| 1703 }, |
| 1704 _checkFallback: function() { |
| 1705 if (this._shouldUpdateSelection) { |
| 1706 this._updateSelected(); |
| 1707 } |
| 1708 }, |
| 1709 _addListener: function(eventName) { |
| 1710 this.listen(this, eventName, '_activateHandler'); |
| 1711 }, |
| 1712 _removeListener: function(eventName) { |
| 1713 this.unlisten(this, eventName, '_activateHandler'); |
| 1714 }, |
| 1715 _activateEventChanged: function(eventName, old) { |
| 1716 this._removeListener(old); |
| 1717 this._addListener(eventName); |
| 1718 }, |
| 1719 _updateItems: function() { |
| 1720 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*
'); |
| 1721 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 1722 this._setItems(nodes); |
| 1723 }, |
| 1724 _updateAttrForSelected: function() { |
| 1725 if (this._shouldUpdateSelection) { |
| 1726 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 1727 } |
| 1728 }, |
| 1729 _updateSelected: function() { |
| 1730 this._selectSelected(this.selected); |
| 1731 }, |
| 1732 _selectSelected: function(selected) { |
| 1733 this._selection.select(this._valueToItem(this.selected)); |
| 1734 if (this.fallbackSelection && this.items.length && this._selection.get() ===
undefined) { |
| 1735 this.selected = this.fallbackSelection; |
| 1736 } |
| 1737 }, |
| 1738 _filterItem: function(node) { |
| 1739 return !this._excludedLocalNames[node.localName]; |
| 1740 }, |
| 1741 _valueToItem: function(value) { |
| 1742 return value == null ? null : this.items[this._valueToIndex(value)]; |
| 1743 }, |
| 1744 _valueToIndex: function(value) { |
| 1745 if (this.attrForSelected) { |
| 1746 for (var i = 0, item; item = this.items[i]; i++) { |
| 1747 if (this._valueForItem(item) == value) { |
| 1748 return i; |
| 1749 } |
| 1750 } |
| 1751 } else { |
| 1752 return Number(value); |
| 1753 } |
| 1754 }, |
| 1755 _indexToValue: function(index) { |
| 1756 if (this.attrForSelected) { |
| 1757 var item = this.items[index]; |
| 1758 if (item) { |
| 1759 return this._valueForItem(item); |
| 1760 } |
| 1761 } else { |
| 1762 return index; |
| 1763 } |
| 1764 }, |
| 1765 _valueForItem: function(item) { |
| 1766 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; |
| 1767 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe
lected); |
| 1768 }, |
| 1769 _applySelection: function(item, isSelected) { |
| 1770 if (this.selectedClass) { |
| 1771 this.toggleClass(this.selectedClass, isSelected, item); |
| 1772 } |
| 1773 if (this.selectedAttribute) { |
| 1774 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 1775 } |
| 1776 this._selectionChange(); |
| 1777 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), { |
| 1778 item: item |
| 1779 }); |
| 1780 }, |
| 1781 _selectionChange: function() { |
| 1782 this._setSelectedItem(this._selection.get()); |
| 1783 }, |
| 1784 _observeItems: function(node) { |
| 1785 return Polymer.dom(node).observeNodes(function(mutation) { |
| 1786 this._updateItems(); |
| 1787 if (this._shouldUpdateSelection) { |
| 1788 this._updateSelected(); |
| 1789 } |
| 1790 this.fire('iron-items-changed', mutation, { |
| 1791 bubbles: false, |
| 1792 cancelable: false |
| 1793 }); |
| 1794 }); |
| 1795 }, |
| 1796 _activateHandler: function(e) { |
| 1797 var t = e.target; |
| 1798 var items = this.items; |
| 1799 while (t && t != this) { |
| 1800 var i = items.indexOf(t); |
| 1801 if (i >= 0) { |
| 1802 var value = this._indexToValue(i); |
| 1803 this._itemActivate(value, t); |
| 1804 return; |
| 1805 } |
| 1806 t = t.parentNode; |
| 1807 } |
| 1808 }, |
| 1809 _itemActivate: function(value, item) { |
| 1810 if (!this.fire('iron-activate', { |
| 1811 selected: value, |
| 1812 item: item |
| 1813 }, { |
| 1814 cancelable: true |
| 1815 }).defaultPrevented) { |
| 1816 this.select(value); |
| 1817 } |
| 1818 } |
| 1819 }; |
| 1820 |
| 1821 Polymer({ |
| 1822 is: 'iron-pages', |
| 1823 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronSelectableBehavior ], |
| 1824 properties: { |
| 1825 activateEvent: { |
| 1826 type: String, |
| 1827 value: null |
| 1828 } |
| 1829 }, |
| 1830 observers: [ '_selectedPageChanged(selected)' ], |
| 1831 _selectedPageChanged: function(selected, old) { |
| 1832 this.async(this.notifyResize); |
| 1833 } |
| 1834 }); |
| 1835 |
| 1836 (function() { |
| 1837 'use strict'; |
| 1838 var KEY_IDENTIFIER = { |
| 1839 'U+0008': 'backspace', |
| 1840 'U+0009': 'tab', |
| 1841 'U+001B': 'esc', |
| 1842 'U+0020': 'space', |
| 1843 'U+007F': 'del' |
| 1844 }; |
| 1845 var KEY_CODE = { |
| 1846 8: 'backspace', |
| 1847 9: 'tab', |
| 1848 13: 'enter', |
| 1849 27: 'esc', |
| 1850 33: 'pageup', |
| 1851 34: 'pagedown', |
| 1852 35: 'end', |
| 1853 36: 'home', |
| 1854 32: 'space', |
| 1855 37: 'left', |
| 1856 38: 'up', |
| 1857 39: 'right', |
| 1858 40: 'down', |
| 1859 46: 'del', |
| 1860 106: '*' |
| 1861 }; |
| 1862 var MODIFIER_KEYS = { |
| 1863 shift: 'shiftKey', |
| 1864 ctrl: 'ctrlKey', |
| 1865 alt: 'altKey', |
| 1866 meta: 'metaKey' |
| 1867 }; |
| 1868 var KEY_CHAR = /[a-z0-9*]/; |
| 1869 var IDENT_CHAR = /U\+/; |
| 1870 var ARROW_KEY = /^arrow/; |
| 1871 var SPACE_KEY = /^space(bar)?/; |
| 1872 var ESC_KEY = /^escape$/; |
| 1873 function transformKey(key, noSpecialChars) { |
| 1874 var validKey = ''; |
| 1875 if (key) { |
| 1876 var lKey = key.toLowerCase(); |
| 1877 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 1878 validKey = 'space'; |
| 1879 } else if (ESC_KEY.test(lKey)) { |
| 1880 validKey = 'esc'; |
| 1881 } else if (lKey.length == 1) { |
| 1882 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 1883 validKey = lKey; |
| 1884 } |
| 1885 } else if (ARROW_KEY.test(lKey)) { |
| 1886 validKey = lKey.replace('arrow', ''); |
| 1887 } else if (lKey == 'multiply') { |
| 1888 validKey = '*'; |
| 1889 } else { |
| 1890 validKey = lKey; |
| 1891 } |
| 1892 } |
| 1893 return validKey; |
| 1894 } |
| 1895 function transformKeyIdentifier(keyIdent) { |
| 1896 var validKey = ''; |
| 1897 if (keyIdent) { |
| 1898 if (keyIdent in KEY_IDENTIFIER) { |
| 1899 validKey = KEY_IDENTIFIER[keyIdent]; |
| 1900 } else if (IDENT_CHAR.test(keyIdent)) { |
| 1901 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 1902 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 1903 } else { |
| 1904 validKey = keyIdent.toLowerCase(); |
| 1905 } |
| 1906 } |
| 1907 return validKey; |
| 1908 } |
| 1909 function transformKeyCode(keyCode) { |
| 1910 var validKey = ''; |
| 1911 if (Number(keyCode)) { |
| 1912 if (keyCode >= 65 && keyCode <= 90) { |
| 1913 validKey = String.fromCharCode(32 + keyCode); |
| 1914 } else if (keyCode >= 112 && keyCode <= 123) { |
| 1915 validKey = 'f' + (keyCode - 112); |
| 1916 } else if (keyCode >= 48 && keyCode <= 57) { |
| 1917 validKey = String(keyCode - 48); |
| 1918 } else if (keyCode >= 96 && keyCode <= 105) { |
| 1919 validKey = String(keyCode - 96); |
| 1920 } else { |
| 1921 validKey = KEY_CODE[keyCode]; |
| 1922 } |
| 1923 } |
| 1924 return validKey; |
| 1925 } |
| 1926 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 1927 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier(
keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke
yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| 1928 } |
| 1929 function keyComboMatchesEvent(keyCombo, event) { |
| 1930 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 1931 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift
Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event
.altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey); |
| 1932 } |
| 1933 function parseKeyComboString(keyComboString) { |
| 1934 if (keyComboString.length === 1) { |
| 1935 return { |
| 1936 combo: keyComboString, |
| 1937 key: keyComboString, |
| 1938 event: 'keydown' |
| 1939 }; |
| 1940 } |
| 1941 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar
t) { |
| 1942 var eventParts = keyComboPart.split(':'); |
| 1943 var keyName = eventParts[0]; |
| 1944 var event = eventParts[1]; |
| 1945 if (keyName in MODIFIER_KEYS) { |
| 1946 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 1947 parsedKeyCombo.hasModifiers = true; |
| 1948 } else { |
| 1949 parsedKeyCombo.key = keyName; |
| 1950 parsedKeyCombo.event = event || 'keydown'; |
| 1951 } |
| 1952 return parsedKeyCombo; |
| 1953 }, { |
| 1954 combo: keyComboString.split(':').shift() |
| 1955 }); |
| 1956 } |
| 1957 function parseEventString(eventString) { |
| 1958 return eventString.trim().split(' ').map(function(keyComboString) { |
| 1959 return parseKeyComboString(keyComboString); |
| 1960 }); |
| 1961 } |
| 1962 Polymer.IronA11yKeysBehavior = { |
| 1936 properties: { | 1963 properties: { |
| 1937 /** | 1964 keyEventTarget: { |
| 1938 * A model representing the deserialized path through the route tree, as | |
| 1939 * well as the current queryParams. | |
| 1940 * | |
| 1941 * A route object is the kernel of the routing system. It is intended to | |
| 1942 * be fed into consuming elements such as `app-route`. | |
| 1943 * | |
| 1944 * @type {?Object} | |
| 1945 */ | |
| 1946 route: { | |
| 1947 type: Object, | 1965 type: Object, |
| 1948 notify: true | 1966 value: function() { |
| 1949 }, | 1967 return this; |
| 1950 | |
| 1951 /** | |
| 1952 * A set of key/value pairs that are universally accessible to branches of | |
| 1953 * the route tree. | |
| 1954 * | |
| 1955 * @type {?Object} | |
| 1956 */ | |
| 1957 queryParams: { | |
| 1958 type: Object, | |
| 1959 notify: true | |
| 1960 }, | |
| 1961 | |
| 1962 /** | |
| 1963 * The serialized path through the route tree. This corresponds to the | |
| 1964 * `window.location.pathname` value, and will update to reflect changes | |
| 1965 * to that value. | |
| 1966 */ | |
| 1967 path: { | |
| 1968 type: String, | |
| 1969 notify: true, | |
| 1970 } | |
| 1971 }, | |
| 1972 | |
| 1973 observers: [ | |
| 1974 '_locationChanged(path, queryParams)', | |
| 1975 '_routeChanged(route.prefix, route.path)', | |
| 1976 '_routeQueryParamsChanged(route.__queryParams)' | |
| 1977 ], | |
| 1978 | |
| 1979 created: function() { | |
| 1980 this.linkPaths('route.__queryParams', 'queryParams'); | |
| 1981 this.linkPaths('queryParams', 'route.__queryParams'); | |
| 1982 }, | |
| 1983 | |
| 1984 /** | |
| 1985 * Handler called when the path or queryParams change. | |
| 1986 */ | |
| 1987 _locationChanged: function() { | |
| 1988 if (this.route && | |
| 1989 this.route.path === this.path && | |
| 1990 this.queryParams === this.route.__queryParams) { | |
| 1991 return; | |
| 1992 } | |
| 1993 this.route = { | |
| 1994 prefix: '', | |
| 1995 path: this.path, | |
| 1996 __queryParams: this.queryParams | |
| 1997 }; | |
| 1998 }, | |
| 1999 | |
| 2000 /** | |
| 2001 * Handler called when the route prefix and route path change. | |
| 2002 */ | |
| 2003 _routeChanged: function() { | |
| 2004 if (!this.route) { | |
| 2005 return; | |
| 2006 } | |
| 2007 | |
| 2008 this.path = this.route.prefix + this.route.path; | |
| 2009 }, | |
| 2010 | |
| 2011 /** | |
| 2012 * Handler called when the route queryParams change. | |
| 2013 * | |
| 2014 * @param {Object} queryParams A set of key/value pairs that are | |
| 2015 * universally accessible to branches of the route tree. | |
| 2016 */ | |
| 2017 _routeQueryParamsChanged: function(queryParams) { | |
| 2018 if (!this.route) { | |
| 2019 return; | |
| 2020 } | |
| 2021 this.queryParams = queryParams; | |
| 2022 } | |
| 2023 }; | |
| 2024 'use strict'; | |
| 2025 | |
| 2026 Polymer({ | |
| 2027 is: 'app-location', | |
| 2028 | |
| 2029 properties: { | |
| 2030 /** | |
| 2031 * A model representing the deserialized path through the route tree, as | |
| 2032 * well as the current queryParams. | |
| 2033 */ | |
| 2034 route: { | |
| 2035 type: Object, | |
| 2036 notify: true | |
| 2037 }, | |
| 2038 | |
| 2039 /** | |
| 2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in | |
| 2041 * alternative to the `path`. For example, if deploying an app to a stat
ic | |
| 2042 * web server (e.g., Github Pages) - where one does not have control ove
r | |
| 2043 * server-side routing - it is usually a better experience to use the ha
sh | |
| 2044 * to represent paths through one's app. | |
| 2045 * | |
| 2046 * When this property is set to true, the `hash` will be used in place o
f | |
| 2047 | |
| 2048 * the `path` for generating a `route`. | |
| 2049 */ | |
| 2050 useHashAsPath: { | |
| 2051 type: Boolean, | |
| 2052 value: false | |
| 2053 }, | |
| 2054 | |
| 2055 /** | |
| 2056 * A regexp that defines the set of URLs that should be considered part | |
| 2057 * of this web app. | |
| 2058 * | |
| 2059 * Clicking on a link that matches this regex won't result in a full pag
e | |
| 2060 * navigation, but will instead just update the URL state in place. | |
| 2061 * | |
| 2062 * This regexp is given everything after the origin in an absolute | |
| 2063 * URL. So to match just URLs that start with /search/ do: | |
| 2064 * url-space-regex="^/search/" | |
| 2065 * | |
| 2066 * @type {string|RegExp} | |
| 2067 */ | |
| 2068 urlSpaceRegex: { | |
| 2069 type: String, | |
| 2070 notify: true | |
| 2071 }, | |
| 2072 | |
| 2073 /** | |
| 2074 * A set of key/value pairs that are universally accessible to branches | |
| 2075 * of the route tree. | |
| 2076 */ | |
| 2077 __queryParams: { | |
| 2078 type: Object | |
| 2079 }, | |
| 2080 | |
| 2081 /** | |
| 2082 * The pathname component of the current URL. | |
| 2083 */ | |
| 2084 __path: { | |
| 2085 type: String | |
| 2086 }, | |
| 2087 | |
| 2088 /** | |
| 2089 * The query string portion of the current URL. | |
| 2090 */ | |
| 2091 __query: { | |
| 2092 type: String | |
| 2093 }, | |
| 2094 | |
| 2095 /** | |
| 2096 * The hash portion of the current URL. | |
| 2097 */ | |
| 2098 __hash: { | |
| 2099 type: String | |
| 2100 }, | |
| 2101 | |
| 2102 /** | |
| 2103 * The route path, which will be either the hash or the path, depending | |
| 2104 * on useHashAsPath. | |
| 2105 */ | |
| 2106 path: { | |
| 2107 type: String, | |
| 2108 observer: '__onPathChanged' | |
| 2109 } | 1968 } |
| 2110 }, | 1969 }, |
| 2111 | 1970 stopKeyboardEventPropagation: { |
| 2112 behaviors: [Polymer.AppRouteConverterBehavior], | |
| 2113 | |
| 2114 observers: [ | |
| 2115 '__computeRoutePath(useHashAsPath, __hash, __path)' | |
| 2116 ], | |
| 2117 | |
| 2118 __computeRoutePath: function() { | |
| 2119 this.path = this.useHashAsPath ? this.__hash : this.__path; | |
| 2120 }, | |
| 2121 | |
| 2122 __onPathChanged: function() { | |
| 2123 if (!this._readied) { | |
| 2124 return; | |
| 2125 } | |
| 2126 | |
| 2127 if (this.useHashAsPath) { | |
| 2128 this.__hash = this.path; | |
| 2129 } else { | |
| 2130 this.__path = this.path; | |
| 2131 } | |
| 2132 } | |
| 2133 }); | |
| 2134 'use strict'; | |
| 2135 | |
| 2136 Polymer({ | |
| 2137 is: 'app-route', | |
| 2138 | |
| 2139 properties: { | |
| 2140 /** | |
| 2141 * The URL component managed by this element. | |
| 2142 */ | |
| 2143 route: { | |
| 2144 type: Object, | |
| 2145 notify: true | |
| 2146 }, | |
| 2147 | |
| 2148 /** | |
| 2149 * The pattern of slash-separated segments to match `path` against. | |
| 2150 * | |
| 2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar" | |
| 2152 * but not "/foobar". | |
| 2153 * | |
| 2154 * Path segments like `/:named` are mapped to properties on the `data` obj
ect. | |
| 2155 */ | |
| 2156 pattern: { | |
| 2157 type: String | |
| 2158 }, | |
| 2159 | |
| 2160 /** | |
| 2161 * The parameterized values that are extracted from the route as | |
| 2162 * described by `pattern`. | |
| 2163 */ | |
| 2164 data: { | |
| 2165 type: Object, | |
| 2166 value: function() {return {};}, | |
| 2167 notify: true | |
| 2168 }, | |
| 2169 | |
| 2170 /** | |
| 2171 * @type {?Object} | |
| 2172 */ | |
| 2173 queryParams: { | |
| 2174 type: Object, | |
| 2175 value: function() { | |
| 2176 return {}; | |
| 2177 }, | |
| 2178 notify: true | |
| 2179 }, | |
| 2180 | |
| 2181 /** | |
| 2182 * The part of `path` NOT consumed by `pattern`. | |
| 2183 */ | |
| 2184 tail: { | |
| 2185 type: Object, | |
| 2186 value: function() {return {path: null, prefix: null, __queryParams: null
};}, | |
| 2187 notify: true | |
| 2188 }, | |
| 2189 | |
| 2190 active: { | |
| 2191 type: Boolean, | |
| 2192 notify: true, | |
| 2193 readOnly: true | |
| 2194 }, | |
| 2195 | |
| 2196 _queryParamsUpdating: { | |
| 2197 type: Boolean, | 1971 type: Boolean, |
| 2198 value: false | 1972 value: false |
| 2199 }, | 1973 }, |
| 2200 /** | 1974 _boundKeyHandlers: { |
| 2201 * @type {?string} | |
| 2202 */ | |
| 2203 _matched: { | |
| 2204 type: String, | |
| 2205 value: '' | |
| 2206 } | |
| 2207 }, | |
| 2208 | |
| 2209 observers: [ | |
| 2210 '__tryToMatch(route.path, pattern)', | |
| 2211 '__updatePathOnDataChange(data.*)', | |
| 2212 '__tailPathChanged(tail.path)', | |
| 2213 '__routeQueryParamsChanged(route.__queryParams)', | |
| 2214 '__tailQueryParamsChanged(tail.__queryParams)', | |
| 2215 '__queryParamsChanged(queryParams.*)' | |
| 2216 ], | |
| 2217 | |
| 2218 created: function() { | |
| 2219 this.linkPaths('route.__queryParams', 'tail.__queryParams'); | |
| 2220 this.linkPaths('tail.__queryParams', 'route.__queryParams'); | |
| 2221 }, | |
| 2222 | |
| 2223 /** | |
| 2224 * Deal with the query params object being assigned to wholesale. | |
| 2225 * @export | |
| 2226 */ | |
| 2227 __routeQueryParamsChanged: function(queryParams) { | |
| 2228 if (queryParams && this.tail) { | |
| 2229 this.set('tail.__queryParams', queryParams); | |
| 2230 | |
| 2231 if (!this.active || this._queryParamsUpdating) { | |
| 2232 return; | |
| 2233 } | |
| 2234 | |
| 2235 // Copy queryParams and track whether there are any differences compared | |
| 2236 // to the existing query params. | |
| 2237 var copyOfQueryParams = {}; | |
| 2238 var anythingChanged = false; | |
| 2239 for (var key in queryParams) { | |
| 2240 copyOfQueryParams[key] = queryParams[key]; | |
| 2241 if (anythingChanged || | |
| 2242 !this.queryParams || | |
| 2243 queryParams[key] !== this.queryParams[key]) { | |
| 2244 anythingChanged = true; | |
| 2245 } | |
| 2246 } | |
| 2247 // Need to check whether any keys were deleted | |
| 2248 for (var key in this.queryParams) { | |
| 2249 if (anythingChanged || !(key in queryParams)) { | |
| 2250 anythingChanged = true; | |
| 2251 break; | |
| 2252 } | |
| 2253 } | |
| 2254 | |
| 2255 if (!anythingChanged) { | |
| 2256 return; | |
| 2257 } | |
| 2258 this._queryParamsUpdating = true; | |
| 2259 this.set('queryParams', copyOfQueryParams); | |
| 2260 this._queryParamsUpdating = false; | |
| 2261 } | |
| 2262 }, | |
| 2263 | |
| 2264 /** | |
| 2265 * @export | |
| 2266 */ | |
| 2267 __tailQueryParamsChanged: function(queryParams) { | |
| 2268 if (queryParams && this.route) { | |
| 2269 this.set('route.__queryParams', queryParams); | |
| 2270 } | |
| 2271 }, | |
| 2272 | |
| 2273 /** | |
| 2274 * @export | |
| 2275 */ | |
| 2276 __queryParamsChanged: function(changes) { | |
| 2277 if (!this.active || this._queryParamsUpdating) { | |
| 2278 return; | |
| 2279 } | |
| 2280 | |
| 2281 this.set('route.__' + changes.path, changes.value); | |
| 2282 }, | |
| 2283 | |
| 2284 __resetProperties: function() { | |
| 2285 this._setActive(false); | |
| 2286 this._matched = null; | |
| 2287 //this.tail = { path: null, prefix: null, queryParams: null }; | |
| 2288 //this.data = {}; | |
| 2289 }, | |
| 2290 | |
| 2291 /** | |
| 2292 * @export | |
| 2293 */ | |
| 2294 __tryToMatch: function() { | |
| 2295 if (!this.route) { | |
| 2296 return; | |
| 2297 } | |
| 2298 var path = this.route.path; | |
| 2299 var pattern = this.pattern; | |
| 2300 if (!pattern) { | |
| 2301 return; | |
| 2302 } | |
| 2303 | |
| 2304 if (!path) { | |
| 2305 this.__resetProperties(); | |
| 2306 return; | |
| 2307 } | |
| 2308 | |
| 2309 var remainingPieces = path.split('/'); | |
| 2310 var patternPieces = pattern.split('/'); | |
| 2311 | |
| 2312 var matched = []; | |
| 2313 var namedMatches = {}; | |
| 2314 | |
| 2315 for (var i=0; i < patternPieces.length; i++) { | |
| 2316 var patternPiece = patternPieces[i]; | |
| 2317 if (!patternPiece && patternPiece !== '') { | |
| 2318 break; | |
| 2319 } | |
| 2320 var pathPiece = remainingPieces.shift(); | |
| 2321 | |
| 2322 // We don't match this path. | |
| 2323 if (!pathPiece && pathPiece !== '') { | |
| 2324 this.__resetProperties(); | |
| 2325 return; | |
| 2326 } | |
| 2327 matched.push(pathPiece); | |
| 2328 | |
| 2329 if (patternPiece.charAt(0) == ':') { | |
| 2330 namedMatches[patternPiece.slice(1)] = pathPiece; | |
| 2331 } else if (patternPiece !== pathPiece) { | |
| 2332 this.__resetProperties(); | |
| 2333 return; | |
| 2334 } | |
| 2335 } | |
| 2336 | |
| 2337 this._matched = matched.join('/'); | |
| 2338 | |
| 2339 // Properties that must be updated atomically. | |
| 2340 var propertyUpdates = {}; | |
| 2341 | |
| 2342 //this.active | |
| 2343 if (!this.active) { | |
| 2344 propertyUpdates.active = true; | |
| 2345 } | |
| 2346 | |
| 2347 // this.tail | |
| 2348 var tailPrefix = this.route.prefix + this._matched; | |
| 2349 var tailPath = remainingPieces.join('/'); | |
| 2350 if (remainingPieces.length > 0) { | |
| 2351 tailPath = '/' + tailPath; | |
| 2352 } | |
| 2353 if (!this.tail || | |
| 2354 this.tail.prefix !== tailPrefix || | |
| 2355 this.tail.path !== tailPath) { | |
| 2356 propertyUpdates.tail = { | |
| 2357 prefix: tailPrefix, | |
| 2358 path: tailPath, | |
| 2359 __queryParams: this.route.__queryParams | |
| 2360 }; | |
| 2361 } | |
| 2362 | |
| 2363 // this.data | |
| 2364 propertyUpdates.data = namedMatches; | |
| 2365 this._dataInUrl = {}; | |
| 2366 for (var key in namedMatches) { | |
| 2367 this._dataInUrl[key] = namedMatches[key]; | |
| 2368 } | |
| 2369 | |
| 2370 this.__setMulti(propertyUpdates); | |
| 2371 }, | |
| 2372 | |
| 2373 /** | |
| 2374 * @export | |
| 2375 */ | |
| 2376 __tailPathChanged: function() { | |
| 2377 if (!this.active) { | |
| 2378 return; | |
| 2379 } | |
| 2380 var tailPath = this.tail.path; | |
| 2381 var newPath = this._matched; | |
| 2382 if (tailPath) { | |
| 2383 if (tailPath.charAt(0) !== '/') { | |
| 2384 tailPath = '/' + tailPath; | |
| 2385 } | |
| 2386 newPath += tailPath; | |
| 2387 } | |
| 2388 this.set('route.path', newPath); | |
| 2389 }, | |
| 2390 | |
| 2391 /** | |
| 2392 * @export | |
| 2393 */ | |
| 2394 __updatePathOnDataChange: function() { | |
| 2395 if (!this.route || !this.active) { | |
| 2396 return; | |
| 2397 } | |
| 2398 var newPath = this.__getLink({}); | |
| 2399 var oldPath = this.__getLink(this._dataInUrl); | |
| 2400 if (newPath === oldPath) { | |
| 2401 return; | |
| 2402 } | |
| 2403 this.set('route.path', newPath); | |
| 2404 }, | |
| 2405 | |
| 2406 __getLink: function(overrideValues) { | |
| 2407 var values = {tail: null}; | |
| 2408 for (var key in this.data) { | |
| 2409 values[key] = this.data[key]; | |
| 2410 } | |
| 2411 for (var key in overrideValues) { | |
| 2412 values[key] = overrideValues[key]; | |
| 2413 } | |
| 2414 var patternPieces = this.pattern.split('/'); | |
| 2415 var interp = patternPieces.map(function(value) { | |
| 2416 if (value[0] == ':') { | |
| 2417 value = values[value.slice(1)]; | |
| 2418 } | |
| 2419 return value; | |
| 2420 }, this); | |
| 2421 if (values.tail && values.tail.path) { | |
| 2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { | |
| 2423 interp.push(values.tail.path.slice(1)); | |
| 2424 } else { | |
| 2425 interp.push(values.tail.path); | |
| 2426 } | |
| 2427 } | |
| 2428 return interp.join('/'); | |
| 2429 }, | |
| 2430 | |
| 2431 __setMulti: function(setObj) { | |
| 2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at | |
| 2433 // internal data structures. I would not advise that you copy this | |
| 2434 // example. | |
| 2435 // | |
| 2436 // In the future this will be a feature of Polymer itself. | |
| 2437 // See: https://github.com/Polymer/polymer/issues/3640 | |
| 2438 // | |
| 2439 // Hacking around with private methods like this is juggling footguns, | |
| 2440 // and is likely to have unexpected and unsupported rough edges. | |
| 2441 // | |
| 2442 // Be ye so warned. | |
| 2443 for (var property in setObj) { | |
| 2444 this._propertySetter(property, setObj[property]); | |
| 2445 } | |
| 2446 | |
| 2447 for (var property in setObj) { | |
| 2448 this._pathEffector(property, this[property]); | |
| 2449 this._notifyPathUp(property, this[property]); | |
| 2450 } | |
| 2451 } | |
| 2452 }); | |
| 2453 Polymer({ | |
| 2454 | |
| 2455 is: 'iron-media-query', | |
| 2456 | |
| 2457 properties: { | |
| 2458 | |
| 2459 /** | |
| 2460 * The Boolean return value of the media query. | |
| 2461 */ | |
| 2462 queryMatches: { | |
| 2463 type: Boolean, | |
| 2464 value: false, | |
| 2465 readOnly: true, | |
| 2466 notify: true | |
| 2467 }, | |
| 2468 | |
| 2469 /** | |
| 2470 * The CSS media query to evaluate. | |
| 2471 */ | |
| 2472 query: { | |
| 2473 type: String, | |
| 2474 observer: 'queryChanged' | |
| 2475 }, | |
| 2476 | |
| 2477 /** | |
| 2478 * If true, the query attribute is assumed to be a complete media query | |
| 2479 * string rather than a single media feature. | |
| 2480 */ | |
| 2481 full: { | |
| 2482 type: Boolean, | |
| 2483 value: false | |
| 2484 }, | |
| 2485 | |
| 2486 /** | |
| 2487 * @type {function(MediaQueryList)} | |
| 2488 */ | |
| 2489 _boundMQHandler: { | |
| 2490 value: function() { | |
| 2491 return this.queryHandler.bind(this); | |
| 2492 } | |
| 2493 }, | |
| 2494 | |
| 2495 /** | |
| 2496 * @type {MediaQueryList} | |
| 2497 */ | |
| 2498 _mq: { | |
| 2499 value: null | |
| 2500 } | |
| 2501 }, | |
| 2502 | |
| 2503 attached: function() { | |
| 2504 this.style.display = 'none'; | |
| 2505 this.queryChanged(); | |
| 2506 }, | |
| 2507 | |
| 2508 detached: function() { | |
| 2509 this._remove(); | |
| 2510 }, | |
| 2511 | |
| 2512 _add: function() { | |
| 2513 if (this._mq) { | |
| 2514 this._mq.addListener(this._boundMQHandler); | |
| 2515 } | |
| 2516 }, | |
| 2517 | |
| 2518 _remove: function() { | |
| 2519 if (this._mq) { | |
| 2520 this._mq.removeListener(this._boundMQHandler); | |
| 2521 } | |
| 2522 this._mq = null; | |
| 2523 }, | |
| 2524 | |
| 2525 queryChanged: function() { | |
| 2526 this._remove(); | |
| 2527 var query = this.query; | |
| 2528 if (!query) { | |
| 2529 return; | |
| 2530 } | |
| 2531 if (!this.full && query[0] !== '(') { | |
| 2532 query = '(' + query + ')'; | |
| 2533 } | |
| 2534 this._mq = window.matchMedia(query); | |
| 2535 this._add(); | |
| 2536 this.queryHandler(this._mq); | |
| 2537 }, | |
| 2538 | |
| 2539 queryHandler: function(mq) { | |
| 2540 this._setQueryMatches(mq.matches); | |
| 2541 } | |
| 2542 | |
| 2543 }); | |
| 2544 /** | |
| 2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | |
| 2546 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | |
| 2547 * size or hidden state of their children) and "resizables" (elements that nee
d to be | |
| 2548 * notified when they are resized or un-hidden by their parents in order to ta
ke | |
| 2549 * action on their new measurements). | |
| 2550 * | |
| 2551 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | |
| 2552 * their element definition and listen for the `iron-resize` event on themselv
es. | |
| 2553 * This event will be fired when they become showing after having been hidden, | |
| 2554 * when they are resized explicitly by another resizable, or when the window h
as been | |
| 2555 * resized. | |
| 2556 * | |
| 2557 * Note, the `iron-resize` event is non-bubbling. | |
| 2558 * | |
| 2559 * @polymerBehavior Polymer.IronResizableBehavior | |
| 2560 * @demo demo/index.html | |
| 2561 **/ | |
| 2562 Polymer.IronResizableBehavior = { | |
| 2563 properties: { | |
| 2564 /** | |
| 2565 * The closest ancestor element that implements `IronResizableBehavior`. | |
| 2566 */ | |
| 2567 _parentResizable: { | |
| 2568 type: Object, | |
| 2569 observer: '_parentResizableChanged' | |
| 2570 }, | |
| 2571 | |
| 2572 /** | |
| 2573 * True if this element is currently notifying its descedant elements of | |
| 2574 * resize. | |
| 2575 */ | |
| 2576 _notifyingDescendant: { | |
| 2577 type: Boolean, | |
| 2578 value: false | |
| 2579 } | |
| 2580 }, | |
| 2581 | |
| 2582 listeners: { | |
| 2583 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | |
| 2584 }, | |
| 2585 | |
| 2586 created: function() { | |
| 2587 // We don't really need property effects on these, and also we want them | |
| 2588 // to be created before the `_parentResizable` observer fires: | |
| 2589 this._interestedResizables = []; | |
| 2590 this._boundNotifyResize = this.notifyResize.bind(this); | |
| 2591 }, | |
| 2592 | |
| 2593 attached: function() { | |
| 2594 this.fire('iron-request-resize-notifications', null, { | |
| 2595 node: this, | |
| 2596 bubbles: true, | |
| 2597 cancelable: true | |
| 2598 }); | |
| 2599 | |
| 2600 if (!this._parentResizable) { | |
| 2601 window.addEventListener('resize', this._boundNotifyResize); | |
| 2602 this.notifyResize(); | |
| 2603 } | |
| 2604 }, | |
| 2605 | |
| 2606 detached: function() { | |
| 2607 if (this._parentResizable) { | |
| 2608 this._parentResizable.stopResizeNotificationsFor(this); | |
| 2609 } else { | |
| 2610 window.removeEventListener('resize', this._boundNotifyResize); | |
| 2611 } | |
| 2612 | |
| 2613 this._parentResizable = null; | |
| 2614 }, | |
| 2615 | |
| 2616 /** | |
| 2617 * Can be called to manually notify a resizable and its descendant | |
| 2618 * resizables of a resize change. | |
| 2619 */ | |
| 2620 notifyResize: function() { | |
| 2621 if (!this.isAttached) { | |
| 2622 return; | |
| 2623 } | |
| 2624 | |
| 2625 this._interestedResizables.forEach(function(resizable) { | |
| 2626 if (this.resizerShouldNotify(resizable)) { | |
| 2627 this._notifyDescendant(resizable); | |
| 2628 } | |
| 2629 }, this); | |
| 2630 | |
| 2631 this._fireResize(); | |
| 2632 }, | |
| 2633 | |
| 2634 /** | |
| 2635 * Used to assign the closest resizable ancestor to this resizable | |
| 2636 * if the ancestor detects a request for notifications. | |
| 2637 */ | |
| 2638 assignParentResizable: function(parentResizable) { | |
| 2639 this._parentResizable = parentResizable; | |
| 2640 }, | |
| 2641 | |
| 2642 /** | |
| 2643 * Used to remove a resizable descendant from the list of descendants | |
| 2644 * that should be notified of a resize change. | |
| 2645 */ | |
| 2646 stopResizeNotificationsFor: function(target) { | |
| 2647 var index = this._interestedResizables.indexOf(target); | |
| 2648 | |
| 2649 if (index > -1) { | |
| 2650 this._interestedResizables.splice(index, 1); | |
| 2651 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | |
| 2652 } | |
| 2653 }, | |
| 2654 | |
| 2655 /** | |
| 2656 * This method can be overridden to filter nested elements that should or | |
| 2657 * should not be notified by the current element. Return true if an element | |
| 2658 * should be notified, or false if it should not be notified. | |
| 2659 * | |
| 2660 * @param {HTMLElement} element A candidate descendant element that | |
| 2661 * implements `IronResizableBehavior`. | |
| 2662 * @return {boolean} True if the `element` should be notified of resize. | |
| 2663 */ | |
| 2664 resizerShouldNotify: function(element) { return true; }, | |
| 2665 | |
| 2666 _onDescendantIronResize: function(event) { | |
| 2667 if (this._notifyingDescendant) { | |
| 2668 event.stopPropagation(); | |
| 2669 return; | |
| 2670 } | |
| 2671 | |
| 2672 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
| 2673 // otherwise non-bubbling event "just work." We do it manually here for | |
| 2674 // the case where Polymer is not using shadow roots for whatever reason: | |
| 2675 if (!Polymer.Settings.useShadow) { | |
| 2676 this._fireResize(); | |
| 2677 } | |
| 2678 }, | |
| 2679 | |
| 2680 _fireResize: function() { | |
| 2681 this.fire('iron-resize', null, { | |
| 2682 node: this, | |
| 2683 bubbles: false | |
| 2684 }); | |
| 2685 }, | |
| 2686 | |
| 2687 _onIronRequestResizeNotifications: function(event) { | |
| 2688 var target = event.path ? event.path[0] : event.target; | |
| 2689 | |
| 2690 if (target === this) { | |
| 2691 return; | |
| 2692 } | |
| 2693 | |
| 2694 if (this._interestedResizables.indexOf(target) === -1) { | |
| 2695 this._interestedResizables.push(target); | |
| 2696 this.listen(target, 'iron-resize', '_onDescendantIronResize'); | |
| 2697 } | |
| 2698 | |
| 2699 target.assignParentResizable(this); | |
| 2700 this._notifyDescendant(target); | |
| 2701 | |
| 2702 event.stopPropagation(); | |
| 2703 }, | |
| 2704 | |
| 2705 _parentResizableChanged: function(parentResizable) { | |
| 2706 if (parentResizable) { | |
| 2707 window.removeEventListener('resize', this._boundNotifyResize); | |
| 2708 } | |
| 2709 }, | |
| 2710 | |
| 2711 _notifyDescendant: function(descendant) { | |
| 2712 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
| 2713 // important not to notify them if the parent is not attached yet (or | |
| 2714 // else they will get redundantly notified when the parent attaches). | |
| 2715 if (!this.isAttached) { | |
| 2716 return; | |
| 2717 } | |
| 2718 | |
| 2719 this._notifyingDescendant = true; | |
| 2720 descendant.notifyResize(); | |
| 2721 this._notifyingDescendant = false; | |
| 2722 } | |
| 2723 }; | |
| 2724 /** | |
| 2725 * @param {!Function} selectCallback | |
| 2726 * @constructor | |
| 2727 */ | |
| 2728 Polymer.IronSelection = function(selectCallback) { | |
| 2729 this.selection = []; | |
| 2730 this.selectCallback = selectCallback; | |
| 2731 }; | |
| 2732 | |
| 2733 Polymer.IronSelection.prototype = { | |
| 2734 | |
| 2735 /** | |
| 2736 * Retrieves the selected item(s). | |
| 2737 * | |
| 2738 * @method get | |
| 2739 * @returns Returns the selected item(s). If the multi property is true, | |
| 2740 * `get` will return an array, otherwise it will return | |
| 2741 * the selected item or undefined if there is no selection. | |
| 2742 */ | |
| 2743 get: function() { | |
| 2744 return this.multi ? this.selection.slice() : this.selection[0]; | |
| 2745 }, | |
| 2746 | |
| 2747 /** | |
| 2748 * Clears all the selection except the ones indicated. | |
| 2749 * | |
| 2750 * @method clear | |
| 2751 * @param {Array} excludes items to be excluded. | |
| 2752 */ | |
| 2753 clear: function(excludes) { | |
| 2754 this.selection.slice().forEach(function(item) { | |
| 2755 if (!excludes || excludes.indexOf(item) < 0) { | |
| 2756 this.setItemSelected(item, false); | |
| 2757 } | |
| 2758 }, this); | |
| 2759 }, | |
| 2760 | |
| 2761 /** | |
| 2762 * Indicates if a given item is selected. | |
| 2763 * | |
| 2764 * @method isSelected | |
| 2765 * @param {*} item The item whose selection state should be checked. | |
| 2766 * @returns Returns true if `item` is selected. | |
| 2767 */ | |
| 2768 isSelected: function(item) { | |
| 2769 return this.selection.indexOf(item) >= 0; | |
| 2770 }, | |
| 2771 | |
| 2772 /** | |
| 2773 * Sets the selection state for a given item to either selected or deselecte
d. | |
| 2774 * | |
| 2775 * @method setItemSelected | |
| 2776 * @param {*} item The item to select. | |
| 2777 * @param {boolean} isSelected True for selected, false for deselected. | |
| 2778 */ | |
| 2779 setItemSelected: function(item, isSelected) { | |
| 2780 if (item != null) { | |
| 2781 if (isSelected !== this.isSelected(item)) { | |
| 2782 // proceed to update selection only if requested state differs from cu
rrent | |
| 2783 if (isSelected) { | |
| 2784 this.selection.push(item); | |
| 2785 } else { | |
| 2786 var i = this.selection.indexOf(item); | |
| 2787 if (i >= 0) { | |
| 2788 this.selection.splice(i, 1); | |
| 2789 } | |
| 2790 } | |
| 2791 if (this.selectCallback) { | |
| 2792 this.selectCallback(item, isSelected); | |
| 2793 } | |
| 2794 } | |
| 2795 } | |
| 2796 }, | |
| 2797 | |
| 2798 /** | |
| 2799 * Sets the selection state for a given item. If the `multi` property | |
| 2800 * is true, then the selected state of `item` will be toggled; otherwise | |
| 2801 * the `item` will be selected. | |
| 2802 * | |
| 2803 * @method select | |
| 2804 * @param {*} item The item to select. | |
| 2805 */ | |
| 2806 select: function(item) { | |
| 2807 if (this.multi) { | |
| 2808 this.toggle(item); | |
| 2809 } else if (this.get() !== item) { | |
| 2810 this.setItemSelected(this.get(), false); | |
| 2811 this.setItemSelected(item, true); | |
| 2812 } | |
| 2813 }, | |
| 2814 | |
| 2815 /** | |
| 2816 * Toggles the selection state for `item`. | |
| 2817 * | |
| 2818 * @method toggle | |
| 2819 * @param {*} item The item to toggle. | |
| 2820 */ | |
| 2821 toggle: function(item) { | |
| 2822 this.setItemSelected(item, !this.isSelected(item)); | |
| 2823 } | |
| 2824 | |
| 2825 }; | |
| 2826 /** @polymerBehavior */ | |
| 2827 Polymer.IronSelectableBehavior = { | |
| 2828 | |
| 2829 /** | |
| 2830 * Fired when iron-selector is activated (selected or deselected). | |
| 2831 * It is fired before the selected items are changed. | |
| 2832 * Cancel the event to abort selection. | |
| 2833 * | |
| 2834 * @event iron-activate | |
| 2835 */ | |
| 2836 | |
| 2837 /** | |
| 2838 * Fired when an item is selected | |
| 2839 * | |
| 2840 * @event iron-select | |
| 2841 */ | |
| 2842 | |
| 2843 /** | |
| 2844 * Fired when an item is deselected | |
| 2845 * | |
| 2846 * @event iron-deselect | |
| 2847 */ | |
| 2848 | |
| 2849 /** | |
| 2850 * Fired when the list of selectable items changes (e.g., items are | |
| 2851 * added or removed). The detail of the event is a mutation record that | |
| 2852 * describes what changed. | |
| 2853 * | |
| 2854 * @event iron-items-changed | |
| 2855 */ | |
| 2856 | |
| 2857 properties: { | |
| 2858 | |
| 2859 /** | |
| 2860 * If you want to use an attribute value or property of an element for | |
| 2861 * `selected` instead of the index, set this to the name of the attribute | |
| 2862 * or property. Hyphenated values are converted to camel case when used to | |
| 2863 * look up the property of a selectable element. Camel cased values are | |
| 2864 * *not* converted to hyphenated values for attribute lookup. It's | |
| 2865 * recommended that you provide the hyphenated form of the name so that | |
| 2866 * selection works in both cases. (Use `attr-or-property-name` instead of | |
| 2867 * `attrOrPropertyName`.) | |
| 2868 */ | |
| 2869 attrForSelected: { | |
| 2870 type: String, | |
| 2871 value: null | |
| 2872 }, | |
| 2873 | |
| 2874 /** | |
| 2875 * Gets or sets the selected element. The default is to use the index of t
he item. | |
| 2876 * @type {string|number} | |
| 2877 */ | |
| 2878 selected: { | |
| 2879 type: String, | |
| 2880 notify: true | |
| 2881 }, | |
| 2882 | |
| 2883 /** | |
| 2884 * Returns the currently selected item. | |
| 2885 * | |
| 2886 * @type {?Object} | |
| 2887 */ | |
| 2888 selectedItem: { | |
| 2889 type: Object, | |
| 2890 readOnly: true, | |
| 2891 notify: true | |
| 2892 }, | |
| 2893 | |
| 2894 /** | |
| 2895 * The event that fires from items when they are selected. Selectable | |
| 2896 * will listen for this event from items and update the selection state. | |
| 2897 * Set to empty string to listen to no events. | |
| 2898 */ | |
| 2899 activateEvent: { | |
| 2900 type: String, | |
| 2901 value: 'tap', | |
| 2902 observer: '_activateEventChanged' | |
| 2903 }, | |
| 2904 | |
| 2905 /** | |
| 2906 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
| 2907 * are selectable. | |
| 2908 */ | |
| 2909 selectable: String, | |
| 2910 | |
| 2911 /** | |
| 2912 * The class to set on elements when selected. | |
| 2913 */ | |
| 2914 selectedClass: { | |
| 2915 type: String, | |
| 2916 value: 'iron-selected' | |
| 2917 }, | |
| 2918 | |
| 2919 /** | |
| 2920 * The attribute to set on elements when selected. | |
| 2921 */ | |
| 2922 selectedAttribute: { | |
| 2923 type: String, | |
| 2924 value: null | |
| 2925 }, | |
| 2926 | |
| 2927 /** | |
| 2928 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
| 2929 * is not found. | |
| 2930 */ | |
| 2931 fallbackSelection: { | |
| 2932 type: String, | |
| 2933 value: null | |
| 2934 }, | |
| 2935 | |
| 2936 /** | |
| 2937 * The list of items from which a selection can be made. | |
| 2938 */ | |
| 2939 items: { | |
| 2940 type: Array, | 1975 type: Array, |
| 2941 readOnly: true, | |
| 2942 notify: true, | |
| 2943 value: function() { | 1976 value: function() { |
| 2944 return []; | 1977 return []; |
| 2945 } | 1978 } |
| 2946 }, | 1979 }, |
| 2947 | 1980 _imperativeKeyBindings: { |
| 2948 /** | |
| 2949 * The set of excluded elements where the key is the `localName` | |
| 2950 * of the element that will be ignored from the item list. | |
| 2951 * | |
| 2952 * @default {template: 1} | |
| 2953 */ | |
| 2954 _excludedLocalNames: { | |
| 2955 type: Object, | 1981 type: Object, |
| 2956 value: function() { | 1982 value: function() { |
| 2957 return { | 1983 return {}; |
| 2958 'template': 1 | |
| 2959 }; | |
| 2960 } | 1984 } |
| 2961 } | 1985 } |
| 2962 }, | 1986 }, |
| 2963 | 1987 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], |
| 2964 observers: [ | 1988 keyBindings: {}, |
| 2965 '_updateAttrForSelected(attrForSelected)', | 1989 registered: function() { |
| 2966 '_updateSelected(selected)', | 1990 this._prepKeyBindings(); |
| 2967 '_checkFallback(fallbackSelection)' | 1991 }, |
| 2968 ], | |
| 2969 | |
| 2970 created: function() { | |
| 2971 this._bindFilterItem = this._filterItem.bind(this); | |
| 2972 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | |
| 2973 }, | |
| 2974 | |
| 2975 attached: function() { | 1992 attached: function() { |
| 2976 this._observer = this._observeItems(this); | 1993 this._listenKeyEventListeners(); |
| 2977 this._updateItems(); | 1994 }, |
| 2978 if (!this._shouldUpdateSelection) { | |
| 2979 this._updateSelected(); | |
| 2980 } | |
| 2981 this._addListener(this.activateEvent); | |
| 2982 }, | |
| 2983 | |
| 2984 detached: function() { | 1995 detached: function() { |
| 2985 if (this._observer) { | 1996 this._unlistenKeyEventListeners(); |
| 2986 Polymer.dom(this).unobserveNodes(this._observer); | 1997 }, |
| 2987 } | 1998 addOwnKeyBinding: function(eventString, handlerName) { |
| 2988 this._removeListener(this.activateEvent); | 1999 this._imperativeKeyBindings[eventString] = handlerName; |
| 2989 }, | 2000 this._prepKeyBindings(); |
| 2990 | 2001 this._resetKeyEventListeners(); |
| 2991 /** | 2002 }, |
| 2992 * Returns the index of the given item. | 2003 removeOwnKeyBindings: function() { |
| 2993 * | 2004 this._imperativeKeyBindings = {}; |
| 2994 * @method indexOf | 2005 this._prepKeyBindings(); |
| 2995 * @param {Object} item | 2006 this._resetKeyEventListeners(); |
| 2996 * @returns Returns the index of the item | 2007 }, |
| 2997 */ | 2008 keyboardEventMatchesKeys: function(event, eventString) { |
| 2998 indexOf: function(item) { | 2009 var keyCombos = parseEventString(eventString); |
| 2999 return this.items.indexOf(item); | 2010 for (var i = 0; i < keyCombos.length; ++i) { |
| 3000 }, | 2011 if (keyComboMatchesEvent(keyCombos[i], event)) { |
| 3001 | 2012 return true; |
| 3002 /** | 2013 } |
| 3003 * Selects the given value. | 2014 } |
| 3004 * | 2015 return false; |
| 3005 * @method select | 2016 }, |
| 3006 * @param {string|number} value the value to select. | 2017 _collectKeyBindings: function() { |
| 3007 */ | 2018 var keyBindings = this.behaviors.map(function(behavior) { |
| 3008 select: function(value) { | 2019 return behavior.keyBindings; |
| 3009 this.selected = value; | 2020 }); |
| 3010 }, | 2021 if (keyBindings.indexOf(this.keyBindings) === -1) { |
| 3011 | 2022 keyBindings.push(this.keyBindings); |
| 3012 /** | 2023 } |
| 3013 * Selects the previous item. | 2024 return keyBindings; |
| 3014 * | 2025 }, |
| 3015 * @method selectPrevious | 2026 _prepKeyBindings: function() { |
| 3016 */ | 2027 this._keyBindings = {}; |
| 3017 selectPrevious: function() { | 2028 this._collectKeyBindings().forEach(function(keyBindings) { |
| 3018 var length = this.items.length; | 2029 for (var eventString in keyBindings) { |
| 3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | 2030 this._addKeyBinding(eventString, keyBindings[eventString]); |
| 3020 this.selected = this._indexToValue(index); | 2031 } |
| 3021 }, | 2032 }, this); |
| 3022 | 2033 for (var eventString in this._imperativeKeyBindings) { |
| 3023 /** | 2034 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString
]); |
| 3024 * Selects the next item. | 2035 } |
| 3025 * | 2036 for (var eventName in this._keyBindings) { |
| 3026 * @method selectNext | 2037 this._keyBindings[eventName].sort(function(kb1, kb2) { |
| 3027 */ | 2038 var b1 = kb1[0].hasModifiers; |
| 3028 selectNext: function() { | 2039 var b2 = kb2[0].hasModifiers; |
| 3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | 2040 return b1 === b2 ? 0 : b1 ? -1 : 1; |
| 3030 this.selected = this._indexToValue(index); | 2041 }); |
| 3031 }, | 2042 } |
| 3032 | 2043 }, |
| 3033 /** | 2044 _addKeyBinding: function(eventString, handlerName) { |
| 3034 * Selects the item at the given index. | 2045 parseEventString(eventString).forEach(function(keyCombo) { |
| 3035 * | 2046 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] ||
[]; |
| 3036 * @method selectIndex | 2047 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); |
| 3037 */ | 2048 }, this); |
| 3038 selectIndex: function(index) { | 2049 }, |
| 3039 this.select(this._indexToValue(index)); | 2050 _resetKeyEventListeners: function() { |
| 3040 }, | 2051 this._unlistenKeyEventListeners(); |
| 3041 | 2052 if (this.isAttached) { |
| 3042 /** | 2053 this._listenKeyEventListeners(); |
| 3043 * Force a synchronous update of the `items` property. | 2054 } |
| 3044 * | 2055 }, |
| 3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to | 2056 _listenKeyEventListeners: function() { |
| 3046 * updates to the set of selectable items after updates to the DOM list and | 2057 if (!this.keyEventTarget) { |
| 3047 * selection state have been made. | 2058 return; |
| 3048 * | 2059 } |
| 3049 * WARNING: If you are using this method, you should probably consider an | 2060 Object.keys(this._keyBindings).forEach(function(eventName) { |
| 3050 * alternate approach. Synchronously querying for items is potentially | 2061 var keyBindings = this._keyBindings[eventName]; |
| 3051 * slow for many use cases. The `items` property will update asynchronously | 2062 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
| 3052 * on its own to reflect selectable items in the DOM. | 2063 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa
ndler ]); |
| 3053 */ | 2064 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| 3054 forceSynchronousItemUpdate: function() { | 2065 }, this); |
| 3055 this._updateItems(); | 2066 }, |
| 3056 }, | 2067 _unlistenKeyEventListeners: function() { |
| 3057 | 2068 var keyHandlerTuple; |
| 3058 get _shouldUpdateSelection() { | 2069 var keyEventTarget; |
| 3059 return this.selected != null; | 2070 var eventName; |
| 3060 }, | 2071 var boundKeyHandler; |
| 3061 | 2072 while (this._boundKeyHandlers.length) { |
| 3062 _checkFallback: function() { | 2073 keyHandlerTuple = this._boundKeyHandlers.pop(); |
| 3063 if (this._shouldUpdateSelection) { | 2074 keyEventTarget = keyHandlerTuple[0]; |
| 3064 this._updateSelected(); | 2075 eventName = keyHandlerTuple[1]; |
| 3065 } | 2076 boundKeyHandler = keyHandlerTuple[2]; |
| 3066 }, | 2077 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| 3067 | 2078 } |
| 3068 _addListener: function(eventName) { | 2079 }, |
| 3069 this.listen(this, eventName, '_activateHandler'); | 2080 _onKeyBindingEvent: function(keyBindings, event) { |
| 3070 }, | 2081 if (this.stopKeyboardEventPropagation) { |
| 3071 | 2082 event.stopPropagation(); |
| 3072 _removeListener: function(eventName) { | 2083 } |
| 3073 this.unlisten(this, eventName, '_activateHandler'); | 2084 if (event.defaultPrevented) { |
| 3074 }, | 2085 return; |
| 3075 | 2086 } |
| 3076 _activateEventChanged: function(eventName, old) { | 2087 for (var i = 0; i < keyBindings.length; i++) { |
| 3077 this._removeListener(old); | 2088 var keyCombo = keyBindings[i][0]; |
| 3078 this._addListener(eventName); | 2089 var handlerName = keyBindings[i][1]; |
| 3079 }, | 2090 if (keyComboMatchesEvent(keyCombo, event)) { |
| 3080 | 2091 this._triggerKeyHandler(keyCombo, handlerName, event); |
| 3081 _updateItems: function() { | 2092 if (event.defaultPrevented) { |
| 3082 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | 2093 return; |
| 3083 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | |
| 3084 this._setItems(nodes); | |
| 3085 }, | |
| 3086 | |
| 3087 _updateAttrForSelected: function() { | |
| 3088 if (this._shouldUpdateSelection) { | |
| 3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | |
| 3090 } | |
| 3091 }, | |
| 3092 | |
| 3093 _updateSelected: function() { | |
| 3094 this._selectSelected(this.selected); | |
| 3095 }, | |
| 3096 | |
| 3097 _selectSelected: function(selected) { | |
| 3098 this._selection.select(this._valueToItem(this.selected)); | |
| 3099 // Check for items, since this array is populated only when attached | |
| 3100 // Since Number(0) is falsy, explicitly check for undefined | |
| 3101 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | |
| 3102 this.selected = this.fallbackSelection; | |
| 3103 } | |
| 3104 }, | |
| 3105 | |
| 3106 _filterItem: function(node) { | |
| 3107 return !this._excludedLocalNames[node.localName]; | |
| 3108 }, | |
| 3109 | |
| 3110 _valueToItem: function(value) { | |
| 3111 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
| 3112 }, | |
| 3113 | |
| 3114 _valueToIndex: function(value) { | |
| 3115 if (this.attrForSelected) { | |
| 3116 for (var i = 0, item; item = this.items[i]; i++) { | |
| 3117 if (this._valueForItem(item) == value) { | |
| 3118 return i; | |
| 3119 } | 2094 } |
| 3120 } | 2095 } |
| 2096 } |
| 2097 }, |
| 2098 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 2099 var detail = Object.create(keyCombo); |
| 2100 detail.keyboardEvent = keyboardEvent; |
| 2101 var event = new CustomEvent(keyCombo.event, { |
| 2102 detail: detail, |
| 2103 cancelable: true |
| 2104 }); |
| 2105 this[handlerName].call(this, event); |
| 2106 if (event.defaultPrevented) { |
| 2107 keyboardEvent.preventDefault(); |
| 2108 } |
| 2109 } |
| 2110 }; |
| 2111 })(); |
| 2112 |
| 2113 Polymer.IronControlState = { |
| 2114 properties: { |
| 2115 focused: { |
| 2116 type: Boolean, |
| 2117 value: false, |
| 2118 notify: true, |
| 2119 readOnly: true, |
| 2120 reflectToAttribute: true |
| 2121 }, |
| 2122 disabled: { |
| 2123 type: Boolean, |
| 2124 value: false, |
| 2125 notify: true, |
| 2126 observer: '_disabledChanged', |
| 2127 reflectToAttribute: true |
| 2128 }, |
| 2129 _oldTabIndex: { |
| 2130 type: Number |
| 2131 }, |
| 2132 _boundFocusBlurHandler: { |
| 2133 type: Function, |
| 2134 value: function() { |
| 2135 return this._focusBlurHandler.bind(this); |
| 2136 } |
| 2137 } |
| 2138 }, |
| 2139 observers: [ '_changedControlState(focused, disabled)' ], |
| 2140 ready: function() { |
| 2141 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2142 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2143 }, |
| 2144 _focusBlurHandler: function(event) { |
| 2145 if (event.target === this) { |
| 2146 this._setFocused(event.type === 'focus'); |
| 2147 } else if (!this.shadowRoot) { |
| 2148 var target = Polymer.dom(event).localTarget; |
| 2149 if (!this.isLightDescendant(target)) { |
| 2150 this.fire(event.type, { |
| 2151 sourceEvent: event |
| 2152 }, { |
| 2153 node: this, |
| 2154 bubbles: event.bubbles, |
| 2155 cancelable: event.cancelable |
| 2156 }); |
| 2157 } |
| 2158 } |
| 2159 }, |
| 2160 _disabledChanged: function(disabled, old) { |
| 2161 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2162 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2163 if (disabled) { |
| 2164 this._oldTabIndex = this.tabIndex; |
| 2165 this._setFocused(false); |
| 2166 this.tabIndex = -1; |
| 2167 this.blur(); |
| 2168 } else if (this._oldTabIndex !== undefined) { |
| 2169 this.tabIndex = this._oldTabIndex; |
| 2170 } |
| 2171 }, |
| 2172 _changedControlState: function() { |
| 2173 if (this._controlStateChanged) { |
| 2174 this._controlStateChanged(); |
| 2175 } |
| 2176 } |
| 2177 }; |
| 2178 |
| 2179 Polymer.IronButtonStateImpl = { |
| 2180 properties: { |
| 2181 pressed: { |
| 2182 type: Boolean, |
| 2183 readOnly: true, |
| 2184 value: false, |
| 2185 reflectToAttribute: true, |
| 2186 observer: '_pressedChanged' |
| 2187 }, |
| 2188 toggles: { |
| 2189 type: Boolean, |
| 2190 value: false, |
| 2191 reflectToAttribute: true |
| 2192 }, |
| 2193 active: { |
| 2194 type: Boolean, |
| 2195 value: false, |
| 2196 notify: true, |
| 2197 reflectToAttribute: true |
| 2198 }, |
| 2199 pointerDown: { |
| 2200 type: Boolean, |
| 2201 readOnly: true, |
| 2202 value: false |
| 2203 }, |
| 2204 receivedFocusFromKeyboard: { |
| 2205 type: Boolean, |
| 2206 readOnly: true |
| 2207 }, |
| 2208 ariaActiveAttribute: { |
| 2209 type: String, |
| 2210 value: 'aria-pressed', |
| 2211 observer: '_ariaActiveAttributeChanged' |
| 2212 } |
| 2213 }, |
| 2214 listeners: { |
| 2215 down: '_downHandler', |
| 2216 up: '_upHandler', |
| 2217 tap: '_tapHandler' |
| 2218 }, |
| 2219 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi
veAttribute)' ], |
| 2220 keyBindings: { |
| 2221 'enter:keydown': '_asyncClick', |
| 2222 'space:keydown': '_spaceKeyDownHandler', |
| 2223 'space:keyup': '_spaceKeyUpHandler' |
| 2224 }, |
| 2225 _mouseEventRe: /^mouse/, |
| 2226 _tapHandler: function() { |
| 2227 if (this.toggles) { |
| 2228 this._userActivate(!this.active); |
| 2229 } else { |
| 2230 this.active = false; |
| 2231 } |
| 2232 }, |
| 2233 _detectKeyboardFocus: function(focused) { |
| 2234 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 2235 }, |
| 2236 _userActivate: function(active) { |
| 2237 if (this.active !== active) { |
| 2238 this.active = active; |
| 2239 this.fire('change'); |
| 2240 } |
| 2241 }, |
| 2242 _downHandler: function(event) { |
| 2243 this._setPointerDown(true); |
| 2244 this._setPressed(true); |
| 2245 this._setReceivedFocusFromKeyboard(false); |
| 2246 }, |
| 2247 _upHandler: function() { |
| 2248 this._setPointerDown(false); |
| 2249 this._setPressed(false); |
| 2250 }, |
| 2251 _spaceKeyDownHandler: function(event) { |
| 2252 var keyboardEvent = event.detail.keyboardEvent; |
| 2253 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2254 if (this.isLightDescendant(target)) return; |
| 2255 keyboardEvent.preventDefault(); |
| 2256 keyboardEvent.stopImmediatePropagation(); |
| 2257 this._setPressed(true); |
| 2258 }, |
| 2259 _spaceKeyUpHandler: function(event) { |
| 2260 var keyboardEvent = event.detail.keyboardEvent; |
| 2261 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2262 if (this.isLightDescendant(target)) return; |
| 2263 if (this.pressed) { |
| 2264 this._asyncClick(); |
| 2265 } |
| 2266 this._setPressed(false); |
| 2267 }, |
| 2268 _asyncClick: function() { |
| 2269 this.async(function() { |
| 2270 this.click(); |
| 2271 }, 1); |
| 2272 }, |
| 2273 _pressedChanged: function(pressed) { |
| 2274 this._changedButtonState(); |
| 2275 }, |
| 2276 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 2277 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 2278 this.removeAttribute(oldValue); |
| 2279 } |
| 2280 }, |
| 2281 _activeChanged: function(active, ariaActiveAttribute) { |
| 2282 if (this.toggles) { |
| 2283 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false'); |
| 2284 } else { |
| 2285 this.removeAttribute(this.ariaActiveAttribute); |
| 2286 } |
| 2287 this._changedButtonState(); |
| 2288 }, |
| 2289 _controlStateChanged: function() { |
| 2290 if (this.disabled) { |
| 2291 this._setPressed(false); |
| 2292 } else { |
| 2293 this._changedButtonState(); |
| 2294 } |
| 2295 }, |
| 2296 _changedButtonState: function() { |
| 2297 if (this._buttonStateChanged) { |
| 2298 this._buttonStateChanged(); |
| 2299 } |
| 2300 } |
| 2301 }; |
| 2302 |
| 2303 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat
eImpl ]; |
| 2304 |
| 2305 (function() { |
| 2306 var Utility = { |
| 2307 distance: function(x1, y1, x2, y2) { |
| 2308 var xDelta = x1 - x2; |
| 2309 var yDelta = y1 - y2; |
| 2310 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 2311 }, |
| 2312 now: window.performance && window.performance.now ? window.performance.now.b
ind(window.performance) : Date.now |
| 2313 }; |
| 2314 function ElementMetrics(element) { |
| 2315 this.element = element; |
| 2316 this.width = this.boundingRect.width; |
| 2317 this.height = this.boundingRect.height; |
| 2318 this.size = Math.max(this.width, this.height); |
| 2319 } |
| 2320 ElementMetrics.prototype = { |
| 2321 get boundingRect() { |
| 2322 return this.element.getBoundingClientRect(); |
| 2323 }, |
| 2324 furthestCornerDistanceFrom: function(x, y) { |
| 2325 var topLeft = Utility.distance(x, y, 0, 0); |
| 2326 var topRight = Utility.distance(x, y, this.width, 0); |
| 2327 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 2328 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 2329 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 2330 } |
| 2331 }; |
| 2332 function Ripple(element) { |
| 2333 this.element = element; |
| 2334 this.color = window.getComputedStyle(element).color; |
| 2335 this.wave = document.createElement('div'); |
| 2336 this.waveContainer = document.createElement('div'); |
| 2337 this.wave.style.backgroundColor = this.color; |
| 2338 this.wave.classList.add('wave'); |
| 2339 this.waveContainer.classList.add('wave-container'); |
| 2340 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 2341 this.resetInteractionState(); |
| 2342 } |
| 2343 Ripple.MAX_RADIUS = 300; |
| 2344 Ripple.prototype = { |
| 2345 get recenters() { |
| 2346 return this.element.recenters; |
| 2347 }, |
| 2348 get center() { |
| 2349 return this.element.center; |
| 2350 }, |
| 2351 get mouseDownElapsed() { |
| 2352 var elapsed; |
| 2353 if (!this.mouseDownStart) { |
| 2354 return 0; |
| 2355 } |
| 2356 elapsed = Utility.now() - this.mouseDownStart; |
| 2357 if (this.mouseUpStart) { |
| 2358 elapsed -= this.mouseUpElapsed; |
| 2359 } |
| 2360 return elapsed; |
| 2361 }, |
| 2362 get mouseUpElapsed() { |
| 2363 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0; |
| 2364 }, |
| 2365 get mouseDownElapsedSeconds() { |
| 2366 return this.mouseDownElapsed / 1e3; |
| 2367 }, |
| 2368 get mouseUpElapsedSeconds() { |
| 2369 return this.mouseUpElapsed / 1e3; |
| 2370 }, |
| 2371 get mouseInteractionSeconds() { |
| 2372 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 2373 }, |
| 2374 get initialOpacity() { |
| 2375 return this.element.initialOpacity; |
| 2376 }, |
| 2377 get opacityDecayVelocity() { |
| 2378 return this.element.opacityDecayVelocity; |
| 2379 }, |
| 2380 get radius() { |
| 2381 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 2382 var height2 = this.containerMetrics.height * this.containerMetrics.height; |
| 2383 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS)
* 1.1 + 5; |
| 2384 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS); |
| 2385 var timeNow = this.mouseInteractionSeconds / duration; |
| 2386 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 2387 return Math.abs(size); |
| 2388 }, |
| 2389 get opacity() { |
| 2390 if (!this.mouseUpStart) { |
| 2391 return this.initialOpacity; |
| 2392 } |
| 2393 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this
.opacityDecayVelocity); |
| 2394 }, |
| 2395 get outerOpacity() { |
| 2396 var outerOpacity = this.mouseUpElapsedSeconds * .3; |
| 2397 var waveOpacity = this.opacity; |
| 2398 return Math.max(0, Math.min(outerOpacity, waveOpacity)); |
| 2399 }, |
| 2400 get isOpacityFullyDecayed() { |
| 2401 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl
e.MAX_RADIUS); |
| 2402 }, |
| 2403 get isRestingAtMaxRadius() { |
| 2404 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this
.maxRadius, Ripple.MAX_RADIUS); |
| 2405 }, |
| 2406 get isAnimationComplete() { |
| 2407 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa
xRadius; |
| 2408 }, |
| 2409 get translationFraction() { |
| 2410 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr
t(2)); |
| 2411 }, |
| 2412 get xNow() { |
| 2413 if (this.xEnd) { |
| 2414 return this.xStart + this.translationFraction * (this.xEnd - this.xStart
); |
| 2415 } |
| 2416 return this.xStart; |
| 2417 }, |
| 2418 get yNow() { |
| 2419 if (this.yEnd) { |
| 2420 return this.yStart + this.translationFraction * (this.yEnd - this.yStart
); |
| 2421 } |
| 2422 return this.yStart; |
| 2423 }, |
| 2424 get isMouseDown() { |
| 2425 return this.mouseDownStart && !this.mouseUpStart; |
| 2426 }, |
| 2427 resetInteractionState: function() { |
| 2428 this.maxRadius = 0; |
| 2429 this.mouseDownStart = 0; |
| 2430 this.mouseUpStart = 0; |
| 2431 this.xStart = 0; |
| 2432 this.yStart = 0; |
| 2433 this.xEnd = 0; |
| 2434 this.yEnd = 0; |
| 2435 this.slideDistance = 0; |
| 2436 this.containerMetrics = new ElementMetrics(this.element); |
| 2437 }, |
| 2438 draw: function() { |
| 2439 var scale; |
| 2440 var translateString; |
| 2441 var dx; |
| 2442 var dy; |
| 2443 this.wave.style.opacity = this.opacity; |
| 2444 scale = this.radius / (this.containerMetrics.size / 2); |
| 2445 dx = this.xNow - this.containerMetrics.width / 2; |
| 2446 dy = this.yNow - this.containerMetrics.height / 2; |
| 2447 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy
+ 'px)'; |
| 2448 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + '
px, 0)'; |
| 2449 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 2450 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 2451 }, |
| 2452 downAction: function(event) { |
| 2453 var xCenter = this.containerMetrics.width / 2; |
| 2454 var yCenter = this.containerMetrics.height / 2; |
| 2455 this.resetInteractionState(); |
| 2456 this.mouseDownStart = Utility.now(); |
| 2457 if (this.center) { |
| 2458 this.xStart = xCenter; |
| 2459 this.yStart = yCenter; |
| 2460 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 3121 } else { | 2461 } else { |
| 3122 return Number(value); | 2462 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec
t.left : this.containerMetrics.width / 2; |
| 3123 } | 2463 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec
t.top : this.containerMetrics.height / 2; |
| 3124 }, | 2464 } |
| 3125 | 2465 if (this.recenters) { |
| 3126 _indexToValue: function(index) { | 2466 this.xEnd = xCenter; |
| 3127 if (this.attrForSelected) { | 2467 this.yEnd = yCenter; |
| 3128 var item = this.items[index]; | 2468 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
| 3129 if (item) { | 2469 } |
| 3130 return this._valueForItem(item); | 2470 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt
art, this.yStart); |
| 2471 this.waveContainer.style.top = (this.containerMetrics.height - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2472 this.waveContainer.style.left = (this.containerMetrics.width - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2473 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 2474 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 2475 }, |
| 2476 upAction: function(event) { |
| 2477 if (!this.isMouseDown) { |
| 2478 return; |
| 2479 } |
| 2480 this.mouseUpStart = Utility.now(); |
| 2481 }, |
| 2482 remove: function() { |
| 2483 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer)
; |
| 2484 } |
| 2485 }; |
| 2486 Polymer({ |
| 2487 is: 'paper-ripple', |
| 2488 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 2489 properties: { |
| 2490 initialOpacity: { |
| 2491 type: Number, |
| 2492 value: .25 |
| 2493 }, |
| 2494 opacityDecayVelocity: { |
| 2495 type: Number, |
| 2496 value: .8 |
| 2497 }, |
| 2498 recenters: { |
| 2499 type: Boolean, |
| 2500 value: false |
| 2501 }, |
| 2502 center: { |
| 2503 type: Boolean, |
| 2504 value: false |
| 2505 }, |
| 2506 ripples: { |
| 2507 type: Array, |
| 2508 value: function() { |
| 2509 return []; |
| 3131 } | 2510 } |
| 2511 }, |
| 2512 animating: { |
| 2513 type: Boolean, |
| 2514 readOnly: true, |
| 2515 reflectToAttribute: true, |
| 2516 value: false |
| 2517 }, |
| 2518 holdDown: { |
| 2519 type: Boolean, |
| 2520 value: false, |
| 2521 observer: '_holdDownChanged' |
| 2522 }, |
| 2523 noink: { |
| 2524 type: Boolean, |
| 2525 value: false |
| 2526 }, |
| 2527 _animating: { |
| 2528 type: Boolean |
| 2529 }, |
| 2530 _boundAnimate: { |
| 2531 type: Function, |
| 2532 value: function() { |
| 2533 return this.animate.bind(this); |
| 2534 } |
| 2535 } |
| 2536 }, |
| 2537 get target() { |
| 2538 return this.keyEventTarget; |
| 2539 }, |
| 2540 keyBindings: { |
| 2541 'enter:keydown': '_onEnterKeydown', |
| 2542 'space:keydown': '_onSpaceKeydown', |
| 2543 'space:keyup': '_onSpaceKeyup' |
| 2544 }, |
| 2545 attached: function() { |
| 2546 if (this.parentNode.nodeType == 11) { |
| 2547 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
| 3132 } else { | 2548 } else { |
| 3133 return index; | 2549 this.keyEventTarget = this.parentNode; |
| 3134 } | 2550 } |
| 3135 }, | 2551 var keyEventTarget = this.keyEventTarget; |
| 3136 | 2552 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
| 3137 _valueForItem: function(item) { | 2553 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
| 3138 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | 2554 }, |
| 3139 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | 2555 detached: function() { |
| 3140 }, | 2556 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
| 3141 | 2557 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
| 3142 _applySelection: function(item, isSelected) { | 2558 this.keyEventTarget = null; |
| 3143 if (this.selectedClass) { | 2559 }, |
| 3144 this.toggleClass(this.selectedClass, isSelected, item); | 2560 get shouldKeepAnimating() { |
| 3145 } | 2561 for (var index = 0; index < this.ripples.length; ++index) { |
| 3146 if (this.selectedAttribute) { | 2562 if (!this.ripples[index].isAnimationComplete) { |
| 3147 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 2563 return true; |
| 3148 } | |
| 3149 this._selectionChange(); | |
| 3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | |
| 3151 }, | |
| 3152 | |
| 3153 _selectionChange: function() { | |
| 3154 this._setSelectedItem(this._selection.get()); | |
| 3155 }, | |
| 3156 | |
| 3157 // observe items change under the given node. | |
| 3158 _observeItems: function(node) { | |
| 3159 return Polymer.dom(node).observeNodes(function(mutation) { | |
| 3160 this._updateItems(); | |
| 3161 | |
| 3162 if (this._shouldUpdateSelection) { | |
| 3163 this._updateSelected(); | |
| 3164 } | 2564 } |
| 3165 | 2565 } |
| 3166 // Let other interested parties know about the change so that | 2566 return false; |
| 3167 // we don't have to recreate mutation observers everywhere. | 2567 }, |
| 3168 this.fire('iron-items-changed', mutation, { | 2568 simulatedRipple: function() { |
| 3169 bubbles: false, | 2569 this.downAction(null); |
| 3170 cancelable: false | 2570 this.async(function() { |
| 3171 }); | 2571 this.upAction(); |
| 2572 }, 1); |
| 2573 }, |
| 2574 uiDownAction: function(event) { |
| 2575 if (!this.noink) { |
| 2576 this.downAction(event); |
| 2577 } |
| 2578 }, |
| 2579 downAction: function(event) { |
| 2580 if (this.holdDown && this.ripples.length > 0) { |
| 2581 return; |
| 2582 } |
| 2583 var ripple = this.addRipple(); |
| 2584 ripple.downAction(event); |
| 2585 if (!this._animating) { |
| 2586 this._animating = true; |
| 2587 this.animate(); |
| 2588 } |
| 2589 }, |
| 2590 uiUpAction: function(event) { |
| 2591 if (!this.noink) { |
| 2592 this.upAction(event); |
| 2593 } |
| 2594 }, |
| 2595 upAction: function(event) { |
| 2596 if (this.holdDown) { |
| 2597 return; |
| 2598 } |
| 2599 this.ripples.forEach(function(ripple) { |
| 2600 ripple.upAction(event); |
| 3172 }); | 2601 }); |
| 3173 }, | 2602 this._animating = true; |
| 3174 | 2603 this.animate(); |
| 3175 _activateHandler: function(e) { | 2604 }, |
| 3176 var t = e.target; | 2605 onAnimationComplete: function() { |
| 3177 var items = this.items; | 2606 this._animating = false; |
| 3178 while (t && t != this) { | 2607 this.$.background.style.backgroundColor = null; |
| 3179 var i = items.indexOf(t); | 2608 this.fire('transitionend'); |
| 3180 if (i >= 0) { | 2609 }, |
| 3181 var value = this._indexToValue(i); | 2610 addRipple: function() { |
| 3182 this._itemActivate(value, t); | 2611 var ripple = new Ripple(this); |
| 3183 return; | 2612 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 2613 this.$.background.style.backgroundColor = ripple.color; |
| 2614 this.ripples.push(ripple); |
| 2615 this._setAnimating(true); |
| 2616 return ripple; |
| 2617 }, |
| 2618 removeRipple: function(ripple) { |
| 2619 var rippleIndex = this.ripples.indexOf(ripple); |
| 2620 if (rippleIndex < 0) { |
| 2621 return; |
| 2622 } |
| 2623 this.ripples.splice(rippleIndex, 1); |
| 2624 ripple.remove(); |
| 2625 if (!this.ripples.length) { |
| 2626 this._setAnimating(false); |
| 2627 } |
| 2628 }, |
| 2629 animate: function() { |
| 2630 if (!this._animating) { |
| 2631 return; |
| 2632 } |
| 2633 var index; |
| 2634 var ripple; |
| 2635 for (index = 0; index < this.ripples.length; ++index) { |
| 2636 ripple = this.ripples[index]; |
| 2637 ripple.draw(); |
| 2638 this.$.background.style.opacity = ripple.outerOpacity; |
| 2639 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 2640 this.removeRipple(ripple); |
| 3184 } | 2641 } |
| 3185 t = t.parentNode; | 2642 } |
| 3186 } | 2643 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
| 3187 }, | 2644 this.onAnimationComplete(); |
| 3188 | 2645 } else { |
| 3189 _itemActivate: function(value, item) { | 2646 window.requestAnimationFrame(this._boundAnimate); |
| 3190 if (!this.fire('iron-activate', | 2647 } |
| 3191 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | 2648 }, |
| 3192 this.select(value); | 2649 _onEnterKeydown: function() { |
| 3193 } | 2650 this.uiDownAction(); |
| 3194 } | 2651 this.async(this.uiUpAction, 1); |
| 3195 | 2652 }, |
| 3196 }; | 2653 _onSpaceKeydown: function() { |
| 2654 this.uiDownAction(); |
| 2655 }, |
| 2656 _onSpaceKeyup: function() { |
| 2657 this.uiUpAction(); |
| 2658 }, |
| 2659 _holdDownChanged: function(newVal, oldVal) { |
| 2660 if (oldVal === undefined) { |
| 2661 return; |
| 2662 } |
| 2663 if (newVal) { |
| 2664 this.downAction(); |
| 2665 } else { |
| 2666 this.upAction(); |
| 2667 } |
| 2668 } |
| 2669 }); |
| 2670 })(); |
| 2671 |
| 2672 Polymer.PaperRippleBehavior = { |
| 2673 properties: { |
| 2674 noink: { |
| 2675 type: Boolean, |
| 2676 observer: '_noinkChanged' |
| 2677 }, |
| 2678 _rippleContainer: { |
| 2679 type: Object |
| 2680 } |
| 2681 }, |
| 2682 _buttonStateChanged: function() { |
| 2683 if (this.focused) { |
| 2684 this.ensureRipple(); |
| 2685 } |
| 2686 }, |
| 2687 _downHandler: function(event) { |
| 2688 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 2689 if (this.pressed) { |
| 2690 this.ensureRipple(event); |
| 2691 } |
| 2692 }, |
| 2693 ensureRipple: function(optTriggeringEvent) { |
| 2694 if (!this.hasRipple()) { |
| 2695 this._ripple = this._createRipple(); |
| 2696 this._ripple.noink = this.noink; |
| 2697 var rippleContainer = this._rippleContainer || this.root; |
| 2698 if (rippleContainer) { |
| 2699 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 2700 } |
| 2701 if (optTriggeringEvent) { |
| 2702 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 2703 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 2704 if (domContainer.deepContains(target)) { |
| 2705 this._ripple.uiDownAction(optTriggeringEvent); |
| 2706 } |
| 2707 } |
| 2708 } |
| 2709 }, |
| 2710 getRipple: function() { |
| 2711 this.ensureRipple(); |
| 2712 return this._ripple; |
| 2713 }, |
| 2714 hasRipple: function() { |
| 2715 return Boolean(this._ripple); |
| 2716 }, |
| 2717 _createRipple: function() { |
| 2718 return document.createElement('paper-ripple'); |
| 2719 }, |
| 2720 _noinkChanged: function(noink) { |
| 2721 if (this.hasRipple()) { |
| 2722 this._ripple.noink = noink; |
| 2723 } |
| 2724 } |
| 2725 }; |
| 2726 |
| 2727 Polymer.PaperButtonBehaviorImpl = { |
| 2728 properties: { |
| 2729 elevation: { |
| 2730 type: Number, |
| 2731 reflectToAttribute: true, |
| 2732 readOnly: true |
| 2733 } |
| 2734 }, |
| 2735 observers: [ '_calculateElevation(focused, disabled, active, pressed, received
FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ], |
| 2736 hostAttributes: { |
| 2737 role: 'button', |
| 2738 tabindex: '0', |
| 2739 animated: true |
| 2740 }, |
| 2741 _calculateElevation: function() { |
| 2742 var e = 1; |
| 2743 if (this.disabled) { |
| 2744 e = 0; |
| 2745 } else if (this.active || this.pressed) { |
| 2746 e = 4; |
| 2747 } else if (this.receivedFocusFromKeyboard) { |
| 2748 e = 3; |
| 2749 } |
| 2750 this._setElevation(e); |
| 2751 }, |
| 2752 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
| 2753 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
| 2754 }, |
| 2755 _spaceKeyDownHandler: function(event) { |
| 2756 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
| 2757 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
| 2758 this._ripple.uiDownAction(); |
| 2759 } |
| 2760 }, |
| 2761 _spaceKeyUpHandler: function(event) { |
| 2762 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
| 2763 if (this.hasRipple()) { |
| 2764 this._ripple.uiUpAction(); |
| 2765 } |
| 2766 } |
| 2767 }; |
| 2768 |
| 2769 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat
e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ]; |
| 2770 |
| 3197 Polymer({ | 2771 Polymer({ |
| 3198 | 2772 is: 'paper-button', |
| 3199 is: 'iron-pages', | 2773 behaviors: [ Polymer.PaperButtonBehavior ], |
| 3200 | 2774 properties: { |
| 3201 behaviors: [ | 2775 raised: { |
| 3202 Polymer.IronResizableBehavior, | 2776 type: Boolean, |
| 3203 Polymer.IronSelectableBehavior | 2777 reflectToAttribute: true, |
| 3204 ], | 2778 value: false, |
| 3205 | 2779 observer: '_calculateElevation' |
| 3206 properties: { | 2780 } |
| 3207 | 2781 }, |
| 3208 // as the selected page is the only one visible, activateEvent | 2782 _calculateElevation: function() { |
| 3209 // is both non-sensical and problematic; e.g. in cases where a user | 2783 if (!this.raised) { |
| 3210 // handler attempts to change the page and the activateEvent | 2784 this._setElevation(0); |
| 3211 // handler immediately changes it back | 2785 } else { |
| 3212 activateEvent: { | 2786 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
| 3213 type: String, | 2787 } |
| 3214 value: null | 2788 } |
| 3215 } | 2789 }); |
| 3216 | 2790 |
| 2791 (function() { |
| 2792 var metaDatas = {}; |
| 2793 var metaArrays = {}; |
| 2794 var singleton = null; |
| 2795 Polymer.IronMeta = Polymer({ |
| 2796 is: 'iron-meta', |
| 2797 properties: { |
| 2798 type: { |
| 2799 type: String, |
| 2800 value: 'default', |
| 2801 observer: '_typeChanged' |
| 3217 }, | 2802 }, |
| 3218 | 2803 key: { |
| 3219 observers: [ | 2804 type: String, |
| 3220 '_selectedPageChanged(selected)' | 2805 observer: '_keyChanged' |
| 3221 ], | 2806 }, |
| 3222 | 2807 value: { |
| 3223 _selectedPageChanged: function(selected, old) { | 2808 type: Object, |
| 3224 this.async(this.notifyResize); | 2809 notify: true, |
| 3225 } | 2810 observer: '_valueChanged' |
| 3226 }); | 2811 }, |
| 3227 (function() { | 2812 self: { |
| 3228 'use strict'; | 2813 type: Boolean, |
| 3229 | 2814 observer: '_selfChanged' |
| 3230 /** | 2815 }, |
| 3231 * Chrome uses an older version of DOM Level 3 Keyboard Events | 2816 list: { |
| 3232 * | 2817 type: Array, |
| 3233 * Most keys are labeled as text, but some are Unicode codepoints. | 2818 notify: true |
| 3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | 2819 } |
| 3235 */ | 2820 }, |
| 3236 var KEY_IDENTIFIER = { | 2821 hostAttributes: { |
| 3237 'U+0008': 'backspace', | 2822 hidden: true |
| 3238 'U+0009': 'tab', | 2823 }, |
| 3239 'U+001B': 'esc', | 2824 factoryImpl: function(config) { |
| 3240 'U+0020': 'space', | 2825 if (config) { |
| 3241 'U+007F': 'del' | 2826 for (var n in config) { |
| 3242 }; | 2827 switch (n) { |
| 3243 | 2828 case 'type': |
| 3244 /** | 2829 case 'key': |
| 3245 * Special table for KeyboardEvent.keyCode. | 2830 case 'value': |
| 3246 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | 2831 this[n] = config[n]; |
| 3247 * than that. | 2832 break; |
| 3248 * | |
| 3249 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
| 3250 */ | |
| 3251 var KEY_CODE = { | |
| 3252 8: 'backspace', | |
| 3253 9: 'tab', | |
| 3254 13: 'enter', | |
| 3255 27: 'esc', | |
| 3256 33: 'pageup', | |
| 3257 34: 'pagedown', | |
| 3258 35: 'end', | |
| 3259 36: 'home', | |
| 3260 32: 'space', | |
| 3261 37: 'left', | |
| 3262 38: 'up', | |
| 3263 39: 'right', | |
| 3264 40: 'down', | |
| 3265 46: 'del', | |
| 3266 106: '*' | |
| 3267 }; | |
| 3268 | |
| 3269 /** | |
| 3270 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
| 3271 * combo string to the property name that references those same keys | |
| 3272 * in a KeyboardEvent instance. | |
| 3273 */ | |
| 3274 var MODIFIER_KEYS = { | |
| 3275 'shift': 'shiftKey', | |
| 3276 'ctrl': 'ctrlKey', | |
| 3277 'alt': 'altKey', | |
| 3278 'meta': 'metaKey' | |
| 3279 }; | |
| 3280 | |
| 3281 /** | |
| 3282 * KeyboardEvent.key is mostly represented by printable character made by | |
| 3283 * the keyboard, with unprintable keys labeled nicely. | |
| 3284 * | |
| 3285 * However, on OS X, Alt+char can make a Unicode character that follows an | |
| 3286 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
| 3287 */ | |
| 3288 var KEY_CHAR = /[a-z0-9*]/; | |
| 3289 | |
| 3290 /** | |
| 3291 * Matches a keyIdentifier string. | |
| 3292 */ | |
| 3293 var IDENT_CHAR = /U\+/; | |
| 3294 | |
| 3295 /** | |
| 3296 * Matches arrow keys in Gecko 27.0+ | |
| 3297 */ | |
| 3298 var ARROW_KEY = /^arrow/; | |
| 3299 | |
| 3300 /** | |
| 3301 * Matches space keys everywhere (notably including IE10's exceptional name | |
| 3302 * `spacebar`). | |
| 3303 */ | |
| 3304 var SPACE_KEY = /^space(bar)?/; | |
| 3305 | |
| 3306 /** | |
| 3307 * Matches ESC key. | |
| 3308 * | |
| 3309 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
| 3310 */ | |
| 3311 var ESC_KEY = /^escape$/; | |
| 3312 | |
| 3313 /** | |
| 3314 * Transforms the key. | |
| 3315 * @param {string} key The KeyBoardEvent.key | |
| 3316 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
| 3317 * alpha-numeric characters. | |
| 3318 */ | |
| 3319 function transformKey(key, noSpecialChars) { | |
| 3320 var validKey = ''; | |
| 3321 if (key) { | |
| 3322 var lKey = key.toLowerCase(); | |
| 3323 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
| 3324 validKey = 'space'; | |
| 3325 } else if (ESC_KEY.test(lKey)) { | |
| 3326 validKey = 'esc'; | |
| 3327 } else if (lKey.length == 1) { | |
| 3328 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
| 3329 validKey = lKey; | |
| 3330 } | |
| 3331 } else if (ARROW_KEY.test(lKey)) { | |
| 3332 validKey = lKey.replace('arrow', ''); | |
| 3333 } else if (lKey == 'multiply') { | |
| 3334 // numpad '*' can map to Multiply on IE/Windows | |
| 3335 validKey = '*'; | |
| 3336 } else { | |
| 3337 validKey = lKey; | |
| 3338 } | |
| 3339 } | |
| 3340 return validKey; | |
| 3341 } | |
| 3342 | |
| 3343 function transformKeyIdentifier(keyIdent) { | |
| 3344 var validKey = ''; | |
| 3345 if (keyIdent) { | |
| 3346 if (keyIdent in KEY_IDENTIFIER) { | |
| 3347 validKey = KEY_IDENTIFIER[keyIdent]; | |
| 3348 } else if (IDENT_CHAR.test(keyIdent)) { | |
| 3349 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
| 3350 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
| 3351 } else { | |
| 3352 validKey = keyIdent.toLowerCase(); | |
| 3353 } | |
| 3354 } | |
| 3355 return validKey; | |
| 3356 } | |
| 3357 | |
| 3358 function transformKeyCode(keyCode) { | |
| 3359 var validKey = ''; | |
| 3360 if (Number(keyCode)) { | |
| 3361 if (keyCode >= 65 && keyCode <= 90) { | |
| 3362 // ascii a-z | |
| 3363 // lowercase is 32 offset from uppercase | |
| 3364 validKey = String.fromCharCode(32 + keyCode); | |
| 3365 } else if (keyCode >= 112 && keyCode <= 123) { | |
| 3366 // function keys f1-f12 | |
| 3367 validKey = 'f' + (keyCode - 112); | |
| 3368 } else if (keyCode >= 48 && keyCode <= 57) { | |
| 3369 // top 0-9 keys | |
| 3370 validKey = String(keyCode - 48); | |
| 3371 } else if (keyCode >= 96 && keyCode <= 105) { | |
| 3372 // num pad 0-9 | |
| 3373 validKey = String(keyCode - 96); | |
| 3374 } else { | |
| 3375 validKey = KEY_CODE[keyCode]; | |
| 3376 } | |
| 3377 } | |
| 3378 return validKey; | |
| 3379 } | |
| 3380 | |
| 3381 /** | |
| 3382 * Calculates the normalized key for a KeyboardEvent. | |
| 3383 * @param {KeyboardEvent} keyEvent | |
| 3384 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
| 3385 * transformation to alpha-numeric chars. This is useful with key | |
| 3386 * combinations like shift + 2, which on FF for MacOS produces | |
| 3387 * keyEvent.key = @ | |
| 3388 * To get 2 returned, set noSpecialChars = true | |
| 3389 * To get @ returned, set noSpecialChars = false | |
| 3390 */ | |
| 3391 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
| 3392 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
| 3393 // .detail.key to support artificial keyboard events. | |
| 3394 return transformKey(keyEvent.key, noSpecialChars) || | |
| 3395 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
| 3396 transformKeyCode(keyEvent.keyCode) || | |
| 3397 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | |
| 3398 } | |
| 3399 | |
| 3400 function keyComboMatchesEvent(keyCombo, event) { | |
| 3401 // For combos with modifiers we support only alpha-numeric keys | |
| 3402 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
| 3403 return keyEvent === keyCombo.key && | |
| 3404 (!keyCombo.hasModifiers || ( | |
| 3405 !!event.shiftKey === !!keyCombo.shiftKey && | |
| 3406 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
| 3407 !!event.altKey === !!keyCombo.altKey && | |
| 3408 !!event.metaKey === !!keyCombo.metaKey) | |
| 3409 ); | |
| 3410 } | |
| 3411 | |
| 3412 function parseKeyComboString(keyComboString) { | |
| 3413 if (keyComboString.length === 1) { | |
| 3414 return { | |
| 3415 combo: keyComboString, | |
| 3416 key: keyComboString, | |
| 3417 event: 'keydown' | |
| 3418 }; | |
| 3419 } | |
| 3420 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
| 3421 var eventParts = keyComboPart.split(':'); | |
| 3422 var keyName = eventParts[0]; | |
| 3423 var event = eventParts[1]; | |
| 3424 | |
| 3425 if (keyName in MODIFIER_KEYS) { | |
| 3426 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
| 3427 parsedKeyCombo.hasModifiers = true; | |
| 3428 } else { | |
| 3429 parsedKeyCombo.key = keyName; | |
| 3430 parsedKeyCombo.event = event || 'keydown'; | |
| 3431 } | |
| 3432 | |
| 3433 return parsedKeyCombo; | |
| 3434 }, { | |
| 3435 combo: keyComboString.split(':').shift() | |
| 3436 }); | |
| 3437 } | |
| 3438 | |
| 3439 function parseEventString(eventString) { | |
| 3440 return eventString.trim().split(' ').map(function(keyComboString) { | |
| 3441 return parseKeyComboString(keyComboString); | |
| 3442 }); | |
| 3443 } | |
| 3444 | |
| 3445 /** | |
| 3446 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
| 3447 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
| 3448 * The element takes care of browser differences with respect to Keyboard ev
ents | |
| 3449 * and uses an expressive syntax to filter key presses. | |
| 3450 * | |
| 3451 * Use the `keyBindings` prototype property to express what combination of k
eys | |
| 3452 * will trigger the callback. A key binding has the format | |
| 3453 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
| 3454 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
| 3455 * | |
| 3456 * keyBindings: { | |
| 3457 * 'space': '_onKeydown', // same as 'space:keydown' | |
| 3458 * 'shift+tab': '_onKeydown', | |
| 3459 * 'enter:keypress': '_onKeypress', | |
| 3460 * 'esc:keyup': '_onKeyup' | |
| 3461 * } | |
| 3462 * | |
| 3463 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
| 3464 * | |
| 3465 * _onKeydown: function(event) { | |
| 3466 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
| 3467 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
| 3468 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
| 3469 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
| 3470 * } | |
| 3471 * | |
| 3472 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
| 3473 * node. | |
| 3474 * | |
| 3475 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
| 3476 * for an example. | |
| 3477 * | |
| 3478 * @demo demo/index.html | |
| 3479 * @polymerBehavior | |
| 3480 */ | |
| 3481 Polymer.IronA11yKeysBehavior = { | |
| 3482 properties: { | |
| 3483 /** | |
| 3484 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
| 3485 * `null` to disable the listeners. | |
| 3486 * @type {?EventTarget} | |
| 3487 */ | |
| 3488 keyEventTarget: { | |
| 3489 type: Object, | |
| 3490 value: function() { | |
| 3491 return this; | |
| 3492 } | |
| 3493 }, | |
| 3494 | |
| 3495 /** | |
| 3496 * If true, this property will cause the implementing element to | |
| 3497 * automatically stop propagation on any handled KeyboardEvents. | |
| 3498 */ | |
| 3499 stopKeyboardEventPropagation: { | |
| 3500 type: Boolean, | |
| 3501 value: false | |
| 3502 }, | |
| 3503 | |
| 3504 _boundKeyHandlers: { | |
| 3505 type: Array, | |
| 3506 value: function() { | |
| 3507 return []; | |
| 3508 } | |
| 3509 }, | |
| 3510 | |
| 3511 // We use this due to a limitation in IE10 where instances will have | |
| 3512 // own properties of everything on the "prototype". | |
| 3513 _imperativeKeyBindings: { | |
| 3514 type: Object, | |
| 3515 value: function() { | |
| 3516 return {}; | |
| 3517 } | 2833 } |
| 3518 } | 2834 } |
| 2835 } |
| 2836 }, |
| 2837 created: function() { |
| 2838 this._metaDatas = metaDatas; |
| 2839 this._metaArrays = metaArrays; |
| 2840 }, |
| 2841 _keyChanged: function(key, old) { |
| 2842 this._resetRegistration(old); |
| 2843 }, |
| 2844 _valueChanged: function(value) { |
| 2845 this._resetRegistration(this.key); |
| 2846 }, |
| 2847 _selfChanged: function(self) { |
| 2848 if (self) { |
| 2849 this.value = this; |
| 2850 } |
| 2851 }, |
| 2852 _typeChanged: function(type) { |
| 2853 this._unregisterKey(this.key); |
| 2854 if (!metaDatas[type]) { |
| 2855 metaDatas[type] = {}; |
| 2856 } |
| 2857 this._metaData = metaDatas[type]; |
| 2858 if (!metaArrays[type]) { |
| 2859 metaArrays[type] = []; |
| 2860 } |
| 2861 this.list = metaArrays[type]; |
| 2862 this._registerKeyValue(this.key, this.value); |
| 2863 }, |
| 2864 byKey: function(key) { |
| 2865 return this._metaData && this._metaData[key]; |
| 2866 }, |
| 2867 _resetRegistration: function(oldKey) { |
| 2868 this._unregisterKey(oldKey); |
| 2869 this._registerKeyValue(this.key, this.value); |
| 2870 }, |
| 2871 _unregisterKey: function(key) { |
| 2872 this._unregister(key, this._metaData, this.list); |
| 2873 }, |
| 2874 _registerKeyValue: function(key, value) { |
| 2875 this._register(key, value, this._metaData, this.list); |
| 2876 }, |
| 2877 _register: function(key, value, data, list) { |
| 2878 if (key && data && value !== undefined) { |
| 2879 data[key] = value; |
| 2880 list.push(value); |
| 2881 } |
| 2882 }, |
| 2883 _unregister: function(key, data, list) { |
| 2884 if (key && data) { |
| 2885 if (key in data) { |
| 2886 var value = data[key]; |
| 2887 delete data[key]; |
| 2888 this.arrayDelete(list, value); |
| 2889 } |
| 2890 } |
| 2891 } |
| 2892 }); |
| 2893 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
| 2894 if (singleton === null) { |
| 2895 singleton = new Polymer.IronMeta(); |
| 2896 } |
| 2897 return singleton; |
| 2898 }; |
| 2899 Polymer.IronMetaQuery = Polymer({ |
| 2900 is: 'iron-meta-query', |
| 2901 properties: { |
| 2902 type: { |
| 2903 type: String, |
| 2904 value: 'default', |
| 2905 observer: '_typeChanged' |
| 3519 }, | 2906 }, |
| 3520 | 2907 key: { |
| 3521 observers: [ | 2908 type: String, |
| 3522 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | 2909 observer: '_keyChanged' |
| 3523 ], | |
| 3524 | |
| 3525 | |
| 3526 /** | |
| 3527 * To be used to express what combination of keys will trigger the relati
ve | |
| 3528 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
| 3529 * @type {Object} | |
| 3530 */ | |
| 3531 keyBindings: {}, | |
| 3532 | |
| 3533 registered: function() { | |
| 3534 this._prepKeyBindings(); | |
| 3535 }, | 2910 }, |
| 3536 | 2911 value: { |
| 3537 attached: function() { | 2912 type: Object, |
| 3538 this._listenKeyEventListeners(); | 2913 notify: true, |
| 2914 readOnly: true |
| 3539 }, | 2915 }, |
| 3540 | 2916 list: { |
| 3541 detached: function() { | 2917 type: Array, |
| 3542 this._unlistenKeyEventListeners(); | 2918 notify: true |
| 3543 }, | 2919 } |
| 3544 | 2920 }, |
| 3545 /** | 2921 factoryImpl: function(config) { |
| 3546 * Can be used to imperatively add a key binding to the implementing | 2922 if (config) { |
| 3547 * element. This is the imperative equivalent of declaring a keybinding | 2923 for (var n in config) { |
| 3548 * in the `keyBindings` prototype property. | 2924 switch (n) { |
| 3549 */ | 2925 case 'type': |
| 3550 addOwnKeyBinding: function(eventString, handlerName) { | 2926 case 'key': |
| 3551 this._imperativeKeyBindings[eventString] = handlerName; | 2927 this[n] = config[n]; |
| 3552 this._prepKeyBindings(); | 2928 break; |
| 3553 this._resetKeyEventListeners(); | |
| 3554 }, | |
| 3555 | |
| 3556 /** | |
| 3557 * When called, will remove all imperatively-added key bindings. | |
| 3558 */ | |
| 3559 removeOwnKeyBindings: function() { | |
| 3560 this._imperativeKeyBindings = {}; | |
| 3561 this._prepKeyBindings(); | |
| 3562 this._resetKeyEventListeners(); | |
| 3563 }, | |
| 3564 | |
| 3565 /** | |
| 3566 * Returns true if a keyboard event matches `eventString`. | |
| 3567 * | |
| 3568 * @param {KeyboardEvent} event | |
| 3569 * @param {string} eventString | |
| 3570 * @return {boolean} | |
| 3571 */ | |
| 3572 keyboardEventMatchesKeys: function(event, eventString) { | |
| 3573 var keyCombos = parseEventString(eventString); | |
| 3574 for (var i = 0; i < keyCombos.length; ++i) { | |
| 3575 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
| 3576 return true; | |
| 3577 } | 2929 } |
| 3578 } | 2930 } |
| 3579 return false; | 2931 } |
| 3580 }, | 2932 }, |
| 3581 | 2933 created: function() { |
| 3582 _collectKeyBindings: function() { | 2934 this._metaDatas = metaDatas; |
| 3583 var keyBindings = this.behaviors.map(function(behavior) { | 2935 this._metaArrays = metaArrays; |
| 3584 return behavior.keyBindings; | 2936 }, |
| 3585 }); | 2937 _keyChanged: function(key) { |
| 3586 | 2938 this._setValue(this._metaData && this._metaData[key]); |
| 3587 if (keyBindings.indexOf(this.keyBindings) === -1) { | 2939 }, |
| 3588 keyBindings.push(this.keyBindings); | 2940 _typeChanged: function(type) { |
| 2941 this._metaData = metaDatas[type]; |
| 2942 this.list = metaArrays[type]; |
| 2943 if (this.key) { |
| 2944 this._keyChanged(this.key); |
| 2945 } |
| 2946 }, |
| 2947 byKey: function(key) { |
| 2948 return this._metaData && this._metaData[key]; |
| 2949 } |
| 2950 }); |
| 2951 })(); |
| 2952 |
| 2953 Polymer({ |
| 2954 is: 'iron-icon', |
| 2955 properties: { |
| 2956 icon: { |
| 2957 type: String, |
| 2958 observer: '_iconChanged' |
| 2959 }, |
| 2960 theme: { |
| 2961 type: String, |
| 2962 observer: '_updateIcon' |
| 2963 }, |
| 2964 src: { |
| 2965 type: String, |
| 2966 observer: '_srcChanged' |
| 2967 }, |
| 2968 _meta: { |
| 2969 value: Polymer.Base.create('iron-meta', { |
| 2970 type: 'iconset' |
| 2971 }), |
| 2972 observer: '_updateIcon' |
| 2973 } |
| 2974 }, |
| 2975 _DEFAULT_ICONSET: 'icons', |
| 2976 _iconChanged: function(icon) { |
| 2977 var parts = (icon || '').split(':'); |
| 2978 this._iconName = parts.pop(); |
| 2979 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
| 2980 this._updateIcon(); |
| 2981 }, |
| 2982 _srcChanged: function(src) { |
| 2983 this._updateIcon(); |
| 2984 }, |
| 2985 _usesIconset: function() { |
| 2986 return this.icon || !this.src; |
| 2987 }, |
| 2988 _updateIcon: function() { |
| 2989 if (this._usesIconset()) { |
| 2990 if (this._img && this._img.parentNode) { |
| 2991 Polymer.dom(this.root).removeChild(this._img); |
| 2992 } |
| 2993 if (this._iconName === "") { |
| 2994 if (this._iconset) { |
| 2995 this._iconset.removeIcon(this); |
| 3589 } | 2996 } |
| 3590 | 2997 } else if (this._iconsetName && this._meta) { |
| 3591 return keyBindings; | 2998 this._iconset = this._meta.byKey(this._iconsetName); |
| 3592 }, | 2999 if (this._iconset) { |
| 3593 | 3000 this._iconset.applyIcon(this, this._iconName, this.theme); |
| 3594 _prepKeyBindings: function() { | 3001 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
| 3595 this._keyBindings = {}; | 3002 } else { |
| 3596 | 3003 this.listen(window, 'iron-iconset-added', '_updateIcon'); |
| 3597 this._collectKeyBindings().forEach(function(keyBindings) { | |
| 3598 for (var eventString in keyBindings) { | |
| 3599 this._addKeyBinding(eventString, keyBindings[eventString]); | |
| 3600 } | |
| 3601 }, this); | |
| 3602 | |
| 3603 for (var eventString in this._imperativeKeyBindings) { | |
| 3604 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
| 3605 } | 3004 } |
| 3606 | 3005 } |
| 3607 // Give precedence to combos with modifiers to be checked first. | 3006 } else { |
| 3608 for (var eventName in this._keyBindings) { | 3007 if (this._iconset) { |
| 3609 this._keyBindings[eventName].sort(function (kb1, kb2) { | 3008 this._iconset.removeIcon(this); |
| 3610 var b1 = kb1[0].hasModifiers; | 3009 } |
| 3611 var b2 = kb2[0].hasModifiers; | 3010 if (!this._img) { |
| 3612 return (b1 === b2) ? 0 : b1 ? -1 : 1; | 3011 this._img = document.createElement('img'); |
| 3613 }) | 3012 this._img.style.width = '100%'; |
| 3013 this._img.style.height = '100%'; |
| 3014 this._img.draggable = false; |
| 3015 } |
| 3016 this._img.src = this.src; |
| 3017 Polymer.dom(this.root).appendChild(this._img); |
| 3018 } |
| 3019 } |
| 3020 }); |
| 3021 |
| 3022 Polymer.PaperInkyFocusBehaviorImpl = { |
| 3023 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ], |
| 3024 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 3025 if (receivedFocusFromKeyboard) { |
| 3026 this.ensureRipple(); |
| 3027 } |
| 3028 if (this.hasRipple()) { |
| 3029 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 3030 } |
| 3031 }, |
| 3032 _createRipple: function() { |
| 3033 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 3034 ripple.id = 'ink'; |
| 3035 ripple.setAttribute('center', ''); |
| 3036 ripple.classList.add('circle'); |
| 3037 return ripple; |
| 3038 } |
| 3039 }; |
| 3040 |
| 3041 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS
tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ]; |
| 3042 |
| 3043 Polymer({ |
| 3044 is: 'paper-icon-button', |
| 3045 hostAttributes: { |
| 3046 role: 'button', |
| 3047 tabindex: '0' |
| 3048 }, |
| 3049 behaviors: [ Polymer.PaperInkyFocusBehavior ], |
| 3050 properties: { |
| 3051 src: { |
| 3052 type: String |
| 3053 }, |
| 3054 icon: { |
| 3055 type: String |
| 3056 }, |
| 3057 alt: { |
| 3058 type: String, |
| 3059 observer: "_altChanged" |
| 3060 } |
| 3061 }, |
| 3062 _altChanged: function(newValue, oldValue) { |
| 3063 var label = this.getAttribute('aria-label'); |
| 3064 if (!label || oldValue == label) { |
| 3065 this.setAttribute('aria-label', newValue); |
| 3066 } |
| 3067 } |
| 3068 }); |
| 3069 |
| 3070 Polymer({ |
| 3071 is: 'paper-tab', |
| 3072 behaviors: [ Polymer.IronControlState, Polymer.IronButtonState, Polymer.PaperR
ippleBehavior ], |
| 3073 properties: { |
| 3074 link: { |
| 3075 type: Boolean, |
| 3076 value: false, |
| 3077 reflectToAttribute: true |
| 3078 } |
| 3079 }, |
| 3080 hostAttributes: { |
| 3081 role: 'tab' |
| 3082 }, |
| 3083 listeners: { |
| 3084 down: '_updateNoink', |
| 3085 tap: '_onTap' |
| 3086 }, |
| 3087 attached: function() { |
| 3088 this._updateNoink(); |
| 3089 }, |
| 3090 get _parentNoink() { |
| 3091 var parent = Polymer.dom(this).parentNode; |
| 3092 return !!parent && !!parent.noink; |
| 3093 }, |
| 3094 _updateNoink: function() { |
| 3095 this.noink = !!this.noink || !!this._parentNoink; |
| 3096 }, |
| 3097 _onTap: function(event) { |
| 3098 if (this.link) { |
| 3099 var anchor = this.queryEffectiveChildren('a'); |
| 3100 if (!anchor) { |
| 3101 return; |
| 3102 } |
| 3103 if (event.target === anchor) { |
| 3104 return; |
| 3105 } |
| 3106 anchor.click(); |
| 3107 } |
| 3108 } |
| 3109 }); |
| 3110 |
| 3111 Polymer.IronMultiSelectableBehaviorImpl = { |
| 3112 properties: { |
| 3113 multi: { |
| 3114 type: Boolean, |
| 3115 value: false, |
| 3116 observer: 'multiChanged' |
| 3117 }, |
| 3118 selectedValues: { |
| 3119 type: Array, |
| 3120 notify: true |
| 3121 }, |
| 3122 selectedItems: { |
| 3123 type: Array, |
| 3124 readOnly: true, |
| 3125 notify: true |
| 3126 } |
| 3127 }, |
| 3128 observers: [ '_updateSelected(selectedValues.splices)' ], |
| 3129 select: function(value) { |
| 3130 if (this.multi) { |
| 3131 if (this.selectedValues) { |
| 3132 this._toggleSelected(value); |
| 3133 } else { |
| 3134 this.selectedValues = [ value ]; |
| 3135 } |
| 3136 } else { |
| 3137 this.selected = value; |
| 3138 } |
| 3139 }, |
| 3140 multiChanged: function(multi) { |
| 3141 this._selection.multi = multi; |
| 3142 }, |
| 3143 get _shouldUpdateSelection() { |
| 3144 return this.selected != null || this.selectedValues != null && this.selected
Values.length; |
| 3145 }, |
| 3146 _updateAttrForSelected: function() { |
| 3147 if (!this.multi) { |
| 3148 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 3149 } else if (this._shouldUpdateSelection) { |
| 3150 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 3151 return this._indexToValue(this.indexOf(selectedItem)); |
| 3152 }, this).filter(function(unfilteredValue) { |
| 3153 return unfilteredValue != null; |
| 3154 }, this); |
| 3155 } |
| 3156 }, |
| 3157 _updateSelected: function() { |
| 3158 if (this.multi) { |
| 3159 this._selectMulti(this.selectedValues); |
| 3160 } else { |
| 3161 this._selectSelected(this.selected); |
| 3162 } |
| 3163 }, |
| 3164 _selectMulti: function(values) { |
| 3165 if (values) { |
| 3166 var selectedItems = this._valuesToItems(values); |
| 3167 this._selection.clear(selectedItems); |
| 3168 for (var i = 0; i < selectedItems.length; i++) { |
| 3169 this._selection.setItemSelected(selectedItems[i], true); |
| 3170 } |
| 3171 if (this.fallbackSelection && this.items.length && !this._selection.get().
length) { |
| 3172 var fallback = this._valueToItem(this.fallbackSelection); |
| 3173 if (fallback) { |
| 3174 this.selectedValues = [ this.fallbackSelection ]; |
| 3614 } | 3175 } |
| 3615 }, | 3176 } |
| 3616 | 3177 } else { |
| 3617 _addKeyBinding: function(eventString, handlerName) { | 3178 this._selection.clear(); |
| 3618 parseEventString(eventString).forEach(function(keyCombo) { | 3179 } |
| 3619 this._keyBindings[keyCombo.event] = | 3180 }, |
| 3620 this._keyBindings[keyCombo.event] || []; | 3181 _selectionChange: function() { |
| 3621 | 3182 var s = this._selection.get(); |
| 3622 this._keyBindings[keyCombo.event].push([ | 3183 if (this.multi) { |
| 3623 keyCombo, | 3184 this._setSelectedItems(s); |
| 3624 handlerName | 3185 } else { |
| 3625 ]); | 3186 this._setSelectedItems([ s ]); |
| 3626 }, this); | 3187 this._setSelectedItem(s); |
| 3627 }, | 3188 } |
| 3628 | 3189 }, |
| 3629 _resetKeyEventListeners: function() { | 3190 _toggleSelected: function(value) { |
| 3630 this._unlistenKeyEventListeners(); | 3191 var i = this.selectedValues.indexOf(value); |
| 3631 | 3192 var unselected = i < 0; |
| 3632 if (this.isAttached) { | 3193 if (unselected) { |
| 3633 this._listenKeyEventListeners(); | 3194 this.push('selectedValues', value); |
| 3634 } | 3195 } else { |
| 3635 }, | 3196 this.splice('selectedValues', i, 1); |
| 3636 | 3197 } |
| 3637 _listenKeyEventListeners: function() { | 3198 }, |
| 3638 if (!this.keyEventTarget) { | 3199 _valuesToItems: function(values) { |
| 3639 return; | 3200 return values == null ? null : values.map(function(value) { |
| 3640 } | 3201 return this._valueToItem(value); |
| 3641 Object.keys(this._keyBindings).forEach(function(eventName) { | 3202 }, this); |
| 3642 var keyBindings = this._keyBindings[eventName]; | 3203 } |
| 3643 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | 3204 }; |
| 3644 | 3205 |
| 3645 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | 3206 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer.
IronMultiSelectableBehaviorImpl ]; |
| 3646 | 3207 |
| 3647 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | 3208 Polymer.IronMenuBehaviorImpl = { |
| 3648 }, this); | 3209 properties: { |
| 3649 }, | 3210 focusedItem: { |
| 3650 | 3211 observer: '_focusedItemChanged', |
| 3651 _unlistenKeyEventListeners: function() { | 3212 readOnly: true, |
| 3652 var keyHandlerTuple; | 3213 type: Object |
| 3653 var keyEventTarget; | 3214 }, |
| 3654 var eventName; | 3215 attrForItemTitle: { |
| 3655 var boundKeyHandler; | 3216 type: String |
| 3656 | 3217 } |
| 3657 while (this._boundKeyHandlers.length) { | 3218 }, |
| 3658 // My kingdom for block-scope binding and destructuring assignment.. | 3219 hostAttributes: { |
| 3659 keyHandlerTuple = this._boundKeyHandlers.pop(); | 3220 role: 'menu', |
| 3660 keyEventTarget = keyHandlerTuple[0]; | 3221 tabindex: '0' |
| 3661 eventName = keyHandlerTuple[1]; | 3222 }, |
| 3662 boundKeyHandler = keyHandlerTuple[2]; | 3223 observers: [ '_updateMultiselectable(multi)' ], |
| 3663 | 3224 listeners: { |
| 3664 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | 3225 focus: '_onFocus', |
| 3665 } | 3226 keydown: '_onKeydown', |
| 3666 }, | 3227 'iron-items-changed': '_onIronItemsChanged' |
| 3667 | 3228 }, |
| 3668 _onKeyBindingEvent: function(keyBindings, event) { | 3229 keyBindings: { |
| 3669 if (this.stopKeyboardEventPropagation) { | 3230 up: '_onUpKey', |
| 3670 event.stopPropagation(); | 3231 down: '_onDownKey', |
| 3671 } | 3232 esc: '_onEscKey', |
| 3672 | 3233 'shift+tab:keydown': '_onShiftTabDown' |
| 3673 // if event has been already prevented, don't do anything | 3234 }, |
| 3674 if (event.defaultPrevented) { | 3235 attached: function() { |
| 3675 return; | 3236 this._resetTabindices(); |
| 3676 } | 3237 }, |
| 3677 | 3238 select: function(value) { |
| 3678 for (var i = 0; i < keyBindings.length; i++) { | 3239 if (this._defaultFocusAsync) { |
| 3679 var keyCombo = keyBindings[i][0]; | 3240 this.cancelAsync(this._defaultFocusAsync); |
| 3680 var handlerName = keyBindings[i][1]; | 3241 this._defaultFocusAsync = null; |
| 3681 if (keyComboMatchesEvent(keyCombo, event)) { | 3242 } |
| 3682 this._triggerKeyHandler(keyCombo, handlerName, event); | 3243 var item = this._valueToItem(value); |
| 3683 // exit the loop if eventDefault was prevented | 3244 if (item && item.hasAttribute('disabled')) return; |
| 3684 if (event.defaultPrevented) { | 3245 this._setFocusedItem(item); |
| 3685 return; | 3246 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| 3686 } | 3247 }, |
| 3687 } | 3248 _resetTabindices: function() { |
| 3688 } | 3249 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0]
: this.selectedItem; |
| 3689 }, | 3250 this.items.forEach(function(item) { |
| 3690 | 3251 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| 3691 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | 3252 }, this); |
| 3692 var detail = Object.create(keyCombo); | 3253 }, |
| 3693 detail.keyboardEvent = keyboardEvent; | 3254 _updateMultiselectable: function(multi) { |
| 3694 var event = new CustomEvent(keyCombo.event, { | 3255 if (multi) { |
| 3695 detail: detail, | 3256 this.setAttribute('aria-multiselectable', 'true'); |
| 3696 cancelable: true | 3257 } else { |
| 3697 }); | 3258 this.removeAttribute('aria-multiselectable'); |
| 3698 this[handlerName].call(this, event); | 3259 } |
| 3699 if (event.defaultPrevented) { | 3260 }, |
| 3700 keyboardEvent.preventDefault(); | 3261 _focusWithKeyboardEvent: function(event) { |
| 3701 } | 3262 for (var i = 0, item; item = this.items[i]; i++) { |
| 3702 } | 3263 var attr = this.attrForItemTitle || 'textContent'; |
| 3703 }; | 3264 var title = item[attr] || item.getAttribute(attr); |
| 3704 })(); | 3265 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo
werCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| 3705 /** | 3266 this._setFocusedItem(item); |
| 3706 * @demo demo/index.html | 3267 break; |
| 3707 * @polymerBehavior | 3268 } |
| 3708 */ | 3269 } |
| 3709 Polymer.IronControlState = { | 3270 }, |
| 3710 | 3271 _focusPrevious: function() { |
| 3711 properties: { | 3272 var length = this.items.length; |
| 3712 | 3273 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 3713 /** | 3274 for (var i = 1; i < length + 1; i++) { |
| 3714 * If true, the element currently has focus. | 3275 var item = this.items[(curFocusIndex - i + length) % length]; |
| 3715 */ | 3276 if (!item.hasAttribute('disabled')) { |
| 3716 focused: { | 3277 this._setFocusedItem(item); |
| 3717 type: Boolean, | |
| 3718 value: false, | |
| 3719 notify: true, | |
| 3720 readOnly: true, | |
| 3721 reflectToAttribute: true | |
| 3722 }, | |
| 3723 | |
| 3724 /** | |
| 3725 * If true, the user cannot interact with this element. | |
| 3726 */ | |
| 3727 disabled: { | |
| 3728 type: Boolean, | |
| 3729 value: false, | |
| 3730 notify: true, | |
| 3731 observer: '_disabledChanged', | |
| 3732 reflectToAttribute: true | |
| 3733 }, | |
| 3734 | |
| 3735 _oldTabIndex: { | |
| 3736 type: Number | |
| 3737 }, | |
| 3738 | |
| 3739 _boundFocusBlurHandler: { | |
| 3740 type: Function, | |
| 3741 value: function() { | |
| 3742 return this._focusBlurHandler.bind(this); | |
| 3743 } | |
| 3744 } | |
| 3745 | |
| 3746 }, | |
| 3747 | |
| 3748 observers: [ | |
| 3749 '_changedControlState(focused, disabled)' | |
| 3750 ], | |
| 3751 | |
| 3752 ready: function() { | |
| 3753 this.addEventListener('focus', this._boundFocusBlurHandler, true); | |
| 3754 this.addEventListener('blur', this._boundFocusBlurHandler, true); | |
| 3755 }, | |
| 3756 | |
| 3757 _focusBlurHandler: function(event) { | |
| 3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | |
| 3759 // eventually become `this` due to retargeting; if we are not in | |
| 3760 // ShadowDOM land, `event.target` will eventually become `this` due | |
| 3761 // to the second conditional which fires a synthetic event (that is also | |
| 3762 // handled). In either case, we can disregard `event.path`. | |
| 3763 | |
| 3764 if (event.target === this) { | |
| 3765 this._setFocused(event.type === 'focus'); | |
| 3766 } else if (!this.shadowRoot) { | |
| 3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | |
| 3768 if (!this.isLightDescendant(target)) { | |
| 3769 this.fire(event.type, {sourceEvent: event}, { | |
| 3770 node: this, | |
| 3771 bubbles: event.bubbles, | |
| 3772 cancelable: event.cancelable | |
| 3773 }); | |
| 3774 } | |
| 3775 } | |
| 3776 }, | |
| 3777 | |
| 3778 _disabledChanged: function(disabled, old) { | |
| 3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | |
| 3780 this.style.pointerEvents = disabled ? 'none' : ''; | |
| 3781 if (disabled) { | |
| 3782 this._oldTabIndex = this.tabIndex; | |
| 3783 this._setFocused(false); | |
| 3784 this.tabIndex = -1; | |
| 3785 this.blur(); | |
| 3786 } else if (this._oldTabIndex !== undefined) { | |
| 3787 this.tabIndex = this._oldTabIndex; | |
| 3788 } | |
| 3789 }, | |
| 3790 | |
| 3791 _changedControlState: function() { | |
| 3792 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
| 3793 if (this._controlStateChanged) { | |
| 3794 this._controlStateChanged(); | |
| 3795 } | |
| 3796 } | |
| 3797 | |
| 3798 }; | |
| 3799 /** | |
| 3800 * @demo demo/index.html | |
| 3801 * @polymerBehavior Polymer.IronButtonState | |
| 3802 */ | |
| 3803 Polymer.IronButtonStateImpl = { | |
| 3804 | |
| 3805 properties: { | |
| 3806 | |
| 3807 /** | |
| 3808 * If true, the user is currently holding down the button. | |
| 3809 */ | |
| 3810 pressed: { | |
| 3811 type: Boolean, | |
| 3812 readOnly: true, | |
| 3813 value: false, | |
| 3814 reflectToAttribute: true, | |
| 3815 observer: '_pressedChanged' | |
| 3816 }, | |
| 3817 | |
| 3818 /** | |
| 3819 * If true, the button toggles the active state with each tap or press | |
| 3820 * of the spacebar. | |
| 3821 */ | |
| 3822 toggles: { | |
| 3823 type: Boolean, | |
| 3824 value: false, | |
| 3825 reflectToAttribute: true | |
| 3826 }, | |
| 3827 | |
| 3828 /** | |
| 3829 * If true, the button is a toggle and is currently in the active state. | |
| 3830 */ | |
| 3831 active: { | |
| 3832 type: Boolean, | |
| 3833 value: false, | |
| 3834 notify: true, | |
| 3835 reflectToAttribute: true | |
| 3836 }, | |
| 3837 | |
| 3838 /** | |
| 3839 * True if the element is currently being pressed by a "pointer," which | |
| 3840 * is loosely defined as mouse or touch input (but specifically excluding | |
| 3841 * keyboard input). | |
| 3842 */ | |
| 3843 pointerDown: { | |
| 3844 type: Boolean, | |
| 3845 readOnly: true, | |
| 3846 value: false | |
| 3847 }, | |
| 3848 | |
| 3849 /** | |
| 3850 * True if the input device that caused the element to receive focus | |
| 3851 * was a keyboard. | |
| 3852 */ | |
| 3853 receivedFocusFromKeyboard: { | |
| 3854 type: Boolean, | |
| 3855 readOnly: true | |
| 3856 }, | |
| 3857 | |
| 3858 /** | |
| 3859 * The aria attribute to be set if the button is a toggle and in the | |
| 3860 * active state. | |
| 3861 */ | |
| 3862 ariaActiveAttribute: { | |
| 3863 type: String, | |
| 3864 value: 'aria-pressed', | |
| 3865 observer: '_ariaActiveAttributeChanged' | |
| 3866 } | |
| 3867 }, | |
| 3868 | |
| 3869 listeners: { | |
| 3870 down: '_downHandler', | |
| 3871 up: '_upHandler', | |
| 3872 tap: '_tapHandler' | |
| 3873 }, | |
| 3874 | |
| 3875 observers: [ | |
| 3876 '_detectKeyboardFocus(focused)', | |
| 3877 '_activeChanged(active, ariaActiveAttribute)' | |
| 3878 ], | |
| 3879 | |
| 3880 keyBindings: { | |
| 3881 'enter:keydown': '_asyncClick', | |
| 3882 'space:keydown': '_spaceKeyDownHandler', | |
| 3883 'space:keyup': '_spaceKeyUpHandler', | |
| 3884 }, | |
| 3885 | |
| 3886 _mouseEventRe: /^mouse/, | |
| 3887 | |
| 3888 _tapHandler: function() { | |
| 3889 if (this.toggles) { | |
| 3890 // a tap is needed to toggle the active state | |
| 3891 this._userActivate(!this.active); | |
| 3892 } else { | |
| 3893 this.active = false; | |
| 3894 } | |
| 3895 }, | |
| 3896 | |
| 3897 _detectKeyboardFocus: function(focused) { | |
| 3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
| 3899 }, | |
| 3900 | |
| 3901 // to emulate native checkbox, (de-)activations from a user interaction fire | |
| 3902 // 'change' events | |
| 3903 _userActivate: function(active) { | |
| 3904 if (this.active !== active) { | |
| 3905 this.active = active; | |
| 3906 this.fire('change'); | |
| 3907 } | |
| 3908 }, | |
| 3909 | |
| 3910 _downHandler: function(event) { | |
| 3911 this._setPointerDown(true); | |
| 3912 this._setPressed(true); | |
| 3913 this._setReceivedFocusFromKeyboard(false); | |
| 3914 }, | |
| 3915 | |
| 3916 _upHandler: function() { | |
| 3917 this._setPointerDown(false); | |
| 3918 this._setPressed(false); | |
| 3919 }, | |
| 3920 | |
| 3921 /** | |
| 3922 * @param {!KeyboardEvent} event . | |
| 3923 */ | |
| 3924 _spaceKeyDownHandler: function(event) { | |
| 3925 var keyboardEvent = event.detail.keyboardEvent; | |
| 3926 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 3927 | |
| 3928 // Ignore the event if this is coming from a focused light child, since th
at | |
| 3929 // element will deal with it. | |
| 3930 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 3931 return; | 3278 return; |
| 3932 | 3279 } |
| 3933 keyboardEvent.preventDefault(); | 3280 } |
| 3934 keyboardEvent.stopImmediatePropagation(); | 3281 }, |
| 3935 this._setPressed(true); | 3282 _focusNext: function() { |
| 3936 }, | 3283 var length = this.items.length; |
| 3937 | 3284 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 3938 /** | 3285 for (var i = 1; i < length + 1; i++) { |
| 3939 * @param {!KeyboardEvent} event . | 3286 var item = this.items[(curFocusIndex + i) % length]; |
| 3940 */ | 3287 if (!item.hasAttribute('disabled')) { |
| 3941 _spaceKeyUpHandler: function(event) { | 3288 this._setFocusedItem(item); |
| 3942 var keyboardEvent = event.detail.keyboardEvent; | |
| 3943 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 3944 | |
| 3945 // Ignore the event if this is coming from a focused light child, since th
at | |
| 3946 // element will deal with it. | |
| 3947 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 3948 return; | 3289 return; |
| 3949 | 3290 } |
| 3950 if (this.pressed) { | 3291 } |
| 3951 this._asyncClick(); | 3292 }, |
| 3952 } | 3293 _applySelection: function(item, isSelected) { |
| 3953 this._setPressed(false); | 3294 if (isSelected) { |
| 3954 }, | 3295 item.setAttribute('aria-selected', 'true'); |
| 3955 | 3296 } else { |
| 3956 // trigger click asynchronously, the asynchrony is useful to allow one | 3297 item.removeAttribute('aria-selected'); |
| 3957 // event handler to unwind before triggering another event | 3298 } |
| 3958 _asyncClick: function() { | 3299 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| 3959 this.async(function() { | 3300 }, |
| 3960 this.click(); | 3301 _focusedItemChanged: function(focusedItem, old) { |
| 3961 }, 1); | 3302 old && old.setAttribute('tabindex', '-1'); |
| 3962 }, | 3303 if (focusedItem) { |
| 3963 | 3304 focusedItem.setAttribute('tabindex', '0'); |
| 3964 // any of these changes are considered a change to button state | 3305 focusedItem.focus(); |
| 3965 | 3306 } |
| 3966 _pressedChanged: function(pressed) { | 3307 }, |
| 3967 this._changedButtonState(); | 3308 _onIronItemsChanged: function(event) { |
| 3968 }, | 3309 if (event.detail.addedNodes.length) { |
| 3969 | 3310 this._resetTabindices(); |
| 3970 _ariaActiveAttributeChanged: function(value, oldValue) { | 3311 } |
| 3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | 3312 }, |
| 3972 this.removeAttribute(oldValue); | 3313 _onShiftTabDown: function(event) { |
| 3973 } | 3314 var oldTabIndex = this.getAttribute('tabindex'); |
| 3974 }, | 3315 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| 3975 | 3316 this._setFocusedItem(null); |
| 3976 _activeChanged: function(active, ariaActiveAttribute) { | 3317 this.setAttribute('tabindex', '-1'); |
| 3977 if (this.toggles) { | 3318 this.async(function() { |
| 3978 this.setAttribute(this.ariaActiveAttribute, | 3319 this.setAttribute('tabindex', oldTabIndex); |
| 3979 active ? 'true' : 'false'); | 3320 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 3980 } else { | 3321 }, 1); |
| 3981 this.removeAttribute(this.ariaActiveAttribute); | 3322 }, |
| 3982 } | 3323 _onFocus: function(event) { |
| 3983 this._changedButtonState(); | 3324 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| 3984 }, | 3325 return; |
| 3985 | 3326 } |
| 3986 _controlStateChanged: function() { | 3327 var rootTarget = Polymer.dom(event).rootTarget; |
| 3987 if (this.disabled) { | 3328 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th
is.isLightDescendant(rootTarget)) { |
| 3988 this._setPressed(false); | 3329 return; |
| 3989 } else { | 3330 } |
| 3990 this._changedButtonState(); | 3331 this._defaultFocusAsync = this.async(function() { |
| 3991 } | 3332 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0
] : this.selectedItem; |
| 3992 }, | 3333 this._setFocusedItem(null); |
| 3993 | 3334 if (selectedItem) { |
| 3994 // provide hook for follow-on behaviors to react to button-state | 3335 this._setFocusedItem(selectedItem); |
| 3995 | 3336 } else if (this.items[0]) { |
| 3996 _changedButtonState: function() { | 3337 this._focusNext(); |
| 3997 if (this._buttonStateChanged) { | 3338 } |
| 3998 this._buttonStateChanged(); // abstract | |
| 3999 } | |
| 4000 } | |
| 4001 | |
| 4002 }; | |
| 4003 | |
| 4004 /** @polymerBehavior */ | |
| 4005 Polymer.IronButtonState = [ | |
| 4006 Polymer.IronA11yKeysBehavior, | |
| 4007 Polymer.IronButtonStateImpl | |
| 4008 ]; | |
| 4009 (function() { | |
| 4010 var Utility = { | |
| 4011 distance: function(x1, y1, x2, y2) { | |
| 4012 var xDelta = (x1 - x2); | |
| 4013 var yDelta = (y1 - y2); | |
| 4014 | |
| 4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
| 4016 }, | |
| 4017 | |
| 4018 now: window.performance && window.performance.now ? | |
| 4019 window.performance.now.bind(window.performance) : Date.now | |
| 4020 }; | |
| 4021 | |
| 4022 /** | |
| 4023 * @param {HTMLElement} element | |
| 4024 * @constructor | |
| 4025 */ | |
| 4026 function ElementMetrics(element) { | |
| 4027 this.element = element; | |
| 4028 this.width = this.boundingRect.width; | |
| 4029 this.height = this.boundingRect.height; | |
| 4030 | |
| 4031 this.size = Math.max(this.width, this.height); | |
| 4032 } | |
| 4033 | |
| 4034 ElementMetrics.prototype = { | |
| 4035 get boundingRect () { | |
| 4036 return this.element.getBoundingClientRect(); | |
| 4037 }, | |
| 4038 | |
| 4039 furthestCornerDistanceFrom: function(x, y) { | |
| 4040 var topLeft = Utility.distance(x, y, 0, 0); | |
| 4041 var topRight = Utility.distance(x, y, this.width, 0); | |
| 4042 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
| 4043 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
| 4044 | |
| 4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
| 4046 } | |
| 4047 }; | |
| 4048 | |
| 4049 /** | |
| 4050 * @param {HTMLElement} element | |
| 4051 * @constructor | |
| 4052 */ | |
| 4053 function Ripple(element) { | |
| 4054 this.element = element; | |
| 4055 this.color = window.getComputedStyle(element).color; | |
| 4056 | |
| 4057 this.wave = document.createElement('div'); | |
| 4058 this.waveContainer = document.createElement('div'); | |
| 4059 this.wave.style.backgroundColor = this.color; | |
| 4060 this.wave.classList.add('wave'); | |
| 4061 this.waveContainer.classList.add('wave-container'); | |
| 4062 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
| 4063 | |
| 4064 this.resetInteractionState(); | |
| 4065 } | |
| 4066 | |
| 4067 Ripple.MAX_RADIUS = 300; | |
| 4068 | |
| 4069 Ripple.prototype = { | |
| 4070 get recenters() { | |
| 4071 return this.element.recenters; | |
| 4072 }, | |
| 4073 | |
| 4074 get center() { | |
| 4075 return this.element.center; | |
| 4076 }, | |
| 4077 | |
| 4078 get mouseDownElapsed() { | |
| 4079 var elapsed; | |
| 4080 | |
| 4081 if (!this.mouseDownStart) { | |
| 4082 return 0; | |
| 4083 } | |
| 4084 | |
| 4085 elapsed = Utility.now() - this.mouseDownStart; | |
| 4086 | |
| 4087 if (this.mouseUpStart) { | |
| 4088 elapsed -= this.mouseUpElapsed; | |
| 4089 } | |
| 4090 | |
| 4091 return elapsed; | |
| 4092 }, | |
| 4093 | |
| 4094 get mouseUpElapsed() { | |
| 4095 return this.mouseUpStart ? | |
| 4096 Utility.now () - this.mouseUpStart : 0; | |
| 4097 }, | |
| 4098 | |
| 4099 get mouseDownElapsedSeconds() { | |
| 4100 return this.mouseDownElapsed / 1000; | |
| 4101 }, | |
| 4102 | |
| 4103 get mouseUpElapsedSeconds() { | |
| 4104 return this.mouseUpElapsed / 1000; | |
| 4105 }, | |
| 4106 | |
| 4107 get mouseInteractionSeconds() { | |
| 4108 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
| 4109 }, | |
| 4110 | |
| 4111 get initialOpacity() { | |
| 4112 return this.element.initialOpacity; | |
| 4113 }, | |
| 4114 | |
| 4115 get opacityDecayVelocity() { | |
| 4116 return this.element.opacityDecayVelocity; | |
| 4117 }, | |
| 4118 | |
| 4119 get radius() { | |
| 4120 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
| 4121 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
| 4122 var waveRadius = Math.min( | |
| 4123 Math.sqrt(width2 + height2), | |
| 4124 Ripple.MAX_RADIUS | |
| 4125 ) * 1.1 + 5; | |
| 4126 | |
| 4127 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
| 4128 var timeNow = this.mouseInteractionSeconds / duration; | |
| 4129 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
| 4130 | |
| 4131 return Math.abs(size); | |
| 4132 }, | |
| 4133 | |
| 4134 get opacity() { | |
| 4135 if (!this.mouseUpStart) { | |
| 4136 return this.initialOpacity; | |
| 4137 } | |
| 4138 | |
| 4139 return Math.max( | |
| 4140 0, | |
| 4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
| 4142 ); | |
| 4143 }, | |
| 4144 | |
| 4145 get outerOpacity() { | |
| 4146 // Linear increase in background opacity, capped at the opacity | |
| 4147 // of the wavefront (waveOpacity). | |
| 4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
| 4149 var waveOpacity = this.opacity; | |
| 4150 | |
| 4151 return Math.max( | |
| 4152 0, | |
| 4153 Math.min(outerOpacity, waveOpacity) | |
| 4154 ); | |
| 4155 }, | |
| 4156 | |
| 4157 get isOpacityFullyDecayed() { | |
| 4158 return this.opacity < 0.01 && | |
| 4159 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 4160 }, | |
| 4161 | |
| 4162 get isRestingAtMaxRadius() { | |
| 4163 return this.opacity >= this.initialOpacity && | |
| 4164 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 4165 }, | |
| 4166 | |
| 4167 get isAnimationComplete() { | |
| 4168 return this.mouseUpStart ? | |
| 4169 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
| 4170 }, | |
| 4171 | |
| 4172 get translationFraction() { | |
| 4173 return Math.min( | |
| 4174 1, | |
| 4175 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
| 4176 ); | |
| 4177 }, | |
| 4178 | |
| 4179 get xNow() { | |
| 4180 if (this.xEnd) { | |
| 4181 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
| 4182 } | |
| 4183 | |
| 4184 return this.xStart; | |
| 4185 }, | |
| 4186 | |
| 4187 get yNow() { | |
| 4188 if (this.yEnd) { | |
| 4189 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
| 4190 } | |
| 4191 | |
| 4192 return this.yStart; | |
| 4193 }, | |
| 4194 | |
| 4195 get isMouseDown() { | |
| 4196 return this.mouseDownStart && !this.mouseUpStart; | |
| 4197 }, | |
| 4198 | |
| 4199 resetInteractionState: function() { | |
| 4200 this.maxRadius = 0; | |
| 4201 this.mouseDownStart = 0; | |
| 4202 this.mouseUpStart = 0; | |
| 4203 | |
| 4204 this.xStart = 0; | |
| 4205 this.yStart = 0; | |
| 4206 this.xEnd = 0; | |
| 4207 this.yEnd = 0; | |
| 4208 this.slideDistance = 0; | |
| 4209 | |
| 4210 this.containerMetrics = new ElementMetrics(this.element); | |
| 4211 }, | |
| 4212 | |
| 4213 draw: function() { | |
| 4214 var scale; | |
| 4215 var translateString; | |
| 4216 var dx; | |
| 4217 var dy; | |
| 4218 | |
| 4219 this.wave.style.opacity = this.opacity; | |
| 4220 | |
| 4221 scale = this.radius / (this.containerMetrics.size / 2); | |
| 4222 dx = this.xNow - (this.containerMetrics.width / 2); | |
| 4223 dy = this.yNow - (this.containerMetrics.height / 2); | |
| 4224 | |
| 4225 | |
| 4226 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
| 4227 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
| 4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
| 4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
| 4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
| 4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
| 4232 }, | |
| 4233 | |
| 4234 /** @param {Event=} event */ | |
| 4235 downAction: function(event) { | |
| 4236 var xCenter = this.containerMetrics.width / 2; | |
| 4237 var yCenter = this.containerMetrics.height / 2; | |
| 4238 | |
| 4239 this.resetInteractionState(); | |
| 4240 this.mouseDownStart = Utility.now(); | |
| 4241 | |
| 4242 if (this.center) { | |
| 4243 this.xStart = xCenter; | |
| 4244 this.yStart = yCenter; | |
| 4245 this.slideDistance = Utility.distance( | |
| 4246 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 4247 ); | |
| 4248 } else { | |
| 4249 this.xStart = event ? | |
| 4250 event.detail.x - this.containerMetrics.boundingRect.left : | |
| 4251 this.containerMetrics.width / 2; | |
| 4252 this.yStart = event ? | |
| 4253 event.detail.y - this.containerMetrics.boundingRect.top : | |
| 4254 this.containerMetrics.height / 2; | |
| 4255 } | |
| 4256 | |
| 4257 if (this.recenters) { | |
| 4258 this.xEnd = xCenter; | |
| 4259 this.yEnd = yCenter; | |
| 4260 this.slideDistance = Utility.distance( | |
| 4261 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 4262 ); | |
| 4263 } | |
| 4264 | |
| 4265 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
| 4266 this.xStart, | |
| 4267 this.yStart | |
| 4268 ); | |
| 4269 | |
| 4270 this.waveContainer.style.top = | |
| 4271 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
| 4272 this.waveContainer.style.left = | |
| 4273 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
| 4274 | |
| 4275 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
| 4276 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
| 4277 }, | |
| 4278 | |
| 4279 /** @param {Event=} event */ | |
| 4280 upAction: function(event) { | |
| 4281 if (!this.isMouseDown) { | |
| 4282 return; | |
| 4283 } | |
| 4284 | |
| 4285 this.mouseUpStart = Utility.now(); | |
| 4286 }, | |
| 4287 | |
| 4288 remove: function() { | |
| 4289 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
| 4290 this.waveContainer | |
| 4291 ); | |
| 4292 } | |
| 4293 }; | |
| 4294 | |
| 4295 Polymer({ | |
| 4296 is: 'paper-ripple', | |
| 4297 | |
| 4298 behaviors: [ | |
| 4299 Polymer.IronA11yKeysBehavior | |
| 4300 ], | |
| 4301 | |
| 4302 properties: { | |
| 4303 /** | |
| 4304 * The initial opacity set on the wave. | |
| 4305 * | |
| 4306 * @attribute initialOpacity | |
| 4307 * @type number | |
| 4308 * @default 0.25 | |
| 4309 */ | |
| 4310 initialOpacity: { | |
| 4311 type: Number, | |
| 4312 value: 0.25 | |
| 4313 }, | |
| 4314 | |
| 4315 /** | |
| 4316 * How fast (opacity per second) the wave fades out. | |
| 4317 * | |
| 4318 * @attribute opacityDecayVelocity | |
| 4319 * @type number | |
| 4320 * @default 0.8 | |
| 4321 */ | |
| 4322 opacityDecayVelocity: { | |
| 4323 type: Number, | |
| 4324 value: 0.8 | |
| 4325 }, | |
| 4326 | |
| 4327 /** | |
| 4328 * If true, ripples will exhibit a gravitational pull towards | |
| 4329 * the center of their container as they fade away. | |
| 4330 * | |
| 4331 * @attribute recenters | |
| 4332 * @type boolean | |
| 4333 * @default false | |
| 4334 */ | |
| 4335 recenters: { | |
| 4336 type: Boolean, | |
| 4337 value: false | |
| 4338 }, | |
| 4339 | |
| 4340 /** | |
| 4341 * If true, ripples will center inside its container | |
| 4342 * | |
| 4343 * @attribute recenters | |
| 4344 * @type boolean | |
| 4345 * @default false | |
| 4346 */ | |
| 4347 center: { | |
| 4348 type: Boolean, | |
| 4349 value: false | |
| 4350 }, | |
| 4351 | |
| 4352 /** | |
| 4353 * A list of the visual ripples. | |
| 4354 * | |
| 4355 * @attribute ripples | |
| 4356 * @type Array | |
| 4357 * @default [] | |
| 4358 */ | |
| 4359 ripples: { | |
| 4360 type: Array, | |
| 4361 value: function() { | |
| 4362 return []; | |
| 4363 } | |
| 4364 }, | |
| 4365 | |
| 4366 /** | |
| 4367 * True when there are visible ripples animating within the | |
| 4368 * element. | |
| 4369 */ | |
| 4370 animating: { | |
| 4371 type: Boolean, | |
| 4372 readOnly: true, | |
| 4373 reflectToAttribute: true, | |
| 4374 value: false | |
| 4375 }, | |
| 4376 | |
| 4377 /** | |
| 4378 * If true, the ripple will remain in the "down" state until `holdDown` | |
| 4379 * is set to false again. | |
| 4380 */ | |
| 4381 holdDown: { | |
| 4382 type: Boolean, | |
| 4383 value: false, | |
| 4384 observer: '_holdDownChanged' | |
| 4385 }, | |
| 4386 | |
| 4387 /** | |
| 4388 * If true, the ripple will not generate a ripple effect | |
| 4389 * via pointer interaction. | |
| 4390 * Calling ripple's imperative api like `simulatedRipple` will | |
| 4391 * still generate the ripple effect. | |
| 4392 */ | |
| 4393 noink: { | |
| 4394 type: Boolean, | |
| 4395 value: false | |
| 4396 }, | |
| 4397 | |
| 4398 _animating: { | |
| 4399 type: Boolean | |
| 4400 }, | |
| 4401 | |
| 4402 _boundAnimate: { | |
| 4403 type: Function, | |
| 4404 value: function() { | |
| 4405 return this.animate.bind(this); | |
| 4406 } | |
| 4407 } | |
| 4408 }, | |
| 4409 | |
| 4410 get target () { | |
| 4411 return this.keyEventTarget; | |
| 4412 }, | |
| 4413 | |
| 4414 keyBindings: { | |
| 4415 'enter:keydown': '_onEnterKeydown', | |
| 4416 'space:keydown': '_onSpaceKeydown', | |
| 4417 'space:keyup': '_onSpaceKeyup' | |
| 4418 }, | |
| 4419 | |
| 4420 attached: function() { | |
| 4421 // Set up a11yKeysBehavior to listen to key events on the target, | |
| 4422 // so that space and enter activate the ripple even if the target doesn'
t | |
| 4423 // handle key events. The key handlers deal with `noink` themselves. | |
| 4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
| 4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
| 4426 } else { | |
| 4427 this.keyEventTarget = this.parentNode; | |
| 4428 } | |
| 4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
| 4430 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
| 4431 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
| 4432 }, | |
| 4433 | |
| 4434 detached: function() { | |
| 4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
| 4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
| 4437 this.keyEventTarget = null; | |
| 4438 }, | |
| 4439 | |
| 4440 get shouldKeepAnimating () { | |
| 4441 for (var index = 0; index < this.ripples.length; ++index) { | |
| 4442 if (!this.ripples[index].isAnimationComplete) { | |
| 4443 return true; | |
| 4444 } | |
| 4445 } | |
| 4446 | |
| 4447 return false; | |
| 4448 }, | |
| 4449 | |
| 4450 simulatedRipple: function() { | |
| 4451 this.downAction(null); | |
| 4452 | |
| 4453 // Please see polymer/polymer#1305 | |
| 4454 this.async(function() { | |
| 4455 this.upAction(); | |
| 4456 }, 1); | |
| 4457 }, | |
| 4458 | |
| 4459 /** | |
| 4460 * Provokes a ripple down effect via a UI event, | |
| 4461 * respecting the `noink` property. | |
| 4462 * @param {Event=} event | |
| 4463 */ | |
| 4464 uiDownAction: function(event) { | |
| 4465 if (!this.noink) { | |
| 4466 this.downAction(event); | |
| 4467 } | |
| 4468 }, | |
| 4469 | |
| 4470 /** | |
| 4471 * Provokes a ripple down effect via a UI event, | |
| 4472 * *not* respecting the `noink` property. | |
| 4473 * @param {Event=} event | |
| 4474 */ | |
| 4475 downAction: function(event) { | |
| 4476 if (this.holdDown && this.ripples.length > 0) { | |
| 4477 return; | |
| 4478 } | |
| 4479 | |
| 4480 var ripple = this.addRipple(); | |
| 4481 | |
| 4482 ripple.downAction(event); | |
| 4483 | |
| 4484 if (!this._animating) { | |
| 4485 this._animating = true; | |
| 4486 this.animate(); | |
| 4487 } | |
| 4488 }, | |
| 4489 | |
| 4490 /** | |
| 4491 * Provokes a ripple up effect via a UI event, | |
| 4492 * respecting the `noink` property. | |
| 4493 * @param {Event=} event | |
| 4494 */ | |
| 4495 uiUpAction: function(event) { | |
| 4496 if (!this.noink) { | |
| 4497 this.upAction(event); | |
| 4498 } | |
| 4499 }, | |
| 4500 | |
| 4501 /** | |
| 4502 * Provokes a ripple up effect via a UI event, | |
| 4503 * *not* respecting the `noink` property. | |
| 4504 * @param {Event=} event | |
| 4505 */ | |
| 4506 upAction: function(event) { | |
| 4507 if (this.holdDown) { | |
| 4508 return; | |
| 4509 } | |
| 4510 | |
| 4511 this.ripples.forEach(function(ripple) { | |
| 4512 ripple.upAction(event); | |
| 4513 }); | |
| 4514 | |
| 4515 this._animating = true; | |
| 4516 this.animate(); | |
| 4517 }, | |
| 4518 | |
| 4519 onAnimationComplete: function() { | |
| 4520 this._animating = false; | |
| 4521 this.$.background.style.backgroundColor = null; | |
| 4522 this.fire('transitionend'); | |
| 4523 }, | |
| 4524 | |
| 4525 addRipple: function() { | |
| 4526 var ripple = new Ripple(this); | |
| 4527 | |
| 4528 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
| 4529 this.$.background.style.backgroundColor = ripple.color; | |
| 4530 this.ripples.push(ripple); | |
| 4531 | |
| 4532 this._setAnimating(true); | |
| 4533 | |
| 4534 return ripple; | |
| 4535 }, | |
| 4536 | |
| 4537 removeRipple: function(ripple) { | |
| 4538 var rippleIndex = this.ripples.indexOf(ripple); | |
| 4539 | |
| 4540 if (rippleIndex < 0) { | |
| 4541 return; | |
| 4542 } | |
| 4543 | |
| 4544 this.ripples.splice(rippleIndex, 1); | |
| 4545 | |
| 4546 ripple.remove(); | |
| 4547 | |
| 4548 if (!this.ripples.length) { | |
| 4549 this._setAnimating(false); | |
| 4550 } | |
| 4551 }, | |
| 4552 | |
| 4553 animate: function() { | |
| 4554 if (!this._animating) { | |
| 4555 return; | |
| 4556 } | |
| 4557 var index; | |
| 4558 var ripple; | |
| 4559 | |
| 4560 for (index = 0; index < this.ripples.length; ++index) { | |
| 4561 ripple = this.ripples[index]; | |
| 4562 | |
| 4563 ripple.draw(); | |
| 4564 | |
| 4565 this.$.background.style.opacity = ripple.outerOpacity; | |
| 4566 | |
| 4567 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
| 4568 this.removeRipple(ripple); | |
| 4569 } | |
| 4570 } | |
| 4571 | |
| 4572 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
| 4573 this.onAnimationComplete(); | |
| 4574 } else { | |
| 4575 window.requestAnimationFrame(this._boundAnimate); | |
| 4576 } | |
| 4577 }, | |
| 4578 | |
| 4579 _onEnterKeydown: function() { | |
| 4580 this.uiDownAction(); | |
| 4581 this.async(this.uiUpAction, 1); | |
| 4582 }, | |
| 4583 | |
| 4584 _onSpaceKeydown: function() { | |
| 4585 this.uiDownAction(); | |
| 4586 }, | |
| 4587 | |
| 4588 _onSpaceKeyup: function() { | |
| 4589 this.uiUpAction(); | |
| 4590 }, | |
| 4591 | |
| 4592 // note: holdDown does not respect noink since it can be a focus based | |
| 4593 // effect. | |
| 4594 _holdDownChanged: function(newVal, oldVal) { | |
| 4595 if (oldVal === undefined) { | |
| 4596 return; | |
| 4597 } | |
| 4598 if (newVal) { | |
| 4599 this.downAction(); | |
| 4600 } else { | |
| 4601 this.upAction(); | |
| 4602 } | |
| 4603 } | |
| 4604 | |
| 4605 /** | |
| 4606 Fired when the animation finishes. | |
| 4607 This is useful if you want to wait until | |
| 4608 the ripple animation finishes to perform some action. | |
| 4609 | |
| 4610 @event transitionend | |
| 4611 @param {{node: Object}} detail Contains the animated node. | |
| 4612 */ | |
| 4613 }); | 3339 }); |
| 4614 })(); | 3340 }, |
| 4615 /** | 3341 _onUpKey: function(event) { |
| 4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | 3342 this._focusPrevious(); |
| 4617 * when the element has focus via pointer or keyboard. | 3343 event.detail.keyboardEvent.preventDefault(); |
| 4618 * | 3344 }, |
| 4619 * NOTE: This behavior is intended to be used in conjunction with and after | 3345 _onDownKey: function(event) { |
| 4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | 3346 this._focusNext(); |
| 4621 * | 3347 event.detail.keyboardEvent.preventDefault(); |
| 4622 * @polymerBehavior Polymer.PaperRippleBehavior | 3348 }, |
| 4623 */ | 3349 _onEscKey: function(event) { |
| 4624 Polymer.PaperRippleBehavior = { | 3350 this.focusedItem.blur(); |
| 4625 properties: { | 3351 }, |
| 4626 /** | 3352 _onKeydown: function(event) { |
| 4627 * If true, the element will not produce a ripple effect when interacted | 3353 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| 4628 * with via the pointer. | 3354 this._focusWithKeyboardEvent(event); |
| 4629 */ | 3355 } |
| 4630 noink: { | 3356 event.stopPropagation(); |
| 4631 type: Boolean, | 3357 }, |
| 4632 observer: '_noinkChanged' | 3358 _activateHandler: function(event) { |
| 4633 }, | 3359 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| 4634 | 3360 event.stopPropagation(); |
| 4635 /** | 3361 } |
| 4636 * @type {Element|undefined} | 3362 }; |
| 4637 */ | 3363 |
| 4638 _rippleContainer: { | 3364 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 4639 type: Object, | 3365 |
| 4640 } | 3366 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1
1yKeysBehavior, Polymer.IronMenuBehaviorImpl ]; |
| 4641 }, | 3367 |
| 4642 | 3368 Polymer.IronMenubarBehaviorImpl = { |
| 4643 /** | 3369 hostAttributes: { |
| 4644 * Ensures a `<paper-ripple>` element is available when the element is | 3370 role: 'menubar' |
| 4645 * focused. | 3371 }, |
| 4646 */ | 3372 keyBindings: { |
| 4647 _buttonStateChanged: function() { | 3373 left: '_onLeftKey', |
| 4648 if (this.focused) { | 3374 right: '_onRightKey' |
| 4649 this.ensureRipple(); | 3375 }, |
| 4650 } | 3376 _onUpKey: function(event) { |
| 4651 }, | 3377 this.focusedItem.click(); |
| 4652 | 3378 event.detail.keyboardEvent.preventDefault(); |
| 4653 /** | 3379 }, |
| 4654 * In addition to the functionality provided in `IronButtonState`, ensures | 3380 _onDownKey: function(event) { |
| 4655 * a ripple effect is created when the element is in a `pressed` state. | 3381 this.focusedItem.click(); |
| 4656 */ | 3382 event.detail.keyboardEvent.preventDefault(); |
| 4657 _downHandler: function(event) { | 3383 }, |
| 4658 Polymer.IronButtonStateImpl._downHandler.call(this, event); | 3384 get _isRTL() { |
| 4659 if (this.pressed) { | 3385 return window.getComputedStyle(this)['direction'] === 'rtl'; |
| 4660 this.ensureRipple(event); | 3386 }, |
| 4661 } | 3387 _onLeftKey: function(event) { |
| 4662 }, | 3388 if (this._isRTL) { |
| 4663 | 3389 this._focusNext(); |
| 4664 /** | 3390 } else { |
| 4665 * Ensures this element contains a ripple effect. For startup efficiency | 3391 this._focusPrevious(); |
| 4666 * the ripple effect is dynamically on demand when needed. | 3392 } |
| 4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | 3393 event.detail.keyboardEvent.preventDefault(); |
| 4668 * ripple. | 3394 }, |
| 4669 */ | 3395 _onRightKey: function(event) { |
| 4670 ensureRipple: function(optTriggeringEvent) { | 3396 if (this._isRTL) { |
| 4671 if (!this.hasRipple()) { | 3397 this._focusPrevious(); |
| 4672 this._ripple = this._createRipple(); | 3398 } else { |
| 4673 this._ripple.noink = this.noink; | 3399 this._focusNext(); |
| 4674 var rippleContainer = this._rippleContainer || this.root; | 3400 } |
| 4675 if (rippleContainer) { | 3401 event.detail.keyboardEvent.preventDefault(); |
| 4676 Polymer.dom(rippleContainer).appendChild(this._ripple); | 3402 }, |
| 4677 } | 3403 _onKeydown: function(event) { |
| 4678 if (optTriggeringEvent) { | 3404 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { |
| 4679 // Check if the event happened inside of the ripple container | 3405 return; |
| 4680 // Fall back to host instead of the root because distributed text | 3406 } |
| 4681 // nodes are not valid event targets | 3407 this._focusWithKeyboardEvent(event); |
| 4682 var domContainer = Polymer.dom(this._rippleContainer || this); | 3408 } |
| 4683 var target = Polymer.dom(optTriggeringEvent).rootTarget; | 3409 }; |
| 4684 if (domContainer.deepContains( /** @type {Node} */(target))) { | 3410 |
| 4685 this._ripple.uiDownAction(optTriggeringEvent); | 3411 Polymer.IronMenubarBehavior = [ Polymer.IronMenuBehavior, Polymer.IronMenubarBeh
aviorImpl ]; |
| 4686 } | 3412 |
| 4687 } | |
| 4688 } | |
| 4689 }, | |
| 4690 | |
| 4691 /** | |
| 4692 * Returns the `<paper-ripple>` element used by this element to create | |
| 4693 * ripple effects. The element's ripple is created on demand, when | |
| 4694 * necessary, and calling this method will force the | |
| 4695 * ripple to be created. | |
| 4696 */ | |
| 4697 getRipple: function() { | |
| 4698 this.ensureRipple(); | |
| 4699 return this._ripple; | |
| 4700 }, | |
| 4701 | |
| 4702 /** | |
| 4703 * Returns true if this element currently contains a ripple effect. | |
| 4704 * @return {boolean} | |
| 4705 */ | |
| 4706 hasRipple: function() { | |
| 4707 return Boolean(this._ripple); | |
| 4708 }, | |
| 4709 | |
| 4710 /** | |
| 4711 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
| 4712 * Override this method to customize the ripple element. | |
| 4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
| 4714 */ | |
| 4715 _createRipple: function() { | |
| 4716 return /** @type {!PaperRippleElement} */ ( | |
| 4717 document.createElement('paper-ripple')); | |
| 4718 }, | |
| 4719 | |
| 4720 _noinkChanged: function(noink) { | |
| 4721 if (this.hasRipple()) { | |
| 4722 this._ripple.noink = noink; | |
| 4723 } | |
| 4724 } | |
| 4725 }; | |
| 4726 /** @polymerBehavior Polymer.PaperButtonBehavior */ | |
| 4727 Polymer.PaperButtonBehaviorImpl = { | |
| 4728 properties: { | |
| 4729 /** | |
| 4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
| 4731 * shadow, and each increasing number greater than 0 will be "deeper" | |
| 4732 * than the last. | |
| 4733 * | |
| 4734 * @attribute elevation | |
| 4735 * @type number | |
| 4736 * @default 1 | |
| 4737 */ | |
| 4738 elevation: { | |
| 4739 type: Number, | |
| 4740 reflectToAttribute: true, | |
| 4741 readOnly: true | |
| 4742 } | |
| 4743 }, | |
| 4744 | |
| 4745 observers: [ | |
| 4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
| 4747 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
| 4748 ], | |
| 4749 | |
| 4750 hostAttributes: { | |
| 4751 role: 'button', | |
| 4752 tabindex: '0', | |
| 4753 animated: true | |
| 4754 }, | |
| 4755 | |
| 4756 _calculateElevation: function() { | |
| 4757 var e = 1; | |
| 4758 if (this.disabled) { | |
| 4759 e = 0; | |
| 4760 } else if (this.active || this.pressed) { | |
| 4761 e = 4; | |
| 4762 } else if (this.receivedFocusFromKeyboard) { | |
| 4763 e = 3; | |
| 4764 } | |
| 4765 this._setElevation(e); | |
| 4766 }, | |
| 4767 | |
| 4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
| 4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
| 4770 }, | |
| 4771 | |
| 4772 /** | |
| 4773 * In addition to `IronButtonState` behavior, when space key goes down, | |
| 4774 * create a ripple down effect. | |
| 4775 * | |
| 4776 * @param {!KeyboardEvent} event . | |
| 4777 */ | |
| 4778 _spaceKeyDownHandler: function(event) { | |
| 4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
| 4780 // Ensure that there is at most one ripple when the space key is held down
. | |
| 4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
| 4782 this._ripple.uiDownAction(); | |
| 4783 } | |
| 4784 }, | |
| 4785 | |
| 4786 /** | |
| 4787 * In addition to `IronButtonState` behavior, when space key goes up, | |
| 4788 * create a ripple up effect. | |
| 4789 * | |
| 4790 * @param {!KeyboardEvent} event . | |
| 4791 */ | |
| 4792 _spaceKeyUpHandler: function(event) { | |
| 4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
| 4794 if (this.hasRipple()) { | |
| 4795 this._ripple.uiUpAction(); | |
| 4796 } | |
| 4797 } | |
| 4798 }; | |
| 4799 | |
| 4800 /** @polymerBehavior */ | |
| 4801 Polymer.PaperButtonBehavior = [ | |
| 4802 Polymer.IronButtonState, | |
| 4803 Polymer.IronControlState, | |
| 4804 Polymer.PaperRippleBehavior, | |
| 4805 Polymer.PaperButtonBehaviorImpl | |
| 4806 ]; | |
| 4807 Polymer({ | 3413 Polymer({ |
| 4808 is: 'paper-button', | 3414 is: 'iron-iconset-svg', |
| 4809 | 3415 properties: { |
| 4810 behaviors: [ | 3416 name: { |
| 4811 Polymer.PaperButtonBehavior | 3417 type: String, |
| 4812 ], | 3418 observer: '_nameChanged' |
| 4813 | 3419 }, |
| 4814 properties: { | 3420 size: { |
| 4815 /** | 3421 type: Number, |
| 4816 * If true, the button should be styled with a shadow. | 3422 value: 24 |
| 4817 */ | 3423 } |
| 4818 raised: { | 3424 }, |
| 4819 type: Boolean, | 3425 attached: function() { |
| 4820 reflectToAttribute: true, | 3426 this.style.display = 'none'; |
| 4821 value: false, | 3427 }, |
| 4822 observer: '_calculateElevation' | 3428 getIconNames: function() { |
| 4823 } | 3429 this._icons = this._createIconMap(); |
| 4824 }, | 3430 return Object.keys(this._icons).map(function(n) { |
| 4825 | 3431 return this.name + ':' + n; |
| 4826 _calculateElevation: function() { | 3432 }, this); |
| 4827 if (!this.raised) { | 3433 }, |
| 4828 this._setElevation(0); | 3434 applyIcon: function(element, iconName) { |
| 4829 } else { | 3435 element = element.root || element; |
| 4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | 3436 this.removeIcon(element); |
| 4831 } | 3437 var svg = this._cloneIcon(iconName); |
| 4832 } | 3438 if (svg) { |
| 4833 | 3439 var pde = Polymer.dom(element); |
| 4834 /** | 3440 pde.insertBefore(svg, pde.childNodes[0]); |
| 4835 Fired when the animation finishes. | 3441 return element._svgIcon = svg; |
| 4836 This is useful if you want to wait until | 3442 } |
| 4837 the ripple animation finishes to perform some action. | 3443 return null; |
| 4838 | 3444 }, |
| 4839 @event transitionend | 3445 removeIcon: function(element) { |
| 4840 Event param: {{node: Object}} detail Contains the animated node. | 3446 if (element._svgIcon) { |
| 4841 */ | 3447 Polymer.dom(element).removeChild(element._svgIcon); |
| 3448 element._svgIcon = null; |
| 3449 } |
| 3450 }, |
| 3451 _nameChanged: function() { |
| 3452 new Polymer.IronMeta({ |
| 3453 type: 'iconset', |
| 3454 key: this.name, |
| 3455 value: this |
| 4842 }); | 3456 }); |
| 4843 (function() { | 3457 this.async(function() { |
| 4844 | 3458 this.fire('iron-iconset-added', this, { |
| 4845 // monostate data | 3459 node: window |
| 4846 var metaDatas = {}; | 3460 }); |
| 4847 var metaArrays = {}; | |
| 4848 var singleton = null; | |
| 4849 | |
| 4850 Polymer.IronMeta = Polymer({ | |
| 4851 | |
| 4852 is: 'iron-meta', | |
| 4853 | |
| 4854 properties: { | |
| 4855 | |
| 4856 /** | |
| 4857 * The type of meta-data. All meta-data of the same type is stored | |
| 4858 * together. | |
| 4859 */ | |
| 4860 type: { | |
| 4861 type: String, | |
| 4862 value: 'default', | |
| 4863 observer: '_typeChanged' | |
| 4864 }, | |
| 4865 | |
| 4866 /** | |
| 4867 * The key used to store `value` under the `type` namespace. | |
| 4868 */ | |
| 4869 key: { | |
| 4870 type: String, | |
| 4871 observer: '_keyChanged' | |
| 4872 }, | |
| 4873 | |
| 4874 /** | |
| 4875 * The meta-data to store or retrieve. | |
| 4876 */ | |
| 4877 value: { | |
| 4878 type: Object, | |
| 4879 notify: true, | |
| 4880 observer: '_valueChanged' | |
| 4881 }, | |
| 4882 | |
| 4883 /** | |
| 4884 * If true, `value` is set to the iron-meta instance itself. | |
| 4885 */ | |
| 4886 self: { | |
| 4887 type: Boolean, | |
| 4888 observer: '_selfChanged' | |
| 4889 }, | |
| 4890 | |
| 4891 /** | |
| 4892 * Array of all meta-data values for the given type. | |
| 4893 */ | |
| 4894 list: { | |
| 4895 type: Array, | |
| 4896 notify: true | |
| 4897 } | |
| 4898 | |
| 4899 }, | |
| 4900 | |
| 4901 hostAttributes: { | |
| 4902 hidden: true | |
| 4903 }, | |
| 4904 | |
| 4905 /** | |
| 4906 * Only runs if someone invokes the factory/constructor directly | |
| 4907 * e.g. `new Polymer.IronMeta()` | |
| 4908 * | |
| 4909 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
| 4910 */ | |
| 4911 factoryImpl: function(config) { | |
| 4912 if (config) { | |
| 4913 for (var n in config) { | |
| 4914 switch(n) { | |
| 4915 case 'type': | |
| 4916 case 'key': | |
| 4917 case 'value': | |
| 4918 this[n] = config[n]; | |
| 4919 break; | |
| 4920 } | |
| 4921 } | |
| 4922 } | |
| 4923 }, | |
| 4924 | |
| 4925 created: function() { | |
| 4926 // TODO(sjmiles): good for debugging? | |
| 4927 this._metaDatas = metaDatas; | |
| 4928 this._metaArrays = metaArrays; | |
| 4929 }, | |
| 4930 | |
| 4931 _keyChanged: function(key, old) { | |
| 4932 this._resetRegistration(old); | |
| 4933 }, | |
| 4934 | |
| 4935 _valueChanged: function(value) { | |
| 4936 this._resetRegistration(this.key); | |
| 4937 }, | |
| 4938 | |
| 4939 _selfChanged: function(self) { | |
| 4940 if (self) { | |
| 4941 this.value = this; | |
| 4942 } | |
| 4943 }, | |
| 4944 | |
| 4945 _typeChanged: function(type) { | |
| 4946 this._unregisterKey(this.key); | |
| 4947 if (!metaDatas[type]) { | |
| 4948 metaDatas[type] = {}; | |
| 4949 } | |
| 4950 this._metaData = metaDatas[type]; | |
| 4951 if (!metaArrays[type]) { | |
| 4952 metaArrays[type] = []; | |
| 4953 } | |
| 4954 this.list = metaArrays[type]; | |
| 4955 this._registerKeyValue(this.key, this.value); | |
| 4956 }, | |
| 4957 | |
| 4958 /** | |
| 4959 * Retrieves meta data value by key. | |
| 4960 * | |
| 4961 * @method byKey | |
| 4962 * @param {string} key The key of the meta-data to be returned. | |
| 4963 * @return {*} | |
| 4964 */ | |
| 4965 byKey: function(key) { | |
| 4966 return this._metaData && this._metaData[key]; | |
| 4967 }, | |
| 4968 | |
| 4969 _resetRegistration: function(oldKey) { | |
| 4970 this._unregisterKey(oldKey); | |
| 4971 this._registerKeyValue(this.key, this.value); | |
| 4972 }, | |
| 4973 | |
| 4974 _unregisterKey: function(key) { | |
| 4975 this._unregister(key, this._metaData, this.list); | |
| 4976 }, | |
| 4977 | |
| 4978 _registerKeyValue: function(key, value) { | |
| 4979 this._register(key, value, this._metaData, this.list); | |
| 4980 }, | |
| 4981 | |
| 4982 _register: function(key, value, data, list) { | |
| 4983 if (key && data && value !== undefined) { | |
| 4984 data[key] = value; | |
| 4985 list.push(value); | |
| 4986 } | |
| 4987 }, | |
| 4988 | |
| 4989 _unregister: function(key, data, list) { | |
| 4990 if (key && data) { | |
| 4991 if (key in data) { | |
| 4992 var value = data[key]; | |
| 4993 delete data[key]; | |
| 4994 this.arrayDelete(list, value); | |
| 4995 } | |
| 4996 } | |
| 4997 } | |
| 4998 | |
| 4999 }); | 3461 }); |
| 5000 | 3462 }, |
| 5001 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 3463 _createIconMap: function() { |
| 5002 if (singleton === null) { | 3464 var icons = Object.create(null); |
| 5003 singleton = new Polymer.IronMeta(); | 3465 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) { |
| 5004 } | 3466 icons[icon.id] = icon; |
| 5005 return singleton; | |
| 5006 }; | |
| 5007 | |
| 5008 /** | |
| 5009 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | |
| 5010 | |
| 5011 Examples: | |
| 5012 | |
| 5013 If I create an instance like this: | |
| 5014 | |
| 5015 <iron-meta key="info" value="foo/bar"></iron-meta> | |
| 5016 | |
| 5017 Note that value="foo/bar" is the metadata I've defined. I could define more | |
| 5018 attributes or use child nodes to define additional metadata. | |
| 5019 | |
| 5020 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | |
| 5021 | |
| 5022 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | |
| 5023 | |
| 5024 @group Polymer Iron Elements | |
| 5025 @element iron-meta-query | |
| 5026 */ | |
| 5027 Polymer.IronMetaQuery = Polymer({ | |
| 5028 | |
| 5029 is: 'iron-meta-query', | |
| 5030 | |
| 5031 properties: { | |
| 5032 | |
| 5033 /** | |
| 5034 * The type of meta-data. All meta-data of the same type is stored | |
| 5035 * together. | |
| 5036 */ | |
| 5037 type: { | |
| 5038 type: String, | |
| 5039 value: 'default', | |
| 5040 observer: '_typeChanged' | |
| 5041 }, | |
| 5042 | |
| 5043 /** | |
| 5044 * Specifies a key to use for retrieving `value` from the `type` | |
| 5045 * namespace. | |
| 5046 */ | |
| 5047 key: { | |
| 5048 type: String, | |
| 5049 observer: '_keyChanged' | |
| 5050 }, | |
| 5051 | |
| 5052 /** | |
| 5053 * The meta-data to store or retrieve. | |
| 5054 */ | |
| 5055 value: { | |
| 5056 type: Object, | |
| 5057 notify: true, | |
| 5058 readOnly: true | |
| 5059 }, | |
| 5060 | |
| 5061 /** | |
| 5062 * Array of all meta-data values for the given type. | |
| 5063 */ | |
| 5064 list: { | |
| 5065 type: Array, | |
| 5066 notify: true | |
| 5067 } | |
| 5068 | |
| 5069 }, | |
| 5070 | |
| 5071 /** | |
| 5072 * Actually a factory method, not a true constructor. Only runs if | |
| 5073 * someone invokes it directly (via `new Polymer.IronMeta()`); | |
| 5074 * | |
| 5075 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
| 5076 */ | |
| 5077 factoryImpl: function(config) { | |
| 5078 if (config) { | |
| 5079 for (var n in config) { | |
| 5080 switch(n) { | |
| 5081 case 'type': | |
| 5082 case 'key': | |
| 5083 this[n] = config[n]; | |
| 5084 break; | |
| 5085 } | |
| 5086 } | |
| 5087 } | |
| 5088 }, | |
| 5089 | |
| 5090 created: function() { | |
| 5091 // TODO(sjmiles): good for debugging? | |
| 5092 this._metaDatas = metaDatas; | |
| 5093 this._metaArrays = metaArrays; | |
| 5094 }, | |
| 5095 | |
| 5096 _keyChanged: function(key) { | |
| 5097 this._setValue(this._metaData && this._metaData[key]); | |
| 5098 }, | |
| 5099 | |
| 5100 _typeChanged: function(type) { | |
| 5101 this._metaData = metaDatas[type]; | |
| 5102 this.list = metaArrays[type]; | |
| 5103 if (this.key) { | |
| 5104 this._keyChanged(this.key); | |
| 5105 } | |
| 5106 }, | |
| 5107 | |
| 5108 /** | |
| 5109 * Retrieves meta data value by key. | |
| 5110 * @param {string} key The key of the meta-data to be returned. | |
| 5111 * @return {*} | |
| 5112 */ | |
| 5113 byKey: function(key) { | |
| 5114 return this._metaData && this._metaData[key]; | |
| 5115 } | |
| 5116 | |
| 5117 }); | 3467 }); |
| 5118 | 3468 return icons; |
| 5119 })(); | 3469 }, |
| 3470 _cloneIcon: function(id) { |
| 3471 this._icons = this._icons || this._createIconMap(); |
| 3472 return this._prepareSvgClone(this._icons[id], this.size); |
| 3473 }, |
| 3474 _prepareSvgClone: function(sourceSvg, size) { |
| 3475 if (sourceSvg) { |
| 3476 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h
ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') ||
'0 0 ' + size + ' ' + size; |
| 3477 svg.setAttribute('viewBox', viewBox); |
| 3478 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
| 3479 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he
ight: 100%;'; |
| 3480 svg.appendChild(content).removeAttribute('id'); |
| 3481 return svg; |
| 3482 } |
| 3483 return null; |
| 3484 } |
| 3485 }); |
| 3486 |
| 5120 Polymer({ | 3487 Polymer({ |
| 5121 | 3488 is: 'paper-tabs', |
| 5122 is: 'iron-icon', | 3489 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronMenubarBehavior ], |
| 5123 | 3490 properties: { |
| 5124 properties: { | 3491 noink: { |
| 5125 | 3492 type: Boolean, |
| 5126 /** | 3493 value: false, |
| 5127 * The name of the icon to use. The name should be of the form: | 3494 observer: '_noinkChanged' |
| 5128 * `iconset_name:icon_name`. | 3495 }, |
| 5129 */ | 3496 noBar: { |
| 5130 icon: { | 3497 type: Boolean, |
| 5131 type: String, | 3498 value: false |
| 5132 observer: '_iconChanged' | 3499 }, |
| 5133 }, | 3500 noSlide: { |
| 5134 | 3501 type: Boolean, |
| 5135 /** | 3502 value: false |
| 5136 * The name of the theme to used, if one is specified by the | 3503 }, |
| 5137 * iconset. | 3504 scrollable: { |
| 5138 */ | 3505 type: Boolean, |
| 5139 theme: { | 3506 value: false |
| 5140 type: String, | 3507 }, |
| 5141 observer: '_updateIcon' | 3508 fitContainer: { |
| 5142 }, | 3509 type: Boolean, |
| 5143 | 3510 value: false |
| 5144 /** | 3511 }, |
| 5145 * If using iron-icon without an iconset, you can set the src to be | 3512 disableDrag: { |
| 5146 * the URL of an individual icon image file. Note that this will take | 3513 type: Boolean, |
| 5147 * precedence over a given icon attribute. | 3514 value: false |
| 5148 */ | 3515 }, |
| 5149 src: { | 3516 hideScrollButtons: { |
| 5150 type: String, | 3517 type: Boolean, |
| 5151 observer: '_srcChanged' | 3518 value: false |
| 5152 }, | 3519 }, |
| 5153 | 3520 alignBottom: { |
| 5154 /** | 3521 type: Boolean, |
| 5155 * @type {!Polymer.IronMeta} | 3522 value: false |
| 5156 */ | 3523 }, |
| 5157 _meta: { | 3524 selectable: { |
| 5158 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | 3525 type: String, |
| 5159 observer: '_updateIcon' | 3526 value: 'paper-tab' |
| 5160 } | 3527 }, |
| 5161 | 3528 autoselect: { |
| 5162 }, | 3529 type: Boolean, |
| 5163 | 3530 value: false |
| 5164 _DEFAULT_ICONSET: 'icons', | 3531 }, |
| 5165 | 3532 autoselectDelay: { |
| 5166 _iconChanged: function(icon) { | 3533 type: Number, |
| 5167 var parts = (icon || '').split(':'); | 3534 value: 0 |
| 5168 this._iconName = parts.pop(); | 3535 }, |
| 5169 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; | 3536 _step: { |
| 5170 this._updateIcon(); | 3537 type: Number, |
| 5171 }, | 3538 value: 10 |
| 5172 | 3539 }, |
| 5173 _srcChanged: function(src) { | 3540 _holdDelay: { |
| 5174 this._updateIcon(); | 3541 type: Number, |
| 5175 }, | 3542 value: 1 |
| 5176 | 3543 }, |
| 5177 _usesIconset: function() { | 3544 _leftHidden: { |
| 5178 return this.icon || !this.src; | 3545 type: Boolean, |
| 5179 }, | 3546 value: false |
| 5180 | 3547 }, |
| 5181 /** @suppress {visibility} */ | 3548 _rightHidden: { |
| 5182 _updateIcon: function() { | 3549 type: Boolean, |
| 5183 if (this._usesIconset()) { | 3550 value: false |
| 5184 if (this._img && this._img.parentNode) { | 3551 }, |
| 5185 Polymer.dom(this.root).removeChild(this._img); | 3552 _previousTab: { |
| 5186 } | 3553 type: Object |
| 5187 if (this._iconName === "") { | 3554 } |
| 5188 if (this._iconset) { | 3555 }, |
| 5189 this._iconset.removeIcon(this); | 3556 hostAttributes: { |
| 5190 } | 3557 role: 'tablist' |
| 5191 } else if (this._iconsetName && this._meta) { | 3558 }, |
| 5192 this._iconset = /** @type {?Polymer.Iconset} */ ( | 3559 listeners: { |
| 5193 this._meta.byKey(this._iconsetName)); | 3560 'iron-resize': '_onTabSizingChanged', |
| 5194 if (this._iconset) { | 3561 'iron-items-changed': '_onTabSizingChanged', |
| 5195 this._iconset.applyIcon(this, this._iconName, this.theme); | 3562 'iron-select': '_onIronSelect', |
| 5196 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); | 3563 'iron-deselect': '_onIronDeselect' |
| 5197 } else { | 3564 }, |
| 5198 this.listen(window, 'iron-iconset-added', '_updateIcon'); | 3565 keyBindings: { |
| 5199 } | 3566 'left:keyup right:keyup': '_onArrowKeyup' |
| 5200 } | 3567 }, |
| 5201 } else { | 3568 created: function() { |
| 5202 if (this._iconset) { | 3569 this._holdJob = null; |
| 5203 this._iconset.removeIcon(this); | 3570 this._pendingActivationItem = undefined; |
| 5204 } | 3571 this._pendingActivationTimeout = undefined; |
| 5205 if (!this._img) { | 3572 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind(thi
s); |
| 5206 this._img = document.createElement('img'); | 3573 this.addEventListener('blur', this._onBlurCapture.bind(this), true); |
| 5207 this._img.style.width = '100%'; | 3574 }, |
| 5208 this._img.style.height = '100%'; | 3575 ready: function() { |
| 5209 this._img.draggable = false; | 3576 this.setScrollDirection('y', this.$.tabsContainer); |
| 5210 } | 3577 }, |
| 5211 this._img.src = this.src; | 3578 detached: function() { |
| 5212 Polymer.dom(this.root).appendChild(this._img); | 3579 this._cancelPendingActivation(); |
| 5213 } | 3580 }, |
| 5214 } | 3581 _noinkChanged: function(noink) { |
| 5215 | 3582 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); |
| 3583 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAttribu
te); |
| 3584 }, |
| 3585 _setNoinkAttribute: function(element) { |
| 3586 element.setAttribute('noink', ''); |
| 3587 }, |
| 3588 _removeNoinkAttribute: function(element) { |
| 3589 element.removeAttribute('noink'); |
| 3590 }, |
| 3591 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScrollButt
ons) { |
| 3592 if (!scrollable || hideScrollButtons) { |
| 3593 return 'hidden'; |
| 3594 } |
| 3595 if (hideThisButton) { |
| 3596 return 'not-visible'; |
| 3597 } |
| 3598 return ''; |
| 3599 }, |
| 3600 _computeTabsContentClass: function(scrollable, fitContainer) { |
| 3601 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '') :
' fit-container'; |
| 3602 }, |
| 3603 _computeSelectionBarClass: function(noBar, alignBottom) { |
| 3604 if (noBar) { |
| 3605 return 'hidden'; |
| 3606 } else if (alignBottom) { |
| 3607 return 'align-bottom'; |
| 3608 } |
| 3609 return ''; |
| 3610 }, |
| 3611 _onTabSizingChanged: function() { |
| 3612 this.debounce('_onTabSizingChanged', function() { |
| 3613 this._scroll(); |
| 3614 this._tabChanged(this.selectedItem); |
| 3615 }, 10); |
| 3616 }, |
| 3617 _onIronSelect: function(event) { |
| 3618 this._tabChanged(event.detail.item, this._previousTab); |
| 3619 this._previousTab = event.detail.item; |
| 3620 this.cancelDebouncer('tab-changed'); |
| 3621 }, |
| 3622 _onIronDeselect: function(event) { |
| 3623 this.debounce('tab-changed', function() { |
| 3624 this._tabChanged(null, this._previousTab); |
| 3625 this._previousTab = null; |
| 3626 }, 1); |
| 3627 }, |
| 3628 _activateHandler: function() { |
| 3629 this._cancelPendingActivation(); |
| 3630 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); |
| 3631 }, |
| 3632 _scheduleActivation: function(item, delay) { |
| 3633 this._pendingActivationItem = item; |
| 3634 this._pendingActivationTimeout = this.async(this._bindDelayedActivationHandl
er, delay); |
| 3635 }, |
| 3636 _delayedActivationHandler: function() { |
| 3637 var item = this._pendingActivationItem; |
| 3638 this._pendingActivationItem = undefined; |
| 3639 this._pendingActivationTimeout = undefined; |
| 3640 item.fire(this.activateEvent, null, { |
| 3641 bubbles: true, |
| 3642 cancelable: true |
| 5216 }); | 3643 }); |
| 5217 /** | 3644 }, |
| 5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | 3645 _cancelPendingActivation: function() { |
| 5219 * | 3646 if (this._pendingActivationTimeout !== undefined) { |
| 5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior | 3647 this.cancelAsync(this._pendingActivationTimeout); |
| 5221 */ | 3648 this._pendingActivationItem = undefined; |
| 5222 Polymer.PaperInkyFocusBehaviorImpl = { | 3649 this._pendingActivationTimeout = undefined; |
| 5223 observers: [ | 3650 } |
| 5224 '_focusedChanged(receivedFocusFromKeyboard)' | 3651 }, |
| 5225 ], | 3652 _onArrowKeyup: function(event) { |
| 5226 | 3653 if (this.autoselect) { |
| 5227 _focusedChanged: function(receivedFocusFromKeyboard) { | 3654 this._scheduleActivation(this.focusedItem, this.autoselectDelay); |
| 5228 if (receivedFocusFromKeyboard) { | 3655 } |
| 5229 this.ensureRipple(); | 3656 }, |
| 5230 } | 3657 _onBlurCapture: function(event) { |
| 5231 if (this.hasRipple()) { | 3658 if (event.target === this._pendingActivationItem) { |
| 5232 this._ripple.holdDown = receivedFocusFromKeyboard; | 3659 this._cancelPendingActivation(); |
| 5233 } | 3660 } |
| 5234 }, | 3661 }, |
| 5235 | 3662 get _tabContainerScrollSize() { |
| 5236 _createRipple: function() { | 3663 return Math.max(0, this.$.tabsContainer.scrollWidth - this.$.tabsContainer.o
ffsetWidth); |
| 5237 var ripple = Polymer.PaperRippleBehavior._createRipple(); | 3664 }, |
| 5238 ripple.id = 'ink'; | 3665 _scroll: function(e, detail) { |
| 5239 ripple.setAttribute('center', ''); | 3666 if (!this.scrollable) { |
| 5240 ripple.classList.add('circle'); | 3667 return; |
| 5241 return ripple; | 3668 } |
| 5242 } | 3669 var ddx = detail && -detail.ddx || 0; |
| 5243 }; | 3670 this._affectScroll(ddx); |
| 5244 | 3671 }, |
| 5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | 3672 _down: function(e) { |
| 5246 Polymer.PaperInkyFocusBehavior = [ | 3673 this.async(function() { |
| 5247 Polymer.IronButtonState, | |
| 5248 Polymer.IronControlState, | |
| 5249 Polymer.PaperRippleBehavior, | |
| 5250 Polymer.PaperInkyFocusBehaviorImpl | |
| 5251 ]; | |
| 5252 Polymer({ | |
| 5253 is: 'paper-icon-button', | |
| 5254 | |
| 5255 hostAttributes: { | |
| 5256 role: 'button', | |
| 5257 tabindex: '0' | |
| 5258 }, | |
| 5259 | |
| 5260 behaviors: [ | |
| 5261 Polymer.PaperInkyFocusBehavior | |
| 5262 ], | |
| 5263 | |
| 5264 properties: { | |
| 5265 /** | |
| 5266 * The URL of an image for the icon. If the src property is specified, | |
| 5267 * the icon property should not be. | |
| 5268 */ | |
| 5269 src: { | |
| 5270 type: String | |
| 5271 }, | |
| 5272 | |
| 5273 /** | |
| 5274 * Specifies the icon name or index in the set of icons available in | |
| 5275 * the icon's icon set. If the icon property is specified, | |
| 5276 * the src property should not be. | |
| 5277 */ | |
| 5278 icon: { | |
| 5279 type: String | |
| 5280 }, | |
| 5281 | |
| 5282 /** | |
| 5283 * Specifies the alternate text for the button, for accessibility. | |
| 5284 */ | |
| 5285 alt: { | |
| 5286 type: String, | |
| 5287 observer: "_altChanged" | |
| 5288 } | |
| 5289 }, | |
| 5290 | |
| 5291 _altChanged: function(newValue, oldValue) { | |
| 5292 var label = this.getAttribute('aria-label'); | |
| 5293 | |
| 5294 // Don't stomp over a user-set aria-label. | |
| 5295 if (!label || oldValue == label) { | |
| 5296 this.setAttribute('aria-label', newValue); | |
| 5297 } | |
| 5298 } | |
| 5299 }); | |
| 5300 Polymer({ | |
| 5301 is: 'paper-tab', | |
| 5302 | |
| 5303 behaviors: [ | |
| 5304 Polymer.IronControlState, | |
| 5305 Polymer.IronButtonState, | |
| 5306 Polymer.PaperRippleBehavior | |
| 5307 ], | |
| 5308 | |
| 5309 properties: { | |
| 5310 | |
| 5311 /** | |
| 5312 * If true, the tab will forward keyboard clicks (enter/space) to | |
| 5313 * the first anchor element found in its descendants | |
| 5314 */ | |
| 5315 link: { | |
| 5316 type: Boolean, | |
| 5317 value: false, | |
| 5318 reflectToAttribute: true | |
| 5319 } | |
| 5320 | |
| 5321 }, | |
| 5322 | |
| 5323 hostAttributes: { | |
| 5324 role: 'tab' | |
| 5325 }, | |
| 5326 | |
| 5327 listeners: { | |
| 5328 down: '_updateNoink', | |
| 5329 tap: '_onTap' | |
| 5330 }, | |
| 5331 | |
| 5332 attached: function() { | |
| 5333 this._updateNoink(); | |
| 5334 }, | |
| 5335 | |
| 5336 get _parentNoink () { | |
| 5337 var parent = Polymer.dom(this).parentNode; | |
| 5338 return !!parent && !!parent.noink; | |
| 5339 }, | |
| 5340 | |
| 5341 _updateNoink: function() { | |
| 5342 this.noink = !!this.noink || !!this._parentNoink; | |
| 5343 }, | |
| 5344 | |
| 5345 _onTap: function(event) { | |
| 5346 if (this.link) { | |
| 5347 var anchor = this.queryEffectiveChildren('a'); | |
| 5348 | |
| 5349 if (!anchor) { | |
| 5350 return; | |
| 5351 } | |
| 5352 | |
| 5353 // Don't get stuck in a loop delegating | |
| 5354 // the listener from the child anchor | |
| 5355 if (event.target === anchor) { | |
| 5356 return; | |
| 5357 } | |
| 5358 | |
| 5359 anchor.click(); | |
| 5360 } | |
| 5361 } | |
| 5362 | |
| 5363 }); | |
| 5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
| 5365 Polymer.IronMultiSelectableBehaviorImpl = { | |
| 5366 properties: { | |
| 5367 | |
| 5368 /** | |
| 5369 * If true, multiple selections are allowed. | |
| 5370 */ | |
| 5371 multi: { | |
| 5372 type: Boolean, | |
| 5373 value: false, | |
| 5374 observer: 'multiChanged' | |
| 5375 }, | |
| 5376 | |
| 5377 /** | |
| 5378 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
| 5379 * is true. | |
| 5380 */ | |
| 5381 selectedValues: { | |
| 5382 type: Array, | |
| 5383 notify: true | |
| 5384 }, | |
| 5385 | |
| 5386 /** | |
| 5387 * Returns an array of currently selected items. | |
| 5388 */ | |
| 5389 selectedItems: { | |
| 5390 type: Array, | |
| 5391 readOnly: true, | |
| 5392 notify: true | |
| 5393 }, | |
| 5394 | |
| 5395 }, | |
| 5396 | |
| 5397 observers: [ | |
| 5398 '_updateSelected(selectedValues.splices)' | |
| 5399 ], | |
| 5400 | |
| 5401 /** | |
| 5402 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 5403 * `value` will be toggled; otherwise the `value` will be selected. | |
| 5404 * | |
| 5405 * @method select | |
| 5406 * @param {string|number} value the value to select. | |
| 5407 */ | |
| 5408 select: function(value) { | |
| 5409 if (this.multi) { | |
| 5410 if (this.selectedValues) { | |
| 5411 this._toggleSelected(value); | |
| 5412 } else { | |
| 5413 this.selectedValues = [value]; | |
| 5414 } | |
| 5415 } else { | |
| 5416 this.selected = value; | |
| 5417 } | |
| 5418 }, | |
| 5419 | |
| 5420 multiChanged: function(multi) { | |
| 5421 this._selection.multi = multi; | |
| 5422 }, | |
| 5423 | |
| 5424 get _shouldUpdateSelection() { | |
| 5425 return this.selected != null || | |
| 5426 (this.selectedValues != null && this.selectedValues.length); | |
| 5427 }, | |
| 5428 | |
| 5429 _updateAttrForSelected: function() { | |
| 5430 if (!this.multi) { | |
| 5431 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
| 5432 } else if (this._shouldUpdateSelection) { | |
| 5433 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
| 5434 return this._indexToValue(this.indexOf(selectedItem)); | |
| 5435 }, this).filter(function(unfilteredValue) { | |
| 5436 return unfilteredValue != null; | |
| 5437 }, this); | |
| 5438 } | |
| 5439 }, | |
| 5440 | |
| 5441 _updateSelected: function() { | |
| 5442 if (this.multi) { | |
| 5443 this._selectMulti(this.selectedValues); | |
| 5444 } else { | |
| 5445 this._selectSelected(this.selected); | |
| 5446 } | |
| 5447 }, | |
| 5448 | |
| 5449 _selectMulti: function(values) { | |
| 5450 if (values) { | |
| 5451 var selectedItems = this._valuesToItems(values); | |
| 5452 // clear all but the current selected items | |
| 5453 this._selection.clear(selectedItems); | |
| 5454 // select only those not selected yet | |
| 5455 for (var i = 0; i < selectedItems.length; i++) { | |
| 5456 this._selection.setItemSelected(selectedItems[i], true); | |
| 5457 } | |
| 5458 // Check for items, since this array is populated only when attached | |
| 5459 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
| 5460 var fallback = this._valueToItem(this.fallbackSelection); | |
| 5461 if (fallback) { | |
| 5462 this.selectedValues = [this.fallbackSelection]; | |
| 5463 } | |
| 5464 } | |
| 5465 } else { | |
| 5466 this._selection.clear(); | |
| 5467 } | |
| 5468 }, | |
| 5469 | |
| 5470 _selectionChange: function() { | |
| 5471 var s = this._selection.get(); | |
| 5472 if (this.multi) { | |
| 5473 this._setSelectedItems(s); | |
| 5474 } else { | |
| 5475 this._setSelectedItems([s]); | |
| 5476 this._setSelectedItem(s); | |
| 5477 } | |
| 5478 }, | |
| 5479 | |
| 5480 _toggleSelected: function(value) { | |
| 5481 var i = this.selectedValues.indexOf(value); | |
| 5482 var unselected = i < 0; | |
| 5483 if (unselected) { | |
| 5484 this.push('selectedValues',value); | |
| 5485 } else { | |
| 5486 this.splice('selectedValues',i,1); | |
| 5487 } | |
| 5488 }, | |
| 5489 | |
| 5490 _valuesToItems: function(values) { | |
| 5491 return (values == null) ? null : values.map(function(value) { | |
| 5492 return this._valueToItem(value); | |
| 5493 }, this); | |
| 5494 } | |
| 5495 }; | |
| 5496 | |
| 5497 /** @polymerBehavior */ | |
| 5498 Polymer.IronMultiSelectableBehavior = [ | |
| 5499 Polymer.IronSelectableBehavior, | |
| 5500 Polymer.IronMultiSelectableBehaviorImpl | |
| 5501 ]; | |
| 5502 /** | |
| 5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
| 5504 * | |
| 5505 * @demo demo/index.html | |
| 5506 * @polymerBehavior Polymer.IronMenuBehavior | |
| 5507 */ | |
| 5508 Polymer.IronMenuBehaviorImpl = { | |
| 5509 | |
| 5510 properties: { | |
| 5511 | |
| 5512 /** | |
| 5513 * Returns the currently focused item. | |
| 5514 * @type {?Object} | |
| 5515 */ | |
| 5516 focusedItem: { | |
| 5517 observer: '_focusedItemChanged', | |
| 5518 readOnly: true, | |
| 5519 type: Object | |
| 5520 }, | |
| 5521 | |
| 5522 /** | |
| 5523 * The attribute to use on menu items to look up the item title. Typing th
e first | |
| 5524 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
| 5525 * will be used. | |
| 5526 */ | |
| 5527 attrForItemTitle: { | |
| 5528 type: String | |
| 5529 } | |
| 5530 }, | |
| 5531 | |
| 5532 hostAttributes: { | |
| 5533 'role': 'menu', | |
| 5534 'tabindex': '0' | |
| 5535 }, | |
| 5536 | |
| 5537 observers: [ | |
| 5538 '_updateMultiselectable(multi)' | |
| 5539 ], | |
| 5540 | |
| 5541 listeners: { | |
| 5542 'focus': '_onFocus', | |
| 5543 'keydown': '_onKeydown', | |
| 5544 'iron-items-changed': '_onIronItemsChanged' | |
| 5545 }, | |
| 5546 | |
| 5547 keyBindings: { | |
| 5548 'up': '_onUpKey', | |
| 5549 'down': '_onDownKey', | |
| 5550 'esc': '_onEscKey', | |
| 5551 'shift+tab:keydown': '_onShiftTabDown' | |
| 5552 }, | |
| 5553 | |
| 5554 attached: function() { | |
| 5555 this._resetTabindices(); | |
| 5556 }, | |
| 5557 | |
| 5558 /** | |
| 5559 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 5560 * `value` will be toggled; otherwise the `value` will be selected. | |
| 5561 * | |
| 5562 * @param {string|number} value the value to select. | |
| 5563 */ | |
| 5564 select: function(value) { | |
| 5565 // Cancel automatically focusing a default item if the menu received focus | |
| 5566 // through a user action selecting a particular item. | |
| 5567 if (this._defaultFocusAsync) { | 3674 if (this._defaultFocusAsync) { |
| 5568 this.cancelAsync(this._defaultFocusAsync); | 3675 this.cancelAsync(this._defaultFocusAsync); |
| 5569 this._defaultFocusAsync = null; | 3676 this._defaultFocusAsync = null; |
| 5570 } | 3677 } |
| 5571 var item = this._valueToItem(value); | 3678 }, 1); |
| 5572 if (item && item.hasAttribute('disabled')) return; | 3679 }, |
| 5573 this._setFocusedItem(item); | 3680 _affectScroll: function(dx) { |
| 5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | 3681 this.$.tabsContainer.scrollLeft += dx; |
| 5575 }, | 3682 var scrollLeft = this.$.tabsContainer.scrollLeft; |
| 5576 | 3683 this._leftHidden = scrollLeft === 0; |
| 5577 /** | 3684 this._rightHidden = scrollLeft === this._tabContainerScrollSize; |
| 5578 * Resets all tabindex attributes to the appropriate value based on the | 3685 }, |
| 5579 * current selection state. The appropriate value is `0` (focusable) for | 3686 _onLeftScrollButtonDown: function() { |
| 5580 * the default selected item, and `-1` (not keyboard focusable) for all | 3687 this._scrollToLeft(); |
| 5581 * other items. | 3688 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDelay); |
| 5582 */ | 3689 }, |
| 5583 _resetTabindices: function() { | 3690 _onRightScrollButtonDown: function() { |
| 5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | 3691 this._scrollToRight(); |
| 5585 | 3692 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDelay)
; |
| 5586 this.items.forEach(function(item) { | 3693 }, |
| 5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | 3694 _onScrollButtonUp: function() { |
| 5588 }, this); | 3695 clearInterval(this._holdJob); |
| 5589 }, | 3696 this._holdJob = null; |
| 5590 | 3697 }, |
| 5591 /** | 3698 _scrollToLeft: function() { |
| 5592 * Sets appropriate ARIA based on whether or not the menu is meant to be | 3699 this._affectScroll(-this._step); |
| 5593 * multi-selectable. | 3700 }, |
| 5594 * | 3701 _scrollToRight: function() { |
| 5595 * @param {boolean} multi True if the menu should be multi-selectable. | 3702 this._affectScroll(this._step); |
| 5596 */ | 3703 }, |
| 5597 _updateMultiselectable: function(multi) { | 3704 _tabChanged: function(tab, old) { |
| 5598 if (multi) { | 3705 if (!tab) { |
| 5599 this.setAttribute('aria-multiselectable', 'true'); | 3706 this.$.selectionBar.classList.remove('expand'); |
| 5600 } else { | 3707 this.$.selectionBar.classList.remove('contract'); |
| 5601 this.removeAttribute('aria-multiselectable'); | 3708 this._positionBar(0, 0); |
| 5602 } | 3709 return; |
| 5603 }, | 3710 } |
| 5604 | 3711 var r = this.$.tabsContent.getBoundingClientRect(); |
| 5605 /** | 3712 var w = r.width; |
| 5606 * Given a KeyboardEvent, this method will focus the appropriate item in the | 3713 var tabRect = tab.getBoundingClientRect(); |
| 5607 * menu (if there is a relevant item, and it is possible to focus it). | 3714 var tabOffsetLeft = tabRect.left - r.left; |
| 5608 * | 3715 this._pos = { |
| 5609 * @param {KeyboardEvent} event A KeyboardEvent. | 3716 width: this._calcPercent(tabRect.width, w), |
| 5610 */ | 3717 left: this._calcPercent(tabOffsetLeft, w) |
| 5611 _focusWithKeyboardEvent: function(event) { | 3718 }; |
| 5612 for (var i = 0, item; item = this.items[i]; i++) { | 3719 if (this.noSlide || old == null) { |
| 5613 var attr = this.attrForItemTitle || 'textContent'; | 3720 this.$.selectionBar.classList.remove('expand'); |
| 5614 var title = item[attr] || item.getAttribute(attr); | 3721 this.$.selectionBar.classList.remove('contract'); |
| 5615 | 3722 this._positionBar(this._pos.width, this._pos.left); |
| 5616 if (!item.hasAttribute('disabled') && title && | 3723 return; |
| 5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | 3724 } |
| 5618 this._setFocusedItem(item); | 3725 var oldRect = old.getBoundingClientRect(); |
| 5619 break; | 3726 var oldIndex = this.items.indexOf(old); |
| 5620 } | 3727 var index = this.items.indexOf(tab); |
| 5621 } | 3728 var m = 5; |
| 5622 }, | 3729 this.$.selectionBar.classList.add('expand'); |
| 5623 | 3730 var moveRight = oldIndex < index; |
| 5624 /** | 3731 var isRTL = this._isRTL; |
| 5625 * Focuses the previous item (relative to the currently focused item) in the | 3732 if (isRTL) { |
| 5626 * menu, disabled items will be skipped. | 3733 moveRight = !moveRight; |
| 5627 * Loop until length + 1 to handle case of single item in menu. | 3734 } |
| 5628 */ | 3735 if (moveRight) { |
| 5629 _focusPrevious: function() { | 3736 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - oldRect
.left, w) - m, this._left); |
| 5630 var length = this.items.length; | 3737 } else { |
| 5631 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 3738 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tabRect
.left, w) - m, this._calcPercent(tabOffsetLeft, w) + m); |
| 5632 for (var i = 1; i < length + 1; i++) { | 3739 } |
| 5633 var item = this.items[(curFocusIndex - i + length) % length]; | 3740 if (this.scrollable) { |
| 5634 if (!item.hasAttribute('disabled')) { | 3741 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); |
| 5635 this._setFocusedItem(item); | 3742 } |
| 5636 return; | 3743 }, |
| 5637 } | 3744 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { |
| 5638 } | 3745 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; |
| 5639 }, | 3746 if (l < 0) { |
| 5640 | 3747 this.$.tabsContainer.scrollLeft += l; |
| 5641 /** | 3748 } else { |
| 5642 * Focuses the next item (relative to the currently focused item) in the | 3749 l += tabWidth - this.$.tabsContainer.offsetWidth; |
| 5643 * menu, disabled items will be skipped. | 3750 if (l > 0) { |
| 5644 * Loop until length + 1 to handle case of single item in menu. | 3751 this.$.tabsContainer.scrollLeft += l; |
| 5645 */ | 3752 } |
| 5646 _focusNext: function() { | 3753 } |
| 5647 var length = this.items.length; | 3754 }, |
| 5648 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 3755 _calcPercent: function(w, w0) { |
| 5649 for (var i = 1; i < length + 1; i++) { | 3756 return 100 * w / w0; |
| 5650 var item = this.items[(curFocusIndex + i) % length]; | 3757 }, |
| 5651 if (!item.hasAttribute('disabled')) { | 3758 _positionBar: function(width, left) { |
| 5652 this._setFocusedItem(item); | 3759 width = width || 0; |
| 5653 return; | 3760 left = left || 0; |
| 5654 } | 3761 this._width = width; |
| 5655 } | 3762 this._left = left; |
| 5656 }, | 3763 this.transform('translateX(' + left + '%) scaleX(' + width / 100 + ')', this
.$.selectionBar); |
| 5657 | 3764 }, |
| 5658 /** | 3765 _onBarTransitionEnd: function(e) { |
| 5659 * Mutates items in the menu based on provided selection details, so that | 3766 var cl = this.$.selectionBar.classList; |
| 5660 * all items correctly reflect selection state. | 3767 if (cl.contains('expand')) { |
| 5661 * | 3768 cl.remove('expand'); |
| 5662 * @param {Element} item An item in the menu. | 3769 cl.add('contract'); |
| 5663 * @param {boolean} isSelected True if the item should be shown in a | 3770 this._positionBar(this._pos.width, this._pos.left); |
| 5664 * selected state, otherwise false. | 3771 } else if (cl.contains('contract')) { |
| 5665 */ | 3772 cl.remove('contract'); |
| 5666 _applySelection: function(item, isSelected) { | 3773 } |
| 5667 if (isSelected) { | 3774 } |
| 5668 item.setAttribute('aria-selected', 'true'); | 3775 }); |
| 5669 } else { | 3776 |
| 5670 item.removeAttribute('aria-selected'); | 3777 (function() { |
| 5671 } | 3778 'use strict'; |
| 5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | 3779 Polymer.IronA11yAnnouncer = Polymer({ |
| 5673 }, | 3780 is: 'iron-a11y-announcer', |
| 5674 | |
| 5675 /** | |
| 5676 * Discretely updates tabindex values among menu items as the focused item | |
| 5677 * changes. | |
| 5678 * | |
| 5679 * @param {Element} focusedItem The element that is currently focused. | |
| 5680 * @param {?Element} old The last element that was considered focused, if | |
| 5681 * applicable. | |
| 5682 */ | |
| 5683 _focusedItemChanged: function(focusedItem, old) { | |
| 5684 old && old.setAttribute('tabindex', '-1'); | |
| 5685 if (focusedItem) { | |
| 5686 focusedItem.setAttribute('tabindex', '0'); | |
| 5687 focusedItem.focus(); | |
| 5688 } | |
| 5689 }, | |
| 5690 | |
| 5691 /** | |
| 5692 * A handler that responds to mutation changes related to the list of items | |
| 5693 * in the menu. | |
| 5694 * | |
| 5695 * @param {CustomEvent} event An event containing mutation records as its | |
| 5696 * detail. | |
| 5697 */ | |
| 5698 _onIronItemsChanged: function(event) { | |
| 5699 if (event.detail.addedNodes.length) { | |
| 5700 this._resetTabindices(); | |
| 5701 } | |
| 5702 }, | |
| 5703 | |
| 5704 /** | |
| 5705 * Handler that is called when a shift+tab keypress is detected by the menu. | |
| 5706 * | |
| 5707 * @param {CustomEvent} event A key combination event. | |
| 5708 */ | |
| 5709 _onShiftTabDown: function(event) { | |
| 5710 var oldTabIndex = this.getAttribute('tabindex'); | |
| 5711 | |
| 5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
| 5713 | |
| 5714 this._setFocusedItem(null); | |
| 5715 | |
| 5716 this.setAttribute('tabindex', '-1'); | |
| 5717 | |
| 5718 this.async(function() { | |
| 5719 this.setAttribute('tabindex', oldTabIndex); | |
| 5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 5721 // NOTE(cdata): polymer/polymer#1305 | |
| 5722 }, 1); | |
| 5723 }, | |
| 5724 | |
| 5725 /** | |
| 5726 * Handler that is called when the menu receives focus. | |
| 5727 * | |
| 5728 * @param {FocusEvent} event A focus event. | |
| 5729 */ | |
| 5730 _onFocus: function(event) { | |
| 5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
| 5732 // do not focus the menu itself | |
| 5733 return; | |
| 5734 } | |
| 5735 | |
| 5736 // Do not focus the selected tab if the deepest target is part of the | |
| 5737 // menu element's local DOM and is focusable. | |
| 5738 var rootTarget = /** @type {?HTMLElement} */( | |
| 5739 Polymer.dom(event).rootTarget); | |
| 5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
| 5741 return; | |
| 5742 } | |
| 5743 | |
| 5744 // clear the cached focus item | |
| 5745 this._defaultFocusAsync = this.async(function() { | |
| 5746 // focus the selected item when the menu receives focus, or the first it
em | |
| 5747 // if no item is selected | |
| 5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
| 5749 | |
| 5750 this._setFocusedItem(null); | |
| 5751 | |
| 5752 if (selectedItem) { | |
| 5753 this._setFocusedItem(selectedItem); | |
| 5754 } else if (this.items[0]) { | |
| 5755 // We find the first none-disabled item (if one exists) | |
| 5756 this._focusNext(); | |
| 5757 } | |
| 5758 }); | |
| 5759 }, | |
| 5760 | |
| 5761 /** | |
| 5762 * Handler that is called when the up key is pressed. | |
| 5763 * | |
| 5764 * @param {CustomEvent} event A key combination event. | |
| 5765 */ | |
| 5766 _onUpKey: function(event) { | |
| 5767 // up and down arrows moves the focus | |
| 5768 this._focusPrevious(); | |
| 5769 event.detail.keyboardEvent.preventDefault(); | |
| 5770 }, | |
| 5771 | |
| 5772 /** | |
| 5773 * Handler that is called when the down key is pressed. | |
| 5774 * | |
| 5775 * @param {CustomEvent} event A key combination event. | |
| 5776 */ | |
| 5777 _onDownKey: function(event) { | |
| 5778 this._focusNext(); | |
| 5779 event.detail.keyboardEvent.preventDefault(); | |
| 5780 }, | |
| 5781 | |
| 5782 /** | |
| 5783 * Handler that is called when the esc key is pressed. | |
| 5784 * | |
| 5785 * @param {CustomEvent} event A key combination event. | |
| 5786 */ | |
| 5787 _onEscKey: function(event) { | |
| 5788 // esc blurs the control | |
| 5789 this.focusedItem.blur(); | |
| 5790 }, | |
| 5791 | |
| 5792 /** | |
| 5793 * Handler that is called when a keydown event is detected. | |
| 5794 * | |
| 5795 * @param {KeyboardEvent} event A keyboard event. | |
| 5796 */ | |
| 5797 _onKeydown: function(event) { | |
| 5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
| 5799 // all other keys focus the menu item starting with that character | |
| 5800 this._focusWithKeyboardEvent(event); | |
| 5801 } | |
| 5802 event.stopPropagation(); | |
| 5803 }, | |
| 5804 | |
| 5805 // override _activateHandler | |
| 5806 _activateHandler: function(event) { | |
| 5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
| 5808 event.stopPropagation(); | |
| 5809 } | |
| 5810 }; | |
| 5811 | |
| 5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 5813 | |
| 5814 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
| 5815 Polymer.IronMenuBehavior = [ | |
| 5816 Polymer.IronMultiSelectableBehavior, | |
| 5817 Polymer.IronA11yKeysBehavior, | |
| 5818 Polymer.IronMenuBehaviorImpl | |
| 5819 ]; | |
| 5820 /** | |
| 5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior. | |
| 5822 * | |
| 5823 * @polymerBehavior Polymer.IronMenubarBehavior | |
| 5824 */ | |
| 5825 Polymer.IronMenubarBehaviorImpl = { | |
| 5826 | |
| 5827 hostAttributes: { | |
| 5828 'role': 'menubar' | |
| 5829 }, | |
| 5830 | |
| 5831 keyBindings: { | |
| 5832 'left': '_onLeftKey', | |
| 5833 'right': '_onRightKey' | |
| 5834 }, | |
| 5835 | |
| 5836 _onUpKey: function(event) { | |
| 5837 this.focusedItem.click(); | |
| 5838 event.detail.keyboardEvent.preventDefault(); | |
| 5839 }, | |
| 5840 | |
| 5841 _onDownKey: function(event) { | |
| 5842 this.focusedItem.click(); | |
| 5843 event.detail.keyboardEvent.preventDefault(); | |
| 5844 }, | |
| 5845 | |
| 5846 get _isRTL() { | |
| 5847 return window.getComputedStyle(this)['direction'] === 'rtl'; | |
| 5848 }, | |
| 5849 | |
| 5850 _onLeftKey: function(event) { | |
| 5851 if (this._isRTL) { | |
| 5852 this._focusNext(); | |
| 5853 } else { | |
| 5854 this._focusPrevious(); | |
| 5855 } | |
| 5856 event.detail.keyboardEvent.preventDefault(); | |
| 5857 }, | |
| 5858 | |
| 5859 _onRightKey: function(event) { | |
| 5860 if (this._isRTL) { | |
| 5861 this._focusPrevious(); | |
| 5862 } else { | |
| 5863 this._focusNext(); | |
| 5864 } | |
| 5865 event.detail.keyboardEvent.preventDefault(); | |
| 5866 }, | |
| 5867 | |
| 5868 _onKeydown: function(event) { | |
| 5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { | |
| 5870 return; | |
| 5871 } | |
| 5872 | |
| 5873 // all other keys focus the menu item starting with that character | |
| 5874 this._focusWithKeyboardEvent(event); | |
| 5875 } | |
| 5876 | |
| 5877 }; | |
| 5878 | |
| 5879 /** @polymerBehavior Polymer.IronMenubarBehavior */ | |
| 5880 Polymer.IronMenubarBehavior = [ | |
| 5881 Polymer.IronMenuBehavior, | |
| 5882 Polymer.IronMenubarBehaviorImpl | |
| 5883 ]; | |
| 5884 /** | |
| 5885 * The `iron-iconset-svg` element allows users to define their own icon sets | |
| 5886 * that contain svg icons. The svg icon elements should be children of the | |
| 5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | |
| 5888 * | |
| 5889 * Using svg elements to create icons has a few advantages over traditional | |
| 5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so | |
| 5891 * they are resolution independent and should look good on any device. They | |
| 5892 * are stylable via css. Icons can be themed, colorized, and even animated. | |
| 5893 * | |
| 5894 * Example: | |
| 5895 * | |
| 5896 * <iron-iconset-svg name="my-svg-icons" size="24"> | |
| 5897 * <svg> | |
| 5898 * <defs> | |
| 5899 * <g id="shape"> | |
| 5900 * <rect x="12" y="0" width="12" height="24" /> | |
| 5901 * <circle cx="12" cy="12" r="12" /> | |
| 5902 * </g> | |
| 5903 * </defs> | |
| 5904 * </svg> | |
| 5905 * </iron-iconset-svg> | |
| 5906 * | |
| 5907 * This will automatically register the icon set "my-svg-icons" to the iconset | |
| 5908 * database. To use these icons from within another element, make a | |
| 5909 * `iron-iconset` element and call the `byId` method | |
| 5910 * to retrieve a given iconset. To apply a particular icon inside an | |
| 5911 * element use the `applyIcon` method. For example: | |
| 5912 * | |
| 5913 * iconset.applyIcon(iconNode, 'car'); | |
| 5914 * | |
| 5915 * @element iron-iconset-svg | |
| 5916 * @demo demo/index.html | |
| 5917 * @implements {Polymer.Iconset} | |
| 5918 */ | |
| 5919 Polymer({ | |
| 5920 is: 'iron-iconset-svg', | |
| 5921 | |
| 5922 properties: { | 3781 properties: { |
| 5923 | 3782 mode: { |
| 5924 /** | |
| 5925 * The name of the iconset. | |
| 5926 */ | |
| 5927 name: { | |
| 5928 type: String, | 3783 type: String, |
| 5929 observer: '_nameChanged' | 3784 value: 'polite' |
| 5930 }, | 3785 }, |
| 5931 | 3786 _text: { |
| 5932 /** | |
| 5933 * The size of an individual icon. Note that icons must be square. | |
| 5934 */ | |
| 5935 size: { | |
| 5936 type: Number, | |
| 5937 value: 24 | |
| 5938 } | |
| 5939 | |
| 5940 }, | |
| 5941 | |
| 5942 attached: function() { | |
| 5943 this.style.display = 'none'; | |
| 5944 }, | |
| 5945 | |
| 5946 /** | |
| 5947 * Construct an array of all icon names in this iconset. | |
| 5948 * | |
| 5949 * @return {!Array} Array of icon names. | |
| 5950 */ | |
| 5951 getIconNames: function() { | |
| 5952 this._icons = this._createIconMap(); | |
| 5953 return Object.keys(this._icons).map(function(n) { | |
| 5954 return this.name + ':' + n; | |
| 5955 }, this); | |
| 5956 }, | |
| 5957 | |
| 5958 /** | |
| 5959 * Applies an icon to the given element. | |
| 5960 * | |
| 5961 * An svg icon is prepended to the element's shadowRoot if it exists, | |
| 5962 * otherwise to the element itself. | |
| 5963 * | |
| 5964 * @method applyIcon | |
| 5965 * @param {Element} element Element to which the icon is applied. | |
| 5966 * @param {string} iconName Name of the icon to apply. | |
| 5967 * @return {?Element} The svg element which renders the icon. | |
| 5968 */ | |
| 5969 applyIcon: function(element, iconName) { | |
| 5970 // insert svg element into shadow root, if it exists | |
| 5971 element = element.root || element; | |
| 5972 // Remove old svg element | |
| 5973 this.removeIcon(element); | |
| 5974 // install new svg element | |
| 5975 var svg = this._cloneIcon(iconName); | |
| 5976 if (svg) { | |
| 5977 var pde = Polymer.dom(element); | |
| 5978 pde.insertBefore(svg, pde.childNodes[0]); | |
| 5979 return element._svgIcon = svg; | |
| 5980 } | |
| 5981 return null; | |
| 5982 }, | |
| 5983 | |
| 5984 /** | |
| 5985 * Remove an icon from the given element by undoing the changes effected | |
| 5986 * by `applyIcon`. | |
| 5987 * | |
| 5988 * @param {Element} element The element from which the icon is removed. | |
| 5989 */ | |
| 5990 removeIcon: function(element) { | |
| 5991 // Remove old svg element | |
| 5992 if (element._svgIcon) { | |
| 5993 Polymer.dom(element).removeChild(element._svgIcon); | |
| 5994 element._svgIcon = null; | |
| 5995 } | |
| 5996 }, | |
| 5997 | |
| 5998 /** | |
| 5999 * | |
| 6000 * When name is changed, register iconset metadata | |
| 6001 * | |
| 6002 */ | |
| 6003 _nameChanged: function() { | |
| 6004 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | |
| 6005 this.async(function() { | |
| 6006 this.fire('iron-iconset-added', this, {node: window}); | |
| 6007 }); | |
| 6008 }, | |
| 6009 | |
| 6010 /** | |
| 6011 * Create a map of child SVG elements by id. | |
| 6012 * | |
| 6013 * @return {!Object} Map of id's to SVG elements. | |
| 6014 */ | |
| 6015 _createIconMap: function() { | |
| 6016 // Objects chained to Object.prototype (`{}`) have members. Specifically, | |
| 6017 // on FF there is a `watch` method that confuses the icon map, so we | |
| 6018 // need to use a null-based object here. | |
| 6019 var icons = Object.create(null); | |
| 6020 Polymer.dom(this).querySelectorAll('[id]') | |
| 6021 .forEach(function(icon) { | |
| 6022 icons[icon.id] = icon; | |
| 6023 }); | |
| 6024 return icons; | |
| 6025 }, | |
| 6026 | |
| 6027 /** | |
| 6028 * Produce installable clone of the SVG element matching `id` in this | |
| 6029 * iconset, or `undefined` if there is no matching element. | |
| 6030 * | |
| 6031 * @return {Element} Returns an installable clone of the SVG element | |
| 6032 * matching `id`. | |
| 6033 */ | |
| 6034 _cloneIcon: function(id) { | |
| 6035 // create the icon map on-demand, since the iconset itself has no discrete | |
| 6036 // signal to know when it's children are fully parsed | |
| 6037 this._icons = this._icons || this._createIconMap(); | |
| 6038 return this._prepareSvgClone(this._icons[id], this.size); | |
| 6039 }, | |
| 6040 | |
| 6041 /** | |
| 6042 * @param {Element} sourceSvg | |
| 6043 * @param {number} size | |
| 6044 * @return {Element} | |
| 6045 */ | |
| 6046 _prepareSvgClone: function(sourceSvg, size) { | |
| 6047 if (sourceSvg) { | |
| 6048 var content = sourceSvg.cloneNode(true), | |
| 6049 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | |
| 6050 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | |
| 6051 svg.setAttribute('viewBox', viewBox); | |
| 6052 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | |
| 6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
| 6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
| 6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | |
| 6056 svg.appendChild(content).removeAttribute('id'); | |
| 6057 return svg; | |
| 6058 } | |
| 6059 return null; | |
| 6060 } | |
| 6061 | |
| 6062 }); | |
| 6063 Polymer({ | |
| 6064 is: 'paper-tabs', | |
| 6065 | |
| 6066 behaviors: [ | |
| 6067 Polymer.IronResizableBehavior, | |
| 6068 Polymer.IronMenubarBehavior | |
| 6069 ], | |
| 6070 | |
| 6071 properties: { | |
| 6072 /** | |
| 6073 * If true, ink ripple effect is disabled. When this property is changed
, | |
| 6074 * all descendant `<paper-tab>` elements have their `noink` property | |
| 6075 * changed to the new value as well. | |
| 6076 */ | |
| 6077 noink: { | |
| 6078 type: Boolean, | |
| 6079 value: false, | |
| 6080 observer: '_noinkChanged' | |
| 6081 }, | |
| 6082 | |
| 6083 /** | |
| 6084 * If true, the bottom bar to indicate the selected tab will not be show
n. | |
| 6085 */ | |
| 6086 noBar: { | |
| 6087 type: Boolean, | |
| 6088 value: false | |
| 6089 }, | |
| 6090 | |
| 6091 /** | |
| 6092 * If true, the slide effect for the bottom bar is disabled. | |
| 6093 */ | |
| 6094 noSlide: { | |
| 6095 type: Boolean, | |
| 6096 value: false | |
| 6097 }, | |
| 6098 | |
| 6099 /** | |
| 6100 * If true, tabs are scrollable and the tab width is based on the label
width. | |
| 6101 */ | |
| 6102 scrollable: { | |
| 6103 type: Boolean, | |
| 6104 value: false | |
| 6105 }, | |
| 6106 | |
| 6107 /** | |
| 6108 * If true, tabs expand to fit their container. This currently only appl
ies when | |
| 6109 * scrollable is true. | |
| 6110 */ | |
| 6111 fitContainer: { | |
| 6112 type: Boolean, | |
| 6113 value: false | |
| 6114 }, | |
| 6115 | |
| 6116 /** | |
| 6117 * If true, dragging on the tabs to scroll is disabled. | |
| 6118 */ | |
| 6119 disableDrag: { | |
| 6120 type: Boolean, | |
| 6121 value: false | |
| 6122 }, | |
| 6123 | |
| 6124 /** | |
| 6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla
ble tabs. | |
| 6126 */ | |
| 6127 hideScrollButtons: { | |
| 6128 type: Boolean, | |
| 6129 value: false | |
| 6130 }, | |
| 6131 | |
| 6132 /** | |
| 6133 * If true, the tabs are aligned to bottom (the selection bar appears at
the top). | |
| 6134 */ | |
| 6135 alignBottom: { | |
| 6136 type: Boolean, | |
| 6137 value: false | |
| 6138 }, | |
| 6139 | |
| 6140 selectable: { | |
| 6141 type: String, | |
| 6142 value: 'paper-tab' | |
| 6143 }, | |
| 6144 | |
| 6145 /** | |
| 6146 * If true, tabs are automatically selected when focused using the | |
| 6147 * keyboard. | |
| 6148 */ | |
| 6149 autoselect: { | |
| 6150 type: Boolean, | |
| 6151 value: false | |
| 6152 }, | |
| 6153 | |
| 6154 /** | |
| 6155 * The delay (in milliseconds) between when the user stops interacting | |
| 6156 * with the tabs through the keyboard and when the focused item is | |
| 6157 * automatically selected (if `autoselect` is true). | |
| 6158 */ | |
| 6159 autoselectDelay: { | |
| 6160 type: Number, | |
| 6161 value: 0 | |
| 6162 }, | |
| 6163 | |
| 6164 _step: { | |
| 6165 type: Number, | |
| 6166 value: 10 | |
| 6167 }, | |
| 6168 | |
| 6169 _holdDelay: { | |
| 6170 type: Number, | |
| 6171 value: 1 | |
| 6172 }, | |
| 6173 | |
| 6174 _leftHidden: { | |
| 6175 type: Boolean, | |
| 6176 value: false | |
| 6177 }, | |
| 6178 | |
| 6179 _rightHidden: { | |
| 6180 type: Boolean, | |
| 6181 value: false | |
| 6182 }, | |
| 6183 | |
| 6184 _previousTab: { | |
| 6185 type: Object | |
| 6186 } | |
| 6187 }, | |
| 6188 | |
| 6189 hostAttributes: { | |
| 6190 role: 'tablist' | |
| 6191 }, | |
| 6192 | |
| 6193 listeners: { | |
| 6194 'iron-resize': '_onTabSizingChanged', | |
| 6195 'iron-items-changed': '_onTabSizingChanged', | |
| 6196 'iron-select': '_onIronSelect', | |
| 6197 'iron-deselect': '_onIronDeselect' | |
| 6198 }, | |
| 6199 | |
| 6200 keyBindings: { | |
| 6201 'left:keyup right:keyup': '_onArrowKeyup' | |
| 6202 }, | |
| 6203 | |
| 6204 created: function() { | |
| 6205 this._holdJob = null; | |
| 6206 this._pendingActivationItem = undefined; | |
| 6207 this._pendingActivationTimeout = undefined; | |
| 6208 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind
(this); | |
| 6209 this.addEventListener('blur', this._onBlurCapture.bind(this), true); | |
| 6210 }, | |
| 6211 | |
| 6212 ready: function() { | |
| 6213 this.setScrollDirection('y', this.$.tabsContainer); | |
| 6214 }, | |
| 6215 | |
| 6216 detached: function() { | |
| 6217 this._cancelPendingActivation(); | |
| 6218 }, | |
| 6219 | |
| 6220 _noinkChanged: function(noink) { | |
| 6221 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); | |
| 6222 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt
ribute); | |
| 6223 }, | |
| 6224 | |
| 6225 _setNoinkAttribute: function(element) { | |
| 6226 element.setAttribute('noink', ''); | |
| 6227 }, | |
| 6228 | |
| 6229 _removeNoinkAttribute: function(element) { | |
| 6230 element.removeAttribute('noink'); | |
| 6231 }, | |
| 6232 | |
| 6233 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll
Buttons) { | |
| 6234 if (!scrollable || hideScrollButtons) { | |
| 6235 return 'hidden'; | |
| 6236 } | |
| 6237 | |
| 6238 if (hideThisButton) { | |
| 6239 return 'not-visible'; | |
| 6240 } | |
| 6241 | |
| 6242 return ''; | |
| 6243 }, | |
| 6244 | |
| 6245 _computeTabsContentClass: function(scrollable, fitContainer) { | |
| 6246 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : ''
) : ' fit-container'; | |
| 6247 }, | |
| 6248 | |
| 6249 _computeSelectionBarClass: function(noBar, alignBottom) { | |
| 6250 if (noBar) { | |
| 6251 return 'hidden'; | |
| 6252 } else if (alignBottom) { | |
| 6253 return 'align-bottom'; | |
| 6254 } | |
| 6255 | |
| 6256 return ''; | |
| 6257 }, | |
| 6258 | |
| 6259 // TODO(cdata): Add `track` response back in when gesture lands. | |
| 6260 | |
| 6261 _onTabSizingChanged: function() { | |
| 6262 this.debounce('_onTabSizingChanged', function() { | |
| 6263 this._scroll(); | |
| 6264 this._tabChanged(this.selectedItem); | |
| 6265 }, 10); | |
| 6266 }, | |
| 6267 | |
| 6268 _onIronSelect: function(event) { | |
| 6269 this._tabChanged(event.detail.item, this._previousTab); | |
| 6270 this._previousTab = event.detail.item; | |
| 6271 this.cancelDebouncer('tab-changed'); | |
| 6272 }, | |
| 6273 | |
| 6274 _onIronDeselect: function(event) { | |
| 6275 this.debounce('tab-changed', function() { | |
| 6276 this._tabChanged(null, this._previousTab); | |
| 6277 this._previousTab = null; | |
| 6278 // See polymer/polymer#1305 | |
| 6279 }, 1); | |
| 6280 }, | |
| 6281 | |
| 6282 _activateHandler: function() { | |
| 6283 // Cancel item activations scheduled by keyboard events when any other | |
| 6284 // action causes an item to be activated (e.g. clicks). | |
| 6285 this._cancelPendingActivation(); | |
| 6286 | |
| 6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); | |
| 6288 }, | |
| 6289 | |
| 6290 /** | |
| 6291 * Activates an item after a delay (in milliseconds). | |
| 6292 */ | |
| 6293 _scheduleActivation: function(item, delay) { | |
| 6294 this._pendingActivationItem = item; | |
| 6295 this._pendingActivationTimeout = this.async( | |
| 6296 this._bindDelayedActivationHandler, delay); | |
| 6297 }, | |
| 6298 | |
| 6299 /** | |
| 6300 * Activates the last item given to `_scheduleActivation`. | |
| 6301 */ | |
| 6302 _delayedActivationHandler: function() { | |
| 6303 var item = this._pendingActivationItem; | |
| 6304 this._pendingActivationItem = undefined; | |
| 6305 this._pendingActivationTimeout = undefined; | |
| 6306 item.fire(this.activateEvent, null, { | |
| 6307 bubbles: true, | |
| 6308 cancelable: true | |
| 6309 }); | |
| 6310 }, | |
| 6311 | |
| 6312 /** | |
| 6313 * Cancels a previously scheduled item activation made with | |
| 6314 * `_scheduleActivation`. | |
| 6315 */ | |
| 6316 _cancelPendingActivation: function() { | |
| 6317 if (this._pendingActivationTimeout !== undefined) { | |
| 6318 this.cancelAsync(this._pendingActivationTimeout); | |
| 6319 this._pendingActivationItem = undefined; | |
| 6320 this._pendingActivationTimeout = undefined; | |
| 6321 } | |
| 6322 }, | |
| 6323 | |
| 6324 _onArrowKeyup: function(event) { | |
| 6325 if (this.autoselect) { | |
| 6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay); | |
| 6327 } | |
| 6328 }, | |
| 6329 | |
| 6330 _onBlurCapture: function(event) { | |
| 6331 // Cancel a scheduled item activation (if any) when that item is | |
| 6332 // blurred. | |
| 6333 if (event.target === this._pendingActivationItem) { | |
| 6334 this._cancelPendingActivation(); | |
| 6335 } | |
| 6336 }, | |
| 6337 | |
| 6338 get _tabContainerScrollSize () { | |
| 6339 return Math.max( | |
| 6340 0, | |
| 6341 this.$.tabsContainer.scrollWidth - | |
| 6342 this.$.tabsContainer.offsetWidth | |
| 6343 ); | |
| 6344 }, | |
| 6345 | |
| 6346 _scroll: function(e, detail) { | |
| 6347 if (!this.scrollable) { | |
| 6348 return; | |
| 6349 } | |
| 6350 | |
| 6351 var ddx = (detail && -detail.ddx) || 0; | |
| 6352 this._affectScroll(ddx); | |
| 6353 }, | |
| 6354 | |
| 6355 _down: function(e) { | |
| 6356 // go one beat async to defeat IronMenuBehavior | |
| 6357 // autorefocus-on-no-selection timeout | |
| 6358 this.async(function() { | |
| 6359 if (this._defaultFocusAsync) { | |
| 6360 this.cancelAsync(this._defaultFocusAsync); | |
| 6361 this._defaultFocusAsync = null; | |
| 6362 } | |
| 6363 }, 1); | |
| 6364 }, | |
| 6365 | |
| 6366 _affectScroll: function(dx) { | |
| 6367 this.$.tabsContainer.scrollLeft += dx; | |
| 6368 | |
| 6369 var scrollLeft = this.$.tabsContainer.scrollLeft; | |
| 6370 | |
| 6371 this._leftHidden = scrollLeft === 0; | |
| 6372 this._rightHidden = scrollLeft === this._tabContainerScrollSize; | |
| 6373 }, | |
| 6374 | |
| 6375 _onLeftScrollButtonDown: function() { | |
| 6376 this._scrollToLeft(); | |
| 6377 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel
ay); | |
| 6378 }, | |
| 6379 | |
| 6380 _onRightScrollButtonDown: function() { | |
| 6381 this._scrollToRight(); | |
| 6382 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe
lay); | |
| 6383 }, | |
| 6384 | |
| 6385 _onScrollButtonUp: function() { | |
| 6386 clearInterval(this._holdJob); | |
| 6387 this._holdJob = null; | |
| 6388 }, | |
| 6389 | |
| 6390 _scrollToLeft: function() { | |
| 6391 this._affectScroll(-this._step); | |
| 6392 }, | |
| 6393 | |
| 6394 _scrollToRight: function() { | |
| 6395 this._affectScroll(this._step); | |
| 6396 }, | |
| 6397 | |
| 6398 _tabChanged: function(tab, old) { | |
| 6399 if (!tab) { | |
| 6400 // Remove the bar without animation. | |
| 6401 this.$.selectionBar.classList.remove('expand'); | |
| 6402 this.$.selectionBar.classList.remove('contract'); | |
| 6403 this._positionBar(0, 0); | |
| 6404 return; | |
| 6405 } | |
| 6406 | |
| 6407 var r = this.$.tabsContent.getBoundingClientRect(); | |
| 6408 var w = r.width; | |
| 6409 var tabRect = tab.getBoundingClientRect(); | |
| 6410 var tabOffsetLeft = tabRect.left - r.left; | |
| 6411 | |
| 6412 this._pos = { | |
| 6413 width: this._calcPercent(tabRect.width, w), | |
| 6414 left: this._calcPercent(tabOffsetLeft, w) | |
| 6415 }; | |
| 6416 | |
| 6417 if (this.noSlide || old == null) { | |
| 6418 // Position the bar without animation. | |
| 6419 this.$.selectionBar.classList.remove('expand'); | |
| 6420 this.$.selectionBar.classList.remove('contract'); | |
| 6421 this._positionBar(this._pos.width, this._pos.left); | |
| 6422 return; | |
| 6423 } | |
| 6424 | |
| 6425 var oldRect = old.getBoundingClientRect(); | |
| 6426 var oldIndex = this.items.indexOf(old); | |
| 6427 var index = this.items.indexOf(tab); | |
| 6428 var m = 5; | |
| 6429 | |
| 6430 // bar animation: expand | |
| 6431 this.$.selectionBar.classList.add('expand'); | |
| 6432 | |
| 6433 var moveRight = oldIndex < index; | |
| 6434 var isRTL = this._isRTL; | |
| 6435 if (isRTL) { | |
| 6436 moveRight = !moveRight; | |
| 6437 } | |
| 6438 | |
| 6439 if (moveRight) { | |
| 6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old
Rect.left, w) - m, | |
| 6441 this._left); | |
| 6442 } else { | |
| 6443 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab
Rect.left, w) - m, | |
| 6444 this._calcPercent(tabOffsetLeft, w) + m); | |
| 6445 } | |
| 6446 | |
| 6447 if (this.scrollable) { | |
| 6448 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); | |
| 6449 } | |
| 6450 }, | |
| 6451 | |
| 6452 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { | |
| 6453 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; | |
| 6454 if (l < 0) { | |
| 6455 this.$.tabsContainer.scrollLeft += l; | |
| 6456 } else { | |
| 6457 l += (tabWidth - this.$.tabsContainer.offsetWidth); | |
| 6458 if (l > 0) { | |
| 6459 this.$.tabsContainer.scrollLeft += l; | |
| 6460 } | |
| 6461 } | |
| 6462 }, | |
| 6463 | |
| 6464 _calcPercent: function(w, w0) { | |
| 6465 return 100 * w / w0; | |
| 6466 }, | |
| 6467 | |
| 6468 _positionBar: function(width, left) { | |
| 6469 width = width || 0; | |
| 6470 left = left || 0; | |
| 6471 | |
| 6472 this._width = width; | |
| 6473 this._left = left; | |
| 6474 this.transform( | |
| 6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')', | |
| 6476 this.$.selectionBar); | |
| 6477 }, | |
| 6478 | |
| 6479 _onBarTransitionEnd: function(e) { | |
| 6480 var cl = this.$.selectionBar.classList; | |
| 6481 // bar animation: expand -> contract | |
| 6482 if (cl.contains('expand')) { | |
| 6483 cl.remove('expand'); | |
| 6484 cl.add('contract'); | |
| 6485 this._positionBar(this._pos.width, this._pos.left); | |
| 6486 // bar animation done | |
| 6487 } else if (cl.contains('contract')) { | |
| 6488 cl.remove('contract'); | |
| 6489 } | |
| 6490 } | |
| 6491 }); | |
| 6492 (function() { | |
| 6493 'use strict'; | |
| 6494 | |
| 6495 Polymer.IronA11yAnnouncer = Polymer({ | |
| 6496 is: 'iron-a11y-announcer', | |
| 6497 | |
| 6498 properties: { | |
| 6499 | |
| 6500 /** | |
| 6501 * The value of mode is used to set the `aria-live` attribute | |
| 6502 * for the element that will be announced. Valid values are: `off`, | |
| 6503 * `polite` and `assertive`. | |
| 6504 */ | |
| 6505 mode: { | |
| 6506 type: String, | |
| 6507 value: 'polite' | |
| 6508 }, | |
| 6509 | |
| 6510 _text: { | |
| 6511 type: String, | |
| 6512 value: '' | |
| 6513 } | |
| 6514 }, | |
| 6515 | |
| 6516 created: function() { | |
| 6517 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 6518 Polymer.IronA11yAnnouncer.instance = this; | |
| 6519 } | |
| 6520 | |
| 6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
| 6522 }, | |
| 6523 | |
| 6524 /** | |
| 6525 * Cause a text string to be announced by screen readers. | |
| 6526 * | |
| 6527 * @param {string} text The text that should be announced. | |
| 6528 */ | |
| 6529 announce: function(text) { | |
| 6530 this._text = ''; | |
| 6531 this.async(function() { | |
| 6532 this._text = text; | |
| 6533 }, 100); | |
| 6534 }, | |
| 6535 | |
| 6536 _onIronAnnounce: function(event) { | |
| 6537 if (event.detail && event.detail.text) { | |
| 6538 this.announce(event.detail.text); | |
| 6539 } | |
| 6540 } | |
| 6541 }); | |
| 6542 | |
| 6543 Polymer.IronA11yAnnouncer.instance = null; | |
| 6544 | |
| 6545 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
| 6546 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
| 6548 } | |
| 6549 | |
| 6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
| 6551 }; | |
| 6552 })(); | |
| 6553 /** | |
| 6554 * Singleton IronMeta instance. | |
| 6555 */ | |
| 6556 Polymer.IronValidatableBehaviorMeta = null; | |
| 6557 | |
| 6558 /** | |
| 6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
| 6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
| 6561 * | |
| 6562 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
| 6563 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
| 6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
| 6565 * custom element will get a public `validate()`, which | |
| 6566 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
| 6567 * which can be used for styling. | |
| 6568 * | |
| 6569 * To implement the custom validation logic of your element, you must override | |
| 6570 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
| 6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
| 6572 * for an example. | |
| 6573 * | |
| 6574 * ### Accessibility | |
| 6575 * | |
| 6576 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
| 6577 * `aria-invalid` attribute. | |
| 6578 * | |
| 6579 * @demo demo/index.html | |
| 6580 * @polymerBehavior | |
| 6581 */ | |
| 6582 Polymer.IronValidatableBehavior = { | |
| 6583 | |
| 6584 properties: { | |
| 6585 | |
| 6586 /** | |
| 6587 * Name of the validator to use. | |
| 6588 */ | |
| 6589 validator: { | |
| 6590 type: String | |
| 6591 }, | |
| 6592 | |
| 6593 /** | |
| 6594 * True if the last call to `validate` is invalid. | |
| 6595 */ | |
| 6596 invalid: { | |
| 6597 notify: true, | |
| 6598 reflectToAttribute: true, | |
| 6599 type: Boolean, | |
| 6600 value: false | |
| 6601 }, | |
| 6602 | |
| 6603 /** | |
| 6604 * This property is deprecated and should not be used. Use the global | |
| 6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
| 6606 */ | |
| 6607 _validatorMeta: { | |
| 6608 type: Object | |
| 6609 }, | |
| 6610 | |
| 6611 /** | |
| 6612 * Namespace for this validator. This property is deprecated and should | |
| 6613 * not be used. For all intents and purposes, please consider it a | |
| 6614 * read-only, config-time property. | |
| 6615 */ | |
| 6616 validatorType: { | |
| 6617 type: String, | |
| 6618 value: 'validator' | |
| 6619 }, | |
| 6620 | |
| 6621 _validator: { | |
| 6622 type: Object, | |
| 6623 computed: '__computeValidator(validator)' | |
| 6624 } | |
| 6625 }, | |
| 6626 | |
| 6627 observers: [ | |
| 6628 '_invalidChanged(invalid)' | |
| 6629 ], | |
| 6630 | |
| 6631 registered: function() { | |
| 6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
| 6633 }, | |
| 6634 | |
| 6635 _invalidChanged: function() { | |
| 6636 if (this.invalid) { | |
| 6637 this.setAttribute('aria-invalid', 'true'); | |
| 6638 } else { | |
| 6639 this.removeAttribute('aria-invalid'); | |
| 6640 } | |
| 6641 }, | |
| 6642 | |
| 6643 /** | |
| 6644 * @return {boolean} True if the validator `validator` exists. | |
| 6645 */ | |
| 6646 hasValidator: function() { | |
| 6647 return this._validator != null; | |
| 6648 }, | |
| 6649 | |
| 6650 /** | |
| 6651 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
| 6652 * your element to have custom validation logic, do not override this method
; | |
| 6653 * override `_getValidity(value)` instead. | |
| 6654 | |
| 6655 * @param {Object} value The value to be validated. By default, it is passed | |
| 6656 * to the validator's `validate()` function, if a validator is set. | |
| 6657 * @return {boolean} True if `value` is valid. | |
| 6658 */ | |
| 6659 validate: function(value) { | |
| 6660 this.invalid = !this._getValidity(value); | |
| 6661 return !this.invalid; | |
| 6662 }, | |
| 6663 | |
| 6664 /** | |
| 6665 * Returns true if `value` is valid. By default, it is passed | |
| 6666 * to the validator's `validate()` function, if a validator is set. You | |
| 6667 * should override this method if you want to implement custom validity | |
| 6668 * logic for your element. | |
| 6669 * | |
| 6670 * @param {Object} value The value to be validated. | |
| 6671 * @return {boolean} True if `value` is valid. | |
| 6672 */ | |
| 6673 | |
| 6674 _getValidity: function(value) { | |
| 6675 if (this.hasValidator()) { | |
| 6676 return this._validator.validate(value); | |
| 6677 } | |
| 6678 return true; | |
| 6679 }, | |
| 6680 | |
| 6681 __computeValidator: function() { | |
| 6682 return Polymer.IronValidatableBehaviorMeta && | |
| 6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
| 6684 } | |
| 6685 }; | |
| 6686 /* | |
| 6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
| 6688 to `<input>`. | |
| 6689 | |
| 6690 ### Two-way binding | |
| 6691 | |
| 6692 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
| 6693 | |
| 6694 <input value="{{myValue::input}}"> | |
| 6695 | |
| 6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
| 6697 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
| 6698 | |
| 6699 <input is="iron-input" bind-value="{{myValue}}"> | |
| 6700 | |
| 6701 ### Custom validators | |
| 6702 | |
| 6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
| 6704 | |
| 6705 <input is="iron-input" validator="my-custom-validator"> | |
| 6706 | |
| 6707 ### Stopping invalid input | |
| 6708 | |
| 6709 It may be desirable to only allow users to enter certain characters. You can use
the | |
| 6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
| 6711 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
| 6712 | |
| 6713 \x3c!-- only allow characters that match [0-9] --\x3e | |
| 6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
| 6715 | |
| 6716 @hero hero.svg | |
| 6717 @demo demo/index.html | |
| 6718 */ | |
| 6719 | |
| 6720 Polymer({ | |
| 6721 | |
| 6722 is: 'iron-input', | |
| 6723 | |
| 6724 extends: 'input', | |
| 6725 | |
| 6726 behaviors: [ | |
| 6727 Polymer.IronValidatableBehavior | |
| 6728 ], | |
| 6729 | |
| 6730 properties: { | |
| 6731 | |
| 6732 /** | |
| 6733 * Use this property instead of `value` for two-way data binding. | |
| 6734 */ | |
| 6735 bindValue: { | |
| 6736 observer: '_bindValueChanged', | |
| 6737 type: String | |
| 6738 }, | |
| 6739 | |
| 6740 /** | |
| 6741 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
| 6742 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
| 6743 * Pasted input will have each character checked individually; if any char
acter | |
| 6744 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
| 6745 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
| 6746 */ | |
| 6747 preventInvalidInput: { | |
| 6748 type: Boolean | |
| 6749 }, | |
| 6750 | |
| 6751 /** | |
| 6752 * Regular expression that list the characters allowed as input. | |
| 6753 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
| 6754 * each individual character will be checked against the pattern (rather t
han checking | |
| 6755 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
| 6756 * for example, `[a-zA-Z0-9.+-!;:]` | |
| 6757 */ | |
| 6758 allowedPattern: { | |
| 6759 type: String, | |
| 6760 observer: "_allowedPatternChanged" | |
| 6761 }, | |
| 6762 | |
| 6763 _previousValidInput: { | |
| 6764 type: String, | 3787 type: String, |
| 6765 value: '' | 3788 value: '' |
| 6766 }, | 3789 } |
| 6767 | 3790 }, |
| 6768 _patternAlreadyChecked: { | |
| 6769 type: Boolean, | |
| 6770 value: false | |
| 6771 } | |
| 6772 | |
| 6773 }, | |
| 6774 | |
| 6775 listeners: { | |
| 6776 'input': '_onInput', | |
| 6777 'keypress': '_onKeypress' | |
| 6778 }, | |
| 6779 | |
| 6780 /** @suppress {checkTypes} */ | |
| 6781 registered: function() { | |
| 6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
| 6783 if (!this._canDispatchEventOnDisabled()) { | |
| 6784 this._origDispatchEvent = this.dispatchEvent; | |
| 6785 this.dispatchEvent = this._dispatchEventFirefoxIE; | |
| 6786 } | |
| 6787 }, | |
| 6788 | |
| 6789 created: function() { | 3791 created: function() { |
| 6790 Polymer.IronA11yAnnouncer.requestAvailability(); | 3792 if (!Polymer.IronA11yAnnouncer.instance) { |
| 6791 }, | 3793 Polymer.IronA11yAnnouncer.instance = this; |
| 6792 | 3794 } |
| 6793 _canDispatchEventOnDisabled: function() { | 3795 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(
this)); |
| 6794 var input = document.createElement('input'); | 3796 }, |
| 6795 var canDispatch = false; | 3797 announce: function(text) { |
| 6796 input.disabled = true; | 3798 this._text = ''; |
| 6797 | 3799 this.async(function() { |
| 6798 input.addEventListener('feature-check-dispatch-event', function() { | 3800 this._text = text; |
| 6799 canDispatch = true; | 3801 }, 100); |
| 6800 }); | 3802 }, |
| 6801 | 3803 _onIronAnnounce: function(event) { |
| 6802 try { | 3804 if (event.detail && event.detail.text) { |
| 6803 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 3805 this.announce(event.detail.text); |
| 6804 } catch(e) {} | 3806 } |
| 6805 | 3807 } |
| 6806 return canDispatch; | 3808 }); |
| 6807 }, | 3809 Polymer.IronA11yAnnouncer.instance = null; |
| 6808 | 3810 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
| 6809 _dispatchEventFirefoxIE: function() { | 3811 if (!Polymer.IronA11yAnnouncer.instance) { |
| 6810 // Due to Firefox bug, events fired on disabled form controls can throw | 3812 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann
ouncer'); |
| 6811 // errors; furthermore, neither IE nor Firefox will actually dispatch | 3813 } |
| 6812 // events from disabled form controls; as such, we toggle disable around | 3814 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
| 6813 // the dispatch to allow notifying properties to notify | 3815 }; |
| 6814 // See issue #47 for details | 3816 })(); |
| 6815 var disabled = this.disabled; | 3817 |
| 6816 this.disabled = false; | 3818 Polymer.IronValidatableBehaviorMeta = null; |
| 6817 this._origDispatchEvent.apply(this, arguments); | 3819 |
| 6818 this.disabled = disabled; | 3820 Polymer.IronValidatableBehavior = { |
| 6819 }, | 3821 properties: { |
| 6820 | 3822 validator: { |
| 6821 get _patternRegExp() { | 3823 type: String |
| 6822 var pattern; | 3824 }, |
| 6823 if (this.allowedPattern) { | 3825 invalid: { |
| 6824 pattern = new RegExp(this.allowedPattern); | 3826 notify: true, |
| 3827 reflectToAttribute: true, |
| 3828 type: Boolean, |
| 3829 value: false |
| 3830 }, |
| 3831 _validatorMeta: { |
| 3832 type: Object |
| 3833 }, |
| 3834 validatorType: { |
| 3835 type: String, |
| 3836 value: 'validator' |
| 3837 }, |
| 3838 _validator: { |
| 3839 type: Object, |
| 3840 computed: '__computeValidator(validator)' |
| 3841 } |
| 3842 }, |
| 3843 observers: [ '_invalidChanged(invalid)' ], |
| 3844 registered: function() { |
| 3845 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({ |
| 3846 type: 'validator' |
| 3847 }); |
| 3848 }, |
| 3849 _invalidChanged: function() { |
| 3850 if (this.invalid) { |
| 3851 this.setAttribute('aria-invalid', 'true'); |
| 3852 } else { |
| 3853 this.removeAttribute('aria-invalid'); |
| 3854 } |
| 3855 }, |
| 3856 hasValidator: function() { |
| 3857 return this._validator != null; |
| 3858 }, |
| 3859 validate: function(value) { |
| 3860 this.invalid = !this._getValidity(value); |
| 3861 return !this.invalid; |
| 3862 }, |
| 3863 _getValidity: function(value) { |
| 3864 if (this.hasValidator()) { |
| 3865 return this._validator.validate(value); |
| 3866 } |
| 3867 return true; |
| 3868 }, |
| 3869 __computeValidator: function() { |
| 3870 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio
rMeta.byKey(this.validator); |
| 3871 } |
| 3872 }; |
| 3873 |
| 3874 Polymer({ |
| 3875 is: 'iron-input', |
| 3876 "extends": 'input', |
| 3877 behaviors: [ Polymer.IronValidatableBehavior ], |
| 3878 properties: { |
| 3879 bindValue: { |
| 3880 observer: '_bindValueChanged', |
| 3881 type: String |
| 3882 }, |
| 3883 preventInvalidInput: { |
| 3884 type: Boolean |
| 3885 }, |
| 3886 allowedPattern: { |
| 3887 type: String, |
| 3888 observer: "_allowedPatternChanged" |
| 3889 }, |
| 3890 _previousValidInput: { |
| 3891 type: String, |
| 3892 value: '' |
| 3893 }, |
| 3894 _patternAlreadyChecked: { |
| 3895 type: Boolean, |
| 3896 value: false |
| 3897 } |
| 3898 }, |
| 3899 listeners: { |
| 3900 input: '_onInput', |
| 3901 keypress: '_onKeypress' |
| 3902 }, |
| 3903 registered: function() { |
| 3904 if (!this._canDispatchEventOnDisabled()) { |
| 3905 this._origDispatchEvent = this.dispatchEvent; |
| 3906 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 3907 } |
| 3908 }, |
| 3909 created: function() { |
| 3910 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 3911 }, |
| 3912 _canDispatchEventOnDisabled: function() { |
| 3913 var input = document.createElement('input'); |
| 3914 var canDispatch = false; |
| 3915 input.disabled = true; |
| 3916 input.addEventListener('feature-check-dispatch-event', function() { |
| 3917 canDispatch = true; |
| 3918 }); |
| 3919 try { |
| 3920 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 3921 } catch (e) {} |
| 3922 return canDispatch; |
| 3923 }, |
| 3924 _dispatchEventFirefoxIE: function() { |
| 3925 var disabled = this.disabled; |
| 3926 this.disabled = false; |
| 3927 this._origDispatchEvent.apply(this, arguments); |
| 3928 this.disabled = disabled; |
| 3929 }, |
| 3930 get _patternRegExp() { |
| 3931 var pattern; |
| 3932 if (this.allowedPattern) { |
| 3933 pattern = new RegExp(this.allowedPattern); |
| 3934 } else { |
| 3935 switch (this.type) { |
| 3936 case 'number': |
| 3937 pattern = /[0-9.,e-]/; |
| 3938 break; |
| 3939 } |
| 3940 } |
| 3941 return pattern; |
| 3942 }, |
| 3943 ready: function() { |
| 3944 this.bindValue = this.value; |
| 3945 }, |
| 3946 _bindValueChanged: function() { |
| 3947 if (this.value !== this.bindValue) { |
| 3948 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue ==
= false) ? '' : this.bindValue; |
| 3949 } |
| 3950 this.fire('bind-value-changed', { |
| 3951 value: this.bindValue |
| 3952 }); |
| 3953 }, |
| 3954 _allowedPatternChanged: function() { |
| 3955 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 3956 }, |
| 3957 _onInput: function() { |
| 3958 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 3959 var valid = this._checkPatternValidity(); |
| 3960 if (!valid) { |
| 3961 this._announceInvalidCharacter('Invalid string of characters not entered
.'); |
| 3962 this.value = this._previousValidInput; |
| 3963 } |
| 3964 } |
| 3965 this.bindValue = this.value; |
| 3966 this._previousValidInput = this.value; |
| 3967 this._patternAlreadyChecked = false; |
| 3968 }, |
| 3969 _isPrintable: function(event) { |
| 3970 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC
ode == 13 || event.keyCode == 27; |
| 3971 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; |
| 3972 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 3973 }, |
| 3974 _onKeypress: function(event) { |
| 3975 if (!this.preventInvalidInput && this.type !== 'number') { |
| 3976 return; |
| 3977 } |
| 3978 var regexp = this._patternRegExp; |
| 3979 if (!regexp) { |
| 3980 return; |
| 3981 } |
| 3982 if (event.metaKey || event.ctrlKey || event.altKey) return; |
| 3983 this._patternAlreadyChecked = true; |
| 3984 var thisChar = String.fromCharCode(event.charCode); |
| 3985 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 3986 event.preventDefault(); |
| 3987 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent
ered.'); |
| 3988 } |
| 3989 }, |
| 3990 _checkPatternValidity: function() { |
| 3991 var regexp = this._patternRegExp; |
| 3992 if (!regexp) { |
| 3993 return true; |
| 3994 } |
| 3995 for (var i = 0; i < this.value.length; i++) { |
| 3996 if (!regexp.test(this.value[i])) { |
| 3997 return false; |
| 3998 } |
| 3999 } |
| 4000 return true; |
| 4001 }, |
| 4002 validate: function() { |
| 4003 var valid = this.checkValidity(); |
| 4004 if (valid) { |
| 4005 if (this.required && this.value === '') { |
| 4006 valid = false; |
| 4007 } else if (this.hasValidator()) { |
| 4008 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); |
| 4009 } |
| 4010 } |
| 4011 this.invalid = !valid; |
| 4012 this.fire('iron-input-validate'); |
| 4013 return valid; |
| 4014 }, |
| 4015 _announceInvalidCharacter: function(message) { |
| 4016 this.fire('iron-announce', { |
| 4017 text: message |
| 4018 }); |
| 4019 } |
| 4020 }); |
| 4021 |
| 4022 Polymer({ |
| 4023 is: 'paper-input-container', |
| 4024 properties: { |
| 4025 noLabelFloat: { |
| 4026 type: Boolean, |
| 4027 value: false |
| 4028 }, |
| 4029 alwaysFloatLabel: { |
| 4030 type: Boolean, |
| 4031 value: false |
| 4032 }, |
| 4033 attrForValue: { |
| 4034 type: String, |
| 4035 value: 'bind-value' |
| 4036 }, |
| 4037 autoValidate: { |
| 4038 type: Boolean, |
| 4039 value: false |
| 4040 }, |
| 4041 invalid: { |
| 4042 observer: '_invalidChanged', |
| 4043 type: Boolean, |
| 4044 value: false |
| 4045 }, |
| 4046 focused: { |
| 4047 readOnly: true, |
| 4048 type: Boolean, |
| 4049 value: false, |
| 4050 notify: true |
| 4051 }, |
| 4052 _addons: { |
| 4053 type: Array |
| 4054 }, |
| 4055 _inputHasContent: { |
| 4056 type: Boolean, |
| 4057 value: false |
| 4058 }, |
| 4059 _inputSelector: { |
| 4060 type: String, |
| 4061 value: 'input,textarea,.paper-input-input' |
| 4062 }, |
| 4063 _boundOnFocus: { |
| 4064 type: Function, |
| 4065 value: function() { |
| 4066 return this._onFocus.bind(this); |
| 4067 } |
| 4068 }, |
| 4069 _boundOnBlur: { |
| 4070 type: Function, |
| 4071 value: function() { |
| 4072 return this._onBlur.bind(this); |
| 4073 } |
| 4074 }, |
| 4075 _boundOnInput: { |
| 4076 type: Function, |
| 4077 value: function() { |
| 4078 return this._onInput.bind(this); |
| 4079 } |
| 4080 }, |
| 4081 _boundValueChanged: { |
| 4082 type: Function, |
| 4083 value: function() { |
| 4084 return this._onValueChanged.bind(this); |
| 4085 } |
| 4086 } |
| 4087 }, |
| 4088 listeners: { |
| 4089 'addon-attached': '_onAddonAttached', |
| 4090 'iron-input-validate': '_onIronInputValidate' |
| 4091 }, |
| 4092 get _valueChangedEvent() { |
| 4093 return this.attrForValue + '-changed'; |
| 4094 }, |
| 4095 get _propertyForValue() { |
| 4096 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 4097 }, |
| 4098 get _inputElement() { |
| 4099 return Polymer.dom(this).querySelector(this._inputSelector); |
| 4100 }, |
| 4101 get _inputElementValue() { |
| 4102 return this._inputElement[this._propertyForValue] || this._inputElement.valu
e; |
| 4103 }, |
| 4104 ready: function() { |
| 4105 if (!this._addons) { |
| 4106 this._addons = []; |
| 4107 } |
| 4108 this.addEventListener('focus', this._boundOnFocus, true); |
| 4109 this.addEventListener('blur', this._boundOnBlur, true); |
| 4110 }, |
| 4111 attached: function() { |
| 4112 if (this.attrForValue) { |
| 4113 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa
lueChanged); |
| 4114 } else { |
| 4115 this.addEventListener('input', this._onInput); |
| 4116 } |
| 4117 if (this._inputElementValue != '') { |
| 4118 this._handleValueAndAutoValidate(this._inputElement); |
| 4119 } else { |
| 4120 this._handleValue(this._inputElement); |
| 4121 } |
| 4122 }, |
| 4123 _onAddonAttached: function(event) { |
| 4124 if (!this._addons) { |
| 4125 this._addons = []; |
| 4126 } |
| 4127 var target = event.target; |
| 4128 if (this._addons.indexOf(target) === -1) { |
| 4129 this._addons.push(target); |
| 4130 if (this.isAttached) { |
| 4131 this._handleValue(this._inputElement); |
| 4132 } |
| 4133 } |
| 4134 }, |
| 4135 _onFocus: function() { |
| 4136 this._setFocused(true); |
| 4137 }, |
| 4138 _onBlur: function() { |
| 4139 this._setFocused(false); |
| 4140 this._handleValueAndAutoValidate(this._inputElement); |
| 4141 }, |
| 4142 _onInput: function(event) { |
| 4143 this._handleValueAndAutoValidate(event.target); |
| 4144 }, |
| 4145 _onValueChanged: function(event) { |
| 4146 this._handleValueAndAutoValidate(event.target); |
| 4147 }, |
| 4148 _handleValue: function(inputElement) { |
| 4149 var value = this._inputElementValue; |
| 4150 if (value || value === 0 || inputElement.type === 'number' && !inputElement.
checkValidity()) { |
| 4151 this._inputHasContent = true; |
| 4152 } else { |
| 4153 this._inputHasContent = false; |
| 4154 } |
| 4155 this.updateAddons({ |
| 4156 inputElement: inputElement, |
| 4157 value: value, |
| 4158 invalid: this.invalid |
| 4159 }); |
| 4160 }, |
| 4161 _handleValueAndAutoValidate: function(inputElement) { |
| 4162 if (this.autoValidate) { |
| 4163 var valid; |
| 4164 if (inputElement.validate) { |
| 4165 valid = inputElement.validate(this._inputElementValue); |
| 6825 } else { | 4166 } else { |
| 6826 switch (this.type) { | 4167 valid = inputElement.checkValidity(); |
| 6827 case 'number': | 4168 } |
| 6828 pattern = /[0-9.,e-]/; | |
| 6829 break; | |
| 6830 } | |
| 6831 } | |
| 6832 return pattern; | |
| 6833 }, | |
| 6834 | |
| 6835 ready: function() { | |
| 6836 this.bindValue = this.value; | |
| 6837 }, | |
| 6838 | |
| 6839 /** | |
| 6840 * @suppress {checkTypes} | |
| 6841 */ | |
| 6842 _bindValueChanged: function() { | |
| 6843 if (this.value !== this.bindValue) { | |
| 6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | |
| 6845 } | |
| 6846 // manually notify because we don't want to notify until after setting val
ue | |
| 6847 this.fire('bind-value-changed', {value: this.bindValue}); | |
| 6848 }, | |
| 6849 | |
| 6850 _allowedPatternChanged: function() { | |
| 6851 // Force to prevent invalid input when an `allowed-pattern` is set | |
| 6852 this.preventInvalidInput = this.allowedPattern ? true : false; | |
| 6853 }, | |
| 6854 | |
| 6855 _onInput: function() { | |
| 6856 // Need to validate each of the characters pasted if they haven't | |
| 6857 // been validated inside `_onKeypress` already. | |
| 6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | |
| 6859 var valid = this._checkPatternValidity(); | |
| 6860 if (!valid) { | |
| 6861 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | |
| 6862 this.value = this._previousValidInput; | |
| 6863 } | |
| 6864 } | |
| 6865 | |
| 6866 this.bindValue = this.value; | |
| 6867 this._previousValidInput = this.value; | |
| 6868 this._patternAlreadyChecked = false; | |
| 6869 }, | |
| 6870 | |
| 6871 _isPrintable: function(event) { | |
| 6872 // What a control/printable character is varies wildly based on the browse
r. | |
| 6873 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
| 6874 // in Chrome, but the *do* on Firefox | |
| 6875 // - in Firefox, when they do send a `keypress` event, control chars have | |
| 6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
| 6877 // - printable characters always send a keypress event. | |
| 6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
| 6879 // always matches the charCode. | |
| 6880 // None of this makes any sense. | |
| 6881 | |
| 6882 // For these keys, ASCII code == browser keycode. | |
| 6883 var anyNonPrintable = | |
| 6884 (event.keyCode == 8) || // backspace | |
| 6885 (event.keyCode == 9) || // tab | |
| 6886 (event.keyCode == 13) || // enter | |
| 6887 (event.keyCode == 27); // escape | |
| 6888 | |
| 6889 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
| 6890 var mozNonPrintable = | |
| 6891 (event.keyCode == 19) || // pause | |
| 6892 (event.keyCode == 20) || // caps lock | |
| 6893 (event.keyCode == 45) || // insert | |
| 6894 (event.keyCode == 46) || // delete | |
| 6895 (event.keyCode == 144) || // num lock | |
| 6896 (event.keyCode == 145) || // scroll lock | |
| 6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | |
| 6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys | |
| 6899 | |
| 6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | |
| 6901 }, | |
| 6902 | |
| 6903 _onKeypress: function(event) { | |
| 6904 if (!this.preventInvalidInput && this.type !== 'number') { | |
| 6905 return; | |
| 6906 } | |
| 6907 var regexp = this._patternRegExp; | |
| 6908 if (!regexp) { | |
| 6909 return; | |
| 6910 } | |
| 6911 | |
| 6912 // Handle special keys and backspace | |
| 6913 if (event.metaKey || event.ctrlKey || event.altKey) | |
| 6914 return; | |
| 6915 | |
| 6916 // Check the pattern either here or in `_onInput`, but not in both. | |
| 6917 this._patternAlreadyChecked = true; | |
| 6918 | |
| 6919 var thisChar = String.fromCharCode(event.charCode); | |
| 6920 if (this._isPrintable(event) && !regexp.test(thisChar)) { | |
| 6921 event.preventDefault(); | |
| 6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | |
| 6923 } | |
| 6924 }, | |
| 6925 | |
| 6926 _checkPatternValidity: function() { | |
| 6927 var regexp = this._patternRegExp; | |
| 6928 if (!regexp) { | |
| 6929 return true; | |
| 6930 } | |
| 6931 for (var i = 0; i < this.value.length; i++) { | |
| 6932 if (!regexp.test(this.value[i])) { | |
| 6933 return false; | |
| 6934 } | |
| 6935 } | |
| 6936 return true; | |
| 6937 }, | |
| 6938 | |
| 6939 /** | |
| 6940 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
| 6941 * then any constraints. | |
| 6942 * @return {boolean} True if the value is valid. | |
| 6943 */ | |
| 6944 validate: function() { | |
| 6945 // First, check what the browser thinks. Some inputs (like type=number) | |
| 6946 // behave weirdly and will set the value to "" if something invalid is | |
| 6947 // entered, but will set the validity correctly. | |
| 6948 var valid = this.checkValidity(); | |
| 6949 | |
| 6950 // Only do extra checking if the browser thought this was valid. | |
| 6951 if (valid) { | |
| 6952 // Empty, required input is invalid | |
| 6953 if (this.required && this.value === '') { | |
| 6954 valid = false; | |
| 6955 } else if (this.hasValidator()) { | |
| 6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | |
| 6957 } | |
| 6958 } | |
| 6959 | |
| 6960 this.invalid = !valid; | 4169 this.invalid = !valid; |
| 6961 this.fire('iron-input-validate'); | 4170 } |
| 6962 return valid; | 4171 this._handleValue(inputElement); |
| 6963 }, | 4172 }, |
| 6964 | 4173 _onIronInputValidate: function(event) { |
| 6965 _announceInvalidCharacter: function(message) { | 4174 this.invalid = this._inputElement.invalid; |
| 6966 this.fire('iron-announce', { text: message }); | 4175 }, |
| 6967 } | 4176 _invalidChanged: function() { |
| 6968 }); | 4177 if (this._addons) { |
| 6969 | |
| 6970 /* | |
| 6971 The `iron-input-validate` event is fired whenever `validate()` is called. | |
| 6972 @event iron-input-validate | |
| 6973 */ | |
| 6974 Polymer({ | |
| 6975 is: 'paper-input-container', | |
| 6976 | |
| 6977 properties: { | |
| 6978 /** | |
| 6979 * Set to true to disable the floating label. The label disappears when th
e input value is | |
| 6980 * not null. | |
| 6981 */ | |
| 6982 noLabelFloat: { | |
| 6983 type: Boolean, | |
| 6984 value: false | |
| 6985 }, | |
| 6986 | |
| 6987 /** | |
| 6988 * Set to true to always float the floating label. | |
| 6989 */ | |
| 6990 alwaysFloatLabel: { | |
| 6991 type: Boolean, | |
| 6992 value: false | |
| 6993 }, | |
| 6994 | |
| 6995 /** | |
| 6996 * The attribute to listen for value changes on. | |
| 6997 */ | |
| 6998 attrForValue: { | |
| 6999 type: String, | |
| 7000 value: 'bind-value' | |
| 7001 }, | |
| 7002 | |
| 7003 /** | |
| 7004 * Set to true to auto-validate the input value when it changes. | |
| 7005 */ | |
| 7006 autoValidate: { | |
| 7007 type: Boolean, | |
| 7008 value: false | |
| 7009 }, | |
| 7010 | |
| 7011 /** | |
| 7012 * True if the input is invalid. This property is set automatically when t
he input value | |
| 7013 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
| 7014 */ | |
| 7015 invalid: { | |
| 7016 observer: '_invalidChanged', | |
| 7017 type: Boolean, | |
| 7018 value: false | |
| 7019 }, | |
| 7020 | |
| 7021 /** | |
| 7022 * True if the input has focus. | |
| 7023 */ | |
| 7024 focused: { | |
| 7025 readOnly: true, | |
| 7026 type: Boolean, | |
| 7027 value: false, | |
| 7028 notify: true | |
| 7029 }, | |
| 7030 | |
| 7031 _addons: { | |
| 7032 type: Array | |
| 7033 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
| 7034 // distributed child is attached, which may occur before configuration f
or this element | |
| 7035 // in polyfill. | |
| 7036 }, | |
| 7037 | |
| 7038 _inputHasContent: { | |
| 7039 type: Boolean, | |
| 7040 value: false | |
| 7041 }, | |
| 7042 | |
| 7043 _inputSelector: { | |
| 7044 type: String, | |
| 7045 value: 'input,textarea,.paper-input-input' | |
| 7046 }, | |
| 7047 | |
| 7048 _boundOnFocus: { | |
| 7049 type: Function, | |
| 7050 value: function() { | |
| 7051 return this._onFocus.bind(this); | |
| 7052 } | |
| 7053 }, | |
| 7054 | |
| 7055 _boundOnBlur: { | |
| 7056 type: Function, | |
| 7057 value: function() { | |
| 7058 return this._onBlur.bind(this); | |
| 7059 } | |
| 7060 }, | |
| 7061 | |
| 7062 _boundOnInput: { | |
| 7063 type: Function, | |
| 7064 value: function() { | |
| 7065 return this._onInput.bind(this); | |
| 7066 } | |
| 7067 }, | |
| 7068 | |
| 7069 _boundValueChanged: { | |
| 7070 type: Function, | |
| 7071 value: function() { | |
| 7072 return this._onValueChanged.bind(this); | |
| 7073 } | |
| 7074 } | |
| 7075 }, | |
| 7076 | |
| 7077 listeners: { | |
| 7078 'addon-attached': '_onAddonAttached', | |
| 7079 'iron-input-validate': '_onIronInputValidate' | |
| 7080 }, | |
| 7081 | |
| 7082 get _valueChangedEvent() { | |
| 7083 return this.attrForValue + '-changed'; | |
| 7084 }, | |
| 7085 | |
| 7086 get _propertyForValue() { | |
| 7087 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
| 7088 }, | |
| 7089 | |
| 7090 get _inputElement() { | |
| 7091 return Polymer.dom(this).querySelector(this._inputSelector); | |
| 7092 }, | |
| 7093 | |
| 7094 get _inputElementValue() { | |
| 7095 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
| 7096 }, | |
| 7097 | |
| 7098 ready: function() { | |
| 7099 if (!this._addons) { | |
| 7100 this._addons = []; | |
| 7101 } | |
| 7102 this.addEventListener('focus', this._boundOnFocus, true); | |
| 7103 this.addEventListener('blur', this._boundOnBlur, true); | |
| 7104 }, | |
| 7105 | |
| 7106 attached: function() { | |
| 7107 if (this.attrForValue) { | |
| 7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
| 7109 } else { | |
| 7110 this.addEventListener('input', this._onInput); | |
| 7111 } | |
| 7112 | |
| 7113 // Only validate when attached if the input already has a value. | |
| 7114 if (this._inputElementValue != '') { | |
| 7115 this._handleValueAndAutoValidate(this._inputElement); | |
| 7116 } else { | |
| 7117 this._handleValue(this._inputElement); | |
| 7118 } | |
| 7119 }, | |
| 7120 | |
| 7121 _onAddonAttached: function(event) { | |
| 7122 if (!this._addons) { | |
| 7123 this._addons = []; | |
| 7124 } | |
| 7125 var target = event.target; | |
| 7126 if (this._addons.indexOf(target) === -1) { | |
| 7127 this._addons.push(target); | |
| 7128 if (this.isAttached) { | |
| 7129 this._handleValue(this._inputElement); | |
| 7130 } | |
| 7131 } | |
| 7132 }, | |
| 7133 | |
| 7134 _onFocus: function() { | |
| 7135 this._setFocused(true); | |
| 7136 }, | |
| 7137 | |
| 7138 _onBlur: function() { | |
| 7139 this._setFocused(false); | |
| 7140 this._handleValueAndAutoValidate(this._inputElement); | |
| 7141 }, | |
| 7142 | |
| 7143 _onInput: function(event) { | |
| 7144 this._handleValueAndAutoValidate(event.target); | |
| 7145 }, | |
| 7146 | |
| 7147 _onValueChanged: function(event) { | |
| 7148 this._handleValueAndAutoValidate(event.target); | |
| 7149 }, | |
| 7150 | |
| 7151 _handleValue: function(inputElement) { | |
| 7152 var value = this._inputElementValue; | |
| 7153 | |
| 7154 // type="number" hack needed because this.value is empty until it's valid | |
| 7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
| 7156 this._inputHasContent = true; | |
| 7157 } else { | |
| 7158 this._inputHasContent = false; | |
| 7159 } | |
| 7160 | |
| 7161 this.updateAddons({ | 4178 this.updateAddons({ |
| 7162 inputElement: inputElement, | |
| 7163 value: value, | |
| 7164 invalid: this.invalid | 4179 invalid: this.invalid |
| 7165 }); | 4180 }); |
| 7166 }, | 4181 } |
| 7167 | 4182 }, |
| 7168 _handleValueAndAutoValidate: function(inputElement) { | 4183 updateAddons: function(state) { |
| 7169 if (this.autoValidate) { | 4184 for (var addon, index = 0; addon = this._addons[index]; index++) { |
| 7170 var valid; | 4185 addon.update(state); |
| 7171 if (inputElement.validate) { | 4186 } |
| 7172 valid = inputElement.validate(this._inputElementValue); | 4187 }, |
| 7173 } else { | 4188 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i
nvalid, _inputHasContent) { |
| 7174 valid = inputElement.checkValidity(); | 4189 var cls = 'input-content'; |
| 7175 } | 4190 if (!noLabelFloat) { |
| 7176 this.invalid = !valid; | 4191 var label = this.querySelector('label'); |
| 7177 } | 4192 if (alwaysFloatLabel || _inputHasContent) { |
| 7178 | 4193 cls += ' label-is-floating'; |
| 7179 // Call this last to notify the add-ons. | 4194 this.$.labelAndInputContainer.style.position = 'static'; |
| 7180 this._handleValue(inputElement); | 4195 if (invalid) { |
| 7181 }, | 4196 cls += ' is-invalid'; |
| 7182 | 4197 } else if (focused) { |
| 7183 _onIronInputValidate: function(event) { | 4198 cls += " label-is-highlighted"; |
| 7184 this.invalid = this._inputElement.invalid; | |
| 7185 }, | |
| 7186 | |
| 7187 _invalidChanged: function() { | |
| 7188 if (this._addons) { | |
| 7189 this.updateAddons({invalid: this.invalid}); | |
| 7190 } | |
| 7191 }, | |
| 7192 | |
| 7193 /** | |
| 7194 * Call this to update the state of add-ons. | |
| 7195 * @param {Object} state Add-on state. | |
| 7196 */ | |
| 7197 updateAddons: function(state) { | |
| 7198 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
| 7199 addon.update(state); | |
| 7200 } | |
| 7201 }, | |
| 7202 | |
| 7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
| 7204 var cls = 'input-content'; | |
| 7205 if (!noLabelFloat) { | |
| 7206 var label = this.querySelector('label'); | |
| 7207 | |
| 7208 if (alwaysFloatLabel || _inputHasContent) { | |
| 7209 cls += ' label-is-floating'; | |
| 7210 // If the label is floating, ignore any offsets that may have been | |
| 7211 // applied from a prefix element. | |
| 7212 this.$.labelAndInputContainer.style.position = 'static'; | |
| 7213 | |
| 7214 if (invalid) { | |
| 7215 cls += ' is-invalid'; | |
| 7216 } else if (focused) { | |
| 7217 cls += " label-is-highlighted"; | |
| 7218 } | |
| 7219 } else { | |
| 7220 // When the label is not floating, it should overlap the input element
. | |
| 7221 if (label) { | |
| 7222 this.$.labelAndInputContainer.style.position = 'relative'; | |
| 7223 } | |
| 7224 } | 4199 } |
| 7225 } else { | 4200 } else { |
| 7226 if (_inputHasContent) { | 4201 if (label) { |
| 7227 cls += ' label-is-hidden'; | 4202 this.$.labelAndInputContainer.style.position = 'relative'; |
| 7228 } | 4203 } |
| 7229 } | 4204 } |
| 7230 return cls; | 4205 } else { |
| 7231 }, | 4206 if (_inputHasContent) { |
| 7232 | 4207 cls += ' label-is-hidden'; |
| 7233 _computeUnderlineClass: function(focused, invalid) { | 4208 } |
| 7234 var cls = 'underline'; | 4209 } |
| 7235 if (invalid) { | 4210 return cls; |
| 7236 cls += ' is-invalid'; | 4211 }, |
| 7237 } else if (focused) { | 4212 _computeUnderlineClass: function(focused, invalid) { |
| 7238 cls += ' is-highlighted' | 4213 var cls = 'underline'; |
| 7239 } | 4214 if (invalid) { |
| 7240 return cls; | 4215 cls += ' is-invalid'; |
| 7241 }, | 4216 } else if (focused) { |
| 7242 | 4217 cls += ' is-highlighted'; |
| 7243 _computeAddOnContentClass: function(focused, invalid) { | 4218 } |
| 7244 var cls = 'add-on-content'; | 4219 return cls; |
| 7245 if (invalid) { | 4220 }, |
| 7246 cls += ' is-invalid'; | 4221 _computeAddOnContentClass: function(focused, invalid) { |
| 7247 } else if (focused) { | 4222 var cls = 'add-on-content'; |
| 7248 cls += ' is-highlighted' | 4223 if (invalid) { |
| 7249 } | 4224 cls += ' is-invalid'; |
| 7250 return cls; | 4225 } else if (focused) { |
| 7251 } | 4226 cls += ' is-highlighted'; |
| 7252 }); | 4227 } |
| 7253 /** @polymerBehavior */ | 4228 return cls; |
| 7254 Polymer.PaperSpinnerBehavior = { | 4229 } |
| 7255 | 4230 }); |
| 7256 listeners: { | 4231 |
| 7257 'animationend': '__reset', | 4232 Polymer.PaperSpinnerBehavior = { |
| 7258 'webkitAnimationEnd': '__reset' | 4233 listeners: { |
| 7259 }, | 4234 animationend: '__reset', |
| 7260 | 4235 webkitAnimationEnd: '__reset' |
| 7261 properties: { | 4236 }, |
| 7262 /** | 4237 properties: { |
| 7263 * Displays the spinner. | 4238 active: { |
| 7264 */ | 4239 type: Boolean, |
| 7265 active: { | 4240 value: false, |
| 7266 type: Boolean, | 4241 reflectToAttribute: true, |
| 7267 value: false, | 4242 observer: '__activeChanged' |
| 7268 reflectToAttribute: true, | 4243 }, |
| 7269 observer: '__activeChanged' | 4244 alt: { |
| 7270 }, | 4245 type: String, |
| 7271 | 4246 value: 'loading', |
| 7272 /** | 4247 observer: '__altChanged' |
| 7273 * Alternative text content for accessibility support. | 4248 }, |
| 7274 * If alt is present, it will add an aria-label whose content matches alt
when active. | 4249 __coolingDown: { |
| 7275 * If alt is not present, it will default to 'loading' as the alt value. | 4250 type: Boolean, |
| 7276 */ | 4251 value: false |
| 7277 alt: { | 4252 } |
| 7278 type: String, | 4253 }, |
| 7279 value: 'loading', | 4254 __computeContainerClasses: function(active, coolingDown) { |
| 7280 observer: '__altChanged' | 4255 return [ active || coolingDown ? 'active' : '', coolingDown ? 'cooldown' : '
' ].join(' '); |
| 7281 }, | 4256 }, |
| 7282 | 4257 __activeChanged: function(active, old) { |
| 7283 __coolingDown: { | 4258 this.__setAriaHidden(!active); |
| 7284 type: Boolean, | 4259 this.__coolingDown = !active && old; |
| 7285 value: false | 4260 }, |
| 7286 } | 4261 __altChanged: function(alt) { |
| 7287 }, | 4262 if (alt === this.getPropertyInfo('alt').value) { |
| 7288 | 4263 this.alt = this.getAttribute('aria-label') || alt; |
| 7289 __computeContainerClasses: function(active, coolingDown) { | 4264 } else { |
| 7290 return [ | 4265 this.__setAriaHidden(alt === ''); |
| 7291 active || coolingDown ? 'active' : '', | 4266 this.setAttribute('aria-label', alt); |
| 7292 coolingDown ? 'cooldown' : '' | 4267 } |
| 7293 ].join(' '); | 4268 }, |
| 7294 }, | 4269 __setAriaHidden: function(hidden) { |
| 7295 | 4270 var attr = 'aria-hidden'; |
| 7296 __activeChanged: function(active, old) { | 4271 if (hidden) { |
| 7297 this.__setAriaHidden(!active); | 4272 this.setAttribute(attr, 'true'); |
| 7298 this.__coolingDown = !active && old; | 4273 } else { |
| 7299 }, | 4274 this.removeAttribute(attr); |
| 7300 | 4275 } |
| 7301 __altChanged: function(alt) { | 4276 }, |
| 7302 // user-provided `aria-label` takes precedence over prototype default | 4277 __reset: function() { |
| 7303 if (alt === this.getPropertyInfo('alt').value) { | 4278 this.active = false; |
| 7304 this.alt = this.getAttribute('aria-label') || alt; | 4279 this.__coolingDown = false; |
| 7305 } else { | 4280 } |
| 7306 this.__setAriaHidden(alt===''); | 4281 }; |
| 7307 this.setAttribute('aria-label', alt); | 4282 |
| 7308 } | |
| 7309 }, | |
| 7310 | |
| 7311 __setAriaHidden: function(hidden) { | |
| 7312 var attr = 'aria-hidden'; | |
| 7313 if (hidden) { | |
| 7314 this.setAttribute(attr, 'true'); | |
| 7315 } else { | |
| 7316 this.removeAttribute(attr); | |
| 7317 } | |
| 7318 }, | |
| 7319 | |
| 7320 __reset: function() { | |
| 7321 this.active = false; | |
| 7322 this.__coolingDown = false; | |
| 7323 } | |
| 7324 }; | |
| 7325 Polymer({ | 4283 Polymer({ |
| 7326 is: 'paper-spinner-lite', | 4284 is: 'paper-spinner-lite', |
| 7327 | 4285 behaviors: [ Polymer.PaperSpinnerBehavior ] |
| 7328 behaviors: [ | 4286 }); |
| 7329 Polymer.PaperSpinnerBehavior | 4287 |
| 7330 ] | |
| 7331 }); | |
| 7332 // Copyright 2016 The Chromium Authors. All rights reserved. | 4288 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7333 // Use of this source code is governed by a BSD-style license that can be | 4289 // Use of this source code is governed by a BSD-style license that can be |
| 7334 // found in the LICENSE file. | 4290 // found in the LICENSE file. |
| 7335 | |
| 7336 /** | |
| 7337 * Implements an incremental search field which can be shown and hidden. | |
| 7338 * Canonical implementation is <cr-search-field>. | |
| 7339 * @polymerBehavior | |
| 7340 */ | |
| 7341 var CrSearchFieldBehavior = { | 4291 var CrSearchFieldBehavior = { |
| 7342 properties: { | 4292 properties: { |
| 7343 label: { | 4293 label: { |
| 7344 type: String, | 4294 type: String, |
| 7345 value: '', | 4295 value: '' |
| 7346 }, | 4296 }, |
| 7347 | |
| 7348 clearLabel: { | 4297 clearLabel: { |
| 7349 type: String, | 4298 type: String, |
| 7350 value: '', | 4299 value: '' |
| 7351 }, | 4300 }, |
| 7352 | |
| 7353 showingSearch: { | 4301 showingSearch: { |
| 7354 type: Boolean, | 4302 type: Boolean, |
| 7355 value: false, | 4303 value: false, |
| 7356 notify: true, | 4304 notify: true, |
| 7357 observer: 'showingSearchChanged_', | 4305 observer: 'showingSearchChanged_', |
| 7358 reflectToAttribute: true | 4306 reflectToAttribute: true |
| 7359 }, | 4307 }, |
| 7360 | |
| 7361 /** @private */ | |
| 7362 lastValue_: { | 4308 lastValue_: { |
| 7363 type: String, | 4309 type: String, |
| 7364 value: '', | 4310 value: '' |
| 7365 }, | 4311 } |
| 7366 }, | 4312 }, |
| 7367 | |
| 7368 /** | |
| 7369 * @abstract | |
| 7370 * @return {!HTMLInputElement} The input field element the behavior should | |
| 7371 * use. | |
| 7372 */ | |
| 7373 getSearchInput: function() {}, | 4313 getSearchInput: function() {}, |
| 7374 | |
| 7375 /** | |
| 7376 * @return {string} The value of the search field. | |
| 7377 */ | |
| 7378 getValue: function() { | 4314 getValue: function() { |
| 7379 return this.getSearchInput().value; | 4315 return this.getSearchInput().value; |
| 7380 }, | 4316 }, |
| 7381 | |
| 7382 /** | |
| 7383 * Sets the value of the search field. | |
| 7384 * @param {string} value | |
| 7385 */ | |
| 7386 setValue: function(value) { | 4317 setValue: function(value) { |
| 7387 // Use bindValue when setting the input value so that changes propagate | |
| 7388 // correctly. | |
| 7389 this.getSearchInput().bindValue = value; | 4318 this.getSearchInput().bindValue = value; |
| 7390 this.onValueChanged_(value); | 4319 this.onValueChanged_(value); |
| 7391 }, | 4320 }, |
| 7392 | |
| 7393 showAndFocus: function() { | 4321 showAndFocus: function() { |
| 7394 this.showingSearch = true; | 4322 this.showingSearch = true; |
| 7395 this.focus_(); | 4323 this.focus_(); |
| 7396 }, | 4324 }, |
| 7397 | |
| 7398 /** @private */ | |
| 7399 focus_: function() { | 4325 focus_: function() { |
| 7400 this.getSearchInput().focus(); | 4326 this.getSearchInput().focus(); |
| 7401 }, | 4327 }, |
| 7402 | |
| 7403 onSearchTermSearch: function() { | 4328 onSearchTermSearch: function() { |
| 7404 this.onValueChanged_(this.getValue()); | 4329 this.onValueChanged_(this.getValue()); |
| 7405 }, | 4330 }, |
| 7406 | |
| 7407 /** | |
| 7408 * Updates the internal state of the search field based on a change that has | |
| 7409 * already happened. | |
| 7410 * @param {string} newValue | |
| 7411 * @private | |
| 7412 */ | |
| 7413 onValueChanged_: function(newValue) { | 4331 onValueChanged_: function(newValue) { |
| 7414 if (newValue == this.lastValue_) | 4332 if (newValue == this.lastValue_) return; |
| 7415 return; | |
| 7416 | |
| 7417 this.fire('search-changed', newValue); | 4333 this.fire('search-changed', newValue); |
| 7418 this.lastValue_ = newValue; | 4334 this.lastValue_ = newValue; |
| 7419 }, | 4335 }, |
| 7420 | |
| 7421 onSearchTermKeydown: function(e) { | 4336 onSearchTermKeydown: function(e) { |
| 7422 if (e.key == 'Escape') | 4337 if (e.key == 'Escape') this.showingSearch = false; |
| 7423 this.showingSearch = false; | 4338 }, |
| 7424 }, | |
| 7425 | |
| 7426 /** @private */ | |
| 7427 showingSearchChanged_: function() { | 4339 showingSearchChanged_: function() { |
| 7428 if (this.showingSearch) { | 4340 if (this.showingSearch) { |
| 7429 this.focus_(); | 4341 this.focus_(); |
| 7430 return; | 4342 return; |
| 7431 } | 4343 } |
| 7432 | |
| 7433 this.setValue(''); | 4344 this.setValue(''); |
| 7434 this.getSearchInput().blur(); | 4345 this.getSearchInput().blur(); |
| 7435 } | 4346 } |
| 7436 }; | 4347 }; |
| 4348 |
| 7437 // Copyright 2016 The Chromium Authors. All rights reserved. | 4349 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7438 // Use of this source code is governed by a BSD-style license that can be | 4350 // Use of this source code is governed by a BSD-style license that can be |
| 7439 // found in the LICENSE file. | 4351 // found in the LICENSE file. |
| 7440 | |
| 7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field. | |
| 7442 Polymer({ | 4352 Polymer({ |
| 7443 is: 'cr-toolbar-search-field', | 4353 is: 'cr-toolbar-search-field', |
| 7444 | 4354 behaviors: [ CrSearchFieldBehavior ], |
| 7445 behaviors: [CrSearchFieldBehavior], | |
| 7446 | |
| 7447 properties: { | 4355 properties: { |
| 7448 narrow: { | 4356 narrow: { |
| 7449 type: Boolean, | 4357 type: Boolean, |
| 7450 reflectToAttribute: true, | 4358 reflectToAttribute: true |
| 7451 }, | 4359 }, |
| 7452 | |
| 7453 // Prompt text to display in the search field. | |
| 7454 label: String, | 4360 label: String, |
| 7455 | |
| 7456 // Tooltip to display on the clear search button. | |
| 7457 clearLabel: String, | 4361 clearLabel: String, |
| 7458 | |
| 7459 // When true, show a loading spinner to indicate that the backend is | |
| 7460 // processing the search. Will only show if the search field is open. | |
| 7461 spinnerActive: { | 4362 spinnerActive: { |
| 7462 type: Boolean, | 4363 type: Boolean, |
| 7463 reflectToAttribute: true | 4364 reflectToAttribute: true |
| 7464 }, | 4365 }, |
| 7465 | 4366 hasSearchText_: Boolean |
| 7466 /** @private */ | 4367 }, |
| 7467 hasSearchText_: Boolean, | |
| 7468 }, | |
| 7469 | |
| 7470 listeners: { | 4368 listeners: { |
| 7471 'tap': 'showSearch_', | 4369 tap: 'showSearch_', |
| 7472 'searchInput.bind-value-changed': 'onBindValueChanged_', | 4370 'searchInput.bind-value-changed': 'onBindValueChanged_' |
| 7473 }, | 4371 }, |
| 7474 | |
| 7475 /** @return {!HTMLInputElement} */ | |
| 7476 getSearchInput: function() { | 4372 getSearchInput: function() { |
| 7477 return this.$.searchInput; | 4373 return this.$.searchInput; |
| 7478 }, | 4374 }, |
| 7479 | |
| 7480 /** @return {boolean} */ | |
| 7481 isSearchFocused: function() { | 4375 isSearchFocused: function() { |
| 7482 return this.$.searchTerm.focused; | 4376 return this.$.searchTerm.focused; |
| 7483 }, | 4377 }, |
| 7484 | |
| 7485 /** | |
| 7486 * @param {boolean} narrow | |
| 7487 * @return {number} | |
| 7488 * @private | |
| 7489 */ | |
| 7490 computeIconTabIndex_: function(narrow) { | 4378 computeIconTabIndex_: function(narrow) { |
| 7491 return narrow ? 0 : -1; | 4379 return narrow ? 0 : -1; |
| 7492 }, | 4380 }, |
| 7493 | |
| 7494 /** | |
| 7495 * @param {boolean} spinnerActive | |
| 7496 * @param {boolean} showingSearch | |
| 7497 * @return {boolean} | |
| 7498 * @private | |
| 7499 */ | |
| 7500 isSpinnerShown_: function(spinnerActive, showingSearch) { | 4381 isSpinnerShown_: function(spinnerActive, showingSearch) { |
| 7501 return spinnerActive && showingSearch; | 4382 return spinnerActive && showingSearch; |
| 7502 }, | 4383 }, |
| 7503 | |
| 7504 /** @private */ | |
| 7505 onInputBlur_: function() { | 4384 onInputBlur_: function() { |
| 7506 if (!this.hasSearchText_) | 4385 if (!this.hasSearchText_) this.showingSearch = false; |
| 7507 this.showingSearch = false; | 4386 }, |
| 7508 }, | |
| 7509 | |
| 7510 /** | |
| 7511 * Update the state of the search field whenever the underlying input value | |
| 7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately | |
| 7513 * after any change, whether the result of user input or JS modification. | |
| 7514 * @private | |
| 7515 */ | |
| 7516 onBindValueChanged_: function() { | 4387 onBindValueChanged_: function() { |
| 7517 var newValue = this.$.searchInput.bindValue; | 4388 var newValue = this.$.searchInput.bindValue; |
| 7518 this.hasSearchText_ = newValue != ''; | 4389 this.hasSearchText_ = newValue != ''; |
| 7519 if (newValue != '') | 4390 if (newValue != '') this.showingSearch = true; |
| 7520 this.showingSearch = true; | 4391 }, |
| 7521 }, | |
| 7522 | |
| 7523 /** | |
| 7524 * @param {Event} e | |
| 7525 * @private | |
| 7526 */ | |
| 7527 showSearch_: function(e) { | 4392 showSearch_: function(e) { |
| 7528 if (e.target != this.$.clearSearch) | 4393 if (e.target != this.$.clearSearch) this.showingSearch = true; |
| 7529 this.showingSearch = true; | 4394 }, |
| 7530 }, | |
| 7531 | |
| 7532 /** | |
| 7533 * @param {Event} e | |
| 7534 * @private | |
| 7535 */ | |
| 7536 hideSearch_: function(e) { | 4395 hideSearch_: function(e) { |
| 7537 this.showingSearch = false; | 4396 this.showingSearch = false; |
| 7538 e.stopPropagation(); | 4397 e.stopPropagation(); |
| 7539 } | 4398 } |
| 7540 }); | 4399 }); |
| 4400 |
| 7541 // Copyright 2016 The Chromium Authors. All rights reserved. | 4401 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7542 // Use of this source code is governed by a BSD-style license that can be | 4402 // Use of this source code is governed by a BSD-style license that can be |
| 7543 // found in the LICENSE file. | 4403 // found in the LICENSE file. |
| 7544 | |
| 7545 Polymer({ | 4404 Polymer({ |
| 7546 is: 'cr-toolbar', | 4405 is: 'cr-toolbar', |
| 7547 | |
| 7548 properties: { | 4406 properties: { |
| 7549 // Name to display in the toolbar, in titlecase. | |
| 7550 pageName: String, | 4407 pageName: String, |
| 7551 | |
| 7552 // Prompt text to display in the search field. | |
| 7553 searchPrompt: String, | 4408 searchPrompt: String, |
| 7554 | |
| 7555 // Tooltip to display on the clear search button. | |
| 7556 clearLabel: String, | 4409 clearLabel: String, |
| 7557 | |
| 7558 // Tooltip to display on the menu button. | |
| 7559 menuLabel: String, | 4410 menuLabel: String, |
| 7560 | |
| 7561 // Value is proxied through to cr-toolbar-search-field. When true, | |
| 7562 // the search field will show a processing spinner. | |
| 7563 spinnerActive: Boolean, | 4411 spinnerActive: Boolean, |
| 7564 | |
| 7565 // Controls whether the menu button is shown at the start of the menu. | |
| 7566 showMenu: { | 4412 showMenu: { |
| 7567 type: Boolean, | 4413 type: Boolean, |
| 7568 reflectToAttribute: true, | 4414 reflectToAttribute: true, |
| 7569 value: true | 4415 value: true |
| 7570 }, | 4416 }, |
| 7571 | |
| 7572 /** @private */ | |
| 7573 narrow_: { | 4417 narrow_: { |
| 7574 type: Boolean, | 4418 type: Boolean, |
| 7575 reflectToAttribute: true | 4419 reflectToAttribute: true |
| 7576 }, | 4420 }, |
| 7577 | |
| 7578 /** @private */ | |
| 7579 showingSearch_: { | 4421 showingSearch_: { |
| 7580 type: Boolean, | 4422 type: Boolean, |
| 7581 reflectToAttribute: true, | 4423 reflectToAttribute: true |
| 7582 }, | 4424 } |
| 7583 }, | 4425 }, |
| 7584 | |
| 7585 /** @return {!CrToolbarSearchFieldElement} */ | |
| 7586 getSearchField: function() { | 4426 getSearchField: function() { |
| 7587 return this.$.search; | 4427 return this.$.search; |
| 7588 }, | 4428 }, |
| 7589 | |
| 7590 /** @private */ | |
| 7591 onMenuTap_: function(e) { | 4429 onMenuTap_: function(e) { |
| 7592 this.fire('cr-menu-tap'); | 4430 this.fire('cr-menu-tap'); |
| 7593 } | 4431 } |
| 7594 }); | 4432 }); |
| 4433 |
| 7595 // Copyright 2015 The Chromium Authors. All rights reserved. | 4434 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 7596 // Use of this source code is governed by a BSD-style license that can be | 4435 // Use of this source code is governed by a BSD-style license that can be |
| 7597 // found in the LICENSE file. | 4436 // found in the LICENSE file. |
| 7598 | |
| 7599 Polymer({ | 4437 Polymer({ |
| 7600 is: 'history-toolbar', | 4438 is: 'history-toolbar', |
| 7601 properties: { | 4439 properties: { |
| 7602 // Number of history items currently selected. | |
| 7603 // TODO(calamity): bind this to | |
| 7604 // listContainer.selectedItem.selectedPaths.length. | |
| 7605 count: { | 4440 count: { |
| 7606 type: Number, | 4441 type: Number, |
| 7607 value: 0, | 4442 value: 0, |
| 7608 observer: 'changeToolbarView_' | 4443 observer: 'changeToolbarView_' |
| 7609 }, | 4444 }, |
| 7610 | |
| 7611 // True if 1 or more history items are selected. When this value changes | |
| 7612 // the background colour changes. | |
| 7613 itemsSelected_: { | 4445 itemsSelected_: { |
| 7614 type: Boolean, | 4446 type: Boolean, |
| 7615 value: false, | 4447 value: false, |
| 7616 reflectToAttribute: true | 4448 reflectToAttribute: true |
| 7617 }, | 4449 }, |
| 7618 | |
| 7619 // The most recent term entered in the search field. Updated incrementally | |
| 7620 // as the user types. | |
| 7621 searchTerm: { | 4450 searchTerm: { |
| 7622 type: String, | 4451 type: String, |
| 7623 notify: true, | 4452 notify: true |
| 7624 }, | 4453 }, |
| 7625 | |
| 7626 // True if the backend is processing and a spinner should be shown in the | |
| 7627 // toolbar. | |
| 7628 spinnerActive: { | 4454 spinnerActive: { |
| 7629 type: Boolean, | 4455 type: Boolean, |
| 7630 value: false | 4456 value: false |
| 7631 }, | 4457 }, |
| 7632 | |
| 7633 hasDrawer: { | 4458 hasDrawer: { |
| 7634 type: Boolean, | 4459 type: Boolean, |
| 7635 observer: 'hasDrawerChanged_', | 4460 observer: 'hasDrawerChanged_', |
| 7636 reflectToAttribute: true, | 4461 reflectToAttribute: true |
| 7637 }, | 4462 }, |
| 7638 | |
| 7639 // Whether domain-grouped history is enabled. | |
| 7640 isGroupedMode: { | 4463 isGroupedMode: { |
| 7641 type: Boolean, | 4464 type: Boolean, |
| 7642 reflectToAttribute: true, | 4465 reflectToAttribute: true |
| 7643 }, | 4466 }, |
| 7644 | |
| 7645 // The period to search over. Matches BrowsingHistoryHandler::Range. | |
| 7646 groupedRange: { | 4467 groupedRange: { |
| 7647 type: Number, | 4468 type: Number, |
| 7648 value: 0, | 4469 value: 0, |
| 7649 reflectToAttribute: true, | 4470 reflectToAttribute: true, |
| 7650 notify: true | 4471 notify: true |
| 7651 }, | 4472 }, |
| 7652 | |
| 7653 // The start time of the query range. | |
| 7654 queryStartTime: String, | 4473 queryStartTime: String, |
| 7655 | 4474 queryEndTime: String |
| 7656 // The end time of the query range. | 4475 }, |
| 7657 queryEndTime: String, | |
| 7658 }, | |
| 7659 | |
| 7660 /** | |
| 7661 * Changes the toolbar background color depending on whether any history items | |
| 7662 * are currently selected. | |
| 7663 * @private | |
| 7664 */ | |
| 7665 changeToolbarView_: function() { | 4476 changeToolbarView_: function() { |
| 7666 this.itemsSelected_ = this.count > 0; | 4477 this.itemsSelected_ = this.count > 0; |
| 7667 }, | 4478 }, |
| 7668 | |
| 7669 /** | |
| 7670 * When changing the search term externally, update the search field to | |
| 7671 * reflect the new search term. | |
| 7672 * @param {string} search | |
| 7673 */ | |
| 7674 setSearchTerm: function(search) { | 4479 setSearchTerm: function(search) { |
| 7675 if (this.searchTerm == search) | 4480 if (this.searchTerm == search) return; |
| 7676 return; | |
| 7677 | |
| 7678 this.searchTerm = search; | 4481 this.searchTerm = search; |
| 7679 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) | 4482 var searchField = this.$['main-toolbar'].getSearchField(); |
| 7680 .getSearchField(); | |
| 7681 searchField.showAndFocus(); | 4483 searchField.showAndFocus(); |
| 7682 searchField.setValue(search); | 4484 searchField.setValue(search); |
| 7683 }, | 4485 }, |
| 7684 | |
| 7685 /** | |
| 7686 * @param {!CustomEvent} event | |
| 7687 * @private | |
| 7688 */ | |
| 7689 onSearchChanged_: function(event) { | 4486 onSearchChanged_: function(event) { |
| 7690 this.searchTerm = /** @type {string} */ (event.detail); | 4487 this.searchTerm = event.detail; |
| 7691 }, | 4488 }, |
| 7692 | |
| 7693 onClearSelectionTap_: function() { | 4489 onClearSelectionTap_: function() { |
| 7694 this.fire('unselect-all'); | 4490 this.fire('unselect-all'); |
| 7695 }, | 4491 }, |
| 7696 | |
| 7697 onDeleteTap_: function() { | 4492 onDeleteTap_: function() { |
| 7698 this.fire('delete-selected'); | 4493 this.fire('delete-selected'); |
| 7699 }, | 4494 }, |
| 7700 | |
| 7701 get searchBar() { | 4495 get searchBar() { |
| 7702 return this.$['main-toolbar'].getSearchField(); | 4496 return this.$['main-toolbar'].getSearchField(); |
| 7703 }, | 4497 }, |
| 7704 | |
| 7705 showSearchField: function() { | 4498 showSearchField: function() { |
| 7706 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) | 4499 this.$['main-toolbar'].getSearchField().showAndFocus(); |
| 7707 .getSearchField() | 4500 }, |
| 7708 .showAndFocus(); | |
| 7709 }, | |
| 7710 | |
| 7711 /** | |
| 7712 * If the user is a supervised user the delete button is not shown. | |
| 7713 * @private | |
| 7714 */ | |
| 7715 deletingAllowed_: function() { | 4501 deletingAllowed_: function() { |
| 7716 return loadTimeData.getBoolean('allowDeletingHistory'); | 4502 return loadTimeData.getBoolean('allowDeletingHistory'); |
| 7717 }, | 4503 }, |
| 7718 | |
| 7719 numberOfItemsSelected_: function(count) { | 4504 numberOfItemsSelected_: function(count) { |
| 7720 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; | 4505 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; |
| 7721 }, | 4506 }, |
| 7722 | |
| 7723 getHistoryInterval_: function(queryStartTime, queryEndTime) { | 4507 getHistoryInterval_: function(queryStartTime, queryEndTime) { |
| 7724 // TODO(calamity): Fix the format of these dates. | 4508 return loadTimeData.getStringF('historyInterval', queryStartTime, queryEndTi
me); |
| 7725 return loadTimeData.getStringF( | 4509 }, |
| 7726 'historyInterval', queryStartTime, queryEndTime); | |
| 7727 }, | |
| 7728 | |
| 7729 /** @private */ | |
| 7730 hasDrawerChanged_: function() { | 4510 hasDrawerChanged_: function() { |
| 7731 this.updateStyles(); | 4511 this.updateStyles(); |
| 7732 }, | 4512 } |
| 7733 }); | 4513 }); |
| 4514 |
| 7734 // Copyright 2016 The Chromium Authors. All rights reserved. | 4515 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7735 // Use of this source code is governed by a BSD-style license that can be | 4516 // Use of this source code is governed by a BSD-style license that can be |
| 7736 // found in the LICENSE file. | 4517 // found in the LICENSE file. |
| 7737 | |
| 7738 /** | |
| 7739 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the | |
| 7740 * dialog is closed via close(), a 'close' event is fired. If the dialog is | |
| 7741 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event. | |
| 7742 * Additionally clients can inspect the dialog's |returnValue| property inside | |
| 7743 * the 'close' event listener to determine whether it was canceled or just | |
| 7744 * closed, where a truthy value means success, and a falsy value means it was | |
| 7745 * canceled. | |
| 7746 */ | |
| 7747 Polymer({ | 4518 Polymer({ |
| 7748 is: 'cr-dialog', | 4519 is: 'cr-dialog', |
| 7749 extends: 'dialog', | 4520 "extends": 'dialog', |
| 7750 | |
| 7751 /** @override */ | |
| 7752 created: function() { | 4521 created: function() { |
| 7753 // If the active history entry changes (i.e. user clicks back button), | |
| 7754 // all open dialogs should be cancelled. | |
| 7755 window.addEventListener('popstate', function() { | 4522 window.addEventListener('popstate', function() { |
| 7756 if (this.open) | 4523 if (this.open) this.cancel(); |
| 7757 this.cancel(); | |
| 7758 }.bind(this)); | 4524 }.bind(this)); |
| 7759 }, | 4525 }, |
| 7760 | |
| 7761 cancel: function() { | 4526 cancel: function() { |
| 7762 this.fire('cancel'); | 4527 this.fire('cancel'); |
| 7763 HTMLDialogElement.prototype.close.call(this, ''); | 4528 HTMLDialogElement.prototype.close.call(this, ''); |
| 7764 }, | 4529 }, |
| 7765 | |
| 7766 /** | |
| 7767 * @param {string=} opt_returnValue | |
| 7768 * @override | |
| 7769 */ | |
| 7770 close: function(opt_returnValue) { | 4530 close: function(opt_returnValue) { |
| 7771 HTMLDialogElement.prototype.close.call(this, 'success'); | 4531 HTMLDialogElement.prototype.close.call(this, 'success'); |
| 7772 }, | 4532 }, |
| 7773 | |
| 7774 /** @return {!PaperIconButtonElement} */ | |
| 7775 getCloseButton: function() { | 4533 getCloseButton: function() { |
| 7776 return this.$.close; | 4534 return this.$.close; |
| 7777 }, | 4535 } |
| 7778 }); | 4536 }); |
| 7779 /** | |
| 7780 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
| 7781 optionally centers it in the window or another element. | |
| 7782 | 4537 |
| 7783 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | 4538 Polymer.IronFitBehavior = { |
| 7784 by CSS. | 4539 properties: { |
| 4540 sizingTarget: { |
| 4541 type: Object, |
| 4542 value: function() { |
| 4543 return this; |
| 4544 } |
| 4545 }, |
| 4546 fitInto: { |
| 4547 type: Object, |
| 4548 value: window |
| 4549 }, |
| 4550 noOverlap: { |
| 4551 type: Boolean |
| 4552 }, |
| 4553 positionTarget: { |
| 4554 type: Element |
| 4555 }, |
| 4556 horizontalAlign: { |
| 4557 type: String |
| 4558 }, |
| 4559 verticalAlign: { |
| 4560 type: String |
| 4561 }, |
| 4562 dynamicAlign: { |
| 4563 type: Boolean |
| 4564 }, |
| 4565 horizontalOffset: { |
| 4566 type: Number, |
| 4567 value: 0, |
| 4568 notify: true |
| 4569 }, |
| 4570 verticalOffset: { |
| 4571 type: Number, |
| 4572 value: 0, |
| 4573 notify: true |
| 4574 }, |
| 4575 autoFitOnAttach: { |
| 4576 type: Boolean, |
| 4577 value: false |
| 4578 }, |
| 4579 _fitInfo: { |
| 4580 type: Object |
| 4581 } |
| 4582 }, |
| 4583 get _fitWidth() { |
| 4584 var fitWidth; |
| 4585 if (this.fitInto === window) { |
| 4586 fitWidth = this.fitInto.innerWidth; |
| 4587 } else { |
| 4588 fitWidth = this.fitInto.getBoundingClientRect().width; |
| 4589 } |
| 4590 return fitWidth; |
| 4591 }, |
| 4592 get _fitHeight() { |
| 4593 var fitHeight; |
| 4594 if (this.fitInto === window) { |
| 4595 fitHeight = this.fitInto.innerHeight; |
| 4596 } else { |
| 4597 fitHeight = this.fitInto.getBoundingClientRect().height; |
| 4598 } |
| 4599 return fitHeight; |
| 4600 }, |
| 4601 get _fitLeft() { |
| 4602 var fitLeft; |
| 4603 if (this.fitInto === window) { |
| 4604 fitLeft = 0; |
| 4605 } else { |
| 4606 fitLeft = this.fitInto.getBoundingClientRect().left; |
| 4607 } |
| 4608 return fitLeft; |
| 4609 }, |
| 4610 get _fitTop() { |
| 4611 var fitTop; |
| 4612 if (this.fitInto === window) { |
| 4613 fitTop = 0; |
| 4614 } else { |
| 4615 fitTop = this.fitInto.getBoundingClientRect().top; |
| 4616 } |
| 4617 return fitTop; |
| 4618 }, |
| 4619 get _defaultPositionTarget() { |
| 4620 var parent = Polymer.dom(this).parentNode; |
| 4621 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 4622 parent = parent.host; |
| 4623 } |
| 4624 return parent; |
| 4625 }, |
| 4626 get _localeHorizontalAlign() { |
| 4627 if (this._isRTL) { |
| 4628 if (this.horizontalAlign === 'right') { |
| 4629 return 'left'; |
| 4630 } |
| 4631 if (this.horizontalAlign === 'left') { |
| 4632 return 'right'; |
| 4633 } |
| 4634 } |
| 4635 return this.horizontalAlign; |
| 4636 }, |
| 4637 attached: function() { |
| 4638 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
| 4639 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
| 4640 if (this.autoFitOnAttach) { |
| 4641 if (window.getComputedStyle(this).display === 'none') { |
| 4642 setTimeout(function() { |
| 4643 this.fit(); |
| 4644 }.bind(this)); |
| 4645 } else { |
| 4646 this.fit(); |
| 4647 } |
| 4648 } |
| 4649 }, |
| 4650 fit: function() { |
| 4651 this.position(); |
| 4652 this.constrain(); |
| 4653 this.center(); |
| 4654 }, |
| 4655 _discoverInfo: function() { |
| 4656 if (this._fitInfo) { |
| 4657 return; |
| 4658 } |
| 4659 var target = window.getComputedStyle(this); |
| 4660 var sizer = window.getComputedStyle(this.sizingTarget); |
| 4661 this._fitInfo = { |
| 4662 inlineStyle: { |
| 4663 top: this.style.top || '', |
| 4664 left: this.style.left || '', |
| 4665 position: this.style.position || '' |
| 4666 }, |
| 4667 sizerInlineStyle: { |
| 4668 maxWidth: this.sizingTarget.style.maxWidth || '', |
| 4669 maxHeight: this.sizingTarget.style.maxHeight || '', |
| 4670 boxSizing: this.sizingTarget.style.boxSizing || '' |
| 4671 }, |
| 4672 positionedBy: { |
| 4673 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? '
bottom' : null, |
| 4674 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto'
? 'right' : null |
| 4675 }, |
| 4676 sizedBy: { |
| 4677 height: sizer.maxHeight !== 'none', |
| 4678 width: sizer.maxWidth !== 'none', |
| 4679 minWidth: parseInt(sizer.minWidth, 10) || 0, |
| 4680 minHeight: parseInt(sizer.minHeight, 10) || 0 |
| 4681 }, |
| 4682 margin: { |
| 4683 top: parseInt(target.marginTop, 10) || 0, |
| 4684 right: parseInt(target.marginRight, 10) || 0, |
| 4685 bottom: parseInt(target.marginBottom, 10) || 0, |
| 4686 left: parseInt(target.marginLeft, 10) || 0 |
| 4687 } |
| 4688 }; |
| 4689 if (this.verticalOffset) { |
| 4690 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs
et; |
| 4691 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
| 4692 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
| 4693 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px
'; |
| 4694 } |
| 4695 if (this.horizontalOffset) { |
| 4696 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf
fset; |
| 4697 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
| 4698 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
| 4699 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + '
px'; |
| 4700 } |
| 4701 }, |
| 4702 resetFit: function() { |
| 4703 var info = this._fitInfo || {}; |
| 4704 for (var property in info.sizerInlineStyle) { |
| 4705 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
| 4706 } |
| 4707 for (var property in info.inlineStyle) { |
| 4708 this.style[property] = info.inlineStyle[property]; |
| 4709 } |
| 4710 this._fitInfo = null; |
| 4711 }, |
| 4712 refit: function() { |
| 4713 var scrollLeft = this.sizingTarget.scrollLeft; |
| 4714 var scrollTop = this.sizingTarget.scrollTop; |
| 4715 this.resetFit(); |
| 4716 this.fit(); |
| 4717 this.sizingTarget.scrollLeft = scrollLeft; |
| 4718 this.sizingTarget.scrollTop = scrollTop; |
| 4719 }, |
| 4720 position: function() { |
| 4721 if (!this.horizontalAlign && !this.verticalAlign) { |
| 4722 return; |
| 4723 } |
| 4724 this._discoverInfo(); |
| 4725 this.style.position = 'fixed'; |
| 4726 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4727 this.style.left = '0px'; |
| 4728 this.style.top = '0px'; |
| 4729 var rect = this.getBoundingClientRect(); |
| 4730 var positionRect = this.__getNormalizedRect(this.positionTarget); |
| 4731 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4732 var margin = this._fitInfo.margin; |
| 4733 var size = { |
| 4734 width: rect.width + margin.left + margin.right, |
| 4735 height: rect.height + margin.top + margin.bottom |
| 4736 }; |
| 4737 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical
Align, size, positionRect, fitRect); |
| 4738 var left = position.left + margin.left; |
| 4739 var top = position.top + margin.top; |
| 4740 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
| 4741 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
| 4742 var minWidth = this._fitInfo.sizedBy.minWidth; |
| 4743 var minHeight = this._fitInfo.sizedBy.minHeight; |
| 4744 if (left < margin.left) { |
| 4745 left = margin.left; |
| 4746 if (right - left < minWidth) { |
| 4747 left = right - minWidth; |
| 4748 } |
| 4749 } |
| 4750 if (top < margin.top) { |
| 4751 top = margin.top; |
| 4752 if (bottom - top < minHeight) { |
| 4753 top = bottom - minHeight; |
| 4754 } |
| 4755 } |
| 4756 this.sizingTarget.style.maxWidth = right - left + 'px'; |
| 4757 this.sizingTarget.style.maxHeight = bottom - top + 'px'; |
| 4758 this.style.left = left - rect.left + 'px'; |
| 4759 this.style.top = top - rect.top + 'px'; |
| 4760 }, |
| 4761 constrain: function() { |
| 4762 if (this.horizontalAlign || this.verticalAlign) { |
| 4763 return; |
| 4764 } |
| 4765 this._discoverInfo(); |
| 4766 var info = this._fitInfo; |
| 4767 if (!info.positionedBy.vertically) { |
| 4768 this.style.position = 'fixed'; |
| 4769 this.style.top = '0px'; |
| 4770 } |
| 4771 if (!info.positionedBy.horizontally) { |
| 4772 this.style.position = 'fixed'; |
| 4773 this.style.left = '0px'; |
| 4774 } |
| 4775 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4776 var rect = this.getBoundingClientRect(); |
| 4777 if (!info.sizedBy.height) { |
| 4778 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom',
'Height'); |
| 4779 } |
| 4780 if (!info.sizedBy.width) { |
| 4781 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right'
, 'Width'); |
| 4782 } |
| 4783 }, |
| 4784 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4785 this.__sizeDimension(rect, positionedBy, start, end, extent); |
| 4786 }, |
| 4787 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4788 var info = this._fitInfo; |
| 4789 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4790 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
| 4791 var flip = positionedBy === end; |
| 4792 var offset = flip ? max - rect[end] : rect[start]; |
| 4793 var margin = info.margin[flip ? start : end]; |
| 4794 var offsetExtent = 'offset' + extent; |
| 4795 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
| 4796 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs
et + 'px'; |
| 4797 }, |
| 4798 center: function() { |
| 4799 if (this.horizontalAlign || this.verticalAlign) { |
| 4800 return; |
| 4801 } |
| 4802 this._discoverInfo(); |
| 4803 var positionedBy = this._fitInfo.positionedBy; |
| 4804 if (positionedBy.vertically && positionedBy.horizontally) { |
| 4805 return; |
| 4806 } |
| 4807 this.style.position = 'fixed'; |
| 4808 if (!positionedBy.vertically) { |
| 4809 this.style.top = '0px'; |
| 4810 } |
| 4811 if (!positionedBy.horizontally) { |
| 4812 this.style.left = '0px'; |
| 4813 } |
| 4814 var rect = this.getBoundingClientRect(); |
| 4815 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4816 if (!positionedBy.vertically) { |
| 4817 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
| 4818 this.style.top = top + 'px'; |
| 4819 } |
| 4820 if (!positionedBy.horizontally) { |
| 4821 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
| 4822 this.style.left = left + 'px'; |
| 4823 } |
| 4824 }, |
| 4825 __getNormalizedRect: function(target) { |
| 4826 if (target === document.documentElement || target === window) { |
| 4827 return { |
| 4828 top: 0, |
| 4829 left: 0, |
| 4830 width: window.innerWidth, |
| 4831 height: window.innerHeight, |
| 4832 right: window.innerWidth, |
| 4833 bottom: window.innerHeight |
| 4834 }; |
| 4835 } |
| 4836 return target.getBoundingClientRect(); |
| 4837 }, |
| 4838 __getCroppedArea: function(position, size, fitRect) { |
| 4839 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom -
(position.top + size.height)); |
| 4840 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right
- (position.left + size.width)); |
| 4841 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size
.height; |
| 4842 }, |
| 4843 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
| 4844 var positions = [ { |
| 4845 verticalAlign: 'top', |
| 4846 horizontalAlign: 'left', |
| 4847 top: positionRect.top, |
| 4848 left: positionRect.left |
| 4849 }, { |
| 4850 verticalAlign: 'top', |
| 4851 horizontalAlign: 'right', |
| 4852 top: positionRect.top, |
| 4853 left: positionRect.right - size.width |
| 4854 }, { |
| 4855 verticalAlign: 'bottom', |
| 4856 horizontalAlign: 'left', |
| 4857 top: positionRect.bottom - size.height, |
| 4858 left: positionRect.left |
| 4859 }, { |
| 4860 verticalAlign: 'bottom', |
| 4861 horizontalAlign: 'right', |
| 4862 top: positionRect.bottom - size.height, |
| 4863 left: positionRect.right - size.width |
| 4864 } ]; |
| 4865 if (this.noOverlap) { |
| 4866 for (var i = 0, l = positions.length; i < l; i++) { |
| 4867 var copy = {}; |
| 4868 for (var key in positions[i]) { |
| 4869 copy[key] = positions[i][key]; |
| 4870 } |
| 4871 positions.push(copy); |
| 4872 } |
| 4873 positions[0].top = positions[1].top += positionRect.height; |
| 4874 positions[2].top = positions[3].top -= positionRect.height; |
| 4875 positions[4].left = positions[6].left += positionRect.width; |
| 4876 positions[5].left = positions[7].left -= positionRect.width; |
| 4877 } |
| 4878 vAlign = vAlign === 'auto' ? null : vAlign; |
| 4879 hAlign = hAlign === 'auto' ? null : hAlign; |
| 4880 var position; |
| 4881 for (var i = 0; i < positions.length; i++) { |
| 4882 var pos = positions[i]; |
| 4883 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign
&& pos.horizontalAlign === hAlign) { |
| 4884 position = pos; |
| 4885 break; |
| 4886 } |
| 4887 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos
.horizontalAlign === hAlign); |
| 4888 if (!this.dynamicAlign && !alignOk) { |
| 4889 continue; |
| 4890 } |
| 4891 position = position || pos; |
| 4892 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
| 4893 var diff = pos.croppedArea - position.croppedArea; |
| 4894 if (diff < 0 || diff === 0 && alignOk) { |
| 4895 position = pos; |
| 4896 } |
| 4897 if (position.croppedArea === 0 && alignOk) { |
| 4898 break; |
| 4899 } |
| 4900 } |
| 4901 return position; |
| 4902 } |
| 4903 }; |
| 7785 | 4904 |
| 7786 CSS properties | Action | 4905 (function() { |
| 7787 -----------------------------|------------------------------------------- | 4906 'use strict'; |
| 7788 `position` set | Element is not centered horizontally or verticall
y | 4907 Polymer({ |
| 7789 `top` or `bottom` set | Element is not vertically centered | 4908 is: 'iron-overlay-backdrop', |
| 7790 `left` or `right` set | Element is not horizontally centered | |
| 7791 `max-height` set | Element respects `max-height` | |
| 7792 `max-width` set | Element respects `max-width` | |
| 7793 | |
| 7794 `Polymer.IronFitBehavior` can position an element into another element using | |
| 7795 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
| 7796 | |
| 7797 <div class="container"> | |
| 7798 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
| 7799 Positioned into the container | |
| 7800 </iron-fit-impl> | |
| 7801 </div> | |
| 7802 | |
| 7803 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
| 7804 | |
| 7805 <div class="container"> | |
| 7806 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
| 7807 Positioned around the container | |
| 7808 </iron-fit-impl> | |
| 7809 </div> | |
| 7810 | |
| 7811 @demo demo/index.html | |
| 7812 @polymerBehavior | |
| 7813 */ | |
| 7814 | |
| 7815 Polymer.IronFitBehavior = { | |
| 7816 | |
| 7817 properties: { | 4909 properties: { |
| 7818 | |
| 7819 /** | |
| 7820 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
| 7821 * but it can be set to a child element. This is useful, for example, for
implementing a | |
| 7822 * scrolling region inside the element. | |
| 7823 * @type {!Element} | |
| 7824 */ | |
| 7825 sizingTarget: { | |
| 7826 type: Object, | |
| 7827 value: function() { | |
| 7828 return this; | |
| 7829 } | |
| 7830 }, | |
| 7831 | |
| 7832 /** | |
| 7833 * The element to fit `this` into. | |
| 7834 */ | |
| 7835 fitInto: { | |
| 7836 type: Object, | |
| 7837 value: window | |
| 7838 }, | |
| 7839 | |
| 7840 /** | |
| 7841 * Will position the element around the positionTarget without overlapping
it. | |
| 7842 */ | |
| 7843 noOverlap: { | |
| 7844 type: Boolean | |
| 7845 }, | |
| 7846 | |
| 7847 /** | |
| 7848 * The element that should be used to position the element. If not set, it
will | |
| 7849 * default to the parent node. | |
| 7850 * @type {!Element} | |
| 7851 */ | |
| 7852 positionTarget: { | |
| 7853 type: Element | |
| 7854 }, | |
| 7855 | |
| 7856 /** | |
| 7857 * The orientation against which to align the element horizontally | |
| 7858 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
| 7859 */ | |
| 7860 horizontalAlign: { | |
| 7861 type: String | |
| 7862 }, | |
| 7863 | |
| 7864 /** | |
| 7865 * The orientation against which to align the element vertically | |
| 7866 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
| 7867 */ | |
| 7868 verticalAlign: { | |
| 7869 type: String | |
| 7870 }, | |
| 7871 | |
| 7872 /** | |
| 7873 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
| 7874 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
| 7875 */ | |
| 7876 dynamicAlign: { | |
| 7877 type: Boolean | |
| 7878 }, | |
| 7879 | |
| 7880 /** | |
| 7881 * The same as setting margin-left and margin-right css properties. | |
| 7882 * @deprecated | |
| 7883 */ | |
| 7884 horizontalOffset: { | |
| 7885 type: Number, | |
| 7886 value: 0, | |
| 7887 notify: true | |
| 7888 }, | |
| 7889 | |
| 7890 /** | |
| 7891 * The same as setting margin-top and margin-bottom css properties. | |
| 7892 * @deprecated | |
| 7893 */ | |
| 7894 verticalOffset: { | |
| 7895 type: Number, | |
| 7896 value: 0, | |
| 7897 notify: true | |
| 7898 }, | |
| 7899 | |
| 7900 /** | |
| 7901 * Set to true to auto-fit on attach. | |
| 7902 */ | |
| 7903 autoFitOnAttach: { | |
| 7904 type: Boolean, | |
| 7905 value: false | |
| 7906 }, | |
| 7907 | |
| 7908 /** @type {?Object} */ | |
| 7909 _fitInfo: { | |
| 7910 type: Object | |
| 7911 } | |
| 7912 }, | |
| 7913 | |
| 7914 get _fitWidth() { | |
| 7915 var fitWidth; | |
| 7916 if (this.fitInto === window) { | |
| 7917 fitWidth = this.fitInto.innerWidth; | |
| 7918 } else { | |
| 7919 fitWidth = this.fitInto.getBoundingClientRect().width; | |
| 7920 } | |
| 7921 return fitWidth; | |
| 7922 }, | |
| 7923 | |
| 7924 get _fitHeight() { | |
| 7925 var fitHeight; | |
| 7926 if (this.fitInto === window) { | |
| 7927 fitHeight = this.fitInto.innerHeight; | |
| 7928 } else { | |
| 7929 fitHeight = this.fitInto.getBoundingClientRect().height; | |
| 7930 } | |
| 7931 return fitHeight; | |
| 7932 }, | |
| 7933 | |
| 7934 get _fitLeft() { | |
| 7935 var fitLeft; | |
| 7936 if (this.fitInto === window) { | |
| 7937 fitLeft = 0; | |
| 7938 } else { | |
| 7939 fitLeft = this.fitInto.getBoundingClientRect().left; | |
| 7940 } | |
| 7941 return fitLeft; | |
| 7942 }, | |
| 7943 | |
| 7944 get _fitTop() { | |
| 7945 var fitTop; | |
| 7946 if (this.fitInto === window) { | |
| 7947 fitTop = 0; | |
| 7948 } else { | |
| 7949 fitTop = this.fitInto.getBoundingClientRect().top; | |
| 7950 } | |
| 7951 return fitTop; | |
| 7952 }, | |
| 7953 | |
| 7954 /** | |
| 7955 * The element that should be used to position the element, | |
| 7956 * if no position target is configured. | |
| 7957 */ | |
| 7958 get _defaultPositionTarget() { | |
| 7959 var parent = Polymer.dom(this).parentNode; | |
| 7960 | |
| 7961 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
| 7962 parent = parent.host; | |
| 7963 } | |
| 7964 | |
| 7965 return parent; | |
| 7966 }, | |
| 7967 | |
| 7968 /** | |
| 7969 * The horizontal align value, accounting for the RTL/LTR text direction. | |
| 7970 */ | |
| 7971 get _localeHorizontalAlign() { | |
| 7972 if (this._isRTL) { | |
| 7973 // In RTL, "left" becomes "right". | |
| 7974 if (this.horizontalAlign === 'right') { | |
| 7975 return 'left'; | |
| 7976 } | |
| 7977 if (this.horizontalAlign === 'left') { | |
| 7978 return 'right'; | |
| 7979 } | |
| 7980 } | |
| 7981 return this.horizontalAlign; | |
| 7982 }, | |
| 7983 | |
| 7984 attached: function() { | |
| 7985 // Memoize this to avoid expensive calculations & relayouts. | |
| 7986 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | |
| 7987 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | |
| 7988 if (this.autoFitOnAttach) { | |
| 7989 if (window.getComputedStyle(this).display === 'none') { | |
| 7990 setTimeout(function() { | |
| 7991 this.fit(); | |
| 7992 }.bind(this)); | |
| 7993 } else { | |
| 7994 this.fit(); | |
| 7995 } | |
| 7996 } | |
| 7997 }, | |
| 7998 | |
| 7999 /** | |
| 8000 * Positions and fits the element into the `fitInto` element. | |
| 8001 */ | |
| 8002 fit: function() { | |
| 8003 this.position(); | |
| 8004 this.constrain(); | |
| 8005 this.center(); | |
| 8006 }, | |
| 8007 | |
| 8008 /** | |
| 8009 * Memoize information needed to position and size the target element. | |
| 8010 * @suppress {deprecated} | |
| 8011 */ | |
| 8012 _discoverInfo: function() { | |
| 8013 if (this._fitInfo) { | |
| 8014 return; | |
| 8015 } | |
| 8016 var target = window.getComputedStyle(this); | |
| 8017 var sizer = window.getComputedStyle(this.sizingTarget); | |
| 8018 | |
| 8019 this._fitInfo = { | |
| 8020 inlineStyle: { | |
| 8021 top: this.style.top || '', | |
| 8022 left: this.style.left || '', | |
| 8023 position: this.style.position || '' | |
| 8024 }, | |
| 8025 sizerInlineStyle: { | |
| 8026 maxWidth: this.sizingTarget.style.maxWidth || '', | |
| 8027 maxHeight: this.sizingTarget.style.maxHeight || '', | |
| 8028 boxSizing: this.sizingTarget.style.boxSizing || '' | |
| 8029 }, | |
| 8030 positionedBy: { | |
| 8031 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto'
? | |
| 8032 'bottom' : null), | |
| 8033 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut
o' ? | |
| 8034 'right' : null) | |
| 8035 }, | |
| 8036 sizedBy: { | |
| 8037 height: sizer.maxHeight !== 'none', | |
| 8038 width: sizer.maxWidth !== 'none', | |
| 8039 minWidth: parseInt(sizer.minWidth, 10) || 0, | |
| 8040 minHeight: parseInt(sizer.minHeight, 10) || 0 | |
| 8041 }, | |
| 8042 margin: { | |
| 8043 top: parseInt(target.marginTop, 10) || 0, | |
| 8044 right: parseInt(target.marginRight, 10) || 0, | |
| 8045 bottom: parseInt(target.marginBottom, 10) || 0, | |
| 8046 left: parseInt(target.marginLeft, 10) || 0 | |
| 8047 } | |
| 8048 }; | |
| 8049 | |
| 8050 // Support these properties until they are removed. | |
| 8051 if (this.verticalOffset) { | |
| 8052 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | |
| 8053 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | |
| 8054 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | |
| 8055 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | |
| 8056 } | |
| 8057 if (this.horizontalOffset) { | |
| 8058 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | |
| 8059 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | |
| 8060 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | |
| 8061 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | |
| 8062 } | |
| 8063 }, | |
| 8064 | |
| 8065 /** | |
| 8066 * Resets the target element's position and size constraints, and clear | |
| 8067 * the memoized data. | |
| 8068 */ | |
| 8069 resetFit: function() { | |
| 8070 var info = this._fitInfo || {}; | |
| 8071 for (var property in info.sizerInlineStyle) { | |
| 8072 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | |
| 8073 } | |
| 8074 for (var property in info.inlineStyle) { | |
| 8075 this.style[property] = info.inlineStyle[property]; | |
| 8076 } | |
| 8077 | |
| 8078 this._fitInfo = null; | |
| 8079 }, | |
| 8080 | |
| 8081 /** | |
| 8082 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
| 8083 * the element or the `fitInto` element has been resized, or if any of the | |
| 8084 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
| 8085 * It preserves the scroll position of the sizingTarget. | |
| 8086 */ | |
| 8087 refit: function() { | |
| 8088 var scrollLeft = this.sizingTarget.scrollLeft; | |
| 8089 var scrollTop = this.sizingTarget.scrollTop; | |
| 8090 this.resetFit(); | |
| 8091 this.fit(); | |
| 8092 this.sizingTarget.scrollLeft = scrollLeft; | |
| 8093 this.sizingTarget.scrollTop = scrollTop; | |
| 8094 }, | |
| 8095 | |
| 8096 /** | |
| 8097 * Positions the element according to `horizontalAlign, verticalAlign`. | |
| 8098 */ | |
| 8099 position: function() { | |
| 8100 if (!this.horizontalAlign && !this.verticalAlign) { | |
| 8101 // needs to be centered, and it is done after constrain. | |
| 8102 return; | |
| 8103 } | |
| 8104 this._discoverInfo(); | |
| 8105 | |
| 8106 this.style.position = 'fixed'; | |
| 8107 // Need border-box for margin/padding. | |
| 8108 this.sizingTarget.style.boxSizing = 'border-box'; | |
| 8109 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
| 8110 this.style.left = '0px'; | |
| 8111 this.style.top = '0px'; | |
| 8112 | |
| 8113 var rect = this.getBoundingClientRect(); | |
| 8114 var positionRect = this.__getNormalizedRect(this.positionTarget); | |
| 8115 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 8116 | |
| 8117 var margin = this._fitInfo.margin; | |
| 8118 | |
| 8119 // Consider the margin as part of the size for position calculations. | |
| 8120 var size = { | |
| 8121 width: rect.width + margin.left + margin.right, | |
| 8122 height: rect.height + margin.top + margin.bottom | |
| 8123 }; | |
| 8124 | |
| 8125 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | |
| 8126 | |
| 8127 var left = position.left + margin.left; | |
| 8128 var top = position.top + margin.top; | |
| 8129 | |
| 8130 // Use original size (without margin). | |
| 8131 var right = Math.min(fitRect.right - margin.right, left + rect.width); | |
| 8132 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | |
| 8133 | |
| 8134 var minWidth = this._fitInfo.sizedBy.minWidth; | |
| 8135 var minHeight = this._fitInfo.sizedBy.minHeight; | |
| 8136 if (left < margin.left) { | |
| 8137 left = margin.left; | |
| 8138 if (right - left < minWidth) { | |
| 8139 left = right - minWidth; | |
| 8140 } | |
| 8141 } | |
| 8142 if (top < margin.top) { | |
| 8143 top = margin.top; | |
| 8144 if (bottom - top < minHeight) { | |
| 8145 top = bottom - minHeight; | |
| 8146 } | |
| 8147 } | |
| 8148 | |
| 8149 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | |
| 8150 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | |
| 8151 | |
| 8152 // Remove the offset caused by any stacking context. | |
| 8153 this.style.left = (left - rect.left) + 'px'; | |
| 8154 this.style.top = (top - rect.top) + 'px'; | |
| 8155 }, | |
| 8156 | |
| 8157 /** | |
| 8158 * Constrains the size of the element to `fitInto` by setting `max-height` | |
| 8159 * and/or `max-width`. | |
| 8160 */ | |
| 8161 constrain: function() { | |
| 8162 if (this.horizontalAlign || this.verticalAlign) { | |
| 8163 return; | |
| 8164 } | |
| 8165 this._discoverInfo(); | |
| 8166 | |
| 8167 var info = this._fitInfo; | |
| 8168 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
| 8169 if (!info.positionedBy.vertically) { | |
| 8170 this.style.position = 'fixed'; | |
| 8171 this.style.top = '0px'; | |
| 8172 } | |
| 8173 if (!info.positionedBy.horizontally) { | |
| 8174 this.style.position = 'fixed'; | |
| 8175 this.style.left = '0px'; | |
| 8176 } | |
| 8177 | |
| 8178 // need border-box for margin/padding | |
| 8179 this.sizingTarget.style.boxSizing = 'border-box'; | |
| 8180 // constrain the width and height if not already set | |
| 8181 var rect = this.getBoundingClientRect(); | |
| 8182 if (!info.sizedBy.height) { | |
| 8183 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | |
| 8184 } | |
| 8185 if (!info.sizedBy.width) { | |
| 8186 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | |
| 8187 } | |
| 8188 }, | |
| 8189 | |
| 8190 /** | |
| 8191 * @protected | |
| 8192 * @deprecated | |
| 8193 */ | |
| 8194 _sizeDimension: function(rect, positionedBy, start, end, extent) { | |
| 8195 this.__sizeDimension(rect, positionedBy, start, end, extent); | |
| 8196 }, | |
| 8197 | |
| 8198 /** | |
| 8199 * @private | |
| 8200 */ | |
| 8201 __sizeDimension: function(rect, positionedBy, start, end, extent) { | |
| 8202 var info = this._fitInfo; | |
| 8203 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 8204 var max = extent === 'Width' ? fitRect.width : fitRect.height; | |
| 8205 var flip = (positionedBy === end); | |
| 8206 var offset = flip ? max - rect[end] : rect[start]; | |
| 8207 var margin = info.margin[flip ? start : end]; | |
| 8208 var offsetExtent = 'offset' + extent; | |
| 8209 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | |
| 8210 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | |
| 8211 }, | |
| 8212 | |
| 8213 /** | |
| 8214 * Centers horizontally and vertically if not already positioned. This also
sets | |
| 8215 * `position:fixed`. | |
| 8216 */ | |
| 8217 center: function() { | |
| 8218 if (this.horizontalAlign || this.verticalAlign) { | |
| 8219 return; | |
| 8220 } | |
| 8221 this._discoverInfo(); | |
| 8222 | |
| 8223 var positionedBy = this._fitInfo.positionedBy; | |
| 8224 if (positionedBy.vertically && positionedBy.horizontally) { | |
| 8225 // Already positioned. | |
| 8226 return; | |
| 8227 } | |
| 8228 // Need position:fixed to center | |
| 8229 this.style.position = 'fixed'; | |
| 8230 // Take into account the offset caused by parents that create stacking | |
| 8231 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
| 8232 // measure the bounding rect. | |
| 8233 if (!positionedBy.vertically) { | |
| 8234 this.style.top = '0px'; | |
| 8235 } | |
| 8236 if (!positionedBy.horizontally) { | |
| 8237 this.style.left = '0px'; | |
| 8238 } | |
| 8239 // It will take in consideration margins and transforms | |
| 8240 var rect = this.getBoundingClientRect(); | |
| 8241 var fitRect = this.__getNormalizedRect(this.fitInto); | |
| 8242 if (!positionedBy.vertically) { | |
| 8243 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | |
| 8244 this.style.top = top + 'px'; | |
| 8245 } | |
| 8246 if (!positionedBy.horizontally) { | |
| 8247 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | |
| 8248 this.style.left = left + 'px'; | |
| 8249 } | |
| 8250 }, | |
| 8251 | |
| 8252 __getNormalizedRect: function(target) { | |
| 8253 if (target === document.documentElement || target === window) { | |
| 8254 return { | |
| 8255 top: 0, | |
| 8256 left: 0, | |
| 8257 width: window.innerWidth, | |
| 8258 height: window.innerHeight, | |
| 8259 right: window.innerWidth, | |
| 8260 bottom: window.innerHeight | |
| 8261 }; | |
| 8262 } | |
| 8263 return target.getBoundingClientRect(); | |
| 8264 }, | |
| 8265 | |
| 8266 __getCroppedArea: function(position, size, fitRect) { | |
| 8267 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | |
| 8268 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | |
| 8269 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | |
| 8270 }, | |
| 8271 | |
| 8272 | |
| 8273 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | |
| 8274 // All the possible configurations. | |
| 8275 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
| 8276 var positions = [{ | |
| 8277 verticalAlign: 'top', | |
| 8278 horizontalAlign: 'left', | |
| 8279 top: positionRect.top, | |
| 8280 left: positionRect.left | |
| 8281 }, { | |
| 8282 verticalAlign: 'top', | |
| 8283 horizontalAlign: 'right', | |
| 8284 top: positionRect.top, | |
| 8285 left: positionRect.right - size.width | |
| 8286 }, { | |
| 8287 verticalAlign: 'bottom', | |
| 8288 horizontalAlign: 'left', | |
| 8289 top: positionRect.bottom - size.height, | |
| 8290 left: positionRect.left | |
| 8291 }, { | |
| 8292 verticalAlign: 'bottom', | |
| 8293 horizontalAlign: 'right', | |
| 8294 top: positionRect.bottom - size.height, | |
| 8295 left: positionRect.right - size.width | |
| 8296 }]; | |
| 8297 | |
| 8298 if (this.noOverlap) { | |
| 8299 // Duplicate. | |
| 8300 for (var i = 0, l = positions.length; i < l; i++) { | |
| 8301 var copy = {}; | |
| 8302 for (var key in positions[i]) { | |
| 8303 copy[key] = positions[i][key]; | |
| 8304 } | |
| 8305 positions.push(copy); | |
| 8306 } | |
| 8307 // Horizontal overlap only. | |
| 8308 positions[0].top = positions[1].top += positionRect.height; | |
| 8309 positions[2].top = positions[3].top -= positionRect.height; | |
| 8310 // Vertical overlap only. | |
| 8311 positions[4].left = positions[6].left += positionRect.width; | |
| 8312 positions[5].left = positions[7].left -= positionRect.width; | |
| 8313 } | |
| 8314 | |
| 8315 // Consider auto as null for coding convenience. | |
| 8316 vAlign = vAlign === 'auto' ? null : vAlign; | |
| 8317 hAlign = hAlign === 'auto' ? null : hAlign; | |
| 8318 | |
| 8319 var position; | |
| 8320 for (var i = 0; i < positions.length; i++) { | |
| 8321 var pos = positions[i]; | |
| 8322 | |
| 8323 // If both vAlign and hAlign are defined, return exact match. | |
| 8324 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
| 8325 // we'll have to check the croppedArea to make the best choice. | |
| 8326 if (!this.dynamicAlign && !this.noOverlap && | |
| 8327 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | |
| 8328 position = pos; | |
| 8329 break; | |
| 8330 } | |
| 8331 | |
| 8332 // Align is ok if alignment preferences are respected. If no preferences
, | |
| 8333 // it is considered ok. | |
| 8334 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | |
| 8335 (!hAlign || pos.horizontalAlign === hAlign); | |
| 8336 | |
| 8337 // Filter out elements that don't match the alignment (if defined). | |
| 8338 // With dynamicAlign, we need to consider all the positions to find the | |
| 8339 // one that minimizes the cropped area. | |
| 8340 if (!this.dynamicAlign && !alignOk) { | |
| 8341 continue; | |
| 8342 } | |
| 8343 | |
| 8344 position = position || pos; | |
| 8345 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | |
| 8346 var diff = pos.croppedArea - position.croppedArea; | |
| 8347 // Check which crops less. If it crops equally, check if align is ok. | |
| 8348 if (diff < 0 || (diff === 0 && alignOk)) { | |
| 8349 position = pos; | |
| 8350 } | |
| 8351 // If not cropped and respects the align requirements, keep it. | |
| 8352 // This allows to prefer positions overlapping horizontally over the | |
| 8353 // ones overlapping vertically. | |
| 8354 if (position.croppedArea === 0 && alignOk) { | |
| 8355 break; | |
| 8356 } | |
| 8357 } | |
| 8358 | |
| 8359 return position; | |
| 8360 } | |
| 8361 | |
| 8362 }; | |
| 8363 (function() { | |
| 8364 'use strict'; | |
| 8365 | |
| 8366 Polymer({ | |
| 8367 | |
| 8368 is: 'iron-overlay-backdrop', | |
| 8369 | |
| 8370 properties: { | |
| 8371 | |
| 8372 /** | |
| 8373 * Returns true if the backdrop is opened. | |
| 8374 */ | |
| 8375 opened: { | 4910 opened: { |
| 8376 reflectToAttribute: true, | 4911 reflectToAttribute: true, |
| 8377 type: Boolean, | 4912 type: Boolean, |
| 8378 value: false, | 4913 value: false, |
| 8379 observer: '_openedChanged' | 4914 observer: '_openedChanged' |
| 8380 } | 4915 } |
| 8381 | 4916 }, |
| 8382 }, | |
| 8383 | |
| 8384 listeners: { | 4917 listeners: { |
| 8385 'transitionend': '_onTransitionend' | 4918 transitionend: '_onTransitionend' |
| 8386 }, | 4919 }, |
| 8387 | |
| 8388 created: function() { | 4920 created: function() { |
| 8389 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
| 8390 this.__openedRaf = null; | 4921 this.__openedRaf = null; |
| 8391 }, | 4922 }, |
| 8392 | |
| 8393 attached: function() { | 4923 attached: function() { |
| 8394 this.opened && this._openedChanged(this.opened); | 4924 this.opened && this._openedChanged(this.opened); |
| 8395 }, | 4925 }, |
| 8396 | |
| 8397 /** | |
| 8398 * Appends the backdrop to document body if needed. | |
| 8399 */ | |
| 8400 prepare: function() { | 4926 prepare: function() { |
| 8401 if (this.opened && !this.parentNode) { | 4927 if (this.opened && !this.parentNode) { |
| 8402 Polymer.dom(document.body).appendChild(this); | 4928 Polymer.dom(document.body).appendChild(this); |
| 8403 } | 4929 } |
| 8404 }, | 4930 }, |
| 8405 | |
| 8406 /** | |
| 8407 * Shows the backdrop. | |
| 8408 */ | |
| 8409 open: function() { | 4931 open: function() { |
| 8410 this.opened = true; | 4932 this.opened = true; |
| 8411 }, | 4933 }, |
| 8412 | |
| 8413 /** | |
| 8414 * Hides the backdrop. | |
| 8415 */ | |
| 8416 close: function() { | 4934 close: function() { |
| 8417 this.opened = false; | 4935 this.opened = false; |
| 8418 }, | 4936 }, |
| 8419 | |
| 8420 /** | |
| 8421 * Removes the backdrop from document body if needed. | |
| 8422 */ | |
| 8423 complete: function() { | 4937 complete: function() { |
| 8424 if (!this.opened && this.parentNode === document.body) { | 4938 if (!this.opened && this.parentNode === document.body) { |
| 8425 Polymer.dom(this.parentNode).removeChild(this); | 4939 Polymer.dom(this.parentNode).removeChild(this); |
| 8426 } | 4940 } |
| 8427 }, | 4941 }, |
| 8428 | |
| 8429 _onTransitionend: function(event) { | 4942 _onTransitionend: function(event) { |
| 8430 if (event && event.target === this) { | 4943 if (event && event.target === this) { |
| 8431 this.complete(); | 4944 this.complete(); |
| 8432 } | 4945 } |
| 8433 }, | 4946 }, |
| 8434 | |
| 8435 /** | |
| 8436 * @param {boolean} opened | |
| 8437 * @private | |
| 8438 */ | |
| 8439 _openedChanged: function(opened) { | 4947 _openedChanged: function(opened) { |
| 8440 if (opened) { | 4948 if (opened) { |
| 8441 // Auto-attach. | |
| 8442 this.prepare(); | 4949 this.prepare(); |
| 8443 } else { | 4950 } else { |
| 8444 // Animation might be disabled via the mixin or opacity custom property. | |
| 8445 // If it is disabled in other ways, it's up to the user to call complete
. | |
| 8446 var cs = window.getComputedStyle(this); | 4951 var cs = window.getComputedStyle(this); |
| 8447 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 4952 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
| 8448 this.complete(); | 4953 this.complete(); |
| 8449 } | 4954 } |
| 8450 } | 4955 } |
| 8451 | |
| 8452 if (!this.isAttached) { | 4956 if (!this.isAttached) { |
| 8453 return; | 4957 return; |
| 8454 } | 4958 } |
| 8455 | |
| 8456 // Always cancel previous requestAnimationFrame. | |
| 8457 if (this.__openedRaf) { | 4959 if (this.__openedRaf) { |
| 8458 window.cancelAnimationFrame(this.__openedRaf); | 4960 window.cancelAnimationFrame(this.__openedRaf); |
| 8459 this.__openedRaf = null; | 4961 this.__openedRaf = null; |
| 8460 } | 4962 } |
| 8461 // Force relayout to ensure proper transitions. | |
| 8462 this.scrollTop = this.scrollTop; | 4963 this.scrollTop = this.scrollTop; |
| 8463 this.__openedRaf = window.requestAnimationFrame(function() { | 4964 this.__openedRaf = window.requestAnimationFrame(function() { |
| 8464 this.__openedRaf = null; | 4965 this.__openedRaf = null; |
| 8465 this.toggleClass('opened', this.opened); | 4966 this.toggleClass('opened', this.opened); |
| 8466 }.bind(this)); | 4967 }.bind(this)); |
| 8467 } | 4968 } |
| 8468 }); | 4969 }); |
| 8469 | |
| 8470 })(); | 4970 })(); |
| 8471 /** | 4971 |
| 8472 * @struct | 4972 Polymer.IronOverlayManagerClass = function() { |
| 8473 * @constructor | 4973 this._overlays = []; |
| 8474 * @private | 4974 this._minimumZ = 101; |
| 8475 */ | 4975 this._backdropElement = null; |
| 8476 Polymer.IronOverlayManagerClass = function() { | 4976 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
| 8477 /** | 4977 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
| 8478 * Used to keep track of the opened overlays. | 4978 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
| 8479 * @private {Array<Element>} | 4979 }; |
| 8480 */ | 4980 |
| 8481 this._overlays = []; | 4981 Polymer.IronOverlayManagerClass.prototype = { |
| 8482 | 4982 constructor: Polymer.IronOverlayManagerClass, |
| 8483 /** | 4983 get backdropElement() { |
| 8484 * iframes have a default z-index of 100, | 4984 if (!this._backdropElement) { |
| 8485 * so this default should be at least that. | 4985 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
| 8486 * @private {number} | 4986 } |
| 8487 */ | 4987 return this._backdropElement; |
| 8488 this._minimumZ = 101; | 4988 }, |
| 8489 | 4989 get deepActiveElement() { |
| 8490 /** | 4990 var active = document.activeElement || document.body; |
| 8491 * Memoized backdrop element. | 4991 while (active.root && Polymer.dom(active.root).activeElement) { |
| 8492 * @private {Element|null} | 4992 active = Polymer.dom(active.root).activeElement; |
| 8493 */ | 4993 } |
| 8494 this._backdropElement = null; | 4994 return active; |
| 8495 | 4995 }, |
| 8496 // Enable document-wide tap recognizer. | 4996 _bringOverlayAtIndexToFront: function(i) { |
| 8497 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 4997 var overlay = this._overlays[i]; |
| 8498 | 4998 if (!overlay) { |
| 8499 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 4999 return; |
| 8500 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 5000 } |
| 8501 }; | 5001 var lastI = this._overlays.length - 1; |
| 8502 | 5002 var currentOverlay = this._overlays[lastI]; |
| 8503 Polymer.IronOverlayManagerClass.prototype = { | 5003 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
| 8504 | 5004 lastI--; |
| 8505 constructor: Polymer.IronOverlayManagerClass, | 5005 } |
| 8506 | 5006 if (i >= lastI) { |
| 8507 /** | 5007 return; |
| 8508 * The shared backdrop element. | 5008 } |
| 8509 * @type {!Element} backdropElement | 5009 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
| 8510 */ | 5010 if (this._getZ(overlay) <= minimumZ) { |
| 8511 get backdropElement() { | 5011 this._applyOverlayZ(overlay, minimumZ); |
| 8512 if (!this._backdropElement) { | 5012 } |
| 8513 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 5013 while (i < lastI) { |
| 8514 } | 5014 this._overlays[i] = this._overlays[i + 1]; |
| 8515 return this._backdropElement; | 5015 i++; |
| 8516 }, | 5016 } |
| 8517 | 5017 this._overlays[lastI] = overlay; |
| 8518 /** | 5018 }, |
| 8519 * The deepest active element. | 5019 addOrRemoveOverlay: function(overlay) { |
| 8520 * @type {!Element} activeElement the active element | 5020 if (overlay.opened) { |
| 8521 */ | 5021 this.addOverlay(overlay); |
| 8522 get deepActiveElement() { | 5022 } else { |
| 8523 // document.activeElement can be null | 5023 this.removeOverlay(overlay); |
| 8524 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | 5024 } |
| 8525 // In case of null, default it to document.body. | 5025 }, |
| 8526 var active = document.activeElement || document.body; | 5026 addOverlay: function(overlay) { |
| 8527 while (active.root && Polymer.dom(active.root).activeElement) { | 5027 var i = this._overlays.indexOf(overlay); |
| 8528 active = Polymer.dom(active.root).activeElement; | 5028 if (i >= 0) { |
| 8529 } | 5029 this._bringOverlayAtIndexToFront(i); |
| 8530 return active; | |
| 8531 }, | |
| 8532 | |
| 8533 /** | |
| 8534 * Brings the overlay at the specified index to the front. | |
| 8535 * @param {number} i | |
| 8536 * @private | |
| 8537 */ | |
| 8538 _bringOverlayAtIndexToFront: function(i) { | |
| 8539 var overlay = this._overlays[i]; | |
| 8540 if (!overlay) { | |
| 8541 return; | |
| 8542 } | |
| 8543 var lastI = this._overlays.length - 1; | |
| 8544 var currentOverlay = this._overlays[lastI]; | |
| 8545 // Ensure always-on-top overlay stays on top. | |
| 8546 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
| 8547 lastI--; | |
| 8548 } | |
| 8549 // If already the top element, return. | |
| 8550 if (i >= lastI) { | |
| 8551 return; | |
| 8552 } | |
| 8553 // Update z-index to be on top. | |
| 8554 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | |
| 8555 if (this._getZ(overlay) <= minimumZ) { | |
| 8556 this._applyOverlayZ(overlay, minimumZ); | |
| 8557 } | |
| 8558 | |
| 8559 // Shift other overlays behind the new on top. | |
| 8560 while (i < lastI) { | |
| 8561 this._overlays[i] = this._overlays[i + 1]; | |
| 8562 i++; | |
| 8563 } | |
| 8564 this._overlays[lastI] = overlay; | |
| 8565 }, | |
| 8566 | |
| 8567 /** | |
| 8568 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
| 8569 * Also updates the backdrop z-index. | |
| 8570 * @param {!Element} overlay | |
| 8571 */ | |
| 8572 addOrRemoveOverlay: function(overlay) { | |
| 8573 if (overlay.opened) { | |
| 8574 this.addOverlay(overlay); | |
| 8575 } else { | |
| 8576 this.removeOverlay(overlay); | |
| 8577 } | |
| 8578 }, | |
| 8579 | |
| 8580 /** | |
| 8581 * Tracks overlays for z-index and focus management. | |
| 8582 * Ensures the last added overlay with always-on-top remains on top. | |
| 8583 * @param {!Element} overlay | |
| 8584 */ | |
| 8585 addOverlay: function(overlay) { | |
| 8586 var i = this._overlays.indexOf(overlay); | |
| 8587 if (i >= 0) { | |
| 8588 this._bringOverlayAtIndexToFront(i); | |
| 8589 this.trackBackdrop(); | |
| 8590 return; | |
| 8591 } | |
| 8592 var insertionIndex = this._overlays.length; | |
| 8593 var currentOverlay = this._overlays[insertionIndex - 1]; | |
| 8594 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | |
| 8595 var newZ = this._getZ(overlay); | |
| 8596 | |
| 8597 // Ensure always-on-top overlay stays on top. | |
| 8598 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
| 8599 // This bumps the z-index of +2. | |
| 8600 this._applyOverlayZ(currentOverlay, minimumZ); | |
| 8601 insertionIndex--; | |
| 8602 // Update minimumZ to match previous overlay's z-index. | |
| 8603 var previousOverlay = this._overlays[insertionIndex - 1]; | |
| 8604 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | |
| 8605 } | |
| 8606 | |
| 8607 // Update z-index and insert overlay. | |
| 8608 if (newZ <= minimumZ) { | |
| 8609 this._applyOverlayZ(overlay, minimumZ); | |
| 8610 } | |
| 8611 this._overlays.splice(insertionIndex, 0, overlay); | |
| 8612 | |
| 8613 this.trackBackdrop(); | 5030 this.trackBackdrop(); |
| 8614 }, | 5031 return; |
| 8615 | 5032 } |
| 8616 /** | 5033 var insertionIndex = this._overlays.length; |
| 8617 * @param {!Element} overlay | 5034 var currentOverlay = this._overlays[insertionIndex - 1]; |
| 8618 */ | 5035 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
| 8619 removeOverlay: function(overlay) { | 5036 var newZ = this._getZ(overlay); |
| 8620 var i = this._overlays.indexOf(overlay); | 5037 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
| 8621 if (i === -1) { | 5038 this._applyOverlayZ(currentOverlay, minimumZ); |
| 8622 return; | 5039 insertionIndex--; |
| 8623 } | 5040 var previousOverlay = this._overlays[insertionIndex - 1]; |
| 8624 this._overlays.splice(i, 1); | 5041 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
| 8625 | 5042 } |
| 8626 this.trackBackdrop(); | 5043 if (newZ <= minimumZ) { |
| 8627 }, | 5044 this._applyOverlayZ(overlay, minimumZ); |
| 8628 | 5045 } |
| 8629 /** | 5046 this._overlays.splice(insertionIndex, 0, overlay); |
| 8630 * Returns the current overlay. | 5047 this.trackBackdrop(); |
| 8631 * @return {Element|undefined} | 5048 }, |
| 8632 */ | 5049 removeOverlay: function(overlay) { |
| 8633 currentOverlay: function() { | 5050 var i = this._overlays.indexOf(overlay); |
| 8634 var i = this._overlays.length - 1; | 5051 if (i === -1) { |
| 8635 return this._overlays[i]; | 5052 return; |
| 8636 }, | 5053 } |
| 8637 | 5054 this._overlays.splice(i, 1); |
| 8638 /** | 5055 this.trackBackdrop(); |
| 8639 * Returns the current overlay z-index. | 5056 }, |
| 8640 * @return {number} | 5057 currentOverlay: function() { |
| 8641 */ | 5058 var i = this._overlays.length - 1; |
| 8642 currentOverlayZ: function() { | 5059 return this._overlays[i]; |
| 8643 return this._getZ(this.currentOverlay()); | 5060 }, |
| 8644 }, | 5061 currentOverlayZ: function() { |
| 8645 | 5062 return this._getZ(this.currentOverlay()); |
| 8646 /** | 5063 }, |
| 8647 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | 5064 ensureMinimumZ: function(minimumZ) { |
| 8648 * This does not effect the z-index of any existing overlays. | 5065 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
| 8649 * @param {number} minimumZ | 5066 }, |
| 8650 */ | 5067 focusOverlay: function() { |
| 8651 ensureMinimumZ: function(minimumZ) { | 5068 var current = this.currentOverlay(); |
| 8652 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 5069 if (current) { |
| 8653 }, | 5070 current._applyFocus(); |
| 8654 | 5071 } |
| 8655 focusOverlay: function() { | 5072 }, |
| 8656 var current = /** @type {?} */ (this.currentOverlay()); | 5073 trackBackdrop: function() { |
| 8657 if (current) { | 5074 var overlay = this._overlayWithBackdrop(); |
| 8658 current._applyFocus(); | 5075 if (!overlay && !this._backdropElement) { |
| 8659 } | 5076 return; |
| 8660 }, | 5077 } |
| 8661 | 5078 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
| 8662 /** | 5079 this.backdropElement.opened = !!overlay; |
| 8663 * Updates the backdrop z-index. | 5080 }, |
| 8664 */ | 5081 getBackdrops: function() { |
| 8665 trackBackdrop: function() { | 5082 var backdrops = []; |
| 8666 var overlay = this._overlayWithBackdrop(); | 5083 for (var i = 0; i < this._overlays.length; i++) { |
| 8667 // Avoid creating the backdrop if there is no overlay with backdrop. | 5084 if (this._overlays[i].withBackdrop) { |
| 8668 if (!overlay && !this._backdropElement) { | 5085 backdrops.push(this._overlays[i]); |
| 8669 return; | 5086 } |
| 8670 } | 5087 } |
| 8671 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 5088 return backdrops; |
| 8672 this.backdropElement.opened = !!overlay; | 5089 }, |
| 8673 }, | 5090 backdropZ: function() { |
| 8674 | 5091 return this._getZ(this._overlayWithBackdrop()) - 1; |
| 8675 /** | 5092 }, |
| 8676 * @return {Array<Element>} | 5093 _overlayWithBackdrop: function() { |
| 8677 */ | 5094 for (var i = 0; i < this._overlays.length; i++) { |
| 8678 getBackdrops: function() { | 5095 if (this._overlays[i].withBackdrop) { |
| 8679 var backdrops = []; | 5096 return this._overlays[i]; |
| 8680 for (var i = 0; i < this._overlays.length; i++) { | 5097 } |
| 8681 if (this._overlays[i].withBackdrop) { | 5098 } |
| 8682 backdrops.push(this._overlays[i]); | 5099 }, |
| 8683 } | 5100 _getZ: function(overlay) { |
| 8684 } | 5101 var z = this._minimumZ; |
| 8685 return backdrops; | 5102 if (overlay) { |
| 8686 }, | 5103 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z
Index); |
| 8687 | 5104 if (z1 === z1) { |
| 8688 /** | 5105 z = z1; |
| 8689 * Returns the z-index for the backdrop. | 5106 } |
| 8690 * @return {number} | 5107 } |
| 8691 */ | 5108 return z; |
| 8692 backdropZ: function() { | 5109 }, |
| 8693 return this._getZ(this._overlayWithBackdrop()) - 1; | 5110 _setZ: function(element, z) { |
| 8694 }, | 5111 element.style.zIndex = z; |
| 8695 | 5112 }, |
| 8696 /** | 5113 _applyOverlayZ: function(overlay, aboveZ) { |
| 8697 * Returns the first opened overlay that has a backdrop. | 5114 this._setZ(overlay, aboveZ + 2); |
| 8698 * @return {Element|undefined} | 5115 }, |
| 8699 * @private | 5116 _overlayInPath: function(path) { |
| 8700 */ | 5117 path = path || []; |
| 8701 _overlayWithBackdrop: function() { | 5118 for (var i = 0; i < path.length; i++) { |
| 8702 for (var i = 0; i < this._overlays.length; i++) { | 5119 if (path[i]._manager === this) { |
| 8703 if (this._overlays[i].withBackdrop) { | 5120 return path[i]; |
| 8704 return this._overlays[i]; | 5121 } |
| 8705 } | 5122 } |
| 8706 } | 5123 }, |
| 8707 }, | 5124 _onCaptureClick: function(event) { |
| 8708 | 5125 var overlay = this.currentOverlay(); |
| 8709 /** | 5126 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
| 8710 * Calculates the minimum z-index for the overlay. | 5127 overlay._onCaptureClick(event); |
| 8711 * @param {Element=} overlay | 5128 } |
| 8712 * @private | 5129 }, |
| 8713 */ | 5130 _onCaptureFocus: function(event) { |
| 8714 _getZ: function(overlay) { | 5131 var overlay = this.currentOverlay(); |
| 8715 var z = this._minimumZ; | 5132 if (overlay) { |
| 8716 if (overlay) { | 5133 overlay._onCaptureFocus(event); |
| 8717 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 5134 } |
| 8718 // Check if is a number | 5135 }, |
| 8719 // Number.isNaN not supported in IE 10+ | 5136 _onCaptureKeyDown: function(event) { |
| 8720 if (z1 === z1) { | 5137 var overlay = this.currentOverlay(); |
| 8721 z = z1; | 5138 if (overlay) { |
| 8722 } | 5139 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
| 8723 } | 5140 overlay._onCaptureEsc(event); |
| 8724 return z; | 5141 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't
ab')) { |
| 8725 }, | 5142 overlay._onCaptureTab(event); |
| 8726 | 5143 } |
| 8727 /** | 5144 } |
| 8728 * @param {!Element} element | 5145 }, |
| 8729 * @param {number|string} z | 5146 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
| 8730 * @private | 5147 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
| 8731 */ | 5148 } |
| 8732 _setZ: function(element, z) { | 5149 }; |
| 8733 element.style.zIndex = z; | 5150 |
| 8734 }, | 5151 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
| 8735 | 5152 |
| 8736 /** | |
| 8737 * @param {!Element} overlay | |
| 8738 * @param {number} aboveZ | |
| 8739 * @private | |
| 8740 */ | |
| 8741 _applyOverlayZ: function(overlay, aboveZ) { | |
| 8742 this._setZ(overlay, aboveZ + 2); | |
| 8743 }, | |
| 8744 | |
| 8745 /** | |
| 8746 * Returns the deepest overlay in the path. | |
| 8747 * @param {Array<Element>=} path | |
| 8748 * @return {Element|undefined} | |
| 8749 * @suppress {missingProperties} | |
| 8750 * @private | |
| 8751 */ | |
| 8752 _overlayInPath: function(path) { | |
| 8753 path = path || []; | |
| 8754 for (var i = 0; i < path.length; i++) { | |
| 8755 if (path[i]._manager === this) { | |
| 8756 return path[i]; | |
| 8757 } | |
| 8758 } | |
| 8759 }, | |
| 8760 | |
| 8761 /** | |
| 8762 * Ensures the click event is delegated to the right overlay. | |
| 8763 * @param {!Event} event | |
| 8764 * @private | |
| 8765 */ | |
| 8766 _onCaptureClick: function(event) { | |
| 8767 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8768 // Check if clicked outside of top overlay. | |
| 8769 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | |
| 8770 overlay._onCaptureClick(event); | |
| 8771 } | |
| 8772 }, | |
| 8773 | |
| 8774 /** | |
| 8775 * Ensures the focus event is delegated to the right overlay. | |
| 8776 * @param {!Event} event | |
| 8777 * @private | |
| 8778 */ | |
| 8779 _onCaptureFocus: function(event) { | |
| 8780 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8781 if (overlay) { | |
| 8782 overlay._onCaptureFocus(event); | |
| 8783 } | |
| 8784 }, | |
| 8785 | |
| 8786 /** | |
| 8787 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
| 8788 * @param {!Event} event | |
| 8789 * @private | |
| 8790 */ | |
| 8791 _onCaptureKeyDown: function(event) { | |
| 8792 var overlay = /** @type {?} */ (this.currentOverlay()); | |
| 8793 if (overlay) { | |
| 8794 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | |
| 8795 overlay._onCaptureEsc(event); | |
| 8796 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | |
| 8797 overlay._onCaptureTab(event); | |
| 8798 } | |
| 8799 } | |
| 8800 }, | |
| 8801 | |
| 8802 /** | |
| 8803 * Returns if the overlay1 should be behind overlay2. | |
| 8804 * @param {!Element} overlay1 | |
| 8805 * @param {!Element} overlay2 | |
| 8806 * @return {boolean} | |
| 8807 * @suppress {missingProperties} | |
| 8808 * @private | |
| 8809 */ | |
| 8810 _shouldBeBehindOverlay: function(overlay1, overlay2) { | |
| 8811 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | |
| 8812 } | |
| 8813 }; | |
| 8814 | |
| 8815 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | |
| 8816 (function() { | 5153 (function() { |
| 8817 'use strict'; | 5154 'use strict'; |
| 8818 | |
| 8819 /** | |
| 8820 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
| 8821 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
| 8822 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
| 8823 | |
| 8824 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
| 8825 for an example. | |
| 8826 | |
| 8827 ### Closing and canceling | |
| 8828 | |
| 8829 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
| 8830 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
| 8831 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
| 8832 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
| 8833 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
| 8834 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
| 8835 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
| 8836 | |
| 8837 ### Positioning | |
| 8838 | |
| 8839 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
| 8840 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
| 8841 | |
| 8842 ### Backdrop | |
| 8843 | |
| 8844 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
| 8845 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
| 8846 options. | |
| 8847 | |
| 8848 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
| 8849 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
| 8850 to achieve a different behavior. | |
| 8851 | |
| 8852 ### Limitations | |
| 8853 | |
| 8854 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
| 8855 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
| 8856 context. You should place this element as a child of `<body>` whenever possible. | |
| 8857 | |
| 8858 @demo demo/index.html | |
| 8859 @polymerBehavior Polymer.IronOverlayBehavior | |
| 8860 */ | |
| 8861 | |
| 8862 Polymer.IronOverlayBehaviorImpl = { | 5155 Polymer.IronOverlayBehaviorImpl = { |
| 8863 | |
| 8864 properties: { | 5156 properties: { |
| 8865 | |
| 8866 /** | |
| 8867 * True if the overlay is currently displayed. | |
| 8868 */ | |
| 8869 opened: { | 5157 opened: { |
| 8870 observer: '_openedChanged', | 5158 observer: '_openedChanged', |
| 8871 type: Boolean, | 5159 type: Boolean, |
| 8872 value: false, | 5160 value: false, |
| 8873 notify: true | 5161 notify: true |
| 8874 }, | 5162 }, |
| 8875 | |
| 8876 /** | |
| 8877 * True if the overlay was canceled when it was last closed. | |
| 8878 */ | |
| 8879 canceled: { | 5163 canceled: { |
| 8880 observer: '_canceledChanged', | 5164 observer: '_canceledChanged', |
| 8881 readOnly: true, | 5165 readOnly: true, |
| 8882 type: Boolean, | 5166 type: Boolean, |
| 8883 value: false | 5167 value: false |
| 8884 }, | 5168 }, |
| 8885 | |
| 8886 /** | |
| 8887 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
| 8888 * within the light DOM of the overlay. | |
| 8889 */ | |
| 8890 withBackdrop: { | 5169 withBackdrop: { |
| 8891 observer: '_withBackdropChanged', | 5170 observer: '_withBackdropChanged', |
| 8892 type: Boolean | 5171 type: Boolean |
| 8893 }, | 5172 }, |
| 8894 | |
| 8895 /** | |
| 8896 * Set to true to disable auto-focusing the overlay or child nodes with | |
| 8897 * the `autofocus` attribute` when the overlay is opened. | |
| 8898 */ | |
| 8899 noAutoFocus: { | 5173 noAutoFocus: { |
| 8900 type: Boolean, | 5174 type: Boolean, |
| 8901 value: false | 5175 value: false |
| 8902 }, | 5176 }, |
| 8903 | |
| 8904 /** | |
| 8905 * Set to true to disable canceling the overlay with the ESC key. | |
| 8906 */ | |
| 8907 noCancelOnEscKey: { | 5177 noCancelOnEscKey: { |
| 8908 type: Boolean, | 5178 type: Boolean, |
| 8909 value: false | 5179 value: false |
| 8910 }, | 5180 }, |
| 8911 | |
| 8912 /** | |
| 8913 * Set to true to disable canceling the overlay by clicking outside it. | |
| 8914 */ | |
| 8915 noCancelOnOutsideClick: { | 5181 noCancelOnOutsideClick: { |
| 8916 type: Boolean, | 5182 type: Boolean, |
| 8917 value: false | 5183 value: false |
| 8918 }, | 5184 }, |
| 8919 | |
| 8920 /** | |
| 8921 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
| 8922 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
| 8923 * behavior can provide other reasons in addition to `canceled`. | |
| 8924 */ | |
| 8925 closingReason: { | 5185 closingReason: { |
| 8926 // was a getter before, but needs to be a property so other | |
| 8927 // behaviors can override this. | |
| 8928 type: Object | 5186 type: Object |
| 8929 }, | 5187 }, |
| 8930 | |
| 8931 /** | |
| 8932 * Set to true to enable restoring of focus when overlay is closed. | |
| 8933 */ | |
| 8934 restoreFocusOnClose: { | 5188 restoreFocusOnClose: { |
| 8935 type: Boolean, | 5189 type: Boolean, |
| 8936 value: false | 5190 value: false |
| 8937 }, | 5191 }, |
| 8938 | |
| 8939 /** | |
| 8940 * Set to true to keep overlay always on top. | |
| 8941 */ | |
| 8942 alwaysOnTop: { | 5192 alwaysOnTop: { |
| 8943 type: Boolean | 5193 type: Boolean |
| 8944 }, | 5194 }, |
| 8945 | |
| 8946 /** | |
| 8947 * Shortcut to access to the overlay manager. | |
| 8948 * @private | |
| 8949 * @type {Polymer.IronOverlayManagerClass} | |
| 8950 */ | |
| 8951 _manager: { | 5195 _manager: { |
| 8952 type: Object, | 5196 type: Object, |
| 8953 value: Polymer.IronOverlayManager | 5197 value: Polymer.IronOverlayManager |
| 8954 }, | 5198 }, |
| 8955 | |
| 8956 /** | |
| 8957 * The node being focused. | |
| 8958 * @type {?Node} | |
| 8959 */ | |
| 8960 _focusedChild: { | 5199 _focusedChild: { |
| 8961 type: Object | 5200 type: Object |
| 8962 } | 5201 } |
| 8963 | 5202 }, |
| 8964 }, | |
| 8965 | |
| 8966 listeners: { | 5203 listeners: { |
| 8967 'iron-resize': '_onIronResize' | 5204 'iron-resize': '_onIronResize' |
| 8968 }, | 5205 }, |
| 8969 | |
| 8970 /** | |
| 8971 * The backdrop element. | |
| 8972 * @type {Element} | |
| 8973 */ | |
| 8974 get backdropElement() { | 5206 get backdropElement() { |
| 8975 return this._manager.backdropElement; | 5207 return this._manager.backdropElement; |
| 8976 }, | 5208 }, |
| 8977 | |
| 8978 /** | |
| 8979 * Returns the node to give focus to. | |
| 8980 * @type {Node} | |
| 8981 */ | |
| 8982 get _focusNode() { | 5209 get _focusNode() { |
| 8983 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 5210 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
| 8984 }, | 5211 }, |
| 8985 | |
| 8986 /** | |
| 8987 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
| 8988 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
| 8989 * to wrap the focus for overlays `with-backdrop`. | |
| 8990 * | |
| 8991 * If you know what is your content (specifically the first and last focusab
le children), | |
| 8992 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
| 8993 * @type {Array<Node>} | |
| 8994 * @protected | |
| 8995 */ | |
| 8996 get _focusableNodes() { | 5212 get _focusableNodes() { |
| 8997 // Elements that can be focused even if they have [disabled] attribute. | 5213 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin
dex]', '[contentEditable=true]' ]; |
| 8998 var FOCUSABLE_WITH_DISABLED = [ | 5214 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button'
]; |
| 8999 'a[href]', | 5215 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"])'; |
| 9000 'area[href]', | |
| 9001 'iframe', | |
| 9002 '[tabindex]', | |
| 9003 '[contentEditable=true]' | |
| 9004 ]; | |
| 9005 | |
| 9006 // Elements that cannot be focused if they have [disabled] attribute. | |
| 9007 var FOCUSABLE_WITHOUT_DISABLED = [ | |
| 9008 'input', | |
| 9009 'select', | |
| 9010 'textarea', | |
| 9011 'button' | |
| 9012 ]; | |
| 9013 | |
| 9014 // Discard elements with tabindex=-1 (makes them not focusable). | |
| 9015 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | |
| 9016 ':not([tabindex="-1"]),' + | |
| 9017 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | |
| 9018 ':not([disabled]):not([tabindex="-1"])'; | |
| 9019 | |
| 9020 var focusables = Polymer.dom(this).querySelectorAll(selector); | 5216 var focusables = Polymer.dom(this).querySelectorAll(selector); |
| 9021 if (this.tabIndex >= 0) { | 5217 if (this.tabIndex >= 0) { |
| 9022 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
| 9023 // and the overlay should be the first of the list. | |
| 9024 focusables.splice(0, 0, this); | 5218 focusables.splice(0, 0, this); |
| 9025 } | 5219 } |
| 9026 // Sort by tabindex. | 5220 return focusables.sort(function(a, b) { |
| 9027 return focusables.sort(function (a, b) { | |
| 9028 if (a.tabIndex === b.tabIndex) { | 5221 if (a.tabIndex === b.tabIndex) { |
| 9029 return 0; | 5222 return 0; |
| 9030 } | 5223 } |
| 9031 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 5224 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
| 9032 return 1; | 5225 return 1; |
| 9033 } | 5226 } |
| 9034 return -1; | 5227 return -1; |
| 9035 }); | 5228 }); |
| 9036 }, | 5229 }, |
| 9037 | |
| 9038 ready: function() { | 5230 ready: function() { |
| 9039 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
| 9040 this.__isAnimating = false; | 5231 this.__isAnimating = false; |
| 9041 // with-backdrop needs tabindex to be set in order to trap the focus. | |
| 9042 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
| 9043 this.__shouldRemoveTabIndex = false; | 5232 this.__shouldRemoveTabIndex = false; |
| 9044 // Used for wrapping the focus on TAB / Shift+TAB. | |
| 9045 this.__firstFocusableNode = this.__lastFocusableNode = null; | 5233 this.__firstFocusableNode = this.__lastFocusableNode = null; |
| 9046 // Used by __onNextAnimationFrame to cancel any previous callback. | |
| 9047 this.__raf = null; | 5234 this.__raf = null; |
| 9048 // Focused node before overlay gets opened. Can be restored on close. | |
| 9049 this.__restoreFocusNode = null; | 5235 this.__restoreFocusNode = null; |
| 9050 this._ensureSetup(); | 5236 this._ensureSetup(); |
| 9051 }, | 5237 }, |
| 9052 | |
| 9053 attached: function() { | 5238 attached: function() { |
| 9054 // Call _openedChanged here so that position can be computed correctly. | |
| 9055 if (this.opened) { | 5239 if (this.opened) { |
| 9056 this._openedChanged(this.opened); | 5240 this._openedChanged(this.opened); |
| 9057 } | 5241 } |
| 9058 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 5242 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
| 9059 }, | 5243 }, |
| 9060 | |
| 9061 detached: function() { | 5244 detached: function() { |
| 9062 Polymer.dom(this).unobserveNodes(this._observer); | 5245 Polymer.dom(this).unobserveNodes(this._observer); |
| 9063 this._observer = null; | 5246 this._observer = null; |
| 9064 if (this.__raf) { | 5247 if (this.__raf) { |
| 9065 window.cancelAnimationFrame(this.__raf); | 5248 window.cancelAnimationFrame(this.__raf); |
| 9066 this.__raf = null; | 5249 this.__raf = null; |
| 9067 } | 5250 } |
| 9068 this._manager.removeOverlay(this); | 5251 this._manager.removeOverlay(this); |
| 9069 }, | 5252 }, |
| 9070 | |
| 9071 /** | |
| 9072 * Toggle the opened state of the overlay. | |
| 9073 */ | |
| 9074 toggle: function() { | 5253 toggle: function() { |
| 9075 this._setCanceled(false); | 5254 this._setCanceled(false); |
| 9076 this.opened = !this.opened; | 5255 this.opened = !this.opened; |
| 9077 }, | 5256 }, |
| 9078 | |
| 9079 /** | |
| 9080 * Open the overlay. | |
| 9081 */ | |
| 9082 open: function() { | 5257 open: function() { |
| 9083 this._setCanceled(false); | 5258 this._setCanceled(false); |
| 9084 this.opened = true; | 5259 this.opened = true; |
| 9085 }, | 5260 }, |
| 9086 | |
| 9087 /** | |
| 9088 * Close the overlay. | |
| 9089 */ | |
| 9090 close: function() { | 5261 close: function() { |
| 9091 this._setCanceled(false); | 5262 this._setCanceled(false); |
| 9092 this.opened = false; | 5263 this.opened = false; |
| 9093 }, | 5264 }, |
| 9094 | |
| 9095 /** | |
| 9096 * Cancels the overlay. | |
| 9097 * @param {Event=} event The original event | |
| 9098 */ | |
| 9099 cancel: function(event) { | 5265 cancel: function(event) { |
| 9100 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 5266 var cancelEvent = this.fire('iron-overlay-canceled', event, { |
| 5267 cancelable: true |
| 5268 }); |
| 9101 if (cancelEvent.defaultPrevented) { | 5269 if (cancelEvent.defaultPrevented) { |
| 9102 return; | 5270 return; |
| 9103 } | 5271 } |
| 9104 | |
| 9105 this._setCanceled(true); | 5272 this._setCanceled(true); |
| 9106 this.opened = false; | 5273 this.opened = false; |
| 9107 }, | 5274 }, |
| 9108 | |
| 9109 _ensureSetup: function() { | 5275 _ensureSetup: function() { |
| 9110 if (this._overlaySetup) { | 5276 if (this._overlaySetup) { |
| 9111 return; | 5277 return; |
| 9112 } | 5278 } |
| 9113 this._overlaySetup = true; | 5279 this._overlaySetup = true; |
| 9114 this.style.outline = 'none'; | 5280 this.style.outline = 'none'; |
| 9115 this.style.display = 'none'; | 5281 this.style.display = 'none'; |
| 9116 }, | 5282 }, |
| 9117 | |
| 9118 /** | |
| 9119 * Called when `opened` changes. | |
| 9120 * @param {boolean=} opened | |
| 9121 * @protected | |
| 9122 */ | |
| 9123 _openedChanged: function(opened) { | 5283 _openedChanged: function(opened) { |
| 9124 if (opened) { | 5284 if (opened) { |
| 9125 this.removeAttribute('aria-hidden'); | 5285 this.removeAttribute('aria-hidden'); |
| 9126 } else { | 5286 } else { |
| 9127 this.setAttribute('aria-hidden', 'true'); | 5287 this.setAttribute('aria-hidden', 'true'); |
| 9128 } | 5288 } |
| 9129 | |
| 9130 // Defer any animation-related code on attached | |
| 9131 // (_openedChanged gets called again on attached). | |
| 9132 if (!this.isAttached) { | 5289 if (!this.isAttached) { |
| 9133 return; | 5290 return; |
| 9134 } | 5291 } |
| 9135 | |
| 9136 this.__isAnimating = true; | 5292 this.__isAnimating = true; |
| 9137 | |
| 9138 // Use requestAnimationFrame for non-blocking rendering. | |
| 9139 this.__onNextAnimationFrame(this.__openedChanged); | 5293 this.__onNextAnimationFrame(this.__openedChanged); |
| 9140 }, | 5294 }, |
| 9141 | |
| 9142 _canceledChanged: function() { | 5295 _canceledChanged: function() { |
| 9143 this.closingReason = this.closingReason || {}; | 5296 this.closingReason = this.closingReason || {}; |
| 9144 this.closingReason.canceled = this.canceled; | 5297 this.closingReason.canceled = this.canceled; |
| 9145 }, | 5298 }, |
| 9146 | |
| 9147 _withBackdropChanged: function() { | 5299 _withBackdropChanged: function() { |
| 9148 // If tabindex is already set, no need to override it. | |
| 9149 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 5300 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
| 9150 this.setAttribute('tabindex', '-1'); | 5301 this.setAttribute('tabindex', '-1'); |
| 9151 this.__shouldRemoveTabIndex = true; | 5302 this.__shouldRemoveTabIndex = true; |
| 9152 } else if (this.__shouldRemoveTabIndex) { | 5303 } else if (this.__shouldRemoveTabIndex) { |
| 9153 this.removeAttribute('tabindex'); | 5304 this.removeAttribute('tabindex'); |
| 9154 this.__shouldRemoveTabIndex = false; | 5305 this.__shouldRemoveTabIndex = false; |
| 9155 } | 5306 } |
| 9156 if (this.opened && this.isAttached) { | 5307 if (this.opened && this.isAttached) { |
| 9157 this._manager.trackBackdrop(); | 5308 this._manager.trackBackdrop(); |
| 9158 } | 5309 } |
| 9159 }, | 5310 }, |
| 9160 | |
| 9161 /** | |
| 9162 * tasks which must occur before opening; e.g. making the element visible. | |
| 9163 * @protected | |
| 9164 */ | |
| 9165 _prepareRenderOpened: function() { | 5311 _prepareRenderOpened: function() { |
| 9166 // Store focused node. | |
| 9167 this.__restoreFocusNode = this._manager.deepActiveElement; | 5312 this.__restoreFocusNode = this._manager.deepActiveElement; |
| 9168 | |
| 9169 // Needed to calculate the size of the overlay so that transitions on its
size | |
| 9170 // will have the correct starting points. | |
| 9171 this._preparePositioning(); | 5313 this._preparePositioning(); |
| 9172 this.refit(); | 5314 this.refit(); |
| 9173 this._finishPositioning(); | 5315 this._finishPositioning(); |
| 9174 | |
| 9175 // Safari will apply the focus to the autofocus element when displayed | |
| 9176 // for the first time, so we make sure to return the focus where it was. | |
| 9177 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 5316 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
| 9178 this._focusNode.blur(); | 5317 this._focusNode.blur(); |
| 9179 this.__restoreFocusNode.focus(); | 5318 this.__restoreFocusNode.focus(); |
| 9180 } | 5319 } |
| 9181 }, | 5320 }, |
| 9182 | |
| 9183 /** | |
| 9184 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
| 9185 * @protected | |
| 9186 */ | |
| 9187 _renderOpened: function() { | 5321 _renderOpened: function() { |
| 9188 this._finishRenderOpened(); | 5322 this._finishRenderOpened(); |
| 9189 }, | 5323 }, |
| 9190 | |
| 9191 /** | |
| 9192 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
| 9193 * @protected | |
| 9194 */ | |
| 9195 _renderClosed: function() { | 5324 _renderClosed: function() { |
| 9196 this._finishRenderClosed(); | 5325 this._finishRenderClosed(); |
| 9197 }, | 5326 }, |
| 9198 | |
| 9199 /** | |
| 9200 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
| 9201 * @protected | |
| 9202 */ | |
| 9203 _finishRenderOpened: function() { | 5327 _finishRenderOpened: function() { |
| 9204 this.notifyResize(); | 5328 this.notifyResize(); |
| 9205 this.__isAnimating = false; | 5329 this.__isAnimating = false; |
| 9206 | |
| 9207 // Store it so we don't query too much. | |
| 9208 var focusableNodes = this._focusableNodes; | 5330 var focusableNodes = this._focusableNodes; |
| 9209 this.__firstFocusableNode = focusableNodes[0]; | 5331 this.__firstFocusableNode = focusableNodes[0]; |
| 9210 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 5332 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
| 9211 | |
| 9212 this.fire('iron-overlay-opened'); | 5333 this.fire('iron-overlay-opened'); |
| 9213 }, | 5334 }, |
| 9214 | |
| 9215 /** | |
| 9216 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
| 9217 * @protected | |
| 9218 */ | |
| 9219 _finishRenderClosed: function() { | 5335 _finishRenderClosed: function() { |
| 9220 // Hide the overlay. | |
| 9221 this.style.display = 'none'; | 5336 this.style.display = 'none'; |
| 9222 // Reset z-index only at the end of the animation. | |
| 9223 this.style.zIndex = ''; | 5337 this.style.zIndex = ''; |
| 9224 this.notifyResize(); | 5338 this.notifyResize(); |
| 9225 this.__isAnimating = false; | 5339 this.__isAnimating = false; |
| 9226 this.fire('iron-overlay-closed', this.closingReason); | 5340 this.fire('iron-overlay-closed', this.closingReason); |
| 9227 }, | 5341 }, |
| 9228 | |
| 9229 _preparePositioning: function() { | 5342 _preparePositioning: function() { |
| 9230 this.style.transition = this.style.webkitTransition = 'none'; | 5343 this.style.transition = this.style.webkitTransition = 'none'; |
| 9231 this.style.transform = this.style.webkitTransform = 'none'; | 5344 this.style.transform = this.style.webkitTransform = 'none'; |
| 9232 this.style.display = ''; | 5345 this.style.display = ''; |
| 9233 }, | 5346 }, |
| 9234 | |
| 9235 _finishPositioning: function() { | 5347 _finishPositioning: function() { |
| 9236 // First, make it invisible & reactivate animations. | |
| 9237 this.style.display = 'none'; | 5348 this.style.display = 'none'; |
| 9238 // Force reflow before re-enabling animations so that they don't start. | |
| 9239 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
| 9240 this.scrollTop = this.scrollTop; | 5349 this.scrollTop = this.scrollTop; |
| 9241 this.style.transition = this.style.webkitTransition = ''; | 5350 this.style.transition = this.style.webkitTransition = ''; |
| 9242 this.style.transform = this.style.webkitTransform = ''; | 5351 this.style.transform = this.style.webkitTransform = ''; |
| 9243 // Now that animations are enabled, make it visible again | |
| 9244 this.style.display = ''; | 5352 this.style.display = ''; |
| 9245 // Force reflow, so that following animations are properly started. | |
| 9246 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
| 9247 this.scrollTop = this.scrollTop; | 5353 this.scrollTop = this.scrollTop; |
| 9248 }, | 5354 }, |
| 9249 | |
| 9250 /** | |
| 9251 * Applies focus according to the opened state. | |
| 9252 * @protected | |
| 9253 */ | |
| 9254 _applyFocus: function() { | 5355 _applyFocus: function() { |
| 9255 if (this.opened) { | 5356 if (this.opened) { |
| 9256 if (!this.noAutoFocus) { | 5357 if (!this.noAutoFocus) { |
| 9257 this._focusNode.focus(); | 5358 this._focusNode.focus(); |
| 9258 } | 5359 } |
| 9259 } | 5360 } else { |
| 9260 else { | |
| 9261 this._focusNode.blur(); | 5361 this._focusNode.blur(); |
| 9262 this._focusedChild = null; | 5362 this._focusedChild = null; |
| 9263 // Restore focus. | |
| 9264 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 5363 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
| 9265 this.__restoreFocusNode.focus(); | 5364 this.__restoreFocusNode.focus(); |
| 9266 } | 5365 } |
| 9267 this.__restoreFocusNode = null; | 5366 this.__restoreFocusNode = null; |
| 9268 // If many overlays get closed at the same time, one of them would still | |
| 9269 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
| 9270 // infinitely, so we check for this not to be the current overlay. | |
| 9271 var currentOverlay = this._manager.currentOverlay(); | 5367 var currentOverlay = this._manager.currentOverlay(); |
| 9272 if (currentOverlay && this !== currentOverlay) { | 5368 if (currentOverlay && this !== currentOverlay) { |
| 9273 currentOverlay._applyFocus(); | 5369 currentOverlay._applyFocus(); |
| 9274 } | 5370 } |
| 9275 } | 5371 } |
| 9276 }, | 5372 }, |
| 9277 | |
| 9278 /** | |
| 9279 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
| 9280 * @param {!Event} event | |
| 9281 * @protected | |
| 9282 */ | |
| 9283 _onCaptureClick: function(event) { | 5373 _onCaptureClick: function(event) { |
| 9284 if (!this.noCancelOnOutsideClick) { | 5374 if (!this.noCancelOnOutsideClick) { |
| 9285 this.cancel(event); | 5375 this.cancel(event); |
| 9286 } | 5376 } |
| 9287 }, | 5377 }, |
| 9288 | 5378 _onCaptureFocus: function(event) { |
| 9289 /** | |
| 9290 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
| 9291 * @param {!Event} event | |
| 9292 * @protected | |
| 9293 */ | |
| 9294 _onCaptureFocus: function (event) { | |
| 9295 if (!this.withBackdrop) { | 5379 if (!this.withBackdrop) { |
| 9296 return; | 5380 return; |
| 9297 } | 5381 } |
| 9298 var path = Polymer.dom(event).path; | 5382 var path = Polymer.dom(event).path; |
| 9299 if (path.indexOf(this) === -1) { | 5383 if (path.indexOf(this) === -1) { |
| 9300 event.stopPropagation(); | 5384 event.stopPropagation(); |
| 9301 this._applyFocus(); | 5385 this._applyFocus(); |
| 9302 } else { | 5386 } else { |
| 9303 this._focusedChild = path[0]; | 5387 this._focusedChild = path[0]; |
| 9304 } | 5388 } |
| 9305 }, | 5389 }, |
| 9306 | |
| 9307 /** | |
| 9308 * Handles the ESC key event and cancels (closes) the overlay. | |
| 9309 * @param {!Event} event | |
| 9310 * @protected | |
| 9311 */ | |
| 9312 _onCaptureEsc: function(event) { | 5390 _onCaptureEsc: function(event) { |
| 9313 if (!this.noCancelOnEscKey) { | 5391 if (!this.noCancelOnEscKey) { |
| 9314 this.cancel(event); | 5392 this.cancel(event); |
| 9315 } | 5393 } |
| 9316 }, | 5394 }, |
| 9317 | |
| 9318 /** | |
| 9319 * Handles TAB key events to track focus changes. | |
| 9320 * Will wrap focus for overlays withBackdrop. | |
| 9321 * @param {!Event} event | |
| 9322 * @protected | |
| 9323 */ | |
| 9324 _onCaptureTab: function(event) { | 5395 _onCaptureTab: function(event) { |
| 9325 if (!this.withBackdrop) { | 5396 if (!this.withBackdrop) { |
| 9326 return; | 5397 return; |
| 9327 } | 5398 } |
| 9328 // TAB wraps from last to first focusable. | |
| 9329 // Shift + TAB wraps from first to last focusable. | |
| 9330 var shift = event.shiftKey; | 5399 var shift = event.shiftKey; |
| 9331 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 5400 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
| 9332 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 5401 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
| 9333 var shouldWrap = false; | 5402 var shouldWrap = false; |
| 9334 if (nodeToCheck === nodeToSet) { | 5403 if (nodeToCheck === nodeToSet) { |
| 9335 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
| 9336 // with 0 or 1 focusables; in either case we still need to trap the | |
| 9337 // focus within the overlay. | |
| 9338 shouldWrap = true; | 5404 shouldWrap = true; |
| 9339 } else { | 5405 } else { |
| 9340 // In dom=shadow, the manager will receive focus changes on the main | |
| 9341 // root but not the ones within other shadow roots, so we can't rely on | |
| 9342 // _focusedChild, but we should check the deepest active element. | |
| 9343 var focusedNode = this._manager.deepActiveElement; | 5406 var focusedNode = this._manager.deepActiveElement; |
| 9344 // If the active element is not the nodeToCheck but the overlay itself, | 5407 shouldWrap = focusedNode === nodeToCheck || focusedNode === this; |
| 9345 // it means the focus is about to go outside the overlay, hence we | 5408 } |
| 9346 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
| 9347 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | |
| 9348 } | |
| 9349 | |
| 9350 if (shouldWrap) { | 5409 if (shouldWrap) { |
| 9351 // When the overlay contains the last focusable element of the document | |
| 9352 // and it's already focused, pressing TAB would move the focus outside | |
| 9353 // the document (e.g. to the browser search bar). Similarly, when the | |
| 9354 // overlay contains the first focusable element of the document and it's | |
| 9355 // already focused, pressing Shift+TAB would move the focus outside the | |
| 9356 // document (e.g. to the browser search bar). | |
| 9357 // In both cases, we would not receive a focus event, but only a blur. | |
| 9358 // In order to achieve focus wrapping, we prevent this TAB event and | |
| 9359 // force the focus. This will also prevent the focus to temporarily move | |
| 9360 // outside the overlay, which might cause scrolling. | |
| 9361 event.preventDefault(); | 5410 event.preventDefault(); |
| 9362 this._focusedChild = nodeToSet; | 5411 this._focusedChild = nodeToSet; |
| 9363 this._applyFocus(); | 5412 this._applyFocus(); |
| 9364 } | 5413 } |
| 9365 }, | 5414 }, |
| 9366 | |
| 9367 /** | |
| 9368 * Refits if the overlay is opened and not animating. | |
| 9369 * @protected | |
| 9370 */ | |
| 9371 _onIronResize: function() { | 5415 _onIronResize: function() { |
| 9372 if (this.opened && !this.__isAnimating) { | 5416 if (this.opened && !this.__isAnimating) { |
| 9373 this.__onNextAnimationFrame(this.refit); | 5417 this.__onNextAnimationFrame(this.refit); |
| 9374 } | 5418 } |
| 9375 }, | 5419 }, |
| 9376 | |
| 9377 /** | |
| 9378 * Will call notifyResize if overlay is opened. | |
| 9379 * Can be overridden in order to avoid multiple observers on the same node. | |
| 9380 * @protected | |
| 9381 */ | |
| 9382 _onNodesChange: function() { | 5420 _onNodesChange: function() { |
| 9383 if (this.opened && !this.__isAnimating) { | 5421 if (this.opened && !this.__isAnimating) { |
| 9384 this.notifyResize(); | 5422 this.notifyResize(); |
| 9385 } | 5423 } |
| 9386 }, | 5424 }, |
| 9387 | |
| 9388 /** | |
| 9389 * Tasks executed when opened changes: prepare for the opening, move the | |
| 9390 * focus, update the manager, render opened/closed. | |
| 9391 * @private | |
| 9392 */ | |
| 9393 __openedChanged: function() { | 5425 __openedChanged: function() { |
| 9394 if (this.opened) { | 5426 if (this.opened) { |
| 9395 // Make overlay visible, then add it to the manager. | |
| 9396 this._prepareRenderOpened(); | 5427 this._prepareRenderOpened(); |
| 9397 this._manager.addOverlay(this); | 5428 this._manager.addOverlay(this); |
| 9398 // Move the focus to the child node with [autofocus]. | |
| 9399 this._applyFocus(); | 5429 this._applyFocus(); |
| 9400 | |
| 9401 this._renderOpened(); | 5430 this._renderOpened(); |
| 9402 } else { | 5431 } else { |
| 9403 // Remove overlay, then restore the focus before actually closing. | |
| 9404 this._manager.removeOverlay(this); | 5432 this._manager.removeOverlay(this); |
| 9405 this._applyFocus(); | 5433 this._applyFocus(); |
| 9406 | |
| 9407 this._renderClosed(); | 5434 this._renderClosed(); |
| 9408 } | 5435 } |
| 9409 }, | 5436 }, |
| 9410 | |
| 9411 /** | |
| 9412 * Executes a callback on the next animation frame, overriding any previous | |
| 9413 * callback awaiting for the next animation frame. e.g. | |
| 9414 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
| 9415 * `callback1` will never be invoked. | |
| 9416 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
| 9417 * @private | |
| 9418 */ | |
| 9419 __onNextAnimationFrame: function(callback) { | 5437 __onNextAnimationFrame: function(callback) { |
| 9420 if (this.__raf) { | 5438 if (this.__raf) { |
| 9421 window.cancelAnimationFrame(this.__raf); | 5439 window.cancelAnimationFrame(this.__raf); |
| 9422 } | 5440 } |
| 9423 var self = this; | 5441 var self = this; |
| 9424 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 5442 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
| 9425 self.__raf = null; | 5443 self.__raf = null; |
| 9426 callback.call(self); | 5444 callback.call(self); |
| 9427 }); | 5445 }); |
| 9428 } | 5446 } |
| 9429 | |
| 9430 }; | 5447 }; |
| 9431 | 5448 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable
Behavior, Polymer.IronOverlayBehaviorImpl ]; |
| 9432 /** @polymerBehavior */ | |
| 9433 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
| 9434 | |
| 9435 /** | |
| 9436 * Fired after the overlay opens. | |
| 9437 * @event iron-overlay-opened | |
| 9438 */ | |
| 9439 | |
| 9440 /** | |
| 9441 * Fired when the overlay is canceled, but before it is closed. | |
| 9442 * @event iron-overlay-canceled | |
| 9443 * @param {Event} event The closing of the overlay can be prevented | |
| 9444 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
| 9445 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
| 9446 */ | |
| 9447 | |
| 9448 /** | |
| 9449 * Fired after the overlay closes. | |
| 9450 * @event iron-overlay-closed | |
| 9451 * @param {Event} event The `event.detail` is the `closingReason` property | |
| 9452 * (contains `canceled`, whether the overlay was canceled). | |
| 9453 */ | |
| 9454 | |
| 9455 })(); | 5449 })(); |
| 9456 /** | 5450 |
| 9457 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | 5451 Polymer.NeonAnimatableBehavior = { |
| 9458 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | 5452 properties: { |
| 9459 * @polymerBehavior | 5453 animationConfig: { |
| 9460 */ | 5454 type: Object |
| 9461 Polymer.NeonAnimatableBehavior = { | 5455 }, |
| 9462 | 5456 entryAnimation: { |
| 5457 observer: '_entryAnimationChanged', |
| 5458 type: String |
| 5459 }, |
| 5460 exitAnimation: { |
| 5461 observer: '_exitAnimationChanged', |
| 5462 type: String |
| 5463 } |
| 5464 }, |
| 5465 _entryAnimationChanged: function() { |
| 5466 this.animationConfig = this.animationConfig || {}; |
| 5467 this.animationConfig['entry'] = [ { |
| 5468 name: this.entryAnimation, |
| 5469 node: this |
| 5470 } ]; |
| 5471 }, |
| 5472 _exitAnimationChanged: function() { |
| 5473 this.animationConfig = this.animationConfig || {}; |
| 5474 this.animationConfig['exit'] = [ { |
| 5475 name: this.exitAnimation, |
| 5476 node: this |
| 5477 } ]; |
| 5478 }, |
| 5479 _copyProperties: function(config1, config2) { |
| 5480 for (var property in config2) { |
| 5481 config1[property] = config2[property]; |
| 5482 } |
| 5483 }, |
| 5484 _cloneConfig: function(config) { |
| 5485 var clone = { |
| 5486 isClone: true |
| 5487 }; |
| 5488 this._copyProperties(clone, config); |
| 5489 return clone; |
| 5490 }, |
| 5491 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
| 5492 if (!this.animationConfig) { |
| 5493 return; |
| 5494 } |
| 5495 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun
ction') { |
| 5496 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid
e of your components 'properties' object instead of outside of it.")); |
| 5497 return; |
| 5498 } |
| 5499 var thisConfig; |
| 5500 if (type) { |
| 5501 thisConfig = this.animationConfig[type]; |
| 5502 } else { |
| 5503 thisConfig = this.animationConfig; |
| 5504 } |
| 5505 if (!Array.isArray(thisConfig)) { |
| 5506 thisConfig = [ thisConfig ]; |
| 5507 } |
| 5508 if (thisConfig) { |
| 5509 for (var config, index = 0; config = thisConfig[index]; index++) { |
| 5510 if (config.animatable) { |
| 5511 config.animatable._getAnimationConfigRecursive(config.type || type, ma
p, allConfigs); |
| 5512 } else { |
| 5513 if (config.id) { |
| 5514 var cachedConfig = map[config.id]; |
| 5515 if (cachedConfig) { |
| 5516 if (!cachedConfig.isClone) { |
| 5517 map[config.id] = this._cloneConfig(cachedConfig); |
| 5518 cachedConfig = map[config.id]; |
| 5519 } |
| 5520 this._copyProperties(cachedConfig, config); |
| 5521 } else { |
| 5522 map[config.id] = config; |
| 5523 } |
| 5524 } else { |
| 5525 allConfigs.push(config); |
| 5526 } |
| 5527 } |
| 5528 } |
| 5529 } |
| 5530 }, |
| 5531 getAnimationConfig: function(type) { |
| 5532 var map = {}; |
| 5533 var allConfigs = []; |
| 5534 this._getAnimationConfigRecursive(type, map, allConfigs); |
| 5535 for (var key in map) { |
| 5536 allConfigs.push(map[key]); |
| 5537 } |
| 5538 return allConfigs; |
| 5539 } |
| 5540 }; |
| 5541 |
| 5542 Polymer.NeonAnimationRunnerBehaviorImpl = { |
| 5543 _configureAnimations: function(configs) { |
| 5544 var results = []; |
| 5545 if (configs.length > 0) { |
| 5546 for (var config, index = 0; config = configs[index]; index++) { |
| 5547 var neonAnimation = document.createElement(config.name); |
| 5548 if (neonAnimation.isNeonAnimation) { |
| 5549 var result = null; |
| 5550 try { |
| 5551 result = neonAnimation.configure(config); |
| 5552 if (typeof result.cancel != 'function') { |
| 5553 result = document.timeline.play(result); |
| 5554 } |
| 5555 } catch (e) { |
| 5556 result = null; |
| 5557 console.warn('Couldnt play', '(', config.name, ').', e); |
| 5558 } |
| 5559 if (result) { |
| 5560 results.push({ |
| 5561 neonAnimation: neonAnimation, |
| 5562 config: config, |
| 5563 animation: result |
| 5564 }); |
| 5565 } |
| 5566 } else { |
| 5567 console.warn(this.is + ':', config.name, 'not found!'); |
| 5568 } |
| 5569 } |
| 5570 } |
| 5571 return results; |
| 5572 }, |
| 5573 _shouldComplete: function(activeEntries) { |
| 5574 var finished = true; |
| 5575 for (var i = 0; i < activeEntries.length; i++) { |
| 5576 if (activeEntries[i].animation.playState != 'finished') { |
| 5577 finished = false; |
| 5578 break; |
| 5579 } |
| 5580 } |
| 5581 return finished; |
| 5582 }, |
| 5583 _complete: function(activeEntries) { |
| 5584 for (var i = 0; i < activeEntries.length; i++) { |
| 5585 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
| 5586 } |
| 5587 for (var i = 0; i < activeEntries.length; i++) { |
| 5588 activeEntries[i].animation.cancel(); |
| 5589 } |
| 5590 }, |
| 5591 playAnimation: function(type, cookie) { |
| 5592 var configs = this.getAnimationConfig(type); |
| 5593 if (!configs) { |
| 5594 return; |
| 5595 } |
| 5596 this._active = this._active || {}; |
| 5597 if (this._active[type]) { |
| 5598 this._complete(this._active[type]); |
| 5599 delete this._active[type]; |
| 5600 } |
| 5601 var activeEntries = this._configureAnimations(configs); |
| 5602 if (activeEntries.length == 0) { |
| 5603 this.fire('neon-animation-finish', cookie, { |
| 5604 bubbles: false |
| 5605 }); |
| 5606 return; |
| 5607 } |
| 5608 this._active[type] = activeEntries; |
| 5609 for (var i = 0; i < activeEntries.length; i++) { |
| 5610 activeEntries[i].animation.onfinish = function() { |
| 5611 if (this._shouldComplete(activeEntries)) { |
| 5612 this._complete(activeEntries); |
| 5613 delete this._active[type]; |
| 5614 this.fire('neon-animation-finish', cookie, { |
| 5615 bubbles: false |
| 5616 }); |
| 5617 } |
| 5618 }.bind(this); |
| 5619 } |
| 5620 }, |
| 5621 cancelAnimation: function() { |
| 5622 for (var k in this._animations) { |
| 5623 this._animations[k].cancel(); |
| 5624 } |
| 5625 this._animations = {}; |
| 5626 } |
| 5627 }; |
| 5628 |
| 5629 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer.
NeonAnimationRunnerBehaviorImpl ]; |
| 5630 |
| 5631 Polymer.NeonAnimationBehavior = { |
| 5632 properties: { |
| 5633 animationTiming: { |
| 5634 type: Object, |
| 5635 value: function() { |
| 5636 return { |
| 5637 duration: 500, |
| 5638 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
| 5639 fill: 'both' |
| 5640 }; |
| 5641 } |
| 5642 } |
| 5643 }, |
| 5644 isNeonAnimation: true, |
| 5645 timingFromConfig: function(config) { |
| 5646 if (config.timing) { |
| 5647 for (var property in config.timing) { |
| 5648 this.animationTiming[property] = config.timing[property]; |
| 5649 } |
| 5650 } |
| 5651 return this.animationTiming; |
| 5652 }, |
| 5653 setPrefixedProperty: function(node, property, value) { |
| 5654 var map = { |
| 5655 transform: [ 'webkitTransform' ], |
| 5656 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ] |
| 5657 }; |
| 5658 var prefixes = map[property]; |
| 5659 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
| 5660 node.style[prefix] = value; |
| 5661 } |
| 5662 node.style[property] = value; |
| 5663 }, |
| 5664 complete: function() {} |
| 5665 }; |
| 5666 |
| 5667 Polymer({ |
| 5668 is: 'opaque-animation', |
| 5669 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5670 configure: function(config) { |
| 5671 var node = config.node; |
| 5672 this._effect = new KeyframeEffect(node, [ { |
| 5673 opacity: '1' |
| 5674 }, { |
| 5675 opacity: '1' |
| 5676 } ], this.timingFromConfig(config)); |
| 5677 node.style.opacity = '0'; |
| 5678 return this._effect; |
| 5679 }, |
| 5680 complete: function(config) { |
| 5681 config.node.style.opacity = ''; |
| 5682 } |
| 5683 }); |
| 5684 |
| 5685 (function() { |
| 5686 'use strict'; |
| 5687 var LAST_TOUCH_POSITION = { |
| 5688 pageX: 0, |
| 5689 pageY: 0 |
| 5690 }; |
| 5691 var ROOT_TARGET = null; |
| 5692 var SCROLLABLE_NODES = []; |
| 5693 Polymer.IronDropdownScrollManager = { |
| 5694 get currentLockingElement() { |
| 5695 return this._lockingElements[this._lockingElements.length - 1]; |
| 5696 }, |
| 5697 elementIsScrollLocked: function(element) { |
| 5698 var currentLockingElement = this.currentLockingElement; |
| 5699 if (currentLockingElement === undefined) return false; |
| 5700 var scrollLocked; |
| 5701 if (this._hasCachedLockedElement(element)) { |
| 5702 return true; |
| 5703 } |
| 5704 if (this._hasCachedUnlockedElement(element)) { |
| 5705 return false; |
| 5706 } |
| 5707 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen
t && !this._composedTreeContains(currentLockingElement, element); |
| 5708 if (scrollLocked) { |
| 5709 this._lockedElementCache.push(element); |
| 5710 } else { |
| 5711 this._unlockedElementCache.push(element); |
| 5712 } |
| 5713 return scrollLocked; |
| 5714 }, |
| 5715 pushScrollLock: function(element) { |
| 5716 if (this._lockingElements.indexOf(element) >= 0) { |
| 5717 return; |
| 5718 } |
| 5719 if (this._lockingElements.length === 0) { |
| 5720 this._lockScrollInteractions(); |
| 5721 } |
| 5722 this._lockingElements.push(element); |
| 5723 this._lockedElementCache = []; |
| 5724 this._unlockedElementCache = []; |
| 5725 }, |
| 5726 removeScrollLock: function(element) { |
| 5727 var index = this._lockingElements.indexOf(element); |
| 5728 if (index === -1) { |
| 5729 return; |
| 5730 } |
| 5731 this._lockingElements.splice(index, 1); |
| 5732 this._lockedElementCache = []; |
| 5733 this._unlockedElementCache = []; |
| 5734 if (this._lockingElements.length === 0) { |
| 5735 this._unlockScrollInteractions(); |
| 5736 } |
| 5737 }, |
| 5738 _lockingElements: [], |
| 5739 _lockedElementCache: null, |
| 5740 _unlockedElementCache: null, |
| 5741 _hasCachedLockedElement: function(element) { |
| 5742 return this._lockedElementCache.indexOf(element) > -1; |
| 5743 }, |
| 5744 _hasCachedUnlockedElement: function(element) { |
| 5745 return this._unlockedElementCache.indexOf(element) > -1; |
| 5746 }, |
| 5747 _composedTreeContains: function(element, child) { |
| 5748 var contentElements; |
| 5749 var distributedNodes; |
| 5750 var contentIndex; |
| 5751 var nodeIndex; |
| 5752 if (element.contains(child)) { |
| 5753 return true; |
| 5754 } |
| 5755 contentElements = Polymer.dom(element).querySelectorAll('content'); |
| 5756 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd
ex) { |
| 5757 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib
utedNodes(); |
| 5758 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { |
| 5759 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { |
| 5760 return true; |
| 5761 } |
| 5762 } |
| 5763 } |
| 5764 return false; |
| 5765 }, |
| 5766 _scrollInteractionHandler: function(event) { |
| 5767 if (event.cancelable && this._shouldPreventScrolling(event)) { |
| 5768 event.preventDefault(); |
| 5769 } |
| 5770 if (event.targetTouches) { |
| 5771 var touch = event.targetTouches[0]; |
| 5772 LAST_TOUCH_POSITION.pageX = touch.pageX; |
| 5773 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 5774 } |
| 5775 }, |
| 5776 _lockScrollInteractions: function() { |
| 5777 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac
tionHandler.bind(this); |
| 5778 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 5779 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 5780 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true
); |
| 5781 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 5782 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 5783 }, |
| 5784 _unlockScrollInteractions: function() { |
| 5785 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 5786 document.removeEventListener('mousewheel', this._boundScrollHandler, true)
; |
| 5787 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t
rue); |
| 5788 document.removeEventListener('touchstart', this._boundScrollHandler, true)
; |
| 5789 document.removeEventListener('touchmove', this._boundScrollHandler, true); |
| 5790 }, |
| 5791 _shouldPreventScrolling: function(event) { |
| 5792 var target = Polymer.dom(event).rootTarget; |
| 5793 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 5794 ROOT_TARGET = target; |
| 5795 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 5796 } |
| 5797 if (!SCROLLABLE_NODES.length) { |
| 5798 return true; |
| 5799 } |
| 5800 if (event.type === 'touchstart') { |
| 5801 return false; |
| 5802 } |
| 5803 var info = this._getScrollInfo(event); |
| 5804 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY)
; |
| 5805 }, |
| 5806 _getScrollableNodes: function(nodes) { |
| 5807 var scrollables = []; |
| 5808 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 5809 for (var i = 0; i <= lockingIndex; i++) { |
| 5810 var node = nodes[i]; |
| 5811 if (node.nodeType === 11) { |
| 5812 continue; |
| 5813 } |
| 5814 var style = node.style; |
| 5815 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 5816 style = window.getComputedStyle(node); |
| 5817 } |
| 5818 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 5819 scrollables.push(node); |
| 5820 } |
| 5821 } |
| 5822 return scrollables; |
| 5823 }, |
| 5824 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 5825 if (!deltaX && !deltaY) { |
| 5826 return; |
| 5827 } |
| 5828 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 5829 for (var i = 0; i < nodes.length; i++) { |
| 5830 var node = nodes[i]; |
| 5831 var canScroll = false; |
| 5832 if (verticalScroll) { |
| 5833 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc
rollHeight - node.clientHeight; |
| 5834 } else { |
| 5835 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.
scrollWidth - node.clientWidth; |
| 5836 } |
| 5837 if (canScroll) { |
| 5838 return node; |
| 5839 } |
| 5840 } |
| 5841 }, |
| 5842 _getScrollInfo: function(event) { |
| 5843 var info = { |
| 5844 deltaX: event.deltaX, |
| 5845 deltaY: event.deltaY |
| 5846 }; |
| 5847 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) { |
| 5848 info.deltaX = -event.wheelDeltaX; |
| 5849 info.deltaY = -event.wheelDeltaY; |
| 5850 } else if ('axis' in event) { |
| 5851 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 5852 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 5853 } else if (event.targetTouches) { |
| 5854 var touch = event.targetTouches[0]; |
| 5855 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 5856 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 5857 } |
| 5858 return info; |
| 5859 } |
| 5860 }; |
| 5861 })(); |
| 5862 |
| 5863 (function() { |
| 5864 'use strict'; |
| 5865 Polymer({ |
| 5866 is: 'iron-dropdown', |
| 5867 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer
.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], |
| 9463 properties: { | 5868 properties: { |
| 9464 | 5869 horizontalAlign: { |
| 9465 /** | 5870 type: String, |
| 9466 * Animation configuration. See README for more info. | 5871 value: 'left', |
| 9467 */ | 5872 reflectToAttribute: true |
| 9468 animationConfig: { | 5873 }, |
| 5874 verticalAlign: { |
| 5875 type: String, |
| 5876 value: 'top', |
| 5877 reflectToAttribute: true |
| 5878 }, |
| 5879 openAnimationConfig: { |
| 9469 type: Object | 5880 type: Object |
| 9470 }, | 5881 }, |
| 9471 | 5882 closeAnimationConfig: { |
| 9472 /** | 5883 type: Object |
| 9473 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | |
| 9474 * manually if using this. The animated node is set to `this` if using thi
s property. | |
| 9475 */ | |
| 9476 entryAnimation: { | |
| 9477 observer: '_entryAnimationChanged', | |
| 9478 type: String | |
| 9479 }, | 5884 }, |
| 9480 | 5885 focusTarget: { |
| 9481 /** | 5886 type: Object |
| 9482 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | 5887 }, |
| 9483 * manually if using this. The animated node is set to `this` if using thi
s property. | 5888 noAnimations: { |
| 9484 */ | 5889 type: Boolean, |
| 9485 exitAnimation: { | 5890 value: false |
| 9486 observer: '_exitAnimationChanged', | 5891 }, |
| 9487 type: String | 5892 allowOutsideScroll: { |
| 9488 } | 5893 type: Boolean, |
| 9489 | 5894 value: false |
| 9490 }, | 5895 }, |
| 9491 | 5896 _boundOnCaptureScroll: { |
| 9492 _entryAnimationChanged: function() { | 5897 type: Function, |
| 9493 this.animationConfig = this.animationConfig || {}; | 5898 value: function() { |
| 9494 this.animationConfig['entry'] = [{ | 5899 return this._onCaptureScroll.bind(this); |
| 9495 name: this.entryAnimation, | 5900 } |
| 9496 node: this | 5901 } |
| 9497 }]; | 5902 }, |
| 9498 }, | 5903 listeners: { |
| 9499 | 5904 'neon-animation-finish': '_onNeonAnimationFinish' |
| 9500 _exitAnimationChanged: function() { | 5905 }, |
| 9501 this.animationConfig = this.animationConfig || {}; | 5906 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont
alAlign, verticalOffset, horizontalOffset)' ], |
| 9502 this.animationConfig['exit'] = [{ | 5907 get containedElement() { |
| 9503 name: this.exitAnimation, | 5908 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| 9504 node: this | 5909 }, |
| 9505 }]; | 5910 get _focusTarget() { |
| 9506 }, | 5911 return this.focusTarget || this.containedElement; |
| 9507 | 5912 }, |
| 9508 _copyProperties: function(config1, config2) { | 5913 ready: function() { |
| 9509 // shallowly copy properties from config2 to config1 | 5914 this._scrollTop = 0; |
| 9510 for (var property in config2) { | 5915 this._scrollLeft = 0; |
| 9511 config1[property] = config2[property]; | 5916 this._refitOnScrollRAF = null; |
| 9512 } | 5917 }, |
| 9513 }, | 5918 detached: function() { |
| 9514 | 5919 this.cancelAnimation(); |
| 9515 _cloneConfig: function(config) { | 5920 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 9516 var clone = { | 5921 }, |
| 9517 isClone: true | 5922 _openedChanged: function() { |
| 5923 if (this.opened && this.disabled) { |
| 5924 this.cancel(); |
| 5925 } else { |
| 5926 this.cancelAnimation(); |
| 5927 this.sizingTarget = this.containedElement || this.sizingTarget; |
| 5928 this._updateAnimationConfig(); |
| 5929 this._saveScrollPosition(); |
| 5930 if (this.opened) { |
| 5931 document.addEventListener('scroll', this._boundOnCaptureScroll); |
| 5932 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro
llLock(this); |
| 5933 } else { |
| 5934 document.removeEventListener('scroll', this._boundOnCaptureScroll); |
| 5935 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5936 } |
| 5937 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); |
| 5938 } |
| 5939 }, |
| 5940 _renderOpened: function() { |
| 5941 if (!this.noAnimations && this.animationConfig.open) { |
| 5942 this.$.contentWrapper.classList.add('animating'); |
| 5943 this.playAnimation('open'); |
| 5944 } else { |
| 5945 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); |
| 5946 } |
| 5947 }, |
| 5948 _renderClosed: function() { |
| 5949 if (!this.noAnimations && this.animationConfig.close) { |
| 5950 this.$.contentWrapper.classList.add('animating'); |
| 5951 this.playAnimation('close'); |
| 5952 } else { |
| 5953 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); |
| 5954 } |
| 5955 }, |
| 5956 _onNeonAnimationFinish: function() { |
| 5957 this.$.contentWrapper.classList.remove('animating'); |
| 5958 if (this.opened) { |
| 5959 this._finishRenderOpened(); |
| 5960 } else { |
| 5961 this._finishRenderClosed(); |
| 5962 } |
| 5963 }, |
| 5964 _onCaptureScroll: function() { |
| 5965 if (!this.allowOutsideScroll) { |
| 5966 this._restoreScrollPosition(); |
| 5967 } else { |
| 5968 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol
lRAF); |
| 5969 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th
is)); |
| 5970 } |
| 5971 }, |
| 5972 _saveScrollPosition: function() { |
| 5973 if (document.scrollingElement) { |
| 5974 this._scrollTop = document.scrollingElement.scrollTop; |
| 5975 this._scrollLeft = document.scrollingElement.scrollLeft; |
| 5976 } else { |
| 5977 this._scrollTop = Math.max(document.documentElement.scrollTop, document.
body.scrollTop); |
| 5978 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen
t.body.scrollLeft); |
| 5979 } |
| 5980 }, |
| 5981 _restoreScrollPosition: function() { |
| 5982 if (document.scrollingElement) { |
| 5983 document.scrollingElement.scrollTop = this._scrollTop; |
| 5984 document.scrollingElement.scrollLeft = this._scrollLeft; |
| 5985 } else { |
| 5986 document.documentElement.scrollTop = this._scrollTop; |
| 5987 document.documentElement.scrollLeft = this._scrollLeft; |
| 5988 document.body.scrollTop = this._scrollTop; |
| 5989 document.body.scrollLeft = this._scrollLeft; |
| 5990 } |
| 5991 }, |
| 5992 _updateAnimationConfig: function() { |
| 5993 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati
onConfig || []); |
| 5994 for (var i = 0; i < animations.length; i++) { |
| 5995 animations[i].node = this.containedElement; |
| 5996 } |
| 5997 this.animationConfig = { |
| 5998 open: this.openAnimationConfig, |
| 5999 close: this.closeAnimationConfig |
| 9518 }; | 6000 }; |
| 9519 this._copyProperties(clone, config); | 6001 }, |
| 9520 return clone; | 6002 _updateOverlayPosition: function() { |
| 9521 }, | 6003 if (this.isAttached) { |
| 9522 | 6004 this.notifyResize(); |
| 9523 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 6005 } |
| 9524 if (!this.animationConfig) { | 6006 }, |
| 9525 return; | 6007 _applyFocus: function() { |
| 9526 } | 6008 var focusTarget = this.focusTarget || this.containedElement; |
| 9527 | 6009 if (focusTarget && this.opened && !this.noAutoFocus) { |
| 9528 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | 6010 focusTarget.focus(); |
| 9529 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | 6011 } else { |
| 9530 » return; | 6012 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
| 9531 } | 6013 } |
| 9532 | 6014 } |
| 9533 // type is optional | 6015 }); |
| 9534 var thisConfig; | 6016 })(); |
| 9535 if (type) { | 6017 |
| 9536 thisConfig = this.animationConfig[type]; | |
| 9537 } else { | |
| 9538 thisConfig = this.animationConfig; | |
| 9539 } | |
| 9540 | |
| 9541 if (!Array.isArray(thisConfig)) { | |
| 9542 thisConfig = [thisConfig]; | |
| 9543 } | |
| 9544 | |
| 9545 // iterate animations and recurse to process configurations from child nod
es | |
| 9546 if (thisConfig) { | |
| 9547 for (var config, index = 0; config = thisConfig[index]; index++) { | |
| 9548 if (config.animatable) { | |
| 9549 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | |
| 9550 } else { | |
| 9551 if (config.id) { | |
| 9552 var cachedConfig = map[config.id]; | |
| 9553 if (cachedConfig) { | |
| 9554 // merge configurations with the same id, making a clone lazily | |
| 9555 if (!cachedConfig.isClone) { | |
| 9556 map[config.id] = this._cloneConfig(cachedConfig) | |
| 9557 cachedConfig = map[config.id]; | |
| 9558 } | |
| 9559 this._copyProperties(cachedConfig, config); | |
| 9560 } else { | |
| 9561 // put any configs with an id into a map | |
| 9562 map[config.id] = config; | |
| 9563 } | |
| 9564 } else { | |
| 9565 allConfigs.push(config); | |
| 9566 } | |
| 9567 } | |
| 9568 } | |
| 9569 } | |
| 9570 }, | |
| 9571 | |
| 9572 /** | |
| 9573 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
| 9574 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
| 9575 * should define the property `animationConfig`, which is either a configura
tion object | |
| 9576 * or a map of animation type to array of configuration objects. | |
| 9577 */ | |
| 9578 getAnimationConfig: function(type) { | |
| 9579 var map = {}; | |
| 9580 var allConfigs = []; | |
| 9581 this._getAnimationConfigRecursive(type, map, allConfigs); | |
| 9582 // append the configurations saved in the map to the array | |
| 9583 for (var key in map) { | |
| 9584 allConfigs.push(map[key]); | |
| 9585 } | |
| 9586 return allConfigs; | |
| 9587 } | |
| 9588 | |
| 9589 }; | |
| 9590 /** | |
| 9591 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
| 9592 * | |
| 9593 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
| 9594 */ | |
| 9595 Polymer.NeonAnimationRunnerBehaviorImpl = { | |
| 9596 | |
| 9597 _configureAnimations: function(configs) { | |
| 9598 var results = []; | |
| 9599 if (configs.length > 0) { | |
| 9600 for (var config, index = 0; config = configs[index]; index++) { | |
| 9601 var neonAnimation = document.createElement(config.name); | |
| 9602 // is this element actually a neon animation? | |
| 9603 if (neonAnimation.isNeonAnimation) { | |
| 9604 var result = null; | |
| 9605 // configuration or play could fail if polyfills aren't loaded | |
| 9606 try { | |
| 9607 result = neonAnimation.configure(config); | |
| 9608 // Check if we have an Effect rather than an Animation | |
| 9609 if (typeof result.cancel != 'function') { | |
| 9610 result = document.timeline.play(result); | |
| 9611 } | |
| 9612 } catch (e) { | |
| 9613 result = null; | |
| 9614 console.warn('Couldnt play', '(', config.name, ').', e); | |
| 9615 } | |
| 9616 if (result) { | |
| 9617 results.push({ | |
| 9618 neonAnimation: neonAnimation, | |
| 9619 config: config, | |
| 9620 animation: result, | |
| 9621 }); | |
| 9622 } | |
| 9623 } else { | |
| 9624 console.warn(this.is + ':', config.name, 'not found!'); | |
| 9625 } | |
| 9626 } | |
| 9627 } | |
| 9628 return results; | |
| 9629 }, | |
| 9630 | |
| 9631 _shouldComplete: function(activeEntries) { | |
| 9632 var finished = true; | |
| 9633 for (var i = 0; i < activeEntries.length; i++) { | |
| 9634 if (activeEntries[i].animation.playState != 'finished') { | |
| 9635 finished = false; | |
| 9636 break; | |
| 9637 } | |
| 9638 } | |
| 9639 return finished; | |
| 9640 }, | |
| 9641 | |
| 9642 _complete: function(activeEntries) { | |
| 9643 for (var i = 0; i < activeEntries.length; i++) { | |
| 9644 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | |
| 9645 } | |
| 9646 for (var i = 0; i < activeEntries.length; i++) { | |
| 9647 activeEntries[i].animation.cancel(); | |
| 9648 } | |
| 9649 }, | |
| 9650 | |
| 9651 /** | |
| 9652 * Plays an animation with an optional `type`. | |
| 9653 * @param {string=} type | |
| 9654 * @param {!Object=} cookie | |
| 9655 */ | |
| 9656 playAnimation: function(type, cookie) { | |
| 9657 var configs = this.getAnimationConfig(type); | |
| 9658 if (!configs) { | |
| 9659 return; | |
| 9660 } | |
| 9661 this._active = this._active || {}; | |
| 9662 if (this._active[type]) { | |
| 9663 this._complete(this._active[type]); | |
| 9664 delete this._active[type]; | |
| 9665 } | |
| 9666 | |
| 9667 var activeEntries = this._configureAnimations(configs); | |
| 9668 | |
| 9669 if (activeEntries.length == 0) { | |
| 9670 this.fire('neon-animation-finish', cookie, {bubbles: false}); | |
| 9671 return; | |
| 9672 } | |
| 9673 | |
| 9674 this._active[type] = activeEntries; | |
| 9675 | |
| 9676 for (var i = 0; i < activeEntries.length; i++) { | |
| 9677 activeEntries[i].animation.onfinish = function() { | |
| 9678 if (this._shouldComplete(activeEntries)) { | |
| 9679 this._complete(activeEntries); | |
| 9680 delete this._active[type]; | |
| 9681 this.fire('neon-animation-finish', cookie, {bubbles: false}); | |
| 9682 } | |
| 9683 }.bind(this); | |
| 9684 } | |
| 9685 }, | |
| 9686 | |
| 9687 /** | |
| 9688 * Cancels the currently running animations. | |
| 9689 */ | |
| 9690 cancelAnimation: function() { | |
| 9691 for (var k in this._animations) { | |
| 9692 this._animations[k].cancel(); | |
| 9693 } | |
| 9694 this._animations = {}; | |
| 9695 } | |
| 9696 }; | |
| 9697 | |
| 9698 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | |
| 9699 Polymer.NeonAnimationRunnerBehavior = [ | |
| 9700 Polymer.NeonAnimatableBehavior, | |
| 9701 Polymer.NeonAnimationRunnerBehaviorImpl | |
| 9702 ]; | |
| 9703 /** | |
| 9704 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
| 9705 * @polymerBehavior | |
| 9706 */ | |
| 9707 Polymer.NeonAnimationBehavior = { | |
| 9708 | |
| 9709 properties: { | |
| 9710 | |
| 9711 /** | |
| 9712 * Defines the animation timing. | |
| 9713 */ | |
| 9714 animationTiming: { | |
| 9715 type: Object, | |
| 9716 value: function() { | |
| 9717 return { | |
| 9718 duration: 500, | |
| 9719 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | |
| 9720 fill: 'both' | |
| 9721 } | |
| 9722 } | |
| 9723 } | |
| 9724 | |
| 9725 }, | |
| 9726 | |
| 9727 /** | |
| 9728 * Can be used to determine that elements implement this behavior. | |
| 9729 */ | |
| 9730 isNeonAnimation: true, | |
| 9731 | |
| 9732 /** | |
| 9733 * Do any animation configuration here. | |
| 9734 */ | |
| 9735 // configure: function(config) { | |
| 9736 // }, | |
| 9737 | |
| 9738 /** | |
| 9739 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | |
| 9740 * by the animation. | |
| 9741 */ | |
| 9742 timingFromConfig: function(config) { | |
| 9743 if (config.timing) { | |
| 9744 for (var property in config.timing) { | |
| 9745 this.animationTiming[property] = config.timing[property]; | |
| 9746 } | |
| 9747 } | |
| 9748 return this.animationTiming; | |
| 9749 }, | |
| 9750 | |
| 9751 /** | |
| 9752 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | |
| 9753 */ | |
| 9754 setPrefixedProperty: function(node, property, value) { | |
| 9755 var map = { | |
| 9756 'transform': ['webkitTransform'], | |
| 9757 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | |
| 9758 }; | |
| 9759 var prefixes = map[property]; | |
| 9760 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | |
| 9761 node.style[prefix] = value; | |
| 9762 } | |
| 9763 node.style[property] = value; | |
| 9764 }, | |
| 9765 | |
| 9766 /** | |
| 9767 * Called when the animation finishes. | |
| 9768 */ | |
| 9769 complete: function() {} | |
| 9770 | |
| 9771 }; | |
| 9772 Polymer({ | 6018 Polymer({ |
| 9773 | 6019 is: 'fade-in-animation', |
| 9774 is: 'opaque-animation', | 6020 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 9775 | 6021 configure: function(config) { |
| 9776 behaviors: [ | 6022 var node = config.node; |
| 9777 Polymer.NeonAnimationBehavior | 6023 this._effect = new KeyframeEffect(node, [ { |
| 9778 ], | 6024 opacity: '0' |
| 9779 | 6025 }, { |
| 9780 configure: function(config) { | 6026 opacity: '1' |
| 9781 var node = config.node; | 6027 } ], this.timingFromConfig(config)); |
| 9782 this._effect = new KeyframeEffect(node, [ | 6028 return this._effect; |
| 9783 {'opacity': '1'}, | 6029 } |
| 9784 {'opacity': '1'} | 6030 }); |
| 9785 ], this.timingFromConfig(config)); | 6031 |
| 9786 node.style.opacity = '0'; | |
| 9787 return this._effect; | |
| 9788 }, | |
| 9789 | |
| 9790 complete: function(config) { | |
| 9791 config.node.style.opacity = ''; | |
| 9792 } | |
| 9793 | |
| 9794 }); | |
| 9795 (function() { | |
| 9796 'use strict'; | |
| 9797 // Used to calculate the scroll direction during touch events. | |
| 9798 var LAST_TOUCH_POSITION = { | |
| 9799 pageX: 0, | |
| 9800 pageY: 0 | |
| 9801 }; | |
| 9802 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
| 9803 var ROOT_TARGET = null; | |
| 9804 var SCROLLABLE_NODES = []; | |
| 9805 | |
| 9806 /** | |
| 9807 * The IronDropdownScrollManager is intended to provide a central source | |
| 9808 * of authority and control over which elements in a document are currently | |
| 9809 * allowed to scroll. | |
| 9810 */ | |
| 9811 | |
| 9812 Polymer.IronDropdownScrollManager = { | |
| 9813 | |
| 9814 /** | |
| 9815 * The current element that defines the DOM boundaries of the | |
| 9816 * scroll lock. This is always the most recently locking element. | |
| 9817 */ | |
| 9818 get currentLockingElement() { | |
| 9819 return this._lockingElements[this._lockingElements.length - 1]; | |
| 9820 }, | |
| 9821 | |
| 9822 /** | |
| 9823 * Returns true if the provided element is "scroll locked", which is to | |
| 9824 * say that it cannot be scrolled via pointer or keyboard interactions. | |
| 9825 * | |
| 9826 * @param {HTMLElement} element An HTML element instance which may or may | |
| 9827 * not be scroll locked. | |
| 9828 */ | |
| 9829 elementIsScrollLocked: function(element) { | |
| 9830 var currentLockingElement = this.currentLockingElement; | |
| 9831 | |
| 9832 if (currentLockingElement === undefined) | |
| 9833 return false; | |
| 9834 | |
| 9835 var scrollLocked; | |
| 9836 | |
| 9837 if (this._hasCachedLockedElement(element)) { | |
| 9838 return true; | |
| 9839 } | |
| 9840 | |
| 9841 if (this._hasCachedUnlockedElement(element)) { | |
| 9842 return false; | |
| 9843 } | |
| 9844 | |
| 9845 scrollLocked = !!currentLockingElement && | |
| 9846 currentLockingElement !== element && | |
| 9847 !this._composedTreeContains(currentLockingElement, element); | |
| 9848 | |
| 9849 if (scrollLocked) { | |
| 9850 this._lockedElementCache.push(element); | |
| 9851 } else { | |
| 9852 this._unlockedElementCache.push(element); | |
| 9853 } | |
| 9854 | |
| 9855 return scrollLocked; | |
| 9856 }, | |
| 9857 | |
| 9858 /** | |
| 9859 * Push an element onto the current scroll lock stack. The most recently | |
| 9860 * pushed element and its children will be considered scrollable. All | |
| 9861 * other elements will not be scrollable. | |
| 9862 * | |
| 9863 * Scroll locking is implemented as a stack so that cases such as | |
| 9864 * dropdowns within dropdowns are handled well. | |
| 9865 * | |
| 9866 * @param {HTMLElement} element The element that should lock scroll. | |
| 9867 */ | |
| 9868 pushScrollLock: function(element) { | |
| 9869 // Prevent pushing the same element twice | |
| 9870 if (this._lockingElements.indexOf(element) >= 0) { | |
| 9871 return; | |
| 9872 } | |
| 9873 | |
| 9874 if (this._lockingElements.length === 0) { | |
| 9875 this._lockScrollInteractions(); | |
| 9876 } | |
| 9877 | |
| 9878 this._lockingElements.push(element); | |
| 9879 | |
| 9880 this._lockedElementCache = []; | |
| 9881 this._unlockedElementCache = []; | |
| 9882 }, | |
| 9883 | |
| 9884 /** | |
| 9885 * Remove an element from the scroll lock stack. The element being | |
| 9886 * removed does not need to be the most recently pushed element. However, | |
| 9887 * the scroll lock constraints only change when the most recently pushed | |
| 9888 * element is removed. | |
| 9889 * | |
| 9890 * @param {HTMLElement} element The element to remove from the scroll | |
| 9891 * lock stack. | |
| 9892 */ | |
| 9893 removeScrollLock: function(element) { | |
| 9894 var index = this._lockingElements.indexOf(element); | |
| 9895 | |
| 9896 if (index === -1) { | |
| 9897 return; | |
| 9898 } | |
| 9899 | |
| 9900 this._lockingElements.splice(index, 1); | |
| 9901 | |
| 9902 this._lockedElementCache = []; | |
| 9903 this._unlockedElementCache = []; | |
| 9904 | |
| 9905 if (this._lockingElements.length === 0) { | |
| 9906 this._unlockScrollInteractions(); | |
| 9907 } | |
| 9908 }, | |
| 9909 | |
| 9910 _lockingElements: [], | |
| 9911 | |
| 9912 _lockedElementCache: null, | |
| 9913 | |
| 9914 _unlockedElementCache: null, | |
| 9915 | |
| 9916 _hasCachedLockedElement: function(element) { | |
| 9917 return this._lockedElementCache.indexOf(element) > -1; | |
| 9918 }, | |
| 9919 | |
| 9920 _hasCachedUnlockedElement: function(element) { | |
| 9921 return this._unlockedElementCache.indexOf(element) > -1; | |
| 9922 }, | |
| 9923 | |
| 9924 _composedTreeContains: function(element, child) { | |
| 9925 // NOTE(cdata): This method iterates over content elements and their | |
| 9926 // corresponding distributed nodes to implement a contains-like method | |
| 9927 // that pierces through the composed tree of the ShadowDOM. Results of | |
| 9928 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
| 9929 // guard against potentially expensive lookups happening repeatedly as | |
| 9930 // a user scrolls / touchmoves. | |
| 9931 var contentElements; | |
| 9932 var distributedNodes; | |
| 9933 var contentIndex; | |
| 9934 var nodeIndex; | |
| 9935 | |
| 9936 if (element.contains(child)) { | |
| 9937 return true; | |
| 9938 } | |
| 9939 | |
| 9940 contentElements = Polymer.dom(element).querySelectorAll('content'); | |
| 9941 | |
| 9942 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | |
| 9943 | |
| 9944 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | |
| 9945 | |
| 9946 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | |
| 9947 | |
| 9948 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | |
| 9949 return true; | |
| 9950 } | |
| 9951 } | |
| 9952 } | |
| 9953 | |
| 9954 return false; | |
| 9955 }, | |
| 9956 | |
| 9957 _scrollInteractionHandler: function(event) { | |
| 9958 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
| 9959 // progress and cannot be interrupted. | |
| 9960 if (event.cancelable && this._shouldPreventScrolling(event)) { | |
| 9961 event.preventDefault(); | |
| 9962 } | |
| 9963 // If event has targetTouches (touch event), update last touch position. | |
| 9964 if (event.targetTouches) { | |
| 9965 var touch = event.targetTouches[0]; | |
| 9966 LAST_TOUCH_POSITION.pageX = touch.pageX; | |
| 9967 LAST_TOUCH_POSITION.pageY = touch.pageY; | |
| 9968 } | |
| 9969 }, | |
| 9970 | |
| 9971 _lockScrollInteractions: function() { | |
| 9972 this._boundScrollHandler = this._boundScrollHandler || | |
| 9973 this._scrollInteractionHandler.bind(this); | |
| 9974 // Modern `wheel` event for mouse wheel scrolling: | |
| 9975 document.addEventListener('wheel', this._boundScrollHandler, true); | |
| 9976 // Older, non-standard `mousewheel` event for some FF: | |
| 9977 document.addEventListener('mousewheel', this._boundScrollHandler, true); | |
| 9978 // IE: | |
| 9979 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | |
| 9980 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
| 9981 document.addEventListener('touchstart', this._boundScrollHandler, true); | |
| 9982 // Mobile devices can scroll on touch move: | |
| 9983 document.addEventListener('touchmove', this._boundScrollHandler, true); | |
| 9984 }, | |
| 9985 | |
| 9986 _unlockScrollInteractions: function() { | |
| 9987 document.removeEventListener('wheel', this._boundScrollHandler, true); | |
| 9988 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | |
| 9989 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | |
| 9990 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | |
| 9991 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | |
| 9992 }, | |
| 9993 | |
| 9994 /** | |
| 9995 * Returns true if the event causes scroll outside the current locking | |
| 9996 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
| 9997 * outside the locking element when it is already at its scroll boundaries
. | |
| 9998 * @param {!Event} event | |
| 9999 * @return {boolean} | |
| 10000 * @private | |
| 10001 */ | |
| 10002 _shouldPreventScrolling: function(event) { | |
| 10003 | |
| 10004 // Update if root target changed. For touch events, ensure we don't | |
| 10005 // update during touchmove. | |
| 10006 var target = Polymer.dom(event).rootTarget; | |
| 10007 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | |
| 10008 ROOT_TARGET = target; | |
| 10009 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | |
| 10010 } | |
| 10011 | |
| 10012 // Prevent event if no scrollable nodes. | |
| 10013 if (!SCROLLABLE_NODES.length) { | |
| 10014 return true; | |
| 10015 } | |
| 10016 // Don't prevent touchstart event inside the locking element when it has | |
| 10017 // scrollable nodes. | |
| 10018 if (event.type === 'touchstart') { | |
| 10019 return false; | |
| 10020 } | |
| 10021 // Get deltaX/Y. | |
| 10022 var info = this._getScrollInfo(event); | |
| 10023 // Prevent if there is no child that can scroll. | |
| 10024 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | |
| 10025 }, | |
| 10026 | |
| 10027 /** | |
| 10028 * Returns an array of scrollable nodes up to the current locking element, | |
| 10029 * which is included too if scrollable. | |
| 10030 * @param {!Array<Node>} nodes | |
| 10031 * @return {Array<Node>} scrollables | |
| 10032 * @private | |
| 10033 */ | |
| 10034 _getScrollableNodes: function(nodes) { | |
| 10035 var scrollables = []; | |
| 10036 var lockingIndex = nodes.indexOf(this.currentLockingElement); | |
| 10037 // Loop from root target to locking element (included). | |
| 10038 for (var i = 0; i <= lockingIndex; i++) { | |
| 10039 var node = nodes[i]; | |
| 10040 // Skip document fragments. | |
| 10041 if (node.nodeType === 11) { | |
| 10042 continue; | |
| 10043 } | |
| 10044 // Check inline style before checking computed style. | |
| 10045 var style = node.style; | |
| 10046 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | |
| 10047 style = window.getComputedStyle(node); | |
| 10048 } | |
| 10049 if (style.overflow === 'scroll' || style.overflow === 'auto') { | |
| 10050 scrollables.push(node); | |
| 10051 } | |
| 10052 } | |
| 10053 return scrollables; | |
| 10054 }, | |
| 10055 | |
| 10056 /** | |
| 10057 * Returns the node that is scrolling. If there is no scrolling, | |
| 10058 * returns undefined. | |
| 10059 * @param {!Array<Node>} nodes | |
| 10060 * @param {number} deltaX Scroll delta on the x-axis | |
| 10061 * @param {number} deltaY Scroll delta on the y-axis | |
| 10062 * @return {Node|undefined} | |
| 10063 * @private | |
| 10064 */ | |
| 10065 _getScrollingNode: function(nodes, deltaX, deltaY) { | |
| 10066 // No scroll. | |
| 10067 if (!deltaX && !deltaY) { | |
| 10068 return; | |
| 10069 } | |
| 10070 // Check only one axis according to where there is more scroll. | |
| 10071 // Prefer vertical to horizontal. | |
| 10072 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | |
| 10073 for (var i = 0; i < nodes.length; i++) { | |
| 10074 var node = nodes[i]; | |
| 10075 var canScroll = false; | |
| 10076 if (verticalScroll) { | |
| 10077 // delta < 0 is scroll up, delta > 0 is scroll down. | |
| 10078 canScroll = deltaY < 0 ? node.scrollTop > 0 : | |
| 10079 node.scrollTop < node.scrollHeight - node.clientHeight; | |
| 10080 } else { | |
| 10081 // delta < 0 is scroll left, delta > 0 is scroll right. | |
| 10082 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | |
| 10083 node.scrollLeft < node.scrollWidth - node.clientWidth; | |
| 10084 } | |
| 10085 if (canScroll) { | |
| 10086 return node; | |
| 10087 } | |
| 10088 } | |
| 10089 }, | |
| 10090 | |
| 10091 /** | |
| 10092 * Returns scroll `deltaX` and `deltaY`. | |
| 10093 * @param {!Event} event The scroll event | |
| 10094 * @return {{ | |
| 10095 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
| 10096 * negative: scroll left, 0: no scroll), | |
| 10097 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
| 10098 * negative: scroll up, 0: no scroll) | |
| 10099 * }} info | |
| 10100 * @private | |
| 10101 */ | |
| 10102 _getScrollInfo: function(event) { | |
| 10103 var info = { | |
| 10104 deltaX: event.deltaX, | |
| 10105 deltaY: event.deltaY | |
| 10106 }; | |
| 10107 // Already available. | |
| 10108 if ('deltaX' in event) { | |
| 10109 // do nothing, values are already good. | |
| 10110 } | |
| 10111 // Safari has scroll info in `wheelDeltaX/Y`. | |
| 10112 else if ('wheelDeltaX' in event) { | |
| 10113 info.deltaX = -event.wheelDeltaX; | |
| 10114 info.deltaY = -event.wheelDeltaY; | |
| 10115 } | |
| 10116 // Firefox has scroll info in `detail` and `axis`. | |
| 10117 else if ('axis' in event) { | |
| 10118 info.deltaX = event.axis === 1 ? event.detail : 0; | |
| 10119 info.deltaY = event.axis === 2 ? event.detail : 0; | |
| 10120 } | |
| 10121 // On mobile devices, calculate scroll direction. | |
| 10122 else if (event.targetTouches) { | |
| 10123 var touch = event.targetTouches[0]; | |
| 10124 // Touch moves from right to left => scrolling goes right. | |
| 10125 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | |
| 10126 // Touch moves from down to up => scrolling goes down. | |
| 10127 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | |
| 10128 } | |
| 10129 return info; | |
| 10130 } | |
| 10131 }; | |
| 10132 })(); | |
| 10133 (function() { | |
| 10134 'use strict'; | |
| 10135 | |
| 10136 Polymer({ | |
| 10137 is: 'iron-dropdown', | |
| 10138 | |
| 10139 behaviors: [ | |
| 10140 Polymer.IronControlState, | |
| 10141 Polymer.IronA11yKeysBehavior, | |
| 10142 Polymer.IronOverlayBehavior, | |
| 10143 Polymer.NeonAnimationRunnerBehavior | |
| 10144 ], | |
| 10145 | |
| 10146 properties: { | |
| 10147 /** | |
| 10148 * The orientation against which to align the dropdown content | |
| 10149 * horizontally relative to the dropdown trigger. | |
| 10150 * Overridden from `Polymer.IronFitBehavior`. | |
| 10151 */ | |
| 10152 horizontalAlign: { | |
| 10153 type: String, | |
| 10154 value: 'left', | |
| 10155 reflectToAttribute: true | |
| 10156 }, | |
| 10157 | |
| 10158 /** | |
| 10159 * The orientation against which to align the dropdown content | |
| 10160 * vertically relative to the dropdown trigger. | |
| 10161 * Overridden from `Polymer.IronFitBehavior`. | |
| 10162 */ | |
| 10163 verticalAlign: { | |
| 10164 type: String, | |
| 10165 value: 'top', | |
| 10166 reflectToAttribute: true | |
| 10167 }, | |
| 10168 | |
| 10169 /** | |
| 10170 * An animation config. If provided, this will be used to animate the | |
| 10171 * opening of the dropdown. | |
| 10172 */ | |
| 10173 openAnimationConfig: { | |
| 10174 type: Object | |
| 10175 }, | |
| 10176 | |
| 10177 /** | |
| 10178 * An animation config. If provided, this will be used to animate the | |
| 10179 * closing of the dropdown. | |
| 10180 */ | |
| 10181 closeAnimationConfig: { | |
| 10182 type: Object | |
| 10183 }, | |
| 10184 | |
| 10185 /** | |
| 10186 * If provided, this will be the element that will be focused when | |
| 10187 * the dropdown opens. | |
| 10188 */ | |
| 10189 focusTarget: { | |
| 10190 type: Object | |
| 10191 }, | |
| 10192 | |
| 10193 /** | |
| 10194 * Set to true to disable animations when opening and closing the | |
| 10195 * dropdown. | |
| 10196 */ | |
| 10197 noAnimations: { | |
| 10198 type: Boolean, | |
| 10199 value: false | |
| 10200 }, | |
| 10201 | |
| 10202 /** | |
| 10203 * By default, the dropdown will constrain scrolling on the page | |
| 10204 * to itself when opened. | |
| 10205 * Set to true in order to prevent scroll from being constrained | |
| 10206 * to the dropdown when it opens. | |
| 10207 */ | |
| 10208 allowOutsideScroll: { | |
| 10209 type: Boolean, | |
| 10210 value: false | |
| 10211 }, | |
| 10212 | |
| 10213 /** | |
| 10214 * Callback for scroll events. | |
| 10215 * @type {Function} | |
| 10216 * @private | |
| 10217 */ | |
| 10218 _boundOnCaptureScroll: { | |
| 10219 type: Function, | |
| 10220 value: function() { | |
| 10221 return this._onCaptureScroll.bind(this); | |
| 10222 } | |
| 10223 } | |
| 10224 }, | |
| 10225 | |
| 10226 listeners: { | |
| 10227 'neon-animation-finish': '_onNeonAnimationFinish' | |
| 10228 }, | |
| 10229 | |
| 10230 observers: [ | |
| 10231 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | |
| 10232 ], | |
| 10233 | |
| 10234 /** | |
| 10235 * The element that is contained by the dropdown, if any. | |
| 10236 */ | |
| 10237 get containedElement() { | |
| 10238 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
| 10239 }, | |
| 10240 | |
| 10241 /** | |
| 10242 * The element that should be focused when the dropdown opens. | |
| 10243 * @deprecated | |
| 10244 */ | |
| 10245 get _focusTarget() { | |
| 10246 return this.focusTarget || this.containedElement; | |
| 10247 }, | |
| 10248 | |
| 10249 ready: function() { | |
| 10250 // Memoized scrolling position, used to block scrolling outside. | |
| 10251 this._scrollTop = 0; | |
| 10252 this._scrollLeft = 0; | |
| 10253 // Used to perform a non-blocking refit on scroll. | |
| 10254 this._refitOnScrollRAF = null; | |
| 10255 }, | |
| 10256 | |
| 10257 detached: function() { | |
| 10258 this.cancelAnimation(); | |
| 10259 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
| 10260 }, | |
| 10261 | |
| 10262 /** | |
| 10263 * Called when the value of `opened` changes. | |
| 10264 * Overridden from `IronOverlayBehavior` | |
| 10265 */ | |
| 10266 _openedChanged: function() { | |
| 10267 if (this.opened && this.disabled) { | |
| 10268 this.cancel(); | |
| 10269 } else { | |
| 10270 this.cancelAnimation(); | |
| 10271 this.sizingTarget = this.containedElement || this.sizingTarget; | |
| 10272 this._updateAnimationConfig(); | |
| 10273 this._saveScrollPosition(); | |
| 10274 if (this.opened) { | |
| 10275 document.addEventListener('scroll', this._boundOnCaptureScroll); | |
| 10276 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | |
| 10277 } else { | |
| 10278 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | |
| 10279 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
| 10280 } | |
| 10281 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | |
| 10282 } | |
| 10283 }, | |
| 10284 | |
| 10285 /** | |
| 10286 * Overridden from `IronOverlayBehavior`. | |
| 10287 */ | |
| 10288 _renderOpened: function() { | |
| 10289 if (!this.noAnimations && this.animationConfig.open) { | |
| 10290 this.$.contentWrapper.classList.add('animating'); | |
| 10291 this.playAnimation('open'); | |
| 10292 } else { | |
| 10293 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | |
| 10294 } | |
| 10295 }, | |
| 10296 | |
| 10297 /** | |
| 10298 * Overridden from `IronOverlayBehavior`. | |
| 10299 */ | |
| 10300 _renderClosed: function() { | |
| 10301 | |
| 10302 if (!this.noAnimations && this.animationConfig.close) { | |
| 10303 this.$.contentWrapper.classList.add('animating'); | |
| 10304 this.playAnimation('close'); | |
| 10305 } else { | |
| 10306 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | |
| 10307 } | |
| 10308 }, | |
| 10309 | |
| 10310 /** | |
| 10311 * Called when animation finishes on the dropdown (when opening or | |
| 10312 * closing). Responsible for "completing" the process of opening or | |
| 10313 * closing the dropdown by positioning it or setting its display to | |
| 10314 * none. | |
| 10315 */ | |
| 10316 _onNeonAnimationFinish: function() { | |
| 10317 this.$.contentWrapper.classList.remove('animating'); | |
| 10318 if (this.opened) { | |
| 10319 this._finishRenderOpened(); | |
| 10320 } else { | |
| 10321 this._finishRenderClosed(); | |
| 10322 } | |
| 10323 }, | |
| 10324 | |
| 10325 _onCaptureScroll: function() { | |
| 10326 if (!this.allowOutsideScroll) { | |
| 10327 this._restoreScrollPosition(); | |
| 10328 } else { | |
| 10329 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | |
| 10330 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | |
| 10331 } | |
| 10332 }, | |
| 10333 | |
| 10334 /** | |
| 10335 * Memoizes the scroll position of the outside scrolling element. | |
| 10336 * @private | |
| 10337 */ | |
| 10338 _saveScrollPosition: function() { | |
| 10339 if (document.scrollingElement) { | |
| 10340 this._scrollTop = document.scrollingElement.scrollTop; | |
| 10341 this._scrollLeft = document.scrollingElement.scrollLeft; | |
| 10342 } else { | |
| 10343 // Since we don't know if is the body or html, get max. | |
| 10344 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | |
| 10345 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | |
| 10346 } | |
| 10347 }, | |
| 10348 | |
| 10349 /** | |
| 10350 * Resets the scroll position of the outside scrolling element. | |
| 10351 * @private | |
| 10352 */ | |
| 10353 _restoreScrollPosition: function() { | |
| 10354 if (document.scrollingElement) { | |
| 10355 document.scrollingElement.scrollTop = this._scrollTop; | |
| 10356 document.scrollingElement.scrollLeft = this._scrollLeft; | |
| 10357 } else { | |
| 10358 // Since we don't know if is the body or html, set both. | |
| 10359 document.documentElement.scrollTop = this._scrollTop; | |
| 10360 document.documentElement.scrollLeft = this._scrollLeft; | |
| 10361 document.body.scrollTop = this._scrollTop; | |
| 10362 document.body.scrollLeft = this._scrollLeft; | |
| 10363 } | |
| 10364 }, | |
| 10365 | |
| 10366 /** | |
| 10367 * Constructs the final animation config from different properties used | |
| 10368 * to configure specific parts of the opening and closing animations. | |
| 10369 */ | |
| 10370 _updateAnimationConfig: function() { | |
| 10371 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | |
| 10372 for (var i = 0; i < animations.length; i++) { | |
| 10373 animations[i].node = this.containedElement; | |
| 10374 } | |
| 10375 this.animationConfig = { | |
| 10376 open: this.openAnimationConfig, | |
| 10377 close: this.closeAnimationConfig | |
| 10378 }; | |
| 10379 }, | |
| 10380 | |
| 10381 /** | |
| 10382 * Updates the overlay position based on configured horizontal | |
| 10383 * and vertical alignment. | |
| 10384 */ | |
| 10385 _updateOverlayPosition: function() { | |
| 10386 if (this.isAttached) { | |
| 10387 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
| 10388 this.notifyResize(); | |
| 10389 } | |
| 10390 }, | |
| 10391 | |
| 10392 /** | |
| 10393 * Apply focus to focusTarget or containedElement | |
| 10394 */ | |
| 10395 _applyFocus: function () { | |
| 10396 var focusTarget = this.focusTarget || this.containedElement; | |
| 10397 if (focusTarget && this.opened && !this.noAutoFocus) { | |
| 10398 focusTarget.focus(); | |
| 10399 } else { | |
| 10400 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | |
| 10401 } | |
| 10402 } | |
| 10403 }); | |
| 10404 })(); | |
| 10405 Polymer({ | 6032 Polymer({ |
| 10406 | 6033 is: 'fade-out-animation', |
| 10407 is: 'fade-in-animation', | 6034 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 10408 | 6035 configure: function(config) { |
| 10409 behaviors: [ | 6036 var node = config.node; |
| 10410 Polymer.NeonAnimationBehavior | 6037 this._effect = new KeyframeEffect(node, [ { |
| 10411 ], | 6038 opacity: '1' |
| 10412 | 6039 }, { |
| 10413 configure: function(config) { | 6040 opacity: '0' |
| 10414 var node = config.node; | 6041 } ], this.timingFromConfig(config)); |
| 10415 this._effect = new KeyframeEffect(node, [ | 6042 return this._effect; |
| 10416 {'opacity': '0'}, | 6043 } |
| 10417 {'opacity': '1'} | 6044 }); |
| 10418 ], this.timingFromConfig(config)); | 6045 |
| 10419 return this._effect; | |
| 10420 } | |
| 10421 | |
| 10422 }); | |
| 10423 Polymer({ | 6046 Polymer({ |
| 10424 | 6047 is: 'paper-menu-grow-height-animation', |
| 10425 is: 'fade-out-animation', | 6048 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 10426 | 6049 configure: function(config) { |
| 10427 behaviors: [ | 6050 var node = config.node; |
| 10428 Polymer.NeonAnimationBehavior | 6051 var rect = node.getBoundingClientRect(); |
| 10429 ], | 6052 var height = rect.height; |
| 10430 | 6053 this._effect = new KeyframeEffect(node, [ { |
| 10431 configure: function(config) { | 6054 height: height / 2 + 'px' |
| 10432 var node = config.node; | 6055 }, { |
| 10433 this._effect = new KeyframeEffect(node, [ | 6056 height: height + 'px' |
| 10434 {'opacity': '1'}, | 6057 } ], this.timingFromConfig(config)); |
| 10435 {'opacity': '0'} | 6058 return this._effect; |
| 10436 ], this.timingFromConfig(config)); | 6059 } |
| 10437 return this._effect; | 6060 }); |
| 10438 } | 6061 |
| 10439 | |
| 10440 }); | |
| 10441 Polymer({ | 6062 Polymer({ |
| 10442 is: 'paper-menu-grow-height-animation', | 6063 is: 'paper-menu-grow-width-animation', |
| 10443 | 6064 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 10444 behaviors: [ | 6065 configure: function(config) { |
| 10445 Polymer.NeonAnimationBehavior | 6066 var node = config.node; |
| 10446 ], | 6067 var rect = node.getBoundingClientRect(); |
| 10447 | 6068 var width = rect.width; |
| 10448 configure: function(config) { | 6069 this._effect = new KeyframeEffect(node, [ { |
| 10449 var node = config.node; | 6070 width: width / 2 + 'px' |
| 10450 var rect = node.getBoundingClientRect(); | 6071 }, { |
| 10451 var height = rect.height; | 6072 width: width + 'px' |
| 10452 | 6073 } ], this.timingFromConfig(config)); |
| 10453 this._effect = new KeyframeEffect(node, [{ | 6074 return this._effect; |
| 10454 height: (height / 2) + 'px' | 6075 } |
| 10455 }, { | 6076 }); |
| 10456 height: height + 'px' | 6077 |
| 10457 }], this.timingFromConfig(config)); | 6078 Polymer({ |
| 10458 | 6079 is: 'paper-menu-shrink-width-animation', |
| 10459 return this._effect; | 6080 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 10460 } | 6081 configure: function(config) { |
| 10461 }); | 6082 var node = config.node; |
| 10462 | 6083 var rect = node.getBoundingClientRect(); |
| 10463 Polymer({ | 6084 var width = rect.width; |
| 10464 is: 'paper-menu-grow-width-animation', | 6085 this._effect = new KeyframeEffect(node, [ { |
| 10465 | 6086 width: width + 'px' |
| 10466 behaviors: [ | 6087 }, { |
| 10467 Polymer.NeonAnimationBehavior | 6088 width: width - width / 20 + 'px' |
| 10468 ], | 6089 } ], this.timingFromConfig(config)); |
| 10469 | 6090 return this._effect; |
| 10470 configure: function(config) { | 6091 } |
| 10471 var node = config.node; | 6092 }); |
| 10472 var rect = node.getBoundingClientRect(); | 6093 |
| 10473 var width = rect.width; | 6094 Polymer({ |
| 10474 | 6095 is: 'paper-menu-shrink-height-animation', |
| 10475 this._effect = new KeyframeEffect(node, [{ | 6096 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 10476 width: (width / 2) + 'px' | 6097 configure: function(config) { |
| 10477 }, { | 6098 var node = config.node; |
| 10478 width: width + 'px' | 6099 var rect = node.getBoundingClientRect(); |
| 10479 }], this.timingFromConfig(config)); | 6100 var height = rect.height; |
| 10480 | 6101 var top = rect.top; |
| 10481 return this._effect; | 6102 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
| 10482 } | 6103 this._effect = new KeyframeEffect(node, [ { |
| 10483 }); | 6104 height: height + 'px', |
| 10484 | 6105 transform: 'translateY(0)' |
| 10485 Polymer({ | 6106 }, { |
| 10486 is: 'paper-menu-shrink-width-animation', | 6107 height: height / 2 + 'px', |
| 10487 | 6108 transform: 'translateY(-20px)' |
| 10488 behaviors: [ | 6109 } ], this.timingFromConfig(config)); |
| 10489 Polymer.NeonAnimationBehavior | 6110 return this._effect; |
| 10490 ], | 6111 } |
| 10491 | 6112 }); |
| 10492 configure: function(config) { | 6113 |
| 10493 var node = config.node; | |
| 10494 var rect = node.getBoundingClientRect(); | |
| 10495 var width = rect.width; | |
| 10496 | |
| 10497 this._effect = new KeyframeEffect(node, [{ | |
| 10498 width: width + 'px' | |
| 10499 }, { | |
| 10500 width: width - (width / 20) + 'px' | |
| 10501 }], this.timingFromConfig(config)); | |
| 10502 | |
| 10503 return this._effect; | |
| 10504 } | |
| 10505 }); | |
| 10506 | |
| 10507 Polymer({ | |
| 10508 is: 'paper-menu-shrink-height-animation', | |
| 10509 | |
| 10510 behaviors: [ | |
| 10511 Polymer.NeonAnimationBehavior | |
| 10512 ], | |
| 10513 | |
| 10514 configure: function(config) { | |
| 10515 var node = config.node; | |
| 10516 var rect = node.getBoundingClientRect(); | |
| 10517 var height = rect.height; | |
| 10518 var top = rect.top; | |
| 10519 | |
| 10520 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); | |
| 10521 | |
| 10522 this._effect = new KeyframeEffect(node, [{ | |
| 10523 height: height + 'px', | |
| 10524 transform: 'translateY(0)' | |
| 10525 }, { | |
| 10526 height: height / 2 + 'px', | |
| 10527 transform: 'translateY(-20px)' | |
| 10528 }], this.timingFromConfig(config)); | |
| 10529 | |
| 10530 return this._effect; | |
| 10531 } | |
| 10532 }); | |
| 10533 // Copyright 2016 The Chromium Authors. All rights reserved. | 6114 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 10534 // Use of this source code is governed by a BSD-style license that can be | 6115 // Use of this source code is governed by a BSD-style license that can be |
| 10535 // found in the LICENSE file. | 6116 // found in the LICENSE file. |
| 10536 | |
| 10537 /** Same as paper-menu-button's custom easing cubic-bezier param. */ | |
| 10538 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; | 6117 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
| 10539 | 6118 |
| 10540 Polymer({ | 6119 Polymer({ |
| 10541 is: 'cr-shared-menu', | 6120 is: 'cr-shared-menu', |
| 10542 | 6121 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 10543 behaviors: [Polymer.IronA11yKeysBehavior], | |
| 10544 | |
| 10545 properties: { | 6122 properties: { |
| 10546 menuOpen: { | 6123 menuOpen: { |
| 10547 type: Boolean, | 6124 type: Boolean, |
| 10548 observer: 'menuOpenChanged_', | 6125 observer: 'menuOpenChanged_', |
| 10549 value: false, | 6126 value: false, |
| 10550 notify: true, | 6127 notify: true |
| 10551 }, | 6128 }, |
| 10552 | |
| 10553 /** | |
| 10554 * The contextual item that this menu was clicked for. | |
| 10555 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> | |
| 10556 * @type {?Object} | |
| 10557 */ | |
| 10558 itemData: { | 6129 itemData: { |
| 10559 type: Object, | 6130 type: Object, |
| 10560 value: null, | 6131 value: null |
| 10561 }, | 6132 }, |
| 10562 | |
| 10563 /** @override */ | |
| 10564 keyEventTarget: { | 6133 keyEventTarget: { |
| 10565 type: Object, | 6134 type: Object, |
| 10566 value: function() { | 6135 value: function() { |
| 10567 return this.$.menu; | 6136 return this.$.menu; |
| 10568 } | 6137 } |
| 10569 }, | 6138 }, |
| 10570 | |
| 10571 openAnimationConfig: { | 6139 openAnimationConfig: { |
| 10572 type: Object, | 6140 type: Object, |
| 10573 value: function() { | 6141 value: function() { |
| 10574 return [{ | 6142 return [ { |
| 10575 name: 'fade-in-animation', | 6143 name: 'fade-in-animation', |
| 10576 timing: { | 6144 timing: { |
| 10577 delay: 50, | 6145 delay: 50, |
| 10578 duration: 200 | 6146 duration: 200 |
| 10579 } | 6147 } |
| 10580 }, { | 6148 }, { |
| 10581 name: 'paper-menu-grow-width-animation', | 6149 name: 'paper-menu-grow-width-animation', |
| 10582 timing: { | 6150 timing: { |
| 10583 delay: 50, | 6151 delay: 50, |
| 10584 duration: 150, | 6152 duration: 150, |
| 10585 easing: SLIDE_CUBIC_BEZIER | 6153 easing: SLIDE_CUBIC_BEZIER |
| 10586 } | 6154 } |
| 10587 }, { | 6155 }, { |
| 10588 name: 'paper-menu-grow-height-animation', | 6156 name: 'paper-menu-grow-height-animation', |
| 10589 timing: { | 6157 timing: { |
| 10590 delay: 100, | 6158 delay: 100, |
| 10591 duration: 275, | 6159 duration: 275, |
| 10592 easing: SLIDE_CUBIC_BEZIER | 6160 easing: SLIDE_CUBIC_BEZIER |
| 10593 } | 6161 } |
| 10594 }]; | 6162 } ]; |
| 10595 } | 6163 } |
| 10596 }, | 6164 }, |
| 10597 | |
| 10598 closeAnimationConfig: { | 6165 closeAnimationConfig: { |
| 10599 type: Object, | 6166 type: Object, |
| 10600 value: function() { | 6167 value: function() { |
| 10601 return [{ | 6168 return [ { |
| 10602 name: 'fade-out-animation', | 6169 name: 'fade-out-animation', |
| 10603 timing: { | 6170 timing: { |
| 10604 duration: 150 | 6171 duration: 150 |
| 10605 } | 6172 } |
| 10606 }]; | 6173 } ]; |
| 10607 } | 6174 } |
| 10608 } | 6175 } |
| 10609 }, | 6176 }, |
| 10610 | |
| 10611 keyBindings: { | 6177 keyBindings: { |
| 10612 'tab': 'onTabPressed_', | 6178 tab: 'onTabPressed_' |
| 10613 }, | 6179 }, |
| 10614 | |
| 10615 listeners: { | 6180 listeners: { |
| 10616 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', | 6181 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_' |
| 10617 }, | 6182 }, |
| 10618 | |
| 10619 /** | |
| 10620 * The last anchor that was used to open a menu. It's necessary for toggling. | |
| 10621 * @private {?Element} | |
| 10622 */ | |
| 10623 lastAnchor_: null, | 6183 lastAnchor_: null, |
| 10624 | |
| 10625 /** | |
| 10626 * The first focusable child in the menu's light DOM. | |
| 10627 * @private {?Element} | |
| 10628 */ | |
| 10629 firstFocus_: null, | 6184 firstFocus_: null, |
| 10630 | |
| 10631 /** | |
| 10632 * The last focusable child in the menu's light DOM. | |
| 10633 * @private {?Element} | |
| 10634 */ | |
| 10635 lastFocus_: null, | 6185 lastFocus_: null, |
| 10636 | |
| 10637 /** @override */ | |
| 10638 attached: function() { | 6186 attached: function() { |
| 10639 window.addEventListener('resize', this.closeMenu.bind(this)); | 6187 window.addEventListener('resize', this.closeMenu.bind(this)); |
| 10640 }, | 6188 }, |
| 10641 | |
| 10642 /** Closes the menu. */ | |
| 10643 closeMenu: function() { | 6189 closeMenu: function() { |
| 10644 if (this.root.activeElement == null) { | 6190 if (this.root.activeElement == null) { |
| 10645 // Something else has taken focus away from the menu. Do not attempt to | |
| 10646 // restore focus to the button which opened the menu. | |
| 10647 this.$.dropdown.restoreFocusOnClose = false; | 6191 this.$.dropdown.restoreFocusOnClose = false; |
| 10648 } | 6192 } |
| 10649 this.menuOpen = false; | 6193 this.menuOpen = false; |
| 10650 }, | 6194 }, |
| 10651 | |
| 10652 /** | |
| 10653 * Opens the menu at the anchor location. | |
| 10654 * @param {!Element} anchor The location to display the menu. | |
| 10655 * @param {!Object} itemData The contextual item's data. | |
| 10656 */ | |
| 10657 openMenu: function(anchor, itemData) { | 6195 openMenu: function(anchor, itemData) { |
| 10658 if (this.lastAnchor_ == anchor && this.menuOpen) | 6196 if (this.lastAnchor_ == anchor && this.menuOpen) return; |
| 10659 return; | 6197 if (this.menuOpen) this.closeMenu(); |
| 10660 | |
| 10661 if (this.menuOpen) | |
| 10662 this.closeMenu(); | |
| 10663 | |
| 10664 this.itemData = itemData; | 6198 this.itemData = itemData; |
| 10665 this.lastAnchor_ = anchor; | 6199 this.lastAnchor_ = anchor; |
| 10666 this.$.dropdown.restoreFocusOnClose = true; | 6200 this.$.dropdown.restoreFocusOnClose = true; |
| 10667 | 6201 var focusableChildren = Polymer.dom(this).querySelectorAll('[tabindex]:not([
hidden]),button:not([hidden])'); |
| 10668 var focusableChildren = Polymer.dom(this).querySelectorAll( | |
| 10669 '[tabindex]:not([hidden]),button:not([hidden])'); | |
| 10670 if (focusableChildren.length > 0) { | 6202 if (focusableChildren.length > 0) { |
| 10671 this.$.dropdown.focusTarget = focusableChildren[0]; | 6203 this.$.dropdown.focusTarget = focusableChildren[0]; |
| 10672 this.firstFocus_ = focusableChildren[0]; | 6204 this.firstFocus_ = focusableChildren[0]; |
| 10673 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; | 6205 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
| 10674 } | 6206 } |
| 10675 | |
| 10676 // Move the menu to the anchor. | |
| 10677 this.$.dropdown.positionTarget = anchor; | 6207 this.$.dropdown.positionTarget = anchor; |
| 10678 this.menuOpen = true; | 6208 this.menuOpen = true; |
| 10679 }, | 6209 }, |
| 10680 | |
| 10681 /** | |
| 10682 * Toggles the menu for the anchor that is passed in. | |
| 10683 * @param {!Element} anchor The location to display the menu. | |
| 10684 * @param {!Object} itemData The contextual item's data. | |
| 10685 */ | |
| 10686 toggleMenu: function(anchor, itemData) { | 6210 toggleMenu: function(anchor, itemData) { |
| 10687 if (anchor == this.lastAnchor_ && this.menuOpen) | 6211 if (anchor == this.lastAnchor_ && this.menuOpen) this.closeMenu(); else this
.openMenu(anchor, itemData); |
| 10688 this.closeMenu(); | 6212 }, |
| 10689 else | |
| 10690 this.openMenu(anchor, itemData); | |
| 10691 }, | |
| 10692 | |
| 10693 /** | |
| 10694 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from | |
| 10695 * the first element with a nonzero tabindex to the last such element. | |
| 10696 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available | |
| 10697 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). | |
| 10698 * @param {CustomEvent} e | |
| 10699 */ | |
| 10700 onTabPressed_: function(e) { | 6213 onTabPressed_: function(e) { |
| 10701 if (!this.firstFocus_ || !this.lastFocus_) | 6214 if (!this.firstFocus_ || !this.lastFocus_) return; |
| 10702 return; | |
| 10703 | |
| 10704 var toFocus; | 6215 var toFocus; |
| 10705 var keyEvent = e.detail.keyboardEvent; | 6216 var keyEvent = e.detail.keyboardEvent; |
| 10706 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) | 6217 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) toFocus = this
.lastFocus_; else if (keyEvent.target == this.lastFocus_) toFocus = this.firstFo
cus_; |
| 10707 toFocus = this.lastFocus_; | 6218 if (!toFocus) return; |
| 10708 else if (keyEvent.target == this.lastFocus_) | |
| 10709 toFocus = this.firstFocus_; | |
| 10710 | |
| 10711 if (!toFocus) | |
| 10712 return; | |
| 10713 | |
| 10714 e.preventDefault(); | 6219 e.preventDefault(); |
| 10715 toFocus.focus(); | 6220 toFocus.focus(); |
| 10716 }, | 6221 }, |
| 10717 | |
| 10718 /** | |
| 10719 * Ensure the menu is reset properly when it is closed by the dropdown (eg, | |
| 10720 * clicking outside). | |
| 10721 * @private | |
| 10722 */ | |
| 10723 menuOpenChanged_: function() { | 6222 menuOpenChanged_: function() { |
| 10724 if (!this.menuOpen) { | 6223 if (!this.menuOpen) { |
| 10725 this.itemData = null; | 6224 this.itemData = null; |
| 10726 this.lastAnchor_ = null; | 6225 this.lastAnchor_ = null; |
| 10727 } | 6226 } |
| 10728 }, | 6227 }, |
| 10729 | |
| 10730 /** | |
| 10731 * Prevent focus restoring when tapping outside the menu. This stops the | |
| 10732 * focus moving around unexpectedly when closing the menu with the mouse. | |
| 10733 * @param {CustomEvent} e | |
| 10734 * @private | |
| 10735 */ | |
| 10736 onOverlayCanceled_: function(e) { | 6228 onOverlayCanceled_: function(e) { |
| 10737 if (e.detail.type == 'tap') | 6229 if (e.detail.type == 'tap') this.$.dropdown.restoreFocusOnClose = false; |
| 10738 this.$.dropdown.restoreFocusOnClose = false; | 6230 } |
| 10739 }, | 6231 }); |
| 10740 }); | 6232 |
| 10741 /** @polymerBehavior Polymer.PaperItemBehavior */ | 6233 Polymer.PaperItemBehaviorImpl = { |
| 10742 Polymer.PaperItemBehaviorImpl = { | 6234 hostAttributes: { |
| 10743 hostAttributes: { | 6235 role: 'option', |
| 10744 role: 'option', | 6236 tabindex: '0' |
| 10745 tabindex: '0' | 6237 } |
| 10746 } | 6238 }; |
| 10747 }; | 6239 |
| 10748 | 6240 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState,
Polymer.PaperItemBehaviorImpl ]; |
| 10749 /** @polymerBehavior */ | 6241 |
| 10750 Polymer.PaperItemBehavior = [ | |
| 10751 Polymer.IronButtonState, | |
| 10752 Polymer.IronControlState, | |
| 10753 Polymer.PaperItemBehaviorImpl | |
| 10754 ]; | |
| 10755 Polymer({ | 6242 Polymer({ |
| 10756 is: 'paper-item', | 6243 is: 'paper-item', |
| 10757 | 6244 behaviors: [ Polymer.PaperItemBehavior ] |
| 10758 behaviors: [ | 6245 }); |
| 10759 Polymer.PaperItemBehavior | 6246 |
| 10760 ] | |
| 10761 }); | |
| 10762 Polymer({ | 6247 Polymer({ |
| 10763 | 6248 is: 'iron-collapse', |
| 10764 is: 'iron-collapse', | 6249 behaviors: [ Polymer.IronResizableBehavior ], |
| 10765 | 6250 properties: { |
| 10766 behaviors: [ | 6251 horizontal: { |
| 10767 Polymer.IronResizableBehavior | 6252 type: Boolean, |
| 10768 ], | 6253 value: false, |
| 10769 | 6254 observer: '_horizontalChanged' |
| 10770 properties: { | 6255 }, |
| 10771 | 6256 opened: { |
| 10772 /** | 6257 type: Boolean, |
| 10773 * If true, the orientation is horizontal; otherwise is vertical. | 6258 value: false, |
| 10774 * | 6259 notify: true, |
| 10775 * @attribute horizontal | 6260 observer: '_openedChanged' |
| 10776 */ | 6261 }, |
| 10777 horizontal: { | 6262 noAnimation: { |
| 10778 type: Boolean, | 6263 type: Boolean |
| 10779 value: false, | 6264 } |
| 10780 observer: '_horizontalChanged' | 6265 }, |
| 10781 }, | 6266 get dimension() { |
| 10782 | 6267 return this.horizontal ? 'width' : 'height'; |
| 10783 /** | 6268 }, |
| 10784 * Set opened to true to show the collapse element and to false to hide it
. | 6269 get _dimensionMax() { |
| 10785 * | 6270 return this.horizontal ? 'maxWidth' : 'maxHeight'; |
| 10786 * @attribute opened | 6271 }, |
| 10787 */ | 6272 get _dimensionMaxCss() { |
| 10788 opened: { | 6273 return this.horizontal ? 'max-width' : 'max-height'; |
| 10789 type: Boolean, | 6274 }, |
| 10790 value: false, | 6275 hostAttributes: { |
| 10791 notify: true, | 6276 role: 'group', |
| 10792 observer: '_openedChanged' | 6277 'aria-hidden': 'true', |
| 10793 }, | 6278 'aria-expanded': 'false' |
| 10794 | 6279 }, |
| 10795 /** | 6280 listeners: { |
| 10796 * Set noAnimation to true to disable animations | 6281 transitionend: '_transitionEnd' |
| 10797 * | 6282 }, |
| 10798 * @attribute noAnimation | 6283 attached: function() { |
| 10799 */ | 6284 this._transitionEnd(); |
| 10800 noAnimation: { | 6285 }, |
| 10801 type: Boolean | 6286 toggle: function() { |
| 10802 }, | 6287 this.opened = !this.opened; |
| 10803 | 6288 }, |
| 10804 }, | 6289 show: function() { |
| 10805 | 6290 this.opened = true; |
| 10806 get dimension() { | 6291 }, |
| 10807 return this.horizontal ? 'width' : 'height'; | 6292 hide: function() { |
| 10808 }, | 6293 this.opened = false; |
| 10809 | 6294 }, |
| 10810 /** | 6295 updateSize: function(size, animated) { |
| 10811 * `maxWidth` or `maxHeight`. | 6296 var curSize = this.style[this._dimensionMax]; |
| 10812 * @private | 6297 if (curSize === size || size === 'auto' && !curSize) { |
| 10813 */ | 6298 return; |
| 10814 get _dimensionMax() { | 6299 } |
| 10815 return this.horizontal ? 'maxWidth' : 'maxHeight'; | 6300 this._updateTransition(false); |
| 10816 }, | 6301 if (animated && !this.noAnimation && this._isDisplayed) { |
| 10817 | 6302 var startSize = this._calcSize(); |
| 10818 /** | |
| 10819 * `max-width` or `max-height`. | |
| 10820 * @private | |
| 10821 */ | |
| 10822 get _dimensionMaxCss() { | |
| 10823 return this.horizontal ? 'max-width' : 'max-height'; | |
| 10824 }, | |
| 10825 | |
| 10826 hostAttributes: { | |
| 10827 role: 'group', | |
| 10828 'aria-hidden': 'true', | |
| 10829 'aria-expanded': 'false' | |
| 10830 }, | |
| 10831 | |
| 10832 listeners: { | |
| 10833 transitionend: '_transitionEnd' | |
| 10834 }, | |
| 10835 | |
| 10836 attached: function() { | |
| 10837 // It will take care of setting correct classes and styles. | |
| 10838 this._transitionEnd(); | |
| 10839 }, | |
| 10840 | |
| 10841 /** | |
| 10842 * Toggle the opened state. | |
| 10843 * | |
| 10844 * @method toggle | |
| 10845 */ | |
| 10846 toggle: function() { | |
| 10847 this.opened = !this.opened; | |
| 10848 }, | |
| 10849 | |
| 10850 show: function() { | |
| 10851 this.opened = true; | |
| 10852 }, | |
| 10853 | |
| 10854 hide: function() { | |
| 10855 this.opened = false; | |
| 10856 }, | |
| 10857 | |
| 10858 /** | |
| 10859 * Updates the size of the element. | |
| 10860 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop
erty value, usually `auto` or `0px`. | |
| 10861 * @param {boolean=} animated if `true` updates the size with an animation,
otherwise without. | |
| 10862 */ | |
| 10863 updateSize: function(size, animated) { | |
| 10864 // No change! | |
| 10865 var curSize = this.style[this._dimensionMax]; | |
| 10866 if (curSize === size || (size === 'auto' && !curSize)) { | |
| 10867 return; | |
| 10868 } | |
| 10869 | |
| 10870 this._updateTransition(false); | |
| 10871 // If we can animate, must do some prep work. | |
| 10872 if (animated && !this.noAnimation && this._isDisplayed) { | |
| 10873 // Animation will start at the current size. | |
| 10874 var startSize = this._calcSize(); | |
| 10875 // For `auto` we must calculate what is the final size for the animation
. | |
| 10876 // After the transition is done, _transitionEnd will set the size back t
o `auto`. | |
| 10877 if (size === 'auto') { | |
| 10878 this.style[this._dimensionMax] = ''; | |
| 10879 size = this._calcSize(); | |
| 10880 } | |
| 10881 // Go to startSize without animation. | |
| 10882 this.style[this._dimensionMax] = startSize; | |
| 10883 // Force layout to ensure transition will go. Set scrollTop to itself | |
| 10884 // so that compilers won't remove it. | |
| 10885 this.scrollTop = this.scrollTop; | |
| 10886 // Enable animation. | |
| 10887 this._updateTransition(true); | |
| 10888 } | |
| 10889 // Set the final size. | |
| 10890 if (size === 'auto') { | 6303 if (size === 'auto') { |
| 10891 this.style[this._dimensionMax] = ''; | 6304 this.style[this._dimensionMax] = ''; |
| 6305 size = this._calcSize(); |
| 6306 } |
| 6307 this.style[this._dimensionMax] = startSize; |
| 6308 this.scrollTop = this.scrollTop; |
| 6309 this._updateTransition(true); |
| 6310 } |
| 6311 if (size === 'auto') { |
| 6312 this.style[this._dimensionMax] = ''; |
| 6313 } else { |
| 6314 this.style[this._dimensionMax] = size; |
| 6315 } |
| 6316 }, |
| 6317 enableTransition: function(enabled) { |
| 6318 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` in
stead.'); |
| 6319 this.noAnimation = !enabled; |
| 6320 }, |
| 6321 _updateTransition: function(enabled) { |
| 6322 this.style.transitionDuration = enabled && !this.noAnimation ? '' : '0s'; |
| 6323 }, |
| 6324 _horizontalChanged: function() { |
| 6325 this.style.transitionProperty = this._dimensionMaxCss; |
| 6326 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxW
idth'; |
| 6327 this.style[otherDimension] = ''; |
| 6328 this.updateSize(this.opened ? 'auto' : '0px', false); |
| 6329 }, |
| 6330 _openedChanged: function() { |
| 6331 this.setAttribute('aria-expanded', this.opened); |
| 6332 this.setAttribute('aria-hidden', !this.opened); |
| 6333 this.toggleClass('iron-collapse-closed', false); |
| 6334 this.toggleClass('iron-collapse-opened', false); |
| 6335 this.updateSize(this.opened ? 'auto' : '0px', true); |
| 6336 if (this.opened) { |
| 6337 this.focus(); |
| 6338 } |
| 6339 if (this.noAnimation) { |
| 6340 this._transitionEnd(); |
| 6341 } |
| 6342 }, |
| 6343 _transitionEnd: function() { |
| 6344 if (this.opened) { |
| 6345 this.style[this._dimensionMax] = ''; |
| 6346 } |
| 6347 this.toggleClass('iron-collapse-closed', !this.opened); |
| 6348 this.toggleClass('iron-collapse-opened', this.opened); |
| 6349 this._updateTransition(false); |
| 6350 this.notifyResize(); |
| 6351 }, |
| 6352 get _isDisplayed() { |
| 6353 var rect = this.getBoundingClientRect(); |
| 6354 for (var prop in rect) { |
| 6355 if (rect[prop] !== 0) return true; |
| 6356 } |
| 6357 return false; |
| 6358 }, |
| 6359 _calcSize: function() { |
| 6360 return this.getBoundingClientRect()[this.dimension] + 'px'; |
| 6361 } |
| 6362 }); |
| 6363 |
| 6364 Polymer.IronFormElementBehavior = { |
| 6365 properties: { |
| 6366 name: { |
| 6367 type: String |
| 6368 }, |
| 6369 value: { |
| 6370 notify: true, |
| 6371 type: String |
| 6372 }, |
| 6373 required: { |
| 6374 type: Boolean, |
| 6375 value: false |
| 6376 }, |
| 6377 _parentForm: { |
| 6378 type: Object |
| 6379 } |
| 6380 }, |
| 6381 attached: function() { |
| 6382 this.fire('iron-form-element-register'); |
| 6383 }, |
| 6384 detached: function() { |
| 6385 if (this._parentForm) { |
| 6386 this._parentForm.fire('iron-form-element-unregister', { |
| 6387 target: this |
| 6388 }); |
| 6389 } |
| 6390 } |
| 6391 }; |
| 6392 |
| 6393 Polymer.IronCheckedElementBehaviorImpl = { |
| 6394 properties: { |
| 6395 checked: { |
| 6396 type: Boolean, |
| 6397 value: false, |
| 6398 reflectToAttribute: true, |
| 6399 notify: true, |
| 6400 observer: '_checkedChanged' |
| 6401 }, |
| 6402 toggles: { |
| 6403 type: Boolean, |
| 6404 value: true, |
| 6405 reflectToAttribute: true |
| 6406 }, |
| 6407 value: { |
| 6408 type: String, |
| 6409 value: 'on', |
| 6410 observer: '_valueChanged' |
| 6411 } |
| 6412 }, |
| 6413 observers: [ '_requiredChanged(required)' ], |
| 6414 created: function() { |
| 6415 this._hasIronCheckedElementBehavior = true; |
| 6416 }, |
| 6417 _getValidity: function(_value) { |
| 6418 return this.disabled || !this.required || this.checked; |
| 6419 }, |
| 6420 _requiredChanged: function() { |
| 6421 if (this.required) { |
| 6422 this.setAttribute('aria-required', 'true'); |
| 6423 } else { |
| 6424 this.removeAttribute('aria-required'); |
| 6425 } |
| 6426 }, |
| 6427 _checkedChanged: function() { |
| 6428 this.active = this.checked; |
| 6429 this.fire('iron-change'); |
| 6430 }, |
| 6431 _valueChanged: function() { |
| 6432 if (this.value === undefined || this.value === null) { |
| 6433 this.value = 'on'; |
| 6434 } |
| 6435 } |
| 6436 }; |
| 6437 |
| 6438 Polymer.IronCheckedElementBehavior = [ Polymer.IronFormElementBehavior, Polymer.
IronValidatableBehavior, Polymer.IronCheckedElementBehaviorImpl ]; |
| 6439 |
| 6440 Polymer.PaperCheckedElementBehaviorImpl = { |
| 6441 _checkedChanged: function() { |
| 6442 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); |
| 6443 if (this.hasRipple()) { |
| 6444 if (this.checked) { |
| 6445 this._ripple.setAttribute('checked', ''); |
| 10892 } else { | 6446 } else { |
| 10893 this.style[this._dimensionMax] = size; | 6447 this._ripple.removeAttribute('checked'); |
| 10894 } | 6448 } |
| 10895 }, | 6449 } |
| 10896 | 6450 }, |
| 10897 /** | 6451 _buttonStateChanged: function() { |
| 10898 * enableTransition() is deprecated, but left over so it doesn't break exist
ing code. | 6452 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); |
| 10899 * Please use `noAnimation` property instead. | 6453 if (this.disabled) { |
| 10900 * | 6454 return; |
| 10901 * @method enableTransition | 6455 } |
| 10902 * @deprecated since version 1.0.4 | 6456 if (this.isAttached) { |
| 10903 */ | 6457 this.checked = this.active; |
| 10904 enableTransition: function(enabled) { | 6458 } |
| 10905 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation`
instead.'); | 6459 } |
| 10906 this.noAnimation = !enabled; | 6460 }; |
| 10907 }, | 6461 |
| 10908 | 6462 Polymer.PaperCheckedElementBehavior = [ Polymer.PaperInkyFocusBehavior, Polymer.
IronCheckedElementBehavior, Polymer.PaperCheckedElementBehaviorImpl ]; |
| 10909 _updateTransition: function(enabled) { | 6463 |
| 10910 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s'
; | |
| 10911 }, | |
| 10912 | |
| 10913 _horizontalChanged: function() { | |
| 10914 this.style.transitionProperty = this._dimensionMaxCss; | |
| 10915 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma
xWidth'; | |
| 10916 this.style[otherDimension] = ''; | |
| 10917 this.updateSize(this.opened ? 'auto' : '0px', false); | |
| 10918 }, | |
| 10919 | |
| 10920 _openedChanged: function() { | |
| 10921 this.setAttribute('aria-expanded', this.opened); | |
| 10922 this.setAttribute('aria-hidden', !this.opened); | |
| 10923 | |
| 10924 this.toggleClass('iron-collapse-closed', false); | |
| 10925 this.toggleClass('iron-collapse-opened', false); | |
| 10926 this.updateSize(this.opened ? 'auto' : '0px', true); | |
| 10927 | |
| 10928 // Focus the current collapse. | |
| 10929 if (this.opened) { | |
| 10930 this.focus(); | |
| 10931 } | |
| 10932 if (this.noAnimation) { | |
| 10933 this._transitionEnd(); | |
| 10934 } | |
| 10935 }, | |
| 10936 | |
| 10937 _transitionEnd: function() { | |
| 10938 if (this.opened) { | |
| 10939 this.style[this._dimensionMax] = ''; | |
| 10940 } | |
| 10941 this.toggleClass('iron-collapse-closed', !this.opened); | |
| 10942 this.toggleClass('iron-collapse-opened', this.opened); | |
| 10943 this._updateTransition(false); | |
| 10944 this.notifyResize(); | |
| 10945 }, | |
| 10946 | |
| 10947 /** | |
| 10948 * Simplistic heuristic to detect if element has a parent with display: none | |
| 10949 * | |
| 10950 * @private | |
| 10951 */ | |
| 10952 get _isDisplayed() { | |
| 10953 var rect = this.getBoundingClientRect(); | |
| 10954 for (var prop in rect) { | |
| 10955 if (rect[prop] !== 0) return true; | |
| 10956 } | |
| 10957 return false; | |
| 10958 }, | |
| 10959 | |
| 10960 _calcSize: function() { | |
| 10961 return this.getBoundingClientRect()[this.dimension] + 'px'; | |
| 10962 } | |
| 10963 | |
| 10964 }); | |
| 10965 /** | |
| 10966 Polymer.IronFormElementBehavior enables a custom element to be included | |
| 10967 in an `iron-form`. | |
| 10968 | |
| 10969 @demo demo/index.html | |
| 10970 @polymerBehavior | |
| 10971 */ | |
| 10972 Polymer.IronFormElementBehavior = { | |
| 10973 | |
| 10974 properties: { | |
| 10975 /** | |
| 10976 * Fired when the element is added to an `iron-form`. | |
| 10977 * | |
| 10978 * @event iron-form-element-register | |
| 10979 */ | |
| 10980 | |
| 10981 /** | |
| 10982 * Fired when the element is removed from an `iron-form`. | |
| 10983 * | |
| 10984 * @event iron-form-element-unregister | |
| 10985 */ | |
| 10986 | |
| 10987 /** | |
| 10988 * The name of this element. | |
| 10989 */ | |
| 10990 name: { | |
| 10991 type: String | |
| 10992 }, | |
| 10993 | |
| 10994 /** | |
| 10995 * The value for this element. | |
| 10996 */ | |
| 10997 value: { | |
| 10998 notify: true, | |
| 10999 type: String | |
| 11000 }, | |
| 11001 | |
| 11002 /** | |
| 11003 * Set to true to mark the input as required. If used in a form, a | |
| 11004 * custom element that uses this behavior should also use | |
| 11005 * Polymer.IronValidatableBehavior and define a custom validation method. | |
| 11006 * Otherwise, a `required` element will always be considered valid. | |
| 11007 * It's also strongly recommended to provide a visual style for the elemen
t | |
| 11008 * when its value is invalid. | |
| 11009 */ | |
| 11010 required: { | |
| 11011 type: Boolean, | |
| 11012 value: false | |
| 11013 }, | |
| 11014 | |
| 11015 /** | |
| 11016 * The form that the element is registered to. | |
| 11017 */ | |
| 11018 _parentForm: { | |
| 11019 type: Object | |
| 11020 } | |
| 11021 }, | |
| 11022 | |
| 11023 attached: function() { | |
| 11024 // Note: the iron-form that this element belongs to will set this | |
| 11025 // element's _parentForm property when handling this event. | |
| 11026 this.fire('iron-form-element-register'); | |
| 11027 }, | |
| 11028 | |
| 11029 detached: function() { | |
| 11030 if (this._parentForm) { | |
| 11031 this._parentForm.fire('iron-form-element-unregister', {target: this}); | |
| 11032 } | |
| 11033 } | |
| 11034 | |
| 11035 }; | |
| 11036 /** | |
| 11037 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element | |
| 11038 * that has a `checked` property, which can be used for validation if the | |
| 11039 * element is also `required`. Element instances implementing this behavior | |
| 11040 * will also be registered for use in an `iron-form` element. | |
| 11041 * | |
| 11042 * @demo demo/index.html | |
| 11043 * @polymerBehavior Polymer.IronCheckedElementBehavior | |
| 11044 */ | |
| 11045 Polymer.IronCheckedElementBehaviorImpl = { | |
| 11046 | |
| 11047 properties: { | |
| 11048 /** | |
| 11049 * Fired when the checked state changes. | |
| 11050 * | |
| 11051 * @event iron-change | |
| 11052 */ | |
| 11053 | |
| 11054 /** | |
| 11055 * Gets or sets the state, `true` is checked and `false` is unchecked. | |
| 11056 */ | |
| 11057 checked: { | |
| 11058 type: Boolean, | |
| 11059 value: false, | |
| 11060 reflectToAttribute: true, | |
| 11061 notify: true, | |
| 11062 observer: '_checkedChanged' | |
| 11063 }, | |
| 11064 | |
| 11065 /** | |
| 11066 * If true, the button toggles the active state with each tap or press | |
| 11067 * of the spacebar. | |
| 11068 */ | |
| 11069 toggles: { | |
| 11070 type: Boolean, | |
| 11071 value: true, | |
| 11072 reflectToAttribute: true | |
| 11073 }, | |
| 11074 | |
| 11075 /* Overriden from Polymer.IronFormElementBehavior */ | |
| 11076 value: { | |
| 11077 type: String, | |
| 11078 value: 'on', | |
| 11079 observer: '_valueChanged' | |
| 11080 } | |
| 11081 }, | |
| 11082 | |
| 11083 observers: [ | |
| 11084 '_requiredChanged(required)' | |
| 11085 ], | |
| 11086 | |
| 11087 created: function() { | |
| 11088 // Used by `iron-form` to handle the case that an element with this behavi
or | |
| 11089 // doesn't have a role of 'checkbox' or 'radio', but should still only be | |
| 11090 // included when the form is serialized if `this.checked === true`. | |
| 11091 this._hasIronCheckedElementBehavior = true; | |
| 11092 }, | |
| 11093 | |
| 11094 /** | |
| 11095 * Returns false if the element is required and not checked, and true otherw
ise. | |
| 11096 * @param {*=} _value Ignored. | |
| 11097 * @return {boolean} true if `required` is false or if `checked` is true. | |
| 11098 */ | |
| 11099 _getValidity: function(_value) { | |
| 11100 return this.disabled || !this.required || this.checked; | |
| 11101 }, | |
| 11102 | |
| 11103 /** | |
| 11104 * Update the aria-required label when `required` is changed. | |
| 11105 */ | |
| 11106 _requiredChanged: function() { | |
| 11107 if (this.required) { | |
| 11108 this.setAttribute('aria-required', 'true'); | |
| 11109 } else { | |
| 11110 this.removeAttribute('aria-required'); | |
| 11111 } | |
| 11112 }, | |
| 11113 | |
| 11114 /** | |
| 11115 * Fire `iron-changed` when the checked state changes. | |
| 11116 */ | |
| 11117 _checkedChanged: function() { | |
| 11118 this.active = this.checked; | |
| 11119 this.fire('iron-change'); | |
| 11120 }, | |
| 11121 | |
| 11122 /** | |
| 11123 * Reset value to 'on' if it is set to `undefined`. | |
| 11124 */ | |
| 11125 _valueChanged: function() { | |
| 11126 if (this.value === undefined || this.value === null) { | |
| 11127 this.value = 'on'; | |
| 11128 } | |
| 11129 } | |
| 11130 }; | |
| 11131 | |
| 11132 /** @polymerBehavior Polymer.IronCheckedElementBehavior */ | |
| 11133 Polymer.IronCheckedElementBehavior = [ | |
| 11134 Polymer.IronFormElementBehavior, | |
| 11135 Polymer.IronValidatableBehavior, | |
| 11136 Polymer.IronCheckedElementBehaviorImpl | |
| 11137 ]; | |
| 11138 /** | |
| 11139 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element | |
| 11140 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio
r` | |
| 11141 * and is compatible with having a ripple effect. | |
| 11142 * @polymerBehavior Polymer.PaperCheckedElementBehavior | |
| 11143 */ | |
| 11144 Polymer.PaperCheckedElementBehaviorImpl = { | |
| 11145 /** | |
| 11146 * Synchronizes the element's checked state with its ripple effect. | |
| 11147 */ | |
| 11148 _checkedChanged: function() { | |
| 11149 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); | |
| 11150 if (this.hasRipple()) { | |
| 11151 if (this.checked) { | |
| 11152 this._ripple.setAttribute('checked', ''); | |
| 11153 } else { | |
| 11154 this._ripple.removeAttribute('checked'); | |
| 11155 } | |
| 11156 } | |
| 11157 }, | |
| 11158 | |
| 11159 /** | |
| 11160 * Synchronizes the element's `active` and `checked` state. | |
| 11161 */ | |
| 11162 _buttonStateChanged: function() { | |
| 11163 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); | |
| 11164 if (this.disabled) { | |
| 11165 return; | |
| 11166 } | |
| 11167 if (this.isAttached) { | |
| 11168 this.checked = this.active; | |
| 11169 } | |
| 11170 } | |
| 11171 }; | |
| 11172 | |
| 11173 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */ | |
| 11174 Polymer.PaperCheckedElementBehavior = [ | |
| 11175 Polymer.PaperInkyFocusBehavior, | |
| 11176 Polymer.IronCheckedElementBehavior, | |
| 11177 Polymer.PaperCheckedElementBehaviorImpl | |
| 11178 ]; | |
| 11179 Polymer({ | 6464 Polymer({ |
| 11180 is: 'paper-checkbox', | 6465 is: 'paper-checkbox', |
| 11181 | 6466 behaviors: [ Polymer.PaperCheckedElementBehavior ], |
| 11182 behaviors: [ | 6467 hostAttributes: { |
| 11183 Polymer.PaperCheckedElementBehavior | 6468 role: 'checkbox', |
| 11184 ], | 6469 'aria-checked': false, |
| 11185 | 6470 tabindex: 0 |
| 11186 hostAttributes: { | 6471 }, |
| 11187 role: 'checkbox', | 6472 properties: { |
| 11188 'aria-checked': false, | 6473 ariaActiveAttribute: { |
| 11189 tabindex: 0 | 6474 type: String, |
| 11190 }, | 6475 value: 'aria-checked' |
| 11191 | 6476 } |
| 11192 properties: { | 6477 }, |
| 11193 /** | 6478 _computeCheckboxClass: function(checked, invalid) { |
| 11194 * Fired when the checked state changes due to user interaction. | 6479 var className = ''; |
| 11195 * | 6480 if (checked) { |
| 11196 * @event change | 6481 className += 'checked '; |
| 11197 */ | 6482 } |
| 11198 | 6483 if (invalid) { |
| 11199 /** | 6484 className += 'invalid'; |
| 11200 * Fired when the checked state changes. | 6485 } |
| 11201 * | 6486 return className; |
| 11202 * @event iron-change | 6487 }, |
| 11203 */ | 6488 _computeCheckmarkClass: function(checked) { |
| 11204 ariaActiveAttribute: { | 6489 return checked ? '' : 'hidden'; |
| 11205 type: String, | 6490 }, |
| 11206 value: 'aria-checked' | 6491 _createRipple: function() { |
| 11207 } | 6492 this._rippleContainer = this.$.checkboxContainer; |
| 11208 }, | 6493 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); |
| 11209 | 6494 } |
| 11210 _computeCheckboxClass: function(checked, invalid) { | 6495 }); |
| 11211 var className = ''; | 6496 |
| 11212 if (checked) { | |
| 11213 className += 'checked '; | |
| 11214 } | |
| 11215 if (invalid) { | |
| 11216 className += 'invalid'; | |
| 11217 } | |
| 11218 return className; | |
| 11219 }, | |
| 11220 | |
| 11221 _computeCheckmarkClass: function(checked) { | |
| 11222 return checked ? '' : 'hidden'; | |
| 11223 }, | |
| 11224 | |
| 11225 // create ripple inside the checkboxContainer | |
| 11226 _createRipple: function() { | |
| 11227 this._rippleContainer = this.$.checkboxContainer; | |
| 11228 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); | |
| 11229 } | |
| 11230 | |
| 11231 }); | |
| 11232 Polymer({ | 6497 Polymer({ |
| 11233 is: 'paper-icon-button-light', | 6498 is: 'paper-icon-button-light', |
| 11234 extends: 'button', | 6499 "extends": 'button', |
| 11235 | 6500 behaviors: [ Polymer.PaperRippleBehavior ], |
| 11236 behaviors: [ | 6501 listeners: { |
| 11237 Polymer.PaperRippleBehavior | 6502 down: '_rippleDown', |
| 11238 ], | 6503 up: '_rippleUp', |
| 11239 | 6504 focus: '_rippleDown', |
| 11240 listeners: { | 6505 blur: '_rippleUp' |
| 11241 'down': '_rippleDown', | 6506 }, |
| 11242 'up': '_rippleUp', | 6507 _rippleDown: function() { |
| 11243 'focus': '_rippleDown', | 6508 this.getRipple().downAction(); |
| 11244 'blur': '_rippleUp', | 6509 }, |
| 11245 }, | 6510 _rippleUp: function() { |
| 11246 | 6511 this.getRipple().upAction(); |
| 11247 _rippleDown: function() { | 6512 }, |
| 11248 this.getRipple().downAction(); | 6513 ensureRipple: function(var_args) { |
| 11249 }, | 6514 var lastRipple = this._ripple; |
| 11250 | 6515 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
| 11251 _rippleUp: function() { | 6516 if (this._ripple && this._ripple !== lastRipple) { |
| 11252 this.getRipple().upAction(); | 6517 this._ripple.center = true; |
| 11253 }, | 6518 this._ripple.classList.add('circle'); |
| 11254 | 6519 } |
| 11255 /** | 6520 } |
| 11256 * @param {...*} var_args | 6521 }); |
| 11257 */ | 6522 |
| 11258 ensureRipple: function(var_args) { | |
| 11259 var lastRipple = this._ripple; | |
| 11260 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | |
| 11261 if (this._ripple && this._ripple !== lastRipple) { | |
| 11262 this._ripple.center = true; | |
| 11263 this._ripple.classList.add('circle'); | |
| 11264 } | |
| 11265 } | |
| 11266 }); | |
| 11267 // Copyright 2016 The Chromium Authors. All rights reserved. | 6523 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11268 // Use of this source code is governed by a BSD-style license that can be | 6524 // Use of this source code is governed by a BSD-style license that can be |
| 11269 // found in the LICENSE file. | 6525 // found in the LICENSE file. |
| 11270 | |
| 11271 cr.define('cr.icon', function() { | 6526 cr.define('cr.icon', function() { |
| 11272 /** | |
| 11273 * @return {!Array<number>} The scale factors supported by this platform for | |
| 11274 * webui resources. | |
| 11275 */ | |
| 11276 function getSupportedScaleFactors() { | 6527 function getSupportedScaleFactors() { |
| 11277 var supportedScaleFactors = []; | 6528 var supportedScaleFactors = []; |
| 11278 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { | 6529 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { |
| 11279 // All desktop platforms support zooming which also updates the | |
| 11280 // renderer's device scale factors (a.k.a devicePixelRatio), and | |
| 11281 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in | |
| 11282 // image-set on these platforms so that the renderer can pick the | |
| 11283 // closest image for the current device scale factor. | |
| 11284 supportedScaleFactors.push(1); | 6530 supportedScaleFactors.push(1); |
| 11285 supportedScaleFactors.push(2); | 6531 supportedScaleFactors.push(2); |
| 11286 } else { | 6532 } else { |
| 11287 // For other platforms that use fixed device scale factor, use | |
| 11288 // the window's device pixel ratio. | |
| 11289 // TODO(oshima): Investigate if Android/iOS need to use image-set. | |
| 11290 supportedScaleFactors.push(window.devicePixelRatio); | 6533 supportedScaleFactors.push(window.devicePixelRatio); |
| 11291 } | 6534 } |
| 11292 return supportedScaleFactors; | 6535 return supportedScaleFactors; |
| 11293 } | 6536 } |
| 11294 | |
| 11295 /** | |
| 11296 * Returns the URL of the image, or an image set of URLs for the profile | |
| 11297 * avatar. Default avatars have resources available for multiple scalefactors, | |
| 11298 * whereas the GAIA profile image only comes in one size. | |
| 11299 * | |
| 11300 * @param {string} path The path of the image. | |
| 11301 * @return {string} The url, or an image set of URLs of the avatar image. | |
| 11302 */ | |
| 11303 function getProfileAvatarIcon(path) { | 6537 function getProfileAvatarIcon(path) { |
| 11304 var chromeThemePath = 'chrome://theme'; | 6538 var chromeThemePath = 'chrome://theme'; |
| 11305 var isDefaultAvatar = | 6539 var isDefaultAvatar = path.slice(0, chromeThemePath.length) == chromeThemePa
th; |
| 11306 (path.slice(0, chromeThemePath.length) == chromeThemePath); | 6540 return isDefaultAvatar ? imageset(path + '@scalefactorx') : url(path); |
| 11307 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); | 6541 } |
| 11308 } | |
| 11309 | |
| 11310 /** | |
| 11311 * Generates a CSS -webkit-image-set for a chrome:// url. | |
| 11312 * An entry in the image set is added for each of getSupportedScaleFactors(). | |
| 11313 * The scale-factor-specific url is generated by replacing the first instance | |
| 11314 * of 'scalefactor' in |path| with the numeric scale factor. | |
| 11315 * @param {string} path The URL to generate an image set for. | |
| 11316 * 'scalefactor' should be a substring of |path|. | |
| 11317 * @return {string} The CSS -webkit-image-set. | |
| 11318 */ | |
| 11319 function imageset(path) { | 6542 function imageset(path) { |
| 11320 var supportedScaleFactors = getSupportedScaleFactors(); | 6543 var supportedScaleFactors = getSupportedScaleFactors(); |
| 11321 | |
| 11322 var replaceStartIndex = path.indexOf('scalefactor'); | 6544 var replaceStartIndex = path.indexOf('scalefactor'); |
| 11323 if (replaceStartIndex < 0) | 6545 if (replaceStartIndex < 0) return url(path); |
| 11324 return url(path); | |
| 11325 | |
| 11326 var s = ''; | 6546 var s = ''; |
| 11327 for (var i = 0; i < supportedScaleFactors.length; ++i) { | 6547 for (var i = 0; i < supportedScaleFactors.length; ++i) { |
| 11328 var scaleFactor = supportedScaleFactors[i]; | 6548 var scaleFactor = supportedScaleFactors[i]; |
| 11329 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + | 6549 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + scaleFactor
+ path.substr(replaceStartIndex + 'scalefactor'.length); |
| 11330 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length); | |
| 11331 | |
| 11332 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; | 6550 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; |
| 11333 | 6551 if (i != supportedScaleFactors.length - 1) s += ', '; |
| 11334 if (i != supportedScaleFactors.length - 1) | |
| 11335 s += ', '; | |
| 11336 } | 6552 } |
| 11337 return '-webkit-image-set(' + s + ')'; | 6553 return '-webkit-image-set(' + s + ')'; |
| 11338 } | 6554 } |
| 11339 | |
| 11340 /** | |
| 11341 * A regular expression for identifying favicon URLs. | |
| 11342 * @const {!RegExp} | |
| 11343 */ | |
| 11344 var FAVICON_URL_REGEX = /\.ico$/i; | 6555 var FAVICON_URL_REGEX = /\.ico$/i; |
| 11345 | |
| 11346 /** | |
| 11347 * Creates a CSS -webkit-image-set for a favicon request. | |
| 11348 * @param {string} url Either the URL of the original page or of the favicon | |
| 11349 * itself. | |
| 11350 * @param {number=} opt_size Optional preferred size of the favicon. | |
| 11351 * @param {string=} opt_type Optional type of favicon to request. Valid values | |
| 11352 * are 'favicon' and 'touch-icon'. Default is 'favicon'. | |
| 11353 * @return {string} -webkit-image-set for the favicon. | |
| 11354 */ | |
| 11355 function getFaviconImageSet(url, opt_size, opt_type) { | 6556 function getFaviconImageSet(url, opt_size, opt_type) { |
| 11356 var size = opt_size || 16; | 6557 var size = opt_size || 16; |
| 11357 var type = opt_type || 'favicon'; | 6558 var type = opt_type || 'favicon'; |
| 11358 | 6559 return imageset('chrome://' + type + '/size/' + size + '@scalefactorx/' + (F
AVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); |
| 11359 return imageset( | 6560 } |
| 11360 'chrome://' + type + '/size/' + size + '@scalefactorx/' + | |
| 11361 // Note: Literal 'iconurl' must match |kIconURLParameter| in | |
| 11362 // components/favicon_base/favicon_url_parser.cc. | |
| 11363 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); | |
| 11364 } | |
| 11365 | |
| 11366 return { | 6561 return { |
| 11367 getSupportedScaleFactors: getSupportedScaleFactors, | 6562 getSupportedScaleFactors: getSupportedScaleFactors, |
| 11368 getProfileAvatarIcon: getProfileAvatarIcon, | 6563 getProfileAvatarIcon: getProfileAvatarIcon, |
| 11369 getFaviconImageSet: getFaviconImageSet, | 6564 getFaviconImageSet: getFaviconImageSet |
| 11370 }; | 6565 }; |
| 11371 }); | 6566 }); |
| 6567 |
| 11372 // Copyright 2016 The Chromium Authors. All rights reserved. | 6568 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11373 // Use of this source code is governed by a BSD-style license that can be | 6569 // Use of this source code is governed by a BSD-style license that can be |
| 11374 // found in the LICENSE file. | 6570 // found in the LICENSE file. |
| 11375 | |
| 11376 /** | |
| 11377 * @fileoverview Defines a singleton object, md_history.BrowserService, which | |
| 11378 * provides access to chrome.send APIs. | |
| 11379 */ | |
| 11380 | |
| 11381 cr.define('md_history', function() { | 6571 cr.define('md_history', function() { |
| 11382 /** @constructor */ | |
| 11383 function BrowserService() { | 6572 function BrowserService() { |
| 11384 /** @private {Array<!HistoryEntry>} */ | |
| 11385 this.pendingDeleteItems_ = null; | 6573 this.pendingDeleteItems_ = null; |
| 11386 /** @private {PromiseResolver} */ | |
| 11387 this.pendingDeletePromise_ = null; | 6574 this.pendingDeletePromise_ = null; |
| 11388 } | 6575 } |
| 11389 | |
| 11390 BrowserService.prototype = { | 6576 BrowserService.prototype = { |
| 11391 /** | |
| 11392 * @param {!Array<!HistoryEntry>} items | |
| 11393 * @return {Promise<!Array<!HistoryEntry>>} | |
| 11394 */ | |
| 11395 deleteItems: function(items) { | 6577 deleteItems: function(items) { |
| 11396 if (this.pendingDeleteItems_ != null) { | 6578 if (this.pendingDeleteItems_ != null) { |
| 11397 // There's already a deletion in progress, reject immediately. | 6579 return new Promise(function(resolve, reject) { |
| 11398 return new Promise(function(resolve, reject) { reject(items); }); | 6580 reject(items); |
| 6581 }); |
| 11399 } | 6582 } |
| 11400 | |
| 11401 var removalList = items.map(function(item) { | 6583 var removalList = items.map(function(item) { |
| 11402 return { | 6584 return { |
| 11403 url: item.url, | 6585 url: item.url, |
| 11404 timestamps: item.allTimestamps | 6586 timestamps: item.allTimestamps |
| 11405 }; | 6587 }; |
| 11406 }); | 6588 }); |
| 11407 | |
| 11408 this.pendingDeleteItems_ = items; | 6589 this.pendingDeleteItems_ = items; |
| 11409 this.pendingDeletePromise_ = new PromiseResolver(); | 6590 this.pendingDeletePromise_ = new PromiseResolver(); |
| 11410 | |
| 11411 chrome.send('removeVisits', removalList); | 6591 chrome.send('removeVisits', removalList); |
| 11412 | |
| 11413 return this.pendingDeletePromise_.promise; | 6592 return this.pendingDeletePromise_.promise; |
| 11414 }, | 6593 }, |
| 11415 | |
| 11416 /** | |
| 11417 * @param {!string} url | |
| 11418 */ | |
| 11419 removeBookmark: function(url) { | 6594 removeBookmark: function(url) { |
| 11420 chrome.send('removeBookmark', [url]); | 6595 chrome.send('removeBookmark', [ url ]); |
| 11421 }, | 6596 }, |
| 11422 | |
| 11423 /** | |
| 11424 * @param {string} sessionTag | |
| 11425 */ | |
| 11426 openForeignSessionAllTabs: function(sessionTag) { | 6597 openForeignSessionAllTabs: function(sessionTag) { |
| 11427 chrome.send('openForeignSession', [sessionTag]); | 6598 chrome.send('openForeignSession', [ sessionTag ]); |
| 11428 }, | 6599 }, |
| 11429 | |
| 11430 /** | |
| 11431 * @param {string} sessionTag | |
| 11432 * @param {number} windowId | |
| 11433 * @param {number} tabId | |
| 11434 * @param {MouseEvent} e | |
| 11435 */ | |
| 11436 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { | 6600 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { |
| 11437 chrome.send('openForeignSession', [ | 6601 chrome.send('openForeignSession', [ sessionTag, String(windowId), String(t
abId), e.button || 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey ]); |
| 11438 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, | 6602 }, |
| 11439 e.ctrlKey, e.metaKey, e.shiftKey | |
| 11440 ]); | |
| 11441 }, | |
| 11442 | |
| 11443 /** | |
| 11444 * @param {string} sessionTag | |
| 11445 */ | |
| 11446 deleteForeignSession: function(sessionTag) { | 6603 deleteForeignSession: function(sessionTag) { |
| 11447 chrome.send('deleteForeignSession', [sessionTag]); | 6604 chrome.send('deleteForeignSession', [ sessionTag ]); |
| 11448 }, | 6605 }, |
| 11449 | |
| 11450 openClearBrowsingData: function() { | 6606 openClearBrowsingData: function() { |
| 11451 chrome.send('clearBrowsingData'); | 6607 chrome.send('clearBrowsingData'); |
| 11452 }, | 6608 }, |
| 11453 | |
| 11454 /** | |
| 11455 * Record an action in UMA. | |
| 11456 * @param {string} actionDesc The name of the action to be logged. | |
| 11457 */ | |
| 11458 recordAction: function(actionDesc) { | 6609 recordAction: function(actionDesc) { |
| 11459 chrome.send('metricsHandler:recordAction', [actionDesc]); | 6610 chrome.send('metricsHandler:recordAction', [ actionDesc ]); |
| 11460 }, | 6611 }, |
| 11461 | |
| 11462 /** | |
| 11463 * @param {boolean} successful | |
| 11464 * @private | |
| 11465 */ | |
| 11466 resolveDelete_: function(successful) { | 6612 resolveDelete_: function(successful) { |
| 11467 if (this.pendingDeleteItems_ == null || | 6613 if (this.pendingDeleteItems_ == null || this.pendingDeletePromise_ == null
) { |
| 11468 this.pendingDeletePromise_ == null) { | |
| 11469 return; | 6614 return; |
| 11470 } | 6615 } |
| 11471 | 6616 if (successful) this.pendingDeletePromise_.resolve(this.pendingDeleteItems
_); else this.pendingDeletePromise_.reject(this.pendingDeleteItems_); |
| 11472 if (successful) | |
| 11473 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_); | |
| 11474 else | |
| 11475 this.pendingDeletePromise_.reject(this.pendingDeleteItems_); | |
| 11476 | |
| 11477 this.pendingDeleteItems_ = null; | 6617 this.pendingDeleteItems_ = null; |
| 11478 this.pendingDeletePromise_ = null; | 6618 this.pendingDeletePromise_ = null; |
| 11479 }, | 6619 } |
| 11480 }; | 6620 }; |
| 11481 | |
| 11482 cr.addSingletonGetter(BrowserService); | 6621 cr.addSingletonGetter(BrowserService); |
| 11483 | 6622 return { |
| 11484 return {BrowserService: BrowserService}; | 6623 BrowserService: BrowserService |
| 11485 }); | 6624 }; |
| 11486 | 6625 }); |
| 11487 /** | 6626 |
| 11488 * Called by the history backend when deletion was succesful. | |
| 11489 */ | |
| 11490 function deleteComplete() { | 6627 function deleteComplete() { |
| 11491 md_history.BrowserService.getInstance().resolveDelete_(true); | 6628 md_history.BrowserService.getInstance().resolveDelete_(true); |
| 11492 } | 6629 } |
| 11493 | 6630 |
| 11494 /** | |
| 11495 * Called by the history backend when the deletion failed. | |
| 11496 */ | |
| 11497 function deleteFailed() { | 6631 function deleteFailed() { |
| 11498 md_history.BrowserService.getInstance().resolveDelete_(false); | 6632 md_history.BrowserService.getInstance().resolveDelete_(false); |
| 11499 }; | 6633 } |
| 6634 |
| 11500 // Copyright 2016 The Chromium Authors. All rights reserved. | 6635 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11501 // Use of this source code is governed by a BSD-style license that can be | 6636 // Use of this source code is governed by a BSD-style license that can be |
| 11502 // found in the LICENSE file. | 6637 // found in the LICENSE file. |
| 11503 | |
| 11504 Polymer({ | 6638 Polymer({ |
| 11505 is: 'history-searched-label', | 6639 is: 'history-searched-label', |
| 11506 | |
| 11507 properties: { | 6640 properties: { |
| 11508 // The text to show in this label. | |
| 11509 title: String, | 6641 title: String, |
| 11510 | 6642 searchTerm: String |
| 11511 // The search term to bold within the title. | 6643 }, |
| 11512 searchTerm: String, | 6644 observers: [ 'setSearchedTextToBold_(title, searchTerm)' ], |
| 11513 }, | |
| 11514 | |
| 11515 observers: ['setSearchedTextToBold_(title, searchTerm)'], | |
| 11516 | |
| 11517 /** | |
| 11518 * Updates the page title. If a search term is specified, highlights any | |
| 11519 * occurrences of the search term in bold. | |
| 11520 * @private | |
| 11521 */ | |
| 11522 setSearchedTextToBold_: function() { | 6645 setSearchedTextToBold_: function() { |
| 11523 var i = 0; | 6646 var i = 0; |
| 11524 var titleElem = this.$.container; | 6647 var titleElem = this.$.container; |
| 11525 var titleText = this.title; | 6648 var titleText = this.title; |
| 11526 | |
| 11527 if (this.searchTerm == '' || this.searchTerm == null) { | 6649 if (this.searchTerm == '' || this.searchTerm == null) { |
| 11528 titleElem.textContent = titleText; | 6650 titleElem.textContent = titleText; |
| 11529 return; | 6651 return; |
| 11530 } | 6652 } |
| 11531 | |
| 11532 var re = new RegExp(quoteString(this.searchTerm), 'gim'); | 6653 var re = new RegExp(quoteString(this.searchTerm), 'gim'); |
| 11533 var match; | 6654 var match; |
| 11534 titleElem.textContent = ''; | 6655 titleElem.textContent = ''; |
| 11535 while (match = re.exec(titleText)) { | 6656 while (match = re.exec(titleText)) { |
| 11536 if (match.index > i) | 6657 if (match.index > i) titleElem.appendChild(document.createTextNode(titleTe
xt.slice(i, match.index))); |
| 11537 titleElem.appendChild(document.createTextNode( | |
| 11538 titleText.slice(i, match.index))); | |
| 11539 i = re.lastIndex; | 6658 i = re.lastIndex; |
| 11540 // Mark the highlighted text in bold. | |
| 11541 var b = document.createElement('b'); | 6659 var b = document.createElement('b'); |
| 11542 b.textContent = titleText.substring(match.index, i); | 6660 b.textContent = titleText.substring(match.index, i); |
| 11543 titleElem.appendChild(b); | 6661 titleElem.appendChild(b); |
| 11544 } | 6662 } |
| 11545 if (i < titleText.length) | 6663 if (i < titleText.length) titleElem.appendChild(document.createTextNode(titl
eText.slice(i))); |
| 11546 titleElem.appendChild( | 6664 } |
| 11547 document.createTextNode(titleText.slice(i))); | 6665 }); |
| 11548 }, | 6666 |
| 11549 }); | |
| 11550 // Copyright 2015 The Chromium Authors. All rights reserved. | 6667 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11551 // Use of this source code is governed by a BSD-style license that can be | 6668 // Use of this source code is governed by a BSD-style license that can be |
| 11552 // found in the LICENSE file. | 6669 // found in the LICENSE file. |
| 11553 | |
| 11554 cr.define('md_history', function() { | 6670 cr.define('md_history', function() { |
| 11555 var HistoryItem = Polymer({ | 6671 var HistoryItem = Polymer({ |
| 11556 is: 'history-item', | 6672 is: 'history-item', |
| 11557 | |
| 11558 properties: { | 6673 properties: { |
| 11559 // Underlying HistoryEntry data for this item. Contains read-only fields | 6674 item: { |
| 11560 // from the history backend, as well as fields computed by history-list. | 6675 type: Object, |
| 11561 item: {type: Object, observer: 'showIcon_'}, | 6676 observer: 'showIcon_' |
| 11562 | 6677 }, |
| 11563 // Search term used to obtain this history-item. | 6678 searchTerm: { |
| 11564 searchTerm: {type: String}, | 6679 type: String |
| 11565 | 6680 }, |
| 11566 selected: {type: Boolean, notify: true}, | 6681 selected: { |
| 11567 | 6682 type: Boolean, |
| 11568 isFirstItem: {type: Boolean, reflectToAttribute: true}, | 6683 notify: true |
| 11569 | 6684 }, |
| 11570 isCardStart: {type: Boolean, reflectToAttribute: true}, | 6685 isFirstItem: { |
| 11571 | 6686 type: Boolean, |
| 11572 isCardEnd: {type: Boolean, reflectToAttribute: true}, | 6687 reflectToAttribute: true |
| 11573 | 6688 }, |
| 11574 // True if the item is being displayed embedded in another element and | 6689 isCardStart: { |
| 11575 // should not manage its own borders or size. | 6690 type: Boolean, |
| 11576 embedded: {type: Boolean, reflectToAttribute: true}, | 6691 reflectToAttribute: true |
| 11577 | 6692 }, |
| 11578 hasTimeGap: {type: Boolean}, | 6693 isCardEnd: { |
| 11579 | 6694 type: Boolean, |
| 11580 numberOfItems: {type: Number}, | 6695 reflectToAttribute: true |
| 11581 | 6696 }, |
| 11582 // The path of this history item inside its parent. | 6697 embedded: { |
| 11583 path: String, | 6698 type: Boolean, |
| 11584 }, | 6699 reflectToAttribute: true |
| 11585 | 6700 }, |
| 11586 /** | 6701 hasTimeGap: { |
| 11587 * When a history-item is selected the toolbar is notified and increases | 6702 type: Boolean |
| 11588 * or decreases its count of selected items accordingly. | 6703 }, |
| 11589 * @param {MouseEvent} e | 6704 numberOfItems: { |
| 11590 * @private | 6705 type: Number |
| 11591 */ | 6706 }, |
| 6707 path: String |
| 6708 }, |
| 11592 onCheckboxSelected_: function(e) { | 6709 onCheckboxSelected_: function(e) { |
| 11593 // TODO(calamity): Fire this event whenever |selected| changes. | |
| 11594 this.fire('history-checkbox-select', { | 6710 this.fire('history-checkbox-select', { |
| 11595 element: this, | 6711 element: this, |
| 11596 shiftKey: e.shiftKey, | 6712 shiftKey: e.shiftKey |
| 11597 }); | 6713 }); |
| 11598 e.preventDefault(); | 6714 e.preventDefault(); |
| 11599 }, | 6715 }, |
| 11600 | |
| 11601 /** | |
| 11602 * @param {MouseEvent} e | |
| 11603 * @private | |
| 11604 */ | |
| 11605 onCheckboxMousedown_: function(e) { | 6716 onCheckboxMousedown_: function(e) { |
| 11606 // Prevent shift clicking a checkbox from selecting text. | 6717 if (e.shiftKey) e.preventDefault(); |
| 11607 if (e.shiftKey) | 6718 }, |
| 11608 e.preventDefault(); | |
| 11609 }, | |
| 11610 | |
| 11611 /** | |
| 11612 * Remove bookmark of current item when bookmark-star is clicked. | |
| 11613 * @private | |
| 11614 */ | |
| 11615 onRemoveBookmarkTap_: function() { | 6719 onRemoveBookmarkTap_: function() { |
| 11616 if (!this.item.starred) | 6720 if (!this.item.starred) return; |
| 11617 return; | 6721 if (this.$$('#bookmark-star') == this.root.activeElement) this.$['menu-but
ton'].focus(); |
| 11618 | 6722 md_history.BrowserService.getInstance().removeBookmark(this.item.url); |
| 11619 if (this.$$('#bookmark-star') == this.root.activeElement) | |
| 11620 this.$['menu-button'].focus(); | |
| 11621 | |
| 11622 md_history.BrowserService.getInstance() | |
| 11623 .removeBookmark(this.item.url); | |
| 11624 this.fire('remove-bookmark-stars', this.item.url); | 6723 this.fire('remove-bookmark-stars', this.item.url); |
| 11625 }, | 6724 }, |
| 11626 | |
| 11627 /** | |
| 11628 * Fires a custom event when the menu button is clicked. Sends the details | |
| 11629 * of the history item and where the menu should appear. | |
| 11630 */ | |
| 11631 onMenuButtonTap_: function(e) { | 6725 onMenuButtonTap_: function(e) { |
| 11632 this.fire('toggle-menu', { | 6726 this.fire('toggle-menu', { |
| 11633 target: Polymer.dom(e).localTarget, | 6727 target: Polymer.dom(e).localTarget, |
| 11634 item: this.item, | 6728 item: this.item, |
| 11635 path: this.path, | 6729 path: this.path |
| 11636 }); | 6730 }); |
| 11637 | |
| 11638 // Stops the 'tap' event from closing the menu when it opens. | |
| 11639 e.stopPropagation(); | 6731 e.stopPropagation(); |
| 11640 }, | 6732 }, |
| 11641 | |
| 11642 /** | |
| 11643 * Set the favicon image, based on the URL of the history item. | |
| 11644 * @private | |
| 11645 */ | |
| 11646 showIcon_: function() { | 6733 showIcon_: function() { |
| 11647 this.$.icon.style.backgroundImage = | 6734 this.$.icon.style.backgroundImage = cr.icon.getFaviconImageSet(this.item.u
rl); |
| 11648 cr.icon.getFaviconImageSet(this.item.url); | 6735 }, |
| 11649 }, | |
| 11650 | |
| 11651 selectionNotAllowed_: function() { | 6736 selectionNotAllowed_: function() { |
| 11652 return !loadTimeData.getBoolean('allowDeletingHistory'); | 6737 return !loadTimeData.getBoolean('allowDeletingHistory'); |
| 11653 }, | 6738 }, |
| 11654 | |
| 11655 /** | |
| 11656 * Generates the title for this history card. | |
| 11657 * @param {number} numberOfItems The number of items in the card. | |
| 11658 * @param {string} search The search term associated with these results. | |
| 11659 * @private | |
| 11660 */ | |
| 11661 cardTitle_: function(numberOfItems, historyDate, search) { | 6739 cardTitle_: function(numberOfItems, historyDate, search) { |
| 11662 if (!search) | 6740 if (!search) return this.item.dateRelativeDay; |
| 11663 return this.item.dateRelativeDay; | |
| 11664 | |
| 11665 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; | 6741 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; |
| 11666 return loadTimeData.getStringF('foundSearchResults', numberOfItems, | 6742 return loadTimeData.getStringF('foundSearchResults', numberOfItems, loadTi
meData.getString(resultId), search); |
| 11667 loadTimeData.getString(resultId), search); | 6743 }, |
| 11668 }, | |
| 11669 | |
| 11670 /** | |
| 11671 * Crop long item titles to reduce their effect on layout performance. See | |
| 11672 * crbug.com/621347. | |
| 11673 * @param {string} title | |
| 11674 * @return {string} | |
| 11675 */ | |
| 11676 cropItemTitle_: function(title) { | 6744 cropItemTitle_: function(title) { |
| 11677 return (title.length > TITLE_MAX_LENGTH) ? | 6745 return title.length > TITLE_MAX_LENGTH ? title.substr(0, TITLE_MAX_LENGTH)
: title; |
| 11678 title.substr(0, TITLE_MAX_LENGTH) : | |
| 11679 title; | |
| 11680 } | 6746 } |
| 11681 }); | 6747 }); |
| 11682 | |
| 11683 /** | |
| 11684 * Check whether the time difference between the given history item and the | |
| 11685 * next one is large enough for a spacer to be required. | |
| 11686 * @param {Array<HistoryEntry>} visits | |
| 11687 * @param {number} currentIndex | |
| 11688 * @param {string} searchedTerm | |
| 11689 * @return {boolean} Whether or not time gap separator is required. | |
| 11690 * @private | |
| 11691 */ | |
| 11692 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { | 6748 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { |
| 11693 if (currentIndex >= visits.length - 1 || visits.length == 0) | 6749 if (currentIndex >= visits.length - 1 || visits.length == 0) return false; |
| 11694 return false; | |
| 11695 | |
| 11696 var currentItem = visits[currentIndex]; | 6750 var currentItem = visits[currentIndex]; |
| 11697 var nextItem = visits[currentIndex + 1]; | 6751 var nextItem = visits[currentIndex + 1]; |
| 11698 | 6752 if (searchedTerm) return currentItem.dateShort != nextItem.dateShort; |
| 11699 if (searchedTerm) | 6753 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && currentItem.d
ateRelativeDay == nextItem.dateRelativeDay; |
| 11700 return currentItem.dateShort != nextItem.dateShort; | |
| 11701 | |
| 11702 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && | |
| 11703 currentItem.dateRelativeDay == nextItem.dateRelativeDay; | |
| 11704 }; | 6754 }; |
| 11705 | 6755 return { |
| 11706 return { HistoryItem: HistoryItem }; | 6756 HistoryItem: HistoryItem |
| 11707 }); | 6757 }; |
| 6758 }); |
| 6759 |
| 11708 // Copyright 2016 The Chromium Authors. All rights reserved. | 6760 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11709 // Use of this source code is governed by a BSD-style license that can be | 6761 // Use of this source code is governed by a BSD-style license that can be |
| 11710 // found in the LICENSE file. | 6762 // found in the LICENSE file. |
| 11711 | |
| 11712 /** | |
| 11713 * @constructor | |
| 11714 * @param {string} currentPath | |
| 11715 */ | |
| 11716 var SelectionTreeNode = function(currentPath) { | 6763 var SelectionTreeNode = function(currentPath) { |
| 11717 /** @type {string} */ | |
| 11718 this.currentPath = currentPath; | 6764 this.currentPath = currentPath; |
| 11719 /** @type {boolean} */ | |
| 11720 this.leaf = false; | 6765 this.leaf = false; |
| 11721 /** @type {Array<number>} */ | |
| 11722 this.indexes = []; | 6766 this.indexes = []; |
| 11723 /** @type {Array<SelectionTreeNode>} */ | |
| 11724 this.children = []; | 6767 this.children = []; |
| 11725 }; | 6768 }; |
| 11726 | 6769 |
| 11727 /** | |
| 11728 * @param {number} index | |
| 11729 * @param {string} path | |
| 11730 */ | |
| 11731 SelectionTreeNode.prototype.addChild = function(index, path) { | 6770 SelectionTreeNode.prototype.addChild = function(index, path) { |
| 11732 this.indexes.push(index); | 6771 this.indexes.push(index); |
| 11733 this.children[index] = new SelectionTreeNode(path); | 6772 this.children[index] = new SelectionTreeNode(path); |
| 11734 }; | 6773 }; |
| 11735 | 6774 |
| 11736 /** @polymerBehavior */ | |
| 11737 var HistoryListBehavior = { | 6775 var HistoryListBehavior = { |
| 11738 properties: { | 6776 properties: { |
| 11739 /** | |
| 11740 * Polymer paths to the history items contained in this list. | |
| 11741 * @type {!Set<string>} selectedPaths | |
| 11742 */ | |
| 11743 selectedPaths: { | 6777 selectedPaths: { |
| 11744 type: Object, | 6778 type: Object, |
| 11745 value: /** @return {!Set<string>} */ function() { return new Set(); } | 6779 value: function() { |
| 11746 }, | 6780 return new Set(); |
| 11747 | 6781 } |
| 11748 lastSelectedPath: String, | 6782 }, |
| 11749 }, | 6783 lastSelectedPath: String |
| 11750 | 6784 }, |
| 11751 listeners: { | 6785 listeners: { |
| 11752 'history-checkbox-select': 'itemSelected_', | 6786 'history-checkbox-select': 'itemSelected_' |
| 11753 }, | 6787 }, |
| 11754 | 6788 hasResults: function(historyDataLength) { |
| 11755 /** | 6789 return historyDataLength > 0; |
| 11756 * @param {number} historyDataLength | 6790 }, |
| 11757 * @return {boolean} | |
| 11758 * @private | |
| 11759 */ | |
| 11760 hasResults: function(historyDataLength) { return historyDataLength > 0; }, | |
| 11761 | |
| 11762 /** | |
| 11763 * @param {string} searchedTerm | |
| 11764 * @param {boolean} isLoading | |
| 11765 * @return {string} | |
| 11766 * @private | |
| 11767 */ | |
| 11768 noResultsMessage: function(searchedTerm, isLoading) { | 6791 noResultsMessage: function(searchedTerm, isLoading) { |
| 11769 if (isLoading) | 6792 if (isLoading) return ''; |
| 11770 return ''; | |
| 11771 | |
| 11772 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; | 6793 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
| 11773 return loadTimeData.getString(messageId); | 6794 return loadTimeData.getString(messageId); |
| 11774 }, | 6795 }, |
| 11775 | |
| 11776 /** | |
| 11777 * Deselect each item in |selectedPaths|. | |
| 11778 */ | |
| 11779 unselectAllItems: function() { | 6796 unselectAllItems: function() { |
| 11780 this.selectedPaths.forEach(function(path) { | 6797 this.selectedPaths.forEach(function(path) { |
| 11781 this.set(path + '.selected', false); | 6798 this.set(path + '.selected', false); |
| 11782 }.bind(this)); | 6799 }.bind(this)); |
| 11783 | |
| 11784 this.selectedPaths.clear(); | 6800 this.selectedPaths.clear(); |
| 11785 }, | 6801 }, |
| 11786 | |
| 11787 /** | |
| 11788 * Performs a request to the backend to delete all selected items. If | |
| 11789 * successful, removes them from the view. Does not prompt the user before | |
| 11790 * deleting -- see <history-list-container> for a version of this method which | |
| 11791 * does prompt. | |
| 11792 */ | |
| 11793 deleteSelected: function() { | 6802 deleteSelected: function() { |
| 11794 var toBeRemoved = | 6803 var toBeRemoved = Array.from(this.selectedPaths.values()).map(function(path)
{ |
| 11795 Array.from(this.selectedPaths.values()).map(function(path) { | 6804 return this.get(path); |
| 11796 return this.get(path); | 6805 }.bind(this)); |
| 11797 }.bind(this)); | 6806 md_history.BrowserService.getInstance().deleteItems(toBeRemoved).then(functi
on() { |
| 11798 | 6807 this.removeItemsByPath(Array.from(this.selectedPaths)); |
| 11799 md_history.BrowserService.getInstance() | 6808 this.fire('unselect-all'); |
| 11800 .deleteItems(toBeRemoved) | 6809 }.bind(this)); |
| 11801 .then(function() { | 6810 }, |
| 11802 this.removeItemsByPath(Array.from(this.selectedPaths)); | |
| 11803 this.fire('unselect-all'); | |
| 11804 }.bind(this)); | |
| 11805 }, | |
| 11806 | |
| 11807 /** | |
| 11808 * Removes the history items in |paths|. Assumes paths are of a.0.b.0... | |
| 11809 * structure. | |
| 11810 * | |
| 11811 * We want to use notifySplices to update the arrays for performance reasons | |
| 11812 * which requires manually batching and sending the notifySplices for each | |
| 11813 * level. To do this, we build a tree where each node is an array and then | |
| 11814 * depth traverse it to remove items. Each time a node has all children | |
| 11815 * deleted, we can also remove the node. | |
| 11816 * | |
| 11817 * @param {Array<string>} paths | |
| 11818 * @private | |
| 11819 */ | |
| 11820 removeItemsByPath: function(paths) { | 6811 removeItemsByPath: function(paths) { |
| 11821 if (paths.length == 0) | 6812 if (paths.length == 0) return; |
| 11822 return; | |
| 11823 | |
| 11824 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); | 6813 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); |
| 11825 }, | 6814 }, |
| 11826 | |
| 11827 /** | |
| 11828 * Creates the tree to traverse in order to remove |paths| from this list. | |
| 11829 * Assumes paths are of a.0.b.0... | |
| 11830 * structure. | |
| 11831 * | |
| 11832 * @param {Array<string>} paths | |
| 11833 * @return {SelectionTreeNode} | |
| 11834 * @private | |
| 11835 */ | |
| 11836 buildRemovalTree_: function(paths) { | 6815 buildRemovalTree_: function(paths) { |
| 11837 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); | 6816 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); |
| 11838 | |
| 11839 // Build a tree to each history item specified in |paths|. | |
| 11840 paths.forEach(function(path) { | 6817 paths.forEach(function(path) { |
| 11841 var components = path.split('.'); | 6818 var components = path.split('.'); |
| 11842 var node = rootNode; | 6819 var node = rootNode; |
| 11843 components.shift(); | 6820 components.shift(); |
| 11844 while (components.length > 1) { | 6821 while (components.length > 1) { |
| 11845 var index = Number(components.shift()); | 6822 var index = Number(components.shift()); |
| 11846 var arrayName = components.shift(); | 6823 var arrayName = components.shift(); |
| 11847 | 6824 if (!node.children[index]) node.addChild(index, [ node.currentPath, inde
x, arrayName ].join('.')); |
| 11848 if (!node.children[index]) | |
| 11849 node.addChild(index, [node.currentPath, index, arrayName].join('.')); | |
| 11850 | |
| 11851 node = node.children[index]; | 6825 node = node.children[index]; |
| 11852 } | 6826 } |
| 11853 node.leaf = true; | 6827 node.leaf = true; |
| 11854 node.indexes.push(Number(components.shift())); | 6828 node.indexes.push(Number(components.shift())); |
| 11855 }); | 6829 }); |
| 11856 | |
| 11857 return rootNode; | 6830 return rootNode; |
| 11858 }, | 6831 }, |
| 11859 | |
| 11860 /** | |
| 11861 * Removes the history items underneath |node| and deletes container arrays as | |
| 11862 * they become empty. | |
| 11863 * @param {SelectionTreeNode} node | |
| 11864 * @return {boolean} Whether this node's array should be deleted. | |
| 11865 * @private | |
| 11866 */ | |
| 11867 removeItemsBeneathNode_: function(node) { | 6832 removeItemsBeneathNode_: function(node) { |
| 11868 var array = this.get(node.currentPath); | 6833 var array = this.get(node.currentPath); |
| 11869 var splices = []; | 6834 var splices = []; |
| 11870 | 6835 node.indexes.sort(function(a, b) { |
| 11871 node.indexes.sort(function(a, b) { return b - a; }); | 6836 return b - a; |
| 6837 }); |
| 11872 node.indexes.forEach(function(index) { | 6838 node.indexes.forEach(function(index) { |
| 11873 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { | 6839 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { |
| 11874 var item = array.splice(index, 1); | 6840 var item = array.splice(index, 1); |
| 11875 splices.push({ | 6841 splices.push({ |
| 11876 index: index, | 6842 index: index, |
| 11877 removed: [item], | 6843 removed: [ item ], |
| 11878 addedCount: 0, | 6844 addedCount: 0, |
| 11879 object: array, | 6845 object: array, |
| 11880 type: 'splice' | 6846 type: 'splice' |
| 11881 }); | 6847 }); |
| 11882 } | 6848 } |
| 11883 }.bind(this)); | 6849 }.bind(this)); |
| 11884 | 6850 if (array.length == 0) return true; |
| 11885 if (array.length == 0) | |
| 11886 return true; | |
| 11887 | |
| 11888 // notifySplices gives better performance than individually splicing as it | |
| 11889 // batches all of the updates together. | |
| 11890 this.notifySplices(node.currentPath, splices); | 6851 this.notifySplices(node.currentPath, splices); |
| 11891 return false; | 6852 return false; |
| 11892 }, | 6853 }, |
| 11893 | |
| 11894 /** | |
| 11895 * @param {Event} e | |
| 11896 * @private | |
| 11897 */ | |
| 11898 itemSelected_: function(e) { | 6854 itemSelected_: function(e) { |
| 11899 var item = e.detail.element; | 6855 var item = e.detail.element; |
| 11900 var paths = []; | 6856 var paths = []; |
| 11901 var itemPath = item.path; | 6857 var itemPath = item.path; |
| 11902 | |
| 11903 // Handle shift selection. Change the selection state of all items between | |
| 11904 // |path| and |lastSelected| to the selection state of |item|. | |
| 11905 if (e.detail.shiftKey && this.lastSelectedPath) { | 6858 if (e.detail.shiftKey && this.lastSelectedPath) { |
| 11906 var itemPathComponents = itemPath.split('.'); | 6859 var itemPathComponents = itemPath.split('.'); |
| 11907 var itemIndex = Number(itemPathComponents.pop()); | 6860 var itemIndex = Number(itemPathComponents.pop()); |
| 11908 var itemArrayPath = itemPathComponents.join('.'); | 6861 var itemArrayPath = itemPathComponents.join('.'); |
| 11909 | |
| 11910 var lastItemPathComponents = this.lastSelectedPath.split('.'); | 6862 var lastItemPathComponents = this.lastSelectedPath.split('.'); |
| 11911 var lastItemIndex = Number(lastItemPathComponents.pop()); | 6863 var lastItemIndex = Number(lastItemPathComponents.pop()); |
| 11912 if (itemArrayPath == lastItemPathComponents.join('.')) { | 6864 if (itemArrayPath == lastItemPathComponents.join('.')) { |
| 11913 for (var i = Math.min(itemIndex, lastItemIndex); | 6865 for (var i = Math.min(itemIndex, lastItemIndex); i <= Math.max(itemIndex
, lastItemIndex); i++) { |
| 11914 i <= Math.max(itemIndex, lastItemIndex); i++) { | |
| 11915 paths.push(itemArrayPath + '.' + i); | 6866 paths.push(itemArrayPath + '.' + i); |
| 11916 } | 6867 } |
| 11917 } | 6868 } |
| 11918 } | 6869 } |
| 11919 | 6870 if (paths.length == 0) paths.push(item.path); |
| 11920 if (paths.length == 0) | |
| 11921 paths.push(item.path); | |
| 11922 | |
| 11923 paths.forEach(function(path) { | 6871 paths.forEach(function(path) { |
| 11924 this.set(path + '.selected', item.selected); | 6872 this.set(path + '.selected', item.selected); |
| 11925 | |
| 11926 if (item.selected) { | 6873 if (item.selected) { |
| 11927 this.selectedPaths.add(path); | 6874 this.selectedPaths.add(path); |
| 11928 return; | 6875 return; |
| 11929 } | 6876 } |
| 11930 | |
| 11931 this.selectedPaths.delete(path); | 6877 this.selectedPaths.delete(path); |
| 11932 }.bind(this)); | 6878 }.bind(this)); |
| 6879 this.lastSelectedPath = itemPath; |
| 6880 } |
| 6881 }; |
| 11933 | 6882 |
| 11934 this.lastSelectedPath = itemPath; | |
| 11935 }, | |
| 11936 }; | |
| 11937 // Copyright 2016 The Chromium Authors. All rights reserved. | 6883 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11938 // Use of this source code is governed by a BSD-style license that can be | 6884 // Use of this source code is governed by a BSD-style license that can be |
| 11939 // found in the LICENSE file. | 6885 // found in the LICENSE file. |
| 11940 | |
| 11941 /** | |
| 11942 * @typedef {{domain: string, | |
| 11943 * visits: !Array<HistoryEntry>, | |
| 11944 * rendered: boolean, | |
| 11945 * expanded: boolean}} | |
| 11946 */ | |
| 11947 var HistoryDomain; | 6886 var HistoryDomain; |
| 11948 | 6887 |
| 11949 /** | |
| 11950 * @typedef {{title: string, | |
| 11951 * domains: !Array<HistoryDomain>}} | |
| 11952 */ | |
| 11953 var HistoryGroup; | 6888 var HistoryGroup; |
| 11954 | 6889 |
| 11955 Polymer({ | 6890 Polymer({ |
| 11956 is: 'history-grouped-list', | 6891 is: 'history-grouped-list', |
| 11957 | 6892 behaviors: [ HistoryListBehavior ], |
| 11958 behaviors: [HistoryListBehavior], | |
| 11959 | |
| 11960 properties: { | 6893 properties: { |
| 11961 // An array of history entries in reverse chronological order. | |
| 11962 historyData: { | 6894 historyData: { |
| 11963 type: Array, | 6895 type: Array |
| 11964 }, | 6896 }, |
| 11965 | |
| 11966 /** | |
| 11967 * @type {Array<HistoryGroup>} | |
| 11968 */ | |
| 11969 groupedHistoryData_: { | 6897 groupedHistoryData_: { |
| 11970 type: Array, | 6898 type: Array |
| 11971 }, | 6899 }, |
| 11972 | |
| 11973 searchedTerm: { | 6900 searchedTerm: { |
| 11974 type: String, | 6901 type: String, |
| 11975 value: '' | 6902 value: '' |
| 11976 }, | 6903 }, |
| 11977 | |
| 11978 range: { | 6904 range: { |
| 11979 type: Number, | 6905 type: Number |
| 11980 }, | 6906 }, |
| 11981 | |
| 11982 queryStartTime: String, | 6907 queryStartTime: String, |
| 11983 queryEndTime: String, | 6908 queryEndTime: String |
| 11984 }, | 6909 }, |
| 11985 | 6910 observers: [ 'updateGroupedHistoryData_(range, historyData)' ], |
| 11986 observers: [ | |
| 11987 'updateGroupedHistoryData_(range, historyData)' | |
| 11988 ], | |
| 11989 | |
| 11990 /** | |
| 11991 * Make a list of domains from visits. | |
| 11992 * @param {!Array<!HistoryEntry>} visits | |
| 11993 * @return {!Array<!HistoryDomain>} | |
| 11994 */ | |
| 11995 createHistoryDomains_: function(visits) { | 6911 createHistoryDomains_: function(visits) { |
| 11996 var domainIndexes = {}; | 6912 var domainIndexes = {}; |
| 11997 var domains = []; | 6913 var domains = []; |
| 11998 | |
| 11999 // Group the visits into a dictionary and generate a list of domains. | |
| 12000 for (var i = 0, visit; visit = visits[i]; i++) { | 6914 for (var i = 0, visit; visit = visits[i]; i++) { |
| 12001 var domain = visit.domain; | 6915 var domain = visit.domain; |
| 12002 if (domainIndexes[domain] == undefined) { | 6916 if (domainIndexes[domain] == undefined) { |
| 12003 domainIndexes[domain] = domains.length; | 6917 domainIndexes[domain] = domains.length; |
| 12004 domains.push({ | 6918 domains.push({ |
| 12005 domain: domain, | 6919 domain: domain, |
| 12006 visits: [], | 6920 visits: [], |
| 12007 expanded: false, | 6921 expanded: false, |
| 12008 rendered: false, | 6922 rendered: false |
| 12009 }); | 6923 }); |
| 12010 } | 6924 } |
| 12011 domains[domainIndexes[domain]].visits.push(visit); | 6925 domains[domainIndexes[domain]].visits.push(visit); |
| 12012 } | 6926 } |
| 12013 var sortByVisits = function(a, b) { | 6927 var sortByVisits = function(a, b) { |
| 12014 return b.visits.length - a.visits.length; | 6928 return b.visits.length - a.visits.length; |
| 12015 }; | 6929 }; |
| 12016 domains.sort(sortByVisits); | 6930 domains.sort(sortByVisits); |
| 12017 | |
| 12018 return domains; | 6931 return domains; |
| 12019 }, | 6932 }, |
| 12020 | |
| 12021 updateGroupedHistoryData_: function() { | 6933 updateGroupedHistoryData_: function() { |
| 12022 if (this.historyData.length == 0) { | 6934 if (this.historyData.length == 0) { |
| 12023 this.groupedHistoryData_ = []; | 6935 this.groupedHistoryData_ = []; |
| 12024 return; | 6936 return; |
| 12025 } | 6937 } |
| 12026 | |
| 12027 if (this.range == HistoryRange.WEEK) { | 6938 if (this.range == HistoryRange.WEEK) { |
| 12028 // Group each day into a list of results. | |
| 12029 var days = []; | 6939 var days = []; |
| 12030 var currentDayVisits = [this.historyData[0]]; | 6940 var currentDayVisits = [ this.historyData[0] ]; |
| 12031 | |
| 12032 var pushCurrentDay = function() { | 6941 var pushCurrentDay = function() { |
| 12033 days.push({ | 6942 days.push({ |
| 12034 title: this.searchedTerm ? currentDayVisits[0].dateShort : | 6943 title: this.searchedTerm ? currentDayVisits[0].dateShort : currentDayV
isits[0].dateRelativeDay, |
| 12035 currentDayVisits[0].dateRelativeDay, | 6944 domains: this.createHistoryDomains_(currentDayVisits) |
| 12036 domains: this.createHistoryDomains_(currentDayVisits), | |
| 12037 }); | 6945 }); |
| 12038 }.bind(this); | 6946 }.bind(this); |
| 12039 | |
| 12040 var visitsSameDay = function(a, b) { | 6947 var visitsSameDay = function(a, b) { |
| 12041 if (this.searchedTerm) | 6948 if (this.searchedTerm) return a.dateShort == b.dateShort; |
| 12042 return a.dateShort == b.dateShort; | |
| 12043 | |
| 12044 return a.dateRelativeDay == b.dateRelativeDay; | 6949 return a.dateRelativeDay == b.dateRelativeDay; |
| 12045 }.bind(this); | 6950 }.bind(this); |
| 12046 | |
| 12047 for (var i = 1; i < this.historyData.length; i++) { | 6951 for (var i = 1; i < this.historyData.length; i++) { |
| 12048 var visit = this.historyData[i]; | 6952 var visit = this.historyData[i]; |
| 12049 if (!visitsSameDay(visit, currentDayVisits[0])) { | 6953 if (!visitsSameDay(visit, currentDayVisits[0])) { |
| 12050 pushCurrentDay(); | 6954 pushCurrentDay(); |
| 12051 currentDayVisits = []; | 6955 currentDayVisits = []; |
| 12052 } | 6956 } |
| 12053 currentDayVisits.push(visit); | 6957 currentDayVisits.push(visit); |
| 12054 } | 6958 } |
| 12055 pushCurrentDay(); | 6959 pushCurrentDay(); |
| 12056 | |
| 12057 this.groupedHistoryData_ = days; | 6960 this.groupedHistoryData_ = days; |
| 12058 } else if (this.range == HistoryRange.MONTH) { | 6961 } else if (this.range == HistoryRange.MONTH) { |
| 12059 // Group each all visits into a single list. | 6962 this.groupedHistoryData_ = [ { |
| 12060 this.groupedHistoryData_ = [{ | |
| 12061 title: this.queryStartTime + ' – ' + this.queryEndTime, | 6963 title: this.queryStartTime + ' – ' + this.queryEndTime, |
| 12062 domains: this.createHistoryDomains_(this.historyData) | 6964 domains: this.createHistoryDomains_(this.historyData) |
| 12063 }]; | 6965 } ]; |
| 12064 } | 6966 } |
| 12065 }, | 6967 }, |
| 12066 | |
| 12067 /** | |
| 12068 * @param {{model:Object, currentTarget:IronCollapseElement}} e | |
| 12069 */ | |
| 12070 toggleDomainExpanded_: function(e) { | 6968 toggleDomainExpanded_: function(e) { |
| 12071 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); | 6969 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); |
| 12072 e.model.set('domain.rendered', true); | 6970 e.model.set('domain.rendered', true); |
| 12073 | 6971 setTimeout(function() { |
| 12074 // Give the history-items time to render. | 6972 collapse.toggle(); |
| 12075 setTimeout(function() { collapse.toggle() }, 0); | 6973 }, 0); |
| 12076 }, | 6974 }, |
| 12077 | |
| 12078 /** | |
| 12079 * Check whether the time difference between the given history item and the | |
| 12080 * next one is large enough for a spacer to be required. | |
| 12081 * @param {number} groupIndex | |
| 12082 * @param {number} domainIndex | |
| 12083 * @param {number} itemIndex | |
| 12084 * @return {boolean} Whether or not time gap separator is required. | |
| 12085 * @private | |
| 12086 */ | |
| 12087 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { | 6975 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { |
| 12088 var visits = | 6976 var visits = this.groupedHistoryData_[groupIndex].domains[domainIndex].visit
s; |
| 12089 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; | 6977 return md_history.HistoryItem.needsTimeGap(visits, itemIndex, this.searchedT
erm); |
| 12090 | 6978 }, |
| 12091 return md_history.HistoryItem.needsTimeGap( | |
| 12092 visits, itemIndex, this.searchedTerm); | |
| 12093 }, | |
| 12094 | |
| 12095 /** | |
| 12096 * @param {number} groupIndex | |
| 12097 * @param {number} domainIndex | |
| 12098 * @param {number} itemIndex | |
| 12099 * @return {string} | |
| 12100 * @private | |
| 12101 */ | |
| 12102 pathForItem_: function(groupIndex, domainIndex, itemIndex) { | 6979 pathForItem_: function(groupIndex, domainIndex, itemIndex) { |
| 12103 return [ | 6980 return [ 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits'
, itemIndex ].join('.'); |
| 12104 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', | 6981 }, |
| 12105 itemIndex | |
| 12106 ].join('.'); | |
| 12107 }, | |
| 12108 | |
| 12109 /** | |
| 12110 * @param {HistoryDomain} domain | |
| 12111 * @return {string} | |
| 12112 * @private | |
| 12113 */ | |
| 12114 getWebsiteIconStyle_: function(domain) { | 6982 getWebsiteIconStyle_: function(domain) { |
| 12115 return 'background-image: ' + | 6983 return 'background-image: ' + cr.icon.getFaviconImageSet(domain.visits[0].ur
l); |
| 12116 cr.icon.getFaviconImageSet(domain.visits[0].url); | 6984 }, |
| 12117 }, | |
| 12118 | |
| 12119 /** | |
| 12120 * @param {boolean} expanded | |
| 12121 * @return {string} | |
| 12122 * @private | |
| 12123 */ | |
| 12124 getDropdownIcon_: function(expanded) { | 6985 getDropdownIcon_: function(expanded) { |
| 12125 return expanded ? 'cr:expand-less' : 'cr:expand-more'; | 6986 return expanded ? 'cr:expand-less' : 'cr:expand-more'; |
| 12126 }, | 6987 } |
| 12127 }); | 6988 }); |
| 12128 /** | |
| 12129 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
| 12130 * designated scroll target. | |
| 12131 * | |
| 12132 * Elements that consume this behavior can override the `_scrollHandler` | |
| 12133 * method to add logic on the scroll event. | |
| 12134 * | |
| 12135 * @demo demo/scrolling-region.html Scrolling Region | |
| 12136 * @demo demo/document.html Document Element | |
| 12137 * @polymerBehavior | |
| 12138 */ | |
| 12139 Polymer.IronScrollTargetBehavior = { | |
| 12140 | 6989 |
| 12141 properties: { | 6990 Polymer.IronScrollTargetBehavior = { |
| 6991 properties: { |
| 6992 scrollTarget: { |
| 6993 type: HTMLElement, |
| 6994 value: function() { |
| 6995 return this._defaultScrollTarget; |
| 6996 } |
| 6997 } |
| 6998 }, |
| 6999 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ], |
| 7000 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 7001 var eventTarget; |
| 7002 if (this._oldScrollTarget) { |
| 7003 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro
llTarget; |
| 7004 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 7005 this._oldScrollTarget = null; |
| 7006 } |
| 7007 if (!isAttached) { |
| 7008 return; |
| 7009 } |
| 7010 if (scrollTarget === 'document') { |
| 7011 this.scrollTarget = this._doc; |
| 7012 } else if (typeof scrollTarget === 'string') { |
| 7013 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer.
dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 7014 } else if (this._isValidScrollTarget()) { |
| 7015 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 7016 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler
.bind(this); |
| 7017 this._oldScrollTarget = scrollTarget; |
| 7018 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 7019 } |
| 7020 }, |
| 7021 _scrollHandler: function scrollHandler() {}, |
| 7022 get _defaultScrollTarget() { |
| 7023 return this._doc; |
| 7024 }, |
| 7025 get _doc() { |
| 7026 return this.ownerDocument.documentElement; |
| 7027 }, |
| 7028 get _scrollTop() { |
| 7029 if (this._isValidScrollTarget()) { |
| 7030 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT
arget.scrollTop; |
| 7031 } |
| 7032 return 0; |
| 7033 }, |
| 7034 get _scrollLeft() { |
| 7035 if (this._isValidScrollTarget()) { |
| 7036 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT
arget.scrollLeft; |
| 7037 } |
| 7038 return 0; |
| 7039 }, |
| 7040 set _scrollTop(top) { |
| 7041 if (this.scrollTarget === this._doc) { |
| 7042 window.scrollTo(window.pageXOffset, top); |
| 7043 } else if (this._isValidScrollTarget()) { |
| 7044 this.scrollTarget.scrollTop = top; |
| 7045 } |
| 7046 }, |
| 7047 set _scrollLeft(left) { |
| 7048 if (this.scrollTarget === this._doc) { |
| 7049 window.scrollTo(left, window.pageYOffset); |
| 7050 } else if (this._isValidScrollTarget()) { |
| 7051 this.scrollTarget.scrollLeft = left; |
| 7052 } |
| 7053 }, |
| 7054 scroll: function(left, top) { |
| 7055 if (this.scrollTarget === this._doc) { |
| 7056 window.scrollTo(left, top); |
| 7057 } else if (this._isValidScrollTarget()) { |
| 7058 this.scrollTarget.scrollLeft = left; |
| 7059 this.scrollTarget.scrollTop = top; |
| 7060 } |
| 7061 }, |
| 7062 get _scrollTargetWidth() { |
| 7063 if (this._isValidScrollTarget()) { |
| 7064 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa
rget.offsetWidth; |
| 7065 } |
| 7066 return 0; |
| 7067 }, |
| 7068 get _scrollTargetHeight() { |
| 7069 if (this._isValidScrollTarget()) { |
| 7070 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT
arget.offsetHeight; |
| 7071 } |
| 7072 return 0; |
| 7073 }, |
| 7074 _isValidScrollTarget: function() { |
| 7075 return this.scrollTarget instanceof HTMLElement; |
| 7076 } |
| 7077 }; |
| 12142 | 7078 |
| 12143 /** | |
| 12144 * Specifies the element that will handle the scroll event | |
| 12145 * on the behalf of the current element. This is typically a reference to
an element, | |
| 12146 * but there are a few more posibilities: | |
| 12147 * | |
| 12148 * ### Elements id | |
| 12149 * | |
| 12150 *```html | |
| 12151 * <div id="scrollable-element" style="overflow: auto;"> | |
| 12152 * <x-element scroll-target="scrollable-element"> | |
| 12153 * \x3c!-- Content--\x3e | |
| 12154 * </x-element> | |
| 12155 * </div> | |
| 12156 *``` | |
| 12157 * In this case, the `scrollTarget` will point to the outer div element. | |
| 12158 * | |
| 12159 * ### Document scrolling | |
| 12160 * | |
| 12161 * For document scrolling, you can use the reserved word `document`: | |
| 12162 * | |
| 12163 *```html | |
| 12164 * <x-element scroll-target="document"> | |
| 12165 * \x3c!-- Content --\x3e | |
| 12166 * </x-element> | |
| 12167 *``` | |
| 12168 * | |
| 12169 * ### Elements reference | |
| 12170 * | |
| 12171 *```js | |
| 12172 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
| 12173 *``` | |
| 12174 * | |
| 12175 * @type {HTMLElement} | |
| 12176 */ | |
| 12177 scrollTarget: { | |
| 12178 type: HTMLElement, | |
| 12179 value: function() { | |
| 12180 return this._defaultScrollTarget; | |
| 12181 } | |
| 12182 } | |
| 12183 }, | |
| 12184 | |
| 12185 observers: [ | |
| 12186 '_scrollTargetChanged(scrollTarget, isAttached)' | |
| 12187 ], | |
| 12188 | |
| 12189 _scrollTargetChanged: function(scrollTarget, isAttached) { | |
| 12190 var eventTarget; | |
| 12191 | |
| 12192 if (this._oldScrollTarget) { | |
| 12193 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | |
| 12194 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | |
| 12195 this._oldScrollTarget = null; | |
| 12196 } | |
| 12197 | |
| 12198 if (!isAttached) { | |
| 12199 return; | |
| 12200 } | |
| 12201 // Support element id references | |
| 12202 if (scrollTarget === 'document') { | |
| 12203 | |
| 12204 this.scrollTarget = this._doc; | |
| 12205 | |
| 12206 } else if (typeof scrollTarget === 'string') { | |
| 12207 | |
| 12208 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | |
| 12209 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | |
| 12210 | |
| 12211 } else if (this._isValidScrollTarget()) { | |
| 12212 | |
| 12213 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | |
| 12214 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | |
| 12215 this._oldScrollTarget = scrollTarget; | |
| 12216 | |
| 12217 eventTarget.addEventListener('scroll', this._boundScrollHandler); | |
| 12218 } | |
| 12219 }, | |
| 12220 | |
| 12221 /** | |
| 12222 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
| 12223 * | |
| 12224 * @protected | |
| 12225 */ | |
| 12226 _scrollHandler: function scrollHandler() {}, | |
| 12227 | |
| 12228 /** | |
| 12229 * The default scroll target. Consumers of this behavior may want to customi
ze | |
| 12230 * the default scroll target. | |
| 12231 * | |
| 12232 * @type {Element} | |
| 12233 */ | |
| 12234 get _defaultScrollTarget() { | |
| 12235 return this._doc; | |
| 12236 }, | |
| 12237 | |
| 12238 /** | |
| 12239 * Shortcut for the document element | |
| 12240 * | |
| 12241 * @type {Element} | |
| 12242 */ | |
| 12243 get _doc() { | |
| 12244 return this.ownerDocument.documentElement; | |
| 12245 }, | |
| 12246 | |
| 12247 /** | |
| 12248 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 12249 * | |
| 12250 * @type {number} | |
| 12251 */ | |
| 12252 get _scrollTop() { | |
| 12253 if (this._isValidScrollTarget()) { | |
| 12254 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
| 12255 } | |
| 12256 return 0; | |
| 12257 }, | |
| 12258 | |
| 12259 /** | |
| 12260 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
| 12261 * | |
| 12262 * @type {number} | |
| 12263 */ | |
| 12264 get _scrollLeft() { | |
| 12265 if (this._isValidScrollTarget()) { | |
| 12266 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
| 12267 } | |
| 12268 return 0; | |
| 12269 }, | |
| 12270 | |
| 12271 /** | |
| 12272 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 12273 * | |
| 12274 * @type {number} | |
| 12275 */ | |
| 12276 set _scrollTop(top) { | |
| 12277 if (this.scrollTarget === this._doc) { | |
| 12278 window.scrollTo(window.pageXOffset, top); | |
| 12279 } else if (this._isValidScrollTarget()) { | |
| 12280 this.scrollTarget.scrollTop = top; | |
| 12281 } | |
| 12282 }, | |
| 12283 | |
| 12284 /** | |
| 12285 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
| 12286 * | |
| 12287 * @type {number} | |
| 12288 */ | |
| 12289 set _scrollLeft(left) { | |
| 12290 if (this.scrollTarget === this._doc) { | |
| 12291 window.scrollTo(left, window.pageYOffset); | |
| 12292 } else if (this._isValidScrollTarget()) { | |
| 12293 this.scrollTarget.scrollLeft = left; | |
| 12294 } | |
| 12295 }, | |
| 12296 | |
| 12297 /** | |
| 12298 * Scrolls the content to a particular place. | |
| 12299 * | |
| 12300 * @method scroll | |
| 12301 * @param {number} left The left position | |
| 12302 * @param {number} top The top position | |
| 12303 */ | |
| 12304 scroll: function(left, top) { | |
| 12305 if (this.scrollTarget === this._doc) { | |
| 12306 window.scrollTo(left, top); | |
| 12307 } else if (this._isValidScrollTarget()) { | |
| 12308 this.scrollTarget.scrollLeft = left; | |
| 12309 this.scrollTarget.scrollTop = top; | |
| 12310 } | |
| 12311 }, | |
| 12312 | |
| 12313 /** | |
| 12314 * Gets the width of the scroll target. | |
| 12315 * | |
| 12316 * @type {number} | |
| 12317 */ | |
| 12318 get _scrollTargetWidth() { | |
| 12319 if (this._isValidScrollTarget()) { | |
| 12320 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
| 12321 } | |
| 12322 return 0; | |
| 12323 }, | |
| 12324 | |
| 12325 /** | |
| 12326 * Gets the height of the scroll target. | |
| 12327 * | |
| 12328 * @type {number} | |
| 12329 */ | |
| 12330 get _scrollTargetHeight() { | |
| 12331 if (this._isValidScrollTarget()) { | |
| 12332 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
| 12333 } | |
| 12334 return 0; | |
| 12335 }, | |
| 12336 | |
| 12337 /** | |
| 12338 * Returns true if the scroll target is a valid HTMLElement. | |
| 12339 * | |
| 12340 * @return {boolean} | |
| 12341 */ | |
| 12342 _isValidScrollTarget: function() { | |
| 12343 return this.scrollTarget instanceof HTMLElement; | |
| 12344 } | |
| 12345 }; | |
| 12346 (function() { | 7079 (function() { |
| 12347 | |
| 12348 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 7080 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
| 12349 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 7081 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
| 12350 var DEFAULT_PHYSICAL_COUNT = 3; | 7082 var DEFAULT_PHYSICAL_COUNT = 3; |
| 12351 var HIDDEN_Y = '-10000px'; | 7083 var HIDDEN_Y = '-10000px'; |
| 12352 var DEFAULT_GRID_SIZE = 200; | 7084 var DEFAULT_GRID_SIZE = 200; |
| 12353 var SECRET_TABINDEX = -100; | 7085 var SECRET_TABINDEX = -100; |
| 12354 | |
| 12355 Polymer({ | 7086 Polymer({ |
| 12356 | |
| 12357 is: 'iron-list', | 7087 is: 'iron-list', |
| 12358 | |
| 12359 properties: { | 7088 properties: { |
| 12360 | |
| 12361 /** | |
| 12362 * An array containing items determining how many instances of the templat
e | |
| 12363 * to stamp and that that each template instance should bind to. | |
| 12364 */ | |
| 12365 items: { | 7089 items: { |
| 12366 type: Array | 7090 type: Array |
| 12367 }, | 7091 }, |
| 12368 | |
| 12369 /** | |
| 12370 * The max count of physical items the pool can extend to. | |
| 12371 */ | |
| 12372 maxPhysicalCount: { | 7092 maxPhysicalCount: { |
| 12373 type: Number, | 7093 type: Number, |
| 12374 value: 500 | 7094 value: 500 |
| 12375 }, | 7095 }, |
| 12376 | |
| 12377 /** | |
| 12378 * The name of the variable to add to the binding scope for the array | |
| 12379 * element associated with a given template instance. | |
| 12380 */ | |
| 12381 as: { | 7096 as: { |
| 12382 type: String, | 7097 type: String, |
| 12383 value: 'item' | 7098 value: 'item' |
| 12384 }, | 7099 }, |
| 12385 | |
| 12386 /** | |
| 12387 * The name of the variable to add to the binding scope with the index | |
| 12388 * for the row. | |
| 12389 */ | |
| 12390 indexAs: { | 7100 indexAs: { |
| 12391 type: String, | 7101 type: String, |
| 12392 value: 'index' | 7102 value: 'index' |
| 12393 }, | 7103 }, |
| 12394 | |
| 12395 /** | |
| 12396 * The name of the variable to add to the binding scope to indicate | |
| 12397 * if the row is selected. | |
| 12398 */ | |
| 12399 selectedAs: { | 7104 selectedAs: { |
| 12400 type: String, | 7105 type: String, |
| 12401 value: 'selected' | 7106 value: 'selected' |
| 12402 }, | 7107 }, |
| 12403 | |
| 12404 /** | |
| 12405 * When true, the list is rendered as a grid. Grid items must have | |
| 12406 * fixed width and height set via CSS. e.g. | |
| 12407 * | |
| 12408 * ```html | |
| 12409 * <iron-list grid> | |
| 12410 * <template> | |
| 12411 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
| 12412 * </template> | |
| 12413 * </iron-list> | |
| 12414 * ``` | |
| 12415 */ | |
| 12416 grid: { | 7108 grid: { |
| 12417 type: Boolean, | 7109 type: Boolean, |
| 12418 value: false, | 7110 value: false, |
| 12419 reflectToAttribute: true | 7111 reflectToAttribute: true |
| 12420 }, | 7112 }, |
| 12421 | |
| 12422 /** | |
| 12423 * When true, tapping a row will select the item, placing its data model | |
| 12424 * in the set of selected items retrievable via the selection property. | |
| 12425 * | |
| 12426 * Note that tapping focusable elements within the list item will not | |
| 12427 * result in selection, since they are presumed to have their * own action
. | |
| 12428 */ | |
| 12429 selectionEnabled: { | 7113 selectionEnabled: { |
| 12430 type: Boolean, | 7114 type: Boolean, |
| 12431 value: false | 7115 value: false |
| 12432 }, | 7116 }, |
| 12433 | |
| 12434 /** | |
| 12435 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
| 12436 * if no item is selected. | |
| 12437 */ | |
| 12438 selectedItem: { | 7117 selectedItem: { |
| 12439 type: Object, | 7118 type: Object, |
| 12440 notify: true | 7119 notify: true |
| 12441 }, | 7120 }, |
| 12442 | |
| 12443 /** | |
| 12444 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
| 12445 */ | |
| 12446 selectedItems: { | 7121 selectedItems: { |
| 12447 type: Object, | 7122 type: Object, |
| 12448 notify: true | 7123 notify: true |
| 12449 }, | 7124 }, |
| 12450 | |
| 12451 /** | |
| 12452 * When `true`, multiple items may be selected at once (in this case, | |
| 12453 * `selected` is an array of currently selected items). When `false`, | |
| 12454 * only one item may be selected at a time. | |
| 12455 */ | |
| 12456 multiSelection: { | 7125 multiSelection: { |
| 12457 type: Boolean, | 7126 type: Boolean, |
| 12458 value: false | 7127 value: false |
| 12459 } | 7128 } |
| 12460 }, | 7129 }, |
| 12461 | 7130 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn
abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)'
], |
| 12462 observers: [ | 7131 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro
nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ], |
| 12463 '_itemsChanged(items.*)', | |
| 12464 '_selectionEnabledChanged(selectionEnabled)', | |
| 12465 '_multiSelectionChanged(multiSelection)', | |
| 12466 '_setOverflow(scrollTarget)' | |
| 12467 ], | |
| 12468 | |
| 12469 behaviors: [ | |
| 12470 Polymer.Templatizer, | |
| 12471 Polymer.IronResizableBehavior, | |
| 12472 Polymer.IronA11yKeysBehavior, | |
| 12473 Polymer.IronScrollTargetBehavior | |
| 12474 ], | |
| 12475 | |
| 12476 keyBindings: { | 7132 keyBindings: { |
| 12477 'up': '_didMoveUp', | 7133 up: '_didMoveUp', |
| 12478 'down': '_didMoveDown', | 7134 down: '_didMoveDown', |
| 12479 'enter': '_didEnter' | 7135 enter: '_didEnter' |
| 12480 }, | 7136 }, |
| 12481 | 7137 _ratio: .5, |
| 12482 /** | |
| 12483 * The ratio of hidden tiles that should remain in the scroll direction. | |
| 12484 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
| 12485 */ | |
| 12486 _ratio: 0.5, | |
| 12487 | |
| 12488 /** | |
| 12489 * The padding-top value for the list. | |
| 12490 */ | |
| 12491 _scrollerPaddingTop: 0, | 7138 _scrollerPaddingTop: 0, |
| 12492 | |
| 12493 /** | |
| 12494 * This value is the same as `scrollTop`. | |
| 12495 */ | |
| 12496 _scrollPosition: 0, | 7139 _scrollPosition: 0, |
| 12497 | |
| 12498 /** | |
| 12499 * The sum of the heights of all the tiles in the DOM. | |
| 12500 */ | |
| 12501 _physicalSize: 0, | 7140 _physicalSize: 0, |
| 12502 | |
| 12503 /** | |
| 12504 * The average `offsetHeight` of the tiles observed till now. | |
| 12505 */ | |
| 12506 _physicalAverage: 0, | 7141 _physicalAverage: 0, |
| 12507 | |
| 12508 /** | |
| 12509 * The number of tiles which `offsetHeight` > 0 observed until now. | |
| 12510 */ | |
| 12511 _physicalAverageCount: 0, | 7142 _physicalAverageCount: 0, |
| 12512 | |
| 12513 /** | |
| 12514 * The Y position of the item rendered in the `_physicalStart` | |
| 12515 * tile relative to the scrolling list. | |
| 12516 */ | |
| 12517 _physicalTop: 0, | 7143 _physicalTop: 0, |
| 12518 | |
| 12519 /** | |
| 12520 * The number of items in the list. | |
| 12521 */ | |
| 12522 _virtualCount: 0, | 7144 _virtualCount: 0, |
| 12523 | |
| 12524 /** | |
| 12525 * A map between an item key and its physical item index | |
| 12526 */ | |
| 12527 _physicalIndexForKey: null, | 7145 _physicalIndexForKey: null, |
| 12528 | |
| 12529 /** | |
| 12530 * The estimated scroll height based on `_physicalAverage` | |
| 12531 */ | |
| 12532 _estScrollHeight: 0, | 7146 _estScrollHeight: 0, |
| 12533 | |
| 12534 /** | |
| 12535 * The scroll height of the dom node | |
| 12536 */ | |
| 12537 _scrollHeight: 0, | 7147 _scrollHeight: 0, |
| 12538 | |
| 12539 /** | |
| 12540 * The height of the list. This is referred as the viewport in the context o
f list. | |
| 12541 */ | |
| 12542 _viewportHeight: 0, | 7148 _viewportHeight: 0, |
| 12543 | |
| 12544 /** | |
| 12545 * The width of the list. This is referred as the viewport in the context of
list. | |
| 12546 */ | |
| 12547 _viewportWidth: 0, | 7149 _viewportWidth: 0, |
| 12548 | |
| 12549 /** | |
| 12550 * An array of DOM nodes that are currently in the tree | |
| 12551 * @type {?Array<!TemplatizerNode>} | |
| 12552 */ | |
| 12553 _physicalItems: null, | 7150 _physicalItems: null, |
| 12554 | |
| 12555 /** | |
| 12556 * An array of heights for each item in `_physicalItems` | |
| 12557 * @type {?Array<number>} | |
| 12558 */ | |
| 12559 _physicalSizes: null, | 7151 _physicalSizes: null, |
| 12560 | |
| 12561 /** | |
| 12562 * A cached value for the first visible index. | |
| 12563 * See `firstVisibleIndex` | |
| 12564 * @type {?number} | |
| 12565 */ | |
| 12566 _firstVisibleIndexVal: null, | 7152 _firstVisibleIndexVal: null, |
| 12567 | |
| 12568 /** | |
| 12569 * A cached value for the last visible index. | |
| 12570 * See `lastVisibleIndex` | |
| 12571 * @type {?number} | |
| 12572 */ | |
| 12573 _lastVisibleIndexVal: null, | 7153 _lastVisibleIndexVal: null, |
| 12574 | |
| 12575 /** | |
| 12576 * A Polymer collection for the items. | |
| 12577 * @type {?Polymer.Collection} | |
| 12578 */ | |
| 12579 _collection: null, | 7154 _collection: null, |
| 12580 | |
| 12581 /** | |
| 12582 * True if the current item list was rendered for the first time | |
| 12583 * after attached. | |
| 12584 */ | |
| 12585 _itemsRendered: false, | 7155 _itemsRendered: false, |
| 12586 | |
| 12587 /** | |
| 12588 * The page that is currently rendered. | |
| 12589 */ | |
| 12590 _lastPage: null, | 7156 _lastPage: null, |
| 12591 | |
| 12592 /** | |
| 12593 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
| 12594 */ | |
| 12595 _maxPages: 3, | 7157 _maxPages: 3, |
| 12596 | |
| 12597 /** | |
| 12598 * The currently focused physical item. | |
| 12599 */ | |
| 12600 _focusedItem: null, | 7158 _focusedItem: null, |
| 12601 | |
| 12602 /** | |
| 12603 * The index of the `_focusedItem`. | |
| 12604 */ | |
| 12605 _focusedIndex: -1, | 7159 _focusedIndex: -1, |
| 12606 | |
| 12607 /** | |
| 12608 * The the item that is focused if it is moved offscreen. | |
| 12609 * @private {?TemplatizerNode} | |
| 12610 */ | |
| 12611 _offscreenFocusedItem: null, | 7160 _offscreenFocusedItem: null, |
| 12612 | |
| 12613 /** | |
| 12614 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
| 12615 * list when that item is moved offscreen. | |
| 12616 */ | |
| 12617 _focusBackfillItem: null, | 7161 _focusBackfillItem: null, |
| 12618 | |
| 12619 /** | |
| 12620 * The maximum items per row | |
| 12621 */ | |
| 12622 _itemsPerRow: 1, | 7162 _itemsPerRow: 1, |
| 12623 | |
| 12624 /** | |
| 12625 * The width of each grid item | |
| 12626 */ | |
| 12627 _itemWidth: 0, | 7163 _itemWidth: 0, |
| 12628 | |
| 12629 /** | |
| 12630 * The height of the row in grid layout. | |
| 12631 */ | |
| 12632 _rowHeight: 0, | 7164 _rowHeight: 0, |
| 12633 | |
| 12634 /** | |
| 12635 * The bottom of the physical content. | |
| 12636 */ | |
| 12637 get _physicalBottom() { | 7165 get _physicalBottom() { |
| 12638 return this._physicalTop + this._physicalSize; | 7166 return this._physicalTop + this._physicalSize; |
| 12639 }, | 7167 }, |
| 12640 | |
| 12641 /** | |
| 12642 * The bottom of the scroll. | |
| 12643 */ | |
| 12644 get _scrollBottom() { | 7168 get _scrollBottom() { |
| 12645 return this._scrollPosition + this._viewportHeight; | 7169 return this._scrollPosition + this._viewportHeight; |
| 12646 }, | 7170 }, |
| 12647 | |
| 12648 /** | |
| 12649 * The n-th item rendered in the last physical item. | |
| 12650 */ | |
| 12651 get _virtualEnd() { | 7171 get _virtualEnd() { |
| 12652 return this._virtualStart + this._physicalCount - 1; | 7172 return this._virtualStart + this._physicalCount - 1; |
| 12653 }, | 7173 }, |
| 12654 | |
| 12655 /** | |
| 12656 * The height of the physical content that isn't on the screen. | |
| 12657 */ | |
| 12658 get _hiddenContentSize() { | 7174 get _hiddenContentSize() { |
| 12659 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 7175 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
| 12660 return size - this._viewportHeight; | 7176 return size - this._viewportHeight; |
| 12661 }, | 7177 }, |
| 12662 | |
| 12663 /** | |
| 12664 * The maximum scroll top value. | |
| 12665 */ | |
| 12666 get _maxScrollTop() { | 7178 get _maxScrollTop() { |
| 12667 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 7179 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
| 12668 }, | 7180 }, |
| 12669 | |
| 12670 /** | |
| 12671 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
| 12672 */ | |
| 12673 _minVirtualStart: 0, | 7181 _minVirtualStart: 0, |
| 12674 | |
| 12675 /** | |
| 12676 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
| 12677 */ | |
| 12678 get _maxVirtualStart() { | 7182 get _maxVirtualStart() { |
| 12679 return Math.max(0, this._virtualCount - this._physicalCount); | 7183 return Math.max(0, this._virtualCount - this._physicalCount); |
| 12680 }, | 7184 }, |
| 12681 | |
| 12682 /** | |
| 12683 * The n-th item rendered in the `_physicalStart` tile. | |
| 12684 */ | |
| 12685 _virtualStartVal: 0, | 7185 _virtualStartVal: 0, |
| 12686 | |
| 12687 set _virtualStart(val) { | 7186 set _virtualStart(val) { |
| 12688 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 7187 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
| 12689 }, | 7188 }, |
| 12690 | |
| 12691 get _virtualStart() { | 7189 get _virtualStart() { |
| 12692 return this._virtualStartVal || 0; | 7190 return this._virtualStartVal || 0; |
| 12693 }, | 7191 }, |
| 12694 | |
| 12695 /** | |
| 12696 * The k-th tile that is at the top of the scrolling list. | |
| 12697 */ | |
| 12698 _physicalStartVal: 0, | 7192 _physicalStartVal: 0, |
| 12699 | |
| 12700 set _physicalStart(val) { | 7193 set _physicalStart(val) { |
| 12701 this._physicalStartVal = val % this._physicalCount; | 7194 this._physicalStartVal = val % this._physicalCount; |
| 12702 if (this._physicalStartVal < 0) { | 7195 if (this._physicalStartVal < 0) { |
| 12703 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 7196 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
| 12704 } | 7197 } |
| 12705 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 7198 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12706 }, | 7199 }, |
| 12707 | |
| 12708 get _physicalStart() { | 7200 get _physicalStart() { |
| 12709 return this._physicalStartVal || 0; | 7201 return this._physicalStartVal || 0; |
| 12710 }, | 7202 }, |
| 12711 | |
| 12712 /** | |
| 12713 * The number of tiles in the DOM. | |
| 12714 */ | |
| 12715 _physicalCountVal: 0, | 7203 _physicalCountVal: 0, |
| 12716 | |
| 12717 set _physicalCount(val) { | 7204 set _physicalCount(val) { |
| 12718 this._physicalCountVal = val; | 7205 this._physicalCountVal = val; |
| 12719 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 7206 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12720 }, | 7207 }, |
| 12721 | |
| 12722 get _physicalCount() { | 7208 get _physicalCount() { |
| 12723 return this._physicalCountVal; | 7209 return this._physicalCountVal; |
| 12724 }, | 7210 }, |
| 12725 | |
| 12726 /** | |
| 12727 * The k-th tile that is at the bottom of the scrolling list. | |
| 12728 */ | |
| 12729 _physicalEnd: 0, | 7211 _physicalEnd: 0, |
| 12730 | |
| 12731 /** | |
| 12732 * An optimal physical size such that we will have enough physical items | |
| 12733 * to fill up the viewport and recycle when the user scrolls. | |
| 12734 * | |
| 12735 * This default value assumes that we will at least have the equivalent | |
| 12736 * to a viewport of physical items above and below the user's viewport. | |
| 12737 */ | |
| 12738 get _optPhysicalSize() { | 7212 get _optPhysicalSize() { |
| 12739 if (this.grid) { | 7213 if (this.grid) { |
| 12740 return this._estRowsInView * this._rowHeight * this._maxPages; | 7214 return this._estRowsInView * this._rowHeight * this._maxPages; |
| 12741 } | 7215 } |
| 12742 return this._viewportHeight * this._maxPages; | 7216 return this._viewportHeight * this._maxPages; |
| 12743 }, | 7217 }, |
| 12744 | |
| 12745 get _optPhysicalCount() { | 7218 get _optPhysicalCount() { |
| 12746 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 7219 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
| 12747 }, | 7220 }, |
| 12748 | |
| 12749 /** | |
| 12750 * True if the current list is visible. | |
| 12751 */ | |
| 12752 get _isVisible() { | 7221 get _isVisible() { |
| 12753 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 7222 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
| 12754 }, | 7223 }, |
| 12755 | |
| 12756 /** | |
| 12757 * Gets the index of the first visible item in the viewport. | |
| 12758 * | |
| 12759 * @type {number} | |
| 12760 */ | |
| 12761 get firstVisibleIndex() { | 7224 get firstVisibleIndex() { |
| 12762 if (this._firstVisibleIndexVal === null) { | 7225 if (this._firstVisibleIndexVal === null) { |
| 12763 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 7226 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
| 12764 | 7227 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) { |
| 12765 this._firstVisibleIndexVal = this._iterateItems( | 7228 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12766 function(pidx, vidx) { | 7229 if (physicalOffset > this._scrollPosition) { |
| 12767 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 7230 return this.grid ? vidx - vidx % this._itemsPerRow : vidx; |
| 12768 | 7231 } |
| 12769 if (physicalOffset > this._scrollPosition) { | 7232 if (this.grid && this._virtualCount - 1 === vidx) { |
| 12770 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 7233 return vidx - vidx % this._itemsPerRow; |
| 12771 } | 7234 } |
| 12772 // Handle a partially rendered final row in grid mode | 7235 }) || 0; |
| 12773 if (this.grid && this._virtualCount - 1 === vidx) { | |
| 12774 return vidx - (vidx % this._itemsPerRow); | |
| 12775 } | |
| 12776 }) || 0; | |
| 12777 } | 7236 } |
| 12778 return this._firstVisibleIndexVal; | 7237 return this._firstVisibleIndexVal; |
| 12779 }, | 7238 }, |
| 12780 | |
| 12781 /** | |
| 12782 * Gets the index of the last visible item in the viewport. | |
| 12783 * | |
| 12784 * @type {number} | |
| 12785 */ | |
| 12786 get lastVisibleIndex() { | 7239 get lastVisibleIndex() { |
| 12787 if (this._lastVisibleIndexVal === null) { | 7240 if (this._lastVisibleIndexVal === null) { |
| 12788 if (this.grid) { | 7241 if (this.grid) { |
| 12789 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 7242 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
| 12790 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 7243 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
| 12791 } else { | 7244 } else { |
| 12792 var physicalOffset = this._physicalTop; | 7245 var physicalOffset = this._physicalTop; |
| 12793 this._iterateItems(function(pidx, vidx) { | 7246 this._iterateItems(function(pidx, vidx) { |
| 12794 if (physicalOffset < this._scrollBottom) { | 7247 if (physicalOffset < this._scrollBottom) { |
| 12795 this._lastVisibleIndexVal = vidx; | 7248 this._lastVisibleIndexVal = vidx; |
| 12796 } else { | 7249 } else { |
| 12797 // Break _iterateItems | |
| 12798 return true; | 7250 return true; |
| 12799 } | 7251 } |
| 12800 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 7252 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12801 }); | 7253 }); |
| 12802 } | 7254 } |
| 12803 } | 7255 } |
| 12804 return this._lastVisibleIndexVal; | 7256 return this._lastVisibleIndexVal; |
| 12805 }, | 7257 }, |
| 12806 | |
| 12807 get _defaultScrollTarget() { | 7258 get _defaultScrollTarget() { |
| 12808 return this; | 7259 return this; |
| 12809 }, | 7260 }, |
| 12810 get _virtualRowCount() { | 7261 get _virtualRowCount() { |
| 12811 return Math.ceil(this._virtualCount / this._itemsPerRow); | 7262 return Math.ceil(this._virtualCount / this._itemsPerRow); |
| 12812 }, | 7263 }, |
| 12813 | |
| 12814 get _estRowsInView() { | 7264 get _estRowsInView() { |
| 12815 return Math.ceil(this._viewportHeight / this._rowHeight); | 7265 return Math.ceil(this._viewportHeight / this._rowHeight); |
| 12816 }, | 7266 }, |
| 12817 | |
| 12818 get _physicalRows() { | 7267 get _physicalRows() { |
| 12819 return Math.ceil(this._physicalCount / this._itemsPerRow); | 7268 return Math.ceil(this._physicalCount / this._itemsPerRow); |
| 12820 }, | 7269 }, |
| 12821 | |
| 12822 ready: function() { | 7270 ready: function() { |
| 12823 this.addEventListener('focus', this._didFocus.bind(this), true); | 7271 this.addEventListener('focus', this._didFocus.bind(this), true); |
| 12824 }, | 7272 }, |
| 12825 | |
| 12826 attached: function() { | 7273 attached: function() { |
| 12827 this.updateViewportBoundaries(); | 7274 this.updateViewportBoundaries(); |
| 12828 this._render(); | 7275 this._render(); |
| 12829 // `iron-resize` is fired when the list is attached if the event is added | |
| 12830 // before attached causing unnecessary work. | |
| 12831 this.listen(this, 'iron-resize', '_resizeHandler'); | 7276 this.listen(this, 'iron-resize', '_resizeHandler'); |
| 12832 }, | 7277 }, |
| 12833 | |
| 12834 detached: function() { | 7278 detached: function() { |
| 12835 this._itemsRendered = false; | 7279 this._itemsRendered = false; |
| 12836 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 7280 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
| 12837 }, | 7281 }, |
| 12838 | |
| 12839 /** | |
| 12840 * Set the overflow property if this element has its own scrolling region | |
| 12841 */ | |
| 12842 _setOverflow: function(scrollTarget) { | 7282 _setOverflow: function(scrollTarget) { |
| 12843 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 7283 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
| 12844 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 7284 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 12845 }, | 7285 }, |
| 12846 | |
| 12847 /** | |
| 12848 * Invoke this method if you dynamically update the viewport's | |
| 12849 * size or CSS padding. | |
| 12850 * | |
| 12851 * @method updateViewportBoundaries | |
| 12852 */ | |
| 12853 updateViewportBoundaries: function() { | 7286 updateViewportBoundaries: function() { |
| 12854 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 7287 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo
w.getComputedStyle(this)['padding-top'], 10); |
| 12855 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
| 12856 | |
| 12857 this._viewportHeight = this._scrollTargetHeight; | 7288 this._viewportHeight = this._scrollTargetHeight; |
| 12858 if (this.grid) { | 7289 if (this.grid) { |
| 12859 this._updateGridMetrics(); | 7290 this._updateGridMetrics(); |
| 12860 } | 7291 } |
| 12861 }, | 7292 }, |
| 12862 | |
| 12863 /** | |
| 12864 * Update the models, the position of the | |
| 12865 * items in the viewport and recycle tiles as needed. | |
| 12866 */ | |
| 12867 _scrollHandler: function() { | 7293 _scrollHandler: function() { |
| 12868 // clamp the `scrollTop` value | |
| 12869 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 7294 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
| 12870 var delta = scrollTop - this._scrollPosition; | 7295 var delta = scrollTop - this._scrollPosition; |
| 12871 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 7296 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
| 12872 var ratio = this._ratio; | 7297 var ratio = this._ratio; |
| 12873 var recycledTiles = 0; | 7298 var recycledTiles = 0; |
| 12874 var hiddenContentSize = this._hiddenContentSize; | 7299 var hiddenContentSize = this._hiddenContentSize; |
| 12875 var currentRatio = ratio; | 7300 var currentRatio = ratio; |
| 12876 var movingUp = []; | 7301 var movingUp = []; |
| 12877 | |
| 12878 // track the last `scrollTop` | |
| 12879 this._scrollPosition = scrollTop; | 7302 this._scrollPosition = scrollTop; |
| 12880 | |
| 12881 // clear cached visible indexes | |
| 12882 this._firstVisibleIndexVal = null; | 7303 this._firstVisibleIndexVal = null; |
| 12883 this._lastVisibleIndexVal = null; | 7304 this._lastVisibleIndexVal = null; |
| 12884 | |
| 12885 scrollBottom = this._scrollBottom; | 7305 scrollBottom = this._scrollBottom; |
| 12886 physicalBottom = this._physicalBottom; | 7306 physicalBottom = this._physicalBottom; |
| 12887 | |
| 12888 // random access | |
| 12889 if (Math.abs(delta) > this._physicalSize) { | 7307 if (Math.abs(delta) > this._physicalSize) { |
| 12890 this._physicalTop += delta; | 7308 this._physicalTop += delta; |
| 12891 recycledTiles = Math.round(delta / this._physicalAverage); | 7309 recycledTiles = Math.round(delta / this._physicalAverage); |
| 12892 } | 7310 } else if (delta < 0) { |
| 12893 // scroll up | |
| 12894 else if (delta < 0) { | |
| 12895 var topSpace = scrollTop - this._physicalTop; | 7311 var topSpace = scrollTop - this._physicalTop; |
| 12896 var virtualStart = this._virtualStart; | 7312 var virtualStart = this._virtualStart; |
| 12897 | |
| 12898 recycledTileSet = []; | 7313 recycledTileSet = []; |
| 12899 | |
| 12900 kth = this._physicalEnd; | 7314 kth = this._physicalEnd; |
| 12901 currentRatio = topSpace / hiddenContentSize; | 7315 currentRatio = topSpace / hiddenContentSize; |
| 12902 | 7316 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen
t(kth) > scrollBottom) { |
| 12903 // move tiles from bottom to top | |
| 12904 while ( | |
| 12905 // approximate `currentRatio` to `ratio` | |
| 12906 currentRatio < ratio && | |
| 12907 // recycle less physical items than the total | |
| 12908 recycledTiles < this._physicalCount && | |
| 12909 // ensure that these recycled tiles are needed | |
| 12910 virtualStart - recycledTiles > 0 && | |
| 12911 // ensure that the tile is not visible | |
| 12912 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
| 12913 ) { | |
| 12914 | |
| 12915 tileHeight = this._getPhysicalSizeIncrement(kth); | 7317 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12916 currentRatio += tileHeight / hiddenContentSize; | 7318 currentRatio += tileHeight / hiddenContentSize; |
| 12917 physicalBottom -= tileHeight; | 7319 physicalBottom -= tileHeight; |
| 12918 recycledTileSet.push(kth); | 7320 recycledTileSet.push(kth); |
| 12919 recycledTiles++; | 7321 recycledTiles++; |
| 12920 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 7322 kth = kth === 0 ? this._physicalCount - 1 : kth - 1; |
| 12921 } | 7323 } |
| 12922 | |
| 12923 movingUp = recycledTileSet; | 7324 movingUp = recycledTileSet; |
| 12924 recycledTiles = -recycledTiles; | 7325 recycledTiles = -recycledTiles; |
| 12925 } | 7326 } else if (delta > 0) { |
| 12926 // scroll down | |
| 12927 else if (delta > 0) { | |
| 12928 var bottomSpace = physicalBottom - scrollBottom; | 7327 var bottomSpace = physicalBottom - scrollBottom; |
| 12929 var virtualEnd = this._virtualEnd; | 7328 var virtualEnd = this._virtualEnd; |
| 12930 var lastVirtualItemIndex = this._virtualCount-1; | 7329 var lastVirtualItemIndex = this._virtualCount - 1; |
| 12931 | |
| 12932 recycledTileSet = []; | 7330 recycledTileSet = []; |
| 12933 | |
| 12934 kth = this._physicalStart; | 7331 kth = this._physicalStart; |
| 12935 currentRatio = bottomSpace / hiddenContentSize; | 7332 currentRatio = bottomSpace / hiddenContentSize; |
| 12936 | 7333 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get
PhysicalSizeIncrement(kth) < scrollTop) { |
| 12937 // move tiles from top to bottom | |
| 12938 while ( | |
| 12939 // approximate `currentRatio` to `ratio` | |
| 12940 currentRatio < ratio && | |
| 12941 // recycle less physical items than the total | |
| 12942 recycledTiles < this._physicalCount && | |
| 12943 // ensure that these recycled tiles are needed | |
| 12944 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
| 12945 // ensure that the tile is not visible | |
| 12946 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
| 12947 ) { | |
| 12948 | |
| 12949 tileHeight = this._getPhysicalSizeIncrement(kth); | 7334 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12950 currentRatio += tileHeight / hiddenContentSize; | 7335 currentRatio += tileHeight / hiddenContentSize; |
| 12951 | |
| 12952 this._physicalTop += tileHeight; | 7336 this._physicalTop += tileHeight; |
| 12953 recycledTileSet.push(kth); | 7337 recycledTileSet.push(kth); |
| 12954 recycledTiles++; | 7338 recycledTiles++; |
| 12955 kth = (kth + 1) % this._physicalCount; | 7339 kth = (kth + 1) % this._physicalCount; |
| 12956 } | 7340 } |
| 12957 } | 7341 } |
| 12958 | |
| 12959 if (recycledTiles === 0) { | 7342 if (recycledTiles === 0) { |
| 12960 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
| 12961 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 7343 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
| 12962 this._increasePoolIfNeeded(); | 7344 this._increasePoolIfNeeded(); |
| 12963 } | 7345 } |
| 12964 } else { | 7346 } else { |
| 12965 this._virtualStart = this._virtualStart + recycledTiles; | 7347 this._virtualStart = this._virtualStart + recycledTiles; |
| 12966 this._physicalStart = this._physicalStart + recycledTiles; | 7348 this._physicalStart = this._physicalStart + recycledTiles; |
| 12967 this._update(recycledTileSet, movingUp); | 7349 this._update(recycledTileSet, movingUp); |
| 12968 } | 7350 } |
| 12969 }, | 7351 }, |
| 12970 | |
| 12971 /** | |
| 12972 * Update the list of items, starting from the `_virtualStart` item. | |
| 12973 * @param {!Array<number>=} itemSet | |
| 12974 * @param {!Array<number>=} movingUp | |
| 12975 */ | |
| 12976 _update: function(itemSet, movingUp) { | 7352 _update: function(itemSet, movingUp) { |
| 12977 // manage focus | |
| 12978 this._manageFocus(); | 7353 this._manageFocus(); |
| 12979 // update models | |
| 12980 this._assignModels(itemSet); | 7354 this._assignModels(itemSet); |
| 12981 // measure heights | |
| 12982 this._updateMetrics(itemSet); | 7355 this._updateMetrics(itemSet); |
| 12983 // adjust offset after measuring | |
| 12984 if (movingUp) { | 7356 if (movingUp) { |
| 12985 while (movingUp.length) { | 7357 while (movingUp.length) { |
| 12986 var idx = movingUp.pop(); | 7358 var idx = movingUp.pop(); |
| 12987 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 7359 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
| 12988 } | 7360 } |
| 12989 } | 7361 } |
| 12990 // update the position of the items | |
| 12991 this._positionItems(); | 7362 this._positionItems(); |
| 12992 // set the scroller size | |
| 12993 this._updateScrollerSize(); | 7363 this._updateScrollerSize(); |
| 12994 // increase the pool of physical items | |
| 12995 this._increasePoolIfNeeded(); | 7364 this._increasePoolIfNeeded(); |
| 12996 }, | 7365 }, |
| 12997 | |
| 12998 /** | |
| 12999 * Creates a pool of DOM elements and attaches them to the local dom. | |
| 13000 */ | |
| 13001 _createPool: function(size) { | 7366 _createPool: function(size) { |
| 13002 var physicalItems = new Array(size); | 7367 var physicalItems = new Array(size); |
| 13003 | |
| 13004 this._ensureTemplatized(); | 7368 this._ensureTemplatized(); |
| 13005 | |
| 13006 for (var i = 0; i < size; i++) { | 7369 for (var i = 0; i < size; i++) { |
| 13007 var inst = this.stamp(null); | 7370 var inst = this.stamp(null); |
| 13008 // First element child is item; Safari doesn't support children[0] | |
| 13009 // on a doc fragment | |
| 13010 physicalItems[i] = inst.root.querySelector('*'); | 7371 physicalItems[i] = inst.root.querySelector('*'); |
| 13011 Polymer.dom(this).appendChild(inst.root); | 7372 Polymer.dom(this).appendChild(inst.root); |
| 13012 } | 7373 } |
| 13013 return physicalItems; | 7374 return physicalItems; |
| 13014 }, | 7375 }, |
| 13015 | |
| 13016 /** | |
| 13017 * Increases the pool of physical items only if needed. | |
| 13018 * | |
| 13019 * @return {boolean} True if the pool was increased. | |
| 13020 */ | |
| 13021 _increasePoolIfNeeded: function() { | 7376 _increasePoolIfNeeded: function() { |
| 13022 // Base case 1: the list has no height. | |
| 13023 if (this._viewportHeight === 0) { | 7377 if (this._viewportHeight === 0) { |
| 13024 return false; | 7378 return false; |
| 13025 } | 7379 } |
| 13026 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
| 13027 // with physical items, don't increase the pool. | |
| 13028 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 7380 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
| 13029 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 7381 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
| 13030 return false; | 7382 return false; |
| 13031 } | 7383 } |
| 13032 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
| 13033 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 7384 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
| 13034 | |
| 13035 if (currentPage === 0) { | 7385 if (currentPage === 0) { |
| 13036 // fill the first page | 7386 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * .5))); |
| 13037 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
| 13038 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 7387 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
| 13039 // paint the page and defer the next increase | |
| 13040 // wait 16ms which is rough enough to get paint cycle. | |
| 13041 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 7388 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
| 13042 } else { | 7389 } else { |
| 13043 // fill the rest of the pages | |
| 13044 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 7390 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
| 13045 } | 7391 } |
| 13046 | |
| 13047 this._lastPage = currentPage; | 7392 this._lastPage = currentPage; |
| 13048 | |
| 13049 return true; | 7393 return true; |
| 13050 }, | 7394 }, |
| 13051 | |
| 13052 /** | |
| 13053 * Increases the pool size. | |
| 13054 */ | |
| 13055 _increasePool: function(missingItems) { | 7395 _increasePool: function(missingItems) { |
| 13056 var nextPhysicalCount = Math.min( | 7396 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this.
_virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS
ICAL_COUNT)); |
| 13057 this._physicalCount + missingItems, | |
| 13058 this._virtualCount - this._virtualStart, | |
| 13059 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
| 13060 ); | |
| 13061 var prevPhysicalCount = this._physicalCount; | 7397 var prevPhysicalCount = this._physicalCount; |
| 13062 var delta = nextPhysicalCount - prevPhysicalCount; | 7398 var delta = nextPhysicalCount - prevPhysicalCount; |
| 13063 | |
| 13064 if (delta <= 0) { | 7399 if (delta <= 0) { |
| 13065 return; | 7400 return; |
| 13066 } | 7401 } |
| 13067 | |
| 13068 [].push.apply(this._physicalItems, this._createPool(delta)); | 7402 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 13069 [].push.apply(this._physicalSizes, new Array(delta)); | 7403 [].push.apply(this._physicalSizes, new Array(delta)); |
| 13070 | |
| 13071 this._physicalCount = prevPhysicalCount + delta; | 7404 this._physicalCount = prevPhysicalCount + delta; |
| 13072 | 7405 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this.
_focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd
) { |
| 13073 // update the physical start if we need to preserve the model of the focus
ed item. | |
| 13074 // In this situation, the focused item is currently rendered and its model
would | |
| 13075 // have changed after increasing the pool if the physical start remained u
nchanged. | |
| 13076 if (this._physicalStart > this._physicalEnd && | |
| 13077 this._isIndexRendered(this._focusedIndex) && | |
| 13078 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
| 13079 this._physicalStart = this._physicalStart + delta; | 7406 this._physicalStart = this._physicalStart + delta; |
| 13080 } | 7407 } |
| 13081 this._update(); | 7408 this._update(); |
| 13082 }, | 7409 }, |
| 13083 | |
| 13084 /** | |
| 13085 * Render a new list of items. This method does exactly the same as `update`
, | |
| 13086 * but it also ensures that only one `update` cycle is created. | |
| 13087 */ | |
| 13088 _render: function() { | 7410 _render: function() { |
| 13089 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 7411 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
| 13090 | |
| 13091 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 7412 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 13092 this._lastPage = 0; | 7413 this._lastPage = 0; |
| 13093 this._update(); | 7414 this._update(); |
| 13094 this._itemsRendered = true; | 7415 this._itemsRendered = true; |
| 13095 } | 7416 } |
| 13096 }, | 7417 }, |
| 13097 | |
| 13098 /** | |
| 13099 * Templetizes the user template. | |
| 13100 */ | |
| 13101 _ensureTemplatized: function() { | 7418 _ensureTemplatized: function() { |
| 13102 if (!this.ctor) { | 7419 if (!this.ctor) { |
| 13103 // Template instance props that should be excluded from forwarding | |
| 13104 var props = {}; | 7420 var props = {}; |
| 13105 props.__key__ = true; | 7421 props.__key__ = true; |
| 13106 props[this.as] = true; | 7422 props[this.as] = true; |
| 13107 props[this.indexAs] = true; | 7423 props[this.indexAs] = true; |
| 13108 props[this.selectedAs] = true; | 7424 props[this.selectedAs] = true; |
| 13109 props.tabIndex = true; | 7425 props.tabIndex = true; |
| 13110 | |
| 13111 this._instanceProps = props; | 7426 this._instanceProps = props; |
| 13112 this._userTemplate = Polymer.dom(this).querySelector('template'); | 7427 this._userTemplate = Polymer.dom(this).querySelector('template'); |
| 13113 | |
| 13114 if (this._userTemplate) { | 7428 if (this._userTemplate) { |
| 13115 this.templatize(this._userTemplate); | 7429 this.templatize(this._userTemplate); |
| 13116 } else { | 7430 } else { |
| 13117 console.warn('iron-list requires a template to be provided in light-do
m'); | 7431 console.warn('iron-list requires a template to be provided in light-do
m'); |
| 13118 } | 7432 } |
| 13119 } | 7433 } |
| 13120 }, | 7434 }, |
| 13121 | |
| 13122 /** | |
| 13123 * Implements extension point from Templatizer mixin. | |
| 13124 */ | |
| 13125 _getStampedChildren: function() { | 7435 _getStampedChildren: function() { |
| 13126 return this._physicalItems; | 7436 return this._physicalItems; |
| 13127 }, | 7437 }, |
| 13128 | |
| 13129 /** | |
| 13130 * Implements extension point from Templatizer | |
| 13131 * Called as a side effect of a template instance path change, responsible | |
| 13132 * for notifying items.<key-for-instance>.<path> change up to host. | |
| 13133 */ | |
| 13134 _forwardInstancePath: function(inst, path, value) { | 7438 _forwardInstancePath: function(inst, path, value) { |
| 13135 if (path.indexOf(this.as + '.') === 0) { | 7439 if (path.indexOf(this.as + '.') === 0) { |
| 13136 this.notifyPath('items.' + inst.__key__ + '.' + | 7440 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt
h + 1), value); |
| 13137 path.slice(this.as.length + 1), value); | |
| 13138 } | 7441 } |
| 13139 }, | 7442 }, |
| 13140 | |
| 13141 /** | |
| 13142 * Implements extension point from Templatizer mixin | |
| 13143 * Called as side-effect of a host property change, responsible for | |
| 13144 * notifying parent path change on each row. | |
| 13145 */ | |
| 13146 _forwardParentProp: function(prop, value) { | 7443 _forwardParentProp: function(prop, value) { |
| 13147 if (this._physicalItems) { | 7444 if (this._physicalItems) { |
| 13148 this._physicalItems.forEach(function(item) { | 7445 this._physicalItems.forEach(function(item) { |
| 13149 item._templateInstance[prop] = value; | 7446 item._templateInstance[prop] = value; |
| 13150 }, this); | 7447 }, this); |
| 13151 } | 7448 } |
| 13152 }, | 7449 }, |
| 13153 | |
| 13154 /** | |
| 13155 * Implements extension point from Templatizer | |
| 13156 * Called as side-effect of a host path change, responsible for | |
| 13157 * notifying parent.<path> path change on each row. | |
| 13158 */ | |
| 13159 _forwardParentPath: function(path, value) { | 7450 _forwardParentPath: function(path, value) { |
| 13160 if (this._physicalItems) { | 7451 if (this._physicalItems) { |
| 13161 this._physicalItems.forEach(function(item) { | 7452 this._physicalItems.forEach(function(item) { |
| 13162 item._templateInstance.notifyPath(path, value, true); | 7453 item._templateInstance.notifyPath(path, value, true); |
| 13163 }, this); | 7454 }, this); |
| 13164 } | 7455 } |
| 13165 }, | 7456 }, |
| 13166 | |
| 13167 /** | |
| 13168 * Called as a side effect of a host items.<key>.<path> path change, | |
| 13169 * responsible for notifying item.<path> changes. | |
| 13170 */ | |
| 13171 _forwardItemPath: function(path, value) { | 7457 _forwardItemPath: function(path, value) { |
| 13172 if (!this._physicalIndexForKey) { | 7458 if (!this._physicalIndexForKey) { |
| 13173 return; | 7459 return; |
| 13174 } | 7460 } |
| 13175 var dot = path.indexOf('.'); | 7461 var dot = path.indexOf('.'); |
| 13176 var key = path.substring(0, dot < 0 ? path.length : dot); | 7462 var key = path.substring(0, dot < 0 ? path.length : dot); |
| 13177 var idx = this._physicalIndexForKey[key]; | 7463 var idx = this._physicalIndexForKey[key]; |
| 13178 var offscreenItem = this._offscreenFocusedItem; | 7464 var offscreenItem = this._offscreenFocusedItem; |
| 13179 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 7465 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? offscreenItem : this._physicalItems[idx]; |
| 13180 offscreenItem : this._physicalItems[idx]; | |
| 13181 | |
| 13182 if (!el || el._templateInstance.__key__ !== key) { | 7466 if (!el || el._templateInstance.__key__ !== key) { |
| 13183 return; | 7467 return; |
| 13184 } | 7468 } |
| 13185 if (dot >= 0) { | 7469 if (dot >= 0) { |
| 13186 path = this.as + '.' + path.substring(dot+1); | 7470 path = this.as + '.' + path.substring(dot + 1); |
| 13187 el._templateInstance.notifyPath(path, value, true); | 7471 el._templateInstance.notifyPath(path, value, true); |
| 13188 } else { | 7472 } else { |
| 13189 // Update selection if needed | |
| 13190 var currentItem = el._templateInstance[this.as]; | 7473 var currentItem = el._templateInstance[this.as]; |
| 13191 if (Array.isArray(this.selectedItems)) { | 7474 if (Array.isArray(this.selectedItems)) { |
| 13192 for (var i = 0; i < this.selectedItems.length; i++) { | 7475 for (var i = 0; i < this.selectedItems.length; i++) { |
| 13193 if (this.selectedItems[i] === currentItem) { | 7476 if (this.selectedItems[i] === currentItem) { |
| 13194 this.set('selectedItems.' + i, value); | 7477 this.set('selectedItems.' + i, value); |
| 13195 break; | 7478 break; |
| 13196 } | 7479 } |
| 13197 } | 7480 } |
| 13198 } else if (this.selectedItem === currentItem) { | 7481 } else if (this.selectedItem === currentItem) { |
| 13199 this.set('selectedItem', value); | 7482 this.set('selectedItem', value); |
| 13200 } | 7483 } |
| 13201 el._templateInstance[this.as] = value; | 7484 el._templateInstance[this.as] = value; |
| 13202 } | 7485 } |
| 13203 }, | 7486 }, |
| 13204 | |
| 13205 /** | |
| 13206 * Called when the items have changed. That is, ressignments | |
| 13207 * to `items`, splices or updates to a single item. | |
| 13208 */ | |
| 13209 _itemsChanged: function(change) { | 7487 _itemsChanged: function(change) { |
| 13210 if (change.path === 'items') { | 7488 if (change.path === 'items') { |
| 13211 // reset items | |
| 13212 this._virtualStart = 0; | 7489 this._virtualStart = 0; |
| 13213 this._physicalTop = 0; | 7490 this._physicalTop = 0; |
| 13214 this._virtualCount = this.items ? this.items.length : 0; | 7491 this._virtualCount = this.items ? this.items.length : 0; |
| 13215 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 7492 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
| 13216 this._physicalIndexForKey = {}; | 7493 this._physicalIndexForKey = {}; |
| 13217 this._firstVisibleIndexVal = null; | 7494 this._firstVisibleIndexVal = null; |
| 13218 this._lastVisibleIndexVal = null; | 7495 this._lastVisibleIndexVal = null; |
| 13219 | |
| 13220 this._resetScrollPosition(0); | 7496 this._resetScrollPosition(0); |
| 13221 this._removeFocusedItem(); | 7497 this._removeFocusedItem(); |
| 13222 // create the initial physical items | |
| 13223 if (!this._physicalItems) { | 7498 if (!this._physicalItems) { |
| 13224 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 7499 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
| 13225 this._physicalItems = this._createPool(this._physicalCount); | 7500 this._physicalItems = this._createPool(this._physicalCount); |
| 13226 this._physicalSizes = new Array(this._physicalCount); | 7501 this._physicalSizes = new Array(this._physicalCount); |
| 13227 } | 7502 } |
| 13228 | |
| 13229 this._physicalStart = 0; | 7503 this._physicalStart = 0; |
| 13230 | |
| 13231 } else if (change.path === 'items.splices') { | 7504 } else if (change.path === 'items.splices') { |
| 13232 | |
| 13233 this._adjustVirtualIndex(change.value.indexSplices); | 7505 this._adjustVirtualIndex(change.value.indexSplices); |
| 13234 this._virtualCount = this.items ? this.items.length : 0; | 7506 this._virtualCount = this.items ? this.items.length : 0; |
| 13235 | |
| 13236 } else { | 7507 } else { |
| 13237 // update a single item | |
| 13238 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 7508 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
| 13239 return; | 7509 return; |
| 13240 } | 7510 } |
| 13241 | |
| 13242 this._itemsRendered = false; | 7511 this._itemsRendered = false; |
| 13243 this._debounceTemplate(this._render); | 7512 this._debounceTemplate(this._render); |
| 13244 }, | 7513 }, |
| 13245 | |
| 13246 /** | |
| 13247 * @param {!Array<!PolymerSplice>} splices | |
| 13248 */ | |
| 13249 _adjustVirtualIndex: function(splices) { | 7514 _adjustVirtualIndex: function(splices) { |
| 13250 splices.forEach(function(splice) { | 7515 splices.forEach(function(splice) { |
| 13251 // deselect removed items | |
| 13252 splice.removed.forEach(this._removeItem, this); | 7516 splice.removed.forEach(this._removeItem, this); |
| 13253 // We only need to care about changes happening above the current positi
on | |
| 13254 if (splice.index < this._virtualStart) { | 7517 if (splice.index < this._virtualStart) { |
| 13255 var delta = Math.max( | 7518 var delta = Math.max(splice.addedCount - splice.removed.length, splice
.index - this._virtualStart); |
| 13256 splice.addedCount - splice.removed.length, | |
| 13257 splice.index - this._virtualStart); | |
| 13258 | |
| 13259 this._virtualStart = this._virtualStart + delta; | 7519 this._virtualStart = this._virtualStart + delta; |
| 13260 | |
| 13261 if (this._focusedIndex >= 0) { | 7520 if (this._focusedIndex >= 0) { |
| 13262 this._focusedIndex = this._focusedIndex + delta; | 7521 this._focusedIndex = this._focusedIndex + delta; |
| 13263 } | 7522 } |
| 13264 } | 7523 } |
| 13265 }, this); | 7524 }, this); |
| 13266 }, | 7525 }, |
| 13267 | |
| 13268 _removeItem: function(item) { | 7526 _removeItem: function(item) { |
| 13269 this.$.selector.deselect(item); | 7527 this.$.selector.deselect(item); |
| 13270 // remove the current focused item | |
| 13271 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 7528 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
| 13272 this._removeFocusedItem(); | 7529 this._removeFocusedItem(); |
| 13273 } | 7530 } |
| 13274 }, | 7531 }, |
| 13275 | |
| 13276 /** | |
| 13277 * Executes a provided function per every physical index in `itemSet` | |
| 13278 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
| 13279 * | |
| 13280 * @param {!function(number, number)} fn | |
| 13281 * @param {!Array<number>=} itemSet | |
| 13282 */ | |
| 13283 _iterateItems: function(fn, itemSet) { | 7532 _iterateItems: function(fn, itemSet) { |
| 13284 var pidx, vidx, rtn, i; | 7533 var pidx, vidx, rtn, i; |
| 13285 | |
| 13286 if (arguments.length === 2 && itemSet) { | 7534 if (arguments.length === 2 && itemSet) { |
| 13287 for (i = 0; i < itemSet.length; i++) { | 7535 for (i = 0; i < itemSet.length; i++) { |
| 13288 pidx = itemSet[i]; | 7536 pidx = itemSet[i]; |
| 13289 vidx = this._computeVidx(pidx); | 7537 vidx = this._computeVidx(pidx); |
| 13290 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7538 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13291 return rtn; | 7539 return rtn; |
| 13292 } | 7540 } |
| 13293 } | 7541 } |
| 13294 } else { | 7542 } else { |
| 13295 pidx = this._physicalStart; | 7543 pidx = this._physicalStart; |
| 13296 vidx = this._virtualStart; | 7544 vidx = this._virtualStart; |
| 13297 | 7545 for (;pidx < this._physicalCount; pidx++, vidx++) { |
| 13298 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
| 13299 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7546 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13300 return rtn; | 7547 return rtn; |
| 13301 } | 7548 } |
| 13302 } | 7549 } |
| 13303 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 7550 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
| 13304 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7551 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13305 return rtn; | 7552 return rtn; |
| 13306 } | 7553 } |
| 13307 } | 7554 } |
| 13308 } | 7555 } |
| 13309 }, | 7556 }, |
| 13310 | |
| 13311 /** | |
| 13312 * Returns the virtual index for a given physical index | |
| 13313 * | |
| 13314 * @param {number} pidx Physical index | |
| 13315 * @return {number} | |
| 13316 */ | |
| 13317 _computeVidx: function(pidx) { | 7557 _computeVidx: function(pidx) { |
| 13318 if (pidx >= this._physicalStart) { | 7558 if (pidx >= this._physicalStart) { |
| 13319 return this._virtualStart + (pidx - this._physicalStart); | 7559 return this._virtualStart + (pidx - this._physicalStart); |
| 13320 } | 7560 } |
| 13321 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 7561 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
| 13322 }, | 7562 }, |
| 13323 | |
| 13324 /** | |
| 13325 * Assigns the data models to a given set of items. | |
| 13326 * @param {!Array<number>=} itemSet | |
| 13327 */ | |
| 13328 _assignModels: function(itemSet) { | 7563 _assignModels: function(itemSet) { |
| 13329 this._iterateItems(function(pidx, vidx) { | 7564 this._iterateItems(function(pidx, vidx) { |
| 13330 var el = this._physicalItems[pidx]; | 7565 var el = this._physicalItems[pidx]; |
| 13331 var inst = el._templateInstance; | 7566 var inst = el._templateInstance; |
| 13332 var item = this.items && this.items[vidx]; | 7567 var item = this.items && this.items[vidx]; |
| 13333 | |
| 13334 if (item != null) { | 7568 if (item != null) { |
| 13335 inst[this.as] = item; | 7569 inst[this.as] = item; |
| 13336 inst.__key__ = this._collection.getKey(item); | 7570 inst.__key__ = this._collection.getKey(item); |
| 13337 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 7571 inst[this.selectedAs] = this.$.selector.isSelected(item); |
| 13338 inst[this.indexAs] = vidx; | 7572 inst[this.indexAs] = vidx; |
| 13339 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 7573 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
| 13340 this._physicalIndexForKey[inst.__key__] = pidx; | 7574 this._physicalIndexForKey[inst.__key__] = pidx; |
| 13341 el.removeAttribute('hidden'); | 7575 el.removeAttribute('hidden'); |
| 13342 } else { | 7576 } else { |
| 13343 inst.__key__ = null; | 7577 inst.__key__ = null; |
| 13344 el.setAttribute('hidden', ''); | 7578 el.setAttribute('hidden', ''); |
| 13345 } | 7579 } |
| 13346 }, itemSet); | 7580 }, itemSet); |
| 13347 }, | 7581 }, |
| 13348 | 7582 _updateMetrics: function(itemSet) { |
| 13349 /** | |
| 13350 * Updates the height for a given set of items. | |
| 13351 * | |
| 13352 * @param {!Array<number>=} itemSet | |
| 13353 */ | |
| 13354 _updateMetrics: function(itemSet) { | |
| 13355 // Make sure we distributed all the physical items | |
| 13356 // so we can measure them | |
| 13357 Polymer.dom.flush(); | 7583 Polymer.dom.flush(); |
| 13358 | |
| 13359 var newPhysicalSize = 0; | 7584 var newPhysicalSize = 0; |
| 13360 var oldPhysicalSize = 0; | 7585 var oldPhysicalSize = 0; |
| 13361 var prevAvgCount = this._physicalAverageCount; | 7586 var prevAvgCount = this._physicalAverageCount; |
| 13362 var prevPhysicalAvg = this._physicalAverage; | 7587 var prevPhysicalAvg = this._physicalAverage; |
| 13363 | |
| 13364 this._iterateItems(function(pidx, vidx) { | 7588 this._iterateItems(function(pidx, vidx) { |
| 13365 | |
| 13366 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 7589 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
| 13367 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 7590 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
| 13368 newPhysicalSize += this._physicalSizes[pidx]; | 7591 newPhysicalSize += this._physicalSizes[pidx]; |
| 13369 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 7592 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| 13370 | |
| 13371 }, itemSet); | 7593 }, itemSet); |
| 13372 | |
| 13373 this._viewportHeight = this._scrollTargetHeight; | 7594 this._viewportHeight = this._scrollTargetHeight; |
| 13374 if (this.grid) { | 7595 if (this.grid) { |
| 13375 this._updateGridMetrics(); | 7596 this._updateGridMetrics(); |
| 13376 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 7597 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
| 13377 } else { | 7598 } else { |
| 13378 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 7599 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
| 13379 } | 7600 } |
| 13380 | |
| 13381 // update the average if we measured something | |
| 13382 if (this._physicalAverageCount !== prevAvgCount) { | 7601 if (this._physicalAverageCount !== prevAvgCount) { |
| 13383 this._physicalAverage = Math.round( | 7602 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new
PhysicalSize) / this._physicalAverageCount); |
| 13384 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
| 13385 this._physicalAverageCount); | |
| 13386 } | 7603 } |
| 13387 }, | 7604 }, |
| 13388 | |
| 13389 _updateGridMetrics: function() { | 7605 _updateGridMetrics: function() { |
| 13390 this._viewportWidth = this.$.items.offsetWidth; | 7606 this._viewportWidth = this.$.items.offsetWidth; |
| 13391 // Set item width to the value of the _physicalItems offsetWidth | |
| 13392 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 7607 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
| 13393 // Set row height to the value of the _physicalItems offsetHeight | |
| 13394 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 7608 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
| 13395 // If in grid mode compute how many items with exist in each row | |
| 13396 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 7609 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
| 13397 }, | 7610 }, |
| 13398 | |
| 13399 /** | |
| 13400 * Updates the position of the physical items. | |
| 13401 */ | |
| 13402 _positionItems: function() { | 7611 _positionItems: function() { |
| 13403 this._adjustScrollPosition(); | 7612 this._adjustScrollPosition(); |
| 13404 | |
| 13405 var y = this._physicalTop; | 7613 var y = this._physicalTop; |
| 13406 | |
| 13407 if (this.grid) { | 7614 if (this.grid) { |
| 13408 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 7615 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
| 13409 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 7616 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
| 13410 | |
| 13411 this._iterateItems(function(pidx, vidx) { | 7617 this._iterateItems(function(pidx, vidx) { |
| 13412 | |
| 13413 var modulus = vidx % this._itemsPerRow; | 7618 var modulus = vidx % this._itemsPerRow; |
| 13414 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | 7619 var x = Math.floor(modulus * this._itemWidth + rowOffset); |
| 13415 | |
| 13416 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | 7620 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
| 13417 | |
| 13418 if (this._shouldRenderNextRow(vidx)) { | 7621 if (this._shouldRenderNextRow(vidx)) { |
| 13419 y += this._rowHeight; | 7622 y += this._rowHeight; |
| 13420 } | 7623 } |
| 13421 | |
| 13422 }); | 7624 }); |
| 13423 } else { | 7625 } else { |
| 13424 this._iterateItems(function(pidx, vidx) { | 7626 this._iterateItems(function(pidx, vidx) { |
| 13425 | |
| 13426 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 7627 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
| 13427 y += this._physicalSizes[pidx]; | 7628 y += this._physicalSizes[pidx]; |
| 13428 | |
| 13429 }); | 7629 }); |
| 13430 } | 7630 } |
| 13431 }, | 7631 }, |
| 13432 | |
| 13433 _getPhysicalSizeIncrement: function(pidx) { | 7632 _getPhysicalSizeIncrement: function(pidx) { |
| 13434 if (!this.grid) { | 7633 if (!this.grid) { |
| 13435 return this._physicalSizes[pidx]; | 7634 return this._physicalSizes[pidx]; |
| 13436 } | 7635 } |
| 13437 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 7636 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
| 13438 return 0; | 7637 return 0; |
| 13439 } | 7638 } |
| 13440 return this._rowHeight; | 7639 return this._rowHeight; |
| 13441 }, | 7640 }, |
| 13442 | |
| 13443 /** | |
| 13444 * Returns, based on the current index, | |
| 13445 * whether or not the next index will need | |
| 13446 * to be rendered on a new row. | |
| 13447 * | |
| 13448 * @param {number} vidx Virtual index | |
| 13449 * @return {boolean} | |
| 13450 */ | |
| 13451 _shouldRenderNextRow: function(vidx) { | 7641 _shouldRenderNextRow: function(vidx) { |
| 13452 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 7642 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
| 13453 }, | 7643 }, |
| 13454 | |
| 13455 /** | |
| 13456 * Adjusts the scroll position when it was overestimated. | |
| 13457 */ | |
| 13458 _adjustScrollPosition: function() { | 7644 _adjustScrollPosition: function() { |
| 13459 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 7645 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(
this._scrollPosition + this._physicalTop, 0); |
| 13460 Math.min(this._scrollPosition + this._physicalTop, 0); | |
| 13461 | |
| 13462 if (deltaHeight) { | 7646 if (deltaHeight) { |
| 13463 this._physicalTop = this._physicalTop - deltaHeight; | 7647 this._physicalTop = this._physicalTop - deltaHeight; |
| 13464 // juking scroll position during interial scrolling on iOS is no bueno | |
| 13465 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 7648 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
| 13466 this._resetScrollPosition(this._scrollTop - deltaHeight); | 7649 this._resetScrollPosition(this._scrollTop - deltaHeight); |
| 13467 } | 7650 } |
| 13468 } | 7651 } |
| 13469 }, | 7652 }, |
| 13470 | |
| 13471 /** | |
| 13472 * Sets the position of the scroll. | |
| 13473 */ | |
| 13474 _resetScrollPosition: function(pos) { | 7653 _resetScrollPosition: function(pos) { |
| 13475 if (this.scrollTarget) { | 7654 if (this.scrollTarget) { |
| 13476 this._scrollTop = pos; | 7655 this._scrollTop = pos; |
| 13477 this._scrollPosition = this._scrollTop; | 7656 this._scrollPosition = this._scrollTop; |
| 13478 } | 7657 } |
| 13479 }, | 7658 }, |
| 13480 | |
| 13481 /** | |
| 13482 * Sets the scroll height, that's the height of the content, | |
| 13483 * | |
| 13484 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
| 13485 */ | |
| 13486 _updateScrollerSize: function(forceUpdate) { | 7659 _updateScrollerSize: function(forceUpdate) { |
| 13487 if (this.grid) { | 7660 if (this.grid) { |
| 13488 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 7661 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
| 13489 } else { | 7662 } else { |
| 13490 this._estScrollHeight = (this._physicalBottom + | 7663 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou
nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage; |
| 13491 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
| 13492 } | 7664 } |
| 13493 | |
| 13494 forceUpdate = forceUpdate || this._scrollHeight === 0; | 7665 forceUpdate = forceUpdate || this._scrollHeight === 0; |
| 13495 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 7666 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
| 13496 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 7667 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
| 13497 | |
| 13498 // amortize height adjustment, so it won't trigger repaints very often | |
| 13499 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 7668 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
| 13500 this.$.items.style.height = this._estScrollHeight + 'px'; | 7669 this.$.items.style.height = this._estScrollHeight + 'px'; |
| 13501 this._scrollHeight = this._estScrollHeight; | 7670 this._scrollHeight = this._estScrollHeight; |
| 13502 } | 7671 } |
| 13503 }, | 7672 }, |
| 13504 | 7673 scrollToItem: function(item) { |
| 13505 /** | |
| 13506 * Scroll to a specific item in the virtual list regardless | |
| 13507 * of the physical items in the DOM tree. | |
| 13508 * | |
| 13509 * @method scrollToItem | |
| 13510 * @param {(Object)} item The item to be scrolled to | |
| 13511 */ | |
| 13512 scrollToItem: function(item){ | |
| 13513 return this.scrollToIndex(this.items.indexOf(item)); | 7674 return this.scrollToIndex(this.items.indexOf(item)); |
| 13514 }, | 7675 }, |
| 13515 | |
| 13516 /** | |
| 13517 * Scroll to a specific index in the virtual list regardless | |
| 13518 * of the physical items in the DOM tree. | |
| 13519 * | |
| 13520 * @method scrollToIndex | |
| 13521 * @param {number} idx The index of the item | |
| 13522 */ | |
| 13523 scrollToIndex: function(idx) { | 7676 scrollToIndex: function(idx) { |
| 13524 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 7677 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
| 13525 return; | 7678 return; |
| 13526 } | 7679 } |
| 13527 | |
| 13528 Polymer.dom.flush(); | 7680 Polymer.dom.flush(); |
| 13529 | 7681 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1); |
| 13530 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
| 13531 // update the virtual start only when needed | |
| 13532 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 7682 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
| 13533 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 7683 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1; |
| 13534 } | 7684 } |
| 13535 // manage focus | |
| 13536 this._manageFocus(); | 7685 this._manageFocus(); |
| 13537 // assign new models | |
| 13538 this._assignModels(); | 7686 this._assignModels(); |
| 13539 // measure the new sizes | |
| 13540 this._updateMetrics(); | 7687 this._updateMetrics(); |
| 13541 | 7688 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
| 13542 // estimate new physical offset | |
| 13543 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
| 13544 this._physicalTop = estPhysicalTop; | 7689 this._physicalTop = estPhysicalTop; |
| 13545 | |
| 13546 var currentTopItem = this._physicalStart; | 7690 var currentTopItem = this._physicalStart; |
| 13547 var currentVirtualItem = this._virtualStart; | 7691 var currentVirtualItem = this._virtualStart; |
| 13548 var targetOffsetTop = 0; | 7692 var targetOffsetTop = 0; |
| 13549 var hiddenContentSize = this._hiddenContentSize; | 7693 var hiddenContentSize = this._hiddenContentSize; |
| 13550 | |
| 13551 // scroll to the item as much as we can | |
| 13552 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 7694 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
| 13553 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 7695 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
| 13554 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 7696 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
| 13555 currentVirtualItem++; | 7697 currentVirtualItem++; |
| 13556 } | 7698 } |
| 13557 // update the scroller size | |
| 13558 this._updateScrollerSize(true); | 7699 this._updateScrollerSize(true); |
| 13559 // update the position of the items | |
| 13560 this._positionItems(); | 7700 this._positionItems(); |
| 13561 // set the new scroll position | |
| 13562 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 7701 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
| 13563 // increase the pool of physical items if needed | |
| 13564 this._increasePoolIfNeeded(); | 7702 this._increasePoolIfNeeded(); |
| 13565 // clear cached visible index | |
| 13566 this._firstVisibleIndexVal = null; | 7703 this._firstVisibleIndexVal = null; |
| 13567 this._lastVisibleIndexVal = null; | 7704 this._lastVisibleIndexVal = null; |
| 13568 }, | 7705 }, |
| 13569 | |
| 13570 /** | |
| 13571 * Reset the physical average and the average count. | |
| 13572 */ | |
| 13573 _resetAverage: function() { | 7706 _resetAverage: function() { |
| 13574 this._physicalAverage = 0; | 7707 this._physicalAverage = 0; |
| 13575 this._physicalAverageCount = 0; | 7708 this._physicalAverageCount = 0; |
| 13576 }, | 7709 }, |
| 13577 | |
| 13578 /** | |
| 13579 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
| 13580 * when the element is resized. | |
| 13581 */ | |
| 13582 _resizeHandler: function() { | 7710 _resizeHandler: function() { |
| 13583 // iOS fires the resize event when the address bar slides up | |
| 13584 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 7711 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
| 13585 return; | 7712 return; |
| 13586 } | 7713 } |
| 13587 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
| 13588 // changing the scroll position from a resize handler would result in | |
| 13589 // the scroll position being reset. Waiting 1ms fixes the issue. | |
| 13590 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 7714 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
| 13591 this.updateViewportBoundaries(); | 7715 this.updateViewportBoundaries(); |
| 13592 this._render(); | 7716 this._render(); |
| 13593 | |
| 13594 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 7717 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
| 13595 this._resetAverage(); | 7718 this._resetAverage(); |
| 13596 this.scrollToIndex(this.firstVisibleIndex); | 7719 this.scrollToIndex(this.firstVisibleIndex); |
| 13597 } | 7720 } |
| 13598 }.bind(this), 1)); | 7721 }.bind(this), 1)); |
| 13599 }, | 7722 }, |
| 13600 | |
| 13601 _getModelFromItem: function(item) { | 7723 _getModelFromItem: function(item) { |
| 13602 var key = this._collection.getKey(item); | 7724 var key = this._collection.getKey(item); |
| 13603 var pidx = this._physicalIndexForKey[key]; | 7725 var pidx = this._physicalIndexForKey[key]; |
| 13604 | |
| 13605 if (pidx != null) { | 7726 if (pidx != null) { |
| 13606 return this._physicalItems[pidx]._templateInstance; | 7727 return this._physicalItems[pidx]._templateInstance; |
| 13607 } | 7728 } |
| 13608 return null; | 7729 return null; |
| 13609 }, | 7730 }, |
| 13610 | |
| 13611 /** | |
| 13612 * Gets a valid item instance from its index or the object value. | |
| 13613 * | |
| 13614 * @param {(Object|number)} item The item object or its index | |
| 13615 */ | |
| 13616 _getNormalizedItem: function(item) { | 7731 _getNormalizedItem: function(item) { |
| 13617 if (this._collection.getKey(item) === undefined) { | 7732 if (this._collection.getKey(item) === undefined) { |
| 13618 if (typeof item === 'number') { | 7733 if (typeof item === 'number') { |
| 13619 item = this.items[item]; | 7734 item = this.items[item]; |
| 13620 if (!item) { | 7735 if (!item) { |
| 13621 throw new RangeError('<item> not found'); | 7736 throw new RangeError('<item> not found'); |
| 13622 } | 7737 } |
| 13623 return item; | 7738 return item; |
| 13624 } | 7739 } |
| 13625 throw new TypeError('<item> should be a valid item'); | 7740 throw new TypeError('<item> should be a valid item'); |
| 13626 } | 7741 } |
| 13627 return item; | 7742 return item; |
| 13628 }, | 7743 }, |
| 13629 | |
| 13630 /** | |
| 13631 * Select the list item at the given index. | |
| 13632 * | |
| 13633 * @method selectItem | |
| 13634 * @param {(Object|number)} item The item object or its index | |
| 13635 */ | |
| 13636 selectItem: function(item) { | 7744 selectItem: function(item) { |
| 13637 item = this._getNormalizedItem(item); | 7745 item = this._getNormalizedItem(item); |
| 13638 var model = this._getModelFromItem(item); | 7746 var model = this._getModelFromItem(item); |
| 13639 | |
| 13640 if (!this.multiSelection && this.selectedItem) { | 7747 if (!this.multiSelection && this.selectedItem) { |
| 13641 this.deselectItem(this.selectedItem); | 7748 this.deselectItem(this.selectedItem); |
| 13642 } | 7749 } |
| 13643 if (model) { | 7750 if (model) { |
| 13644 model[this.selectedAs] = true; | 7751 model[this.selectedAs] = true; |
| 13645 } | 7752 } |
| 13646 this.$.selector.select(item); | 7753 this.$.selector.select(item); |
| 13647 this.updateSizeForItem(item); | 7754 this.updateSizeForItem(item); |
| 13648 }, | 7755 }, |
| 13649 | |
| 13650 /** | |
| 13651 * Deselects the given item list if it is already selected. | |
| 13652 * | |
| 13653 | |
| 13654 * @method deselect | |
| 13655 * @param {(Object|number)} item The item object or its index | |
| 13656 */ | |
| 13657 deselectItem: function(item) { | 7756 deselectItem: function(item) { |
| 13658 item = this._getNormalizedItem(item); | 7757 item = this._getNormalizedItem(item); |
| 13659 var model = this._getModelFromItem(item); | 7758 var model = this._getModelFromItem(item); |
| 13660 | |
| 13661 if (model) { | 7759 if (model) { |
| 13662 model[this.selectedAs] = false; | 7760 model[this.selectedAs] = false; |
| 13663 } | 7761 } |
| 13664 this.$.selector.deselect(item); | 7762 this.$.selector.deselect(item); |
| 13665 this.updateSizeForItem(item); | 7763 this.updateSizeForItem(item); |
| 13666 }, | 7764 }, |
| 13667 | |
| 13668 /** | |
| 13669 * Select or deselect a given item depending on whether the item | |
| 13670 * has already been selected. | |
| 13671 * | |
| 13672 * @method toggleSelectionForItem | |
| 13673 * @param {(Object|number)} item The item object or its index | |
| 13674 */ | |
| 13675 toggleSelectionForItem: function(item) { | 7765 toggleSelectionForItem: function(item) { |
| 13676 item = this._getNormalizedItem(item); | 7766 item = this._getNormalizedItem(item); |
| 13677 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 7767 if (this.$.selector.isSelected(item)) { |
| 13678 this.deselectItem(item); | 7768 this.deselectItem(item); |
| 13679 } else { | 7769 } else { |
| 13680 this.selectItem(item); | 7770 this.selectItem(item); |
| 13681 } | 7771 } |
| 13682 }, | 7772 }, |
| 13683 | |
| 13684 /** | |
| 13685 * Clears the current selection state of the list. | |
| 13686 * | |
| 13687 * @method clearSelection | |
| 13688 */ | |
| 13689 clearSelection: function() { | 7773 clearSelection: function() { |
| 13690 function unselect(item) { | 7774 function unselect(item) { |
| 13691 var model = this._getModelFromItem(item); | 7775 var model = this._getModelFromItem(item); |
| 13692 if (model) { | 7776 if (model) { |
| 13693 model[this.selectedAs] = false; | 7777 model[this.selectedAs] = false; |
| 13694 } | 7778 } |
| 13695 } | 7779 } |
| 13696 | |
| 13697 if (Array.isArray(this.selectedItems)) { | 7780 if (Array.isArray(this.selectedItems)) { |
| 13698 this.selectedItems.forEach(unselect, this); | 7781 this.selectedItems.forEach(unselect, this); |
| 13699 } else if (this.selectedItem) { | 7782 } else if (this.selectedItem) { |
| 13700 unselect.call(this, this.selectedItem); | 7783 unselect.call(this, this.selectedItem); |
| 13701 } | 7784 } |
| 13702 | 7785 this.$.selector.clearSelection(); |
| 13703 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
| 13704 }, | 7786 }, |
| 13705 | |
| 13706 /** | |
| 13707 * Add an event listener to `tap` if `selectionEnabled` is true, | |
| 13708 * it will remove the listener otherwise. | |
| 13709 */ | |
| 13710 _selectionEnabledChanged: function(selectionEnabled) { | 7787 _selectionEnabledChanged: function(selectionEnabled) { |
| 13711 var handler = selectionEnabled ? this.listen : this.unlisten; | 7788 var handler = selectionEnabled ? this.listen : this.unlisten; |
| 13712 handler.call(this, this, 'tap', '_selectionHandler'); | 7789 handler.call(this, this, 'tap', '_selectionHandler'); |
| 13713 }, | 7790 }, |
| 13714 | |
| 13715 /** | |
| 13716 * Select an item from an event object. | |
| 13717 */ | |
| 13718 _selectionHandler: function(e) { | 7791 _selectionHandler: function(e) { |
| 13719 var model = this.modelForElement(e.target); | 7792 var model = this.modelForElement(e.target); |
| 13720 if (!model) { | 7793 if (!model) { |
| 13721 return; | 7794 return; |
| 13722 } | 7795 } |
| 13723 var modelTabIndex, activeElTabIndex; | 7796 var modelTabIndex, activeElTabIndex; |
| 13724 var target = Polymer.dom(e).path[0]; | 7797 var target = Polymer.dom(e).path[0]; |
| 13725 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 7798 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
| 13726 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 7799 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
| 13727 // Safari does not focus certain form controls via mouse | 7800 if (target.localName === 'input' || target.localName === 'button' || targe
t.localName === 'select') { |
| 13728 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
| 13729 if (target.localName === 'input' || | |
| 13730 target.localName === 'button' || | |
| 13731 target.localName === 'select') { | |
| 13732 return; | 7801 return; |
| 13733 } | 7802 } |
| 13734 // Set a temporary tabindex | |
| 13735 modelTabIndex = model.tabIndex; | 7803 modelTabIndex = model.tabIndex; |
| 13736 model.tabIndex = SECRET_TABINDEX; | 7804 model.tabIndex = SECRET_TABINDEX; |
| 13737 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 7805 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
| 13738 model.tabIndex = modelTabIndex; | 7806 model.tabIndex = modelTabIndex; |
| 13739 // Only select the item if the tap wasn't on a focusable child | |
| 13740 // or the element bound to `tabIndex` | |
| 13741 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 7807 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
| 13742 return; | 7808 return; |
| 13743 } | 7809 } |
| 13744 this.toggleSelectionForItem(model[this.as]); | 7810 this.toggleSelectionForItem(model[this.as]); |
| 13745 }, | 7811 }, |
| 13746 | |
| 13747 _multiSelectionChanged: function(multiSelection) { | 7812 _multiSelectionChanged: function(multiSelection) { |
| 13748 this.clearSelection(); | 7813 this.clearSelection(); |
| 13749 this.$.selector.multi = multiSelection; | 7814 this.$.selector.multi = multiSelection; |
| 13750 }, | 7815 }, |
| 13751 | |
| 13752 /** | |
| 13753 * Updates the size of an item. | |
| 13754 * | |
| 13755 * @method updateSizeForItem | |
| 13756 * @param {(Object|number)} item The item object or its index | |
| 13757 */ | |
| 13758 updateSizeForItem: function(item) { | 7816 updateSizeForItem: function(item) { |
| 13759 item = this._getNormalizedItem(item); | 7817 item = this._getNormalizedItem(item); |
| 13760 var key = this._collection.getKey(item); | 7818 var key = this._collection.getKey(item); |
| 13761 var pidx = this._physicalIndexForKey[key]; | 7819 var pidx = this._physicalIndexForKey[key]; |
| 13762 | |
| 13763 if (pidx != null) { | 7820 if (pidx != null) { |
| 13764 this._updateMetrics([pidx]); | 7821 this._updateMetrics([ pidx ]); |
| 13765 this._positionItems(); | 7822 this._positionItems(); |
| 13766 } | 7823 } |
| 13767 }, | 7824 }, |
| 13768 | |
| 13769 /** | |
| 13770 * Creates a temporary backfill item in the rendered pool of physical items | |
| 13771 * to replace the main focused item. The focused item has tabIndex = 0 | |
| 13772 * and might be currently focused by the user. | |
| 13773 * | |
| 13774 * This dynamic replacement helps to preserve the focus state. | |
| 13775 */ | |
| 13776 _manageFocus: function() { | 7825 _manageFocus: function() { |
| 13777 var fidx = this._focusedIndex; | 7826 var fidx = this._focusedIndex; |
| 13778 | |
| 13779 if (fidx >= 0 && fidx < this._virtualCount) { | 7827 if (fidx >= 0 && fidx < this._virtualCount) { |
| 13780 // if it's a valid index, check if that index is rendered | |
| 13781 // in a physical item. | |
| 13782 if (this._isIndexRendered(fidx)) { | 7828 if (this._isIndexRendered(fidx)) { |
| 13783 this._restoreFocusedItem(); | 7829 this._restoreFocusedItem(); |
| 13784 } else { | 7830 } else { |
| 13785 this._createFocusBackfillItem(); | 7831 this._createFocusBackfillItem(); |
| 13786 } | 7832 } |
| 13787 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 7833 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
| 13788 // otherwise, assign the initial focused index. | |
| 13789 this._focusedIndex = this._virtualStart; | 7834 this._focusedIndex = this._virtualStart; |
| 13790 this._focusedItem = this._physicalItems[this._physicalStart]; | 7835 this._focusedItem = this._physicalItems[this._physicalStart]; |
| 13791 } | 7836 } |
| 13792 }, | 7837 }, |
| 13793 | |
| 13794 _isIndexRendered: function(idx) { | 7838 _isIndexRendered: function(idx) { |
| 13795 return idx >= this._virtualStart && idx <= this._virtualEnd; | 7839 return idx >= this._virtualStart && idx <= this._virtualEnd; |
| 13796 }, | 7840 }, |
| 13797 | |
| 13798 _isIndexVisible: function(idx) { | 7841 _isIndexVisible: function(idx) { |
| 13799 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 7842 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
| 13800 }, | 7843 }, |
| 13801 | |
| 13802 _getPhysicalIndex: function(idx) { | 7844 _getPhysicalIndex: function(idx) { |
| 13803 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 7845 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
| 13804 }, | 7846 }, |
| 13805 | |
| 13806 _focusPhysicalItem: function(idx) { | 7847 _focusPhysicalItem: function(idx) { |
| 13807 if (idx < 0 || idx >= this._virtualCount) { | 7848 if (idx < 0 || idx >= this._virtualCount) { |
| 13808 return; | 7849 return; |
| 13809 } | 7850 } |
| 13810 this._restoreFocusedItem(); | 7851 this._restoreFocusedItem(); |
| 13811 // scroll to index to make sure it's rendered | |
| 13812 if (!this._isIndexRendered(idx)) { | 7852 if (!this._isIndexRendered(idx)) { |
| 13813 this.scrollToIndex(idx); | 7853 this.scrollToIndex(idx); |
| 13814 } | 7854 } |
| 13815 | |
| 13816 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 7855 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
| 13817 var model = physicalItem._templateInstance; | 7856 var model = physicalItem._templateInstance; |
| 13818 var focusable; | 7857 var focusable; |
| 13819 | |
| 13820 // set a secret tab index | |
| 13821 model.tabIndex = SECRET_TABINDEX; | 7858 model.tabIndex = SECRET_TABINDEX; |
| 13822 // check if focusable element is the physical item | |
| 13823 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 7859 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
| 13824 focusable = physicalItem; | 7860 focusable = physicalItem; |
| 13825 } | 7861 } |
| 13826 // search for the element which tabindex is bound to the secret tab index | |
| 13827 if (!focusable) { | 7862 if (!focusable) { |
| 13828 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 7863 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
| 13829 } | 7864 } |
| 13830 // restore the tab index | |
| 13831 model.tabIndex = 0; | 7865 model.tabIndex = 0; |
| 13832 // focus the focusable element | |
| 13833 this._focusedIndex = idx; | 7866 this._focusedIndex = idx; |
| 13834 focusable && focusable.focus(); | 7867 focusable && focusable.focus(); |
| 13835 }, | 7868 }, |
| 13836 | |
| 13837 _removeFocusedItem: function() { | 7869 _removeFocusedItem: function() { |
| 13838 if (this._offscreenFocusedItem) { | 7870 if (this._offscreenFocusedItem) { |
| 13839 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 7871 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
| 13840 } | 7872 } |
| 13841 this._offscreenFocusedItem = null; | 7873 this._offscreenFocusedItem = null; |
| 13842 this._focusBackfillItem = null; | 7874 this._focusBackfillItem = null; |
| 13843 this._focusedItem = null; | 7875 this._focusedItem = null; |
| 13844 this._focusedIndex = -1; | 7876 this._focusedIndex = -1; |
| 13845 }, | 7877 }, |
| 13846 | |
| 13847 _createFocusBackfillItem: function() { | 7878 _createFocusBackfillItem: function() { |
| 13848 var pidx, fidx = this._focusedIndex; | 7879 var pidx, fidx = this._focusedIndex; |
| 13849 if (this._offscreenFocusedItem || fidx < 0) { | 7880 if (this._offscreenFocusedItem || fidx < 0) { |
| 13850 return; | 7881 return; |
| 13851 } | 7882 } |
| 13852 if (!this._focusBackfillItem) { | 7883 if (!this._focusBackfillItem) { |
| 13853 // create a physical item, so that it backfills the focused item. | |
| 13854 var stampedTemplate = this.stamp(null); | 7884 var stampedTemplate = this.stamp(null); |
| 13855 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 7885 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
| 13856 Polymer.dom(this).appendChild(stampedTemplate.root); | 7886 Polymer.dom(this).appendChild(stampedTemplate.root); |
| 13857 } | 7887 } |
| 13858 // get the physical index for the focused index | |
| 13859 pidx = this._getPhysicalIndex(fidx); | 7888 pidx = this._getPhysicalIndex(fidx); |
| 13860 | |
| 13861 if (pidx != null) { | 7889 if (pidx != null) { |
| 13862 // set the offcreen focused physical item | |
| 13863 this._offscreenFocusedItem = this._physicalItems[pidx]; | 7890 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 13864 // backfill the focused physical item | |
| 13865 this._physicalItems[pidx] = this._focusBackfillItem; | 7891 this._physicalItems[pidx] = this._focusBackfillItem; |
| 13866 // hide the focused physical | |
| 13867 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 7892 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 13868 } | 7893 } |
| 13869 }, | 7894 }, |
| 13870 | |
| 13871 _restoreFocusedItem: function() { | 7895 _restoreFocusedItem: function() { |
| 13872 var pidx, fidx = this._focusedIndex; | 7896 var pidx, fidx = this._focusedIndex; |
| 13873 | |
| 13874 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 7897 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
| 13875 return; | 7898 return; |
| 13876 } | 7899 } |
| 13877 // assign models to the focused index | |
| 13878 this._assignModels(); | 7900 this._assignModels(); |
| 13879 // get the new physical index for the focused index | |
| 13880 pidx = this._getPhysicalIndex(fidx); | 7901 pidx = this._getPhysicalIndex(fidx); |
| 13881 | |
| 13882 if (pidx != null) { | 7902 if (pidx != null) { |
| 13883 // flip the focus backfill | |
| 13884 this._focusBackfillItem = this._physicalItems[pidx]; | 7903 this._focusBackfillItem = this._physicalItems[pidx]; |
| 13885 // restore the focused physical item | |
| 13886 this._physicalItems[pidx] = this._offscreenFocusedItem; | 7904 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 13887 // reset the offscreen focused item | |
| 13888 this._offscreenFocusedItem = null; | 7905 this._offscreenFocusedItem = null; |
| 13889 // hide the physical item that backfills | |
| 13890 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 7906 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
| 13891 } | 7907 } |
| 13892 }, | 7908 }, |
| 13893 | |
| 13894 _didFocus: function(e) { | 7909 _didFocus: function(e) { |
| 13895 var targetModel = this.modelForElement(e.target); | 7910 var targetModel = this.modelForElement(e.target); |
| 13896 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 7911 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
| 13897 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 7912 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
| 13898 var fidx = this._focusedIndex; | 7913 var fidx = this._focusedIndex; |
| 13899 | |
| 13900 if (!targetModel || !focusedModel) { | 7914 if (!targetModel || !focusedModel) { |
| 13901 return; | 7915 return; |
| 13902 } | 7916 } |
| 13903 if (focusedModel === targetModel) { | 7917 if (focusedModel === targetModel) { |
| 13904 // if the user focused the same item, then bring it into view if it's no
t visible | |
| 13905 if (!this._isIndexVisible(fidx)) { | 7918 if (!this._isIndexVisible(fidx)) { |
| 13906 this.scrollToIndex(fidx); | 7919 this.scrollToIndex(fidx); |
| 13907 } | 7920 } |
| 13908 } else { | 7921 } else { |
| 13909 this._restoreFocusedItem(); | 7922 this._restoreFocusedItem(); |
| 13910 // restore tabIndex for the currently focused item | |
| 13911 focusedModel.tabIndex = -1; | 7923 focusedModel.tabIndex = -1; |
| 13912 // set the tabIndex for the next focused item | |
| 13913 targetModel.tabIndex = 0; | 7924 targetModel.tabIndex = 0; |
| 13914 fidx = targetModel[this.indexAs]; | 7925 fidx = targetModel[this.indexAs]; |
| 13915 this._focusedIndex = fidx; | 7926 this._focusedIndex = fidx; |
| 13916 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 7927 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
| 13917 | |
| 13918 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 7928 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
| 13919 this._update(); | 7929 this._update(); |
| 13920 } | 7930 } |
| 13921 } | 7931 } |
| 13922 }, | 7932 }, |
| 13923 | |
| 13924 _didMoveUp: function() { | 7933 _didMoveUp: function() { |
| 13925 this._focusPhysicalItem(this._focusedIndex - 1); | 7934 this._focusPhysicalItem(this._focusedIndex - 1); |
| 13926 }, | 7935 }, |
| 13927 | |
| 13928 _didMoveDown: function(e) { | 7936 _didMoveDown: function(e) { |
| 13929 // disable scroll when pressing the down key | |
| 13930 e.detail.keyboardEvent.preventDefault(); | 7937 e.detail.keyboardEvent.preventDefault(); |
| 13931 this._focusPhysicalItem(this._focusedIndex + 1); | 7938 this._focusPhysicalItem(this._focusedIndex + 1); |
| 13932 }, | 7939 }, |
| 13933 | |
| 13934 _didEnter: function(e) { | 7940 _didEnter: function(e) { |
| 13935 this._focusPhysicalItem(this._focusedIndex); | 7941 this._focusPhysicalItem(this._focusedIndex); |
| 13936 this._selectionHandler(e.detail.keyboardEvent); | 7942 this._selectionHandler(e.detail.keyboardEvent); |
| 13937 } | 7943 } |
| 13938 }); | 7944 }); |
| 7945 })(); |
| 13939 | 7946 |
| 13940 })(); | |
| 13941 Polymer({ | 7947 Polymer({ |
| 7948 is: 'iron-scroll-threshold', |
| 7949 properties: { |
| 7950 upperThreshold: { |
| 7951 type: Number, |
| 7952 value: 100 |
| 7953 }, |
| 7954 lowerThreshold: { |
| 7955 type: Number, |
| 7956 value: 100 |
| 7957 }, |
| 7958 upperTriggered: { |
| 7959 type: Boolean, |
| 7960 value: false, |
| 7961 notify: true, |
| 7962 readOnly: true |
| 7963 }, |
| 7964 lowerTriggered: { |
| 7965 type: Boolean, |
| 7966 value: false, |
| 7967 notify: true, |
| 7968 readOnly: true |
| 7969 }, |
| 7970 horizontal: { |
| 7971 type: Boolean, |
| 7972 value: false |
| 7973 } |
| 7974 }, |
| 7975 behaviors: [ Polymer.IronScrollTargetBehavior ], |
| 7976 observers: [ '_setOverflow(scrollTarget)', '_initCheck(horizontal, isAttached)
' ], |
| 7977 get _defaultScrollTarget() { |
| 7978 return this; |
| 7979 }, |
| 7980 _setOverflow: function(scrollTarget) { |
| 7981 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 7982 }, |
| 7983 _scrollHandler: function() { |
| 7984 var THROTTLE_THRESHOLD = 200; |
| 7985 if (!this.isDebouncerActive('_checkTheshold')) { |
| 7986 this.debounce('_checkTheshold', function() { |
| 7987 this.checkScrollThesholds(); |
| 7988 }, THROTTLE_THRESHOLD); |
| 7989 } |
| 7990 }, |
| 7991 _initCheck: function(horizontal, isAttached) { |
| 7992 if (isAttached) { |
| 7993 this.debounce('_init', function() { |
| 7994 this.clearTriggers(); |
| 7995 this.checkScrollThesholds(); |
| 7996 }); |
| 7997 } |
| 7998 }, |
| 7999 checkScrollThesholds: function() { |
| 8000 if (!this.scrollTarget || this.lowerTriggered && this.upperTriggered) { |
| 8001 return; |
| 8002 } |
| 8003 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTop; |
| 8004 var lowerScrollValue = this.horizontal ? this.scrollTarget.scrollWidth - thi
s._scrollTargetWidth - this._scrollLeft : this.scrollTarget.scrollHeight - this.
_scrollTargetHeight - this._scrollTop; |
| 8005 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { |
| 8006 this._setUpperTriggered(true); |
| 8007 this.fire('upper-threshold'); |
| 8008 } |
| 8009 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { |
| 8010 this._setLowerTriggered(true); |
| 8011 this.fire('lower-threshold'); |
| 8012 } |
| 8013 }, |
| 8014 clearTriggers: function() { |
| 8015 this._setUpperTriggered(false); |
| 8016 this._setLowerTriggered(false); |
| 8017 } |
| 8018 }); |
| 13942 | 8019 |
| 13943 is: 'iron-scroll-threshold', | |
| 13944 | |
| 13945 properties: { | |
| 13946 | |
| 13947 /** | |
| 13948 * Distance from the top (or left, for horizontal) bound of the scroller | |
| 13949 * where the "upper trigger" will fire. | |
| 13950 */ | |
| 13951 upperThreshold: { | |
| 13952 type: Number, | |
| 13953 value: 100 | |
| 13954 }, | |
| 13955 | |
| 13956 /** | |
| 13957 * Distance from the bottom (or right, for horizontal) bound of the scroll
er | |
| 13958 * where the "lower trigger" will fire. | |
| 13959 */ | |
| 13960 lowerThreshold: { | |
| 13961 type: Number, | |
| 13962 value: 100 | |
| 13963 }, | |
| 13964 | |
| 13965 /** | |
| 13966 * Read-only value that tracks the triggered state of the upper threshold. | |
| 13967 */ | |
| 13968 upperTriggered: { | |
| 13969 type: Boolean, | |
| 13970 value: false, | |
| 13971 notify: true, | |
| 13972 readOnly: true | |
| 13973 }, | |
| 13974 | |
| 13975 /** | |
| 13976 * Read-only value that tracks the triggered state of the lower threshold. | |
| 13977 */ | |
| 13978 lowerTriggered: { | |
| 13979 type: Boolean, | |
| 13980 value: false, | |
| 13981 notify: true, | |
| 13982 readOnly: true | |
| 13983 }, | |
| 13984 | |
| 13985 /** | |
| 13986 * True if the orientation of the scroller is horizontal. | |
| 13987 */ | |
| 13988 horizontal: { | |
| 13989 type: Boolean, | |
| 13990 value: false | |
| 13991 } | |
| 13992 }, | |
| 13993 | |
| 13994 behaviors: [ | |
| 13995 Polymer.IronScrollTargetBehavior | |
| 13996 ], | |
| 13997 | |
| 13998 observers: [ | |
| 13999 '_setOverflow(scrollTarget)', | |
| 14000 '_initCheck(horizontal, isAttached)' | |
| 14001 ], | |
| 14002 | |
| 14003 get _defaultScrollTarget() { | |
| 14004 return this; | |
| 14005 }, | |
| 14006 | |
| 14007 _setOverflow: function(scrollTarget) { | |
| 14008 this.style.overflow = scrollTarget === this ? 'auto' : ''; | |
| 14009 }, | |
| 14010 | |
| 14011 _scrollHandler: function() { | |
| 14012 // throttle the work on the scroll event | |
| 14013 var THROTTLE_THRESHOLD = 200; | |
| 14014 if (!this.isDebouncerActive('_checkTheshold')) { | |
| 14015 this.debounce('_checkTheshold', function() { | |
| 14016 this.checkScrollThesholds(); | |
| 14017 }, THROTTLE_THRESHOLD); | |
| 14018 } | |
| 14019 }, | |
| 14020 | |
| 14021 _initCheck: function(horizontal, isAttached) { | |
| 14022 if (isAttached) { | |
| 14023 this.debounce('_init', function() { | |
| 14024 this.clearTriggers(); | |
| 14025 this.checkScrollThesholds(); | |
| 14026 }); | |
| 14027 } | |
| 14028 }, | |
| 14029 | |
| 14030 /** | |
| 14031 * Checks the scroll thresholds. | |
| 14032 * This method is automatically called by iron-scroll-threshold. | |
| 14033 * | |
| 14034 * @method checkScrollThesholds | |
| 14035 */ | |
| 14036 checkScrollThesholds: function() { | |
| 14037 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) { | |
| 14038 return; | |
| 14039 } | |
| 14040 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo
p; | |
| 14041 var lowerScrollValue = this.horizontal ? | |
| 14042 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll
Left : | |
| 14043 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._
scrollTop; | |
| 14044 | |
| 14045 // Detect upper threshold | |
| 14046 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { | |
| 14047 this._setUpperTriggered(true); | |
| 14048 this.fire('upper-threshold'); | |
| 14049 } | |
| 14050 // Detect lower threshold | |
| 14051 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { | |
| 14052 this._setLowerTriggered(true); | |
| 14053 this.fire('lower-threshold'); | |
| 14054 } | |
| 14055 }, | |
| 14056 | |
| 14057 /** | |
| 14058 * Clear the upper and lower threshold states. | |
| 14059 * | |
| 14060 * @method clearTriggers | |
| 14061 */ | |
| 14062 clearTriggers: function() { | |
| 14063 this._setUpperTriggered(false); | |
| 14064 this._setLowerTriggered(false); | |
| 14065 } | |
| 14066 | |
| 14067 /** | |
| 14068 * Fires when the lower threshold has been reached. | |
| 14069 * | |
| 14070 * @event lower-threshold | |
| 14071 */ | |
| 14072 | |
| 14073 /** | |
| 14074 * Fires when the upper threshold has been reached. | |
| 14075 * | |
| 14076 * @event upper-threshold | |
| 14077 */ | |
| 14078 | |
| 14079 }); | |
| 14080 // Copyright 2015 The Chromium Authors. All rights reserved. | 8020 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 14081 // Use of this source code is governed by a BSD-style license that can be | 8021 // Use of this source code is governed by a BSD-style license that can be |
| 14082 // found in the LICENSE file. | 8022 // found in the LICENSE file. |
| 14083 | |
| 14084 Polymer({ | 8023 Polymer({ |
| 14085 is: 'history-list', | 8024 is: 'history-list', |
| 14086 | 8025 behaviors: [ HistoryListBehavior ], |
| 14087 behaviors: [HistoryListBehavior], | |
| 14088 | |
| 14089 properties: { | 8026 properties: { |
| 14090 // The search term for the current query. Set when the query returns. | |
| 14091 searchedTerm: { | 8027 searchedTerm: { |
| 14092 type: String, | 8028 type: String, |
| 14093 value: '', | 8029 value: '' |
| 14094 }, | 8030 }, |
| 14095 | |
| 14096 querying: Boolean, | 8031 querying: Boolean, |
| 14097 | |
| 14098 // An array of history entries in reverse chronological order. | |
| 14099 historyData_: Array, | 8032 historyData_: Array, |
| 14100 | |
| 14101 resultLoadingDisabled_: { | 8033 resultLoadingDisabled_: { |
| 14102 type: Boolean, | 8034 type: Boolean, |
| 14103 value: false, | 8035 value: false |
| 14104 }, | 8036 } |
| 14105 }, | 8037 }, |
| 14106 | |
| 14107 listeners: { | 8038 listeners: { |
| 14108 'scroll': 'notifyListScroll_', | 8039 scroll: 'notifyListScroll_', |
| 14109 'remove-bookmark-stars': 'removeBookmarkStars_', | 8040 'remove-bookmark-stars': 'removeBookmarkStars_' |
| 14110 }, | 8041 }, |
| 14111 | |
| 14112 /** @override */ | |
| 14113 attached: function() { | 8042 attached: function() { |
| 14114 // It is possible (eg, when middle clicking the reload button) for all other | 8043 this.$['infinite-list'].notifyResize(); |
| 14115 // resize events to fire before the list is attached and can be measured. | |
| 14116 // Adding another resize here ensures it will get sized correctly. | |
| 14117 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize(); | |
| 14118 this.$['infinite-list'].scrollTarget = this; | 8044 this.$['infinite-list'].scrollTarget = this; |
| 14119 this.$['scroll-threshold'].scrollTarget = this; | 8045 this.$['scroll-threshold'].scrollTarget = this; |
| 14120 }, | 8046 }, |
| 14121 | |
| 14122 /** | |
| 14123 * Remove bookmark star for history items with matching URLs. | |
| 14124 * @param {{detail: !string}} e | |
| 14125 * @private | |
| 14126 */ | |
| 14127 removeBookmarkStars_: function(e) { | 8047 removeBookmarkStars_: function(e) { |
| 14128 var url = e.detail; | 8048 var url = e.detail; |
| 14129 | 8049 if (this.historyData_ === undefined) return; |
| 14130 if (this.historyData_ === undefined) | |
| 14131 return; | |
| 14132 | |
| 14133 for (var i = 0; i < this.historyData_.length; i++) { | 8050 for (var i = 0; i < this.historyData_.length; i++) { |
| 14134 if (this.historyData_[i].url == url) | 8051 if (this.historyData_[i].url == url) this.set('historyData_.' + i + '.star
red', false); |
| 14135 this.set('historyData_.' + i + '.starred', false); | 8052 } |
| 14136 } | 8053 }, |
| 14137 }, | |
| 14138 | |
| 14139 /** | |
| 14140 * Disables history result loading when there are no more history results. | |
| 14141 */ | |
| 14142 disableResultLoading: function() { | 8054 disableResultLoading: function() { |
| 14143 this.resultLoadingDisabled_ = true; | 8055 this.resultLoadingDisabled_ = true; |
| 14144 }, | 8056 }, |
| 14145 | |
| 14146 /** | |
| 14147 * Adds the newly updated history results into historyData_. Adds new fields | |
| 14148 * for each result. | |
| 14149 * @param {!Array<!HistoryEntry>} historyResults The new history results. | |
| 14150 * @param {boolean} incremental Whether the result is from loading more | |
| 14151 * history, or a new search/list reload. | |
| 14152 */ | |
| 14153 addNewResults: function(historyResults, incremental) { | 8057 addNewResults: function(historyResults, incremental) { |
| 14154 var results = historyResults.slice(); | 8058 var results = historyResults.slice(); |
| 14155 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) | 8059 this.$['scroll-threshold'].clearTriggers(); |
| 14156 .clearTriggers(); | |
| 14157 | |
| 14158 if (!incremental) { | 8060 if (!incremental) { |
| 14159 this.resultLoadingDisabled_ = false; | 8061 this.resultLoadingDisabled_ = false; |
| 14160 if (this.historyData_) | 8062 if (this.historyData_) this.splice('historyData_', 0, this.historyData_.le
ngth); |
| 14161 this.splice('historyData_', 0, this.historyData_.length); | |
| 14162 this.fire('unselect-all'); | 8063 this.fire('unselect-all'); |
| 14163 } | 8064 } |
| 14164 | |
| 14165 if (this.historyData_) { | 8065 if (this.historyData_) { |
| 14166 // If we have previously received data, push the new items onto the | |
| 14167 // existing array. | |
| 14168 results.unshift('historyData_'); | 8066 results.unshift('historyData_'); |
| 14169 this.push.apply(this, results); | 8067 this.push.apply(this, results); |
| 14170 } else { | 8068 } else { |
| 14171 // The first time we receive data, use set() to ensure the iron-list is | |
| 14172 // initialized correctly. | |
| 14173 this.set('historyData_', results); | 8069 this.set('historyData_', results); |
| 14174 } | 8070 } |
| 14175 }, | 8071 }, |
| 14176 | |
| 14177 /** | |
| 14178 * Called when the page is scrolled to near the bottom of the list. | |
| 14179 * @private | |
| 14180 */ | |
| 14181 loadMoreData_: function() { | 8072 loadMoreData_: function() { |
| 14182 if (this.resultLoadingDisabled_ || this.querying) | 8073 if (this.resultLoadingDisabled_ || this.querying) return; |
| 14183 return; | |
| 14184 | |
| 14185 this.fire('load-more-history'); | 8074 this.fire('load-more-history'); |
| 14186 }, | 8075 }, |
| 14187 | |
| 14188 /** | |
| 14189 * Check whether the time difference between the given history item and the | |
| 14190 * next one is large enough for a spacer to be required. | |
| 14191 * @param {HistoryEntry} item | |
| 14192 * @param {number} index The index of |item| in |historyData_|. | |
| 14193 * @param {number} length The length of |historyData_|. | |
| 14194 * @return {boolean} Whether or not time gap separator is required. | |
| 14195 * @private | |
| 14196 */ | |
| 14197 needsTimeGap_: function(item, index, length) { | 8076 needsTimeGap_: function(item, index, length) { |
| 14198 return md_history.HistoryItem.needsTimeGap( | 8077 return md_history.HistoryItem.needsTimeGap(this.historyData_, index, this.se
archedTerm); |
| 14199 this.historyData_, index, this.searchedTerm); | 8078 }, |
| 14200 }, | |
| 14201 | |
| 14202 /** | |
| 14203 * True if the given item is the beginning of a new card. | |
| 14204 * @param {HistoryEntry} item | |
| 14205 * @param {number} i Index of |item| within |historyData_|. | |
| 14206 * @param {number} length | |
| 14207 * @return {boolean} | |
| 14208 * @private | |
| 14209 */ | |
| 14210 isCardStart_: function(item, i, length) { | 8079 isCardStart_: function(item, i, length) { |
| 14211 if (length == 0 || i > length - 1) | 8080 if (length == 0 || i > length - 1) return false; |
| 14212 return false; | 8081 return i == 0 || this.historyData_[i].dateRelativeDay != this.historyData_[i
- 1].dateRelativeDay; |
| 14213 return i == 0 || | 8082 }, |
| 14214 this.historyData_[i].dateRelativeDay != | |
| 14215 this.historyData_[i - 1].dateRelativeDay; | |
| 14216 }, | |
| 14217 | |
| 14218 /** | |
| 14219 * True if the given item is the end of a card. | |
| 14220 * @param {HistoryEntry} item | |
| 14221 * @param {number} i Index of |item| within |historyData_|. | |
| 14222 * @param {number} length | |
| 14223 * @return {boolean} | |
| 14224 * @private | |
| 14225 */ | |
| 14226 isCardEnd_: function(item, i, length) { | 8083 isCardEnd_: function(item, i, length) { |
| 14227 if (length == 0 || i > length - 1) | 8084 if (length == 0 || i > length - 1) return false; |
| 14228 return false; | 8085 return i == length - 1 || this.historyData_[i].dateRelativeDay != this.histo
ryData_[i + 1].dateRelativeDay; |
| 14229 return i == length - 1 || | 8086 }, |
| 14230 this.historyData_[i].dateRelativeDay != | |
| 14231 this.historyData_[i + 1].dateRelativeDay; | |
| 14232 }, | |
| 14233 | |
| 14234 /** | |
| 14235 * @param {number} index | |
| 14236 * @return {boolean} | |
| 14237 * @private | |
| 14238 */ | |
| 14239 isFirstItem_: function(index) { | 8087 isFirstItem_: function(index) { |
| 14240 return index == 0; | 8088 return index == 0; |
| 14241 }, | 8089 }, |
| 14242 | |
| 14243 /** | |
| 14244 * @private | |
| 14245 */ | |
| 14246 notifyListScroll_: function() { | 8090 notifyListScroll_: function() { |
| 14247 this.fire('history-list-scrolled'); | 8091 this.fire('history-list-scrolled'); |
| 14248 }, | 8092 }, |
| 14249 | |
| 14250 /** | |
| 14251 * @param {number} index | |
| 14252 * @return {string} | |
| 14253 * @private | |
| 14254 */ | |
| 14255 pathForItem_: function(index) { | 8093 pathForItem_: function(index) { |
| 14256 return 'historyData_.' + index; | 8094 return 'historyData_.' + index; |
| 14257 }, | 8095 } |
| 14258 }); | 8096 }); |
| 8097 |
| 14259 // Copyright 2016 The Chromium Authors. All rights reserved. | 8098 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14260 // Use of this source code is governed by a BSD-style license that can be | 8099 // Use of this source code is governed by a BSD-style license that can be |
| 14261 // found in the LICENSE file. | 8100 // found in the LICENSE file. |
| 14262 | |
| 14263 /** | |
| 14264 * @fileoverview | |
| 14265 * history-lazy-render is a simple variant of dom-if designed for lazy rendering | |
| 14266 * of elements that are accessed imperatively. | |
| 14267 * Usage: | |
| 14268 * <template is="history-lazy-render" id="menu"> | |
| 14269 * <heavy-menu></heavy-menu> | |
| 14270 * </template> | |
| 14271 * | |
| 14272 * this.$.menu.get().then(function(menu) { | |
| 14273 * menu.show(); | |
| 14274 * }); | |
| 14275 */ | |
| 14276 | |
| 14277 Polymer({ | 8101 Polymer({ |
| 14278 is: 'history-lazy-render', | 8102 is: 'history-lazy-render', |
| 14279 extends: 'template', | 8103 "extends": 'template', |
| 14280 | 8104 behaviors: [ Polymer.Templatizer ], |
| 14281 behaviors: [ | |
| 14282 Polymer.Templatizer | |
| 14283 ], | |
| 14284 | |
| 14285 /** @private {Promise<Element>} */ | |
| 14286 _renderPromise: null, | 8105 _renderPromise: null, |
| 14287 | |
| 14288 /** @private {TemplateInstance} */ | |
| 14289 _instance: null, | 8106 _instance: null, |
| 14290 | |
| 14291 /** | |
| 14292 * Stamp the template into the DOM tree asynchronously | |
| 14293 * @return {Promise<Element>} Promise which resolves when the template has | |
| 14294 * been stamped. | |
| 14295 */ | |
| 14296 get: function() { | 8107 get: function() { |
| 14297 if (!this._renderPromise) { | 8108 if (!this._renderPromise) { |
| 14298 this._renderPromise = new Promise(function(resolve) { | 8109 this._renderPromise = new Promise(function(resolve) { |
| 14299 this._debounceTemplate(function() { | 8110 this._debounceTemplate(function() { |
| 14300 this._render(); | 8111 this._render(); |
| 14301 this._renderPromise = null; | 8112 this._renderPromise = null; |
| 14302 resolve(this.getIfExists()); | 8113 resolve(this.getIfExists()); |
| 14303 }.bind(this)); | 8114 }.bind(this)); |
| 14304 }.bind(this)); | 8115 }.bind(this)); |
| 14305 } | 8116 } |
| 14306 return this._renderPromise; | 8117 return this._renderPromise; |
| 14307 }, | 8118 }, |
| 14308 | |
| 14309 /** | |
| 14310 * @return {?Element} The element contained in the template, if it has | |
| 14311 * already been stamped. | |
| 14312 */ | |
| 14313 getIfExists: function() { | 8119 getIfExists: function() { |
| 14314 if (this._instance) { | 8120 if (this._instance) { |
| 14315 var children = this._instance._children; | 8121 var children = this._instance._children; |
| 14316 | |
| 14317 for (var i = 0; i < children.length; i++) { | 8122 for (var i = 0; i < children.length; i++) { |
| 14318 if (children[i].nodeType == Node.ELEMENT_NODE) | 8123 if (children[i].nodeType == Node.ELEMENT_NODE) return children[i]; |
| 14319 return children[i]; | |
| 14320 } | 8124 } |
| 14321 } | 8125 } |
| 14322 return null; | 8126 return null; |
| 14323 }, | 8127 }, |
| 14324 | |
| 14325 _render: function() { | 8128 _render: function() { |
| 14326 if (!this.ctor) | 8129 if (!this.ctor) this.templatize(this); |
| 14327 this.templatize(this); | |
| 14328 var parentNode = this.parentNode; | 8130 var parentNode = this.parentNode; |
| 14329 if (parentNode && !this._instance) { | 8131 if (parentNode && !this._instance) { |
| 14330 this._instance = /** @type {TemplateInstance} */(this.stamp({})); | 8132 this._instance = this.stamp({}); |
| 14331 var root = this._instance.root; | 8133 var root = this._instance.root; |
| 14332 parentNode.insertBefore(root, this); | 8134 parentNode.insertBefore(root, this); |
| 14333 } | 8135 } |
| 14334 }, | 8136 }, |
| 14335 | |
| 14336 /** | |
| 14337 * @param {string} prop | |
| 14338 * @param {Object} value | |
| 14339 */ | |
| 14340 _forwardParentProp: function(prop, value) { | 8137 _forwardParentProp: function(prop, value) { |
| 14341 if (this._instance) | 8138 if (this._instance) this._instance.__setProperty(prop, value, true); |
| 14342 this._instance.__setProperty(prop, value, true); | 8139 }, |
| 14343 }, | |
| 14344 | |
| 14345 /** | |
| 14346 * @param {string} path | |
| 14347 * @param {Object} value | |
| 14348 */ | |
| 14349 _forwardParentPath: function(path, value) { | 8140 _forwardParentPath: function(path, value) { |
| 14350 if (this._instance) | 8141 if (this._instance) this._instance._notifyPath(path, value, true); |
| 14351 this._instance._notifyPath(path, value, true); | |
| 14352 } | 8142 } |
| 14353 }); | 8143 }); |
| 8144 |
| 14354 // Copyright 2016 The Chromium Authors. All rights reserved. | 8145 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14355 // Use of this source code is governed by a BSD-style license that can be | 8146 // Use of this source code is governed by a BSD-style license that can be |
| 14356 // found in the LICENSE file. | 8147 // found in the LICENSE file. |
| 14357 | |
| 14358 Polymer({ | 8148 Polymer({ |
| 14359 is: 'history-list-container', | 8149 is: 'history-list-container', |
| 14360 | |
| 14361 properties: { | 8150 properties: { |
| 14362 // The path of the currently selected page. | |
| 14363 selectedPage_: String, | 8151 selectedPage_: String, |
| 14364 | |
| 14365 // Whether domain-grouped history is enabled. | |
| 14366 grouped: Boolean, | 8152 grouped: Boolean, |
| 14367 | |
| 14368 /** @type {!QueryState} */ | |
| 14369 queryState: Object, | 8153 queryState: Object, |
| 14370 | 8154 queryResult: Object |
| 14371 /** @type {!QueryResult} */ | 8155 }, |
| 14372 queryResult: Object, | 8156 observers: [ 'groupedRangeChanged_(queryState.range)' ], |
| 14373 }, | |
| 14374 | |
| 14375 observers: [ | |
| 14376 'groupedRangeChanged_(queryState.range)', | |
| 14377 ], | |
| 14378 | |
| 14379 listeners: { | 8157 listeners: { |
| 14380 'history-list-scrolled': 'closeMenu_', | 8158 'history-list-scrolled': 'closeMenu_', |
| 14381 'load-more-history': 'loadMoreHistory_', | 8159 'load-more-history': 'loadMoreHistory_', |
| 14382 'toggle-menu': 'toggleMenu_', | 8160 'toggle-menu': 'toggleMenu_' |
| 14383 }, | 8161 }, |
| 14384 | |
| 14385 /** | |
| 14386 * @param {HistoryQuery} info An object containing information about the | |
| 14387 * query. | |
| 14388 * @param {!Array<HistoryEntry>} results A list of results. | |
| 14389 */ | |
| 14390 historyResult: function(info, results) { | 8162 historyResult: function(info, results) { |
| 14391 this.initializeResults_(info, results); | 8163 this.initializeResults_(info, results); |
| 14392 this.closeMenu_(); | 8164 this.closeMenu_(); |
| 14393 | |
| 14394 if (this.selectedPage_ == 'grouped-list') { | 8165 if (this.selectedPage_ == 'grouped-list') { |
| 14395 this.$$('#grouped-list').historyData = results; | 8166 this.$$('#grouped-list').historyData = results; |
| 14396 return; | 8167 return; |
| 14397 } | 8168 } |
| 14398 | 8169 var list = this.$['infinite-list']; |
| 14399 var list = /** @type {HistoryListElement} */(this.$['infinite-list']); | |
| 14400 list.addNewResults(results, this.queryState.incremental); | 8170 list.addNewResults(results, this.queryState.incremental); |
| 14401 if (info.finished) | 8171 if (info.finished) list.disableResultLoading(); |
| 14402 list.disableResultLoading(); | 8172 }, |
| 14403 }, | |
| 14404 | |
| 14405 /** | |
| 14406 * Queries the history backend for results based on queryState. | |
| 14407 * @param {boolean} incremental Whether the new query should continue where | |
| 14408 * the previous query stopped. | |
| 14409 */ | |
| 14410 queryHistory: function(incremental) { | 8173 queryHistory: function(incremental) { |
| 14411 var queryState = this.queryState; | 8174 var queryState = this.queryState; |
| 14412 // Disable querying until the first set of results have been returned. If | |
| 14413 // there is a search, query immediately to support search query params from | |
| 14414 // the URL. | |
| 14415 var noResults = !this.queryResult || this.queryResult.results == null; | 8175 var noResults = !this.queryResult || this.queryResult.results == null; |
| 14416 if (queryState.queryingDisabled || | 8176 if (queryState.queryingDisabled || !this.queryState.searchTerm && noResults)
{ |
| 14417 (!this.queryState.searchTerm && noResults)) { | |
| 14418 return; | 8177 return; |
| 14419 } | 8178 } |
| 14420 | |
| 14421 // Close any open dialog if a new query is initiated. | |
| 14422 var dialog = this.$.dialog.getIfExists(); | 8179 var dialog = this.$.dialog.getIfExists(); |
| 14423 if (!incremental && dialog && dialog.open) | 8180 if (!incremental && dialog && dialog.open) dialog.close(); |
| 14424 dialog.close(); | |
| 14425 | |
| 14426 this.set('queryState.querying', true); | 8181 this.set('queryState.querying', true); |
| 14427 this.set('queryState.incremental', incremental); | 8182 this.set('queryState.incremental', incremental); |
| 14428 | |
| 14429 var lastVisitTime = 0; | 8183 var lastVisitTime = 0; |
| 14430 if (incremental) { | 8184 if (incremental) { |
| 14431 var lastVisit = this.queryResult.results.slice(-1)[0]; | 8185 var lastVisit = this.queryResult.results.slice(-1)[0]; |
| 14432 lastVisitTime = lastVisit ? lastVisit.time : 0; | 8186 lastVisitTime = lastVisit ? lastVisit.time : 0; |
| 14433 } | 8187 } |
| 14434 | 8188 var maxResults = queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAG
E : 0; |
| 14435 var maxResults = | 8189 chrome.send('queryHistory', [ queryState.searchTerm, queryState.groupedOffse
t, queryState.range, lastVisitTime, maxResults ]); |
| 14436 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0; | 8190 }, |
| 14437 chrome.send('queryHistory', [ | |
| 14438 queryState.searchTerm, queryState.groupedOffset, queryState.range, | |
| 14439 lastVisitTime, maxResults | |
| 14440 ]); | |
| 14441 }, | |
| 14442 | |
| 14443 historyDeleted: function() { | 8191 historyDeleted: function() { |
| 14444 // Do not reload the list when there are items checked. | 8192 if (this.getSelectedItemCount() > 0) return; |
| 14445 if (this.getSelectedItemCount() > 0) | |
| 14446 return; | |
| 14447 | |
| 14448 // Reload the list with current search state. | |
| 14449 this.queryHistory(false); | 8193 this.queryHistory(false); |
| 14450 }, | 8194 }, |
| 14451 | |
| 14452 /** @return {number} */ | |
| 14453 getSelectedItemCount: function() { | 8195 getSelectedItemCount: function() { |
| 14454 return this.getSelectedList_().selectedPaths.size; | 8196 return this.getSelectedList_().selectedPaths.size; |
| 14455 }, | 8197 }, |
| 14456 | |
| 14457 unselectAllItems: function(count) { | 8198 unselectAllItems: function(count) { |
| 14458 var selectedList = this.getSelectedList_(); | 8199 var selectedList = this.getSelectedList_(); |
| 14459 if (selectedList) | 8200 if (selectedList) selectedList.unselectAllItems(count); |
| 14460 selectedList.unselectAllItems(count); | 8201 }, |
| 14461 }, | |
| 14462 | |
| 14463 /** | |
| 14464 * Delete all the currently selected history items. Will prompt the user with | |
| 14465 * a dialog to confirm that the deletion should be performed. | |
| 14466 */ | |
| 14467 deleteSelectedWithPrompt: function() { | 8202 deleteSelectedWithPrompt: function() { |
| 14468 if (!loadTimeData.getBoolean('allowDeletingHistory')) | 8203 if (!loadTimeData.getBoolean('allowDeletingHistory')) return; |
| 14469 return; | |
| 14470 this.$.dialog.get().then(function(dialog) { | 8204 this.$.dialog.get().then(function(dialog) { |
| 14471 dialog.showModal(); | 8205 dialog.showModal(); |
| 14472 }); | 8206 }); |
| 14473 }, | 8207 }, |
| 14474 | |
| 14475 /** | |
| 14476 * @param {HistoryRange} range | |
| 14477 * @private | |
| 14478 */ | |
| 14479 groupedRangeChanged_: function(range) { | 8208 groupedRangeChanged_: function(range) { |
| 14480 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? | 8209 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? 'infin
ite-list' : 'grouped-list'; |
| 14481 'infinite-list' : 'grouped-list'; | |
| 14482 | |
| 14483 this.queryHistory(false); | 8210 this.queryHistory(false); |
| 14484 }, | 8211 }, |
| 14485 | 8212 loadMoreHistory_: function() { |
| 14486 /** @private */ | 8213 this.queryHistory(true); |
| 14487 loadMoreHistory_: function() { this.queryHistory(true); }, | 8214 }, |
| 14488 | |
| 14489 /** | |
| 14490 * @param {HistoryQuery} info | |
| 14491 * @param {!Array<HistoryEntry>} results | |
| 14492 * @private | |
| 14493 */ | |
| 14494 initializeResults_: function(info, results) { | 8215 initializeResults_: function(info, results) { |
| 14495 if (results.length == 0) | 8216 if (results.length == 0) return; |
| 14496 return; | |
| 14497 | |
| 14498 var currentDate = results[0].dateRelativeDay; | 8217 var currentDate = results[0].dateRelativeDay; |
| 14499 | |
| 14500 for (var i = 0; i < results.length; i++) { | 8218 for (var i = 0; i < results.length; i++) { |
| 14501 // Sets the default values for these fields to prevent undefined types. | |
| 14502 results[i].selected = false; | 8219 results[i].selected = false; |
| 14503 results[i].readableTimestamp = | 8220 results[i].readableTimestamp = info.term == '' ? results[i].dateTimeOfDay
: results[i].dateShort; |
| 14504 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; | |
| 14505 | |
| 14506 if (results[i].dateRelativeDay != currentDate) { | 8221 if (results[i].dateRelativeDay != currentDate) { |
| 14507 currentDate = results[i].dateRelativeDay; | 8222 currentDate = results[i].dateRelativeDay; |
| 14508 } | 8223 } |
| 14509 } | 8224 } |
| 14510 }, | 8225 }, |
| 14511 | |
| 14512 /** @private */ | |
| 14513 onDialogConfirmTap_: function() { | 8226 onDialogConfirmTap_: function() { |
| 14514 this.getSelectedList_().deleteSelected(); | 8227 this.getSelectedList_().deleteSelected(); |
| 14515 var dialog = assert(this.$.dialog.getIfExists()); | 8228 var dialog = assert(this.$.dialog.getIfExists()); |
| 14516 dialog.close(); | 8229 dialog.close(); |
| 14517 }, | 8230 }, |
| 14518 | |
| 14519 /** @private */ | |
| 14520 onDialogCancelTap_: function() { | 8231 onDialogCancelTap_: function() { |
| 14521 var dialog = assert(this.$.dialog.getIfExists()); | 8232 var dialog = assert(this.$.dialog.getIfExists()); |
| 14522 dialog.close(); | 8233 dialog.close(); |
| 14523 }, | 8234 }, |
| 14524 | |
| 14525 /** | |
| 14526 * Closes the overflow menu. | |
| 14527 * @private | |
| 14528 */ | |
| 14529 closeMenu_: function() { | 8235 closeMenu_: function() { |
| 14530 var menu = this.$.sharedMenu.getIfExists(); | 8236 var menu = this.$.sharedMenu.getIfExists(); |
| 14531 if (menu) | 8237 if (menu) menu.closeMenu(); |
| 14532 menu.closeMenu(); | 8238 }, |
| 14533 }, | |
| 14534 | |
| 14535 /** | |
| 14536 * Opens the overflow menu unless the menu is already open and the same button | |
| 14537 * is pressed. | |
| 14538 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e | |
| 14539 * @return {Promise<Element>} | |
| 14540 * @private | |
| 14541 */ | |
| 14542 toggleMenu_: function(e) { | 8239 toggleMenu_: function(e) { |
| 14543 var target = e.detail.target; | 8240 var target = e.detail.target; |
| 14544 return this.$.sharedMenu.get().then(function(menu) { | 8241 return this.$.sharedMenu.get().then(function(menu) { |
| 14545 /** @type {CrSharedMenuElement} */(menu).toggleMenu( | 8242 menu.toggleMenu(target, e.detail); |
| 14546 target, e.detail); | 8243 }); |
| 14547 }); | 8244 }, |
| 14548 }, | |
| 14549 | |
| 14550 /** @private */ | |
| 14551 onMoreFromSiteTap_: function() { | 8245 onMoreFromSiteTap_: function() { |
| 14552 var menu = assert(this.$.sharedMenu.getIfExists()); | 8246 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 14553 this.fire('search-domain', {domain: menu.itemData.item.domain}); | 8247 this.fire('search-domain', { |
| 8248 domain: menu.itemData.item.domain |
| 8249 }); |
| 14554 menu.closeMenu(); | 8250 menu.closeMenu(); |
| 14555 }, | 8251 }, |
| 14556 | |
| 14557 /** @private */ | |
| 14558 onRemoveFromHistoryTap_: function() { | 8252 onRemoveFromHistoryTap_: function() { |
| 14559 var menu = assert(this.$.sharedMenu.getIfExists()); | 8253 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 14560 var itemData = menu.itemData; | 8254 var itemData = menu.itemData; |
| 14561 md_history.BrowserService.getInstance() | 8255 md_history.BrowserService.getInstance().deleteItems([ itemData.item ]).then(
function(items) { |
| 14562 .deleteItems([itemData.item]) | 8256 this.getSelectedList_().removeItemsByPath([ itemData.path ]); |
| 14563 .then(function(items) { | 8257 this.fire('unselect-all'); |
| 14564 this.getSelectedList_().removeItemsByPath([itemData.path]); | 8258 }.bind(this)); |
| 14565 // This unselect-all is to reset the toolbar when deleting a selected | |
| 14566 // item. TODO(tsergeant): Make this automatic based on observing list | |
| 14567 // modifications. | |
| 14568 this.fire('unselect-all'); | |
| 14569 }.bind(this)); | |
| 14570 menu.closeMenu(); | 8259 menu.closeMenu(); |
| 14571 }, | 8260 }, |
| 14572 | |
| 14573 /** | |
| 14574 * @return {HTMLElement} | |
| 14575 * @private | |
| 14576 */ | |
| 14577 getSelectedList_: function() { | 8261 getSelectedList_: function() { |
| 14578 return this.$.content.selectedItem; | 8262 return this.$.content.selectedItem; |
| 14579 }, | 8263 } |
| 14580 }); | 8264 }); |
| 8265 |
| 14581 // Copyright 2016 The Chromium Authors. All rights reserved. | 8266 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14582 // Use of this source code is governed by a BSD-style license that can be | 8267 // Use of this source code is governed by a BSD-style license that can be |
| 14583 // found in the LICENSE file. | 8268 // found in the LICENSE file. |
| 14584 | |
| 14585 Polymer({ | 8269 Polymer({ |
| 14586 is: 'history-synced-device-card', | 8270 is: 'history-synced-device-card', |
| 14587 | |
| 14588 properties: { | 8271 properties: { |
| 14589 // Name of the synced device. | |
| 14590 device: String, | 8272 device: String, |
| 14591 | |
| 14592 // When the device information was last updated. | |
| 14593 lastUpdateTime: String, | 8273 lastUpdateTime: String, |
| 14594 | |
| 14595 /** | |
| 14596 * The list of tabs open for this device. | |
| 14597 * @type {!Array<!ForeignSessionTab>} | |
| 14598 */ | |
| 14599 tabs: { | 8274 tabs: { |
| 14600 type: Array, | 8275 type: Array, |
| 14601 value: function() { return []; }, | 8276 value: function() { |
| 8277 return []; |
| 8278 }, |
| 14602 observer: 'updateIcons_' | 8279 observer: 'updateIcons_' |
| 14603 }, | 8280 }, |
| 14604 | |
| 14605 /** | |
| 14606 * The indexes where a window separator should be shown. The use of a | |
| 14607 * separate array here is necessary for window separators to appear | |
| 14608 * correctly in search. See http://crrev.com/2022003002 for more details. | |
| 14609 * @type {!Array<number>} | |
| 14610 */ | |
| 14611 separatorIndexes: Array, | 8281 separatorIndexes: Array, |
| 14612 | |
| 14613 // Whether the card is open. | |
| 14614 opened: Boolean, | 8282 opened: Boolean, |
| 14615 | |
| 14616 searchTerm: String, | 8283 searchTerm: String, |
| 14617 | 8284 sessionTag: String |
| 14618 // Internal identifier for the device. | 8285 }, |
| 14619 sessionTag: String, | |
| 14620 }, | |
| 14621 | |
| 14622 /** | |
| 14623 * Open a single synced tab. Listens to 'click' rather than 'tap' | |
| 14624 * to determine what modifier keys were pressed. | |
| 14625 * @param {DomRepeatClickEvent} e | |
| 14626 * @private | |
| 14627 */ | |
| 14628 openTab_: function(e) { | 8286 openTab_: function(e) { |
| 14629 var tab = /** @type {ForeignSessionTab} */(e.model.tab); | 8287 var tab = e.model.tab; |
| 14630 md_history.BrowserService.getInstance().openForeignSessionTab( | 8288 md_history.BrowserService.getInstance().openForeignSessionTab(this.sessionTa
g, tab.windowId, tab.sessionId, e); |
| 14631 this.sessionTag, tab.windowId, tab.sessionId, e); | |
| 14632 e.preventDefault(); | 8289 e.preventDefault(); |
| 14633 }, | 8290 }, |
| 14634 | |
| 14635 /** | |
| 14636 * Toggles the dropdown display of synced tabs for each device card. | |
| 14637 */ | |
| 14638 toggleTabCard: function() { | 8291 toggleTabCard: function() { |
| 14639 this.$.collapse.toggle(); | 8292 this.$.collapse.toggle(); |
| 14640 this.$['dropdown-indicator'].icon = | 8293 this.$['dropdown-indicator'].icon = this.$.collapse.opened ? 'cr:expand-less
' : 'cr:expand-more'; |
| 14641 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; | 8294 }, |
| 14642 }, | |
| 14643 | |
| 14644 /** | |
| 14645 * When the synced tab information is set, the icon associated with the tab | |
| 14646 * website is also set. | |
| 14647 * @private | |
| 14648 */ | |
| 14649 updateIcons_: function() { | 8295 updateIcons_: function() { |
| 14650 this.async(function() { | 8296 this.async(function() { |
| 14651 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); | 8297 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); |
| 14652 | |
| 14653 for (var i = 0; i < this.tabs.length; i++) { | 8298 for (var i = 0; i < this.tabs.length; i++) { |
| 14654 icons[i].style.backgroundImage = | 8299 icons[i].style.backgroundImage = cr.icon.getFaviconImageSet(this.tabs[i]
.url); |
| 14655 cr.icon.getFaviconImageSet(this.tabs[i].url); | 8300 } |
| 14656 } | 8301 }); |
| 14657 }); | 8302 }, |
| 14658 }, | |
| 14659 | |
| 14660 /** @private */ | |
| 14661 isWindowSeparatorIndex_: function(index, separatorIndexes) { | 8303 isWindowSeparatorIndex_: function(index, separatorIndexes) { |
| 14662 return this.separatorIndexes.indexOf(index) != -1; | 8304 return this.separatorIndexes.indexOf(index) != -1; |
| 14663 }, | 8305 }, |
| 14664 | |
| 14665 /** | |
| 14666 * @param {boolean} opened | |
| 14667 * @return {string} | |
| 14668 * @private | |
| 14669 */ | |
| 14670 getCollapseIcon_: function(opened) { | 8306 getCollapseIcon_: function(opened) { |
| 14671 return opened ? 'cr:expand-less' : 'cr:expand-more'; | 8307 return opened ? 'cr:expand-less' : 'cr:expand-more'; |
| 14672 }, | 8308 }, |
| 14673 | |
| 14674 /** | |
| 14675 * @param {boolean} opened | |
| 14676 * @return {string} | |
| 14677 * @private | |
| 14678 */ | |
| 14679 getCollapseTitle_: function(opened) { | 8309 getCollapseTitle_: function(opened) { |
| 14680 return opened ? loadTimeData.getString('collapseSessionButton') : | 8310 return opened ? loadTimeData.getString('collapseSessionButton') : loadTimeDa
ta.getString('expandSessionButton'); |
| 14681 loadTimeData.getString('expandSessionButton'); | 8311 }, |
| 14682 }, | |
| 14683 | |
| 14684 /** | |
| 14685 * @param {CustomEvent} e | |
| 14686 * @private | |
| 14687 */ | |
| 14688 onMenuButtonTap_: function(e) { | 8312 onMenuButtonTap_: function(e) { |
| 14689 this.fire('toggle-menu', { | 8313 this.fire('toggle-menu', { |
| 14690 target: Polymer.dom(e).localTarget, | 8314 target: Polymer.dom(e).localTarget, |
| 14691 tag: this.sessionTag | 8315 tag: this.sessionTag |
| 14692 }); | 8316 }); |
| 14693 e.stopPropagation(); // Prevent iron-collapse. | 8317 e.stopPropagation(); |
| 14694 }, | 8318 } |
| 14695 }); | 8319 }); |
| 8320 |
| 14696 // Copyright 2016 The Chromium Authors. All rights reserved. | 8321 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14697 // Use of this source code is governed by a BSD-style license that can be | 8322 // Use of this source code is governed by a BSD-style license that can be |
| 14698 // found in the LICENSE file. | 8323 // found in the LICENSE file. |
| 14699 | |
| 14700 /** | |
| 14701 * @typedef {{device: string, | |
| 14702 * lastUpdateTime: string, | |
| 14703 * opened: boolean, | |
| 14704 * separatorIndexes: !Array<number>, | |
| 14705 * timestamp: number, | |
| 14706 * tabs: !Array<!ForeignSessionTab>, | |
| 14707 * tag: string}} | |
| 14708 */ | |
| 14709 var ForeignDeviceInternal; | 8324 var ForeignDeviceInternal; |
| 14710 | 8325 |
| 14711 Polymer({ | 8326 Polymer({ |
| 14712 is: 'history-synced-device-manager', | 8327 is: 'history-synced-device-manager', |
| 14713 | |
| 14714 properties: { | 8328 properties: { |
| 14715 /** | |
| 14716 * @type {?Array<!ForeignSession>} | |
| 14717 */ | |
| 14718 sessionList: { | 8329 sessionList: { |
| 14719 type: Array, | 8330 type: Array, |
| 14720 observer: 'updateSyncedDevices' | 8331 observer: 'updateSyncedDevices' |
| 14721 }, | 8332 }, |
| 14722 | |
| 14723 searchTerm: { | 8333 searchTerm: { |
| 14724 type: String, | 8334 type: String, |
| 14725 observer: 'searchTermChanged' | 8335 observer: 'searchTermChanged' |
| 14726 }, | 8336 }, |
| 14727 | |
| 14728 /** | |
| 14729 * An array of synced devices with synced tab data. | |
| 14730 * @type {!Array<!ForeignDeviceInternal>} | |
| 14731 */ | |
| 14732 syncedDevices_: { | 8337 syncedDevices_: { |
| 14733 type: Array, | 8338 type: Array, |
| 14734 value: function() { return []; } | 8339 value: function() { |
| 14735 }, | 8340 return []; |
| 14736 | 8341 } |
| 14737 /** @private */ | 8342 }, |
| 14738 signInState_: { | 8343 signInState_: { |
| 14739 type: Boolean, | 8344 type: Boolean, |
| 14740 value: loadTimeData.getBoolean('isUserSignedIn'), | 8345 value: loadTimeData.getBoolean('isUserSignedIn') |
| 14741 }, | 8346 }, |
| 14742 | |
| 14743 /** @private */ | |
| 14744 guestSession_: { | 8347 guestSession_: { |
| 14745 type: Boolean, | 8348 type: Boolean, |
| 14746 value: loadTimeData.getBoolean('isGuestSession'), | 8349 value: loadTimeData.getBoolean('isGuestSession') |
| 14747 }, | 8350 }, |
| 14748 | |
| 14749 /** @private */ | |
| 14750 fetchingSyncedTabs_: { | 8351 fetchingSyncedTabs_: { |
| 14751 type: Boolean, | 8352 type: Boolean, |
| 14752 value: false, | 8353 value: false |
| 14753 } | 8354 } |
| 14754 }, | 8355 }, |
| 14755 | |
| 14756 listeners: { | 8356 listeners: { |
| 14757 'toggle-menu': 'onToggleMenu_', | 8357 'toggle-menu': 'onToggleMenu_', |
| 14758 'scroll': 'onListScroll_' | 8358 scroll: 'onListScroll_' |
| 14759 }, | 8359 }, |
| 14760 | |
| 14761 /** @override */ | |
| 14762 attached: function() { | 8360 attached: function() { |
| 14763 // Update the sign in state. | |
| 14764 chrome.send('otherDevicesInitialized'); | 8361 chrome.send('otherDevicesInitialized'); |
| 14765 }, | 8362 }, |
| 14766 | |
| 14767 /** | |
| 14768 * @param {!ForeignSession} session | |
| 14769 * @return {!ForeignDeviceInternal} | |
| 14770 */ | |
| 14771 createInternalDevice_: function(session) { | 8363 createInternalDevice_: function(session) { |
| 14772 var tabs = []; | 8364 var tabs = []; |
| 14773 var separatorIndexes = []; | 8365 var separatorIndexes = []; |
| 14774 for (var i = 0; i < session.windows.length; i++) { | 8366 for (var i = 0; i < session.windows.length; i++) { |
| 14775 var windowId = session.windows[i].sessionId; | 8367 var windowId = session.windows[i].sessionId; |
| 14776 var newTabs = session.windows[i].tabs; | 8368 var newTabs = session.windows[i].tabs; |
| 14777 if (newTabs.length == 0) | 8369 if (newTabs.length == 0) continue; |
| 14778 continue; | |
| 14779 | |
| 14780 newTabs.forEach(function(tab) { | 8370 newTabs.forEach(function(tab) { |
| 14781 tab.windowId = windowId; | 8371 tab.windowId = windowId; |
| 14782 }); | 8372 }); |
| 14783 | |
| 14784 var windowAdded = false; | 8373 var windowAdded = false; |
| 14785 if (!this.searchTerm) { | 8374 if (!this.searchTerm) { |
| 14786 // Add all the tabs if there is no search term. | |
| 14787 tabs = tabs.concat(newTabs); | 8375 tabs = tabs.concat(newTabs); |
| 14788 windowAdded = true; | 8376 windowAdded = true; |
| 14789 } else { | 8377 } else { |
| 14790 var searchText = this.searchTerm.toLowerCase(); | 8378 var searchText = this.searchTerm.toLowerCase(); |
| 14791 for (var j = 0; j < newTabs.length; j++) { | 8379 for (var j = 0; j < newTabs.length; j++) { |
| 14792 var tab = newTabs[j]; | 8380 var tab = newTabs[j]; |
| 14793 if (tab.title.toLowerCase().indexOf(searchText) != -1) { | 8381 if (tab.title.toLowerCase().indexOf(searchText) != -1) { |
| 14794 tabs.push(tab); | 8382 tabs.push(tab); |
| 14795 windowAdded = true; | 8383 windowAdded = true; |
| 14796 } | 8384 } |
| 14797 } | 8385 } |
| 14798 } | 8386 } |
| 14799 if (windowAdded && i != session.windows.length - 1) | 8387 if (windowAdded && i != session.windows.length - 1) separatorIndexes.push(
tabs.length - 1); |
| 14800 separatorIndexes.push(tabs.length - 1); | |
| 14801 } | 8388 } |
| 14802 return { | 8389 return { |
| 14803 device: session.name, | 8390 device: session.name, |
| 14804 lastUpdateTime: '– ' + session.modifiedTime, | 8391 lastUpdateTime: '– ' + session.modifiedTime, |
| 14805 opened: true, | 8392 opened: true, |
| 14806 separatorIndexes: separatorIndexes, | 8393 separatorIndexes: separatorIndexes, |
| 14807 timestamp: session.timestamp, | 8394 timestamp: session.timestamp, |
| 14808 tabs: tabs, | 8395 tabs: tabs, |
| 14809 tag: session.tag, | 8396 tag: session.tag |
| 14810 }; | 8397 }; |
| 14811 }, | 8398 }, |
| 14812 | |
| 14813 onSignInTap_: function() { | 8399 onSignInTap_: function() { |
| 14814 chrome.send('startSignInFlow'); | 8400 chrome.send('startSignInFlow'); |
| 14815 }, | 8401 }, |
| 14816 | |
| 14817 onListScroll_: function() { | 8402 onListScroll_: function() { |
| 14818 var menu = this.$.menu.getIfExists(); | 8403 var menu = this.$.menu.getIfExists(); |
| 14819 if (menu) | 8404 if (menu) menu.closeMenu(); |
| 14820 menu.closeMenu(); | 8405 }, |
| 14821 }, | |
| 14822 | |
| 14823 onToggleMenu_: function(e) { | 8406 onToggleMenu_: function(e) { |
| 14824 this.$.menu.get().then(function(menu) { | 8407 this.$.menu.get().then(function(menu) { |
| 14825 menu.toggleMenu(e.detail.target, e.detail.tag); | 8408 menu.toggleMenu(e.detail.target, e.detail.tag); |
| 14826 }); | 8409 }); |
| 14827 }, | 8410 }, |
| 14828 | |
| 14829 onOpenAllTap_: function() { | 8411 onOpenAllTap_: function() { |
| 14830 var menu = assert(this.$.menu.getIfExists()); | 8412 var menu = assert(this.$.menu.getIfExists()); |
| 14831 md_history.BrowserService.getInstance().openForeignSessionAllTabs( | 8413 md_history.BrowserService.getInstance().openForeignSessionAllTabs(menu.itemD
ata); |
| 14832 menu.itemData); | |
| 14833 menu.closeMenu(); | 8414 menu.closeMenu(); |
| 14834 }, | 8415 }, |
| 14835 | |
| 14836 onDeleteSessionTap_: function() { | 8416 onDeleteSessionTap_: function() { |
| 14837 var menu = assert(this.$.menu.getIfExists()); | 8417 var menu = assert(this.$.menu.getIfExists()); |
| 14838 md_history.BrowserService.getInstance().deleteForeignSession( | 8418 md_history.BrowserService.getInstance().deleteForeignSession(menu.itemData); |
| 14839 menu.itemData); | |
| 14840 menu.closeMenu(); | 8419 menu.closeMenu(); |
| 14841 }, | 8420 }, |
| 14842 | |
| 14843 /** @private */ | |
| 14844 clearDisplayedSyncedDevices_: function() { | 8421 clearDisplayedSyncedDevices_: function() { |
| 14845 this.syncedDevices_ = []; | 8422 this.syncedDevices_ = []; |
| 14846 }, | 8423 }, |
| 14847 | 8424 showNoSyncedMessage: function(signInState, syncedDevicesLength, guestSession)
{ |
| 14848 /** | 8425 if (guestSession) return true; |
| 14849 * Decide whether or not should display no synced tabs message. | |
| 14850 * @param {boolean} signInState | |
| 14851 * @param {number} syncedDevicesLength | |
| 14852 * @param {boolean} guestSession | |
| 14853 * @return {boolean} | |
| 14854 */ | |
| 14855 showNoSyncedMessage: function( | |
| 14856 signInState, syncedDevicesLength, guestSession) { | |
| 14857 if (guestSession) | |
| 14858 return true; | |
| 14859 | |
| 14860 return signInState && syncedDevicesLength == 0; | 8426 return signInState && syncedDevicesLength == 0; |
| 14861 }, | 8427 }, |
| 14862 | |
| 14863 /** | |
| 14864 * Shows the signin guide when the user is not signed in and not in a guest | |
| 14865 * session. | |
| 14866 * @param {boolean} signInState | |
| 14867 * @param {boolean} guestSession | |
| 14868 * @return {boolean} | |
| 14869 */ | |
| 14870 showSignInGuide: function(signInState, guestSession) { | 8428 showSignInGuide: function(signInState, guestSession) { |
| 14871 var show = !signInState && !guestSession; | 8429 var show = !signInState && !guestSession; |
| 14872 if (show) { | 8430 if (show) { |
| 14873 md_history.BrowserService.getInstance().recordAction( | 8431 md_history.BrowserService.getInstance().recordAction('Signin_Impression_Fr
omRecentTabs'); |
| 14874 'Signin_Impression_FromRecentTabs'); | 8432 } |
| 14875 } | |
| 14876 | |
| 14877 return show; | 8433 return show; |
| 14878 }, | 8434 }, |
| 14879 | |
| 14880 /** | |
| 14881 * Decide what message should be displayed when user is logged in and there | |
| 14882 * are no synced tabs. | |
| 14883 * @param {boolean} fetchingSyncedTabs | |
| 14884 * @return {string} | |
| 14885 */ | |
| 14886 noSyncedTabsMessage: function(fetchingSyncedTabs) { | 8435 noSyncedTabsMessage: function(fetchingSyncedTabs) { |
| 14887 return loadTimeData.getString( | 8436 return loadTimeData.getString(fetchingSyncedTabs ? 'loading' : 'noSyncedResu
lts'); |
| 14888 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); | 8437 }, |
| 14889 }, | |
| 14890 | |
| 14891 /** | |
| 14892 * Replaces the currently displayed synced tabs with |sessionList|. It is | |
| 14893 * common for only a single session within the list to have changed, We try to | |
| 14894 * avoid doing extra work in this case. The logic could be more intelligent | |
| 14895 * about updating individual tabs rather than replacing whole sessions, but | |
| 14896 * this approach seems to have acceptable performance. | |
| 14897 * @param {?Array<!ForeignSession>} sessionList | |
| 14898 */ | |
| 14899 updateSyncedDevices: function(sessionList) { | 8438 updateSyncedDevices: function(sessionList) { |
| 14900 this.fetchingSyncedTabs_ = false; | 8439 this.fetchingSyncedTabs_ = false; |
| 14901 | 8440 if (!sessionList) return; |
| 14902 if (!sessionList) | |
| 14903 return; | |
| 14904 | |
| 14905 // First, update any existing devices that have changed. | |
| 14906 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); | 8441 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); |
| 14907 for (var i = 0; i < updateCount; i++) { | 8442 for (var i = 0; i < updateCount; i++) { |
| 14908 var oldDevice = this.syncedDevices_[i]; | 8443 var oldDevice = this.syncedDevices_[i]; |
| 14909 if (oldDevice.tag != sessionList[i].tag || | 8444 if (oldDevice.tag != sessionList[i].tag || oldDevice.timestamp != sessionL
ist[i].timestamp) { |
| 14910 oldDevice.timestamp != sessionList[i].timestamp) { | 8445 this.splice('syncedDevices_', i, 1, this.createInternalDevice_(sessionLi
st[i])); |
| 14911 this.splice( | 8446 } |
| 14912 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); | 8447 } |
| 14913 } | |
| 14914 } | |
| 14915 | |
| 14916 if (sessionList.length >= this.syncedDevices_.length) { | 8448 if (sessionList.length >= this.syncedDevices_.length) { |
| 14917 // The list grew; append new items. | |
| 14918 for (var i = updateCount; i < sessionList.length; i++) { | 8449 for (var i = updateCount; i < sessionList.length; i++) { |
| 14919 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); | 8450 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); |
| 14920 } | 8451 } |
| 14921 } else { | 8452 } else { |
| 14922 // The list shrank; remove deleted items. | 8453 this.splice('syncedDevices_', updateCount, this.syncedDevices_.length - up
dateCount); |
| 14923 this.splice( | 8454 } |
| 14924 'syncedDevices_', updateCount, | 8455 }, |
| 14925 this.syncedDevices_.length - updateCount); | |
| 14926 } | |
| 14927 }, | |
| 14928 | |
| 14929 /** | |
| 14930 * End fetching synced tabs when sync is disabled. | |
| 14931 */ | |
| 14932 tabSyncDisabled: function() { | 8456 tabSyncDisabled: function() { |
| 14933 this.fetchingSyncedTabs_ = false; | 8457 this.fetchingSyncedTabs_ = false; |
| 14934 this.clearDisplayedSyncedDevices_(); | 8458 this.clearDisplayedSyncedDevices_(); |
| 14935 }, | 8459 }, |
| 14936 | |
| 14937 /** | |
| 14938 * Get called when user's sign in state changes, this will affect UI of synced | |
| 14939 * tabs page. Sign in promo gets displayed when user is signed out, and | |
| 14940 * different messages are shown when there are no synced tabs. | |
| 14941 * @param {boolean} isUserSignedIn | |
| 14942 */ | |
| 14943 updateSignInState: function(isUserSignedIn) { | 8460 updateSignInState: function(isUserSignedIn) { |
| 14944 // If user's sign in state didn't change, then don't change message or | 8461 if (this.signInState_ == isUserSignedIn) return; |
| 14945 // update UI. | |
| 14946 if (this.signInState_ == isUserSignedIn) | |
| 14947 return; | |
| 14948 | |
| 14949 this.signInState_ = isUserSignedIn; | 8462 this.signInState_ = isUserSignedIn; |
| 14950 | |
| 14951 // User signed out, clear synced device list and show the sign in promo. | |
| 14952 if (!isUserSignedIn) { | 8463 if (!isUserSignedIn) { |
| 14953 this.clearDisplayedSyncedDevices_(); | 8464 this.clearDisplayedSyncedDevices_(); |
| 14954 return; | 8465 return; |
| 14955 } | 8466 } |
| 14956 // User signed in, show the loading message when querying for synced | |
| 14957 // devices. | |
| 14958 this.fetchingSyncedTabs_ = true; | 8467 this.fetchingSyncedTabs_ = true; |
| 14959 }, | 8468 }, |
| 14960 | |
| 14961 searchTermChanged: function(searchTerm) { | 8469 searchTermChanged: function(searchTerm) { |
| 14962 this.clearDisplayedSyncedDevices_(); | 8470 this.clearDisplayedSyncedDevices_(); |
| 14963 this.updateSyncedDevices(this.sessionList); | 8471 this.updateSyncedDevices(this.sessionList); |
| 14964 } | 8472 } |
| 14965 }); | 8473 }); |
| 14966 /** | 8474 |
| 14967 `iron-selector` is an element which can be used to manage a list of elements | 8475 Polymer({ |
| 14968 that can be selected. Tapping on the item will make the item selected. The `
selected` indicates | 8476 is: 'iron-selector', |
| 14969 which item is being selected. The default is to use the index of the item. | 8477 behaviors: [ Polymer.IronMultiSelectableBehavior ] |
| 14970 | 8478 }); |
| 14971 Example: | 8479 |
| 14972 | |
| 14973 <iron-selector selected="0"> | |
| 14974 <div>Item 1</div> | |
| 14975 <div>Item 2</div> | |
| 14976 <div>Item 3</div> | |
| 14977 </iron-selector> | |
| 14978 | |
| 14979 If you want to use the attribute value of an element for `selected` instead of
the index, | |
| 14980 set `attrForSelected` to the name of the attribute. For example, if you want
to select item by | |
| 14981 `name`, set `attrForSelected` to `name`. | |
| 14982 | |
| 14983 Example: | |
| 14984 | |
| 14985 <iron-selector attr-for-selected="name" selected="foo"> | |
| 14986 <div name="foo">Foo</div> | |
| 14987 <div name="bar">Bar</div> | |
| 14988 <div name="zot">Zot</div> | |
| 14989 </iron-selector> | |
| 14990 | |
| 14991 You can specify a default fallback with `fallbackSelection` in case the `selec
ted` attribute does | |
| 14992 not match the `attrForSelected` attribute of any elements. | |
| 14993 | |
| 14994 Example: | |
| 14995 | |
| 14996 <iron-selector attr-for-selected="name" selected="non-existing" | |
| 14997 fallback-selection="default"> | |
| 14998 <div name="foo">Foo</div> | |
| 14999 <div name="bar">Bar</div> | |
| 15000 <div name="default">Default</div> | |
| 15001 </iron-selector> | |
| 15002 | |
| 15003 Note: When the selector is multi, the selection will set to `fallbackSelection
` iff | |
| 15004 the number of matching elements is zero. | |
| 15005 | |
| 15006 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the
selected element. | |
| 15007 | |
| 15008 Example: | |
| 15009 | |
| 15010 <style> | |
| 15011 .iron-selected { | |
| 15012 background: #eee; | |
| 15013 } | |
| 15014 </style> | |
| 15015 | |
| 15016 ... | |
| 15017 | |
| 15018 <iron-selector selected="0"> | |
| 15019 <div>Item 1</div> | |
| 15020 <div>Item 2</div> | |
| 15021 <div>Item 3</div> | |
| 15022 </iron-selector> | |
| 15023 | |
| 15024 @demo demo/index.html | |
| 15025 */ | |
| 15026 | |
| 15027 Polymer({ | |
| 15028 | |
| 15029 is: 'iron-selector', | |
| 15030 | |
| 15031 behaviors: [ | |
| 15032 Polymer.IronMultiSelectableBehavior | |
| 15033 ] | |
| 15034 | |
| 15035 }); | |
| 15036 // Copyright 2016 The Chromium Authors. All rights reserved. | 8480 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 15037 // Use of this source code is governed by a BSD-style license that can be | 8481 // Use of this source code is governed by a BSD-style license that can be |
| 15038 // found in the LICENSE file. | 8482 // found in the LICENSE file. |
| 15039 | |
| 15040 Polymer({ | 8483 Polymer({ |
| 15041 is: 'history-side-bar', | 8484 is: 'history-side-bar', |
| 15042 | |
| 15043 properties: { | 8485 properties: { |
| 15044 selectedPage: { | 8486 selectedPage: { |
| 15045 type: String, | 8487 type: String, |
| 15046 notify: true | 8488 notify: true |
| 15047 }, | 8489 }, |
| 15048 | |
| 15049 route: Object, | 8490 route: Object, |
| 15050 | |
| 15051 showFooter: Boolean, | 8491 showFooter: Boolean, |
| 15052 | |
| 15053 // If true, the sidebar is contained within an app-drawer. | |
| 15054 drawer: { | 8492 drawer: { |
| 15055 type: Boolean, | 8493 type: Boolean, |
| 15056 reflectToAttribute: true | 8494 reflectToAttribute: true |
| 15057 }, | 8495 } |
| 15058 }, | 8496 }, |
| 15059 | |
| 15060 /** @private */ | |
| 15061 onSelectorActivate_: function() { | 8497 onSelectorActivate_: function() { |
| 15062 this.fire('history-close-drawer'); | 8498 this.fire('history-close-drawer'); |
| 15063 }, | 8499 }, |
| 15064 | |
| 15065 /** | |
| 15066 * Relocates the user to the clear browsing data section of the settings page. | |
| 15067 * @param {Event} e | |
| 15068 * @private | |
| 15069 */ | |
| 15070 onClearBrowsingDataTap_: function(e) { | 8500 onClearBrowsingDataTap_: function(e) { |
| 15071 md_history.BrowserService.getInstance().openClearBrowsingData(); | 8501 md_history.BrowserService.getInstance().openClearBrowsingData(); |
| 15072 e.preventDefault(); | 8502 e.preventDefault(); |
| 15073 }, | 8503 }, |
| 15074 | |
| 15075 /** | |
| 15076 * @param {Object} route | |
| 15077 * @private | |
| 15078 */ | |
| 15079 getQueryString_: function(route) { | 8504 getQueryString_: function(route) { |
| 15080 return window.location.search; | 8505 return window.location.search; |
| 15081 } | 8506 } |
| 15082 }); | 8507 }); |
| 8508 |
| 15083 // Copyright 2016 The Chromium Authors. All rights reserved. | 8509 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 15084 // Use of this source code is governed by a BSD-style license that can be | 8510 // Use of this source code is governed by a BSD-style license that can be |
| 15085 // found in the LICENSE file. | 8511 // found in the LICENSE file. |
| 15086 | |
| 15087 Polymer({ | 8512 Polymer({ |
| 15088 is: 'history-app', | 8513 is: 'history-app', |
| 15089 | |
| 15090 properties: { | 8514 properties: { |
| 15091 showSidebarFooter: Boolean, | 8515 showSidebarFooter: Boolean, |
| 15092 | 8516 selectedPage_: { |
| 15093 // The id of the currently selected page. | 8517 type: String, |
| 15094 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, | 8518 value: 'history', |
| 15095 | 8519 observer: 'unselectAll' |
| 15096 // Whether domain-grouped history is enabled. | 8520 }, |
| 15097 grouped_: {type: Boolean, reflectToAttribute: true}, | 8521 grouped_: { |
| 15098 | 8522 type: Boolean, |
| 15099 /** @type {!QueryState} */ | 8523 reflectToAttribute: true |
| 8524 }, |
| 15100 queryState_: { | 8525 queryState_: { |
| 15101 type: Object, | 8526 type: Object, |
| 15102 value: function() { | 8527 value: function() { |
| 15103 return { | 8528 return { |
| 15104 // Whether the most recent query was incremental. | |
| 15105 incremental: false, | 8529 incremental: false, |
| 15106 // A query is initiated by page load. | |
| 15107 querying: true, | 8530 querying: true, |
| 15108 queryingDisabled: false, | 8531 queryingDisabled: false, |
| 15109 _range: HistoryRange.ALL_TIME, | 8532 _range: HistoryRange.ALL_TIME, |
| 15110 searchTerm: '', | 8533 searchTerm: '', |
| 15111 // TODO(calamity): Make history toolbar buttons change the offset | |
| 15112 groupedOffset: 0, | 8534 groupedOffset: 0, |
| 15113 | 8535 set range(val) { |
| 15114 set range(val) { this._range = Number(val); }, | 8536 this._range = Number(val); |
| 15115 get range() { return this._range; }, | 8537 }, |
| 8538 get range() { |
| 8539 return this._range; |
| 8540 } |
| 15116 }; | 8541 }; |
| 15117 } | 8542 } |
| 15118 }, | 8543 }, |
| 15119 | |
| 15120 /** @type {!QueryResult} */ | |
| 15121 queryResult_: { | 8544 queryResult_: { |
| 15122 type: Object, | 8545 type: Object, |
| 15123 value: function() { | 8546 value: function() { |
| 15124 return { | 8547 return { |
| 15125 info: null, | 8548 info: null, |
| 15126 results: null, | 8549 results: null, |
| 15127 sessionList: null, | 8550 sessionList: null |
| 15128 }; | 8551 }; |
| 15129 } | 8552 } |
| 15130 }, | 8553 }, |
| 15131 | |
| 15132 // Route data for the current page. | |
| 15133 routeData_: Object, | 8554 routeData_: Object, |
| 15134 | |
| 15135 // The query params for the page. | |
| 15136 queryParams_: Object, | 8555 queryParams_: Object, |
| 15137 | 8556 hasDrawer_: Boolean |
| 15138 // True if the window is narrow enough for the page to have a drawer. | 8557 }, |
| 15139 hasDrawer_: Boolean, | 8558 observers: [ 'routeDataChanged_(routeData_.page)', 'selectedPageChanged_(selec
tedPage_)', 'searchTermChanged_(queryState_.searchTerm)', 'searchQueryParamChang
ed_(queryParams_.q)' ], |
| 15140 }, | |
| 15141 | |
| 15142 observers: [ | |
| 15143 // routeData_.page <=> selectedPage | |
| 15144 'routeDataChanged_(routeData_.page)', | |
| 15145 'selectedPageChanged_(selectedPage_)', | |
| 15146 | |
| 15147 // queryParams_.q <=> queryState.searchTerm | |
| 15148 'searchTermChanged_(queryState_.searchTerm)', | |
| 15149 'searchQueryParamChanged_(queryParams_.q)', | |
| 15150 | |
| 15151 ], | |
| 15152 | |
| 15153 // TODO(calamity): Replace these event listeners with data bound properties. | |
| 15154 listeners: { | 8559 listeners: { |
| 15155 'cr-menu-tap': 'onMenuTap_', | 8560 'cr-menu-tap': 'onMenuTap_', |
| 15156 'history-checkbox-select': 'checkboxSelected', | 8561 'history-checkbox-select': 'checkboxSelected', |
| 15157 'unselect-all': 'unselectAll', | 8562 'unselect-all': 'unselectAll', |
| 15158 'delete-selected': 'deleteSelected', | 8563 'delete-selected': 'deleteSelected', |
| 15159 'search-domain': 'searchDomain_', | 8564 'search-domain': 'searchDomain_', |
| 15160 'history-close-drawer': 'closeDrawer_', | 8565 'history-close-drawer': 'closeDrawer_' |
| 15161 }, | 8566 }, |
| 15162 | |
| 15163 /** @override */ | |
| 15164 ready: function() { | 8567 ready: function() { |
| 15165 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); | 8568 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); |
| 15166 | |
| 15167 cr.ui.decorate('command', cr.ui.Command); | 8569 cr.ui.decorate('command', cr.ui.Command); |
| 15168 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 8570 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
| 15169 document.addEventListener('command', this.onCommand_.bind(this)); | 8571 document.addEventListener('command', this.onCommand_.bind(this)); |
| 15170 | |
| 15171 // Redirect legacy search URLs to URLs compatible with material history. | |
| 15172 if (window.location.hash) { | 8572 if (window.location.hash) { |
| 15173 window.location.href = window.location.href.split('#')[0] + '?' + | 8573 window.location.href = window.location.href.split('#')[0] + '?' + window.l
ocation.hash.substr(1); |
| 15174 window.location.hash.substr(1); | 8574 } |
| 15175 } | 8575 }, |
| 15176 }, | |
| 15177 | |
| 15178 /** @private */ | |
| 15179 onMenuTap_: function() { | 8576 onMenuTap_: function() { |
| 15180 var drawer = this.$$('#drawer'); | 8577 var drawer = this.$$('#drawer'); |
| 15181 if (drawer) | 8578 if (drawer) drawer.toggle(); |
| 15182 drawer.toggle(); | 8579 }, |
| 15183 }, | |
| 15184 | |
| 15185 /** | |
| 15186 * Listens for history-item being selected or deselected (through checkbox) | |
| 15187 * and changes the view of the top toolbar. | |
| 15188 * @param {{detail: {countAddition: number}}} e | |
| 15189 */ | |
| 15190 checkboxSelected: function(e) { | 8580 checkboxSelected: function(e) { |
| 15191 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); | 8581 var toolbar = this.$.toolbar; |
| 15192 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history) | 8582 toolbar.count = this.$.history.getSelectedItemCount(); |
| 15193 .getSelectedItemCount(); | 8583 }, |
| 15194 }, | |
| 15195 | |
| 15196 /** | |
| 15197 * Listens for call to cancel selection and loops through all items to set | |
| 15198 * checkbox to be unselected. | |
| 15199 * @private | |
| 15200 */ | |
| 15201 unselectAll: function() { | 8584 unselectAll: function() { |
| 15202 var listContainer = | 8585 var listContainer = this.$.history; |
| 15203 /** @type {HistoryListContainerElement} */ (this.$.history); | 8586 var toolbar = this.$.toolbar; |
| 15204 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); | |
| 15205 listContainer.unselectAllItems(toolbar.count); | 8587 listContainer.unselectAllItems(toolbar.count); |
| 15206 toolbar.count = 0; | 8588 toolbar.count = 0; |
| 15207 }, | 8589 }, |
| 15208 | |
| 15209 deleteSelected: function() { | 8590 deleteSelected: function() { |
| 15210 this.$.history.deleteSelectedWithPrompt(); | 8591 this.$.history.deleteSelectedWithPrompt(); |
| 15211 }, | 8592 }, |
| 15212 | |
| 15213 /** | |
| 15214 * @param {HistoryQuery} info An object containing information about the | |
| 15215 * query. | |
| 15216 * @param {!Array<HistoryEntry>} results A list of results. | |
| 15217 */ | |
| 15218 historyResult: function(info, results) { | 8593 historyResult: function(info, results) { |
| 15219 this.set('queryState_.querying', false); | 8594 this.set('queryState_.querying', false); |
| 15220 this.set('queryResult_.info', info); | 8595 this.set('queryResult_.info', info); |
| 15221 this.set('queryResult_.results', results); | 8596 this.set('queryResult_.results', results); |
| 15222 var listContainer = | 8597 var listContainer = this.$['history']; |
| 15223 /** @type {HistoryListContainerElement} */ (this.$['history']); | |
| 15224 listContainer.historyResult(info, results); | 8598 listContainer.historyResult(info, results); |
| 15225 }, | 8599 }, |
| 15226 | 8600 searchDomain_: function(e) { |
| 15227 /** | 8601 this.$.toolbar.setSearchTerm(e.detail.domain); |
| 15228 * Fired when the user presses 'More from this site'. | 8602 }, |
| 15229 * @param {{detail: {domain: string}}} e | |
| 15230 */ | |
| 15231 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); }, | |
| 15232 | |
| 15233 /** | |
| 15234 * @param {Event} e | |
| 15235 * @private | |
| 15236 */ | |
| 15237 onCanExecute_: function(e) { | 8603 onCanExecute_: function(e) { |
| 15238 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 8604 e = e; |
| 15239 switch (e.command.id) { | 8605 switch (e.command.id) { |
| 15240 case 'find-command': | 8606 case 'find-command': |
| 15241 e.canExecute = true; | 8607 e.canExecute = true; |
| 15242 break; | 8608 break; |
| 15243 case 'slash-command': | 8609 |
| 15244 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); | 8610 case 'slash-command': |
| 15245 break; | 8611 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); |
| 15246 case 'delete-command': | 8612 break; |
| 15247 e.canExecute = this.$.toolbar.count > 0; | 8613 |
| 15248 break; | 8614 case 'delete-command': |
| 15249 } | 8615 e.canExecute = this.$.toolbar.count > 0; |
| 15250 }, | 8616 break; |
| 15251 | 8617 } |
| 15252 /** | 8618 }, |
| 15253 * @param {string} searchTerm | |
| 15254 * @private | |
| 15255 */ | |
| 15256 searchTermChanged_: function(searchTerm) { | 8619 searchTermChanged_: function(searchTerm) { |
| 15257 this.set('queryParams_.q', searchTerm || null); | 8620 this.set('queryParams_.q', searchTerm || null); |
| 15258 this.$['history'].queryHistory(false); | 8621 this.$['history'].queryHistory(false); |
| 15259 }, | 8622 }, |
| 15260 | |
| 15261 /** | |
| 15262 * @param {string} searchQuery | |
| 15263 * @private | |
| 15264 */ | |
| 15265 searchQueryParamChanged_: function(searchQuery) { | 8623 searchQueryParamChanged_: function(searchQuery) { |
| 15266 this.$.toolbar.setSearchTerm(searchQuery || ''); | 8624 this.$.toolbar.setSearchTerm(searchQuery || ''); |
| 15267 }, | 8625 }, |
| 15268 | |
| 15269 /** | |
| 15270 * @param {Event} e | |
| 15271 * @private | |
| 15272 */ | |
| 15273 onCommand_: function(e) { | 8626 onCommand_: function(e) { |
| 15274 if (e.command.id == 'find-command' || e.command.id == 'slash-command') | 8627 if (e.command.id == 'find-command' || e.command.id == 'slash-command') this.
$.toolbar.showSearchField(); |
| 15275 this.$.toolbar.showSearchField(); | 8628 if (e.command.id == 'delete-command') this.deleteSelected(); |
| 15276 if (e.command.id == 'delete-command') | 8629 }, |
| 15277 this.deleteSelected(); | |
| 15278 }, | |
| 15279 | |
| 15280 /** | |
| 15281 * @param {!Array<!ForeignSession>} sessionList Array of objects describing | |
| 15282 * the sessions from other devices. | |
| 15283 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile? | |
| 15284 */ | |
| 15285 setForeignSessions: function(sessionList, isTabSyncEnabled) { | 8630 setForeignSessions: function(sessionList, isTabSyncEnabled) { |
| 15286 if (!isTabSyncEnabled) { | 8631 if (!isTabSyncEnabled) { |
| 15287 var syncedDeviceManagerElem = | 8632 var syncedDeviceManagerElem = this.$$('history-synced-device-manager'); |
| 15288 /** @type {HistorySyncedDeviceManagerElement} */this | 8633 if (syncedDeviceManagerElem) syncedDeviceManagerElem.tabSyncDisabled(); |
| 15289 .$$('history-synced-device-manager'); | |
| 15290 if (syncedDeviceManagerElem) | |
| 15291 syncedDeviceManagerElem.tabSyncDisabled(); | |
| 15292 return; | 8634 return; |
| 15293 } | 8635 } |
| 15294 | |
| 15295 this.set('queryResult_.sessionList', sessionList); | 8636 this.set('queryResult_.sessionList', sessionList); |
| 15296 }, | 8637 }, |
| 15297 | |
| 15298 /** | |
| 15299 * Called when browsing data is cleared. | |
| 15300 */ | |
| 15301 historyDeleted: function() { | 8638 historyDeleted: function() { |
| 15302 this.$.history.historyDeleted(); | 8639 this.$.history.historyDeleted(); |
| 15303 }, | 8640 }, |
| 15304 | |
| 15305 /** | |
| 15306 * Update sign in state of synced device manager after user logs in or out. | |
| 15307 * @param {boolean} isUserSignedIn | |
| 15308 */ | |
| 15309 updateSignInState: function(isUserSignedIn) { | 8641 updateSignInState: function(isUserSignedIn) { |
| 15310 var syncedDeviceManagerElem = | 8642 var syncedDeviceManagerElem = this.$$('history-synced-device-manager'); |
| 15311 /** @type {HistorySyncedDeviceManagerElement} */this | 8643 if (syncedDeviceManagerElem) syncedDeviceManagerElem.updateSignInState(isUse
rSignedIn); |
| 15312 .$$('history-synced-device-manager'); | 8644 }, |
| 15313 if (syncedDeviceManagerElem) | |
| 15314 syncedDeviceManagerElem.updateSignInState(isUserSignedIn); | |
| 15315 }, | |
| 15316 | |
| 15317 /** | |
| 15318 * @param {string} selectedPage | |
| 15319 * @return {boolean} | |
| 15320 * @private | |
| 15321 */ | |
| 15322 syncedTabsSelected_: function(selectedPage) { | 8645 syncedTabsSelected_: function(selectedPage) { |
| 15323 return selectedPage == 'syncedTabs'; | 8646 return selectedPage == 'syncedTabs'; |
| 15324 }, | 8647 }, |
| 15325 | |
| 15326 /** | |
| 15327 * @param {boolean} querying | |
| 15328 * @param {boolean} incremental | |
| 15329 * @param {string} searchTerm | |
| 15330 * @return {boolean} Whether a loading spinner should be shown (implies the | |
| 15331 * backend is querying a new search term). | |
| 15332 * @private | |
| 15333 */ | |
| 15334 shouldShowSpinner_: function(querying, incremental, searchTerm) { | 8648 shouldShowSpinner_: function(querying, incremental, searchTerm) { |
| 15335 return querying && !incremental && searchTerm != ''; | 8649 return querying && !incremental && searchTerm != ''; |
| 15336 }, | 8650 }, |
| 15337 | |
| 15338 /** | |
| 15339 * @param {string} page | |
| 15340 * @private | |
| 15341 */ | |
| 15342 routeDataChanged_: function(page) { | 8651 routeDataChanged_: function(page) { |
| 15343 this.selectedPage_ = page; | 8652 this.selectedPage_ = page; |
| 15344 }, | 8653 }, |
| 15345 | |
| 15346 /** | |
| 15347 * @param {string} selectedPage | |
| 15348 * @private | |
| 15349 */ | |
| 15350 selectedPageChanged_: function(selectedPage) { | 8654 selectedPageChanged_: function(selectedPage) { |
| 15351 this.set('routeData_.page', selectedPage); | 8655 this.set('routeData_.page', selectedPage); |
| 15352 }, | 8656 }, |
| 15353 | |
| 15354 /** | |
| 15355 * This computed binding is needed to make the iron-pages selector update when | |
| 15356 * the synced-device-manager is instantiated for the first time. Otherwise the | |
| 15357 * fallback selection will continue to be used after the corresponding item is | |
| 15358 * added as a child of iron-pages. | |
| 15359 * @param {string} selectedPage | |
| 15360 * @param {Array} items | |
| 15361 * @return {string} | |
| 15362 * @private | |
| 15363 */ | |
| 15364 getSelectedPage_: function(selectedPage, items) { | 8657 getSelectedPage_: function(selectedPage, items) { |
| 15365 return selectedPage; | 8658 return selectedPage; |
| 15366 }, | 8659 }, |
| 15367 | |
| 15368 /** @private */ | |
| 15369 closeDrawer_: function() { | 8660 closeDrawer_: function() { |
| 15370 var drawer = this.$$('#drawer'); | 8661 var drawer = this.$$('#drawer'); |
| 15371 if (drawer) | 8662 if (drawer) drawer.close(); |
| 15372 drawer.close(); | 8663 } |
| 15373 }, | |
| 15374 }); | 8664 }); |
| OLD | NEW |