OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * The global object. | 6 * The global object. |
7 * @type {!Object} | 7 * @type {!Object} |
8 * @const | 8 * @const |
9 */ | 9 */ |
10 var global = this; | 10 var global = this; |
11 | 11 |
| 12 /** @typedef {{eventName: string, uid: number}} */ |
| 13 var WebUIListener; |
| 14 |
12 /** Platform, package, object property, and Event support. **/ | 15 /** Platform, package, object property, and Event support. **/ |
13 var cr = function() { | 16 var cr = function() { |
14 'use strict'; | 17 'use strict'; |
15 | 18 |
16 /** | 19 /** |
17 * Builds an object structure for the provided namespace path, | 20 * Builds an object structure for the provided namespace path, |
18 * ensuring that names that already exist are not overwritten. For | 21 * ensuring that names that already exist are not overwritten. For |
19 * example: | 22 * example: |
20 * "a.b.c" -> a = {};a.b={};a.b.c={}; | 23 * "a.b.c" -> a = {};a.b={};a.b.c={}; |
21 * @param {string} name Name of the object that this file defines. | 24 * @param {string} name Name of the object that this file defines. |
(...skipping 328 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
350 function sendWithPromise(methodName, var_args) { | 353 function sendWithPromise(methodName, var_args) { |
351 var args = Array.prototype.slice.call(arguments, 1); | 354 var args = Array.prototype.slice.call(arguments, 1); |
352 return new Promise(function(resolve, reject) { | 355 return new Promise(function(resolve, reject) { |
353 var id = methodName + '_' + createUid(); | 356 var id = methodName + '_' + createUid(); |
354 chromeSendResolverMap[id] = resolve; | 357 chromeSendResolverMap[id] = resolve; |
355 chrome.send(methodName, [id].concat(args)); | 358 chrome.send(methodName, [id].concat(args)); |
356 }); | 359 }); |
357 } | 360 } |
358 | 361 |
359 /** | 362 /** |
360 * A registry of callbacks keyed by event name. Used by addWebUIListener to | 363 * A map of maps associating event names with listeners. The 2nd level map |
361 * register listeners. | 364 * associates a listener ID with the callback function, such that individual |
362 * @type {!Object<Array<Function>>} | 365 * listeners can be removed from an event without affecting other listeners of |
| 366 * the same event. |
| 367 * @type {!Object<!Object<!Function>>} |
363 */ | 368 */ |
364 var webUIListenerMap = Object.create(null); | 369 var webUIListenerMap = {}; |
365 | 370 |
366 /** | 371 /** |
367 * The named method the WebUI handler calls directly when an event occurs. | 372 * The named method the WebUI handler calls directly when an event occurs. |
368 * The WebUI handler must supply the name of the event as the first argument | 373 * The WebUI handler must supply the name of the event as the first argument |
369 * of the JS invocation; additionally, the handler may supply any number of | 374 * of the JS invocation; additionally, the handler may supply any number of |
370 * other arguments that will be forwarded to the listener callbacks. | 375 * other arguments that will be forwarded to the listener callbacks. |
371 * @param {string} event The name of the event that has occurred. | 376 * @param {string} event The name of the event that has occurred. |
| 377 * @param {...*} var_args Additional arguments passed from C++. |
372 */ | 378 */ |
373 function webUIListenerCallback(event) { | 379 function webUIListenerCallback(event, var_args) { |
374 var listenerCallbacks = webUIListenerMap[event]; | 380 var eventListenersMap = webUIListenerMap[event]; |
375 for (var i = 0; i < listenerCallbacks.length; i++) { | 381 if (!eventListenersMap) { |
376 var callback = listenerCallbacks[i]; | 382 // C++ event sent for an event that has no listeners. |
377 callback.apply(null, Array.prototype.slice.call(arguments, 1)); | 383 // TODO(dpapad): Should a warning be displayed here? |
| 384 return; |
| 385 } |
| 386 |
| 387 var args = Array.prototype.slice.call(arguments, 1); |
| 388 for (var listenerId in eventListenersMap) { |
| 389 eventListenersMap[listenerId].apply(null, args); |
378 } | 390 } |
379 } | 391 } |
380 | 392 |
381 /** | 393 /** |
382 * Registers a listener for an event fired from WebUI handlers. Any number of | 394 * Registers a listener for an event fired from WebUI handlers. Any number of |
383 * listeners may register for a single event. | 395 * listeners may register for a single event. |
384 * @param {string} event The event to listen to. | 396 * @param {string} eventName The event to listen to. |
385 * @param {Function} callback The callback run when the event is fired. | 397 * @param {!Function} callback The callback run when the event is fired. |
| 398 * @return {!WebUIListener} An object to be used for removing a listener via |
| 399 * cr.removeWebUIListener. Should be treated as read-only. |
386 */ | 400 */ |
387 function addWebUIListener(event, callback) { | 401 function addWebUIListener(eventName, callback) { |
388 if (event in webUIListenerMap) | 402 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
389 webUIListenerMap[event].push(callback); | 403 var uid = createUid(); |
390 else | 404 webUIListenerMap[eventName][uid] = callback; |
391 webUIListenerMap[event] = [callback]; | 405 return {eventName: eventName, uid: uid}; |
| 406 } |
| 407 |
| 408 /** |
| 409 * Removes a listener. Does nothing if the specified listener is not found. |
| 410 * @param {!WebUIListener} listener The listener to be removed (as returned by |
| 411 * addWebUIListener). |
| 412 * @return {boolean} Whether the given listener was found and actually |
| 413 * removed. |
| 414 */ |
| 415 function removeWebUIListener(listener) { |
| 416 var listenerExists = webUIListenerMap[listener.eventName] && |
| 417 webUIListenerMap[listener.eventName][listener.uid]; |
| 418 if (listenerExists) { |
| 419 delete webUIListenerMap[listener.eventName][listener.uid]; |
| 420 return true; |
| 421 } |
| 422 return false; |
392 } | 423 } |
393 | 424 |
394 return { | 425 return { |
395 addSingletonGetter: addSingletonGetter, | 426 addSingletonGetter: addSingletonGetter, |
396 createUid: createUid, | 427 createUid: createUid, |
397 define: define, | 428 define: define, |
398 defineProperty: defineProperty, | 429 defineProperty: defineProperty, |
399 dispatchPropertyChange: dispatchPropertyChange, | 430 dispatchPropertyChange: dispatchPropertyChange, |
400 dispatchSimpleEvent: dispatchSimpleEvent, | 431 dispatchSimpleEvent: dispatchSimpleEvent, |
401 exportPath: exportPath, | 432 exportPath: exportPath, |
402 getUid: getUid, | 433 getUid: getUid, |
403 makePublic: makePublic, | 434 makePublic: makePublic, |
404 webUIResponse: webUIResponse, | 435 PropertyKind: PropertyKind, |
| 436 |
| 437 // C++ <-> JS communication related methods. |
| 438 addWebUIListener: addWebUIListener, |
| 439 removeWebUIListener: removeWebUIListener, |
405 sendWithPromise: sendWithPromise, | 440 sendWithPromise: sendWithPromise, |
406 webUIListenerCallback: webUIListenerCallback, | 441 webUIListenerCallback: webUIListenerCallback, |
407 addWebUIListener: addWebUIListener, | 442 webUIResponse: webUIResponse, |
408 PropertyKind: PropertyKind, | |
409 | 443 |
410 get doc() { | 444 get doc() { |
411 return document; | 445 return document; |
412 }, | 446 }, |
413 | 447 |
414 /** Whether we are using a Mac or not. */ | 448 /** Whether we are using a Mac or not. */ |
415 get isMac() { | 449 get isMac() { |
416 return /Mac/.test(navigator.platform); | 450 return /Mac/.test(navigator.platform); |
417 }, | 451 }, |
418 | 452 |
419 /** Whether this is on the Windows platform or not. */ | 453 /** Whether this is on the Windows platform or not. */ |
420 get isWindows() { | 454 get isWindows() { |
421 return /Win/.test(navigator.platform); | 455 return /Win/.test(navigator.platform); |
422 }, | 456 }, |
423 | 457 |
424 /** Whether this is on chromeOS or not. */ | 458 /** Whether this is on chromeOS or not. */ |
425 get isChromeOS() { | 459 get isChromeOS() { |
426 return /CrOS/.test(navigator.userAgent); | 460 return /CrOS/.test(navigator.userAgent); |
427 }, | 461 }, |
428 | 462 |
429 /** Whether this is on vanilla Linux (not chromeOS). */ | 463 /** Whether this is on vanilla Linux (not chromeOS). */ |
430 get isLinux() { | 464 get isLinux() { |
431 return /Linux/.test(navigator.userAgent); | 465 return /Linux/.test(navigator.userAgent); |
432 }, | 466 }, |
| 467 |
| 468 /** Whether this is on Android. */ |
| 469 get isAndroid() { |
| 470 return /Android/.test(navigator.userAgent); |
| 471 } |
433 }; | 472 }; |
434 }(); | 473 }(); |
435 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 474 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
436 // Use of this source code is governed by a BSD-style license that can be | 475 // Use of this source code is governed by a BSD-style license that can be |
437 // found in the LICENSE file. | 476 // found in the LICENSE file. |
438 | 477 |
439 cr.define('cr.ui', function() { | 478 cr.define('cr.ui', function() { |
440 | 479 |
441 /** | 480 /** |
442 * Decorates elements as an instance of a class. | 481 * Decorates elements as an instance of a class. |
(...skipping 537 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
980 | 1019 |
981 // <include src="../../../../ui/webui/resources/js/assert.js"> | 1020 // <include src="../../../../ui/webui/resources/js/assert.js"> |
982 | 1021 |
983 /** | 1022 /** |
984 * Alias for document.getElementById. Found elements must be HTMLElements. | 1023 * Alias for document.getElementById. Found elements must be HTMLElements. |
985 * @param {string} id The ID of the element to find. | 1024 * @param {string} id The ID of the element to find. |
986 * @return {HTMLElement} The found element or null if not found. | 1025 * @return {HTMLElement} The found element or null if not found. |
987 */ | 1026 */ |
988 function $(id) { | 1027 function $(id) { |
989 var el = document.getElementById(id); | 1028 var el = document.getElementById(id); |
990 var message = | 1029 return el ? assertInstanceof(el, HTMLElement) : null; |
991 'Element ' + el + ' with id "' + id + '" is not an HTMLElement.'; | 1030 } |
992 return el ? assertInstanceof(el, HTMLElement, message) : null; | 1031 |
| 1032 // TODO(devlin): This should return SVGElement, but closure compiler is missing |
| 1033 // those externs. |
| 1034 /** |
| 1035 * Alias for document.getElementById. Found elements must be SVGElements. |
| 1036 * @param {string} id The ID of the element to find. |
| 1037 * @return {Element} The found element or null if not found. |
| 1038 */ |
| 1039 function getSVGElement(id) { |
| 1040 var el = document.getElementById(id); |
| 1041 return el ? assertInstanceof(el, Element) : null; |
993 } | 1042 } |
994 | 1043 |
995 /** | 1044 /** |
996 * Add an accessible message to the page that will be announced to | 1045 * Add an accessible message to the page that will be announced to |
997 * users who have spoken feedback on, but will be invisible to all | 1046 * users who have spoken feedback on, but will be invisible to all |
998 * other users. It's removed right away so it doesn't clutter the DOM. | 1047 * other users. It's removed right away so it doesn't clutter the DOM. |
999 * @param {string} msg The text to be pronounced. | 1048 * @param {string} msg The text to be pronounced. |
1000 */ | 1049 */ |
1001 function announceAccessibleMessage(msg) { | 1050 function announceAccessibleMessage(msg) { |
1002 var element = document.createElement('div'); | 1051 var element = document.createElement('div'); |
(...skipping 495 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1498 } | 1547 } |
1499 | 1548 |
1500 /** | 1549 /** |
1501 * @param {*} value The value to check. | 1550 * @param {*} value The value to check. |
1502 * @param {function(new: T, ...)} type A user-defined constructor. | 1551 * @param {function(new: T, ...)} type A user-defined constructor. |
1503 * @param {string=} opt_message A message to show when this is hit. | 1552 * @param {string=} opt_message A message to show when this is hit. |
1504 * @return {T} | 1553 * @return {T} |
1505 * @template T | 1554 * @template T |
1506 */ | 1555 */ |
1507 function assertInstanceof(value, type, opt_message) { | 1556 function assertInstanceof(value, type, opt_message) { |
1508 assert(value instanceof type, | 1557 // We don't use assert immediately here so that we avoid constructing an error |
1509 opt_message || value + ' is not a[n] ' + (type.name || typeof type)); | 1558 // message if we don't have to. |
| 1559 if (!(value instanceof type)) { |
| 1560 assertNotReached(opt_message || 'Value ' + value + |
| 1561 ' is not a[n] ' + (type.name || typeof type)); |
| 1562 } |
1510 return value; | 1563 return value; |
1511 }; | 1564 }; |
1512 // Copyright 2015 The Chromium Authors. All rights reserved. | 1565 // Copyright 2015 The Chromium Authors. All rights reserved. |
1513 // Use of this source code is governed by a BSD-style license that can be | 1566 // Use of this source code is governed by a BSD-style license that can be |
1514 // found in the LICENSE file. | 1567 // found in the LICENSE file. |
1515 | 1568 |
1516 cr.define('downloads', function() { | 1569 cr.define('downloads', function() { |
1517 /** | 1570 /** |
1518 * @param {string} chromeSendName | 1571 * @param {string} chromeSendName |
1519 * @return {function(string):void} A chrome.send() callback with curried name. | 1572 * @return {function(string):void} A chrome.send() callback with curried name. |
(...skipping 452 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1972 if (!this.isAttached) { | 2025 if (!this.isAttached) { |
1973 return; | 2026 return; |
1974 } | 2027 } |
1975 | 2028 |
1976 this._notifyingDescendant = true; | 2029 this._notifyingDescendant = true; |
1977 descendant.notifyResize(); | 2030 descendant.notifyResize(); |
1978 this._notifyingDescendant = false; | 2031 this._notifyingDescendant = false; |
1979 } | 2032 } |
1980 }; | 2033 }; |
1981 (function() { | 2034 (function() { |
| 2035 'use strict'; |
| 2036 |
| 2037 /** |
| 2038 * Chrome uses an older version of DOM Level 3 Keyboard Events |
| 2039 * |
| 2040 * Most keys are labeled as text, but some are Unicode codepoints. |
| 2041 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set |
| 2042 */ |
| 2043 var KEY_IDENTIFIER = { |
| 2044 'U+0008': 'backspace', |
| 2045 'U+0009': 'tab', |
| 2046 'U+001B': 'esc', |
| 2047 'U+0020': 'space', |
| 2048 'U+007F': 'del' |
| 2049 }; |
| 2050 |
| 2051 /** |
| 2052 * Special table for KeyboardEvent.keyCode. |
| 2053 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er |
| 2054 * than that. |
| 2055 * |
| 2056 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode |
| 2057 */ |
| 2058 var KEY_CODE = { |
| 2059 8: 'backspace', |
| 2060 9: 'tab', |
| 2061 13: 'enter', |
| 2062 27: 'esc', |
| 2063 33: 'pageup', |
| 2064 34: 'pagedown', |
| 2065 35: 'end', |
| 2066 36: 'home', |
| 2067 32: 'space', |
| 2068 37: 'left', |
| 2069 38: 'up', |
| 2070 39: 'right', |
| 2071 40: 'down', |
| 2072 46: 'del', |
| 2073 106: '*' |
| 2074 }; |
| 2075 |
| 2076 /** |
| 2077 * MODIFIER_KEYS maps the short name for modifier keys used in a key |
| 2078 * combo string to the property name that references those same keys |
| 2079 * in a KeyboardEvent instance. |
| 2080 */ |
| 2081 var MODIFIER_KEYS = { |
| 2082 'shift': 'shiftKey', |
| 2083 'ctrl': 'ctrlKey', |
| 2084 'alt': 'altKey', |
| 2085 'meta': 'metaKey' |
| 2086 }; |
| 2087 |
| 2088 /** |
| 2089 * KeyboardEvent.key is mostly represented by printable character made by |
| 2090 * the keyboard, with unprintable keys labeled nicely. |
| 2091 * |
| 2092 * However, on OS X, Alt+char can make a Unicode character that follows an |
| 2093 * Apple-specific mapping. In this case, we fall back to .keyCode. |
| 2094 */ |
| 2095 var KEY_CHAR = /[a-z0-9*]/; |
| 2096 |
| 2097 /** |
| 2098 * Matches a keyIdentifier string. |
| 2099 */ |
| 2100 var IDENT_CHAR = /U\+/; |
| 2101 |
| 2102 /** |
| 2103 * Matches arrow keys in Gecko 27.0+ |
| 2104 */ |
| 2105 var ARROW_KEY = /^arrow/; |
| 2106 |
| 2107 /** |
| 2108 * Matches space keys everywhere (notably including IE10's exceptional name |
| 2109 * `spacebar`). |
| 2110 */ |
| 2111 var SPACE_KEY = /^space(bar)?/; |
| 2112 |
| 2113 /** |
| 2114 * Transforms the key. |
| 2115 * @param {string} key The KeyBoardEvent.key |
| 2116 * @param {Boolean} [noSpecialChars] Limits the transformation to |
| 2117 * alpha-numeric characters. |
| 2118 */ |
| 2119 function transformKey(key, noSpecialChars) { |
| 2120 var validKey = ''; |
| 2121 if (key) { |
| 2122 var lKey = key.toLowerCase(); |
| 2123 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 2124 validKey = 'space'; |
| 2125 } else if (lKey.length == 1) { |
| 2126 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 2127 validKey = lKey; |
| 2128 } |
| 2129 } else if (ARROW_KEY.test(lKey)) { |
| 2130 validKey = lKey.replace('arrow', ''); |
| 2131 } else if (lKey == 'multiply') { |
| 2132 // numpad '*' can map to Multiply on IE/Windows |
| 2133 validKey = '*'; |
| 2134 } else { |
| 2135 validKey = lKey; |
| 2136 } |
| 2137 } |
| 2138 return validKey; |
| 2139 } |
| 2140 |
| 2141 function transformKeyIdentifier(keyIdent) { |
| 2142 var validKey = ''; |
| 2143 if (keyIdent) { |
| 2144 if (keyIdent in KEY_IDENTIFIER) { |
| 2145 validKey = KEY_IDENTIFIER[keyIdent]; |
| 2146 } else if (IDENT_CHAR.test(keyIdent)) { |
| 2147 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 2148 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 2149 } else { |
| 2150 validKey = keyIdent.toLowerCase(); |
| 2151 } |
| 2152 } |
| 2153 return validKey; |
| 2154 } |
| 2155 |
| 2156 function transformKeyCode(keyCode) { |
| 2157 var validKey = ''; |
| 2158 if (Number(keyCode)) { |
| 2159 if (keyCode >= 65 && keyCode <= 90) { |
| 2160 // ascii a-z |
| 2161 // lowercase is 32 offset from uppercase |
| 2162 validKey = String.fromCharCode(32 + keyCode); |
| 2163 } else if (keyCode >= 112 && keyCode <= 123) { |
| 2164 // function keys f1-f12 |
| 2165 validKey = 'f' + (keyCode - 112); |
| 2166 } else if (keyCode >= 48 && keyCode <= 57) { |
| 2167 // top 0-9 keys |
| 2168 validKey = String(48 - keyCode); |
| 2169 } else if (keyCode >= 96 && keyCode <= 105) { |
| 2170 // num pad 0-9 |
| 2171 validKey = String(96 - keyCode); |
| 2172 } else { |
| 2173 validKey = KEY_CODE[keyCode]; |
| 2174 } |
| 2175 } |
| 2176 return validKey; |
| 2177 } |
| 2178 |
| 2179 /** |
| 2180 * Calculates the normalized key for a KeyboardEvent. |
| 2181 * @param {KeyboardEvent} keyEvent |
| 2182 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key |
| 2183 * transformation to alpha-numeric chars. This is useful with key |
| 2184 * combinations like shift + 2, which on FF for MacOS produces |
| 2185 * keyEvent.key = @ |
| 2186 * To get 2 returned, set noSpecialChars = true |
| 2187 * To get @ returned, set noSpecialChars = false |
| 2188 */ |
| 2189 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 2190 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to |
| 2191 // .detail.key to support artificial keyboard events. |
| 2192 return transformKey(keyEvent.key, noSpecialChars) || |
| 2193 transformKeyIdentifier(keyEvent.keyIdentifier) || |
| 2194 transformKeyCode(keyEvent.keyCode) || |
| 2195 transformKey(keyEvent.detail.key, noSpecialChars) || ''; |
| 2196 } |
| 2197 |
| 2198 function keyComboMatchesEvent(keyCombo, event) { |
| 2199 // For combos with modifiers we support only alpha-numeric keys |
| 2200 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 2201 return keyEvent === keyCombo.key && |
| 2202 (!keyCombo.hasModifiers || ( |
| 2203 !!event.shiftKey === !!keyCombo.shiftKey && |
| 2204 !!event.ctrlKey === !!keyCombo.ctrlKey && |
| 2205 !!event.altKey === !!keyCombo.altKey && |
| 2206 !!event.metaKey === !!keyCombo.metaKey) |
| 2207 ); |
| 2208 } |
| 2209 |
| 2210 function parseKeyComboString(keyComboString) { |
| 2211 if (keyComboString.length === 1) { |
| 2212 return { |
| 2213 combo: keyComboString, |
| 2214 key: keyComboString, |
| 2215 event: 'keydown' |
| 2216 }; |
| 2217 } |
| 2218 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { |
| 2219 var eventParts = keyComboPart.split(':'); |
| 2220 var keyName = eventParts[0]; |
| 2221 var event = eventParts[1]; |
| 2222 |
| 2223 if (keyName in MODIFIER_KEYS) { |
| 2224 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 2225 parsedKeyCombo.hasModifiers = true; |
| 2226 } else { |
| 2227 parsedKeyCombo.key = keyName; |
| 2228 parsedKeyCombo.event = event || 'keydown'; |
| 2229 } |
| 2230 |
| 2231 return parsedKeyCombo; |
| 2232 }, { |
| 2233 combo: keyComboString.split(':').shift() |
| 2234 }); |
| 2235 } |
| 2236 |
| 2237 function parseEventString(eventString) { |
| 2238 return eventString.trim().split(' ').map(function(keyComboString) { |
| 2239 return parseKeyComboString(keyComboString); |
| 2240 }); |
| 2241 } |
| 2242 |
| 2243 /** |
| 2244 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing |
| 2245 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). |
| 2246 * The element takes care of browser differences with respect to Keyboard ev
ents |
| 2247 * and uses an expressive syntax to filter key presses. |
| 2248 * |
| 2249 * Use the `keyBindings` prototype property to express what combination of k
eys |
| 2250 * will trigger the event to fire. |
| 2251 * |
| 2252 * Use the `key-event-target` attribute to set up event handlers on a specif
ic |
| 2253 * node. |
| 2254 * The `keys-pressed` event will fire when one of the key combinations set w
ith the |
| 2255 * `keys` property is pressed. |
| 2256 * |
| 2257 * @demo demo/index.html |
| 2258 * @polymerBehavior |
| 2259 */ |
| 2260 Polymer.IronA11yKeysBehavior = { |
| 2261 properties: { |
| 2262 /** |
| 2263 * The HTMLElement that will be firing relevant KeyboardEvents. |
| 2264 */ |
| 2265 keyEventTarget: { |
| 2266 type: Object, |
| 2267 value: function() { |
| 2268 return this; |
| 2269 } |
| 2270 }, |
| 2271 |
| 2272 /** |
| 2273 * If true, this property will cause the implementing element to |
| 2274 * automatically stop propagation on any handled KeyboardEvents. |
| 2275 */ |
| 2276 stopKeyboardEventPropagation: { |
| 2277 type: Boolean, |
| 2278 value: false |
| 2279 }, |
| 2280 |
| 2281 _boundKeyHandlers: { |
| 2282 type: Array, |
| 2283 value: function() { |
| 2284 return []; |
| 2285 } |
| 2286 }, |
| 2287 |
| 2288 // We use this due to a limitation in IE10 where instances will have |
| 2289 // own properties of everything on the "prototype". |
| 2290 _imperativeKeyBindings: { |
| 2291 type: Object, |
| 2292 value: function() { |
| 2293 return {}; |
| 2294 } |
| 2295 } |
| 2296 }, |
| 2297 |
| 2298 observers: [ |
| 2299 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' |
| 2300 ], |
| 2301 |
| 2302 keyBindings: {}, |
| 2303 |
| 2304 registered: function() { |
| 2305 this._prepKeyBindings(); |
| 2306 }, |
| 2307 |
| 2308 attached: function() { |
| 2309 this._listenKeyEventListeners(); |
| 2310 }, |
| 2311 |
| 2312 detached: function() { |
| 2313 this._unlistenKeyEventListeners(); |
| 2314 }, |
| 2315 |
| 2316 /** |
| 2317 * Can be used to imperatively add a key binding to the implementing |
| 2318 * element. This is the imperative equivalent of declaring a keybinding |
| 2319 * in the `keyBindings` prototype property. |
| 2320 */ |
| 2321 addOwnKeyBinding: function(eventString, handlerName) { |
| 2322 this._imperativeKeyBindings[eventString] = handlerName; |
| 2323 this._prepKeyBindings(); |
| 2324 this._resetKeyEventListeners(); |
| 2325 }, |
| 2326 |
| 2327 /** |
| 2328 * When called, will remove all imperatively-added key bindings. |
| 2329 */ |
| 2330 removeOwnKeyBindings: function() { |
| 2331 this._imperativeKeyBindings = {}; |
| 2332 this._prepKeyBindings(); |
| 2333 this._resetKeyEventListeners(); |
| 2334 }, |
| 2335 |
| 2336 keyboardEventMatchesKeys: function(event, eventString) { |
| 2337 var keyCombos = parseEventString(eventString); |
| 2338 for (var i = 0; i < keyCombos.length; ++i) { |
| 2339 if (keyComboMatchesEvent(keyCombos[i], event)) { |
| 2340 return true; |
| 2341 } |
| 2342 } |
| 2343 return false; |
| 2344 }, |
| 2345 |
| 2346 _collectKeyBindings: function() { |
| 2347 var keyBindings = this.behaviors.map(function(behavior) { |
| 2348 return behavior.keyBindings; |
| 2349 }); |
| 2350 |
| 2351 if (keyBindings.indexOf(this.keyBindings) === -1) { |
| 2352 keyBindings.push(this.keyBindings); |
| 2353 } |
| 2354 |
| 2355 return keyBindings; |
| 2356 }, |
| 2357 |
| 2358 _prepKeyBindings: function() { |
| 2359 this._keyBindings = {}; |
| 2360 |
| 2361 this._collectKeyBindings().forEach(function(keyBindings) { |
| 2362 for (var eventString in keyBindings) { |
| 2363 this._addKeyBinding(eventString, keyBindings[eventString]); |
| 2364 } |
| 2365 }, this); |
| 2366 |
| 2367 for (var eventString in this._imperativeKeyBindings) { |
| 2368 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); |
| 2369 } |
| 2370 |
| 2371 // Give precedence to combos with modifiers to be checked first. |
| 2372 for (var eventName in this._keyBindings) { |
| 2373 this._keyBindings[eventName].sort(function (kb1, kb2) { |
| 2374 var b1 = kb1[0].hasModifiers; |
| 2375 var b2 = kb2[0].hasModifiers; |
| 2376 return (b1 === b2) ? 0 : b1 ? -1 : 1; |
| 2377 }) |
| 2378 } |
| 2379 }, |
| 2380 |
| 2381 _addKeyBinding: function(eventString, handlerName) { |
| 2382 parseEventString(eventString).forEach(function(keyCombo) { |
| 2383 this._keyBindings[keyCombo.event] = |
| 2384 this._keyBindings[keyCombo.event] || []; |
| 2385 |
| 2386 this._keyBindings[keyCombo.event].push([ |
| 2387 keyCombo, |
| 2388 handlerName |
| 2389 ]); |
| 2390 }, this); |
| 2391 }, |
| 2392 |
| 2393 _resetKeyEventListeners: function() { |
| 2394 this._unlistenKeyEventListeners(); |
| 2395 |
| 2396 if (this.isAttached) { |
| 2397 this._listenKeyEventListeners(); |
| 2398 } |
| 2399 }, |
| 2400 |
| 2401 _listenKeyEventListeners: function() { |
| 2402 Object.keys(this._keyBindings).forEach(function(eventName) { |
| 2403 var keyBindings = this._keyBindings[eventName]; |
| 2404 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
| 2405 |
| 2406 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); |
| 2407 |
| 2408 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| 2409 }, this); |
| 2410 }, |
| 2411 |
| 2412 _unlistenKeyEventListeners: function() { |
| 2413 var keyHandlerTuple; |
| 2414 var keyEventTarget; |
| 2415 var eventName; |
| 2416 var boundKeyHandler; |
| 2417 |
| 2418 while (this._boundKeyHandlers.length) { |
| 2419 // My kingdom for block-scope binding and destructuring assignment.. |
| 2420 keyHandlerTuple = this._boundKeyHandlers.pop(); |
| 2421 keyEventTarget = keyHandlerTuple[0]; |
| 2422 eventName = keyHandlerTuple[1]; |
| 2423 boundKeyHandler = keyHandlerTuple[2]; |
| 2424 |
| 2425 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| 2426 } |
| 2427 }, |
| 2428 |
| 2429 _onKeyBindingEvent: function(keyBindings, event) { |
| 2430 if (this.stopKeyboardEventPropagation) { |
| 2431 event.stopPropagation(); |
| 2432 } |
| 2433 |
| 2434 // if event has been already prevented, don't do anything |
| 2435 if (event.defaultPrevented) { |
| 2436 return; |
| 2437 } |
| 2438 |
| 2439 for (var i = 0; i < keyBindings.length; i++) { |
| 2440 var keyCombo = keyBindings[i][0]; |
| 2441 var handlerName = keyBindings[i][1]; |
| 2442 if (keyComboMatchesEvent(keyCombo, event)) { |
| 2443 this._triggerKeyHandler(keyCombo, handlerName, event); |
| 2444 // exit the loop if eventDefault was prevented |
| 2445 if (event.defaultPrevented) { |
| 2446 return; |
| 2447 } |
| 2448 } |
| 2449 } |
| 2450 }, |
| 2451 |
| 2452 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 2453 var detail = Object.create(keyCombo); |
| 2454 detail.keyboardEvent = keyboardEvent; |
| 2455 var event = new CustomEvent(keyCombo.event, { |
| 2456 detail: detail, |
| 2457 cancelable: true |
| 2458 }); |
| 2459 this[handlerName].call(this, event); |
| 2460 if (event.defaultPrevented) { |
| 2461 keyboardEvent.preventDefault(); |
| 2462 } |
| 2463 } |
| 2464 }; |
| 2465 })(); |
| 2466 /** |
| 2467 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a |
| 2468 * designated scroll target. |
| 2469 * |
| 2470 * Elements that consume this behavior can override the `_scrollHandler` |
| 2471 * method to add logic on the scroll event. |
| 2472 * |
| 2473 * @demo demo/scrolling-region.html Scrolling Region |
| 2474 * @demo demo/document.html Document Element |
| 2475 * @polymerBehavior |
| 2476 */ |
| 2477 Polymer.IronScrollTargetBehavior = { |
| 2478 |
| 2479 properties: { |
| 2480 |
| 2481 /** |
| 2482 * Specifies the element that will handle the scroll event |
| 2483 * on the behalf of the current element. This is typically a reference to
an `Element`, |
| 2484 * but there are a few more posibilities: |
| 2485 * |
| 2486 * ### Elements id |
| 2487 * |
| 2488 *```html |
| 2489 * <div id="scrollable-element" style="overflow-y: auto;"> |
| 2490 * <x-element scroll-target="scrollable-element"> |
| 2491 * Content |
| 2492 * </x-element> |
| 2493 * </div> |
| 2494 *``` |
| 2495 * In this case, `scrollTarget` will point to the outer div element. Alter
natively, |
| 2496 * you can set the property programatically: |
| 2497 * |
| 2498 *```js |
| 2499 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); |
| 2500 *``` |
| 2501 * |
| 2502 * @type {HTMLElement} |
| 2503 */ |
| 2504 scrollTarget: { |
| 2505 type: HTMLElement, |
| 2506 value: function() { |
| 2507 return this._defaultScrollTarget; |
| 2508 } |
| 2509 } |
| 2510 }, |
| 2511 |
| 2512 observers: [ |
| 2513 '_scrollTargetChanged(scrollTarget, isAttached)' |
| 2514 ], |
| 2515 |
| 2516 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 2517 // Remove lister to the current scroll target |
| 2518 if (this._oldScrollTarget) { |
| 2519 if (this._oldScrollTarget === this._doc) { |
| 2520 window.removeEventListener('scroll', this._boundScrollHandler); |
| 2521 } else if (this._oldScrollTarget.removeEventListener) { |
| 2522 this._oldScrollTarget.removeEventListener('scroll', this._boundScrollH
andler); |
| 2523 } |
| 2524 this._oldScrollTarget = null; |
| 2525 } |
| 2526 if (isAttached) { |
| 2527 // Support element id references |
| 2528 if (typeof scrollTarget === 'string') { |
| 2529 |
| 2530 var ownerRoot = Polymer.dom(this).getOwnerRoot(); |
| 2531 this.scrollTarget = (ownerRoot && ownerRoot.$) ? |
| 2532 ownerRoot.$[scrollTarget] : Polymer.dom(this.ownerDocument).queryS
elector('#' + scrollTarget); |
| 2533 |
| 2534 } else if (this._scrollHandler) { |
| 2535 |
| 2536 this._boundScrollHandler = this._boundScrollHandler || this._scrollHan
dler.bind(this); |
| 2537 // Add a new listener |
| 2538 if (scrollTarget === this._doc) { |
| 2539 window.addEventListener('scroll', this._boundScrollHandler); |
| 2540 if (this._scrollTop !== 0 || this._scrollLeft !== 0) { |
| 2541 this._scrollHandler(); |
| 2542 } |
| 2543 } else if (scrollTarget && scrollTarget.addEventListener) { |
| 2544 scrollTarget.addEventListener('scroll', this._boundScrollHandler); |
| 2545 } |
| 2546 this._oldScrollTarget = scrollTarget; |
| 2547 } |
| 2548 } |
| 2549 }, |
| 2550 |
| 2551 /** |
| 2552 * Runs on every scroll event. Consumer of this behavior may want to overrid
e this method. |
| 2553 * |
| 2554 * @protected |
| 2555 */ |
| 2556 _scrollHandler: function scrollHandler() {}, |
| 2557 |
| 2558 /** |
| 2559 * The default scroll target. Consumers of this behavior may want to customi
ze |
| 2560 * the default scroll target. |
| 2561 * |
| 2562 * @type {Element} |
| 2563 */ |
| 2564 get _defaultScrollTarget() { |
| 2565 return this._doc; |
| 2566 }, |
| 2567 |
| 2568 /** |
| 2569 * Shortcut for the document element |
| 2570 * |
| 2571 * @type {Element} |
| 2572 */ |
| 2573 get _doc() { |
| 2574 return this.ownerDocument.documentElement; |
| 2575 }, |
| 2576 |
| 2577 /** |
| 2578 * Gets the number of pixels that the content of an element is scrolled upwa
rd. |
| 2579 * |
| 2580 * @type {number} |
| 2581 */ |
| 2582 get _scrollTop() { |
| 2583 if (this._isValidScrollTarget()) { |
| 2584 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; |
| 2585 } |
| 2586 return 0; |
| 2587 }, |
| 2588 |
| 2589 /** |
| 2590 * Gets the number of pixels that the content of an element is scrolled to t
he left. |
| 2591 * |
| 2592 * @type {number} |
| 2593 */ |
| 2594 get _scrollLeft() { |
| 2595 if (this._isValidScrollTarget()) { |
| 2596 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; |
| 2597 } |
| 2598 return 0; |
| 2599 }, |
| 2600 |
| 2601 /** |
| 2602 * Sets the number of pixels that the content of an element is scrolled upwa
rd. |
| 2603 * |
| 2604 * @type {number} |
| 2605 */ |
| 2606 set _scrollTop(top) { |
| 2607 if (this.scrollTarget === this._doc) { |
| 2608 window.scrollTo(window.pageXOffset, top); |
| 2609 } else if (this._isValidScrollTarget()) { |
| 2610 this.scrollTarget.scrollTop = top; |
| 2611 } |
| 2612 }, |
| 2613 |
| 2614 /** |
| 2615 * Sets the number of pixels that the content of an element is scrolled to t
he left. |
| 2616 * |
| 2617 * @type {number} |
| 2618 */ |
| 2619 set _scrollLeft(left) { |
| 2620 if (this.scrollTarget === this._doc) { |
| 2621 window.scrollTo(left, window.pageYOffset); |
| 2622 } else if (this._isValidScrollTarget()) { |
| 2623 this.scrollTarget.scrollLeft = left; |
| 2624 } |
| 2625 }, |
| 2626 |
| 2627 /** |
| 2628 * Scrolls the content to a particular place. |
| 2629 * |
| 2630 * @method scroll |
| 2631 * @param {number} top The top position |
| 2632 * @param {number} left The left position |
| 2633 */ |
| 2634 scroll: function(top, left) { |
| 2635 if (this.scrollTarget === this._doc) { |
| 2636 window.scrollTo(top, left); |
| 2637 } else if (this._isValidScrollTarget()) { |
| 2638 this.scrollTarget.scrollTop = top; |
| 2639 this.scrollTarget.scrollLeft = left; |
| 2640 } |
| 2641 }, |
| 2642 |
| 2643 /** |
| 2644 * Gets the width of the scroll target. |
| 2645 * |
| 2646 * @type {number} |
| 2647 */ |
| 2648 get _scrollTargetWidth() { |
| 2649 if (this._isValidScrollTarget()) { |
| 2650 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; |
| 2651 } |
| 2652 return 0; |
| 2653 }, |
| 2654 |
| 2655 /** |
| 2656 * Gets the height of the scroll target. |
| 2657 * |
| 2658 * @type {number} |
| 2659 */ |
| 2660 get _scrollTargetHeight() { |
| 2661 if (this._isValidScrollTarget()) { |
| 2662 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; |
| 2663 } |
| 2664 return 0; |
| 2665 }, |
| 2666 |
| 2667 /** |
| 2668 * Returns true if the scroll target is a valid HTMLElement. |
| 2669 * |
| 2670 * @return {boolean} |
| 2671 */ |
| 2672 _isValidScrollTarget: function() { |
| 2673 return this.scrollTarget instanceof HTMLElement; |
| 2674 } |
| 2675 }; |
| 2676 (function() { |
1982 | 2677 |
1983 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 2678 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
1984 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 2679 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
1985 var DEFAULT_PHYSICAL_COUNT = 3; | 2680 var DEFAULT_PHYSICAL_COUNT = 3; |
1986 var MAX_PHYSICAL_COUNT = 500; | 2681 var MAX_PHYSICAL_COUNT = 500; |
| 2682 var HIDDEN_Y = '-10000px'; |
1987 | 2683 |
1988 Polymer({ | 2684 Polymer({ |
1989 | 2685 |
1990 is: 'iron-list', | 2686 is: 'iron-list', |
1991 | 2687 |
1992 properties: { | 2688 properties: { |
1993 | 2689 |
1994 /** | 2690 /** |
1995 * An array containing items determining how many instances of the templat
e | 2691 * An array containing items determining how many instances of the templat
e |
1996 * to stamp and that that each template instance should bind to. | 2692 * to stamp and that that each template instance should bind to. |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2062 */ | 2758 */ |
2063 multiSelection: { | 2759 multiSelection: { |
2064 type: Boolean, | 2760 type: Boolean, |
2065 value: false | 2761 value: false |
2066 } | 2762 } |
2067 }, | 2763 }, |
2068 | 2764 |
2069 observers: [ | 2765 observers: [ |
2070 '_itemsChanged(items.*)', | 2766 '_itemsChanged(items.*)', |
2071 '_selectionEnabledChanged(selectionEnabled)', | 2767 '_selectionEnabledChanged(selectionEnabled)', |
2072 '_multiSelectionChanged(multiSelection)' | 2768 '_multiSelectionChanged(multiSelection)', |
| 2769 '_setOverflow(scrollTarget)' |
2073 ], | 2770 ], |
2074 | 2771 |
2075 behaviors: [ | 2772 behaviors: [ |
2076 Polymer.Templatizer, | 2773 Polymer.Templatizer, |
2077 Polymer.IronResizableBehavior | 2774 Polymer.IronResizableBehavior, |
| 2775 Polymer.IronA11yKeysBehavior, |
| 2776 Polymer.IronScrollTargetBehavior |
2078 ], | 2777 ], |
2079 | 2778 |
2080 listeners: { | 2779 listeners: { |
2081 'iron-resize': '_resizeHandler' | 2780 'iron-resize': '_resizeHandler' |
2082 }, | 2781 }, |
2083 | 2782 |
| 2783 keyBindings: { |
| 2784 'up': '_didMoveUp', |
| 2785 'down': '_didMoveDown', |
| 2786 'enter': '_didEnter' |
| 2787 }, |
| 2788 |
2084 /** | 2789 /** |
2085 * The ratio of hidden tiles that should remain in the scroll direction. | 2790 * The ratio of hidden tiles that should remain in the scroll direction. |
2086 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | 2791 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. |
2087 */ | 2792 */ |
2088 _ratio: 0.5, | 2793 _ratio: 0.5, |
2089 | 2794 |
2090 /** | 2795 /** |
2091 * The element that controls the scroll | |
2092 * @type {?Element} | |
2093 */ | |
2094 _scroller: null, | |
2095 | |
2096 /** | |
2097 * The padding-top value of the `scroller` element | 2796 * The padding-top value of the `scroller` element |
2098 */ | 2797 */ |
2099 _scrollerPaddingTop: 0, | 2798 _scrollerPaddingTop: 0, |
2100 | 2799 |
2101 /** | 2800 /** |
2102 * This value is the same as `scrollTop`. | 2801 * This value is the same as `scrollTop`. |
2103 */ | 2802 */ |
2104 _scrollPosition: 0, | 2803 _scrollPosition: 0, |
2105 | 2804 |
2106 /** | 2805 /** |
(...skipping 10 matching lines...) Expand all Loading... |
2117 * The k-th tile that is at the bottom of the scrolling list. | 2816 * The k-th tile that is at the bottom of the scrolling list. |
2118 */ | 2817 */ |
2119 _physicalEnd: 0, | 2818 _physicalEnd: 0, |
2120 | 2819 |
2121 /** | 2820 /** |
2122 * The sum of the heights of all the tiles in the DOM. | 2821 * The sum of the heights of all the tiles in the DOM. |
2123 */ | 2822 */ |
2124 _physicalSize: 0, | 2823 _physicalSize: 0, |
2125 | 2824 |
2126 /** | 2825 /** |
2127 * The average `offsetHeight` of the tiles observed till now. | 2826 * The average `F` of the tiles observed till now. |
2128 */ | 2827 */ |
2129 _physicalAverage: 0, | 2828 _physicalAverage: 0, |
2130 | 2829 |
2131 /** | 2830 /** |
2132 * The number of tiles which `offsetHeight` > 0 observed until now. | 2831 * The number of tiles which `offsetHeight` > 0 observed until now. |
2133 */ | 2832 */ |
2134 _physicalAverageCount: 0, | 2833 _physicalAverageCount: 0, |
2135 | 2834 |
2136 /** | 2835 /** |
2137 * The Y position of the item rendered in the `_physicalStart` | 2836 * The Y position of the item rendered in the `_physicalStart` |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2175 */ | 2874 */ |
2176 _physicalItems: null, | 2875 _physicalItems: null, |
2177 | 2876 |
2178 /** | 2877 /** |
2179 * An array of heights for each item in `_physicalItems` | 2878 * An array of heights for each item in `_physicalItems` |
2180 * @type {?Array<number>} | 2879 * @type {?Array<number>} |
2181 */ | 2880 */ |
2182 _physicalSizes: null, | 2881 _physicalSizes: null, |
2183 | 2882 |
2184 /** | 2883 /** |
2185 * A cached value for the visible index. | 2884 * A cached value for the first visible index. |
2186 * See `firstVisibleIndex` | 2885 * See `firstVisibleIndex` |
2187 * @type {?number} | 2886 * @type {?number} |
2188 */ | 2887 */ |
2189 _firstVisibleIndexVal: null, | 2888 _firstVisibleIndexVal: null, |
2190 | 2889 |
2191 /** | 2890 /** |
| 2891 * A cached value for the last visible index. |
| 2892 * See `lastVisibleIndex` |
| 2893 * @type {?number} |
| 2894 */ |
| 2895 _lastVisibleIndexVal: null, |
| 2896 |
| 2897 |
| 2898 /** |
2192 * A Polymer collection for the items. | 2899 * A Polymer collection for the items. |
2193 * @type {?Polymer.Collection} | 2900 * @type {?Polymer.Collection} |
2194 */ | 2901 */ |
2195 _collection: null, | 2902 _collection: null, |
2196 | 2903 |
2197 /** | 2904 /** |
2198 * True if the current item list was rendered for the first time | 2905 * True if the current item list was rendered for the first time |
2199 * after attached. | 2906 * after attached. |
2200 */ | 2907 */ |
2201 _itemsRendered: false, | 2908 _itemsRendered: false, |
2202 | 2909 |
2203 /** | 2910 /** |
2204 * The page that is currently rendered. | 2911 * The page that is currently rendered. |
2205 */ | 2912 */ |
2206 _lastPage: null, | 2913 _lastPage: null, |
2207 | 2914 |
2208 /** | 2915 /** |
2209 * The max number of pages to render. One page is equivalent to the height o
f the list. | 2916 * The max number of pages to render. One page is equivalent to the height o
f the list. |
2210 */ | 2917 */ |
2211 _maxPages: 3, | 2918 _maxPages: 3, |
2212 | 2919 |
2213 /** | 2920 /** |
| 2921 * The currently focused item index. |
| 2922 */ |
| 2923 _focusedIndex: 0, |
| 2924 |
| 2925 /** |
| 2926 * The the item that is focused if it is moved offscreen. |
| 2927 * @private {?TemplatizerNode} |
| 2928 */ |
| 2929 _offscreenFocusedItem: null, |
| 2930 |
| 2931 /** |
| 2932 * The item that backfills the `_offscreenFocusedItem` in the physical items |
| 2933 * list when that item is moved offscreen. |
| 2934 */ |
| 2935 _focusBackfillItem: null, |
| 2936 |
| 2937 /** |
2214 * The bottom of the physical content. | 2938 * The bottom of the physical content. |
2215 */ | 2939 */ |
2216 get _physicalBottom() { | 2940 get _physicalBottom() { |
2217 return this._physicalTop + this._physicalSize; | 2941 return this._physicalTop + this._physicalSize; |
2218 }, | 2942 }, |
2219 | 2943 |
2220 /** | 2944 /** |
2221 * The bottom of the scroll. | 2945 * The bottom of the scroll. |
2222 */ | 2946 */ |
2223 get _scrollBottom() { | 2947 get _scrollBottom() { |
2224 return this._scrollPosition + this._viewportSize; | 2948 return this._scrollPosition + this._viewportSize; |
2225 }, | 2949 }, |
2226 | 2950 |
2227 /** | 2951 /** |
2228 * The n-th item rendered in the last physical item. | 2952 * The n-th item rendered in the last physical item. |
2229 */ | 2953 */ |
2230 get _virtualEnd() { | 2954 get _virtualEnd() { |
2231 return this._virtualStartVal + this._physicalCount - 1; | 2955 return this._virtualStart + this._physicalCount - 1; |
2232 }, | 2956 }, |
2233 | 2957 |
2234 /** | 2958 /** |
2235 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | 2959 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. |
2236 */ | 2960 */ |
2237 _minVirtualStart: 0, | 2961 _minVirtualStart: 0, |
2238 | 2962 |
2239 /** | 2963 /** |
2240 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | 2964 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. |
2241 */ | 2965 */ |
(...skipping 14 matching lines...) Expand all Loading... |
2256 get _maxScrollTop() { | 2980 get _maxScrollTop() { |
2257 return this._estScrollHeight - this._viewportSize; | 2981 return this._estScrollHeight - this._viewportSize; |
2258 }, | 2982 }, |
2259 | 2983 |
2260 /** | 2984 /** |
2261 * Sets the n-th item rendered in `_physicalStart` | 2985 * Sets the n-th item rendered in `_physicalStart` |
2262 */ | 2986 */ |
2263 set _virtualStart(val) { | 2987 set _virtualStart(val) { |
2264 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart | 2988 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart |
2265 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 2989 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
2266 this._physicalStart = this._virtualStartVal % this._physicalCount; | 2990 if (this._physicalCount === 0) { |
2267 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 2991 this._physicalStart = 0; |
| 2992 this._physicalEnd = 0; |
| 2993 } else { |
| 2994 this._physicalStart = this._virtualStartVal % this._physicalCount; |
| 2995 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % th
is._physicalCount; |
| 2996 } |
2268 }, | 2997 }, |
2269 | 2998 |
2270 /** | 2999 /** |
2271 * Gets the n-th item rendered in `_physicalStart` | 3000 * Gets the n-th item rendered in `_physicalStart` |
2272 */ | 3001 */ |
2273 get _virtualStart() { | 3002 get _virtualStart() { |
2274 return this._virtualStartVal; | 3003 return this._virtualStartVal; |
2275 }, | 3004 }, |
2276 | 3005 |
2277 /** | 3006 /** |
2278 * An optimal physical size such that we will have enough physical items | 3007 * An optimal physical size such that we will have enough physical items |
2279 * to fill up the viewport and recycle when the user scrolls. | 3008 * to fill up the viewport and recycle when the user scrolls. |
2280 * | 3009 * |
2281 * This default value assumes that we will at least have the equivalent | 3010 * This default value assumes that we will at least have the equivalent |
2282 * to a viewport of physical items above and below the user's viewport. | 3011 * to a viewport of physical items above and below the user's viewport. |
2283 */ | 3012 */ |
2284 get _optPhysicalSize() { | 3013 get _optPhysicalSize() { |
2285 return this._viewportSize * this._maxPages; | 3014 return this._viewportSize * this._maxPages; |
2286 }, | 3015 }, |
2287 | 3016 |
2288 /** | 3017 /** |
2289 * True if the current list is visible. | 3018 * True if the current list is visible. |
2290 */ | 3019 */ |
2291 get _isVisible() { | 3020 get _isVisible() { |
2292 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol
ler.offsetHeight); | 3021 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
2293 }, | 3022 }, |
2294 | 3023 |
2295 /** | 3024 /** |
2296 * Gets the index of the first visible item in the viewport. | 3025 * Gets the index of the first visible item in the viewport. |
2297 * | 3026 * |
2298 * @type {number} | 3027 * @type {number} |
2299 */ | 3028 */ |
2300 get firstVisibleIndex() { | 3029 get firstVisibleIndex() { |
2301 var physicalOffset; | |
2302 | |
2303 if (this._firstVisibleIndexVal === null) { | 3030 if (this._firstVisibleIndexVal === null) { |
2304 physicalOffset = this._physicalTop; | 3031 var physicalOffset = this._physicalTop; |
2305 | 3032 |
2306 this._firstVisibleIndexVal = this._iterateItems( | 3033 this._firstVisibleIndexVal = this._iterateItems( |
2307 function(pidx, vidx) { | 3034 function(pidx, vidx) { |
2308 physicalOffset += this._physicalSizes[pidx]; | 3035 physicalOffset += this._physicalSizes[pidx]; |
2309 | 3036 |
2310 if (physicalOffset > this._scrollPosition) { | 3037 if (physicalOffset > this._scrollPosition) { |
2311 return vidx; | 3038 return vidx; |
2312 } | 3039 } |
2313 }) || 0; | 3040 }) || 0; |
2314 } | 3041 } |
2315 | |
2316 return this._firstVisibleIndexVal; | 3042 return this._firstVisibleIndexVal; |
2317 }, | 3043 }, |
2318 | 3044 |
| 3045 /** |
| 3046 * Gets the index of the last visible item in the viewport. |
| 3047 * |
| 3048 * @type {number} |
| 3049 */ |
| 3050 get lastVisibleIndex() { |
| 3051 if (this._lastVisibleIndexVal === null) { |
| 3052 var physicalOffset = this._physicalTop; |
| 3053 |
| 3054 this._iterateItems(function(pidx, vidx) { |
| 3055 physicalOffset += this._physicalSizes[pidx]; |
| 3056 |
| 3057 if(physicalOffset <= this._scrollBottom) { |
| 3058 this._lastVisibleIndexVal = vidx; |
| 3059 } |
| 3060 }); |
| 3061 } |
| 3062 return this._lastVisibleIndexVal; |
| 3063 }, |
| 3064 |
2319 ready: function() { | 3065 ready: function() { |
2320 if (IOS_TOUCH_SCROLLING) { | 3066 this.addEventListener('focus', this._didFocus.bind(this), true); |
2321 this._scrollListener = function() { | 3067 }, |
2322 requestAnimationFrame(this._scrollHandler.bind(this)); | 3068 |
2323 }.bind(this); | |
2324 } else { | |
2325 this._scrollListener = this._scrollHandler.bind(this); | |
2326 } | |
2327 }, | |
2328 | |
2329 /** | |
2330 * When the element has been attached to the DOM tree. | |
2331 */ | |
2332 attached: function() { | 3069 attached: function() { |
2333 // delegate to the parent's scroller | |
2334 // e.g. paper-scroll-header-panel | |
2335 var el = Polymer.dom(this); | |
2336 | |
2337 var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode); | |
2338 if (parentNode && parentNode.scroller) { | |
2339 this._scroller = parentNode.scroller; | |
2340 } else { | |
2341 this._scroller = this; | |
2342 this.classList.add('has-scroller'); | |
2343 } | |
2344 | |
2345 if (IOS_TOUCH_SCROLLING) { | |
2346 this._scroller.style.webkitOverflowScrolling = 'touch'; | |
2347 } | |
2348 | |
2349 this._scroller.addEventListener('scroll', this._scrollListener); | |
2350 | |
2351 this.updateViewportBoundaries(); | 3070 this.updateViewportBoundaries(); |
2352 this._render(); | 3071 this._render(); |
2353 }, | 3072 }, |
2354 | 3073 |
2355 /** | |
2356 * When the element has been removed from the DOM tree. | |
2357 */ | |
2358 detached: function() { | 3074 detached: function() { |
2359 this._itemsRendered = false; | 3075 this._itemsRendered = false; |
2360 if (this._scroller) { | 3076 }, |
2361 this._scroller.removeEventListener('scroll', this._scrollListener); | 3077 |
2362 } | 3078 get _defaultScrollTarget() { |
| 3079 return this; |
| 3080 }, |
| 3081 |
| 3082 /** |
| 3083 * Set the overflow property if this element has its own scrolling region |
| 3084 */ |
| 3085 _setOverflow: function(scrollTarget) { |
| 3086 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
| 3087 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
2363 }, | 3088 }, |
2364 | 3089 |
2365 /** | 3090 /** |
2366 * Invoke this method if you dynamically update the viewport's | 3091 * Invoke this method if you dynamically update the viewport's |
2367 * size or CSS padding. | 3092 * size or CSS padding. |
2368 * | 3093 * |
2369 * @method updateViewportBoundaries | 3094 * @method updateViewportBoundaries |
2370 */ | 3095 */ |
2371 updateViewportBoundaries: function() { | 3096 updateViewportBoundaries: function() { |
2372 var scrollerStyle = window.getComputedStyle(this._scroller); | 3097 var scrollerStyle = window.getComputedStyle(this.scrollTarget); |
2373 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); | 3098 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); |
2374 this._viewportSize = this._scroller.offsetHeight; | 3099 this._viewportSize = this._scrollTargetHeight; |
2375 }, | 3100 }, |
2376 | 3101 |
2377 /** | 3102 /** |
2378 * Update the models, the position of the | 3103 * Update the models, the position of the |
2379 * items in the viewport and recycle tiles as needed. | 3104 * items in the viewport and recycle tiles as needed. |
2380 */ | 3105 */ |
2381 _refresh: function() { | 3106 _scrollHandler: function() { |
2382 // clamp the `scrollTop` value | 3107 // clamp the `scrollTop` value |
2383 // IE 10|11 scrollTop may go above `_maxScrollTop` | 3108 // IE 10|11 scrollTop may go above `_maxScrollTop` |
2384 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` | 3109 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` |
2385 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc
rollTop)); | 3110 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
2386 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 3111 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
2387 var ratio = this._ratio; | 3112 var ratio = this._ratio; |
2388 var delta = scrollTop - this._scrollPosition; | 3113 var delta = scrollTop - this._scrollPosition; |
2389 var recycledTiles = 0; | 3114 var recycledTiles = 0; |
2390 var hiddenContentSize = this._hiddenContentSize; | 3115 var hiddenContentSize = this._hiddenContentSize; |
2391 var currentRatio = ratio; | 3116 var currentRatio = ratio; |
2392 var movingUp = []; | 3117 var movingUp = []; |
2393 | 3118 |
2394 // track the last `scrollTop` | 3119 // track the last `scrollTop` |
2395 this._scrollPosition = scrollTop; | 3120 this._scrollPosition = scrollTop; |
2396 | 3121 |
2397 // clear cached visible index | 3122 // clear cached visible index |
2398 this._firstVisibleIndexVal = null; | 3123 this._firstVisibleIndexVal = null; |
| 3124 this._lastVisibleIndexVal = null; |
2399 | 3125 |
2400 scrollBottom = this._scrollBottom; | 3126 scrollBottom = this._scrollBottom; |
2401 physicalBottom = this._physicalBottom; | 3127 physicalBottom = this._physicalBottom; |
2402 | 3128 |
2403 // random access | 3129 // random access |
2404 if (Math.abs(delta) > this._physicalSize) { | 3130 if (Math.abs(delta) > this._physicalSize) { |
2405 this._physicalTop += delta; | 3131 this._physicalTop += delta; |
2406 recycledTiles = Math.round(delta / this._physicalAverage); | 3132 recycledTiles = Math.round(delta / this._physicalAverage); |
2407 } | 3133 } |
2408 // scroll up | 3134 // scroll up |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2478 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 3204 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
2479 this.async(this._increasePool.bind(this, 1)); | 3205 this.async(this._increasePool.bind(this, 1)); |
2480 } | 3206 } |
2481 } else { | 3207 } else { |
2482 this._virtualStart = this._virtualStart + recycledTiles; | 3208 this._virtualStart = this._virtualStart + recycledTiles; |
2483 this._update(recycledTileSet, movingUp); | 3209 this._update(recycledTileSet, movingUp); |
2484 } | 3210 } |
2485 }, | 3211 }, |
2486 | 3212 |
2487 /** | 3213 /** |
2488 * Update the list of items, starting from the `_virtualStartVal` item. | 3214 * Update the list of items, starting from the `_virtualStart` item. |
2489 * @param {!Array<number>=} itemSet | 3215 * @param {!Array<number>=} itemSet |
2490 * @param {!Array<number>=} movingUp | 3216 * @param {!Array<number>=} movingUp |
2491 */ | 3217 */ |
2492 _update: function(itemSet, movingUp) { | 3218 _update: function(itemSet, movingUp) { |
| 3219 // manage focus |
| 3220 if (this._isIndexRendered(this._focusedIndex)) { |
| 3221 this._restoreFocusedItem(); |
| 3222 } else { |
| 3223 this._createFocusBackfillItem(); |
| 3224 } |
2493 // update models | 3225 // update models |
2494 this._assignModels(itemSet); | 3226 this._assignModels(itemSet); |
2495 | |
2496 // measure heights | 3227 // measure heights |
2497 this._updateMetrics(itemSet); | 3228 this._updateMetrics(itemSet); |
2498 | |
2499 // adjust offset after measuring | 3229 // adjust offset after measuring |
2500 if (movingUp) { | 3230 if (movingUp) { |
2501 while (movingUp.length) { | 3231 while (movingUp.length) { |
2502 this._physicalTop -= this._physicalSizes[movingUp.pop()]; | 3232 this._physicalTop -= this._physicalSizes[movingUp.pop()]; |
2503 } | 3233 } |
2504 } | 3234 } |
2505 // update the position of the items | 3235 // update the position of the items |
2506 this._positionItems(); | 3236 this._positionItems(); |
2507 | |
2508 // set the scroller size | 3237 // set the scroller size |
2509 this._updateScrollerSize(); | 3238 this._updateScrollerSize(); |
2510 | |
2511 // increase the pool of physical items | 3239 // increase the pool of physical items |
2512 this._increasePoolIfNeeded(); | 3240 this._increasePoolIfNeeded(); |
2513 }, | 3241 }, |
2514 | 3242 |
2515 /** | 3243 /** |
2516 * Creates a pool of DOM elements and attaches them to the local dom. | 3244 * Creates a pool of DOM elements and attaches them to the local dom. |
2517 */ | 3245 */ |
2518 _createPool: function(size) { | 3246 _createPool: function(size) { |
2519 var physicalItems = new Array(size); | 3247 var physicalItems = new Array(size); |
2520 | 3248 |
2521 this._ensureTemplatized(); | 3249 this._ensureTemplatized(); |
2522 | 3250 |
2523 for (var i = 0; i < size; i++) { | 3251 for (var i = 0; i < size; i++) { |
2524 var inst = this.stamp(null); | 3252 var inst = this.stamp(null); |
2525 // First element child is item; Safari doesn't support children[0] | 3253 // First element child is item; Safari doesn't support children[0] |
2526 // on a doc fragment | 3254 // on a doc fragment |
2527 physicalItems[i] = inst.root.querySelector('*'); | 3255 physicalItems[i] = inst.root.querySelector('*'); |
2528 Polymer.dom(this).appendChild(inst.root); | 3256 Polymer.dom(this).appendChild(inst.root); |
2529 } | 3257 } |
2530 | |
2531 return physicalItems; | 3258 return physicalItems; |
2532 }, | 3259 }, |
2533 | 3260 |
2534 /** | 3261 /** |
2535 * Increases the pool of physical items only if needed. | 3262 * Increases the pool of physical items only if needed. |
2536 * This function will allocate additional physical items | 3263 * This function will allocate additional physical items |
2537 * if the physical size is shorter than `_optPhysicalSize` | 3264 * if the physical size is shorter than `_optPhysicalSize` |
2538 */ | 3265 */ |
2539 _increasePoolIfNeeded: function() { | 3266 _increasePoolIfNeeded: function() { |
2540 if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize
) { | 3267 if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSiz
e) { |
2541 // 0 <= `currentPage` <= `_maxPages` | 3268 return false; |
2542 var currentPage = Math.floor(this._physicalSize / this._viewportSize); | 3269 } |
2543 | 3270 // 0 <= `currentPage` <= `_maxPages` |
2544 if (currentPage === 0) { | 3271 var currentPage = Math.floor(this._physicalSize / this._viewportSize); |
2545 // fill the first page | 3272 if (currentPage === 0) { |
2546 this.async(this._increasePool.bind(this, Math.round(this._physicalCoun
t * 0.5))); | 3273 // fill the first page |
2547 } else if (this._lastPage !== currentPage) { | 3274 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); |
2548 // once a page is filled up, paint it and defer the next increase | 3275 } else if (this._lastPage !== currentPage) { |
2549 requestAnimationFrame(this._increasePool.bind(this, 1)); | 3276 // paint the page and defer the next increase |
2550 } else { | 3277 // wait 16ms which is rough enough to get paint cycle. |
2551 // fill the rest of the pages | 3278 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, 1), 16)); |
2552 this.async(this._increasePool.bind(this, 1)); | 3279 } else { |
2553 } | 3280 // fill the rest of the pages |
2554 this._lastPage = currentPage; | 3281 this._debounceTemplate(this._increasePool.bind(this, 1)); |
2555 return true; | 3282 } |
2556 } | 3283 this._lastPage = currentPage; |
2557 return false; | 3284 return true; |
2558 }, | 3285 }, |
2559 | 3286 |
2560 /** | 3287 /** |
2561 * Increases the pool size. | 3288 * Increases the pool size. |
2562 */ | 3289 */ |
2563 _increasePool: function(missingItems) { | 3290 _increasePool: function(missingItems) { |
2564 // limit the size | 3291 // limit the size |
2565 var nextPhysicalCount = Math.min( | 3292 var nextPhysicalCount = Math.min( |
2566 this._physicalCount + missingItems, | 3293 this._physicalCount + missingItems, |
2567 this._virtualCount, | 3294 this._virtualCount - this._virtualStart, |
2568 MAX_PHYSICAL_COUNT | 3295 MAX_PHYSICAL_COUNT |
2569 ); | 3296 ); |
2570 var prevPhysicalCount = this._physicalCount; | 3297 var prevPhysicalCount = this._physicalCount; |
2571 var delta = nextPhysicalCount - prevPhysicalCount; | 3298 var delta = nextPhysicalCount - prevPhysicalCount; |
2572 | 3299 |
2573 if (delta > 0) { | 3300 if (delta > 0) { |
2574 [].push.apply(this._physicalItems, this._createPool(delta)); | 3301 [].push.apply(this._physicalItems, this._createPool(delta)); |
2575 [].push.apply(this._physicalSizes, new Array(delta)); | 3302 [].push.apply(this._physicalSizes, new Array(delta)); |
2576 | 3303 |
2577 this._physicalCount = prevPhysicalCount + delta; | 3304 this._physicalCount = prevPhysicalCount + delta; |
2578 // tail call | 3305 // tail call |
2579 return this._update(); | 3306 return this._update(); |
2580 } | 3307 } |
2581 }, | 3308 }, |
2582 | 3309 |
2583 /** | 3310 /** |
2584 * Render a new list of items. This method does exactly the same as `update`
, | 3311 * Render a new list of items. This method does exactly the same as `update`
, |
2585 * but it also ensures that only one `update` cycle is created. | 3312 * but it also ensures that only one `update` cycle is created. |
2586 */ | 3313 */ |
2587 _render: function() { | 3314 _render: function() { |
2588 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 3315 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
2589 | 3316 |
2590 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 3317 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
2591 this._lastPage = 0; | 3318 this._lastPage = 0; |
2592 this._update(); | 3319 this._update(); |
| 3320 this._scrollHandler(); |
2593 this._itemsRendered = true; | 3321 this._itemsRendered = true; |
2594 } | 3322 } |
2595 }, | 3323 }, |
2596 | 3324 |
2597 /** | 3325 /** |
2598 * Templetizes the user template. | 3326 * Templetizes the user template. |
2599 */ | 3327 */ |
2600 _ensureTemplatized: function() { | 3328 _ensureTemplatized: function() { |
2601 if (!this.ctor) { | 3329 if (!this.ctor) { |
2602 // Template instance props that should be excluded from forwarding | 3330 // Template instance props that should be excluded from forwarding |
2603 var props = {}; | 3331 var props = {}; |
2604 | |
2605 props.__key__ = true; | 3332 props.__key__ = true; |
2606 props[this.as] = true; | 3333 props[this.as] = true; |
2607 props[this.indexAs] = true; | 3334 props[this.indexAs] = true; |
2608 props[this.selectedAs] = true; | 3335 props[this.selectedAs] = true; |
| 3336 props.tabIndex = true; |
2609 | 3337 |
2610 this._instanceProps = props; | 3338 this._instanceProps = props; |
2611 this._userTemplate = Polymer.dom(this).querySelector('template'); | 3339 this._userTemplate = Polymer.dom(this).querySelector('template'); |
2612 | 3340 |
2613 if (this._userTemplate) { | 3341 if (this._userTemplate) { |
2614 this.templatize(this._userTemplate); | 3342 this.templatize(this._userTemplate); |
2615 } else { | 3343 } else { |
2616 console.warn('iron-list requires a template to be provided in light-do
m'); | 3344 console.warn('iron-list requires a template to be provided in light-do
m'); |
2617 } | 3345 } |
2618 } | 3346 } |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2666 /** | 3394 /** |
2667 * Called as a side effect of a host items.<key>.<path> path change, | 3395 * Called as a side effect of a host items.<key>.<path> path change, |
2668 * responsible for notifying item.<path> changes to row for key. | 3396 * responsible for notifying item.<path> changes to row for key. |
2669 */ | 3397 */ |
2670 _forwardItemPath: function(path, value) { | 3398 _forwardItemPath: function(path, value) { |
2671 if (this._physicalIndexForKey) { | 3399 if (this._physicalIndexForKey) { |
2672 var dot = path.indexOf('.'); | 3400 var dot = path.indexOf('.'); |
2673 var key = path.substring(0, dot < 0 ? path.length : dot); | 3401 var key = path.substring(0, dot < 0 ? path.length : dot); |
2674 var idx = this._physicalIndexForKey[key]; | 3402 var idx = this._physicalIndexForKey[key]; |
2675 var row = this._physicalItems[idx]; | 3403 var row = this._physicalItems[idx]; |
| 3404 |
| 3405 if (idx === this._focusedIndex && this._offscreenFocusedItem) { |
| 3406 row = this._offscreenFocusedItem; |
| 3407 } |
2676 if (row) { | 3408 if (row) { |
2677 var inst = row._templateInstance; | 3409 var inst = row._templateInstance; |
2678 if (dot >= 0) { | 3410 if (dot >= 0) { |
2679 path = this.as + '.' + path.substring(dot+1); | 3411 path = this.as + '.' + path.substring(dot+1); |
2680 inst.notifyPath(path, value, true); | 3412 inst.notifyPath(path, value, true); |
2681 } else { | 3413 } else { |
2682 inst[this.as] = value; | 3414 inst[this.as] = value; |
2683 } | 3415 } |
2684 } | 3416 } |
2685 } | 3417 } |
2686 }, | 3418 }, |
2687 | 3419 |
2688 /** | 3420 /** |
2689 * Called when the items have changed. That is, ressignments | 3421 * Called when the items have changed. That is, ressignments |
2690 * to `items`, splices or updates to a single item. | 3422 * to `items`, splices or updates to a single item. |
2691 */ | 3423 */ |
2692 _itemsChanged: function(change) { | 3424 _itemsChanged: function(change) { |
2693 if (change.path === 'items') { | 3425 if (change.path === 'items') { |
| 3426 |
| 3427 this._restoreFocusedItem(); |
2694 // render the new set | 3428 // render the new set |
2695 this._itemsRendered = false; | 3429 this._itemsRendered = false; |
2696 | |
2697 // update the whole set | 3430 // update the whole set |
2698 this._virtualStartVal = 0; | 3431 this._virtualStart = 0; |
2699 this._physicalTop = 0; | 3432 this._physicalTop = 0; |
2700 this._virtualCount = this.items ? this.items.length : 0; | 3433 this._virtualCount = this.items ? this.items.length : 0; |
| 3434 this._focusedIndex = 0; |
2701 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 3435 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
2702 this._physicalIndexForKey = {}; | 3436 this._physicalIndexForKey = {}; |
2703 | 3437 |
2704 // scroll to the top | |
2705 this._resetScrollPosition(0); | 3438 this._resetScrollPosition(0); |
2706 | 3439 |
2707 // create the initial physical items | 3440 // create the initial physical items |
2708 if (!this._physicalItems) { | 3441 if (!this._physicalItems) { |
2709 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 3442 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
2710 this._physicalItems = this._createPool(this._physicalCount); | 3443 this._physicalItems = this._createPool(this._physicalCount); |
2711 this._physicalSizes = new Array(this._physicalCount); | 3444 this._physicalSizes = new Array(this._physicalCount); |
2712 } | 3445 } |
2713 | 3446 this._debounceTemplate(this._render); |
2714 this.debounce('refresh', this._render); | |
2715 | 3447 |
2716 } else if (change.path === 'items.splices') { | 3448 } else if (change.path === 'items.splices') { |
2717 // render the new set | 3449 // render the new set |
2718 this._itemsRendered = false; | 3450 this._itemsRendered = false; |
2719 | |
2720 this._adjustVirtualIndex(change.value.indexSplices); | 3451 this._adjustVirtualIndex(change.value.indexSplices); |
2721 this._virtualCount = this.items ? this.items.length : 0; | 3452 this._virtualCount = this.items ? this.items.length : 0; |
2722 | 3453 |
2723 this.debounce('refresh', this._render); | 3454 this._debounceTemplate(this._render); |
| 3455 |
| 3456 if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount)
{ |
| 3457 this._focusedIndex = 0; |
| 3458 } |
| 3459 this._debounceTemplate(this._render); |
2724 | 3460 |
2725 } else { | 3461 } else { |
2726 // update a single item | 3462 // update a single item |
2727 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 3463 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
2728 } | 3464 } |
2729 }, | 3465 }, |
2730 | 3466 |
2731 /** | 3467 /** |
2732 * @param {!Array<!PolymerSplice>} splices | 3468 * @param {!Array<!PolymerSplice>} splices |
2733 */ | 3469 */ |
2734 _adjustVirtualIndex: function(splices) { | 3470 _adjustVirtualIndex: function(splices) { |
2735 var i, splice, idx; | 3471 var i, splice, idx; |
2736 | 3472 |
2737 for (i = 0; i < splices.length; i++) { | 3473 for (i = 0; i < splices.length; i++) { |
2738 splice = splices[i]; | 3474 splice = splices[i]; |
2739 | 3475 |
2740 // deselect removed items | 3476 // deselect removed items |
2741 splice.removed.forEach(this.$.selector.deselect, this.$.selector); | 3477 splice.removed.forEach(this.$.selector.deselect, this.$.selector); |
2742 | 3478 |
2743 idx = splice.index; | 3479 idx = splice.index; |
2744 // We only need to care about changes happening above the current positi
on | 3480 // We only need to care about changes happening above the current positi
on |
2745 if (idx >= this._virtualStartVal) { | 3481 if (idx >= this._virtualStart) { |
2746 break; | 3482 break; |
2747 } | 3483 } |
2748 | 3484 |
2749 this._virtualStart = this._virtualStart + | 3485 this._virtualStart = this._virtualStart + |
2750 Math.max(splice.addedCount - splice.removed.length, idx - this._virt
ualStartVal); | 3486 Math.max(splice.addedCount - splice.removed.length, idx - this._virt
ualStart); |
2751 } | 3487 } |
2752 }, | |
2753 | |
2754 _scrollHandler: function() { | |
2755 this._refresh(); | |
2756 }, | 3488 }, |
2757 | 3489 |
2758 /** | 3490 /** |
2759 * Executes a provided function per every physical index in `itemSet` | 3491 * Executes a provided function per every physical index in `itemSet` |
2760 * `itemSet` default value is equivalent to the entire set of physical index
es. | 3492 * `itemSet` default value is equivalent to the entire set of physical index
es. |
2761 * | 3493 * |
2762 * @param {!function(number, number)} fn | 3494 * @param {!function(number, number)} fn |
2763 * @param {!Array<number>=} itemSet | 3495 * @param {!Array<number>=} itemSet |
2764 */ | 3496 */ |
2765 _iterateItems: function(fn, itemSet) { | 3497 _iterateItems: function(fn, itemSet) { |
2766 var pidx, vidx, rtn, i; | 3498 var pidx, vidx, rtn, i; |
2767 | 3499 |
2768 if (arguments.length === 2 && itemSet) { | 3500 if (arguments.length === 2 && itemSet) { |
2769 for (i = 0; i < itemSet.length; i++) { | 3501 for (i = 0; i < itemSet.length; i++) { |
2770 pidx = itemSet[i]; | 3502 pidx = itemSet[i]; |
2771 if (pidx >= this._physicalStart) { | 3503 if (pidx >= this._physicalStart) { |
2772 vidx = this._virtualStartVal + (pidx - this._physicalStart); | 3504 vidx = this._virtualStart + (pidx - this._physicalStart); |
2773 } else { | 3505 } else { |
2774 vidx = this._virtualStartVal + (this._physicalCount - this._physical
Start) + pidx; | 3506 vidx = this._virtualStart + (this._physicalCount - this._physicalSta
rt) + pidx; |
2775 } | 3507 } |
2776 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 3508 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
2777 return rtn; | 3509 return rtn; |
2778 } | 3510 } |
2779 } | 3511 } |
2780 } else { | 3512 } else { |
2781 pidx = this._physicalStart; | 3513 pidx = this._physicalStart; |
2782 vidx = this._virtualStartVal; | 3514 vidx = this._virtualStart; |
2783 | 3515 |
2784 for (; pidx < this._physicalCount; pidx++, vidx++) { | 3516 for (; pidx < this._physicalCount; pidx++, vidx++) { |
2785 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 3517 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
2786 return rtn; | 3518 return rtn; |
2787 } | 3519 } |
2788 } | 3520 } |
2789 | 3521 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
2790 pidx = 0; | |
2791 | |
2792 for (; pidx < this._physicalStart; pidx++, vidx++) { | |
2793 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 3522 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
2794 return rtn; | 3523 return rtn; |
2795 } | 3524 } |
2796 } | 3525 } |
2797 } | 3526 } |
2798 }, | 3527 }, |
2799 | 3528 |
2800 /** | 3529 /** |
2801 * Assigns the data models to a given set of items. | 3530 * Assigns the data models to a given set of items. |
2802 * @param {!Array<number>=} itemSet | 3531 * @param {!Array<number>=} itemSet |
2803 */ | 3532 */ |
2804 _assignModels: function(itemSet) { | 3533 _assignModels: function(itemSet) { |
2805 this._iterateItems(function(pidx, vidx) { | 3534 this._iterateItems(function(pidx, vidx) { |
2806 var el = this._physicalItems[pidx]; | 3535 var el = this._physicalItems[pidx]; |
2807 var inst = el._templateInstance; | 3536 var inst = el._templateInstance; |
2808 var item = this.items && this.items[vidx]; | 3537 var item = this.items && this.items[vidx]; |
2809 | 3538 |
2810 if (item) { | 3539 if (item !== undefined && item !== null) { |
2811 inst[this.as] = item; | 3540 inst[this.as] = item; |
2812 inst.__key__ = this._collection.getKey(item); | 3541 inst.__key__ = this._collection.getKey(item); |
2813 inst[this.selectedAs] = | 3542 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); |
2814 /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(it
em); | |
2815 inst[this.indexAs] = vidx; | 3543 inst[this.indexAs] = vidx; |
| 3544 inst.tabIndex = vidx === this._focusedIndex ? 0 : -1; |
2816 el.removeAttribute('hidden'); | 3545 el.removeAttribute('hidden'); |
2817 this._physicalIndexForKey[inst.__key__] = pidx; | 3546 this._physicalIndexForKey[inst.__key__] = pidx; |
2818 } else { | 3547 } else { |
2819 inst.__key__ = null; | 3548 inst.__key__ = null; |
2820 el.setAttribute('hidden', ''); | 3549 el.setAttribute('hidden', ''); |
2821 } | 3550 } |
2822 | 3551 |
2823 }, itemSet); | 3552 }, itemSet); |
2824 }, | 3553 }, |
2825 | 3554 |
2826 /** | 3555 /** |
2827 * Updates the height for a given set of items. | 3556 * Updates the height for a given set of items. |
2828 * | 3557 * |
2829 * @param {!Array<number>=} itemSet | 3558 * @param {!Array<number>=} itemSet |
2830 */ | 3559 */ |
2831 _updateMetrics: function(itemSet) { | 3560 _updateMetrics: function(itemSet) { |
| 3561 // Make sure we distributed all the physical items |
| 3562 // so we can measure them |
| 3563 Polymer.dom.flush(); |
| 3564 |
2832 var newPhysicalSize = 0; | 3565 var newPhysicalSize = 0; |
2833 var oldPhysicalSize = 0; | 3566 var oldPhysicalSize = 0; |
2834 var prevAvgCount = this._physicalAverageCount; | 3567 var prevAvgCount = this._physicalAverageCount; |
2835 var prevPhysicalAvg = this._physicalAverage; | 3568 var prevPhysicalAvg = this._physicalAverage; |
2836 // Make sure we distributed all the physical items | |
2837 // so we can measure them | |
2838 Polymer.dom.flush(); | |
2839 | 3569 |
2840 this._iterateItems(function(pidx, vidx) { | 3570 this._iterateItems(function(pidx, vidx) { |
| 3571 |
2841 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 3572 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
2842 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 3573 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
2843 newPhysicalSize += this._physicalSizes[pidx]; | 3574 newPhysicalSize += this._physicalSizes[pidx]; |
2844 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 3575 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| 3576 |
2845 }, itemSet); | 3577 }, itemSet); |
2846 | 3578 |
2847 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSiz
e; | 3579 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSiz
e; |
2848 this._viewportSize = this._scroller.offsetHeight; | 3580 this._viewportSize = this._scrollTargetHeight; |
2849 | 3581 |
2850 // update the average if we measured something | 3582 // update the average if we measured something |
2851 if (this._physicalAverageCount !== prevAvgCount) { | 3583 if (this._physicalAverageCount !== prevAvgCount) { |
2852 this._physicalAverage = Math.round( | 3584 this._physicalAverage = Math.round( |
2853 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | 3585 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
2854 this._physicalAverageCount); | 3586 this._physicalAverageCount); |
2855 } | 3587 } |
2856 }, | 3588 }, |
2857 | 3589 |
2858 /** | 3590 /** |
2859 * Updates the position of the physical items. | 3591 * Updates the position of the physical items. |
2860 */ | 3592 */ |
2861 _positionItems: function() { | 3593 _positionItems: function() { |
2862 this._adjustScrollPosition(); | 3594 this._adjustScrollPosition(); |
2863 | 3595 |
2864 var y = this._physicalTop; | 3596 var y = this._physicalTop; |
2865 | 3597 |
2866 this._iterateItems(function(pidx) { | 3598 this._iterateItems(function(pidx) { |
2867 | 3599 |
2868 this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pid
x]); | 3600 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
2869 y += this._physicalSizes[pidx]; | 3601 y += this._physicalSizes[pidx]; |
2870 | 3602 |
2871 }); | 3603 }); |
2872 }, | 3604 }, |
2873 | 3605 |
2874 /** | 3606 /** |
2875 * Adjusts the scroll position when it was overestimated. | 3607 * Adjusts the scroll position when it was overestimated. |
2876 */ | 3608 */ |
2877 _adjustScrollPosition: function() { | 3609 _adjustScrollPosition: function() { |
2878 var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop : | 3610 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
2879 Math.min(this._scrollPosition + this._physicalTop, 0); | 3611 Math.min(this._scrollPosition + this._physicalTop, 0); |
2880 | 3612 |
2881 if (deltaHeight) { | 3613 if (deltaHeight) { |
2882 this._physicalTop = this._physicalTop - deltaHeight; | 3614 this._physicalTop = this._physicalTop - deltaHeight; |
2883 | |
2884 // juking scroll position during interial scrolling on iOS is no bueno | 3615 // juking scroll position during interial scrolling on iOS is no bueno |
2885 if (!IOS_TOUCH_SCROLLING) { | 3616 if (!IOS_TOUCH_SCROLLING) { |
2886 this._resetScrollPosition(this._scroller.scrollTop - deltaHeight); | 3617 this._resetScrollPosition(this._scrollTop - deltaHeight); |
2887 } | 3618 } |
2888 } | 3619 } |
2889 }, | 3620 }, |
2890 | 3621 |
2891 /** | 3622 /** |
2892 * Sets the position of the scroll. | 3623 * Sets the position of the scroll. |
2893 */ | 3624 */ |
2894 _resetScrollPosition: function(pos) { | 3625 _resetScrollPosition: function(pos) { |
2895 if (this._scroller) { | 3626 if (this.scrollTarget) { |
2896 this._scroller.scrollTop = pos; | 3627 this._scrollTop = pos; |
2897 this._scrollPosition = this._scroller.scrollTop; | 3628 this._scrollPosition = this._scrollTop; |
2898 } | 3629 } |
2899 }, | 3630 }, |
2900 | 3631 |
2901 /** | 3632 /** |
2902 * Sets the scroll height, that's the height of the content, | 3633 * Sets the scroll height, that's the height of the content, |
2903 * | 3634 * |
2904 * @param {boolean=} forceUpdate If true, updates the height no matter what. | 3635 * @param {boolean=} forceUpdate If true, updates the height no matter what. |
2905 */ | 3636 */ |
2906 _updateScrollerSize: function(forceUpdate) { | 3637 _updateScrollerSize: function(forceUpdate) { |
2907 this._estScrollHeight = (this._physicalBottom + | 3638 this._estScrollHeight = (this._physicalBottom + |
2908 Math.max(this._virtualCount - this._physicalCount - this._virtualStart
Val, 0) * this._physicalAverage); | 3639 Math.max(this._virtualCount - this._physicalCount - this._virtualStart
, 0) * this._physicalAverage); |
2909 | 3640 |
2910 forceUpdate = forceUpdate || this._scrollHeight === 0; | 3641 forceUpdate = forceUpdate || this._scrollHeight === 0; |
2911 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 3642 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
2912 | 3643 |
2913 // amortize height adjustment, so it won't trigger repaints very often | 3644 // amortize height adjustment, so it won't trigger repaints very often |
2914 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 3645 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
2915 this.$.items.style.height = this._estScrollHeight + 'px'; | 3646 this.$.items.style.height = this._estScrollHeight + 'px'; |
2916 this._scrollHeight = this._estScrollHeight; | 3647 this._scrollHeight = this._estScrollHeight; |
2917 } | 3648 } |
2918 }, | 3649 }, |
2919 | 3650 |
2920 /** | 3651 /** |
2921 * Scroll to a specific item in the virtual list regardless | 3652 * Scroll to a specific item in the virtual list regardless |
2922 * of the physical items in the DOM tree. | 3653 * of the physical items in the DOM tree. |
2923 * | 3654 * |
2924 * @method scrollToIndex | 3655 * @method scrollToIndex |
2925 * @param {number} idx The index of the item | 3656 * @param {number} idx The index of the item |
2926 */ | 3657 */ |
2927 scrollToIndex: function(idx) { | 3658 scrollToIndex: function(idx) { |
2928 if (typeof idx !== 'number') { | 3659 if (typeof idx !== 'number') { |
2929 return; | 3660 return; |
2930 } | 3661 } |
2931 | 3662 |
| 3663 Polymer.dom.flush(); |
| 3664 |
2932 var firstVisible = this.firstVisibleIndex; | 3665 var firstVisible = this.firstVisibleIndex; |
2933 | |
2934 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | 3666 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
2935 | 3667 |
2936 // start at the previous virtual item | 3668 // start at the previous virtual item |
2937 // so we have a item above the first visible item | 3669 // so we have a item above the first visible item |
2938 this._virtualStart = idx - 1; | 3670 this._virtualStart = idx - 1; |
2939 | |
2940 // assign new models | 3671 // assign new models |
2941 this._assignModels(); | 3672 this._assignModels(); |
2942 | |
2943 // measure the new sizes | 3673 // measure the new sizes |
2944 this._updateMetrics(); | 3674 this._updateMetrics(); |
2945 | |
2946 // estimate new physical offset | 3675 // estimate new physical offset |
2947 this._physicalTop = this._virtualStart * this._physicalAverage; | 3676 this._physicalTop = this._virtualStart * this._physicalAverage; |
2948 | 3677 |
2949 var currentTopItem = this._physicalStart; | 3678 var currentTopItem = this._physicalStart; |
2950 var currentVirtualItem = this._virtualStart; | 3679 var currentVirtualItem = this._virtualStart; |
2951 var targetOffsetTop = 0; | 3680 var targetOffsetTop = 0; |
2952 var hiddenContentSize = this._hiddenContentSize; | 3681 var hiddenContentSize = this._hiddenContentSize; |
2953 | 3682 |
2954 // scroll to the item as much as we can | 3683 // scroll to the item as much as we can |
2955 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { | 3684 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { |
2956 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; | 3685 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; |
2957 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 3686 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
2958 currentVirtualItem++; | 3687 currentVirtualItem++; |
2959 } | 3688 } |
2960 | |
2961 // update the scroller size | 3689 // update the scroller size |
2962 this._updateScrollerSize(true); | 3690 this._updateScrollerSize(true); |
2963 | |
2964 // update the position of the items | 3691 // update the position of the items |
2965 this._positionItems(); | 3692 this._positionItems(); |
2966 | |
2967 // set the new scroll position | 3693 // set the new scroll position |
2968 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); | 3694 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop + 1); |
2969 | |
2970 // increase the pool of physical items if needed | 3695 // increase the pool of physical items if needed |
2971 this._increasePoolIfNeeded(); | 3696 this._increasePoolIfNeeded(); |
2972 | |
2973 // clear cached visible index | 3697 // clear cached visible index |
2974 this._firstVisibleIndexVal = null; | 3698 this._firstVisibleIndexVal = null; |
| 3699 this._lastVisibleIndexVal = null; |
2975 }, | 3700 }, |
2976 | 3701 |
2977 /** | 3702 /** |
2978 * Reset the physical average and the average count. | 3703 * Reset the physical average and the average count. |
2979 */ | 3704 */ |
2980 _resetAverage: function() { | 3705 _resetAverage: function() { |
2981 this._physicalAverage = 0; | 3706 this._physicalAverage = 0; |
2982 this._physicalAverageCount = 0; | 3707 this._physicalAverageCount = 0; |
2983 }, | 3708 }, |
2984 | 3709 |
2985 /** | 3710 /** |
2986 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | 3711 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` |
2987 * when the element is resized. | 3712 * when the element is resized. |
2988 */ | 3713 */ |
2989 _resizeHandler: function() { | 3714 _resizeHandler: function() { |
2990 this.debounce('resize', function() { | 3715 // iOS fires the resize event when the address bar slides up |
| 3716 if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100)
{ |
| 3717 return; |
| 3718 } |
| 3719 this._debounceTemplate(function() { |
2991 this._render(); | 3720 this._render(); |
2992 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 3721 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
2993 this._resetAverage(); | 3722 this._resetAverage(); |
2994 this.updateViewportBoundaries(); | 3723 this.updateViewportBoundaries(); |
2995 this.scrollToIndex(this.firstVisibleIndex); | 3724 this.scrollToIndex(this.firstVisibleIndex); |
2996 } | 3725 } |
2997 }); | 3726 }); |
2998 }, | 3727 }, |
2999 | 3728 |
3000 _getModelFromItem: function(item) { | 3729 _getModelFromItem: function(item) { |
3001 var key = this._collection.getKey(item); | 3730 var key = this._collection.getKey(item); |
3002 var pidx = this._physicalIndexForKey[key]; | 3731 var pidx = this._physicalIndexForKey[key]; |
3003 | 3732 |
3004 if (pidx !== undefined) { | 3733 if (pidx !== undefined) { |
3005 return this._physicalItems[pidx]._templateInstance; | 3734 return this._physicalItems[pidx]._templateInstance; |
3006 } | 3735 } |
3007 return null; | 3736 return null; |
3008 }, | 3737 }, |
3009 | 3738 |
3010 /** | 3739 /** |
3011 * Gets a valid item instance from its index or the object value. | 3740 * Gets a valid item instance from its index or the object value. |
3012 * | 3741 * |
3013 * @param {(Object|number)} item The item object or its index | 3742 * @param {(Object|number)} item The item object or its index |
3014 */ | 3743 */ |
3015 _getNormalizedItem: function(item) { | 3744 _getNormalizedItem: function(item) { |
3016 if (typeof item === 'number') { | 3745 if (this._collection.getKey(item) === undefined) { |
3017 item = this.items[item]; | 3746 if (typeof item === 'number') { |
3018 if (!item) { | 3747 item = this.items[item]; |
3019 throw new RangeError('<item> not found'); | 3748 if (!item) { |
3020 } | 3749 throw new RangeError('<item> not found'); |
3021 } else if (this._collection.getKey(item) === undefined) { | 3750 } |
| 3751 return item; |
| 3752 } |
3022 throw new TypeError('<item> should be a valid item'); | 3753 throw new TypeError('<item> should be a valid item'); |
3023 } | 3754 } |
3024 return item; | 3755 return item; |
3025 }, | 3756 }, |
3026 | 3757 |
3027 /** | 3758 /** |
3028 * Select the list item at the given index. | 3759 * Select the list item at the given index. |
3029 * | 3760 * |
3030 * @method selectItem | 3761 * @method selectItem |
3031 * @param {(Object|number)} item The item object or its index | 3762 * @param {(Object|number)} item The item object or its index |
3032 */ | 3763 */ |
3033 selectItem: function(item) { | 3764 selectItem: function(item) { |
3034 item = this._getNormalizedItem(item); | 3765 item = this._getNormalizedItem(item); |
3035 var model = this._getModelFromItem(item); | 3766 var model = this._getModelFromItem(item); |
3036 | 3767 |
3037 if (!this.multiSelection && this.selectedItem) { | 3768 if (!this.multiSelection && this.selectedItem) { |
3038 this.deselectItem(this.selectedItem); | 3769 this.deselectItem(this.selectedItem); |
3039 } | 3770 } |
3040 if (model) { | 3771 if (model) { |
3041 model[this.selectedAs] = true; | 3772 model[this.selectedAs] = true; |
3042 } | 3773 } |
3043 this.$.selector.select(item); | 3774 this.$.selector.select(item); |
| 3775 this.updateSizeForItem(item); |
3044 }, | 3776 }, |
3045 | 3777 |
3046 /** | 3778 /** |
3047 * Deselects the given item list if it is already selected. | 3779 * Deselects the given item list if it is already selected. |
3048 * | 3780 * |
3049 | 3781 |
3050 * @method deselect | 3782 * @method deselect |
3051 * @param {(Object|number)} item The item object or its index | 3783 * @param {(Object|number)} item The item object or its index |
3052 */ | 3784 */ |
3053 deselectItem: function(item) { | 3785 deselectItem: function(item) { |
3054 item = this._getNormalizedItem(item); | 3786 item = this._getNormalizedItem(item); |
3055 var model = this._getModelFromItem(item); | 3787 var model = this._getModelFromItem(item); |
3056 | 3788 |
3057 if (model) { | 3789 if (model) { |
3058 model[this.selectedAs] = false; | 3790 model[this.selectedAs] = false; |
3059 } | 3791 } |
3060 this.$.selector.deselect(item); | 3792 this.$.selector.deselect(item); |
| 3793 this.updateSizeForItem(item); |
3061 }, | 3794 }, |
3062 | 3795 |
3063 /** | 3796 /** |
3064 * Select or deselect a given item depending on whether the item | 3797 * Select or deselect a given item depending on whether the item |
3065 * has already been selected. | 3798 * has already been selected. |
3066 * | 3799 * |
3067 * @method toggleSelectionForItem | 3800 * @method toggleSelectionForItem |
3068 * @param {(Object|number)} item The item object or its index | 3801 * @param {(Object|number)} item The item object or its index |
3069 */ | 3802 */ |
3070 toggleSelectionForItem: function(item) { | 3803 toggleSelectionForItem: function(item) { |
(...skipping 25 matching lines...) Expand all Loading... |
3096 } | 3829 } |
3097 | 3830 |
3098 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | 3831 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
3099 }, | 3832 }, |
3100 | 3833 |
3101 /** | 3834 /** |
3102 * Add an event listener to `tap` if `selectionEnabled` is true, | 3835 * Add an event listener to `tap` if `selectionEnabled` is true, |
3103 * it will remove the listener otherwise. | 3836 * it will remove the listener otherwise. |
3104 */ | 3837 */ |
3105 _selectionEnabledChanged: function(selectionEnabled) { | 3838 _selectionEnabledChanged: function(selectionEnabled) { |
3106 if (selectionEnabled) { | 3839 var handler = selectionEnabled ? this.listen : this.unlisten; |
3107 this.listen(this, 'tap', '_selectionHandler'); | 3840 handler.call(this, this, 'tap', '_selectionHandler'); |
3108 this.listen(this, 'keypress', '_selectionHandler'); | |
3109 } else { | |
3110 this.unlisten(this, 'tap', '_selectionHandler'); | |
3111 this.unlisten(this, 'keypress', '_selectionHandler'); | |
3112 } | |
3113 }, | 3841 }, |
3114 | 3842 |
3115 /** | 3843 /** |
3116 * Select an item from an event object. | 3844 * Select an item from an event object. |
3117 */ | 3845 */ |
3118 _selectionHandler: function(e) { | 3846 _selectionHandler: function(e) { |
3119 if (e.type !== 'keypress' || e.keyCode === 13) { | 3847 if (this.selectionEnabled) { |
3120 var model = this.modelForElement(e.target); | 3848 var model = this.modelForElement(e.target); |
3121 if (model) { | 3849 if (model) { |
3122 this.toggleSelectionForItem(model[this.as]); | 3850 this.toggleSelectionForItem(model[this.as]); |
3123 } | 3851 } |
3124 } | 3852 } |
3125 }, | 3853 }, |
3126 | 3854 |
3127 _multiSelectionChanged: function(multiSelection) { | 3855 _multiSelectionChanged: function(multiSelection) { |
3128 this.clearSelection(); | 3856 this.clearSelection(); |
3129 this.$.selector.multi = multiSelection; | 3857 this.$.selector.multi = multiSelection; |
3130 }, | 3858 }, |
3131 | 3859 |
3132 /** | 3860 /** |
3133 * Updates the size of an item. | 3861 * Updates the size of an item. |
3134 * | 3862 * |
3135 * @method updateSizeForItem | 3863 * @method updateSizeForItem |
3136 * @param {(Object|number)} item The item object or its index | 3864 * @param {(Object|number)} item The item object or its index |
3137 */ | 3865 */ |
3138 updateSizeForItem: function(item) { | 3866 updateSizeForItem: function(item) { |
3139 item = this._getNormalizedItem(item); | 3867 item = this._getNormalizedItem(item); |
3140 var key = this._collection.getKey(item); | 3868 var key = this._collection.getKey(item); |
3141 var pidx = this._physicalIndexForKey[key]; | 3869 var pidx = this._physicalIndexForKey[key]; |
3142 | 3870 |
3143 if (pidx !== undefined) { | 3871 if (pidx !== undefined) { |
3144 this._updateMetrics([pidx]); | 3872 this._updateMetrics([pidx]); |
3145 this._positionItems(); | 3873 this._positionItems(); |
3146 } | 3874 } |
| 3875 }, |
| 3876 |
| 3877 _isIndexRendered: function(idx) { |
| 3878 return idx >= this._virtualStart && idx <= this._virtualEnd; |
| 3879 }, |
| 3880 |
| 3881 _getPhysicalItemForIndex: function(idx, force) { |
| 3882 if (!this._collection) { |
| 3883 return null; |
| 3884 } |
| 3885 if (!this._isIndexRendered(idx)) { |
| 3886 if (force) { |
| 3887 this.scrollToIndex(idx); |
| 3888 return this._getPhysicalItemForIndex(idx, false); |
| 3889 } |
| 3890 return null; |
| 3891 } |
| 3892 var item = this._getNormalizedItem(idx); |
| 3893 var physicalItem = this._physicalItems[this._physicalIndexForKey[this._col
lection.getKey(item)]]; |
| 3894 |
| 3895 return physicalItem || null; |
| 3896 }, |
| 3897 |
| 3898 _focusPhysicalItem: function(idx) { |
| 3899 this._restoreFocusedItem(); |
| 3900 |
| 3901 var physicalItem = this._getPhysicalItemForIndex(idx, true); |
| 3902 if (!physicalItem) { |
| 3903 return; |
| 3904 } |
| 3905 var SECRET = ~(Math.random() * 100); |
| 3906 var model = physicalItem._templateInstance; |
| 3907 var focusable; |
| 3908 |
| 3909 model.tabIndex = SECRET; |
| 3910 // the focusable element could be the entire physical item |
| 3911 if (physicalItem.tabIndex === SECRET) { |
| 3912 focusable = physicalItem; |
| 3913 } |
| 3914 // the focusable element could be somewhere within the physical item |
| 3915 if (!focusable) { |
| 3916 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET + '"]'); |
| 3917 } |
| 3918 // restore the tab index |
| 3919 model.tabIndex = 0; |
| 3920 focusable && focusable.focus(); |
| 3921 }, |
| 3922 |
| 3923 _restoreFocusedItem: function() { |
| 3924 if (!this._offscreenFocusedItem) { |
| 3925 return; |
| 3926 } |
| 3927 var item = this._getNormalizedItem(this._focusedIndex); |
| 3928 var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; |
| 3929 |
| 3930 if (pidx !== undefined) { |
| 3931 this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]); |
| 3932 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 3933 } |
| 3934 this._offscreenFocusedItem = null; |
| 3935 }, |
| 3936 |
| 3937 _removeFocusedItem: function() { |
| 3938 if (!this._offscreenFocusedItem) { |
| 3939 return; |
| 3940 } |
| 3941 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
| 3942 this._offscreenFocusedItem = null; |
| 3943 this._focusBackfillItem = null; |
| 3944 }, |
| 3945 |
| 3946 _createFocusBackfillItem: function() { |
| 3947 if (this._offscreenFocusedItem) { |
| 3948 return; |
| 3949 } |
| 3950 var item = this._getNormalizedItem(this._focusedIndex); |
| 3951 var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; |
| 3952 |
| 3953 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 3954 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 3955 |
| 3956 if (!this._focusBackfillItem) { |
| 3957 var stampedTemplate = this.stamp(null); |
| 3958 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
| 3959 Polymer.dom(this).appendChild(stampedTemplate.root); |
| 3960 } |
| 3961 this._physicalItems[pidx] = this._focusBackfillItem; |
| 3962 }, |
| 3963 |
| 3964 _didFocus: function(e) { |
| 3965 var targetModel = this.modelForElement(e.target); |
| 3966 var fidx = this._focusedIndex; |
| 3967 |
| 3968 if (!targetModel) { |
| 3969 return; |
| 3970 } |
| 3971 this._restoreFocusedItem(); |
| 3972 |
| 3973 if (this.modelForElement(this._offscreenFocusedItem) === targetModel) { |
| 3974 this.scrollToIndex(fidx); |
| 3975 } else { |
| 3976 // restore tabIndex for the currently focused item |
| 3977 this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1; |
| 3978 // set the tabIndex for the next focused item |
| 3979 targetModel.tabIndex = 0; |
| 3980 fidx = /** @type {{index: number}} */(targetModel).index; |
| 3981 this._focusedIndex = fidx; |
| 3982 // bring the item into view |
| 3983 if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) { |
| 3984 this.scrollToIndex(fidx); |
| 3985 } else { |
| 3986 this._update(); |
| 3987 } |
| 3988 } |
| 3989 }, |
| 3990 |
| 3991 _didMoveUp: function() { |
| 3992 this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1)); |
| 3993 }, |
| 3994 |
| 3995 _didMoveDown: function() { |
| 3996 this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex +
1)); |
| 3997 }, |
| 3998 |
| 3999 _didEnter: function(e) { |
| 4000 // focus the currently focused physical item |
| 4001 this._focusPhysicalItem(this._focusedIndex); |
| 4002 // toggle selection |
| 4003 this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).key
boardEvent); |
3147 } | 4004 } |
3148 }); | 4005 }); |
3149 | 4006 |
3150 })(); | 4007 })(); |
3151 (function() { | 4008 (function() { |
3152 | 4009 |
3153 // monostate data | 4010 // monostate data |
3154 var metaDatas = {}; | 4011 var metaDatas = {}; |
3155 var metaArrays = {}; | 4012 var metaArrays = {}; |
3156 var singleton = null; | 4013 var singleton = null; |
(...skipping 526 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3683 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | 4540 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 |
3684 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | 4541 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root |
3685 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | 4542 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; |
3686 svg.appendChild(content).removeAttribute('id'); | 4543 svg.appendChild(content).removeAttribute('id'); |
3687 return svg; | 4544 return svg; |
3688 } | 4545 } |
3689 return null; | 4546 return null; |
3690 } | 4547 } |
3691 | 4548 |
3692 }); | 4549 }); |
3693 (function() { | |
3694 'use strict'; | |
3695 | |
3696 /** | |
3697 * Chrome uses an older version of DOM Level 3 Keyboard Events | |
3698 * | |
3699 * Most keys are labeled as text, but some are Unicode codepoints. | |
3700 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | |
3701 */ | |
3702 var KEY_IDENTIFIER = { | |
3703 'U+0008': 'backspace', | |
3704 'U+0009': 'tab', | |
3705 'U+001B': 'esc', | |
3706 'U+0020': 'space', | |
3707 'U+007F': 'del' | |
3708 }; | |
3709 | |
3710 /** | |
3711 * Special table for KeyboardEvent.keyCode. | |
3712 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | |
3713 * than that. | |
3714 * | |
3715 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
3716 */ | |
3717 var KEY_CODE = { | |
3718 8: 'backspace', | |
3719 9: 'tab', | |
3720 13: 'enter', | |
3721 27: 'esc', | |
3722 33: 'pageup', | |
3723 34: 'pagedown', | |
3724 35: 'end', | |
3725 36: 'home', | |
3726 32: 'space', | |
3727 37: 'left', | |
3728 38: 'up', | |
3729 39: 'right', | |
3730 40: 'down', | |
3731 46: 'del', | |
3732 106: '*' | |
3733 }; | |
3734 | |
3735 /** | |
3736 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
3737 * combo string to the property name that references those same keys | |
3738 * in a KeyboardEvent instance. | |
3739 */ | |
3740 var MODIFIER_KEYS = { | |
3741 'shift': 'shiftKey', | |
3742 'ctrl': 'ctrlKey', | |
3743 'alt': 'altKey', | |
3744 'meta': 'metaKey' | |
3745 }; | |
3746 | |
3747 /** | |
3748 * KeyboardEvent.key is mostly represented by printable character made by | |
3749 * the keyboard, with unprintable keys labeled nicely. | |
3750 * | |
3751 * However, on OS X, Alt+char can make a Unicode character that follows an | |
3752 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
3753 */ | |
3754 var KEY_CHAR = /[a-z0-9*]/; | |
3755 | |
3756 /** | |
3757 * Matches a keyIdentifier string. | |
3758 */ | |
3759 var IDENT_CHAR = /U\+/; | |
3760 | |
3761 /** | |
3762 * Matches arrow keys in Gecko 27.0+ | |
3763 */ | |
3764 var ARROW_KEY = /^arrow/; | |
3765 | |
3766 /** | |
3767 * Matches space keys everywhere (notably including IE10's exceptional name | |
3768 * `spacebar`). | |
3769 */ | |
3770 var SPACE_KEY = /^space(bar)?/; | |
3771 | |
3772 /** | |
3773 * Transforms the key. | |
3774 * @param {string} key The KeyBoardEvent.key | |
3775 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
3776 * alpha-numeric characters. | |
3777 */ | |
3778 function transformKey(key, noSpecialChars) { | |
3779 var validKey = ''; | |
3780 if (key) { | |
3781 var lKey = key.toLowerCase(); | |
3782 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
3783 validKey = 'space'; | |
3784 } else if (lKey.length == 1) { | |
3785 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
3786 validKey = lKey; | |
3787 } | |
3788 } else if (ARROW_KEY.test(lKey)) { | |
3789 validKey = lKey.replace('arrow', ''); | |
3790 } else if (lKey == 'multiply') { | |
3791 // numpad '*' can map to Multiply on IE/Windows | |
3792 validKey = '*'; | |
3793 } else { | |
3794 validKey = lKey; | |
3795 } | |
3796 } | |
3797 return validKey; | |
3798 } | |
3799 | |
3800 function transformKeyIdentifier(keyIdent) { | |
3801 var validKey = ''; | |
3802 if (keyIdent) { | |
3803 if (keyIdent in KEY_IDENTIFIER) { | |
3804 validKey = KEY_IDENTIFIER[keyIdent]; | |
3805 } else if (IDENT_CHAR.test(keyIdent)) { | |
3806 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
3807 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
3808 } else { | |
3809 validKey = keyIdent.toLowerCase(); | |
3810 } | |
3811 } | |
3812 return validKey; | |
3813 } | |
3814 | |
3815 function transformKeyCode(keyCode) { | |
3816 var validKey = ''; | |
3817 if (Number(keyCode)) { | |
3818 if (keyCode >= 65 && keyCode <= 90) { | |
3819 // ascii a-z | |
3820 // lowercase is 32 offset from uppercase | |
3821 validKey = String.fromCharCode(32 + keyCode); | |
3822 } else if (keyCode >= 112 && keyCode <= 123) { | |
3823 // function keys f1-f12 | |
3824 validKey = 'f' + (keyCode - 112); | |
3825 } else if (keyCode >= 48 && keyCode <= 57) { | |
3826 // top 0-9 keys | |
3827 validKey = String(48 - keyCode); | |
3828 } else if (keyCode >= 96 && keyCode <= 105) { | |
3829 // num pad 0-9 | |
3830 validKey = String(96 - keyCode); | |
3831 } else { | |
3832 validKey = KEY_CODE[keyCode]; | |
3833 } | |
3834 } | |
3835 return validKey; | |
3836 } | |
3837 | |
3838 /** | |
3839 * Calculates the normalized key for a KeyboardEvent. | |
3840 * @param {KeyboardEvent} keyEvent | |
3841 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
3842 * transformation to alpha-numeric chars. This is useful with key | |
3843 * combinations like shift + 2, which on FF for MacOS produces | |
3844 * keyEvent.key = @ | |
3845 * To get 2 returned, set noSpecialChars = true | |
3846 * To get @ returned, set noSpecialChars = false | |
3847 */ | |
3848 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
3849 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
3850 // .detail.key to support artificial keyboard events. | |
3851 return transformKey(keyEvent.key, noSpecialChars) || | |
3852 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
3853 transformKeyCode(keyEvent.keyCode) || | |
3854 transformKey(keyEvent.detail.key, noSpecialChars) || ''; | |
3855 } | |
3856 | |
3857 function keyComboMatchesEvent(keyCombo, event) { | |
3858 // For combos with modifiers we support only alpha-numeric keys | |
3859 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
3860 return keyEvent === keyCombo.key && | |
3861 (!keyCombo.hasModifiers || ( | |
3862 !!event.shiftKey === !!keyCombo.shiftKey && | |
3863 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
3864 !!event.altKey === !!keyCombo.altKey && | |
3865 !!event.metaKey === !!keyCombo.metaKey) | |
3866 ); | |
3867 } | |
3868 | |
3869 function parseKeyComboString(keyComboString) { | |
3870 if (keyComboString.length === 1) { | |
3871 return { | |
3872 combo: keyComboString, | |
3873 key: keyComboString, | |
3874 event: 'keydown' | |
3875 }; | |
3876 } | |
3877 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
3878 var eventParts = keyComboPart.split(':'); | |
3879 var keyName = eventParts[0]; | |
3880 var event = eventParts[1]; | |
3881 | |
3882 if (keyName in MODIFIER_KEYS) { | |
3883 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
3884 parsedKeyCombo.hasModifiers = true; | |
3885 } else { | |
3886 parsedKeyCombo.key = keyName; | |
3887 parsedKeyCombo.event = event || 'keydown'; | |
3888 } | |
3889 | |
3890 return parsedKeyCombo; | |
3891 }, { | |
3892 combo: keyComboString.split(':').shift() | |
3893 }); | |
3894 } | |
3895 | |
3896 function parseEventString(eventString) { | |
3897 return eventString.trim().split(' ').map(function(keyComboString) { | |
3898 return parseKeyComboString(keyComboString); | |
3899 }); | |
3900 } | |
3901 | |
3902 /** | |
3903 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
3904 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
3905 * The element takes care of browser differences with respect to Keyboard ev
ents | |
3906 * and uses an expressive syntax to filter key presses. | |
3907 * | |
3908 * Use the `keyBindings` prototype property to express what combination of k
eys | |
3909 * will trigger the event to fire. | |
3910 * | |
3911 * Use the `key-event-target` attribute to set up event handlers on a specif
ic | |
3912 * node. | |
3913 * The `keys-pressed` event will fire when one of the key combinations set w
ith the | |
3914 * `keys` property is pressed. | |
3915 * | |
3916 * @demo demo/index.html | |
3917 * @polymerBehavior | |
3918 */ | |
3919 Polymer.IronA11yKeysBehavior = { | |
3920 properties: { | |
3921 /** | |
3922 * The HTMLElement that will be firing relevant KeyboardEvents. | |
3923 */ | |
3924 keyEventTarget: { | |
3925 type: Object, | |
3926 value: function() { | |
3927 return this; | |
3928 } | |
3929 }, | |
3930 | |
3931 /** | |
3932 * If true, this property will cause the implementing element to | |
3933 * automatically stop propagation on any handled KeyboardEvents. | |
3934 */ | |
3935 stopKeyboardEventPropagation: { | |
3936 type: Boolean, | |
3937 value: false | |
3938 }, | |
3939 | |
3940 _boundKeyHandlers: { | |
3941 type: Array, | |
3942 value: function() { | |
3943 return []; | |
3944 } | |
3945 }, | |
3946 | |
3947 // We use this due to a limitation in IE10 where instances will have | |
3948 // own properties of everything on the "prototype". | |
3949 _imperativeKeyBindings: { | |
3950 type: Object, | |
3951 value: function() { | |
3952 return {}; | |
3953 } | |
3954 } | |
3955 }, | |
3956 | |
3957 observers: [ | |
3958 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | |
3959 ], | |
3960 | |
3961 keyBindings: {}, | |
3962 | |
3963 registered: function() { | |
3964 this._prepKeyBindings(); | |
3965 }, | |
3966 | |
3967 attached: function() { | |
3968 this._listenKeyEventListeners(); | |
3969 }, | |
3970 | |
3971 detached: function() { | |
3972 this._unlistenKeyEventListeners(); | |
3973 }, | |
3974 | |
3975 /** | |
3976 * Can be used to imperatively add a key binding to the implementing | |
3977 * element. This is the imperative equivalent of declaring a keybinding | |
3978 * in the `keyBindings` prototype property. | |
3979 */ | |
3980 addOwnKeyBinding: function(eventString, handlerName) { | |
3981 this._imperativeKeyBindings[eventString] = handlerName; | |
3982 this._prepKeyBindings(); | |
3983 this._resetKeyEventListeners(); | |
3984 }, | |
3985 | |
3986 /** | |
3987 * When called, will remove all imperatively-added key bindings. | |
3988 */ | |
3989 removeOwnKeyBindings: function() { | |
3990 this._imperativeKeyBindings = {}; | |
3991 this._prepKeyBindings(); | |
3992 this._resetKeyEventListeners(); | |
3993 }, | |
3994 | |
3995 keyboardEventMatchesKeys: function(event, eventString) { | |
3996 var keyCombos = parseEventString(eventString); | |
3997 for (var i = 0; i < keyCombos.length; ++i) { | |
3998 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
3999 return true; | |
4000 } | |
4001 } | |
4002 return false; | |
4003 }, | |
4004 | |
4005 _collectKeyBindings: function() { | |
4006 var keyBindings = this.behaviors.map(function(behavior) { | |
4007 return behavior.keyBindings; | |
4008 }); | |
4009 | |
4010 if (keyBindings.indexOf(this.keyBindings) === -1) { | |
4011 keyBindings.push(this.keyBindings); | |
4012 } | |
4013 | |
4014 return keyBindings; | |
4015 }, | |
4016 | |
4017 _prepKeyBindings: function() { | |
4018 this._keyBindings = {}; | |
4019 | |
4020 this._collectKeyBindings().forEach(function(keyBindings) { | |
4021 for (var eventString in keyBindings) { | |
4022 this._addKeyBinding(eventString, keyBindings[eventString]); | |
4023 } | |
4024 }, this); | |
4025 | |
4026 for (var eventString in this._imperativeKeyBindings) { | |
4027 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
4028 } | |
4029 | |
4030 // Give precedence to combos with modifiers to be checked first. | |
4031 for (var eventName in this._keyBindings) { | |
4032 this._keyBindings[eventName].sort(function (kb1, kb2) { | |
4033 var b1 = kb1[0].hasModifiers; | |
4034 var b2 = kb2[0].hasModifiers; | |
4035 return (b1 === b2) ? 0 : b1 ? -1 : 1; | |
4036 }) | |
4037 } | |
4038 }, | |
4039 | |
4040 _addKeyBinding: function(eventString, handlerName) { | |
4041 parseEventString(eventString).forEach(function(keyCombo) { | |
4042 this._keyBindings[keyCombo.event] = | |
4043 this._keyBindings[keyCombo.event] || []; | |
4044 | |
4045 this._keyBindings[keyCombo.event].push([ | |
4046 keyCombo, | |
4047 handlerName | |
4048 ]); | |
4049 }, this); | |
4050 }, | |
4051 | |
4052 _resetKeyEventListeners: function() { | |
4053 this._unlistenKeyEventListeners(); | |
4054 | |
4055 if (this.isAttached) { | |
4056 this._listenKeyEventListeners(); | |
4057 } | |
4058 }, | |
4059 | |
4060 _listenKeyEventListeners: function() { | |
4061 Object.keys(this._keyBindings).forEach(function(eventName) { | |
4062 var keyBindings = this._keyBindings[eventName]; | |
4063 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | |
4064 | |
4065 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | |
4066 | |
4067 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | |
4068 }, this); | |
4069 }, | |
4070 | |
4071 _unlistenKeyEventListeners: function() { | |
4072 var keyHandlerTuple; | |
4073 var keyEventTarget; | |
4074 var eventName; | |
4075 var boundKeyHandler; | |
4076 | |
4077 while (this._boundKeyHandlers.length) { | |
4078 // My kingdom for block-scope binding and destructuring assignment.. | |
4079 keyHandlerTuple = this._boundKeyHandlers.pop(); | |
4080 keyEventTarget = keyHandlerTuple[0]; | |
4081 eventName = keyHandlerTuple[1]; | |
4082 boundKeyHandler = keyHandlerTuple[2]; | |
4083 | |
4084 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | |
4085 } | |
4086 }, | |
4087 | |
4088 _onKeyBindingEvent: function(keyBindings, event) { | |
4089 if (this.stopKeyboardEventPropagation) { | |
4090 event.stopPropagation(); | |
4091 } | |
4092 | |
4093 // if event has been already prevented, don't do anything | |
4094 if (event.defaultPrevented) { | |
4095 return; | |
4096 } | |
4097 | |
4098 for (var i = 0; i < keyBindings.length; i++) { | |
4099 var keyCombo = keyBindings[i][0]; | |
4100 var handlerName = keyBindings[i][1]; | |
4101 if (keyComboMatchesEvent(keyCombo, event)) { | |
4102 this._triggerKeyHandler(keyCombo, handlerName, event); | |
4103 // exit the loop if eventDefault was prevented | |
4104 if (event.defaultPrevented) { | |
4105 return; | |
4106 } | |
4107 } | |
4108 } | |
4109 }, | |
4110 | |
4111 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | |
4112 var detail = Object.create(keyCombo); | |
4113 detail.keyboardEvent = keyboardEvent; | |
4114 var event = new CustomEvent(keyCombo.event, { | |
4115 detail: detail, | |
4116 cancelable: true | |
4117 }); | |
4118 this[handlerName].call(this, event); | |
4119 if (event.defaultPrevented) { | |
4120 keyboardEvent.preventDefault(); | |
4121 } | |
4122 } | |
4123 }; | |
4124 })(); | |
4125 /** | 4550 /** |
4126 * @demo demo/index.html | 4551 * @demo demo/index.html |
4127 * @polymerBehavior | 4552 * @polymerBehavior |
4128 */ | 4553 */ |
4129 Polymer.IronControlState = { | 4554 Polymer.IronControlState = { |
4130 | 4555 |
4131 properties: { | 4556 properties: { |
4132 | 4557 |
4133 /** | 4558 /** |
4134 * If true, the element currently has focus. | 4559 * If true, the element currently has focus. |
(...skipping 5924 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
10059 Manager.get().updateItem_(index, data); | 10484 Manager.get().updateItem_(index, data); |
10060 }; | 10485 }; |
10061 | 10486 |
10062 return {Manager: Manager}; | 10487 return {Manager: Manager}; |
10063 }); | 10488 }); |
10064 // Copyright 2015 The Chromium Authors. All rights reserved. | 10489 // Copyright 2015 The Chromium Authors. All rights reserved. |
10065 // Use of this source code is governed by a BSD-style license that can be | 10490 // Use of this source code is governed by a BSD-style license that can be |
10066 // found in the LICENSE file. | 10491 // found in the LICENSE file. |
10067 | 10492 |
10068 window.addEventListener('load', downloads.Manager.onLoad); | 10493 window.addEventListener('load', downloads.Manager.onLoad); |
OLD | NEW |