| OLD | NEW |
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * @fileoverview Assertion support. | |
| 7 */ | |
| 8 | |
| 9 /** | |
| 10 * Verify |condition| is truthy and return |condition| if so. | |
| 11 * @template T | |
| 12 * @param {T} condition A condition to check for truthiness. Note that this | |
| 13 * may be used to test whether a value is defined or not, and we don't want | |
| 14 * to force a cast to Boolean. | |
| 15 * @param {string=} opt_message A message to show on failure. | |
| 16 * @return {T} A non-null |condition|. | |
| 17 */ | |
| 18 function assert(condition, opt_message) { | |
| 19 if (!condition) { | |
| 20 var message = 'Assertion failed'; | |
| 21 if (opt_message) | |
| 22 message = message + ': ' + opt_message; | |
| 23 var error = new Error(message); | |
| 24 var global = function() { return this; }(); | |
| 25 if (global.traceAssertionsForTesting) | |
| 26 console.warn(error.stack); | |
| 27 throw error; | |
| 28 } | |
| 29 return condition; | |
| 30 } | |
| 31 | |
| 32 /** | |
| 33 * Call this from places in the code that should never be reached. | |
| 34 * | |
| 35 * For example, handling all the values of enum with a switch() like this: | |
| 36 * | |
| 37 * function getValueFromEnum(enum) { | |
| 38 * switch (enum) { | |
| 39 * case ENUM_FIRST_OF_TWO: | |
| 40 * return first | |
| 41 * case ENUM_LAST_OF_TWO: | |
| 42 * return last; | |
| 43 * } | |
| 44 * assertNotReached(); | |
| 45 * return document; | |
| 46 * } | |
| 47 * | |
| 48 * This code should only be hit in the case of serious programmer error or | |
| 49 * unexpected input. | |
| 50 * | |
| 51 * @param {string=} opt_message A message to show when this is hit. | |
| 52 */ | |
| 53 function assertNotReached(opt_message) { | |
| 54 assert(false, opt_message || 'Unreachable code hit'); | |
| 55 } | |
| 56 | |
| 57 /** | |
| 58 * @param {*} value The value to check. | |
| 59 * @param {function(new: T, ...)} type A user-defined constructor. | |
| 60 * @param {string=} opt_message A message to show when this is hit. | |
| 61 * @return {T} | |
| 62 * @template T | |
| 63 */ | |
| 64 function assertInstanceof(value, type, opt_message) { | |
| 65 // We don't use assert immediately here so that we avoid constructing an error | |
| 66 // message if we don't have to. | |
| 67 if (!(value instanceof type)) { | |
| 68 assertNotReached(opt_message || 'Value ' + value + | |
| 69 ' is not a[n] ' + (type.name || typeof type)); | |
| 70 } | |
| 71 return value; | |
| 72 }; | |
| 73 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 74 // 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 |
| 75 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 76 | 4 |
| 77 /** | 5 /** |
| 78 * @fileoverview PromiseResolver is a helper class that allows creating a | 6 * @fileoverview PromiseResolver is a helper class that allows creating a |
| 79 * Promise that will be fulfilled (resolved or rejected) some time later. | 7 * Promise that will be fulfilled (resolved or rejected) some time later. |
| 80 * | 8 * |
| 81 * Example: | 9 * Example: |
| 82 * var resolver = new PromiseResolver(); | 10 * var resolver = new PromiseResolver(); |
| (...skipping 1056 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1139 this.preventDefault(); | 1067 this.preventDefault(); |
| 1140 } | 1068 } |
| 1141 }; | 1069 }; |
| 1142 | 1070 |
| 1143 // Export | 1071 // Export |
| 1144 return { | 1072 return { |
| 1145 Command: Command, | 1073 Command: Command, |
| 1146 CanExecuteEvent: CanExecuteEvent | 1074 CanExecuteEvent: CanExecuteEvent |
| 1147 }; | 1075 }; |
| 1148 }); | 1076 }); |
| 1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1077 Polymer({ |
| 1150 // Use of this source code is governed by a BSD-style license that can be | 1078 is: 'app-drawer', |
| 1151 // found in the LICENSE file. | 1079 |
| 1152 | 1080 properties: { |
| 1153 // <include src="../../../../ui/webui/resources/js/assert.js"> | 1081 /** |
| 1154 | 1082 * The opened state of the drawer. |
| 1155 /** | 1083 */ |
| 1156 * Alias for document.getElementById. Found elements must be HTMLElements. | 1084 opened: { |
| 1157 * @param {string} id The ID of the element to find. | 1085 type: Boolean, |
| 1158 * @return {HTMLElement} The found element or null if not found. | 1086 value: false, |
| 1159 */ | 1087 notify: true, |
| 1160 function $(id) { | 1088 reflectToAttribute: true |
| 1161 var el = document.getElementById(id); | 1089 }, |
| 1162 return el ? assertInstanceof(el, HTMLElement) : null; | 1090 |
| 1163 } | 1091 /** |
| 1164 | 1092 * The drawer does not have a scrim and cannot be swiped close. |
| 1165 // TODO(devlin): This should return SVGElement, but closure compiler is missing | 1093 */ |
| 1166 // those externs. | 1094 persistent: { |
| 1167 /** | 1095 type: Boolean, |
| 1168 * Alias for document.getElementById. Found elements must be SVGElements. | 1096 value: false, |
| 1169 * @param {string} id The ID of the element to find. | 1097 reflectToAttribute: true |
| 1170 * @return {Element} The found element or null if not found. | 1098 }, |
| 1171 */ | 1099 |
| 1172 function getSVGElement(id) { | 1100 /** |
| 1173 var el = document.getElementById(id); | 1101 * The alignment of the drawer on the screen ('left', 'right', 'start' o
r 'end'). |
| 1174 return el ? assertInstanceof(el, Element) : null; | 1102 * 'start' computes to left and 'end' to right in LTR layout and vice ve
rsa in RTL |
| 1175 } | 1103 * layout. |
| 1176 | 1104 */ |
| 1177 /** | 1105 align: { |
| 1178 * Add an accessible message to the page that will be announced to | 1106 type: String, |
| 1179 * users who have spoken feedback on, but will be invisible to all | 1107 value: 'left' |
| 1180 * other users. It's removed right away so it doesn't clutter the DOM. | 1108 }, |
| 1181 * @param {string} msg The text to be pronounced. | 1109 |
| 1182 */ | 1110 /** |
| 1183 function announceAccessibleMessage(msg) { | 1111 * The computed, read-only position of the drawer on the screen ('left'
or 'right'). |
| 1184 var element = document.createElement('div'); | 1112 */ |
| 1185 element.setAttribute('aria-live', 'polite'); | 1113 position: { |
| 1186 element.style.position = 'relative'; | 1114 type: String, |
| 1187 element.style.left = '-9999px'; | 1115 readOnly: true, |
| 1188 element.style.height = '0px'; | 1116 value: 'left', |
| 1189 element.innerText = msg; | 1117 reflectToAttribute: true |
| 1190 document.body.appendChild(element); | 1118 }, |
| 1191 window.setTimeout(function() { | 1119 |
| 1192 document.body.removeChild(element); | 1120 /** |
| 1193 }, 0); | 1121 * Create an area at the edge of the screen to swipe open the drawer. |
| 1194 } | 1122 */ |
| 1195 | 1123 swipeOpen: { |
| 1196 /** | 1124 type: Boolean, |
| 1197 * Generates a CSS url string. | 1125 value: false, |
| 1198 * @param {string} s The URL to generate the CSS url for. | 1126 reflectToAttribute: true |
| 1199 * @return {string} The CSS url string. | 1127 }, |
| 1200 */ | 1128 |
| 1201 function url(s) { | 1129 /** |
| 1202 // http://www.w3.org/TR/css3-values/#uris | 1130 * Trap keyboard focus when the drawer is opened and not persistent. |
| 1203 // Parentheses, commas, whitespace characters, single quotes (') and double | 1131 */ |
| 1204 // quotes (") appearing in a URI must be escaped with a backslash | 1132 noFocusTrap: { |
| 1205 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | 1133 type: Boolean, |
| 1206 // WebKit has a bug when it comes to URLs that end with \ | 1134 value: false |
| 1207 // https://bugs.webkit.org/show_bug.cgi?id=28885 | 1135 } |
| 1208 if (/\\\\$/.test(s2)) { | 1136 }, |
| 1209 // Add a space to work around the WebKit bug. | 1137 |
| 1210 s2 += ' '; | 1138 observers: [ |
| 1211 } | 1139 'resetLayout(position)', |
| 1212 return 'url("' + s2 + '")'; | 1140 '_resetPosition(align, isAttached)' |
| 1213 } | 1141 ], |
| 1214 | 1142 |
| 1215 /** | 1143 _translateOffset: 0, |
| 1216 * Parses query parameters from Location. | 1144 |
| 1217 * @param {Location} location The URL to generate the CSS url for. | 1145 _trackDetails: null, |
| 1218 * @return {Object} Dictionary containing name value pairs for URL | 1146 |
| 1219 */ | 1147 _drawerState: 0, |
| 1220 function parseQueryParams(location) { | 1148 |
| 1221 var params = {}; | 1149 _boundEscKeydownHandler: null, |
| 1222 var query = unescape(location.search.substring(1)); | 1150 |
| 1223 var vars = query.split('&'); | 1151 _firstTabStop: null, |
| 1224 for (var i = 0; i < vars.length; i++) { | 1152 |
| 1225 var pair = vars[i].split('='); | 1153 _lastTabStop: null, |
| 1226 params[pair[0]] = pair[1]; | 1154 |
| 1227 } | 1155 ready: function() { |
| 1228 return params; | 1156 // Set the scroll direction so you can vertically scroll inside the draw
er. |
| 1229 } | 1157 this.setScrollDirection('y'); |
| 1230 | 1158 |
| 1231 /** | 1159 // Only transition the drawer after its first render (e.g. app-drawer-la
yout |
| 1232 * Creates a new URL by appending or replacing the given query key and value. | 1160 // may need to set the initial opened state which should not be transiti
oned). |
| 1233 * Not supporting URL with username and password. | 1161 this._setTransitionDuration('0s'); |
| 1234 * @param {Location} location The original URL. | 1162 }, |
| 1235 * @param {string} key The query parameter name. | 1163 |
| 1236 * @param {string} value The query parameter value. | 1164 attached: function() { |
| 1237 * @return {string} The constructed new URL. | 1165 // Only transition the drawer after its first render (e.g. app-drawer-la
yout |
| 1238 */ | 1166 // may need to set the initial opened state which should not be transiti
oned). |
| 1239 function setQueryParam(location, key, value) { | 1167 Polymer.RenderStatus.afterNextRender(this, function() { |
| 1240 var query = parseQueryParams(location); | 1168 this._setTransitionDuration(''); |
| 1241 query[encodeURIComponent(key)] = encodeURIComponent(value); | 1169 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); |
| 1242 | 1170 this._resetDrawerState(); |
| 1243 var newQuery = ''; | 1171 |
| 1244 for (var q in query) { | 1172 this.listen(this, 'track', '_track'); |
| 1245 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; | 1173 this.addEventListener('transitionend', this._transitionend.bind(this))
; |
| 1246 } | 1174 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) |
| 1247 | 1175 }); |
| 1248 return location.origin + location.pathname + newQuery + location.hash; | 1176 }, |
| 1249 } | 1177 |
| 1250 | 1178 detached: function() { |
| 1251 /** | 1179 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
| 1252 * @param {Node} el A node to search for ancestors with |className|. | 1180 }, |
| 1253 * @param {string} className A class to search for. | 1181 |
| 1254 * @return {Element} A node with class of |className| or null if none is found. | 1182 /** |
| 1255 */ | 1183 * Opens the drawer. |
| 1256 function findAncestorByClass(el, className) { | 1184 */ |
| 1257 return /** @type {Element} */(findAncestor(el, function(el) { | 1185 open: function() { |
| 1258 return el.classList && el.classList.contains(className); | 1186 this.opened = true; |
| 1259 })); | 1187 }, |
| 1260 } | 1188 |
| 1261 | 1189 /** |
| 1262 /** | 1190 * Closes the drawer. |
| 1263 * Return the first ancestor for which the {@code predicate} returns true. | 1191 */ |
| 1264 * @param {Node} node The node to check. | 1192 close: function() { |
| 1265 * @param {function(Node):boolean} predicate The function that tests the | 1193 this.opened = false; |
| 1266 * nodes. | 1194 }, |
| 1267 * @return {Node} The found ancestor or null if not found. | 1195 |
| 1268 */ | 1196 /** |
| 1269 function findAncestor(node, predicate) { | 1197 * Toggles the drawer open and close. |
| 1270 var last = false; | 1198 */ |
| 1271 while (node != null && !(last = predicate(node))) { | 1199 toggle: function() { |
| 1272 node = node.parentNode; | 1200 this.opened = !this.opened; |
| 1273 } | 1201 }, |
| 1274 return last ? node : null; | 1202 |
| 1275 } | 1203 /** |
| 1276 | 1204 * Gets the width of the drawer. |
| 1277 function swapDomNodes(a, b) { | 1205 * |
| 1278 var afterA = a.nextSibling; | 1206 * @return {number} The width of the drawer in pixels. |
| 1279 if (afterA == b) { | 1207 */ |
| 1280 swapDomNodes(b, a); | 1208 getWidth: function() { |
| 1281 return; | 1209 return this.$.contentContainer.offsetWidth; |
| 1282 } | 1210 }, |
| 1283 var aParent = a.parentNode; | 1211 |
| 1284 b.parentNode.replaceChild(a, b); | 1212 /** |
| 1285 aParent.insertBefore(b, afterA); | 1213 * Resets the layout. If you changed the size of app-header via CSS |
| 1286 } | 1214 * you can notify the changes by either firing the `iron-resize` event |
| 1287 | 1215 * or calling `resetLayout` directly. |
| 1288 /** | 1216 * |
| 1289 * Disables text selection and dragging, with optional whitelist callbacks. | 1217 * @method resetLayout |
| 1290 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function | 1218 */ |
| 1291 * is defined and returns true, the onselectionstart event will be | 1219 resetLayout: function() { |
| 1292 * surpressed. | 1220 this.debounce('_resetLayout', function() { |
| 1293 * @param {function(Event):boolean=} opt_allowDragStart Unless this function | 1221 this.fire('app-drawer-reset-layout'); |
| 1294 * is defined and returns true, the ondragstart event will be surpressed. | 1222 }, 1); |
| 1295 */ | 1223 }, |
| 1296 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { | 1224 |
| 1297 // Disable text selection. | 1225 _isRTL: function() { |
| 1298 document.onselectstart = function(e) { | 1226 return window.getComputedStyle(this).direction === 'rtl'; |
| 1299 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) | 1227 }, |
| 1300 e.preventDefault(); | 1228 |
| 1301 }; | 1229 _resetPosition: function() { |
| 1302 | 1230 switch (this.align) { |
| 1303 // Disable dragging. | 1231 case 'start': |
| 1304 document.ondragstart = function(e) { | 1232 this._setPosition(this._isRTL() ? 'right' : 'left'); |
| 1305 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) | 1233 return; |
| 1306 e.preventDefault(); | 1234 case 'end': |
| 1307 }; | 1235 this._setPosition(this._isRTL() ? 'left' : 'right'); |
| 1308 } | 1236 return; |
| 1309 | 1237 } |
| 1310 /** | 1238 this._setPosition(this.align); |
| 1311 * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead. | 1239 }, |
| 1312 * Call this to stop clicks on <a href="#"> links from scrolling to the top of | 1240 |
| 1313 * the page (and possibly showing a # in the link). | 1241 _escKeydownHandler: function(event) { |
| 1314 */ | 1242 var ESC_KEYCODE = 27; |
| 1315 function preventDefaultOnPoundLinkClicks() { | 1243 if (event.keyCode === ESC_KEYCODE) { |
| 1316 document.addEventListener('click', function(e) { | 1244 // Prevent any side effects if app-drawer closes. |
| 1317 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { | 1245 event.preventDefault(); |
| 1318 return el.tagName == 'A'; | 1246 this.close(); |
| 1247 } |
| 1248 }, |
| 1249 |
| 1250 _track: function(event) { |
| 1251 if (this.persistent) { |
| 1252 return; |
| 1253 } |
| 1254 |
| 1255 // Disable user selection on desktop. |
| 1256 event.preventDefault(); |
| 1257 |
| 1258 switch (event.detail.state) { |
| 1259 case 'start': |
| 1260 this._trackStart(event); |
| 1261 break; |
| 1262 case 'track': |
| 1263 this._trackMove(event); |
| 1264 break; |
| 1265 case 'end': |
| 1266 this._trackEnd(event); |
| 1267 break; |
| 1268 } |
| 1269 }, |
| 1270 |
| 1271 _trackStart: function(event) { |
| 1272 this._drawerState = this._DRAWER_STATE.TRACKING; |
| 1273 |
| 1274 // Disable transitions since style attributes will reflect user track ev
ents. |
| 1275 this._setTransitionDuration('0s'); |
| 1276 this.style.visibility = 'visible'; |
| 1277 |
| 1278 var rect = this.$.contentContainer.getBoundingClientRect(); |
| 1279 if (this.position === 'left') { |
| 1280 this._translateOffset = rect.left; |
| 1281 } else { |
| 1282 this._translateOffset = rect.right - window.innerWidth; |
| 1283 } |
| 1284 |
| 1285 this._trackDetails = []; |
| 1286 }, |
| 1287 |
| 1288 _trackMove: function(event) { |
| 1289 this._translateDrawer(event.detail.dx + this._translateOffset); |
| 1290 |
| 1291 // Use Date.now() since event.timeStamp is inconsistent across browsers
(e.g. most |
| 1292 // browsers use milliseconds but FF 44 uses microseconds). |
| 1293 this._trackDetails.push({ |
| 1294 dx: event.detail.dx, |
| 1295 timeStamp: Date.now() |
| 1296 }); |
| 1297 }, |
| 1298 |
| 1299 _trackEnd: function(event) { |
| 1300 var x = event.detail.dx + this._translateOffset; |
| 1301 var drawerWidth = this.getWidth(); |
| 1302 var isPositionLeft = this.position === 'left'; |
| 1303 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : |
| 1304 (x <= 0 || x >= drawerWidth); |
| 1305 |
| 1306 if (!isInEndState) { |
| 1307 // No longer need the track events after this method returns - allow t
hem to be GC'd. |
| 1308 var trackDetails = this._trackDetails; |
| 1309 this._trackDetails = null; |
| 1310 |
| 1311 this._flingDrawer(event, trackDetails); |
| 1312 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 1313 return; |
| 1314 } |
| 1315 } |
| 1316 |
| 1317 // If the drawer is not flinging, toggle the opened state based on the p
osition of |
| 1318 // the drawer. |
| 1319 var halfWidth = drawerWidth / 2; |
| 1320 if (event.detail.dx < -halfWidth) { |
| 1321 this.opened = this.position === 'right'; |
| 1322 } else if (event.detail.dx > halfWidth) { |
| 1323 this.opened = this.position === 'left'; |
| 1324 } |
| 1325 |
| 1326 // Trigger app-drawer-transitioned now since there will be no transition
end event. |
| 1327 if (isInEndState) { |
| 1328 this._resetDrawerState(); |
| 1329 } |
| 1330 |
| 1331 this._setTransitionDuration(''); |
| 1332 this._resetDrawerTranslate(); |
| 1333 this.style.visibility = ''; |
| 1334 }, |
| 1335 |
| 1336 _calculateVelocity: function(event, trackDetails) { |
| 1337 // Find the oldest track event that is within 100ms using binary search. |
| 1338 var now = Date.now(); |
| 1339 var timeLowerBound = now - 100; |
| 1340 var trackDetail; |
| 1341 var min = 0; |
| 1342 var max = trackDetails.length - 1; |
| 1343 |
| 1344 while (min <= max) { |
| 1345 // Floor of average of min and max. |
| 1346 var mid = (min + max) >> 1; |
| 1347 var d = trackDetails[mid]; |
| 1348 if (d.timeStamp >= timeLowerBound) { |
| 1349 trackDetail = d; |
| 1350 max = mid - 1; |
| 1351 } else { |
| 1352 min = mid + 1; |
| 1353 } |
| 1354 } |
| 1355 |
| 1356 if (trackDetail) { |
| 1357 var dx = event.detail.dx - trackDetail.dx; |
| 1358 var dt = (now - trackDetail.timeStamp) || 1; |
| 1359 return dx / dt; |
| 1360 } |
| 1361 return 0; |
| 1362 }, |
| 1363 |
| 1364 _flingDrawer: function(event, trackDetails) { |
| 1365 var velocity = this._calculateVelocity(event, trackDetails); |
| 1366 |
| 1367 // Do not fling if velocity is not above a threshold. |
| 1368 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { |
| 1369 return; |
| 1370 } |
| 1371 |
| 1372 this._drawerState = this._DRAWER_STATE.FLINGING; |
| 1373 |
| 1374 var x = event.detail.dx + this._translateOffset; |
| 1375 var drawerWidth = this.getWidth(); |
| 1376 var isPositionLeft = this.position === 'left'; |
| 1377 var isVelocityPositive = velocity > 0; |
| 1378 var isClosingLeft = !isVelocityPositive && isPositionLeft; |
| 1379 var isClosingRight = isVelocityPositive && !isPositionLeft; |
| 1380 var dx; |
| 1381 if (isClosingLeft) { |
| 1382 dx = -(x + drawerWidth); |
| 1383 } else if (isClosingRight) { |
| 1384 dx = (drawerWidth - x); |
| 1385 } else { |
| 1386 dx = -x; |
| 1387 } |
| 1388 |
| 1389 // Enforce a minimum transition velocity to make the drawer feel snappy. |
| 1390 if (isVelocityPositive) { |
| 1391 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); |
| 1392 this.opened = this.position === 'left'; |
| 1393 } else { |
| 1394 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); |
| 1395 this.opened = this.position === 'right'; |
| 1396 } |
| 1397 |
| 1398 // Calculate the amount of time needed to finish the transition based on
the |
| 1399 // initial slope of the timing function. |
| 1400 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity)
+ 'ms'); |
| 1401 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); |
| 1402 |
| 1403 this._resetDrawerTranslate(); |
| 1404 }, |
| 1405 |
| 1406 _transitionend: function(event) { |
| 1407 // contentContainer will transition on opened state changed, and scrim w
ill |
| 1408 // transition on persistent state changed when opened - these are the |
| 1409 // transitions we are interested in. |
| 1410 var target = Polymer.dom(event).rootTarget; |
| 1411 if (target === this.$.contentContainer || target === this.$.scrim) { |
| 1412 |
| 1413 // If the drawer was flinging, we need to reset the style attributes. |
| 1414 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 1415 this._setTransitionDuration(''); |
| 1416 this._setTransitionTimingFunction(''); |
| 1417 this.style.visibility = ''; |
| 1418 } |
| 1419 |
| 1420 this._resetDrawerState(); |
| 1421 } |
| 1422 }, |
| 1423 |
| 1424 _setTransitionDuration: function(duration) { |
| 1425 this.$.contentContainer.style.transitionDuration = duration; |
| 1426 this.$.scrim.style.transitionDuration = duration; |
| 1427 }, |
| 1428 |
| 1429 _setTransitionTimingFunction: function(timingFunction) { |
| 1430 this.$.contentContainer.style.transitionTimingFunction = timingFunction; |
| 1431 this.$.scrim.style.transitionTimingFunction = timingFunction; |
| 1432 }, |
| 1433 |
| 1434 _translateDrawer: function(x) { |
| 1435 var drawerWidth = this.getWidth(); |
| 1436 |
| 1437 if (this.position === 'left') { |
| 1438 x = Math.max(-drawerWidth, Math.min(x, 0)); |
| 1439 this.$.scrim.style.opacity = 1 + x / drawerWidth; |
| 1440 } else { |
| 1441 x = Math.max(0, Math.min(x, drawerWidth)); |
| 1442 this.$.scrim.style.opacity = 1 - x / drawerWidth; |
| 1443 } |
| 1444 |
| 1445 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); |
| 1446 }, |
| 1447 |
| 1448 _resetDrawerTranslate: function() { |
| 1449 this.$.scrim.style.opacity = ''; |
| 1450 this.transform('', this.$.contentContainer); |
| 1451 }, |
| 1452 |
| 1453 _resetDrawerState: function() { |
| 1454 var oldState = this._drawerState; |
| 1455 if (this.opened) { |
| 1456 this._drawerState = this.persistent ? |
| 1457 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED; |
| 1458 } else { |
| 1459 this._drawerState = this._DRAWER_STATE.CLOSED; |
| 1460 } |
| 1461 |
| 1462 if (oldState !== this._drawerState) { |
| 1463 if (this._drawerState === this._DRAWER_STATE.OPENED) { |
| 1464 this._setKeyboardFocusTrap(); |
| 1465 document.addEventListener('keydown', this._boundEscKeydownHandler); |
| 1466 document.body.style.overflow = 'hidden'; |
| 1467 } else { |
| 1468 document.removeEventListener('keydown', this._boundEscKeydownHandler
); |
| 1469 document.body.style.overflow = ''; |
| 1470 } |
| 1471 |
| 1472 // Don't fire the event on initial load. |
| 1473 if (oldState !== this._DRAWER_STATE.INIT) { |
| 1474 this.fire('app-drawer-transitioned'); |
| 1475 } |
| 1476 } |
| 1477 }, |
| 1478 |
| 1479 _setKeyboardFocusTrap: function() { |
| 1480 if (this.noFocusTrap) { |
| 1481 return; |
| 1482 } |
| 1483 |
| 1484 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated)
, this will |
| 1485 // not select focusable elements inside shadow roots. |
| 1486 var focusableElementsSelector = [ |
| 1487 'a[href]:not([tabindex="-1"])', |
| 1488 'area[href]:not([tabindex="-1"])', |
| 1489 'input:not([disabled]):not([tabindex="-1"])', |
| 1490 'select:not([disabled]):not([tabindex="-1"])', |
| 1491 'textarea:not([disabled]):not([tabindex="-1"])', |
| 1492 'button:not([disabled]):not([tabindex="-1"])', |
| 1493 'iframe:not([tabindex="-1"])', |
| 1494 '[tabindex]:not([tabindex="-1"])', |
| 1495 '[contentEditable=true]:not([tabindex="-1"])' |
| 1496 ].join(','); |
| 1497 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem
entsSelector); |
| 1498 |
| 1499 if (focusableElements.length > 0) { |
| 1500 this._firstTabStop = focusableElements[0]; |
| 1501 this._lastTabStop = focusableElements[focusableElements.length - 1]; |
| 1502 } else { |
| 1503 // Reset saved tab stops when there are no focusable elements in the d
rawer. |
| 1504 this._firstTabStop = null; |
| 1505 this._lastTabStop = null; |
| 1506 } |
| 1507 |
| 1508 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the
first focusable |
| 1509 // element in the drawer, if it exists. Use the tabindex attribute since
the this.tabIndex |
| 1510 // property in IE/Edge returns 0 (instead of -1) when the attribute is n
ot set. |
| 1511 var tabindex = this.getAttribute('tabindex'); |
| 1512 if (tabindex && parseInt(tabindex, 10) > -1) { |
| 1513 this.focus(); |
| 1514 } else if (this._firstTabStop) { |
| 1515 this._firstTabStop.focus(); |
| 1516 } |
| 1517 }, |
| 1518 |
| 1519 _tabKeydownHandler: function(event) { |
| 1520 if (this.noFocusTrap) { |
| 1521 return; |
| 1522 } |
| 1523 |
| 1524 var TAB_KEYCODE = 9; |
| 1525 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode ===
TAB_KEYCODE) { |
| 1526 if (event.shiftKey) { |
| 1527 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f
irstTabStop) { |
| 1528 event.preventDefault(); |
| 1529 this._lastTabStop.focus(); |
| 1530 } |
| 1531 } else { |
| 1532 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la
stTabStop) { |
| 1533 event.preventDefault(); |
| 1534 this._firstTabStop.focus(); |
| 1535 } |
| 1536 } |
| 1537 } |
| 1538 }, |
| 1539 |
| 1540 _MIN_FLING_THRESHOLD: 0.2, |
| 1541 |
| 1542 _MIN_TRANSITION_VELOCITY: 1.2, |
| 1543 |
| 1544 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', |
| 1545 |
| 1546 _FLING_INITIAL_SLOPE: 1.5, |
| 1547 |
| 1548 _DRAWER_STATE: { |
| 1549 INIT: 0, |
| 1550 OPENED: 1, |
| 1551 OPENED_PERSISTENT: 2, |
| 1552 CLOSED: 3, |
| 1553 TRACKING: 4, |
| 1554 FLINGING: 5 |
| 1555 } |
| 1556 |
| 1557 /** |
| 1558 * Fired when the layout of app-drawer has changed. |
| 1559 * |
| 1560 * @event app-drawer-reset-layout |
| 1561 */ |
| 1562 |
| 1563 /** |
| 1564 * Fired when app-drawer has finished transitioning. |
| 1565 * |
| 1566 * @event app-drawer-transitioned |
| 1567 */ |
| 1319 }); | 1568 }); |
| 1320 // Use getAttribute() to prevent URL normalization. | 1569 (function() { |
| 1321 if (anchor && anchor.getAttribute('href') == '#') | 1570 'use strict'; |
| 1322 e.preventDefault(); | 1571 |
| 1323 }); | 1572 Polymer({ |
| 1324 } | 1573 is: 'iron-location', |
| 1325 | 1574 properties: { |
| 1326 /** | 1575 /** |
| 1327 * Check the directionality of the page. | 1576 * The pathname component of the URL. |
| 1328 * @return {boolean} True if Chrome is running an RTL UI. | 1577 */ |
| 1329 */ | 1578 path: { |
| 1330 function isRTL() { | 1579 type: String, |
| 1331 return document.documentElement.dir == 'rtl'; | 1580 notify: true, |
| 1332 } | 1581 value: function() { |
| 1333 | 1582 return window.decodeURIComponent(window.location.pathname); |
| 1334 /** | 1583 } |
| 1335 * Get an element that's known to exist by its ID. We use this instead of just | 1584 }, |
| 1336 * calling getElementById and not checking the result because this lets us | 1585 /** |
| 1337 * satisfy the JSCompiler type system. | 1586 * The query string portion of the URL. |
| 1338 * @param {string} id The identifier name. | 1587 */ |
| 1339 * @return {!HTMLElement} the Element. | 1588 query: { |
| 1340 */ | 1589 type: String, |
| 1341 function getRequiredElement(id) { | 1590 notify: true, |
| 1342 return assertInstanceof($(id), HTMLElement, | 1591 value: function() { |
| 1343 'Missing required element: ' + id); | 1592 return window.decodeURIComponent(window.location.search.slice(1)); |
| 1344 } | 1593 } |
| 1345 | 1594 }, |
| 1346 /** | 1595 /** |
| 1347 * Query an element that's known to exist by a selector. We use this instead of | 1596 * The hash component of the URL. |
| 1348 * just calling querySelector and not checking the result because this lets us | 1597 */ |
| 1349 * satisfy the JSCompiler type system. | 1598 hash: { |
| 1350 * @param {string} selectors CSS selectors to query the element. | 1599 type: String, |
| 1351 * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional | 1600 notify: true, |
| 1352 * context object for querySelector. | 1601 value: function() { |
| 1353 * @return {!HTMLElement} the Element. | 1602 return window.decodeURIComponent(window.location.hash.slice(1)); |
| 1354 */ | 1603 } |
| 1355 function queryRequiredElement(selectors, opt_context) { | 1604 }, |
| 1356 var element = (opt_context || document).querySelector(selectors); | 1605 /** |
| 1357 return assertInstanceof(element, HTMLElement, | 1606 * If the user was on a URL for less than `dwellTime` milliseconds, it |
| 1358 'Missing required element: ' + selectors); | 1607 * won't be added to the browser's history, but instead will be replaced |
| 1359 } | 1608 * by the next entry. |
| 1360 | 1609 * |
| 1361 // Handle click on a link. If the link points to a chrome: or file: url, then | 1610 * This is to prevent large numbers of entries from clogging up the user
's |
| 1362 // call into the browser to do the navigation. | 1611 * browser history. Disable by setting to a negative number. |
| 1363 document.addEventListener('click', function(e) { | 1612 */ |
| 1364 if (e.defaultPrevented) | 1613 dwellTime: { |
| 1365 return; | 1614 type: Number, |
| 1366 | 1615 value: 2000 |
| 1367 var el = e.target; | 1616 }, |
| 1368 if (el.nodeType == Node.ELEMENT_NODE && | 1617 |
| 1369 el.webkitMatchesSelector('A, A *')) { | 1618 /** |
| 1370 while (el.tagName != 'A') { | 1619 * A regexp that defines the set of URLs that should be considered part |
| 1371 el = el.parentElement; | 1620 * of this web app. |
| 1372 } | 1621 * |
| 1373 | 1622 * Clicking on a link that matches this regex won't result in a full pag
e |
| 1374 if ((el.protocol == 'file:' || el.protocol == 'about:') && | 1623 * navigation, but will instead just update the URL state in place. |
| 1375 (e.button == 0 || e.button == 1)) { | 1624 * |
| 1376 chrome.send('navigateToUrl', [ | 1625 * This regexp is given everything after the origin in an absolute |
| 1377 el.href, | 1626 * URL. So to match just URLs that start with /search/ do: |
| 1378 el.target, | 1627 * url-space-regex="^/search/" |
| 1379 e.button, | 1628 * |
| 1380 e.altKey, | 1629 * @type {string|RegExp} |
| 1381 e.ctrlKey, | 1630 */ |
| 1382 e.metaKey, | 1631 urlSpaceRegex: { |
| 1383 e.shiftKey | 1632 type: String, |
| 1384 ]); | 1633 value: '' |
| 1385 e.preventDefault(); | 1634 }, |
| 1386 } | 1635 |
| 1387 } | 1636 /** |
| 1388 }); | 1637 * urlSpaceRegex, but coerced into a regexp. |
| 1389 | 1638 * |
| 1390 /** | 1639 * @type {RegExp} |
| 1391 * Creates a new URL which is the old URL with a GET param of key=value. | 1640 */ |
| 1392 * @param {string} url The base URL. There is not sanity checking on the URL so | 1641 _urlSpaceRegExp: { |
| 1393 * it must be passed in a proper format. | 1642 computed: '_makeRegExp(urlSpaceRegex)' |
| 1394 * @param {string} key The key of the param. | 1643 }, |
| 1395 * @param {string} value The value of the param. | 1644 |
| 1396 * @return {string} The new URL. | 1645 _lastChangedAt: { |
| 1397 */ | 1646 type: Number |
| 1398 function appendParam(url, key, value) { | 1647 }, |
| 1399 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 1648 |
| 1400 | 1649 _initialized: { |
| 1401 if (url.indexOf('?') == -1) | 1650 type: Boolean, |
| 1402 return url + '?' + param; | 1651 value: false |
| 1403 return url + '&' + param; | 1652 } |
| 1404 } | 1653 }, |
| 1405 | 1654 hostAttributes: { |
| 1406 /** | 1655 hidden: true |
| 1407 * Creates an element of a specified type with a specified class name. | 1656 }, |
| 1408 * @param {string} type The node type. | 1657 observers: [ |
| 1409 * @param {string} className The class name to use. | 1658 '_updateUrl(path, query, hash)' |
| 1410 * @return {Element} The created element. | 1659 ], |
| 1411 */ | 1660 attached: function() { |
| 1412 function createElementWithClassName(type, className) { | 1661 this.listen(window, 'hashchange', '_hashChanged'); |
| 1413 var elm = document.createElement(type); | 1662 this.listen(window, 'location-changed', '_urlChanged'); |
| 1414 elm.className = className; | 1663 this.listen(window, 'popstate', '_urlChanged'); |
| 1415 return elm; | 1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g
lobalOnClick'); |
| 1416 } | 1665 // Give a 200ms grace period to make initial redirects without any |
| 1417 | 1666 // additions to the user's history. |
| 1418 /** | 1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); |
| 1419 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | 1668 |
| 1420 * or when no paint happens during the animation). This function sets up | 1669 this._initialized = true; |
| 1421 * a timer and emulate the event if it is not fired when the timer expires. | 1670 this._urlChanged(); |
| 1422 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | 1671 }, |
| 1423 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | 1672 detached: function() { |
| 1424 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | 1673 this.unlisten(window, 'hashchange', '_hashChanged'); |
| 1425 * using the transitionDuration style value. | 1674 this.unlisten(window, 'location-changed', '_urlChanged'); |
| 1426 */ | 1675 this.unlisten(window, 'popstate', '_urlChanged'); |
| 1427 function ensureTransitionEndEvent(el, opt_timeOut) { | 1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '
_globalOnClick'); |
| 1428 if (opt_timeOut === undefined) { | 1677 this._initialized = false; |
| 1429 var style = getComputedStyle(el); | 1678 }, |
| 1430 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 1679 _hashChanged: function() { |
| 1431 | 1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1))
; |
| 1432 // Give an additional 50ms buffer for the animation to complete. | 1681 }, |
| 1433 opt_timeOut += 50; | 1682 _urlChanged: function() { |
| 1434 } | 1683 // We want to extract all info out of the updated URL before we |
| 1435 | 1684 // try to write anything back into it. |
| 1436 var fired = false; | 1685 // |
| 1437 el.addEventListener('webkitTransitionEnd', function f(e) { | 1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old |
| 1438 el.removeEventListener('webkitTransitionEnd', f); | 1687 // one when we set this.hash. Likewise for query. |
| 1439 fired = true; | 1688 this._dontUpdateUrl = true; |
| 1440 }); | 1689 this._hashChanged(); |
| 1441 window.setTimeout(function() { | 1690 this.path = window.decodeURIComponent(window.location.pathname); |
| 1442 if (!fired) | 1691 this.query = window.decodeURIComponent( |
| 1443 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | 1692 window.location.search.substring(1)); |
| 1444 }, opt_timeOut); | 1693 this._dontUpdateUrl = false; |
| 1445 } | 1694 this._updateUrl(); |
| 1446 | 1695 }, |
| 1447 /** | 1696 _getUrl: function() { |
| 1448 * Alias for document.scrollTop getter. | 1697 var partiallyEncodedPath = window.encodeURI( |
| 1449 * @param {!HTMLDocument} doc The document node where information will be | 1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F'); |
| 1450 * queried from. | 1699 var partiallyEncodedQuery = ''; |
| 1451 * @return {number} The Y document scroll offset. | 1700 if (this.query) { |
| 1452 */ | 1701 partiallyEncodedQuery = '?' + window.encodeURI( |
| 1453 function scrollTopForDocument(doc) { | 1702 this.query).replace(/\#/g, '%23'); |
| 1454 return doc.documentElement.scrollTop || doc.body.scrollTop; | 1703 } |
| 1455 } | 1704 var partiallyEncodedHash = ''; |
| 1456 | 1705 if (this.hash) { |
| 1457 /** | 1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash); |
| 1458 * Alias for document.scrollTop setter. | 1707 } |
| 1459 * @param {!HTMLDocument} doc The document node where information will be | 1708 return ( |
| 1460 * queried from. | 1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash)
; |
| 1461 * @param {number} value The target Y scroll offset. | 1710 }, |
| 1462 */ | 1711 _updateUrl: function() { |
| 1463 function setScrollTopForDocument(doc, value) { | 1712 if (this._dontUpdateUrl || !this._initialized) { |
| 1464 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 1713 return; |
| 1465 } | 1714 } |
| 1466 | 1715 if (this.path === window.decodeURIComponent(window.location.pathname) && |
| 1467 /** | 1716 this.query === window.decodeURIComponent( |
| 1468 * Alias for document.scrollLeft getter. | 1717 window.location.search.substring(1)) && |
| 1469 * @param {!HTMLDocument} doc The document node where information will be | 1718 this.hash === window.decodeURIComponent( |
| 1470 * queried from. | 1719 window.location.hash.substring(1))) { |
| 1471 * @return {number} The X document scroll offset. | 1720 // Nothing to do, the current URL is a representation of our propertie
s. |
| 1472 */ | 1721 return; |
| 1473 function scrollLeftForDocument(doc) { | 1722 } |
| 1474 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 1723 var newUrl = this._getUrl(); |
| 1475 } | 1724 // Need to use a full URL in case the containing page has a base URI. |
| 1476 | 1725 var fullNewUrl = new URL( |
| 1477 /** | 1726 newUrl, window.location.protocol + '//' + window.location.host).href
; |
| 1478 * Alias for document.scrollLeft setter. | 1727 var now = window.performance.now(); |
| 1479 * @param {!HTMLDocument} doc The document node where information will be | 1728 var shouldReplace = |
| 1480 * queried from. | 1729 this._lastChangedAt + this.dwellTime > now; |
| 1481 * @param {number} value The target X scroll offset. | 1730 this._lastChangedAt = now; |
| 1482 */ | 1731 if (shouldReplace) { |
| 1483 function setScrollLeftForDocument(doc, value) { | 1732 window.history.replaceState({}, '', fullNewUrl); |
| 1484 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 1733 } else { |
| 1485 } | 1734 window.history.pushState({}, '', fullNewUrl); |
| 1486 | 1735 } |
| 1487 /** | 1736 this.fire('location-changed', {}, {node: window}); |
| 1488 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | 1737 }, |
| 1489 * @param {string} original The original string. | 1738 /** |
| 1490 * @return {string} The string with all the characters mentioned above replaced. | 1739 * A necessary evil so that links work as expected. Does its best to |
| 1491 */ | 1740 * bail out early if possible. |
| 1492 function HTMLEscape(original) { | 1741 * |
| 1493 return original.replace(/&/g, '&') | 1742 * @param {MouseEvent} event . |
| 1494 .replace(/</g, '<') | 1743 */ |
| 1495 .replace(/>/g, '>') | 1744 _globalOnClick: function(event) { |
| 1496 .replace(/"/g, '"') | 1745 // If another event handler has stopped this event then there's nothing |
| 1497 .replace(/'/g, '''); | 1746 // for us to do. This can happen e.g. when there are multiple |
| 1498 } | 1747 // iron-location elements in a page. |
| 1499 | 1748 if (event.defaultPrevented) { |
| 1500 /** | 1749 return; |
| 1501 * Shortens the provided string (if necessary) to a string of length at most | 1750 } |
| 1502 * |maxLength|. | 1751 var href = this._getSameOriginLinkHref(event); |
| 1503 * @param {string} original The original string. | 1752 if (!href) { |
| 1504 * @param {number} maxLength The maximum length allowed for the string. | 1753 return; |
| 1505 * @return {string} The original string if its length does not exceed | 1754 } |
| 1506 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | 1755 event.preventDefault(); |
| 1507 * appended. | 1756 // If the navigation is to the current page we shouldn't add a history |
| 1508 */ | 1757 // entry or fire a change event. |
| 1509 function elide(original, maxLength) { | 1758 if (href === window.location.href) { |
| 1510 if (original.length <= maxLength) | 1759 return; |
| 1511 return original; | 1760 } |
| 1512 return original.substring(0, maxLength - 1) + '\u2026'; | 1761 window.history.pushState({}, '', href); |
| 1513 } | 1762 this.fire('location-changed', {}, {node: window}); |
| 1514 | 1763 }, |
| 1515 /** | 1764 /** |
| 1516 * Quote a string so it can be used in a regular expression. | 1765 * Returns the absolute URL of the link (if any) that this click event |
| 1517 * @param {string} str The source string. | 1766 * is clicking on, if we can and should override the resulting full |
| 1518 * @return {string} The escaped string. | 1767 * page navigation. Returns null otherwise. |
| 1519 */ | 1768 * |
| 1520 function quoteString(str) { | 1769 * @param {MouseEvent} event . |
| 1521 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 1770 * @return {string?} . |
| 1522 } | 1771 */ |
| 1523 | 1772 _getSameOriginLinkHref: function(event) { |
| 1524 // <if expr="is_ios"> | 1773 // We only care about left-clicks. |
| 1525 // Polyfill 'key' in KeyboardEvent for iOS. | 1774 if (event.button !== 0) { |
| 1526 // This function is not intended to be complete but should | 1775 return null; |
| 1527 // be sufficient enough to have iOS work correctly while | 1776 } |
| 1528 // it does not support key yet. | 1777 // We don't want modified clicks, where the intent is to open the page |
| 1529 if (!('key' in KeyboardEvent.prototype)) { | 1778 // in a new tab. |
| 1530 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 1779 if (event.metaKey || event.ctrlKey) { |
| 1531 /** @this {KeyboardEvent} */ | 1780 return null; |
| 1532 get: function () { | 1781 } |
| 1533 // 0-9 | 1782 var eventPath = Polymer.dom(event).path; |
| 1534 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | 1783 var anchor = null; |
| 1535 return String.fromCharCode(this.keyCode); | 1784 for (var i = 0; i < eventPath.length; i++) { |
| 1536 | 1785 var element = eventPath[i]; |
| 1537 // A-Z | 1786 if (element.tagName === 'A' && element.href) { |
| 1538 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | 1787 anchor = element; |
| 1539 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 1788 break; |
| 1540 if (this.shiftKey) | 1789 } |
| 1541 result = result.toUpperCase(); | 1790 } |
| 1542 return result; | 1791 |
| 1543 } | 1792 // If there's no link there's nothing to do. |
| 1544 | 1793 if (!anchor) { |
| 1545 // Special characters | 1794 return null; |
| 1546 switch(this.keyCode) { | 1795 } |
| 1547 case 0x08: return 'Backspace'; | 1796 |
| 1548 case 0x09: return 'Tab'; | 1797 // Target blank is a new tab, don't intercept. |
| 1549 case 0x0d: return 'Enter'; | 1798 if (anchor.target === '_blank') { |
| 1550 case 0x10: return 'Shift'; | 1799 return null; |
| 1551 case 0x11: return 'Control'; | 1800 } |
| 1552 case 0x12: return 'Alt'; | 1801 // If the link is for an existing parent frame, don't intercept. |
| 1553 case 0x1b: return 'Escape'; | 1802 if ((anchor.target === '_top' || |
| 1554 case 0x20: return ' '; | 1803 anchor.target === '_parent') && |
| 1555 case 0x21: return 'PageUp'; | 1804 window.top !== window) { |
| 1556 case 0x22: return 'PageDown'; | 1805 return null; |
| 1557 case 0x23: return 'End'; | 1806 } |
| 1558 case 0x24: return 'Home'; | 1807 |
| 1559 case 0x25: return 'ArrowLeft'; | 1808 var href = anchor.href; |
| 1560 case 0x26: return 'ArrowUp'; | 1809 |
| 1561 case 0x27: return 'ArrowRight'; | 1810 // It only makes sense for us to intercept same-origin navigations. |
| 1562 case 0x28: return 'ArrowDown'; | 1811 // pushState/replaceState don't work with cross-origin links. |
| 1563 case 0x2d: return 'Insert'; | 1812 var url; |
| 1564 case 0x2e: return 'Delete'; | 1813 if (document.baseURI != null) { |
| 1565 case 0x5b: return 'Meta'; | 1814 url = new URL(href, /** @type {string} */(document.baseURI)); |
| 1566 case 0x70: return 'F1'; | 1815 } else { |
| 1567 case 0x71: return 'F2'; | 1816 url = new URL(href); |
| 1568 case 0x72: return 'F3'; | 1817 } |
| 1569 case 0x73: return 'F4'; | 1818 |
| 1570 case 0x74: return 'F5'; | 1819 var origin; |
| 1571 case 0x75: return 'F6'; | 1820 |
| 1572 case 0x76: return 'F7'; | 1821 // IE Polyfill |
| 1573 case 0x77: return 'F8'; | 1822 if (window.location.origin) { |
| 1574 case 0x78: return 'F9'; | 1823 origin = window.location.origin; |
| 1575 case 0x79: return 'F10'; | 1824 } else { |
| 1576 case 0x7a: return 'F11'; | 1825 origin = window.location.protocol + '//' + window.location.hostname; |
| 1577 case 0x7b: return 'F12'; | 1826 |
| 1578 case 0xbb: return '='; | 1827 if (window.location.port) { |
| 1579 case 0xbd: return '-'; | 1828 origin += ':' + window.location.port; |
| 1580 case 0xdb: return '['; | 1829 } |
| 1581 case 0xdd: return ']'; | 1830 } |
| 1582 } | 1831 |
| 1583 return 'Unidentified'; | 1832 if (url.origin !== origin) { |
| 1833 return null; |
| 1834 } |
| 1835 var normalizedHref = url.pathname + url.search + url.hash; |
| 1836 |
| 1837 // If we've been configured not to handle this url... don't handle it! |
| 1838 if (this._urlSpaceRegExp && |
| 1839 !this._urlSpaceRegExp.test(normalizedHref)) { |
| 1840 return null; |
| 1841 } |
| 1842 // Need to use a full URL in case the containing page has a base URI. |
| 1843 var fullNormalizedHref = new URL( |
| 1844 normalizedHref, window.location.href).href; |
| 1845 return fullNormalizedHref; |
| 1846 }, |
| 1847 _makeRegExp: function(urlSpaceRegex) { |
| 1848 return RegExp(urlSpaceRegex); |
| 1849 } |
| 1850 }); |
| 1851 })(); |
| 1852 'use strict'; |
| 1853 |
| 1854 Polymer({ |
| 1855 is: 'iron-query-params', |
| 1856 properties: { |
| 1857 paramsString: { |
| 1858 type: String, |
| 1859 notify: true, |
| 1860 observer: 'paramsStringChanged', |
| 1861 }, |
| 1862 paramsObject: { |
| 1863 type: Object, |
| 1864 notify: true, |
| 1865 value: function() { |
| 1866 return {}; |
| 1867 } |
| 1868 }, |
| 1869 _dontReact: { |
| 1870 type: Boolean, |
| 1871 value: false |
| 1872 } |
| 1873 }, |
| 1874 hostAttributes: { |
| 1875 hidden: true |
| 1876 }, |
| 1877 observers: [ |
| 1878 'paramsObjectChanged(paramsObject.*)' |
| 1879 ], |
| 1880 paramsStringChanged: function() { |
| 1881 this._dontReact = true; |
| 1882 this.paramsObject = this._decodeParams(this.paramsString); |
| 1883 this._dontReact = false; |
| 1884 }, |
| 1885 paramsObjectChanged: function() { |
| 1886 if (this._dontReact) { |
| 1887 return; |
| 1888 } |
| 1889 this.paramsString = this._encodeParams(this.paramsObject); |
| 1890 }, |
| 1891 _encodeParams: function(params) { |
| 1892 var encodedParams = []; |
| 1893 for (var key in params) { |
| 1894 var value = params[key]; |
| 1895 if (value === '') { |
| 1896 encodedParams.push(encodeURIComponent(key)); |
| 1897 } else if (value) { |
| 1898 encodedParams.push( |
| 1899 encodeURIComponent(key) + |
| 1900 '=' + |
| 1901 encodeURIComponent(value.toString()) |
| 1902 ); |
| 1903 } |
| 1904 } |
| 1905 return encodedParams.join('&'); |
| 1906 }, |
| 1907 _decodeParams: function(paramString) { |
| 1908 var params = {}; |
| 1909 |
| 1910 // Work around a bug in decodeURIComponent where + is not |
| 1911 // converted to spaces: |
| 1912 paramString = (paramString || '').replace(/\+/g, '%20'); |
| 1913 |
| 1914 var paramList = paramString.split('&'); |
| 1915 for (var i = 0; i < paramList.length; i++) { |
| 1916 var param = paramList[i].split('='); |
| 1917 if (param[0]) { |
| 1918 params[decodeURIComponent(param[0])] = |
| 1919 decodeURIComponent(param[1] || ''); |
| 1920 } |
| 1921 } |
| 1922 return params; |
| 1584 } | 1923 } |
| 1585 }); | 1924 }); |
| 1586 } else { | 1925 'use strict'; |
| 1587 window.console.log("KeyboardEvent.Key polyfill not required"); | 1926 |
| 1588 } | 1927 /** |
| 1589 // </if> /* is_ios */ | 1928 * Provides bidirectional mapping between `path` and `queryParams` and a |
| 1929 * app-route compatible `route` object. |
| 1930 * |
| 1931 * For more information, see the docs for `app-route-converter`. |
| 1932 * |
| 1933 * @polymerBehavior |
| 1934 */ |
| 1935 Polymer.AppRouteConverterBehavior = { |
| 1936 properties: { |
| 1937 /** |
| 1938 * A model representing the deserialized path through the route tree, as |
| 1939 * well as the current queryParams. |
| 1940 * |
| 1941 * A route object is the kernel of the routing system. It is intended to |
| 1942 * be fed into consuming elements such as `app-route`. |
| 1943 * |
| 1944 * @type {?Object} |
| 1945 */ |
| 1946 route: { |
| 1947 type: Object, |
| 1948 notify: true |
| 1949 }, |
| 1950 |
| 1951 /** |
| 1952 * A set of key/value pairs that are universally accessible to branches of |
| 1953 * the route tree. |
| 1954 * |
| 1955 * @type {?Object} |
| 1956 */ |
| 1957 queryParams: { |
| 1958 type: Object, |
| 1959 notify: true |
| 1960 }, |
| 1961 |
| 1962 /** |
| 1963 * The serialized path through the route tree. This corresponds to the |
| 1964 * `window.location.pathname` value, and will update to reflect changes |
| 1965 * to that value. |
| 1966 */ |
| 1967 path: { |
| 1968 type: String, |
| 1969 notify: true, |
| 1970 } |
| 1971 }, |
| 1972 |
| 1973 observers: [ |
| 1974 '_locationChanged(path, queryParams)', |
| 1975 '_routeChanged(route.prefix, route.path)', |
| 1976 '_routeQueryParamsChanged(route.__queryParams)' |
| 1977 ], |
| 1978 |
| 1979 created: function() { |
| 1980 this.linkPaths('route.__queryParams', 'queryParams'); |
| 1981 this.linkPaths('queryParams', 'route.__queryParams'); |
| 1982 }, |
| 1983 |
| 1984 /** |
| 1985 * Handler called when the path or queryParams change. |
| 1986 */ |
| 1987 _locationChanged: function() { |
| 1988 if (this.route && |
| 1989 this.route.path === this.path && |
| 1990 this.queryParams === this.route.__queryParams) { |
| 1991 return; |
| 1992 } |
| 1993 this.route = { |
| 1994 prefix: '', |
| 1995 path: this.path, |
| 1996 __queryParams: this.queryParams |
| 1997 }; |
| 1998 }, |
| 1999 |
| 2000 /** |
| 2001 * Handler called when the route prefix and route path change. |
| 2002 */ |
| 2003 _routeChanged: function() { |
| 2004 if (!this.route) { |
| 2005 return; |
| 2006 } |
| 2007 |
| 2008 this.path = this.route.prefix + this.route.path; |
| 2009 }, |
| 2010 |
| 2011 /** |
| 2012 * Handler called when the route queryParams change. |
| 2013 * |
| 2014 * @param {Object} queryParams A set of key/value pairs that are |
| 2015 * universally accessible to branches of the route tree. |
| 2016 */ |
| 2017 _routeQueryParamsChanged: function(queryParams) { |
| 2018 if (!this.route) { |
| 2019 return; |
| 2020 } |
| 2021 this.queryParams = queryParams; |
| 2022 } |
| 2023 }; |
| 2024 'use strict'; |
| 2025 |
| 2026 Polymer({ |
| 2027 is: 'app-location', |
| 2028 |
| 2029 properties: { |
| 2030 /** |
| 2031 * A model representing the deserialized path through the route tree, as |
| 2032 * well as the current queryParams. |
| 2033 */ |
| 2034 route: { |
| 2035 type: Object, |
| 2036 notify: true |
| 2037 }, |
| 2038 |
| 2039 /** |
| 2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in |
| 2041 * alternative to the `path`. For example, if deploying an app to a stat
ic |
| 2042 * web server (e.g., Github Pages) - where one does not have control ove
r |
| 2043 * server-side routing - it is usually a better experience to use the ha
sh |
| 2044 * to represent paths through one's app. |
| 2045 * |
| 2046 * When this property is set to true, the `hash` will be used in place o
f |
| 2047 |
| 2048 * the `path` for generating a `route`. |
| 2049 */ |
| 2050 useHashAsPath: { |
| 2051 type: Boolean, |
| 2052 value: false |
| 2053 }, |
| 2054 |
| 2055 /** |
| 2056 * A regexp that defines the set of URLs that should be considered part |
| 2057 * of this web app. |
| 2058 * |
| 2059 * Clicking on a link that matches this regex won't result in a full pag
e |
| 2060 * navigation, but will instead just update the URL state in place. |
| 2061 * |
| 2062 * This regexp is given everything after the origin in an absolute |
| 2063 * URL. So to match just URLs that start with /search/ do: |
| 2064 * url-space-regex="^/search/" |
| 2065 * |
| 2066 * @type {string|RegExp} |
| 2067 */ |
| 2068 urlSpaceRegex: { |
| 2069 type: String, |
| 2070 notify: true |
| 2071 }, |
| 2072 |
| 2073 /** |
| 2074 * A set of key/value pairs that are universally accessible to branches |
| 2075 * of the route tree. |
| 2076 */ |
| 2077 __queryParams: { |
| 2078 type: Object |
| 2079 }, |
| 2080 |
| 2081 /** |
| 2082 * The pathname component of the current URL. |
| 2083 */ |
| 2084 __path: { |
| 2085 type: String |
| 2086 }, |
| 2087 |
| 2088 /** |
| 2089 * The query string portion of the current URL. |
| 2090 */ |
| 2091 __query: { |
| 2092 type: String |
| 2093 }, |
| 2094 |
| 2095 /** |
| 2096 * The hash portion of the current URL. |
| 2097 */ |
| 2098 __hash: { |
| 2099 type: String |
| 2100 }, |
| 2101 |
| 2102 /** |
| 2103 * The route path, which will be either the hash or the path, depending |
| 2104 * on useHashAsPath. |
| 2105 */ |
| 2106 path: { |
| 2107 type: String, |
| 2108 observer: '__onPathChanged' |
| 2109 } |
| 2110 }, |
| 2111 |
| 2112 behaviors: [Polymer.AppRouteConverterBehavior], |
| 2113 |
| 2114 observers: [ |
| 2115 '__computeRoutePath(useHashAsPath, __hash, __path)' |
| 2116 ], |
| 2117 |
| 2118 __computeRoutePath: function() { |
| 2119 this.path = this.useHashAsPath ? this.__hash : this.__path; |
| 2120 }, |
| 2121 |
| 2122 __onPathChanged: function() { |
| 2123 if (!this._readied) { |
| 2124 return; |
| 2125 } |
| 2126 |
| 2127 if (this.useHashAsPath) { |
| 2128 this.__hash = this.path; |
| 2129 } else { |
| 2130 this.__path = this.path; |
| 2131 } |
| 2132 } |
| 2133 }); |
| 2134 'use strict'; |
| 2135 |
| 2136 Polymer({ |
| 2137 is: 'app-route', |
| 2138 |
| 2139 properties: { |
| 2140 /** |
| 2141 * The URL component managed by this element. |
| 2142 */ |
| 2143 route: { |
| 2144 type: Object, |
| 2145 notify: true |
| 2146 }, |
| 2147 |
| 2148 /** |
| 2149 * The pattern of slash-separated segments to match `path` against. |
| 2150 * |
| 2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar" |
| 2152 * but not "/foobar". |
| 2153 * |
| 2154 * Path segments like `/:named` are mapped to properties on the `data` obj
ect. |
| 2155 */ |
| 2156 pattern: { |
| 2157 type: String |
| 2158 }, |
| 2159 |
| 2160 /** |
| 2161 * The parameterized values that are extracted from the route as |
| 2162 * described by `pattern`. |
| 2163 */ |
| 2164 data: { |
| 2165 type: Object, |
| 2166 value: function() {return {};}, |
| 2167 notify: true |
| 2168 }, |
| 2169 |
| 2170 /** |
| 2171 * @type {?Object} |
| 2172 */ |
| 2173 queryParams: { |
| 2174 type: Object, |
| 2175 value: function() { |
| 2176 return {}; |
| 2177 }, |
| 2178 notify: true |
| 2179 }, |
| 2180 |
| 2181 /** |
| 2182 * The part of `path` NOT consumed by `pattern`. |
| 2183 */ |
| 2184 tail: { |
| 2185 type: Object, |
| 2186 value: function() {return {path: null, prefix: null, __queryParams: null
};}, |
| 2187 notify: true |
| 2188 }, |
| 2189 |
| 2190 active: { |
| 2191 type: Boolean, |
| 2192 notify: true, |
| 2193 readOnly: true |
| 2194 }, |
| 2195 |
| 2196 _queryParamsUpdating: { |
| 2197 type: Boolean, |
| 2198 value: false |
| 2199 }, |
| 2200 /** |
| 2201 * @type {?string} |
| 2202 */ |
| 2203 _matched: { |
| 2204 type: String, |
| 2205 value: '' |
| 2206 } |
| 2207 }, |
| 2208 |
| 2209 observers: [ |
| 2210 '__tryToMatch(route.path, pattern)', |
| 2211 '__updatePathOnDataChange(data.*)', |
| 2212 '__tailPathChanged(tail.path)', |
| 2213 '__routeQueryParamsChanged(route.__queryParams)', |
| 2214 '__tailQueryParamsChanged(tail.__queryParams)', |
| 2215 '__queryParamsChanged(queryParams.*)' |
| 2216 ], |
| 2217 |
| 2218 created: function() { |
| 2219 this.linkPaths('route.__queryParams', 'tail.__queryParams'); |
| 2220 this.linkPaths('tail.__queryParams', 'route.__queryParams'); |
| 2221 }, |
| 2222 |
| 2223 /** |
| 2224 * Deal with the query params object being assigned to wholesale. |
| 2225 * @export |
| 2226 */ |
| 2227 __routeQueryParamsChanged: function(queryParams) { |
| 2228 if (queryParams && this.tail) { |
| 2229 this.set('tail.__queryParams', queryParams); |
| 2230 |
| 2231 if (!this.active || this._queryParamsUpdating) { |
| 2232 return; |
| 2233 } |
| 2234 |
| 2235 // Copy queryParams and track whether there are any differences compared |
| 2236 // to the existing query params. |
| 2237 var copyOfQueryParams = {}; |
| 2238 var anythingChanged = false; |
| 2239 for (var key in queryParams) { |
| 2240 copyOfQueryParams[key] = queryParams[key]; |
| 2241 if (anythingChanged || |
| 2242 !this.queryParams || |
| 2243 queryParams[key] !== this.queryParams[key]) { |
| 2244 anythingChanged = true; |
| 2245 } |
| 2246 } |
| 2247 // Need to check whether any keys were deleted |
| 2248 for (var key in this.queryParams) { |
| 2249 if (anythingChanged || !(key in queryParams)) { |
| 2250 anythingChanged = true; |
| 2251 break; |
| 2252 } |
| 2253 } |
| 2254 |
| 2255 if (!anythingChanged) { |
| 2256 return; |
| 2257 } |
| 2258 this._queryParamsUpdating = true; |
| 2259 this.set('queryParams', copyOfQueryParams); |
| 2260 this._queryParamsUpdating = false; |
| 2261 } |
| 2262 }, |
| 2263 |
| 2264 /** |
| 2265 * @export |
| 2266 */ |
| 2267 __tailQueryParamsChanged: function(queryParams) { |
| 2268 if (queryParams && this.route) { |
| 2269 this.set('route.__queryParams', queryParams); |
| 2270 } |
| 2271 }, |
| 2272 |
| 2273 /** |
| 2274 * @export |
| 2275 */ |
| 2276 __queryParamsChanged: function(changes) { |
| 2277 if (!this.active || this._queryParamsUpdating) { |
| 2278 return; |
| 2279 } |
| 2280 |
| 2281 this.set('route.__' + changes.path, changes.value); |
| 2282 }, |
| 2283 |
| 2284 __resetProperties: function() { |
| 2285 this._setActive(false); |
| 2286 this._matched = null; |
| 2287 //this.tail = { path: null, prefix: null, queryParams: null }; |
| 2288 //this.data = {}; |
| 2289 }, |
| 2290 |
| 2291 /** |
| 2292 * @export |
| 2293 */ |
| 2294 __tryToMatch: function() { |
| 2295 if (!this.route) { |
| 2296 return; |
| 2297 } |
| 2298 var path = this.route.path; |
| 2299 var pattern = this.pattern; |
| 2300 if (!pattern) { |
| 2301 return; |
| 2302 } |
| 2303 |
| 2304 if (!path) { |
| 2305 this.__resetProperties(); |
| 2306 return; |
| 2307 } |
| 2308 |
| 2309 var remainingPieces = path.split('/'); |
| 2310 var patternPieces = pattern.split('/'); |
| 2311 |
| 2312 var matched = []; |
| 2313 var namedMatches = {}; |
| 2314 |
| 2315 for (var i=0; i < patternPieces.length; i++) { |
| 2316 var patternPiece = patternPieces[i]; |
| 2317 if (!patternPiece && patternPiece !== '') { |
| 2318 break; |
| 2319 } |
| 2320 var pathPiece = remainingPieces.shift(); |
| 2321 |
| 2322 // We don't match this path. |
| 2323 if (!pathPiece && pathPiece !== '') { |
| 2324 this.__resetProperties(); |
| 2325 return; |
| 2326 } |
| 2327 matched.push(pathPiece); |
| 2328 |
| 2329 if (patternPiece.charAt(0) == ':') { |
| 2330 namedMatches[patternPiece.slice(1)] = pathPiece; |
| 2331 } else if (patternPiece !== pathPiece) { |
| 2332 this.__resetProperties(); |
| 2333 return; |
| 2334 } |
| 2335 } |
| 2336 |
| 2337 this._matched = matched.join('/'); |
| 2338 |
| 2339 // Properties that must be updated atomically. |
| 2340 var propertyUpdates = {}; |
| 2341 |
| 2342 //this.active |
| 2343 if (!this.active) { |
| 2344 propertyUpdates.active = true; |
| 2345 } |
| 2346 |
| 2347 // this.tail |
| 2348 var tailPrefix = this.route.prefix + this._matched; |
| 2349 var tailPath = remainingPieces.join('/'); |
| 2350 if (remainingPieces.length > 0) { |
| 2351 tailPath = '/' + tailPath; |
| 2352 } |
| 2353 if (!this.tail || |
| 2354 this.tail.prefix !== tailPrefix || |
| 2355 this.tail.path !== tailPath) { |
| 2356 propertyUpdates.tail = { |
| 2357 prefix: tailPrefix, |
| 2358 path: tailPath, |
| 2359 __queryParams: this.route.__queryParams |
| 2360 }; |
| 2361 } |
| 2362 |
| 2363 // this.data |
| 2364 propertyUpdates.data = namedMatches; |
| 2365 this._dataInUrl = {}; |
| 2366 for (var key in namedMatches) { |
| 2367 this._dataInUrl[key] = namedMatches[key]; |
| 2368 } |
| 2369 |
| 2370 this.__setMulti(propertyUpdates); |
| 2371 }, |
| 2372 |
| 2373 /** |
| 2374 * @export |
| 2375 */ |
| 2376 __tailPathChanged: function() { |
| 2377 if (!this.active) { |
| 2378 return; |
| 2379 } |
| 2380 var tailPath = this.tail.path; |
| 2381 var newPath = this._matched; |
| 2382 if (tailPath) { |
| 2383 if (tailPath.charAt(0) !== '/') { |
| 2384 tailPath = '/' + tailPath; |
| 2385 } |
| 2386 newPath += tailPath; |
| 2387 } |
| 2388 this.set('route.path', newPath); |
| 2389 }, |
| 2390 |
| 2391 /** |
| 2392 * @export |
| 2393 */ |
| 2394 __updatePathOnDataChange: function() { |
| 2395 if (!this.route || !this.active) { |
| 2396 return; |
| 2397 } |
| 2398 var newPath = this.__getLink({}); |
| 2399 var oldPath = this.__getLink(this._dataInUrl); |
| 2400 if (newPath === oldPath) { |
| 2401 return; |
| 2402 } |
| 2403 this.set('route.path', newPath); |
| 2404 }, |
| 2405 |
| 2406 __getLink: function(overrideValues) { |
| 2407 var values = {tail: null}; |
| 2408 for (var key in this.data) { |
| 2409 values[key] = this.data[key]; |
| 2410 } |
| 2411 for (var key in overrideValues) { |
| 2412 values[key] = overrideValues[key]; |
| 2413 } |
| 2414 var patternPieces = this.pattern.split('/'); |
| 2415 var interp = patternPieces.map(function(value) { |
| 2416 if (value[0] == ':') { |
| 2417 value = values[value.slice(1)]; |
| 2418 } |
| 2419 return value; |
| 2420 }, this); |
| 2421 if (values.tail && values.tail.path) { |
| 2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { |
| 2423 interp.push(values.tail.path.slice(1)); |
| 2424 } else { |
| 2425 interp.push(values.tail.path); |
| 2426 } |
| 2427 } |
| 2428 return interp.join('/'); |
| 2429 }, |
| 2430 |
| 2431 __setMulti: function(setObj) { |
| 2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at |
| 2433 // internal data structures. I would not advise that you copy this |
| 2434 // example. |
| 2435 // |
| 2436 // In the future this will be a feature of Polymer itself. |
| 2437 // See: https://github.com/Polymer/polymer/issues/3640 |
| 2438 // |
| 2439 // Hacking around with private methods like this is juggling footguns, |
| 2440 // and is likely to have unexpected and unsupported rough edges. |
| 2441 // |
| 2442 // Be ye so warned. |
| 2443 for (var property in setObj) { |
| 2444 this._propertySetter(property, setObj[property]); |
| 2445 } |
| 2446 |
| 2447 for (var property in setObj) { |
| 2448 this._pathEffector(property, this[property]); |
| 2449 this._notifyPathUp(property, this[property]); |
| 2450 } |
| 2451 } |
| 2452 }); |
| 2453 Polymer({ |
| 2454 |
| 2455 is: 'iron-media-query', |
| 2456 |
| 2457 properties: { |
| 2458 |
| 2459 /** |
| 2460 * The Boolean return value of the media query. |
| 2461 */ |
| 2462 queryMatches: { |
| 2463 type: Boolean, |
| 2464 value: false, |
| 2465 readOnly: true, |
| 2466 notify: true |
| 2467 }, |
| 2468 |
| 2469 /** |
| 2470 * The CSS media query to evaluate. |
| 2471 */ |
| 2472 query: { |
| 2473 type: String, |
| 2474 observer: 'queryChanged' |
| 2475 }, |
| 2476 |
| 2477 /** |
| 2478 * If true, the query attribute is assumed to be a complete media query |
| 2479 * string rather than a single media feature. |
| 2480 */ |
| 2481 full: { |
| 2482 type: Boolean, |
| 2483 value: false |
| 2484 }, |
| 2485 |
| 2486 /** |
| 2487 * @type {function(MediaQueryList)} |
| 2488 */ |
| 2489 _boundMQHandler: { |
| 2490 value: function() { |
| 2491 return this.queryHandler.bind(this); |
| 2492 } |
| 2493 }, |
| 2494 |
| 2495 /** |
| 2496 * @type {MediaQueryList} |
| 2497 */ |
| 2498 _mq: { |
| 2499 value: null |
| 2500 } |
| 2501 }, |
| 2502 |
| 2503 attached: function() { |
| 2504 this.style.display = 'none'; |
| 2505 this.queryChanged(); |
| 2506 }, |
| 2507 |
| 2508 detached: function() { |
| 2509 this._remove(); |
| 2510 }, |
| 2511 |
| 2512 _add: function() { |
| 2513 if (this._mq) { |
| 2514 this._mq.addListener(this._boundMQHandler); |
| 2515 } |
| 2516 }, |
| 2517 |
| 2518 _remove: function() { |
| 2519 if (this._mq) { |
| 2520 this._mq.removeListener(this._boundMQHandler); |
| 2521 } |
| 2522 this._mq = null; |
| 2523 }, |
| 2524 |
| 2525 queryChanged: function() { |
| 2526 this._remove(); |
| 2527 var query = this.query; |
| 2528 if (!query) { |
| 2529 return; |
| 2530 } |
| 2531 if (!this.full && query[0] !== '(') { |
| 2532 query = '(' + query + ')'; |
| 2533 } |
| 2534 this._mq = window.matchMedia(query); |
| 2535 this._add(); |
| 2536 this.queryHandler(this._mq); |
| 2537 }, |
| 2538 |
| 2539 queryHandler: function(mq) { |
| 2540 this._setQueryMatches(mq.matches); |
| 2541 } |
| 2542 |
| 2543 }); |
| 1590 /** | 2544 /** |
| 1591 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | 2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to |
| 1592 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | 2546 * coordinate the flow of resize events between "resizers" (elements that cont
rol the |
| 1593 * size or hidden state of their children) and "resizables" (elements that nee
d to be | 2547 * size or hidden state of their children) and "resizables" (elements that nee
d to be |
| 1594 * notified when they are resized or un-hidden by their parents in order to ta
ke | 2548 * notified when they are resized or un-hidden by their parents in order to ta
ke |
| 1595 * action on their new measurements). | 2549 * action on their new measurements). |
| 1596 * | 2550 * |
| 1597 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | 2551 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to |
| 1598 * their element definition and listen for the `iron-resize` event on themselv
es. | 2552 * their element definition and listen for the `iron-resize` event on themselv
es. |
| 1599 * This event will be fired when they become showing after having been hidden, | 2553 * This event will be fired when they become showing after having been hidden, |
| (...skipping 160 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1760 // else they will get redundantly notified when the parent attaches). | 2714 // else they will get redundantly notified when the parent attaches). |
| 1761 if (!this.isAttached) { | 2715 if (!this.isAttached) { |
| 1762 return; | 2716 return; |
| 1763 } | 2717 } |
| 1764 | 2718 |
| 1765 this._notifyingDescendant = true; | 2719 this._notifyingDescendant = true; |
| 1766 descendant.notifyResize(); | 2720 descendant.notifyResize(); |
| 1767 this._notifyingDescendant = false; | 2721 this._notifyingDescendant = false; |
| 1768 } | 2722 } |
| 1769 }; | 2723 }; |
| 2724 /** |
| 2725 * @param {!Function} selectCallback |
| 2726 * @constructor |
| 2727 */ |
| 2728 Polymer.IronSelection = function(selectCallback) { |
| 2729 this.selection = []; |
| 2730 this.selectCallback = selectCallback; |
| 2731 }; |
| 2732 |
| 2733 Polymer.IronSelection.prototype = { |
| 2734 |
| 2735 /** |
| 2736 * Retrieves the selected item(s). |
| 2737 * |
| 2738 * @method get |
| 2739 * @returns Returns the selected item(s). If the multi property is true, |
| 2740 * `get` will return an array, otherwise it will return |
| 2741 * the selected item or undefined if there is no selection. |
| 2742 */ |
| 2743 get: function() { |
| 2744 return this.multi ? this.selection.slice() : this.selection[0]; |
| 2745 }, |
| 2746 |
| 2747 /** |
| 2748 * Clears all the selection except the ones indicated. |
| 2749 * |
| 2750 * @method clear |
| 2751 * @param {Array} excludes items to be excluded. |
| 2752 */ |
| 2753 clear: function(excludes) { |
| 2754 this.selection.slice().forEach(function(item) { |
| 2755 if (!excludes || excludes.indexOf(item) < 0) { |
| 2756 this.setItemSelected(item, false); |
| 2757 } |
| 2758 }, this); |
| 2759 }, |
| 2760 |
| 2761 /** |
| 2762 * Indicates if a given item is selected. |
| 2763 * |
| 2764 * @method isSelected |
| 2765 * @param {*} item The item whose selection state should be checked. |
| 2766 * @returns Returns true if `item` is selected. |
| 2767 */ |
| 2768 isSelected: function(item) { |
| 2769 return this.selection.indexOf(item) >= 0; |
| 2770 }, |
| 2771 |
| 2772 /** |
| 2773 * Sets the selection state for a given item to either selected or deselecte
d. |
| 2774 * |
| 2775 * @method setItemSelected |
| 2776 * @param {*} item The item to select. |
| 2777 * @param {boolean} isSelected True for selected, false for deselected. |
| 2778 */ |
| 2779 setItemSelected: function(item, isSelected) { |
| 2780 if (item != null) { |
| 2781 if (isSelected !== this.isSelected(item)) { |
| 2782 // proceed to update selection only if requested state differs from cu
rrent |
| 2783 if (isSelected) { |
| 2784 this.selection.push(item); |
| 2785 } else { |
| 2786 var i = this.selection.indexOf(item); |
| 2787 if (i >= 0) { |
| 2788 this.selection.splice(i, 1); |
| 2789 } |
| 2790 } |
| 2791 if (this.selectCallback) { |
| 2792 this.selectCallback(item, isSelected); |
| 2793 } |
| 2794 } |
| 2795 } |
| 2796 }, |
| 2797 |
| 2798 /** |
| 2799 * Sets the selection state for a given item. If the `multi` property |
| 2800 * is true, then the selected state of `item` will be toggled; otherwise |
| 2801 * the `item` will be selected. |
| 2802 * |
| 2803 * @method select |
| 2804 * @param {*} item The item to select. |
| 2805 */ |
| 2806 select: function(item) { |
| 2807 if (this.multi) { |
| 2808 this.toggle(item); |
| 2809 } else if (this.get() !== item) { |
| 2810 this.setItemSelected(this.get(), false); |
| 2811 this.setItemSelected(item, true); |
| 2812 } |
| 2813 }, |
| 2814 |
| 2815 /** |
| 2816 * Toggles the selection state for `item`. |
| 2817 * |
| 2818 * @method toggle |
| 2819 * @param {*} item The item to toggle. |
| 2820 */ |
| 2821 toggle: function(item) { |
| 2822 this.setItemSelected(item, !this.isSelected(item)); |
| 2823 } |
| 2824 |
| 2825 }; |
| 2826 /** @polymerBehavior */ |
| 2827 Polymer.IronSelectableBehavior = { |
| 2828 |
| 2829 /** |
| 2830 * Fired when iron-selector is activated (selected or deselected). |
| 2831 * It is fired before the selected items are changed. |
| 2832 * Cancel the event to abort selection. |
| 2833 * |
| 2834 * @event iron-activate |
| 2835 */ |
| 2836 |
| 2837 /** |
| 2838 * Fired when an item is selected |
| 2839 * |
| 2840 * @event iron-select |
| 2841 */ |
| 2842 |
| 2843 /** |
| 2844 * Fired when an item is deselected |
| 2845 * |
| 2846 * @event iron-deselect |
| 2847 */ |
| 2848 |
| 2849 /** |
| 2850 * Fired when the list of selectable items changes (e.g., items are |
| 2851 * added or removed). The detail of the event is a mutation record that |
| 2852 * describes what changed. |
| 2853 * |
| 2854 * @event iron-items-changed |
| 2855 */ |
| 2856 |
| 2857 properties: { |
| 2858 |
| 2859 /** |
| 2860 * If you want to use an attribute value or property of an element for |
| 2861 * `selected` instead of the index, set this to the name of the attribute |
| 2862 * or property. Hyphenated values are converted to camel case when used to |
| 2863 * look up the property of a selectable element. Camel cased values are |
| 2864 * *not* converted to hyphenated values for attribute lookup. It's |
| 2865 * recommended that you provide the hyphenated form of the name so that |
| 2866 * selection works in both cases. (Use `attr-or-property-name` instead of |
| 2867 * `attrOrPropertyName`.) |
| 2868 */ |
| 2869 attrForSelected: { |
| 2870 type: String, |
| 2871 value: null |
| 2872 }, |
| 2873 |
| 2874 /** |
| 2875 * Gets or sets the selected element. The default is to use the index of t
he item. |
| 2876 * @type {string|number} |
| 2877 */ |
| 2878 selected: { |
| 2879 type: String, |
| 2880 notify: true |
| 2881 }, |
| 2882 |
| 2883 /** |
| 2884 * Returns the currently selected item. |
| 2885 * |
| 2886 * @type {?Object} |
| 2887 */ |
| 2888 selectedItem: { |
| 2889 type: Object, |
| 2890 readOnly: true, |
| 2891 notify: true |
| 2892 }, |
| 2893 |
| 2894 /** |
| 2895 * The event that fires from items when they are selected. Selectable |
| 2896 * will listen for this event from items and update the selection state. |
| 2897 * Set to empty string to listen to no events. |
| 2898 */ |
| 2899 activateEvent: { |
| 2900 type: String, |
| 2901 value: 'tap', |
| 2902 observer: '_activateEventChanged' |
| 2903 }, |
| 2904 |
| 2905 /** |
| 2906 * This is a CSS selector string. If this is set, only items that match t
he CSS selector |
| 2907 * are selectable. |
| 2908 */ |
| 2909 selectable: String, |
| 2910 |
| 2911 /** |
| 2912 * The class to set on elements when selected. |
| 2913 */ |
| 2914 selectedClass: { |
| 2915 type: String, |
| 2916 value: 'iron-selected' |
| 2917 }, |
| 2918 |
| 2919 /** |
| 2920 * The attribute to set on elements when selected. |
| 2921 */ |
| 2922 selectedAttribute: { |
| 2923 type: String, |
| 2924 value: null |
| 2925 }, |
| 2926 |
| 2927 /** |
| 2928 * Default fallback if the selection based on selected with `attrForSelect
ed` |
| 2929 * is not found. |
| 2930 */ |
| 2931 fallbackSelection: { |
| 2932 type: String, |
| 2933 value: null |
| 2934 }, |
| 2935 |
| 2936 /** |
| 2937 * The list of items from which a selection can be made. |
| 2938 */ |
| 2939 items: { |
| 2940 type: Array, |
| 2941 readOnly: true, |
| 2942 notify: true, |
| 2943 value: function() { |
| 2944 return []; |
| 2945 } |
| 2946 }, |
| 2947 |
| 2948 /** |
| 2949 * The set of excluded elements where the key is the `localName` |
| 2950 * of the element that will be ignored from the item list. |
| 2951 * |
| 2952 * @default {template: 1} |
| 2953 */ |
| 2954 _excludedLocalNames: { |
| 2955 type: Object, |
| 2956 value: function() { |
| 2957 return { |
| 2958 'template': 1 |
| 2959 }; |
| 2960 } |
| 2961 } |
| 2962 }, |
| 2963 |
| 2964 observers: [ |
| 2965 '_updateAttrForSelected(attrForSelected)', |
| 2966 '_updateSelected(selected)', |
| 2967 '_checkFallback(fallbackSelection)' |
| 2968 ], |
| 2969 |
| 2970 created: function() { |
| 2971 this._bindFilterItem = this._filterItem.bind(this); |
| 2972 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); |
| 2973 }, |
| 2974 |
| 2975 attached: function() { |
| 2976 this._observer = this._observeItems(this); |
| 2977 this._updateItems(); |
| 2978 if (!this._shouldUpdateSelection) { |
| 2979 this._updateSelected(); |
| 2980 } |
| 2981 this._addListener(this.activateEvent); |
| 2982 }, |
| 2983 |
| 2984 detached: function() { |
| 2985 if (this._observer) { |
| 2986 Polymer.dom(this).unobserveNodes(this._observer); |
| 2987 } |
| 2988 this._removeListener(this.activateEvent); |
| 2989 }, |
| 2990 |
| 2991 /** |
| 2992 * Returns the index of the given item. |
| 2993 * |
| 2994 * @method indexOf |
| 2995 * @param {Object} item |
| 2996 * @returns Returns the index of the item |
| 2997 */ |
| 2998 indexOf: function(item) { |
| 2999 return this.items.indexOf(item); |
| 3000 }, |
| 3001 |
| 3002 /** |
| 3003 * Selects the given value. |
| 3004 * |
| 3005 * @method select |
| 3006 * @param {string|number} value the value to select. |
| 3007 */ |
| 3008 select: function(value) { |
| 3009 this.selected = value; |
| 3010 }, |
| 3011 |
| 3012 /** |
| 3013 * Selects the previous item. |
| 3014 * |
| 3015 * @method selectPrevious |
| 3016 */ |
| 3017 selectPrevious: function() { |
| 3018 var length = this.items.length; |
| 3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; |
| 3020 this.selected = this._indexToValue(index); |
| 3021 }, |
| 3022 |
| 3023 /** |
| 3024 * Selects the next item. |
| 3025 * |
| 3026 * @method selectNext |
| 3027 */ |
| 3028 selectNext: function() { |
| 3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; |
| 3030 this.selected = this._indexToValue(index); |
| 3031 }, |
| 3032 |
| 3033 /** |
| 3034 * Selects the item at the given index. |
| 3035 * |
| 3036 * @method selectIndex |
| 3037 */ |
| 3038 selectIndex: function(index) { |
| 3039 this.select(this._indexToValue(index)); |
| 3040 }, |
| 3041 |
| 3042 /** |
| 3043 * Force a synchronous update of the `items` property. |
| 3044 * |
| 3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to |
| 3046 * updates to the set of selectable items after updates to the DOM list and |
| 3047 * selection state have been made. |
| 3048 * |
| 3049 * WARNING: If you are using this method, you should probably consider an |
| 3050 * alternate approach. Synchronously querying for items is potentially |
| 3051 * slow for many use cases. The `items` property will update asynchronously |
| 3052 * on its own to reflect selectable items in the DOM. |
| 3053 */ |
| 3054 forceSynchronousItemUpdate: function() { |
| 3055 this._updateItems(); |
| 3056 }, |
| 3057 |
| 3058 get _shouldUpdateSelection() { |
| 3059 return this.selected != null; |
| 3060 }, |
| 3061 |
| 3062 _checkFallback: function() { |
| 3063 if (this._shouldUpdateSelection) { |
| 3064 this._updateSelected(); |
| 3065 } |
| 3066 }, |
| 3067 |
| 3068 _addListener: function(eventName) { |
| 3069 this.listen(this, eventName, '_activateHandler'); |
| 3070 }, |
| 3071 |
| 3072 _removeListener: function(eventName) { |
| 3073 this.unlisten(this, eventName, '_activateHandler'); |
| 3074 }, |
| 3075 |
| 3076 _activateEventChanged: function(eventName, old) { |
| 3077 this._removeListener(old); |
| 3078 this._addListener(eventName); |
| 3079 }, |
| 3080 |
| 3081 _updateItems: function() { |
| 3082 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); |
| 3083 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 3084 this._setItems(nodes); |
| 3085 }, |
| 3086 |
| 3087 _updateAttrForSelected: function() { |
| 3088 if (this._shouldUpdateSelection) { |
| 3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 3090 } |
| 3091 }, |
| 3092 |
| 3093 _updateSelected: function() { |
| 3094 this._selectSelected(this.selected); |
| 3095 }, |
| 3096 |
| 3097 _selectSelected: function(selected) { |
| 3098 this._selection.select(this._valueToItem(this.selected)); |
| 3099 // Check for items, since this array is populated only when attached |
| 3100 // Since Number(0) is falsy, explicitly check for undefined |
| 3101 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { |
| 3102 this.selected = this.fallbackSelection; |
| 3103 } |
| 3104 }, |
| 3105 |
| 3106 _filterItem: function(node) { |
| 3107 return !this._excludedLocalNames[node.localName]; |
| 3108 }, |
| 3109 |
| 3110 _valueToItem: function(value) { |
| 3111 return (value == null) ? null : this.items[this._valueToIndex(value)]; |
| 3112 }, |
| 3113 |
| 3114 _valueToIndex: function(value) { |
| 3115 if (this.attrForSelected) { |
| 3116 for (var i = 0, item; item = this.items[i]; i++) { |
| 3117 if (this._valueForItem(item) == value) { |
| 3118 return i; |
| 3119 } |
| 3120 } |
| 3121 } else { |
| 3122 return Number(value); |
| 3123 } |
| 3124 }, |
| 3125 |
| 3126 _indexToValue: function(index) { |
| 3127 if (this.attrForSelected) { |
| 3128 var item = this.items[index]; |
| 3129 if (item) { |
| 3130 return this._valueForItem(item); |
| 3131 } |
| 3132 } else { |
| 3133 return index; |
| 3134 } |
| 3135 }, |
| 3136 |
| 3137 _valueForItem: function(item) { |
| 3138 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; |
| 3139 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); |
| 3140 }, |
| 3141 |
| 3142 _applySelection: function(item, isSelected) { |
| 3143 if (this.selectedClass) { |
| 3144 this.toggleClass(this.selectedClass, isSelected, item); |
| 3145 } |
| 3146 if (this.selectedAttribute) { |
| 3147 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 3148 } |
| 3149 this._selectionChange(); |
| 3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); |
| 3151 }, |
| 3152 |
| 3153 _selectionChange: function() { |
| 3154 this._setSelectedItem(this._selection.get()); |
| 3155 }, |
| 3156 |
| 3157 // observe items change under the given node. |
| 3158 _observeItems: function(node) { |
| 3159 return Polymer.dom(node).observeNodes(function(mutation) { |
| 3160 this._updateItems(); |
| 3161 |
| 3162 if (this._shouldUpdateSelection) { |
| 3163 this._updateSelected(); |
| 3164 } |
| 3165 |
| 3166 // Let other interested parties know about the change so that |
| 3167 // we don't have to recreate mutation observers everywhere. |
| 3168 this.fire('iron-items-changed', mutation, { |
| 3169 bubbles: false, |
| 3170 cancelable: false |
| 3171 }); |
| 3172 }); |
| 3173 }, |
| 3174 |
| 3175 _activateHandler: function(e) { |
| 3176 var t = e.target; |
| 3177 var items = this.items; |
| 3178 while (t && t != this) { |
| 3179 var i = items.indexOf(t); |
| 3180 if (i >= 0) { |
| 3181 var value = this._indexToValue(i); |
| 3182 this._itemActivate(value, t); |
| 3183 return; |
| 3184 } |
| 3185 t = t.parentNode; |
| 3186 } |
| 3187 }, |
| 3188 |
| 3189 _itemActivate: function(value, item) { |
| 3190 if (!this.fire('iron-activate', |
| 3191 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { |
| 3192 this.select(value); |
| 3193 } |
| 3194 } |
| 3195 |
| 3196 }; |
| 3197 Polymer({ |
| 3198 |
| 3199 is: 'iron-pages', |
| 3200 |
| 3201 behaviors: [ |
| 3202 Polymer.IronResizableBehavior, |
| 3203 Polymer.IronSelectableBehavior |
| 3204 ], |
| 3205 |
| 3206 properties: { |
| 3207 |
| 3208 // as the selected page is the only one visible, activateEvent |
| 3209 // is both non-sensical and problematic; e.g. in cases where a user |
| 3210 // handler attempts to change the page and the activateEvent |
| 3211 // handler immediately changes it back |
| 3212 activateEvent: { |
| 3213 type: String, |
| 3214 value: null |
| 3215 } |
| 3216 |
| 3217 }, |
| 3218 |
| 3219 observers: [ |
| 3220 '_selectedPageChanged(selected)' |
| 3221 ], |
| 3222 |
| 3223 _selectedPageChanged: function(selected, old) { |
| 3224 this.async(this.notifyResize); |
| 3225 } |
| 3226 }); |
| 1770 (function() { | 3227 (function() { |
| 1771 'use strict'; | 3228 'use strict'; |
| 1772 | 3229 |
| 1773 /** | 3230 /** |
| 1774 * Chrome uses an older version of DOM Level 3 Keyboard Events | 3231 * Chrome uses an older version of DOM Level 3 Keyboard Events |
| 1775 * | 3232 * |
| 1776 * Most keys are labeled as text, but some are Unicode codepoints. | 3233 * Most keys are labeled as text, but some are Unicode codepoints. |
| 1777 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | 3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set |
| 1778 */ | 3235 */ |
| 1779 var KEY_IDENTIFIER = { | 3236 var KEY_IDENTIFIER = { |
| (...skipping 459 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2239 cancelable: true | 3696 cancelable: true |
| 2240 }); | 3697 }); |
| 2241 this[handlerName].call(this, event); | 3698 this[handlerName].call(this, event); |
| 2242 if (event.defaultPrevented) { | 3699 if (event.defaultPrevented) { |
| 2243 keyboardEvent.preventDefault(); | 3700 keyboardEvent.preventDefault(); |
| 2244 } | 3701 } |
| 2245 } | 3702 } |
| 2246 }; | 3703 }; |
| 2247 })(); | 3704 })(); |
| 2248 /** | 3705 /** |
| 2249 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | 3706 * @demo demo/index.html |
| 2250 * designated scroll target. | |
| 2251 * | |
| 2252 * Elements that consume this behavior can override the `_scrollHandler` | |
| 2253 * method to add logic on the scroll event. | |
| 2254 * | |
| 2255 * @demo demo/scrolling-region.html Scrolling Region | |
| 2256 * @demo demo/document.html Document Element | |
| 2257 * @polymerBehavior | 3707 * @polymerBehavior |
| 2258 */ | 3708 */ |
| 2259 Polymer.IronScrollTargetBehavior = { | 3709 Polymer.IronControlState = { |
| 2260 | 3710 |
| 2261 properties: { | 3711 properties: { |
| 2262 | 3712 |
| 2263 /** | 3713 /** |
| 2264 * Specifies the element that will handle the scroll event | 3714 * If true, the element currently has focus. |
| 2265 * on the behalf of the current element. This is typically a reference to
an element, | 3715 */ |
| 2266 * but there are a few more posibilities: | 3716 focused: { |
| 2267 * | 3717 type: Boolean, |
| 2268 * ### Elements id | 3718 value: false, |
| 2269 * | 3719 notify: true, |
| 2270 *```html | 3720 readOnly: true, |
| 2271 * <div id="scrollable-element" style="overflow: auto;"> | 3721 reflectToAttribute: true |
| 2272 * <x-element scroll-target="scrollable-element"> | 3722 }, |
| 2273 * \x3c!-- Content--\x3e | 3723 |
| 2274 * </x-element> | 3724 /** |
| 2275 * </div> | 3725 * If true, the user cannot interact with this element. |
| 2276 *``` | 3726 */ |
| 2277 * In this case, the `scrollTarget` will point to the outer div element. | 3727 disabled: { |
| 2278 * | 3728 type: Boolean, |
| 2279 * ### Document scrolling | 3729 value: false, |
| 2280 * | 3730 notify: true, |
| 2281 * For document scrolling, you can use the reserved word `document`: | 3731 observer: '_disabledChanged', |
| 2282 * | 3732 reflectToAttribute: true |
| 2283 *```html | 3733 }, |
| 2284 * <x-element scroll-target="document"> | 3734 |
| 2285 * \x3c!-- Content --\x3e | 3735 _oldTabIndex: { |
| 2286 * </x-element> | 3736 type: Number |
| 2287 *``` | 3737 }, |
| 2288 * | 3738 |
| 2289 * ### Elements reference | 3739 _boundFocusBlurHandler: { |
| 2290 * | 3740 type: Function, |
| 2291 *```js | |
| 2292 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
| 2293 *``` | |
| 2294 * | |
| 2295 * @type {HTMLElement} | |
| 2296 */ | |
| 2297 scrollTarget: { | |
| 2298 type: HTMLElement, | |
| 2299 value: function() { | 3741 value: function() { |
| 2300 return this._defaultScrollTarget; | 3742 return this._focusBlurHandler.bind(this); |
| 2301 } | 3743 } |
| 2302 } | 3744 } |
| 3745 |
| 2303 }, | 3746 }, |
| 2304 | 3747 |
| 2305 observers: [ | 3748 observers: [ |
| 2306 '_scrollTargetChanged(scrollTarget, isAttached)' | 3749 '_changedControlState(focused, disabled)' |
| 2307 ], | 3750 ], |
| 2308 | 3751 |
| 2309 _scrollTargetChanged: function(scrollTarget, isAttached) { | 3752 ready: function() { |
| 2310 var eventTarget; | 3753 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2311 | 3754 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2312 if (this._oldScrollTarget) { | 3755 }, |
| 2313 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | 3756 |
| 2314 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | 3757 _focusBlurHandler: function(event) { |
| 2315 this._oldScrollTarget = null; | 3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will |
| 2316 } | 3759 // eventually become `this` due to retargeting; if we are not in |
| 2317 | 3760 // ShadowDOM land, `event.target` will eventually become `this` due |
| 2318 if (!isAttached) { | 3761 // to the second conditional which fires a synthetic event (that is also |
| 2319 return; | 3762 // handled). In either case, we can disregard `event.path`. |
| 2320 } | 3763 |
| 2321 // Support element id references | 3764 if (event.target === this) { |
| 2322 if (scrollTarget === 'document') { | 3765 this._setFocused(event.type === 'focus'); |
| 2323 | 3766 } else if (!this.shadowRoot) { |
| 2324 this.scrollTarget = this._doc; | 3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget); |
| 2325 | 3768 if (!this.isLightDescendant(target)) { |
| 2326 } else if (typeof scrollTarget === 'string') { | 3769 this.fire(event.type, {sourceEvent: event}, { |
| 2327 | 3770 node: this, |
| 2328 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | 3771 bubbles: event.bubbles, |
| 2329 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | 3772 cancelable: event.cancelable |
| 2330 | 3773 }); |
| 2331 } else if (this._isValidScrollTarget()) { | 3774 } |
| 2332 | 3775 } |
| 2333 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | 3776 }, |
| 2334 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | 3777 |
| 2335 this._oldScrollTarget = scrollTarget; | 3778 _disabledChanged: function(disabled, old) { |
| 2336 | 3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2337 eventTarget.addEventListener('scroll', this._boundScrollHandler); | 3780 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2338 } | 3781 if (disabled) { |
| 2339 }, | 3782 this._oldTabIndex = this.tabIndex; |
| 2340 | 3783 this._setFocused(false); |
| 2341 /** | 3784 this.tabIndex = -1; |
| 2342 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | 3785 this.blur(); |
| 2343 * | 3786 } else if (this._oldTabIndex !== undefined) { |
| 2344 * @protected | 3787 this.tabIndex = this._oldTabIndex; |
| 2345 */ | 3788 } |
| 2346 _scrollHandler: function scrollHandler() {}, | 3789 }, |
| 2347 | 3790 |
| 2348 /** | 3791 _changedControlState: function() { |
| 2349 * The default scroll target. Consumers of this behavior may want to customi
ze | 3792 // _controlStateChanged is abstract, follow-on behaviors may implement it |
| 2350 * the default scroll target. | 3793 if (this._controlStateChanged) { |
| 2351 * | 3794 this._controlStateChanged(); |
| 2352 * @type {Element} | 3795 } |
| 2353 */ | |
| 2354 get _defaultScrollTarget() { | |
| 2355 return this._doc; | |
| 2356 }, | |
| 2357 | |
| 2358 /** | |
| 2359 * Shortcut for the document element | |
| 2360 * | |
| 2361 * @type {Element} | |
| 2362 */ | |
| 2363 get _doc() { | |
| 2364 return this.ownerDocument.documentElement; | |
| 2365 }, | |
| 2366 | |
| 2367 /** | |
| 2368 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 2369 * | |
| 2370 * @type {number} | |
| 2371 */ | |
| 2372 get _scrollTop() { | |
| 2373 if (this._isValidScrollTarget()) { | |
| 2374 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
| 2375 } | |
| 2376 return 0; | |
| 2377 }, | |
| 2378 | |
| 2379 /** | |
| 2380 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
| 2381 * | |
| 2382 * @type {number} | |
| 2383 */ | |
| 2384 get _scrollLeft() { | |
| 2385 if (this._isValidScrollTarget()) { | |
| 2386 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
| 2387 } | |
| 2388 return 0; | |
| 2389 }, | |
| 2390 | |
| 2391 /** | |
| 2392 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
| 2393 * | |
| 2394 * @type {number} | |
| 2395 */ | |
| 2396 set _scrollTop(top) { | |
| 2397 if (this.scrollTarget === this._doc) { | |
| 2398 window.scrollTo(window.pageXOffset, top); | |
| 2399 } else if (this._isValidScrollTarget()) { | |
| 2400 this.scrollTarget.scrollTop = top; | |
| 2401 } | |
| 2402 }, | |
| 2403 | |
| 2404 /** | |
| 2405 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
| 2406 * | |
| 2407 * @type {number} | |
| 2408 */ | |
| 2409 set _scrollLeft(left) { | |
| 2410 if (this.scrollTarget === this._doc) { | |
| 2411 window.scrollTo(left, window.pageYOffset); | |
| 2412 } else if (this._isValidScrollTarget()) { | |
| 2413 this.scrollTarget.scrollLeft = left; | |
| 2414 } | |
| 2415 }, | |
| 2416 | |
| 2417 /** | |
| 2418 * Scrolls the content to a particular place. | |
| 2419 * | |
| 2420 * @method scroll | |
| 2421 * @param {number} left The left position | |
| 2422 * @param {number} top The top position | |
| 2423 */ | |
| 2424 scroll: function(left, top) { | |
| 2425 if (this.scrollTarget === this._doc) { | |
| 2426 window.scrollTo(left, top); | |
| 2427 } else if (this._isValidScrollTarget()) { | |
| 2428 this.scrollTarget.scrollLeft = left; | |
| 2429 this.scrollTarget.scrollTop = top; | |
| 2430 } | |
| 2431 }, | |
| 2432 | |
| 2433 /** | |
| 2434 * Gets the width of the scroll target. | |
| 2435 * | |
| 2436 * @type {number} | |
| 2437 */ | |
| 2438 get _scrollTargetWidth() { | |
| 2439 if (this._isValidScrollTarget()) { | |
| 2440 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
| 2441 } | |
| 2442 return 0; | |
| 2443 }, | |
| 2444 | |
| 2445 /** | |
| 2446 * Gets the height of the scroll target. | |
| 2447 * | |
| 2448 * @type {number} | |
| 2449 */ | |
| 2450 get _scrollTargetHeight() { | |
| 2451 if (this._isValidScrollTarget()) { | |
| 2452 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
| 2453 } | |
| 2454 return 0; | |
| 2455 }, | |
| 2456 | |
| 2457 /** | |
| 2458 * Returns true if the scroll target is a valid HTMLElement. | |
| 2459 * | |
| 2460 * @return {boolean} | |
| 2461 */ | |
| 2462 _isValidScrollTarget: function() { | |
| 2463 return this.scrollTarget instanceof HTMLElement; | |
| 2464 } | 3796 } |
| 3797 |
| 2465 }; | 3798 }; |
| 2466 (function() { | 3799 /** |
| 2467 | 3800 * @demo demo/index.html |
| 2468 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 3801 * @polymerBehavior Polymer.IronButtonState |
| 2469 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 3802 */ |
| 2470 var DEFAULT_PHYSICAL_COUNT = 3; | 3803 Polymer.IronButtonStateImpl = { |
| 2471 var HIDDEN_Y = '-10000px'; | |
| 2472 var DEFAULT_GRID_SIZE = 200; | |
| 2473 var SECRET_TABINDEX = -100; | |
| 2474 | |
| 2475 Polymer({ | |
| 2476 | |
| 2477 is: 'iron-list', | |
| 2478 | 3804 |
| 2479 properties: { | 3805 properties: { |
| 2480 | 3806 |
| 2481 /** | 3807 /** |
| 2482 * An array containing items determining how many instances of the templat
e | 3808 * If true, the user is currently holding down the button. |
| 2483 * to stamp and that that each template instance should bind to. | 3809 */ |
| 2484 */ | 3810 pressed: { |
| 2485 items: { | 3811 type: Boolean, |
| 2486 type: Array | 3812 readOnly: true, |
| 2487 }, | 3813 value: false, |
| 2488 | 3814 reflectToAttribute: true, |
| 2489 /** | 3815 observer: '_pressedChanged' |
| 2490 * The max count of physical items the pool can extend to. | 3816 }, |
| 2491 */ | 3817 |
| 2492 maxPhysicalCount: { | 3818 /** |
| 2493 type: Number, | 3819 * If true, the button toggles the active state with each tap or press |
| 2494 value: 500 | 3820 * of the spacebar. |
| 2495 }, | 3821 */ |
| 2496 | 3822 toggles: { |
| 2497 /** | |
| 2498 * The name of the variable to add to the binding scope for the array | |
| 2499 * element associated with a given template instance. | |
| 2500 */ | |
| 2501 as: { | |
| 2502 type: String, | |
| 2503 value: 'item' | |
| 2504 }, | |
| 2505 | |
| 2506 /** | |
| 2507 * The name of the variable to add to the binding scope with the index | |
| 2508 * for the row. | |
| 2509 */ | |
| 2510 indexAs: { | |
| 2511 type: String, | |
| 2512 value: 'index' | |
| 2513 }, | |
| 2514 | |
| 2515 /** | |
| 2516 * The name of the variable to add to the binding scope to indicate | |
| 2517 * if the row is selected. | |
| 2518 */ | |
| 2519 selectedAs: { | |
| 2520 type: String, | |
| 2521 value: 'selected' | |
| 2522 }, | |
| 2523 | |
| 2524 /** | |
| 2525 * When true, the list is rendered as a grid. Grid items must have | |
| 2526 * fixed width and height set via CSS. e.g. | |
| 2527 * | |
| 2528 * ```html | |
| 2529 * <iron-list grid> | |
| 2530 * <template> | |
| 2531 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
| 2532 * </template> | |
| 2533 * </iron-list> | |
| 2534 * ``` | |
| 2535 */ | |
| 2536 grid: { | |
| 2537 type: Boolean, | 3823 type: Boolean, |
| 2538 value: false, | 3824 value: false, |
| 2539 reflectToAttribute: true | 3825 reflectToAttribute: true |
| 2540 }, | 3826 }, |
| 2541 | 3827 |
| 2542 /** | 3828 /** |
| 2543 * When true, tapping a row will select the item, placing its data model | 3829 * If true, the button is a toggle and is currently in the active state. |
| 2544 * in the set of selected items retrievable via the selection property. | 3830 */ |
| 3831 active: { |
| 3832 type: Boolean, |
| 3833 value: false, |
| 3834 notify: true, |
| 3835 reflectToAttribute: true |
| 3836 }, |
| 3837 |
| 3838 /** |
| 3839 * True if the element is currently being pressed by a "pointer," which |
| 3840 * is loosely defined as mouse or touch input (but specifically excluding |
| 3841 * keyboard input). |
| 3842 */ |
| 3843 pointerDown: { |
| 3844 type: Boolean, |
| 3845 readOnly: true, |
| 3846 value: false |
| 3847 }, |
| 3848 |
| 3849 /** |
| 3850 * True if the input device that caused the element to receive focus |
| 3851 * was a keyboard. |
| 3852 */ |
| 3853 receivedFocusFromKeyboard: { |
| 3854 type: Boolean, |
| 3855 readOnly: true |
| 3856 }, |
| 3857 |
| 3858 /** |
| 3859 * The aria attribute to be set if the button is a toggle and in the |
| 3860 * active state. |
| 3861 */ |
| 3862 ariaActiveAttribute: { |
| 3863 type: String, |
| 3864 value: 'aria-pressed', |
| 3865 observer: '_ariaActiveAttributeChanged' |
| 3866 } |
| 3867 }, |
| 3868 |
| 3869 listeners: { |
| 3870 down: '_downHandler', |
| 3871 up: '_upHandler', |
| 3872 tap: '_tapHandler' |
| 3873 }, |
| 3874 |
| 3875 observers: [ |
| 3876 '_detectKeyboardFocus(focused)', |
| 3877 '_activeChanged(active, ariaActiveAttribute)' |
| 3878 ], |
| 3879 |
| 3880 keyBindings: { |
| 3881 'enter:keydown': '_asyncClick', |
| 3882 'space:keydown': '_spaceKeyDownHandler', |
| 3883 'space:keyup': '_spaceKeyUpHandler', |
| 3884 }, |
| 3885 |
| 3886 _mouseEventRe: /^mouse/, |
| 3887 |
| 3888 _tapHandler: function() { |
| 3889 if (this.toggles) { |
| 3890 // a tap is needed to toggle the active state |
| 3891 this._userActivate(!this.active); |
| 3892 } else { |
| 3893 this.active = false; |
| 3894 } |
| 3895 }, |
| 3896 |
| 3897 _detectKeyboardFocus: function(focused) { |
| 3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 3899 }, |
| 3900 |
| 3901 // to emulate native checkbox, (de-)activations from a user interaction fire |
| 3902 // 'change' events |
| 3903 _userActivate: function(active) { |
| 3904 if (this.active !== active) { |
| 3905 this.active = active; |
| 3906 this.fire('change'); |
| 3907 } |
| 3908 }, |
| 3909 |
| 3910 _downHandler: function(event) { |
| 3911 this._setPointerDown(true); |
| 3912 this._setPressed(true); |
| 3913 this._setReceivedFocusFromKeyboard(false); |
| 3914 }, |
| 3915 |
| 3916 _upHandler: function() { |
| 3917 this._setPointerDown(false); |
| 3918 this._setPressed(false); |
| 3919 }, |
| 3920 |
| 3921 /** |
| 3922 * @param {!KeyboardEvent} event . |
| 3923 */ |
| 3924 _spaceKeyDownHandler: function(event) { |
| 3925 var keyboardEvent = event.detail.keyboardEvent; |
| 3926 var target = Polymer.dom(keyboardEvent).localTarget; |
| 3927 |
| 3928 // Ignore the event if this is coming from a focused light child, since th
at |
| 3929 // element will deal with it. |
| 3930 if (this.isLightDescendant(/** @type {Node} */(target))) |
| 3931 return; |
| 3932 |
| 3933 keyboardEvent.preventDefault(); |
| 3934 keyboardEvent.stopImmediatePropagation(); |
| 3935 this._setPressed(true); |
| 3936 }, |
| 3937 |
| 3938 /** |
| 3939 * @param {!KeyboardEvent} event . |
| 3940 */ |
| 3941 _spaceKeyUpHandler: function(event) { |
| 3942 var keyboardEvent = event.detail.keyboardEvent; |
| 3943 var target = Polymer.dom(keyboardEvent).localTarget; |
| 3944 |
| 3945 // Ignore the event if this is coming from a focused light child, since th
at |
| 3946 // element will deal with it. |
| 3947 if (this.isLightDescendant(/** @type {Node} */(target))) |
| 3948 return; |
| 3949 |
| 3950 if (this.pressed) { |
| 3951 this._asyncClick(); |
| 3952 } |
| 3953 this._setPressed(false); |
| 3954 }, |
| 3955 |
| 3956 // trigger click asynchronously, the asynchrony is useful to allow one |
| 3957 // event handler to unwind before triggering another event |
| 3958 _asyncClick: function() { |
| 3959 this.async(function() { |
| 3960 this.click(); |
| 3961 }, 1); |
| 3962 }, |
| 3963 |
| 3964 // any of these changes are considered a change to button state |
| 3965 |
| 3966 _pressedChanged: function(pressed) { |
| 3967 this._changedButtonState(); |
| 3968 }, |
| 3969 |
| 3970 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 3972 this.removeAttribute(oldValue); |
| 3973 } |
| 3974 }, |
| 3975 |
| 3976 _activeChanged: function(active, ariaActiveAttribute) { |
| 3977 if (this.toggles) { |
| 3978 this.setAttribute(this.ariaActiveAttribute, |
| 3979 active ? 'true' : 'false'); |
| 3980 } else { |
| 3981 this.removeAttribute(this.ariaActiveAttribute); |
| 3982 } |
| 3983 this._changedButtonState(); |
| 3984 }, |
| 3985 |
| 3986 _controlStateChanged: function() { |
| 3987 if (this.disabled) { |
| 3988 this._setPressed(false); |
| 3989 } else { |
| 3990 this._changedButtonState(); |
| 3991 } |
| 3992 }, |
| 3993 |
| 3994 // provide hook for follow-on behaviors to react to button-state |
| 3995 |
| 3996 _changedButtonState: function() { |
| 3997 if (this._buttonStateChanged) { |
| 3998 this._buttonStateChanged(); // abstract |
| 3999 } |
| 4000 } |
| 4001 |
| 4002 }; |
| 4003 |
| 4004 /** @polymerBehavior */ |
| 4005 Polymer.IronButtonState = [ |
| 4006 Polymer.IronA11yKeysBehavior, |
| 4007 Polymer.IronButtonStateImpl |
| 4008 ]; |
| 4009 (function() { |
| 4010 var Utility = { |
| 4011 distance: function(x1, y1, x2, y2) { |
| 4012 var xDelta = (x1 - x2); |
| 4013 var yDelta = (y1 - y2); |
| 4014 |
| 4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 4016 }, |
| 4017 |
| 4018 now: window.performance && window.performance.now ? |
| 4019 window.performance.now.bind(window.performance) : Date.now |
| 4020 }; |
| 4021 |
| 4022 /** |
| 4023 * @param {HTMLElement} element |
| 4024 * @constructor |
| 4025 */ |
| 4026 function ElementMetrics(element) { |
| 4027 this.element = element; |
| 4028 this.width = this.boundingRect.width; |
| 4029 this.height = this.boundingRect.height; |
| 4030 |
| 4031 this.size = Math.max(this.width, this.height); |
| 4032 } |
| 4033 |
| 4034 ElementMetrics.prototype = { |
| 4035 get boundingRect () { |
| 4036 return this.element.getBoundingClientRect(); |
| 4037 }, |
| 4038 |
| 4039 furthestCornerDistanceFrom: function(x, y) { |
| 4040 var topLeft = Utility.distance(x, y, 0, 0); |
| 4041 var topRight = Utility.distance(x, y, this.width, 0); |
| 4042 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 4043 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 4044 |
| 4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 4046 } |
| 4047 }; |
| 4048 |
| 4049 /** |
| 4050 * @param {HTMLElement} element |
| 4051 * @constructor |
| 4052 */ |
| 4053 function Ripple(element) { |
| 4054 this.element = element; |
| 4055 this.color = window.getComputedStyle(element).color; |
| 4056 |
| 4057 this.wave = document.createElement('div'); |
| 4058 this.waveContainer = document.createElement('div'); |
| 4059 this.wave.style.backgroundColor = this.color; |
| 4060 this.wave.classList.add('wave'); |
| 4061 this.waveContainer.classList.add('wave-container'); |
| 4062 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 4063 |
| 4064 this.resetInteractionState(); |
| 4065 } |
| 4066 |
| 4067 Ripple.MAX_RADIUS = 300; |
| 4068 |
| 4069 Ripple.prototype = { |
| 4070 get recenters() { |
| 4071 return this.element.recenters; |
| 4072 }, |
| 4073 |
| 4074 get center() { |
| 4075 return this.element.center; |
| 4076 }, |
| 4077 |
| 4078 get mouseDownElapsed() { |
| 4079 var elapsed; |
| 4080 |
| 4081 if (!this.mouseDownStart) { |
| 4082 return 0; |
| 4083 } |
| 4084 |
| 4085 elapsed = Utility.now() - this.mouseDownStart; |
| 4086 |
| 4087 if (this.mouseUpStart) { |
| 4088 elapsed -= this.mouseUpElapsed; |
| 4089 } |
| 4090 |
| 4091 return elapsed; |
| 4092 }, |
| 4093 |
| 4094 get mouseUpElapsed() { |
| 4095 return this.mouseUpStart ? |
| 4096 Utility.now () - this.mouseUpStart : 0; |
| 4097 }, |
| 4098 |
| 4099 get mouseDownElapsedSeconds() { |
| 4100 return this.mouseDownElapsed / 1000; |
| 4101 }, |
| 4102 |
| 4103 get mouseUpElapsedSeconds() { |
| 4104 return this.mouseUpElapsed / 1000; |
| 4105 }, |
| 4106 |
| 4107 get mouseInteractionSeconds() { |
| 4108 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 4109 }, |
| 4110 |
| 4111 get initialOpacity() { |
| 4112 return this.element.initialOpacity; |
| 4113 }, |
| 4114 |
| 4115 get opacityDecayVelocity() { |
| 4116 return this.element.opacityDecayVelocity; |
| 4117 }, |
| 4118 |
| 4119 get radius() { |
| 4120 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 4121 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; |
| 4122 var waveRadius = Math.min( |
| 4123 Math.sqrt(width2 + height2), |
| 4124 Ripple.MAX_RADIUS |
| 4125 ) * 1.1 + 5; |
| 4126 |
| 4127 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); |
| 4128 var timeNow = this.mouseInteractionSeconds / duration; |
| 4129 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 4130 |
| 4131 return Math.abs(size); |
| 4132 }, |
| 4133 |
| 4134 get opacity() { |
| 4135 if (!this.mouseUpStart) { |
| 4136 return this.initialOpacity; |
| 4137 } |
| 4138 |
| 4139 return Math.max( |
| 4140 0, |
| 4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity |
| 4142 ); |
| 4143 }, |
| 4144 |
| 4145 get outerOpacity() { |
| 4146 // Linear increase in background opacity, capped at the opacity |
| 4147 // of the wavefront (waveOpacity). |
| 4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; |
| 4149 var waveOpacity = this.opacity; |
| 4150 |
| 4151 return Math.max( |
| 4152 0, |
| 4153 Math.min(outerOpacity, waveOpacity) |
| 4154 ); |
| 4155 }, |
| 4156 |
| 4157 get isOpacityFullyDecayed() { |
| 4158 return this.opacity < 0.01 && |
| 4159 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); |
| 4160 }, |
| 4161 |
| 4162 get isRestingAtMaxRadius() { |
| 4163 return this.opacity >= this.initialOpacity && |
| 4164 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); |
| 4165 }, |
| 4166 |
| 4167 get isAnimationComplete() { |
| 4168 return this.mouseUpStart ? |
| 4169 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; |
| 4170 }, |
| 4171 |
| 4172 get translationFraction() { |
| 4173 return Math.min( |
| 4174 1, |
| 4175 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) |
| 4176 ); |
| 4177 }, |
| 4178 |
| 4179 get xNow() { |
| 4180 if (this.xEnd) { |
| 4181 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); |
| 4182 } |
| 4183 |
| 4184 return this.xStart; |
| 4185 }, |
| 4186 |
| 4187 get yNow() { |
| 4188 if (this.yEnd) { |
| 4189 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); |
| 4190 } |
| 4191 |
| 4192 return this.yStart; |
| 4193 }, |
| 4194 |
| 4195 get isMouseDown() { |
| 4196 return this.mouseDownStart && !this.mouseUpStart; |
| 4197 }, |
| 4198 |
| 4199 resetInteractionState: function() { |
| 4200 this.maxRadius = 0; |
| 4201 this.mouseDownStart = 0; |
| 4202 this.mouseUpStart = 0; |
| 4203 |
| 4204 this.xStart = 0; |
| 4205 this.yStart = 0; |
| 4206 this.xEnd = 0; |
| 4207 this.yEnd = 0; |
| 4208 this.slideDistance = 0; |
| 4209 |
| 4210 this.containerMetrics = new ElementMetrics(this.element); |
| 4211 }, |
| 4212 |
| 4213 draw: function() { |
| 4214 var scale; |
| 4215 var translateString; |
| 4216 var dx; |
| 4217 var dy; |
| 4218 |
| 4219 this.wave.style.opacity = this.opacity; |
| 4220 |
| 4221 scale = this.radius / (this.containerMetrics.size / 2); |
| 4222 dx = this.xNow - (this.containerMetrics.width / 2); |
| 4223 dy = this.yNow - (this.containerMetrics.height / 2); |
| 4224 |
| 4225 |
| 4226 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. |
| 4227 // https://bugs.webkit.org/show_bug.cgi?id=98538 |
| 4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; |
| 4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; |
| 4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 4232 }, |
| 4233 |
| 4234 /** @param {Event=} event */ |
| 4235 downAction: function(event) { |
| 4236 var xCenter = this.containerMetrics.width / 2; |
| 4237 var yCenter = this.containerMetrics.height / 2; |
| 4238 |
| 4239 this.resetInteractionState(); |
| 4240 this.mouseDownStart = Utility.now(); |
| 4241 |
| 4242 if (this.center) { |
| 4243 this.xStart = xCenter; |
| 4244 this.yStart = yCenter; |
| 4245 this.slideDistance = Utility.distance( |
| 4246 this.xStart, this.yStart, this.xEnd, this.yEnd |
| 4247 ); |
| 4248 } else { |
| 4249 this.xStart = event ? |
| 4250 event.detail.x - this.containerMetrics.boundingRect.left : |
| 4251 this.containerMetrics.width / 2; |
| 4252 this.yStart = event ? |
| 4253 event.detail.y - this.containerMetrics.boundingRect.top : |
| 4254 this.containerMetrics.height / 2; |
| 4255 } |
| 4256 |
| 4257 if (this.recenters) { |
| 4258 this.xEnd = xCenter; |
| 4259 this.yEnd = yCenter; |
| 4260 this.slideDistance = Utility.distance( |
| 4261 this.xStart, this.yStart, this.xEnd, this.yEnd |
| 4262 ); |
| 4263 } |
| 4264 |
| 4265 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( |
| 4266 this.xStart, |
| 4267 this.yStart |
| 4268 ); |
| 4269 |
| 4270 this.waveContainer.style.top = |
| 4271 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; |
| 4272 this.waveContainer.style.left = |
| 4273 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; |
| 4274 |
| 4275 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 4276 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 4277 }, |
| 4278 |
| 4279 /** @param {Event=} event */ |
| 4280 upAction: function(event) { |
| 4281 if (!this.isMouseDown) { |
| 4282 return; |
| 4283 } |
| 4284 |
| 4285 this.mouseUpStart = Utility.now(); |
| 4286 }, |
| 4287 |
| 4288 remove: function() { |
| 4289 Polymer.dom(this.waveContainer.parentNode).removeChild( |
| 4290 this.waveContainer |
| 4291 ); |
| 4292 } |
| 4293 }; |
| 4294 |
| 4295 Polymer({ |
| 4296 is: 'paper-ripple', |
| 4297 |
| 4298 behaviors: [ |
| 4299 Polymer.IronA11yKeysBehavior |
| 4300 ], |
| 4301 |
| 4302 properties: { |
| 4303 /** |
| 4304 * The initial opacity set on the wave. |
| 4305 * |
| 4306 * @attribute initialOpacity |
| 4307 * @type number |
| 4308 * @default 0.25 |
| 4309 */ |
| 4310 initialOpacity: { |
| 4311 type: Number, |
| 4312 value: 0.25 |
| 4313 }, |
| 4314 |
| 4315 /** |
| 4316 * How fast (opacity per second) the wave fades out. |
| 4317 * |
| 4318 * @attribute opacityDecayVelocity |
| 4319 * @type number |
| 4320 * @default 0.8 |
| 4321 */ |
| 4322 opacityDecayVelocity: { |
| 4323 type: Number, |
| 4324 value: 0.8 |
| 4325 }, |
| 4326 |
| 4327 /** |
| 4328 * If true, ripples will exhibit a gravitational pull towards |
| 4329 * the center of their container as they fade away. |
| 4330 * |
| 4331 * @attribute recenters |
| 4332 * @type boolean |
| 4333 * @default false |
| 4334 */ |
| 4335 recenters: { |
| 4336 type: Boolean, |
| 4337 value: false |
| 4338 }, |
| 4339 |
| 4340 /** |
| 4341 * If true, ripples will center inside its container |
| 4342 * |
| 4343 * @attribute recenters |
| 4344 * @type boolean |
| 4345 * @default false |
| 4346 */ |
| 4347 center: { |
| 4348 type: Boolean, |
| 4349 value: false |
| 4350 }, |
| 4351 |
| 4352 /** |
| 4353 * A list of the visual ripples. |
| 4354 * |
| 4355 * @attribute ripples |
| 4356 * @type Array |
| 4357 * @default [] |
| 4358 */ |
| 4359 ripples: { |
| 4360 type: Array, |
| 4361 value: function() { |
| 4362 return []; |
| 4363 } |
| 4364 }, |
| 4365 |
| 4366 /** |
| 4367 * True when there are visible ripples animating within the |
| 4368 * element. |
| 4369 */ |
| 4370 animating: { |
| 4371 type: Boolean, |
| 4372 readOnly: true, |
| 4373 reflectToAttribute: true, |
| 4374 value: false |
| 4375 }, |
| 4376 |
| 4377 /** |
| 4378 * If true, the ripple will remain in the "down" state until `holdDown` |
| 4379 * is set to false again. |
| 4380 */ |
| 4381 holdDown: { |
| 4382 type: Boolean, |
| 4383 value: false, |
| 4384 observer: '_holdDownChanged' |
| 4385 }, |
| 4386 |
| 4387 /** |
| 4388 * If true, the ripple will not generate a ripple effect |
| 4389 * via pointer interaction. |
| 4390 * Calling ripple's imperative api like `simulatedRipple` will |
| 4391 * still generate the ripple effect. |
| 4392 */ |
| 4393 noink: { |
| 4394 type: Boolean, |
| 4395 value: false |
| 4396 }, |
| 4397 |
| 4398 _animating: { |
| 4399 type: Boolean |
| 4400 }, |
| 4401 |
| 4402 _boundAnimate: { |
| 4403 type: Function, |
| 4404 value: function() { |
| 4405 return this.animate.bind(this); |
| 4406 } |
| 4407 } |
| 4408 }, |
| 4409 |
| 4410 get target () { |
| 4411 return this.keyEventTarget; |
| 4412 }, |
| 4413 |
| 4414 keyBindings: { |
| 4415 'enter:keydown': '_onEnterKeydown', |
| 4416 'space:keydown': '_onSpaceKeydown', |
| 4417 'space:keyup': '_onSpaceKeyup' |
| 4418 }, |
| 4419 |
| 4420 attached: function() { |
| 4421 // Set up a11yKeysBehavior to listen to key events on the target, |
| 4422 // so that space and enter activate the ripple even if the target doesn'
t |
| 4423 // handle key events. The key handlers deal with `noink` themselves. |
| 4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE |
| 4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
| 4426 } else { |
| 4427 this.keyEventTarget = this.parentNode; |
| 4428 } |
| 4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); |
| 4430 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
| 4431 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
| 4432 }, |
| 4433 |
| 4434 detached: function() { |
| 4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
| 4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
| 4437 this.keyEventTarget = null; |
| 4438 }, |
| 4439 |
| 4440 get shouldKeepAnimating () { |
| 4441 for (var index = 0; index < this.ripples.length; ++index) { |
| 4442 if (!this.ripples[index].isAnimationComplete) { |
| 4443 return true; |
| 4444 } |
| 4445 } |
| 4446 |
| 4447 return false; |
| 4448 }, |
| 4449 |
| 4450 simulatedRipple: function() { |
| 4451 this.downAction(null); |
| 4452 |
| 4453 // Please see polymer/polymer#1305 |
| 4454 this.async(function() { |
| 4455 this.upAction(); |
| 4456 }, 1); |
| 4457 }, |
| 4458 |
| 4459 /** |
| 4460 * Provokes a ripple down effect via a UI event, |
| 4461 * respecting the `noink` property. |
| 4462 * @param {Event=} event |
| 4463 */ |
| 4464 uiDownAction: function(event) { |
| 4465 if (!this.noink) { |
| 4466 this.downAction(event); |
| 4467 } |
| 4468 }, |
| 4469 |
| 4470 /** |
| 4471 * Provokes a ripple down effect via a UI event, |
| 4472 * *not* respecting the `noink` property. |
| 4473 * @param {Event=} event |
| 4474 */ |
| 4475 downAction: function(event) { |
| 4476 if (this.holdDown && this.ripples.length > 0) { |
| 4477 return; |
| 4478 } |
| 4479 |
| 4480 var ripple = this.addRipple(); |
| 4481 |
| 4482 ripple.downAction(event); |
| 4483 |
| 4484 if (!this._animating) { |
| 4485 this._animating = true; |
| 4486 this.animate(); |
| 4487 } |
| 4488 }, |
| 4489 |
| 4490 /** |
| 4491 * Provokes a ripple up effect via a UI event, |
| 4492 * respecting the `noink` property. |
| 4493 * @param {Event=} event |
| 4494 */ |
| 4495 uiUpAction: function(event) { |
| 4496 if (!this.noink) { |
| 4497 this.upAction(event); |
| 4498 } |
| 4499 }, |
| 4500 |
| 4501 /** |
| 4502 * Provokes a ripple up effect via a UI event, |
| 4503 * *not* respecting the `noink` property. |
| 4504 * @param {Event=} event |
| 4505 */ |
| 4506 upAction: function(event) { |
| 4507 if (this.holdDown) { |
| 4508 return; |
| 4509 } |
| 4510 |
| 4511 this.ripples.forEach(function(ripple) { |
| 4512 ripple.upAction(event); |
| 4513 }); |
| 4514 |
| 4515 this._animating = true; |
| 4516 this.animate(); |
| 4517 }, |
| 4518 |
| 4519 onAnimationComplete: function() { |
| 4520 this._animating = false; |
| 4521 this.$.background.style.backgroundColor = null; |
| 4522 this.fire('transitionend'); |
| 4523 }, |
| 4524 |
| 4525 addRipple: function() { |
| 4526 var ripple = new Ripple(this); |
| 4527 |
| 4528 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 4529 this.$.background.style.backgroundColor = ripple.color; |
| 4530 this.ripples.push(ripple); |
| 4531 |
| 4532 this._setAnimating(true); |
| 4533 |
| 4534 return ripple; |
| 4535 }, |
| 4536 |
| 4537 removeRipple: function(ripple) { |
| 4538 var rippleIndex = this.ripples.indexOf(ripple); |
| 4539 |
| 4540 if (rippleIndex < 0) { |
| 4541 return; |
| 4542 } |
| 4543 |
| 4544 this.ripples.splice(rippleIndex, 1); |
| 4545 |
| 4546 ripple.remove(); |
| 4547 |
| 4548 if (!this.ripples.length) { |
| 4549 this._setAnimating(false); |
| 4550 } |
| 4551 }, |
| 4552 |
| 4553 animate: function() { |
| 4554 if (!this._animating) { |
| 4555 return; |
| 4556 } |
| 4557 var index; |
| 4558 var ripple; |
| 4559 |
| 4560 for (index = 0; index < this.ripples.length; ++index) { |
| 4561 ripple = this.ripples[index]; |
| 4562 |
| 4563 ripple.draw(); |
| 4564 |
| 4565 this.$.background.style.opacity = ripple.outerOpacity; |
| 4566 |
| 4567 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 4568 this.removeRipple(ripple); |
| 4569 } |
| 4570 } |
| 4571 |
| 4572 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
| 4573 this.onAnimationComplete(); |
| 4574 } else { |
| 4575 window.requestAnimationFrame(this._boundAnimate); |
| 4576 } |
| 4577 }, |
| 4578 |
| 4579 _onEnterKeydown: function() { |
| 4580 this.uiDownAction(); |
| 4581 this.async(this.uiUpAction, 1); |
| 4582 }, |
| 4583 |
| 4584 _onSpaceKeydown: function() { |
| 4585 this.uiDownAction(); |
| 4586 }, |
| 4587 |
| 4588 _onSpaceKeyup: function() { |
| 4589 this.uiUpAction(); |
| 4590 }, |
| 4591 |
| 4592 // note: holdDown does not respect noink since it can be a focus based |
| 4593 // effect. |
| 4594 _holdDownChanged: function(newVal, oldVal) { |
| 4595 if (oldVal === undefined) { |
| 4596 return; |
| 4597 } |
| 4598 if (newVal) { |
| 4599 this.downAction(); |
| 4600 } else { |
| 4601 this.upAction(); |
| 4602 } |
| 4603 } |
| 4604 |
| 4605 /** |
| 4606 Fired when the animation finishes. |
| 4607 This is useful if you want to wait until |
| 4608 the ripple animation finishes to perform some action. |
| 4609 |
| 4610 @event transitionend |
| 4611 @param {{node: Object}} detail Contains the animated node. |
| 4612 */ |
| 4613 }); |
| 4614 })(); |
| 4615 /** |
| 4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple |
| 4617 * when the element has focus via pointer or keyboard. |
| 4618 * |
| 4619 * NOTE: This behavior is intended to be used in conjunction with and after |
| 4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`. |
| 4621 * |
| 4622 * @polymerBehavior Polymer.PaperRippleBehavior |
| 4623 */ |
| 4624 Polymer.PaperRippleBehavior = { |
| 4625 properties: { |
| 4626 /** |
| 4627 * If true, the element will not produce a ripple effect when interacted |
| 4628 * with via the pointer. |
| 4629 */ |
| 4630 noink: { |
| 4631 type: Boolean, |
| 4632 observer: '_noinkChanged' |
| 4633 }, |
| 4634 |
| 4635 /** |
| 4636 * @type {Element|undefined} |
| 4637 */ |
| 4638 _rippleContainer: { |
| 4639 type: Object, |
| 4640 } |
| 4641 }, |
| 4642 |
| 4643 /** |
| 4644 * Ensures a `<paper-ripple>` element is available when the element is |
| 4645 * focused. |
| 4646 */ |
| 4647 _buttonStateChanged: function() { |
| 4648 if (this.focused) { |
| 4649 this.ensureRipple(); |
| 4650 } |
| 4651 }, |
| 4652 |
| 4653 /** |
| 4654 * In addition to the functionality provided in `IronButtonState`, ensures |
| 4655 * a ripple effect is created when the element is in a `pressed` state. |
| 4656 */ |
| 4657 _downHandler: function(event) { |
| 4658 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 4659 if (this.pressed) { |
| 4660 this.ensureRipple(event); |
| 4661 } |
| 4662 }, |
| 4663 |
| 4664 /** |
| 4665 * Ensures this element contains a ripple effect. For startup efficiency |
| 4666 * the ripple effect is dynamically on demand when needed. |
| 4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the |
| 4668 * ripple. |
| 4669 */ |
| 4670 ensureRipple: function(optTriggeringEvent) { |
| 4671 if (!this.hasRipple()) { |
| 4672 this._ripple = this._createRipple(); |
| 4673 this._ripple.noink = this.noink; |
| 4674 var rippleContainer = this._rippleContainer || this.root; |
| 4675 if (rippleContainer) { |
| 4676 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 4677 } |
| 4678 if (optTriggeringEvent) { |
| 4679 // Check if the event happened inside of the ripple container |
| 4680 // Fall back to host instead of the root because distributed text |
| 4681 // nodes are not valid event targets |
| 4682 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 4683 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 4684 if (domContainer.deepContains( /** @type {Node} */(target))) { |
| 4685 this._ripple.uiDownAction(optTriggeringEvent); |
| 4686 } |
| 4687 } |
| 4688 } |
| 4689 }, |
| 4690 |
| 4691 /** |
| 4692 * Returns the `<paper-ripple>` element used by this element to create |
| 4693 * ripple effects. The element's ripple is created on demand, when |
| 4694 * necessary, and calling this method will force the |
| 4695 * ripple to be created. |
| 4696 */ |
| 4697 getRipple: function() { |
| 4698 this.ensureRipple(); |
| 4699 return this._ripple; |
| 4700 }, |
| 4701 |
| 4702 /** |
| 4703 * Returns true if this element currently contains a ripple effect. |
| 4704 * @return {boolean} |
| 4705 */ |
| 4706 hasRipple: function() { |
| 4707 return Boolean(this._ripple); |
| 4708 }, |
| 4709 |
| 4710 /** |
| 4711 * Create the element's ripple effect via creating a `<paper-ripple>`. |
| 4712 * Override this method to customize the ripple element. |
| 4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. |
| 4714 */ |
| 4715 _createRipple: function() { |
| 4716 return /** @type {!PaperRippleElement} */ ( |
| 4717 document.createElement('paper-ripple')); |
| 4718 }, |
| 4719 |
| 4720 _noinkChanged: function(noink) { |
| 4721 if (this.hasRipple()) { |
| 4722 this._ripple.noink = noink; |
| 4723 } |
| 4724 } |
| 4725 }; |
| 4726 /** @polymerBehavior Polymer.PaperButtonBehavior */ |
| 4727 Polymer.PaperButtonBehaviorImpl = { |
| 4728 properties: { |
| 4729 /** |
| 4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the |
| 4731 * shadow, and each increasing number greater than 0 will be "deeper" |
| 4732 * than the last. |
| 2545 * | 4733 * |
| 2546 * Note that tapping focusable elements within the list item will not | 4734 * @attribute elevation |
| 2547 * result in selection, since they are presumed to have their * own action
. | 4735 * @type number |
| 2548 */ | 4736 * @default 1 |
| 2549 selectionEnabled: { | 4737 */ |
| 2550 type: Boolean, | 4738 elevation: { |
| 2551 value: false | 4739 type: Number, |
| 2552 }, | 4740 reflectToAttribute: true, |
| 2553 | 4741 readOnly: true |
| 2554 /** | |
| 2555 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
| 2556 * if no item is selected. | |
| 2557 */ | |
| 2558 selectedItem: { | |
| 2559 type: Object, | |
| 2560 notify: true | |
| 2561 }, | |
| 2562 | |
| 2563 /** | |
| 2564 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
| 2565 */ | |
| 2566 selectedItems: { | |
| 2567 type: Object, | |
| 2568 notify: true | |
| 2569 }, | |
| 2570 | |
| 2571 /** | |
| 2572 * When `true`, multiple items may be selected at once (in this case, | |
| 2573 * `selected` is an array of currently selected items). When `false`, | |
| 2574 * only one item may be selected at a time. | |
| 2575 */ | |
| 2576 multiSelection: { | |
| 2577 type: Boolean, | |
| 2578 value: false | |
| 2579 } | 4742 } |
| 2580 }, | 4743 }, |
| 2581 | 4744 |
| 2582 observers: [ | 4745 observers: [ |
| 2583 '_itemsChanged(items.*)', | 4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', |
| 2584 '_selectionEnabledChanged(selectionEnabled)', | 4747 '_computeKeyboardClass(receivedFocusFromKeyboard)' |
| 2585 '_multiSelectionChanged(multiSelection)', | |
| 2586 '_setOverflow(scrollTarget)' | |
| 2587 ], | 4748 ], |
| 2588 | 4749 |
| 2589 behaviors: [ | 4750 hostAttributes: { |
| 2590 Polymer.Templatizer, | 4751 role: 'button', |
| 2591 Polymer.IronResizableBehavior, | 4752 tabindex: '0', |
| 2592 Polymer.IronA11yKeysBehavior, | 4753 animated: true |
| 2593 Polymer.IronScrollTargetBehavior | 4754 }, |
| 2594 ], | 4755 |
| 2595 | 4756 _calculateElevation: function() { |
| 2596 keyBindings: { | 4757 var e = 1; |
| 2597 'up': '_didMoveUp', | 4758 if (this.disabled) { |
| 2598 'down': '_didMoveDown', | 4759 e = 0; |
| 2599 'enter': '_didEnter' | 4760 } else if (this.active || this.pressed) { |
| 2600 }, | 4761 e = 4; |
| 2601 | 4762 } else if (this.receivedFocusFromKeyboard) { |
| 2602 /** | 4763 e = 3; |
| 2603 * The ratio of hidden tiles that should remain in the scroll direction. | 4764 } |
| 2604 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | 4765 this._setElevation(e); |
| 2605 */ | 4766 }, |
| 2606 _ratio: 0.5, | 4767 |
| 2607 | 4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
| 2608 /** | 4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
| 2609 * The padding-top value for the list. | 4770 }, |
| 2610 */ | 4771 |
| 2611 _scrollerPaddingTop: 0, | 4772 /** |
| 2612 | 4773 * In addition to `IronButtonState` behavior, when space key goes down, |
| 2613 /** | 4774 * create a ripple down effect. |
| 2614 * This value is the same as `scrollTop`. | |
| 2615 */ | |
| 2616 _scrollPosition: 0, | |
| 2617 | |
| 2618 /** | |
| 2619 * The sum of the heights of all the tiles in the DOM. | |
| 2620 */ | |
| 2621 _physicalSize: 0, | |
| 2622 | |
| 2623 /** | |
| 2624 * The average `offsetHeight` of the tiles observed till now. | |
| 2625 */ | |
| 2626 _physicalAverage: 0, | |
| 2627 | |
| 2628 /** | |
| 2629 * The number of tiles which `offsetHeight` > 0 observed until now. | |
| 2630 */ | |
| 2631 _physicalAverageCount: 0, | |
| 2632 | |
| 2633 /** | |
| 2634 * The Y position of the item rendered in the `_physicalStart` | |
| 2635 * tile relative to the scrolling list. | |
| 2636 */ | |
| 2637 _physicalTop: 0, | |
| 2638 | |
| 2639 /** | |
| 2640 * The number of items in the list. | |
| 2641 */ | |
| 2642 _virtualCount: 0, | |
| 2643 | |
| 2644 /** | |
| 2645 * A map between an item key and its physical item index | |
| 2646 */ | |
| 2647 _physicalIndexForKey: null, | |
| 2648 | |
| 2649 /** | |
| 2650 * The estimated scroll height based on `_physicalAverage` | |
| 2651 */ | |
| 2652 _estScrollHeight: 0, | |
| 2653 | |
| 2654 /** | |
| 2655 * The scroll height of the dom node | |
| 2656 */ | |
| 2657 _scrollHeight: 0, | |
| 2658 | |
| 2659 /** | |
| 2660 * The height of the list. This is referred as the viewport in the context o
f list. | |
| 2661 */ | |
| 2662 _viewportHeight: 0, | |
| 2663 | |
| 2664 /** | |
| 2665 * The width of the list. This is referred as the viewport in the context of
list. | |
| 2666 */ | |
| 2667 _viewportWidth: 0, | |
| 2668 | |
| 2669 /** | |
| 2670 * An array of DOM nodes that are currently in the tree | |
| 2671 * @type {?Array<!TemplatizerNode>} | |
| 2672 */ | |
| 2673 _physicalItems: null, | |
| 2674 | |
| 2675 /** | |
| 2676 * An array of heights for each item in `_physicalItems` | |
| 2677 * @type {?Array<number>} | |
| 2678 */ | |
| 2679 _physicalSizes: null, | |
| 2680 | |
| 2681 /** | |
| 2682 * A cached value for the first visible index. | |
| 2683 * See `firstVisibleIndex` | |
| 2684 * @type {?number} | |
| 2685 */ | |
| 2686 _firstVisibleIndexVal: null, | |
| 2687 | |
| 2688 /** | |
| 2689 * A cached value for the last visible index. | |
| 2690 * See `lastVisibleIndex` | |
| 2691 * @type {?number} | |
| 2692 */ | |
| 2693 _lastVisibleIndexVal: null, | |
| 2694 | |
| 2695 /** | |
| 2696 * A Polymer collection for the items. | |
| 2697 * @type {?Polymer.Collection} | |
| 2698 */ | |
| 2699 _collection: null, | |
| 2700 | |
| 2701 /** | |
| 2702 * True if the current item list was rendered for the first time | |
| 2703 * after attached. | |
| 2704 */ | |
| 2705 _itemsRendered: false, | |
| 2706 | |
| 2707 /** | |
| 2708 * The page that is currently rendered. | |
| 2709 */ | |
| 2710 _lastPage: null, | |
| 2711 | |
| 2712 /** | |
| 2713 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
| 2714 */ | |
| 2715 _maxPages: 3, | |
| 2716 | |
| 2717 /** | |
| 2718 * The currently focused physical item. | |
| 2719 */ | |
| 2720 _focusedItem: null, | |
| 2721 | |
| 2722 /** | |
| 2723 * The index of the `_focusedItem`. | |
| 2724 */ | |
| 2725 _focusedIndex: -1, | |
| 2726 | |
| 2727 /** | |
| 2728 * The the item that is focused if it is moved offscreen. | |
| 2729 * @private {?TemplatizerNode} | |
| 2730 */ | |
| 2731 _offscreenFocusedItem: null, | |
| 2732 | |
| 2733 /** | |
| 2734 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
| 2735 * list when that item is moved offscreen. | |
| 2736 */ | |
| 2737 _focusBackfillItem: null, | |
| 2738 | |
| 2739 /** | |
| 2740 * The maximum items per row | |
| 2741 */ | |
| 2742 _itemsPerRow: 1, | |
| 2743 | |
| 2744 /** | |
| 2745 * The width of each grid item | |
| 2746 */ | |
| 2747 _itemWidth: 0, | |
| 2748 | |
| 2749 /** | |
| 2750 * The height of the row in grid layout. | |
| 2751 */ | |
| 2752 _rowHeight: 0, | |
| 2753 | |
| 2754 /** | |
| 2755 * The bottom of the physical content. | |
| 2756 */ | |
| 2757 get _physicalBottom() { | |
| 2758 return this._physicalTop + this._physicalSize; | |
| 2759 }, | |
| 2760 | |
| 2761 /** | |
| 2762 * The bottom of the scroll. | |
| 2763 */ | |
| 2764 get _scrollBottom() { | |
| 2765 return this._scrollPosition + this._viewportHeight; | |
| 2766 }, | |
| 2767 | |
| 2768 /** | |
| 2769 * The n-th item rendered in the last physical item. | |
| 2770 */ | |
| 2771 get _virtualEnd() { | |
| 2772 return this._virtualStart + this._physicalCount - 1; | |
| 2773 }, | |
| 2774 | |
| 2775 /** | |
| 2776 * The height of the physical content that isn't on the screen. | |
| 2777 */ | |
| 2778 get _hiddenContentSize() { | |
| 2779 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | |
| 2780 return size - this._viewportHeight; | |
| 2781 }, | |
| 2782 | |
| 2783 /** | |
| 2784 * The maximum scroll top value. | |
| 2785 */ | |
| 2786 get _maxScrollTop() { | |
| 2787 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | |
| 2788 }, | |
| 2789 | |
| 2790 /** | |
| 2791 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
| 2792 */ | |
| 2793 _minVirtualStart: 0, | |
| 2794 | |
| 2795 /** | |
| 2796 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
| 2797 */ | |
| 2798 get _maxVirtualStart() { | |
| 2799 return Math.max(0, this._virtualCount - this._physicalCount); | |
| 2800 }, | |
| 2801 | |
| 2802 /** | |
| 2803 * The n-th item rendered in the `_physicalStart` tile. | |
| 2804 */ | |
| 2805 _virtualStartVal: 0, | |
| 2806 | |
| 2807 set _virtualStart(val) { | |
| 2808 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | |
| 2809 }, | |
| 2810 | |
| 2811 get _virtualStart() { | |
| 2812 return this._virtualStartVal || 0; | |
| 2813 }, | |
| 2814 | |
| 2815 /** | |
| 2816 * The k-th tile that is at the top of the scrolling list. | |
| 2817 */ | |
| 2818 _physicalStartVal: 0, | |
| 2819 | |
| 2820 set _physicalStart(val) { | |
| 2821 this._physicalStartVal = val % this._physicalCount; | |
| 2822 if (this._physicalStartVal < 0) { | |
| 2823 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | |
| 2824 } | |
| 2825 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | |
| 2826 }, | |
| 2827 | |
| 2828 get _physicalStart() { | |
| 2829 return this._physicalStartVal || 0; | |
| 2830 }, | |
| 2831 | |
| 2832 /** | |
| 2833 * The number of tiles in the DOM. | |
| 2834 */ | |
| 2835 _physicalCountVal: 0, | |
| 2836 | |
| 2837 set _physicalCount(val) { | |
| 2838 this._physicalCountVal = val; | |
| 2839 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | |
| 2840 }, | |
| 2841 | |
| 2842 get _physicalCount() { | |
| 2843 return this._physicalCountVal; | |
| 2844 }, | |
| 2845 | |
| 2846 /** | |
| 2847 * The k-th tile that is at the bottom of the scrolling list. | |
| 2848 */ | |
| 2849 _physicalEnd: 0, | |
| 2850 | |
| 2851 /** | |
| 2852 * An optimal physical size such that we will have enough physical items | |
| 2853 * to fill up the viewport and recycle when the user scrolls. | |
| 2854 * | 4775 * |
| 2855 * This default value assumes that we will at least have the equivalent | 4776 * @param {!KeyboardEvent} event . |
| 2856 * to a viewport of physical items above and below the user's viewport. | 4777 */ |
| 2857 */ | 4778 _spaceKeyDownHandler: function(event) { |
| 2858 get _optPhysicalSize() { | 4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
| 2859 if (this.grid) { | 4780 // Ensure that there is at most one ripple when the space key is held down
. |
| 2860 return this._estRowsInView * this._rowHeight * this._maxPages; | 4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
| 2861 } | 4782 this._ripple.uiDownAction(); |
| 2862 return this._viewportHeight * this._maxPages; | 4783 } |
| 2863 }, | 4784 }, |
| 2864 | 4785 |
| 2865 get _optPhysicalCount() { | 4786 /** |
| 2866 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 4787 * In addition to `IronButtonState` behavior, when space key goes up, |
| 2867 }, | 4788 * create a ripple up effect. |
| 2868 | |
| 2869 /** | |
| 2870 * True if the current list is visible. | |
| 2871 */ | |
| 2872 get _isVisible() { | |
| 2873 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | |
| 2874 }, | |
| 2875 | |
| 2876 /** | |
| 2877 * Gets the index of the first visible item in the viewport. | |
| 2878 * | 4789 * |
| 2879 * @type {number} | 4790 * @param {!KeyboardEvent} event . |
| 2880 */ | 4791 */ |
| 2881 get firstVisibleIndex() { | 4792 _spaceKeyUpHandler: function(event) { |
| 2882 if (this._firstVisibleIndexVal === null) { | 4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
| 2883 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 4794 if (this.hasRipple()) { |
| 2884 | 4795 this._ripple.uiUpAction(); |
| 2885 this._firstVisibleIndexVal = this._iterateItems( | 4796 } |
| 2886 function(pidx, vidx) { | 4797 } |
| 2887 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 4798 }; |
| 2888 | 4799 |
| 2889 if (physicalOffset > this._scrollPosition) { | 4800 /** @polymerBehavior */ |
| 2890 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 4801 Polymer.PaperButtonBehavior = [ |
| 2891 } | 4802 Polymer.IronButtonState, |
| 2892 // Handle a partially rendered final row in grid mode | 4803 Polymer.IronControlState, |
| 2893 if (this.grid && this._virtualCount - 1 === vidx) { | 4804 Polymer.PaperRippleBehavior, |
| 2894 return vidx - (vidx % this._itemsPerRow); | 4805 Polymer.PaperButtonBehaviorImpl |
| 2895 } | 4806 ]; |
| 2896 }) || 0; | 4807 Polymer({ |
| 2897 } | 4808 is: 'paper-button', |
| 2898 return this._firstVisibleIndexVal; | 4809 |
| 2899 }, | 4810 behaviors: [ |
| 2900 | 4811 Polymer.PaperButtonBehavior |
| 2901 /** | 4812 ], |
| 2902 * Gets the index of the last visible item in the viewport. | 4813 |
| 2903 * | 4814 properties: { |
| 2904 * @type {number} | 4815 /** |
| 2905 */ | 4816 * If true, the button should be styled with a shadow. |
| 2906 get lastVisibleIndex() { | 4817 */ |
| 2907 if (this._lastVisibleIndexVal === null) { | 4818 raised: { |
| 2908 if (this.grid) { | 4819 type: Boolean, |
| 2909 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 4820 reflectToAttribute: true, |
| 2910 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 4821 value: false, |
| 4822 observer: '_calculateElevation' |
| 4823 } |
| 4824 }, |
| 4825 |
| 4826 _calculateElevation: function() { |
| 4827 if (!this.raised) { |
| 4828 this._setElevation(0); |
| 2911 } else { | 4829 } else { |
| 2912 var physicalOffset = this._physicalTop; | 4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
| 2913 this._iterateItems(function(pidx, vidx) { | 4831 } |
| 2914 if (physicalOffset < this._scrollBottom) { | 4832 } |
| 2915 this._lastVisibleIndexVal = vidx; | 4833 |
| 2916 } else { | 4834 /** |
| 2917 // Break _iterateItems | 4835 Fired when the animation finishes. |
| 2918 return true; | 4836 This is useful if you want to wait until |
| 2919 } | 4837 the ripple animation finishes to perform some action. |
| 2920 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 4838 |
| 2921 }); | 4839 @event transitionend |
| 2922 } | 4840 Event param: {{node: Object}} detail Contains the animated node. |
| 2923 } | 4841 */ |
| 2924 return this._lastVisibleIndexVal; | 4842 }); |
| 2925 }, | |
| 2926 | |
| 2927 get _defaultScrollTarget() { | |
| 2928 return this; | |
| 2929 }, | |
| 2930 get _virtualRowCount() { | |
| 2931 return Math.ceil(this._virtualCount / this._itemsPerRow); | |
| 2932 }, | |
| 2933 | |
| 2934 get _estRowsInView() { | |
| 2935 return Math.ceil(this._viewportHeight / this._rowHeight); | |
| 2936 }, | |
| 2937 | |
| 2938 get _physicalRows() { | |
| 2939 return Math.ceil(this._physicalCount / this._itemsPerRow); | |
| 2940 }, | |
| 2941 | |
| 2942 ready: function() { | |
| 2943 this.addEventListener('focus', this._didFocus.bind(this), true); | |
| 2944 }, | |
| 2945 | |
| 2946 attached: function() { | |
| 2947 this.updateViewportBoundaries(); | |
| 2948 this._render(); | |
| 2949 // `iron-resize` is fired when the list is attached if the event is added | |
| 2950 // before attached causing unnecessary work. | |
| 2951 this.listen(this, 'iron-resize', '_resizeHandler'); | |
| 2952 }, | |
| 2953 | |
| 2954 detached: function() { | |
| 2955 this._itemsRendered = false; | |
| 2956 this.unlisten(this, 'iron-resize', '_resizeHandler'); | |
| 2957 }, | |
| 2958 | |
| 2959 /** | |
| 2960 * Set the overflow property if this element has its own scrolling region | |
| 2961 */ | |
| 2962 _setOverflow: function(scrollTarget) { | |
| 2963 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | |
| 2964 this.style.overflow = scrollTarget === this ? 'auto' : ''; | |
| 2965 }, | |
| 2966 | |
| 2967 /** | |
| 2968 * Invoke this method if you dynamically update the viewport's | |
| 2969 * size or CSS padding. | |
| 2970 * | |
| 2971 * @method updateViewportBoundaries | |
| 2972 */ | |
| 2973 updateViewportBoundaries: function() { | |
| 2974 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | |
| 2975 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
| 2976 | |
| 2977 this._viewportHeight = this._scrollTargetHeight; | |
| 2978 if (this.grid) { | |
| 2979 this._updateGridMetrics(); | |
| 2980 } | |
| 2981 }, | |
| 2982 | |
| 2983 /** | |
| 2984 * Update the models, the position of the | |
| 2985 * items in the viewport and recycle tiles as needed. | |
| 2986 */ | |
| 2987 _scrollHandler: function() { | |
| 2988 // clamp the `scrollTop` value | |
| 2989 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | |
| 2990 var delta = scrollTop - this._scrollPosition; | |
| 2991 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | |
| 2992 var ratio = this._ratio; | |
| 2993 var recycledTiles = 0; | |
| 2994 var hiddenContentSize = this._hiddenContentSize; | |
| 2995 var currentRatio = ratio; | |
| 2996 var movingUp = []; | |
| 2997 | |
| 2998 // track the last `scrollTop` | |
| 2999 this._scrollPosition = scrollTop; | |
| 3000 | |
| 3001 // clear cached visible indexes | |
| 3002 this._firstVisibleIndexVal = null; | |
| 3003 this._lastVisibleIndexVal = null; | |
| 3004 | |
| 3005 scrollBottom = this._scrollBottom; | |
| 3006 physicalBottom = this._physicalBottom; | |
| 3007 | |
| 3008 // random access | |
| 3009 if (Math.abs(delta) > this._physicalSize) { | |
| 3010 this._physicalTop += delta; | |
| 3011 recycledTiles = Math.round(delta / this._physicalAverage); | |
| 3012 } | |
| 3013 // scroll up | |
| 3014 else if (delta < 0) { | |
| 3015 var topSpace = scrollTop - this._physicalTop; | |
| 3016 var virtualStart = this._virtualStart; | |
| 3017 | |
| 3018 recycledTileSet = []; | |
| 3019 | |
| 3020 kth = this._physicalEnd; | |
| 3021 currentRatio = topSpace / hiddenContentSize; | |
| 3022 | |
| 3023 // move tiles from bottom to top | |
| 3024 while ( | |
| 3025 // approximate `currentRatio` to `ratio` | |
| 3026 currentRatio < ratio && | |
| 3027 // recycle less physical items than the total | |
| 3028 recycledTiles < this._physicalCount && | |
| 3029 // ensure that these recycled tiles are needed | |
| 3030 virtualStart - recycledTiles > 0 && | |
| 3031 // ensure that the tile is not visible | |
| 3032 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
| 3033 ) { | |
| 3034 | |
| 3035 tileHeight = this._getPhysicalSizeIncrement(kth); | |
| 3036 currentRatio += tileHeight / hiddenContentSize; | |
| 3037 physicalBottom -= tileHeight; | |
| 3038 recycledTileSet.push(kth); | |
| 3039 recycledTiles++; | |
| 3040 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | |
| 3041 } | |
| 3042 | |
| 3043 movingUp = recycledTileSet; | |
| 3044 recycledTiles = -recycledTiles; | |
| 3045 } | |
| 3046 // scroll down | |
| 3047 else if (delta > 0) { | |
| 3048 var bottomSpace = physicalBottom - scrollBottom; | |
| 3049 var virtualEnd = this._virtualEnd; | |
| 3050 var lastVirtualItemIndex = this._virtualCount-1; | |
| 3051 | |
| 3052 recycledTileSet = []; | |
| 3053 | |
| 3054 kth = this._physicalStart; | |
| 3055 currentRatio = bottomSpace / hiddenContentSize; | |
| 3056 | |
| 3057 // move tiles from top to bottom | |
| 3058 while ( | |
| 3059 // approximate `currentRatio` to `ratio` | |
| 3060 currentRatio < ratio && | |
| 3061 // recycle less physical items than the total | |
| 3062 recycledTiles < this._physicalCount && | |
| 3063 // ensure that these recycled tiles are needed | |
| 3064 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
| 3065 // ensure that the tile is not visible | |
| 3066 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
| 3067 ) { | |
| 3068 | |
| 3069 tileHeight = this._getPhysicalSizeIncrement(kth); | |
| 3070 currentRatio += tileHeight / hiddenContentSize; | |
| 3071 | |
| 3072 this._physicalTop += tileHeight; | |
| 3073 recycledTileSet.push(kth); | |
| 3074 recycledTiles++; | |
| 3075 kth = (kth + 1) % this._physicalCount; | |
| 3076 } | |
| 3077 } | |
| 3078 | |
| 3079 if (recycledTiles === 0) { | |
| 3080 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
| 3081 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | |
| 3082 this._increasePoolIfNeeded(); | |
| 3083 } | |
| 3084 } else { | |
| 3085 this._virtualStart = this._virtualStart + recycledTiles; | |
| 3086 this._physicalStart = this._physicalStart + recycledTiles; | |
| 3087 this._update(recycledTileSet, movingUp); | |
| 3088 } | |
| 3089 }, | |
| 3090 | |
| 3091 /** | |
| 3092 * Update the list of items, starting from the `_virtualStart` item. | |
| 3093 * @param {!Array<number>=} itemSet | |
| 3094 * @param {!Array<number>=} movingUp | |
| 3095 */ | |
| 3096 _update: function(itemSet, movingUp) { | |
| 3097 // manage focus | |
| 3098 this._manageFocus(); | |
| 3099 // update models | |
| 3100 this._assignModels(itemSet); | |
| 3101 // measure heights | |
| 3102 this._updateMetrics(itemSet); | |
| 3103 // adjust offset after measuring | |
| 3104 if (movingUp) { | |
| 3105 while (movingUp.length) { | |
| 3106 var idx = movingUp.pop(); | |
| 3107 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | |
| 3108 } | |
| 3109 } | |
| 3110 // update the position of the items | |
| 3111 this._positionItems(); | |
| 3112 // set the scroller size | |
| 3113 this._updateScrollerSize(); | |
| 3114 // increase the pool of physical items | |
| 3115 this._increasePoolIfNeeded(); | |
| 3116 }, | |
| 3117 | |
| 3118 /** | |
| 3119 * Creates a pool of DOM elements and attaches them to the local dom. | |
| 3120 */ | |
| 3121 _createPool: function(size) { | |
| 3122 var physicalItems = new Array(size); | |
| 3123 | |
| 3124 this._ensureTemplatized(); | |
| 3125 | |
| 3126 for (var i = 0; i < size; i++) { | |
| 3127 var inst = this.stamp(null); | |
| 3128 // First element child is item; Safari doesn't support children[0] | |
| 3129 // on a doc fragment | |
| 3130 physicalItems[i] = inst.root.querySelector('*'); | |
| 3131 Polymer.dom(this).appendChild(inst.root); | |
| 3132 } | |
| 3133 return physicalItems; | |
| 3134 }, | |
| 3135 | |
| 3136 /** | |
| 3137 * Increases the pool of physical items only if needed. | |
| 3138 * | |
| 3139 * @return {boolean} True if the pool was increased. | |
| 3140 */ | |
| 3141 _increasePoolIfNeeded: function() { | |
| 3142 // Base case 1: the list has no height. | |
| 3143 if (this._viewportHeight === 0) { | |
| 3144 return false; | |
| 3145 } | |
| 3146 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
| 3147 // with physical items, don't increase the pool. | |
| 3148 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | |
| 3149 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | |
| 3150 return false; | |
| 3151 } | |
| 3152 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
| 3153 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | |
| 3154 | |
| 3155 if (currentPage === 0) { | |
| 3156 // fill the first page | |
| 3157 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
| 3158 } else if (this._lastPage !== currentPage && isClientHeightFull) { | |
| 3159 // paint the page and defer the next increase | |
| 3160 // wait 16ms which is rough enough to get paint cycle. | |
| 3161 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | |
| 3162 } else { | |
| 3163 // fill the rest of the pages | |
| 3164 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | |
| 3165 } | |
| 3166 | |
| 3167 this._lastPage = currentPage; | |
| 3168 | |
| 3169 return true; | |
| 3170 }, | |
| 3171 | |
| 3172 /** | |
| 3173 * Increases the pool size. | |
| 3174 */ | |
| 3175 _increasePool: function(missingItems) { | |
| 3176 var nextPhysicalCount = Math.min( | |
| 3177 this._physicalCount + missingItems, | |
| 3178 this._virtualCount - this._virtualStart, | |
| 3179 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
| 3180 ); | |
| 3181 var prevPhysicalCount = this._physicalCount; | |
| 3182 var delta = nextPhysicalCount - prevPhysicalCount; | |
| 3183 | |
| 3184 if (delta <= 0) { | |
| 3185 return; | |
| 3186 } | |
| 3187 | |
| 3188 [].push.apply(this._physicalItems, this._createPool(delta)); | |
| 3189 [].push.apply(this._physicalSizes, new Array(delta)); | |
| 3190 | |
| 3191 this._physicalCount = prevPhysicalCount + delta; | |
| 3192 | |
| 3193 // update the physical start if we need to preserve the model of the focus
ed item. | |
| 3194 // In this situation, the focused item is currently rendered and its model
would | |
| 3195 // have changed after increasing the pool if the physical start remained u
nchanged. | |
| 3196 if (this._physicalStart > this._physicalEnd && | |
| 3197 this._isIndexRendered(this._focusedIndex) && | |
| 3198 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
| 3199 this._physicalStart = this._physicalStart + delta; | |
| 3200 } | |
| 3201 this._update(); | |
| 3202 }, | |
| 3203 | |
| 3204 /** | |
| 3205 * Render a new list of items. This method does exactly the same as `update`
, | |
| 3206 * but it also ensures that only one `update` cycle is created. | |
| 3207 */ | |
| 3208 _render: function() { | |
| 3209 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | |
| 3210 | |
| 3211 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | |
| 3212 this._lastPage = 0; | |
| 3213 this._update(); | |
| 3214 this._itemsRendered = true; | |
| 3215 } | |
| 3216 }, | |
| 3217 | |
| 3218 /** | |
| 3219 * Templetizes the user template. | |
| 3220 */ | |
| 3221 _ensureTemplatized: function() { | |
| 3222 if (!this.ctor) { | |
| 3223 // Template instance props that should be excluded from forwarding | |
| 3224 var props = {}; | |
| 3225 props.__key__ = true; | |
| 3226 props[this.as] = true; | |
| 3227 props[this.indexAs] = true; | |
| 3228 props[this.selectedAs] = true; | |
| 3229 props.tabIndex = true; | |
| 3230 | |
| 3231 this._instanceProps = props; | |
| 3232 this._userTemplate = Polymer.dom(this).querySelector('template'); | |
| 3233 | |
| 3234 if (this._userTemplate) { | |
| 3235 this.templatize(this._userTemplate); | |
| 3236 } else { | |
| 3237 console.warn('iron-list requires a template to be provided in light-do
m'); | |
| 3238 } | |
| 3239 } | |
| 3240 }, | |
| 3241 | |
| 3242 /** | |
| 3243 * Implements extension point from Templatizer mixin. | |
| 3244 */ | |
| 3245 _getStampedChildren: function() { | |
| 3246 return this._physicalItems; | |
| 3247 }, | |
| 3248 | |
| 3249 /** | |
| 3250 * Implements extension point from Templatizer | |
| 3251 * Called as a side effect of a template instance path change, responsible | |
| 3252 * for notifying items.<key-for-instance>.<path> change up to host. | |
| 3253 */ | |
| 3254 _forwardInstancePath: function(inst, path, value) { | |
| 3255 if (path.indexOf(this.as + '.') === 0) { | |
| 3256 this.notifyPath('items.' + inst.__key__ + '.' + | |
| 3257 path.slice(this.as.length + 1), value); | |
| 3258 } | |
| 3259 }, | |
| 3260 | |
| 3261 /** | |
| 3262 * Implements extension point from Templatizer mixin | |
| 3263 * Called as side-effect of a host property change, responsible for | |
| 3264 * notifying parent path change on each row. | |
| 3265 */ | |
| 3266 _forwardParentProp: function(prop, value) { | |
| 3267 if (this._physicalItems) { | |
| 3268 this._physicalItems.forEach(function(item) { | |
| 3269 item._templateInstance[prop] = value; | |
| 3270 }, this); | |
| 3271 } | |
| 3272 }, | |
| 3273 | |
| 3274 /** | |
| 3275 * Implements extension point from Templatizer | |
| 3276 * Called as side-effect of a host path change, responsible for | |
| 3277 * notifying parent.<path> path change on each row. | |
| 3278 */ | |
| 3279 _forwardParentPath: function(path, value) { | |
| 3280 if (this._physicalItems) { | |
| 3281 this._physicalItems.forEach(function(item) { | |
| 3282 item._templateInstance.notifyPath(path, value, true); | |
| 3283 }, this); | |
| 3284 } | |
| 3285 }, | |
| 3286 | |
| 3287 /** | |
| 3288 * Called as a side effect of a host items.<key>.<path> path change, | |
| 3289 * responsible for notifying item.<path> changes. | |
| 3290 */ | |
| 3291 _forwardItemPath: function(path, value) { | |
| 3292 if (!this._physicalIndexForKey) { | |
| 3293 return; | |
| 3294 } | |
| 3295 var dot = path.indexOf('.'); | |
| 3296 var key = path.substring(0, dot < 0 ? path.length : dot); | |
| 3297 var idx = this._physicalIndexForKey[key]; | |
| 3298 var offscreenItem = this._offscreenFocusedItem; | |
| 3299 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | |
| 3300 offscreenItem : this._physicalItems[idx]; | |
| 3301 | |
| 3302 if (!el || el._templateInstance.__key__ !== key) { | |
| 3303 return; | |
| 3304 } | |
| 3305 if (dot >= 0) { | |
| 3306 path = this.as + '.' + path.substring(dot+1); | |
| 3307 el._templateInstance.notifyPath(path, value, true); | |
| 3308 } else { | |
| 3309 // Update selection if needed | |
| 3310 var currentItem = el._templateInstance[this.as]; | |
| 3311 if (Array.isArray(this.selectedItems)) { | |
| 3312 for (var i = 0; i < this.selectedItems.length; i++) { | |
| 3313 if (this.selectedItems[i] === currentItem) { | |
| 3314 this.set('selectedItems.' + i, value); | |
| 3315 break; | |
| 3316 } | |
| 3317 } | |
| 3318 } else if (this.selectedItem === currentItem) { | |
| 3319 this.set('selectedItem', value); | |
| 3320 } | |
| 3321 el._templateInstance[this.as] = value; | |
| 3322 } | |
| 3323 }, | |
| 3324 | |
| 3325 /** | |
| 3326 * Called when the items have changed. That is, ressignments | |
| 3327 * to `items`, splices or updates to a single item. | |
| 3328 */ | |
| 3329 _itemsChanged: function(change) { | |
| 3330 if (change.path === 'items') { | |
| 3331 // reset items | |
| 3332 this._virtualStart = 0; | |
| 3333 this._physicalTop = 0; | |
| 3334 this._virtualCount = this.items ? this.items.length : 0; | |
| 3335 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | |
| 3336 this._physicalIndexForKey = {}; | |
| 3337 this._firstVisibleIndexVal = null; | |
| 3338 this._lastVisibleIndexVal = null; | |
| 3339 | |
| 3340 this._resetScrollPosition(0); | |
| 3341 this._removeFocusedItem(); | |
| 3342 // create the initial physical items | |
| 3343 if (!this._physicalItems) { | |
| 3344 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | |
| 3345 this._physicalItems = this._createPool(this._physicalCount); | |
| 3346 this._physicalSizes = new Array(this._physicalCount); | |
| 3347 } | |
| 3348 | |
| 3349 this._physicalStart = 0; | |
| 3350 | |
| 3351 } else if (change.path === 'items.splices') { | |
| 3352 | |
| 3353 this._adjustVirtualIndex(change.value.indexSplices); | |
| 3354 this._virtualCount = this.items ? this.items.length : 0; | |
| 3355 | |
| 3356 } else { | |
| 3357 // update a single item | |
| 3358 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | |
| 3359 return; | |
| 3360 } | |
| 3361 | |
| 3362 this._itemsRendered = false; | |
| 3363 this._debounceTemplate(this._render); | |
| 3364 }, | |
| 3365 | |
| 3366 /** | |
| 3367 * @param {!Array<!PolymerSplice>} splices | |
| 3368 */ | |
| 3369 _adjustVirtualIndex: function(splices) { | |
| 3370 splices.forEach(function(splice) { | |
| 3371 // deselect removed items | |
| 3372 splice.removed.forEach(this._removeItem, this); | |
| 3373 // We only need to care about changes happening above the current positi
on | |
| 3374 if (splice.index < this._virtualStart) { | |
| 3375 var delta = Math.max( | |
| 3376 splice.addedCount - splice.removed.length, | |
| 3377 splice.index - this._virtualStart); | |
| 3378 | |
| 3379 this._virtualStart = this._virtualStart + delta; | |
| 3380 | |
| 3381 if (this._focusedIndex >= 0) { | |
| 3382 this._focusedIndex = this._focusedIndex + delta; | |
| 3383 } | |
| 3384 } | |
| 3385 }, this); | |
| 3386 }, | |
| 3387 | |
| 3388 _removeItem: function(item) { | |
| 3389 this.$.selector.deselect(item); | |
| 3390 // remove the current focused item | |
| 3391 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | |
| 3392 this._removeFocusedItem(); | |
| 3393 } | |
| 3394 }, | |
| 3395 | |
| 3396 /** | |
| 3397 * Executes a provided function per every physical index in `itemSet` | |
| 3398 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
| 3399 * | |
| 3400 * @param {!function(number, number)} fn | |
| 3401 * @param {!Array<number>=} itemSet | |
| 3402 */ | |
| 3403 _iterateItems: function(fn, itemSet) { | |
| 3404 var pidx, vidx, rtn, i; | |
| 3405 | |
| 3406 if (arguments.length === 2 && itemSet) { | |
| 3407 for (i = 0; i < itemSet.length; i++) { | |
| 3408 pidx = itemSet[i]; | |
| 3409 vidx = this._computeVidx(pidx); | |
| 3410 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
| 3411 return rtn; | |
| 3412 } | |
| 3413 } | |
| 3414 } else { | |
| 3415 pidx = this._physicalStart; | |
| 3416 vidx = this._virtualStart; | |
| 3417 | |
| 3418 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
| 3419 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
| 3420 return rtn; | |
| 3421 } | |
| 3422 } | |
| 3423 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | |
| 3424 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
| 3425 return rtn; | |
| 3426 } | |
| 3427 } | |
| 3428 } | |
| 3429 }, | |
| 3430 | |
| 3431 /** | |
| 3432 * Returns the virtual index for a given physical index | |
| 3433 * | |
| 3434 * @param {number} pidx Physical index | |
| 3435 * @return {number} | |
| 3436 */ | |
| 3437 _computeVidx: function(pidx) { | |
| 3438 if (pidx >= this._physicalStart) { | |
| 3439 return this._virtualStart + (pidx - this._physicalStart); | |
| 3440 } | |
| 3441 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | |
| 3442 }, | |
| 3443 | |
| 3444 /** | |
| 3445 * Assigns the data models to a given set of items. | |
| 3446 * @param {!Array<number>=} itemSet | |
| 3447 */ | |
| 3448 _assignModels: function(itemSet) { | |
| 3449 this._iterateItems(function(pidx, vidx) { | |
| 3450 var el = this._physicalItems[pidx]; | |
| 3451 var inst = el._templateInstance; | |
| 3452 var item = this.items && this.items[vidx]; | |
| 3453 | |
| 3454 if (item != null) { | |
| 3455 inst[this.as] = item; | |
| 3456 inst.__key__ = this._collection.getKey(item); | |
| 3457 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | |
| 3458 inst[this.indexAs] = vidx; | |
| 3459 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | |
| 3460 this._physicalIndexForKey[inst.__key__] = pidx; | |
| 3461 el.removeAttribute('hidden'); | |
| 3462 } else { | |
| 3463 inst.__key__ = null; | |
| 3464 el.setAttribute('hidden', ''); | |
| 3465 } | |
| 3466 }, itemSet); | |
| 3467 }, | |
| 3468 | |
| 3469 /** | |
| 3470 * Updates the height for a given set of items. | |
| 3471 * | |
| 3472 * @param {!Array<number>=} itemSet | |
| 3473 */ | |
| 3474 _updateMetrics: function(itemSet) { | |
| 3475 // Make sure we distributed all the physical items | |
| 3476 // so we can measure them | |
| 3477 Polymer.dom.flush(); | |
| 3478 | |
| 3479 var newPhysicalSize = 0; | |
| 3480 var oldPhysicalSize = 0; | |
| 3481 var prevAvgCount = this._physicalAverageCount; | |
| 3482 var prevPhysicalAvg = this._physicalAverage; | |
| 3483 | |
| 3484 this._iterateItems(function(pidx, vidx) { | |
| 3485 | |
| 3486 oldPhysicalSize += this._physicalSizes[pidx] || 0; | |
| 3487 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | |
| 3488 newPhysicalSize += this._physicalSizes[pidx]; | |
| 3489 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | |
| 3490 | |
| 3491 }, itemSet); | |
| 3492 | |
| 3493 this._viewportHeight = this._scrollTargetHeight; | |
| 3494 if (this.grid) { | |
| 3495 this._updateGridMetrics(); | |
| 3496 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | |
| 3497 } else { | |
| 3498 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | |
| 3499 } | |
| 3500 | |
| 3501 // update the average if we measured something | |
| 3502 if (this._physicalAverageCount !== prevAvgCount) { | |
| 3503 this._physicalAverage = Math.round( | |
| 3504 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
| 3505 this._physicalAverageCount); | |
| 3506 } | |
| 3507 }, | |
| 3508 | |
| 3509 _updateGridMetrics: function() { | |
| 3510 this._viewportWidth = this.$.items.offsetWidth; | |
| 3511 // Set item width to the value of the _physicalItems offsetWidth | |
| 3512 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | |
| 3513 // Set row height to the value of the _physicalItems offsetHeight | |
| 3514 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | |
| 3515 // If in grid mode compute how many items with exist in each row | |
| 3516 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | |
| 3517 }, | |
| 3518 | |
| 3519 /** | |
| 3520 * Updates the position of the physical items. | |
| 3521 */ | |
| 3522 _positionItems: function() { | |
| 3523 this._adjustScrollPosition(); | |
| 3524 | |
| 3525 var y = this._physicalTop; | |
| 3526 | |
| 3527 if (this.grid) { | |
| 3528 var totalItemWidth = this._itemsPerRow * this._itemWidth; | |
| 3529 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | |
| 3530 | |
| 3531 this._iterateItems(function(pidx, vidx) { | |
| 3532 | |
| 3533 var modulus = vidx % this._itemsPerRow; | |
| 3534 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | |
| 3535 | |
| 3536 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | |
| 3537 | |
| 3538 if (this._shouldRenderNextRow(vidx)) { | |
| 3539 y += this._rowHeight; | |
| 3540 } | |
| 3541 | |
| 3542 }); | |
| 3543 } else { | |
| 3544 this._iterateItems(function(pidx, vidx) { | |
| 3545 | |
| 3546 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | |
| 3547 y += this._physicalSizes[pidx]; | |
| 3548 | |
| 3549 }); | |
| 3550 } | |
| 3551 }, | |
| 3552 | |
| 3553 _getPhysicalSizeIncrement: function(pidx) { | |
| 3554 if (!this.grid) { | |
| 3555 return this._physicalSizes[pidx]; | |
| 3556 } | |
| 3557 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | |
| 3558 return 0; | |
| 3559 } | |
| 3560 return this._rowHeight; | |
| 3561 }, | |
| 3562 | |
| 3563 /** | |
| 3564 * Returns, based on the current index, | |
| 3565 * whether or not the next index will need | |
| 3566 * to be rendered on a new row. | |
| 3567 * | |
| 3568 * @param {number} vidx Virtual index | |
| 3569 * @return {boolean} | |
| 3570 */ | |
| 3571 _shouldRenderNextRow: function(vidx) { | |
| 3572 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | |
| 3573 }, | |
| 3574 | |
| 3575 /** | |
| 3576 * Adjusts the scroll position when it was overestimated. | |
| 3577 */ | |
| 3578 _adjustScrollPosition: function() { | |
| 3579 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | |
| 3580 Math.min(this._scrollPosition + this._physicalTop, 0); | |
| 3581 | |
| 3582 if (deltaHeight) { | |
| 3583 this._physicalTop = this._physicalTop - deltaHeight; | |
| 3584 // juking scroll position during interial scrolling on iOS is no bueno | |
| 3585 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | |
| 3586 this._resetScrollPosition(this._scrollTop - deltaHeight); | |
| 3587 } | |
| 3588 } | |
| 3589 }, | |
| 3590 | |
| 3591 /** | |
| 3592 * Sets the position of the scroll. | |
| 3593 */ | |
| 3594 _resetScrollPosition: function(pos) { | |
| 3595 if (this.scrollTarget) { | |
| 3596 this._scrollTop = pos; | |
| 3597 this._scrollPosition = this._scrollTop; | |
| 3598 } | |
| 3599 }, | |
| 3600 | |
| 3601 /** | |
| 3602 * Sets the scroll height, that's the height of the content, | |
| 3603 * | |
| 3604 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
| 3605 */ | |
| 3606 _updateScrollerSize: function(forceUpdate) { | |
| 3607 if (this.grid) { | |
| 3608 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | |
| 3609 } else { | |
| 3610 this._estScrollHeight = (this._physicalBottom + | |
| 3611 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
| 3612 } | |
| 3613 | |
| 3614 forceUpdate = forceUpdate || this._scrollHeight === 0; | |
| 3615 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | |
| 3616 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | |
| 3617 | |
| 3618 // amortize height adjustment, so it won't trigger repaints very often | |
| 3619 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | |
| 3620 this.$.items.style.height = this._estScrollHeight + 'px'; | |
| 3621 this._scrollHeight = this._estScrollHeight; | |
| 3622 } | |
| 3623 }, | |
| 3624 | |
| 3625 /** | |
| 3626 * Scroll to a specific item in the virtual list regardless | |
| 3627 * of the physical items in the DOM tree. | |
| 3628 * | |
| 3629 * @method scrollToItem | |
| 3630 * @param {(Object)} item The item to be scrolled to | |
| 3631 */ | |
| 3632 scrollToItem: function(item){ | |
| 3633 return this.scrollToIndex(this.items.indexOf(item)); | |
| 3634 }, | |
| 3635 | |
| 3636 /** | |
| 3637 * Scroll to a specific index in the virtual list regardless | |
| 3638 * of the physical items in the DOM tree. | |
| 3639 * | |
| 3640 * @method scrollToIndex | |
| 3641 * @param {number} idx The index of the item | |
| 3642 */ | |
| 3643 scrollToIndex: function(idx) { | |
| 3644 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | |
| 3645 return; | |
| 3646 } | |
| 3647 | |
| 3648 Polymer.dom.flush(); | |
| 3649 | |
| 3650 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
| 3651 // update the virtual start only when needed | |
| 3652 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | |
| 3653 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | |
| 3654 } | |
| 3655 // manage focus | |
| 3656 this._manageFocus(); | |
| 3657 // assign new models | |
| 3658 this._assignModels(); | |
| 3659 // measure the new sizes | |
| 3660 this._updateMetrics(); | |
| 3661 | |
| 3662 // estimate new physical offset | |
| 3663 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
| 3664 this._physicalTop = estPhysicalTop; | |
| 3665 | |
| 3666 var currentTopItem = this._physicalStart; | |
| 3667 var currentVirtualItem = this._virtualStart; | |
| 3668 var targetOffsetTop = 0; | |
| 3669 var hiddenContentSize = this._hiddenContentSize; | |
| 3670 | |
| 3671 // scroll to the item as much as we can | |
| 3672 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | |
| 3673 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | |
| 3674 currentTopItem = (currentTopItem + 1) % this._physicalCount; | |
| 3675 currentVirtualItem++; | |
| 3676 } | |
| 3677 // update the scroller size | |
| 3678 this._updateScrollerSize(true); | |
| 3679 // update the position of the items | |
| 3680 this._positionItems(); | |
| 3681 // set the new scroll position | |
| 3682 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | |
| 3683 // increase the pool of physical items if needed | |
| 3684 this._increasePoolIfNeeded(); | |
| 3685 // clear cached visible index | |
| 3686 this._firstVisibleIndexVal = null; | |
| 3687 this._lastVisibleIndexVal = null; | |
| 3688 }, | |
| 3689 | |
| 3690 /** | |
| 3691 * Reset the physical average and the average count. | |
| 3692 */ | |
| 3693 _resetAverage: function() { | |
| 3694 this._physicalAverage = 0; | |
| 3695 this._physicalAverageCount = 0; | |
| 3696 }, | |
| 3697 | |
| 3698 /** | |
| 3699 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
| 3700 * when the element is resized. | |
| 3701 */ | |
| 3702 _resizeHandler: function() { | |
| 3703 // iOS fires the resize event when the address bar slides up | |
| 3704 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | |
| 3705 return; | |
| 3706 } | |
| 3707 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
| 3708 // changing the scroll position from a resize handler would result in | |
| 3709 // the scroll position being reset. Waiting 1ms fixes the issue. | |
| 3710 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | |
| 3711 this.updateViewportBoundaries(); | |
| 3712 this._render(); | |
| 3713 | |
| 3714 if (this._itemsRendered && this._physicalItems && this._isVisible) { | |
| 3715 this._resetAverage(); | |
| 3716 this.scrollToIndex(this.firstVisibleIndex); | |
| 3717 } | |
| 3718 }.bind(this), 1)); | |
| 3719 }, | |
| 3720 | |
| 3721 _getModelFromItem: function(item) { | |
| 3722 var key = this._collection.getKey(item); | |
| 3723 var pidx = this._physicalIndexForKey[key]; | |
| 3724 | |
| 3725 if (pidx != null) { | |
| 3726 return this._physicalItems[pidx]._templateInstance; | |
| 3727 } | |
| 3728 return null; | |
| 3729 }, | |
| 3730 | |
| 3731 /** | |
| 3732 * Gets a valid item instance from its index or the object value. | |
| 3733 * | |
| 3734 * @param {(Object|number)} item The item object or its index | |
| 3735 */ | |
| 3736 _getNormalizedItem: function(item) { | |
| 3737 if (this._collection.getKey(item) === undefined) { | |
| 3738 if (typeof item === 'number') { | |
| 3739 item = this.items[item]; | |
| 3740 if (!item) { | |
| 3741 throw new RangeError('<item> not found'); | |
| 3742 } | |
| 3743 return item; | |
| 3744 } | |
| 3745 throw new TypeError('<item> should be a valid item'); | |
| 3746 } | |
| 3747 return item; | |
| 3748 }, | |
| 3749 | |
| 3750 /** | |
| 3751 * Select the list item at the given index. | |
| 3752 * | |
| 3753 * @method selectItem | |
| 3754 * @param {(Object|number)} item The item object or its index | |
| 3755 */ | |
| 3756 selectItem: function(item) { | |
| 3757 item = this._getNormalizedItem(item); | |
| 3758 var model = this._getModelFromItem(item); | |
| 3759 | |
| 3760 if (!this.multiSelection && this.selectedItem) { | |
| 3761 this.deselectItem(this.selectedItem); | |
| 3762 } | |
| 3763 if (model) { | |
| 3764 model[this.selectedAs] = true; | |
| 3765 } | |
| 3766 this.$.selector.select(item); | |
| 3767 this.updateSizeForItem(item); | |
| 3768 }, | |
| 3769 | |
| 3770 /** | |
| 3771 * Deselects the given item list if it is already selected. | |
| 3772 * | |
| 3773 | |
| 3774 * @method deselect | |
| 3775 * @param {(Object|number)} item The item object or its index | |
| 3776 */ | |
| 3777 deselectItem: function(item) { | |
| 3778 item = this._getNormalizedItem(item); | |
| 3779 var model = this._getModelFromItem(item); | |
| 3780 | |
| 3781 if (model) { | |
| 3782 model[this.selectedAs] = false; | |
| 3783 } | |
| 3784 this.$.selector.deselect(item); | |
| 3785 this.updateSizeForItem(item); | |
| 3786 }, | |
| 3787 | |
| 3788 /** | |
| 3789 * Select or deselect a given item depending on whether the item | |
| 3790 * has already been selected. | |
| 3791 * | |
| 3792 * @method toggleSelectionForItem | |
| 3793 * @param {(Object|number)} item The item object or its index | |
| 3794 */ | |
| 3795 toggleSelectionForItem: function(item) { | |
| 3796 item = this._getNormalizedItem(item); | |
| 3797 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | |
| 3798 this.deselectItem(item); | |
| 3799 } else { | |
| 3800 this.selectItem(item); | |
| 3801 } | |
| 3802 }, | |
| 3803 | |
| 3804 /** | |
| 3805 * Clears the current selection state of the list. | |
| 3806 * | |
| 3807 * @method clearSelection | |
| 3808 */ | |
| 3809 clearSelection: function() { | |
| 3810 function unselect(item) { | |
| 3811 var model = this._getModelFromItem(item); | |
| 3812 if (model) { | |
| 3813 model[this.selectedAs] = false; | |
| 3814 } | |
| 3815 } | |
| 3816 | |
| 3817 if (Array.isArray(this.selectedItems)) { | |
| 3818 this.selectedItems.forEach(unselect, this); | |
| 3819 } else if (this.selectedItem) { | |
| 3820 unselect.call(this, this.selectedItem); | |
| 3821 } | |
| 3822 | |
| 3823 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
| 3824 }, | |
| 3825 | |
| 3826 /** | |
| 3827 * Add an event listener to `tap` if `selectionEnabled` is true, | |
| 3828 * it will remove the listener otherwise. | |
| 3829 */ | |
| 3830 _selectionEnabledChanged: function(selectionEnabled) { | |
| 3831 var handler = selectionEnabled ? this.listen : this.unlisten; | |
| 3832 handler.call(this, this, 'tap', '_selectionHandler'); | |
| 3833 }, | |
| 3834 | |
| 3835 /** | |
| 3836 * Select an item from an event object. | |
| 3837 */ | |
| 3838 _selectionHandler: function(e) { | |
| 3839 var model = this.modelForElement(e.target); | |
| 3840 if (!model) { | |
| 3841 return; | |
| 3842 } | |
| 3843 var modelTabIndex, activeElTabIndex; | |
| 3844 var target = Polymer.dom(e).path[0]; | |
| 3845 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | |
| 3846 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | |
| 3847 // Safari does not focus certain form controls via mouse | |
| 3848 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
| 3849 if (target.localName === 'input' || | |
| 3850 target.localName === 'button' || | |
| 3851 target.localName === 'select') { | |
| 3852 return; | |
| 3853 } | |
| 3854 // Set a temporary tabindex | |
| 3855 modelTabIndex = model.tabIndex; | |
| 3856 model.tabIndex = SECRET_TABINDEX; | |
| 3857 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | |
| 3858 model.tabIndex = modelTabIndex; | |
| 3859 // Only select the item if the tap wasn't on a focusable child | |
| 3860 // or the element bound to `tabIndex` | |
| 3861 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | |
| 3862 return; | |
| 3863 } | |
| 3864 this.toggleSelectionForItem(model[this.as]); | |
| 3865 }, | |
| 3866 | |
| 3867 _multiSelectionChanged: function(multiSelection) { | |
| 3868 this.clearSelection(); | |
| 3869 this.$.selector.multi = multiSelection; | |
| 3870 }, | |
| 3871 | |
| 3872 /** | |
| 3873 * Updates the size of an item. | |
| 3874 * | |
| 3875 * @method updateSizeForItem | |
| 3876 * @param {(Object|number)} item The item object or its index | |
| 3877 */ | |
| 3878 updateSizeForItem: function(item) { | |
| 3879 item = this._getNormalizedItem(item); | |
| 3880 var key = this._collection.getKey(item); | |
| 3881 var pidx = this._physicalIndexForKey[key]; | |
| 3882 | |
| 3883 if (pidx != null) { | |
| 3884 this._updateMetrics([pidx]); | |
| 3885 this._positionItems(); | |
| 3886 } | |
| 3887 }, | |
| 3888 | |
| 3889 /** | |
| 3890 * Creates a temporary backfill item in the rendered pool of physical items | |
| 3891 * to replace the main focused item. The focused item has tabIndex = 0 | |
| 3892 * and might be currently focused by the user. | |
| 3893 * | |
| 3894 * This dynamic replacement helps to preserve the focus state. | |
| 3895 */ | |
| 3896 _manageFocus: function() { | |
| 3897 var fidx = this._focusedIndex; | |
| 3898 | |
| 3899 if (fidx >= 0 && fidx < this._virtualCount) { | |
| 3900 // if it's a valid index, check if that index is rendered | |
| 3901 // in a physical item. | |
| 3902 if (this._isIndexRendered(fidx)) { | |
| 3903 this._restoreFocusedItem(); | |
| 3904 } else { | |
| 3905 this._createFocusBackfillItem(); | |
| 3906 } | |
| 3907 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | |
| 3908 // otherwise, assign the initial focused index. | |
| 3909 this._focusedIndex = this._virtualStart; | |
| 3910 this._focusedItem = this._physicalItems[this._physicalStart]; | |
| 3911 } | |
| 3912 }, | |
| 3913 | |
| 3914 _isIndexRendered: function(idx) { | |
| 3915 return idx >= this._virtualStart && idx <= this._virtualEnd; | |
| 3916 }, | |
| 3917 | |
| 3918 _isIndexVisible: function(idx) { | |
| 3919 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | |
| 3920 }, | |
| 3921 | |
| 3922 _getPhysicalIndex: function(idx) { | |
| 3923 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | |
| 3924 }, | |
| 3925 | |
| 3926 _focusPhysicalItem: function(idx) { | |
| 3927 if (idx < 0 || idx >= this._virtualCount) { | |
| 3928 return; | |
| 3929 } | |
| 3930 this._restoreFocusedItem(); | |
| 3931 // scroll to index to make sure it's rendered | |
| 3932 if (!this._isIndexRendered(idx)) { | |
| 3933 this.scrollToIndex(idx); | |
| 3934 } | |
| 3935 | |
| 3936 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | |
| 3937 var model = physicalItem._templateInstance; | |
| 3938 var focusable; | |
| 3939 | |
| 3940 // set a secret tab index | |
| 3941 model.tabIndex = SECRET_TABINDEX; | |
| 3942 // check if focusable element is the physical item | |
| 3943 if (physicalItem.tabIndex === SECRET_TABINDEX) { | |
| 3944 focusable = physicalItem; | |
| 3945 } | |
| 3946 // search for the element which tabindex is bound to the secret tab index | |
| 3947 if (!focusable) { | |
| 3948 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | |
| 3949 } | |
| 3950 // restore the tab index | |
| 3951 model.tabIndex = 0; | |
| 3952 // focus the focusable element | |
| 3953 this._focusedIndex = idx; | |
| 3954 focusable && focusable.focus(); | |
| 3955 }, | |
| 3956 | |
| 3957 _removeFocusedItem: function() { | |
| 3958 if (this._offscreenFocusedItem) { | |
| 3959 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | |
| 3960 } | |
| 3961 this._offscreenFocusedItem = null; | |
| 3962 this._focusBackfillItem = null; | |
| 3963 this._focusedItem = null; | |
| 3964 this._focusedIndex = -1; | |
| 3965 }, | |
| 3966 | |
| 3967 _createFocusBackfillItem: function() { | |
| 3968 var pidx, fidx = this._focusedIndex; | |
| 3969 if (this._offscreenFocusedItem || fidx < 0) { | |
| 3970 return; | |
| 3971 } | |
| 3972 if (!this._focusBackfillItem) { | |
| 3973 // create a physical item, so that it backfills the focused item. | |
| 3974 var stampedTemplate = this.stamp(null); | |
| 3975 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | |
| 3976 Polymer.dom(this).appendChild(stampedTemplate.root); | |
| 3977 } | |
| 3978 // get the physical index for the focused index | |
| 3979 pidx = this._getPhysicalIndex(fidx); | |
| 3980 | |
| 3981 if (pidx != null) { | |
| 3982 // set the offcreen focused physical item | |
| 3983 this._offscreenFocusedItem = this._physicalItems[pidx]; | |
| 3984 // backfill the focused physical item | |
| 3985 this._physicalItems[pidx] = this._focusBackfillItem; | |
| 3986 // hide the focused physical | |
| 3987 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | |
| 3988 } | |
| 3989 }, | |
| 3990 | |
| 3991 _restoreFocusedItem: function() { | |
| 3992 var pidx, fidx = this._focusedIndex; | |
| 3993 | |
| 3994 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | |
| 3995 return; | |
| 3996 } | |
| 3997 // assign models to the focused index | |
| 3998 this._assignModels(); | |
| 3999 // get the new physical index for the focused index | |
| 4000 pidx = this._getPhysicalIndex(fidx); | |
| 4001 | |
| 4002 if (pidx != null) { | |
| 4003 // flip the focus backfill | |
| 4004 this._focusBackfillItem = this._physicalItems[pidx]; | |
| 4005 // restore the focused physical item | |
| 4006 this._physicalItems[pidx] = this._offscreenFocusedItem; | |
| 4007 // reset the offscreen focused item | |
| 4008 this._offscreenFocusedItem = null; | |
| 4009 // hide the physical item that backfills | |
| 4010 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | |
| 4011 } | |
| 4012 }, | |
| 4013 | |
| 4014 _didFocus: function(e) { | |
| 4015 var targetModel = this.modelForElement(e.target); | |
| 4016 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | |
| 4017 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | |
| 4018 var fidx = this._focusedIndex; | |
| 4019 | |
| 4020 if (!targetModel || !focusedModel) { | |
| 4021 return; | |
| 4022 } | |
| 4023 if (focusedModel === targetModel) { | |
| 4024 // if the user focused the same item, then bring it into view if it's no
t visible | |
| 4025 if (!this._isIndexVisible(fidx)) { | |
| 4026 this.scrollToIndex(fidx); | |
| 4027 } | |
| 4028 } else { | |
| 4029 this._restoreFocusedItem(); | |
| 4030 // restore tabIndex for the currently focused item | |
| 4031 focusedModel.tabIndex = -1; | |
| 4032 // set the tabIndex for the next focused item | |
| 4033 targetModel.tabIndex = 0; | |
| 4034 fidx = targetModel[this.indexAs]; | |
| 4035 this._focusedIndex = fidx; | |
| 4036 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | |
| 4037 | |
| 4038 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | |
| 4039 this._update(); | |
| 4040 } | |
| 4041 } | |
| 4042 }, | |
| 4043 | |
| 4044 _didMoveUp: function() { | |
| 4045 this._focusPhysicalItem(this._focusedIndex - 1); | |
| 4046 }, | |
| 4047 | |
| 4048 _didMoveDown: function(e) { | |
| 4049 // disable scroll when pressing the down key | |
| 4050 e.detail.keyboardEvent.preventDefault(); | |
| 4051 this._focusPhysicalItem(this._focusedIndex + 1); | |
| 4052 }, | |
| 4053 | |
| 4054 _didEnter: function(e) { | |
| 4055 this._focusPhysicalItem(this._focusedIndex); | |
| 4056 this._selectionHandler(e.detail.keyboardEvent); | |
| 4057 } | |
| 4058 }); | |
| 4059 | |
| 4060 })(); | |
| 4061 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 4062 // Use of this source code is governed by a BSD-style license that can be | |
| 4063 // found in the LICENSE file. | |
| 4064 | |
| 4065 cr.define('downloads', function() { | |
| 4066 /** | |
| 4067 * @param {string} chromeSendName | |
| 4068 * @return {function(string):void} A chrome.send() callback with curried name. | |
| 4069 */ | |
| 4070 function chromeSendWithId(chromeSendName) { | |
| 4071 return function(id) { chrome.send(chromeSendName, [id]); }; | |
| 4072 } | |
| 4073 | |
| 4074 /** @constructor */ | |
| 4075 function ActionService() { | |
| 4076 /** @private {Array<string>} */ | |
| 4077 this.searchTerms_ = []; | |
| 4078 } | |
| 4079 | |
| 4080 /** | |
| 4081 * @param {string} s | |
| 4082 * @return {string} |s| without whitespace at the beginning or end. | |
| 4083 */ | |
| 4084 function trim(s) { return s.trim(); } | |
| 4085 | |
| 4086 /** | |
| 4087 * @param {string|undefined} value | |
| 4088 * @return {boolean} Whether |value| is truthy. | |
| 4089 */ | |
| 4090 function truthy(value) { return !!value; } | |
| 4091 | |
| 4092 /** | |
| 4093 * @param {string} searchText Input typed by the user into a search box. | |
| 4094 * @return {Array<string>} A list of terms extracted from |searchText|. | |
| 4095 */ | |
| 4096 ActionService.splitTerms = function(searchText) { | |
| 4097 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
| 4098 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | |
| 4099 }; | |
| 4100 | |
| 4101 ActionService.prototype = { | |
| 4102 /** @param {string} id ID of the download to cancel. */ | |
| 4103 cancel: chromeSendWithId('cancel'), | |
| 4104 | |
| 4105 /** Instructs the browser to clear all finished downloads. */ | |
| 4106 clearAll: function() { | |
| 4107 if (loadTimeData.getBoolean('allowDeletingHistory')) { | |
| 4108 chrome.send('clearAll'); | |
| 4109 this.search(''); | |
| 4110 } | |
| 4111 }, | |
| 4112 | |
| 4113 /** @param {string} id ID of the dangerous download to discard. */ | |
| 4114 discardDangerous: chromeSendWithId('discardDangerous'), | |
| 4115 | |
| 4116 /** @param {string} url URL of a file to download. */ | |
| 4117 download: function(url) { | |
| 4118 var a = document.createElement('a'); | |
| 4119 a.href = url; | |
| 4120 a.setAttribute('download', ''); | |
| 4121 a.click(); | |
| 4122 }, | |
| 4123 | |
| 4124 /** @param {string} id ID of the download that the user started dragging. */ | |
| 4125 drag: chromeSendWithId('drag'), | |
| 4126 | |
| 4127 /** Loads more downloads with the current search terms. */ | |
| 4128 loadMore: function() { | |
| 4129 chrome.send('getDownloads', this.searchTerms_); | |
| 4130 }, | |
| 4131 | |
| 4132 /** | |
| 4133 * @return {boolean} Whether the user is currently searching for downloads | |
| 4134 * (i.e. has a non-empty search term). | |
| 4135 */ | |
| 4136 isSearching: function() { | |
| 4137 return this.searchTerms_.length > 0; | |
| 4138 }, | |
| 4139 | |
| 4140 /** Opens the current local destination for downloads. */ | |
| 4141 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | |
| 4142 | |
| 4143 /** | |
| 4144 * @param {string} id ID of the download to run locally on the user's box. | |
| 4145 */ | |
| 4146 openFile: chromeSendWithId('openFile'), | |
| 4147 | |
| 4148 /** @param {string} id ID the of the progressing download to pause. */ | |
| 4149 pause: chromeSendWithId('pause'), | |
| 4150 | |
| 4151 /** @param {string} id ID of the finished download to remove. */ | |
| 4152 remove: chromeSendWithId('remove'), | |
| 4153 | |
| 4154 /** @param {string} id ID of the paused download to resume. */ | |
| 4155 resume: chromeSendWithId('resume'), | |
| 4156 | |
| 4157 /** | |
| 4158 * @param {string} id ID of the dangerous download to save despite | |
| 4159 * warnings. | |
| 4160 */ | |
| 4161 saveDangerous: chromeSendWithId('saveDangerous'), | |
| 4162 | |
| 4163 /** @param {string} searchText What to search for. */ | |
| 4164 search: function(searchText) { | |
| 4165 var searchTerms = ActionService.splitTerms(searchText); | |
| 4166 var sameTerms = searchTerms.length == this.searchTerms_.length; | |
| 4167 | |
| 4168 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | |
| 4169 if (searchTerms[i] != this.searchTerms_[i]) | |
| 4170 sameTerms = false; | |
| 4171 } | |
| 4172 | |
| 4173 if (sameTerms) | |
| 4174 return; | |
| 4175 | |
| 4176 this.searchTerms_ = searchTerms; | |
| 4177 this.loadMore(); | |
| 4178 }, | |
| 4179 | |
| 4180 /** | |
| 4181 * Shows the local folder a finished download resides in. | |
| 4182 * @param {string} id ID of the download to show. | |
| 4183 */ | |
| 4184 show: chromeSendWithId('show'), | |
| 4185 | |
| 4186 /** Undo download removal. */ | |
| 4187 undo: chrome.send.bind(chrome, 'undo'), | |
| 4188 }; | |
| 4189 | |
| 4190 cr.addSingletonGetter(ActionService); | |
| 4191 | |
| 4192 return {ActionService: ActionService}; | |
| 4193 }); | |
| 4194 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 4195 // Use of this source code is governed by a BSD-style license that can be | |
| 4196 // found in the LICENSE file. | |
| 4197 | |
| 4198 cr.define('downloads', function() { | |
| 4199 /** | |
| 4200 * Explains why a download is in DANGEROUS state. | |
| 4201 * @enum {string} | |
| 4202 */ | |
| 4203 var DangerType = { | |
| 4204 NOT_DANGEROUS: 'NOT_DANGEROUS', | |
| 4205 DANGEROUS_FILE: 'DANGEROUS_FILE', | |
| 4206 DANGEROUS_URL: 'DANGEROUS_URL', | |
| 4207 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | |
| 4208 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | |
| 4209 DANGEROUS_HOST: 'DANGEROUS_HOST', | |
| 4210 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | |
| 4211 }; | |
| 4212 | |
| 4213 /** | |
| 4214 * The states a download can be in. These correspond to states defined in | |
| 4215 * DownloadsDOMHandler::CreateDownloadItemValue | |
| 4216 * @enum {string} | |
| 4217 */ | |
| 4218 var States = { | |
| 4219 IN_PROGRESS: 'IN_PROGRESS', | |
| 4220 CANCELLED: 'CANCELLED', | |
| 4221 COMPLETE: 'COMPLETE', | |
| 4222 PAUSED: 'PAUSED', | |
| 4223 DANGEROUS: 'DANGEROUS', | |
| 4224 INTERRUPTED: 'INTERRUPTED', | |
| 4225 }; | |
| 4226 | |
| 4227 return { | |
| 4228 DangerType: DangerType, | |
| 4229 States: States, | |
| 4230 }; | |
| 4231 }); | |
| 4232 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 4233 // Use of this source code is governed by a BSD-style license that can be | |
| 4234 // found in the LICENSE file. | |
| 4235 | |
| 4236 // Action links are elements that are used to perform an in-page navigation or | |
| 4237 // action (e.g. showing a dialog). | |
| 4238 // | |
| 4239 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
| 4240 // they're subtly different as they're not initially underlined (giving users a | |
| 4241 // clue that underlined links navigate while action links don't). | |
| 4242 // | |
| 4243 // Action links look very similar to normal links when hovered (hand cursor, | |
| 4244 // underlined). This gives the user an idea that clicking this link will do | |
| 4245 // something similar to navigation but in the same page. | |
| 4246 // | |
| 4247 // They can be created in JavaScript like this: | |
| 4248 // | |
| 4249 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
| 4250 // | |
| 4251 // or with a constructor like this: | |
| 4252 // | |
| 4253 // var link = new ActionLink(); | |
| 4254 // | |
| 4255 // They can be used easily from HTML as well, like so: | |
| 4256 // | |
| 4257 // <a is="action-link">Click me!</a> | |
| 4258 // | |
| 4259 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
| 4260 | |
| 4261 /** | |
| 4262 * @constructor | |
| 4263 * @extends {HTMLAnchorElement} | |
| 4264 */ | |
| 4265 var ActionLink = document.registerElement('action-link', { | |
| 4266 prototype: { | |
| 4267 __proto__: HTMLAnchorElement.prototype, | |
| 4268 | |
| 4269 /** @this {ActionLink} */ | |
| 4270 createdCallback: function() { | |
| 4271 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
| 4272 this.tabIndex = this.disabled ? -1 : 0; | |
| 4273 | |
| 4274 if (!this.hasAttribute('role')) | |
| 4275 this.setAttribute('role', 'link'); | |
| 4276 | |
| 4277 this.addEventListener('keydown', function(e) { | |
| 4278 if (!this.disabled && e.key == 'Enter' && !this.href) { | |
| 4279 // Schedule a click asynchronously because other 'keydown' handlers | |
| 4280 // may still run later (e.g. document.addEventListener('keydown')). | |
| 4281 // Specifically options dialogs break when this timeout isn't here. | |
| 4282 // NOTE: this affects the "trusted" state of the ensuing click. I | |
| 4283 // haven't found anything that breaks because of this (yet). | |
| 4284 window.setTimeout(this.click.bind(this), 0); | |
| 4285 } | |
| 4286 }); | |
| 4287 | |
| 4288 function preventDefault(e) { | |
| 4289 e.preventDefault(); | |
| 4290 } | |
| 4291 | |
| 4292 function removePreventDefault() { | |
| 4293 document.removeEventListener('selectstart', preventDefault); | |
| 4294 document.removeEventListener('mouseup', removePreventDefault); | |
| 4295 } | |
| 4296 | |
| 4297 this.addEventListener('mousedown', function() { | |
| 4298 // This handlers strives to match the behavior of <a href="...">. | |
| 4299 | |
| 4300 // While the mouse is down, prevent text selection from dragging. | |
| 4301 document.addEventListener('selectstart', preventDefault); | |
| 4302 document.addEventListener('mouseup', removePreventDefault); | |
| 4303 | |
| 4304 // If focus started via mouse press, don't show an outline. | |
| 4305 if (document.activeElement != this) | |
| 4306 this.classList.add('no-outline'); | |
| 4307 }); | |
| 4308 | |
| 4309 this.addEventListener('blur', function() { | |
| 4310 this.classList.remove('no-outline'); | |
| 4311 }); | |
| 4312 }, | |
| 4313 | |
| 4314 /** @type {boolean} */ | |
| 4315 set disabled(disabled) { | |
| 4316 if (disabled) | |
| 4317 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', ''); | |
| 4318 else | |
| 4319 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled'); | |
| 4320 this.tabIndex = disabled ? -1 : 0; | |
| 4321 }, | |
| 4322 get disabled() { | |
| 4323 return this.hasAttribute('disabled'); | |
| 4324 }, | |
| 4325 | |
| 4326 /** @override */ | |
| 4327 setAttribute: function(attr, val) { | |
| 4328 if (attr.toLowerCase() == 'disabled') | |
| 4329 this.disabled = true; | |
| 4330 else | |
| 4331 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments); | |
| 4332 }, | |
| 4333 | |
| 4334 /** @override */ | |
| 4335 removeAttribute: function(attr) { | |
| 4336 if (attr.toLowerCase() == 'disabled') | |
| 4337 this.disabled = false; | |
| 4338 else | |
| 4339 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | |
| 4340 }, | |
| 4341 }, | |
| 4342 | |
| 4343 extends: 'a', | |
| 4344 }); | |
| 4345 (function() { | 4843 (function() { |
| 4346 | 4844 |
| 4347 // monostate data | 4845 // monostate data |
| 4348 var metaDatas = {}; | 4846 var metaDatas = {}; |
| 4349 var metaArrays = {}; | 4847 var metaArrays = {}; |
| 4350 var singleton = null; | 4848 var singleton = null; |
| 4351 | 4849 |
| 4352 Polymer.IronMeta = Polymer({ | 4850 Polymer.IronMeta = Polymer({ |
| 4353 | 4851 |
| 4354 is: 'iron-meta', | 4852 is: 'iron-meta', |
| (...skipping 355 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 4710 this._img.style.height = '100%'; | 5208 this._img.style.height = '100%'; |
| 4711 this._img.draggable = false; | 5209 this._img.draggable = false; |
| 4712 } | 5210 } |
| 4713 this._img.src = this.src; | 5211 this._img.src = this.src; |
| 4714 Polymer.dom(this.root).appendChild(this._img); | 5212 Polymer.dom(this.root).appendChild(this._img); |
| 4715 } | 5213 } |
| 4716 } | 5214 } |
| 4717 | 5215 |
| 4718 }); | 5216 }); |
| 4719 /** | 5217 /** |
| 4720 * @demo demo/index.html | 5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. |
| 4721 * @polymerBehavior | 5219 * |
| 5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior |
| 4722 */ | 5221 */ |
| 4723 Polymer.IronControlState = { | 5222 Polymer.PaperInkyFocusBehaviorImpl = { |
| 4724 | |
| 4725 properties: { | |
| 4726 | |
| 4727 /** | |
| 4728 * If true, the element currently has focus. | |
| 4729 */ | |
| 4730 focused: { | |
| 4731 type: Boolean, | |
| 4732 value: false, | |
| 4733 notify: true, | |
| 4734 readOnly: true, | |
| 4735 reflectToAttribute: true | |
| 4736 }, | |
| 4737 | |
| 4738 /** | |
| 4739 * If true, the user cannot interact with this element. | |
| 4740 */ | |
| 4741 disabled: { | |
| 4742 type: Boolean, | |
| 4743 value: false, | |
| 4744 notify: true, | |
| 4745 observer: '_disabledChanged', | |
| 4746 reflectToAttribute: true | |
| 4747 }, | |
| 4748 | |
| 4749 _oldTabIndex: { | |
| 4750 type: Number | |
| 4751 }, | |
| 4752 | |
| 4753 _boundFocusBlurHandler: { | |
| 4754 type: Function, | |
| 4755 value: function() { | |
| 4756 return this._focusBlurHandler.bind(this); | |
| 4757 } | |
| 4758 } | |
| 4759 | |
| 4760 }, | |
| 4761 | |
| 4762 observers: [ | 5223 observers: [ |
| 4763 '_changedControlState(focused, disabled)' | 5224 '_focusedChanged(receivedFocusFromKeyboard)' |
| 4764 ], | 5225 ], |
| 4765 | 5226 |
| 4766 ready: function() { | 5227 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 4767 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 5228 if (receivedFocusFromKeyboard) { |
| 4768 this.addEventListener('blur', this._boundFocusBlurHandler, true); | |
| 4769 }, | |
| 4770 | |
| 4771 _focusBlurHandler: function(event) { | |
| 4772 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | |
| 4773 // eventually become `this` due to retargeting; if we are not in | |
| 4774 // ShadowDOM land, `event.target` will eventually become `this` due | |
| 4775 // to the second conditional which fires a synthetic event (that is also | |
| 4776 // handled). In either case, we can disregard `event.path`. | |
| 4777 | |
| 4778 if (event.target === this) { | |
| 4779 this._setFocused(event.type === 'focus'); | |
| 4780 } else if (!this.shadowRoot) { | |
| 4781 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | |
| 4782 if (!this.isLightDescendant(target)) { | |
| 4783 this.fire(event.type, {sourceEvent: event}, { | |
| 4784 node: this, | |
| 4785 bubbles: event.bubbles, | |
| 4786 cancelable: event.cancelable | |
| 4787 }); | |
| 4788 } | |
| 4789 } | |
| 4790 }, | |
| 4791 | |
| 4792 _disabledChanged: function(disabled, old) { | |
| 4793 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | |
| 4794 this.style.pointerEvents = disabled ? 'none' : ''; | |
| 4795 if (disabled) { | |
| 4796 this._oldTabIndex = this.tabIndex; | |
| 4797 this._setFocused(false); | |
| 4798 this.tabIndex = -1; | |
| 4799 this.blur(); | |
| 4800 } else if (this._oldTabIndex !== undefined) { | |
| 4801 this.tabIndex = this._oldTabIndex; | |
| 4802 } | |
| 4803 }, | |
| 4804 | |
| 4805 _changedControlState: function() { | |
| 4806 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
| 4807 if (this._controlStateChanged) { | |
| 4808 this._controlStateChanged(); | |
| 4809 } | |
| 4810 } | |
| 4811 | |
| 4812 }; | |
| 4813 /** | |
| 4814 * @demo demo/index.html | |
| 4815 * @polymerBehavior Polymer.IronButtonState | |
| 4816 */ | |
| 4817 Polymer.IronButtonStateImpl = { | |
| 4818 | |
| 4819 properties: { | |
| 4820 | |
| 4821 /** | |
| 4822 * If true, the user is currently holding down the button. | |
| 4823 */ | |
| 4824 pressed: { | |
| 4825 type: Boolean, | |
| 4826 readOnly: true, | |
| 4827 value: false, | |
| 4828 reflectToAttribute: true, | |
| 4829 observer: '_pressedChanged' | |
| 4830 }, | |
| 4831 | |
| 4832 /** | |
| 4833 * If true, the button toggles the active state with each tap or press | |
| 4834 * of the spacebar. | |
| 4835 */ | |
| 4836 toggles: { | |
| 4837 type: Boolean, | |
| 4838 value: false, | |
| 4839 reflectToAttribute: true | |
| 4840 }, | |
| 4841 | |
| 4842 /** | |
| 4843 * If true, the button is a toggle and is currently in the active state. | |
| 4844 */ | |
| 4845 active: { | |
| 4846 type: Boolean, | |
| 4847 value: false, | |
| 4848 notify: true, | |
| 4849 reflectToAttribute: true | |
| 4850 }, | |
| 4851 | |
| 4852 /** | |
| 4853 * True if the element is currently being pressed by a "pointer," which | |
| 4854 * is loosely defined as mouse or touch input (but specifically excluding | |
| 4855 * keyboard input). | |
| 4856 */ | |
| 4857 pointerDown: { | |
| 4858 type: Boolean, | |
| 4859 readOnly: true, | |
| 4860 value: false | |
| 4861 }, | |
| 4862 | |
| 4863 /** | |
| 4864 * True if the input device that caused the element to receive focus | |
| 4865 * was a keyboard. | |
| 4866 */ | |
| 4867 receivedFocusFromKeyboard: { | |
| 4868 type: Boolean, | |
| 4869 readOnly: true | |
| 4870 }, | |
| 4871 | |
| 4872 /** | |
| 4873 * The aria attribute to be set if the button is a toggle and in the | |
| 4874 * active state. | |
| 4875 */ | |
| 4876 ariaActiveAttribute: { | |
| 4877 type: String, | |
| 4878 value: 'aria-pressed', | |
| 4879 observer: '_ariaActiveAttributeChanged' | |
| 4880 } | |
| 4881 }, | |
| 4882 | |
| 4883 listeners: { | |
| 4884 down: '_downHandler', | |
| 4885 up: '_upHandler', | |
| 4886 tap: '_tapHandler' | |
| 4887 }, | |
| 4888 | |
| 4889 observers: [ | |
| 4890 '_detectKeyboardFocus(focused)', | |
| 4891 '_activeChanged(active, ariaActiveAttribute)' | |
| 4892 ], | |
| 4893 | |
| 4894 keyBindings: { | |
| 4895 'enter:keydown': '_asyncClick', | |
| 4896 'space:keydown': '_spaceKeyDownHandler', | |
| 4897 'space:keyup': '_spaceKeyUpHandler', | |
| 4898 }, | |
| 4899 | |
| 4900 _mouseEventRe: /^mouse/, | |
| 4901 | |
| 4902 _tapHandler: function() { | |
| 4903 if (this.toggles) { | |
| 4904 // a tap is needed to toggle the active state | |
| 4905 this._userActivate(!this.active); | |
| 4906 } else { | |
| 4907 this.active = false; | |
| 4908 } | |
| 4909 }, | |
| 4910 | |
| 4911 _detectKeyboardFocus: function(focused) { | |
| 4912 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
| 4913 }, | |
| 4914 | |
| 4915 // to emulate native checkbox, (de-)activations from a user interaction fire | |
| 4916 // 'change' events | |
| 4917 _userActivate: function(active) { | |
| 4918 if (this.active !== active) { | |
| 4919 this.active = active; | |
| 4920 this.fire('change'); | |
| 4921 } | |
| 4922 }, | |
| 4923 | |
| 4924 _downHandler: function(event) { | |
| 4925 this._setPointerDown(true); | |
| 4926 this._setPressed(true); | |
| 4927 this._setReceivedFocusFromKeyboard(false); | |
| 4928 }, | |
| 4929 | |
| 4930 _upHandler: function() { | |
| 4931 this._setPointerDown(false); | |
| 4932 this._setPressed(false); | |
| 4933 }, | |
| 4934 | |
| 4935 /** | |
| 4936 * @param {!KeyboardEvent} event . | |
| 4937 */ | |
| 4938 _spaceKeyDownHandler: function(event) { | |
| 4939 var keyboardEvent = event.detail.keyboardEvent; | |
| 4940 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 4941 | |
| 4942 // Ignore the event if this is coming from a focused light child, since th
at | |
| 4943 // element will deal with it. | |
| 4944 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 4945 return; | |
| 4946 | |
| 4947 keyboardEvent.preventDefault(); | |
| 4948 keyboardEvent.stopImmediatePropagation(); | |
| 4949 this._setPressed(true); | |
| 4950 }, | |
| 4951 | |
| 4952 /** | |
| 4953 * @param {!KeyboardEvent} event . | |
| 4954 */ | |
| 4955 _spaceKeyUpHandler: function(event) { | |
| 4956 var keyboardEvent = event.detail.keyboardEvent; | |
| 4957 var target = Polymer.dom(keyboardEvent).localTarget; | |
| 4958 | |
| 4959 // Ignore the event if this is coming from a focused light child, since th
at | |
| 4960 // element will deal with it. | |
| 4961 if (this.isLightDescendant(/** @type {Node} */(target))) | |
| 4962 return; | |
| 4963 | |
| 4964 if (this.pressed) { | |
| 4965 this._asyncClick(); | |
| 4966 } | |
| 4967 this._setPressed(false); | |
| 4968 }, | |
| 4969 | |
| 4970 // trigger click asynchronously, the asynchrony is useful to allow one | |
| 4971 // event handler to unwind before triggering another event | |
| 4972 _asyncClick: function() { | |
| 4973 this.async(function() { | |
| 4974 this.click(); | |
| 4975 }, 1); | |
| 4976 }, | |
| 4977 | |
| 4978 // any of these changes are considered a change to button state | |
| 4979 | |
| 4980 _pressedChanged: function(pressed) { | |
| 4981 this._changedButtonState(); | |
| 4982 }, | |
| 4983 | |
| 4984 _ariaActiveAttributeChanged: function(value, oldValue) { | |
| 4985 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | |
| 4986 this.removeAttribute(oldValue); | |
| 4987 } | |
| 4988 }, | |
| 4989 | |
| 4990 _activeChanged: function(active, ariaActiveAttribute) { | |
| 4991 if (this.toggles) { | |
| 4992 this.setAttribute(this.ariaActiveAttribute, | |
| 4993 active ? 'true' : 'false'); | |
| 4994 } else { | |
| 4995 this.removeAttribute(this.ariaActiveAttribute); | |
| 4996 } | |
| 4997 this._changedButtonState(); | |
| 4998 }, | |
| 4999 | |
| 5000 _controlStateChanged: function() { | |
| 5001 if (this.disabled) { | |
| 5002 this._setPressed(false); | |
| 5003 } else { | |
| 5004 this._changedButtonState(); | |
| 5005 } | |
| 5006 }, | |
| 5007 | |
| 5008 // provide hook for follow-on behaviors to react to button-state | |
| 5009 | |
| 5010 _changedButtonState: function() { | |
| 5011 if (this._buttonStateChanged) { | |
| 5012 this._buttonStateChanged(); // abstract | |
| 5013 } | |
| 5014 } | |
| 5015 | |
| 5016 }; | |
| 5017 | |
| 5018 /** @polymerBehavior */ | |
| 5019 Polymer.IronButtonState = [ | |
| 5020 Polymer.IronA11yKeysBehavior, | |
| 5021 Polymer.IronButtonStateImpl | |
| 5022 ]; | |
| 5023 (function() { | |
| 5024 var Utility = { | |
| 5025 distance: function(x1, y1, x2, y2) { | |
| 5026 var xDelta = (x1 - x2); | |
| 5027 var yDelta = (y1 - y2); | |
| 5028 | |
| 5029 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
| 5030 }, | |
| 5031 | |
| 5032 now: window.performance && window.performance.now ? | |
| 5033 window.performance.now.bind(window.performance) : Date.now | |
| 5034 }; | |
| 5035 | |
| 5036 /** | |
| 5037 * @param {HTMLElement} element | |
| 5038 * @constructor | |
| 5039 */ | |
| 5040 function ElementMetrics(element) { | |
| 5041 this.element = element; | |
| 5042 this.width = this.boundingRect.width; | |
| 5043 this.height = this.boundingRect.height; | |
| 5044 | |
| 5045 this.size = Math.max(this.width, this.height); | |
| 5046 } | |
| 5047 | |
| 5048 ElementMetrics.prototype = { | |
| 5049 get boundingRect () { | |
| 5050 return this.element.getBoundingClientRect(); | |
| 5051 }, | |
| 5052 | |
| 5053 furthestCornerDistanceFrom: function(x, y) { | |
| 5054 var topLeft = Utility.distance(x, y, 0, 0); | |
| 5055 var topRight = Utility.distance(x, y, this.width, 0); | |
| 5056 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
| 5057 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
| 5058 | |
| 5059 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
| 5060 } | |
| 5061 }; | |
| 5062 | |
| 5063 /** | |
| 5064 * @param {HTMLElement} element | |
| 5065 * @constructor | |
| 5066 */ | |
| 5067 function Ripple(element) { | |
| 5068 this.element = element; | |
| 5069 this.color = window.getComputedStyle(element).color; | |
| 5070 | |
| 5071 this.wave = document.createElement('div'); | |
| 5072 this.waveContainer = document.createElement('div'); | |
| 5073 this.wave.style.backgroundColor = this.color; | |
| 5074 this.wave.classList.add('wave'); | |
| 5075 this.waveContainer.classList.add('wave-container'); | |
| 5076 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
| 5077 | |
| 5078 this.resetInteractionState(); | |
| 5079 } | |
| 5080 | |
| 5081 Ripple.MAX_RADIUS = 300; | |
| 5082 | |
| 5083 Ripple.prototype = { | |
| 5084 get recenters() { | |
| 5085 return this.element.recenters; | |
| 5086 }, | |
| 5087 | |
| 5088 get center() { | |
| 5089 return this.element.center; | |
| 5090 }, | |
| 5091 | |
| 5092 get mouseDownElapsed() { | |
| 5093 var elapsed; | |
| 5094 | |
| 5095 if (!this.mouseDownStart) { | |
| 5096 return 0; | |
| 5097 } | |
| 5098 | |
| 5099 elapsed = Utility.now() - this.mouseDownStart; | |
| 5100 | |
| 5101 if (this.mouseUpStart) { | |
| 5102 elapsed -= this.mouseUpElapsed; | |
| 5103 } | |
| 5104 | |
| 5105 return elapsed; | |
| 5106 }, | |
| 5107 | |
| 5108 get mouseUpElapsed() { | |
| 5109 return this.mouseUpStart ? | |
| 5110 Utility.now () - this.mouseUpStart : 0; | |
| 5111 }, | |
| 5112 | |
| 5113 get mouseDownElapsedSeconds() { | |
| 5114 return this.mouseDownElapsed / 1000; | |
| 5115 }, | |
| 5116 | |
| 5117 get mouseUpElapsedSeconds() { | |
| 5118 return this.mouseUpElapsed / 1000; | |
| 5119 }, | |
| 5120 | |
| 5121 get mouseInteractionSeconds() { | |
| 5122 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
| 5123 }, | |
| 5124 | |
| 5125 get initialOpacity() { | |
| 5126 return this.element.initialOpacity; | |
| 5127 }, | |
| 5128 | |
| 5129 get opacityDecayVelocity() { | |
| 5130 return this.element.opacityDecayVelocity; | |
| 5131 }, | |
| 5132 | |
| 5133 get radius() { | |
| 5134 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
| 5135 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
| 5136 var waveRadius = Math.min( | |
| 5137 Math.sqrt(width2 + height2), | |
| 5138 Ripple.MAX_RADIUS | |
| 5139 ) * 1.1 + 5; | |
| 5140 | |
| 5141 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
| 5142 var timeNow = this.mouseInteractionSeconds / duration; | |
| 5143 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
| 5144 | |
| 5145 return Math.abs(size); | |
| 5146 }, | |
| 5147 | |
| 5148 get opacity() { | |
| 5149 if (!this.mouseUpStart) { | |
| 5150 return this.initialOpacity; | |
| 5151 } | |
| 5152 | |
| 5153 return Math.max( | |
| 5154 0, | |
| 5155 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
| 5156 ); | |
| 5157 }, | |
| 5158 | |
| 5159 get outerOpacity() { | |
| 5160 // Linear increase in background opacity, capped at the opacity | |
| 5161 // of the wavefront (waveOpacity). | |
| 5162 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
| 5163 var waveOpacity = this.opacity; | |
| 5164 | |
| 5165 return Math.max( | |
| 5166 0, | |
| 5167 Math.min(outerOpacity, waveOpacity) | |
| 5168 ); | |
| 5169 }, | |
| 5170 | |
| 5171 get isOpacityFullyDecayed() { | |
| 5172 return this.opacity < 0.01 && | |
| 5173 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 5174 }, | |
| 5175 | |
| 5176 get isRestingAtMaxRadius() { | |
| 5177 return this.opacity >= this.initialOpacity && | |
| 5178 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 5179 }, | |
| 5180 | |
| 5181 get isAnimationComplete() { | |
| 5182 return this.mouseUpStart ? | |
| 5183 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
| 5184 }, | |
| 5185 | |
| 5186 get translationFraction() { | |
| 5187 return Math.min( | |
| 5188 1, | |
| 5189 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
| 5190 ); | |
| 5191 }, | |
| 5192 | |
| 5193 get xNow() { | |
| 5194 if (this.xEnd) { | |
| 5195 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
| 5196 } | |
| 5197 | |
| 5198 return this.xStart; | |
| 5199 }, | |
| 5200 | |
| 5201 get yNow() { | |
| 5202 if (this.yEnd) { | |
| 5203 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
| 5204 } | |
| 5205 | |
| 5206 return this.yStart; | |
| 5207 }, | |
| 5208 | |
| 5209 get isMouseDown() { | |
| 5210 return this.mouseDownStart && !this.mouseUpStart; | |
| 5211 }, | |
| 5212 | |
| 5213 resetInteractionState: function() { | |
| 5214 this.maxRadius = 0; | |
| 5215 this.mouseDownStart = 0; | |
| 5216 this.mouseUpStart = 0; | |
| 5217 | |
| 5218 this.xStart = 0; | |
| 5219 this.yStart = 0; | |
| 5220 this.xEnd = 0; | |
| 5221 this.yEnd = 0; | |
| 5222 this.slideDistance = 0; | |
| 5223 | |
| 5224 this.containerMetrics = new ElementMetrics(this.element); | |
| 5225 }, | |
| 5226 | |
| 5227 draw: function() { | |
| 5228 var scale; | |
| 5229 var translateString; | |
| 5230 var dx; | |
| 5231 var dy; | |
| 5232 | |
| 5233 this.wave.style.opacity = this.opacity; | |
| 5234 | |
| 5235 scale = this.radius / (this.containerMetrics.size / 2); | |
| 5236 dx = this.xNow - (this.containerMetrics.width / 2); | |
| 5237 dy = this.yNow - (this.containerMetrics.height / 2); | |
| 5238 | |
| 5239 | |
| 5240 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
| 5241 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
| 5242 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
| 5243 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
| 5244 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
| 5245 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
| 5246 }, | |
| 5247 | |
| 5248 /** @param {Event=} event */ | |
| 5249 downAction: function(event) { | |
| 5250 var xCenter = this.containerMetrics.width / 2; | |
| 5251 var yCenter = this.containerMetrics.height / 2; | |
| 5252 | |
| 5253 this.resetInteractionState(); | |
| 5254 this.mouseDownStart = Utility.now(); | |
| 5255 | |
| 5256 if (this.center) { | |
| 5257 this.xStart = xCenter; | |
| 5258 this.yStart = yCenter; | |
| 5259 this.slideDistance = Utility.distance( | |
| 5260 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 5261 ); | |
| 5262 } else { | |
| 5263 this.xStart = event ? | |
| 5264 event.detail.x - this.containerMetrics.boundingRect.left : | |
| 5265 this.containerMetrics.width / 2; | |
| 5266 this.yStart = event ? | |
| 5267 event.detail.y - this.containerMetrics.boundingRect.top : | |
| 5268 this.containerMetrics.height / 2; | |
| 5269 } | |
| 5270 | |
| 5271 if (this.recenters) { | |
| 5272 this.xEnd = xCenter; | |
| 5273 this.yEnd = yCenter; | |
| 5274 this.slideDistance = Utility.distance( | |
| 5275 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 5276 ); | |
| 5277 } | |
| 5278 | |
| 5279 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
| 5280 this.xStart, | |
| 5281 this.yStart | |
| 5282 ); | |
| 5283 | |
| 5284 this.waveContainer.style.top = | |
| 5285 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
| 5286 this.waveContainer.style.left = | |
| 5287 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
| 5288 | |
| 5289 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
| 5290 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
| 5291 }, | |
| 5292 | |
| 5293 /** @param {Event=} event */ | |
| 5294 upAction: function(event) { | |
| 5295 if (!this.isMouseDown) { | |
| 5296 return; | |
| 5297 } | |
| 5298 | |
| 5299 this.mouseUpStart = Utility.now(); | |
| 5300 }, | |
| 5301 | |
| 5302 remove: function() { | |
| 5303 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
| 5304 this.waveContainer | |
| 5305 ); | |
| 5306 } | |
| 5307 }; | |
| 5308 | |
| 5309 Polymer({ | |
| 5310 is: 'paper-ripple', | |
| 5311 | |
| 5312 behaviors: [ | |
| 5313 Polymer.IronA11yKeysBehavior | |
| 5314 ], | |
| 5315 | |
| 5316 properties: { | |
| 5317 /** | |
| 5318 * The initial opacity set on the wave. | |
| 5319 * | |
| 5320 * @attribute initialOpacity | |
| 5321 * @type number | |
| 5322 * @default 0.25 | |
| 5323 */ | |
| 5324 initialOpacity: { | |
| 5325 type: Number, | |
| 5326 value: 0.25 | |
| 5327 }, | |
| 5328 | |
| 5329 /** | |
| 5330 * How fast (opacity per second) the wave fades out. | |
| 5331 * | |
| 5332 * @attribute opacityDecayVelocity | |
| 5333 * @type number | |
| 5334 * @default 0.8 | |
| 5335 */ | |
| 5336 opacityDecayVelocity: { | |
| 5337 type: Number, | |
| 5338 value: 0.8 | |
| 5339 }, | |
| 5340 | |
| 5341 /** | |
| 5342 * If true, ripples will exhibit a gravitational pull towards | |
| 5343 * the center of their container as they fade away. | |
| 5344 * | |
| 5345 * @attribute recenters | |
| 5346 * @type boolean | |
| 5347 * @default false | |
| 5348 */ | |
| 5349 recenters: { | |
| 5350 type: Boolean, | |
| 5351 value: false | |
| 5352 }, | |
| 5353 | |
| 5354 /** | |
| 5355 * If true, ripples will center inside its container | |
| 5356 * | |
| 5357 * @attribute recenters | |
| 5358 * @type boolean | |
| 5359 * @default false | |
| 5360 */ | |
| 5361 center: { | |
| 5362 type: Boolean, | |
| 5363 value: false | |
| 5364 }, | |
| 5365 | |
| 5366 /** | |
| 5367 * A list of the visual ripples. | |
| 5368 * | |
| 5369 * @attribute ripples | |
| 5370 * @type Array | |
| 5371 * @default [] | |
| 5372 */ | |
| 5373 ripples: { | |
| 5374 type: Array, | |
| 5375 value: function() { | |
| 5376 return []; | |
| 5377 } | |
| 5378 }, | |
| 5379 | |
| 5380 /** | |
| 5381 * True when there are visible ripples animating within the | |
| 5382 * element. | |
| 5383 */ | |
| 5384 animating: { | |
| 5385 type: Boolean, | |
| 5386 readOnly: true, | |
| 5387 reflectToAttribute: true, | |
| 5388 value: false | |
| 5389 }, | |
| 5390 | |
| 5391 /** | |
| 5392 * If true, the ripple will remain in the "down" state until `holdDown` | |
| 5393 * is set to false again. | |
| 5394 */ | |
| 5395 holdDown: { | |
| 5396 type: Boolean, | |
| 5397 value: false, | |
| 5398 observer: '_holdDownChanged' | |
| 5399 }, | |
| 5400 | |
| 5401 /** | |
| 5402 * If true, the ripple will not generate a ripple effect | |
| 5403 * via pointer interaction. | |
| 5404 * Calling ripple's imperative api like `simulatedRipple` will | |
| 5405 * still generate the ripple effect. | |
| 5406 */ | |
| 5407 noink: { | |
| 5408 type: Boolean, | |
| 5409 value: false | |
| 5410 }, | |
| 5411 | |
| 5412 _animating: { | |
| 5413 type: Boolean | |
| 5414 }, | |
| 5415 | |
| 5416 _boundAnimate: { | |
| 5417 type: Function, | |
| 5418 value: function() { | |
| 5419 return this.animate.bind(this); | |
| 5420 } | |
| 5421 } | |
| 5422 }, | |
| 5423 | |
| 5424 get target () { | |
| 5425 return this.keyEventTarget; | |
| 5426 }, | |
| 5427 | |
| 5428 keyBindings: { | |
| 5429 'enter:keydown': '_onEnterKeydown', | |
| 5430 'space:keydown': '_onSpaceKeydown', | |
| 5431 'space:keyup': '_onSpaceKeyup' | |
| 5432 }, | |
| 5433 | |
| 5434 attached: function() { | |
| 5435 // Set up a11yKeysBehavior to listen to key events on the target, | |
| 5436 // so that space and enter activate the ripple even if the target doesn'
t | |
| 5437 // handle key events. The key handlers deal with `noink` themselves. | |
| 5438 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
| 5439 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
| 5440 } else { | |
| 5441 this.keyEventTarget = this.parentNode; | |
| 5442 } | |
| 5443 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
| 5444 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
| 5445 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
| 5446 }, | |
| 5447 | |
| 5448 detached: function() { | |
| 5449 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
| 5450 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
| 5451 this.keyEventTarget = null; | |
| 5452 }, | |
| 5453 | |
| 5454 get shouldKeepAnimating () { | |
| 5455 for (var index = 0; index < this.ripples.length; ++index) { | |
| 5456 if (!this.ripples[index].isAnimationComplete) { | |
| 5457 return true; | |
| 5458 } | |
| 5459 } | |
| 5460 | |
| 5461 return false; | |
| 5462 }, | |
| 5463 | |
| 5464 simulatedRipple: function() { | |
| 5465 this.downAction(null); | |
| 5466 | |
| 5467 // Please see polymer/polymer#1305 | |
| 5468 this.async(function() { | |
| 5469 this.upAction(); | |
| 5470 }, 1); | |
| 5471 }, | |
| 5472 | |
| 5473 /** | |
| 5474 * Provokes a ripple down effect via a UI event, | |
| 5475 * respecting the `noink` property. | |
| 5476 * @param {Event=} event | |
| 5477 */ | |
| 5478 uiDownAction: function(event) { | |
| 5479 if (!this.noink) { | |
| 5480 this.downAction(event); | |
| 5481 } | |
| 5482 }, | |
| 5483 | |
| 5484 /** | |
| 5485 * Provokes a ripple down effect via a UI event, | |
| 5486 * *not* respecting the `noink` property. | |
| 5487 * @param {Event=} event | |
| 5488 */ | |
| 5489 downAction: function(event) { | |
| 5490 if (this.holdDown && this.ripples.length > 0) { | |
| 5491 return; | |
| 5492 } | |
| 5493 | |
| 5494 var ripple = this.addRipple(); | |
| 5495 | |
| 5496 ripple.downAction(event); | |
| 5497 | |
| 5498 if (!this._animating) { | |
| 5499 this._animating = true; | |
| 5500 this.animate(); | |
| 5501 } | |
| 5502 }, | |
| 5503 | |
| 5504 /** | |
| 5505 * Provokes a ripple up effect via a UI event, | |
| 5506 * respecting the `noink` property. | |
| 5507 * @param {Event=} event | |
| 5508 */ | |
| 5509 uiUpAction: function(event) { | |
| 5510 if (!this.noink) { | |
| 5511 this.upAction(event); | |
| 5512 } | |
| 5513 }, | |
| 5514 | |
| 5515 /** | |
| 5516 * Provokes a ripple up effect via a UI event, | |
| 5517 * *not* respecting the `noink` property. | |
| 5518 * @param {Event=} event | |
| 5519 */ | |
| 5520 upAction: function(event) { | |
| 5521 if (this.holdDown) { | |
| 5522 return; | |
| 5523 } | |
| 5524 | |
| 5525 this.ripples.forEach(function(ripple) { | |
| 5526 ripple.upAction(event); | |
| 5527 }); | |
| 5528 | |
| 5529 this._animating = true; | |
| 5530 this.animate(); | |
| 5531 }, | |
| 5532 | |
| 5533 onAnimationComplete: function() { | |
| 5534 this._animating = false; | |
| 5535 this.$.background.style.backgroundColor = null; | |
| 5536 this.fire('transitionend'); | |
| 5537 }, | |
| 5538 | |
| 5539 addRipple: function() { | |
| 5540 var ripple = new Ripple(this); | |
| 5541 | |
| 5542 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
| 5543 this.$.background.style.backgroundColor = ripple.color; | |
| 5544 this.ripples.push(ripple); | |
| 5545 | |
| 5546 this._setAnimating(true); | |
| 5547 | |
| 5548 return ripple; | |
| 5549 }, | |
| 5550 | |
| 5551 removeRipple: function(ripple) { | |
| 5552 var rippleIndex = this.ripples.indexOf(ripple); | |
| 5553 | |
| 5554 if (rippleIndex < 0) { | |
| 5555 return; | |
| 5556 } | |
| 5557 | |
| 5558 this.ripples.splice(rippleIndex, 1); | |
| 5559 | |
| 5560 ripple.remove(); | |
| 5561 | |
| 5562 if (!this.ripples.length) { | |
| 5563 this._setAnimating(false); | |
| 5564 } | |
| 5565 }, | |
| 5566 | |
| 5567 animate: function() { | |
| 5568 if (!this._animating) { | |
| 5569 return; | |
| 5570 } | |
| 5571 var index; | |
| 5572 var ripple; | |
| 5573 | |
| 5574 for (index = 0; index < this.ripples.length; ++index) { | |
| 5575 ripple = this.ripples[index]; | |
| 5576 | |
| 5577 ripple.draw(); | |
| 5578 | |
| 5579 this.$.background.style.opacity = ripple.outerOpacity; | |
| 5580 | |
| 5581 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
| 5582 this.removeRipple(ripple); | |
| 5583 } | |
| 5584 } | |
| 5585 | |
| 5586 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
| 5587 this.onAnimationComplete(); | |
| 5588 } else { | |
| 5589 window.requestAnimationFrame(this._boundAnimate); | |
| 5590 } | |
| 5591 }, | |
| 5592 | |
| 5593 _onEnterKeydown: function() { | |
| 5594 this.uiDownAction(); | |
| 5595 this.async(this.uiUpAction, 1); | |
| 5596 }, | |
| 5597 | |
| 5598 _onSpaceKeydown: function() { | |
| 5599 this.uiDownAction(); | |
| 5600 }, | |
| 5601 | |
| 5602 _onSpaceKeyup: function() { | |
| 5603 this.uiUpAction(); | |
| 5604 }, | |
| 5605 | |
| 5606 // note: holdDown does not respect noink since it can be a focus based | |
| 5607 // effect. | |
| 5608 _holdDownChanged: function(newVal, oldVal) { | |
| 5609 if (oldVal === undefined) { | |
| 5610 return; | |
| 5611 } | |
| 5612 if (newVal) { | |
| 5613 this.downAction(); | |
| 5614 } else { | |
| 5615 this.upAction(); | |
| 5616 } | |
| 5617 } | |
| 5618 | |
| 5619 /** | |
| 5620 Fired when the animation finishes. | |
| 5621 This is useful if you want to wait until | |
| 5622 the ripple animation finishes to perform some action. | |
| 5623 | |
| 5624 @event transitionend | |
| 5625 @param {{node: Object}} detail Contains the animated node. | |
| 5626 */ | |
| 5627 }); | |
| 5628 })(); | |
| 5629 /** | |
| 5630 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
| 5631 * when the element has focus via pointer or keyboard. | |
| 5632 * | |
| 5633 * NOTE: This behavior is intended to be used in conjunction with and after | |
| 5634 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
| 5635 * | |
| 5636 * @polymerBehavior Polymer.PaperRippleBehavior | |
| 5637 */ | |
| 5638 Polymer.PaperRippleBehavior = { | |
| 5639 properties: { | |
| 5640 /** | |
| 5641 * If true, the element will not produce a ripple effect when interacted | |
| 5642 * with via the pointer. | |
| 5643 */ | |
| 5644 noink: { | |
| 5645 type: Boolean, | |
| 5646 observer: '_noinkChanged' | |
| 5647 }, | |
| 5648 | |
| 5649 /** | |
| 5650 * @type {Element|undefined} | |
| 5651 */ | |
| 5652 _rippleContainer: { | |
| 5653 type: Object, | |
| 5654 } | |
| 5655 }, | |
| 5656 | |
| 5657 /** | |
| 5658 * Ensures a `<paper-ripple>` element is available when the element is | |
| 5659 * focused. | |
| 5660 */ | |
| 5661 _buttonStateChanged: function() { | |
| 5662 if (this.focused) { | |
| 5663 this.ensureRipple(); | 5229 this.ensureRipple(); |
| 5664 } | 5230 } |
| 5665 }, | 5231 if (this.hasRipple()) { |
| 5666 | 5232 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 5667 /** | 5233 } |
| 5668 * In addition to the functionality provided in `IronButtonState`, ensures | 5234 }, |
| 5669 * a ripple effect is created when the element is in a `pressed` state. | 5235 |
| 5670 */ | |
| 5671 _downHandler: function(event) { | |
| 5672 Polymer.IronButtonStateImpl._downHandler.call(this, event); | |
| 5673 if (this.pressed) { | |
| 5674 this.ensureRipple(event); | |
| 5675 } | |
| 5676 }, | |
| 5677 | |
| 5678 /** | |
| 5679 * Ensures this element contains a ripple effect. For startup efficiency | |
| 5680 * the ripple effect is dynamically on demand when needed. | |
| 5681 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
| 5682 * ripple. | |
| 5683 */ | |
| 5684 ensureRipple: function(optTriggeringEvent) { | |
| 5685 if (!this.hasRipple()) { | |
| 5686 this._ripple = this._createRipple(); | |
| 5687 this._ripple.noink = this.noink; | |
| 5688 var rippleContainer = this._rippleContainer || this.root; | |
| 5689 if (rippleContainer) { | |
| 5690 Polymer.dom(rippleContainer).appendChild(this._ripple); | |
| 5691 } | |
| 5692 if (optTriggeringEvent) { | |
| 5693 // Check if the event happened inside of the ripple container | |
| 5694 // Fall back to host instead of the root because distributed text | |
| 5695 // nodes are not valid event targets | |
| 5696 var domContainer = Polymer.dom(this._rippleContainer || this); | |
| 5697 var target = Polymer.dom(optTriggeringEvent).rootTarget; | |
| 5698 if (domContainer.deepContains( /** @type {Node} */(target))) { | |
| 5699 this._ripple.uiDownAction(optTriggeringEvent); | |
| 5700 } | |
| 5701 } | |
| 5702 } | |
| 5703 }, | |
| 5704 | |
| 5705 /** | |
| 5706 * Returns the `<paper-ripple>` element used by this element to create | |
| 5707 * ripple effects. The element's ripple is created on demand, when | |
| 5708 * necessary, and calling this method will force the | |
| 5709 * ripple to be created. | |
| 5710 */ | |
| 5711 getRipple: function() { | |
| 5712 this.ensureRipple(); | |
| 5713 return this._ripple; | |
| 5714 }, | |
| 5715 | |
| 5716 /** | |
| 5717 * Returns true if this element currently contains a ripple effect. | |
| 5718 * @return {boolean} | |
| 5719 */ | |
| 5720 hasRipple: function() { | |
| 5721 return Boolean(this._ripple); | |
| 5722 }, | |
| 5723 | |
| 5724 /** | |
| 5725 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
| 5726 * Override this method to customize the ripple element. | |
| 5727 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
| 5728 */ | |
| 5729 _createRipple: function() { | 5236 _createRipple: function() { |
| 5730 return /** @type {!PaperRippleElement} */ ( | 5237 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 5731 document.createElement('paper-ripple')); | 5238 ripple.id = 'ink'; |
| 5732 }, | 5239 ripple.setAttribute('center', ''); |
| 5733 | 5240 ripple.classList.add('circle'); |
| 5734 _noinkChanged: function(noink) { | 5241 return ripple; |
| 5735 if (this.hasRipple()) { | |
| 5736 this._ripple.noink = noink; | |
| 5737 } | |
| 5738 } | 5242 } |
| 5739 }; | 5243 }; |
| 5740 /** @polymerBehavior Polymer.PaperButtonBehavior */ | 5244 |
| 5741 Polymer.PaperButtonBehaviorImpl = { | 5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ |
| 5742 properties: { | 5246 Polymer.PaperInkyFocusBehavior = [ |
| 5743 /** | |
| 5744 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
| 5745 * shadow, and each increasing number greater than 0 will be "deeper" | |
| 5746 * than the last. | |
| 5747 * | |
| 5748 * @attribute elevation | |
| 5749 * @type number | |
| 5750 * @default 1 | |
| 5751 */ | |
| 5752 elevation: { | |
| 5753 type: Number, | |
| 5754 reflectToAttribute: true, | |
| 5755 readOnly: true | |
| 5756 } | |
| 5757 }, | |
| 5758 | |
| 5759 observers: [ | |
| 5760 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
| 5761 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
| 5762 ], | |
| 5763 | |
| 5764 hostAttributes: { | |
| 5765 role: 'button', | |
| 5766 tabindex: '0', | |
| 5767 animated: true | |
| 5768 }, | |
| 5769 | |
| 5770 _calculateElevation: function() { | |
| 5771 var e = 1; | |
| 5772 if (this.disabled) { | |
| 5773 e = 0; | |
| 5774 } else if (this.active || this.pressed) { | |
| 5775 e = 4; | |
| 5776 } else if (this.receivedFocusFromKeyboard) { | |
| 5777 e = 3; | |
| 5778 } | |
| 5779 this._setElevation(e); | |
| 5780 }, | |
| 5781 | |
| 5782 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
| 5783 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
| 5784 }, | |
| 5785 | |
| 5786 /** | |
| 5787 * In addition to `IronButtonState` behavior, when space key goes down, | |
| 5788 * create a ripple down effect. | |
| 5789 * | |
| 5790 * @param {!KeyboardEvent} event . | |
| 5791 */ | |
| 5792 _spaceKeyDownHandler: function(event) { | |
| 5793 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
| 5794 // Ensure that there is at most one ripple when the space key is held down
. | |
| 5795 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
| 5796 this._ripple.uiDownAction(); | |
| 5797 } | |
| 5798 }, | |
| 5799 | |
| 5800 /** | |
| 5801 * In addition to `IronButtonState` behavior, when space key goes up, | |
| 5802 * create a ripple up effect. | |
| 5803 * | |
| 5804 * @param {!KeyboardEvent} event . | |
| 5805 */ | |
| 5806 _spaceKeyUpHandler: function(event) { | |
| 5807 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
| 5808 if (this.hasRipple()) { | |
| 5809 this._ripple.uiUpAction(); | |
| 5810 } | |
| 5811 } | |
| 5812 }; | |
| 5813 | |
| 5814 /** @polymerBehavior */ | |
| 5815 Polymer.PaperButtonBehavior = [ | |
| 5816 Polymer.IronButtonState, | 5247 Polymer.IronButtonState, |
| 5817 Polymer.IronControlState, | 5248 Polymer.IronControlState, |
| 5818 Polymer.PaperRippleBehavior, | 5249 Polymer.PaperRippleBehavior, |
| 5819 Polymer.PaperButtonBehaviorImpl | 5250 Polymer.PaperInkyFocusBehaviorImpl |
| 5820 ]; | 5251 ]; |
| 5821 Polymer({ | 5252 Polymer({ |
| 5822 is: 'paper-button', | 5253 is: 'paper-icon-button', |
| 5254 |
| 5255 hostAttributes: { |
| 5256 role: 'button', |
| 5257 tabindex: '0' |
| 5258 }, |
| 5823 | 5259 |
| 5824 behaviors: [ | 5260 behaviors: [ |
| 5825 Polymer.PaperButtonBehavior | 5261 Polymer.PaperInkyFocusBehavior |
| 5826 ], | 5262 ], |
| 5827 | 5263 |
| 5828 properties: { | 5264 properties: { |
| 5829 /** | 5265 /** |
| 5830 * If true, the button should be styled with a shadow. | 5266 * The URL of an image for the icon. If the src property is specified, |
| 5267 * the icon property should not be. |
| 5831 */ | 5268 */ |
| 5832 raised: { | 5269 src: { |
| 5833 type: Boolean, | 5270 type: String |
| 5834 reflectToAttribute: true, | 5271 }, |
| 5835 value: false, | 5272 |
| 5836 observer: '_calculateElevation' | 5273 /** |
| 5837 } | 5274 * Specifies the icon name or index in the set of icons available in |
| 5838 }, | 5275 * the icon's icon set. If the icon property is specified, |
| 5839 | 5276 * the src property should not be. |
| 5840 _calculateElevation: function() { | 5277 */ |
| 5841 if (!this.raised) { | 5278 icon: { |
| 5842 this._setElevation(0); | 5279 type: String |
| 5843 } else { | 5280 }, |
| 5844 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | 5281 |
| 5845 } | 5282 /** |
| 5846 } | 5283 * Specifies the alternate text for the button, for accessibility. |
| 5847 | 5284 */ |
| 5848 /** | 5285 alt: { |
| 5849 Fired when the animation finishes. | 5286 type: String, |
| 5850 This is useful if you want to wait until | 5287 observer: "_altChanged" |
| 5851 the ripple animation finishes to perform some action. | 5288 } |
| 5852 | 5289 }, |
| 5853 @event transitionend | 5290 |
| 5854 Event param: {{node: Object}} detail Contains the animated node. | 5291 _altChanged: function(newValue, oldValue) { |
| 5855 */ | 5292 var label = this.getAttribute('aria-label'); |
| 5293 |
| 5294 // Don't stomp over a user-set aria-label. |
| 5295 if (!label || oldValue == label) { |
| 5296 this.setAttribute('aria-label', newValue); |
| 5297 } |
| 5298 } |
| 5856 }); | 5299 }); |
| 5857 Polymer({ | 5300 Polymer({ |
| 5858 is: 'paper-icon-button-light', | 5301 is: 'paper-tab', |
| 5859 extends: 'button', | |
| 5860 | 5302 |
| 5861 behaviors: [ | 5303 behaviors: [ |
| 5304 Polymer.IronControlState, |
| 5305 Polymer.IronButtonState, |
| 5862 Polymer.PaperRippleBehavior | 5306 Polymer.PaperRippleBehavior |
| 5863 ], | 5307 ], |
| 5864 | 5308 |
| 5309 properties: { |
| 5310 |
| 5311 /** |
| 5312 * If true, the tab will forward keyboard clicks (enter/space) to |
| 5313 * the first anchor element found in its descendants |
| 5314 */ |
| 5315 link: { |
| 5316 type: Boolean, |
| 5317 value: false, |
| 5318 reflectToAttribute: true |
| 5319 } |
| 5320 |
| 5321 }, |
| 5322 |
| 5323 hostAttributes: { |
| 5324 role: 'tab' |
| 5325 }, |
| 5326 |
| 5865 listeners: { | 5327 listeners: { |
| 5866 'down': '_rippleDown', | 5328 down: '_updateNoink', |
| 5867 'up': '_rippleUp', | 5329 tap: '_onTap' |
| 5868 'focus': '_rippleDown', | 5330 }, |
| 5869 'blur': '_rippleUp', | 5331 |
| 5870 }, | 5332 attached: function() { |
| 5871 | 5333 this._updateNoink(); |
| 5872 _rippleDown: function() { | 5334 }, |
| 5873 this.getRipple().downAction(); | 5335 |
| 5874 }, | 5336 get _parentNoink () { |
| 5875 | 5337 var parent = Polymer.dom(this).parentNode; |
| 5876 _rippleUp: function() { | 5338 return !!parent && !!parent.noink; |
| 5877 this.getRipple().upAction(); | 5339 }, |
| 5878 }, | 5340 |
| 5879 | 5341 _updateNoink: function() { |
| 5880 /** | 5342 this.noink = !!this.noink || !!this._parentNoink; |
| 5881 * @param {...*} var_args | 5343 }, |
| 5882 */ | 5344 |
| 5883 ensureRipple: function(var_args) { | 5345 _onTap: function(event) { |
| 5884 var lastRipple = this._ripple; | 5346 if (this.link) { |
| 5885 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | 5347 var anchor = this.queryEffectiveChildren('a'); |
| 5886 if (this._ripple && this._ripple !== lastRipple) { | 5348 |
| 5887 this._ripple.center = true; | 5349 if (!anchor) { |
| 5888 this._ripple.classList.add('circle'); | 5350 return; |
| 5889 } | 5351 } |
| 5890 } | 5352 |
| 5353 // Don't get stuck in a loop delegating |
| 5354 // the listener from the child anchor |
| 5355 if (event.target === anchor) { |
| 5356 return; |
| 5357 } |
| 5358 |
| 5359 anchor.click(); |
| 5360 } |
| 5361 } |
| 5362 |
| 5891 }); | 5363 }); |
| 5892 /** | 5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ |
| 5893 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | 5365 Polymer.IronMultiSelectableBehaviorImpl = { |
| 5894 * | |
| 5895 * @demo demo/index.html | |
| 5896 * @polymerBehavior | |
| 5897 */ | |
| 5898 Polymer.IronRangeBehavior = { | |
| 5899 | |
| 5900 properties: { | |
| 5901 | |
| 5902 /** | |
| 5903 * The number that represents the current value. | |
| 5904 */ | |
| 5905 value: { | |
| 5906 type: Number, | |
| 5907 value: 0, | |
| 5908 notify: true, | |
| 5909 reflectToAttribute: true | |
| 5910 }, | |
| 5911 | |
| 5912 /** | |
| 5913 * The number that indicates the minimum value of the range. | |
| 5914 */ | |
| 5915 min: { | |
| 5916 type: Number, | |
| 5917 value: 0, | |
| 5918 notify: true | |
| 5919 }, | |
| 5920 | |
| 5921 /** | |
| 5922 * The number that indicates the maximum value of the range. | |
| 5923 */ | |
| 5924 max: { | |
| 5925 type: Number, | |
| 5926 value: 100, | |
| 5927 notify: true | |
| 5928 }, | |
| 5929 | |
| 5930 /** | |
| 5931 * Specifies the value granularity of the range's value. | |
| 5932 */ | |
| 5933 step: { | |
| 5934 type: Number, | |
| 5935 value: 1, | |
| 5936 notify: true | |
| 5937 }, | |
| 5938 | |
| 5939 /** | |
| 5940 * Returns the ratio of the value. | |
| 5941 */ | |
| 5942 ratio: { | |
| 5943 type: Number, | |
| 5944 value: 0, | |
| 5945 readOnly: true, | |
| 5946 notify: true | |
| 5947 }, | |
| 5948 }, | |
| 5949 | |
| 5950 observers: [ | |
| 5951 '_update(value, min, max, step)' | |
| 5952 ], | |
| 5953 | |
| 5954 _calcRatio: function(value) { | |
| 5955 return (this._clampValue(value) - this.min) / (this.max - this.min); | |
| 5956 }, | |
| 5957 | |
| 5958 _clampValue: function(value) { | |
| 5959 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | |
| 5960 }, | |
| 5961 | |
| 5962 _calcStep: function(value) { | |
| 5963 // polymer/issues/2493 | |
| 5964 value = parseFloat(value); | |
| 5965 | |
| 5966 if (!this.step) { | |
| 5967 return value; | |
| 5968 } | |
| 5969 | |
| 5970 var numSteps = Math.round((value - this.min) / this.step); | |
| 5971 if (this.step < 1) { | |
| 5972 /** | |
| 5973 * For small values of this.step, if we calculate the step using | |
| 5974 * `Math.round(value / step) * step` we may hit a precision point issue | |
| 5975 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
| 5976 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
| 5977 * | |
| 5978 * as a work around we can divide by the reciprocal of `step` | |
| 5979 */ | |
| 5980 return numSteps / (1 / this.step) + this.min; | |
| 5981 } else { | |
| 5982 return numSteps * this.step + this.min; | |
| 5983 } | |
| 5984 }, | |
| 5985 | |
| 5986 _validateValue: function() { | |
| 5987 var v = this._clampValue(this.value); | |
| 5988 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | |
| 5989 return this.value !== v; | |
| 5990 }, | |
| 5991 | |
| 5992 _update: function() { | |
| 5993 this._validateValue(); | |
| 5994 this._setRatio(this._calcRatio(this.value) * 100); | |
| 5995 } | |
| 5996 | |
| 5997 }; | |
| 5998 Polymer({ | |
| 5999 is: 'paper-progress', | |
| 6000 | |
| 6001 behaviors: [ | |
| 6002 Polymer.IronRangeBehavior | |
| 6003 ], | |
| 6004 | |
| 6005 properties: { | 5366 properties: { |
| 6006 /** | 5367 |
| 6007 * The number that represents the current secondary progress. | 5368 /** |
| 6008 */ | 5369 * If true, multiple selections are allowed. |
| 6009 secondaryProgress: { | 5370 */ |
| 6010 type: Number, | 5371 multi: { |
| 6011 value: 0 | |
| 6012 }, | |
| 6013 | |
| 6014 /** | |
| 6015 * The secondary ratio | |
| 6016 */ | |
| 6017 secondaryRatio: { | |
| 6018 type: Number, | |
| 6019 value: 0, | |
| 6020 readOnly: true | |
| 6021 }, | |
| 6022 | |
| 6023 /** | |
| 6024 * Use an indeterminate progress indicator. | |
| 6025 */ | |
| 6026 indeterminate: { | |
| 6027 type: Boolean, | 5372 type: Boolean, |
| 6028 value: false, | 5373 value: false, |
| 6029 observer: '_toggleIndeterminate' | 5374 observer: 'multiChanged' |
| 6030 }, | 5375 }, |
| 6031 | 5376 |
| 6032 /** | 5377 /** |
| 6033 * True if the progress is disabled. | 5378 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` |
| 6034 */ | 5379 * is true. |
| 6035 disabled: { | 5380 */ |
| 6036 type: Boolean, | 5381 selectedValues: { |
| 6037 value: false, | 5382 type: Array, |
| 6038 reflectToAttribute: true, | 5383 notify: true |
| 6039 observer: '_disabledChanged' | 5384 }, |
| 6040 } | 5385 |
| 5386 /** |
| 5387 * Returns an array of currently selected items. |
| 5388 */ |
| 5389 selectedItems: { |
| 5390 type: Array, |
| 5391 readOnly: true, |
| 5392 notify: true |
| 5393 }, |
| 5394 |
| 6041 }, | 5395 }, |
| 6042 | 5396 |
| 6043 observers: [ | 5397 observers: [ |
| 6044 '_progressChanged(secondaryProgress, value, min, max)' | 5398 '_updateSelected(selectedValues.splices)' |
| 6045 ], | 5399 ], |
| 6046 | 5400 |
| 5401 /** |
| 5402 * Selects the given value. If the `multi` property is true, then the select
ed state of the |
| 5403 * `value` will be toggled; otherwise the `value` will be selected. |
| 5404 * |
| 5405 * @method select |
| 5406 * @param {string|number} value the value to select. |
| 5407 */ |
| 5408 select: function(value) { |
| 5409 if (this.multi) { |
| 5410 if (this.selectedValues) { |
| 5411 this._toggleSelected(value); |
| 5412 } else { |
| 5413 this.selectedValues = [value]; |
| 5414 } |
| 5415 } else { |
| 5416 this.selected = value; |
| 5417 } |
| 5418 }, |
| 5419 |
| 5420 multiChanged: function(multi) { |
| 5421 this._selection.multi = multi; |
| 5422 }, |
| 5423 |
| 5424 get _shouldUpdateSelection() { |
| 5425 return this.selected != null || |
| 5426 (this.selectedValues != null && this.selectedValues.length); |
| 5427 }, |
| 5428 |
| 5429 _updateAttrForSelected: function() { |
| 5430 if (!this.multi) { |
| 5431 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 5432 } else if (this._shouldUpdateSelection) { |
| 5433 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 5434 return this._indexToValue(this.indexOf(selectedItem)); |
| 5435 }, this).filter(function(unfilteredValue) { |
| 5436 return unfilteredValue != null; |
| 5437 }, this); |
| 5438 } |
| 5439 }, |
| 5440 |
| 5441 _updateSelected: function() { |
| 5442 if (this.multi) { |
| 5443 this._selectMulti(this.selectedValues); |
| 5444 } else { |
| 5445 this._selectSelected(this.selected); |
| 5446 } |
| 5447 }, |
| 5448 |
| 5449 _selectMulti: function(values) { |
| 5450 if (values) { |
| 5451 var selectedItems = this._valuesToItems(values); |
| 5452 // clear all but the current selected items |
| 5453 this._selection.clear(selectedItems); |
| 5454 // select only those not selected yet |
| 5455 for (var i = 0; i < selectedItems.length; i++) { |
| 5456 this._selection.setItemSelected(selectedItems[i], true); |
| 5457 } |
| 5458 // Check for items, since this array is populated only when attached |
| 5459 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { |
| 5460 var fallback = this._valueToItem(this.fallbackSelection); |
| 5461 if (fallback) { |
| 5462 this.selectedValues = [this.fallbackSelection]; |
| 5463 } |
| 5464 } |
| 5465 } else { |
| 5466 this._selection.clear(); |
| 5467 } |
| 5468 }, |
| 5469 |
| 5470 _selectionChange: function() { |
| 5471 var s = this._selection.get(); |
| 5472 if (this.multi) { |
| 5473 this._setSelectedItems(s); |
| 5474 } else { |
| 5475 this._setSelectedItems([s]); |
| 5476 this._setSelectedItem(s); |
| 5477 } |
| 5478 }, |
| 5479 |
| 5480 _toggleSelected: function(value) { |
| 5481 var i = this.selectedValues.indexOf(value); |
| 5482 var unselected = i < 0; |
| 5483 if (unselected) { |
| 5484 this.push('selectedValues',value); |
| 5485 } else { |
| 5486 this.splice('selectedValues',i,1); |
| 5487 } |
| 5488 }, |
| 5489 |
| 5490 _valuesToItems: function(values) { |
| 5491 return (values == null) ? null : values.map(function(value) { |
| 5492 return this._valueToItem(value); |
| 5493 }, this); |
| 5494 } |
| 5495 }; |
| 5496 |
| 5497 /** @polymerBehavior */ |
| 5498 Polymer.IronMultiSelectableBehavior = [ |
| 5499 Polymer.IronSelectableBehavior, |
| 5500 Polymer.IronMultiSelectableBehaviorImpl |
| 5501 ]; |
| 5502 /** |
| 5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior. |
| 5504 * |
| 5505 * @demo demo/index.html |
| 5506 * @polymerBehavior Polymer.IronMenuBehavior |
| 5507 */ |
| 5508 Polymer.IronMenuBehaviorImpl = { |
| 5509 |
| 5510 properties: { |
| 5511 |
| 5512 /** |
| 5513 * Returns the currently focused item. |
| 5514 * @type {?Object} |
| 5515 */ |
| 5516 focusedItem: { |
| 5517 observer: '_focusedItemChanged', |
| 5518 readOnly: true, |
| 5519 type: Object |
| 5520 }, |
| 5521 |
| 5522 /** |
| 5523 * The attribute to use on menu items to look up the item title. Typing th
e first |
| 5524 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` |
| 5525 * will be used. |
| 5526 */ |
| 5527 attrForItemTitle: { |
| 5528 type: String |
| 5529 } |
| 5530 }, |
| 5531 |
| 6047 hostAttributes: { | 5532 hostAttributes: { |
| 6048 role: 'progressbar' | 5533 'role': 'menu', |
| 6049 }, | 5534 'tabindex': '0' |
| 6050 | 5535 }, |
| 6051 _toggleIndeterminate: function(indeterminate) { | 5536 |
| 6052 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | 5537 observers: [ |
| 6053 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | 5538 '_updateMultiselectable(multi)' |
| 6054 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 5539 ], |
| 6055 }, | 5540 |
| 6056 | 5541 listeners: { |
| 6057 _transformProgress: function(progress, ratio) { | 5542 'focus': '_onFocus', |
| 6058 var transform = 'scaleX(' + (ratio / 100) + ')'; | 5543 'keydown': '_onKeydown', |
| 6059 progress.style.transform = progress.style.webkitTransform = transform; | 5544 'iron-items-changed': '_onIronItemsChanged' |
| 6060 }, | 5545 }, |
| 6061 | 5546 |
| 6062 _mainRatioChanged: function(ratio) { | 5547 keyBindings: { |
| 6063 this._transformProgress(this.$.primaryProgress, ratio); | 5548 'up': '_onUpKey', |
| 6064 }, | 5549 'down': '_onDownKey', |
| 6065 | 5550 'esc': '_onEscKey', |
| 6066 _progressChanged: function(secondaryProgress, value, min, max) { | 5551 'shift+tab:keydown': '_onShiftTabDown' |
| 6067 secondaryProgress = this._clampValue(secondaryProgress); | 5552 }, |
| 6068 value = this._clampValue(value); | 5553 |
| 6069 | 5554 attached: function() { |
| 6070 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; | 5555 this._resetTabindices(); |
| 6071 var mainRatio = this._calcRatio(value) * 100; | 5556 }, |
| 6072 | 5557 |
| 6073 this._setSecondaryRatio(secondaryRatio); | 5558 /** |
| 6074 this._transformProgress(this.$.secondaryProgress, secondaryRatio); | 5559 * Selects the given value. If the `multi` property is true, then the select
ed state of the |
| 6075 this._transformProgress(this.$.primaryProgress, mainRatio); | 5560 * `value` will be toggled; otherwise the `value` will be selected. |
| 6076 | 5561 * |
| 6077 this.secondaryProgress = secondaryProgress; | 5562 * @param {string|number} value the value to select. |
| 6078 | 5563 */ |
| 6079 this.setAttribute('aria-valuenow', value); | 5564 select: function(value) { |
| 6080 this.setAttribute('aria-valuemin', min); | 5565 // Cancel automatically focusing a default item if the menu received focus |
| 6081 this.setAttribute('aria-valuemax', max); | 5566 // through a user action selecting a particular item. |
| 6082 }, | 5567 if (this._defaultFocusAsync) { |
| 6083 | 5568 this.cancelAsync(this._defaultFocusAsync); |
| 6084 _disabledChanged: function(disabled) { | 5569 this._defaultFocusAsync = null; |
| 6085 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 5570 } |
| 6086 }, | 5571 var item = this._valueToItem(value); |
| 6087 | 5572 if (item && item.hasAttribute('disabled')) return; |
| 6088 _hideSecondaryProgress: function(secondaryRatio) { | 5573 this._setFocusedItem(item); |
| 6089 return secondaryRatio === 0; | 5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| 5575 }, |
| 5576 |
| 5577 /** |
| 5578 * Resets all tabindex attributes to the appropriate value based on the |
| 5579 * current selection state. The appropriate value is `0` (focusable) for |
| 5580 * the default selected item, and `-1` (not keyboard focusable) for all |
| 5581 * other items. |
| 5582 */ |
| 5583 _resetTabindices: function() { |
| 5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; |
| 5585 |
| 5586 this.items.forEach(function(item) { |
| 5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| 5588 }, this); |
| 5589 }, |
| 5590 |
| 5591 /** |
| 5592 * Sets appropriate ARIA based on whether or not the menu is meant to be |
| 5593 * multi-selectable. |
| 5594 * |
| 5595 * @param {boolean} multi True if the menu should be multi-selectable. |
| 5596 */ |
| 5597 _updateMultiselectable: function(multi) { |
| 5598 if (multi) { |
| 5599 this.setAttribute('aria-multiselectable', 'true'); |
| 5600 } else { |
| 5601 this.removeAttribute('aria-multiselectable'); |
| 5602 } |
| 5603 }, |
| 5604 |
| 5605 /** |
| 5606 * Given a KeyboardEvent, this method will focus the appropriate item in the |
| 5607 * menu (if there is a relevant item, and it is possible to focus it). |
| 5608 * |
| 5609 * @param {KeyboardEvent} event A KeyboardEvent. |
| 5610 */ |
| 5611 _focusWithKeyboardEvent: function(event) { |
| 5612 for (var i = 0, item; item = this.items[i]; i++) { |
| 5613 var attr = this.attrForItemTitle || 'textContent'; |
| 5614 var title = item[attr] || item.getAttribute(attr); |
| 5615 |
| 5616 if (!item.hasAttribute('disabled') && title && |
| 5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { |
| 5618 this._setFocusedItem(item); |
| 5619 break; |
| 5620 } |
| 5621 } |
| 5622 }, |
| 5623 |
| 5624 /** |
| 5625 * Focuses the previous item (relative to the currently focused item) in the |
| 5626 * menu, disabled items will be skipped. |
| 5627 * Loop until length + 1 to handle case of single item in menu. |
| 5628 */ |
| 5629 _focusPrevious: function() { |
| 5630 var length = this.items.length; |
| 5631 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 5632 for (var i = 1; i < length + 1; i++) { |
| 5633 var item = this.items[(curFocusIndex - i + length) % length]; |
| 5634 if (!item.hasAttribute('disabled')) { |
| 5635 this._setFocusedItem(item); |
| 5636 return; |
| 5637 } |
| 5638 } |
| 5639 }, |
| 5640 |
| 5641 /** |
| 5642 * Focuses the next item (relative to the currently focused item) in the |
| 5643 * menu, disabled items will be skipped. |
| 5644 * Loop until length + 1 to handle case of single item in menu. |
| 5645 */ |
| 5646 _focusNext: function() { |
| 5647 var length = this.items.length; |
| 5648 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| 5649 for (var i = 1; i < length + 1; i++) { |
| 5650 var item = this.items[(curFocusIndex + i) % length]; |
| 5651 if (!item.hasAttribute('disabled')) { |
| 5652 this._setFocusedItem(item); |
| 5653 return; |
| 5654 } |
| 5655 } |
| 5656 }, |
| 5657 |
| 5658 /** |
| 5659 * Mutates items in the menu based on provided selection details, so that |
| 5660 * all items correctly reflect selection state. |
| 5661 * |
| 5662 * @param {Element} item An item in the menu. |
| 5663 * @param {boolean} isSelected True if the item should be shown in a |
| 5664 * selected state, otherwise false. |
| 5665 */ |
| 5666 _applySelection: function(item, isSelected) { |
| 5667 if (isSelected) { |
| 5668 item.setAttribute('aria-selected', 'true'); |
| 5669 } else { |
| 5670 item.removeAttribute('aria-selected'); |
| 5671 } |
| 5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| 5673 }, |
| 5674 |
| 5675 /** |
| 5676 * Discretely updates tabindex values among menu items as the focused item |
| 5677 * changes. |
| 5678 * |
| 5679 * @param {Element} focusedItem The element that is currently focused. |
| 5680 * @param {?Element} old The last element that was considered focused, if |
| 5681 * applicable. |
| 5682 */ |
| 5683 _focusedItemChanged: function(focusedItem, old) { |
| 5684 old && old.setAttribute('tabindex', '-1'); |
| 5685 if (focusedItem) { |
| 5686 focusedItem.setAttribute('tabindex', '0'); |
| 5687 focusedItem.focus(); |
| 5688 } |
| 5689 }, |
| 5690 |
| 5691 /** |
| 5692 * A handler that responds to mutation changes related to the list of items |
| 5693 * in the menu. |
| 5694 * |
| 5695 * @param {CustomEvent} event An event containing mutation records as its |
| 5696 * detail. |
| 5697 */ |
| 5698 _onIronItemsChanged: function(event) { |
| 5699 if (event.detail.addedNodes.length) { |
| 5700 this._resetTabindices(); |
| 5701 } |
| 5702 }, |
| 5703 |
| 5704 /** |
| 5705 * Handler that is called when a shift+tab keypress is detected by the menu. |
| 5706 * |
| 5707 * @param {CustomEvent} event A key combination event. |
| 5708 */ |
| 5709 _onShiftTabDown: function(event) { |
| 5710 var oldTabIndex = this.getAttribute('tabindex'); |
| 5711 |
| 5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| 5713 |
| 5714 this._setFocusedItem(null); |
| 5715 |
| 5716 this.setAttribute('tabindex', '-1'); |
| 5717 |
| 5718 this.async(function() { |
| 5719 this.setAttribute('tabindex', oldTabIndex); |
| 5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 5721 // NOTE(cdata): polymer/polymer#1305 |
| 5722 }, 1); |
| 5723 }, |
| 5724 |
| 5725 /** |
| 5726 * Handler that is called when the menu receives focus. |
| 5727 * |
| 5728 * @param {FocusEvent} event A focus event. |
| 5729 */ |
| 5730 _onFocus: function(event) { |
| 5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| 5732 // do not focus the menu itself |
| 5733 return; |
| 5734 } |
| 5735 |
| 5736 // Do not focus the selected tab if the deepest target is part of the |
| 5737 // menu element's local DOM and is focusable. |
| 5738 var rootTarget = /** @type {?HTMLElement} */( |
| 5739 Polymer.dom(event).rootTarget); |
| 5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { |
| 5741 return; |
| 5742 } |
| 5743 |
| 5744 // clear the cached focus item |
| 5745 this._defaultFocusAsync = this.async(function() { |
| 5746 // focus the selected item when the menu receives focus, or the first it
em |
| 5747 // if no item is selected |
| 5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; |
| 5749 |
| 5750 this._setFocusedItem(null); |
| 5751 |
| 5752 if (selectedItem) { |
| 5753 this._setFocusedItem(selectedItem); |
| 5754 } else if (this.items[0]) { |
| 5755 // We find the first none-disabled item (if one exists) |
| 5756 this._focusNext(); |
| 5757 } |
| 5758 }); |
| 5759 }, |
| 5760 |
| 5761 /** |
| 5762 * Handler that is called when the up key is pressed. |
| 5763 * |
| 5764 * @param {CustomEvent} event A key combination event. |
| 5765 */ |
| 5766 _onUpKey: function(event) { |
| 5767 // up and down arrows moves the focus |
| 5768 this._focusPrevious(); |
| 5769 event.detail.keyboardEvent.preventDefault(); |
| 5770 }, |
| 5771 |
| 5772 /** |
| 5773 * Handler that is called when the down key is pressed. |
| 5774 * |
| 5775 * @param {CustomEvent} event A key combination event. |
| 5776 */ |
| 5777 _onDownKey: function(event) { |
| 5778 this._focusNext(); |
| 5779 event.detail.keyboardEvent.preventDefault(); |
| 5780 }, |
| 5781 |
| 5782 /** |
| 5783 * Handler that is called when the esc key is pressed. |
| 5784 * |
| 5785 * @param {CustomEvent} event A key combination event. |
| 5786 */ |
| 5787 _onEscKey: function(event) { |
| 5788 // esc blurs the control |
| 5789 this.focusedItem.blur(); |
| 5790 }, |
| 5791 |
| 5792 /** |
| 5793 * Handler that is called when a keydown event is detected. |
| 5794 * |
| 5795 * @param {KeyboardEvent} event A keyboard event. |
| 5796 */ |
| 5797 _onKeydown: function(event) { |
| 5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| 5799 // all other keys focus the menu item starting with that character |
| 5800 this._focusWithKeyboardEvent(event); |
| 5801 } |
| 5802 event.stopPropagation(); |
| 5803 }, |
| 5804 |
| 5805 // override _activateHandler |
| 5806 _activateHandler: function(event) { |
| 5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| 5808 event.stopPropagation(); |
| 6090 } | 5809 } |
| 6091 }); | 5810 }; |
| 5811 |
| 5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| 5813 |
| 5814 /** @polymerBehavior Polymer.IronMenuBehavior */ |
| 5815 Polymer.IronMenuBehavior = [ |
| 5816 Polymer.IronMultiSelectableBehavior, |
| 5817 Polymer.IronA11yKeysBehavior, |
| 5818 Polymer.IronMenuBehaviorImpl |
| 5819 ]; |
| 5820 /** |
| 5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior. |
| 5822 * |
| 5823 * @polymerBehavior Polymer.IronMenubarBehavior |
| 5824 */ |
| 5825 Polymer.IronMenubarBehaviorImpl = { |
| 5826 |
| 5827 hostAttributes: { |
| 5828 'role': 'menubar' |
| 5829 }, |
| 5830 |
| 5831 keyBindings: { |
| 5832 'left': '_onLeftKey', |
| 5833 'right': '_onRightKey' |
| 5834 }, |
| 5835 |
| 5836 _onUpKey: function(event) { |
| 5837 this.focusedItem.click(); |
| 5838 event.detail.keyboardEvent.preventDefault(); |
| 5839 }, |
| 5840 |
| 5841 _onDownKey: function(event) { |
| 5842 this.focusedItem.click(); |
| 5843 event.detail.keyboardEvent.preventDefault(); |
| 5844 }, |
| 5845 |
| 5846 get _isRTL() { |
| 5847 return window.getComputedStyle(this)['direction'] === 'rtl'; |
| 5848 }, |
| 5849 |
| 5850 _onLeftKey: function(event) { |
| 5851 if (this._isRTL) { |
| 5852 this._focusNext(); |
| 5853 } else { |
| 5854 this._focusPrevious(); |
| 5855 } |
| 5856 event.detail.keyboardEvent.preventDefault(); |
| 5857 }, |
| 5858 |
| 5859 _onRightKey: function(event) { |
| 5860 if (this._isRTL) { |
| 5861 this._focusPrevious(); |
| 5862 } else { |
| 5863 this._focusNext(); |
| 5864 } |
| 5865 event.detail.keyboardEvent.preventDefault(); |
| 5866 }, |
| 5867 |
| 5868 _onKeydown: function(event) { |
| 5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { |
| 5870 return; |
| 5871 } |
| 5872 |
| 5873 // all other keys focus the menu item starting with that character |
| 5874 this._focusWithKeyboardEvent(event); |
| 5875 } |
| 5876 |
| 5877 }; |
| 5878 |
| 5879 /** @polymerBehavior Polymer.IronMenubarBehavior */ |
| 5880 Polymer.IronMenubarBehavior = [ |
| 5881 Polymer.IronMenuBehavior, |
| 5882 Polymer.IronMenubarBehaviorImpl |
| 5883 ]; |
| 6092 /** | 5884 /** |
| 6093 * The `iron-iconset-svg` element allows users to define their own icon sets | 5885 * The `iron-iconset-svg` element allows users to define their own icon sets |
| 6094 * that contain svg icons. The svg icon elements should be children of the | 5886 * that contain svg icons. The svg icon elements should be children of the |
| 6095 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | 5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. |
| 6096 * | 5888 * |
| 6097 * Using svg elements to create icons has a few advantages over traditional | 5889 * Using svg elements to create icons has a few advantages over traditional |
| 6098 * bitmap graphics like jpg or png. Icons that use svg are vector based so | 5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so |
| 6099 * they are resolution independent and should look good on any device. They | 5891 * they are resolution independent and should look good on any device. They |
| 6100 * are stylable via css. Icons can be themed, colorized, and even animated. | 5892 * are stylable via css. Icons can be themed, colorized, and even animated. |
| 6101 * | 5893 * |
| (...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 6261 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | 6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 |
| 6262 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | 6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root |
| 6263 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | 6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; |
| 6264 svg.appendChild(content).removeAttribute('id'); | 6056 svg.appendChild(content).removeAttribute('id'); |
| 6265 return svg; | 6057 return svg; |
| 6266 } | 6058 } |
| 6267 return null; | 6059 return null; |
| 6268 } | 6060 } |
| 6269 | 6061 |
| 6270 }); | 6062 }); |
| 6063 Polymer({ |
| 6064 is: 'paper-tabs', |
| 6065 |
| 6066 behaviors: [ |
| 6067 Polymer.IronResizableBehavior, |
| 6068 Polymer.IronMenubarBehavior |
| 6069 ], |
| 6070 |
| 6071 properties: { |
| 6072 /** |
| 6073 * If true, ink ripple effect is disabled. When this property is changed
, |
| 6074 * all descendant `<paper-tab>` elements have their `noink` property |
| 6075 * changed to the new value as well. |
| 6076 */ |
| 6077 noink: { |
| 6078 type: Boolean, |
| 6079 value: false, |
| 6080 observer: '_noinkChanged' |
| 6081 }, |
| 6082 |
| 6083 /** |
| 6084 * If true, the bottom bar to indicate the selected tab will not be show
n. |
| 6085 */ |
| 6086 noBar: { |
| 6087 type: Boolean, |
| 6088 value: false |
| 6089 }, |
| 6090 |
| 6091 /** |
| 6092 * If true, the slide effect for the bottom bar is disabled. |
| 6093 */ |
| 6094 noSlide: { |
| 6095 type: Boolean, |
| 6096 value: false |
| 6097 }, |
| 6098 |
| 6099 /** |
| 6100 * If true, tabs are scrollable and the tab width is based on the label
width. |
| 6101 */ |
| 6102 scrollable: { |
| 6103 type: Boolean, |
| 6104 value: false |
| 6105 }, |
| 6106 |
| 6107 /** |
| 6108 * If true, tabs expand to fit their container. This currently only appl
ies when |
| 6109 * scrollable is true. |
| 6110 */ |
| 6111 fitContainer: { |
| 6112 type: Boolean, |
| 6113 value: false |
| 6114 }, |
| 6115 |
| 6116 /** |
| 6117 * If true, dragging on the tabs to scroll is disabled. |
| 6118 */ |
| 6119 disableDrag: { |
| 6120 type: Boolean, |
| 6121 value: false |
| 6122 }, |
| 6123 |
| 6124 /** |
| 6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla
ble tabs. |
| 6126 */ |
| 6127 hideScrollButtons: { |
| 6128 type: Boolean, |
| 6129 value: false |
| 6130 }, |
| 6131 |
| 6132 /** |
| 6133 * If true, the tabs are aligned to bottom (the selection bar appears at
the top). |
| 6134 */ |
| 6135 alignBottom: { |
| 6136 type: Boolean, |
| 6137 value: false |
| 6138 }, |
| 6139 |
| 6140 selectable: { |
| 6141 type: String, |
| 6142 value: 'paper-tab' |
| 6143 }, |
| 6144 |
| 6145 /** |
| 6146 * If true, tabs are automatically selected when focused using the |
| 6147 * keyboard. |
| 6148 */ |
| 6149 autoselect: { |
| 6150 type: Boolean, |
| 6151 value: false |
| 6152 }, |
| 6153 |
| 6154 /** |
| 6155 * The delay (in milliseconds) between when the user stops interacting |
| 6156 * with the tabs through the keyboard and when the focused item is |
| 6157 * automatically selected (if `autoselect` is true). |
| 6158 */ |
| 6159 autoselectDelay: { |
| 6160 type: Number, |
| 6161 value: 0 |
| 6162 }, |
| 6163 |
| 6164 _step: { |
| 6165 type: Number, |
| 6166 value: 10 |
| 6167 }, |
| 6168 |
| 6169 _holdDelay: { |
| 6170 type: Number, |
| 6171 value: 1 |
| 6172 }, |
| 6173 |
| 6174 _leftHidden: { |
| 6175 type: Boolean, |
| 6176 value: false |
| 6177 }, |
| 6178 |
| 6179 _rightHidden: { |
| 6180 type: Boolean, |
| 6181 value: false |
| 6182 }, |
| 6183 |
| 6184 _previousTab: { |
| 6185 type: Object |
| 6186 } |
| 6187 }, |
| 6188 |
| 6189 hostAttributes: { |
| 6190 role: 'tablist' |
| 6191 }, |
| 6192 |
| 6193 listeners: { |
| 6194 'iron-resize': '_onTabSizingChanged', |
| 6195 'iron-items-changed': '_onTabSizingChanged', |
| 6196 'iron-select': '_onIronSelect', |
| 6197 'iron-deselect': '_onIronDeselect' |
| 6198 }, |
| 6199 |
| 6200 keyBindings: { |
| 6201 'left:keyup right:keyup': '_onArrowKeyup' |
| 6202 }, |
| 6203 |
| 6204 created: function() { |
| 6205 this._holdJob = null; |
| 6206 this._pendingActivationItem = undefined; |
| 6207 this._pendingActivationTimeout = undefined; |
| 6208 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind
(this); |
| 6209 this.addEventListener('blur', this._onBlurCapture.bind(this), true); |
| 6210 }, |
| 6211 |
| 6212 ready: function() { |
| 6213 this.setScrollDirection('y', this.$.tabsContainer); |
| 6214 }, |
| 6215 |
| 6216 detached: function() { |
| 6217 this._cancelPendingActivation(); |
| 6218 }, |
| 6219 |
| 6220 _noinkChanged: function(noink) { |
| 6221 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); |
| 6222 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt
ribute); |
| 6223 }, |
| 6224 |
| 6225 _setNoinkAttribute: function(element) { |
| 6226 element.setAttribute('noink', ''); |
| 6227 }, |
| 6228 |
| 6229 _removeNoinkAttribute: function(element) { |
| 6230 element.removeAttribute('noink'); |
| 6231 }, |
| 6232 |
| 6233 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll
Buttons) { |
| 6234 if (!scrollable || hideScrollButtons) { |
| 6235 return 'hidden'; |
| 6236 } |
| 6237 |
| 6238 if (hideThisButton) { |
| 6239 return 'not-visible'; |
| 6240 } |
| 6241 |
| 6242 return ''; |
| 6243 }, |
| 6244 |
| 6245 _computeTabsContentClass: function(scrollable, fitContainer) { |
| 6246 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : ''
) : ' fit-container'; |
| 6247 }, |
| 6248 |
| 6249 _computeSelectionBarClass: function(noBar, alignBottom) { |
| 6250 if (noBar) { |
| 6251 return 'hidden'; |
| 6252 } else if (alignBottom) { |
| 6253 return 'align-bottom'; |
| 6254 } |
| 6255 |
| 6256 return ''; |
| 6257 }, |
| 6258 |
| 6259 // TODO(cdata): Add `track` response back in when gesture lands. |
| 6260 |
| 6261 _onTabSizingChanged: function() { |
| 6262 this.debounce('_onTabSizingChanged', function() { |
| 6263 this._scroll(); |
| 6264 this._tabChanged(this.selectedItem); |
| 6265 }, 10); |
| 6266 }, |
| 6267 |
| 6268 _onIronSelect: function(event) { |
| 6269 this._tabChanged(event.detail.item, this._previousTab); |
| 6270 this._previousTab = event.detail.item; |
| 6271 this.cancelDebouncer('tab-changed'); |
| 6272 }, |
| 6273 |
| 6274 _onIronDeselect: function(event) { |
| 6275 this.debounce('tab-changed', function() { |
| 6276 this._tabChanged(null, this._previousTab); |
| 6277 this._previousTab = null; |
| 6278 // See polymer/polymer#1305 |
| 6279 }, 1); |
| 6280 }, |
| 6281 |
| 6282 _activateHandler: function() { |
| 6283 // Cancel item activations scheduled by keyboard events when any other |
| 6284 // action causes an item to be activated (e.g. clicks). |
| 6285 this._cancelPendingActivation(); |
| 6286 |
| 6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); |
| 6288 }, |
| 6289 |
| 6290 /** |
| 6291 * Activates an item after a delay (in milliseconds). |
| 6292 */ |
| 6293 _scheduleActivation: function(item, delay) { |
| 6294 this._pendingActivationItem = item; |
| 6295 this._pendingActivationTimeout = this.async( |
| 6296 this._bindDelayedActivationHandler, delay); |
| 6297 }, |
| 6298 |
| 6299 /** |
| 6300 * Activates the last item given to `_scheduleActivation`. |
| 6301 */ |
| 6302 _delayedActivationHandler: function() { |
| 6303 var item = this._pendingActivationItem; |
| 6304 this._pendingActivationItem = undefined; |
| 6305 this._pendingActivationTimeout = undefined; |
| 6306 item.fire(this.activateEvent, null, { |
| 6307 bubbles: true, |
| 6308 cancelable: true |
| 6309 }); |
| 6310 }, |
| 6311 |
| 6312 /** |
| 6313 * Cancels a previously scheduled item activation made with |
| 6314 * `_scheduleActivation`. |
| 6315 */ |
| 6316 _cancelPendingActivation: function() { |
| 6317 if (this._pendingActivationTimeout !== undefined) { |
| 6318 this.cancelAsync(this._pendingActivationTimeout); |
| 6319 this._pendingActivationItem = undefined; |
| 6320 this._pendingActivationTimeout = undefined; |
| 6321 } |
| 6322 }, |
| 6323 |
| 6324 _onArrowKeyup: function(event) { |
| 6325 if (this.autoselect) { |
| 6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay); |
| 6327 } |
| 6328 }, |
| 6329 |
| 6330 _onBlurCapture: function(event) { |
| 6331 // Cancel a scheduled item activation (if any) when that item is |
| 6332 // blurred. |
| 6333 if (event.target === this._pendingActivationItem) { |
| 6334 this._cancelPendingActivation(); |
| 6335 } |
| 6336 }, |
| 6337 |
| 6338 get _tabContainerScrollSize () { |
| 6339 return Math.max( |
| 6340 0, |
| 6341 this.$.tabsContainer.scrollWidth - |
| 6342 this.$.tabsContainer.offsetWidth |
| 6343 ); |
| 6344 }, |
| 6345 |
| 6346 _scroll: function(e, detail) { |
| 6347 if (!this.scrollable) { |
| 6348 return; |
| 6349 } |
| 6350 |
| 6351 var ddx = (detail && -detail.ddx) || 0; |
| 6352 this._affectScroll(ddx); |
| 6353 }, |
| 6354 |
| 6355 _down: function(e) { |
| 6356 // go one beat async to defeat IronMenuBehavior |
| 6357 // autorefocus-on-no-selection timeout |
| 6358 this.async(function() { |
| 6359 if (this._defaultFocusAsync) { |
| 6360 this.cancelAsync(this._defaultFocusAsync); |
| 6361 this._defaultFocusAsync = null; |
| 6362 } |
| 6363 }, 1); |
| 6364 }, |
| 6365 |
| 6366 _affectScroll: function(dx) { |
| 6367 this.$.tabsContainer.scrollLeft += dx; |
| 6368 |
| 6369 var scrollLeft = this.$.tabsContainer.scrollLeft; |
| 6370 |
| 6371 this._leftHidden = scrollLeft === 0; |
| 6372 this._rightHidden = scrollLeft === this._tabContainerScrollSize; |
| 6373 }, |
| 6374 |
| 6375 _onLeftScrollButtonDown: function() { |
| 6376 this._scrollToLeft(); |
| 6377 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel
ay); |
| 6378 }, |
| 6379 |
| 6380 _onRightScrollButtonDown: function() { |
| 6381 this._scrollToRight(); |
| 6382 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe
lay); |
| 6383 }, |
| 6384 |
| 6385 _onScrollButtonUp: function() { |
| 6386 clearInterval(this._holdJob); |
| 6387 this._holdJob = null; |
| 6388 }, |
| 6389 |
| 6390 _scrollToLeft: function() { |
| 6391 this._affectScroll(-this._step); |
| 6392 }, |
| 6393 |
| 6394 _scrollToRight: function() { |
| 6395 this._affectScroll(this._step); |
| 6396 }, |
| 6397 |
| 6398 _tabChanged: function(tab, old) { |
| 6399 if (!tab) { |
| 6400 // Remove the bar without animation. |
| 6401 this.$.selectionBar.classList.remove('expand'); |
| 6402 this.$.selectionBar.classList.remove('contract'); |
| 6403 this._positionBar(0, 0); |
| 6404 return; |
| 6405 } |
| 6406 |
| 6407 var r = this.$.tabsContent.getBoundingClientRect(); |
| 6408 var w = r.width; |
| 6409 var tabRect = tab.getBoundingClientRect(); |
| 6410 var tabOffsetLeft = tabRect.left - r.left; |
| 6411 |
| 6412 this._pos = { |
| 6413 width: this._calcPercent(tabRect.width, w), |
| 6414 left: this._calcPercent(tabOffsetLeft, w) |
| 6415 }; |
| 6416 |
| 6417 if (this.noSlide || old == null) { |
| 6418 // Position the bar without animation. |
| 6419 this.$.selectionBar.classList.remove('expand'); |
| 6420 this.$.selectionBar.classList.remove('contract'); |
| 6421 this._positionBar(this._pos.width, this._pos.left); |
| 6422 return; |
| 6423 } |
| 6424 |
| 6425 var oldRect = old.getBoundingClientRect(); |
| 6426 var oldIndex = this.items.indexOf(old); |
| 6427 var index = this.items.indexOf(tab); |
| 6428 var m = 5; |
| 6429 |
| 6430 // bar animation: expand |
| 6431 this.$.selectionBar.classList.add('expand'); |
| 6432 |
| 6433 var moveRight = oldIndex < index; |
| 6434 var isRTL = this._isRTL; |
| 6435 if (isRTL) { |
| 6436 moveRight = !moveRight; |
| 6437 } |
| 6438 |
| 6439 if (moveRight) { |
| 6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old
Rect.left, w) - m, |
| 6441 this._left); |
| 6442 } else { |
| 6443 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab
Rect.left, w) - m, |
| 6444 this._calcPercent(tabOffsetLeft, w) + m); |
| 6445 } |
| 6446 |
| 6447 if (this.scrollable) { |
| 6448 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); |
| 6449 } |
| 6450 }, |
| 6451 |
| 6452 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { |
| 6453 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; |
| 6454 if (l < 0) { |
| 6455 this.$.tabsContainer.scrollLeft += l; |
| 6456 } else { |
| 6457 l += (tabWidth - this.$.tabsContainer.offsetWidth); |
| 6458 if (l > 0) { |
| 6459 this.$.tabsContainer.scrollLeft += l; |
| 6460 } |
| 6461 } |
| 6462 }, |
| 6463 |
| 6464 _calcPercent: function(w, w0) { |
| 6465 return 100 * w / w0; |
| 6466 }, |
| 6467 |
| 6468 _positionBar: function(width, left) { |
| 6469 width = width || 0; |
| 6470 left = left || 0; |
| 6471 |
| 6472 this._width = width; |
| 6473 this._left = left; |
| 6474 this.transform( |
| 6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')', |
| 6476 this.$.selectionBar); |
| 6477 }, |
| 6478 |
| 6479 _onBarTransitionEnd: function(e) { |
| 6480 var cl = this.$.selectionBar.classList; |
| 6481 // bar animation: expand -> contract |
| 6482 if (cl.contains('expand')) { |
| 6483 cl.remove('expand'); |
| 6484 cl.add('contract'); |
| 6485 this._positionBar(this._pos.width, this._pos.left); |
| 6486 // bar animation done |
| 6487 } else if (cl.contains('contract')) { |
| 6488 cl.remove('contract'); |
| 6489 } |
| 6490 } |
| 6491 }); |
| 6492 (function() { |
| 6493 'use strict'; |
| 6494 |
| 6495 Polymer.IronA11yAnnouncer = Polymer({ |
| 6496 is: 'iron-a11y-announcer', |
| 6497 |
| 6498 properties: { |
| 6499 |
| 6500 /** |
| 6501 * The value of mode is used to set the `aria-live` attribute |
| 6502 * for the element that will be announced. Valid values are: `off`, |
| 6503 * `polite` and `assertive`. |
| 6504 */ |
| 6505 mode: { |
| 6506 type: String, |
| 6507 value: 'polite' |
| 6508 }, |
| 6509 |
| 6510 _text: { |
| 6511 type: String, |
| 6512 value: '' |
| 6513 } |
| 6514 }, |
| 6515 |
| 6516 created: function() { |
| 6517 if (!Polymer.IronA11yAnnouncer.instance) { |
| 6518 Polymer.IronA11yAnnouncer.instance = this; |
| 6519 } |
| 6520 |
| 6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); |
| 6522 }, |
| 6523 |
| 6524 /** |
| 6525 * Cause a text string to be announced by screen readers. |
| 6526 * |
| 6527 * @param {string} text The text that should be announced. |
| 6528 */ |
| 6529 announce: function(text) { |
| 6530 this._text = ''; |
| 6531 this.async(function() { |
| 6532 this._text = text; |
| 6533 }, 100); |
| 6534 }, |
| 6535 |
| 6536 _onIronAnnounce: function(event) { |
| 6537 if (event.detail && event.detail.text) { |
| 6538 this.announce(event.detail.text); |
| 6539 } |
| 6540 } |
| 6541 }); |
| 6542 |
| 6543 Polymer.IronA11yAnnouncer.instance = null; |
| 6544 |
| 6545 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
| 6546 if (!Polymer.IronA11yAnnouncer.instance) { |
| 6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); |
| 6548 } |
| 6549 |
| 6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
| 6551 }; |
| 6552 })(); |
| 6553 /** |
| 6554 * Singleton IronMeta instance. |
| 6555 */ |
| 6556 Polymer.IronValidatableBehaviorMeta = null; |
| 6557 |
| 6558 /** |
| 6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. |
| 6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. |
| 6561 * |
| 6562 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. |
| 6563 * To validate a form imperatively, call the form's `validate()` method, which
in turn will |
| 6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your |
| 6565 * custom element will get a public `validate()`, which |
| 6566 * will return the validity of the element, and a corresponding `invalid` attr
ibute, |
| 6567 * which can be used for styling. |
| 6568 * |
| 6569 * To implement the custom validation logic of your element, you must override |
| 6570 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. |
| 6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) |
| 6572 * for an example. |
| 6573 * |
| 6574 * ### Accessibility |
| 6575 * |
| 6576 * Changing the `invalid` property, either manually or by calling `validate()`
will update the |
| 6577 * `aria-invalid` attribute. |
| 6578 * |
| 6579 * @demo demo/index.html |
| 6580 * @polymerBehavior |
| 6581 */ |
| 6582 Polymer.IronValidatableBehavior = { |
| 6583 |
| 6584 properties: { |
| 6585 |
| 6586 /** |
| 6587 * Name of the validator to use. |
| 6588 */ |
| 6589 validator: { |
| 6590 type: String |
| 6591 }, |
| 6592 |
| 6593 /** |
| 6594 * True if the last call to `validate` is invalid. |
| 6595 */ |
| 6596 invalid: { |
| 6597 notify: true, |
| 6598 reflectToAttribute: true, |
| 6599 type: Boolean, |
| 6600 value: false |
| 6601 }, |
| 6602 |
| 6603 /** |
| 6604 * This property is deprecated and should not be used. Use the global |
| 6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. |
| 6606 */ |
| 6607 _validatorMeta: { |
| 6608 type: Object |
| 6609 }, |
| 6610 |
| 6611 /** |
| 6612 * Namespace for this validator. This property is deprecated and should |
| 6613 * not be used. For all intents and purposes, please consider it a |
| 6614 * read-only, config-time property. |
| 6615 */ |
| 6616 validatorType: { |
| 6617 type: String, |
| 6618 value: 'validator' |
| 6619 }, |
| 6620 |
| 6621 _validator: { |
| 6622 type: Object, |
| 6623 computed: '__computeValidator(validator)' |
| 6624 } |
| 6625 }, |
| 6626 |
| 6627 observers: [ |
| 6628 '_invalidChanged(invalid)' |
| 6629 ], |
| 6630 |
| 6631 registered: function() { |
| 6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); |
| 6633 }, |
| 6634 |
| 6635 _invalidChanged: function() { |
| 6636 if (this.invalid) { |
| 6637 this.setAttribute('aria-invalid', 'true'); |
| 6638 } else { |
| 6639 this.removeAttribute('aria-invalid'); |
| 6640 } |
| 6641 }, |
| 6642 |
| 6643 /** |
| 6644 * @return {boolean} True if the validator `validator` exists. |
| 6645 */ |
| 6646 hasValidator: function() { |
| 6647 return this._validator != null; |
| 6648 }, |
| 6649 |
| 6650 /** |
| 6651 * Returns true if the `value` is valid, and updates `invalid`. If you want |
| 6652 * your element to have custom validation logic, do not override this method
; |
| 6653 * override `_getValidity(value)` instead. |
| 6654 |
| 6655 * @param {Object} value The value to be validated. By default, it is passed |
| 6656 * to the validator's `validate()` function, if a validator is set. |
| 6657 * @return {boolean} True if `value` is valid. |
| 6658 */ |
| 6659 validate: function(value) { |
| 6660 this.invalid = !this._getValidity(value); |
| 6661 return !this.invalid; |
| 6662 }, |
| 6663 |
| 6664 /** |
| 6665 * Returns true if `value` is valid. By default, it is passed |
| 6666 * to the validator's `validate()` function, if a validator is set. You |
| 6667 * should override this method if you want to implement custom validity |
| 6668 * logic for your element. |
| 6669 * |
| 6670 * @param {Object} value The value to be validated. |
| 6671 * @return {boolean} True if `value` is valid. |
| 6672 */ |
| 6673 |
| 6674 _getValidity: function(value) { |
| 6675 if (this.hasValidator()) { |
| 6676 return this._validator.validate(value); |
| 6677 } |
| 6678 return true; |
| 6679 }, |
| 6680 |
| 6681 __computeValidator: function() { |
| 6682 return Polymer.IronValidatableBehaviorMeta && |
| 6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); |
| 6684 } |
| 6685 }; |
| 6686 /* |
| 6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` |
| 6688 to `<input>`. |
| 6689 |
| 6690 ### Two-way binding |
| 6691 |
| 6692 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: |
| 6693 |
| 6694 <input value="{{myValue::input}}"> |
| 6695 |
| 6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used |
| 6697 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. |
| 6698 |
| 6699 <input is="iron-input" bind-value="{{myValue}}"> |
| 6700 |
| 6701 ### Custom validators |
| 6702 |
| 6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. |
| 6704 |
| 6705 <input is="iron-input" validator="my-custom-validator"> |
| 6706 |
| 6707 ### Stopping invalid input |
| 6708 |
| 6709 It may be desirable to only allow users to enter certain characters. You can use
the |
| 6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature |
| 6711 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. |
| 6712 |
| 6713 \x3c!-- only allow characters that match [0-9] --\x3e |
| 6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> |
| 6715 |
| 6716 @hero hero.svg |
| 6717 @demo demo/index.html |
| 6718 */ |
| 6719 |
| 6720 Polymer({ |
| 6721 |
| 6722 is: 'iron-input', |
| 6723 |
| 6724 extends: 'input', |
| 6725 |
| 6726 behaviors: [ |
| 6727 Polymer.IronValidatableBehavior |
| 6728 ], |
| 6729 |
| 6730 properties: { |
| 6731 |
| 6732 /** |
| 6733 * Use this property instead of `value` for two-way data binding. |
| 6734 */ |
| 6735 bindValue: { |
| 6736 observer: '_bindValueChanged', |
| 6737 type: String |
| 6738 }, |
| 6739 |
| 6740 /** |
| 6741 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, |
| 6742 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. |
| 6743 * Pasted input will have each character checked individually; if any char
acter |
| 6744 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. |
| 6745 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). |
| 6746 */ |
| 6747 preventInvalidInput: { |
| 6748 type: Boolean |
| 6749 }, |
| 6750 |
| 6751 /** |
| 6752 * Regular expression that list the characters allowed as input. |
| 6753 * This pattern represents the allowed characters for the field; as the us
er inputs text, |
| 6754 * each individual character will be checked against the pattern (rather t
han checking |
| 6755 * the entire value as a whole). The recommended format should be a list o
f allowed characters; |
| 6756 * for example, `[a-zA-Z0-9.+-!;:]` |
| 6757 */ |
| 6758 allowedPattern: { |
| 6759 type: String, |
| 6760 observer: "_allowedPatternChanged" |
| 6761 }, |
| 6762 |
| 6763 _previousValidInput: { |
| 6764 type: String, |
| 6765 value: '' |
| 6766 }, |
| 6767 |
| 6768 _patternAlreadyChecked: { |
| 6769 type: Boolean, |
| 6770 value: false |
| 6771 } |
| 6772 |
| 6773 }, |
| 6774 |
| 6775 listeners: { |
| 6776 'input': '_onInput', |
| 6777 'keypress': '_onKeypress' |
| 6778 }, |
| 6779 |
| 6780 /** @suppress {checkTypes} */ |
| 6781 registered: function() { |
| 6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). |
| 6783 if (!this._canDispatchEventOnDisabled()) { |
| 6784 this._origDispatchEvent = this.dispatchEvent; |
| 6785 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 6786 } |
| 6787 }, |
| 6788 |
| 6789 created: function() { |
| 6790 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 6791 }, |
| 6792 |
| 6793 _canDispatchEventOnDisabled: function() { |
| 6794 var input = document.createElement('input'); |
| 6795 var canDispatch = false; |
| 6796 input.disabled = true; |
| 6797 |
| 6798 input.addEventListener('feature-check-dispatch-event', function() { |
| 6799 canDispatch = true; |
| 6800 }); |
| 6801 |
| 6802 try { |
| 6803 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 6804 } catch(e) {} |
| 6805 |
| 6806 return canDispatch; |
| 6807 }, |
| 6808 |
| 6809 _dispatchEventFirefoxIE: function() { |
| 6810 // Due to Firefox bug, events fired on disabled form controls can throw |
| 6811 // errors; furthermore, neither IE nor Firefox will actually dispatch |
| 6812 // events from disabled form controls; as such, we toggle disable around |
| 6813 // the dispatch to allow notifying properties to notify |
| 6814 // See issue #47 for details |
| 6815 var disabled = this.disabled; |
| 6816 this.disabled = false; |
| 6817 this._origDispatchEvent.apply(this, arguments); |
| 6818 this.disabled = disabled; |
| 6819 }, |
| 6820 |
| 6821 get _patternRegExp() { |
| 6822 var pattern; |
| 6823 if (this.allowedPattern) { |
| 6824 pattern = new RegExp(this.allowedPattern); |
| 6825 } else { |
| 6826 switch (this.type) { |
| 6827 case 'number': |
| 6828 pattern = /[0-9.,e-]/; |
| 6829 break; |
| 6830 } |
| 6831 } |
| 6832 return pattern; |
| 6833 }, |
| 6834 |
| 6835 ready: function() { |
| 6836 this.bindValue = this.value; |
| 6837 }, |
| 6838 |
| 6839 /** |
| 6840 * @suppress {checkTypes} |
| 6841 */ |
| 6842 _bindValueChanged: function() { |
| 6843 if (this.value !== this.bindValue) { |
| 6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; |
| 6845 } |
| 6846 // manually notify because we don't want to notify until after setting val
ue |
| 6847 this.fire('bind-value-changed', {value: this.bindValue}); |
| 6848 }, |
| 6849 |
| 6850 _allowedPatternChanged: function() { |
| 6851 // Force to prevent invalid input when an `allowed-pattern` is set |
| 6852 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 6853 }, |
| 6854 |
| 6855 _onInput: function() { |
| 6856 // Need to validate each of the characters pasted if they haven't |
| 6857 // been validated inside `_onKeypress` already. |
| 6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 6859 var valid = this._checkPatternValidity(); |
| 6860 if (!valid) { |
| 6861 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); |
| 6862 this.value = this._previousValidInput; |
| 6863 } |
| 6864 } |
| 6865 |
| 6866 this.bindValue = this.value; |
| 6867 this._previousValidInput = this.value; |
| 6868 this._patternAlreadyChecked = false; |
| 6869 }, |
| 6870 |
| 6871 _isPrintable: function(event) { |
| 6872 // What a control/printable character is varies wildly based on the browse
r. |
| 6873 // - most control characters (arrows, backspace) do not send a `keypress`
event |
| 6874 // in Chrome, but the *do* on Firefox |
| 6875 // - in Firefox, when they do send a `keypress` event, control chars have |
| 6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) |
| 6877 // - printable characters always send a keypress event. |
| 6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode |
| 6879 // always matches the charCode. |
| 6880 // None of this makes any sense. |
| 6881 |
| 6882 // For these keys, ASCII code == browser keycode. |
| 6883 var anyNonPrintable = |
| 6884 (event.keyCode == 8) || // backspace |
| 6885 (event.keyCode == 9) || // tab |
| 6886 (event.keyCode == 13) || // enter |
| 6887 (event.keyCode == 27); // escape |
| 6888 |
| 6889 // For these keys, make sure it's a browser keycode and not an ASCII code. |
| 6890 var mozNonPrintable = |
| 6891 (event.keyCode == 19) || // pause |
| 6892 (event.keyCode == 20) || // caps lock |
| 6893 (event.keyCode == 45) || // insert |
| 6894 (event.keyCode == 46) || // delete |
| 6895 (event.keyCode == 144) || // num lock |
| 6896 (event.keyCode == 145) || // scroll lock |
| 6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows |
| 6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys |
| 6899 |
| 6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 6901 }, |
| 6902 |
| 6903 _onKeypress: function(event) { |
| 6904 if (!this.preventInvalidInput && this.type !== 'number') { |
| 6905 return; |
| 6906 } |
| 6907 var regexp = this._patternRegExp; |
| 6908 if (!regexp) { |
| 6909 return; |
| 6910 } |
| 6911 |
| 6912 // Handle special keys and backspace |
| 6913 if (event.metaKey || event.ctrlKey || event.altKey) |
| 6914 return; |
| 6915 |
| 6916 // Check the pattern either here or in `_onInput`, but not in both. |
| 6917 this._patternAlreadyChecked = true; |
| 6918 |
| 6919 var thisChar = String.fromCharCode(event.charCode); |
| 6920 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 6921 event.preventDefault(); |
| 6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); |
| 6923 } |
| 6924 }, |
| 6925 |
| 6926 _checkPatternValidity: function() { |
| 6927 var regexp = this._patternRegExp; |
| 6928 if (!regexp) { |
| 6929 return true; |
| 6930 } |
| 6931 for (var i = 0; i < this.value.length; i++) { |
| 6932 if (!regexp.test(this.value[i])) { |
| 6933 return false; |
| 6934 } |
| 6935 } |
| 6936 return true; |
| 6937 }, |
| 6938 |
| 6939 /** |
| 6940 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, |
| 6941 * then any constraints. |
| 6942 * @return {boolean} True if the value is valid. |
| 6943 */ |
| 6944 validate: function() { |
| 6945 // First, check what the browser thinks. Some inputs (like type=number) |
| 6946 // behave weirdly and will set the value to "" if something invalid is |
| 6947 // entered, but will set the validity correctly. |
| 6948 var valid = this.checkValidity(); |
| 6949 |
| 6950 // Only do extra checking if the browser thought this was valid. |
| 6951 if (valid) { |
| 6952 // Empty, required input is invalid |
| 6953 if (this.required && this.value === '') { |
| 6954 valid = false; |
| 6955 } else if (this.hasValidator()) { |
| 6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); |
| 6957 } |
| 6958 } |
| 6959 |
| 6960 this.invalid = !valid; |
| 6961 this.fire('iron-input-validate'); |
| 6962 return valid; |
| 6963 }, |
| 6964 |
| 6965 _announceInvalidCharacter: function(message) { |
| 6966 this.fire('iron-announce', { text: message }); |
| 6967 } |
| 6968 }); |
| 6969 |
| 6970 /* |
| 6971 The `iron-input-validate` event is fired whenever `validate()` is called. |
| 6972 @event iron-input-validate |
| 6973 */ |
| 6974 Polymer({ |
| 6975 is: 'paper-input-container', |
| 6976 |
| 6977 properties: { |
| 6978 /** |
| 6979 * Set to true to disable the floating label. The label disappears when th
e input value is |
| 6980 * not null. |
| 6981 */ |
| 6982 noLabelFloat: { |
| 6983 type: Boolean, |
| 6984 value: false |
| 6985 }, |
| 6986 |
| 6987 /** |
| 6988 * Set to true to always float the floating label. |
| 6989 */ |
| 6990 alwaysFloatLabel: { |
| 6991 type: Boolean, |
| 6992 value: false |
| 6993 }, |
| 6994 |
| 6995 /** |
| 6996 * The attribute to listen for value changes on. |
| 6997 */ |
| 6998 attrForValue: { |
| 6999 type: String, |
| 7000 value: 'bind-value' |
| 7001 }, |
| 7002 |
| 7003 /** |
| 7004 * Set to true to auto-validate the input value when it changes. |
| 7005 */ |
| 7006 autoValidate: { |
| 7007 type: Boolean, |
| 7008 value: false |
| 7009 }, |
| 7010 |
| 7011 /** |
| 7012 * True if the input is invalid. This property is set automatically when t
he input value |
| 7013 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. |
| 7014 */ |
| 7015 invalid: { |
| 7016 observer: '_invalidChanged', |
| 7017 type: Boolean, |
| 7018 value: false |
| 7019 }, |
| 7020 |
| 7021 /** |
| 7022 * True if the input has focus. |
| 7023 */ |
| 7024 focused: { |
| 7025 readOnly: true, |
| 7026 type: Boolean, |
| 7027 value: false, |
| 7028 notify: true |
| 7029 }, |
| 7030 |
| 7031 _addons: { |
| 7032 type: Array |
| 7033 // do not set a default value here intentionally - it will be initialize
d lazily when a |
| 7034 // distributed child is attached, which may occur before configuration f
or this element |
| 7035 // in polyfill. |
| 7036 }, |
| 7037 |
| 7038 _inputHasContent: { |
| 7039 type: Boolean, |
| 7040 value: false |
| 7041 }, |
| 7042 |
| 7043 _inputSelector: { |
| 7044 type: String, |
| 7045 value: 'input,textarea,.paper-input-input' |
| 7046 }, |
| 7047 |
| 7048 _boundOnFocus: { |
| 7049 type: Function, |
| 7050 value: function() { |
| 7051 return this._onFocus.bind(this); |
| 7052 } |
| 7053 }, |
| 7054 |
| 7055 _boundOnBlur: { |
| 7056 type: Function, |
| 7057 value: function() { |
| 7058 return this._onBlur.bind(this); |
| 7059 } |
| 7060 }, |
| 7061 |
| 7062 _boundOnInput: { |
| 7063 type: Function, |
| 7064 value: function() { |
| 7065 return this._onInput.bind(this); |
| 7066 } |
| 7067 }, |
| 7068 |
| 7069 _boundValueChanged: { |
| 7070 type: Function, |
| 7071 value: function() { |
| 7072 return this._onValueChanged.bind(this); |
| 7073 } |
| 7074 } |
| 7075 }, |
| 7076 |
| 7077 listeners: { |
| 7078 'addon-attached': '_onAddonAttached', |
| 7079 'iron-input-validate': '_onIronInputValidate' |
| 7080 }, |
| 7081 |
| 7082 get _valueChangedEvent() { |
| 7083 return this.attrForValue + '-changed'; |
| 7084 }, |
| 7085 |
| 7086 get _propertyForValue() { |
| 7087 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 7088 }, |
| 7089 |
| 7090 get _inputElement() { |
| 7091 return Polymer.dom(this).querySelector(this._inputSelector); |
| 7092 }, |
| 7093 |
| 7094 get _inputElementValue() { |
| 7095 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; |
| 7096 }, |
| 7097 |
| 7098 ready: function() { |
| 7099 if (!this._addons) { |
| 7100 this._addons = []; |
| 7101 } |
| 7102 this.addEventListener('focus', this._boundOnFocus, true); |
| 7103 this.addEventListener('blur', this._boundOnBlur, true); |
| 7104 }, |
| 7105 |
| 7106 attached: function() { |
| 7107 if (this.attrForValue) { |
| 7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); |
| 7109 } else { |
| 7110 this.addEventListener('input', this._onInput); |
| 7111 } |
| 7112 |
| 7113 // Only validate when attached if the input already has a value. |
| 7114 if (this._inputElementValue != '') { |
| 7115 this._handleValueAndAutoValidate(this._inputElement); |
| 7116 } else { |
| 7117 this._handleValue(this._inputElement); |
| 7118 } |
| 7119 }, |
| 7120 |
| 7121 _onAddonAttached: function(event) { |
| 7122 if (!this._addons) { |
| 7123 this._addons = []; |
| 7124 } |
| 7125 var target = event.target; |
| 7126 if (this._addons.indexOf(target) === -1) { |
| 7127 this._addons.push(target); |
| 7128 if (this.isAttached) { |
| 7129 this._handleValue(this._inputElement); |
| 7130 } |
| 7131 } |
| 7132 }, |
| 7133 |
| 7134 _onFocus: function() { |
| 7135 this._setFocused(true); |
| 7136 }, |
| 7137 |
| 7138 _onBlur: function() { |
| 7139 this._setFocused(false); |
| 7140 this._handleValueAndAutoValidate(this._inputElement); |
| 7141 }, |
| 7142 |
| 7143 _onInput: function(event) { |
| 7144 this._handleValueAndAutoValidate(event.target); |
| 7145 }, |
| 7146 |
| 7147 _onValueChanged: function(event) { |
| 7148 this._handleValueAndAutoValidate(event.target); |
| 7149 }, |
| 7150 |
| 7151 _handleValue: function(inputElement) { |
| 7152 var value = this._inputElementValue; |
| 7153 |
| 7154 // type="number" hack needed because this.value is empty until it's valid |
| 7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { |
| 7156 this._inputHasContent = true; |
| 7157 } else { |
| 7158 this._inputHasContent = false; |
| 7159 } |
| 7160 |
| 7161 this.updateAddons({ |
| 7162 inputElement: inputElement, |
| 7163 value: value, |
| 7164 invalid: this.invalid |
| 7165 }); |
| 7166 }, |
| 7167 |
| 7168 _handleValueAndAutoValidate: function(inputElement) { |
| 7169 if (this.autoValidate) { |
| 7170 var valid; |
| 7171 if (inputElement.validate) { |
| 7172 valid = inputElement.validate(this._inputElementValue); |
| 7173 } else { |
| 7174 valid = inputElement.checkValidity(); |
| 7175 } |
| 7176 this.invalid = !valid; |
| 7177 } |
| 7178 |
| 7179 // Call this last to notify the add-ons. |
| 7180 this._handleValue(inputElement); |
| 7181 }, |
| 7182 |
| 7183 _onIronInputValidate: function(event) { |
| 7184 this.invalid = this._inputElement.invalid; |
| 7185 }, |
| 7186 |
| 7187 _invalidChanged: function() { |
| 7188 if (this._addons) { |
| 7189 this.updateAddons({invalid: this.invalid}); |
| 7190 } |
| 7191 }, |
| 7192 |
| 7193 /** |
| 7194 * Call this to update the state of add-ons. |
| 7195 * @param {Object} state Add-on state. |
| 7196 */ |
| 7197 updateAddons: function(state) { |
| 7198 for (var addon, index = 0; addon = this._addons[index]; index++) { |
| 7199 addon.update(state); |
| 7200 } |
| 7201 }, |
| 7202 |
| 7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { |
| 7204 var cls = 'input-content'; |
| 7205 if (!noLabelFloat) { |
| 7206 var label = this.querySelector('label'); |
| 7207 |
| 7208 if (alwaysFloatLabel || _inputHasContent) { |
| 7209 cls += ' label-is-floating'; |
| 7210 // If the label is floating, ignore any offsets that may have been |
| 7211 // applied from a prefix element. |
| 7212 this.$.labelAndInputContainer.style.position = 'static'; |
| 7213 |
| 7214 if (invalid) { |
| 7215 cls += ' is-invalid'; |
| 7216 } else if (focused) { |
| 7217 cls += " label-is-highlighted"; |
| 7218 } |
| 7219 } else { |
| 7220 // When the label is not floating, it should overlap the input element
. |
| 7221 if (label) { |
| 7222 this.$.labelAndInputContainer.style.position = 'relative'; |
| 7223 } |
| 7224 } |
| 7225 } else { |
| 7226 if (_inputHasContent) { |
| 7227 cls += ' label-is-hidden'; |
| 7228 } |
| 7229 } |
| 7230 return cls; |
| 7231 }, |
| 7232 |
| 7233 _computeUnderlineClass: function(focused, invalid) { |
| 7234 var cls = 'underline'; |
| 7235 if (invalid) { |
| 7236 cls += ' is-invalid'; |
| 7237 } else if (focused) { |
| 7238 cls += ' is-highlighted' |
| 7239 } |
| 7240 return cls; |
| 7241 }, |
| 7242 |
| 7243 _computeAddOnContentClass: function(focused, invalid) { |
| 7244 var cls = 'add-on-content'; |
| 7245 if (invalid) { |
| 7246 cls += ' is-invalid'; |
| 7247 } else if (focused) { |
| 7248 cls += ' is-highlighted' |
| 7249 } |
| 7250 return cls; |
| 7251 } |
| 7252 }); |
| 7253 /** @polymerBehavior */ |
| 7254 Polymer.PaperSpinnerBehavior = { |
| 7255 |
| 7256 listeners: { |
| 7257 'animationend': '__reset', |
| 7258 'webkitAnimationEnd': '__reset' |
| 7259 }, |
| 7260 |
| 7261 properties: { |
| 7262 /** |
| 7263 * Displays the spinner. |
| 7264 */ |
| 7265 active: { |
| 7266 type: Boolean, |
| 7267 value: false, |
| 7268 reflectToAttribute: true, |
| 7269 observer: '__activeChanged' |
| 7270 }, |
| 7271 |
| 7272 /** |
| 7273 * Alternative text content for accessibility support. |
| 7274 * If alt is present, it will add an aria-label whose content matches alt
when active. |
| 7275 * If alt is not present, it will default to 'loading' as the alt value. |
| 7276 */ |
| 7277 alt: { |
| 7278 type: String, |
| 7279 value: 'loading', |
| 7280 observer: '__altChanged' |
| 7281 }, |
| 7282 |
| 7283 __coolingDown: { |
| 7284 type: Boolean, |
| 7285 value: false |
| 7286 } |
| 7287 }, |
| 7288 |
| 7289 __computeContainerClasses: function(active, coolingDown) { |
| 7290 return [ |
| 7291 active || coolingDown ? 'active' : '', |
| 7292 coolingDown ? 'cooldown' : '' |
| 7293 ].join(' '); |
| 7294 }, |
| 7295 |
| 7296 __activeChanged: function(active, old) { |
| 7297 this.__setAriaHidden(!active); |
| 7298 this.__coolingDown = !active && old; |
| 7299 }, |
| 7300 |
| 7301 __altChanged: function(alt) { |
| 7302 // user-provided `aria-label` takes precedence over prototype default |
| 7303 if (alt === this.getPropertyInfo('alt').value) { |
| 7304 this.alt = this.getAttribute('aria-label') || alt; |
| 7305 } else { |
| 7306 this.__setAriaHidden(alt===''); |
| 7307 this.setAttribute('aria-label', alt); |
| 7308 } |
| 7309 }, |
| 7310 |
| 7311 __setAriaHidden: function(hidden) { |
| 7312 var attr = 'aria-hidden'; |
| 7313 if (hidden) { |
| 7314 this.setAttribute(attr, 'true'); |
| 7315 } else { |
| 7316 this.removeAttribute(attr); |
| 7317 } |
| 7318 }, |
| 7319 |
| 7320 __reset: function() { |
| 7321 this.active = false; |
| 7322 this.__coolingDown = false; |
| 7323 } |
| 7324 }; |
| 7325 Polymer({ |
| 7326 is: 'paper-spinner-lite', |
| 7327 |
| 7328 behaviors: [ |
| 7329 Polymer.PaperSpinnerBehavior |
| 7330 ] |
| 7331 }); |
| 7332 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7333 // Use of this source code is governed by a BSD-style license that can be |
| 7334 // found in the LICENSE file. |
| 7335 |
| 7336 /** |
| 7337 * Implements an incremental search field which can be shown and hidden. |
| 7338 * Canonical implementation is <cr-search-field>. |
| 7339 * @polymerBehavior |
| 7340 */ |
| 7341 var CrSearchFieldBehavior = { |
| 7342 properties: { |
| 7343 label: { |
| 7344 type: String, |
| 7345 value: '', |
| 7346 }, |
| 7347 |
| 7348 clearLabel: { |
| 7349 type: String, |
| 7350 value: '', |
| 7351 }, |
| 7352 |
| 7353 showingSearch: { |
| 7354 type: Boolean, |
| 7355 value: false, |
| 7356 notify: true, |
| 7357 observer: 'showingSearchChanged_', |
| 7358 reflectToAttribute: true |
| 7359 }, |
| 7360 |
| 7361 /** @private */ |
| 7362 lastValue_: { |
| 7363 type: String, |
| 7364 value: '', |
| 7365 }, |
| 7366 }, |
| 7367 |
| 7368 /** |
| 7369 * @abstract |
| 7370 * @return {!HTMLInputElement} The input field element the behavior should |
| 7371 * use. |
| 7372 */ |
| 7373 getSearchInput: function() {}, |
| 7374 |
| 7375 /** |
| 7376 * @return {string} The value of the search field. |
| 7377 */ |
| 7378 getValue: function() { |
| 7379 return this.getSearchInput().value; |
| 7380 }, |
| 7381 |
| 7382 /** |
| 7383 * Sets the value of the search field. |
| 7384 * @param {string} value |
| 7385 */ |
| 7386 setValue: function(value) { |
| 7387 // Use bindValue when setting the input value so that changes propagate |
| 7388 // correctly. |
| 7389 this.getSearchInput().bindValue = value; |
| 7390 this.onValueChanged_(value); |
| 7391 }, |
| 7392 |
| 7393 showAndFocus: function() { |
| 7394 this.showingSearch = true; |
| 7395 this.focus_(); |
| 7396 }, |
| 7397 |
| 7398 /** @private */ |
| 7399 focus_: function() { |
| 7400 this.getSearchInput().focus(); |
| 7401 }, |
| 7402 |
| 7403 onSearchTermSearch: function() { |
| 7404 this.onValueChanged_(this.getValue()); |
| 7405 }, |
| 7406 |
| 7407 /** |
| 7408 * Updates the internal state of the search field based on a change that has |
| 7409 * already happened. |
| 7410 * @param {string} newValue |
| 7411 * @private |
| 7412 */ |
| 7413 onValueChanged_: function(newValue) { |
| 7414 if (newValue == this.lastValue_) |
| 7415 return; |
| 7416 |
| 7417 this.fire('search-changed', newValue); |
| 7418 this.lastValue_ = newValue; |
| 7419 }, |
| 7420 |
| 7421 onSearchTermKeydown: function(e) { |
| 7422 if (e.key == 'Escape') |
| 7423 this.showingSearch = false; |
| 7424 }, |
| 7425 |
| 7426 /** @private */ |
| 7427 showingSearchChanged_: function() { |
| 7428 if (this.showingSearch) { |
| 7429 this.focus_(); |
| 7430 return; |
| 7431 } |
| 7432 |
| 7433 this.setValue(''); |
| 7434 this.getSearchInput().blur(); |
| 7435 } |
| 7436 }; |
| 7437 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7438 // Use of this source code is governed by a BSD-style license that can be |
| 7439 // found in the LICENSE file. |
| 7440 |
| 7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field. |
| 7442 Polymer({ |
| 7443 is: 'cr-toolbar-search-field', |
| 7444 |
| 7445 behaviors: [CrSearchFieldBehavior], |
| 7446 |
| 7447 properties: { |
| 7448 narrow: { |
| 7449 type: Boolean, |
| 7450 reflectToAttribute: true, |
| 7451 }, |
| 7452 |
| 7453 // Prompt text to display in the search field. |
| 7454 label: String, |
| 7455 |
| 7456 // Tooltip to display on the clear search button. |
| 7457 clearLabel: String, |
| 7458 |
| 7459 // When true, show a loading spinner to indicate that the backend is |
| 7460 // processing the search. Will only show if the search field is open. |
| 7461 spinnerActive: { |
| 7462 type: Boolean, |
| 7463 reflectToAttribute: true |
| 7464 }, |
| 7465 |
| 7466 /** @private */ |
| 7467 hasSearchText_: Boolean, |
| 7468 }, |
| 7469 |
| 7470 listeners: { |
| 7471 'tap': 'showSearch_', |
| 7472 'searchInput.bind-value-changed': 'onBindValueChanged_', |
| 7473 }, |
| 7474 |
| 7475 /** @return {!HTMLInputElement} */ |
| 7476 getSearchInput: function() { |
| 7477 return this.$.searchInput; |
| 7478 }, |
| 7479 |
| 7480 /** @return {boolean} */ |
| 7481 isSearchFocused: function() { |
| 7482 return this.$.searchTerm.focused; |
| 7483 }, |
| 7484 |
| 7485 /** |
| 7486 * @param {boolean} narrow |
| 7487 * @return {number} |
| 7488 * @private |
| 7489 */ |
| 7490 computeIconTabIndex_: function(narrow) { |
| 7491 return narrow ? 0 : -1; |
| 7492 }, |
| 7493 |
| 7494 /** |
| 7495 * @param {boolean} spinnerActive |
| 7496 * @param {boolean} showingSearch |
| 7497 * @return {boolean} |
| 7498 * @private |
| 7499 */ |
| 7500 isSpinnerShown_: function(spinnerActive, showingSearch) { |
| 7501 return spinnerActive && showingSearch; |
| 7502 }, |
| 7503 |
| 7504 /** @private */ |
| 7505 onInputBlur_: function() { |
| 7506 if (!this.hasSearchText_) |
| 7507 this.showingSearch = false; |
| 7508 }, |
| 7509 |
| 7510 /** |
| 7511 * Update the state of the search field whenever the underlying input value |
| 7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately |
| 7513 * after any change, whether the result of user input or JS modification. |
| 7514 * @private |
| 7515 */ |
| 7516 onBindValueChanged_: function() { |
| 7517 var newValue = this.$.searchInput.bindValue; |
| 7518 this.hasSearchText_ = newValue != ''; |
| 7519 if (newValue != '') |
| 7520 this.showingSearch = true; |
| 7521 }, |
| 7522 |
| 7523 /** |
| 7524 * @param {Event} e |
| 7525 * @private |
| 7526 */ |
| 7527 showSearch_: function(e) { |
| 7528 if (e.target != this.$.clearSearch) |
| 7529 this.showingSearch = true; |
| 7530 }, |
| 7531 |
| 7532 /** |
| 7533 * @param {Event} e |
| 7534 * @private |
| 7535 */ |
| 7536 hideSearch_: function(e) { |
| 7537 this.showingSearch = false; |
| 7538 e.stopPropagation(); |
| 7539 } |
| 7540 }); |
| 7541 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 7542 // Use of this source code is governed by a BSD-style license that can be |
| 7543 // found in the LICENSE file. |
| 7544 |
| 7545 Polymer({ |
| 7546 is: 'cr-toolbar', |
| 7547 |
| 7548 properties: { |
| 7549 // Name to display in the toolbar, in titlecase. |
| 7550 pageName: String, |
| 7551 |
| 7552 // Prompt text to display in the search field. |
| 7553 searchPrompt: String, |
| 7554 |
| 7555 // Tooltip to display on the clear search button. |
| 7556 clearLabel: String, |
| 7557 |
| 7558 // Value is proxied through to cr-toolbar-search-field. When true, |
| 7559 // the search field will show a processing spinner. |
| 7560 spinnerActive: Boolean, |
| 7561 |
| 7562 // Controls whether the menu button is shown at the start of the menu. |
| 7563 showMenu: { |
| 7564 type: Boolean, |
| 7565 reflectToAttribute: true, |
| 7566 value: true |
| 7567 }, |
| 7568 |
| 7569 /** @private */ |
| 7570 narrow_: { |
| 7571 type: Boolean, |
| 7572 reflectToAttribute: true |
| 7573 }, |
| 7574 |
| 7575 /** @private */ |
| 7576 showingSearch_: { |
| 7577 type: Boolean, |
| 7578 reflectToAttribute: true, |
| 7579 }, |
| 7580 }, |
| 7581 |
| 7582 /** @return {!CrToolbarSearchFieldElement} */ |
| 7583 getSearchField: function() { |
| 7584 return this.$.search; |
| 7585 }, |
| 7586 |
| 7587 /** @private */ |
| 7588 onMenuTap_: function(e) { |
| 7589 this.fire('cr-menu-tap'); |
| 7590 } |
| 7591 }); |
| 6271 // Copyright 2015 The Chromium Authors. All rights reserved. | 7592 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 6272 // Use of this source code is governed by a BSD-style license that can be | 7593 // Use of this source code is governed by a BSD-style license that can be |
| 6273 // found in the LICENSE file. | 7594 // found in the LICENSE file. |
| 6274 | 7595 |
| 6275 cr.define('downloads', function() { | 7596 Polymer({ |
| 6276 var Item = Polymer({ | 7597 is: 'history-toolbar', |
| 6277 is: 'downloads-item', | 7598 properties: { |
| 6278 | 7599 // Number of history items currently selected. |
| 6279 properties: { | 7600 // TODO(calamity): bind this to |
| 6280 data: { | 7601 // listContainer.selectedItem.selectedPaths.length. |
| 6281 type: Object, | 7602 count: { |
| 6282 }, | 7603 type: Number, |
| 6283 | 7604 value: 0, |
| 6284 completelyOnDisk_: { | 7605 observer: 'changeToolbarView_' |
| 6285 computed: 'computeCompletelyOnDisk_(' + | 7606 }, |
| 6286 'data.state, data.file_externally_removed)', | 7607 |
| 6287 type: Boolean, | 7608 // True if 1 or more history items are selected. When this value changes |
| 6288 value: true, | 7609 // the background colour changes. |
| 6289 }, | 7610 itemsSelected_: { |
| 6290 | 7611 type: Boolean, |
| 6291 controlledBy_: { | 7612 value: false, |
| 6292 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', | 7613 reflectToAttribute: true |
| 6293 type: String, | 7614 }, |
| 6294 value: '', | 7615 |
| 6295 }, | 7616 // The most recent term entered in the search field. Updated incrementally |
| 6296 | 7617 // as the user types. |
| 6297 isActive_: { | 7618 searchTerm: { |
| 6298 computed: 'computeIsActive_(' + | 7619 type: String, |
| 6299 'data.state, data.file_externally_removed)', | 7620 notify: true, |
| 6300 type: Boolean, | 7621 }, |
| 6301 value: true, | 7622 |
| 6302 }, | 7623 // True if the backend is processing and a spinner should be shown in the |
| 6303 | 7624 // toolbar. |
| 6304 isDangerous_: { | 7625 spinnerActive: { |
| 6305 computed: 'computeIsDangerous_(data.state)', | 7626 type: Boolean, |
| 6306 type: Boolean, | 7627 value: false |
| 6307 value: false, | 7628 }, |
| 6308 }, | 7629 |
| 6309 | 7630 hasDrawer: { |
| 6310 isMalware_: { | 7631 type: Boolean, |
| 6311 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', | 7632 observer: 'hasDrawerChanged_', |
| 6312 type: Boolean, | 7633 reflectToAttribute: true, |
| 6313 value: false, | 7634 }, |
| 6314 }, | 7635 |
| 6315 | 7636 // Whether domain-grouped history is enabled. |
| 6316 isInProgress_: { | 7637 isGroupedMode: { |
| 6317 computed: 'computeIsInProgress_(data.state)', | 7638 type: Boolean, |
| 6318 type: Boolean, | 7639 reflectToAttribute: true, |
| 6319 value: false, | 7640 }, |
| 6320 }, | 7641 |
| 6321 | 7642 // The period to search over. Matches BrowsingHistoryHandler::Range. |
| 6322 pauseOrResumeText_: { | 7643 groupedRange: { |
| 6323 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', | 7644 type: Number, |
| 6324 type: String, | 7645 value: 0, |
| 6325 }, | 7646 reflectToAttribute: true, |
| 6326 | 7647 notify: true |
| 6327 showCancel_: { | 7648 }, |
| 6328 computed: 'computeShowCancel_(data.state)', | 7649 |
| 6329 type: Boolean, | 7650 // The start time of the query range. |
| 6330 value: false, | 7651 queryStartTime: String, |
| 6331 }, | 7652 |
| 6332 | 7653 // The end time of the query range. |
| 6333 showProgress_: { | 7654 queryEndTime: String, |
| 6334 computed: 'computeShowProgress_(showCancel_, data.percent)', | 7655 }, |
| 6335 type: Boolean, | 7656 |
| 6336 value: false, | 7657 /** |
| 6337 }, | 7658 * Changes the toolbar background color depending on whether any history items |
| 6338 }, | 7659 * are currently selected. |
| 6339 | 7660 * @private |
| 6340 observers: [ | 7661 */ |
| 6341 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | 7662 changeToolbarView_: function() { |
| 6342 // and data.by_ext_name directly. Why? | 7663 this.itemsSelected_ = this.count > 0; |
| 6343 'observeControlledBy_(controlledBy_)', | 7664 }, |
| 6344 'observeIsDangerous_(isDangerous_, data)', | 7665 |
| 6345 ], | 7666 /** |
| 6346 | 7667 * When changing the search term externally, update the search field to |
| 6347 ready: function() { | 7668 * reflect the new search term. |
| 6348 this.content = this.$.content; | 7669 * @param {string} search |
| 6349 }, | 7670 */ |
| 6350 | 7671 setSearchTerm: function(search) { |
| 6351 /** @private */ | 7672 if (this.searchTerm == search) |
| 6352 computeClass_: function() { | 7673 return; |
| 6353 var classes = []; | 7674 |
| 6354 | 7675 this.searchTerm = search; |
| 6355 if (this.isActive_) | 7676 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) |
| 6356 classes.push('is-active'); | 7677 .getSearchField(); |
| 6357 | 7678 searchField.showAndFocus(); |
| 6358 if (this.isDangerous_) | 7679 searchField.setValue(search); |
| 6359 classes.push('dangerous'); | 7680 }, |
| 6360 | 7681 |
| 6361 if (this.showProgress_) | 7682 /** |
| 6362 classes.push('show-progress'); | 7683 * @param {!CustomEvent} event |
| 6363 | 7684 * @private |
| 6364 return classes.join(' '); | 7685 */ |
| 6365 }, | 7686 onSearchChanged_: function(event) { |
| 6366 | 7687 this.searchTerm = /** @type {string} */ (event.detail); |
| 6367 /** @private */ | 7688 }, |
| 6368 computeCompletelyOnDisk_: function() { | 7689 |
| 6369 return this.data.state == downloads.States.COMPLETE && | 7690 onClearSelectionTap_: function() { |
| 6370 !this.data.file_externally_removed; | 7691 this.fire('unselect-all'); |
| 6371 }, | 7692 }, |
| 6372 | 7693 |
| 6373 /** @private */ | 7694 onDeleteTap_: function() { |
| 6374 computeControlledBy_: function() { | 7695 this.fire('delete-selected'); |
| 6375 if (!this.data.by_ext_id || !this.data.by_ext_name) | 7696 }, |
| 6376 return ''; | 7697 |
| 6377 | 7698 get searchBar() { |
| 6378 var url = 'chrome://extensions#' + this.data.by_ext_id; | 7699 return this.$['main-toolbar'].getSearchField(); |
| 6379 var name = this.data.by_ext_name; | 7700 }, |
| 6380 return loadTimeData.getStringF('controlledByUrl', url, name); | 7701 |
| 6381 }, | 7702 showSearchField: function() { |
| 6382 | 7703 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) |
| 6383 /** @private */ | 7704 .getSearchField() |
| 6384 computeDangerIcon_: function() { | 7705 .showAndFocus(); |
| 6385 if (!this.isDangerous_) | 7706 }, |
| 6386 return ''; | 7707 |
| 6387 | 7708 /** |
| 6388 switch (this.data.danger_type) { | 7709 * If the user is a supervised user the delete button is not shown. |
| 6389 case downloads.DangerType.DANGEROUS_CONTENT: | 7710 * @private |
| 6390 case downloads.DangerType.DANGEROUS_HOST: | 7711 */ |
| 6391 case downloads.DangerType.DANGEROUS_URL: | 7712 deletingAllowed_: function() { |
| 6392 case downloads.DangerType.POTENTIALLY_UNWANTED: | 7713 return loadTimeData.getBoolean('allowDeletingHistory'); |
| 6393 case downloads.DangerType.UNCOMMON_CONTENT: | 7714 }, |
| 6394 return 'downloads:remove-circle'; | 7715 |
| 6395 default: | 7716 numberOfItemsSelected_: function(count) { |
| 6396 return 'cr:warning'; | 7717 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; |
| 6397 } | 7718 }, |
| 6398 }, | 7719 |
| 6399 | 7720 getHistoryInterval_: function(queryStartTime, queryEndTime) { |
| 6400 /** @private */ | 7721 // TODO(calamity): Fix the format of these dates. |
| 6401 computeDate_: function() { | 7722 return loadTimeData.getStringF( |
| 6402 assert(typeof this.data.hideDate == 'boolean'); | 7723 'historyInterval', queryStartTime, queryEndTime); |
| 6403 if (this.data.hideDate) | 7724 }, |
| 6404 return ''; | 7725 |
| 6405 return assert(this.data.since_string || this.data.date_string); | 7726 /** @private */ |
| 6406 }, | 7727 hasDrawerChanged_: function() { |
| 6407 | 7728 this.updateStyles(); |
| 6408 /** @private */ | 7729 }, |
| 6409 computeDescription_: function() { | |
| 6410 var data = this.data; | |
| 6411 | |
| 6412 switch (data.state) { | |
| 6413 case downloads.States.DANGEROUS: | |
| 6414 var fileName = data.file_name; | |
| 6415 switch (data.danger_type) { | |
| 6416 case downloads.DangerType.DANGEROUS_FILE: | |
| 6417 return loadTimeData.getStringF('dangerFileDesc', fileName); | |
| 6418 case downloads.DangerType.DANGEROUS_URL: | |
| 6419 return loadTimeData.getString('dangerUrlDesc'); | |
| 6420 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. | |
| 6421 case downloads.DangerType.DANGEROUS_HOST: | |
| 6422 return loadTimeData.getStringF('dangerContentDesc', fileName); | |
| 6423 case downloads.DangerType.UNCOMMON_CONTENT: | |
| 6424 return loadTimeData.getStringF('dangerUncommonDesc', fileName); | |
| 6425 case downloads.DangerType.POTENTIALLY_UNWANTED: | |
| 6426 return loadTimeData.getStringF('dangerSettingsDesc', fileName); | |
| 6427 } | |
| 6428 break; | |
| 6429 | |
| 6430 case downloads.States.IN_PROGRESS: | |
| 6431 case downloads.States.PAUSED: // Fallthrough. | |
| 6432 return data.progress_status_text; | |
| 6433 } | |
| 6434 | |
| 6435 return ''; | |
| 6436 }, | |
| 6437 | |
| 6438 /** @private */ | |
| 6439 computeIsActive_: function() { | |
| 6440 return this.data.state != downloads.States.CANCELLED && | |
| 6441 this.data.state != downloads.States.INTERRUPTED && | |
| 6442 !this.data.file_externally_removed; | |
| 6443 }, | |
| 6444 | |
| 6445 /** @private */ | |
| 6446 computeIsDangerous_: function() { | |
| 6447 return this.data.state == downloads.States.DANGEROUS; | |
| 6448 }, | |
| 6449 | |
| 6450 /** @private */ | |
| 6451 computeIsInProgress_: function() { | |
| 6452 return this.data.state == downloads.States.IN_PROGRESS; | |
| 6453 }, | |
| 6454 | |
| 6455 /** @private */ | |
| 6456 computeIsMalware_: function() { | |
| 6457 return this.isDangerous_ && | |
| 6458 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || | |
| 6459 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || | |
| 6460 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || | |
| 6461 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); | |
| 6462 }, | |
| 6463 | |
| 6464 /** @private */ | |
| 6465 computePauseOrResumeText_: function() { | |
| 6466 if (this.isInProgress_) | |
| 6467 return loadTimeData.getString('controlPause'); | |
| 6468 if (this.data.resume) | |
| 6469 return loadTimeData.getString('controlResume'); | |
| 6470 return ''; | |
| 6471 }, | |
| 6472 | |
| 6473 /** @private */ | |
| 6474 computeRemoveStyle_: function() { | |
| 6475 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); | |
| 6476 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; | |
| 6477 return hideRemove ? 'visibility: hidden' : ''; | |
| 6478 }, | |
| 6479 | |
| 6480 /** @private */ | |
| 6481 computeShowCancel_: function() { | |
| 6482 return this.data.state == downloads.States.IN_PROGRESS || | |
| 6483 this.data.state == downloads.States.PAUSED; | |
| 6484 }, | |
| 6485 | |
| 6486 /** @private */ | |
| 6487 computeShowProgress_: function() { | |
| 6488 return this.showCancel_ && this.data.percent >= -1; | |
| 6489 }, | |
| 6490 | |
| 6491 /** @private */ | |
| 6492 computeTag_: function() { | |
| 6493 switch (this.data.state) { | |
| 6494 case downloads.States.CANCELLED: | |
| 6495 return loadTimeData.getString('statusCancelled'); | |
| 6496 | |
| 6497 case downloads.States.INTERRUPTED: | |
| 6498 return this.data.last_reason_text; | |
| 6499 | |
| 6500 case downloads.States.COMPLETE: | |
| 6501 return this.data.file_externally_removed ? | |
| 6502 loadTimeData.getString('statusRemoved') : ''; | |
| 6503 } | |
| 6504 | |
| 6505 return ''; | |
| 6506 }, | |
| 6507 | |
| 6508 /** @private */ | |
| 6509 isIndeterminate_: function() { | |
| 6510 return this.data.percent == -1; | |
| 6511 }, | |
| 6512 | |
| 6513 /** @private */ | |
| 6514 observeControlledBy_: function() { | |
| 6515 this.$['controlled-by'].innerHTML = this.controlledBy_; | |
| 6516 }, | |
| 6517 | |
| 6518 /** @private */ | |
| 6519 observeIsDangerous_: function() { | |
| 6520 if (!this.data) | |
| 6521 return; | |
| 6522 | |
| 6523 if (this.isDangerous_) { | |
| 6524 this.$.url.removeAttribute('href'); | |
| 6525 } else { | |
| 6526 this.$.url.href = assert(this.data.url); | |
| 6527 var filePath = encodeURIComponent(this.data.file_path); | |
| 6528 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; | |
| 6529 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; | |
| 6530 } | |
| 6531 }, | |
| 6532 | |
| 6533 /** @private */ | |
| 6534 onCancelTap_: function() { | |
| 6535 downloads.ActionService.getInstance().cancel(this.data.id); | |
| 6536 }, | |
| 6537 | |
| 6538 /** @private */ | |
| 6539 onDiscardDangerousTap_: function() { | |
| 6540 downloads.ActionService.getInstance().discardDangerous(this.data.id); | |
| 6541 }, | |
| 6542 | |
| 6543 /** | |
| 6544 * @private | |
| 6545 * @param {Event} e | |
| 6546 */ | |
| 6547 onDragStart_: function(e) { | |
| 6548 e.preventDefault(); | |
| 6549 downloads.ActionService.getInstance().drag(this.data.id); | |
| 6550 }, | |
| 6551 | |
| 6552 /** | |
| 6553 * @param {Event} e | |
| 6554 * @private | |
| 6555 */ | |
| 6556 onFileLinkTap_: function(e) { | |
| 6557 e.preventDefault(); | |
| 6558 downloads.ActionService.getInstance().openFile(this.data.id); | |
| 6559 }, | |
| 6560 | |
| 6561 /** @private */ | |
| 6562 onPauseOrResumeTap_: function() { | |
| 6563 if (this.isInProgress_) | |
| 6564 downloads.ActionService.getInstance().pause(this.data.id); | |
| 6565 else | |
| 6566 downloads.ActionService.getInstance().resume(this.data.id); | |
| 6567 }, | |
| 6568 | |
| 6569 /** @private */ | |
| 6570 onRemoveTap_: function() { | |
| 6571 downloads.ActionService.getInstance().remove(this.data.id); | |
| 6572 }, | |
| 6573 | |
| 6574 /** @private */ | |
| 6575 onRetryTap_: function() { | |
| 6576 downloads.ActionService.getInstance().download(this.data.url); | |
| 6577 }, | |
| 6578 | |
| 6579 /** @private */ | |
| 6580 onSaveDangerousTap_: function() { | |
| 6581 downloads.ActionService.getInstance().saveDangerous(this.data.id); | |
| 6582 }, | |
| 6583 | |
| 6584 /** @private */ | |
| 6585 onShowTap_: function() { | |
| 6586 downloads.ActionService.getInstance().show(this.data.id); | |
| 6587 }, | |
| 6588 }); | |
| 6589 | |
| 6590 return {Item: Item}; | |
| 6591 }); | 7730 }); |
| 6592 /** @polymerBehavior Polymer.PaperItemBehavior */ | 7731 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 6593 Polymer.PaperItemBehaviorImpl = { | 7732 // Use of this source code is governed by a BSD-style license that can be |
| 6594 hostAttributes: { | 7733 // found in the LICENSE file. |
| 6595 role: 'option', | 7734 |
| 6596 tabindex: '0' | 7735 /** |
| 6597 } | 7736 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the |
| 6598 }; | 7737 * dialog is closed via close(), a 'close' event is fired. If the dialog is |
| 6599 | 7738 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event. |
| 6600 /** @polymerBehavior */ | 7739 * Additionally clients can inspect the dialog's |returnValue| property inside |
| 6601 Polymer.PaperItemBehavior = [ | 7740 * the 'close' event listener to determine whether it was canceled or just |
| 6602 Polymer.IronButtonState, | 7741 * closed, where a truthy value means success, and a falsy value means it was |
| 6603 Polymer.IronControlState, | 7742 * canceled. |
| 6604 Polymer.PaperItemBehaviorImpl | 7743 */ |
| 6605 ]; | |
| 6606 Polymer({ | 7744 Polymer({ |
| 6607 is: 'paper-item', | 7745 is: 'cr-dialog', |
| 6608 | 7746 extends: 'dialog', |
| 6609 behaviors: [ | 7747 |
| 6610 Polymer.PaperItemBehavior | 7748 cancel: function() { |
| 6611 ] | 7749 this.fire('cancel'); |
| 6612 }); | 7750 HTMLDialogElement.prototype.close.call(this, ''); |
| 6613 /** | 7751 }, |
| 6614 * @param {!Function} selectCallback | 7752 |
| 6615 * @constructor | 7753 /** |
| 6616 */ | 7754 * @param {string=} opt_returnValue |
| 6617 Polymer.IronSelection = function(selectCallback) { | 7755 * @override |
| 6618 this.selection = []; | 7756 */ |
| 6619 this.selectCallback = selectCallback; | 7757 close: function(opt_returnValue) { |
| 6620 }; | 7758 HTMLDialogElement.prototype.close.call(this, 'success'); |
| 6621 | 7759 }, |
| 6622 Polymer.IronSelection.prototype = { | 7760 |
| 6623 | 7761 /** @return {!PaperIconButtonElement} */ |
| 6624 /** | 7762 getCloseButton: function() { |
| 6625 * Retrieves the selected item(s). | 7763 return this.$.close; |
| 6626 * | 7764 }, |
| 6627 * @method get | 7765 }); |
| 6628 * @returns Returns the selected item(s). If the multi property is true, | |
| 6629 * `get` will return an array, otherwise it will return | |
| 6630 * the selected item or undefined if there is no selection. | |
| 6631 */ | |
| 6632 get: function() { | |
| 6633 return this.multi ? this.selection.slice() : this.selection[0]; | |
| 6634 }, | |
| 6635 | |
| 6636 /** | |
| 6637 * Clears all the selection except the ones indicated. | |
| 6638 * | |
| 6639 * @method clear | |
| 6640 * @param {Array} excludes items to be excluded. | |
| 6641 */ | |
| 6642 clear: function(excludes) { | |
| 6643 this.selection.slice().forEach(function(item) { | |
| 6644 if (!excludes || excludes.indexOf(item) < 0) { | |
| 6645 this.setItemSelected(item, false); | |
| 6646 } | |
| 6647 }, this); | |
| 6648 }, | |
| 6649 | |
| 6650 /** | |
| 6651 * Indicates if a given item is selected. | |
| 6652 * | |
| 6653 * @method isSelected | |
| 6654 * @param {*} item The item whose selection state should be checked. | |
| 6655 * @returns Returns true if `item` is selected. | |
| 6656 */ | |
| 6657 isSelected: function(item) { | |
| 6658 return this.selection.indexOf(item) >= 0; | |
| 6659 }, | |
| 6660 | |
| 6661 /** | |
| 6662 * Sets the selection state for a given item to either selected or deselecte
d. | |
| 6663 * | |
| 6664 * @method setItemSelected | |
| 6665 * @param {*} item The item to select. | |
| 6666 * @param {boolean} isSelected True for selected, false for deselected. | |
| 6667 */ | |
| 6668 setItemSelected: function(item, isSelected) { | |
| 6669 if (item != null) { | |
| 6670 if (isSelected !== this.isSelected(item)) { | |
| 6671 // proceed to update selection only if requested state differs from cu
rrent | |
| 6672 if (isSelected) { | |
| 6673 this.selection.push(item); | |
| 6674 } else { | |
| 6675 var i = this.selection.indexOf(item); | |
| 6676 if (i >= 0) { | |
| 6677 this.selection.splice(i, 1); | |
| 6678 } | |
| 6679 } | |
| 6680 if (this.selectCallback) { | |
| 6681 this.selectCallback(item, isSelected); | |
| 6682 } | |
| 6683 } | |
| 6684 } | |
| 6685 }, | |
| 6686 | |
| 6687 /** | |
| 6688 * Sets the selection state for a given item. If the `multi` property | |
| 6689 * is true, then the selected state of `item` will be toggled; otherwise | |
| 6690 * the `item` will be selected. | |
| 6691 * | |
| 6692 * @method select | |
| 6693 * @param {*} item The item to select. | |
| 6694 */ | |
| 6695 select: function(item) { | |
| 6696 if (this.multi) { | |
| 6697 this.toggle(item); | |
| 6698 } else if (this.get() !== item) { | |
| 6699 this.setItemSelected(this.get(), false); | |
| 6700 this.setItemSelected(item, true); | |
| 6701 } | |
| 6702 }, | |
| 6703 | |
| 6704 /** | |
| 6705 * Toggles the selection state for `item`. | |
| 6706 * | |
| 6707 * @method toggle | |
| 6708 * @param {*} item The item to toggle. | |
| 6709 */ | |
| 6710 toggle: function(item) { | |
| 6711 this.setItemSelected(item, !this.isSelected(item)); | |
| 6712 } | |
| 6713 | |
| 6714 }; | |
| 6715 /** @polymerBehavior */ | |
| 6716 Polymer.IronSelectableBehavior = { | |
| 6717 | |
| 6718 /** | |
| 6719 * Fired when iron-selector is activated (selected or deselected). | |
| 6720 * It is fired before the selected items are changed. | |
| 6721 * Cancel the event to abort selection. | |
| 6722 * | |
| 6723 * @event iron-activate | |
| 6724 */ | |
| 6725 | |
| 6726 /** | |
| 6727 * Fired when an item is selected | |
| 6728 * | |
| 6729 * @event iron-select | |
| 6730 */ | |
| 6731 | |
| 6732 /** | |
| 6733 * Fired when an item is deselected | |
| 6734 * | |
| 6735 * @event iron-deselect | |
| 6736 */ | |
| 6737 | |
| 6738 /** | |
| 6739 * Fired when the list of selectable items changes (e.g., items are | |
| 6740 * added or removed). The detail of the event is a mutation record that | |
| 6741 * describes what changed. | |
| 6742 * | |
| 6743 * @event iron-items-changed | |
| 6744 */ | |
| 6745 | |
| 6746 properties: { | |
| 6747 | |
| 6748 /** | |
| 6749 * If you want to use an attribute value or property of an element for | |
| 6750 * `selected` instead of the index, set this to the name of the attribute | |
| 6751 * or property. Hyphenated values are converted to camel case when used to | |
| 6752 * look up the property of a selectable element. Camel cased values are | |
| 6753 * *not* converted to hyphenated values for attribute lookup. It's | |
| 6754 * recommended that you provide the hyphenated form of the name so that | |
| 6755 * selection works in both cases. (Use `attr-or-property-name` instead of | |
| 6756 * `attrOrPropertyName`.) | |
| 6757 */ | |
| 6758 attrForSelected: { | |
| 6759 type: String, | |
| 6760 value: null | |
| 6761 }, | |
| 6762 | |
| 6763 /** | |
| 6764 * Gets or sets the selected element. The default is to use the index of t
he item. | |
| 6765 * @type {string|number} | |
| 6766 */ | |
| 6767 selected: { | |
| 6768 type: String, | |
| 6769 notify: true | |
| 6770 }, | |
| 6771 | |
| 6772 /** | |
| 6773 * Returns the currently selected item. | |
| 6774 * | |
| 6775 * @type {?Object} | |
| 6776 */ | |
| 6777 selectedItem: { | |
| 6778 type: Object, | |
| 6779 readOnly: true, | |
| 6780 notify: true | |
| 6781 }, | |
| 6782 | |
| 6783 /** | |
| 6784 * The event that fires from items when they are selected. Selectable | |
| 6785 * will listen for this event from items and update the selection state. | |
| 6786 * Set to empty string to listen to no events. | |
| 6787 */ | |
| 6788 activateEvent: { | |
| 6789 type: String, | |
| 6790 value: 'tap', | |
| 6791 observer: '_activateEventChanged' | |
| 6792 }, | |
| 6793 | |
| 6794 /** | |
| 6795 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
| 6796 * are selectable. | |
| 6797 */ | |
| 6798 selectable: String, | |
| 6799 | |
| 6800 /** | |
| 6801 * The class to set on elements when selected. | |
| 6802 */ | |
| 6803 selectedClass: { | |
| 6804 type: String, | |
| 6805 value: 'iron-selected' | |
| 6806 }, | |
| 6807 | |
| 6808 /** | |
| 6809 * The attribute to set on elements when selected. | |
| 6810 */ | |
| 6811 selectedAttribute: { | |
| 6812 type: String, | |
| 6813 value: null | |
| 6814 }, | |
| 6815 | |
| 6816 /** | |
| 6817 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
| 6818 * is not found. | |
| 6819 */ | |
| 6820 fallbackSelection: { | |
| 6821 type: String, | |
| 6822 value: null | |
| 6823 }, | |
| 6824 | |
| 6825 /** | |
| 6826 * The list of items from which a selection can be made. | |
| 6827 */ | |
| 6828 items: { | |
| 6829 type: Array, | |
| 6830 readOnly: true, | |
| 6831 notify: true, | |
| 6832 value: function() { | |
| 6833 return []; | |
| 6834 } | |
| 6835 }, | |
| 6836 | |
| 6837 /** | |
| 6838 * The set of excluded elements where the key is the `localName` | |
| 6839 * of the element that will be ignored from the item list. | |
| 6840 * | |
| 6841 * @default {template: 1} | |
| 6842 */ | |
| 6843 _excludedLocalNames: { | |
| 6844 type: Object, | |
| 6845 value: function() { | |
| 6846 return { | |
| 6847 'template': 1 | |
| 6848 }; | |
| 6849 } | |
| 6850 } | |
| 6851 }, | |
| 6852 | |
| 6853 observers: [ | |
| 6854 '_updateAttrForSelected(attrForSelected)', | |
| 6855 '_updateSelected(selected)', | |
| 6856 '_checkFallback(fallbackSelection)' | |
| 6857 ], | |
| 6858 | |
| 6859 created: function() { | |
| 6860 this._bindFilterItem = this._filterItem.bind(this); | |
| 6861 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | |
| 6862 }, | |
| 6863 | |
| 6864 attached: function() { | |
| 6865 this._observer = this._observeItems(this); | |
| 6866 this._updateItems(); | |
| 6867 if (!this._shouldUpdateSelection) { | |
| 6868 this._updateSelected(); | |
| 6869 } | |
| 6870 this._addListener(this.activateEvent); | |
| 6871 }, | |
| 6872 | |
| 6873 detached: function() { | |
| 6874 if (this._observer) { | |
| 6875 Polymer.dom(this).unobserveNodes(this._observer); | |
| 6876 } | |
| 6877 this._removeListener(this.activateEvent); | |
| 6878 }, | |
| 6879 | |
| 6880 /** | |
| 6881 * Returns the index of the given item. | |
| 6882 * | |
| 6883 * @method indexOf | |
| 6884 * @param {Object} item | |
| 6885 * @returns Returns the index of the item | |
| 6886 */ | |
| 6887 indexOf: function(item) { | |
| 6888 return this.items.indexOf(item); | |
| 6889 }, | |
| 6890 | |
| 6891 /** | |
| 6892 * Selects the given value. | |
| 6893 * | |
| 6894 * @method select | |
| 6895 * @param {string|number} value the value to select. | |
| 6896 */ | |
| 6897 select: function(value) { | |
| 6898 this.selected = value; | |
| 6899 }, | |
| 6900 | |
| 6901 /** | |
| 6902 * Selects the previous item. | |
| 6903 * | |
| 6904 * @method selectPrevious | |
| 6905 */ | |
| 6906 selectPrevious: function() { | |
| 6907 var length = this.items.length; | |
| 6908 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | |
| 6909 this.selected = this._indexToValue(index); | |
| 6910 }, | |
| 6911 | |
| 6912 /** | |
| 6913 * Selects the next item. | |
| 6914 * | |
| 6915 * @method selectNext | |
| 6916 */ | |
| 6917 selectNext: function() { | |
| 6918 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | |
| 6919 this.selected = this._indexToValue(index); | |
| 6920 }, | |
| 6921 | |
| 6922 /** | |
| 6923 * Selects the item at the given index. | |
| 6924 * | |
| 6925 * @method selectIndex | |
| 6926 */ | |
| 6927 selectIndex: function(index) { | |
| 6928 this.select(this._indexToValue(index)); | |
| 6929 }, | |
| 6930 | |
| 6931 /** | |
| 6932 * Force a synchronous update of the `items` property. | |
| 6933 * | |
| 6934 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
| 6935 * updates to the set of selectable items after updates to the DOM list and | |
| 6936 * selection state have been made. | |
| 6937 * | |
| 6938 * WARNING: If you are using this method, you should probably consider an | |
| 6939 * alternate approach. Synchronously querying for items is potentially | |
| 6940 * slow for many use cases. The `items` property will update asynchronously | |
| 6941 * on its own to reflect selectable items in the DOM. | |
| 6942 */ | |
| 6943 forceSynchronousItemUpdate: function() { | |
| 6944 this._updateItems(); | |
| 6945 }, | |
| 6946 | |
| 6947 get _shouldUpdateSelection() { | |
| 6948 return this.selected != null; | |
| 6949 }, | |
| 6950 | |
| 6951 _checkFallback: function() { | |
| 6952 if (this._shouldUpdateSelection) { | |
| 6953 this._updateSelected(); | |
| 6954 } | |
| 6955 }, | |
| 6956 | |
| 6957 _addListener: function(eventName) { | |
| 6958 this.listen(this, eventName, '_activateHandler'); | |
| 6959 }, | |
| 6960 | |
| 6961 _removeListener: function(eventName) { | |
| 6962 this.unlisten(this, eventName, '_activateHandler'); | |
| 6963 }, | |
| 6964 | |
| 6965 _activateEventChanged: function(eventName, old) { | |
| 6966 this._removeListener(old); | |
| 6967 this._addListener(eventName); | |
| 6968 }, | |
| 6969 | |
| 6970 _updateItems: function() { | |
| 6971 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | |
| 6972 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | |
| 6973 this._setItems(nodes); | |
| 6974 }, | |
| 6975 | |
| 6976 _updateAttrForSelected: function() { | |
| 6977 if (this._shouldUpdateSelection) { | |
| 6978 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | |
| 6979 } | |
| 6980 }, | |
| 6981 | |
| 6982 _updateSelected: function() { | |
| 6983 this._selectSelected(this.selected); | |
| 6984 }, | |
| 6985 | |
| 6986 _selectSelected: function(selected) { | |
| 6987 this._selection.select(this._valueToItem(this.selected)); | |
| 6988 // Check for items, since this array is populated only when attached | |
| 6989 // Since Number(0) is falsy, explicitly check for undefined | |
| 6990 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | |
| 6991 this.selected = this.fallbackSelection; | |
| 6992 } | |
| 6993 }, | |
| 6994 | |
| 6995 _filterItem: function(node) { | |
| 6996 return !this._excludedLocalNames[node.localName]; | |
| 6997 }, | |
| 6998 | |
| 6999 _valueToItem: function(value) { | |
| 7000 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
| 7001 }, | |
| 7002 | |
| 7003 _valueToIndex: function(value) { | |
| 7004 if (this.attrForSelected) { | |
| 7005 for (var i = 0, item; item = this.items[i]; i++) { | |
| 7006 if (this._valueForItem(item) == value) { | |
| 7007 return i; | |
| 7008 } | |
| 7009 } | |
| 7010 } else { | |
| 7011 return Number(value); | |
| 7012 } | |
| 7013 }, | |
| 7014 | |
| 7015 _indexToValue: function(index) { | |
| 7016 if (this.attrForSelected) { | |
| 7017 var item = this.items[index]; | |
| 7018 if (item) { | |
| 7019 return this._valueForItem(item); | |
| 7020 } | |
| 7021 } else { | |
| 7022 return index; | |
| 7023 } | |
| 7024 }, | |
| 7025 | |
| 7026 _valueForItem: function(item) { | |
| 7027 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | |
| 7028 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | |
| 7029 }, | |
| 7030 | |
| 7031 _applySelection: function(item, isSelected) { | |
| 7032 if (this.selectedClass) { | |
| 7033 this.toggleClass(this.selectedClass, isSelected, item); | |
| 7034 } | |
| 7035 if (this.selectedAttribute) { | |
| 7036 this.toggleAttribute(this.selectedAttribute, isSelected, item); | |
| 7037 } | |
| 7038 this._selectionChange(); | |
| 7039 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | |
| 7040 }, | |
| 7041 | |
| 7042 _selectionChange: function() { | |
| 7043 this._setSelectedItem(this._selection.get()); | |
| 7044 }, | |
| 7045 | |
| 7046 // observe items change under the given node. | |
| 7047 _observeItems: function(node) { | |
| 7048 return Polymer.dom(node).observeNodes(function(mutation) { | |
| 7049 this._updateItems(); | |
| 7050 | |
| 7051 if (this._shouldUpdateSelection) { | |
| 7052 this._updateSelected(); | |
| 7053 } | |
| 7054 | |
| 7055 // Let other interested parties know about the change so that | |
| 7056 // we don't have to recreate mutation observers everywhere. | |
| 7057 this.fire('iron-items-changed', mutation, { | |
| 7058 bubbles: false, | |
| 7059 cancelable: false | |
| 7060 }); | |
| 7061 }); | |
| 7062 }, | |
| 7063 | |
| 7064 _activateHandler: function(e) { | |
| 7065 var t = e.target; | |
| 7066 var items = this.items; | |
| 7067 while (t && t != this) { | |
| 7068 var i = items.indexOf(t); | |
| 7069 if (i >= 0) { | |
| 7070 var value = this._indexToValue(i); | |
| 7071 this._itemActivate(value, t); | |
| 7072 return; | |
| 7073 } | |
| 7074 t = t.parentNode; | |
| 7075 } | |
| 7076 }, | |
| 7077 | |
| 7078 _itemActivate: function(value, item) { | |
| 7079 if (!this.fire('iron-activate', | |
| 7080 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | |
| 7081 this.select(value); | |
| 7082 } | |
| 7083 } | |
| 7084 | |
| 7085 }; | |
| 7086 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
| 7087 Polymer.IronMultiSelectableBehaviorImpl = { | |
| 7088 properties: { | |
| 7089 | |
| 7090 /** | |
| 7091 * If true, multiple selections are allowed. | |
| 7092 */ | |
| 7093 multi: { | |
| 7094 type: Boolean, | |
| 7095 value: false, | |
| 7096 observer: 'multiChanged' | |
| 7097 }, | |
| 7098 | |
| 7099 /** | |
| 7100 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
| 7101 * is true. | |
| 7102 */ | |
| 7103 selectedValues: { | |
| 7104 type: Array, | |
| 7105 notify: true | |
| 7106 }, | |
| 7107 | |
| 7108 /** | |
| 7109 * Returns an array of currently selected items. | |
| 7110 */ | |
| 7111 selectedItems: { | |
| 7112 type: Array, | |
| 7113 readOnly: true, | |
| 7114 notify: true | |
| 7115 }, | |
| 7116 | |
| 7117 }, | |
| 7118 | |
| 7119 observers: [ | |
| 7120 '_updateSelected(selectedValues.splices)' | |
| 7121 ], | |
| 7122 | |
| 7123 /** | |
| 7124 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 7125 * `value` will be toggled; otherwise the `value` will be selected. | |
| 7126 * | |
| 7127 * @method select | |
| 7128 * @param {string|number} value the value to select. | |
| 7129 */ | |
| 7130 select: function(value) { | |
| 7131 if (this.multi) { | |
| 7132 if (this.selectedValues) { | |
| 7133 this._toggleSelected(value); | |
| 7134 } else { | |
| 7135 this.selectedValues = [value]; | |
| 7136 } | |
| 7137 } else { | |
| 7138 this.selected = value; | |
| 7139 } | |
| 7140 }, | |
| 7141 | |
| 7142 multiChanged: function(multi) { | |
| 7143 this._selection.multi = multi; | |
| 7144 }, | |
| 7145 | |
| 7146 get _shouldUpdateSelection() { | |
| 7147 return this.selected != null || | |
| 7148 (this.selectedValues != null && this.selectedValues.length); | |
| 7149 }, | |
| 7150 | |
| 7151 _updateAttrForSelected: function() { | |
| 7152 if (!this.multi) { | |
| 7153 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
| 7154 } else if (this._shouldUpdateSelection) { | |
| 7155 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
| 7156 return this._indexToValue(this.indexOf(selectedItem)); | |
| 7157 }, this).filter(function(unfilteredValue) { | |
| 7158 return unfilteredValue != null; | |
| 7159 }, this); | |
| 7160 } | |
| 7161 }, | |
| 7162 | |
| 7163 _updateSelected: function() { | |
| 7164 if (this.multi) { | |
| 7165 this._selectMulti(this.selectedValues); | |
| 7166 } else { | |
| 7167 this._selectSelected(this.selected); | |
| 7168 } | |
| 7169 }, | |
| 7170 | |
| 7171 _selectMulti: function(values) { | |
| 7172 if (values) { | |
| 7173 var selectedItems = this._valuesToItems(values); | |
| 7174 // clear all but the current selected items | |
| 7175 this._selection.clear(selectedItems); | |
| 7176 // select only those not selected yet | |
| 7177 for (var i = 0; i < selectedItems.length; i++) { | |
| 7178 this._selection.setItemSelected(selectedItems[i], true); | |
| 7179 } | |
| 7180 // Check for items, since this array is populated only when attached | |
| 7181 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
| 7182 var fallback = this._valueToItem(this.fallbackSelection); | |
| 7183 if (fallback) { | |
| 7184 this.selectedValues = [this.fallbackSelection]; | |
| 7185 } | |
| 7186 } | |
| 7187 } else { | |
| 7188 this._selection.clear(); | |
| 7189 } | |
| 7190 }, | |
| 7191 | |
| 7192 _selectionChange: function() { | |
| 7193 var s = this._selection.get(); | |
| 7194 if (this.multi) { | |
| 7195 this._setSelectedItems(s); | |
| 7196 } else { | |
| 7197 this._setSelectedItems([s]); | |
| 7198 this._setSelectedItem(s); | |
| 7199 } | |
| 7200 }, | |
| 7201 | |
| 7202 _toggleSelected: function(value) { | |
| 7203 var i = this.selectedValues.indexOf(value); | |
| 7204 var unselected = i < 0; | |
| 7205 if (unselected) { | |
| 7206 this.push('selectedValues',value); | |
| 7207 } else { | |
| 7208 this.splice('selectedValues',i,1); | |
| 7209 } | |
| 7210 }, | |
| 7211 | |
| 7212 _valuesToItems: function(values) { | |
| 7213 return (values == null) ? null : values.map(function(value) { | |
| 7214 return this._valueToItem(value); | |
| 7215 }, this); | |
| 7216 } | |
| 7217 }; | |
| 7218 | |
| 7219 /** @polymerBehavior */ | |
| 7220 Polymer.IronMultiSelectableBehavior = [ | |
| 7221 Polymer.IronSelectableBehavior, | |
| 7222 Polymer.IronMultiSelectableBehaviorImpl | |
| 7223 ]; | |
| 7224 /** | |
| 7225 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
| 7226 * | |
| 7227 * @demo demo/index.html | |
| 7228 * @polymerBehavior Polymer.IronMenuBehavior | |
| 7229 */ | |
| 7230 Polymer.IronMenuBehaviorImpl = { | |
| 7231 | |
| 7232 properties: { | |
| 7233 | |
| 7234 /** | |
| 7235 * Returns the currently focused item. | |
| 7236 * @type {?Object} | |
| 7237 */ | |
| 7238 focusedItem: { | |
| 7239 observer: '_focusedItemChanged', | |
| 7240 readOnly: true, | |
| 7241 type: Object | |
| 7242 }, | |
| 7243 | |
| 7244 /** | |
| 7245 * The attribute to use on menu items to look up the item title. Typing th
e first | |
| 7246 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
| 7247 * will be used. | |
| 7248 */ | |
| 7249 attrForItemTitle: { | |
| 7250 type: String | |
| 7251 } | |
| 7252 }, | |
| 7253 | |
| 7254 hostAttributes: { | |
| 7255 'role': 'menu', | |
| 7256 'tabindex': '0' | |
| 7257 }, | |
| 7258 | |
| 7259 observers: [ | |
| 7260 '_updateMultiselectable(multi)' | |
| 7261 ], | |
| 7262 | |
| 7263 listeners: { | |
| 7264 'focus': '_onFocus', | |
| 7265 'keydown': '_onKeydown', | |
| 7266 'iron-items-changed': '_onIronItemsChanged' | |
| 7267 }, | |
| 7268 | |
| 7269 keyBindings: { | |
| 7270 'up': '_onUpKey', | |
| 7271 'down': '_onDownKey', | |
| 7272 'esc': '_onEscKey', | |
| 7273 'shift+tab:keydown': '_onShiftTabDown' | |
| 7274 }, | |
| 7275 | |
| 7276 attached: function() { | |
| 7277 this._resetTabindices(); | |
| 7278 }, | |
| 7279 | |
| 7280 /** | |
| 7281 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
| 7282 * `value` will be toggled; otherwise the `value` will be selected. | |
| 7283 * | |
| 7284 * @param {string|number} value the value to select. | |
| 7285 */ | |
| 7286 select: function(value) { | |
| 7287 // Cancel automatically focusing a default item if the menu received focus | |
| 7288 // through a user action selecting a particular item. | |
| 7289 if (this._defaultFocusAsync) { | |
| 7290 this.cancelAsync(this._defaultFocusAsync); | |
| 7291 this._defaultFocusAsync = null; | |
| 7292 } | |
| 7293 var item = this._valueToItem(value); | |
| 7294 if (item && item.hasAttribute('disabled')) return; | |
| 7295 this._setFocusedItem(item); | |
| 7296 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | |
| 7297 }, | |
| 7298 | |
| 7299 /** | |
| 7300 * Resets all tabindex attributes to the appropriate value based on the | |
| 7301 * current selection state. The appropriate value is `0` (focusable) for | |
| 7302 * the default selected item, and `-1` (not keyboard focusable) for all | |
| 7303 * other items. | |
| 7304 */ | |
| 7305 _resetTabindices: function() { | |
| 7306 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | |
| 7307 | |
| 7308 this.items.forEach(function(item) { | |
| 7309 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | |
| 7310 }, this); | |
| 7311 }, | |
| 7312 | |
| 7313 /** | |
| 7314 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
| 7315 * multi-selectable. | |
| 7316 * | |
| 7317 * @param {boolean} multi True if the menu should be multi-selectable. | |
| 7318 */ | |
| 7319 _updateMultiselectable: function(multi) { | |
| 7320 if (multi) { | |
| 7321 this.setAttribute('aria-multiselectable', 'true'); | |
| 7322 } else { | |
| 7323 this.removeAttribute('aria-multiselectable'); | |
| 7324 } | |
| 7325 }, | |
| 7326 | |
| 7327 /** | |
| 7328 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
| 7329 * menu (if there is a relevant item, and it is possible to focus it). | |
| 7330 * | |
| 7331 * @param {KeyboardEvent} event A KeyboardEvent. | |
| 7332 */ | |
| 7333 _focusWithKeyboardEvent: function(event) { | |
| 7334 for (var i = 0, item; item = this.items[i]; i++) { | |
| 7335 var attr = this.attrForItemTitle || 'textContent'; | |
| 7336 var title = item[attr] || item.getAttribute(attr); | |
| 7337 | |
| 7338 if (!item.hasAttribute('disabled') && title && | |
| 7339 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | |
| 7340 this._setFocusedItem(item); | |
| 7341 break; | |
| 7342 } | |
| 7343 } | |
| 7344 }, | |
| 7345 | |
| 7346 /** | |
| 7347 * Focuses the previous item (relative to the currently focused item) in the | |
| 7348 * menu, disabled items will be skipped. | |
| 7349 * Loop until length + 1 to handle case of single item in menu. | |
| 7350 */ | |
| 7351 _focusPrevious: function() { | |
| 7352 var length = this.items.length; | |
| 7353 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
| 7354 for (var i = 1; i < length + 1; i++) { | |
| 7355 var item = this.items[(curFocusIndex - i + length) % length]; | |
| 7356 if (!item.hasAttribute('disabled')) { | |
| 7357 this._setFocusedItem(item); | |
| 7358 return; | |
| 7359 } | |
| 7360 } | |
| 7361 }, | |
| 7362 | |
| 7363 /** | |
| 7364 * Focuses the next item (relative to the currently focused item) in the | |
| 7365 * menu, disabled items will be skipped. | |
| 7366 * Loop until length + 1 to handle case of single item in menu. | |
| 7367 */ | |
| 7368 _focusNext: function() { | |
| 7369 var length = this.items.length; | |
| 7370 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
| 7371 for (var i = 1; i < length + 1; i++) { | |
| 7372 var item = this.items[(curFocusIndex + i) % length]; | |
| 7373 if (!item.hasAttribute('disabled')) { | |
| 7374 this._setFocusedItem(item); | |
| 7375 return; | |
| 7376 } | |
| 7377 } | |
| 7378 }, | |
| 7379 | |
| 7380 /** | |
| 7381 * Mutates items in the menu based on provided selection details, so that | |
| 7382 * all items correctly reflect selection state. | |
| 7383 * | |
| 7384 * @param {Element} item An item in the menu. | |
| 7385 * @param {boolean} isSelected True if the item should be shown in a | |
| 7386 * selected state, otherwise false. | |
| 7387 */ | |
| 7388 _applySelection: function(item, isSelected) { | |
| 7389 if (isSelected) { | |
| 7390 item.setAttribute('aria-selected', 'true'); | |
| 7391 } else { | |
| 7392 item.removeAttribute('aria-selected'); | |
| 7393 } | |
| 7394 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | |
| 7395 }, | |
| 7396 | |
| 7397 /** | |
| 7398 * Discretely updates tabindex values among menu items as the focused item | |
| 7399 * changes. | |
| 7400 * | |
| 7401 * @param {Element} focusedItem The element that is currently focused. | |
| 7402 * @param {?Element} old The last element that was considered focused, if | |
| 7403 * applicable. | |
| 7404 */ | |
| 7405 _focusedItemChanged: function(focusedItem, old) { | |
| 7406 old && old.setAttribute('tabindex', '-1'); | |
| 7407 if (focusedItem) { | |
| 7408 focusedItem.setAttribute('tabindex', '0'); | |
| 7409 focusedItem.focus(); | |
| 7410 } | |
| 7411 }, | |
| 7412 | |
| 7413 /** | |
| 7414 * A handler that responds to mutation changes related to the list of items | |
| 7415 * in the menu. | |
| 7416 * | |
| 7417 * @param {CustomEvent} event An event containing mutation records as its | |
| 7418 * detail. | |
| 7419 */ | |
| 7420 _onIronItemsChanged: function(event) { | |
| 7421 if (event.detail.addedNodes.length) { | |
| 7422 this._resetTabindices(); | |
| 7423 } | |
| 7424 }, | |
| 7425 | |
| 7426 /** | |
| 7427 * Handler that is called when a shift+tab keypress is detected by the menu. | |
| 7428 * | |
| 7429 * @param {CustomEvent} event A key combination event. | |
| 7430 */ | |
| 7431 _onShiftTabDown: function(event) { | |
| 7432 var oldTabIndex = this.getAttribute('tabindex'); | |
| 7433 | |
| 7434 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
| 7435 | |
| 7436 this._setFocusedItem(null); | |
| 7437 | |
| 7438 this.setAttribute('tabindex', '-1'); | |
| 7439 | |
| 7440 this.async(function() { | |
| 7441 this.setAttribute('tabindex', oldTabIndex); | |
| 7442 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 7443 // NOTE(cdata): polymer/polymer#1305 | |
| 7444 }, 1); | |
| 7445 }, | |
| 7446 | |
| 7447 /** | |
| 7448 * Handler that is called when the menu receives focus. | |
| 7449 * | |
| 7450 * @param {FocusEvent} event A focus event. | |
| 7451 */ | |
| 7452 _onFocus: function(event) { | |
| 7453 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
| 7454 // do not focus the menu itself | |
| 7455 return; | |
| 7456 } | |
| 7457 | |
| 7458 // Do not focus the selected tab if the deepest target is part of the | |
| 7459 // menu element's local DOM and is focusable. | |
| 7460 var rootTarget = /** @type {?HTMLElement} */( | |
| 7461 Polymer.dom(event).rootTarget); | |
| 7462 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
| 7463 return; | |
| 7464 } | |
| 7465 | |
| 7466 // clear the cached focus item | |
| 7467 this._defaultFocusAsync = this.async(function() { | |
| 7468 // focus the selected item when the menu receives focus, or the first it
em | |
| 7469 // if no item is selected | |
| 7470 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
| 7471 | |
| 7472 this._setFocusedItem(null); | |
| 7473 | |
| 7474 if (selectedItem) { | |
| 7475 this._setFocusedItem(selectedItem); | |
| 7476 } else if (this.items[0]) { | |
| 7477 // We find the first none-disabled item (if one exists) | |
| 7478 this._focusNext(); | |
| 7479 } | |
| 7480 }); | |
| 7481 }, | |
| 7482 | |
| 7483 /** | |
| 7484 * Handler that is called when the up key is pressed. | |
| 7485 * | |
| 7486 * @param {CustomEvent} event A key combination event. | |
| 7487 */ | |
| 7488 _onUpKey: function(event) { | |
| 7489 // up and down arrows moves the focus | |
| 7490 this._focusPrevious(); | |
| 7491 event.detail.keyboardEvent.preventDefault(); | |
| 7492 }, | |
| 7493 | |
| 7494 /** | |
| 7495 * Handler that is called when the down key is pressed. | |
| 7496 * | |
| 7497 * @param {CustomEvent} event A key combination event. | |
| 7498 */ | |
| 7499 _onDownKey: function(event) { | |
| 7500 this._focusNext(); | |
| 7501 event.detail.keyboardEvent.preventDefault(); | |
| 7502 }, | |
| 7503 | |
| 7504 /** | |
| 7505 * Handler that is called when the esc key is pressed. | |
| 7506 * | |
| 7507 * @param {CustomEvent} event A key combination event. | |
| 7508 */ | |
| 7509 _onEscKey: function(event) { | |
| 7510 // esc blurs the control | |
| 7511 this.focusedItem.blur(); | |
| 7512 }, | |
| 7513 | |
| 7514 /** | |
| 7515 * Handler that is called when a keydown event is detected. | |
| 7516 * | |
| 7517 * @param {KeyboardEvent} event A keyboard event. | |
| 7518 */ | |
| 7519 _onKeydown: function(event) { | |
| 7520 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
| 7521 // all other keys focus the menu item starting with that character | |
| 7522 this._focusWithKeyboardEvent(event); | |
| 7523 } | |
| 7524 event.stopPropagation(); | |
| 7525 }, | |
| 7526 | |
| 7527 // override _activateHandler | |
| 7528 _activateHandler: function(event) { | |
| 7529 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
| 7530 event.stopPropagation(); | |
| 7531 } | |
| 7532 }; | |
| 7533 | |
| 7534 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
| 7535 | |
| 7536 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
| 7537 Polymer.IronMenuBehavior = [ | |
| 7538 Polymer.IronMultiSelectableBehavior, | |
| 7539 Polymer.IronA11yKeysBehavior, | |
| 7540 Polymer.IronMenuBehaviorImpl | |
| 7541 ]; | |
| 7542 (function() { | |
| 7543 Polymer({ | |
| 7544 is: 'paper-menu', | |
| 7545 | |
| 7546 behaviors: [ | |
| 7547 Polymer.IronMenuBehavior | |
| 7548 ] | |
| 7549 }); | |
| 7550 })(); | |
| 7551 /** | 7766 /** |
| 7552 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | 7767 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and |
| 7553 optionally centers it in the window or another element. | 7768 optionally centers it in the window or another element. |
| 7554 | 7769 |
| 7555 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | 7770 The element will only be sized and/or positioned if it has not already been size
d and/or positioned |
| 7556 by CSS. | 7771 by CSS. |
| 7557 | 7772 |
| 7558 CSS properties | Action | 7773 CSS properties | Action |
| 7559 -----------------------------|------------------------------------------- | 7774 -----------------------------|------------------------------------------- |
| 7560 `position` set | Element is not centered horizontally or verticall
y | 7775 `position` set | Element is not centered horizontally or verticall
y |
| (...skipping 2734 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 10295 height: height + 'px', | 10510 height: height + 'px', |
| 10296 transform: 'translateY(0)' | 10511 transform: 'translateY(0)' |
| 10297 }, { | 10512 }, { |
| 10298 height: height / 2 + 'px', | 10513 height: height / 2 + 'px', |
| 10299 transform: 'translateY(-20px)' | 10514 transform: 'translateY(-20px)' |
| 10300 }], this.timingFromConfig(config)); | 10515 }], this.timingFromConfig(config)); |
| 10301 | 10516 |
| 10302 return this._effect; | 10517 return this._effect; |
| 10303 } | 10518 } |
| 10304 }); | 10519 }); |
| 10305 (function() { | 10520 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 10306 'use strict'; | 10521 // Use of this source code is governed by a BSD-style license that can be |
| 10307 | 10522 // found in the LICENSE file. |
| 10308 var config = { | 10523 |
| 10309 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | 10524 /** Same as paper-menu-button's custom easing cubic-bezier param. */ |
| 10310 MAX_ANIMATION_TIME_MS: 400 | 10525 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
| 10311 }; | 10526 |
| 10312 | 10527 Polymer({ |
| 10313 var PaperMenuButton = Polymer({ | 10528 is: 'cr-shared-menu', |
| 10314 is: 'paper-menu-button', | 10529 |
| 10315 | 10530 behaviors: [Polymer.IronA11yKeysBehavior], |
| 10316 /** | 10531 |
| 10317 * Fired when the dropdown opens. | 10532 properties: { |
| 10318 * | 10533 menuOpen: { |
| 10319 * @event paper-dropdown-open | 10534 type: Boolean, |
| 10320 */ | 10535 observer: 'menuOpenChanged_', |
| 10321 | 10536 value: false, |
| 10322 /** | 10537 }, |
| 10323 * Fired when the dropdown closes. | 10538 |
| 10324 * | 10539 /** |
| 10325 * @event paper-dropdown-close | 10540 * The contextual item that this menu was clicked for. |
| 10326 */ | 10541 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> |
| 10327 | 10542 * @type {?Object} |
| 10328 behaviors: [ | 10543 */ |
| 10329 Polymer.IronA11yKeysBehavior, | 10544 itemData: { |
| 10330 Polymer.IronControlState | 10545 type: Object, |
| 10331 ], | 10546 value: null, |
| 10332 | 10547 }, |
| 10333 properties: { | 10548 |
| 10334 /** | 10549 /** @override */ |
| 10335 * True if the content is currently displayed. | 10550 keyEventTarget: { |
| 10336 */ | 10551 type: Object, |
| 10337 opened: { | 10552 value: function() { |
| 10338 type: Boolean, | 10553 return this.$.menu; |
| 10339 value: false, | 10554 } |
| 10340 notify: true, | 10555 }, |
| 10341 observer: '_openedChanged' | 10556 |
| 10342 }, | 10557 openAnimationConfig: { |
| 10343 | 10558 type: Object, |
| 10344 /** | 10559 value: function() { |
| 10345 * The orientation against which to align the menu dropdown | 10560 return [{ |
| 10346 * horizontally relative to the dropdown trigger. | 10561 name: 'fade-in-animation', |
| 10347 */ | 10562 timing: { |
| 10348 horizontalAlign: { | 10563 delay: 50, |
| 10349 type: String, | 10564 duration: 200 |
| 10350 value: 'left', | |
| 10351 reflectToAttribute: true | |
| 10352 }, | |
| 10353 | |
| 10354 /** | |
| 10355 * The orientation against which to align the menu dropdown | |
| 10356 * vertically relative to the dropdown trigger. | |
| 10357 */ | |
| 10358 verticalAlign: { | |
| 10359 type: String, | |
| 10360 value: 'top', | |
| 10361 reflectToAttribute: true | |
| 10362 }, | |
| 10363 | |
| 10364 /** | |
| 10365 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
| 10366 * be considered preferences instead of strict requirements when | |
| 10367 * positioning the dropdown and may be changed if doing so reduces | |
| 10368 * the area of the dropdown falling outside of `fitInto`. | |
| 10369 */ | |
| 10370 dynamicAlign: { | |
| 10371 type: Boolean | |
| 10372 }, | |
| 10373 | |
| 10374 /** | |
| 10375 * A pixel value that will be added to the position calculated for the | |
| 10376 * given `horizontalAlign`. Use a negative value to offset to the | |
| 10377 * left, or a positive value to offset to the right. | |
| 10378 */ | |
| 10379 horizontalOffset: { | |
| 10380 type: Number, | |
| 10381 value: 0, | |
| 10382 notify: true | |
| 10383 }, | |
| 10384 | |
| 10385 /** | |
| 10386 * A pixel value that will be added to the position calculated for the | |
| 10387 * given `verticalAlign`. Use a negative value to offset towards the | |
| 10388 * top, or a positive value to offset towards the bottom. | |
| 10389 */ | |
| 10390 verticalOffset: { | |
| 10391 type: Number, | |
| 10392 value: 0, | |
| 10393 notify: true | |
| 10394 }, | |
| 10395 | |
| 10396 /** | |
| 10397 * If true, the dropdown will be positioned so that it doesn't overlap | |
| 10398 * the button. | |
| 10399 */ | |
| 10400 noOverlap: { | |
| 10401 type: Boolean | |
| 10402 }, | |
| 10403 | |
| 10404 /** | |
| 10405 * Set to true to disable animations when opening and closing the | |
| 10406 * dropdown. | |
| 10407 */ | |
| 10408 noAnimations: { | |
| 10409 type: Boolean, | |
| 10410 value: false | |
| 10411 }, | |
| 10412 | |
| 10413 /** | |
| 10414 * Set to true to disable automatically closing the dropdown after | |
| 10415 * a selection has been made. | |
| 10416 */ | |
| 10417 ignoreSelect: { | |
| 10418 type: Boolean, | |
| 10419 value: false | |
| 10420 }, | |
| 10421 | |
| 10422 /** | |
| 10423 * Set to true to enable automatically closing the dropdown after an | |
| 10424 * item has been activated, even if the selection did not change. | |
| 10425 */ | |
| 10426 closeOnActivate: { | |
| 10427 type: Boolean, | |
| 10428 value: false | |
| 10429 }, | |
| 10430 | |
| 10431 /** | |
| 10432 * An animation config. If provided, this will be used to animate the | |
| 10433 * opening of the dropdown. | |
| 10434 */ | |
| 10435 openAnimationConfig: { | |
| 10436 type: Object, | |
| 10437 value: function() { | |
| 10438 return [{ | |
| 10439 name: 'fade-in-animation', | |
| 10440 timing: { | |
| 10441 delay: 100, | |
| 10442 duration: 200 | |
| 10443 } | |
| 10444 }, { | |
| 10445 name: 'paper-menu-grow-width-animation', | |
| 10446 timing: { | |
| 10447 delay: 100, | |
| 10448 duration: 150, | |
| 10449 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10450 } | |
| 10451 }, { | |
| 10452 name: 'paper-menu-grow-height-animation', | |
| 10453 timing: { | |
| 10454 delay: 100, | |
| 10455 duration: 275, | |
| 10456 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10457 } | |
| 10458 }]; | |
| 10459 } | |
| 10460 }, | |
| 10461 | |
| 10462 /** | |
| 10463 * An animation config. If provided, this will be used to animate the | |
| 10464 * closing of the dropdown. | |
| 10465 */ | |
| 10466 closeAnimationConfig: { | |
| 10467 type: Object, | |
| 10468 value: function() { | |
| 10469 return [{ | |
| 10470 name: 'fade-out-animation', | |
| 10471 timing: { | |
| 10472 duration: 150 | |
| 10473 } | |
| 10474 }, { | |
| 10475 name: 'paper-menu-shrink-width-animation', | |
| 10476 timing: { | |
| 10477 delay: 100, | |
| 10478 duration: 50, | |
| 10479 easing: config.ANIMATION_CUBIC_BEZIER | |
| 10480 } | |
| 10481 }, { | |
| 10482 name: 'paper-menu-shrink-height-animation', | |
| 10483 timing: { | |
| 10484 duration: 200, | |
| 10485 easing: 'ease-in' | |
| 10486 } | |
| 10487 }]; | |
| 10488 } | |
| 10489 }, | |
| 10490 | |
| 10491 /** | |
| 10492 * By default, the dropdown will constrain scrolling on the page | |
| 10493 * to itself when opened. | |
| 10494 * Set to true in order to prevent scroll from being constrained | |
| 10495 * to the dropdown when it opens. | |
| 10496 */ | |
| 10497 allowOutsideScroll: { | |
| 10498 type: Boolean, | |
| 10499 value: false | |
| 10500 }, | |
| 10501 | |
| 10502 /** | |
| 10503 * Whether focus should be restored to the button when the menu closes
. | |
| 10504 */ | |
| 10505 restoreFocusOnClose: { | |
| 10506 type: Boolean, | |
| 10507 value: true | |
| 10508 }, | |
| 10509 | |
| 10510 /** | |
| 10511 * This is the element intended to be bound as the focus target | |
| 10512 * for the `iron-dropdown` contained by `paper-menu-button`. | |
| 10513 */ | |
| 10514 _dropdownContent: { | |
| 10515 type: Object | |
| 10516 } | 10565 } |
| 10517 }, | 10566 }, { |
| 10518 | 10567 name: 'paper-menu-grow-width-animation', |
| 10519 hostAttributes: { | 10568 timing: { |
| 10520 role: 'group', | 10569 delay: 50, |
| 10521 'aria-haspopup': 'true' | 10570 duration: 150, |
| 10522 }, | 10571 easing: SLIDE_CUBIC_BEZIER |
| 10523 | |
| 10524 listeners: { | |
| 10525 'iron-activate': '_onIronActivate', | |
| 10526 'iron-select': '_onIronSelect' | |
| 10527 }, | |
| 10528 | |
| 10529 /** | |
| 10530 * The content element that is contained by the menu button, if any. | |
| 10531 */ | |
| 10532 get contentElement() { | |
| 10533 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
| 10534 }, | |
| 10535 | |
| 10536 /** | |
| 10537 * Toggles the drowpdown content between opened and closed. | |
| 10538 */ | |
| 10539 toggle: function() { | |
| 10540 if (this.opened) { | |
| 10541 this.close(); | |
| 10542 } else { | |
| 10543 this.open(); | |
| 10544 } | 10572 } |
| 10545 }, | 10573 }, { |
| 10546 | 10574 name: 'paper-menu-grow-height-animation', |
| 10547 /** | 10575 timing: { |
| 10548 * Make the dropdown content appear as an overlay positioned relative | 10576 delay: 100, |
| 10549 * to the dropdown trigger. | 10577 duration: 275, |
| 10550 */ | 10578 easing: SLIDE_CUBIC_BEZIER |
| 10551 open: function() { | |
| 10552 if (this.disabled) { | |
| 10553 return; | |
| 10554 } | 10579 } |
| 10555 | 10580 }]; |
| 10556 this.$.dropdown.open(); | 10581 } |
| 10557 }, | 10582 }, |
| 10558 | 10583 |
| 10559 /** | 10584 closeAnimationConfig: { |
| 10560 * Hide the dropdown content. | 10585 type: Object, |
| 10561 */ | 10586 value: function() { |
| 10562 close: function() { | 10587 return [{ |
| 10563 this.$.dropdown.close(); | 10588 name: 'fade-out-animation', |
| 10564 }, | 10589 timing: { |
| 10565 | 10590 duration: 150 |
| 10566 /** | |
| 10567 * When an `iron-select` event is received, the dropdown should | |
| 10568 * automatically close on the assumption that a value has been chosen. | |
| 10569 * | |
| 10570 * @param {CustomEvent} event A CustomEvent instance with type | |
| 10571 * set to `"iron-select"`. | |
| 10572 */ | |
| 10573 _onIronSelect: function(event) { | |
| 10574 if (!this.ignoreSelect) { | |
| 10575 this.close(); | |
| 10576 } | 10591 } |
| 10577 }, | 10592 }]; |
| 10578 | 10593 } |
| 10579 /** | 10594 } |
| 10580 * Closes the dropdown when an `iron-activate` event is received if | 10595 }, |
| 10581 * `closeOnActivate` is true. | 10596 |
| 10582 * | 10597 keyBindings: { |
| 10583 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | 10598 'tab': 'onTabPressed_', |
| 10584 */ | 10599 }, |
| 10585 _onIronActivate: function(event) { | 10600 |
| 10586 if (this.closeOnActivate) { | 10601 listeners: { |
| 10587 this.close(); | 10602 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', |
| 10588 } | 10603 }, |
| 10589 }, | 10604 |
| 10590 | 10605 /** |
| 10591 /** | 10606 * The last anchor that was used to open a menu. It's necessary for toggling. |
| 10592 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | 10607 * @private {?Element} |
| 10593 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | 10608 */ |
| 10594 * | 10609 lastAnchor_: null, |
| 10595 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | 10610 |
| 10596 * @param {boolean} oldOpened The previous value of `opened`. | 10611 /** |
| 10597 */ | 10612 * The first focusable child in the menu's light DOM. |
| 10598 _openedChanged: function(opened, oldOpened) { | 10613 * @private {?Element} |
| 10599 if (opened) { | 10614 */ |
| 10600 // TODO(cdata): Update this when we can measure changes in distribut
ed | 10615 firstFocus_: null, |
| 10601 // children in an idiomatic way. | 10616 |
| 10602 // We poke this property in case the element has changed. This will | 10617 /** |
| 10603 // cause the focus target for the `iron-dropdown` to be updated as | 10618 * The last focusable child in the menu's light DOM. |
| 10604 // necessary: | 10619 * @private {?Element} |
| 10605 this._dropdownContent = this.contentElement; | 10620 */ |
| 10606 this.fire('paper-dropdown-open'); | 10621 lastFocus_: null, |
| 10607 } else if (oldOpened != null) { | 10622 |
| 10608 this.fire('paper-dropdown-close'); | 10623 /** @override */ |
| 10609 } | 10624 attached: function() { |
| 10610 }, | 10625 window.addEventListener('resize', this.closeMenu.bind(this)); |
| 10611 | 10626 }, |
| 10612 /** | 10627 |
| 10613 * If the dropdown is open when disabled becomes true, close the | 10628 /** Closes the menu. */ |
| 10614 * dropdown. | 10629 closeMenu: function() { |
| 10615 * | 10630 if (this.root.activeElement == null) { |
| 10616 * @param {boolean} disabled True if disabled, otherwise false. | 10631 // Something else has taken focus away from the menu. Do not attempt to |
| 10617 */ | 10632 // restore focus to the button which opened the menu. |
| 10618 _disabledChanged: function(disabled) { | 10633 this.$.dropdown.restoreFocusOnClose = false; |
| 10619 Polymer.IronControlState._disabledChanged.apply(this, arguments); | 10634 } |
| 10620 if (disabled && this.opened) { | 10635 this.menuOpen = false; |
| 10621 this.close(); | 10636 }, |
| 10622 } | 10637 |
| 10623 }, | 10638 /** |
| 10624 | 10639 * Opens the menu at the anchor location. |
| 10625 __onIronOverlayCanceled: function(event) { | 10640 * @param {!Element} anchor The location to display the menu. |
| 10626 var uiEvent = event.detail; | 10641 * @param {!Object} itemData The contextual item's data. |
| 10627 var target = Polymer.dom(uiEvent).rootTarget; | 10642 */ |
| 10628 var trigger = this.$.trigger; | 10643 openMenu: function(anchor, itemData) { |
| 10629 var path = Polymer.dom(uiEvent).path; | 10644 if (this.lastAnchor_ == anchor && this.menuOpen) |
| 10630 | 10645 return; |
| 10631 if (path.indexOf(trigger) > -1) { | 10646 |
| 10632 event.preventDefault(); | 10647 if (this.menuOpen) |
| 10633 } | 10648 this.closeMenu(); |
| 10634 } | 10649 |
| 10635 }); | 10650 this.itemData = itemData; |
| 10636 | 10651 this.lastAnchor_ = anchor; |
| 10637 Object.keys(config).forEach(function (key) { | 10652 this.$.dropdown.restoreFocusOnClose = true; |
| 10638 PaperMenuButton[key] = config[key]; | 10653 |
| 10639 }); | 10654 var focusableChildren = Polymer.dom(this).querySelectorAll( |
| 10640 | 10655 '[tabindex]:not([hidden]),button:not([hidden])'); |
| 10641 Polymer.PaperMenuButton = PaperMenuButton; | 10656 if (focusableChildren.length > 0) { |
| 10642 })(); | 10657 this.$.dropdown.focusTarget = focusableChildren[0]; |
| 10643 /** | 10658 this.firstFocus_ = focusableChildren[0]; |
| 10644 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | 10659 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
| 10645 * | 10660 } |
| 10646 * @polymerBehavior Polymer.PaperInkyFocusBehavior | 10661 |
| 10647 */ | 10662 // Move the menu to the anchor. |
| 10648 Polymer.PaperInkyFocusBehaviorImpl = { | 10663 this.$.dropdown.positionTarget = anchor; |
| 10649 observers: [ | 10664 this.menuOpen = true; |
| 10650 '_focusedChanged(receivedFocusFromKeyboard)' | 10665 }, |
| 10651 ], | 10666 |
| 10652 | 10667 /** |
| 10653 _focusedChanged: function(receivedFocusFromKeyboard) { | 10668 * Toggles the menu for the anchor that is passed in. |
| 10654 if (receivedFocusFromKeyboard) { | 10669 * @param {!Element} anchor The location to display the menu. |
| 10655 this.ensureRipple(); | 10670 * @param {!Object} itemData The contextual item's data. |
| 10656 } | 10671 */ |
| 10657 if (this.hasRipple()) { | 10672 toggleMenu: function(anchor, itemData) { |
| 10658 this._ripple.holdDown = receivedFocusFromKeyboard; | 10673 if (anchor == this.lastAnchor_ && this.menuOpen) |
| 10659 } | 10674 this.closeMenu(); |
| 10660 }, | 10675 else |
| 10661 | 10676 this.openMenu(anchor, itemData); |
| 10662 _createRipple: function() { | 10677 }, |
| 10663 var ripple = Polymer.PaperRippleBehavior._createRipple(); | 10678 |
| 10664 ripple.id = 'ink'; | 10679 /** |
| 10665 ripple.setAttribute('center', ''); | 10680 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from |
| 10666 ripple.classList.add('circle'); | 10681 * the first element with a nonzero tabindex to the last such element. |
| 10667 return ripple; | 10682 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available |
| 10683 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). |
| 10684 * @param {CustomEvent} e |
| 10685 */ |
| 10686 onTabPressed_: function(e) { |
| 10687 if (!this.firstFocus_ || !this.lastFocus_) |
| 10688 return; |
| 10689 |
| 10690 var toFocus; |
| 10691 var keyEvent = e.detail.keyboardEvent; |
| 10692 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) |
| 10693 toFocus = this.lastFocus_; |
| 10694 else if (keyEvent.target == this.lastFocus_) |
| 10695 toFocus = this.firstFocus_; |
| 10696 |
| 10697 if (!toFocus) |
| 10698 return; |
| 10699 |
| 10700 e.preventDefault(); |
| 10701 toFocus.focus(); |
| 10702 }, |
| 10703 |
| 10704 /** |
| 10705 * Ensure the menu is reset properly when it is closed by the dropdown (eg, |
| 10706 * clicking outside). |
| 10707 * @private |
| 10708 */ |
| 10709 menuOpenChanged_: function() { |
| 10710 if (!this.menuOpen) { |
| 10711 this.itemData = null; |
| 10712 this.lastAnchor_ = null; |
| 10713 } |
| 10714 }, |
| 10715 |
| 10716 /** |
| 10717 * Prevent focus restoring when tapping outside the menu. This stops the |
| 10718 * focus moving around unexpectedly when closing the menu with the mouse. |
| 10719 * @param {CustomEvent} e |
| 10720 * @private |
| 10721 */ |
| 10722 onOverlayCanceled_: function(e) { |
| 10723 if (e.detail.type == 'tap') |
| 10724 this.$.dropdown.restoreFocusOnClose = false; |
| 10725 }, |
| 10726 }); |
| 10727 /** @polymerBehavior Polymer.PaperItemBehavior */ |
| 10728 Polymer.PaperItemBehaviorImpl = { |
| 10729 hostAttributes: { |
| 10730 role: 'option', |
| 10731 tabindex: '0' |
| 10668 } | 10732 } |
| 10669 }; | 10733 }; |
| 10670 | 10734 |
| 10671 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | 10735 /** @polymerBehavior */ |
| 10672 Polymer.PaperInkyFocusBehavior = [ | 10736 Polymer.PaperItemBehavior = [ |
| 10673 Polymer.IronButtonState, | 10737 Polymer.IronButtonState, |
| 10674 Polymer.IronControlState, | 10738 Polymer.IronControlState, |
| 10675 Polymer.PaperRippleBehavior, | 10739 Polymer.PaperItemBehaviorImpl |
| 10676 Polymer.PaperInkyFocusBehaviorImpl | |
| 10677 ]; | 10740 ]; |
| 10678 Polymer({ | 10741 Polymer({ |
| 10679 is: 'paper-icon-button', | 10742 is: 'paper-item', |
| 10743 |
| 10744 behaviors: [ |
| 10745 Polymer.PaperItemBehavior |
| 10746 ] |
| 10747 }); |
| 10748 Polymer({ |
| 10749 |
| 10750 is: 'iron-collapse', |
| 10751 |
| 10752 behaviors: [ |
| 10753 Polymer.IronResizableBehavior |
| 10754 ], |
| 10755 |
| 10756 properties: { |
| 10757 |
| 10758 /** |
| 10759 * If true, the orientation is horizontal; otherwise is vertical. |
| 10760 * |
| 10761 * @attribute horizontal |
| 10762 */ |
| 10763 horizontal: { |
| 10764 type: Boolean, |
| 10765 value: false, |
| 10766 observer: '_horizontalChanged' |
| 10767 }, |
| 10768 |
| 10769 /** |
| 10770 * Set opened to true to show the collapse element and to false to hide it
. |
| 10771 * |
| 10772 * @attribute opened |
| 10773 */ |
| 10774 opened: { |
| 10775 type: Boolean, |
| 10776 value: false, |
| 10777 notify: true, |
| 10778 observer: '_openedChanged' |
| 10779 }, |
| 10780 |
| 10781 /** |
| 10782 * Set noAnimation to true to disable animations |
| 10783 * |
| 10784 * @attribute noAnimation |
| 10785 */ |
| 10786 noAnimation: { |
| 10787 type: Boolean |
| 10788 }, |
| 10789 |
| 10790 }, |
| 10791 |
| 10792 get dimension() { |
| 10793 return this.horizontal ? 'width' : 'height'; |
| 10794 }, |
| 10795 |
| 10796 /** |
| 10797 * `maxWidth` or `maxHeight`. |
| 10798 * @private |
| 10799 */ |
| 10800 get _dimensionMax() { |
| 10801 return this.horizontal ? 'maxWidth' : 'maxHeight'; |
| 10802 }, |
| 10803 |
| 10804 /** |
| 10805 * `max-width` or `max-height`. |
| 10806 * @private |
| 10807 */ |
| 10808 get _dimensionMaxCss() { |
| 10809 return this.horizontal ? 'max-width' : 'max-height'; |
| 10810 }, |
| 10811 |
| 10812 hostAttributes: { |
| 10813 role: 'group', |
| 10814 'aria-hidden': 'true', |
| 10815 'aria-expanded': 'false' |
| 10816 }, |
| 10817 |
| 10818 listeners: { |
| 10819 transitionend: '_transitionEnd' |
| 10820 }, |
| 10821 |
| 10822 attached: function() { |
| 10823 // It will take care of setting correct classes and styles. |
| 10824 this._transitionEnd(); |
| 10825 }, |
| 10826 |
| 10827 /** |
| 10828 * Toggle the opened state. |
| 10829 * |
| 10830 * @method toggle |
| 10831 */ |
| 10832 toggle: function() { |
| 10833 this.opened = !this.opened; |
| 10834 }, |
| 10835 |
| 10836 show: function() { |
| 10837 this.opened = true; |
| 10838 }, |
| 10839 |
| 10840 hide: function() { |
| 10841 this.opened = false; |
| 10842 }, |
| 10843 |
| 10844 /** |
| 10845 * Updates the size of the element. |
| 10846 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop
erty value, usually `auto` or `0px`. |
| 10847 * @param {boolean=} animated if `true` updates the size with an animation,
otherwise without. |
| 10848 */ |
| 10849 updateSize: function(size, animated) { |
| 10850 // No change! |
| 10851 var curSize = this.style[this._dimensionMax]; |
| 10852 if (curSize === size || (size === 'auto' && !curSize)) { |
| 10853 return; |
| 10854 } |
| 10855 |
| 10856 this._updateTransition(false); |
| 10857 // If we can animate, must do some prep work. |
| 10858 if (animated && !this.noAnimation && this._isDisplayed) { |
| 10859 // Animation will start at the current size. |
| 10860 var startSize = this._calcSize(); |
| 10861 // For `auto` we must calculate what is the final size for the animation
. |
| 10862 // After the transition is done, _transitionEnd will set the size back t
o `auto`. |
| 10863 if (size === 'auto') { |
| 10864 this.style[this._dimensionMax] = ''; |
| 10865 size = this._calcSize(); |
| 10866 } |
| 10867 // Go to startSize without animation. |
| 10868 this.style[this._dimensionMax] = startSize; |
| 10869 // Force layout to ensure transition will go. Set scrollTop to itself |
| 10870 // so that compilers won't remove it. |
| 10871 this.scrollTop = this.scrollTop; |
| 10872 // Enable animation. |
| 10873 this._updateTransition(true); |
| 10874 } |
| 10875 // Set the final size. |
| 10876 if (size === 'auto') { |
| 10877 this.style[this._dimensionMax] = ''; |
| 10878 } else { |
| 10879 this.style[this._dimensionMax] = size; |
| 10880 } |
| 10881 }, |
| 10882 |
| 10883 /** |
| 10884 * enableTransition() is deprecated, but left over so it doesn't break exist
ing code. |
| 10885 * Please use `noAnimation` property instead. |
| 10886 * |
| 10887 * @method enableTransition |
| 10888 * @deprecated since version 1.0.4 |
| 10889 */ |
| 10890 enableTransition: function(enabled) { |
| 10891 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation`
instead.'); |
| 10892 this.noAnimation = !enabled; |
| 10893 }, |
| 10894 |
| 10895 _updateTransition: function(enabled) { |
| 10896 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s'
; |
| 10897 }, |
| 10898 |
| 10899 _horizontalChanged: function() { |
| 10900 this.style.transitionProperty = this._dimensionMaxCss; |
| 10901 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma
xWidth'; |
| 10902 this.style[otherDimension] = ''; |
| 10903 this.updateSize(this.opened ? 'auto' : '0px', false); |
| 10904 }, |
| 10905 |
| 10906 _openedChanged: function() { |
| 10907 this.setAttribute('aria-expanded', this.opened); |
| 10908 this.setAttribute('aria-hidden', !this.opened); |
| 10909 |
| 10910 this.toggleClass('iron-collapse-closed', false); |
| 10911 this.toggleClass('iron-collapse-opened', false); |
| 10912 this.updateSize(this.opened ? 'auto' : '0px', true); |
| 10913 |
| 10914 // Focus the current collapse. |
| 10915 if (this.opened) { |
| 10916 this.focus(); |
| 10917 } |
| 10918 if (this.noAnimation) { |
| 10919 this._transitionEnd(); |
| 10920 } |
| 10921 }, |
| 10922 |
| 10923 _transitionEnd: function() { |
| 10924 if (this.opened) { |
| 10925 this.style[this._dimensionMax] = ''; |
| 10926 } |
| 10927 this.toggleClass('iron-collapse-closed', !this.opened); |
| 10928 this.toggleClass('iron-collapse-opened', this.opened); |
| 10929 this._updateTransition(false); |
| 10930 this.notifyResize(); |
| 10931 }, |
| 10932 |
| 10933 /** |
| 10934 * Simplistic heuristic to detect if element has a parent with display: none |
| 10935 * |
| 10936 * @private |
| 10937 */ |
| 10938 get _isDisplayed() { |
| 10939 var rect = this.getBoundingClientRect(); |
| 10940 for (var prop in rect) { |
| 10941 if (rect[prop] !== 0) return true; |
| 10942 } |
| 10943 return false; |
| 10944 }, |
| 10945 |
| 10946 _calcSize: function() { |
| 10947 return this.getBoundingClientRect()[this.dimension] + 'px'; |
| 10948 } |
| 10949 |
| 10950 }); |
| 10951 /** |
| 10952 Polymer.IronFormElementBehavior enables a custom element to be included |
| 10953 in an `iron-form`. |
| 10954 |
| 10955 @demo demo/index.html |
| 10956 @polymerBehavior |
| 10957 */ |
| 10958 Polymer.IronFormElementBehavior = { |
| 10959 |
| 10960 properties: { |
| 10961 /** |
| 10962 * Fired when the element is added to an `iron-form`. |
| 10963 * |
| 10964 * @event iron-form-element-register |
| 10965 */ |
| 10966 |
| 10967 /** |
| 10968 * Fired when the element is removed from an `iron-form`. |
| 10969 * |
| 10970 * @event iron-form-element-unregister |
| 10971 */ |
| 10972 |
| 10973 /** |
| 10974 * The name of this element. |
| 10975 */ |
| 10976 name: { |
| 10977 type: String |
| 10978 }, |
| 10979 |
| 10980 /** |
| 10981 * The value for this element. |
| 10982 */ |
| 10983 value: { |
| 10984 notify: true, |
| 10985 type: String |
| 10986 }, |
| 10987 |
| 10988 /** |
| 10989 * Set to true to mark the input as required. If used in a form, a |
| 10990 * custom element that uses this behavior should also use |
| 10991 * Polymer.IronValidatableBehavior and define a custom validation method. |
| 10992 * Otherwise, a `required` element will always be considered valid. |
| 10993 * It's also strongly recommended to provide a visual style for the elemen
t |
| 10994 * when its value is invalid. |
| 10995 */ |
| 10996 required: { |
| 10997 type: Boolean, |
| 10998 value: false |
| 10999 }, |
| 11000 |
| 11001 /** |
| 11002 * The form that the element is registered to. |
| 11003 */ |
| 11004 _parentForm: { |
| 11005 type: Object |
| 11006 } |
| 11007 }, |
| 11008 |
| 11009 attached: function() { |
| 11010 // Note: the iron-form that this element belongs to will set this |
| 11011 // element's _parentForm property when handling this event. |
| 11012 this.fire('iron-form-element-register'); |
| 11013 }, |
| 11014 |
| 11015 detached: function() { |
| 11016 if (this._parentForm) { |
| 11017 this._parentForm.fire('iron-form-element-unregister', {target: this}); |
| 11018 } |
| 11019 } |
| 11020 |
| 11021 }; |
| 11022 /** |
| 11023 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element |
| 11024 * that has a `checked` property, which can be used for validation if the |
| 11025 * element is also `required`. Element instances implementing this behavior |
| 11026 * will also be registered for use in an `iron-form` element. |
| 11027 * |
| 11028 * @demo demo/index.html |
| 11029 * @polymerBehavior Polymer.IronCheckedElementBehavior |
| 11030 */ |
| 11031 Polymer.IronCheckedElementBehaviorImpl = { |
| 11032 |
| 11033 properties: { |
| 11034 /** |
| 11035 * Fired when the checked state changes. |
| 11036 * |
| 11037 * @event iron-change |
| 11038 */ |
| 11039 |
| 11040 /** |
| 11041 * Gets or sets the state, `true` is checked and `false` is unchecked. |
| 11042 */ |
| 11043 checked: { |
| 11044 type: Boolean, |
| 11045 value: false, |
| 11046 reflectToAttribute: true, |
| 11047 notify: true, |
| 11048 observer: '_checkedChanged' |
| 11049 }, |
| 11050 |
| 11051 /** |
| 11052 * If true, the button toggles the active state with each tap or press |
| 11053 * of the spacebar. |
| 11054 */ |
| 11055 toggles: { |
| 11056 type: Boolean, |
| 11057 value: true, |
| 11058 reflectToAttribute: true |
| 11059 }, |
| 11060 |
| 11061 /* Overriden from Polymer.IronFormElementBehavior */ |
| 11062 value: { |
| 11063 type: String, |
| 11064 value: 'on', |
| 11065 observer: '_valueChanged' |
| 11066 } |
| 11067 }, |
| 11068 |
| 11069 observers: [ |
| 11070 '_requiredChanged(required)' |
| 11071 ], |
| 11072 |
| 11073 created: function() { |
| 11074 // Used by `iron-form` to handle the case that an element with this behavi
or |
| 11075 // doesn't have a role of 'checkbox' or 'radio', but should still only be |
| 11076 // included when the form is serialized if `this.checked === true`. |
| 11077 this._hasIronCheckedElementBehavior = true; |
| 11078 }, |
| 11079 |
| 11080 /** |
| 11081 * Returns false if the element is required and not checked, and true otherw
ise. |
| 11082 * @param {*=} _value Ignored. |
| 11083 * @return {boolean} true if `required` is false or if `checked` is true. |
| 11084 */ |
| 11085 _getValidity: function(_value) { |
| 11086 return this.disabled || !this.required || this.checked; |
| 11087 }, |
| 11088 |
| 11089 /** |
| 11090 * Update the aria-required label when `required` is changed. |
| 11091 */ |
| 11092 _requiredChanged: function() { |
| 11093 if (this.required) { |
| 11094 this.setAttribute('aria-required', 'true'); |
| 11095 } else { |
| 11096 this.removeAttribute('aria-required'); |
| 11097 } |
| 11098 }, |
| 11099 |
| 11100 /** |
| 11101 * Fire `iron-changed` when the checked state changes. |
| 11102 */ |
| 11103 _checkedChanged: function() { |
| 11104 this.active = this.checked; |
| 11105 this.fire('iron-change'); |
| 11106 }, |
| 11107 |
| 11108 /** |
| 11109 * Reset value to 'on' if it is set to `undefined`. |
| 11110 */ |
| 11111 _valueChanged: function() { |
| 11112 if (this.value === undefined || this.value === null) { |
| 11113 this.value = 'on'; |
| 11114 } |
| 11115 } |
| 11116 }; |
| 11117 |
| 11118 /** @polymerBehavior Polymer.IronCheckedElementBehavior */ |
| 11119 Polymer.IronCheckedElementBehavior = [ |
| 11120 Polymer.IronFormElementBehavior, |
| 11121 Polymer.IronValidatableBehavior, |
| 11122 Polymer.IronCheckedElementBehaviorImpl |
| 11123 ]; |
| 11124 /** |
| 11125 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element |
| 11126 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio
r` |
| 11127 * and is compatible with having a ripple effect. |
| 11128 * @polymerBehavior Polymer.PaperCheckedElementBehavior |
| 11129 */ |
| 11130 Polymer.PaperCheckedElementBehaviorImpl = { |
| 11131 /** |
| 11132 * Synchronizes the element's checked state with its ripple effect. |
| 11133 */ |
| 11134 _checkedChanged: function() { |
| 11135 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); |
| 11136 if (this.hasRipple()) { |
| 11137 if (this.checked) { |
| 11138 this._ripple.setAttribute('checked', ''); |
| 11139 } else { |
| 11140 this._ripple.removeAttribute('checked'); |
| 11141 } |
| 11142 } |
| 11143 }, |
| 11144 |
| 11145 /** |
| 11146 * Synchronizes the element's `active` and `checked` state. |
| 11147 */ |
| 11148 _buttonStateChanged: function() { |
| 11149 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); |
| 11150 if (this.disabled) { |
| 11151 return; |
| 11152 } |
| 11153 if (this.isAttached) { |
| 11154 this.checked = this.active; |
| 11155 } |
| 11156 } |
| 11157 }; |
| 11158 |
| 11159 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */ |
| 11160 Polymer.PaperCheckedElementBehavior = [ |
| 11161 Polymer.PaperInkyFocusBehavior, |
| 11162 Polymer.IronCheckedElementBehavior, |
| 11163 Polymer.PaperCheckedElementBehaviorImpl |
| 11164 ]; |
| 11165 Polymer({ |
| 11166 is: 'paper-checkbox', |
| 11167 |
| 11168 behaviors: [ |
| 11169 Polymer.PaperCheckedElementBehavior |
| 11170 ], |
| 10680 | 11171 |
| 10681 hostAttributes: { | 11172 hostAttributes: { |
| 10682 role: 'button', | 11173 role: 'checkbox', |
| 10683 tabindex: '0' | 11174 'aria-checked': false, |
| 10684 }, | 11175 tabindex: 0 |
| 10685 | 11176 }, |
| 10686 behaviors: [ | |
| 10687 Polymer.PaperInkyFocusBehavior | |
| 10688 ], | |
| 10689 | 11177 |
| 10690 properties: { | 11178 properties: { |
| 10691 /** | 11179 /** |
| 10692 * The URL of an image for the icon. If the src property is specified, | 11180 * Fired when the checked state changes due to user interaction. |
| 10693 * the icon property should not be. | 11181 * |
| 11182 * @event change |
| 10694 */ | 11183 */ |
| 10695 src: { | |
| 10696 type: String | |
| 10697 }, | |
| 10698 | 11184 |
| 10699 /** | 11185 /** |
| 10700 * Specifies the icon name or index in the set of icons available in | 11186 * Fired when the checked state changes. |
| 10701 * the icon's icon set. If the icon property is specified, | 11187 * |
| 10702 * the src property should not be. | 11188 * @event iron-change |
| 10703 */ | 11189 */ |
| 10704 icon: { | 11190 ariaActiveAttribute: { |
| 10705 type: String | |
| 10706 }, | |
| 10707 | |
| 10708 /** | |
| 10709 * Specifies the alternate text for the button, for accessibility. | |
| 10710 */ | |
| 10711 alt: { | |
| 10712 type: String, | 11191 type: String, |
| 10713 observer: "_altChanged" | 11192 value: 'aria-checked' |
| 10714 } | 11193 } |
| 10715 }, | 11194 }, |
| 10716 | 11195 |
| 10717 _altChanged: function(newValue, oldValue) { | 11196 _computeCheckboxClass: function(checked, invalid) { |
| 10718 var label = this.getAttribute('aria-label'); | 11197 var className = ''; |
| 10719 | 11198 if (checked) { |
| 10720 // Don't stomp over a user-set aria-label. | 11199 className += 'checked '; |
| 10721 if (!label || oldValue == label) { | 11200 } |
| 10722 this.setAttribute('aria-label', newValue); | 11201 if (invalid) { |
| 11202 className += 'invalid'; |
| 11203 } |
| 11204 return className; |
| 11205 }, |
| 11206 |
| 11207 _computeCheckmarkClass: function(checked) { |
| 11208 return checked ? '' : 'hidden'; |
| 11209 }, |
| 11210 |
| 11211 // create ripple inside the checkboxContainer |
| 11212 _createRipple: function() { |
| 11213 this._rippleContainer = this.$.checkboxContainer; |
| 11214 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); |
| 11215 } |
| 11216 |
| 11217 }); |
| 11218 Polymer({ |
| 11219 is: 'paper-icon-button-light', |
| 11220 extends: 'button', |
| 11221 |
| 11222 behaviors: [ |
| 11223 Polymer.PaperRippleBehavior |
| 11224 ], |
| 11225 |
| 11226 listeners: { |
| 11227 'down': '_rippleDown', |
| 11228 'up': '_rippleUp', |
| 11229 'focus': '_rippleDown', |
| 11230 'blur': '_rippleUp', |
| 11231 }, |
| 11232 |
| 11233 _rippleDown: function() { |
| 11234 this.getRipple().downAction(); |
| 11235 }, |
| 11236 |
| 11237 _rippleUp: function() { |
| 11238 this.getRipple().upAction(); |
| 11239 }, |
| 11240 |
| 11241 /** |
| 11242 * @param {...*} var_args |
| 11243 */ |
| 11244 ensureRipple: function(var_args) { |
| 11245 var lastRipple = this._ripple; |
| 11246 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
| 11247 if (this._ripple && this._ripple !== lastRipple) { |
| 11248 this._ripple.center = true; |
| 11249 this._ripple.classList.add('circle'); |
| 10723 } | 11250 } |
| 10724 } | 11251 } |
| 10725 }); | 11252 }); |
| 10726 // Copyright 2016 The Chromium Authors. All rights reserved. | 11253 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 10727 // Use of this source code is governed by a BSD-style license that can be | 11254 // Use of this source code is governed by a BSD-style license that can be |
| 10728 // found in the LICENSE file. | 11255 // found in the LICENSE file. |
| 10729 | 11256 |
| 11257 cr.define('cr.icon', function() { |
| 11258 /** |
| 11259 * @return {!Array<number>} The scale factors supported by this platform for |
| 11260 * webui resources. |
| 11261 */ |
| 11262 function getSupportedScaleFactors() { |
| 11263 var supportedScaleFactors = []; |
| 11264 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { |
| 11265 // All desktop platforms support zooming which also updates the |
| 11266 // renderer's device scale factors (a.k.a devicePixelRatio), and |
| 11267 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in |
| 11268 // image-set on these platforms so that the renderer can pick the |
| 11269 // closest image for the current device scale factor. |
| 11270 supportedScaleFactors.push(1); |
| 11271 supportedScaleFactors.push(2); |
| 11272 } else { |
| 11273 // For other platforms that use fixed device scale factor, use |
| 11274 // the window's device pixel ratio. |
| 11275 // TODO(oshima): Investigate if Android/iOS need to use image-set. |
| 11276 supportedScaleFactors.push(window.devicePixelRatio); |
| 11277 } |
| 11278 return supportedScaleFactors; |
| 11279 } |
| 11280 |
| 11281 /** |
| 11282 * Returns the URL of the image, or an image set of URLs for the profile |
| 11283 * avatar. Default avatars have resources available for multiple scalefactors, |
| 11284 * whereas the GAIA profile image only comes in one size. |
| 11285 * |
| 11286 * @param {string} path The path of the image. |
| 11287 * @return {string} The url, or an image set of URLs of the avatar image. |
| 11288 */ |
| 11289 function getProfileAvatarIcon(path) { |
| 11290 var chromeThemePath = 'chrome://theme'; |
| 11291 var isDefaultAvatar = |
| 11292 (path.slice(0, chromeThemePath.length) == chromeThemePath); |
| 11293 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); |
| 11294 } |
| 11295 |
| 11296 /** |
| 11297 * Generates a CSS -webkit-image-set for a chrome:// url. |
| 11298 * An entry in the image set is added for each of getSupportedScaleFactors(). |
| 11299 * The scale-factor-specific url is generated by replacing the first instance |
| 11300 * of 'scalefactor' in |path| with the numeric scale factor. |
| 11301 * @param {string} path The URL to generate an image set for. |
| 11302 * 'scalefactor' should be a substring of |path|. |
| 11303 * @return {string} The CSS -webkit-image-set. |
| 11304 */ |
| 11305 function imageset(path) { |
| 11306 var supportedScaleFactors = getSupportedScaleFactors(); |
| 11307 |
| 11308 var replaceStartIndex = path.indexOf('scalefactor'); |
| 11309 if (replaceStartIndex < 0) |
| 11310 return url(path); |
| 11311 |
| 11312 var s = ''; |
| 11313 for (var i = 0; i < supportedScaleFactors.length; ++i) { |
| 11314 var scaleFactor = supportedScaleFactors[i]; |
| 11315 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + |
| 11316 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length); |
| 11317 |
| 11318 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; |
| 11319 |
| 11320 if (i != supportedScaleFactors.length - 1) |
| 11321 s += ', '; |
| 11322 } |
| 11323 return '-webkit-image-set(' + s + ')'; |
| 11324 } |
| 11325 |
| 11326 /** |
| 11327 * A regular expression for identifying favicon URLs. |
| 11328 * @const {!RegExp} |
| 11329 */ |
| 11330 var FAVICON_URL_REGEX = /\.ico$/i; |
| 11331 |
| 11332 /** |
| 11333 * Creates a CSS -webkit-image-set for a favicon request. |
| 11334 * @param {string} url Either the URL of the original page or of the favicon |
| 11335 * itself. |
| 11336 * @param {number=} opt_size Optional preferred size of the favicon. |
| 11337 * @param {string=} opt_type Optional type of favicon to request. Valid values |
| 11338 * are 'favicon' and 'touch-icon'. Default is 'favicon'. |
| 11339 * @return {string} -webkit-image-set for the favicon. |
| 11340 */ |
| 11341 function getFaviconImageSet(url, opt_size, opt_type) { |
| 11342 var size = opt_size || 16; |
| 11343 var type = opt_type || 'favicon'; |
| 11344 |
| 11345 return imageset( |
| 11346 'chrome://' + type + '/size/' + size + '@scalefactorx/' + |
| 11347 // Note: Literal 'iconurl' must match |kIconURLParameter| in |
| 11348 // components/favicon_base/favicon_url_parser.cc. |
| 11349 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); |
| 11350 } |
| 11351 |
| 11352 return { |
| 11353 getSupportedScaleFactors: getSupportedScaleFactors, |
| 11354 getProfileAvatarIcon: getProfileAvatarIcon, |
| 11355 getFaviconImageSet: getFaviconImageSet, |
| 11356 }; |
| 11357 }); |
| 11358 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11359 // Use of this source code is governed by a BSD-style license that can be |
| 11360 // found in the LICENSE file. |
| 11361 |
| 10730 /** | 11362 /** |
| 10731 * Implements an incremental search field which can be shown and hidden. | 11363 * @fileoverview Defines a singleton object, md_history.BrowserService, which |
| 10732 * Canonical implementation is <cr-search-field>. | 11364 * provides access to chrome.send APIs. |
| 10733 * @polymerBehavior | |
| 10734 */ | 11365 */ |
| 10735 var CrSearchFieldBehavior = { | 11366 |
| 11367 cr.define('md_history', function() { |
| 11368 /** @constructor */ |
| 11369 function BrowserService() { |
| 11370 /** @private {Array<!HistoryEntry>} */ |
| 11371 this.pendingDeleteItems_ = null; |
| 11372 /** @private {PromiseResolver} */ |
| 11373 this.pendingDeletePromise_ = null; |
| 11374 } |
| 11375 |
| 11376 BrowserService.prototype = { |
| 11377 /** |
| 11378 * @param {!Array<!HistoryEntry>} items |
| 11379 * @return {Promise<!Array<!HistoryEntry>>} |
| 11380 */ |
| 11381 deleteItems: function(items) { |
| 11382 if (this.pendingDeleteItems_ != null) { |
| 11383 // There's already a deletion in progress, reject immediately. |
| 11384 return new Promise(function(resolve, reject) { reject(items); }); |
| 11385 } |
| 11386 |
| 11387 var removalList = items.map(function(item) { |
| 11388 return { |
| 11389 url: item.url, |
| 11390 timestamps: item.allTimestamps |
| 11391 }; |
| 11392 }); |
| 11393 |
| 11394 this.pendingDeleteItems_ = items; |
| 11395 this.pendingDeletePromise_ = new PromiseResolver(); |
| 11396 |
| 11397 chrome.send('removeVisits', removalList); |
| 11398 |
| 11399 return this.pendingDeletePromise_.promise; |
| 11400 }, |
| 11401 |
| 11402 /** |
| 11403 * @param {!string} url |
| 11404 */ |
| 11405 removeBookmark: function(url) { |
| 11406 chrome.send('removeBookmark', [url]); |
| 11407 }, |
| 11408 |
| 11409 /** |
| 11410 * @param {string} sessionTag |
| 11411 */ |
| 11412 openForeignSessionAllTabs: function(sessionTag) { |
| 11413 chrome.send('openForeignSession', [sessionTag]); |
| 11414 }, |
| 11415 |
| 11416 /** |
| 11417 * @param {string} sessionTag |
| 11418 * @param {number} windowId |
| 11419 * @param {number} tabId |
| 11420 * @param {MouseEvent} e |
| 11421 */ |
| 11422 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { |
| 11423 chrome.send('openForeignSession', [ |
| 11424 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, |
| 11425 e.ctrlKey, e.metaKey, e.shiftKey |
| 11426 ]); |
| 11427 }, |
| 11428 |
| 11429 /** |
| 11430 * @param {string} sessionTag |
| 11431 */ |
| 11432 deleteForeignSession: function(sessionTag) { |
| 11433 chrome.send('deleteForeignSession', [sessionTag]); |
| 11434 }, |
| 11435 |
| 11436 openClearBrowsingData: function() { |
| 11437 chrome.send('clearBrowsingData'); |
| 11438 }, |
| 11439 |
| 11440 /** |
| 11441 * @param {boolean} successful |
| 11442 * @private |
| 11443 */ |
| 11444 resolveDelete_: function(successful) { |
| 11445 if (this.pendingDeleteItems_ == null || |
| 11446 this.pendingDeletePromise_ == null) { |
| 11447 return; |
| 11448 } |
| 11449 |
| 11450 if (successful) |
| 11451 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_); |
| 11452 else |
| 11453 this.pendingDeletePromise_.reject(this.pendingDeleteItems_); |
| 11454 |
| 11455 this.pendingDeleteItems_ = null; |
| 11456 this.pendingDeletePromise_ = null; |
| 11457 }, |
| 11458 }; |
| 11459 |
| 11460 cr.addSingletonGetter(BrowserService); |
| 11461 |
| 11462 return {BrowserService: BrowserService}; |
| 11463 }); |
| 11464 |
| 11465 /** |
| 11466 * Called by the history backend when deletion was succesful. |
| 11467 */ |
| 11468 function deleteComplete() { |
| 11469 md_history.BrowserService.getInstance().resolveDelete_(true); |
| 11470 } |
| 11471 |
| 11472 /** |
| 11473 * Called by the history backend when the deletion failed. |
| 11474 */ |
| 11475 function deleteFailed() { |
| 11476 md_history.BrowserService.getInstance().resolveDelete_(false); |
| 11477 }; |
| 11478 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11479 // Use of this source code is governed by a BSD-style license that can be |
| 11480 // found in the LICENSE file. |
| 11481 |
| 11482 Polymer({ |
| 11483 is: 'history-searched-label', |
| 11484 |
| 10736 properties: { | 11485 properties: { |
| 10737 label: { | 11486 // The text to show in this label. |
| 10738 type: String, | 11487 title: String, |
| 10739 value: '', | 11488 |
| 10740 }, | 11489 // The search term to bold within the title. |
| 10741 | 11490 searchTerm: String, |
| 10742 clearLabel: { | 11491 }, |
| 10743 type: String, | 11492 |
| 10744 value: '', | 11493 observers: ['setSearchedTextToBold_(title, searchTerm)'], |
| 10745 }, | 11494 |
| 10746 | 11495 /** |
| 10747 showingSearch: { | 11496 * Updates the page title. If a search term is specified, highlights any |
| 10748 type: Boolean, | 11497 * occurrences of the search term in bold. |
| 10749 value: false, | |
| 10750 notify: true, | |
| 10751 observer: 'showingSearchChanged_', | |
| 10752 reflectToAttribute: true | |
| 10753 }, | |
| 10754 | |
| 10755 /** @private */ | |
| 10756 lastValue_: { | |
| 10757 type: String, | |
| 10758 value: '', | |
| 10759 }, | |
| 10760 }, | |
| 10761 | |
| 10762 /** | |
| 10763 * @abstract | |
| 10764 * @return {!HTMLInputElement} The input field element the behavior should | |
| 10765 * use. | |
| 10766 */ | |
| 10767 getSearchInput: function() {}, | |
| 10768 | |
| 10769 /** | |
| 10770 * @return {string} The value of the search field. | |
| 10771 */ | |
| 10772 getValue: function() { | |
| 10773 return this.getSearchInput().value; | |
| 10774 }, | |
| 10775 | |
| 10776 /** | |
| 10777 * Sets the value of the search field. | |
| 10778 * @param {string} value | |
| 10779 */ | |
| 10780 setValue: function(value) { | |
| 10781 // Use bindValue when setting the input value so that changes propagate | |
| 10782 // correctly. | |
| 10783 this.getSearchInput().bindValue = value; | |
| 10784 this.onValueChanged_(value); | |
| 10785 }, | |
| 10786 | |
| 10787 showAndFocus: function() { | |
| 10788 this.showingSearch = true; | |
| 10789 this.focus_(); | |
| 10790 }, | |
| 10791 | |
| 10792 /** @private */ | |
| 10793 focus_: function() { | |
| 10794 this.getSearchInput().focus(); | |
| 10795 }, | |
| 10796 | |
| 10797 onSearchTermSearch: function() { | |
| 10798 this.onValueChanged_(this.getValue()); | |
| 10799 }, | |
| 10800 | |
| 10801 /** | |
| 10802 * Updates the internal state of the search field based on a change that has | |
| 10803 * already happened. | |
| 10804 * @param {string} newValue | |
| 10805 * @private | 11498 * @private |
| 10806 */ | 11499 */ |
| 10807 onValueChanged_: function(newValue) { | 11500 setSearchedTextToBold_: function() { |
| 10808 if (newValue == this.lastValue_) | 11501 var i = 0; |
| 10809 return; | 11502 var titleElem = this.$.container; |
| 10810 | 11503 var titleText = this.title; |
| 10811 this.fire('search-changed', newValue); | 11504 |
| 10812 this.lastValue_ = newValue; | 11505 if (this.searchTerm == '' || this.searchTerm == null) { |
| 10813 }, | 11506 titleElem.textContent = titleText; |
| 10814 | |
| 10815 onSearchTermKeydown: function(e) { | |
| 10816 if (e.key == 'Escape') | |
| 10817 this.showingSearch = false; | |
| 10818 }, | |
| 10819 | |
| 10820 /** @private */ | |
| 10821 showingSearchChanged_: function() { | |
| 10822 if (this.showingSearch) { | |
| 10823 this.focus_(); | |
| 10824 return; | 11507 return; |
| 10825 } | 11508 } |
| 10826 | 11509 |
| 10827 this.setValue(''); | 11510 var re = new RegExp(quoteString(this.searchTerm), 'gim'); |
| 10828 this.getSearchInput().blur(); | 11511 var match; |
| 10829 } | 11512 titleElem.textContent = ''; |
| 11513 while (match = re.exec(titleText)) { |
| 11514 if (match.index > i) |
| 11515 titleElem.appendChild(document.createTextNode( |
| 11516 titleText.slice(i, match.index))); |
| 11517 i = re.lastIndex; |
| 11518 // Mark the highlighted text in bold. |
| 11519 var b = document.createElement('b'); |
| 11520 b.textContent = titleText.substring(match.index, i); |
| 11521 titleElem.appendChild(b); |
| 11522 } |
| 11523 if (i < titleText.length) |
| 11524 titleElem.appendChild( |
| 11525 document.createTextNode(titleText.slice(i))); |
| 11526 }, |
| 11527 }); |
| 11528 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11529 // Use of this source code is governed by a BSD-style license that can be |
| 11530 // found in the LICENSE file. |
| 11531 |
| 11532 cr.define('md_history', function() { |
| 11533 var HistoryItem = Polymer({ |
| 11534 is: 'history-item', |
| 11535 |
| 11536 properties: { |
| 11537 // Underlying HistoryEntry data for this item. Contains read-only fields |
| 11538 // from the history backend, as well as fields computed by history-list. |
| 11539 item: {type: Object, observer: 'showIcon_'}, |
| 11540 |
| 11541 // Search term used to obtain this history-item. |
| 11542 searchTerm: {type: String}, |
| 11543 |
| 11544 selected: {type: Boolean, notify: true}, |
| 11545 |
| 11546 isFirstItem: {type: Boolean, reflectToAttribute: true}, |
| 11547 |
| 11548 isCardStart: {type: Boolean, reflectToAttribute: true}, |
| 11549 |
| 11550 isCardEnd: {type: Boolean, reflectToAttribute: true}, |
| 11551 |
| 11552 // True if the item is being displayed embedded in another element and |
| 11553 // should not manage its own borders or size. |
| 11554 embedded: {type: Boolean, reflectToAttribute: true}, |
| 11555 |
| 11556 hasTimeGap: {type: Boolean}, |
| 11557 |
| 11558 numberOfItems: {type: Number}, |
| 11559 |
| 11560 // The path of this history item inside its parent. |
| 11561 path: String, |
| 11562 }, |
| 11563 |
| 11564 /** |
| 11565 * When a history-item is selected the toolbar is notified and increases |
| 11566 * or decreases its count of selected items accordingly. |
| 11567 * @private |
| 11568 */ |
| 11569 onCheckboxSelected_: function() { |
| 11570 // TODO(calamity): Fire this event whenever |selected| changes. |
| 11571 this.fire('history-checkbox-select', { |
| 11572 element: this, |
| 11573 countAddition: this.$.checkbox.checked ? 1 : -1 |
| 11574 }); |
| 11575 }, |
| 11576 |
| 11577 /** |
| 11578 * Remove bookmark of current item when bookmark-star is clicked. |
| 11579 * @private |
| 11580 */ |
| 11581 onRemoveBookmarkTap_: function() { |
| 11582 if (!this.item.starred) |
| 11583 return; |
| 11584 |
| 11585 if (this.$$('#bookmark-star') == this.root.activeElement) |
| 11586 this.$['menu-button'].focus(); |
| 11587 |
| 11588 md_history.BrowserService.getInstance() |
| 11589 .removeBookmark(this.item.url); |
| 11590 this.fire('remove-bookmark-stars', this.item.url); |
| 11591 }, |
| 11592 |
| 11593 /** |
| 11594 * Fires a custom event when the menu button is clicked. Sends the details |
| 11595 * of the history item and where the menu should appear. |
| 11596 */ |
| 11597 onMenuButtonTap_: function(e) { |
| 11598 this.fire('toggle-menu', { |
| 11599 target: Polymer.dom(e).localTarget, |
| 11600 item: this.item, |
| 11601 }); |
| 11602 |
| 11603 // Stops the 'tap' event from closing the menu when it opens. |
| 11604 e.stopPropagation(); |
| 11605 }, |
| 11606 |
| 11607 /** |
| 11608 * Set the favicon image, based on the URL of the history item. |
| 11609 * @private |
| 11610 */ |
| 11611 showIcon_: function() { |
| 11612 this.$.icon.style.backgroundImage = |
| 11613 cr.icon.getFaviconImageSet(this.item.url); |
| 11614 }, |
| 11615 |
| 11616 selectionNotAllowed_: function() { |
| 11617 return !loadTimeData.getBoolean('allowDeletingHistory'); |
| 11618 }, |
| 11619 |
| 11620 /** |
| 11621 * Generates the title for this history card. |
| 11622 * @param {number} numberOfItems The number of items in the card. |
| 11623 * @param {string} search The search term associated with these results. |
| 11624 * @private |
| 11625 */ |
| 11626 cardTitle_: function(numberOfItems, historyDate, search) { |
| 11627 if (!search) |
| 11628 return this.item.dateRelativeDay; |
| 11629 |
| 11630 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; |
| 11631 return loadTimeData.getStringF('foundSearchResults', numberOfItems, |
| 11632 loadTimeData.getString(resultId), search); |
| 11633 }, |
| 11634 |
| 11635 /** |
| 11636 * Crop long item titles to reduce their effect on layout performance. See |
| 11637 * crbug.com/621347. |
| 11638 * @param {string} title |
| 11639 * @return {string} |
| 11640 */ |
| 11641 cropItemTitle_: function(title) { |
| 11642 return (title.length > TITLE_MAX_LENGTH) ? |
| 11643 title.substr(0, TITLE_MAX_LENGTH) : |
| 11644 title; |
| 11645 } |
| 11646 }); |
| 11647 |
| 11648 /** |
| 11649 * Check whether the time difference between the given history item and the |
| 11650 * next one is large enough for a spacer to be required. |
| 11651 * @param {Array<HistoryEntry>} visits |
| 11652 * @param {number} currentIndex |
| 11653 * @param {string} searchedTerm |
| 11654 * @return {boolean} Whether or not time gap separator is required. |
| 11655 * @private |
| 11656 */ |
| 11657 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { |
| 11658 if (currentIndex >= visits.length - 1 || visits.length == 0) |
| 11659 return false; |
| 11660 |
| 11661 var currentItem = visits[currentIndex]; |
| 11662 var nextItem = visits[currentIndex + 1]; |
| 11663 |
| 11664 if (searchedTerm) |
| 11665 return currentItem.dateShort != nextItem.dateShort; |
| 11666 |
| 11667 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && |
| 11668 currentItem.dateRelativeDay == nextItem.dateRelativeDay; |
| 11669 }; |
| 11670 |
| 11671 return { HistoryItem: HistoryItem }; |
| 11672 }); |
| 11673 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11674 // Use of this source code is governed by a BSD-style license that can be |
| 11675 // found in the LICENSE file. |
| 11676 |
| 11677 /** |
| 11678 * @constructor |
| 11679 * @param {string} currentPath |
| 11680 */ |
| 11681 var SelectionTreeNode = function(currentPath) { |
| 11682 /** @type {string} */ |
| 11683 this.currentPath = currentPath; |
| 11684 /** @type {boolean} */ |
| 11685 this.leaf = false; |
| 11686 /** @type {Array<number>} */ |
| 11687 this.indexes = []; |
| 11688 /** @type {Array<SelectionTreeNode>} */ |
| 11689 this.children = []; |
| 10830 }; | 11690 }; |
| 11691 |
| 11692 /** |
| 11693 * @param {number} index |
| 11694 * @param {string} path |
| 11695 */ |
| 11696 SelectionTreeNode.prototype.addChild = function(index, path) { |
| 11697 this.indexes.push(index); |
| 11698 this.children[index] = new SelectionTreeNode(path); |
| 11699 }; |
| 11700 |
| 11701 /** @polymerBehavior */ |
| 11702 var HistoryListBehavior = { |
| 11703 properties: { |
| 11704 /** |
| 11705 * Polymer paths to the history items contained in this list. |
| 11706 * @type {Array<string>} selectedPaths |
| 11707 */ |
| 11708 selectedPaths: { |
| 11709 type: Array, |
| 11710 value: /** @return {Array<string>} */ function() { return []; } |
| 11711 }, |
| 11712 }, |
| 11713 |
| 11714 listeners: { |
| 11715 'history-checkbox-select': 'itemSelected_', |
| 11716 }, |
| 11717 |
| 11718 /** |
| 11719 * @param {number} historyDataLength |
| 11720 * @return {boolean} |
| 11721 * @private |
| 11722 */ |
| 11723 hasResults: function(historyDataLength) { return historyDataLength > 0; }, |
| 11724 |
| 11725 /** |
| 11726 * @param {string} searchedTerm |
| 11727 * @param {boolean} isLoading |
| 11728 * @return {string} |
| 11729 * @private |
| 11730 */ |
| 11731 noResultsMessage: function(searchedTerm, isLoading) { |
| 11732 if (isLoading) |
| 11733 return ''; |
| 11734 |
| 11735 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
| 11736 return loadTimeData.getString(messageId); |
| 11737 }, |
| 11738 |
| 11739 /** |
| 11740 * Deselect each item in |selectedPaths|. |
| 11741 */ |
| 11742 unselectAllItems: function() { |
| 11743 this.selectedPaths.forEach(function(path) { |
| 11744 this.set(path + '.selected', false); |
| 11745 }.bind(this)); |
| 11746 |
| 11747 this.selectedPaths = []; |
| 11748 }, |
| 11749 |
| 11750 /** |
| 11751 * Performs a request to the backend to delete all selected items. If |
| 11752 * successful, removes them from the view. Does not prompt the user before |
| 11753 * deleting -- see <history-list-container> for a version of this method which |
| 11754 * does prompt. |
| 11755 */ |
| 11756 deleteSelected: function() { |
| 11757 var toBeRemoved = this.selectedPaths.map(function(path) { |
| 11758 return this.get(path); |
| 11759 }.bind(this)); |
| 11760 md_history.BrowserService.getInstance() |
| 11761 .deleteItems(toBeRemoved) |
| 11762 .then(function() { |
| 11763 this.removeItemsByPath(this.selectedPaths); |
| 11764 this.fire('unselect-all'); |
| 11765 }.bind(this)); |
| 11766 }, |
| 11767 |
| 11768 /** |
| 11769 * Removes the history items in |paths|. Assumes paths are of a.0.b.0... |
| 11770 * structure. |
| 11771 * |
| 11772 * We want to use notifySplices to update the arrays for performance reasons |
| 11773 * which requires manually batching and sending the notifySplices for each |
| 11774 * level. To do this, we build a tree where each node is an array and then |
| 11775 * depth traverse it to remove items. Each time a node has all children |
| 11776 * deleted, we can also remove the node. |
| 11777 * |
| 11778 * @param {Array<string>} paths |
| 11779 * @private |
| 11780 */ |
| 11781 removeItemsByPath: function(paths) { |
| 11782 if (paths.length == 0) |
| 11783 return; |
| 11784 |
| 11785 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); |
| 11786 }, |
| 11787 |
| 11788 /** |
| 11789 * Creates the tree to traverse in order to remove |paths| from this list. |
| 11790 * Assumes paths are of a.0.b.0... |
| 11791 * structure. |
| 11792 * |
| 11793 * @param {Array<string>} paths |
| 11794 * @return {SelectionTreeNode} |
| 11795 * @private |
| 11796 */ |
| 11797 buildRemovalTree_: function(paths) { |
| 11798 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); |
| 11799 |
| 11800 // Build a tree to each history item specified in |paths|. |
| 11801 paths.forEach(function(path) { |
| 11802 var components = path.split('.'); |
| 11803 var node = rootNode; |
| 11804 components.shift(); |
| 11805 while (components.length > 1) { |
| 11806 var index = Number(components.shift()); |
| 11807 var arrayName = components.shift(); |
| 11808 |
| 11809 if (!node.children[index]) |
| 11810 node.addChild(index, [node.currentPath, index, arrayName].join('.')); |
| 11811 |
| 11812 node = node.children[index]; |
| 11813 } |
| 11814 node.leaf = true; |
| 11815 node.indexes.push(Number(components.shift())); |
| 11816 }); |
| 11817 |
| 11818 return rootNode; |
| 11819 }, |
| 11820 |
| 11821 /** |
| 11822 * Removes the history items underneath |node| and deletes container arrays as |
| 11823 * they become empty. |
| 11824 * @param {SelectionTreeNode} node |
| 11825 * @return {boolean} Whether this node's array should be deleted. |
| 11826 * @private |
| 11827 */ |
| 11828 removeItemsBeneathNode_: function(node) { |
| 11829 var array = this.get(node.currentPath); |
| 11830 var splices = []; |
| 11831 |
| 11832 node.indexes.sort(function(a, b) { return b - a; }); |
| 11833 node.indexes.forEach(function(index) { |
| 11834 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { |
| 11835 var item = array.splice(index, 1); |
| 11836 splices.push({ |
| 11837 index: index, |
| 11838 removed: [item], |
| 11839 addedCount: 0, |
| 11840 object: array, |
| 11841 type: 'splice' |
| 11842 }); |
| 11843 } |
| 11844 }.bind(this)); |
| 11845 |
| 11846 if (array.length == 0) |
| 11847 return true; |
| 11848 |
| 11849 // notifySplices gives better performance than individually splicing as it |
| 11850 // batches all of the updates together. |
| 11851 this.notifySplices(node.currentPath, splices); |
| 11852 return false; |
| 11853 }, |
| 11854 |
| 11855 /** |
| 11856 * @param {Event} e |
| 11857 * @private |
| 11858 */ |
| 11859 itemSelected_: function(e) { |
| 11860 var item = e.detail.element; |
| 11861 var path = item.path; |
| 11862 if (item.selected) { |
| 11863 this.push('selectedPaths', path); |
| 11864 return; |
| 11865 } |
| 11866 |
| 11867 var index = this.selectedPaths.indexOf(path); |
| 11868 if (index != -1) |
| 11869 this.splice('selectedPaths', index, 1); |
| 11870 }, |
| 11871 }; |
| 11872 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11873 // Use of this source code is governed by a BSD-style license that can be |
| 11874 // found in the LICENSE file. |
| 11875 |
| 11876 /** |
| 11877 * @typedef {{domain: string, |
| 11878 * visits: !Array<HistoryEntry>, |
| 11879 * rendered: boolean, |
| 11880 * expanded: boolean}} |
| 11881 */ |
| 11882 var HistoryDomain; |
| 11883 |
| 11884 /** |
| 11885 * @typedef {{title: string, |
| 11886 * domains: !Array<HistoryDomain>}} |
| 11887 */ |
| 11888 var HistoryGroup; |
| 11889 |
| 11890 Polymer({ |
| 11891 is: 'history-grouped-list', |
| 11892 |
| 11893 behaviors: [HistoryListBehavior], |
| 11894 |
| 11895 properties: { |
| 11896 // An array of history entries in reverse chronological order. |
| 11897 historyData: { |
| 11898 type: Array, |
| 11899 }, |
| 11900 |
| 11901 /** |
| 11902 * @type {Array<HistoryGroup>} |
| 11903 */ |
| 11904 groupedHistoryData_: { |
| 11905 type: Array, |
| 11906 }, |
| 11907 |
| 11908 searchedTerm: { |
| 11909 type: String, |
| 11910 value: '' |
| 11911 }, |
| 11912 |
| 11913 range: { |
| 11914 type: Number, |
| 11915 }, |
| 11916 |
| 11917 queryStartTime: String, |
| 11918 queryEndTime: String, |
| 11919 }, |
| 11920 |
| 11921 observers: [ |
| 11922 'updateGroupedHistoryData_(range, historyData)' |
| 11923 ], |
| 11924 |
| 11925 /** |
| 11926 * Make a list of domains from visits. |
| 11927 * @param {!Array<!HistoryEntry>} visits |
| 11928 * @return {!Array<!HistoryDomain>} |
| 11929 */ |
| 11930 createHistoryDomains_: function(visits) { |
| 11931 var domainIndexes = {}; |
| 11932 var domains = []; |
| 11933 |
| 11934 // Group the visits into a dictionary and generate a list of domains. |
| 11935 for (var i = 0, visit; visit = visits[i]; i++) { |
| 11936 var domain = visit.domain; |
| 11937 if (domainIndexes[domain] == undefined) { |
| 11938 domainIndexes[domain] = domains.length; |
| 11939 domains.push({ |
| 11940 domain: domain, |
| 11941 visits: [], |
| 11942 expanded: false, |
| 11943 rendered: false, |
| 11944 }); |
| 11945 } |
| 11946 domains[domainIndexes[domain]].visits.push(visit); |
| 11947 } |
| 11948 var sortByVisits = function(a, b) { |
| 11949 return b.visits.length - a.visits.length; |
| 11950 }; |
| 11951 domains.sort(sortByVisits); |
| 11952 |
| 11953 return domains; |
| 11954 }, |
| 11955 |
| 11956 updateGroupedHistoryData_: function() { |
| 11957 if (this.historyData.length == 0) { |
| 11958 this.groupedHistoryData_ = []; |
| 11959 return; |
| 11960 } |
| 11961 |
| 11962 if (this.range == HistoryRange.WEEK) { |
| 11963 // Group each day into a list of results. |
| 11964 var days = []; |
| 11965 var currentDayVisits = [this.historyData[0]]; |
| 11966 |
| 11967 var pushCurrentDay = function() { |
| 11968 days.push({ |
| 11969 title: this.searchedTerm ? currentDayVisits[0].dateShort : |
| 11970 currentDayVisits[0].dateRelativeDay, |
| 11971 domains: this.createHistoryDomains_(currentDayVisits), |
| 11972 }); |
| 11973 }.bind(this); |
| 11974 |
| 11975 var visitsSameDay = function(a, b) { |
| 11976 if (this.searchedTerm) |
| 11977 return a.dateShort == b.dateShort; |
| 11978 |
| 11979 return a.dateRelativeDay == b.dateRelativeDay; |
| 11980 }.bind(this); |
| 11981 |
| 11982 for (var i = 1; i < this.historyData.length; i++) { |
| 11983 var visit = this.historyData[i]; |
| 11984 if (!visitsSameDay(visit, currentDayVisits[0])) { |
| 11985 pushCurrentDay(); |
| 11986 currentDayVisits = []; |
| 11987 } |
| 11988 currentDayVisits.push(visit); |
| 11989 } |
| 11990 pushCurrentDay(); |
| 11991 |
| 11992 this.groupedHistoryData_ = days; |
| 11993 } else if (this.range == HistoryRange.MONTH) { |
| 11994 // Group each all visits into a single list. |
| 11995 this.groupedHistoryData_ = [{ |
| 11996 title: this.queryStartTime + ' – ' + this.queryEndTime, |
| 11997 domains: this.createHistoryDomains_(this.historyData) |
| 11998 }]; |
| 11999 } |
| 12000 }, |
| 12001 |
| 12002 /** |
| 12003 * @param {{model:Object, currentTarget:IronCollapseElement}} e |
| 12004 */ |
| 12005 toggleDomainExpanded_: function(e) { |
| 12006 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); |
| 12007 e.model.set('domain.rendered', true); |
| 12008 |
| 12009 // Give the history-items time to render. |
| 12010 setTimeout(function() { collapse.toggle() }, 0); |
| 12011 }, |
| 12012 |
| 12013 /** |
| 12014 * Check whether the time difference between the given history item and the |
| 12015 * next one is large enough for a spacer to be required. |
| 12016 * @param {number} groupIndex |
| 12017 * @param {number} domainIndex |
| 12018 * @param {number} itemIndex |
| 12019 * @return {boolean} Whether or not time gap separator is required. |
| 12020 * @private |
| 12021 */ |
| 12022 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { |
| 12023 var visits = |
| 12024 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; |
| 12025 |
| 12026 return md_history.HistoryItem.needsTimeGap( |
| 12027 visits, itemIndex, this.searchedTerm); |
| 12028 }, |
| 12029 |
| 12030 /** |
| 12031 * @param {number} groupIndex |
| 12032 * @param {number} domainIndex |
| 12033 * @param {number} itemIndex |
| 12034 * @return {string} |
| 12035 * @private |
| 12036 */ |
| 12037 pathForItem_: function(groupIndex, domainIndex, itemIndex) { |
| 12038 return [ |
| 12039 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', |
| 12040 itemIndex |
| 12041 ].join('.'); |
| 12042 }, |
| 12043 |
| 12044 /** |
| 12045 * @param {HistoryDomain} domain |
| 12046 * @return {string} |
| 12047 * @private |
| 12048 */ |
| 12049 getWebsiteIconStyle_: function(domain) { |
| 12050 return 'background-image: ' + |
| 12051 cr.icon.getFaviconImageSet(domain.visits[0].url); |
| 12052 }, |
| 12053 |
| 12054 /** |
| 12055 * @param {boolean} expanded |
| 12056 * @return {string} |
| 12057 * @private |
| 12058 */ |
| 12059 getDropdownIcon_: function(expanded) { |
| 12060 return expanded ? 'cr:expand-less' : 'cr:expand-more'; |
| 12061 }, |
| 12062 }); |
| 12063 /** |
| 12064 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a |
| 12065 * designated scroll target. |
| 12066 * |
| 12067 * Elements that consume this behavior can override the `_scrollHandler` |
| 12068 * method to add logic on the scroll event. |
| 12069 * |
| 12070 * @demo demo/scrolling-region.html Scrolling Region |
| 12071 * @demo demo/document.html Document Element |
| 12072 * @polymerBehavior |
| 12073 */ |
| 12074 Polymer.IronScrollTargetBehavior = { |
| 12075 |
| 12076 properties: { |
| 12077 |
| 12078 /** |
| 12079 * Specifies the element that will handle the scroll event |
| 12080 * on the behalf of the current element. This is typically a reference to
an element, |
| 12081 * but there are a few more posibilities: |
| 12082 * |
| 12083 * ### Elements id |
| 12084 * |
| 12085 *```html |
| 12086 * <div id="scrollable-element" style="overflow: auto;"> |
| 12087 * <x-element scroll-target="scrollable-element"> |
| 12088 * \x3c!-- Content--\x3e |
| 12089 * </x-element> |
| 12090 * </div> |
| 12091 *``` |
| 12092 * In this case, the `scrollTarget` will point to the outer div element. |
| 12093 * |
| 12094 * ### Document scrolling |
| 12095 * |
| 12096 * For document scrolling, you can use the reserved word `document`: |
| 12097 * |
| 12098 *```html |
| 12099 * <x-element scroll-target="document"> |
| 12100 * \x3c!-- Content --\x3e |
| 12101 * </x-element> |
| 12102 *``` |
| 12103 * |
| 12104 * ### Elements reference |
| 12105 * |
| 12106 *```js |
| 12107 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); |
| 12108 *``` |
| 12109 * |
| 12110 * @type {HTMLElement} |
| 12111 */ |
| 12112 scrollTarget: { |
| 12113 type: HTMLElement, |
| 12114 value: function() { |
| 12115 return this._defaultScrollTarget; |
| 12116 } |
| 12117 } |
| 12118 }, |
| 12119 |
| 12120 observers: [ |
| 12121 '_scrollTargetChanged(scrollTarget, isAttached)' |
| 12122 ], |
| 12123 |
| 12124 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 12125 var eventTarget; |
| 12126 |
| 12127 if (this._oldScrollTarget) { |
| 12128 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; |
| 12129 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 12130 this._oldScrollTarget = null; |
| 12131 } |
| 12132 |
| 12133 if (!isAttached) { |
| 12134 return; |
| 12135 } |
| 12136 // Support element id references |
| 12137 if (scrollTarget === 'document') { |
| 12138 |
| 12139 this.scrollTarget = this._doc; |
| 12140 |
| 12141 } else if (typeof scrollTarget === 'string') { |
| 12142 |
| 12143 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : |
| 12144 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 12145 |
| 12146 } else if (this._isValidScrollTarget()) { |
| 12147 |
| 12148 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 12149 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); |
| 12150 this._oldScrollTarget = scrollTarget; |
| 12151 |
| 12152 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 12153 } |
| 12154 }, |
| 12155 |
| 12156 /** |
| 12157 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. |
| 12158 * |
| 12159 * @protected |
| 12160 */ |
| 12161 _scrollHandler: function scrollHandler() {}, |
| 12162 |
| 12163 /** |
| 12164 * The default scroll target. Consumers of this behavior may want to customi
ze |
| 12165 * the default scroll target. |
| 12166 * |
| 12167 * @type {Element} |
| 12168 */ |
| 12169 get _defaultScrollTarget() { |
| 12170 return this._doc; |
| 12171 }, |
| 12172 |
| 12173 /** |
| 12174 * Shortcut for the document element |
| 12175 * |
| 12176 * @type {Element} |
| 12177 */ |
| 12178 get _doc() { |
| 12179 return this.ownerDocument.documentElement; |
| 12180 }, |
| 12181 |
| 12182 /** |
| 12183 * Gets the number of pixels that the content of an element is scrolled upwa
rd. |
| 12184 * |
| 12185 * @type {number} |
| 12186 */ |
| 12187 get _scrollTop() { |
| 12188 if (this._isValidScrollTarget()) { |
| 12189 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; |
| 12190 } |
| 12191 return 0; |
| 12192 }, |
| 12193 |
| 12194 /** |
| 12195 * Gets the number of pixels that the content of an element is scrolled to t
he left. |
| 12196 * |
| 12197 * @type {number} |
| 12198 */ |
| 12199 get _scrollLeft() { |
| 12200 if (this._isValidScrollTarget()) { |
| 12201 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; |
| 12202 } |
| 12203 return 0; |
| 12204 }, |
| 12205 |
| 12206 /** |
| 12207 * Sets the number of pixels that the content of an element is scrolled upwa
rd. |
| 12208 * |
| 12209 * @type {number} |
| 12210 */ |
| 12211 set _scrollTop(top) { |
| 12212 if (this.scrollTarget === this._doc) { |
| 12213 window.scrollTo(window.pageXOffset, top); |
| 12214 } else if (this._isValidScrollTarget()) { |
| 12215 this.scrollTarget.scrollTop = top; |
| 12216 } |
| 12217 }, |
| 12218 |
| 12219 /** |
| 12220 * Sets the number of pixels that the content of an element is scrolled to t
he left. |
| 12221 * |
| 12222 * @type {number} |
| 12223 */ |
| 12224 set _scrollLeft(left) { |
| 12225 if (this.scrollTarget === this._doc) { |
| 12226 window.scrollTo(left, window.pageYOffset); |
| 12227 } else if (this._isValidScrollTarget()) { |
| 12228 this.scrollTarget.scrollLeft = left; |
| 12229 } |
| 12230 }, |
| 12231 |
| 12232 /** |
| 12233 * Scrolls the content to a particular place. |
| 12234 * |
| 12235 * @method scroll |
| 12236 * @param {number} left The left position |
| 12237 * @param {number} top The top position |
| 12238 */ |
| 12239 scroll: function(left, top) { |
| 12240 if (this.scrollTarget === this._doc) { |
| 12241 window.scrollTo(left, top); |
| 12242 } else if (this._isValidScrollTarget()) { |
| 12243 this.scrollTarget.scrollLeft = left; |
| 12244 this.scrollTarget.scrollTop = top; |
| 12245 } |
| 12246 }, |
| 12247 |
| 12248 /** |
| 12249 * Gets the width of the scroll target. |
| 12250 * |
| 12251 * @type {number} |
| 12252 */ |
| 12253 get _scrollTargetWidth() { |
| 12254 if (this._isValidScrollTarget()) { |
| 12255 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; |
| 12256 } |
| 12257 return 0; |
| 12258 }, |
| 12259 |
| 12260 /** |
| 12261 * Gets the height of the scroll target. |
| 12262 * |
| 12263 * @type {number} |
| 12264 */ |
| 12265 get _scrollTargetHeight() { |
| 12266 if (this._isValidScrollTarget()) { |
| 12267 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; |
| 12268 } |
| 12269 return 0; |
| 12270 }, |
| 12271 |
| 12272 /** |
| 12273 * Returns true if the scroll target is a valid HTMLElement. |
| 12274 * |
| 12275 * @return {boolean} |
| 12276 */ |
| 12277 _isValidScrollTarget: function() { |
| 12278 return this.scrollTarget instanceof HTMLElement; |
| 12279 } |
| 12280 }; |
| 10831 (function() { | 12281 (function() { |
| 10832 'use strict'; | 12282 |
| 10833 | 12283 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
| 10834 Polymer.IronA11yAnnouncer = Polymer({ | 12284 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
| 10835 is: 'iron-a11y-announcer', | 12285 var DEFAULT_PHYSICAL_COUNT = 3; |
| 10836 | 12286 var HIDDEN_Y = '-10000px'; |
| 10837 properties: { | 12287 var DEFAULT_GRID_SIZE = 200; |
| 10838 | 12288 var SECRET_TABINDEX = -100; |
| 10839 /** | 12289 |
| 10840 * The value of mode is used to set the `aria-live` attribute | 12290 Polymer({ |
| 10841 * for the element that will be announced. Valid values are: `off`, | 12291 |
| 10842 * `polite` and `assertive`. | 12292 is: 'iron-list', |
| 10843 */ | |
| 10844 mode: { | |
| 10845 type: String, | |
| 10846 value: 'polite' | |
| 10847 }, | |
| 10848 | |
| 10849 _text: { | |
| 10850 type: String, | |
| 10851 value: '' | |
| 10852 } | |
| 10853 }, | |
| 10854 | |
| 10855 created: function() { | |
| 10856 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 10857 Polymer.IronA11yAnnouncer.instance = this; | |
| 10858 } | |
| 10859 | |
| 10860 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
| 10861 }, | |
| 10862 | |
| 10863 /** | |
| 10864 * Cause a text string to be announced by screen readers. | |
| 10865 * | |
| 10866 * @param {string} text The text that should be announced. | |
| 10867 */ | |
| 10868 announce: function(text) { | |
| 10869 this._text = ''; | |
| 10870 this.async(function() { | |
| 10871 this._text = text; | |
| 10872 }, 100); | |
| 10873 }, | |
| 10874 | |
| 10875 _onIronAnnounce: function(event) { | |
| 10876 if (event.detail && event.detail.text) { | |
| 10877 this.announce(event.detail.text); | |
| 10878 } | |
| 10879 } | |
| 10880 }); | |
| 10881 | |
| 10882 Polymer.IronA11yAnnouncer.instance = null; | |
| 10883 | |
| 10884 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
| 10885 if (!Polymer.IronA11yAnnouncer.instance) { | |
| 10886 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
| 10887 } | |
| 10888 | |
| 10889 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
| 10890 }; | |
| 10891 })(); | |
| 10892 /** | |
| 10893 * Singleton IronMeta instance. | |
| 10894 */ | |
| 10895 Polymer.IronValidatableBehaviorMeta = null; | |
| 10896 | |
| 10897 /** | |
| 10898 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
| 10899 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
| 10900 * | |
| 10901 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
| 10902 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
| 10903 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
| 10904 * custom element will get a public `validate()`, which | |
| 10905 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
| 10906 * which can be used for styling. | |
| 10907 * | |
| 10908 * To implement the custom validation logic of your element, you must override | |
| 10909 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
| 10910 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
| 10911 * for an example. | |
| 10912 * | |
| 10913 * ### Accessibility | |
| 10914 * | |
| 10915 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
| 10916 * `aria-invalid` attribute. | |
| 10917 * | |
| 10918 * @demo demo/index.html | |
| 10919 * @polymerBehavior | |
| 10920 */ | |
| 10921 Polymer.IronValidatableBehavior = { | |
| 10922 | 12293 |
| 10923 properties: { | 12294 properties: { |
| 10924 | 12295 |
| 10925 /** | 12296 /** |
| 10926 * Name of the validator to use. | 12297 * An array containing items determining how many instances of the templat
e |
| 12298 * to stamp and that that each template instance should bind to. |
| 10927 */ | 12299 */ |
| 10928 validator: { | 12300 items: { |
| 10929 type: String | 12301 type: Array |
| 10930 }, | 12302 }, |
| 10931 | 12303 |
| 10932 /** | 12304 /** |
| 10933 * True if the last call to `validate` is invalid. | 12305 * The max count of physical items the pool can extend to. |
| 10934 */ | 12306 */ |
| 10935 invalid: { | 12307 maxPhysicalCount: { |
| 10936 notify: true, | 12308 type: Number, |
| 10937 reflectToAttribute: true, | 12309 value: 500 |
| 12310 }, |
| 12311 |
| 12312 /** |
| 12313 * The name of the variable to add to the binding scope for the array |
| 12314 * element associated with a given template instance. |
| 12315 */ |
| 12316 as: { |
| 12317 type: String, |
| 12318 value: 'item' |
| 12319 }, |
| 12320 |
| 12321 /** |
| 12322 * The name of the variable to add to the binding scope with the index |
| 12323 * for the row. |
| 12324 */ |
| 12325 indexAs: { |
| 12326 type: String, |
| 12327 value: 'index' |
| 12328 }, |
| 12329 |
| 12330 /** |
| 12331 * The name of the variable to add to the binding scope to indicate |
| 12332 * if the row is selected. |
| 12333 */ |
| 12334 selectedAs: { |
| 12335 type: String, |
| 12336 value: 'selected' |
| 12337 }, |
| 12338 |
| 12339 /** |
| 12340 * When true, the list is rendered as a grid. Grid items must have |
| 12341 * fixed width and height set via CSS. e.g. |
| 12342 * |
| 12343 * ```html |
| 12344 * <iron-list grid> |
| 12345 * <template> |
| 12346 * <div style="width: 100px; height: 100px;"> 100x100 </div> |
| 12347 * </template> |
| 12348 * </iron-list> |
| 12349 * ``` |
| 12350 */ |
| 12351 grid: { |
| 12352 type: Boolean, |
| 12353 value: false, |
| 12354 reflectToAttribute: true |
| 12355 }, |
| 12356 |
| 12357 /** |
| 12358 * When true, tapping a row will select the item, placing its data model |
| 12359 * in the set of selected items retrievable via the selection property. |
| 12360 * |
| 12361 * Note that tapping focusable elements within the list item will not |
| 12362 * result in selection, since they are presumed to have their * own action
. |
| 12363 */ |
| 12364 selectionEnabled: { |
| 10938 type: Boolean, | 12365 type: Boolean, |
| 10939 value: false | 12366 value: false |
| 10940 }, | 12367 }, |
| 10941 | 12368 |
| 10942 /** | 12369 /** |
| 10943 * This property is deprecated and should not be used. Use the global | 12370 * When `multiSelection` is false, this is the currently selected item, or
`null` |
| 10944 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | 12371 * if no item is selected. |
| 10945 */ | 12372 */ |
| 10946 _validatorMeta: { | 12373 selectedItem: { |
| 10947 type: Object | 12374 type: Object, |
| 12375 notify: true |
| 10948 }, | 12376 }, |
| 10949 | 12377 |
| 10950 /** | 12378 /** |
| 10951 * Namespace for this validator. This property is deprecated and should | 12379 * When `multiSelection` is true, this is an array that contains the selec
ted items. |
| 10952 * not be used. For all intents and purposes, please consider it a | |
| 10953 * read-only, config-time property. | |
| 10954 */ | 12380 */ |
| 10955 validatorType: { | 12381 selectedItems: { |
| 10956 type: String, | |
| 10957 value: 'validator' | |
| 10958 }, | |
| 10959 | |
| 10960 _validator: { | |
| 10961 type: Object, | 12382 type: Object, |
| 10962 computed: '__computeValidator(validator)' | 12383 notify: true |
| 10963 } | 12384 }, |
| 10964 }, | |
| 10965 | |
| 10966 observers: [ | |
| 10967 '_invalidChanged(invalid)' | |
| 10968 ], | |
| 10969 | |
| 10970 registered: function() { | |
| 10971 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
| 10972 }, | |
| 10973 | |
| 10974 _invalidChanged: function() { | |
| 10975 if (this.invalid) { | |
| 10976 this.setAttribute('aria-invalid', 'true'); | |
| 10977 } else { | |
| 10978 this.removeAttribute('aria-invalid'); | |
| 10979 } | |
| 10980 }, | |
| 10981 | |
| 10982 /** | |
| 10983 * @return {boolean} True if the validator `validator` exists. | |
| 10984 */ | |
| 10985 hasValidator: function() { | |
| 10986 return this._validator != null; | |
| 10987 }, | |
| 10988 | |
| 10989 /** | |
| 10990 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
| 10991 * your element to have custom validation logic, do not override this method
; | |
| 10992 * override `_getValidity(value)` instead. | |
| 10993 | |
| 10994 * @param {Object} value The value to be validated. By default, it is passed | |
| 10995 * to the validator's `validate()` function, if a validator is set. | |
| 10996 * @return {boolean} True if `value` is valid. | |
| 10997 */ | |
| 10998 validate: function(value) { | |
| 10999 this.invalid = !this._getValidity(value); | |
| 11000 return !this.invalid; | |
| 11001 }, | |
| 11002 | |
| 11003 /** | |
| 11004 * Returns true if `value` is valid. By default, it is passed | |
| 11005 * to the validator's `validate()` function, if a validator is set. You | |
| 11006 * should override this method if you want to implement custom validity | |
| 11007 * logic for your element. | |
| 11008 * | |
| 11009 * @param {Object} value The value to be validated. | |
| 11010 * @return {boolean} True if `value` is valid. | |
| 11011 */ | |
| 11012 | |
| 11013 _getValidity: function(value) { | |
| 11014 if (this.hasValidator()) { | |
| 11015 return this._validator.validate(value); | |
| 11016 } | |
| 11017 return true; | |
| 11018 }, | |
| 11019 | |
| 11020 __computeValidator: function() { | |
| 11021 return Polymer.IronValidatableBehaviorMeta && | |
| 11022 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
| 11023 } | |
| 11024 }; | |
| 11025 /* | |
| 11026 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
| 11027 to `<input>`. | |
| 11028 | |
| 11029 ### Two-way binding | |
| 11030 | |
| 11031 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
| 11032 | |
| 11033 <input value="{{myValue::input}}"> | |
| 11034 | |
| 11035 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
| 11036 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
| 11037 | |
| 11038 <input is="iron-input" bind-value="{{myValue}}"> | |
| 11039 | |
| 11040 ### Custom validators | |
| 11041 | |
| 11042 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
| 11043 | |
| 11044 <input is="iron-input" validator="my-custom-validator"> | |
| 11045 | |
| 11046 ### Stopping invalid input | |
| 11047 | |
| 11048 It may be desirable to only allow users to enter certain characters. You can use
the | |
| 11049 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
| 11050 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
| 11051 | |
| 11052 \x3c!-- only allow characters that match [0-9] --\x3e | |
| 11053 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
| 11054 | |
| 11055 @hero hero.svg | |
| 11056 @demo demo/index.html | |
| 11057 */ | |
| 11058 | |
| 11059 Polymer({ | |
| 11060 | |
| 11061 is: 'iron-input', | |
| 11062 | |
| 11063 extends: 'input', | |
| 11064 | |
| 11065 behaviors: [ | |
| 11066 Polymer.IronValidatableBehavior | |
| 11067 ], | |
| 11068 | |
| 11069 properties: { | |
| 11070 | 12385 |
| 11071 /** | 12386 /** |
| 11072 * Use this property instead of `value` for two-way data binding. | 12387 * When `true`, multiple items may be selected at once (in this case, |
| 12388 * `selected` is an array of currently selected items). When `false`, |
| 12389 * only one item may be selected at a time. |
| 11073 */ | 12390 */ |
| 11074 bindValue: { | 12391 multiSelection: { |
| 11075 observer: '_bindValueChanged', | |
| 11076 type: String | |
| 11077 }, | |
| 11078 | |
| 11079 /** | |
| 11080 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
| 11081 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
| 11082 * Pasted input will have each character checked individually; if any char
acter | |
| 11083 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
| 11084 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
| 11085 */ | |
| 11086 preventInvalidInput: { | |
| 11087 type: Boolean | |
| 11088 }, | |
| 11089 | |
| 11090 /** | |
| 11091 * Regular expression that list the characters allowed as input. | |
| 11092 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
| 11093 * each individual character will be checked against the pattern (rather t
han checking | |
| 11094 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
| 11095 * for example, `[a-zA-Z0-9.+-!;:]` | |
| 11096 */ | |
| 11097 allowedPattern: { | |
| 11098 type: String, | |
| 11099 observer: "_allowedPatternChanged" | |
| 11100 }, | |
| 11101 | |
| 11102 _previousValidInput: { | |
| 11103 type: String, | |
| 11104 value: '' | |
| 11105 }, | |
| 11106 | |
| 11107 _patternAlreadyChecked: { | |
| 11108 type: Boolean, | 12392 type: Boolean, |
| 11109 value: false | 12393 value: false |
| 11110 } | 12394 } |
| 11111 | 12395 }, |
| 11112 }, | 12396 |
| 11113 | 12397 observers: [ |
| 11114 listeners: { | 12398 '_itemsChanged(items.*)', |
| 11115 'input': '_onInput', | 12399 '_selectionEnabledChanged(selectionEnabled)', |
| 11116 'keypress': '_onKeypress' | 12400 '_multiSelectionChanged(multiSelection)', |
| 11117 }, | 12401 '_setOverflow(scrollTarget)' |
| 11118 | 12402 ], |
| 11119 /** @suppress {checkTypes} */ | 12403 |
| 11120 registered: function() { | 12404 behaviors: [ |
| 11121 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | 12405 Polymer.Templatizer, |
| 11122 if (!this._canDispatchEventOnDisabled()) { | 12406 Polymer.IronResizableBehavior, |
| 11123 this._origDispatchEvent = this.dispatchEvent; | 12407 Polymer.IronA11yKeysBehavior, |
| 11124 this.dispatchEvent = this._dispatchEventFirefoxIE; | 12408 Polymer.IronScrollTargetBehavior |
| 11125 } | 12409 ], |
| 11126 }, | 12410 |
| 11127 | 12411 keyBindings: { |
| 11128 created: function() { | 12412 'up': '_didMoveUp', |
| 11129 Polymer.IronA11yAnnouncer.requestAvailability(); | 12413 'down': '_didMoveDown', |
| 11130 }, | 12414 'enter': '_didEnter' |
| 11131 | 12415 }, |
| 11132 _canDispatchEventOnDisabled: function() { | 12416 |
| 11133 var input = document.createElement('input'); | 12417 /** |
| 11134 var canDispatch = false; | 12418 * The ratio of hidden tiles that should remain in the scroll direction. |
| 11135 input.disabled = true; | 12419 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. |
| 11136 | 12420 */ |
| 11137 input.addEventListener('feature-check-dispatch-event', function() { | 12421 _ratio: 0.5, |
| 11138 canDispatch = true; | 12422 |
| 11139 }); | 12423 /** |
| 11140 | 12424 * The padding-top value for the list. |
| 11141 try { | 12425 */ |
| 11142 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 12426 _scrollerPaddingTop: 0, |
| 11143 } catch(e) {} | 12427 |
| 11144 | 12428 /** |
| 11145 return canDispatch; | 12429 * This value is the same as `scrollTop`. |
| 11146 }, | 12430 */ |
| 11147 | 12431 _scrollPosition: 0, |
| 11148 _dispatchEventFirefoxIE: function() { | 12432 |
| 11149 // Due to Firefox bug, events fired on disabled form controls can throw | 12433 /** |
| 11150 // errors; furthermore, neither IE nor Firefox will actually dispatch | 12434 * The sum of the heights of all the tiles in the DOM. |
| 11151 // events from disabled form controls; as such, we toggle disable around | 12435 */ |
| 11152 // the dispatch to allow notifying properties to notify | 12436 _physicalSize: 0, |
| 11153 // See issue #47 for details | 12437 |
| 11154 var disabled = this.disabled; | 12438 /** |
| 11155 this.disabled = false; | 12439 * The average `offsetHeight` of the tiles observed till now. |
| 11156 this._origDispatchEvent.apply(this, arguments); | 12440 */ |
| 11157 this.disabled = disabled; | 12441 _physicalAverage: 0, |
| 11158 }, | 12442 |
| 11159 | 12443 /** |
| 11160 get _patternRegExp() { | 12444 * The number of tiles which `offsetHeight` > 0 observed until now. |
| 11161 var pattern; | 12445 */ |
| 11162 if (this.allowedPattern) { | 12446 _physicalAverageCount: 0, |
| 11163 pattern = new RegExp(this.allowedPattern); | 12447 |
| 12448 /** |
| 12449 * The Y position of the item rendered in the `_physicalStart` |
| 12450 * tile relative to the scrolling list. |
| 12451 */ |
| 12452 _physicalTop: 0, |
| 12453 |
| 12454 /** |
| 12455 * The number of items in the list. |
| 12456 */ |
| 12457 _virtualCount: 0, |
| 12458 |
| 12459 /** |
| 12460 * A map between an item key and its physical item index |
| 12461 */ |
| 12462 _physicalIndexForKey: null, |
| 12463 |
| 12464 /** |
| 12465 * The estimated scroll height based on `_physicalAverage` |
| 12466 */ |
| 12467 _estScrollHeight: 0, |
| 12468 |
| 12469 /** |
| 12470 * The scroll height of the dom node |
| 12471 */ |
| 12472 _scrollHeight: 0, |
| 12473 |
| 12474 /** |
| 12475 * The height of the list. This is referred as the viewport in the context o
f list. |
| 12476 */ |
| 12477 _viewportHeight: 0, |
| 12478 |
| 12479 /** |
| 12480 * The width of the list. This is referred as the viewport in the context of
list. |
| 12481 */ |
| 12482 _viewportWidth: 0, |
| 12483 |
| 12484 /** |
| 12485 * An array of DOM nodes that are currently in the tree |
| 12486 * @type {?Array<!TemplatizerNode>} |
| 12487 */ |
| 12488 _physicalItems: null, |
| 12489 |
| 12490 /** |
| 12491 * An array of heights for each item in `_physicalItems` |
| 12492 * @type {?Array<number>} |
| 12493 */ |
| 12494 _physicalSizes: null, |
| 12495 |
| 12496 /** |
| 12497 * A cached value for the first visible index. |
| 12498 * See `firstVisibleIndex` |
| 12499 * @type {?number} |
| 12500 */ |
| 12501 _firstVisibleIndexVal: null, |
| 12502 |
| 12503 /** |
| 12504 * A cached value for the last visible index. |
| 12505 * See `lastVisibleIndex` |
| 12506 * @type {?number} |
| 12507 */ |
| 12508 _lastVisibleIndexVal: null, |
| 12509 |
| 12510 /** |
| 12511 * A Polymer collection for the items. |
| 12512 * @type {?Polymer.Collection} |
| 12513 */ |
| 12514 _collection: null, |
| 12515 |
| 12516 /** |
| 12517 * True if the current item list was rendered for the first time |
| 12518 * after attached. |
| 12519 */ |
| 12520 _itemsRendered: false, |
| 12521 |
| 12522 /** |
| 12523 * The page that is currently rendered. |
| 12524 */ |
| 12525 _lastPage: null, |
| 12526 |
| 12527 /** |
| 12528 * The max number of pages to render. One page is equivalent to the height o
f the list. |
| 12529 */ |
| 12530 _maxPages: 3, |
| 12531 |
| 12532 /** |
| 12533 * The currently focused physical item. |
| 12534 */ |
| 12535 _focusedItem: null, |
| 12536 |
| 12537 /** |
| 12538 * The index of the `_focusedItem`. |
| 12539 */ |
| 12540 _focusedIndex: -1, |
| 12541 |
| 12542 /** |
| 12543 * The the item that is focused if it is moved offscreen. |
| 12544 * @private {?TemplatizerNode} |
| 12545 */ |
| 12546 _offscreenFocusedItem: null, |
| 12547 |
| 12548 /** |
| 12549 * The item that backfills the `_offscreenFocusedItem` in the physical items |
| 12550 * list when that item is moved offscreen. |
| 12551 */ |
| 12552 _focusBackfillItem: null, |
| 12553 |
| 12554 /** |
| 12555 * The maximum items per row |
| 12556 */ |
| 12557 _itemsPerRow: 1, |
| 12558 |
| 12559 /** |
| 12560 * The width of each grid item |
| 12561 */ |
| 12562 _itemWidth: 0, |
| 12563 |
| 12564 /** |
| 12565 * The height of the row in grid layout. |
| 12566 */ |
| 12567 _rowHeight: 0, |
| 12568 |
| 12569 /** |
| 12570 * The bottom of the physical content. |
| 12571 */ |
| 12572 get _physicalBottom() { |
| 12573 return this._physicalTop + this._physicalSize; |
| 12574 }, |
| 12575 |
| 12576 /** |
| 12577 * The bottom of the scroll. |
| 12578 */ |
| 12579 get _scrollBottom() { |
| 12580 return this._scrollPosition + this._viewportHeight; |
| 12581 }, |
| 12582 |
| 12583 /** |
| 12584 * The n-th item rendered in the last physical item. |
| 12585 */ |
| 12586 get _virtualEnd() { |
| 12587 return this._virtualStart + this._physicalCount - 1; |
| 12588 }, |
| 12589 |
| 12590 /** |
| 12591 * The height of the physical content that isn't on the screen. |
| 12592 */ |
| 12593 get _hiddenContentSize() { |
| 12594 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
| 12595 return size - this._viewportHeight; |
| 12596 }, |
| 12597 |
| 12598 /** |
| 12599 * The maximum scroll top value. |
| 12600 */ |
| 12601 get _maxScrollTop() { |
| 12602 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
| 12603 }, |
| 12604 |
| 12605 /** |
| 12606 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. |
| 12607 */ |
| 12608 _minVirtualStart: 0, |
| 12609 |
| 12610 /** |
| 12611 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. |
| 12612 */ |
| 12613 get _maxVirtualStart() { |
| 12614 return Math.max(0, this._virtualCount - this._physicalCount); |
| 12615 }, |
| 12616 |
| 12617 /** |
| 12618 * The n-th item rendered in the `_physicalStart` tile. |
| 12619 */ |
| 12620 _virtualStartVal: 0, |
| 12621 |
| 12622 set _virtualStart(val) { |
| 12623 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
| 12624 }, |
| 12625 |
| 12626 get _virtualStart() { |
| 12627 return this._virtualStartVal || 0; |
| 12628 }, |
| 12629 |
| 12630 /** |
| 12631 * The k-th tile that is at the top of the scrolling list. |
| 12632 */ |
| 12633 _physicalStartVal: 0, |
| 12634 |
| 12635 set _physicalStart(val) { |
| 12636 this._physicalStartVal = val % this._physicalCount; |
| 12637 if (this._physicalStartVal < 0) { |
| 12638 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
| 12639 } |
| 12640 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12641 }, |
| 12642 |
| 12643 get _physicalStart() { |
| 12644 return this._physicalStartVal || 0; |
| 12645 }, |
| 12646 |
| 12647 /** |
| 12648 * The number of tiles in the DOM. |
| 12649 */ |
| 12650 _physicalCountVal: 0, |
| 12651 |
| 12652 set _physicalCount(val) { |
| 12653 this._physicalCountVal = val; |
| 12654 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12655 }, |
| 12656 |
| 12657 get _physicalCount() { |
| 12658 return this._physicalCountVal; |
| 12659 }, |
| 12660 |
| 12661 /** |
| 12662 * The k-th tile that is at the bottom of the scrolling list. |
| 12663 */ |
| 12664 _physicalEnd: 0, |
| 12665 |
| 12666 /** |
| 12667 * An optimal physical size such that we will have enough physical items |
| 12668 * to fill up the viewport and recycle when the user scrolls. |
| 12669 * |
| 12670 * This default value assumes that we will at least have the equivalent |
| 12671 * to a viewport of physical items above and below the user's viewport. |
| 12672 */ |
| 12673 get _optPhysicalSize() { |
| 12674 if (this.grid) { |
| 12675 return this._estRowsInView * this._rowHeight * this._maxPages; |
| 12676 } |
| 12677 return this._viewportHeight * this._maxPages; |
| 12678 }, |
| 12679 |
| 12680 get _optPhysicalCount() { |
| 12681 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
| 12682 }, |
| 12683 |
| 12684 /** |
| 12685 * True if the current list is visible. |
| 12686 */ |
| 12687 get _isVisible() { |
| 12688 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
| 12689 }, |
| 12690 |
| 12691 /** |
| 12692 * Gets the index of the first visible item in the viewport. |
| 12693 * |
| 12694 * @type {number} |
| 12695 */ |
| 12696 get firstVisibleIndex() { |
| 12697 if (this._firstVisibleIndexVal === null) { |
| 12698 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
| 12699 |
| 12700 this._firstVisibleIndexVal = this._iterateItems( |
| 12701 function(pidx, vidx) { |
| 12702 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12703 |
| 12704 if (physicalOffset > this._scrollPosition) { |
| 12705 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; |
| 12706 } |
| 12707 // Handle a partially rendered final row in grid mode |
| 12708 if (this.grid && this._virtualCount - 1 === vidx) { |
| 12709 return vidx - (vidx % this._itemsPerRow); |
| 12710 } |
| 12711 }) || 0; |
| 12712 } |
| 12713 return this._firstVisibleIndexVal; |
| 12714 }, |
| 12715 |
| 12716 /** |
| 12717 * Gets the index of the last visible item in the viewport. |
| 12718 * |
| 12719 * @type {number} |
| 12720 */ |
| 12721 get lastVisibleIndex() { |
| 12722 if (this._lastVisibleIndexVal === null) { |
| 12723 if (this.grid) { |
| 12724 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
| 12725 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
| 12726 } else { |
| 12727 var physicalOffset = this._physicalTop; |
| 12728 this._iterateItems(function(pidx, vidx) { |
| 12729 if (physicalOffset < this._scrollBottom) { |
| 12730 this._lastVisibleIndexVal = vidx; |
| 12731 } else { |
| 12732 // Break _iterateItems |
| 12733 return true; |
| 12734 } |
| 12735 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12736 }); |
| 12737 } |
| 12738 } |
| 12739 return this._lastVisibleIndexVal; |
| 12740 }, |
| 12741 |
| 12742 get _defaultScrollTarget() { |
| 12743 return this; |
| 12744 }, |
| 12745 get _virtualRowCount() { |
| 12746 return Math.ceil(this._virtualCount / this._itemsPerRow); |
| 12747 }, |
| 12748 |
| 12749 get _estRowsInView() { |
| 12750 return Math.ceil(this._viewportHeight / this._rowHeight); |
| 12751 }, |
| 12752 |
| 12753 get _physicalRows() { |
| 12754 return Math.ceil(this._physicalCount / this._itemsPerRow); |
| 12755 }, |
| 12756 |
| 12757 ready: function() { |
| 12758 this.addEventListener('focus', this._didFocus.bind(this), true); |
| 12759 }, |
| 12760 |
| 12761 attached: function() { |
| 12762 this.updateViewportBoundaries(); |
| 12763 this._render(); |
| 12764 // `iron-resize` is fired when the list is attached if the event is added |
| 12765 // before attached causing unnecessary work. |
| 12766 this.listen(this, 'iron-resize', '_resizeHandler'); |
| 12767 }, |
| 12768 |
| 12769 detached: function() { |
| 12770 this._itemsRendered = false; |
| 12771 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
| 12772 }, |
| 12773 |
| 12774 /** |
| 12775 * Set the overflow property if this element has its own scrolling region |
| 12776 */ |
| 12777 _setOverflow: function(scrollTarget) { |
| 12778 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
| 12779 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 12780 }, |
| 12781 |
| 12782 /** |
| 12783 * Invoke this method if you dynamically update the viewport's |
| 12784 * size or CSS padding. |
| 12785 * |
| 12786 * @method updateViewportBoundaries |
| 12787 */ |
| 12788 updateViewportBoundaries: function() { |
| 12789 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : |
| 12790 parseInt(window.getComputedStyle(this)['padding-top'], 10); |
| 12791 |
| 12792 this._viewportHeight = this._scrollTargetHeight; |
| 12793 if (this.grid) { |
| 12794 this._updateGridMetrics(); |
| 12795 } |
| 12796 }, |
| 12797 |
| 12798 /** |
| 12799 * Update the models, the position of the |
| 12800 * items in the viewport and recycle tiles as needed. |
| 12801 */ |
| 12802 _scrollHandler: function() { |
| 12803 // clamp the `scrollTop` value |
| 12804 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
| 12805 var delta = scrollTop - this._scrollPosition; |
| 12806 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
| 12807 var ratio = this._ratio; |
| 12808 var recycledTiles = 0; |
| 12809 var hiddenContentSize = this._hiddenContentSize; |
| 12810 var currentRatio = ratio; |
| 12811 var movingUp = []; |
| 12812 |
| 12813 // track the last `scrollTop` |
| 12814 this._scrollPosition = scrollTop; |
| 12815 |
| 12816 // clear cached visible indexes |
| 12817 this._firstVisibleIndexVal = null; |
| 12818 this._lastVisibleIndexVal = null; |
| 12819 |
| 12820 scrollBottom = this._scrollBottom; |
| 12821 physicalBottom = this._physicalBottom; |
| 12822 |
| 12823 // random access |
| 12824 if (Math.abs(delta) > this._physicalSize) { |
| 12825 this._physicalTop += delta; |
| 12826 recycledTiles = Math.round(delta / this._physicalAverage); |
| 12827 } |
| 12828 // scroll up |
| 12829 else if (delta < 0) { |
| 12830 var topSpace = scrollTop - this._physicalTop; |
| 12831 var virtualStart = this._virtualStart; |
| 12832 |
| 12833 recycledTileSet = []; |
| 12834 |
| 12835 kth = this._physicalEnd; |
| 12836 currentRatio = topSpace / hiddenContentSize; |
| 12837 |
| 12838 // move tiles from bottom to top |
| 12839 while ( |
| 12840 // approximate `currentRatio` to `ratio` |
| 12841 currentRatio < ratio && |
| 12842 // recycle less physical items than the total |
| 12843 recycledTiles < this._physicalCount && |
| 12844 // ensure that these recycled tiles are needed |
| 12845 virtualStart - recycledTiles > 0 && |
| 12846 // ensure that the tile is not visible |
| 12847 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom |
| 12848 ) { |
| 12849 |
| 12850 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12851 currentRatio += tileHeight / hiddenContentSize; |
| 12852 physicalBottom -= tileHeight; |
| 12853 recycledTileSet.push(kth); |
| 12854 recycledTiles++; |
| 12855 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; |
| 12856 } |
| 12857 |
| 12858 movingUp = recycledTileSet; |
| 12859 recycledTiles = -recycledTiles; |
| 12860 } |
| 12861 // scroll down |
| 12862 else if (delta > 0) { |
| 12863 var bottomSpace = physicalBottom - scrollBottom; |
| 12864 var virtualEnd = this._virtualEnd; |
| 12865 var lastVirtualItemIndex = this._virtualCount-1; |
| 12866 |
| 12867 recycledTileSet = []; |
| 12868 |
| 12869 kth = this._physicalStart; |
| 12870 currentRatio = bottomSpace / hiddenContentSize; |
| 12871 |
| 12872 // move tiles from top to bottom |
| 12873 while ( |
| 12874 // approximate `currentRatio` to `ratio` |
| 12875 currentRatio < ratio && |
| 12876 // recycle less physical items than the total |
| 12877 recycledTiles < this._physicalCount && |
| 12878 // ensure that these recycled tiles are needed |
| 12879 virtualEnd + recycledTiles < lastVirtualItemIndex && |
| 12880 // ensure that the tile is not visible |
| 12881 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop |
| 12882 ) { |
| 12883 |
| 12884 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12885 currentRatio += tileHeight / hiddenContentSize; |
| 12886 |
| 12887 this._physicalTop += tileHeight; |
| 12888 recycledTileSet.push(kth); |
| 12889 recycledTiles++; |
| 12890 kth = (kth + 1) % this._physicalCount; |
| 12891 } |
| 12892 } |
| 12893 |
| 12894 if (recycledTiles === 0) { |
| 12895 // Try to increase the pool if the list's client height isn't filled up
with physical items |
| 12896 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
| 12897 this._increasePoolIfNeeded(); |
| 12898 } |
| 11164 } else { | 12899 } else { |
| 11165 switch (this.type) { | 12900 this._virtualStart = this._virtualStart + recycledTiles; |
| 11166 case 'number': | 12901 this._physicalStart = this._physicalStart + recycledTiles; |
| 11167 pattern = /[0-9.,e-]/; | 12902 this._update(recycledTileSet, movingUp); |
| 11168 break; | 12903 } |
| 11169 } | 12904 }, |
| 11170 } | 12905 |
| 11171 return pattern; | 12906 /** |
| 11172 }, | 12907 * Update the list of items, starting from the `_virtualStart` item. |
| 11173 | 12908 * @param {!Array<number>=} itemSet |
| 11174 ready: function() { | 12909 * @param {!Array<number>=} movingUp |
| 11175 this.bindValue = this.value; | 12910 */ |
| 11176 }, | 12911 _update: function(itemSet, movingUp) { |
| 11177 | 12912 // manage focus |
| 11178 /** | 12913 this._manageFocus(); |
| 11179 * @suppress {checkTypes} | 12914 // update models |
| 11180 */ | 12915 this._assignModels(itemSet); |
| 11181 _bindValueChanged: function() { | 12916 // measure heights |
| 11182 if (this.value !== this.bindValue) { | 12917 this._updateMetrics(itemSet); |
| 11183 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | 12918 // adjust offset after measuring |
| 11184 } | 12919 if (movingUp) { |
| 11185 // manually notify because we don't want to notify until after setting val
ue | 12920 while (movingUp.length) { |
| 11186 this.fire('bind-value-changed', {value: this.bindValue}); | 12921 var idx = movingUp.pop(); |
| 11187 }, | 12922 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
| 11188 | 12923 } |
| 11189 _allowedPatternChanged: function() { | 12924 } |
| 11190 // Force to prevent invalid input when an `allowed-pattern` is set | 12925 // update the position of the items |
| 11191 this.preventInvalidInput = this.allowedPattern ? true : false; | 12926 this._positionItems(); |
| 11192 }, | 12927 // set the scroller size |
| 11193 | 12928 this._updateScrollerSize(); |
| 11194 _onInput: function() { | 12929 // increase the pool of physical items |
| 11195 // Need to validate each of the characters pasted if they haven't | 12930 this._increasePoolIfNeeded(); |
| 11196 // been validated inside `_onKeypress` already. | 12931 }, |
| 11197 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | 12932 |
| 11198 var valid = this._checkPatternValidity(); | 12933 /** |
| 11199 if (!valid) { | 12934 * Creates a pool of DOM elements and attaches them to the local dom. |
| 11200 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | 12935 */ |
| 11201 this.value = this._previousValidInput; | 12936 _createPool: function(size) { |
| 11202 } | 12937 var physicalItems = new Array(size); |
| 11203 } | 12938 |
| 11204 | 12939 this._ensureTemplatized(); |
| 11205 this.bindValue = this.value; | 12940 |
| 11206 this._previousValidInput = this.value; | 12941 for (var i = 0; i < size; i++) { |
| 11207 this._patternAlreadyChecked = false; | 12942 var inst = this.stamp(null); |
| 11208 }, | 12943 // First element child is item; Safari doesn't support children[0] |
| 11209 | 12944 // on a doc fragment |
| 11210 _isPrintable: function(event) { | 12945 physicalItems[i] = inst.root.querySelector('*'); |
| 11211 // What a control/printable character is varies wildly based on the browse
r. | 12946 Polymer.dom(this).appendChild(inst.root); |
| 11212 // - most control characters (arrows, backspace) do not send a `keypress`
event | 12947 } |
| 11213 // in Chrome, but the *do* on Firefox | 12948 return physicalItems; |
| 11214 // - in Firefox, when they do send a `keypress` event, control chars have | 12949 }, |
| 11215 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | 12950 |
| 11216 // - printable characters always send a keypress event. | 12951 /** |
| 11217 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | 12952 * Increases the pool of physical items only if needed. |
| 11218 // always matches the charCode. | 12953 * |
| 11219 // None of this makes any sense. | 12954 * @return {boolean} True if the pool was increased. |
| 11220 | 12955 */ |
| 11221 // For these keys, ASCII code == browser keycode. | 12956 _increasePoolIfNeeded: function() { |
| 11222 var anyNonPrintable = | 12957 // Base case 1: the list has no height. |
| 11223 (event.keyCode == 8) || // backspace | 12958 if (this._viewportHeight === 0) { |
| 11224 (event.keyCode == 9) || // tab | 12959 return false; |
| 11225 (event.keyCode == 13) || // enter | 12960 } |
| 11226 (event.keyCode == 27); // escape | 12961 // Base case 2: If the physical size is optimal and the list's client heig
ht is full |
| 11227 | 12962 // with physical items, don't increase the pool. |
| 11228 // For these keys, make sure it's a browser keycode and not an ASCII code. | 12963 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
| 11229 var mozNonPrintable = | 12964 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
| 11230 (event.keyCode == 19) || // pause | 12965 return false; |
| 11231 (event.keyCode == 20) || // caps lock | 12966 } |
| 11232 (event.keyCode == 45) || // insert | 12967 // this value should range between [0 <= `currentPage` <= `_maxPages`] |
| 11233 (event.keyCode == 46) || // delete | 12968 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
| 11234 (event.keyCode == 144) || // num lock | 12969 |
| 11235 (event.keyCode == 145) || // scroll lock | 12970 if (currentPage === 0) { |
| 11236 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | 12971 // fill the first page |
| 11237 (event.keyCode > 111 && event.keyCode < 124); // fn keys | 12972 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); |
| 11238 | 12973 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
| 11239 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | 12974 // paint the page and defer the next increase |
| 11240 }, | 12975 // wait 16ms which is rough enough to get paint cycle. |
| 11241 | 12976 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
| 11242 _onKeypress: function(event) { | 12977 } else { |
| 11243 if (!this.preventInvalidInput && this.type !== 'number') { | 12978 // fill the rest of the pages |
| 12979 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
| 12980 } |
| 12981 |
| 12982 this._lastPage = currentPage; |
| 12983 |
| 12984 return true; |
| 12985 }, |
| 12986 |
| 12987 /** |
| 12988 * Increases the pool size. |
| 12989 */ |
| 12990 _increasePool: function(missingItems) { |
| 12991 var nextPhysicalCount = Math.min( |
| 12992 this._physicalCount + missingItems, |
| 12993 this._virtualCount - this._virtualStart, |
| 12994 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) |
| 12995 ); |
| 12996 var prevPhysicalCount = this._physicalCount; |
| 12997 var delta = nextPhysicalCount - prevPhysicalCount; |
| 12998 |
| 12999 if (delta <= 0) { |
| 11244 return; | 13000 return; |
| 11245 } | 13001 } |
| 11246 var regexp = this._patternRegExp; | 13002 |
| 11247 if (!regexp) { | 13003 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 13004 [].push.apply(this._physicalSizes, new Array(delta)); |
| 13005 |
| 13006 this._physicalCount = prevPhysicalCount + delta; |
| 13007 |
| 13008 // update the physical start if we need to preserve the model of the focus
ed item. |
| 13009 // In this situation, the focused item is currently rendered and its model
would |
| 13010 // have changed after increasing the pool if the physical start remained u
nchanged. |
| 13011 if (this._physicalStart > this._physicalEnd && |
| 13012 this._isIndexRendered(this._focusedIndex) && |
| 13013 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { |
| 13014 this._physicalStart = this._physicalStart + delta; |
| 13015 } |
| 13016 this._update(); |
| 13017 }, |
| 13018 |
| 13019 /** |
| 13020 * Render a new list of items. This method does exactly the same as `update`
, |
| 13021 * but it also ensures that only one `update` cycle is created. |
| 13022 */ |
| 13023 _render: function() { |
| 13024 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
| 13025 |
| 13026 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 13027 this._lastPage = 0; |
| 13028 this._update(); |
| 13029 this._itemsRendered = true; |
| 13030 } |
| 13031 }, |
| 13032 |
| 13033 /** |
| 13034 * Templetizes the user template. |
| 13035 */ |
| 13036 _ensureTemplatized: function() { |
| 13037 if (!this.ctor) { |
| 13038 // Template instance props that should be excluded from forwarding |
| 13039 var props = {}; |
| 13040 props.__key__ = true; |
| 13041 props[this.as] = true; |
| 13042 props[this.indexAs] = true; |
| 13043 props[this.selectedAs] = true; |
| 13044 props.tabIndex = true; |
| 13045 |
| 13046 this._instanceProps = props; |
| 13047 this._userTemplate = Polymer.dom(this).querySelector('template'); |
| 13048 |
| 13049 if (this._userTemplate) { |
| 13050 this.templatize(this._userTemplate); |
| 13051 } else { |
| 13052 console.warn('iron-list requires a template to be provided in light-do
m'); |
| 13053 } |
| 13054 } |
| 13055 }, |
| 13056 |
| 13057 /** |
| 13058 * Implements extension point from Templatizer mixin. |
| 13059 */ |
| 13060 _getStampedChildren: function() { |
| 13061 return this._physicalItems; |
| 13062 }, |
| 13063 |
| 13064 /** |
| 13065 * Implements extension point from Templatizer |
| 13066 * Called as a side effect of a template instance path change, responsible |
| 13067 * for notifying items.<key-for-instance>.<path> change up to host. |
| 13068 */ |
| 13069 _forwardInstancePath: function(inst, path, value) { |
| 13070 if (path.indexOf(this.as + '.') === 0) { |
| 13071 this.notifyPath('items.' + inst.__key__ + '.' + |
| 13072 path.slice(this.as.length + 1), value); |
| 13073 } |
| 13074 }, |
| 13075 |
| 13076 /** |
| 13077 * Implements extension point from Templatizer mixin |
| 13078 * Called as side-effect of a host property change, responsible for |
| 13079 * notifying parent path change on each row. |
| 13080 */ |
| 13081 _forwardParentProp: function(prop, value) { |
| 13082 if (this._physicalItems) { |
| 13083 this._physicalItems.forEach(function(item) { |
| 13084 item._templateInstance[prop] = value; |
| 13085 }, this); |
| 13086 } |
| 13087 }, |
| 13088 |
| 13089 /** |
| 13090 * Implements extension point from Templatizer |
| 13091 * Called as side-effect of a host path change, responsible for |
| 13092 * notifying parent.<path> path change on each row. |
| 13093 */ |
| 13094 _forwardParentPath: function(path, value) { |
| 13095 if (this._physicalItems) { |
| 13096 this._physicalItems.forEach(function(item) { |
| 13097 item._templateInstance.notifyPath(path, value, true); |
| 13098 }, this); |
| 13099 } |
| 13100 }, |
| 13101 |
| 13102 /** |
| 13103 * Called as a side effect of a host items.<key>.<path> path change, |
| 13104 * responsible for notifying item.<path> changes. |
| 13105 */ |
| 13106 _forwardItemPath: function(path, value) { |
| 13107 if (!this._physicalIndexForKey) { |
| 11248 return; | 13108 return; |
| 11249 } | 13109 } |
| 11250 | 13110 var dot = path.indexOf('.'); |
| 11251 // Handle special keys and backspace | 13111 var key = path.substring(0, dot < 0 ? path.length : dot); |
| 11252 if (event.metaKey || event.ctrlKey || event.altKey) | 13112 var idx = this._physicalIndexForKey[key]; |
| 13113 var offscreenItem = this._offscreenFocusedItem; |
| 13114 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? |
| 13115 offscreenItem : this._physicalItems[idx]; |
| 13116 |
| 13117 if (!el || el._templateInstance.__key__ !== key) { |
| 11253 return; | 13118 return; |
| 11254 | 13119 } |
| 11255 // Check the pattern either here or in `_onInput`, but not in both. | 13120 if (dot >= 0) { |
| 11256 this._patternAlreadyChecked = true; | 13121 path = this.as + '.' + path.substring(dot+1); |
| 11257 | 13122 el._templateInstance.notifyPath(path, value, true); |
| 11258 var thisChar = String.fromCharCode(event.charCode); | 13123 } else { |
| 11259 if (this._isPrintable(event) && !regexp.test(thisChar)) { | 13124 // Update selection if needed |
| 11260 event.preventDefault(); | 13125 var currentItem = el._templateInstance[this.as]; |
| 11261 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | 13126 if (Array.isArray(this.selectedItems)) { |
| 11262 } | 13127 for (var i = 0; i < this.selectedItems.length; i++) { |
| 11263 }, | 13128 if (this.selectedItems[i] === currentItem) { |
| 11264 | 13129 this.set('selectedItems.' + i, value); |
| 11265 _checkPatternValidity: function() { | 13130 break; |
| 11266 var regexp = this._patternRegExp; | 13131 } |
| 11267 if (!regexp) { | 13132 } |
| 11268 return true; | 13133 } else if (this.selectedItem === currentItem) { |
| 11269 } | 13134 this.set('selectedItem', value); |
| 11270 for (var i = 0; i < this.value.length; i++) { | 13135 } |
| 11271 if (!regexp.test(this.value[i])) { | 13136 el._templateInstance[this.as] = value; |
| 11272 return false; | 13137 } |
| 11273 } | 13138 }, |
| 11274 } | 13139 |
| 11275 return true; | 13140 /** |
| 11276 }, | 13141 * Called when the items have changed. That is, ressignments |
| 11277 | 13142 * to `items`, splices or updates to a single item. |
| 11278 /** | 13143 */ |
| 11279 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | 13144 _itemsChanged: function(change) { |
| 11280 * then any constraints. | 13145 if (change.path === 'items') { |
| 11281 * @return {boolean} True if the value is valid. | 13146 // reset items |
| 11282 */ | 13147 this._virtualStart = 0; |
| 11283 validate: function() { | 13148 this._physicalTop = 0; |
| 11284 // First, check what the browser thinks. Some inputs (like type=number) | 13149 this._virtualCount = this.items ? this.items.length : 0; |
| 11285 // behave weirdly and will set the value to "" if something invalid is | 13150 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
| 11286 // entered, but will set the validity correctly. | 13151 this._physicalIndexForKey = {}; |
| 11287 var valid = this.checkValidity(); | 13152 this._firstVisibleIndexVal = null; |
| 11288 | 13153 this._lastVisibleIndexVal = null; |
| 11289 // Only do extra checking if the browser thought this was valid. | 13154 |
| 11290 if (valid) { | 13155 this._resetScrollPosition(0); |
| 11291 // Empty, required input is invalid | 13156 this._removeFocusedItem(); |
| 11292 if (this.required && this.value === '') { | 13157 // create the initial physical items |
| 11293 valid = false; | 13158 if (!this._physicalItems) { |
| 11294 } else if (this.hasValidator()) { | 13159 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
| 11295 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | 13160 this._physicalItems = this._createPool(this._physicalCount); |
| 11296 } | 13161 this._physicalSizes = new Array(this._physicalCount); |
| 11297 } | 13162 } |
| 11298 | 13163 |
| 11299 this.invalid = !valid; | 13164 this._physicalStart = 0; |
| 11300 this.fire('iron-input-validate'); | 13165 |
| 11301 return valid; | 13166 } else if (change.path === 'items.splices') { |
| 11302 }, | 13167 |
| 11303 | 13168 this._adjustVirtualIndex(change.value.indexSplices); |
| 11304 _announceInvalidCharacter: function(message) { | 13169 this._virtualCount = this.items ? this.items.length : 0; |
| 11305 this.fire('iron-announce', { text: message }); | 13170 |
| 13171 } else { |
| 13172 // update a single item |
| 13173 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
| 13174 return; |
| 13175 } |
| 13176 |
| 13177 this._itemsRendered = false; |
| 13178 this._debounceTemplate(this._render); |
| 13179 }, |
| 13180 |
| 13181 /** |
| 13182 * @param {!Array<!PolymerSplice>} splices |
| 13183 */ |
| 13184 _adjustVirtualIndex: function(splices) { |
| 13185 splices.forEach(function(splice) { |
| 13186 // deselect removed items |
| 13187 splice.removed.forEach(this._removeItem, this); |
| 13188 // We only need to care about changes happening above the current positi
on |
| 13189 if (splice.index < this._virtualStart) { |
| 13190 var delta = Math.max( |
| 13191 splice.addedCount - splice.removed.length, |
| 13192 splice.index - this._virtualStart); |
| 13193 |
| 13194 this._virtualStart = this._virtualStart + delta; |
| 13195 |
| 13196 if (this._focusedIndex >= 0) { |
| 13197 this._focusedIndex = this._focusedIndex + delta; |
| 13198 } |
| 13199 } |
| 13200 }, this); |
| 13201 }, |
| 13202 |
| 13203 _removeItem: function(item) { |
| 13204 this.$.selector.deselect(item); |
| 13205 // remove the current focused item |
| 13206 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
| 13207 this._removeFocusedItem(); |
| 13208 } |
| 13209 }, |
| 13210 |
| 13211 /** |
| 13212 * Executes a provided function per every physical index in `itemSet` |
| 13213 * `itemSet` default value is equivalent to the entire set of physical index
es. |
| 13214 * |
| 13215 * @param {!function(number, number)} fn |
| 13216 * @param {!Array<number>=} itemSet |
| 13217 */ |
| 13218 _iterateItems: function(fn, itemSet) { |
| 13219 var pidx, vidx, rtn, i; |
| 13220 |
| 13221 if (arguments.length === 2 && itemSet) { |
| 13222 for (i = 0; i < itemSet.length; i++) { |
| 13223 pidx = itemSet[i]; |
| 13224 vidx = this._computeVidx(pidx); |
| 13225 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13226 return rtn; |
| 13227 } |
| 13228 } |
| 13229 } else { |
| 13230 pidx = this._physicalStart; |
| 13231 vidx = this._virtualStart; |
| 13232 |
| 13233 for (; pidx < this._physicalCount; pidx++, vidx++) { |
| 13234 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13235 return rtn; |
| 13236 } |
| 13237 } |
| 13238 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
| 13239 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13240 return rtn; |
| 13241 } |
| 13242 } |
| 13243 } |
| 13244 }, |
| 13245 |
| 13246 /** |
| 13247 * Returns the virtual index for a given physical index |
| 13248 * |
| 13249 * @param {number} pidx Physical index |
| 13250 * @return {number} |
| 13251 */ |
| 13252 _computeVidx: function(pidx) { |
| 13253 if (pidx >= this._physicalStart) { |
| 13254 return this._virtualStart + (pidx - this._physicalStart); |
| 13255 } |
| 13256 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
| 13257 }, |
| 13258 |
| 13259 /** |
| 13260 * Assigns the data models to a given set of items. |
| 13261 * @param {!Array<number>=} itemSet |
| 13262 */ |
| 13263 _assignModels: function(itemSet) { |
| 13264 this._iterateItems(function(pidx, vidx) { |
| 13265 var el = this._physicalItems[pidx]; |
| 13266 var inst = el._templateInstance; |
| 13267 var item = this.items && this.items[vidx]; |
| 13268 |
| 13269 if (item != null) { |
| 13270 inst[this.as] = item; |
| 13271 inst.__key__ = this._collection.getKey(item); |
| 13272 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); |
| 13273 inst[this.indexAs] = vidx; |
| 13274 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
| 13275 this._physicalIndexForKey[inst.__key__] = pidx; |
| 13276 el.removeAttribute('hidden'); |
| 13277 } else { |
| 13278 inst.__key__ = null; |
| 13279 el.setAttribute('hidden', ''); |
| 13280 } |
| 13281 }, itemSet); |
| 13282 }, |
| 13283 |
| 13284 /** |
| 13285 * Updates the height for a given set of items. |
| 13286 * |
| 13287 * @param {!Array<number>=} itemSet |
| 13288 */ |
| 13289 _updateMetrics: function(itemSet) { |
| 13290 // Make sure we distributed all the physical items |
| 13291 // so we can measure them |
| 13292 Polymer.dom.flush(); |
| 13293 |
| 13294 var newPhysicalSize = 0; |
| 13295 var oldPhysicalSize = 0; |
| 13296 var prevAvgCount = this._physicalAverageCount; |
| 13297 var prevPhysicalAvg = this._physicalAverage; |
| 13298 |
| 13299 this._iterateItems(function(pidx, vidx) { |
| 13300 |
| 13301 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
| 13302 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
| 13303 newPhysicalSize += this._physicalSizes[pidx]; |
| 13304 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| 13305 |
| 13306 }, itemSet); |
| 13307 |
| 13308 this._viewportHeight = this._scrollTargetHeight; |
| 13309 if (this.grid) { |
| 13310 this._updateGridMetrics(); |
| 13311 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
| 13312 } else { |
| 13313 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
| 13314 } |
| 13315 |
| 13316 // update the average if we measured something |
| 13317 if (this._physicalAverageCount !== prevAvgCount) { |
| 13318 this._physicalAverage = Math.round( |
| 13319 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
| 13320 this._physicalAverageCount); |
| 13321 } |
| 13322 }, |
| 13323 |
| 13324 _updateGridMetrics: function() { |
| 13325 this._viewportWidth = this.$.items.offsetWidth; |
| 13326 // Set item width to the value of the _physicalItems offsetWidth |
| 13327 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
| 13328 // Set row height to the value of the _physicalItems offsetHeight |
| 13329 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
| 13330 // If in grid mode compute how many items with exist in each row |
| 13331 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
| 13332 }, |
| 13333 |
| 13334 /** |
| 13335 * Updates the position of the physical items. |
| 13336 */ |
| 13337 _positionItems: function() { |
| 13338 this._adjustScrollPosition(); |
| 13339 |
| 13340 var y = this._physicalTop; |
| 13341 |
| 13342 if (this.grid) { |
| 13343 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
| 13344 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
| 13345 |
| 13346 this._iterateItems(function(pidx, vidx) { |
| 13347 |
| 13348 var modulus = vidx % this._itemsPerRow; |
| 13349 var x = Math.floor((modulus * this._itemWidth) + rowOffset); |
| 13350 |
| 13351 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
| 13352 |
| 13353 if (this._shouldRenderNextRow(vidx)) { |
| 13354 y += this._rowHeight; |
| 13355 } |
| 13356 |
| 13357 }); |
| 13358 } else { |
| 13359 this._iterateItems(function(pidx, vidx) { |
| 13360 |
| 13361 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
| 13362 y += this._physicalSizes[pidx]; |
| 13363 |
| 13364 }); |
| 13365 } |
| 13366 }, |
| 13367 |
| 13368 _getPhysicalSizeIncrement: function(pidx) { |
| 13369 if (!this.grid) { |
| 13370 return this._physicalSizes[pidx]; |
| 13371 } |
| 13372 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
| 13373 return 0; |
| 13374 } |
| 13375 return this._rowHeight; |
| 13376 }, |
| 13377 |
| 13378 /** |
| 13379 * Returns, based on the current index, |
| 13380 * whether or not the next index will need |
| 13381 * to be rendered on a new row. |
| 13382 * |
| 13383 * @param {number} vidx Virtual index |
| 13384 * @return {boolean} |
| 13385 */ |
| 13386 _shouldRenderNextRow: function(vidx) { |
| 13387 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
| 13388 }, |
| 13389 |
| 13390 /** |
| 13391 * Adjusts the scroll position when it was overestimated. |
| 13392 */ |
| 13393 _adjustScrollPosition: function() { |
| 13394 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
| 13395 Math.min(this._scrollPosition + this._physicalTop, 0); |
| 13396 |
| 13397 if (deltaHeight) { |
| 13398 this._physicalTop = this._physicalTop - deltaHeight; |
| 13399 // juking scroll position during interial scrolling on iOS is no bueno |
| 13400 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
| 13401 this._resetScrollPosition(this._scrollTop - deltaHeight); |
| 13402 } |
| 13403 } |
| 13404 }, |
| 13405 |
| 13406 /** |
| 13407 * Sets the position of the scroll. |
| 13408 */ |
| 13409 _resetScrollPosition: function(pos) { |
| 13410 if (this.scrollTarget) { |
| 13411 this._scrollTop = pos; |
| 13412 this._scrollPosition = this._scrollTop; |
| 13413 } |
| 13414 }, |
| 13415 |
| 13416 /** |
| 13417 * Sets the scroll height, that's the height of the content, |
| 13418 * |
| 13419 * @param {boolean=} forceUpdate If true, updates the height no matter what. |
| 13420 */ |
| 13421 _updateScrollerSize: function(forceUpdate) { |
| 13422 if (this.grid) { |
| 13423 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
| 13424 } else { |
| 13425 this._estScrollHeight = (this._physicalBottom + |
| 13426 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); |
| 13427 } |
| 13428 |
| 13429 forceUpdate = forceUpdate || this._scrollHeight === 0; |
| 13430 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
| 13431 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
| 13432 |
| 13433 // amortize height adjustment, so it won't trigger repaints very often |
| 13434 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
| 13435 this.$.items.style.height = this._estScrollHeight + 'px'; |
| 13436 this._scrollHeight = this._estScrollHeight; |
| 13437 } |
| 13438 }, |
| 13439 |
| 13440 /** |
| 13441 * Scroll to a specific item in the virtual list regardless |
| 13442 * of the physical items in the DOM tree. |
| 13443 * |
| 13444 * @method scrollToItem |
| 13445 * @param {(Object)} item The item to be scrolled to |
| 13446 */ |
| 13447 scrollToItem: function(item){ |
| 13448 return this.scrollToIndex(this.items.indexOf(item)); |
| 13449 }, |
| 13450 |
| 13451 /** |
| 13452 * Scroll to a specific index in the virtual list regardless |
| 13453 * of the physical items in the DOM tree. |
| 13454 * |
| 13455 * @method scrollToIndex |
| 13456 * @param {number} idx The index of the item |
| 13457 */ |
| 13458 scrollToIndex: function(idx) { |
| 13459 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
| 13460 return; |
| 13461 } |
| 13462 |
| 13463 Polymer.dom.flush(); |
| 13464 |
| 13465 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
| 13466 // update the virtual start only when needed |
| 13467 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
| 13468 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); |
| 13469 } |
| 13470 // manage focus |
| 13471 this._manageFocus(); |
| 13472 // assign new models |
| 13473 this._assignModels(); |
| 13474 // measure the new sizes |
| 13475 this._updateMetrics(); |
| 13476 |
| 13477 // estimate new physical offset |
| 13478 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
| 13479 this._physicalTop = estPhysicalTop; |
| 13480 |
| 13481 var currentTopItem = this._physicalStart; |
| 13482 var currentVirtualItem = this._virtualStart; |
| 13483 var targetOffsetTop = 0; |
| 13484 var hiddenContentSize = this._hiddenContentSize; |
| 13485 |
| 13486 // scroll to the item as much as we can |
| 13487 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
| 13488 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
| 13489 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
| 13490 currentVirtualItem++; |
| 13491 } |
| 13492 // update the scroller size |
| 13493 this._updateScrollerSize(true); |
| 13494 // update the position of the items |
| 13495 this._positionItems(); |
| 13496 // set the new scroll position |
| 13497 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
| 13498 // increase the pool of physical items if needed |
| 13499 this._increasePoolIfNeeded(); |
| 13500 // clear cached visible index |
| 13501 this._firstVisibleIndexVal = null; |
| 13502 this._lastVisibleIndexVal = null; |
| 13503 }, |
| 13504 |
| 13505 /** |
| 13506 * Reset the physical average and the average count. |
| 13507 */ |
| 13508 _resetAverage: function() { |
| 13509 this._physicalAverage = 0; |
| 13510 this._physicalAverageCount = 0; |
| 13511 }, |
| 13512 |
| 13513 /** |
| 13514 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` |
| 13515 * when the element is resized. |
| 13516 */ |
| 13517 _resizeHandler: function() { |
| 13518 // iOS fires the resize event when the address bar slides up |
| 13519 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
| 13520 return; |
| 13521 } |
| 13522 // In Desktop Safari 9.0.3, if the scroll bars are always shown, |
| 13523 // changing the scroll position from a resize handler would result in |
| 13524 // the scroll position being reset. Waiting 1ms fixes the issue. |
| 13525 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
| 13526 this.updateViewportBoundaries(); |
| 13527 this._render(); |
| 13528 |
| 13529 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
| 13530 this._resetAverage(); |
| 13531 this.scrollToIndex(this.firstVisibleIndex); |
| 13532 } |
| 13533 }.bind(this), 1)); |
| 13534 }, |
| 13535 |
| 13536 _getModelFromItem: function(item) { |
| 13537 var key = this._collection.getKey(item); |
| 13538 var pidx = this._physicalIndexForKey[key]; |
| 13539 |
| 13540 if (pidx != null) { |
| 13541 return this._physicalItems[pidx]._templateInstance; |
| 13542 } |
| 13543 return null; |
| 13544 }, |
| 13545 |
| 13546 /** |
| 13547 * Gets a valid item instance from its index or the object value. |
| 13548 * |
| 13549 * @param {(Object|number)} item The item object or its index |
| 13550 */ |
| 13551 _getNormalizedItem: function(item) { |
| 13552 if (this._collection.getKey(item) === undefined) { |
| 13553 if (typeof item === 'number') { |
| 13554 item = this.items[item]; |
| 13555 if (!item) { |
| 13556 throw new RangeError('<item> not found'); |
| 13557 } |
| 13558 return item; |
| 13559 } |
| 13560 throw new TypeError('<item> should be a valid item'); |
| 13561 } |
| 13562 return item; |
| 13563 }, |
| 13564 |
| 13565 /** |
| 13566 * Select the list item at the given index. |
| 13567 * |
| 13568 * @method selectItem |
| 13569 * @param {(Object|number)} item The item object or its index |
| 13570 */ |
| 13571 selectItem: function(item) { |
| 13572 item = this._getNormalizedItem(item); |
| 13573 var model = this._getModelFromItem(item); |
| 13574 |
| 13575 if (!this.multiSelection && this.selectedItem) { |
| 13576 this.deselectItem(this.selectedItem); |
| 13577 } |
| 13578 if (model) { |
| 13579 model[this.selectedAs] = true; |
| 13580 } |
| 13581 this.$.selector.select(item); |
| 13582 this.updateSizeForItem(item); |
| 13583 }, |
| 13584 |
| 13585 /** |
| 13586 * Deselects the given item list if it is already selected. |
| 13587 * |
| 13588 |
| 13589 * @method deselect |
| 13590 * @param {(Object|number)} item The item object or its index |
| 13591 */ |
| 13592 deselectItem: function(item) { |
| 13593 item = this._getNormalizedItem(item); |
| 13594 var model = this._getModelFromItem(item); |
| 13595 |
| 13596 if (model) { |
| 13597 model[this.selectedAs] = false; |
| 13598 } |
| 13599 this.$.selector.deselect(item); |
| 13600 this.updateSizeForItem(item); |
| 13601 }, |
| 13602 |
| 13603 /** |
| 13604 * Select or deselect a given item depending on whether the item |
| 13605 * has already been selected. |
| 13606 * |
| 13607 * @method toggleSelectionForItem |
| 13608 * @param {(Object|number)} item The item object or its index |
| 13609 */ |
| 13610 toggleSelectionForItem: function(item) { |
| 13611 item = this._getNormalizedItem(item); |
| 13612 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { |
| 13613 this.deselectItem(item); |
| 13614 } else { |
| 13615 this.selectItem(item); |
| 13616 } |
| 13617 }, |
| 13618 |
| 13619 /** |
| 13620 * Clears the current selection state of the list. |
| 13621 * |
| 13622 * @method clearSelection |
| 13623 */ |
| 13624 clearSelection: function() { |
| 13625 function unselect(item) { |
| 13626 var model = this._getModelFromItem(item); |
| 13627 if (model) { |
| 13628 model[this.selectedAs] = false; |
| 13629 } |
| 13630 } |
| 13631 |
| 13632 if (Array.isArray(this.selectedItems)) { |
| 13633 this.selectedItems.forEach(unselect, this); |
| 13634 } else if (this.selectedItem) { |
| 13635 unselect.call(this, this.selectedItem); |
| 13636 } |
| 13637 |
| 13638 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
| 13639 }, |
| 13640 |
| 13641 /** |
| 13642 * Add an event listener to `tap` if `selectionEnabled` is true, |
| 13643 * it will remove the listener otherwise. |
| 13644 */ |
| 13645 _selectionEnabledChanged: function(selectionEnabled) { |
| 13646 var handler = selectionEnabled ? this.listen : this.unlisten; |
| 13647 handler.call(this, this, 'tap', '_selectionHandler'); |
| 13648 }, |
| 13649 |
| 13650 /** |
| 13651 * Select an item from an event object. |
| 13652 */ |
| 13653 _selectionHandler: function(e) { |
| 13654 var model = this.modelForElement(e.target); |
| 13655 if (!model) { |
| 13656 return; |
| 13657 } |
| 13658 var modelTabIndex, activeElTabIndex; |
| 13659 var target = Polymer.dom(e).path[0]; |
| 13660 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
| 13661 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
| 13662 // Safari does not focus certain form controls via mouse |
| 13663 // https://bugs.webkit.org/show_bug.cgi?id=118043 |
| 13664 if (target.localName === 'input' || |
| 13665 target.localName === 'button' || |
| 13666 target.localName === 'select') { |
| 13667 return; |
| 13668 } |
| 13669 // Set a temporary tabindex |
| 13670 modelTabIndex = model.tabIndex; |
| 13671 model.tabIndex = SECRET_TABINDEX; |
| 13672 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
| 13673 model.tabIndex = modelTabIndex; |
| 13674 // Only select the item if the tap wasn't on a focusable child |
| 13675 // or the element bound to `tabIndex` |
| 13676 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
| 13677 return; |
| 13678 } |
| 13679 this.toggleSelectionForItem(model[this.as]); |
| 13680 }, |
| 13681 |
| 13682 _multiSelectionChanged: function(multiSelection) { |
| 13683 this.clearSelection(); |
| 13684 this.$.selector.multi = multiSelection; |
| 13685 }, |
| 13686 |
| 13687 /** |
| 13688 * Updates the size of an item. |
| 13689 * |
| 13690 * @method updateSizeForItem |
| 13691 * @param {(Object|number)} item The item object or its index |
| 13692 */ |
| 13693 updateSizeForItem: function(item) { |
| 13694 item = this._getNormalizedItem(item); |
| 13695 var key = this._collection.getKey(item); |
| 13696 var pidx = this._physicalIndexForKey[key]; |
| 13697 |
| 13698 if (pidx != null) { |
| 13699 this._updateMetrics([pidx]); |
| 13700 this._positionItems(); |
| 13701 } |
| 13702 }, |
| 13703 |
| 13704 /** |
| 13705 * Creates a temporary backfill item in the rendered pool of physical items |
| 13706 * to replace the main focused item. The focused item has tabIndex = 0 |
| 13707 * and might be currently focused by the user. |
| 13708 * |
| 13709 * This dynamic replacement helps to preserve the focus state. |
| 13710 */ |
| 13711 _manageFocus: function() { |
| 13712 var fidx = this._focusedIndex; |
| 13713 |
| 13714 if (fidx >= 0 && fidx < this._virtualCount) { |
| 13715 // if it's a valid index, check if that index is rendered |
| 13716 // in a physical item. |
| 13717 if (this._isIndexRendered(fidx)) { |
| 13718 this._restoreFocusedItem(); |
| 13719 } else { |
| 13720 this._createFocusBackfillItem(); |
| 13721 } |
| 13722 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
| 13723 // otherwise, assign the initial focused index. |
| 13724 this._focusedIndex = this._virtualStart; |
| 13725 this._focusedItem = this._physicalItems[this._physicalStart]; |
| 13726 } |
| 13727 }, |
| 13728 |
| 13729 _isIndexRendered: function(idx) { |
| 13730 return idx >= this._virtualStart && idx <= this._virtualEnd; |
| 13731 }, |
| 13732 |
| 13733 _isIndexVisible: function(idx) { |
| 13734 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
| 13735 }, |
| 13736 |
| 13737 _getPhysicalIndex: function(idx) { |
| 13738 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
| 13739 }, |
| 13740 |
| 13741 _focusPhysicalItem: function(idx) { |
| 13742 if (idx < 0 || idx >= this._virtualCount) { |
| 13743 return; |
| 13744 } |
| 13745 this._restoreFocusedItem(); |
| 13746 // scroll to index to make sure it's rendered |
| 13747 if (!this._isIndexRendered(idx)) { |
| 13748 this.scrollToIndex(idx); |
| 13749 } |
| 13750 |
| 13751 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
| 13752 var model = physicalItem._templateInstance; |
| 13753 var focusable; |
| 13754 |
| 13755 // set a secret tab index |
| 13756 model.tabIndex = SECRET_TABINDEX; |
| 13757 // check if focusable element is the physical item |
| 13758 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
| 13759 focusable = physicalItem; |
| 13760 } |
| 13761 // search for the element which tabindex is bound to the secret tab index |
| 13762 if (!focusable) { |
| 13763 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
| 13764 } |
| 13765 // restore the tab index |
| 13766 model.tabIndex = 0; |
| 13767 // focus the focusable element |
| 13768 this._focusedIndex = idx; |
| 13769 focusable && focusable.focus(); |
| 13770 }, |
| 13771 |
| 13772 _removeFocusedItem: function() { |
| 13773 if (this._offscreenFocusedItem) { |
| 13774 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
| 13775 } |
| 13776 this._offscreenFocusedItem = null; |
| 13777 this._focusBackfillItem = null; |
| 13778 this._focusedItem = null; |
| 13779 this._focusedIndex = -1; |
| 13780 }, |
| 13781 |
| 13782 _createFocusBackfillItem: function() { |
| 13783 var pidx, fidx = this._focusedIndex; |
| 13784 if (this._offscreenFocusedItem || fidx < 0) { |
| 13785 return; |
| 13786 } |
| 13787 if (!this._focusBackfillItem) { |
| 13788 // create a physical item, so that it backfills the focused item. |
| 13789 var stampedTemplate = this.stamp(null); |
| 13790 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
| 13791 Polymer.dom(this).appendChild(stampedTemplate.root); |
| 13792 } |
| 13793 // get the physical index for the focused index |
| 13794 pidx = this._getPhysicalIndex(fidx); |
| 13795 |
| 13796 if (pidx != null) { |
| 13797 // set the offcreen focused physical item |
| 13798 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 13799 // backfill the focused physical item |
| 13800 this._physicalItems[pidx] = this._focusBackfillItem; |
| 13801 // hide the focused physical |
| 13802 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 13803 } |
| 13804 }, |
| 13805 |
| 13806 _restoreFocusedItem: function() { |
| 13807 var pidx, fidx = this._focusedIndex; |
| 13808 |
| 13809 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
| 13810 return; |
| 13811 } |
| 13812 // assign models to the focused index |
| 13813 this._assignModels(); |
| 13814 // get the new physical index for the focused index |
| 13815 pidx = this._getPhysicalIndex(fidx); |
| 13816 |
| 13817 if (pidx != null) { |
| 13818 // flip the focus backfill |
| 13819 this._focusBackfillItem = this._physicalItems[pidx]; |
| 13820 // restore the focused physical item |
| 13821 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 13822 // reset the offscreen focused item |
| 13823 this._offscreenFocusedItem = null; |
| 13824 // hide the physical item that backfills |
| 13825 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
| 13826 } |
| 13827 }, |
| 13828 |
| 13829 _didFocus: function(e) { |
| 13830 var targetModel = this.modelForElement(e.target); |
| 13831 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
| 13832 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
| 13833 var fidx = this._focusedIndex; |
| 13834 |
| 13835 if (!targetModel || !focusedModel) { |
| 13836 return; |
| 13837 } |
| 13838 if (focusedModel === targetModel) { |
| 13839 // if the user focused the same item, then bring it into view if it's no
t visible |
| 13840 if (!this._isIndexVisible(fidx)) { |
| 13841 this.scrollToIndex(fidx); |
| 13842 } |
| 13843 } else { |
| 13844 this._restoreFocusedItem(); |
| 13845 // restore tabIndex for the currently focused item |
| 13846 focusedModel.tabIndex = -1; |
| 13847 // set the tabIndex for the next focused item |
| 13848 targetModel.tabIndex = 0; |
| 13849 fidx = targetModel[this.indexAs]; |
| 13850 this._focusedIndex = fidx; |
| 13851 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
| 13852 |
| 13853 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
| 13854 this._update(); |
| 13855 } |
| 13856 } |
| 13857 }, |
| 13858 |
| 13859 _didMoveUp: function() { |
| 13860 this._focusPhysicalItem(this._focusedIndex - 1); |
| 13861 }, |
| 13862 |
| 13863 _didMoveDown: function(e) { |
| 13864 // disable scroll when pressing the down key |
| 13865 e.detail.keyboardEvent.preventDefault(); |
| 13866 this._focusPhysicalItem(this._focusedIndex + 1); |
| 13867 }, |
| 13868 |
| 13869 _didEnter: function(e) { |
| 13870 this._focusPhysicalItem(this._focusedIndex); |
| 13871 this._selectionHandler(e.detail.keyboardEvent); |
| 11306 } | 13872 } |
| 11307 }); | 13873 }); |
| 11308 | 13874 |
| 11309 /* | 13875 })(); |
| 11310 The `iron-input-validate` event is fired whenever `validate()` is called. | |
| 11311 @event iron-input-validate | |
| 11312 */ | |
| 11313 Polymer({ | 13876 Polymer({ |
| 11314 is: 'paper-input-container', | 13877 |
| 13878 is: 'iron-scroll-threshold', |
| 11315 | 13879 |
| 11316 properties: { | 13880 properties: { |
| 13881 |
| 11317 /** | 13882 /** |
| 11318 * Set to true to disable the floating label. The label disappears when th
e input value is | 13883 * Distance from the top (or left, for horizontal) bound of the scroller |
| 11319 * not null. | 13884 * where the "upper trigger" will fire. |
| 11320 */ | 13885 */ |
| 11321 noLabelFloat: { | 13886 upperThreshold: { |
| 13887 type: Number, |
| 13888 value: 100 |
| 13889 }, |
| 13890 |
| 13891 /** |
| 13892 * Distance from the bottom (or right, for horizontal) bound of the scroll
er |
| 13893 * where the "lower trigger" will fire. |
| 13894 */ |
| 13895 lowerThreshold: { |
| 13896 type: Number, |
| 13897 value: 100 |
| 13898 }, |
| 13899 |
| 13900 /** |
| 13901 * Read-only value that tracks the triggered state of the upper threshold. |
| 13902 */ |
| 13903 upperTriggered: { |
| 13904 type: Boolean, |
| 13905 value: false, |
| 13906 notify: true, |
| 13907 readOnly: true |
| 13908 }, |
| 13909 |
| 13910 /** |
| 13911 * Read-only value that tracks the triggered state of the lower threshold. |
| 13912 */ |
| 13913 lowerTriggered: { |
| 13914 type: Boolean, |
| 13915 value: false, |
| 13916 notify: true, |
| 13917 readOnly: true |
| 13918 }, |
| 13919 |
| 13920 /** |
| 13921 * True if the orientation of the scroller is horizontal. |
| 13922 */ |
| 13923 horizontal: { |
| 11322 type: Boolean, | 13924 type: Boolean, |
| 11323 value: false | 13925 value: false |
| 11324 }, | 13926 } |
| 11325 | 13927 }, |
| 11326 /** | 13928 |
| 11327 * Set to true to always float the floating label. | 13929 behaviors: [ |
| 11328 */ | 13930 Polymer.IronScrollTargetBehavior |
| 11329 alwaysFloatLabel: { | 13931 ], |
| 11330 type: Boolean, | 13932 |
| 11331 value: false | 13933 observers: [ |
| 11332 }, | 13934 '_setOverflow(scrollTarget)', |
| 11333 | 13935 '_initCheck(horizontal, isAttached)' |
| 11334 /** | 13936 ], |
| 11335 * The attribute to listen for value changes on. | 13937 |
| 11336 */ | 13938 get _defaultScrollTarget() { |
| 11337 attrForValue: { | 13939 return this; |
| 11338 type: String, | 13940 }, |
| 11339 value: 'bind-value' | 13941 |
| 11340 }, | 13942 _setOverflow: function(scrollTarget) { |
| 11341 | 13943 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 11342 /** | 13944 }, |
| 11343 * Set to true to auto-validate the input value when it changes. | 13945 |
| 11344 */ | 13946 _scrollHandler: function() { |
| 11345 autoValidate: { | 13947 // throttle the work on the scroll event |
| 11346 type: Boolean, | 13948 var THROTTLE_THRESHOLD = 200; |
| 11347 value: false | 13949 if (!this.isDebouncerActive('_checkTheshold')) { |
| 11348 }, | 13950 this.debounce('_checkTheshold', function() { |
| 11349 | 13951 this.checkScrollThesholds(); |
| 11350 /** | 13952 }, THROTTLE_THRESHOLD); |
| 11351 * True if the input is invalid. This property is set automatically when t
he input value | 13953 } |
| 11352 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | 13954 }, |
| 11353 */ | 13955 |
| 11354 invalid: { | 13956 _initCheck: function(horizontal, isAttached) { |
| 11355 observer: '_invalidChanged', | 13957 if (isAttached) { |
| 11356 type: Boolean, | 13958 this.debounce('_init', function() { |
| 11357 value: false | 13959 this.clearTriggers(); |
| 11358 }, | 13960 this.checkScrollThesholds(); |
| 11359 | 13961 }); |
| 11360 /** | 13962 } |
| 11361 * True if the input has focus. | 13963 }, |
| 11362 */ | 13964 |
| 11363 focused: { | 13965 /** |
| 11364 readOnly: true, | 13966 * Checks the scroll thresholds. |
| 11365 type: Boolean, | 13967 * This method is automatically called by iron-scroll-threshold. |
| 11366 value: false, | 13968 * |
| 11367 notify: true | 13969 * @method checkScrollThesholds |
| 11368 }, | 13970 */ |
| 11369 | 13971 checkScrollThesholds: function() { |
| 11370 _addons: { | 13972 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) { |
| 11371 type: Array | 13973 return; |
| 11372 // do not set a default value here intentionally - it will be initialize
d lazily when a | 13974 } |
| 11373 // distributed child is attached, which may occur before configuration f
or this element | 13975 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo
p; |
| 11374 // in polyfill. | 13976 var lowerScrollValue = this.horizontal ? |
| 11375 }, | 13977 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll
Left : |
| 11376 | 13978 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._
scrollTop; |
| 11377 _inputHasContent: { | 13979 |
| 11378 type: Boolean, | 13980 // Detect upper threshold |
| 11379 value: false | 13981 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { |
| 11380 }, | 13982 this._setUpperTriggered(true); |
| 11381 | 13983 this.fire('upper-threshold'); |
| 11382 _inputSelector: { | 13984 } |
| 11383 type: String, | 13985 // Detect lower threshold |
| 11384 value: 'input,textarea,.paper-input-input' | 13986 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { |
| 11385 }, | 13987 this._setLowerTriggered(true); |
| 11386 | 13988 this.fire('lower-threshold'); |
| 11387 _boundOnFocus: { | 13989 } |
| 11388 type: Function, | 13990 }, |
| 11389 value: function() { | 13991 |
| 11390 return this._onFocus.bind(this); | 13992 /** |
| 11391 } | 13993 * Clear the upper and lower threshold states. |
| 11392 }, | 13994 * |
| 11393 | 13995 * @method clearTriggers |
| 11394 _boundOnBlur: { | 13996 */ |
| 11395 type: Function, | 13997 clearTriggers: function() { |
| 11396 value: function() { | 13998 this._setUpperTriggered(false); |
| 11397 return this._onBlur.bind(this); | 13999 this._setLowerTriggered(false); |
| 11398 } | |
| 11399 }, | |
| 11400 | |
| 11401 _boundOnInput: { | |
| 11402 type: Function, | |
| 11403 value: function() { | |
| 11404 return this._onInput.bind(this); | |
| 11405 } | |
| 11406 }, | |
| 11407 | |
| 11408 _boundValueChanged: { | |
| 11409 type: Function, | |
| 11410 value: function() { | |
| 11411 return this._onValueChanged.bind(this); | |
| 11412 } | |
| 11413 } | |
| 11414 }, | |
| 11415 | |
| 11416 listeners: { | |
| 11417 'addon-attached': '_onAddonAttached', | |
| 11418 'iron-input-validate': '_onIronInputValidate' | |
| 11419 }, | |
| 11420 | |
| 11421 get _valueChangedEvent() { | |
| 11422 return this.attrForValue + '-changed'; | |
| 11423 }, | |
| 11424 | |
| 11425 get _propertyForValue() { | |
| 11426 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
| 11427 }, | |
| 11428 | |
| 11429 get _inputElement() { | |
| 11430 return Polymer.dom(this).querySelector(this._inputSelector); | |
| 11431 }, | |
| 11432 | |
| 11433 get _inputElementValue() { | |
| 11434 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
| 11435 }, | |
| 11436 | |
| 11437 ready: function() { | |
| 11438 if (!this._addons) { | |
| 11439 this._addons = []; | |
| 11440 } | |
| 11441 this.addEventListener('focus', this._boundOnFocus, true); | |
| 11442 this.addEventListener('blur', this._boundOnBlur, true); | |
| 11443 }, | |
| 11444 | |
| 11445 attached: function() { | |
| 11446 if (this.attrForValue) { | |
| 11447 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
| 11448 } else { | |
| 11449 this.addEventListener('input', this._onInput); | |
| 11450 } | |
| 11451 | |
| 11452 // Only validate when attached if the input already has a value. | |
| 11453 if (this._inputElementValue != '') { | |
| 11454 this._handleValueAndAutoValidate(this._inputElement); | |
| 11455 } else { | |
| 11456 this._handleValue(this._inputElement); | |
| 11457 } | |
| 11458 }, | |
| 11459 | |
| 11460 _onAddonAttached: function(event) { | |
| 11461 if (!this._addons) { | |
| 11462 this._addons = []; | |
| 11463 } | |
| 11464 var target = event.target; | |
| 11465 if (this._addons.indexOf(target) === -1) { | |
| 11466 this._addons.push(target); | |
| 11467 if (this.isAttached) { | |
| 11468 this._handleValue(this._inputElement); | |
| 11469 } | |
| 11470 } | |
| 11471 }, | |
| 11472 | |
| 11473 _onFocus: function() { | |
| 11474 this._setFocused(true); | |
| 11475 }, | |
| 11476 | |
| 11477 _onBlur: function() { | |
| 11478 this._setFocused(false); | |
| 11479 this._handleValueAndAutoValidate(this._inputElement); | |
| 11480 }, | |
| 11481 | |
| 11482 _onInput: function(event) { | |
| 11483 this._handleValueAndAutoValidate(event.target); | |
| 11484 }, | |
| 11485 | |
| 11486 _onValueChanged: function(event) { | |
| 11487 this._handleValueAndAutoValidate(event.target); | |
| 11488 }, | |
| 11489 | |
| 11490 _handleValue: function(inputElement) { | |
| 11491 var value = this._inputElementValue; | |
| 11492 | |
| 11493 // type="number" hack needed because this.value is empty until it's valid | |
| 11494 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
| 11495 this._inputHasContent = true; | |
| 11496 } else { | |
| 11497 this._inputHasContent = false; | |
| 11498 } | |
| 11499 | |
| 11500 this.updateAddons({ | |
| 11501 inputElement: inputElement, | |
| 11502 value: value, | |
| 11503 invalid: this.invalid | |
| 11504 }); | |
| 11505 }, | |
| 11506 | |
| 11507 _handleValueAndAutoValidate: function(inputElement) { | |
| 11508 if (this.autoValidate) { | |
| 11509 var valid; | |
| 11510 if (inputElement.validate) { | |
| 11511 valid = inputElement.validate(this._inputElementValue); | |
| 11512 } else { | |
| 11513 valid = inputElement.checkValidity(); | |
| 11514 } | |
| 11515 this.invalid = !valid; | |
| 11516 } | |
| 11517 | |
| 11518 // Call this last to notify the add-ons. | |
| 11519 this._handleValue(inputElement); | |
| 11520 }, | |
| 11521 | |
| 11522 _onIronInputValidate: function(event) { | |
| 11523 this.invalid = this._inputElement.invalid; | |
| 11524 }, | |
| 11525 | |
| 11526 _invalidChanged: function() { | |
| 11527 if (this._addons) { | |
| 11528 this.updateAddons({invalid: this.invalid}); | |
| 11529 } | |
| 11530 }, | |
| 11531 | |
| 11532 /** | |
| 11533 * Call this to update the state of add-ons. | |
| 11534 * @param {Object} state Add-on state. | |
| 11535 */ | |
| 11536 updateAddons: function(state) { | |
| 11537 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
| 11538 addon.update(state); | |
| 11539 } | |
| 11540 }, | |
| 11541 | |
| 11542 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
| 11543 var cls = 'input-content'; | |
| 11544 if (!noLabelFloat) { | |
| 11545 var label = this.querySelector('label'); | |
| 11546 | |
| 11547 if (alwaysFloatLabel || _inputHasContent) { | |
| 11548 cls += ' label-is-floating'; | |
| 11549 // If the label is floating, ignore any offsets that may have been | |
| 11550 // applied from a prefix element. | |
| 11551 this.$.labelAndInputContainer.style.position = 'static'; | |
| 11552 | |
| 11553 if (invalid) { | |
| 11554 cls += ' is-invalid'; | |
| 11555 } else if (focused) { | |
| 11556 cls += " label-is-highlighted"; | |
| 11557 } | |
| 11558 } else { | |
| 11559 // When the label is not floating, it should overlap the input element
. | |
| 11560 if (label) { | |
| 11561 this.$.labelAndInputContainer.style.position = 'relative'; | |
| 11562 } | |
| 11563 } | |
| 11564 } else { | |
| 11565 if (_inputHasContent) { | |
| 11566 cls += ' label-is-hidden'; | |
| 11567 } | |
| 11568 } | |
| 11569 return cls; | |
| 11570 }, | |
| 11571 | |
| 11572 _computeUnderlineClass: function(focused, invalid) { | |
| 11573 var cls = 'underline'; | |
| 11574 if (invalid) { | |
| 11575 cls += ' is-invalid'; | |
| 11576 } else if (focused) { | |
| 11577 cls += ' is-highlighted' | |
| 11578 } | |
| 11579 return cls; | |
| 11580 }, | |
| 11581 | |
| 11582 _computeAddOnContentClass: function(focused, invalid) { | |
| 11583 var cls = 'add-on-content'; | |
| 11584 if (invalid) { | |
| 11585 cls += ' is-invalid'; | |
| 11586 } else if (focused) { | |
| 11587 cls += ' is-highlighted' | |
| 11588 } | |
| 11589 return cls; | |
| 11590 } | 14000 } |
| 14001 |
| 14002 /** |
| 14003 * Fires when the lower threshold has been reached. |
| 14004 * |
| 14005 * @event lower-threshold |
| 14006 */ |
| 14007 |
| 14008 /** |
| 14009 * Fires when the upper threshold has been reached. |
| 14010 * |
| 14011 * @event upper-threshold |
| 14012 */ |
| 14013 |
| 11591 }); | 14014 }); |
| 11592 // Copyright 2015 The Chromium Authors. All rights reserved. | 14015 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11593 // Use of this source code is governed by a BSD-style license that can be | 14016 // Use of this source code is governed by a BSD-style license that can be |
| 11594 // found in the LICENSE file. | 14017 // found in the LICENSE file. |
| 11595 | 14018 |
| 11596 var SearchField = Polymer({ | 14019 Polymer({ |
| 11597 is: 'cr-search-field', | 14020 is: 'history-list', |
| 11598 | 14021 |
| 11599 behaviors: [CrSearchFieldBehavior], | 14022 behaviors: [HistoryListBehavior], |
| 11600 | 14023 |
| 11601 properties: { | 14024 properties: { |
| 11602 value_: String, | 14025 // The search term for the current query. Set when the query returns. |
| 11603 }, | 14026 searchedTerm: { |
| 11604 | 14027 type: String, |
| 11605 /** @return {!HTMLInputElement} */ | 14028 value: '', |
| 11606 getSearchInput: function() { | 14029 }, |
| 11607 return this.$.searchInput; | 14030 |
| 11608 }, | 14031 lastSearchedTerm_: String, |
| 11609 | 14032 |
| 11610 /** @private */ | 14033 querying: Boolean, |
| 11611 clearSearch_: function() { | 14034 |
| 11612 this.setValue(''); | 14035 // An array of history entries in reverse chronological order. |
| 11613 this.getSearchInput().focus(); | 14036 historyData_: Array, |
| 11614 }, | 14037 |
| 11615 | 14038 resultLoadingDisabled_: { |
| 11616 /** @private */ | 14039 type: Boolean, |
| 11617 toggleShowingSearch_: function() { | 14040 value: false, |
| 11618 this.showingSearch = !this.showingSearch; | 14041 }, |
| 14042 }, |
| 14043 |
| 14044 listeners: { |
| 14045 'infinite-list.scroll': 'notifyListScroll_', |
| 14046 'remove-bookmark-stars': 'removeBookmarkStars_', |
| 14047 }, |
| 14048 |
| 14049 /** @override */ |
| 14050 attached: function() { |
| 14051 // It is possible (eg, when middle clicking the reload button) for all other |
| 14052 // resize events to fire before the list is attached and can be measured. |
| 14053 // Adding another resize here ensures it will get sized correctly. |
| 14054 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize(); |
| 14055 }, |
| 14056 |
| 14057 /** |
| 14058 * Remove bookmark star for history items with matching URLs. |
| 14059 * @param {{detail: !string}} e |
| 14060 * @private |
| 14061 */ |
| 14062 removeBookmarkStars_: function(e) { |
| 14063 var url = e.detail; |
| 14064 |
| 14065 if (this.historyData_ === undefined) |
| 14066 return; |
| 14067 |
| 14068 for (var i = 0; i < this.historyData_.length; i++) { |
| 14069 if (this.historyData_[i].url == url) |
| 14070 this.set('historyData_.' + i + '.starred', false); |
| 14071 } |
| 14072 }, |
| 14073 |
| 14074 /** |
| 14075 * Disables history result loading when there are no more history results. |
| 14076 */ |
| 14077 disableResultLoading: function() { |
| 14078 this.resultLoadingDisabled_ = true; |
| 14079 }, |
| 14080 |
| 14081 /** |
| 14082 * Adds the newly updated history results into historyData_. Adds new fields |
| 14083 * for each result. |
| 14084 * @param {!Array<!HistoryEntry>} historyResults The new history results. |
| 14085 */ |
| 14086 addNewResults: function(historyResults) { |
| 14087 var results = historyResults.slice(); |
| 14088 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) |
| 14089 .clearTriggers(); |
| 14090 |
| 14091 if (this.lastSearchedTerm_ != this.searchedTerm) { |
| 14092 this.resultLoadingDisabled_ = false; |
| 14093 if (this.historyData_) |
| 14094 this.splice('historyData_', 0, this.historyData_.length); |
| 14095 this.fire('unselect-all'); |
| 14096 this.lastSearchedTerm_ = this.searchedTerm; |
| 14097 } |
| 14098 |
| 14099 if (this.historyData_) { |
| 14100 // If we have previously received data, push the new items onto the |
| 14101 // existing array. |
| 14102 results.unshift('historyData_'); |
| 14103 this.push.apply(this, results); |
| 14104 } else { |
| 14105 // The first time we receive data, use set() to ensure the iron-list is |
| 14106 // initialized correctly. |
| 14107 this.set('historyData_', results); |
| 14108 } |
| 14109 }, |
| 14110 |
| 14111 /** |
| 14112 * Called when the page is scrolled to near the bottom of the list. |
| 14113 * @private |
| 14114 */ |
| 14115 loadMoreData_: function() { |
| 14116 if (this.resultLoadingDisabled_ || this.querying) |
| 14117 return; |
| 14118 |
| 14119 this.fire('load-more-history'); |
| 14120 }, |
| 14121 |
| 14122 /** |
| 14123 * Check whether the time difference between the given history item and the |
| 14124 * next one is large enough for a spacer to be required. |
| 14125 * @param {HistoryEntry} item |
| 14126 * @param {number} index The index of |item| in |historyData_|. |
| 14127 * @param {number} length The length of |historyData_|. |
| 14128 * @return {boolean} Whether or not time gap separator is required. |
| 14129 * @private |
| 14130 */ |
| 14131 needsTimeGap_: function(item, index, length) { |
| 14132 return md_history.HistoryItem.needsTimeGap( |
| 14133 this.historyData_, index, this.searchedTerm); |
| 14134 }, |
| 14135 |
| 14136 /** |
| 14137 * True if the given item is the beginning of a new card. |
| 14138 * @param {HistoryEntry} item |
| 14139 * @param {number} i Index of |item| within |historyData_|. |
| 14140 * @param {number} length |
| 14141 * @return {boolean} |
| 14142 * @private |
| 14143 */ |
| 14144 isCardStart_: function(item, i, length) { |
| 14145 if (length == 0 || i > length - 1) |
| 14146 return false; |
| 14147 return i == 0 || |
| 14148 this.historyData_[i].dateRelativeDay != |
| 14149 this.historyData_[i - 1].dateRelativeDay; |
| 14150 }, |
| 14151 |
| 14152 /** |
| 14153 * True if the given item is the end of a card. |
| 14154 * @param {HistoryEntry} item |
| 14155 * @param {number} i Index of |item| within |historyData_|. |
| 14156 * @param {number} length |
| 14157 * @return {boolean} |
| 14158 * @private |
| 14159 */ |
| 14160 isCardEnd_: function(item, i, length) { |
| 14161 if (length == 0 || i > length - 1) |
| 14162 return false; |
| 14163 return i == length - 1 || |
| 14164 this.historyData_[i].dateRelativeDay != |
| 14165 this.historyData_[i + 1].dateRelativeDay; |
| 14166 }, |
| 14167 |
| 14168 /** |
| 14169 * @param {number} index |
| 14170 * @return {boolean} |
| 14171 * @private |
| 14172 */ |
| 14173 isFirstItem_: function(index) { |
| 14174 return index == 0; |
| 14175 }, |
| 14176 |
| 14177 /** |
| 14178 * @private |
| 14179 */ |
| 14180 notifyListScroll_: function() { |
| 14181 this.fire('history-list-scrolled'); |
| 14182 }, |
| 14183 |
| 14184 /** |
| 14185 * @param {number} index |
| 14186 * @return {string} |
| 14187 * @private |
| 14188 */ |
| 14189 pathForItem_: function(index) { |
| 14190 return 'historyData_.' + index; |
| 11619 }, | 14191 }, |
| 11620 }); | 14192 }); |
| 11621 // Copyright 2015 The Chromium Authors. All rights reserved. | 14193 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11622 // Use of this source code is governed by a BSD-style license that can be | 14194 // Use of this source code is governed by a BSD-style license that can be |
| 11623 // found in the LICENSE file. | 14195 // found in the LICENSE file. |
| 11624 | 14196 |
| 11625 cr.define('downloads', function() { | 14197 Polymer({ |
| 11626 var Toolbar = Polymer({ | 14198 is: 'history-list-container', |
| 11627 is: 'downloads-toolbar', | 14199 |
| 11628 | 14200 properties: { |
| 11629 attached: function() { | 14201 // The path of the currently selected page. |
| 11630 // isRTL() only works after i18n_template.js runs to set <html dir>. | 14202 selectedPage_: String, |
| 11631 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 14203 |
| 11632 }, | 14204 // Whether domain-grouped history is enabled. |
| 11633 | 14205 grouped: Boolean, |
| 11634 properties: { | 14206 |
| 11635 downloadsShowing: { | 14207 /** @type {!QueryState} */ |
| 11636 reflectToAttribute: true, | 14208 queryState: Object, |
| 11637 type: Boolean, | 14209 |
| 11638 value: false, | 14210 /** @type {!QueryResult} */ |
| 11639 observer: 'downloadsShowingChanged_', | 14211 queryResult: Object, |
| 11640 }, | 14212 }, |
| 11641 | 14213 |
| 11642 overflowAlign_: { | 14214 observers: [ |
| 11643 type: String, | 14215 'groupedRangeChanged_(queryState.range)', |
| 11644 value: 'right', | 14216 ], |
| 11645 }, | 14217 |
| 11646 }, | 14218 listeners: { |
| 11647 | 14219 'history-list-scrolled': 'closeMenu_', |
| 11648 listeners: { | 14220 'load-more-history': 'loadMoreHistory_', |
| 11649 'paper-dropdown-close': 'onPaperDropdownClose_', | 14221 'toggle-menu': 'toggleMenu_', |
| 11650 'paper-dropdown-open': 'onPaperDropdownOpen_', | 14222 }, |
| 11651 }, | 14223 |
| 11652 | 14224 /** |
| 11653 /** @return {boolean} Whether removal can be undone. */ | 14225 * @param {HistoryQuery} info An object containing information about the |
| 11654 canUndo: function() { | 14226 * query. |
| 11655 return this.$['search-input'] != this.shadowRoot.activeElement; | 14227 * @param {!Array<HistoryEntry>} results A list of results. |
| 11656 }, | 14228 */ |
| 11657 | 14229 historyResult: function(info, results) { |
| 11658 /** @return {boolean} Whether "Clear all" should be allowed. */ | 14230 this.initializeResults_(info, results); |
| 11659 canClearAll: function() { | 14231 this.closeMenu_(); |
| 11660 return !this.$['search-input'].getValue() && this.downloadsShowing; | 14232 |
| 11661 }, | 14233 if (this.selectedPage_ == 'grouped-list') { |
| 11662 | 14234 this.$$('#grouped-list').historyData = results; |
| 11663 onFindCommand: function() { | 14235 return; |
| 11664 this.$['search-input'].showAndFocus(); | 14236 } |
| 11665 }, | 14237 |
| 11666 | 14238 var list = /** @type {HistoryListElement} */(this.$['infinite-list']); |
| 11667 /** @private */ | 14239 list.addNewResults(results); |
| 11668 closeMoreActions_: function() { | 14240 if (info.finished) |
| 11669 this.$.more.close(); | 14241 list.disableResultLoading(); |
| 11670 }, | 14242 }, |
| 11671 | 14243 |
| 11672 /** @private */ | 14244 /** |
| 11673 downloadsShowingChanged_: function() { | 14245 * Queries the history backend for results based on queryState. |
| 11674 this.updateClearAll_(); | 14246 * @param {boolean} incremental Whether the new query should continue where |
| 11675 }, | 14247 * the previous query stopped. |
| 11676 | 14248 */ |
| 11677 /** @private */ | 14249 queryHistory: function(incremental) { |
| 11678 onClearAllTap_: function() { | 14250 var queryState = this.queryState; |
| 11679 assert(this.canClearAll()); | 14251 // Disable querying until the first set of results have been returned. If |
| 11680 downloads.ActionService.getInstance().clearAll(); | 14252 // there is a search, query immediately to support search query params from |
| 11681 }, | 14253 // the URL. |
| 11682 | 14254 var noResults = !this.queryResult || this.queryResult.results == null; |
| 11683 /** @private */ | 14255 if (queryState.queryingDisabled || |
| 11684 onPaperDropdownClose_: function() { | 14256 (!this.queryState.searchTerm && noResults)) { |
| 11685 window.removeEventListener('resize', assert(this.boundClose_)); | 14257 return; |
| 11686 }, | 14258 } |
| 11687 | 14259 |
| 11688 /** | 14260 // Close any open dialog if a new query is initiated. |
| 11689 * @param {!Event} e | 14261 if (!incremental && this.$.dialog.open) |
| 11690 * @private | 14262 this.$.dialog.close(); |
| 11691 */ | 14263 |
| 11692 onItemBlur_: function(e) { | 14264 this.set('queryState.querying', true); |
| 11693 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 14265 this.set('queryState.incremental', incremental); |
| 11694 if (menu.items.indexOf(e.relatedTarget) >= 0) | 14266 |
| 11695 return; | 14267 var lastVisitTime = 0; |
| 11696 | 14268 if (incremental) { |
| 11697 this.$.more.restoreFocusOnClose = false; | 14269 var lastVisit = this.queryResult.results.slice(-1)[0]; |
| 11698 this.closeMoreActions_(); | 14270 lastVisitTime = lastVisit ? lastVisit.time : 0; |
| 11699 this.$.more.restoreFocusOnClose = true; | 14271 } |
| 11700 }, | 14272 |
| 11701 | 14273 var maxResults = |
| 11702 /** @private */ | 14274 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0; |
| 11703 onPaperDropdownOpen_: function() { | 14275 chrome.send('queryHistory', [ |
| 11704 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 14276 queryState.searchTerm, queryState.groupedOffset, queryState.range, |
| 11705 window.addEventListener('resize', this.boundClose_); | 14277 lastVisitTime, maxResults |
| 11706 }, | 14278 ]); |
| 11707 | 14279 }, |
| 11708 /** | 14280 |
| 11709 * @param {!CustomEvent} event | 14281 unselectAllItems: function(count) { |
| 11710 * @private | 14282 this.getSelectedList_().unselectAllItems(count); |
| 11711 */ | 14283 }, |
| 11712 onSearchChanged_: function(event) { | 14284 |
| 11713 downloads.ActionService.getInstance().search( | 14285 /** |
| 11714 /** @type {string} */ (event.detail)); | 14286 * Delete all the currently selected history items. Will prompt the user with |
| 11715 this.updateClearAll_(); | 14287 * a dialog to confirm that the deletion should be performed. |
| 11716 }, | 14288 */ |
| 11717 | 14289 deleteSelectedWithPrompt: function() { |
| 11718 /** @private */ | 14290 if (!loadTimeData.getBoolean('allowDeletingHistory')) |
| 11719 onOpenDownloadsFolderTap_: function() { | 14291 return; |
| 11720 downloads.ActionService.getInstance().openDownloadsFolder(); | 14292 |
| 11721 }, | 14293 this.$.dialog.showModal(); |
| 11722 | 14294 }, |
| 11723 /** @private */ | 14295 |
| 11724 updateClearAll_: function() { | 14296 /** |
| 11725 this.$$('#actions .clear-all').hidden = !this.canClearAll(); | 14297 * @param {HistoryRange} range |
| 11726 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); | 14298 * @private |
| 11727 }, | 14299 */ |
| 11728 }); | 14300 groupedRangeChanged_: function(range) { |
| 11729 | 14301 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? |
| 11730 return {Toolbar: Toolbar}; | 14302 'infinite-list' : 'grouped-list'; |
| 14303 |
| 14304 this.queryHistory(false); |
| 14305 }, |
| 14306 |
| 14307 /** @private */ |
| 14308 loadMoreHistory_: function() { this.queryHistory(true); }, |
| 14309 |
| 14310 /** |
| 14311 * @param {HistoryQuery} info |
| 14312 * @param {!Array<HistoryEntry>} results |
| 14313 * @private |
| 14314 */ |
| 14315 initializeResults_: function(info, results) { |
| 14316 if (results.length == 0) |
| 14317 return; |
| 14318 |
| 14319 var currentDate = results[0].dateRelativeDay; |
| 14320 |
| 14321 for (var i = 0; i < results.length; i++) { |
| 14322 // Sets the default values for these fields to prevent undefined types. |
| 14323 results[i].selected = false; |
| 14324 results[i].readableTimestamp = |
| 14325 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; |
| 14326 |
| 14327 if (results[i].dateRelativeDay != currentDate) { |
| 14328 currentDate = results[i].dateRelativeDay; |
| 14329 } |
| 14330 } |
| 14331 }, |
| 14332 |
| 14333 /** @private */ |
| 14334 onDialogConfirmTap_: function() { |
| 14335 this.getSelectedList_().deleteSelected(); |
| 14336 this.$.dialog.close(); |
| 14337 }, |
| 14338 |
| 14339 /** @private */ |
| 14340 onDialogCancelTap_: function() { |
| 14341 this.$.dialog.close(); |
| 14342 }, |
| 14343 |
| 14344 /** |
| 14345 * Closes the overflow menu. |
| 14346 * @private |
| 14347 */ |
| 14348 closeMenu_: function() { |
| 14349 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).closeMenu(); |
| 14350 }, |
| 14351 |
| 14352 /** |
| 14353 * Opens the overflow menu unless the menu is already open and the same button |
| 14354 * is pressed. |
| 14355 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e |
| 14356 * @private |
| 14357 */ |
| 14358 toggleMenu_: function(e) { |
| 14359 var target = e.detail.target; |
| 14360 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).toggleMenu( |
| 14361 target, e.detail.item); |
| 14362 }, |
| 14363 |
| 14364 /** @private */ |
| 14365 onMoreFromSiteTap_: function() { |
| 14366 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu); |
| 14367 this.fire('search-domain', {domain: menu.itemData.domain}); |
| 14368 menu.closeMenu(); |
| 14369 }, |
| 14370 |
| 14371 /** @private */ |
| 14372 onRemoveFromHistoryTap_: function() { |
| 14373 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu); |
| 14374 md_history.BrowserService.getInstance() |
| 14375 .deleteItems([menu.itemData]) |
| 14376 .then(function(items) { |
| 14377 this.getSelectedList_().removeItemsByPath(items[0].path); |
| 14378 // This unselect-all is to reset the toolbar when deleting a selected |
| 14379 // item. TODO(tsergeant): Make this automatic based on observing list |
| 14380 // modifications. |
| 14381 this.fire('unselect-all'); |
| 14382 }.bind(this)); |
| 14383 menu.closeMenu(); |
| 14384 }, |
| 14385 |
| 14386 /** |
| 14387 * @return {HTMLElement} |
| 14388 * @private |
| 14389 */ |
| 14390 getSelectedList_: function() { |
| 14391 return this.$.content.selectedItem; |
| 14392 }, |
| 11731 }); | 14393 }); |
| 11732 // Copyright 2015 The Chromium Authors. All rights reserved. | 14394 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11733 // Use of this source code is governed by a BSD-style license that can be | 14395 // Use of this source code is governed by a BSD-style license that can be |
| 11734 // found in the LICENSE file. | 14396 // found in the LICENSE file. |
| 11735 | 14397 |
| 11736 cr.define('downloads', function() { | 14398 Polymer({ |
| 11737 var Manager = Polymer({ | 14399 is: 'history-synced-device-card', |
| 11738 is: 'downloads-manager', | 14400 |
| 11739 | 14401 properties: { |
| 11740 properties: { | 14402 // Name of the synced device. |
| 11741 hasDownloads_: { | 14403 device: String, |
| 11742 observer: 'hasDownloadsChanged_', | 14404 |
| 11743 type: Boolean, | 14405 // When the device information was last updated. |
| 11744 }, | 14406 lastUpdateTime: String, |
| 11745 | 14407 |
| 11746 items_: { | 14408 /** |
| 11747 type: Array, | 14409 * The list of tabs open for this device. |
| 11748 value: function() { return []; }, | 14410 * @type {!Array<!ForeignSessionTab>} |
| 11749 }, | 14411 */ |
| 11750 }, | 14412 tabs: { |
| 11751 | 14413 type: Array, |
| 11752 hostAttributes: { | 14414 value: function() { return []; }, |
| 11753 loading: true, | 14415 observer: 'updateIcons_' |
| 11754 }, | 14416 }, |
| 11755 | 14417 |
| 11756 listeners: { | 14418 /** |
| 11757 'downloads-list.scroll': 'onListScroll_', | 14419 * The indexes where a window separator should be shown. The use of a |
| 11758 }, | 14420 * separate array here is necessary for window separators to appear |
| 11759 | 14421 * correctly in search. See http://crrev.com/2022003002 for more details. |
| 11760 observers: [ | 14422 * @type {!Array<number>} |
| 11761 'itemsChanged_(items_.*)', | 14423 */ |
| 11762 ], | 14424 separatorIndexes: Array, |
| 11763 | 14425 |
| 11764 /** @private */ | 14426 // Whether the card is open. |
| 11765 clearAll_: function() { | 14427 cardOpen_: {type: Boolean, value: true}, |
| 11766 this.set('items_', []); | 14428 |
| 11767 }, | 14429 searchTerm: String, |
| 11768 | 14430 |
| 11769 /** @private */ | 14431 // Internal identifier for the device. |
| 11770 hasDownloadsChanged_: function() { | 14432 sessionTag: String, |
| 11771 if (loadTimeData.getBoolean('allowDeletingHistory')) | 14433 }, |
| 11772 this.$.toolbar.downloadsShowing = this.hasDownloads_; | 14434 |
| 11773 | 14435 /** |
| 11774 if (this.hasDownloads_) { | 14436 * Open a single synced tab. Listens to 'click' rather than 'tap' |
| 11775 this.$['downloads-list'].fire('iron-resize'); | 14437 * to determine what modifier keys were pressed. |
| 11776 } else { | 14438 * @param {DomRepeatClickEvent} e |
| 11777 var isSearching = downloads.ActionService.getInstance().isSearching(); | 14439 * @private |
| 11778 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 14440 */ |
| 11779 this.$['no-downloads'].querySelector('span').textContent = | 14441 openTab_: function(e) { |
| 11780 loadTimeData.getString(messageToShow); | 14442 var tab = /** @type {ForeignSessionTab} */(e.model.tab); |
| 11781 } | 14443 md_history.BrowserService.getInstance().openForeignSessionTab( |
| 11782 }, | 14444 this.sessionTag, tab.windowId, tab.sessionId, e); |
| 11783 | 14445 e.preventDefault(); |
| 11784 /** | 14446 }, |
| 11785 * @param {number} index | 14447 |
| 11786 * @param {!Array<!downloads.Data>} list | 14448 /** |
| 11787 * @private | 14449 * Toggles the dropdown display of synced tabs for each device card. |
| 11788 */ | 14450 */ |
| 11789 insertItems_: function(index, list) { | 14451 toggleTabCard: function() { |
| 11790 this.splice.apply(this, ['items_', index, 0].concat(list)); | 14452 this.$.collapse.toggle(); |
| 11791 this.updateHideDates_(index, index + list.length); | 14453 this.$['dropdown-indicator'].icon = |
| 11792 this.removeAttribute('loading'); | 14454 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; |
| 11793 }, | 14455 }, |
| 11794 | 14456 |
| 11795 /** @private */ | 14457 /** |
| 11796 itemsChanged_: function() { | 14458 * When the synced tab information is set, the icon associated with the tab |
| 11797 this.hasDownloads_ = this.items_.length > 0; | 14459 * website is also set. |
| 11798 }, | 14460 * @private |
| 11799 | 14461 */ |
| 11800 /** | 14462 updateIcons_: function() { |
| 11801 * @param {Event} e | 14463 this.async(function() { |
| 11802 * @private | 14464 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); |
| 11803 */ | 14465 |
| 11804 onCanExecute_: function(e) { | 14466 for (var i = 0; i < this.tabs.length; i++) { |
| 11805 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 14467 icons[i].style.backgroundImage = |
| 11806 switch (e.command.id) { | 14468 cr.icon.getFaviconImageSet(this.tabs[i].url); |
| 11807 case 'undo-command': | 14469 } |
| 11808 e.canExecute = this.$.toolbar.canUndo(); | 14470 }); |
| 11809 break; | 14471 }, |
| 11810 case 'clear-all-command': | 14472 |
| 11811 e.canExecute = this.$.toolbar.canClearAll(); | 14473 /** @private */ |
| 11812 break; | 14474 isWindowSeparatorIndex_: function(index, separatorIndexes) { |
| 11813 case 'find-command': | 14475 return this.separatorIndexes.indexOf(index) != -1; |
| 11814 e.canExecute = true; | 14476 }, |
| 11815 break; | 14477 |
| 11816 } | 14478 /** |
| 11817 }, | 14479 * @param {boolean} cardOpen |
| 11818 | 14480 * @return {string} |
| 11819 /** | 14481 */ |
| 11820 * @param {Event} e | 14482 getCollapseTitle_: function(cardOpen) { |
| 11821 * @private | 14483 return cardOpen ? loadTimeData.getString('collapseSessionButton') : |
| 11822 */ | 14484 loadTimeData.getString('expandSessionButton'); |
| 11823 onCommand_: function(e) { | 14485 }, |
| 11824 if (e.command.id == 'clear-all-command') | 14486 |
| 11825 downloads.ActionService.getInstance().clearAll(); | 14487 /** |
| 11826 else if (e.command.id == 'undo-command') | 14488 * @param {CustomEvent} e |
| 11827 downloads.ActionService.getInstance().undo(); | 14489 * @private |
| 11828 else if (e.command.id == 'find-command') | 14490 */ |
| 11829 this.$.toolbar.onFindCommand(); | 14491 onMenuButtonTap_: function(e) { |
| 11830 }, | 14492 this.fire('toggle-menu', { |
| 11831 | 14493 target: Polymer.dom(e).localTarget, |
| 11832 /** @private */ | 14494 tag: this.sessionTag |
| 11833 onListScroll_: function() { | 14495 }); |
| 11834 var list = this.$['downloads-list']; | 14496 e.stopPropagation(); // Prevent iron-collapse. |
| 11835 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 14497 }, |
| 11836 // Approaching the end of the scrollback. Attempt to load more items. | |
| 11837 downloads.ActionService.getInstance().loadMore(); | |
| 11838 } | |
| 11839 }, | |
| 11840 | |
| 11841 /** @private */ | |
| 11842 onLoad_: function() { | |
| 11843 cr.ui.decorate('command', cr.ui.Command); | |
| 11844 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | |
| 11845 document.addEventListener('command', this.onCommand_.bind(this)); | |
| 11846 | |
| 11847 downloads.ActionService.getInstance().loadMore(); | |
| 11848 }, | |
| 11849 | |
| 11850 /** | |
| 11851 * @param {number} index | |
| 11852 * @private | |
| 11853 */ | |
| 11854 removeItem_: function(index) { | |
| 11855 this.splice('items_', index, 1); | |
| 11856 this.updateHideDates_(index, index); | |
| 11857 this.onListScroll_(); | |
| 11858 }, | |
| 11859 | |
| 11860 /** | |
| 11861 * @param {number} start | |
| 11862 * @param {number} end | |
| 11863 * @private | |
| 11864 */ | |
| 11865 updateHideDates_: function(start, end) { | |
| 11866 for (var i = start; i <= end; ++i) { | |
| 11867 var current = this.items_[i]; | |
| 11868 if (!current) | |
| 11869 continue; | |
| 11870 var prev = this.items_[i - 1]; | |
| 11871 current.hideDate = !!prev && prev.date_string == current.date_string; | |
| 11872 } | |
| 11873 }, | |
| 11874 | |
| 11875 /** | |
| 11876 * @param {number} index | |
| 11877 * @param {!downloads.Data} data | |
| 11878 * @private | |
| 11879 */ | |
| 11880 updateItem_: function(index, data) { | |
| 11881 this.set('items_.' + index, data); | |
| 11882 this.updateHideDates_(index, index); | |
| 11883 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | |
| 11884 list.updateSizeForItem(index); | |
| 11885 }, | |
| 11886 }); | |
| 11887 | |
| 11888 Manager.clearAll = function() { | |
| 11889 Manager.get().clearAll_(); | |
| 11890 }; | |
| 11891 | |
| 11892 /** @return {!downloads.Manager} */ | |
| 11893 Manager.get = function() { | |
| 11894 return /** @type {!downloads.Manager} */( | |
| 11895 queryRequiredElement('downloads-manager')); | |
| 11896 }; | |
| 11897 | |
| 11898 Manager.insertItems = function(index, list) { | |
| 11899 Manager.get().insertItems_(index, list); | |
| 11900 }; | |
| 11901 | |
| 11902 Manager.onLoad = function() { | |
| 11903 Manager.get().onLoad_(); | |
| 11904 }; | |
| 11905 | |
| 11906 Manager.removeItem = function(index) { | |
| 11907 Manager.get().removeItem_(index); | |
| 11908 }; | |
| 11909 | |
| 11910 Manager.updateItem = function(index, data) { | |
| 11911 Manager.get().updateItem_(index, data); | |
| 11912 }; | |
| 11913 | |
| 11914 return {Manager: Manager}; | |
| 11915 }); | 14498 }); |
| 11916 // Copyright 2015 The Chromium Authors. All rights reserved. | 14499 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11917 // Use of this source code is governed by a BSD-style license that can be | 14500 // Use of this source code is governed by a BSD-style license that can be |
| 11918 // found in the LICENSE file. | 14501 // found in the LICENSE file. |
| 11919 | 14502 |
| 11920 window.addEventListener('load', downloads.Manager.onLoad); | 14503 /** |
| 14504 * @typedef {{device: string, |
| 14505 * lastUpdateTime: string, |
| 14506 * separatorIndexes: !Array<number>, |
| 14507 * timestamp: number, |
| 14508 * tabs: !Array<!ForeignSessionTab>, |
| 14509 * tag: string}} |
| 14510 */ |
| 14511 var ForeignDeviceInternal; |
| 14512 |
| 14513 Polymer({ |
| 14514 is: 'history-synced-device-manager', |
| 14515 |
| 14516 properties: { |
| 14517 /** |
| 14518 * @type {?Array<!ForeignSession>} |
| 14519 */ |
| 14520 sessionList: { |
| 14521 type: Array, |
| 14522 observer: 'updateSyncedDevices' |
| 14523 }, |
| 14524 |
| 14525 searchTerm: { |
| 14526 type: String, |
| 14527 observer: 'searchTermChanged' |
| 14528 }, |
| 14529 |
| 14530 /** |
| 14531 * An array of synced devices with synced tab data. |
| 14532 * @type {!Array<!ForeignDeviceInternal>} |
| 14533 */ |
| 14534 syncedDevices_: { |
| 14535 type: Array, |
| 14536 value: function() { return []; } |
| 14537 }, |
| 14538 |
| 14539 /** @private */ |
| 14540 signInState_: { |
| 14541 type: Boolean, |
| 14542 value: loadTimeData.getBoolean('isUserSignedIn'), |
| 14543 }, |
| 14544 |
| 14545 /** @private */ |
| 14546 guestSession_: { |
| 14547 type: Boolean, |
| 14548 value: loadTimeData.getBoolean('isGuestSession'), |
| 14549 }, |
| 14550 |
| 14551 /** @private */ |
| 14552 fetchingSyncedTabs_: { |
| 14553 type: Boolean, |
| 14554 value: false, |
| 14555 } |
| 14556 }, |
| 14557 |
| 14558 listeners: { |
| 14559 'toggle-menu': 'onToggleMenu_', |
| 14560 }, |
| 14561 |
| 14562 /** @override */ |
| 14563 attached: function() { |
| 14564 // Update the sign in state. |
| 14565 chrome.send('otherDevicesInitialized'); |
| 14566 }, |
| 14567 |
| 14568 /** |
| 14569 * @param {!ForeignSession} session |
| 14570 * @return {!ForeignDeviceInternal} |
| 14571 */ |
| 14572 createInternalDevice_: function(session) { |
| 14573 var tabs = []; |
| 14574 var separatorIndexes = []; |
| 14575 for (var i = 0; i < session.windows.length; i++) { |
| 14576 var windowId = session.windows[i].sessionId; |
| 14577 var newTabs = session.windows[i].tabs; |
| 14578 if (newTabs.length == 0) |
| 14579 continue; |
| 14580 |
| 14581 newTabs.forEach(function(tab) { |
| 14582 tab.windowId = windowId; |
| 14583 }); |
| 14584 |
| 14585 var windowAdded = false; |
| 14586 if (!this.searchTerm) { |
| 14587 // Add all the tabs if there is no search term. |
| 14588 tabs = tabs.concat(newTabs); |
| 14589 windowAdded = true; |
| 14590 } else { |
| 14591 var searchText = this.searchTerm.toLowerCase(); |
| 14592 for (var j = 0; j < newTabs.length; j++) { |
| 14593 var tab = newTabs[j]; |
| 14594 if (tab.title.toLowerCase().indexOf(searchText) != -1) { |
| 14595 tabs.push(tab); |
| 14596 windowAdded = true; |
| 14597 } |
| 14598 } |
| 14599 } |
| 14600 if (windowAdded && i != session.windows.length - 1) |
| 14601 separatorIndexes.push(tabs.length - 1); |
| 14602 } |
| 14603 return { |
| 14604 device: session.name, |
| 14605 lastUpdateTime: '– ' + session.modifiedTime, |
| 14606 separatorIndexes: separatorIndexes, |
| 14607 timestamp: session.timestamp, |
| 14608 tabs: tabs, |
| 14609 tag: session.tag, |
| 14610 }; |
| 14611 }, |
| 14612 |
| 14613 onSignInTap_: function() { |
| 14614 chrome.send('SyncSetupShowSetupUI'); |
| 14615 chrome.send('SyncSetupStartSignIn', [false]); |
| 14616 }, |
| 14617 |
| 14618 onToggleMenu_: function(e) { |
| 14619 this.$.menu.toggleMenu(e.detail.target, e.detail.tag); |
| 14620 }, |
| 14621 |
| 14622 onOpenAllTap_: function() { |
| 14623 md_history.BrowserService.getInstance().openForeignSessionAllTabs( |
| 14624 this.$.menu.itemData); |
| 14625 this.$.menu.closeMenu(); |
| 14626 }, |
| 14627 |
| 14628 onDeleteSessionTap_: function() { |
| 14629 md_history.BrowserService.getInstance().deleteForeignSession( |
| 14630 this.$.menu.itemData); |
| 14631 this.$.menu.closeMenu(); |
| 14632 }, |
| 14633 |
| 14634 /** @private */ |
| 14635 clearDisplayedSyncedDevices_: function() { |
| 14636 this.syncedDevices_ = []; |
| 14637 }, |
| 14638 |
| 14639 /** |
| 14640 * Decide whether or not should display no synced tabs message. |
| 14641 * @param {boolean} signInState |
| 14642 * @param {number} syncedDevicesLength |
| 14643 * @param {boolean} guestSession |
| 14644 * @return {boolean} |
| 14645 */ |
| 14646 showNoSyncedMessage: function( |
| 14647 signInState, syncedDevicesLength, guestSession) { |
| 14648 if (guestSession) |
| 14649 return true; |
| 14650 |
| 14651 return signInState && syncedDevicesLength == 0; |
| 14652 }, |
| 14653 |
| 14654 /** |
| 14655 * Shows the signin guide when the user is not signed in and not in a guest |
| 14656 * session. |
| 14657 * @param {boolean} signInState |
| 14658 * @param {boolean} guestSession |
| 14659 * @return {boolean} |
| 14660 */ |
| 14661 showSignInGuide: function(signInState, guestSession) { |
| 14662 return !signInState && !guestSession; |
| 14663 }, |
| 14664 |
| 14665 /** |
| 14666 * Decide what message should be displayed when user is logged in and there |
| 14667 * are no synced tabs. |
| 14668 * @param {boolean} fetchingSyncedTabs |
| 14669 * @return {string} |
| 14670 */ |
| 14671 noSyncedTabsMessage: function(fetchingSyncedTabs) { |
| 14672 return loadTimeData.getString( |
| 14673 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); |
| 14674 }, |
| 14675 |
| 14676 /** |
| 14677 * Replaces the currently displayed synced tabs with |sessionList|. It is |
| 14678 * common for only a single session within the list to have changed, We try to |
| 14679 * avoid doing extra work in this case. The logic could be more intelligent |
| 14680 * about updating individual tabs rather than replacing whole sessions, but |
| 14681 * this approach seems to have acceptable performance. |
| 14682 * @param {?Array<!ForeignSession>} sessionList |
| 14683 */ |
| 14684 updateSyncedDevices: function(sessionList) { |
| 14685 this.fetchingSyncedTabs_ = false; |
| 14686 |
| 14687 if (!sessionList) |
| 14688 return; |
| 14689 |
| 14690 // First, update any existing devices that have changed. |
| 14691 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); |
| 14692 for (var i = 0; i < updateCount; i++) { |
| 14693 var oldDevice = this.syncedDevices_[i]; |
| 14694 if (oldDevice.tag != sessionList[i].tag || |
| 14695 oldDevice.timestamp != sessionList[i].timestamp) { |
| 14696 this.splice( |
| 14697 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); |
| 14698 } |
| 14699 } |
| 14700 |
| 14701 // Then, append any new devices. |
| 14702 for (var i = updateCount; i < sessionList.length; i++) { |
| 14703 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); |
| 14704 } |
| 14705 }, |
| 14706 |
| 14707 /** |
| 14708 * Get called when user's sign in state changes, this will affect UI of synced |
| 14709 * tabs page. Sign in promo gets displayed when user is signed out, and |
| 14710 * different messages are shown when there are no synced tabs. |
| 14711 * @param {boolean} isUserSignedIn |
| 14712 */ |
| 14713 updateSignInState: function(isUserSignedIn) { |
| 14714 // If user's sign in state didn't change, then don't change message or |
| 14715 // update UI. |
| 14716 if (this.signInState_ == isUserSignedIn) |
| 14717 return; |
| 14718 |
| 14719 this.signInState_ = isUserSignedIn; |
| 14720 |
| 14721 // User signed out, clear synced device list and show the sign in promo. |
| 14722 if (!isUserSignedIn) { |
| 14723 this.clearDisplayedSyncedDevices_(); |
| 14724 return; |
| 14725 } |
| 14726 // User signed in, show the loading message when querying for synced |
| 14727 // devices. |
| 14728 this.fetchingSyncedTabs_ = true; |
| 14729 }, |
| 14730 |
| 14731 searchTermChanged: function(searchTerm) { |
| 14732 this.clearDisplayedSyncedDevices_(); |
| 14733 this.updateSyncedDevices(this.sessionList); |
| 14734 } |
| 14735 }); |
| 14736 /** |
| 14737 `iron-selector` is an element which can be used to manage a list of elements |
| 14738 that can be selected. Tapping on the item will make the item selected. The `
selected` indicates |
| 14739 which item is being selected. The default is to use the index of the item. |
| 14740 |
| 14741 Example: |
| 14742 |
| 14743 <iron-selector selected="0"> |
| 14744 <div>Item 1</div> |
| 14745 <div>Item 2</div> |
| 14746 <div>Item 3</div> |
| 14747 </iron-selector> |
| 14748 |
| 14749 If you want to use the attribute value of an element for `selected` instead of
the index, |
| 14750 set `attrForSelected` to the name of the attribute. For example, if you want
to select item by |
| 14751 `name`, set `attrForSelected` to `name`. |
| 14752 |
| 14753 Example: |
| 14754 |
| 14755 <iron-selector attr-for-selected="name" selected="foo"> |
| 14756 <div name="foo">Foo</div> |
| 14757 <div name="bar">Bar</div> |
| 14758 <div name="zot">Zot</div> |
| 14759 </iron-selector> |
| 14760 |
| 14761 You can specify a default fallback with `fallbackSelection` in case the `selec
ted` attribute does |
| 14762 not match the `attrForSelected` attribute of any elements. |
| 14763 |
| 14764 Example: |
| 14765 |
| 14766 <iron-selector attr-for-selected="name" selected="non-existing" |
| 14767 fallback-selection="default"> |
| 14768 <div name="foo">Foo</div> |
| 14769 <div name="bar">Bar</div> |
| 14770 <div name="default">Default</div> |
| 14771 </iron-selector> |
| 14772 |
| 14773 Note: When the selector is multi, the selection will set to `fallbackSelection
` iff |
| 14774 the number of matching elements is zero. |
| 14775 |
| 14776 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the
selected element. |
| 14777 |
| 14778 Example: |
| 14779 |
| 14780 <style> |
| 14781 .iron-selected { |
| 14782 background: #eee; |
| 14783 } |
| 14784 </style> |
| 14785 |
| 14786 ... |
| 14787 |
| 14788 <iron-selector selected="0"> |
| 14789 <div>Item 1</div> |
| 14790 <div>Item 2</div> |
| 14791 <div>Item 3</div> |
| 14792 </iron-selector> |
| 14793 |
| 14794 @demo demo/index.html |
| 14795 */ |
| 14796 |
| 14797 Polymer({ |
| 14798 |
| 14799 is: 'iron-selector', |
| 14800 |
| 14801 behaviors: [ |
| 14802 Polymer.IronMultiSelectableBehavior |
| 14803 ] |
| 14804 |
| 14805 }); |
| 14806 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14807 // Use of this source code is governed by a BSD-style license that can be |
| 14808 // found in the LICENSE file. |
| 14809 |
| 14810 Polymer({ |
| 14811 is: 'history-side-bar', |
| 14812 |
| 14813 properties: { |
| 14814 selectedPage: { |
| 14815 type: String, |
| 14816 notify: true |
| 14817 }, |
| 14818 |
| 14819 route: Object, |
| 14820 |
| 14821 showFooter: Boolean, |
| 14822 |
| 14823 // If true, the sidebar is contained within an app-drawer. |
| 14824 drawer: { |
| 14825 type: Boolean, |
| 14826 reflectToAttribute: true |
| 14827 }, |
| 14828 }, |
| 14829 |
| 14830 /** @private */ |
| 14831 onSelectorActivate_: function() { |
| 14832 this.fire('history-close-drawer'); |
| 14833 }, |
| 14834 |
| 14835 /** |
| 14836 * Relocates the user to the clear browsing data section of the settings page. |
| 14837 * @param {Event} e |
| 14838 * @private |
| 14839 */ |
| 14840 onClearBrowsingDataTap_: function(e) { |
| 14841 md_history.BrowserService.getInstance().openClearBrowsingData(); |
| 14842 e.preventDefault(); |
| 14843 }, |
| 14844 |
| 14845 /** |
| 14846 * @param {Object} route |
| 14847 * @private |
| 14848 */ |
| 14849 getQueryString_: function(route) { |
| 14850 return window.location.search; |
| 14851 } |
| 14852 }); |
| 14853 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14854 // Use of this source code is governed by a BSD-style license that can be |
| 14855 // found in the LICENSE file. |
| 14856 |
| 14857 Polymer({ |
| 14858 is: 'history-app', |
| 14859 |
| 14860 properties: { |
| 14861 showSidebarFooter: Boolean, |
| 14862 |
| 14863 // The id of the currently selected page. |
| 14864 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, |
| 14865 |
| 14866 // Whether domain-grouped history is enabled. |
| 14867 grouped_: {type: Boolean, reflectToAttribute: true}, |
| 14868 |
| 14869 /** @type {!QueryState} */ |
| 14870 queryState_: { |
| 14871 type: Object, |
| 14872 value: function() { |
| 14873 return { |
| 14874 // Whether the most recent query was incremental. |
| 14875 incremental: false, |
| 14876 // A query is initiated by page load. |
| 14877 querying: true, |
| 14878 queryingDisabled: false, |
| 14879 _range: HistoryRange.ALL_TIME, |
| 14880 searchTerm: '', |
| 14881 // TODO(calamity): Make history toolbar buttons change the offset |
| 14882 groupedOffset: 0, |
| 14883 |
| 14884 set range(val) { this._range = Number(val); }, |
| 14885 get range() { return this._range; }, |
| 14886 }; |
| 14887 } |
| 14888 }, |
| 14889 |
| 14890 /** @type {!QueryResult} */ |
| 14891 queryResult_: { |
| 14892 type: Object, |
| 14893 value: function() { |
| 14894 return { |
| 14895 info: null, |
| 14896 results: null, |
| 14897 sessionList: null, |
| 14898 }; |
| 14899 } |
| 14900 }, |
| 14901 |
| 14902 // Route data for the current page. |
| 14903 routeData_: Object, |
| 14904 |
| 14905 // The query params for the page. |
| 14906 queryParams_: Object, |
| 14907 |
| 14908 // True if the window is narrow enough for the page to have a drawer. |
| 14909 hasDrawer_: Boolean, |
| 14910 }, |
| 14911 |
| 14912 observers: [ |
| 14913 // routeData_.page <=> selectedPage |
| 14914 'routeDataChanged_(routeData_.page)', |
| 14915 'selectedPageChanged_(selectedPage_)', |
| 14916 |
| 14917 // queryParams_.q <=> queryState.searchTerm |
| 14918 'searchTermChanged_(queryState_.searchTerm)', |
| 14919 'searchQueryParamChanged_(queryParams_.q)', |
| 14920 |
| 14921 ], |
| 14922 |
| 14923 // TODO(calamity): Replace these event listeners with data bound properties. |
| 14924 listeners: { |
| 14925 'cr-menu-tap': 'onMenuTap_', |
| 14926 'history-checkbox-select': 'checkboxSelected', |
| 14927 'unselect-all': 'unselectAll', |
| 14928 'delete-selected': 'deleteSelected', |
| 14929 'search-domain': 'searchDomain_', |
| 14930 'history-close-drawer': 'closeDrawer_', |
| 14931 }, |
| 14932 |
| 14933 /** @override */ |
| 14934 ready: function() { |
| 14935 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); |
| 14936 |
| 14937 cr.ui.decorate('command', cr.ui.Command); |
| 14938 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
| 14939 document.addEventListener('command', this.onCommand_.bind(this)); |
| 14940 |
| 14941 // Redirect legacy search URLs to URLs compatible with material history. |
| 14942 if (window.location.hash) { |
| 14943 window.location.href = window.location.href.split('#')[0] + '?' + |
| 14944 window.location.hash.substr(1); |
| 14945 } |
| 14946 }, |
| 14947 |
| 14948 /** @private */ |
| 14949 onMenuTap_: function() { |
| 14950 var drawer = this.$$('#drawer'); |
| 14951 if (drawer) |
| 14952 drawer.toggle(); |
| 14953 }, |
| 14954 |
| 14955 /** |
| 14956 * Listens for history-item being selected or deselected (through checkbox) |
| 14957 * and changes the view of the top toolbar. |
| 14958 * @param {{detail: {countAddition: number}}} e |
| 14959 */ |
| 14960 checkboxSelected: function(e) { |
| 14961 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); |
| 14962 toolbar.count += e.detail.countAddition; |
| 14963 }, |
| 14964 |
| 14965 /** |
| 14966 * Listens for call to cancel selection and loops through all items to set |
| 14967 * checkbox to be unselected. |
| 14968 * @private |
| 14969 */ |
| 14970 unselectAll: function() { |
| 14971 var listContainer = |
| 14972 /** @type {HistoryListContainerElement} */ (this.$['history']); |
| 14973 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); |
| 14974 listContainer.unselectAllItems(toolbar.count); |
| 14975 toolbar.count = 0; |
| 14976 }, |
| 14977 |
| 14978 deleteSelected: function() { |
| 14979 this.$.history.deleteSelectedWithPrompt(); |
| 14980 }, |
| 14981 |
| 14982 /** |
| 14983 * @param {HistoryQuery} info An object containing information about the |
| 14984 * query. |
| 14985 * @param {!Array<HistoryEntry>} results A list of results. |
| 14986 */ |
| 14987 historyResult: function(info, results) { |
| 14988 this.set('queryState_.querying', false); |
| 14989 this.set('queryResult_.info', info); |
| 14990 this.set('queryResult_.results', results); |
| 14991 var listContainer = |
| 14992 /** @type {HistoryListContainerElement} */ (this.$['history']); |
| 14993 listContainer.historyResult(info, results); |
| 14994 }, |
| 14995 |
| 14996 /** |
| 14997 * Fired when the user presses 'More from this site'. |
| 14998 * @param {{detail: {domain: string}}} e |
| 14999 */ |
| 15000 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); }, |
| 15001 |
| 15002 /** |
| 15003 * @param {Event} e |
| 15004 * @private |
| 15005 */ |
| 15006 onCanExecute_: function(e) { |
| 15007 e = /** @type {cr.ui.CanExecuteEvent} */(e); |
| 15008 switch (e.command.id) { |
| 15009 case 'find-command': |
| 15010 e.canExecute = true; |
| 15011 break; |
| 15012 case 'slash-command': |
| 15013 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); |
| 15014 break; |
| 15015 case 'delete-command': |
| 15016 e.canExecute = this.$.toolbar.count > 0; |
| 15017 break; |
| 15018 } |
| 15019 }, |
| 15020 |
| 15021 /** |
| 15022 * @param {string} searchTerm |
| 15023 * @private |
| 15024 */ |
| 15025 searchTermChanged_: function(searchTerm) { |
| 15026 this.set('queryParams_.q', searchTerm || null); |
| 15027 this.$['history'].queryHistory(false); |
| 15028 }, |
| 15029 |
| 15030 /** |
| 15031 * @param {string} searchQuery |
| 15032 * @private |
| 15033 */ |
| 15034 searchQueryParamChanged_: function(searchQuery) { |
| 15035 this.$.toolbar.setSearchTerm(searchQuery || ''); |
| 15036 }, |
| 15037 |
| 15038 /** |
| 15039 * @param {Event} e |
| 15040 * @private |
| 15041 */ |
| 15042 onCommand_: function(e) { |
| 15043 if (e.command.id == 'find-command' || e.command.id == 'slash-command') |
| 15044 this.$.toolbar.showSearchField(); |
| 15045 if (e.command.id == 'delete-command') |
| 15046 this.deleteSelected(); |
| 15047 }, |
| 15048 |
| 15049 /** |
| 15050 * @param {!Array<!ForeignSession>} sessionList Array of objects describing |
| 15051 * the sessions from other devices. |
| 15052 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile? |
| 15053 */ |
| 15054 setForeignSessions: function(sessionList, isTabSyncEnabled) { |
| 15055 if (!isTabSyncEnabled) |
| 15056 return; |
| 15057 |
| 15058 this.set('queryResult_.sessionList', sessionList); |
| 15059 }, |
| 15060 |
| 15061 /** |
| 15062 * Update sign in state of synced device manager after user logs in or out. |
| 15063 * @param {boolean} isUserSignedIn |
| 15064 */ |
| 15065 updateSignInState: function(isUserSignedIn) { |
| 15066 var syncedDeviceManagerElem = |
| 15067 /** @type {HistorySyncedDeviceManagerElement} */this |
| 15068 .$$('history-synced-device-manager'); |
| 15069 if (syncedDeviceManagerElem) |
| 15070 syncedDeviceManagerElem.updateSignInState(isUserSignedIn); |
| 15071 }, |
| 15072 |
| 15073 /** |
| 15074 * @param {string} selectedPage |
| 15075 * @return {boolean} |
| 15076 * @private |
| 15077 */ |
| 15078 syncedTabsSelected_: function(selectedPage) { |
| 15079 return selectedPage == 'syncedTabs'; |
| 15080 }, |
| 15081 |
| 15082 /** |
| 15083 * @param {boolean} querying |
| 15084 * @param {boolean} incremental |
| 15085 * @param {string} searchTerm |
| 15086 * @return {boolean} Whether a loading spinner should be shown (implies the |
| 15087 * backend is querying a new search term). |
| 15088 * @private |
| 15089 */ |
| 15090 shouldShowSpinner_: function(querying, incremental, searchTerm) { |
| 15091 return querying && !incremental && searchTerm != ''; |
| 15092 }, |
| 15093 |
| 15094 /** |
| 15095 * @param {string} page |
| 15096 * @private |
| 15097 */ |
| 15098 routeDataChanged_: function(page) { |
| 15099 this.selectedPage_ = page; |
| 15100 }, |
| 15101 |
| 15102 /** |
| 15103 * @param {string} selectedPage |
| 15104 * @private |
| 15105 */ |
| 15106 selectedPageChanged_: function(selectedPage) { |
| 15107 this.set('routeData_.page', selectedPage); |
| 15108 }, |
| 15109 |
| 15110 /** |
| 15111 * This computed binding is needed to make the iron-pages selector update when |
| 15112 * the synced-device-manager is instantiated for the first time. Otherwise the |
| 15113 * fallback selection will continue to be used after the corresponding item is |
| 15114 * added as a child of iron-pages. |
| 15115 * @param {string} selectedPage |
| 15116 * @param {Array} items |
| 15117 * @return {string} |
| 15118 * @private |
| 15119 */ |
| 15120 getSelectedPage_: function(selectedPage, items) { |
| 15121 return selectedPage; |
| 15122 }, |
| 15123 |
| 15124 /** @private */ |
| 15125 closeDrawer_: function() { |
| 15126 var drawer = this.$$('#drawer'); |
| 15127 if (drawer) |
| 15128 drawer.close(); |
| 15129 }, |
| 15130 }); |
| OLD | NEW |