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 ['click', 'auxclick'].forEach(function(eventName) { | 1612 */ |
1364 document.addEventListener(eventName, function(e) { | 1613 dwellTime: { |
1365 if (e.defaultPrevented) | 1614 type: Number, |
1366 return; | 1615 value: 2000 |
1367 | 1616 }, |
1368 var eventPath = e.path; | 1617 |
1369 var anchor = null; | 1618 /** |
1370 if (eventPath) { | 1619 * A regexp that defines the set of URLs that should be considered part |
1371 for (var i = 0; i < eventPath.length; i++) { | 1620 * of this web app. |
1372 var element = eventPath[i]; | 1621 * |
1373 if (element.tagName === 'A' && element.href) { | 1622 * Clicking on a link that matches this regex won't result in a full pag
e |
1374 anchor = element; | 1623 * navigation, but will instead just update the URL state in place. |
1375 break; | 1624 * |
1376 } | 1625 * This regexp is given everything after the origin in an absolute |
1377 } | 1626 * URL. So to match just URLs that start with /search/ do: |
1378 } | 1627 * url-space-regex="^/search/" |
1379 | 1628 * |
1380 // Fallback if Event.path is not available. | 1629 * @type {string|RegExp} |
1381 var el = e.target; | 1630 */ |
1382 if (!anchor && el.nodeType == Node.ELEMENT_NODE && | 1631 urlSpaceRegex: { |
1383 el.webkitMatchesSelector('A, A *')) { | 1632 type: String, |
1384 while (el.tagName != 'A') { | 1633 value: '' |
1385 el = el.parentElement; | 1634 }, |
1386 } | 1635 |
1387 anchor = el; | 1636 /** |
1388 } | 1637 * urlSpaceRegex, but coerced into a regexp. |
1389 | 1638 * |
1390 if (!anchor) | 1639 * @type {RegExp} |
1391 return; | 1640 */ |
1392 | 1641 _urlSpaceRegExp: { |
1393 anchor = /** @type {!HTMLAnchorElement} */(anchor); | 1642 computed: '_makeRegExp(urlSpaceRegex)' |
1394 if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && | 1643 }, |
1395 (e.button == 0 || e.button == 1)) { | 1644 |
1396 chrome.send('navigateToUrl', [ | 1645 _lastChangedAt: { |
1397 anchor.href, | 1646 type: Number |
1398 anchor.target, | 1647 }, |
1399 e.button, | 1648 |
1400 e.altKey, | 1649 _initialized: { |
1401 e.ctrlKey, | 1650 type: Boolean, |
1402 e.metaKey, | 1651 value: false |
1403 e.shiftKey | 1652 } |
1404 ]); | 1653 }, |
1405 e.preventDefault(); | 1654 hostAttributes: { |
| 1655 hidden: true |
| 1656 }, |
| 1657 observers: [ |
| 1658 '_updateUrl(path, query, hash)' |
| 1659 ], |
| 1660 attached: function() { |
| 1661 this.listen(window, 'hashchange', '_hashChanged'); |
| 1662 this.listen(window, 'location-changed', '_urlChanged'); |
| 1663 this.listen(window, 'popstate', '_urlChanged'); |
| 1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g
lobalOnClick'); |
| 1665 // Give a 200ms grace period to make initial redirects without any |
| 1666 // additions to the user's history. |
| 1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); |
| 1668 |
| 1669 this._initialized = true; |
| 1670 this._urlChanged(); |
| 1671 }, |
| 1672 detached: function() { |
| 1673 this.unlisten(window, 'hashchange', '_hashChanged'); |
| 1674 this.unlisten(window, 'location-changed', '_urlChanged'); |
| 1675 this.unlisten(window, 'popstate', '_urlChanged'); |
| 1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '
_globalOnClick'); |
| 1677 this._initialized = false; |
| 1678 }, |
| 1679 _hashChanged: function() { |
| 1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1))
; |
| 1681 }, |
| 1682 _urlChanged: function() { |
| 1683 // We want to extract all info out of the updated URL before we |
| 1684 // try to write anything back into it. |
| 1685 // |
| 1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old |
| 1687 // one when we set this.hash. Likewise for query. |
| 1688 this._dontUpdateUrl = true; |
| 1689 this._hashChanged(); |
| 1690 this.path = window.decodeURIComponent(window.location.pathname); |
| 1691 this.query = window.decodeURIComponent( |
| 1692 window.location.search.substring(1)); |
| 1693 this._dontUpdateUrl = false; |
| 1694 this._updateUrl(); |
| 1695 }, |
| 1696 _getUrl: function() { |
| 1697 var partiallyEncodedPath = window.encodeURI( |
| 1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F'); |
| 1699 var partiallyEncodedQuery = ''; |
| 1700 if (this.query) { |
| 1701 partiallyEncodedQuery = '?' + window.encodeURI( |
| 1702 this.query).replace(/\#/g, '%23'); |
| 1703 } |
| 1704 var partiallyEncodedHash = ''; |
| 1705 if (this.hash) { |
| 1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash); |
| 1707 } |
| 1708 return ( |
| 1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash)
; |
| 1710 }, |
| 1711 _updateUrl: function() { |
| 1712 if (this._dontUpdateUrl || !this._initialized) { |
| 1713 return; |
| 1714 } |
| 1715 if (this.path === window.decodeURIComponent(window.location.pathname) && |
| 1716 this.query === window.decodeURIComponent( |
| 1717 window.location.search.substring(1)) && |
| 1718 this.hash === window.decodeURIComponent( |
| 1719 window.location.hash.substring(1))) { |
| 1720 // Nothing to do, the current URL is a representation of our propertie
s. |
| 1721 return; |
| 1722 } |
| 1723 var newUrl = this._getUrl(); |
| 1724 // Need to use a full URL in case the containing page has a base URI. |
| 1725 var fullNewUrl = new URL( |
| 1726 newUrl, window.location.protocol + '//' + window.location.host).href
; |
| 1727 var now = window.performance.now(); |
| 1728 var shouldReplace = |
| 1729 this._lastChangedAt + this.dwellTime > now; |
| 1730 this._lastChangedAt = now; |
| 1731 if (shouldReplace) { |
| 1732 window.history.replaceState({}, '', fullNewUrl); |
| 1733 } else { |
| 1734 window.history.pushState({}, '', fullNewUrl); |
| 1735 } |
| 1736 this.fire('location-changed', {}, {node: window}); |
| 1737 }, |
| 1738 /** |
| 1739 * A necessary evil so that links work as expected. Does its best to |
| 1740 * bail out early if possible. |
| 1741 * |
| 1742 * @param {MouseEvent} event . |
| 1743 */ |
| 1744 _globalOnClick: function(event) { |
| 1745 // If another event handler has stopped this event then there's nothing |
| 1746 // for us to do. This can happen e.g. when there are multiple |
| 1747 // iron-location elements in a page. |
| 1748 if (event.defaultPrevented) { |
| 1749 return; |
| 1750 } |
| 1751 var href = this._getSameOriginLinkHref(event); |
| 1752 if (!href) { |
| 1753 return; |
| 1754 } |
| 1755 event.preventDefault(); |
| 1756 // If the navigation is to the current page we shouldn't add a history |
| 1757 // entry or fire a change event. |
| 1758 if (href === window.location.href) { |
| 1759 return; |
| 1760 } |
| 1761 window.history.pushState({}, '', href); |
| 1762 this.fire('location-changed', {}, {node: window}); |
| 1763 }, |
| 1764 /** |
| 1765 * Returns the absolute URL of the link (if any) that this click event |
| 1766 * is clicking on, if we can and should override the resulting full |
| 1767 * page navigation. Returns null otherwise. |
| 1768 * |
| 1769 * @param {MouseEvent} event . |
| 1770 * @return {string?} . |
| 1771 */ |
| 1772 _getSameOriginLinkHref: function(event) { |
| 1773 // We only care about left-clicks. |
| 1774 if (event.button !== 0) { |
| 1775 return null; |
| 1776 } |
| 1777 // We don't want modified clicks, where the intent is to open the page |
| 1778 // in a new tab. |
| 1779 if (event.metaKey || event.ctrlKey) { |
| 1780 return null; |
| 1781 } |
| 1782 var eventPath = Polymer.dom(event).path; |
| 1783 var anchor = null; |
| 1784 for (var i = 0; i < eventPath.length; i++) { |
| 1785 var element = eventPath[i]; |
| 1786 if (element.tagName === 'A' && element.href) { |
| 1787 anchor = element; |
| 1788 break; |
| 1789 } |
| 1790 } |
| 1791 |
| 1792 // If there's no link there's nothing to do. |
| 1793 if (!anchor) { |
| 1794 return null; |
| 1795 } |
| 1796 |
| 1797 // Target blank is a new tab, don't intercept. |
| 1798 if (anchor.target === '_blank') { |
| 1799 return null; |
| 1800 } |
| 1801 // If the link is for an existing parent frame, don't intercept. |
| 1802 if ((anchor.target === '_top' || |
| 1803 anchor.target === '_parent') && |
| 1804 window.top !== window) { |
| 1805 return null; |
| 1806 } |
| 1807 |
| 1808 var href = anchor.href; |
| 1809 |
| 1810 // It only makes sense for us to intercept same-origin navigations. |
| 1811 // pushState/replaceState don't work with cross-origin links. |
| 1812 var url; |
| 1813 if (document.baseURI != null) { |
| 1814 url = new URL(href, /** @type {string} */(document.baseURI)); |
| 1815 } else { |
| 1816 url = new URL(href); |
| 1817 } |
| 1818 |
| 1819 var origin; |
| 1820 |
| 1821 // IE Polyfill |
| 1822 if (window.location.origin) { |
| 1823 origin = window.location.origin; |
| 1824 } else { |
| 1825 origin = window.location.protocol + '//' + window.location.hostname; |
| 1826 |
| 1827 if (window.location.port) { |
| 1828 origin += ':' + window.location.port; |
| 1829 } |
| 1830 } |
| 1831 |
| 1832 if (url.origin !== origin) { |
| 1833 return null; |
| 1834 } |
| 1835 var normalizedHref = url.pathname + url.search + url.hash; |
| 1836 |
| 1837 // If we've been configured not to handle this url... don't handle it! |
| 1838 if (this._urlSpaceRegExp && |
| 1839 !this._urlSpaceRegExp.test(normalizedHref)) { |
| 1840 return null; |
| 1841 } |
| 1842 // Need to use a full URL in case the containing page has a base URI. |
| 1843 var fullNormalizedHref = new URL( |
| 1844 normalizedHref, window.location.href).href; |
| 1845 return fullNormalizedHref; |
| 1846 }, |
| 1847 _makeRegExp: function(urlSpaceRegex) { |
| 1848 return RegExp(urlSpaceRegex); |
| 1849 } |
| 1850 }); |
| 1851 })(); |
| 1852 'use strict'; |
| 1853 |
| 1854 Polymer({ |
| 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; |
1406 } | 1923 } |
1407 }); | 1924 }); |
1408 }); | 1925 'use strict'; |
1409 | 1926 |
1410 /** | 1927 /** |
1411 * Creates a new URL which is the old URL with a GET param of key=value. | 1928 * Provides bidirectional mapping between `path` and `queryParams` and a |
1412 * @param {string} url The base URL. There is not sanity checking on the URL so | 1929 * app-route compatible `route` object. |
1413 * it must be passed in a proper format. | 1930 * |
1414 * @param {string} key The key of the param. | 1931 * For more information, see the docs for `app-route-converter`. |
1415 * @param {string} value The value of the param. | 1932 * |
1416 * @return {string} The new URL. | 1933 * @polymerBehavior |
1417 */ | 1934 */ |
1418 function appendParam(url, key, value) { | 1935 Polymer.AppRouteConverterBehavior = { |
1419 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); | 1936 properties: { |
1420 | 1937 /** |
1421 if (url.indexOf('?') == -1) | 1938 * A model representing the deserialized path through the route tree, as |
1422 return url + '?' + param; | 1939 * well as the current queryParams. |
1423 return url + '&' + param; | 1940 * |
1424 } | 1941 * A route object is the kernel of the routing system. It is intended to |
1425 | 1942 * be fed into consuming elements such as `app-route`. |
1426 /** | 1943 * |
1427 * Creates an element of a specified type with a specified class name. | 1944 * @type {?Object} |
1428 * @param {string} type The node type. | 1945 */ |
1429 * @param {string} className The class name to use. | 1946 route: { |
1430 * @return {Element} The created element. | 1947 type: Object, |
1431 */ | 1948 notify: true |
1432 function createElementWithClassName(type, className) { | 1949 }, |
1433 var elm = document.createElement(type); | 1950 |
1434 elm.className = className; | 1951 /** |
1435 return elm; | 1952 * A set of key/value pairs that are universally accessible to branches of |
1436 } | 1953 * the route tree. |
1437 | 1954 * |
1438 /** | 1955 * @type {?Object} |
1439 * webkitTransitionEnd does not always fire (e.g. when animation is aborted | 1956 */ |
1440 * or when no paint happens during the animation). This function sets up | 1957 queryParams: { |
1441 * a timer and emulate the event if it is not fired when the timer expires. | 1958 type: Object, |
1442 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. | 1959 notify: true |
1443 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the | 1960 }, |
1444 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| | 1961 |
1445 * using the transitionDuration style value. | 1962 /** |
1446 */ | 1963 * The serialized path through the route tree. This corresponds to the |
1447 function ensureTransitionEndEvent(el, opt_timeOut) { | 1964 * `window.location.pathname` value, and will update to reflect changes |
1448 if (opt_timeOut === undefined) { | 1965 * to that value. |
1449 var style = getComputedStyle(el); | 1966 */ |
1450 opt_timeOut = parseFloat(style.transitionDuration) * 1000; | 1967 path: { |
1451 | 1968 type: String, |
1452 // Give an additional 50ms buffer for the animation to complete. | 1969 notify: true, |
1453 opt_timeOut += 50; | 1970 } |
1454 } | 1971 }, |
1455 | 1972 |
1456 var fired = false; | 1973 observers: [ |
1457 el.addEventListener('webkitTransitionEnd', function f(e) { | 1974 '_locationChanged(path, queryParams)', |
1458 el.removeEventListener('webkitTransitionEnd', f); | 1975 '_routeChanged(route.prefix, route.path)', |
1459 fired = true; | 1976 '_routeQueryParamsChanged(route.__queryParams)' |
1460 }); | 1977 ], |
1461 window.setTimeout(function() { | 1978 |
1462 if (!fired) | 1979 created: function() { |
1463 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); | 1980 this.linkPaths('route.__queryParams', 'queryParams'); |
1464 }, opt_timeOut); | 1981 this.linkPaths('queryParams', 'route.__queryParams'); |
1465 } | 1982 }, |
1466 | 1983 |
1467 /** | 1984 /** |
1468 * Alias for document.scrollTop getter. | 1985 * Handler called when the path or queryParams change. |
1469 * @param {!HTMLDocument} doc The document node where information will be | 1986 */ |
1470 * queried from. | 1987 _locationChanged: function() { |
1471 * @return {number} The Y document scroll offset. | 1988 if (this.route && |
1472 */ | 1989 this.route.path === this.path && |
1473 function scrollTopForDocument(doc) { | 1990 this.queryParams === this.route.__queryParams) { |
1474 return doc.documentElement.scrollTop || doc.body.scrollTop; | 1991 return; |
1475 } | 1992 } |
1476 | 1993 this.route = { |
1477 /** | 1994 prefix: '', |
1478 * Alias for document.scrollTop setter. | 1995 path: this.path, |
1479 * @param {!HTMLDocument} doc The document node where information will be | 1996 __queryParams: this.queryParams |
1480 * queried from. | 1997 }; |
1481 * @param {number} value The target Y scroll offset. | 1998 }, |
1482 */ | 1999 |
1483 function setScrollTopForDocument(doc, value) { | 2000 /** |
1484 doc.documentElement.scrollTop = doc.body.scrollTop = value; | 2001 * Handler called when the route prefix and route path change. |
1485 } | 2002 */ |
1486 | 2003 _routeChanged: function() { |
1487 /** | 2004 if (!this.route) { |
1488 * Alias for document.scrollLeft getter. | 2005 return; |
1489 * @param {!HTMLDocument} doc The document node where information will be | 2006 } |
1490 * queried from. | 2007 |
1491 * @return {number} The X document scroll offset. | 2008 this.path = this.route.prefix + this.route.path; |
1492 */ | 2009 }, |
1493 function scrollLeftForDocument(doc) { | 2010 |
1494 return doc.documentElement.scrollLeft || doc.body.scrollLeft; | 2011 /** |
1495 } | 2012 * Handler called when the route queryParams change. |
1496 | 2013 * |
1497 /** | 2014 * @param {Object} queryParams A set of key/value pairs that are |
1498 * Alias for document.scrollLeft setter. | 2015 * universally accessible to branches of the route tree. |
1499 * @param {!HTMLDocument} doc The document node where information will be | 2016 */ |
1500 * queried from. | 2017 _routeQueryParamsChanged: function(queryParams) { |
1501 * @param {number} value The target X scroll offset. | 2018 if (!this.route) { |
1502 */ | 2019 return; |
1503 function setScrollLeftForDocument(doc, value) { | 2020 } |
1504 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; | 2021 this.queryParams = queryParams; |
1505 } | 2022 } |
1506 | 2023 }; |
1507 /** | 2024 'use strict'; |
1508 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. | 2025 |
1509 * @param {string} original The original string. | 2026 Polymer({ |
1510 * @return {string} The string with all the characters mentioned above replaced. | 2027 is: 'app-location', |
1511 */ | 2028 |
1512 function HTMLEscape(original) { | 2029 properties: { |
1513 return original.replace(/&/g, '&') | 2030 /** |
1514 .replace(/</g, '<') | 2031 * A model representing the deserialized path through the route tree, as |
1515 .replace(/>/g, '>') | 2032 * well as the current queryParams. |
1516 .replace(/"/g, '"') | 2033 */ |
1517 .replace(/'/g, '''); | 2034 route: { |
1518 } | 2035 type: Object, |
1519 | 2036 notify: true |
1520 /** | 2037 }, |
1521 * Shortens the provided string (if necessary) to a string of length at most | 2038 |
1522 * |maxLength|. | 2039 /** |
1523 * @param {string} original The original string. | 2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in |
1524 * @param {number} maxLength The maximum length allowed for the string. | 2041 * alternative to the `path`. For example, if deploying an app to a stat
ic |
1525 * @return {string} The original string if its length does not exceed | 2042 * web server (e.g., Github Pages) - where one does not have control ove
r |
1526 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' | 2043 * server-side routing - it is usually a better experience to use the ha
sh |
1527 * appended. | 2044 * to represent paths through one's app. |
1528 */ | 2045 * |
1529 function elide(original, maxLength) { | 2046 * When this property is set to true, the `hash` will be used in place o
f |
1530 if (original.length <= maxLength) | 2047 |
1531 return original; | 2048 * the `path` for generating a `route`. |
1532 return original.substring(0, maxLength - 1) + '\u2026'; | 2049 */ |
1533 } | 2050 useHashAsPath: { |
1534 | 2051 type: Boolean, |
1535 /** | 2052 value: false |
1536 * Quote a string so it can be used in a regular expression. | 2053 }, |
1537 * @param {string} str The source string. | 2054 |
1538 * @return {string} The escaped string. | 2055 /** |
1539 */ | 2056 * A regexp that defines the set of URLs that should be considered part |
1540 function quoteString(str) { | 2057 * of this web app. |
1541 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); | 2058 * |
1542 } | 2059 * Clicking on a link that matches this regex won't result in a full pag
e |
1543 | 2060 * navigation, but will instead just update the URL state in place. |
1544 // <if expr="is_ios"> | 2061 * |
1545 // Polyfill 'key' in KeyboardEvent for iOS. | 2062 * This regexp is given everything after the origin in an absolute |
1546 // This function is not intended to be complete but should | 2063 * URL. So to match just URLs that start with /search/ do: |
1547 // be sufficient enough to have iOS work correctly while | 2064 * url-space-regex="^/search/" |
1548 // it does not support key yet. | 2065 * |
1549 if (!('key' in KeyboardEvent.prototype)) { | 2066 * @type {string|RegExp} |
1550 Object.defineProperty(KeyboardEvent.prototype, 'key', { | 2067 */ |
1551 /** @this {KeyboardEvent} */ | 2068 urlSpaceRegex: { |
1552 get: function () { | 2069 type: String, |
1553 // 0-9 | 2070 notify: true |
1554 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) | 2071 }, |
1555 return String.fromCharCode(this.keyCode); | 2072 |
1556 | 2073 /** |
1557 // A-Z | 2074 * A set of key/value pairs that are universally accessible to branches |
1558 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { | 2075 * of the route tree. |
1559 var result = String.fromCharCode(this.keyCode).toLowerCase(); | 2076 */ |
1560 if (this.shiftKey) | 2077 __queryParams: { |
1561 result = result.toUpperCase(); | 2078 type: Object |
1562 return result; | 2079 }, |
1563 } | 2080 |
1564 | 2081 /** |
1565 // Special characters | 2082 * The pathname component of the current URL. |
1566 switch(this.keyCode) { | 2083 */ |
1567 case 0x08: return 'Backspace'; | 2084 __path: { |
1568 case 0x09: return 'Tab'; | 2085 type: String |
1569 case 0x0d: return 'Enter'; | 2086 }, |
1570 case 0x10: return 'Shift'; | 2087 |
1571 case 0x11: return 'Control'; | 2088 /** |
1572 case 0x12: return 'Alt'; | 2089 * The query string portion of the current URL. |
1573 case 0x1b: return 'Escape'; | 2090 */ |
1574 case 0x20: return ' '; | 2091 __query: { |
1575 case 0x21: return 'PageUp'; | 2092 type: String |
1576 case 0x22: return 'PageDown'; | 2093 }, |
1577 case 0x23: return 'End'; | 2094 |
1578 case 0x24: return 'Home'; | 2095 /** |
1579 case 0x25: return 'ArrowLeft'; | 2096 * The hash portion of the current URL. |
1580 case 0x26: return 'ArrowUp'; | 2097 */ |
1581 case 0x27: return 'ArrowRight'; | 2098 __hash: { |
1582 case 0x28: return 'ArrowDown'; | 2099 type: String |
1583 case 0x2d: return 'Insert'; | 2100 }, |
1584 case 0x2e: return 'Delete'; | 2101 |
1585 case 0x5b: return 'Meta'; | 2102 /** |
1586 case 0x70: return 'F1'; | 2103 * The route path, which will be either the hash or the path, depending |
1587 case 0x71: return 'F2'; | 2104 * on useHashAsPath. |
1588 case 0x72: return 'F3'; | 2105 */ |
1589 case 0x73: return 'F4'; | 2106 path: { |
1590 case 0x74: return 'F5'; | 2107 type: String, |
1591 case 0x75: return 'F6'; | 2108 observer: '__onPathChanged' |
1592 case 0x76: return 'F7'; | 2109 } |
1593 case 0x77: return 'F8'; | 2110 }, |
1594 case 0x78: return 'F9'; | 2111 |
1595 case 0x79: return 'F10'; | 2112 behaviors: [Polymer.AppRouteConverterBehavior], |
1596 case 0x7a: return 'F11'; | 2113 |
1597 case 0x7b: return 'F12'; | 2114 observers: [ |
1598 case 0xbb: return '='; | 2115 '__computeRoutePath(useHashAsPath, __hash, __path)' |
1599 case 0xbd: return '-'; | 2116 ], |
1600 case 0xdb: return '['; | 2117 |
1601 case 0xdd: return ']'; | 2118 __computeRoutePath: function() { |
1602 } | 2119 this.path = this.useHashAsPath ? this.__hash : this.__path; |
1603 return 'Unidentified'; | 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 } |
1604 } | 2451 } |
1605 }); | 2452 }); |
1606 } else { | 2453 Polymer({ |
1607 window.console.log("KeyboardEvent.Key polyfill not required"); | 2454 |
1608 } | 2455 is: 'iron-media-query', |
1609 // </if> /* is_ios */ | 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 }); |
1610 /** | 2544 /** |
1611 * `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 |
1612 * 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 |
1613 * 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 |
1614 * 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 |
1615 * action on their new measurements). | 2549 * action on their new measurements). |
1616 * | 2550 * |
1617 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | 2551 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to |
1618 * their element definition and listen for the `iron-resize` event on themselv
es. | 2552 * their element definition and listen for the `iron-resize` event on themselv
es. |
1619 * This event will be fired when they become showing after having been hidden, | 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... |
1780 // else they will get redundantly notified when the parent attaches). | 2714 // else they will get redundantly notified when the parent attaches). |
1781 if (!this.isAttached) { | 2715 if (!this.isAttached) { |
1782 return; | 2716 return; |
1783 } | 2717 } |
1784 | 2718 |
1785 this._notifyingDescendant = true; | 2719 this._notifyingDescendant = true; |
1786 descendant.notifyResize(); | 2720 descendant.notifyResize(); |
1787 this._notifyingDescendant = false; | 2721 this._notifyingDescendant = false; |
1788 } | 2722 } |
1789 }; | 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 }); |
1790 (function() { | 3227 (function() { |
1791 'use strict'; | 3228 'use strict'; |
1792 | 3229 |
1793 /** | 3230 /** |
1794 * Chrome uses an older version of DOM Level 3 Keyboard Events | 3231 * Chrome uses an older version of DOM Level 3 Keyboard Events |
1795 * | 3232 * |
1796 * Most keys are labeled as text, but some are Unicode codepoints. | 3233 * Most keys are labeled as text, but some are Unicode codepoints. |
1797 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | 3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set |
1798 */ | 3235 */ |
1799 var KEY_IDENTIFIER = { | 3236 var KEY_IDENTIFIER = { |
(...skipping 459 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2259 cancelable: true | 3696 cancelable: true |
2260 }); | 3697 }); |
2261 this[handlerName].call(this, event); | 3698 this[handlerName].call(this, event); |
2262 if (event.defaultPrevented) { | 3699 if (event.defaultPrevented) { |
2263 keyboardEvent.preventDefault(); | 3700 keyboardEvent.preventDefault(); |
2264 } | 3701 } |
2265 } | 3702 } |
2266 }; | 3703 }; |
2267 })(); | 3704 })(); |
2268 /** | 3705 /** |
2269 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | 3706 * @demo demo/index.html |
2270 * designated scroll target. | |
2271 * | |
2272 * Elements that consume this behavior can override the `_scrollHandler` | |
2273 * method to add logic on the scroll event. | |
2274 * | |
2275 * @demo demo/scrolling-region.html Scrolling Region | |
2276 * @demo demo/document.html Document Element | |
2277 * @polymerBehavior | 3707 * @polymerBehavior |
2278 */ | 3708 */ |
2279 Polymer.IronScrollTargetBehavior = { | 3709 Polymer.IronControlState = { |
2280 | 3710 |
2281 properties: { | 3711 properties: { |
2282 | 3712 |
2283 /** | 3713 /** |
2284 * Specifies the element that will handle the scroll event | 3714 * If true, the element currently has focus. |
2285 * on the behalf of the current element. This is typically a reference to
an element, | 3715 */ |
2286 * but there are a few more posibilities: | 3716 focused: { |
2287 * | 3717 type: Boolean, |
2288 * ### Elements id | 3718 value: false, |
2289 * | 3719 notify: true, |
2290 *```html | 3720 readOnly: true, |
2291 * <div id="scrollable-element" style="overflow: auto;"> | 3721 reflectToAttribute: true |
2292 * <x-element scroll-target="scrollable-element"> | 3722 }, |
2293 * \x3c!-- Content--\x3e | 3723 |
2294 * </x-element> | 3724 /** |
2295 * </div> | 3725 * If true, the user cannot interact with this element. |
2296 *``` | 3726 */ |
2297 * In this case, the `scrollTarget` will point to the outer div element. | 3727 disabled: { |
2298 * | 3728 type: Boolean, |
2299 * ### Document scrolling | 3729 value: false, |
2300 * | 3730 notify: true, |
2301 * For document scrolling, you can use the reserved word `document`: | 3731 observer: '_disabledChanged', |
2302 * | 3732 reflectToAttribute: true |
2303 *```html | 3733 }, |
2304 * <x-element scroll-target="document"> | 3734 |
2305 * \x3c!-- Content --\x3e | 3735 _oldTabIndex: { |
2306 * </x-element> | 3736 type: Number |
2307 *``` | 3737 }, |
2308 * | 3738 |
2309 * ### Elements reference | 3739 _boundFocusBlurHandler: { |
2310 * | 3740 type: Function, |
2311 *```js | |
2312 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
2313 *``` | |
2314 * | |
2315 * @type {HTMLElement} | |
2316 */ | |
2317 scrollTarget: { | |
2318 type: HTMLElement, | |
2319 value: function() { | 3741 value: function() { |
2320 return this._defaultScrollTarget; | 3742 return this._focusBlurHandler.bind(this); |
2321 } | 3743 } |
2322 } | 3744 } |
| 3745 |
2323 }, | 3746 }, |
2324 | 3747 |
2325 observers: [ | 3748 observers: [ |
2326 '_scrollTargetChanged(scrollTarget, isAttached)' | 3749 '_changedControlState(focused, disabled)' |
2327 ], | 3750 ], |
2328 | 3751 |
2329 _scrollTargetChanged: function(scrollTarget, isAttached) { | 3752 ready: function() { |
2330 var eventTarget; | 3753 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
2331 | 3754 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
2332 if (this._oldScrollTarget) { | 3755 }, |
2333 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | 3756 |
2334 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | 3757 _focusBlurHandler: function(event) { |
2335 this._oldScrollTarget = null; | 3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will |
2336 } | 3759 // eventually become `this` due to retargeting; if we are not in |
2337 | 3760 // ShadowDOM land, `event.target` will eventually become `this` due |
2338 if (!isAttached) { | 3761 // to the second conditional which fires a synthetic event (that is also |
2339 return; | 3762 // handled). In either case, we can disregard `event.path`. |
2340 } | 3763 |
2341 // Support element id references | 3764 if (event.target === this) { |
2342 if (scrollTarget === 'document') { | 3765 this._setFocused(event.type === 'focus'); |
2343 | 3766 } else if (!this.shadowRoot) { |
2344 this.scrollTarget = this._doc; | 3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget); |
2345 | 3768 if (!this.isLightDescendant(target)) { |
2346 } else if (typeof scrollTarget === 'string') { | 3769 this.fire(event.type, {sourceEvent: event}, { |
2347 | 3770 node: this, |
2348 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | 3771 bubbles: event.bubbles, |
2349 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | 3772 cancelable: event.cancelable |
2350 | 3773 }); |
2351 } else if (this._isValidScrollTarget()) { | 3774 } |
2352 | 3775 } |
2353 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | 3776 }, |
2354 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | 3777 |
2355 this._oldScrollTarget = scrollTarget; | 3778 _disabledChanged: function(disabled, old) { |
2356 | 3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
2357 eventTarget.addEventListener('scroll', this._boundScrollHandler); | 3780 this.style.pointerEvents = disabled ? 'none' : ''; |
2358 } | 3781 if (disabled) { |
2359 }, | 3782 this._oldTabIndex = this.tabIndex; |
2360 | 3783 this._setFocused(false); |
2361 /** | 3784 this.tabIndex = -1; |
2362 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | 3785 this.blur(); |
2363 * | 3786 } else if (this._oldTabIndex !== undefined) { |
2364 * @protected | 3787 this.tabIndex = this._oldTabIndex; |
2365 */ | 3788 } |
2366 _scrollHandler: function scrollHandler() {}, | 3789 }, |
2367 | 3790 |
2368 /** | 3791 _changedControlState: function() { |
2369 * The default scroll target. Consumers of this behavior may want to customi
ze | 3792 // _controlStateChanged is abstract, follow-on behaviors may implement it |
2370 * the default scroll target. | 3793 if (this._controlStateChanged) { |
2371 * | 3794 this._controlStateChanged(); |
2372 * @type {Element} | 3795 } |
2373 */ | |
2374 get _defaultScrollTarget() { | |
2375 return this._doc; | |
2376 }, | |
2377 | |
2378 /** | |
2379 * Shortcut for the document element | |
2380 * | |
2381 * @type {Element} | |
2382 */ | |
2383 get _doc() { | |
2384 return this.ownerDocument.documentElement; | |
2385 }, | |
2386 | |
2387 /** | |
2388 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
2389 * | |
2390 * @type {number} | |
2391 */ | |
2392 get _scrollTop() { | |
2393 if (this._isValidScrollTarget()) { | |
2394 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
2395 } | |
2396 return 0; | |
2397 }, | |
2398 | |
2399 /** | |
2400 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
2401 * | |
2402 * @type {number} | |
2403 */ | |
2404 get _scrollLeft() { | |
2405 if (this._isValidScrollTarget()) { | |
2406 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
2407 } | |
2408 return 0; | |
2409 }, | |
2410 | |
2411 /** | |
2412 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
2413 * | |
2414 * @type {number} | |
2415 */ | |
2416 set _scrollTop(top) { | |
2417 if (this.scrollTarget === this._doc) { | |
2418 window.scrollTo(window.pageXOffset, top); | |
2419 } else if (this._isValidScrollTarget()) { | |
2420 this.scrollTarget.scrollTop = top; | |
2421 } | |
2422 }, | |
2423 | |
2424 /** | |
2425 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
2426 * | |
2427 * @type {number} | |
2428 */ | |
2429 set _scrollLeft(left) { | |
2430 if (this.scrollTarget === this._doc) { | |
2431 window.scrollTo(left, window.pageYOffset); | |
2432 } else if (this._isValidScrollTarget()) { | |
2433 this.scrollTarget.scrollLeft = left; | |
2434 } | |
2435 }, | |
2436 | |
2437 /** | |
2438 * Scrolls the content to a particular place. | |
2439 * | |
2440 * @method scroll | |
2441 * @param {number} left The left position | |
2442 * @param {number} top The top position | |
2443 */ | |
2444 scroll: function(left, top) { | |
2445 if (this.scrollTarget === this._doc) { | |
2446 window.scrollTo(left, top); | |
2447 } else if (this._isValidScrollTarget()) { | |
2448 this.scrollTarget.scrollLeft = left; | |
2449 this.scrollTarget.scrollTop = top; | |
2450 } | |
2451 }, | |
2452 | |
2453 /** | |
2454 * Gets the width of the scroll target. | |
2455 * | |
2456 * @type {number} | |
2457 */ | |
2458 get _scrollTargetWidth() { | |
2459 if (this._isValidScrollTarget()) { | |
2460 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
2461 } | |
2462 return 0; | |
2463 }, | |
2464 | |
2465 /** | |
2466 * Gets the height of the scroll target. | |
2467 * | |
2468 * @type {number} | |
2469 */ | |
2470 get _scrollTargetHeight() { | |
2471 if (this._isValidScrollTarget()) { | |
2472 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
2473 } | |
2474 return 0; | |
2475 }, | |
2476 | |
2477 /** | |
2478 * Returns true if the scroll target is a valid HTMLElement. | |
2479 * | |
2480 * @return {boolean} | |
2481 */ | |
2482 _isValidScrollTarget: function() { | |
2483 return this.scrollTarget instanceof HTMLElement; | |
2484 } | 3796 } |
| 3797 |
2485 }; | 3798 }; |
2486 (function() { | 3799 /** |
2487 | 3800 * @demo demo/index.html |
2488 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 3801 * @polymerBehavior Polymer.IronButtonState |
2489 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 3802 */ |
2490 var DEFAULT_PHYSICAL_COUNT = 3; | 3803 Polymer.IronButtonStateImpl = { |
2491 var HIDDEN_Y = '-10000px'; | |
2492 var DEFAULT_GRID_SIZE = 200; | |
2493 var SECRET_TABINDEX = -100; | |
2494 | |
2495 Polymer({ | |
2496 | |
2497 is: 'iron-list', | |
2498 | 3804 |
2499 properties: { | 3805 properties: { |
2500 | 3806 |
2501 /** | 3807 /** |
2502 * An array containing items determining how many instances of the templat
e | 3808 * If true, the user is currently holding down the button. |
2503 * to stamp and that that each template instance should bind to. | 3809 */ |
2504 */ | 3810 pressed: { |
2505 items: { | 3811 type: Boolean, |
2506 type: Array | 3812 readOnly: true, |
2507 }, | 3813 value: false, |
2508 | 3814 reflectToAttribute: true, |
2509 /** | 3815 observer: '_pressedChanged' |
2510 * The max count of physical items the pool can extend to. | 3816 }, |
2511 */ | 3817 |
2512 maxPhysicalCount: { | 3818 /** |
2513 type: Number, | 3819 * If true, the button toggles the active state with each tap or press |
2514 value: 500 | 3820 * of the spacebar. |
2515 }, | 3821 */ |
2516 | 3822 toggles: { |
2517 /** | |
2518 * The name of the variable to add to the binding scope for the array | |
2519 * element associated with a given template instance. | |
2520 */ | |
2521 as: { | |
2522 type: String, | |
2523 value: 'item' | |
2524 }, | |
2525 | |
2526 /** | |
2527 * The name of the variable to add to the binding scope with the index | |
2528 * for the row. | |
2529 */ | |
2530 indexAs: { | |
2531 type: String, | |
2532 value: 'index' | |
2533 }, | |
2534 | |
2535 /** | |
2536 * The name of the variable to add to the binding scope to indicate | |
2537 * if the row is selected. | |
2538 */ | |
2539 selectedAs: { | |
2540 type: String, | |
2541 value: 'selected' | |
2542 }, | |
2543 | |
2544 /** | |
2545 * When true, the list is rendered as a grid. Grid items must have | |
2546 * fixed width and height set via CSS. e.g. | |
2547 * | |
2548 * ```html | |
2549 * <iron-list grid> | |
2550 * <template> | |
2551 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
2552 * </template> | |
2553 * </iron-list> | |
2554 * ``` | |
2555 */ | |
2556 grid: { | |
2557 type: Boolean, | 3823 type: Boolean, |
2558 value: false, | 3824 value: false, |
2559 reflectToAttribute: true | 3825 reflectToAttribute: true |
2560 }, | 3826 }, |
2561 | 3827 |
2562 /** | 3828 /** |
2563 * 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. |
2564 * 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. |
2565 * | 4733 * |
2566 * Note that tapping focusable elements within the list item will not | 4734 * @attribute elevation |
2567 * result in selection, since they are presumed to have their * own action
. | 4735 * @type number |
2568 */ | 4736 * @default 1 |
2569 selectionEnabled: { | 4737 */ |
2570 type: Boolean, | 4738 elevation: { |
2571 value: false | 4739 type: Number, |
2572 }, | 4740 reflectToAttribute: true, |
2573 | 4741 readOnly: true |
2574 /** | |
2575 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
2576 * if no item is selected. | |
2577 */ | |
2578 selectedItem: { | |
2579 type: Object, | |
2580 notify: true | |
2581 }, | |
2582 | |
2583 /** | |
2584 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
2585 */ | |
2586 selectedItems: { | |
2587 type: Object, | |
2588 notify: true | |
2589 }, | |
2590 | |
2591 /** | |
2592 * When `true`, multiple items may be selected at once (in this case, | |
2593 * `selected` is an array of currently selected items). When `false`, | |
2594 * only one item may be selected at a time. | |
2595 */ | |
2596 multiSelection: { | |
2597 type: Boolean, | |
2598 value: false | |
2599 } | 4742 } |
2600 }, | 4743 }, |
2601 | 4744 |
2602 observers: [ | 4745 observers: [ |
2603 '_itemsChanged(items.*)', | 4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', |
2604 '_selectionEnabledChanged(selectionEnabled)', | 4747 '_computeKeyboardClass(receivedFocusFromKeyboard)' |
2605 '_multiSelectionChanged(multiSelection)', | |
2606 '_setOverflow(scrollTarget)' | |
2607 ], | 4748 ], |
2608 | 4749 |
2609 behaviors: [ | 4750 hostAttributes: { |
2610 Polymer.Templatizer, | 4751 role: 'button', |
2611 Polymer.IronResizableBehavior, | 4752 tabindex: '0', |
2612 Polymer.IronA11yKeysBehavior, | 4753 animated: true |
2613 Polymer.IronScrollTargetBehavior | 4754 }, |
2614 ], | 4755 |
2615 | 4756 _calculateElevation: function() { |
2616 keyBindings: { | 4757 var e = 1; |
2617 'up': '_didMoveUp', | 4758 if (this.disabled) { |
2618 'down': '_didMoveDown', | 4759 e = 0; |
2619 'enter': '_didEnter' | 4760 } else if (this.active || this.pressed) { |
2620 }, | 4761 e = 4; |
2621 | 4762 } else if (this.receivedFocusFromKeyboard) { |
2622 /** | 4763 e = 3; |
2623 * The ratio of hidden tiles that should remain in the scroll direction. | 4764 } |
2624 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | 4765 this._setElevation(e); |
2625 */ | 4766 }, |
2626 _ratio: 0.5, | 4767 |
2627 | 4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
2628 /** | 4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
2629 * The padding-top value for the list. | 4770 }, |
2630 */ | 4771 |
2631 _scrollerPaddingTop: 0, | 4772 /** |
2632 | 4773 * In addition to `IronButtonState` behavior, when space key goes down, |
2633 /** | 4774 * create a ripple down effect. |
2634 * This value is the same as `scrollTop`. | |
2635 */ | |
2636 _scrollPosition: 0, | |
2637 | |
2638 /** | |
2639 * The sum of the heights of all the tiles in the DOM. | |
2640 */ | |
2641 _physicalSize: 0, | |
2642 | |
2643 /** | |
2644 * The average `offsetHeight` of the tiles observed till now. | |
2645 */ | |
2646 _physicalAverage: 0, | |
2647 | |
2648 /** | |
2649 * The number of tiles which `offsetHeight` > 0 observed until now. | |
2650 */ | |
2651 _physicalAverageCount: 0, | |
2652 | |
2653 /** | |
2654 * The Y position of the item rendered in the `_physicalStart` | |
2655 * tile relative to the scrolling list. | |
2656 */ | |
2657 _physicalTop: 0, | |
2658 | |
2659 /** | |
2660 * The number of items in the list. | |
2661 */ | |
2662 _virtualCount: 0, | |
2663 | |
2664 /** | |
2665 * A map between an item key and its physical item index | |
2666 */ | |
2667 _physicalIndexForKey: null, | |
2668 | |
2669 /** | |
2670 * The estimated scroll height based on `_physicalAverage` | |
2671 */ | |
2672 _estScrollHeight: 0, | |
2673 | |
2674 /** | |
2675 * The scroll height of the dom node | |
2676 */ | |
2677 _scrollHeight: 0, | |
2678 | |
2679 /** | |
2680 * The height of the list. This is referred as the viewport in the context o
f list. | |
2681 */ | |
2682 _viewportHeight: 0, | |
2683 | |
2684 /** | |
2685 * The width of the list. This is referred as the viewport in the context of
list. | |
2686 */ | |
2687 _viewportWidth: 0, | |
2688 | |
2689 /** | |
2690 * An array of DOM nodes that are currently in the tree | |
2691 * @type {?Array<!TemplatizerNode>} | |
2692 */ | |
2693 _physicalItems: null, | |
2694 | |
2695 /** | |
2696 * An array of heights for each item in `_physicalItems` | |
2697 * @type {?Array<number>} | |
2698 */ | |
2699 _physicalSizes: null, | |
2700 | |
2701 /** | |
2702 * A cached value for the first visible index. | |
2703 * See `firstVisibleIndex` | |
2704 * @type {?number} | |
2705 */ | |
2706 _firstVisibleIndexVal: null, | |
2707 | |
2708 /** | |
2709 * A cached value for the last visible index. | |
2710 * See `lastVisibleIndex` | |
2711 * @type {?number} | |
2712 */ | |
2713 _lastVisibleIndexVal: null, | |
2714 | |
2715 /** | |
2716 * A Polymer collection for the items. | |
2717 * @type {?Polymer.Collection} | |
2718 */ | |
2719 _collection: null, | |
2720 | |
2721 /** | |
2722 * True if the current item list was rendered for the first time | |
2723 * after attached. | |
2724 */ | |
2725 _itemsRendered: false, | |
2726 | |
2727 /** | |
2728 * The page that is currently rendered. | |
2729 */ | |
2730 _lastPage: null, | |
2731 | |
2732 /** | |
2733 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
2734 */ | |
2735 _maxPages: 3, | |
2736 | |
2737 /** | |
2738 * The currently focused physical item. | |
2739 */ | |
2740 _focusedItem: null, | |
2741 | |
2742 /** | |
2743 * The index of the `_focusedItem`. | |
2744 */ | |
2745 _focusedIndex: -1, | |
2746 | |
2747 /** | |
2748 * The the item that is focused if it is moved offscreen. | |
2749 * @private {?TemplatizerNode} | |
2750 */ | |
2751 _offscreenFocusedItem: null, | |
2752 | |
2753 /** | |
2754 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
2755 * list when that item is moved offscreen. | |
2756 */ | |
2757 _focusBackfillItem: null, | |
2758 | |
2759 /** | |
2760 * The maximum items per row | |
2761 */ | |
2762 _itemsPerRow: 1, | |
2763 | |
2764 /** | |
2765 * The width of each grid item | |
2766 */ | |
2767 _itemWidth: 0, | |
2768 | |
2769 /** | |
2770 * The height of the row in grid layout. | |
2771 */ | |
2772 _rowHeight: 0, | |
2773 | |
2774 /** | |
2775 * The bottom of the physical content. | |
2776 */ | |
2777 get _physicalBottom() { | |
2778 return this._physicalTop + this._physicalSize; | |
2779 }, | |
2780 | |
2781 /** | |
2782 * The bottom of the scroll. | |
2783 */ | |
2784 get _scrollBottom() { | |
2785 return this._scrollPosition + this._viewportHeight; | |
2786 }, | |
2787 | |
2788 /** | |
2789 * The n-th item rendered in the last physical item. | |
2790 */ | |
2791 get _virtualEnd() { | |
2792 return this._virtualStart + this._physicalCount - 1; | |
2793 }, | |
2794 | |
2795 /** | |
2796 * The height of the physical content that isn't on the screen. | |
2797 */ | |
2798 get _hiddenContentSize() { | |
2799 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | |
2800 return size - this._viewportHeight; | |
2801 }, | |
2802 | |
2803 /** | |
2804 * The maximum scroll top value. | |
2805 */ | |
2806 get _maxScrollTop() { | |
2807 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | |
2808 }, | |
2809 | |
2810 /** | |
2811 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
2812 */ | |
2813 _minVirtualStart: 0, | |
2814 | |
2815 /** | |
2816 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
2817 */ | |
2818 get _maxVirtualStart() { | |
2819 return Math.max(0, this._virtualCount - this._physicalCount); | |
2820 }, | |
2821 | |
2822 /** | |
2823 * The n-th item rendered in the `_physicalStart` tile. | |
2824 */ | |
2825 _virtualStartVal: 0, | |
2826 | |
2827 set _virtualStart(val) { | |
2828 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | |
2829 }, | |
2830 | |
2831 get _virtualStart() { | |
2832 return this._virtualStartVal || 0; | |
2833 }, | |
2834 | |
2835 /** | |
2836 * The k-th tile that is at the top of the scrolling list. | |
2837 */ | |
2838 _physicalStartVal: 0, | |
2839 | |
2840 set _physicalStart(val) { | |
2841 this._physicalStartVal = val % this._physicalCount; | |
2842 if (this._physicalStartVal < 0) { | |
2843 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | |
2844 } | |
2845 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | |
2846 }, | |
2847 | |
2848 get _physicalStart() { | |
2849 return this._physicalStartVal || 0; | |
2850 }, | |
2851 | |
2852 /** | |
2853 * The number of tiles in the DOM. | |
2854 */ | |
2855 _physicalCountVal: 0, | |
2856 | |
2857 set _physicalCount(val) { | |
2858 this._physicalCountVal = val; | |
2859 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | |
2860 }, | |
2861 | |
2862 get _physicalCount() { | |
2863 return this._physicalCountVal; | |
2864 }, | |
2865 | |
2866 /** | |
2867 * The k-th tile that is at the bottom of the scrolling list. | |
2868 */ | |
2869 _physicalEnd: 0, | |
2870 | |
2871 /** | |
2872 * An optimal physical size such that we will have enough physical items | |
2873 * to fill up the viewport and recycle when the user scrolls. | |
2874 * | 4775 * |
2875 * This default value assumes that we will at least have the equivalent | 4776 * @param {!KeyboardEvent} event . |
2876 * to a viewport of physical items above and below the user's viewport. | 4777 */ |
2877 */ | 4778 _spaceKeyDownHandler: function(event) { |
2878 get _optPhysicalSize() { | 4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
2879 if (this.grid) { | 4780 // Ensure that there is at most one ripple when the space key is held down
. |
2880 return this._estRowsInView * this._rowHeight * this._maxPages; | 4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
2881 } | 4782 this._ripple.uiDownAction(); |
2882 return this._viewportHeight * this._maxPages; | 4783 } |
2883 }, | 4784 }, |
2884 | 4785 |
2885 get _optPhysicalCount() { | 4786 /** |
2886 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 4787 * In addition to `IronButtonState` behavior, when space key goes up, |
2887 }, | 4788 * create a ripple up effect. |
2888 | |
2889 /** | |
2890 * True if the current list is visible. | |
2891 */ | |
2892 get _isVisible() { | |
2893 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | |
2894 }, | |
2895 | |
2896 /** | |
2897 * Gets the index of the first visible item in the viewport. | |
2898 * | 4789 * |
2899 * @type {number} | 4790 * @param {!KeyboardEvent} event . |
2900 */ | 4791 */ |
2901 get firstVisibleIndex() { | 4792 _spaceKeyUpHandler: function(event) { |
2902 if (this._firstVisibleIndexVal === null) { | 4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
2903 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 4794 if (this.hasRipple()) { |
2904 | 4795 this._ripple.uiUpAction(); |
2905 this._firstVisibleIndexVal = this._iterateItems( | 4796 } |
2906 function(pidx, vidx) { | 4797 } |
2907 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 4798 }; |
2908 | 4799 |
2909 if (physicalOffset > this._scrollPosition) { | 4800 /** @polymerBehavior */ |
2910 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 4801 Polymer.PaperButtonBehavior = [ |
2911 } | 4802 Polymer.IronButtonState, |
2912 // Handle a partially rendered final row in grid mode | 4803 Polymer.IronControlState, |
2913 if (this.grid && this._virtualCount - 1 === vidx) { | 4804 Polymer.PaperRippleBehavior, |
2914 return vidx - (vidx % this._itemsPerRow); | 4805 Polymer.PaperButtonBehaviorImpl |
2915 } | 4806 ]; |
2916 }) || 0; | 4807 Polymer({ |
2917 } | 4808 is: 'paper-button', |
2918 return this._firstVisibleIndexVal; | 4809 |
2919 }, | 4810 behaviors: [ |
2920 | 4811 Polymer.PaperButtonBehavior |
2921 /** | 4812 ], |
2922 * Gets the index of the last visible item in the viewport. | 4813 |
2923 * | 4814 properties: { |
2924 * @type {number} | 4815 /** |
2925 */ | 4816 * If true, the button should be styled with a shadow. |
2926 get lastVisibleIndex() { | 4817 */ |
2927 if (this._lastVisibleIndexVal === null) { | 4818 raised: { |
2928 if (this.grid) { | 4819 type: Boolean, |
2929 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 4820 reflectToAttribute: true, |
2930 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); |
2931 } else { | 4829 } else { |
2932 var physicalOffset = this._physicalTop; | 4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
2933 this._iterateItems(function(pidx, vidx) { | 4831 } |
2934 if (physicalOffset < this._scrollBottom) { | 4832 } |
2935 this._lastVisibleIndexVal = vidx; | 4833 |
2936 } else { | 4834 /** |
2937 // Break _iterateItems | 4835 Fired when the animation finishes. |
2938 return true; | 4836 This is useful if you want to wait until |
2939 } | 4837 the ripple animation finishes to perform some action. |
2940 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 4838 |
2941 }); | 4839 @event transitionend |
2942 } | 4840 Event param: {{node: Object}} detail Contains the animated node. |
2943 } | 4841 */ |
2944 return this._lastVisibleIndexVal; | 4842 }); |
2945 }, | |
2946 | |
2947 get _defaultScrollTarget() { | |
2948 return this; | |
2949 }, | |
2950 get _virtualRowCount() { | |
2951 return Math.ceil(this._virtualCount / this._itemsPerRow); | |
2952 }, | |
2953 | |
2954 get _estRowsInView() { | |
2955 return Math.ceil(this._viewportHeight / this._rowHeight); | |
2956 }, | |
2957 | |
2958 get _physicalRows() { | |
2959 return Math.ceil(this._physicalCount / this._itemsPerRow); | |
2960 }, | |
2961 | |
2962 ready: function() { | |
2963 this.addEventListener('focus', this._didFocus.bind(this), true); | |
2964 }, | |
2965 | |
2966 attached: function() { | |
2967 this.updateViewportBoundaries(); | |
2968 this._render(); | |
2969 // `iron-resize` is fired when the list is attached if the event is added | |
2970 // before attached causing unnecessary work. | |
2971 this.listen(this, 'iron-resize', '_resizeHandler'); | |
2972 }, | |
2973 | |
2974 detached: function() { | |
2975 this._itemsRendered = false; | |
2976 this.unlisten(this, 'iron-resize', '_resizeHandler'); | |
2977 }, | |
2978 | |
2979 /** | |
2980 * Set the overflow property if this element has its own scrolling region | |
2981 */ | |
2982 _setOverflow: function(scrollTarget) { | |
2983 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | |
2984 this.style.overflow = scrollTarget === this ? 'auto' : ''; | |
2985 }, | |
2986 | |
2987 /** | |
2988 * Invoke this method if you dynamically update the viewport's | |
2989 * size or CSS padding. | |
2990 * | |
2991 * @method updateViewportBoundaries | |
2992 */ | |
2993 updateViewportBoundaries: function() { | |
2994 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | |
2995 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
2996 | |
2997 this._viewportHeight = this._scrollTargetHeight; | |
2998 if (this.grid) { | |
2999 this._updateGridMetrics(); | |
3000 } | |
3001 }, | |
3002 | |
3003 /** | |
3004 * Update the models, the position of the | |
3005 * items in the viewport and recycle tiles as needed. | |
3006 */ | |
3007 _scrollHandler: function() { | |
3008 // clamp the `scrollTop` value | |
3009 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | |
3010 var delta = scrollTop - this._scrollPosition; | |
3011 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | |
3012 var ratio = this._ratio; | |
3013 var recycledTiles = 0; | |
3014 var hiddenContentSize = this._hiddenContentSize; | |
3015 var currentRatio = ratio; | |
3016 var movingUp = []; | |
3017 | |
3018 // track the last `scrollTop` | |
3019 this._scrollPosition = scrollTop; | |
3020 | |
3021 // clear cached visible indexes | |
3022 this._firstVisibleIndexVal = null; | |
3023 this._lastVisibleIndexVal = null; | |
3024 | |
3025 scrollBottom = this._scrollBottom; | |
3026 physicalBottom = this._physicalBottom; | |
3027 | |
3028 // random access | |
3029 if (Math.abs(delta) > this._physicalSize) { | |
3030 this._physicalTop += delta; | |
3031 recycledTiles = Math.round(delta / this._physicalAverage); | |
3032 } | |
3033 // scroll up | |
3034 else if (delta < 0) { | |
3035 var topSpace = scrollTop - this._physicalTop; | |
3036 var virtualStart = this._virtualStart; | |
3037 | |
3038 recycledTileSet = []; | |
3039 | |
3040 kth = this._physicalEnd; | |
3041 currentRatio = topSpace / hiddenContentSize; | |
3042 | |
3043 // move tiles from bottom to top | |
3044 while ( | |
3045 // approximate `currentRatio` to `ratio` | |
3046 currentRatio < ratio && | |
3047 // recycle less physical items than the total | |
3048 recycledTiles < this._physicalCount && | |
3049 // ensure that these recycled tiles are needed | |
3050 virtualStart - recycledTiles > 0 && | |
3051 // ensure that the tile is not visible | |
3052 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
3053 ) { | |
3054 | |
3055 tileHeight = this._getPhysicalSizeIncrement(kth); | |
3056 currentRatio += tileHeight / hiddenContentSize; | |
3057 physicalBottom -= tileHeight; | |
3058 recycledTileSet.push(kth); | |
3059 recycledTiles++; | |
3060 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | |
3061 } | |
3062 | |
3063 movingUp = recycledTileSet; | |
3064 recycledTiles = -recycledTiles; | |
3065 } | |
3066 // scroll down | |
3067 else if (delta > 0) { | |
3068 var bottomSpace = physicalBottom - scrollBottom; | |
3069 var virtualEnd = this._virtualEnd; | |
3070 var lastVirtualItemIndex = this._virtualCount-1; | |
3071 | |
3072 recycledTileSet = []; | |
3073 | |
3074 kth = this._physicalStart; | |
3075 currentRatio = bottomSpace / hiddenContentSize; | |
3076 | |
3077 // move tiles from top to bottom | |
3078 while ( | |
3079 // approximate `currentRatio` to `ratio` | |
3080 currentRatio < ratio && | |
3081 // recycle less physical items than the total | |
3082 recycledTiles < this._physicalCount && | |
3083 // ensure that these recycled tiles are needed | |
3084 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
3085 // ensure that the tile is not visible | |
3086 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
3087 ) { | |
3088 | |
3089 tileHeight = this._getPhysicalSizeIncrement(kth); | |
3090 currentRatio += tileHeight / hiddenContentSize; | |
3091 | |
3092 this._physicalTop += tileHeight; | |
3093 recycledTileSet.push(kth); | |
3094 recycledTiles++; | |
3095 kth = (kth + 1) % this._physicalCount; | |
3096 } | |
3097 } | |
3098 | |
3099 if (recycledTiles === 0) { | |
3100 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
3101 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | |
3102 this._increasePoolIfNeeded(); | |
3103 } | |
3104 } else { | |
3105 this._virtualStart = this._virtualStart + recycledTiles; | |
3106 this._physicalStart = this._physicalStart + recycledTiles; | |
3107 this._update(recycledTileSet, movingUp); | |
3108 } | |
3109 }, | |
3110 | |
3111 /** | |
3112 * Update the list of items, starting from the `_virtualStart` item. | |
3113 * @param {!Array<number>=} itemSet | |
3114 * @param {!Array<number>=} movingUp | |
3115 */ | |
3116 _update: function(itemSet, movingUp) { | |
3117 // manage focus | |
3118 this._manageFocus(); | |
3119 // update models | |
3120 this._assignModels(itemSet); | |
3121 // measure heights | |
3122 this._updateMetrics(itemSet); | |
3123 // adjust offset after measuring | |
3124 if (movingUp) { | |
3125 while (movingUp.length) { | |
3126 var idx = movingUp.pop(); | |
3127 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | |
3128 } | |
3129 } | |
3130 // update the position of the items | |
3131 this._positionItems(); | |
3132 // set the scroller size | |
3133 this._updateScrollerSize(); | |
3134 // increase the pool of physical items | |
3135 this._increasePoolIfNeeded(); | |
3136 }, | |
3137 | |
3138 /** | |
3139 * Creates a pool of DOM elements and attaches them to the local dom. | |
3140 */ | |
3141 _createPool: function(size) { | |
3142 var physicalItems = new Array(size); | |
3143 | |
3144 this._ensureTemplatized(); | |
3145 | |
3146 for (var i = 0; i < size; i++) { | |
3147 var inst = this.stamp(null); | |
3148 // First element child is item; Safari doesn't support children[0] | |
3149 // on a doc fragment | |
3150 physicalItems[i] = inst.root.querySelector('*'); | |
3151 Polymer.dom(this).appendChild(inst.root); | |
3152 } | |
3153 return physicalItems; | |
3154 }, | |
3155 | |
3156 /** | |
3157 * Increases the pool of physical items only if needed. | |
3158 * | |
3159 * @return {boolean} True if the pool was increased. | |
3160 */ | |
3161 _increasePoolIfNeeded: function() { | |
3162 // Base case 1: the list has no height. | |
3163 if (this._viewportHeight === 0) { | |
3164 return false; | |
3165 } | |
3166 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
3167 // with physical items, don't increase the pool. | |
3168 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | |
3169 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | |
3170 return false; | |
3171 } | |
3172 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
3173 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | |
3174 | |
3175 if (currentPage === 0) { | |
3176 // fill the first page | |
3177 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
3178 } else if (this._lastPage !== currentPage && isClientHeightFull) { | |
3179 // paint the page and defer the next increase | |
3180 // wait 16ms which is rough enough to get paint cycle. | |
3181 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | |
3182 } else { | |
3183 // fill the rest of the pages | |
3184 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | |
3185 } | |
3186 | |
3187 this._lastPage = currentPage; | |
3188 | |
3189 return true; | |
3190 }, | |
3191 | |
3192 /** | |
3193 * Increases the pool size. | |
3194 */ | |
3195 _increasePool: function(missingItems) { | |
3196 var nextPhysicalCount = Math.min( | |
3197 this._physicalCount + missingItems, | |
3198 this._virtualCount - this._virtualStart, | |
3199 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
3200 ); | |
3201 var prevPhysicalCount = this._physicalCount; | |
3202 var delta = nextPhysicalCount - prevPhysicalCount; | |
3203 | |
3204 if (delta <= 0) { | |
3205 return; | |
3206 } | |
3207 | |
3208 [].push.apply(this._physicalItems, this._createPool(delta)); | |
3209 [].push.apply(this._physicalSizes, new Array(delta)); | |
3210 | |
3211 this._physicalCount = prevPhysicalCount + delta; | |
3212 | |
3213 // update the physical start if we need to preserve the model of the focus
ed item. | |
3214 // In this situation, the focused item is currently rendered and its model
would | |
3215 // have changed after increasing the pool if the physical start remained u
nchanged. | |
3216 if (this._physicalStart > this._physicalEnd && | |
3217 this._isIndexRendered(this._focusedIndex) && | |
3218 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
3219 this._physicalStart = this._physicalStart + delta; | |
3220 } | |
3221 this._update(); | |
3222 }, | |
3223 | |
3224 /** | |
3225 * Render a new list of items. This method does exactly the same as `update`
, | |
3226 * but it also ensures that only one `update` cycle is created. | |
3227 */ | |
3228 _render: function() { | |
3229 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | |
3230 | |
3231 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | |
3232 this._lastPage = 0; | |
3233 this._update(); | |
3234 this._itemsRendered = true; | |
3235 } | |
3236 }, | |
3237 | |
3238 /** | |
3239 * Templetizes the user template. | |
3240 */ | |
3241 _ensureTemplatized: function() { | |
3242 if (!this.ctor) { | |
3243 // Template instance props that should be excluded from forwarding | |
3244 var props = {}; | |
3245 props.__key__ = true; | |
3246 props[this.as] = true; | |
3247 props[this.indexAs] = true; | |
3248 props[this.selectedAs] = true; | |
3249 props.tabIndex = true; | |
3250 | |
3251 this._instanceProps = props; | |
3252 this._userTemplate = Polymer.dom(this).querySelector('template'); | |
3253 | |
3254 if (this._userTemplate) { | |
3255 this.templatize(this._userTemplate); | |
3256 } else { | |
3257 console.warn('iron-list requires a template to be provided in light-do
m'); | |
3258 } | |
3259 } | |
3260 }, | |
3261 | |
3262 /** | |
3263 * Implements extension point from Templatizer mixin. | |
3264 */ | |
3265 _getStampedChildren: function() { | |
3266 return this._physicalItems; | |
3267 }, | |
3268 | |
3269 /** | |
3270 * Implements extension point from Templatizer | |
3271 * Called as a side effect of a template instance path change, responsible | |
3272 * for notifying items.<key-for-instance>.<path> change up to host. | |
3273 */ | |
3274 _forwardInstancePath: function(inst, path, value) { | |
3275 if (path.indexOf(this.as + '.') === 0) { | |
3276 this.notifyPath('items.' + inst.__key__ + '.' + | |
3277 path.slice(this.as.length + 1), value); | |
3278 } | |
3279 }, | |
3280 | |
3281 /** | |
3282 * Implements extension point from Templatizer mixin | |
3283 * Called as side-effect of a host property change, responsible for | |
3284 * notifying parent path change on each row. | |
3285 */ | |
3286 _forwardParentProp: function(prop, value) { | |
3287 if (this._physicalItems) { | |
3288 this._physicalItems.forEach(function(item) { | |
3289 item._templateInstance[prop] = value; | |
3290 }, this); | |
3291 } | |
3292 }, | |
3293 | |
3294 /** | |
3295 * Implements extension point from Templatizer | |
3296 * Called as side-effect of a host path change, responsible for | |
3297 * notifying parent.<path> path change on each row. | |
3298 */ | |
3299 _forwardParentPath: function(path, value) { | |
3300 if (this._physicalItems) { | |
3301 this._physicalItems.forEach(function(item) { | |
3302 item._templateInstance.notifyPath(path, value, true); | |
3303 }, this); | |
3304 } | |
3305 }, | |
3306 | |
3307 /** | |
3308 * Called as a side effect of a host items.<key>.<path> path change, | |
3309 * responsible for notifying item.<path> changes. | |
3310 */ | |
3311 _forwardItemPath: function(path, value) { | |
3312 if (!this._physicalIndexForKey) { | |
3313 return; | |
3314 } | |
3315 var dot = path.indexOf('.'); | |
3316 var key = path.substring(0, dot < 0 ? path.length : dot); | |
3317 var idx = this._physicalIndexForKey[key]; | |
3318 var offscreenItem = this._offscreenFocusedItem; | |
3319 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | |
3320 offscreenItem : this._physicalItems[idx]; | |
3321 | |
3322 if (!el || el._templateInstance.__key__ !== key) { | |
3323 return; | |
3324 } | |
3325 if (dot >= 0) { | |
3326 path = this.as + '.' + path.substring(dot+1); | |
3327 el._templateInstance.notifyPath(path, value, true); | |
3328 } else { | |
3329 // Update selection if needed | |
3330 var currentItem = el._templateInstance[this.as]; | |
3331 if (Array.isArray(this.selectedItems)) { | |
3332 for (var i = 0; i < this.selectedItems.length; i++) { | |
3333 if (this.selectedItems[i] === currentItem) { | |
3334 this.set('selectedItems.' + i, value); | |
3335 break; | |
3336 } | |
3337 } | |
3338 } else if (this.selectedItem === currentItem) { | |
3339 this.set('selectedItem', value); | |
3340 } | |
3341 el._templateInstance[this.as] = value; | |
3342 } | |
3343 }, | |
3344 | |
3345 /** | |
3346 * Called when the items have changed. That is, ressignments | |
3347 * to `items`, splices or updates to a single item. | |
3348 */ | |
3349 _itemsChanged: function(change) { | |
3350 if (change.path === 'items') { | |
3351 // reset items | |
3352 this._virtualStart = 0; | |
3353 this._physicalTop = 0; | |
3354 this._virtualCount = this.items ? this.items.length : 0; | |
3355 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | |
3356 this._physicalIndexForKey = {}; | |
3357 this._firstVisibleIndexVal = null; | |
3358 this._lastVisibleIndexVal = null; | |
3359 | |
3360 this._resetScrollPosition(0); | |
3361 this._removeFocusedItem(); | |
3362 // create the initial physical items | |
3363 if (!this._physicalItems) { | |
3364 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | |
3365 this._physicalItems = this._createPool(this._physicalCount); | |
3366 this._physicalSizes = new Array(this._physicalCount); | |
3367 } | |
3368 | |
3369 this._physicalStart = 0; | |
3370 | |
3371 } else if (change.path === 'items.splices') { | |
3372 | |
3373 this._adjustVirtualIndex(change.value.indexSplices); | |
3374 this._virtualCount = this.items ? this.items.length : 0; | |
3375 | |
3376 } else { | |
3377 // update a single item | |
3378 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | |
3379 return; | |
3380 } | |
3381 | |
3382 this._itemsRendered = false; | |
3383 this._debounceTemplate(this._render); | |
3384 }, | |
3385 | |
3386 /** | |
3387 * @param {!Array<!PolymerSplice>} splices | |
3388 */ | |
3389 _adjustVirtualIndex: function(splices) { | |
3390 splices.forEach(function(splice) { | |
3391 // deselect removed items | |
3392 splice.removed.forEach(this._removeItem, this); | |
3393 // We only need to care about changes happening above the current positi
on | |
3394 if (splice.index < this._virtualStart) { | |
3395 var delta = Math.max( | |
3396 splice.addedCount - splice.removed.length, | |
3397 splice.index - this._virtualStart); | |
3398 | |
3399 this._virtualStart = this._virtualStart + delta; | |
3400 | |
3401 if (this._focusedIndex >= 0) { | |
3402 this._focusedIndex = this._focusedIndex + delta; | |
3403 } | |
3404 } | |
3405 }, this); | |
3406 }, | |
3407 | |
3408 _removeItem: function(item) { | |
3409 this.$.selector.deselect(item); | |
3410 // remove the current focused item | |
3411 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | |
3412 this._removeFocusedItem(); | |
3413 } | |
3414 }, | |
3415 | |
3416 /** | |
3417 * Executes a provided function per every physical index in `itemSet` | |
3418 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
3419 * | |
3420 * @param {!function(number, number)} fn | |
3421 * @param {!Array<number>=} itemSet | |
3422 */ | |
3423 _iterateItems: function(fn, itemSet) { | |
3424 var pidx, vidx, rtn, i; | |
3425 | |
3426 if (arguments.length === 2 && itemSet) { | |
3427 for (i = 0; i < itemSet.length; i++) { | |
3428 pidx = itemSet[i]; | |
3429 vidx = this._computeVidx(pidx); | |
3430 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
3431 return rtn; | |
3432 } | |
3433 } | |
3434 } else { | |
3435 pidx = this._physicalStart; | |
3436 vidx = this._virtualStart; | |
3437 | |
3438 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
3439 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
3440 return rtn; | |
3441 } | |
3442 } | |
3443 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | |
3444 if ((rtn = fn.call(this, pidx, vidx)) != null) { | |
3445 return rtn; | |
3446 } | |
3447 } | |
3448 } | |
3449 }, | |
3450 | |
3451 /** | |
3452 * Returns the virtual index for a given physical index | |
3453 * | |
3454 * @param {number} pidx Physical index | |
3455 * @return {number} | |
3456 */ | |
3457 _computeVidx: function(pidx) { | |
3458 if (pidx >= this._physicalStart) { | |
3459 return this._virtualStart + (pidx - this._physicalStart); | |
3460 } | |
3461 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | |
3462 }, | |
3463 | |
3464 /** | |
3465 * Assigns the data models to a given set of items. | |
3466 * @param {!Array<number>=} itemSet | |
3467 */ | |
3468 _assignModels: function(itemSet) { | |
3469 this._iterateItems(function(pidx, vidx) { | |
3470 var el = this._physicalItems[pidx]; | |
3471 var inst = el._templateInstance; | |
3472 var item = this.items && this.items[vidx]; | |
3473 | |
3474 if (item != null) { | |
3475 inst[this.as] = item; | |
3476 inst.__key__ = this._collection.getKey(item); | |
3477 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | |
3478 inst[this.indexAs] = vidx; | |
3479 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | |
3480 this._physicalIndexForKey[inst.__key__] = pidx; | |
3481 el.removeAttribute('hidden'); | |
3482 } else { | |
3483 inst.__key__ = null; | |
3484 el.setAttribute('hidden', ''); | |
3485 } | |
3486 }, itemSet); | |
3487 }, | |
3488 | |
3489 /** | |
3490 * Updates the height for a given set of items. | |
3491 * | |
3492 * @param {!Array<number>=} itemSet | |
3493 */ | |
3494 _updateMetrics: function(itemSet) { | |
3495 // Make sure we distributed all the physical items | |
3496 // so we can measure them | |
3497 Polymer.dom.flush(); | |
3498 | |
3499 var newPhysicalSize = 0; | |
3500 var oldPhysicalSize = 0; | |
3501 var prevAvgCount = this._physicalAverageCount; | |
3502 var prevPhysicalAvg = this._physicalAverage; | |
3503 | |
3504 this._iterateItems(function(pidx, vidx) { | |
3505 | |
3506 oldPhysicalSize += this._physicalSizes[pidx] || 0; | |
3507 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | |
3508 newPhysicalSize += this._physicalSizes[pidx]; | |
3509 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | |
3510 | |
3511 }, itemSet); | |
3512 | |
3513 this._viewportHeight = this._scrollTargetHeight; | |
3514 if (this.grid) { | |
3515 this._updateGridMetrics(); | |
3516 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | |
3517 } else { | |
3518 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | |
3519 } | |
3520 | |
3521 // update the average if we measured something | |
3522 if (this._physicalAverageCount !== prevAvgCount) { | |
3523 this._physicalAverage = Math.round( | |
3524 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
3525 this._physicalAverageCount); | |
3526 } | |
3527 }, | |
3528 | |
3529 _updateGridMetrics: function() { | |
3530 this._viewportWidth = this.$.items.offsetWidth; | |
3531 // Set item width to the value of the _physicalItems offsetWidth | |
3532 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | |
3533 // Set row height to the value of the _physicalItems offsetHeight | |
3534 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | |
3535 // If in grid mode compute how many items with exist in each row | |
3536 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | |
3537 }, | |
3538 | |
3539 /** | |
3540 * Updates the position of the physical items. | |
3541 */ | |
3542 _positionItems: function() { | |
3543 this._adjustScrollPosition(); | |
3544 | |
3545 var y = this._physicalTop; | |
3546 | |
3547 if (this.grid) { | |
3548 var totalItemWidth = this._itemsPerRow * this._itemWidth; | |
3549 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | |
3550 | |
3551 this._iterateItems(function(pidx, vidx) { | |
3552 | |
3553 var modulus = vidx % this._itemsPerRow; | |
3554 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | |
3555 | |
3556 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | |
3557 | |
3558 if (this._shouldRenderNextRow(vidx)) { | |
3559 y += this._rowHeight; | |
3560 } | |
3561 | |
3562 }); | |
3563 } else { | |
3564 this._iterateItems(function(pidx, vidx) { | |
3565 | |
3566 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | |
3567 y += this._physicalSizes[pidx]; | |
3568 | |
3569 }); | |
3570 } | |
3571 }, | |
3572 | |
3573 _getPhysicalSizeIncrement: function(pidx) { | |
3574 if (!this.grid) { | |
3575 return this._physicalSizes[pidx]; | |
3576 } | |
3577 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | |
3578 return 0; | |
3579 } | |
3580 return this._rowHeight; | |
3581 }, | |
3582 | |
3583 /** | |
3584 * Returns, based on the current index, | |
3585 * whether or not the next index will need | |
3586 * to be rendered on a new row. | |
3587 * | |
3588 * @param {number} vidx Virtual index | |
3589 * @return {boolean} | |
3590 */ | |
3591 _shouldRenderNextRow: function(vidx) { | |
3592 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | |
3593 }, | |
3594 | |
3595 /** | |
3596 * Adjusts the scroll position when it was overestimated. | |
3597 */ | |
3598 _adjustScrollPosition: function() { | |
3599 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | |
3600 Math.min(this._scrollPosition + this._physicalTop, 0); | |
3601 | |
3602 if (deltaHeight) { | |
3603 this._physicalTop = this._physicalTop - deltaHeight; | |
3604 // juking scroll position during interial scrolling on iOS is no bueno | |
3605 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | |
3606 this._resetScrollPosition(this._scrollTop - deltaHeight); | |
3607 } | |
3608 } | |
3609 }, | |
3610 | |
3611 /** | |
3612 * Sets the position of the scroll. | |
3613 */ | |
3614 _resetScrollPosition: function(pos) { | |
3615 if (this.scrollTarget) { | |
3616 this._scrollTop = pos; | |
3617 this._scrollPosition = this._scrollTop; | |
3618 } | |
3619 }, | |
3620 | |
3621 /** | |
3622 * Sets the scroll height, that's the height of the content, | |
3623 * | |
3624 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
3625 */ | |
3626 _updateScrollerSize: function(forceUpdate) { | |
3627 if (this.grid) { | |
3628 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | |
3629 } else { | |
3630 this._estScrollHeight = (this._physicalBottom + | |
3631 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
3632 } | |
3633 | |
3634 forceUpdate = forceUpdate || this._scrollHeight === 0; | |
3635 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | |
3636 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | |
3637 | |
3638 // amortize height adjustment, so it won't trigger repaints very often | |
3639 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | |
3640 this.$.items.style.height = this._estScrollHeight + 'px'; | |
3641 this._scrollHeight = this._estScrollHeight; | |
3642 } | |
3643 }, | |
3644 | |
3645 /** | |
3646 * Scroll to a specific item in the virtual list regardless | |
3647 * of the physical items in the DOM tree. | |
3648 * | |
3649 * @method scrollToItem | |
3650 * @param {(Object)} item The item to be scrolled to | |
3651 */ | |
3652 scrollToItem: function(item){ | |
3653 return this.scrollToIndex(this.items.indexOf(item)); | |
3654 }, | |
3655 | |
3656 /** | |
3657 * Scroll to a specific index in the virtual list regardless | |
3658 * of the physical items in the DOM tree. | |
3659 * | |
3660 * @method scrollToIndex | |
3661 * @param {number} idx The index of the item | |
3662 */ | |
3663 scrollToIndex: function(idx) { | |
3664 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | |
3665 return; | |
3666 } | |
3667 | |
3668 Polymer.dom.flush(); | |
3669 | |
3670 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
3671 // update the virtual start only when needed | |
3672 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | |
3673 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | |
3674 } | |
3675 // manage focus | |
3676 this._manageFocus(); | |
3677 // assign new models | |
3678 this._assignModels(); | |
3679 // measure the new sizes | |
3680 this._updateMetrics(); | |
3681 | |
3682 // estimate new physical offset | |
3683 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
3684 this._physicalTop = estPhysicalTop; | |
3685 | |
3686 var currentTopItem = this._physicalStart; | |
3687 var currentVirtualItem = this._virtualStart; | |
3688 var targetOffsetTop = 0; | |
3689 var hiddenContentSize = this._hiddenContentSize; | |
3690 | |
3691 // scroll to the item as much as we can | |
3692 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | |
3693 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | |
3694 currentTopItem = (currentTopItem + 1) % this._physicalCount; | |
3695 currentVirtualItem++; | |
3696 } | |
3697 // update the scroller size | |
3698 this._updateScrollerSize(true); | |
3699 // update the position of the items | |
3700 this._positionItems(); | |
3701 // set the new scroll position | |
3702 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | |
3703 // increase the pool of physical items if needed | |
3704 this._increasePoolIfNeeded(); | |
3705 // clear cached visible index | |
3706 this._firstVisibleIndexVal = null; | |
3707 this._lastVisibleIndexVal = null; | |
3708 }, | |
3709 | |
3710 /** | |
3711 * Reset the physical average and the average count. | |
3712 */ | |
3713 _resetAverage: function() { | |
3714 this._physicalAverage = 0; | |
3715 this._physicalAverageCount = 0; | |
3716 }, | |
3717 | |
3718 /** | |
3719 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
3720 * when the element is resized. | |
3721 */ | |
3722 _resizeHandler: function() { | |
3723 // iOS fires the resize event when the address bar slides up | |
3724 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | |
3725 return; | |
3726 } | |
3727 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
3728 // changing the scroll position from a resize handler would result in | |
3729 // the scroll position being reset. Waiting 1ms fixes the issue. | |
3730 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | |
3731 this.updateViewportBoundaries(); | |
3732 this._render(); | |
3733 | |
3734 if (this._itemsRendered && this._physicalItems && this._isVisible) { | |
3735 this._resetAverage(); | |
3736 this.scrollToIndex(this.firstVisibleIndex); | |
3737 } | |
3738 }.bind(this), 1)); | |
3739 }, | |
3740 | |
3741 _getModelFromItem: function(item) { | |
3742 var key = this._collection.getKey(item); | |
3743 var pidx = this._physicalIndexForKey[key]; | |
3744 | |
3745 if (pidx != null) { | |
3746 return this._physicalItems[pidx]._templateInstance; | |
3747 } | |
3748 return null; | |
3749 }, | |
3750 | |
3751 /** | |
3752 * Gets a valid item instance from its index or the object value. | |
3753 * | |
3754 * @param {(Object|number)} item The item object or its index | |
3755 */ | |
3756 _getNormalizedItem: function(item) { | |
3757 if (this._collection.getKey(item) === undefined) { | |
3758 if (typeof item === 'number') { | |
3759 item = this.items[item]; | |
3760 if (!item) { | |
3761 throw new RangeError('<item> not found'); | |
3762 } | |
3763 return item; | |
3764 } | |
3765 throw new TypeError('<item> should be a valid item'); | |
3766 } | |
3767 return item; | |
3768 }, | |
3769 | |
3770 /** | |
3771 * Select the list item at the given index. | |
3772 * | |
3773 * @method selectItem | |
3774 * @param {(Object|number)} item The item object or its index | |
3775 */ | |
3776 selectItem: function(item) { | |
3777 item = this._getNormalizedItem(item); | |
3778 var model = this._getModelFromItem(item); | |
3779 | |
3780 if (!this.multiSelection && this.selectedItem) { | |
3781 this.deselectItem(this.selectedItem); | |
3782 } | |
3783 if (model) { | |
3784 model[this.selectedAs] = true; | |
3785 } | |
3786 this.$.selector.select(item); | |
3787 this.updateSizeForItem(item); | |
3788 }, | |
3789 | |
3790 /** | |
3791 * Deselects the given item list if it is already selected. | |
3792 * | |
3793 | |
3794 * @method deselect | |
3795 * @param {(Object|number)} item The item object or its index | |
3796 */ | |
3797 deselectItem: function(item) { | |
3798 item = this._getNormalizedItem(item); | |
3799 var model = this._getModelFromItem(item); | |
3800 | |
3801 if (model) { | |
3802 model[this.selectedAs] = false; | |
3803 } | |
3804 this.$.selector.deselect(item); | |
3805 this.updateSizeForItem(item); | |
3806 }, | |
3807 | |
3808 /** | |
3809 * Select or deselect a given item depending on whether the item | |
3810 * has already been selected. | |
3811 * | |
3812 * @method toggleSelectionForItem | |
3813 * @param {(Object|number)} item The item object or its index | |
3814 */ | |
3815 toggleSelectionForItem: function(item) { | |
3816 item = this._getNormalizedItem(item); | |
3817 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | |
3818 this.deselectItem(item); | |
3819 } else { | |
3820 this.selectItem(item); | |
3821 } | |
3822 }, | |
3823 | |
3824 /** | |
3825 * Clears the current selection state of the list. | |
3826 * | |
3827 * @method clearSelection | |
3828 */ | |
3829 clearSelection: function() { | |
3830 function unselect(item) { | |
3831 var model = this._getModelFromItem(item); | |
3832 if (model) { | |
3833 model[this.selectedAs] = false; | |
3834 } | |
3835 } | |
3836 | |
3837 if (Array.isArray(this.selectedItems)) { | |
3838 this.selectedItems.forEach(unselect, this); | |
3839 } else if (this.selectedItem) { | |
3840 unselect.call(this, this.selectedItem); | |
3841 } | |
3842 | |
3843 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
3844 }, | |
3845 | |
3846 /** | |
3847 * Add an event listener to `tap` if `selectionEnabled` is true, | |
3848 * it will remove the listener otherwise. | |
3849 */ | |
3850 _selectionEnabledChanged: function(selectionEnabled) { | |
3851 var handler = selectionEnabled ? this.listen : this.unlisten; | |
3852 handler.call(this, this, 'tap', '_selectionHandler'); | |
3853 }, | |
3854 | |
3855 /** | |
3856 * Select an item from an event object. | |
3857 */ | |
3858 _selectionHandler: function(e) { | |
3859 var model = this.modelForElement(e.target); | |
3860 if (!model) { | |
3861 return; | |
3862 } | |
3863 var modelTabIndex, activeElTabIndex; | |
3864 var target = Polymer.dom(e).path[0]; | |
3865 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | |
3866 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | |
3867 // Safari does not focus certain form controls via mouse | |
3868 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
3869 if (target.localName === 'input' || | |
3870 target.localName === 'button' || | |
3871 target.localName === 'select') { | |
3872 return; | |
3873 } | |
3874 // Set a temporary tabindex | |
3875 modelTabIndex = model.tabIndex; | |
3876 model.tabIndex = SECRET_TABINDEX; | |
3877 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | |
3878 model.tabIndex = modelTabIndex; | |
3879 // Only select the item if the tap wasn't on a focusable child | |
3880 // or the element bound to `tabIndex` | |
3881 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | |
3882 return; | |
3883 } | |
3884 this.toggleSelectionForItem(model[this.as]); | |
3885 }, | |
3886 | |
3887 _multiSelectionChanged: function(multiSelection) { | |
3888 this.clearSelection(); | |
3889 this.$.selector.multi = multiSelection; | |
3890 }, | |
3891 | |
3892 /** | |
3893 * Updates the size of an item. | |
3894 * | |
3895 * @method updateSizeForItem | |
3896 * @param {(Object|number)} item The item object or its index | |
3897 */ | |
3898 updateSizeForItem: function(item) { | |
3899 item = this._getNormalizedItem(item); | |
3900 var key = this._collection.getKey(item); | |
3901 var pidx = this._physicalIndexForKey[key]; | |
3902 | |
3903 if (pidx != null) { | |
3904 this._updateMetrics([pidx]); | |
3905 this._positionItems(); | |
3906 } | |
3907 }, | |
3908 | |
3909 /** | |
3910 * Creates a temporary backfill item in the rendered pool of physical items | |
3911 * to replace the main focused item. The focused item has tabIndex = 0 | |
3912 * and might be currently focused by the user. | |
3913 * | |
3914 * This dynamic replacement helps to preserve the focus state. | |
3915 */ | |
3916 _manageFocus: function() { | |
3917 var fidx = this._focusedIndex; | |
3918 | |
3919 if (fidx >= 0 && fidx < this._virtualCount) { | |
3920 // if it's a valid index, check if that index is rendered | |
3921 // in a physical item. | |
3922 if (this._isIndexRendered(fidx)) { | |
3923 this._restoreFocusedItem(); | |
3924 } else { | |
3925 this._createFocusBackfillItem(); | |
3926 } | |
3927 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | |
3928 // otherwise, assign the initial focused index. | |
3929 this._focusedIndex = this._virtualStart; | |
3930 this._focusedItem = this._physicalItems[this._physicalStart]; | |
3931 } | |
3932 }, | |
3933 | |
3934 _isIndexRendered: function(idx) { | |
3935 return idx >= this._virtualStart && idx <= this._virtualEnd; | |
3936 }, | |
3937 | |
3938 _isIndexVisible: function(idx) { | |
3939 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | |
3940 }, | |
3941 | |
3942 _getPhysicalIndex: function(idx) { | |
3943 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | |
3944 }, | |
3945 | |
3946 _focusPhysicalItem: function(idx) { | |
3947 if (idx < 0 || idx >= this._virtualCount) { | |
3948 return; | |
3949 } | |
3950 this._restoreFocusedItem(); | |
3951 // scroll to index to make sure it's rendered | |
3952 if (!this._isIndexRendered(idx)) { | |
3953 this.scrollToIndex(idx); | |
3954 } | |
3955 | |
3956 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | |
3957 var model = physicalItem._templateInstance; | |
3958 var focusable; | |
3959 | |
3960 // set a secret tab index | |
3961 model.tabIndex = SECRET_TABINDEX; | |
3962 // check if focusable element is the physical item | |
3963 if (physicalItem.tabIndex === SECRET_TABINDEX) { | |
3964 focusable = physicalItem; | |
3965 } | |
3966 // search for the element which tabindex is bound to the secret tab index | |
3967 if (!focusable) { | |
3968 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | |
3969 } | |
3970 // restore the tab index | |
3971 model.tabIndex = 0; | |
3972 // focus the focusable element | |
3973 this._focusedIndex = idx; | |
3974 focusable && focusable.focus(); | |
3975 }, | |
3976 | |
3977 _removeFocusedItem: function() { | |
3978 if (this._offscreenFocusedItem) { | |
3979 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | |
3980 } | |
3981 this._offscreenFocusedItem = null; | |
3982 this._focusBackfillItem = null; | |
3983 this._focusedItem = null; | |
3984 this._focusedIndex = -1; | |
3985 }, | |
3986 | |
3987 _createFocusBackfillItem: function() { | |
3988 var pidx, fidx = this._focusedIndex; | |
3989 if (this._offscreenFocusedItem || fidx < 0) { | |
3990 return; | |
3991 } | |
3992 if (!this._focusBackfillItem) { | |
3993 // create a physical item, so that it backfills the focused item. | |
3994 var stampedTemplate = this.stamp(null); | |
3995 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | |
3996 Polymer.dom(this).appendChild(stampedTemplate.root); | |
3997 } | |
3998 // get the physical index for the focused index | |
3999 pidx = this._getPhysicalIndex(fidx); | |
4000 | |
4001 if (pidx != null) { | |
4002 // set the offcreen focused physical item | |
4003 this._offscreenFocusedItem = this._physicalItems[pidx]; | |
4004 // backfill the focused physical item | |
4005 this._physicalItems[pidx] = this._focusBackfillItem; | |
4006 // hide the focused physical | |
4007 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | |
4008 } | |
4009 }, | |
4010 | |
4011 _restoreFocusedItem: function() { | |
4012 var pidx, fidx = this._focusedIndex; | |
4013 | |
4014 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | |
4015 return; | |
4016 } | |
4017 // assign models to the focused index | |
4018 this._assignModels(); | |
4019 // get the new physical index for the focused index | |
4020 pidx = this._getPhysicalIndex(fidx); | |
4021 | |
4022 if (pidx != null) { | |
4023 // flip the focus backfill | |
4024 this._focusBackfillItem = this._physicalItems[pidx]; | |
4025 // restore the focused physical item | |
4026 this._physicalItems[pidx] = this._offscreenFocusedItem; | |
4027 // reset the offscreen focused item | |
4028 this._offscreenFocusedItem = null; | |
4029 // hide the physical item that backfills | |
4030 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | |
4031 } | |
4032 }, | |
4033 | |
4034 _didFocus: function(e) { | |
4035 var targetModel = this.modelForElement(e.target); | |
4036 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | |
4037 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | |
4038 var fidx = this._focusedIndex; | |
4039 | |
4040 if (!targetModel || !focusedModel) { | |
4041 return; | |
4042 } | |
4043 if (focusedModel === targetModel) { | |
4044 // if the user focused the same item, then bring it into view if it's no
t visible | |
4045 if (!this._isIndexVisible(fidx)) { | |
4046 this.scrollToIndex(fidx); | |
4047 } | |
4048 } else { | |
4049 this._restoreFocusedItem(); | |
4050 // restore tabIndex for the currently focused item | |
4051 focusedModel.tabIndex = -1; | |
4052 // set the tabIndex for the next focused item | |
4053 targetModel.tabIndex = 0; | |
4054 fidx = targetModel[this.indexAs]; | |
4055 this._focusedIndex = fidx; | |
4056 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | |
4057 | |
4058 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | |
4059 this._update(); | |
4060 } | |
4061 } | |
4062 }, | |
4063 | |
4064 _didMoveUp: function() { | |
4065 this._focusPhysicalItem(this._focusedIndex - 1); | |
4066 }, | |
4067 | |
4068 _didMoveDown: function(e) { | |
4069 // disable scroll when pressing the down key | |
4070 e.detail.keyboardEvent.preventDefault(); | |
4071 this._focusPhysicalItem(this._focusedIndex + 1); | |
4072 }, | |
4073 | |
4074 _didEnter: function(e) { | |
4075 this._focusPhysicalItem(this._focusedIndex); | |
4076 this._selectionHandler(e.detail.keyboardEvent); | |
4077 } | |
4078 }); | |
4079 | |
4080 })(); | |
4081 // Copyright 2015 The Chromium Authors. All rights reserved. | |
4082 // Use of this source code is governed by a BSD-style license that can be | |
4083 // found in the LICENSE file. | |
4084 | |
4085 cr.define('downloads', function() { | |
4086 /** | |
4087 * @param {string} chromeSendName | |
4088 * @return {function(string):void} A chrome.send() callback with curried name. | |
4089 */ | |
4090 function chromeSendWithId(chromeSendName) { | |
4091 return function(id) { chrome.send(chromeSendName, [id]); }; | |
4092 } | |
4093 | |
4094 /** @constructor */ | |
4095 function ActionService() { | |
4096 /** @private {Array<string>} */ | |
4097 this.searchTerms_ = []; | |
4098 } | |
4099 | |
4100 /** | |
4101 * @param {string} s | |
4102 * @return {string} |s| without whitespace at the beginning or end. | |
4103 */ | |
4104 function trim(s) { return s.trim(); } | |
4105 | |
4106 /** | |
4107 * @param {string|undefined} value | |
4108 * @return {boolean} Whether |value| is truthy. | |
4109 */ | |
4110 function truthy(value) { return !!value; } | |
4111 | |
4112 /** | |
4113 * @param {string} searchText Input typed by the user into a search box. | |
4114 * @return {Array<string>} A list of terms extracted from |searchText|. | |
4115 */ | |
4116 ActionService.splitTerms = function(searchText) { | |
4117 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']). | |
4118 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy); | |
4119 }; | |
4120 | |
4121 ActionService.prototype = { | |
4122 /** @param {string} id ID of the download to cancel. */ | |
4123 cancel: chromeSendWithId('cancel'), | |
4124 | |
4125 /** Instructs the browser to clear all finished downloads. */ | |
4126 clearAll: function() { | |
4127 if (loadTimeData.getBoolean('allowDeletingHistory')) { | |
4128 chrome.send('clearAll'); | |
4129 this.search(''); | |
4130 } | |
4131 }, | |
4132 | |
4133 /** @param {string} id ID of the dangerous download to discard. */ | |
4134 discardDangerous: chromeSendWithId('discardDangerous'), | |
4135 | |
4136 /** @param {string} url URL of a file to download. */ | |
4137 download: function(url) { | |
4138 var a = document.createElement('a'); | |
4139 a.href = url; | |
4140 a.setAttribute('download', ''); | |
4141 a.click(); | |
4142 }, | |
4143 | |
4144 /** @param {string} id ID of the download that the user started dragging. */ | |
4145 drag: chromeSendWithId('drag'), | |
4146 | |
4147 /** Loads more downloads with the current search terms. */ | |
4148 loadMore: function() { | |
4149 chrome.send('getDownloads', this.searchTerms_); | |
4150 }, | |
4151 | |
4152 /** | |
4153 * @return {boolean} Whether the user is currently searching for downloads | |
4154 * (i.e. has a non-empty search term). | |
4155 */ | |
4156 isSearching: function() { | |
4157 return this.searchTerms_.length > 0; | |
4158 }, | |
4159 | |
4160 /** Opens the current local destination for downloads. */ | |
4161 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'), | |
4162 | |
4163 /** | |
4164 * @param {string} id ID of the download to run locally on the user's box. | |
4165 */ | |
4166 openFile: chromeSendWithId('openFile'), | |
4167 | |
4168 /** @param {string} id ID the of the progressing download to pause. */ | |
4169 pause: chromeSendWithId('pause'), | |
4170 | |
4171 /** @param {string} id ID of the finished download to remove. */ | |
4172 remove: chromeSendWithId('remove'), | |
4173 | |
4174 /** @param {string} id ID of the paused download to resume. */ | |
4175 resume: chromeSendWithId('resume'), | |
4176 | |
4177 /** | |
4178 * @param {string} id ID of the dangerous download to save despite | |
4179 * warnings. | |
4180 */ | |
4181 saveDangerous: chromeSendWithId('saveDangerous'), | |
4182 | |
4183 /** @param {string} searchText What to search for. */ | |
4184 search: function(searchText) { | |
4185 var searchTerms = ActionService.splitTerms(searchText); | |
4186 var sameTerms = searchTerms.length == this.searchTerms_.length; | |
4187 | |
4188 for (var i = 0; sameTerms && i < searchTerms.length; ++i) { | |
4189 if (searchTerms[i] != this.searchTerms_[i]) | |
4190 sameTerms = false; | |
4191 } | |
4192 | |
4193 if (sameTerms) | |
4194 return; | |
4195 | |
4196 this.searchTerms_ = searchTerms; | |
4197 this.loadMore(); | |
4198 }, | |
4199 | |
4200 /** | |
4201 * Shows the local folder a finished download resides in. | |
4202 * @param {string} id ID of the download to show. | |
4203 */ | |
4204 show: chromeSendWithId('show'), | |
4205 | |
4206 /** Undo download removal. */ | |
4207 undo: chrome.send.bind(chrome, 'undo'), | |
4208 }; | |
4209 | |
4210 cr.addSingletonGetter(ActionService); | |
4211 | |
4212 return {ActionService: ActionService}; | |
4213 }); | |
4214 // Copyright 2015 The Chromium Authors. All rights reserved. | |
4215 // Use of this source code is governed by a BSD-style license that can be | |
4216 // found in the LICENSE file. | |
4217 | |
4218 cr.define('downloads', function() { | |
4219 /** | |
4220 * Explains why a download is in DANGEROUS state. | |
4221 * @enum {string} | |
4222 */ | |
4223 var DangerType = { | |
4224 NOT_DANGEROUS: 'NOT_DANGEROUS', | |
4225 DANGEROUS_FILE: 'DANGEROUS_FILE', | |
4226 DANGEROUS_URL: 'DANGEROUS_URL', | |
4227 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | |
4228 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | |
4229 DANGEROUS_HOST: 'DANGEROUS_HOST', | |
4230 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | |
4231 }; | |
4232 | |
4233 /** | |
4234 * The states a download can be in. These correspond to states defined in | |
4235 * DownloadsDOMHandler::CreateDownloadItemValue | |
4236 * @enum {string} | |
4237 */ | |
4238 var States = { | |
4239 IN_PROGRESS: 'IN_PROGRESS', | |
4240 CANCELLED: 'CANCELLED', | |
4241 COMPLETE: 'COMPLETE', | |
4242 PAUSED: 'PAUSED', | |
4243 DANGEROUS: 'DANGEROUS', | |
4244 INTERRUPTED: 'INTERRUPTED', | |
4245 }; | |
4246 | |
4247 return { | |
4248 DangerType: DangerType, | |
4249 States: States, | |
4250 }; | |
4251 }); | |
4252 // Copyright 2014 The Chromium Authors. All rights reserved. | |
4253 // Use of this source code is governed by a BSD-style license that can be | |
4254 // found in the LICENSE file. | |
4255 | |
4256 // Action links are elements that are used to perform an in-page navigation or | |
4257 // action (e.g. showing a dialog). | |
4258 // | |
4259 // They look like normal anchor (<a>) tags as their text color is blue. However, | |
4260 // they're subtly different as they're not initially underlined (giving users a | |
4261 // clue that underlined links navigate while action links don't). | |
4262 // | |
4263 // Action links look very similar to normal links when hovered (hand cursor, | |
4264 // underlined). This gives the user an idea that clicking this link will do | |
4265 // something similar to navigation but in the same page. | |
4266 // | |
4267 // They can be created in JavaScript like this: | |
4268 // | |
4269 // var link = document.createElement('a', 'action-link'); // Note second arg. | |
4270 // | |
4271 // or with a constructor like this: | |
4272 // | |
4273 // var link = new ActionLink(); | |
4274 // | |
4275 // They can be used easily from HTML as well, like so: | |
4276 // | |
4277 // <a is="action-link">Click me!</a> | |
4278 // | |
4279 // NOTE: <action-link> and document.createElement('action-link') don't work. | |
4280 | |
4281 /** | |
4282 * @constructor | |
4283 * @extends {HTMLAnchorElement} | |
4284 */ | |
4285 var ActionLink = document.registerElement('action-link', { | |
4286 prototype: { | |
4287 __proto__: HTMLAnchorElement.prototype, | |
4288 | |
4289 /** @this {ActionLink} */ | |
4290 createdCallback: function() { | |
4291 // Action links can start disabled (e.g. <a is="action-link" disabled>). | |
4292 this.tabIndex = this.disabled ? -1 : 0; | |
4293 | |
4294 if (!this.hasAttribute('role')) | |
4295 this.setAttribute('role', 'link'); | |
4296 | |
4297 this.addEventListener('keydown', function(e) { | |
4298 if (!this.disabled && e.key == 'Enter' && !this.href) { | |
4299 // Schedule a click asynchronously because other 'keydown' handlers | |
4300 // may still run later (e.g. document.addEventListener('keydown')). | |
4301 // Specifically options dialogs break when this timeout isn't here. | |
4302 // NOTE: this affects the "trusted" state of the ensuing click. I | |
4303 // haven't found anything that breaks because of this (yet). | |
4304 window.setTimeout(this.click.bind(this), 0); | |
4305 } | |
4306 }); | |
4307 | |
4308 function preventDefault(e) { | |
4309 e.preventDefault(); | |
4310 } | |
4311 | |
4312 function removePreventDefault() { | |
4313 document.removeEventListener('selectstart', preventDefault); | |
4314 document.removeEventListener('mouseup', removePreventDefault); | |
4315 } | |
4316 | |
4317 this.addEventListener('mousedown', function() { | |
4318 // This handlers strives to match the behavior of <a href="...">. | |
4319 | |
4320 // While the mouse is down, prevent text selection from dragging. | |
4321 document.addEventListener('selectstart', preventDefault); | |
4322 document.addEventListener('mouseup', removePreventDefault); | |
4323 | |
4324 // If focus started via mouse press, don't show an outline. | |
4325 if (document.activeElement != this) | |
4326 this.classList.add('no-outline'); | |
4327 }); | |
4328 | |
4329 this.addEventListener('blur', function() { | |
4330 this.classList.remove('no-outline'); | |
4331 }); | |
4332 }, | |
4333 | |
4334 /** @type {boolean} */ | |
4335 set disabled(disabled) { | |
4336 if (disabled) | |
4337 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', ''); | |
4338 else | |
4339 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled'); | |
4340 this.tabIndex = disabled ? -1 : 0; | |
4341 }, | |
4342 get disabled() { | |
4343 return this.hasAttribute('disabled'); | |
4344 }, | |
4345 | |
4346 /** @override */ | |
4347 setAttribute: function(attr, val) { | |
4348 if (attr.toLowerCase() == 'disabled') | |
4349 this.disabled = true; | |
4350 else | |
4351 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments); | |
4352 }, | |
4353 | |
4354 /** @override */ | |
4355 removeAttribute: function(attr) { | |
4356 if (attr.toLowerCase() == 'disabled') | |
4357 this.disabled = false; | |
4358 else | |
4359 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments); | |
4360 }, | |
4361 }, | |
4362 | |
4363 extends: 'a', | |
4364 }); | |
4365 (function() { | 4843 (function() { |
4366 | 4844 |
4367 // monostate data | 4845 // monostate data |
4368 var metaDatas = {}; | 4846 var metaDatas = {}; |
4369 var metaArrays = {}; | 4847 var metaArrays = {}; |
4370 var singleton = null; | 4848 var singleton = null; |
4371 | 4849 |
4372 Polymer.IronMeta = Polymer({ | 4850 Polymer.IronMeta = Polymer({ |
4373 | 4851 |
4374 is: 'iron-meta', | 4852 is: 'iron-meta', |
(...skipping 355 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4730 this._img.style.height = '100%'; | 5208 this._img.style.height = '100%'; |
4731 this._img.draggable = false; | 5209 this._img.draggable = false; |
4732 } | 5210 } |
4733 this._img.src = this.src; | 5211 this._img.src = this.src; |
4734 Polymer.dom(this.root).appendChild(this._img); | 5212 Polymer.dom(this.root).appendChild(this._img); |
4735 } | 5213 } |
4736 } | 5214 } |
4737 | 5215 |
4738 }); | 5216 }); |
4739 /** | 5217 /** |
4740 * @demo demo/index.html | 5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. |
4741 * @polymerBehavior | 5219 * |
| 5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior |
4742 */ | 5221 */ |
4743 Polymer.IronControlState = { | 5222 Polymer.PaperInkyFocusBehaviorImpl = { |
4744 | |
4745 properties: { | |
4746 | |
4747 /** | |
4748 * If true, the element currently has focus. | |
4749 */ | |
4750 focused: { | |
4751 type: Boolean, | |
4752 value: false, | |
4753 notify: true, | |
4754 readOnly: true, | |
4755 reflectToAttribute: true | |
4756 }, | |
4757 | |
4758 /** | |
4759 * If true, the user cannot interact with this element. | |
4760 */ | |
4761 disabled: { | |
4762 type: Boolean, | |
4763 value: false, | |
4764 notify: true, | |
4765 observer: '_disabledChanged', | |
4766 reflectToAttribute: true | |
4767 }, | |
4768 | |
4769 _oldTabIndex: { | |
4770 type: Number | |
4771 }, | |
4772 | |
4773 _boundFocusBlurHandler: { | |
4774 type: Function, | |
4775 value: function() { | |
4776 return this._focusBlurHandler.bind(this); | |
4777 } | |
4778 } | |
4779 | |
4780 }, | |
4781 | |
4782 observers: [ | 5223 observers: [ |
4783 '_changedControlState(focused, disabled)' | 5224 '_focusedChanged(receivedFocusFromKeyboard)' |
4784 ], | 5225 ], |
4785 | 5226 |
4786 ready: function() { | 5227 _focusedChanged: function(receivedFocusFromKeyboard) { |
4787 this.addEventListener('focus', this._boundFocusBlurHandler, true); | 5228 if (receivedFocusFromKeyboard) { |
4788 this.addEventListener('blur', this._boundFocusBlurHandler, true); | |
4789 }, | |
4790 | |
4791 _focusBlurHandler: function(event) { | |
4792 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | |
4793 // eventually become `this` due to retargeting; if we are not in | |
4794 // ShadowDOM land, `event.target` will eventually become `this` due | |
4795 // to the second conditional which fires a synthetic event (that is also | |
4796 // handled). In either case, we can disregard `event.path`. | |
4797 | |
4798 if (event.target === this) { | |
4799 this._setFocused(event.type === 'focus'); | |
4800 } else if (!this.shadowRoot) { | |
4801 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | |
4802 if (!this.isLightDescendant(target)) { | |
4803 this.fire(event.type, {sourceEvent: event}, { | |
4804 node: this, | |
4805 bubbles: event.bubbles, | |
4806 cancelable: event.cancelable | |
4807 }); | |
4808 } | |
4809 } | |
4810 }, | |
4811 | |
4812 _disabledChanged: function(disabled, old) { | |
4813 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | |
4814 this.style.pointerEvents = disabled ? 'none' : ''; | |
4815 if (disabled) { | |
4816 this._oldTabIndex = this.tabIndex; | |
4817 this._setFocused(false); | |
4818 this.tabIndex = -1; | |
4819 this.blur(); | |
4820 } else if (this._oldTabIndex !== undefined) { | |
4821 this.tabIndex = this._oldTabIndex; | |
4822 } | |
4823 }, | |
4824 | |
4825 _changedControlState: function() { | |
4826 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
4827 if (this._controlStateChanged) { | |
4828 this._controlStateChanged(); | |
4829 } | |
4830 } | |
4831 | |
4832 }; | |
4833 /** | |
4834 * @demo demo/index.html | |
4835 * @polymerBehavior Polymer.IronButtonState | |
4836 */ | |
4837 Polymer.IronButtonStateImpl = { | |
4838 | |
4839 properties: { | |
4840 | |
4841 /** | |
4842 * If true, the user is currently holding down the button. | |
4843 */ | |
4844 pressed: { | |
4845 type: Boolean, | |
4846 readOnly: true, | |
4847 value: false, | |
4848 reflectToAttribute: true, | |
4849 observer: '_pressedChanged' | |
4850 }, | |
4851 | |
4852 /** | |
4853 * If true, the button toggles the active state with each tap or press | |
4854 * of the spacebar. | |
4855 */ | |
4856 toggles: { | |
4857 type: Boolean, | |
4858 value: false, | |
4859 reflectToAttribute: true | |
4860 }, | |
4861 | |
4862 /** | |
4863 * If true, the button is a toggle and is currently in the active state. | |
4864 */ | |
4865 active: { | |
4866 type: Boolean, | |
4867 value: false, | |
4868 notify: true, | |
4869 reflectToAttribute: true | |
4870 }, | |
4871 | |
4872 /** | |
4873 * True if the element is currently being pressed by a "pointer," which | |
4874 * is loosely defined as mouse or touch input (but specifically excluding | |
4875 * keyboard input). | |
4876 */ | |
4877 pointerDown: { | |
4878 type: Boolean, | |
4879 readOnly: true, | |
4880 value: false | |
4881 }, | |
4882 | |
4883 /** | |
4884 * True if the input device that caused the element to receive focus | |
4885 * was a keyboard. | |
4886 */ | |
4887 receivedFocusFromKeyboard: { | |
4888 type: Boolean, | |
4889 readOnly: true | |
4890 }, | |
4891 | |
4892 /** | |
4893 * The aria attribute to be set if the button is a toggle and in the | |
4894 * active state. | |
4895 */ | |
4896 ariaActiveAttribute: { | |
4897 type: String, | |
4898 value: 'aria-pressed', | |
4899 observer: '_ariaActiveAttributeChanged' | |
4900 } | |
4901 }, | |
4902 | |
4903 listeners: { | |
4904 down: '_downHandler', | |
4905 up: '_upHandler', | |
4906 tap: '_tapHandler' | |
4907 }, | |
4908 | |
4909 observers: [ | |
4910 '_detectKeyboardFocus(focused)', | |
4911 '_activeChanged(active, ariaActiveAttribute)' | |
4912 ], | |
4913 | |
4914 keyBindings: { | |
4915 'enter:keydown': '_asyncClick', | |
4916 'space:keydown': '_spaceKeyDownHandler', | |
4917 'space:keyup': '_spaceKeyUpHandler', | |
4918 }, | |
4919 | |
4920 _mouseEventRe: /^mouse/, | |
4921 | |
4922 _tapHandler: function() { | |
4923 if (this.toggles) { | |
4924 // a tap is needed to toggle the active state | |
4925 this._userActivate(!this.active); | |
4926 } else { | |
4927 this.active = false; | |
4928 } | |
4929 }, | |
4930 | |
4931 _detectKeyboardFocus: function(focused) { | |
4932 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
4933 }, | |
4934 | |
4935 // to emulate native checkbox, (de-)activations from a user interaction fire | |
4936 // 'change' events | |
4937 _userActivate: function(active) { | |
4938 if (this.active !== active) { | |
4939 this.active = active; | |
4940 this.fire('change'); | |
4941 } | |
4942 }, | |
4943 | |
4944 _downHandler: function(event) { | |
4945 this._setPointerDown(true); | |
4946 this._setPressed(true); | |
4947 this._setReceivedFocusFromKeyboard(false); | |
4948 }, | |
4949 | |
4950 _upHandler: function() { | |
4951 this._setPointerDown(false); | |
4952 this._setPressed(false); | |
4953 }, | |
4954 | |
4955 /** | |
4956 * @param {!KeyboardEvent} event . | |
4957 */ | |
4958 _spaceKeyDownHandler: function(event) { | |
4959 var keyboardEvent = event.detail.keyboardEvent; | |
4960 var target = Polymer.dom(keyboardEvent).localTarget; | |
4961 | |
4962 // Ignore the event if this is coming from a focused light child, since th
at | |
4963 // element will deal with it. | |
4964 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4965 return; | |
4966 | |
4967 keyboardEvent.preventDefault(); | |
4968 keyboardEvent.stopImmediatePropagation(); | |
4969 this._setPressed(true); | |
4970 }, | |
4971 | |
4972 /** | |
4973 * @param {!KeyboardEvent} event . | |
4974 */ | |
4975 _spaceKeyUpHandler: function(event) { | |
4976 var keyboardEvent = event.detail.keyboardEvent; | |
4977 var target = Polymer.dom(keyboardEvent).localTarget; | |
4978 | |
4979 // Ignore the event if this is coming from a focused light child, since th
at | |
4980 // element will deal with it. | |
4981 if (this.isLightDescendant(/** @type {Node} */(target))) | |
4982 return; | |
4983 | |
4984 if (this.pressed) { | |
4985 this._asyncClick(); | |
4986 } | |
4987 this._setPressed(false); | |
4988 }, | |
4989 | |
4990 // trigger click asynchronously, the asynchrony is useful to allow one | |
4991 // event handler to unwind before triggering another event | |
4992 _asyncClick: function() { | |
4993 this.async(function() { | |
4994 this.click(); | |
4995 }, 1); | |
4996 }, | |
4997 | |
4998 // any of these changes are considered a change to button state | |
4999 | |
5000 _pressedChanged: function(pressed) { | |
5001 this._changedButtonState(); | |
5002 }, | |
5003 | |
5004 _ariaActiveAttributeChanged: function(value, oldValue) { | |
5005 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | |
5006 this.removeAttribute(oldValue); | |
5007 } | |
5008 }, | |
5009 | |
5010 _activeChanged: function(active, ariaActiveAttribute) { | |
5011 if (this.toggles) { | |
5012 this.setAttribute(this.ariaActiveAttribute, | |
5013 active ? 'true' : 'false'); | |
5014 } else { | |
5015 this.removeAttribute(this.ariaActiveAttribute); | |
5016 } | |
5017 this._changedButtonState(); | |
5018 }, | |
5019 | |
5020 _controlStateChanged: function() { | |
5021 if (this.disabled) { | |
5022 this._setPressed(false); | |
5023 } else { | |
5024 this._changedButtonState(); | |
5025 } | |
5026 }, | |
5027 | |
5028 // provide hook for follow-on behaviors to react to button-state | |
5029 | |
5030 _changedButtonState: function() { | |
5031 if (this._buttonStateChanged) { | |
5032 this._buttonStateChanged(); // abstract | |
5033 } | |
5034 } | |
5035 | |
5036 }; | |
5037 | |
5038 /** @polymerBehavior */ | |
5039 Polymer.IronButtonState = [ | |
5040 Polymer.IronA11yKeysBehavior, | |
5041 Polymer.IronButtonStateImpl | |
5042 ]; | |
5043 (function() { | |
5044 var Utility = { | |
5045 distance: function(x1, y1, x2, y2) { | |
5046 var xDelta = (x1 - x2); | |
5047 var yDelta = (y1 - y2); | |
5048 | |
5049 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
5050 }, | |
5051 | |
5052 now: window.performance && window.performance.now ? | |
5053 window.performance.now.bind(window.performance) : Date.now | |
5054 }; | |
5055 | |
5056 /** | |
5057 * @param {HTMLElement} element | |
5058 * @constructor | |
5059 */ | |
5060 function ElementMetrics(element) { | |
5061 this.element = element; | |
5062 this.width = this.boundingRect.width; | |
5063 this.height = this.boundingRect.height; | |
5064 | |
5065 this.size = Math.max(this.width, this.height); | |
5066 } | |
5067 | |
5068 ElementMetrics.prototype = { | |
5069 get boundingRect () { | |
5070 return this.element.getBoundingClientRect(); | |
5071 }, | |
5072 | |
5073 furthestCornerDistanceFrom: function(x, y) { | |
5074 var topLeft = Utility.distance(x, y, 0, 0); | |
5075 var topRight = Utility.distance(x, y, this.width, 0); | |
5076 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
5077 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
5078 | |
5079 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
5080 } | |
5081 }; | |
5082 | |
5083 /** | |
5084 * @param {HTMLElement} element | |
5085 * @constructor | |
5086 */ | |
5087 function Ripple(element) { | |
5088 this.element = element; | |
5089 this.color = window.getComputedStyle(element).color; | |
5090 | |
5091 this.wave = document.createElement('div'); | |
5092 this.waveContainer = document.createElement('div'); | |
5093 this.wave.style.backgroundColor = this.color; | |
5094 this.wave.classList.add('wave'); | |
5095 this.waveContainer.classList.add('wave-container'); | |
5096 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
5097 | |
5098 this.resetInteractionState(); | |
5099 } | |
5100 | |
5101 Ripple.MAX_RADIUS = 300; | |
5102 | |
5103 Ripple.prototype = { | |
5104 get recenters() { | |
5105 return this.element.recenters; | |
5106 }, | |
5107 | |
5108 get center() { | |
5109 return this.element.center; | |
5110 }, | |
5111 | |
5112 get mouseDownElapsed() { | |
5113 var elapsed; | |
5114 | |
5115 if (!this.mouseDownStart) { | |
5116 return 0; | |
5117 } | |
5118 | |
5119 elapsed = Utility.now() - this.mouseDownStart; | |
5120 | |
5121 if (this.mouseUpStart) { | |
5122 elapsed -= this.mouseUpElapsed; | |
5123 } | |
5124 | |
5125 return elapsed; | |
5126 }, | |
5127 | |
5128 get mouseUpElapsed() { | |
5129 return this.mouseUpStart ? | |
5130 Utility.now () - this.mouseUpStart : 0; | |
5131 }, | |
5132 | |
5133 get mouseDownElapsedSeconds() { | |
5134 return this.mouseDownElapsed / 1000; | |
5135 }, | |
5136 | |
5137 get mouseUpElapsedSeconds() { | |
5138 return this.mouseUpElapsed / 1000; | |
5139 }, | |
5140 | |
5141 get mouseInteractionSeconds() { | |
5142 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
5143 }, | |
5144 | |
5145 get initialOpacity() { | |
5146 return this.element.initialOpacity; | |
5147 }, | |
5148 | |
5149 get opacityDecayVelocity() { | |
5150 return this.element.opacityDecayVelocity; | |
5151 }, | |
5152 | |
5153 get radius() { | |
5154 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
5155 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
5156 var waveRadius = Math.min( | |
5157 Math.sqrt(width2 + height2), | |
5158 Ripple.MAX_RADIUS | |
5159 ) * 1.1 + 5; | |
5160 | |
5161 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
5162 var timeNow = this.mouseInteractionSeconds / duration; | |
5163 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
5164 | |
5165 return Math.abs(size); | |
5166 }, | |
5167 | |
5168 get opacity() { | |
5169 if (!this.mouseUpStart) { | |
5170 return this.initialOpacity; | |
5171 } | |
5172 | |
5173 return Math.max( | |
5174 0, | |
5175 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
5176 ); | |
5177 }, | |
5178 | |
5179 get outerOpacity() { | |
5180 // Linear increase in background opacity, capped at the opacity | |
5181 // of the wavefront (waveOpacity). | |
5182 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
5183 var waveOpacity = this.opacity; | |
5184 | |
5185 return Math.max( | |
5186 0, | |
5187 Math.min(outerOpacity, waveOpacity) | |
5188 ); | |
5189 }, | |
5190 | |
5191 get isOpacityFullyDecayed() { | |
5192 return this.opacity < 0.01 && | |
5193 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5194 }, | |
5195 | |
5196 get isRestingAtMaxRadius() { | |
5197 return this.opacity >= this.initialOpacity && | |
5198 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
5199 }, | |
5200 | |
5201 get isAnimationComplete() { | |
5202 return this.mouseUpStart ? | |
5203 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
5204 }, | |
5205 | |
5206 get translationFraction() { | |
5207 return Math.min( | |
5208 1, | |
5209 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
5210 ); | |
5211 }, | |
5212 | |
5213 get xNow() { | |
5214 if (this.xEnd) { | |
5215 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
5216 } | |
5217 | |
5218 return this.xStart; | |
5219 }, | |
5220 | |
5221 get yNow() { | |
5222 if (this.yEnd) { | |
5223 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
5224 } | |
5225 | |
5226 return this.yStart; | |
5227 }, | |
5228 | |
5229 get isMouseDown() { | |
5230 return this.mouseDownStart && !this.mouseUpStart; | |
5231 }, | |
5232 | |
5233 resetInteractionState: function() { | |
5234 this.maxRadius = 0; | |
5235 this.mouseDownStart = 0; | |
5236 this.mouseUpStart = 0; | |
5237 | |
5238 this.xStart = 0; | |
5239 this.yStart = 0; | |
5240 this.xEnd = 0; | |
5241 this.yEnd = 0; | |
5242 this.slideDistance = 0; | |
5243 | |
5244 this.containerMetrics = new ElementMetrics(this.element); | |
5245 }, | |
5246 | |
5247 draw: function() { | |
5248 var scale; | |
5249 var translateString; | |
5250 var dx; | |
5251 var dy; | |
5252 | |
5253 this.wave.style.opacity = this.opacity; | |
5254 | |
5255 scale = this.radius / (this.containerMetrics.size / 2); | |
5256 dx = this.xNow - (this.containerMetrics.width / 2); | |
5257 dy = this.yNow - (this.containerMetrics.height / 2); | |
5258 | |
5259 | |
5260 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
5261 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
5262 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
5263 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
5264 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
5265 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
5266 }, | |
5267 | |
5268 /** @param {Event=} event */ | |
5269 downAction: function(event) { | |
5270 var xCenter = this.containerMetrics.width / 2; | |
5271 var yCenter = this.containerMetrics.height / 2; | |
5272 | |
5273 this.resetInteractionState(); | |
5274 this.mouseDownStart = Utility.now(); | |
5275 | |
5276 if (this.center) { | |
5277 this.xStart = xCenter; | |
5278 this.yStart = yCenter; | |
5279 this.slideDistance = Utility.distance( | |
5280 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5281 ); | |
5282 } else { | |
5283 this.xStart = event ? | |
5284 event.detail.x - this.containerMetrics.boundingRect.left : | |
5285 this.containerMetrics.width / 2; | |
5286 this.yStart = event ? | |
5287 event.detail.y - this.containerMetrics.boundingRect.top : | |
5288 this.containerMetrics.height / 2; | |
5289 } | |
5290 | |
5291 if (this.recenters) { | |
5292 this.xEnd = xCenter; | |
5293 this.yEnd = yCenter; | |
5294 this.slideDistance = Utility.distance( | |
5295 this.xStart, this.yStart, this.xEnd, this.yEnd | |
5296 ); | |
5297 } | |
5298 | |
5299 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
5300 this.xStart, | |
5301 this.yStart | |
5302 ); | |
5303 | |
5304 this.waveContainer.style.top = | |
5305 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
5306 this.waveContainer.style.left = | |
5307 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
5308 | |
5309 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
5310 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
5311 }, | |
5312 | |
5313 /** @param {Event=} event */ | |
5314 upAction: function(event) { | |
5315 if (!this.isMouseDown) { | |
5316 return; | |
5317 } | |
5318 | |
5319 this.mouseUpStart = Utility.now(); | |
5320 }, | |
5321 | |
5322 remove: function() { | |
5323 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
5324 this.waveContainer | |
5325 ); | |
5326 } | |
5327 }; | |
5328 | |
5329 Polymer({ | |
5330 is: 'paper-ripple', | |
5331 | |
5332 behaviors: [ | |
5333 Polymer.IronA11yKeysBehavior | |
5334 ], | |
5335 | |
5336 properties: { | |
5337 /** | |
5338 * The initial opacity set on the wave. | |
5339 * | |
5340 * @attribute initialOpacity | |
5341 * @type number | |
5342 * @default 0.25 | |
5343 */ | |
5344 initialOpacity: { | |
5345 type: Number, | |
5346 value: 0.25 | |
5347 }, | |
5348 | |
5349 /** | |
5350 * How fast (opacity per second) the wave fades out. | |
5351 * | |
5352 * @attribute opacityDecayVelocity | |
5353 * @type number | |
5354 * @default 0.8 | |
5355 */ | |
5356 opacityDecayVelocity: { | |
5357 type: Number, | |
5358 value: 0.8 | |
5359 }, | |
5360 | |
5361 /** | |
5362 * If true, ripples will exhibit a gravitational pull towards | |
5363 * the center of their container as they fade away. | |
5364 * | |
5365 * @attribute recenters | |
5366 * @type boolean | |
5367 * @default false | |
5368 */ | |
5369 recenters: { | |
5370 type: Boolean, | |
5371 value: false | |
5372 }, | |
5373 | |
5374 /** | |
5375 * If true, ripples will center inside its container | |
5376 * | |
5377 * @attribute recenters | |
5378 * @type boolean | |
5379 * @default false | |
5380 */ | |
5381 center: { | |
5382 type: Boolean, | |
5383 value: false | |
5384 }, | |
5385 | |
5386 /** | |
5387 * A list of the visual ripples. | |
5388 * | |
5389 * @attribute ripples | |
5390 * @type Array | |
5391 * @default [] | |
5392 */ | |
5393 ripples: { | |
5394 type: Array, | |
5395 value: function() { | |
5396 return []; | |
5397 } | |
5398 }, | |
5399 | |
5400 /** | |
5401 * True when there are visible ripples animating within the | |
5402 * element. | |
5403 */ | |
5404 animating: { | |
5405 type: Boolean, | |
5406 readOnly: true, | |
5407 reflectToAttribute: true, | |
5408 value: false | |
5409 }, | |
5410 | |
5411 /** | |
5412 * If true, the ripple will remain in the "down" state until `holdDown` | |
5413 * is set to false again. | |
5414 */ | |
5415 holdDown: { | |
5416 type: Boolean, | |
5417 value: false, | |
5418 observer: '_holdDownChanged' | |
5419 }, | |
5420 | |
5421 /** | |
5422 * If true, the ripple will not generate a ripple effect | |
5423 * via pointer interaction. | |
5424 * Calling ripple's imperative api like `simulatedRipple` will | |
5425 * still generate the ripple effect. | |
5426 */ | |
5427 noink: { | |
5428 type: Boolean, | |
5429 value: false | |
5430 }, | |
5431 | |
5432 _animating: { | |
5433 type: Boolean | |
5434 }, | |
5435 | |
5436 _boundAnimate: { | |
5437 type: Function, | |
5438 value: function() { | |
5439 return this.animate.bind(this); | |
5440 } | |
5441 } | |
5442 }, | |
5443 | |
5444 get target () { | |
5445 return this.keyEventTarget; | |
5446 }, | |
5447 | |
5448 keyBindings: { | |
5449 'enter:keydown': '_onEnterKeydown', | |
5450 'space:keydown': '_onSpaceKeydown', | |
5451 'space:keyup': '_onSpaceKeyup' | |
5452 }, | |
5453 | |
5454 attached: function() { | |
5455 // Set up a11yKeysBehavior to listen to key events on the target, | |
5456 // so that space and enter activate the ripple even if the target doesn'
t | |
5457 // handle key events. The key handlers deal with `noink` themselves. | |
5458 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
5459 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
5460 } else { | |
5461 this.keyEventTarget = this.parentNode; | |
5462 } | |
5463 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
5464 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
5465 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
5466 }, | |
5467 | |
5468 detached: function() { | |
5469 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
5470 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
5471 this.keyEventTarget = null; | |
5472 }, | |
5473 | |
5474 get shouldKeepAnimating () { | |
5475 for (var index = 0; index < this.ripples.length; ++index) { | |
5476 if (!this.ripples[index].isAnimationComplete) { | |
5477 return true; | |
5478 } | |
5479 } | |
5480 | |
5481 return false; | |
5482 }, | |
5483 | |
5484 simulatedRipple: function() { | |
5485 this.downAction(null); | |
5486 | |
5487 // Please see polymer/polymer#1305 | |
5488 this.async(function() { | |
5489 this.upAction(); | |
5490 }, 1); | |
5491 }, | |
5492 | |
5493 /** | |
5494 * Provokes a ripple down effect via a UI event, | |
5495 * respecting the `noink` property. | |
5496 * @param {Event=} event | |
5497 */ | |
5498 uiDownAction: function(event) { | |
5499 if (!this.noink) { | |
5500 this.downAction(event); | |
5501 } | |
5502 }, | |
5503 | |
5504 /** | |
5505 * Provokes a ripple down effect via a UI event, | |
5506 * *not* respecting the `noink` property. | |
5507 * @param {Event=} event | |
5508 */ | |
5509 downAction: function(event) { | |
5510 if (this.holdDown && this.ripples.length > 0) { | |
5511 return; | |
5512 } | |
5513 | |
5514 var ripple = this.addRipple(); | |
5515 | |
5516 ripple.downAction(event); | |
5517 | |
5518 if (!this._animating) { | |
5519 this._animating = true; | |
5520 this.animate(); | |
5521 } | |
5522 }, | |
5523 | |
5524 /** | |
5525 * Provokes a ripple up effect via a UI event, | |
5526 * respecting the `noink` property. | |
5527 * @param {Event=} event | |
5528 */ | |
5529 uiUpAction: function(event) { | |
5530 if (!this.noink) { | |
5531 this.upAction(event); | |
5532 } | |
5533 }, | |
5534 | |
5535 /** | |
5536 * Provokes a ripple up effect via a UI event, | |
5537 * *not* respecting the `noink` property. | |
5538 * @param {Event=} event | |
5539 */ | |
5540 upAction: function(event) { | |
5541 if (this.holdDown) { | |
5542 return; | |
5543 } | |
5544 | |
5545 this.ripples.forEach(function(ripple) { | |
5546 ripple.upAction(event); | |
5547 }); | |
5548 | |
5549 this._animating = true; | |
5550 this.animate(); | |
5551 }, | |
5552 | |
5553 onAnimationComplete: function() { | |
5554 this._animating = false; | |
5555 this.$.background.style.backgroundColor = null; | |
5556 this.fire('transitionend'); | |
5557 }, | |
5558 | |
5559 addRipple: function() { | |
5560 var ripple = new Ripple(this); | |
5561 | |
5562 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
5563 this.$.background.style.backgroundColor = ripple.color; | |
5564 this.ripples.push(ripple); | |
5565 | |
5566 this._setAnimating(true); | |
5567 | |
5568 return ripple; | |
5569 }, | |
5570 | |
5571 removeRipple: function(ripple) { | |
5572 var rippleIndex = this.ripples.indexOf(ripple); | |
5573 | |
5574 if (rippleIndex < 0) { | |
5575 return; | |
5576 } | |
5577 | |
5578 this.ripples.splice(rippleIndex, 1); | |
5579 | |
5580 ripple.remove(); | |
5581 | |
5582 if (!this.ripples.length) { | |
5583 this._setAnimating(false); | |
5584 } | |
5585 }, | |
5586 | |
5587 animate: function() { | |
5588 if (!this._animating) { | |
5589 return; | |
5590 } | |
5591 var index; | |
5592 var ripple; | |
5593 | |
5594 for (index = 0; index < this.ripples.length; ++index) { | |
5595 ripple = this.ripples[index]; | |
5596 | |
5597 ripple.draw(); | |
5598 | |
5599 this.$.background.style.opacity = ripple.outerOpacity; | |
5600 | |
5601 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
5602 this.removeRipple(ripple); | |
5603 } | |
5604 } | |
5605 | |
5606 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
5607 this.onAnimationComplete(); | |
5608 } else { | |
5609 window.requestAnimationFrame(this._boundAnimate); | |
5610 } | |
5611 }, | |
5612 | |
5613 _onEnterKeydown: function() { | |
5614 this.uiDownAction(); | |
5615 this.async(this.uiUpAction, 1); | |
5616 }, | |
5617 | |
5618 _onSpaceKeydown: function() { | |
5619 this.uiDownAction(); | |
5620 }, | |
5621 | |
5622 _onSpaceKeyup: function() { | |
5623 this.uiUpAction(); | |
5624 }, | |
5625 | |
5626 // note: holdDown does not respect noink since it can be a focus based | |
5627 // effect. | |
5628 _holdDownChanged: function(newVal, oldVal) { | |
5629 if (oldVal === undefined) { | |
5630 return; | |
5631 } | |
5632 if (newVal) { | |
5633 this.downAction(); | |
5634 } else { | |
5635 this.upAction(); | |
5636 } | |
5637 } | |
5638 | |
5639 /** | |
5640 Fired when the animation finishes. | |
5641 This is useful if you want to wait until | |
5642 the ripple animation finishes to perform some action. | |
5643 | |
5644 @event transitionend | |
5645 @param {{node: Object}} detail Contains the animated node. | |
5646 */ | |
5647 }); | |
5648 })(); | |
5649 /** | |
5650 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | |
5651 * when the element has focus via pointer or keyboard. | |
5652 * | |
5653 * NOTE: This behavior is intended to be used in conjunction with and after | |
5654 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | |
5655 * | |
5656 * @polymerBehavior Polymer.PaperRippleBehavior | |
5657 */ | |
5658 Polymer.PaperRippleBehavior = { | |
5659 properties: { | |
5660 /** | |
5661 * If true, the element will not produce a ripple effect when interacted | |
5662 * with via the pointer. | |
5663 */ | |
5664 noink: { | |
5665 type: Boolean, | |
5666 observer: '_noinkChanged' | |
5667 }, | |
5668 | |
5669 /** | |
5670 * @type {Element|undefined} | |
5671 */ | |
5672 _rippleContainer: { | |
5673 type: Object, | |
5674 } | |
5675 }, | |
5676 | |
5677 /** | |
5678 * Ensures a `<paper-ripple>` element is available when the element is | |
5679 * focused. | |
5680 */ | |
5681 _buttonStateChanged: function() { | |
5682 if (this.focused) { | |
5683 this.ensureRipple(); | 5229 this.ensureRipple(); |
5684 } | 5230 } |
5685 }, | 5231 if (this.hasRipple()) { |
5686 | 5232 this._ripple.holdDown = receivedFocusFromKeyboard; |
5687 /** | 5233 } |
5688 * In addition to the functionality provided in `IronButtonState`, ensures | 5234 }, |
5689 * a ripple effect is created when the element is in a `pressed` state. | 5235 |
5690 */ | |
5691 _downHandler: function(event) { | |
5692 Polymer.IronButtonStateImpl._downHandler.call(this, event); | |
5693 if (this.pressed) { | |
5694 this.ensureRipple(event); | |
5695 } | |
5696 }, | |
5697 | |
5698 /** | |
5699 * Ensures this element contains a ripple effect. For startup efficiency | |
5700 * the ripple effect is dynamically on demand when needed. | |
5701 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | |
5702 * ripple. | |
5703 */ | |
5704 ensureRipple: function(optTriggeringEvent) { | |
5705 if (!this.hasRipple()) { | |
5706 this._ripple = this._createRipple(); | |
5707 this._ripple.noink = this.noink; | |
5708 var rippleContainer = this._rippleContainer || this.root; | |
5709 if (rippleContainer) { | |
5710 Polymer.dom(rippleContainer).appendChild(this._ripple); | |
5711 } | |
5712 if (optTriggeringEvent) { | |
5713 // Check if the event happened inside of the ripple container | |
5714 // Fall back to host instead of the root because distributed text | |
5715 // nodes are not valid event targets | |
5716 var domContainer = Polymer.dom(this._rippleContainer || this); | |
5717 var target = Polymer.dom(optTriggeringEvent).rootTarget; | |
5718 if (domContainer.deepContains( /** @type {Node} */(target))) { | |
5719 this._ripple.uiDownAction(optTriggeringEvent); | |
5720 } | |
5721 } | |
5722 } | |
5723 }, | |
5724 | |
5725 /** | |
5726 * Returns the `<paper-ripple>` element used by this element to create | |
5727 * ripple effects. The element's ripple is created on demand, when | |
5728 * necessary, and calling this method will force the | |
5729 * ripple to be created. | |
5730 */ | |
5731 getRipple: function() { | |
5732 this.ensureRipple(); | |
5733 return this._ripple; | |
5734 }, | |
5735 | |
5736 /** | |
5737 * Returns true if this element currently contains a ripple effect. | |
5738 * @return {boolean} | |
5739 */ | |
5740 hasRipple: function() { | |
5741 return Boolean(this._ripple); | |
5742 }, | |
5743 | |
5744 /** | |
5745 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
5746 * Override this method to customize the ripple element. | |
5747 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
5748 */ | |
5749 _createRipple: function() { | 5236 _createRipple: function() { |
5750 return /** @type {!PaperRippleElement} */ ( | 5237 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
5751 document.createElement('paper-ripple')); | 5238 ripple.id = 'ink'; |
5752 }, | 5239 ripple.setAttribute('center', ''); |
5753 | 5240 ripple.classList.add('circle'); |
5754 _noinkChanged: function(noink) { | 5241 return ripple; |
5755 if (this.hasRipple()) { | |
5756 this._ripple.noink = noink; | |
5757 } | |
5758 } | 5242 } |
5759 }; | 5243 }; |
5760 /** @polymerBehavior Polymer.PaperButtonBehavior */ | 5244 |
5761 Polymer.PaperButtonBehaviorImpl = { | 5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ |
5762 properties: { | 5246 Polymer.PaperInkyFocusBehavior = [ |
5763 /** | |
5764 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
5765 * shadow, and each increasing number greater than 0 will be "deeper" | |
5766 * than the last. | |
5767 * | |
5768 * @attribute elevation | |
5769 * @type number | |
5770 * @default 1 | |
5771 */ | |
5772 elevation: { | |
5773 type: Number, | |
5774 reflectToAttribute: true, | |
5775 readOnly: true | |
5776 } | |
5777 }, | |
5778 | |
5779 observers: [ | |
5780 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
5781 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
5782 ], | |
5783 | |
5784 hostAttributes: { | |
5785 role: 'button', | |
5786 tabindex: '0', | |
5787 animated: true | |
5788 }, | |
5789 | |
5790 _calculateElevation: function() { | |
5791 var e = 1; | |
5792 if (this.disabled) { | |
5793 e = 0; | |
5794 } else if (this.active || this.pressed) { | |
5795 e = 4; | |
5796 } else if (this.receivedFocusFromKeyboard) { | |
5797 e = 3; | |
5798 } | |
5799 this._setElevation(e); | |
5800 }, | |
5801 | |
5802 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
5803 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
5804 }, | |
5805 | |
5806 /** | |
5807 * In addition to `IronButtonState` behavior, when space key goes down, | |
5808 * create a ripple down effect. | |
5809 * | |
5810 * @param {!KeyboardEvent} event . | |
5811 */ | |
5812 _spaceKeyDownHandler: function(event) { | |
5813 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
5814 // Ensure that there is at most one ripple when the space key is held down
. | |
5815 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
5816 this._ripple.uiDownAction(); | |
5817 } | |
5818 }, | |
5819 | |
5820 /** | |
5821 * In addition to `IronButtonState` behavior, when space key goes up, | |
5822 * create a ripple up effect. | |
5823 * | |
5824 * @param {!KeyboardEvent} event . | |
5825 */ | |
5826 _spaceKeyUpHandler: function(event) { | |
5827 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
5828 if (this.hasRipple()) { | |
5829 this._ripple.uiUpAction(); | |
5830 } | |
5831 } | |
5832 }; | |
5833 | |
5834 /** @polymerBehavior */ | |
5835 Polymer.PaperButtonBehavior = [ | |
5836 Polymer.IronButtonState, | 5247 Polymer.IronButtonState, |
5837 Polymer.IronControlState, | 5248 Polymer.IronControlState, |
5838 Polymer.PaperRippleBehavior, | 5249 Polymer.PaperRippleBehavior, |
5839 Polymer.PaperButtonBehaviorImpl | 5250 Polymer.PaperInkyFocusBehaviorImpl |
5840 ]; | 5251 ]; |
5841 Polymer({ | 5252 Polymer({ |
5842 is: 'paper-button', | 5253 is: 'paper-icon-button', |
| 5254 |
| 5255 hostAttributes: { |
| 5256 role: 'button', |
| 5257 tabindex: '0' |
| 5258 }, |
5843 | 5259 |
5844 behaviors: [ | 5260 behaviors: [ |
5845 Polymer.PaperButtonBehavior | 5261 Polymer.PaperInkyFocusBehavior |
5846 ], | 5262 ], |
5847 | 5263 |
5848 properties: { | 5264 properties: { |
5849 /** | 5265 /** |
5850 * 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. |
5851 */ | 5268 */ |
5852 raised: { | 5269 src: { |
5853 type: Boolean, | 5270 type: String |
5854 reflectToAttribute: true, | 5271 }, |
5855 value: false, | 5272 |
5856 observer: '_calculateElevation' | 5273 /** |
5857 } | 5274 * Specifies the icon name or index in the set of icons available in |
5858 }, | 5275 * the icon's icon set. If the icon property is specified, |
5859 | 5276 * the src property should not be. |
5860 _calculateElevation: function() { | 5277 */ |
5861 if (!this.raised) { | 5278 icon: { |
5862 this._setElevation(0); | 5279 type: String |
5863 } else { | 5280 }, |
5864 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | 5281 |
5865 } | 5282 /** |
5866 } | 5283 * Specifies the alternate text for the button, for accessibility. |
5867 | 5284 */ |
5868 /** | 5285 alt: { |
5869 Fired when the animation finishes. | 5286 type: String, |
5870 This is useful if you want to wait until | 5287 observer: "_altChanged" |
5871 the ripple animation finishes to perform some action. | 5288 } |
5872 | 5289 }, |
5873 @event transitionend | 5290 |
5874 Event param: {{node: Object}} detail Contains the animated node. | 5291 _altChanged: function(newValue, oldValue) { |
5875 */ | 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 } |
5876 }); | 5299 }); |
5877 Polymer({ | 5300 Polymer({ |
5878 is: 'paper-icon-button-light', | 5301 is: 'paper-tab', |
5879 extends: 'button', | |
5880 | 5302 |
5881 behaviors: [ | 5303 behaviors: [ |
| 5304 Polymer.IronControlState, |
| 5305 Polymer.IronButtonState, |
5882 Polymer.PaperRippleBehavior | 5306 Polymer.PaperRippleBehavior |
5883 ], | 5307 ], |
5884 | 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 |
5885 listeners: { | 5327 listeners: { |
5886 'down': '_rippleDown', | 5328 down: '_updateNoink', |
5887 'up': '_rippleUp', | 5329 tap: '_onTap' |
5888 'focus': '_rippleDown', | 5330 }, |
5889 'blur': '_rippleUp', | 5331 |
5890 }, | 5332 attached: function() { |
5891 | 5333 this._updateNoink(); |
5892 _rippleDown: function() { | 5334 }, |
5893 this.getRipple().downAction(); | 5335 |
5894 }, | 5336 get _parentNoink () { |
5895 | 5337 var parent = Polymer.dom(this).parentNode; |
5896 _rippleUp: function() { | 5338 return !!parent && !!parent.noink; |
5897 this.getRipple().upAction(); | 5339 }, |
5898 }, | 5340 |
5899 | 5341 _updateNoink: function() { |
5900 /** | 5342 this.noink = !!this.noink || !!this._parentNoink; |
5901 * @param {...*} var_args | 5343 }, |
5902 */ | 5344 |
5903 ensureRipple: function(var_args) { | 5345 _onTap: function(event) { |
5904 var lastRipple = this._ripple; | 5346 if (this.link) { |
5905 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | 5347 var anchor = this.queryEffectiveChildren('a'); |
5906 if (this._ripple && this._ripple !== lastRipple) { | 5348 |
5907 this._ripple.center = true; | 5349 if (!anchor) { |
5908 this._ripple.classList.add('circle'); | 5350 return; |
5909 } | 5351 } |
5910 } | 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 |
5911 }); | 5363 }); |
5912 /** | 5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ |
5913 * `iron-range-behavior` provides the behavior for something with a minimum to m
aximum range. | 5365 Polymer.IronMultiSelectableBehaviorImpl = { |
5914 * | |
5915 * @demo demo/index.html | |
5916 * @polymerBehavior | |
5917 */ | |
5918 Polymer.IronRangeBehavior = { | |
5919 | |
5920 properties: { | |
5921 | |
5922 /** | |
5923 * The number that represents the current value. | |
5924 */ | |
5925 value: { | |
5926 type: Number, | |
5927 value: 0, | |
5928 notify: true, | |
5929 reflectToAttribute: true | |
5930 }, | |
5931 | |
5932 /** | |
5933 * The number that indicates the minimum value of the range. | |
5934 */ | |
5935 min: { | |
5936 type: Number, | |
5937 value: 0, | |
5938 notify: true | |
5939 }, | |
5940 | |
5941 /** | |
5942 * The number that indicates the maximum value of the range. | |
5943 */ | |
5944 max: { | |
5945 type: Number, | |
5946 value: 100, | |
5947 notify: true | |
5948 }, | |
5949 | |
5950 /** | |
5951 * Specifies the value granularity of the range's value. | |
5952 */ | |
5953 step: { | |
5954 type: Number, | |
5955 value: 1, | |
5956 notify: true | |
5957 }, | |
5958 | |
5959 /** | |
5960 * Returns the ratio of the value. | |
5961 */ | |
5962 ratio: { | |
5963 type: Number, | |
5964 value: 0, | |
5965 readOnly: true, | |
5966 notify: true | |
5967 }, | |
5968 }, | |
5969 | |
5970 observers: [ | |
5971 '_update(value, min, max, step)' | |
5972 ], | |
5973 | |
5974 _calcRatio: function(value) { | |
5975 return (this._clampValue(value) - this.min) / (this.max - this.min); | |
5976 }, | |
5977 | |
5978 _clampValue: function(value) { | |
5979 return Math.min(this.max, Math.max(this.min, this._calcStep(value))); | |
5980 }, | |
5981 | |
5982 _calcStep: function(value) { | |
5983 // polymer/issues/2493 | |
5984 value = parseFloat(value); | |
5985 | |
5986 if (!this.step) { | |
5987 return value; | |
5988 } | |
5989 | |
5990 var numSteps = Math.round((value - this.min) / this.step); | |
5991 if (this.step < 1) { | |
5992 /** | |
5993 * For small values of this.step, if we calculate the step using | |
5994 * `Math.round(value / step) * step` we may hit a precision point issue | |
5995 * eg. 0.1 * 0.2 = 0.020000000000000004 | |
5996 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html | |
5997 * | |
5998 * as a work around we can divide by the reciprocal of `step` | |
5999 */ | |
6000 return numSteps / (1 / this.step) + this.min; | |
6001 } else { | |
6002 return numSteps * this.step + this.min; | |
6003 } | |
6004 }, | |
6005 | |
6006 _validateValue: function() { | |
6007 var v = this._clampValue(this.value); | |
6008 this.value = this.oldValue = isNaN(v) ? this.oldValue : v; | |
6009 return this.value !== v; | |
6010 }, | |
6011 | |
6012 _update: function() { | |
6013 this._validateValue(); | |
6014 this._setRatio(this._calcRatio(this.value) * 100); | |
6015 } | |
6016 | |
6017 }; | |
6018 Polymer({ | |
6019 is: 'paper-progress', | |
6020 | |
6021 behaviors: [ | |
6022 Polymer.IronRangeBehavior | |
6023 ], | |
6024 | |
6025 properties: { | 5366 properties: { |
6026 /** | 5367 |
6027 * The number that represents the current secondary progress. | 5368 /** |
6028 */ | 5369 * If true, multiple selections are allowed. |
6029 secondaryProgress: { | 5370 */ |
6030 type: Number, | 5371 multi: { |
6031 value: 0 | |
6032 }, | |
6033 | |
6034 /** | |
6035 * The secondary ratio | |
6036 */ | |
6037 secondaryRatio: { | |
6038 type: Number, | |
6039 value: 0, | |
6040 readOnly: true | |
6041 }, | |
6042 | |
6043 /** | |
6044 * Use an indeterminate progress indicator. | |
6045 */ | |
6046 indeterminate: { | |
6047 type: Boolean, | 5372 type: Boolean, |
6048 value: false, | 5373 value: false, |
6049 observer: '_toggleIndeterminate' | 5374 observer: 'multiChanged' |
6050 }, | 5375 }, |
6051 | 5376 |
6052 /** | 5377 /** |
6053 * True if the progress is disabled. | 5378 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` |
6054 */ | 5379 * is true. |
6055 disabled: { | 5380 */ |
6056 type: Boolean, | 5381 selectedValues: { |
6057 value: false, | 5382 type: Array, |
6058 reflectToAttribute: true, | 5383 notify: true |
6059 observer: '_disabledChanged' | 5384 }, |
6060 } | 5385 |
| 5386 /** |
| 5387 * Returns an array of currently selected items. |
| 5388 */ |
| 5389 selectedItems: { |
| 5390 type: Array, |
| 5391 readOnly: true, |
| 5392 notify: true |
| 5393 }, |
| 5394 |
6061 }, | 5395 }, |
6062 | 5396 |
6063 observers: [ | 5397 observers: [ |
6064 '_progressChanged(secondaryProgress, value, min, max)' | 5398 '_updateSelected(selectedValues.splices)' |
6065 ], | 5399 ], |
6066 | 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 |
6067 hostAttributes: { | 5532 hostAttributes: { |
6068 role: 'progressbar' | 5533 'role': 'menu', |
6069 }, | 5534 'tabindex': '0' |
6070 | 5535 }, |
6071 _toggleIndeterminate: function(indeterminate) { | 5536 |
6072 // If we use attribute/class binding, the animation sometimes doesn't tran
slate properly | 5537 observers: [ |
6073 // on Safari 7.1. So instead, we toggle the class here in the update metho
d. | 5538 '_updateMultiselectable(multi)' |
6074 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); | 5539 ], |
6075 }, | 5540 |
6076 | 5541 listeners: { |
6077 _transformProgress: function(progress, ratio) { | 5542 'focus': '_onFocus', |
6078 var transform = 'scaleX(' + (ratio / 100) + ')'; | 5543 'keydown': '_onKeydown', |
6079 progress.style.transform = progress.style.webkitTransform = transform; | 5544 'iron-items-changed': '_onIronItemsChanged' |
6080 }, | 5545 }, |
6081 | 5546 |
6082 _mainRatioChanged: function(ratio) { | 5547 keyBindings: { |
6083 this._transformProgress(this.$.primaryProgress, ratio); | 5548 'up': '_onUpKey', |
6084 }, | 5549 'down': '_onDownKey', |
6085 | 5550 'esc': '_onEscKey', |
6086 _progressChanged: function(secondaryProgress, value, min, max) { | 5551 'shift+tab:keydown': '_onShiftTabDown' |
6087 secondaryProgress = this._clampValue(secondaryProgress); | 5552 }, |
6088 value = this._clampValue(value); | 5553 |
6089 | 5554 attached: function() { |
6090 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; | 5555 this._resetTabindices(); |
6091 var mainRatio = this._calcRatio(value) * 100; | 5556 }, |
6092 | 5557 |
6093 this._setSecondaryRatio(secondaryRatio); | 5558 /** |
6094 this._transformProgress(this.$.secondaryProgress, secondaryRatio); | 5559 * Selects the given value. If the `multi` property is true, then the select
ed state of the |
6095 this._transformProgress(this.$.primaryProgress, mainRatio); | 5560 * `value` will be toggled; otherwise the `value` will be selected. |
6096 | 5561 * |
6097 this.secondaryProgress = secondaryProgress; | 5562 * @param {string|number} value the value to select. |
6098 | 5563 */ |
6099 this.setAttribute('aria-valuenow', value); | 5564 select: function(value) { |
6100 this.setAttribute('aria-valuemin', min); | 5565 // Cancel automatically focusing a default item if the menu received focus |
6101 this.setAttribute('aria-valuemax', max); | 5566 // through a user action selecting a particular item. |
6102 }, | 5567 if (this._defaultFocusAsync) { |
6103 | 5568 this.cancelAsync(this._defaultFocusAsync); |
6104 _disabledChanged: function(disabled) { | 5569 this._defaultFocusAsync = null; |
6105 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | 5570 } |
6106 }, | 5571 var item = this._valueToItem(value); |
6107 | 5572 if (item && item.hasAttribute('disabled')) return; |
6108 _hideSecondaryProgress: function(secondaryRatio) { | 5573 this._setFocusedItem(item); |
6109 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(); |
6110 } | 5809 } |
6111 }); | 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 ]; |
6112 /** | 5884 /** |
6113 * 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 |
6114 * 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 |
6115 * `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. |
6116 * | 5888 * |
6117 * 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 |
6118 * 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 |
6119 * 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 |
6120 * 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. |
6121 * | 5893 * |
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
6281 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | 6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 |
6282 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | 6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root |
6283 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | 6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; |
6284 svg.appendChild(content).removeAttribute('id'); | 6056 svg.appendChild(content).removeAttribute('id'); |
6285 return svg; | 6057 return svg; |
6286 } | 6058 } |
6287 return null; | 6059 return null; |
6288 } | 6060 } |
6289 | 6061 |
6290 }); | 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 }); |
6291 // Copyright 2015 The Chromium Authors. All rights reserved. | 7592 // Copyright 2015 The Chromium Authors. All rights reserved. |
6292 // 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 |
6293 // found in the LICENSE file. | 7594 // found in the LICENSE file. |
6294 | 7595 |
6295 cr.define('downloads', function() { | 7596 Polymer({ |
6296 var Item = Polymer({ | 7597 is: 'history-toolbar', |
6297 is: 'downloads-item', | 7598 properties: { |
6298 | 7599 // Number of history items currently selected. |
6299 properties: { | 7600 // TODO(calamity): bind this to |
6300 data: { | 7601 // listContainer.selectedItem.selectedPaths.length. |
6301 type: Object, | 7602 count: { |
6302 }, | 7603 type: Number, |
6303 | 7604 value: 0, |
6304 completelyOnDisk_: { | 7605 observer: 'changeToolbarView_' |
6305 computed: 'computeCompletelyOnDisk_(' + | 7606 }, |
6306 'data.state, data.file_externally_removed)', | 7607 |
6307 type: Boolean, | 7608 // True if 1 or more history items are selected. When this value changes |
6308 value: true, | 7609 // the background colour changes. |
6309 }, | 7610 itemsSelected_: { |
6310 | 7611 type: Boolean, |
6311 controlledBy_: { | 7612 value: false, |
6312 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', | 7613 reflectToAttribute: true |
6313 type: String, | 7614 }, |
6314 value: '', | 7615 |
6315 }, | 7616 // The most recent term entered in the search field. Updated incrementally |
6316 | 7617 // as the user types. |
6317 isActive_: { | 7618 searchTerm: { |
6318 computed: 'computeIsActive_(' + | 7619 type: String, |
6319 'data.state, data.file_externally_removed)', | 7620 notify: true, |
6320 type: Boolean, | 7621 }, |
6321 value: true, | 7622 |
6322 }, | 7623 // True if the backend is processing and a spinner should be shown in the |
6323 | 7624 // toolbar. |
6324 isDangerous_: { | 7625 spinnerActive: { |
6325 computed: 'computeIsDangerous_(data.state)', | 7626 type: Boolean, |
6326 type: Boolean, | 7627 value: false |
6327 value: false, | 7628 }, |
6328 }, | 7629 |
6329 | 7630 hasDrawer: { |
6330 isMalware_: { | 7631 type: Boolean, |
6331 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', | 7632 observer: 'hasDrawerChanged_', |
6332 type: Boolean, | 7633 reflectToAttribute: true, |
6333 value: false, | 7634 }, |
6334 }, | 7635 |
6335 | 7636 // Whether domain-grouped history is enabled. |
6336 isInProgress_: { | 7637 isGroupedMode: { |
6337 computed: 'computeIsInProgress_(data.state)', | 7638 type: Boolean, |
6338 type: Boolean, | 7639 reflectToAttribute: true, |
6339 value: false, | 7640 }, |
6340 }, | 7641 |
6341 | 7642 // The period to search over. Matches BrowsingHistoryHandler::Range. |
6342 pauseOrResumeText_: { | 7643 groupedRange: { |
6343 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', | 7644 type: Number, |
6344 type: String, | 7645 value: 0, |
6345 }, | 7646 reflectToAttribute: true, |
6346 | 7647 notify: true |
6347 showCancel_: { | 7648 }, |
6348 computed: 'computeShowCancel_(data.state)', | 7649 |
6349 type: Boolean, | 7650 // The start time of the query range. |
6350 value: false, | 7651 queryStartTime: String, |
6351 }, | 7652 |
6352 | 7653 // The end time of the query range. |
6353 showProgress_: { | 7654 queryEndTime: String, |
6354 computed: 'computeShowProgress_(showCancel_, data.percent)', | 7655 }, |
6355 type: Boolean, | 7656 |
6356 value: false, | 7657 /** |
6357 }, | 7658 * Changes the toolbar background color depending on whether any history items |
6358 }, | 7659 * are currently selected. |
6359 | 7660 * @private |
6360 observers: [ | 7661 */ |
6361 // TODO(dbeam): this gets called way more when I observe data.by_ext_id | 7662 changeToolbarView_: function() { |
6362 // and data.by_ext_name directly. Why? | 7663 this.itemsSelected_ = this.count > 0; |
6363 'observeControlledBy_(controlledBy_)', | 7664 }, |
6364 'observeIsDangerous_(isDangerous_, data)', | 7665 |
6365 ], | 7666 /** |
6366 | 7667 * When changing the search term externally, update the search field to |
6367 ready: function() { | 7668 * reflect the new search term. |
6368 this.content = this.$.content; | 7669 * @param {string} search |
6369 }, | 7670 */ |
6370 | 7671 setSearchTerm: function(search) { |
6371 /** @private */ | 7672 if (this.searchTerm == search) |
6372 computeClass_: function() { | 7673 return; |
6373 var classes = []; | 7674 |
6374 | 7675 this.searchTerm = search; |
6375 if (this.isActive_) | 7676 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) |
6376 classes.push('is-active'); | 7677 .getSearchField(); |
6377 | 7678 searchField.showAndFocus(); |
6378 if (this.isDangerous_) | 7679 searchField.setValue(search); |
6379 classes.push('dangerous'); | 7680 }, |
6380 | 7681 |
6381 if (this.showProgress_) | 7682 /** |
6382 classes.push('show-progress'); | 7683 * @param {!CustomEvent} event |
6383 | 7684 * @private |
6384 return classes.join(' '); | 7685 */ |
6385 }, | 7686 onSearchChanged_: function(event) { |
6386 | 7687 this.searchTerm = /** @type {string} */ (event.detail); |
6387 /** @private */ | 7688 }, |
6388 computeCompletelyOnDisk_: function() { | 7689 |
6389 return this.data.state == downloads.States.COMPLETE && | 7690 onClearSelectionTap_: function() { |
6390 !this.data.file_externally_removed; | 7691 this.fire('unselect-all'); |
6391 }, | 7692 }, |
6392 | 7693 |
6393 /** @private */ | 7694 onDeleteTap_: function() { |
6394 computeControlledBy_: function() { | 7695 this.fire('delete-selected'); |
6395 if (!this.data.by_ext_id || !this.data.by_ext_name) | 7696 }, |
6396 return ''; | 7697 |
6397 | 7698 get searchBar() { |
6398 var url = 'chrome://extensions#' + this.data.by_ext_id; | 7699 return this.$['main-toolbar'].getSearchField(); |
6399 var name = this.data.by_ext_name; | 7700 }, |
6400 return loadTimeData.getStringF('controlledByUrl', url, name); | 7701 |
6401 }, | 7702 showSearchField: function() { |
6402 | 7703 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) |
6403 /** @private */ | 7704 .getSearchField() |
6404 computeDangerIcon_: function() { | 7705 .showAndFocus(); |
6405 if (!this.isDangerous_) | 7706 }, |
6406 return ''; | 7707 |
6407 | 7708 /** |
6408 switch (this.data.danger_type) { | 7709 * If the user is a supervised user the delete button is not shown. |
6409 case downloads.DangerType.DANGEROUS_CONTENT: | 7710 * @private |
6410 case downloads.DangerType.DANGEROUS_HOST: | 7711 */ |
6411 case downloads.DangerType.DANGEROUS_URL: | 7712 deletingAllowed_: function() { |
6412 case downloads.DangerType.POTENTIALLY_UNWANTED: | 7713 return loadTimeData.getBoolean('allowDeletingHistory'); |
6413 case downloads.DangerType.UNCOMMON_CONTENT: | 7714 }, |
6414 return 'downloads:remove-circle'; | 7715 |
6415 default: | 7716 numberOfItemsSelected_: function(count) { |
6416 return 'cr:warning'; | 7717 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; |
6417 } | 7718 }, |
6418 }, | 7719 |
6419 | 7720 getHistoryInterval_: function(queryStartTime, queryEndTime) { |
6420 /** @private */ | 7721 // TODO(calamity): Fix the format of these dates. |
6421 computeDate_: function() { | 7722 return loadTimeData.getStringF( |
6422 assert(typeof this.data.hideDate == 'boolean'); | 7723 'historyInterval', queryStartTime, queryEndTime); |
6423 if (this.data.hideDate) | 7724 }, |
6424 return ''; | 7725 |
6425 return assert(this.data.since_string || this.data.date_string); | 7726 /** @private */ |
6426 }, | 7727 hasDrawerChanged_: function() { |
6427 | 7728 this.updateStyles(); |
6428 /** @private */ | 7729 }, |
6429 computeDescription_: function() { | |
6430 var data = this.data; | |
6431 | |
6432 switch (data.state) { | |
6433 case downloads.States.DANGEROUS: | |
6434 var fileName = data.file_name; | |
6435 switch (data.danger_type) { | |
6436 case downloads.DangerType.DANGEROUS_FILE: | |
6437 return loadTimeData.getStringF('dangerFileDesc', fileName); | |
6438 case downloads.DangerType.DANGEROUS_URL: | |
6439 return loadTimeData.getString('dangerUrlDesc'); | |
6440 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. | |
6441 case downloads.DangerType.DANGEROUS_HOST: | |
6442 return loadTimeData.getStringF('dangerContentDesc', fileName); | |
6443 case downloads.DangerType.UNCOMMON_CONTENT: | |
6444 return loadTimeData.getStringF('dangerUncommonDesc', fileName); | |
6445 case downloads.DangerType.POTENTIALLY_UNWANTED: | |
6446 return loadTimeData.getStringF('dangerSettingsDesc', fileName); | |
6447 } | |
6448 break; | |
6449 | |
6450 case downloads.States.IN_PROGRESS: | |
6451 case downloads.States.PAUSED: // Fallthrough. | |
6452 return data.progress_status_text; | |
6453 } | |
6454 | |
6455 return ''; | |
6456 }, | |
6457 | |
6458 /** @private */ | |
6459 computeIsActive_: function() { | |
6460 return this.data.state != downloads.States.CANCELLED && | |
6461 this.data.state != downloads.States.INTERRUPTED && | |
6462 !this.data.file_externally_removed; | |
6463 }, | |
6464 | |
6465 /** @private */ | |
6466 computeIsDangerous_: function() { | |
6467 return this.data.state == downloads.States.DANGEROUS; | |
6468 }, | |
6469 | |
6470 /** @private */ | |
6471 computeIsInProgress_: function() { | |
6472 return this.data.state == downloads.States.IN_PROGRESS; | |
6473 }, | |
6474 | |
6475 /** @private */ | |
6476 computeIsMalware_: function() { | |
6477 return this.isDangerous_ && | |
6478 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || | |
6479 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || | |
6480 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || | |
6481 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); | |
6482 }, | |
6483 | |
6484 /** @private */ | |
6485 computePauseOrResumeText_: function() { | |
6486 if (this.isInProgress_) | |
6487 return loadTimeData.getString('controlPause'); | |
6488 if (this.data.resume) | |
6489 return loadTimeData.getString('controlResume'); | |
6490 return ''; | |
6491 }, | |
6492 | |
6493 /** @private */ | |
6494 computeRemoveStyle_: function() { | |
6495 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); | |
6496 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; | |
6497 return hideRemove ? 'visibility: hidden' : ''; | |
6498 }, | |
6499 | |
6500 /** @private */ | |
6501 computeShowCancel_: function() { | |
6502 return this.data.state == downloads.States.IN_PROGRESS || | |
6503 this.data.state == downloads.States.PAUSED; | |
6504 }, | |
6505 | |
6506 /** @private */ | |
6507 computeShowProgress_: function() { | |
6508 return this.showCancel_ && this.data.percent >= -1; | |
6509 }, | |
6510 | |
6511 /** @private */ | |
6512 computeTag_: function() { | |
6513 switch (this.data.state) { | |
6514 case downloads.States.CANCELLED: | |
6515 return loadTimeData.getString('statusCancelled'); | |
6516 | |
6517 case downloads.States.INTERRUPTED: | |
6518 return this.data.last_reason_text; | |
6519 | |
6520 case downloads.States.COMPLETE: | |
6521 return this.data.file_externally_removed ? | |
6522 loadTimeData.getString('statusRemoved') : ''; | |
6523 } | |
6524 | |
6525 return ''; | |
6526 }, | |
6527 | |
6528 /** @private */ | |
6529 isIndeterminate_: function() { | |
6530 return this.data.percent == -1; | |
6531 }, | |
6532 | |
6533 /** @private */ | |
6534 observeControlledBy_: function() { | |
6535 this.$['controlled-by'].innerHTML = this.controlledBy_; | |
6536 }, | |
6537 | |
6538 /** @private */ | |
6539 observeIsDangerous_: function() { | |
6540 if (!this.data) | |
6541 return; | |
6542 | |
6543 if (this.isDangerous_) { | |
6544 this.$.url.removeAttribute('href'); | |
6545 } else { | |
6546 this.$.url.href = assert(this.data.url); | |
6547 var filePath = encodeURIComponent(this.data.file_path); | |
6548 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; | |
6549 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; | |
6550 } | |
6551 }, | |
6552 | |
6553 /** @private */ | |
6554 onCancelTap_: function() { | |
6555 downloads.ActionService.getInstance().cancel(this.data.id); | |
6556 }, | |
6557 | |
6558 /** @private */ | |
6559 onDiscardDangerousTap_: function() { | |
6560 downloads.ActionService.getInstance().discardDangerous(this.data.id); | |
6561 }, | |
6562 | |
6563 /** | |
6564 * @private | |
6565 * @param {Event} e | |
6566 */ | |
6567 onDragStart_: function(e) { | |
6568 e.preventDefault(); | |
6569 downloads.ActionService.getInstance().drag(this.data.id); | |
6570 }, | |
6571 | |
6572 /** | |
6573 * @param {Event} e | |
6574 * @private | |
6575 */ | |
6576 onFileLinkTap_: function(e) { | |
6577 e.preventDefault(); | |
6578 downloads.ActionService.getInstance().openFile(this.data.id); | |
6579 }, | |
6580 | |
6581 /** @private */ | |
6582 onPauseOrResumeTap_: function() { | |
6583 if (this.isInProgress_) | |
6584 downloads.ActionService.getInstance().pause(this.data.id); | |
6585 else | |
6586 downloads.ActionService.getInstance().resume(this.data.id); | |
6587 }, | |
6588 | |
6589 /** @private */ | |
6590 onRemoveTap_: function() { | |
6591 downloads.ActionService.getInstance().remove(this.data.id); | |
6592 }, | |
6593 | |
6594 /** @private */ | |
6595 onRetryTap_: function() { | |
6596 downloads.ActionService.getInstance().download(this.data.url); | |
6597 }, | |
6598 | |
6599 /** @private */ | |
6600 onSaveDangerousTap_: function() { | |
6601 downloads.ActionService.getInstance().saveDangerous(this.data.id); | |
6602 }, | |
6603 | |
6604 /** @private */ | |
6605 onShowTap_: function() { | |
6606 downloads.ActionService.getInstance().show(this.data.id); | |
6607 }, | |
6608 }); | |
6609 | |
6610 return {Item: Item}; | |
6611 }); | 7730 }); |
6612 /** @polymerBehavior Polymer.PaperItemBehavior */ | 7731 // Copyright 2016 The Chromium Authors. All rights reserved. |
6613 Polymer.PaperItemBehaviorImpl = { | 7732 // Use of this source code is governed by a BSD-style license that can be |
6614 hostAttributes: { | 7733 // found in the LICENSE file. |
6615 role: 'option', | 7734 |
6616 tabindex: '0' | 7735 /** |
6617 } | 7736 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the |
6618 }; | 7737 * dialog is closed via close(), a 'close' event is fired. If the dialog is |
6619 | 7738 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event. |
6620 /** @polymerBehavior */ | 7739 * Additionally clients can inspect the dialog's |returnValue| property inside |
6621 Polymer.PaperItemBehavior = [ | 7740 * the 'close' event listener to determine whether it was canceled or just |
6622 Polymer.IronButtonState, | 7741 * closed, where a truthy value means success, and a falsy value means it was |
6623 Polymer.IronControlState, | 7742 * canceled. |
6624 Polymer.PaperItemBehaviorImpl | 7743 */ |
6625 ]; | |
6626 Polymer({ | 7744 Polymer({ |
6627 is: 'paper-item', | 7745 is: 'cr-dialog', |
6628 | 7746 extends: 'dialog', |
6629 behaviors: [ | 7747 |
6630 Polymer.PaperItemBehavior | 7748 cancel: function() { |
6631 ] | 7749 this.fire('cancel'); |
6632 }); | 7750 HTMLDialogElement.prototype.close.call(this, ''); |
6633 /** | 7751 }, |
6634 * @param {!Function} selectCallback | 7752 |
6635 * @constructor | 7753 /** |
6636 */ | 7754 * @param {string=} opt_returnValue |
6637 Polymer.IronSelection = function(selectCallback) { | 7755 * @override |
6638 this.selection = []; | 7756 */ |
6639 this.selectCallback = selectCallback; | 7757 close: function(opt_returnValue) { |
6640 }; | 7758 HTMLDialogElement.prototype.close.call(this, 'success'); |
6641 | 7759 }, |
6642 Polymer.IronSelection.prototype = { | 7760 |
6643 | 7761 /** @return {!PaperIconButtonElement} */ |
6644 /** | 7762 getCloseButton: function() { |
6645 * Retrieves the selected item(s). | 7763 return this.$.close; |
6646 * | 7764 }, |
6647 * @method get | 7765 }); |
6648 * @returns Returns the selected item(s). If the multi property is true, | |
6649 * `get` will return an array, otherwise it will return | |
6650 * the selected item or undefined if there is no selection. | |
6651 */ | |
6652 get: function() { | |
6653 return this.multi ? this.selection.slice() : this.selection[0]; | |
6654 }, | |
6655 | |
6656 /** | |
6657 * Clears all the selection except the ones indicated. | |
6658 * | |
6659 * @method clear | |
6660 * @param {Array} excludes items to be excluded. | |
6661 */ | |
6662 clear: function(excludes) { | |
6663 this.selection.slice().forEach(function(item) { | |
6664 if (!excludes || excludes.indexOf(item) < 0) { | |
6665 this.setItemSelected(item, false); | |
6666 } | |
6667 }, this); | |
6668 }, | |
6669 | |
6670 /** | |
6671 * Indicates if a given item is selected. | |
6672 * | |
6673 * @method isSelected | |
6674 * @param {*} item The item whose selection state should be checked. | |
6675 * @returns Returns true if `item` is selected. | |
6676 */ | |
6677 isSelected: function(item) { | |
6678 return this.selection.indexOf(item) >= 0; | |
6679 }, | |
6680 | |
6681 /** | |
6682 * Sets the selection state for a given item to either selected or deselecte
d. | |
6683 * | |
6684 * @method setItemSelected | |
6685 * @param {*} item The item to select. | |
6686 * @param {boolean} isSelected True for selected, false for deselected. | |
6687 */ | |
6688 setItemSelected: function(item, isSelected) { | |
6689 if (item != null) { | |
6690 if (isSelected !== this.isSelected(item)) { | |
6691 // proceed to update selection only if requested state differs from cu
rrent | |
6692 if (isSelected) { | |
6693 this.selection.push(item); | |
6694 } else { | |
6695 var i = this.selection.indexOf(item); | |
6696 if (i >= 0) { | |
6697 this.selection.splice(i, 1); | |
6698 } | |
6699 } | |
6700 if (this.selectCallback) { | |
6701 this.selectCallback(item, isSelected); | |
6702 } | |
6703 } | |
6704 } | |
6705 }, | |
6706 | |
6707 /** | |
6708 * Sets the selection state for a given item. If the `multi` property | |
6709 * is true, then the selected state of `item` will be toggled; otherwise | |
6710 * the `item` will be selected. | |
6711 * | |
6712 * @method select | |
6713 * @param {*} item The item to select. | |
6714 */ | |
6715 select: function(item) { | |
6716 if (this.multi) { | |
6717 this.toggle(item); | |
6718 } else if (this.get() !== item) { | |
6719 this.setItemSelected(this.get(), false); | |
6720 this.setItemSelected(item, true); | |
6721 } | |
6722 }, | |
6723 | |
6724 /** | |
6725 * Toggles the selection state for `item`. | |
6726 * | |
6727 * @method toggle | |
6728 * @param {*} item The item to toggle. | |
6729 */ | |
6730 toggle: function(item) { | |
6731 this.setItemSelected(item, !this.isSelected(item)); | |
6732 } | |
6733 | |
6734 }; | |
6735 /** @polymerBehavior */ | |
6736 Polymer.IronSelectableBehavior = { | |
6737 | |
6738 /** | |
6739 * Fired when iron-selector is activated (selected or deselected). | |
6740 * It is fired before the selected items are changed. | |
6741 * Cancel the event to abort selection. | |
6742 * | |
6743 * @event iron-activate | |
6744 */ | |
6745 | |
6746 /** | |
6747 * Fired when an item is selected | |
6748 * | |
6749 * @event iron-select | |
6750 */ | |
6751 | |
6752 /** | |
6753 * Fired when an item is deselected | |
6754 * | |
6755 * @event iron-deselect | |
6756 */ | |
6757 | |
6758 /** | |
6759 * Fired when the list of selectable items changes (e.g., items are | |
6760 * added or removed). The detail of the event is a mutation record that | |
6761 * describes what changed. | |
6762 * | |
6763 * @event iron-items-changed | |
6764 */ | |
6765 | |
6766 properties: { | |
6767 | |
6768 /** | |
6769 * If you want to use an attribute value or property of an element for | |
6770 * `selected` instead of the index, set this to the name of the attribute | |
6771 * or property. Hyphenated values are converted to camel case when used to | |
6772 * look up the property of a selectable element. Camel cased values are | |
6773 * *not* converted to hyphenated values for attribute lookup. It's | |
6774 * recommended that you provide the hyphenated form of the name so that | |
6775 * selection works in both cases. (Use `attr-or-property-name` instead of | |
6776 * `attrOrPropertyName`.) | |
6777 */ | |
6778 attrForSelected: { | |
6779 type: String, | |
6780 value: null | |
6781 }, | |
6782 | |
6783 /** | |
6784 * Gets or sets the selected element. The default is to use the index of t
he item. | |
6785 * @type {string|number} | |
6786 */ | |
6787 selected: { | |
6788 type: String, | |
6789 notify: true | |
6790 }, | |
6791 | |
6792 /** | |
6793 * Returns the currently selected item. | |
6794 * | |
6795 * @type {?Object} | |
6796 */ | |
6797 selectedItem: { | |
6798 type: Object, | |
6799 readOnly: true, | |
6800 notify: true | |
6801 }, | |
6802 | |
6803 /** | |
6804 * The event that fires from items when they are selected. Selectable | |
6805 * will listen for this event from items and update the selection state. | |
6806 * Set to empty string to listen to no events. | |
6807 */ | |
6808 activateEvent: { | |
6809 type: String, | |
6810 value: 'tap', | |
6811 observer: '_activateEventChanged' | |
6812 }, | |
6813 | |
6814 /** | |
6815 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
6816 * are selectable. | |
6817 */ | |
6818 selectable: String, | |
6819 | |
6820 /** | |
6821 * The class to set on elements when selected. | |
6822 */ | |
6823 selectedClass: { | |
6824 type: String, | |
6825 value: 'iron-selected' | |
6826 }, | |
6827 | |
6828 /** | |
6829 * The attribute to set on elements when selected. | |
6830 */ | |
6831 selectedAttribute: { | |
6832 type: String, | |
6833 value: null | |
6834 }, | |
6835 | |
6836 /** | |
6837 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
6838 * is not found. | |
6839 */ | |
6840 fallbackSelection: { | |
6841 type: String, | |
6842 value: null | |
6843 }, | |
6844 | |
6845 /** | |
6846 * The list of items from which a selection can be made. | |
6847 */ | |
6848 items: { | |
6849 type: Array, | |
6850 readOnly: true, | |
6851 notify: true, | |
6852 value: function() { | |
6853 return []; | |
6854 } | |
6855 }, | |
6856 | |
6857 /** | |
6858 * The set of excluded elements where the key is the `localName` | |
6859 * of the element that will be ignored from the item list. | |
6860 * | |
6861 * @default {template: 1} | |
6862 */ | |
6863 _excludedLocalNames: { | |
6864 type: Object, | |
6865 value: function() { | |
6866 return { | |
6867 'template': 1 | |
6868 }; | |
6869 } | |
6870 } | |
6871 }, | |
6872 | |
6873 observers: [ | |
6874 '_updateAttrForSelected(attrForSelected)', | |
6875 '_updateSelected(selected)', | |
6876 '_checkFallback(fallbackSelection)' | |
6877 ], | |
6878 | |
6879 created: function() { | |
6880 this._bindFilterItem = this._filterItem.bind(this); | |
6881 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | |
6882 }, | |
6883 | |
6884 attached: function() { | |
6885 this._observer = this._observeItems(this); | |
6886 this._updateItems(); | |
6887 if (!this._shouldUpdateSelection) { | |
6888 this._updateSelected(); | |
6889 } | |
6890 this._addListener(this.activateEvent); | |
6891 }, | |
6892 | |
6893 detached: function() { | |
6894 if (this._observer) { | |
6895 Polymer.dom(this).unobserveNodes(this._observer); | |
6896 } | |
6897 this._removeListener(this.activateEvent); | |
6898 }, | |
6899 | |
6900 /** | |
6901 * Returns the index of the given item. | |
6902 * | |
6903 * @method indexOf | |
6904 * @param {Object} item | |
6905 * @returns Returns the index of the item | |
6906 */ | |
6907 indexOf: function(item) { | |
6908 return this.items.indexOf(item); | |
6909 }, | |
6910 | |
6911 /** | |
6912 * Selects the given value. | |
6913 * | |
6914 * @method select | |
6915 * @param {string|number} value the value to select. | |
6916 */ | |
6917 select: function(value) { | |
6918 this.selected = value; | |
6919 }, | |
6920 | |
6921 /** | |
6922 * Selects the previous item. | |
6923 * | |
6924 * @method selectPrevious | |
6925 */ | |
6926 selectPrevious: function() { | |
6927 var length = this.items.length; | |
6928 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | |
6929 this.selected = this._indexToValue(index); | |
6930 }, | |
6931 | |
6932 /** | |
6933 * Selects the next item. | |
6934 * | |
6935 * @method selectNext | |
6936 */ | |
6937 selectNext: function() { | |
6938 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | |
6939 this.selected = this._indexToValue(index); | |
6940 }, | |
6941 | |
6942 /** | |
6943 * Selects the item at the given index. | |
6944 * | |
6945 * @method selectIndex | |
6946 */ | |
6947 selectIndex: function(index) { | |
6948 this.select(this._indexToValue(index)); | |
6949 }, | |
6950 | |
6951 /** | |
6952 * Force a synchronous update of the `items` property. | |
6953 * | |
6954 * NOTE: Consider listening for the `iron-items-changed` event to respond to | |
6955 * updates to the set of selectable items after updates to the DOM list and | |
6956 * selection state have been made. | |
6957 * | |
6958 * WARNING: If you are using this method, you should probably consider an | |
6959 * alternate approach. Synchronously querying for items is potentially | |
6960 * slow for many use cases. The `items` property will update asynchronously | |
6961 * on its own to reflect selectable items in the DOM. | |
6962 */ | |
6963 forceSynchronousItemUpdate: function() { | |
6964 this._updateItems(); | |
6965 }, | |
6966 | |
6967 get _shouldUpdateSelection() { | |
6968 return this.selected != null; | |
6969 }, | |
6970 | |
6971 _checkFallback: function() { | |
6972 if (this._shouldUpdateSelection) { | |
6973 this._updateSelected(); | |
6974 } | |
6975 }, | |
6976 | |
6977 _addListener: function(eventName) { | |
6978 this.listen(this, eventName, '_activateHandler'); | |
6979 }, | |
6980 | |
6981 _removeListener: function(eventName) { | |
6982 this.unlisten(this, eventName, '_activateHandler'); | |
6983 }, | |
6984 | |
6985 _activateEventChanged: function(eventName, old) { | |
6986 this._removeListener(old); | |
6987 this._addListener(eventName); | |
6988 }, | |
6989 | |
6990 _updateItems: function() { | |
6991 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | |
6992 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | |
6993 this._setItems(nodes); | |
6994 }, | |
6995 | |
6996 _updateAttrForSelected: function() { | |
6997 if (this._shouldUpdateSelection) { | |
6998 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | |
6999 } | |
7000 }, | |
7001 | |
7002 _updateSelected: function() { | |
7003 this._selectSelected(this.selected); | |
7004 }, | |
7005 | |
7006 _selectSelected: function(selected) { | |
7007 this._selection.select(this._valueToItem(this.selected)); | |
7008 // Check for items, since this array is populated only when attached | |
7009 // Since Number(0) is falsy, explicitly check for undefined | |
7010 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | |
7011 this.selected = this.fallbackSelection; | |
7012 } | |
7013 }, | |
7014 | |
7015 _filterItem: function(node) { | |
7016 return !this._excludedLocalNames[node.localName]; | |
7017 }, | |
7018 | |
7019 _valueToItem: function(value) { | |
7020 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
7021 }, | |
7022 | |
7023 _valueToIndex: function(value) { | |
7024 if (this.attrForSelected) { | |
7025 for (var i = 0, item; item = this.items[i]; i++) { | |
7026 if (this._valueForItem(item) == value) { | |
7027 return i; | |
7028 } | |
7029 } | |
7030 } else { | |
7031 return Number(value); | |
7032 } | |
7033 }, | |
7034 | |
7035 _indexToValue: function(index) { | |
7036 if (this.attrForSelected) { | |
7037 var item = this.items[index]; | |
7038 if (item) { | |
7039 return this._valueForItem(item); | |
7040 } | |
7041 } else { | |
7042 return index; | |
7043 } | |
7044 }, | |
7045 | |
7046 _valueForItem: function(item) { | |
7047 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | |
7048 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | |
7049 }, | |
7050 | |
7051 _applySelection: function(item, isSelected) { | |
7052 if (this.selectedClass) { | |
7053 this.toggleClass(this.selectedClass, isSelected, item); | |
7054 } | |
7055 if (this.selectedAttribute) { | |
7056 this.toggleAttribute(this.selectedAttribute, isSelected, item); | |
7057 } | |
7058 this._selectionChange(); | |
7059 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | |
7060 }, | |
7061 | |
7062 _selectionChange: function() { | |
7063 this._setSelectedItem(this._selection.get()); | |
7064 }, | |
7065 | |
7066 // observe items change under the given node. | |
7067 _observeItems: function(node) { | |
7068 return Polymer.dom(node).observeNodes(function(mutation) { | |
7069 this._updateItems(); | |
7070 | |
7071 if (this._shouldUpdateSelection) { | |
7072 this._updateSelected(); | |
7073 } | |
7074 | |
7075 // Let other interested parties know about the change so that | |
7076 // we don't have to recreate mutation observers everywhere. | |
7077 this.fire('iron-items-changed', mutation, { | |
7078 bubbles: false, | |
7079 cancelable: false | |
7080 }); | |
7081 }); | |
7082 }, | |
7083 | |
7084 _activateHandler: function(e) { | |
7085 var t = e.target; | |
7086 var items = this.items; | |
7087 while (t && t != this) { | |
7088 var i = items.indexOf(t); | |
7089 if (i >= 0) { | |
7090 var value = this._indexToValue(i); | |
7091 this._itemActivate(value, t); | |
7092 return; | |
7093 } | |
7094 t = t.parentNode; | |
7095 } | |
7096 }, | |
7097 | |
7098 _itemActivate: function(value, item) { | |
7099 if (!this.fire('iron-activate', | |
7100 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | |
7101 this.select(value); | |
7102 } | |
7103 } | |
7104 | |
7105 }; | |
7106 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
7107 Polymer.IronMultiSelectableBehaviorImpl = { | |
7108 properties: { | |
7109 | |
7110 /** | |
7111 * If true, multiple selections are allowed. | |
7112 */ | |
7113 multi: { | |
7114 type: Boolean, | |
7115 value: false, | |
7116 observer: 'multiChanged' | |
7117 }, | |
7118 | |
7119 /** | |
7120 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
7121 * is true. | |
7122 */ | |
7123 selectedValues: { | |
7124 type: Array, | |
7125 notify: true | |
7126 }, | |
7127 | |
7128 /** | |
7129 * Returns an array of currently selected items. | |
7130 */ | |
7131 selectedItems: { | |
7132 type: Array, | |
7133 readOnly: true, | |
7134 notify: true | |
7135 }, | |
7136 | |
7137 }, | |
7138 | |
7139 observers: [ | |
7140 '_updateSelected(selectedValues.splices)' | |
7141 ], | |
7142 | |
7143 /** | |
7144 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7145 * `value` will be toggled; otherwise the `value` will be selected. | |
7146 * | |
7147 * @method select | |
7148 * @param {string|number} value the value to select. | |
7149 */ | |
7150 select: function(value) { | |
7151 if (this.multi) { | |
7152 if (this.selectedValues) { | |
7153 this._toggleSelected(value); | |
7154 } else { | |
7155 this.selectedValues = [value]; | |
7156 } | |
7157 } else { | |
7158 this.selected = value; | |
7159 } | |
7160 }, | |
7161 | |
7162 multiChanged: function(multi) { | |
7163 this._selection.multi = multi; | |
7164 }, | |
7165 | |
7166 get _shouldUpdateSelection() { | |
7167 return this.selected != null || | |
7168 (this.selectedValues != null && this.selectedValues.length); | |
7169 }, | |
7170 | |
7171 _updateAttrForSelected: function() { | |
7172 if (!this.multi) { | |
7173 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
7174 } else if (this._shouldUpdateSelection) { | |
7175 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
7176 return this._indexToValue(this.indexOf(selectedItem)); | |
7177 }, this).filter(function(unfilteredValue) { | |
7178 return unfilteredValue != null; | |
7179 }, this); | |
7180 } | |
7181 }, | |
7182 | |
7183 _updateSelected: function() { | |
7184 if (this.multi) { | |
7185 this._selectMulti(this.selectedValues); | |
7186 } else { | |
7187 this._selectSelected(this.selected); | |
7188 } | |
7189 }, | |
7190 | |
7191 _selectMulti: function(values) { | |
7192 if (values) { | |
7193 var selectedItems = this._valuesToItems(values); | |
7194 // clear all but the current selected items | |
7195 this._selection.clear(selectedItems); | |
7196 // select only those not selected yet | |
7197 for (var i = 0; i < selectedItems.length; i++) { | |
7198 this._selection.setItemSelected(selectedItems[i], true); | |
7199 } | |
7200 // Check for items, since this array is populated only when attached | |
7201 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
7202 var fallback = this._valueToItem(this.fallbackSelection); | |
7203 if (fallback) { | |
7204 this.selectedValues = [this.fallbackSelection]; | |
7205 } | |
7206 } | |
7207 } else { | |
7208 this._selection.clear(); | |
7209 } | |
7210 }, | |
7211 | |
7212 _selectionChange: function() { | |
7213 var s = this._selection.get(); | |
7214 if (this.multi) { | |
7215 this._setSelectedItems(s); | |
7216 } else { | |
7217 this._setSelectedItems([s]); | |
7218 this._setSelectedItem(s); | |
7219 } | |
7220 }, | |
7221 | |
7222 _toggleSelected: function(value) { | |
7223 var i = this.selectedValues.indexOf(value); | |
7224 var unselected = i < 0; | |
7225 if (unselected) { | |
7226 this.push('selectedValues',value); | |
7227 } else { | |
7228 this.splice('selectedValues',i,1); | |
7229 } | |
7230 }, | |
7231 | |
7232 _valuesToItems: function(values) { | |
7233 return (values == null) ? null : values.map(function(value) { | |
7234 return this._valueToItem(value); | |
7235 }, this); | |
7236 } | |
7237 }; | |
7238 | |
7239 /** @polymerBehavior */ | |
7240 Polymer.IronMultiSelectableBehavior = [ | |
7241 Polymer.IronSelectableBehavior, | |
7242 Polymer.IronMultiSelectableBehaviorImpl | |
7243 ]; | |
7244 /** | |
7245 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
7246 * | |
7247 * @demo demo/index.html | |
7248 * @polymerBehavior Polymer.IronMenuBehavior | |
7249 */ | |
7250 Polymer.IronMenuBehaviorImpl = { | |
7251 | |
7252 properties: { | |
7253 | |
7254 /** | |
7255 * Returns the currently focused item. | |
7256 * @type {?Object} | |
7257 */ | |
7258 focusedItem: { | |
7259 observer: '_focusedItemChanged', | |
7260 readOnly: true, | |
7261 type: Object | |
7262 }, | |
7263 | |
7264 /** | |
7265 * The attribute to use on menu items to look up the item title. Typing th
e first | |
7266 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
7267 * will be used. | |
7268 */ | |
7269 attrForItemTitle: { | |
7270 type: String | |
7271 } | |
7272 }, | |
7273 | |
7274 hostAttributes: { | |
7275 'role': 'menu', | |
7276 'tabindex': '0' | |
7277 }, | |
7278 | |
7279 observers: [ | |
7280 '_updateMultiselectable(multi)' | |
7281 ], | |
7282 | |
7283 listeners: { | |
7284 'focus': '_onFocus', | |
7285 'keydown': '_onKeydown', | |
7286 'iron-items-changed': '_onIronItemsChanged' | |
7287 }, | |
7288 | |
7289 keyBindings: { | |
7290 'up': '_onUpKey', | |
7291 'down': '_onDownKey', | |
7292 'esc': '_onEscKey', | |
7293 'shift+tab:keydown': '_onShiftTabDown' | |
7294 }, | |
7295 | |
7296 attached: function() { | |
7297 this._resetTabindices(); | |
7298 }, | |
7299 | |
7300 /** | |
7301 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
7302 * `value` will be toggled; otherwise the `value` will be selected. | |
7303 * | |
7304 * @param {string|number} value the value to select. | |
7305 */ | |
7306 select: function(value) { | |
7307 // Cancel automatically focusing a default item if the menu received focus | |
7308 // through a user action selecting a particular item. | |
7309 if (this._defaultFocusAsync) { | |
7310 this.cancelAsync(this._defaultFocusAsync); | |
7311 this._defaultFocusAsync = null; | |
7312 } | |
7313 var item = this._valueToItem(value); | |
7314 if (item && item.hasAttribute('disabled')) return; | |
7315 this._setFocusedItem(item); | |
7316 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | |
7317 }, | |
7318 | |
7319 /** | |
7320 * Resets all tabindex attributes to the appropriate value based on the | |
7321 * current selection state. The appropriate value is `0` (focusable) for | |
7322 * the default selected item, and `-1` (not keyboard focusable) for all | |
7323 * other items. | |
7324 */ | |
7325 _resetTabindices: function() { | |
7326 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | |
7327 | |
7328 this.items.forEach(function(item) { | |
7329 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | |
7330 }, this); | |
7331 }, | |
7332 | |
7333 /** | |
7334 * Sets appropriate ARIA based on whether or not the menu is meant to be | |
7335 * multi-selectable. | |
7336 * | |
7337 * @param {boolean} multi True if the menu should be multi-selectable. | |
7338 */ | |
7339 _updateMultiselectable: function(multi) { | |
7340 if (multi) { | |
7341 this.setAttribute('aria-multiselectable', 'true'); | |
7342 } else { | |
7343 this.removeAttribute('aria-multiselectable'); | |
7344 } | |
7345 }, | |
7346 | |
7347 /** | |
7348 * Given a KeyboardEvent, this method will focus the appropriate item in the | |
7349 * menu (if there is a relevant item, and it is possible to focus it). | |
7350 * | |
7351 * @param {KeyboardEvent} event A KeyboardEvent. | |
7352 */ | |
7353 _focusWithKeyboardEvent: function(event) { | |
7354 for (var i = 0, item; item = this.items[i]; i++) { | |
7355 var attr = this.attrForItemTitle || 'textContent'; | |
7356 var title = item[attr] || item.getAttribute(attr); | |
7357 | |
7358 if (!item.hasAttribute('disabled') && title && | |
7359 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | |
7360 this._setFocusedItem(item); | |
7361 break; | |
7362 } | |
7363 } | |
7364 }, | |
7365 | |
7366 /** | |
7367 * Focuses the previous item (relative to the currently focused item) in the | |
7368 * menu, disabled items will be skipped. | |
7369 * Loop until length + 1 to handle case of single item in menu. | |
7370 */ | |
7371 _focusPrevious: function() { | |
7372 var length = this.items.length; | |
7373 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7374 for (var i = 1; i < length + 1; i++) { | |
7375 var item = this.items[(curFocusIndex - i + length) % length]; | |
7376 if (!item.hasAttribute('disabled')) { | |
7377 this._setFocusedItem(item); | |
7378 return; | |
7379 } | |
7380 } | |
7381 }, | |
7382 | |
7383 /** | |
7384 * Focuses the next item (relative to the currently focused item) in the | |
7385 * menu, disabled items will be skipped. | |
7386 * Loop until length + 1 to handle case of single item in menu. | |
7387 */ | |
7388 _focusNext: function() { | |
7389 var length = this.items.length; | |
7390 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | |
7391 for (var i = 1; i < length + 1; i++) { | |
7392 var item = this.items[(curFocusIndex + i) % length]; | |
7393 if (!item.hasAttribute('disabled')) { | |
7394 this._setFocusedItem(item); | |
7395 return; | |
7396 } | |
7397 } | |
7398 }, | |
7399 | |
7400 /** | |
7401 * Mutates items in the menu based on provided selection details, so that | |
7402 * all items correctly reflect selection state. | |
7403 * | |
7404 * @param {Element} item An item in the menu. | |
7405 * @param {boolean} isSelected True if the item should be shown in a | |
7406 * selected state, otherwise false. | |
7407 */ | |
7408 _applySelection: function(item, isSelected) { | |
7409 if (isSelected) { | |
7410 item.setAttribute('aria-selected', 'true'); | |
7411 } else { | |
7412 item.removeAttribute('aria-selected'); | |
7413 } | |
7414 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | |
7415 }, | |
7416 | |
7417 /** | |
7418 * Discretely updates tabindex values among menu items as the focused item | |
7419 * changes. | |
7420 * | |
7421 * @param {Element} focusedItem The element that is currently focused. | |
7422 * @param {?Element} old The last element that was considered focused, if | |
7423 * applicable. | |
7424 */ | |
7425 _focusedItemChanged: function(focusedItem, old) { | |
7426 old && old.setAttribute('tabindex', '-1'); | |
7427 if (focusedItem) { | |
7428 focusedItem.setAttribute('tabindex', '0'); | |
7429 focusedItem.focus(); | |
7430 } | |
7431 }, | |
7432 | |
7433 /** | |
7434 * A handler that responds to mutation changes related to the list of items | |
7435 * in the menu. | |
7436 * | |
7437 * @param {CustomEvent} event An event containing mutation records as its | |
7438 * detail. | |
7439 */ | |
7440 _onIronItemsChanged: function(event) { | |
7441 if (event.detail.addedNodes.length) { | |
7442 this._resetTabindices(); | |
7443 } | |
7444 }, | |
7445 | |
7446 /** | |
7447 * Handler that is called when a shift+tab keypress is detected by the menu. | |
7448 * | |
7449 * @param {CustomEvent} event A key combination event. | |
7450 */ | |
7451 _onShiftTabDown: function(event) { | |
7452 var oldTabIndex = this.getAttribute('tabindex'); | |
7453 | |
7454 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
7455 | |
7456 this._setFocusedItem(null); | |
7457 | |
7458 this.setAttribute('tabindex', '-1'); | |
7459 | |
7460 this.async(function() { | |
7461 this.setAttribute('tabindex', oldTabIndex); | |
7462 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7463 // NOTE(cdata): polymer/polymer#1305 | |
7464 }, 1); | |
7465 }, | |
7466 | |
7467 /** | |
7468 * Handler that is called when the menu receives focus. | |
7469 * | |
7470 * @param {FocusEvent} event A focus event. | |
7471 */ | |
7472 _onFocus: function(event) { | |
7473 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
7474 // do not focus the menu itself | |
7475 return; | |
7476 } | |
7477 | |
7478 // Do not focus the selected tab if the deepest target is part of the | |
7479 // menu element's local DOM and is focusable. | |
7480 var rootTarget = /** @type {?HTMLElement} */( | |
7481 Polymer.dom(event).rootTarget); | |
7482 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
7483 return; | |
7484 } | |
7485 | |
7486 // clear the cached focus item | |
7487 this._defaultFocusAsync = this.async(function() { | |
7488 // focus the selected item when the menu receives focus, or the first it
em | |
7489 // if no item is selected | |
7490 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
7491 | |
7492 this._setFocusedItem(null); | |
7493 | |
7494 if (selectedItem) { | |
7495 this._setFocusedItem(selectedItem); | |
7496 } else if (this.items[0]) { | |
7497 // We find the first none-disabled item (if one exists) | |
7498 this._focusNext(); | |
7499 } | |
7500 }); | |
7501 }, | |
7502 | |
7503 /** | |
7504 * Handler that is called when the up key is pressed. | |
7505 * | |
7506 * @param {CustomEvent} event A key combination event. | |
7507 */ | |
7508 _onUpKey: function(event) { | |
7509 // up and down arrows moves the focus | |
7510 this._focusPrevious(); | |
7511 event.detail.keyboardEvent.preventDefault(); | |
7512 }, | |
7513 | |
7514 /** | |
7515 * Handler that is called when the down key is pressed. | |
7516 * | |
7517 * @param {CustomEvent} event A key combination event. | |
7518 */ | |
7519 _onDownKey: function(event) { | |
7520 this._focusNext(); | |
7521 event.detail.keyboardEvent.preventDefault(); | |
7522 }, | |
7523 | |
7524 /** | |
7525 * Handler that is called when the esc key is pressed. | |
7526 * | |
7527 * @param {CustomEvent} event A key combination event. | |
7528 */ | |
7529 _onEscKey: function(event) { | |
7530 // esc blurs the control | |
7531 this.focusedItem.blur(); | |
7532 }, | |
7533 | |
7534 /** | |
7535 * Handler that is called when a keydown event is detected. | |
7536 * | |
7537 * @param {KeyboardEvent} event A keyboard event. | |
7538 */ | |
7539 _onKeydown: function(event) { | |
7540 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
7541 // all other keys focus the menu item starting with that character | |
7542 this._focusWithKeyboardEvent(event); | |
7543 } | |
7544 event.stopPropagation(); | |
7545 }, | |
7546 | |
7547 // override _activateHandler | |
7548 _activateHandler: function(event) { | |
7549 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
7550 event.stopPropagation(); | |
7551 } | |
7552 }; | |
7553 | |
7554 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
7555 | |
7556 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
7557 Polymer.IronMenuBehavior = [ | |
7558 Polymer.IronMultiSelectableBehavior, | |
7559 Polymer.IronA11yKeysBehavior, | |
7560 Polymer.IronMenuBehaviorImpl | |
7561 ]; | |
7562 (function() { | |
7563 Polymer({ | |
7564 is: 'paper-menu', | |
7565 | |
7566 behaviors: [ | |
7567 Polymer.IronMenuBehavior | |
7568 ] | |
7569 }); | |
7570 })(); | |
7571 /** | 7766 /** |
7572 `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 |
7573 optionally centers it in the window or another element. | 7768 optionally centers it in the window or another element. |
7574 | 7769 |
7575 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 |
7576 by CSS. | 7771 by CSS. |
7577 | 7772 |
7578 CSS properties | Action | 7773 CSS properties | Action |
7579 -----------------------------|------------------------------------------- | 7774 -----------------------------|------------------------------------------- |
7580 `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... |
10315 height: height + 'px', | 10510 height: height + 'px', |
10316 transform: 'translateY(0)' | 10511 transform: 'translateY(0)' |
10317 }, { | 10512 }, { |
10318 height: height / 2 + 'px', | 10513 height: height / 2 + 'px', |
10319 transform: 'translateY(-20px)' | 10514 transform: 'translateY(-20px)' |
10320 }], this.timingFromConfig(config)); | 10515 }], this.timingFromConfig(config)); |
10321 | 10516 |
10322 return this._effect; | 10517 return this._effect; |
10323 } | 10518 } |
10324 }); | 10519 }); |
10325 (function() { | 10520 // Copyright 2016 The Chromium Authors. All rights reserved. |
10326 'use strict'; | 10521 // Use of this source code is governed by a BSD-style license that can be |
10327 | 10522 // found in the LICENSE file. |
10328 var config = { | 10523 |
10329 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', | 10524 /** Same as paper-menu-button's custom easing cubic-bezier param. */ |
10330 MAX_ANIMATION_TIME_MS: 400 | 10525 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
10331 }; | 10526 |
10332 | 10527 Polymer({ |
10333 var PaperMenuButton = Polymer({ | 10528 is: 'cr-shared-menu', |
10334 is: 'paper-menu-button', | 10529 |
10335 | 10530 behaviors: [Polymer.IronA11yKeysBehavior], |
10336 /** | 10531 |
10337 * Fired when the dropdown opens. | 10532 properties: { |
10338 * | 10533 menuOpen: { |
10339 * @event paper-dropdown-open | 10534 type: Boolean, |
10340 */ | 10535 observer: 'menuOpenChanged_', |
10341 | 10536 value: false, |
10342 /** | 10537 notify: true, |
10343 * Fired when the dropdown closes. | 10538 }, |
10344 * | 10539 |
10345 * @event paper-dropdown-close | 10540 /** |
10346 */ | 10541 * The contextual item that this menu was clicked for. |
10347 | 10542 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> |
10348 behaviors: [ | 10543 * @type {?Object} |
10349 Polymer.IronA11yKeysBehavior, | 10544 */ |
10350 Polymer.IronControlState | 10545 itemData: { |
10351 ], | 10546 type: Object, |
10352 | 10547 value: null, |
10353 properties: { | 10548 }, |
10354 /** | 10549 |
10355 * True if the content is currently displayed. | 10550 /** @override */ |
10356 */ | 10551 keyEventTarget: { |
10357 opened: { | 10552 type: Object, |
10358 type: Boolean, | 10553 value: function() { |
10359 value: false, | 10554 return this.$.menu; |
10360 notify: true, | 10555 } |
10361 observer: '_openedChanged' | 10556 }, |
10362 }, | 10557 |
10363 | 10558 openAnimationConfig: { |
10364 /** | 10559 type: Object, |
10365 * The orientation against which to align the menu dropdown | 10560 value: function() { |
10366 * horizontally relative to the dropdown trigger. | 10561 return [{ |
10367 */ | 10562 name: 'fade-in-animation', |
10368 horizontalAlign: { | 10563 timing: { |
10369 type: String, | 10564 delay: 50, |
10370 value: 'left', | 10565 duration: 200 |
10371 reflectToAttribute: true | |
10372 }, | |
10373 | |
10374 /** | |
10375 * The orientation against which to align the menu dropdown | |
10376 * vertically relative to the dropdown trigger. | |
10377 */ | |
10378 verticalAlign: { | |
10379 type: String, | |
10380 value: 'top', | |
10381 reflectToAttribute: true | |
10382 }, | |
10383 | |
10384 /** | |
10385 * If true, the `horizontalAlign` and `verticalAlign` properties will | |
10386 * be considered preferences instead of strict requirements when | |
10387 * positioning the dropdown and may be changed if doing so reduces | |
10388 * the area of the dropdown falling outside of `fitInto`. | |
10389 */ | |
10390 dynamicAlign: { | |
10391 type: Boolean | |
10392 }, | |
10393 | |
10394 /** | |
10395 * A pixel value that will be added to the position calculated for the | |
10396 * given `horizontalAlign`. Use a negative value to offset to the | |
10397 * left, or a positive value to offset to the right. | |
10398 */ | |
10399 horizontalOffset: { | |
10400 type: Number, | |
10401 value: 0, | |
10402 notify: true | |
10403 }, | |
10404 | |
10405 /** | |
10406 * A pixel value that will be added to the position calculated for the | |
10407 * given `verticalAlign`. Use a negative value to offset towards the | |
10408 * top, or a positive value to offset towards the bottom. | |
10409 */ | |
10410 verticalOffset: { | |
10411 type: Number, | |
10412 value: 0, | |
10413 notify: true | |
10414 }, | |
10415 | |
10416 /** | |
10417 * If true, the dropdown will be positioned so that it doesn't overlap | |
10418 * the button. | |
10419 */ | |
10420 noOverlap: { | |
10421 type: Boolean | |
10422 }, | |
10423 | |
10424 /** | |
10425 * Set to true to disable animations when opening and closing the | |
10426 * dropdown. | |
10427 */ | |
10428 noAnimations: { | |
10429 type: Boolean, | |
10430 value: false | |
10431 }, | |
10432 | |
10433 /** | |
10434 * Set to true to disable automatically closing the dropdown after | |
10435 * a selection has been made. | |
10436 */ | |
10437 ignoreSelect: { | |
10438 type: Boolean, | |
10439 value: false | |
10440 }, | |
10441 | |
10442 /** | |
10443 * Set to true to enable automatically closing the dropdown after an | |
10444 * item has been activated, even if the selection did not change. | |
10445 */ | |
10446 closeOnActivate: { | |
10447 type: Boolean, | |
10448 value: false | |
10449 }, | |
10450 | |
10451 /** | |
10452 * An animation config. If provided, this will be used to animate the | |
10453 * opening of the dropdown. | |
10454 */ | |
10455 openAnimationConfig: { | |
10456 type: Object, | |
10457 value: function() { | |
10458 return [{ | |
10459 name: 'fade-in-animation', | |
10460 timing: { | |
10461 delay: 100, | |
10462 duration: 200 | |
10463 } | |
10464 }, { | |
10465 name: 'paper-menu-grow-width-animation', | |
10466 timing: { | |
10467 delay: 100, | |
10468 duration: 150, | |
10469 easing: config.ANIMATION_CUBIC_BEZIER | |
10470 } | |
10471 }, { | |
10472 name: 'paper-menu-grow-height-animation', | |
10473 timing: { | |
10474 delay: 100, | |
10475 duration: 275, | |
10476 easing: config.ANIMATION_CUBIC_BEZIER | |
10477 } | |
10478 }]; | |
10479 } | |
10480 }, | |
10481 | |
10482 /** | |
10483 * An animation config. If provided, this will be used to animate the | |
10484 * closing of the dropdown. | |
10485 */ | |
10486 closeAnimationConfig: { | |
10487 type: Object, | |
10488 value: function() { | |
10489 return [{ | |
10490 name: 'fade-out-animation', | |
10491 timing: { | |
10492 duration: 150 | |
10493 } | |
10494 }, { | |
10495 name: 'paper-menu-shrink-width-animation', | |
10496 timing: { | |
10497 delay: 100, | |
10498 duration: 50, | |
10499 easing: config.ANIMATION_CUBIC_BEZIER | |
10500 } | |
10501 }, { | |
10502 name: 'paper-menu-shrink-height-animation', | |
10503 timing: { | |
10504 duration: 200, | |
10505 easing: 'ease-in' | |
10506 } | |
10507 }]; | |
10508 } | |
10509 }, | |
10510 | |
10511 /** | |
10512 * By default, the dropdown will constrain scrolling on the page | |
10513 * to itself when opened. | |
10514 * Set to true in order to prevent scroll from being constrained | |
10515 * to the dropdown when it opens. | |
10516 */ | |
10517 allowOutsideScroll: { | |
10518 type: Boolean, | |
10519 value: false | |
10520 }, | |
10521 | |
10522 /** | |
10523 * Whether focus should be restored to the button when the menu closes
. | |
10524 */ | |
10525 restoreFocusOnClose: { | |
10526 type: Boolean, | |
10527 value: true | |
10528 }, | |
10529 | |
10530 /** | |
10531 * This is the element intended to be bound as the focus target | |
10532 * for the `iron-dropdown` contained by `paper-menu-button`. | |
10533 */ | |
10534 _dropdownContent: { | |
10535 type: Object | |
10536 } | 10566 } |
10537 }, | 10567 }, { |
10538 | 10568 name: 'paper-menu-grow-width-animation', |
10539 hostAttributes: { | 10569 timing: { |
10540 role: 'group', | 10570 delay: 50, |
10541 'aria-haspopup': 'true' | 10571 duration: 150, |
10542 }, | 10572 easing: SLIDE_CUBIC_BEZIER |
10543 | |
10544 listeners: { | |
10545 'iron-activate': '_onIronActivate', | |
10546 'iron-select': '_onIronSelect' | |
10547 }, | |
10548 | |
10549 /** | |
10550 * The content element that is contained by the menu button, if any. | |
10551 */ | |
10552 get contentElement() { | |
10553 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10554 }, | |
10555 | |
10556 /** | |
10557 * Toggles the drowpdown content between opened and closed. | |
10558 */ | |
10559 toggle: function() { | |
10560 if (this.opened) { | |
10561 this.close(); | |
10562 } else { | |
10563 this.open(); | |
10564 } | 10573 } |
10565 }, | 10574 }, { |
10566 | 10575 name: 'paper-menu-grow-height-animation', |
10567 /** | 10576 timing: { |
10568 * Make the dropdown content appear as an overlay positioned relative | 10577 delay: 100, |
10569 * to the dropdown trigger. | 10578 duration: 275, |
10570 */ | 10579 easing: SLIDE_CUBIC_BEZIER |
10571 open: function() { | |
10572 if (this.disabled) { | |
10573 return; | |
10574 } | 10580 } |
10575 | 10581 }]; |
10576 this.$.dropdown.open(); | 10582 } |
10577 }, | 10583 }, |
10578 | 10584 |
10579 /** | 10585 closeAnimationConfig: { |
10580 * Hide the dropdown content. | 10586 type: Object, |
10581 */ | 10587 value: function() { |
10582 close: function() { | 10588 return [{ |
10583 this.$.dropdown.close(); | 10589 name: 'fade-out-animation', |
10584 }, | 10590 timing: { |
10585 | 10591 duration: 150 |
10586 /** | |
10587 * When an `iron-select` event is received, the dropdown should | |
10588 * automatically close on the assumption that a value has been chosen. | |
10589 * | |
10590 * @param {CustomEvent} event A CustomEvent instance with type | |
10591 * set to `"iron-select"`. | |
10592 */ | |
10593 _onIronSelect: function(event) { | |
10594 if (!this.ignoreSelect) { | |
10595 this.close(); | |
10596 } | 10592 } |
10597 }, | 10593 }]; |
10598 | 10594 } |
10599 /** | 10595 } |
10600 * Closes the dropdown when an `iron-activate` event is received if | 10596 }, |
10601 * `closeOnActivate` is true. | 10597 |
10602 * | 10598 keyBindings: { |
10603 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. | 10599 'tab': 'onTabPressed_', |
10604 */ | 10600 }, |
10605 _onIronActivate: function(event) { | 10601 |
10606 if (this.closeOnActivate) { | 10602 listeners: { |
10607 this.close(); | 10603 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', |
10608 } | 10604 }, |
10609 }, | 10605 |
10610 | 10606 /** |
10611 /** | 10607 * The last anchor that was used to open a menu. It's necessary for toggling. |
10612 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. | 10608 * @private {?Element} |
10613 * When the dropdown closes, the `paper-menu-button` fires `paper-close`
. | 10609 */ |
10614 * | 10610 lastAnchor_: null, |
10615 * @param {boolean} opened True if the dropdown is opened, otherwise fal
se. | 10611 |
10616 * @param {boolean} oldOpened The previous value of `opened`. | 10612 /** |
10617 */ | 10613 * The first focusable child in the menu's light DOM. |
10618 _openedChanged: function(opened, oldOpened) { | 10614 * @private {?Element} |
10619 if (opened) { | 10615 */ |
10620 // TODO(cdata): Update this when we can measure changes in distribut
ed | 10616 firstFocus_: null, |
10621 // children in an idiomatic way. | 10617 |
10622 // We poke this property in case the element has changed. This will | 10618 /** |
10623 // cause the focus target for the `iron-dropdown` to be updated as | 10619 * The last focusable child in the menu's light DOM. |
10624 // necessary: | 10620 * @private {?Element} |
10625 this._dropdownContent = this.contentElement; | 10621 */ |
10626 this.fire('paper-dropdown-open'); | 10622 lastFocus_: null, |
10627 } else if (oldOpened != null) { | 10623 |
10628 this.fire('paper-dropdown-close'); | 10624 /** @override */ |
10629 } | 10625 attached: function() { |
10630 }, | 10626 window.addEventListener('resize', this.closeMenu.bind(this)); |
10631 | 10627 }, |
10632 /** | 10628 |
10633 * If the dropdown is open when disabled becomes true, close the | 10629 /** Closes the menu. */ |
10634 * dropdown. | 10630 closeMenu: function() { |
10635 * | 10631 if (this.root.activeElement == null) { |
10636 * @param {boolean} disabled True if disabled, otherwise false. | 10632 // Something else has taken focus away from the menu. Do not attempt to |
10637 */ | 10633 // restore focus to the button which opened the menu. |
10638 _disabledChanged: function(disabled) { | 10634 this.$.dropdown.restoreFocusOnClose = false; |
10639 Polymer.IronControlState._disabledChanged.apply(this, arguments); | 10635 } |
10640 if (disabled && this.opened) { | 10636 this.menuOpen = false; |
10641 this.close(); | 10637 }, |
10642 } | 10638 |
10643 }, | 10639 /** |
10644 | 10640 * Opens the menu at the anchor location. |
10645 __onIronOverlayCanceled: function(event) { | 10641 * @param {!Element} anchor The location to display the menu. |
10646 var uiEvent = event.detail; | 10642 * @param {!Object} itemData The contextual item's data. |
10647 var target = Polymer.dom(uiEvent).rootTarget; | 10643 */ |
10648 var trigger = this.$.trigger; | 10644 openMenu: function(anchor, itemData) { |
10649 var path = Polymer.dom(uiEvent).path; | 10645 if (this.lastAnchor_ == anchor && this.menuOpen) |
10650 | 10646 return; |
10651 if (path.indexOf(trigger) > -1) { | 10647 |
10652 event.preventDefault(); | 10648 if (this.menuOpen) |
10653 } | 10649 this.closeMenu(); |
10654 } | 10650 |
10655 }); | 10651 this.itemData = itemData; |
10656 | 10652 this.lastAnchor_ = anchor; |
10657 Object.keys(config).forEach(function (key) { | 10653 this.$.dropdown.restoreFocusOnClose = true; |
10658 PaperMenuButton[key] = config[key]; | 10654 |
10659 }); | 10655 var focusableChildren = Polymer.dom(this).querySelectorAll( |
10660 | 10656 '[tabindex]:not([hidden]),button:not([hidden])'); |
10661 Polymer.PaperMenuButton = PaperMenuButton; | 10657 if (focusableChildren.length > 0) { |
10662 })(); | 10658 this.$.dropdown.focusTarget = focusableChildren[0]; |
10663 /** | 10659 this.firstFocus_ = focusableChildren[0]; |
10664 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | 10660 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
10665 * | 10661 } |
10666 * @polymerBehavior Polymer.PaperInkyFocusBehavior | 10662 |
10667 */ | 10663 // Move the menu to the anchor. |
10668 Polymer.PaperInkyFocusBehaviorImpl = { | 10664 this.$.dropdown.positionTarget = anchor; |
10669 observers: [ | 10665 this.menuOpen = true; |
10670 '_focusedChanged(receivedFocusFromKeyboard)' | 10666 }, |
10671 ], | 10667 |
10672 | 10668 /** |
10673 _focusedChanged: function(receivedFocusFromKeyboard) { | 10669 * Toggles the menu for the anchor that is passed in. |
10674 if (receivedFocusFromKeyboard) { | 10670 * @param {!Element} anchor The location to display the menu. |
10675 this.ensureRipple(); | 10671 * @param {!Object} itemData The contextual item's data. |
10676 } | 10672 */ |
10677 if (this.hasRipple()) { | 10673 toggleMenu: function(anchor, itemData) { |
10678 this._ripple.holdDown = receivedFocusFromKeyboard; | 10674 if (anchor == this.lastAnchor_ && this.menuOpen) |
10679 } | 10675 this.closeMenu(); |
10680 }, | 10676 else |
10681 | 10677 this.openMenu(anchor, itemData); |
10682 _createRipple: function() { | 10678 }, |
10683 var ripple = Polymer.PaperRippleBehavior._createRipple(); | 10679 |
10684 ripple.id = 'ink'; | 10680 /** |
10685 ripple.setAttribute('center', ''); | 10681 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from |
10686 ripple.classList.add('circle'); | 10682 * the first element with a nonzero tabindex to the last such element. |
10687 return ripple; | 10683 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available |
| 10684 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). |
| 10685 * @param {CustomEvent} e |
| 10686 */ |
| 10687 onTabPressed_: function(e) { |
| 10688 if (!this.firstFocus_ || !this.lastFocus_) |
| 10689 return; |
| 10690 |
| 10691 var toFocus; |
| 10692 var keyEvent = e.detail.keyboardEvent; |
| 10693 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) |
| 10694 toFocus = this.lastFocus_; |
| 10695 else if (keyEvent.target == this.lastFocus_) |
| 10696 toFocus = this.firstFocus_; |
| 10697 |
| 10698 if (!toFocus) |
| 10699 return; |
| 10700 |
| 10701 e.preventDefault(); |
| 10702 toFocus.focus(); |
| 10703 }, |
| 10704 |
| 10705 /** |
| 10706 * Ensure the menu is reset properly when it is closed by the dropdown (eg, |
| 10707 * clicking outside). |
| 10708 * @private |
| 10709 */ |
| 10710 menuOpenChanged_: function() { |
| 10711 if (!this.menuOpen) { |
| 10712 this.itemData = null; |
| 10713 this.lastAnchor_ = null; |
| 10714 } |
| 10715 }, |
| 10716 |
| 10717 /** |
| 10718 * Prevent focus restoring when tapping outside the menu. This stops the |
| 10719 * focus moving around unexpectedly when closing the menu with the mouse. |
| 10720 * @param {CustomEvent} e |
| 10721 * @private |
| 10722 */ |
| 10723 onOverlayCanceled_: function(e) { |
| 10724 if (e.detail.type == 'tap') |
| 10725 this.$.dropdown.restoreFocusOnClose = false; |
| 10726 }, |
| 10727 }); |
| 10728 /** @polymerBehavior Polymer.PaperItemBehavior */ |
| 10729 Polymer.PaperItemBehaviorImpl = { |
| 10730 hostAttributes: { |
| 10731 role: 'option', |
| 10732 tabindex: '0' |
10688 } | 10733 } |
10689 }; | 10734 }; |
10690 | 10735 |
10691 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | 10736 /** @polymerBehavior */ |
10692 Polymer.PaperInkyFocusBehavior = [ | 10737 Polymer.PaperItemBehavior = [ |
10693 Polymer.IronButtonState, | 10738 Polymer.IronButtonState, |
10694 Polymer.IronControlState, | 10739 Polymer.IronControlState, |
10695 Polymer.PaperRippleBehavior, | 10740 Polymer.PaperItemBehaviorImpl |
10696 Polymer.PaperInkyFocusBehaviorImpl | |
10697 ]; | 10741 ]; |
10698 Polymer({ | 10742 Polymer({ |
10699 is: 'paper-icon-button', | 10743 is: 'paper-item', |
| 10744 |
| 10745 behaviors: [ |
| 10746 Polymer.PaperItemBehavior |
| 10747 ] |
| 10748 }); |
| 10749 Polymer({ |
| 10750 |
| 10751 is: 'iron-collapse', |
| 10752 |
| 10753 behaviors: [ |
| 10754 Polymer.IronResizableBehavior |
| 10755 ], |
| 10756 |
| 10757 properties: { |
| 10758 |
| 10759 /** |
| 10760 * If true, the orientation is horizontal; otherwise is vertical. |
| 10761 * |
| 10762 * @attribute horizontal |
| 10763 */ |
| 10764 horizontal: { |
| 10765 type: Boolean, |
| 10766 value: false, |
| 10767 observer: '_horizontalChanged' |
| 10768 }, |
| 10769 |
| 10770 /** |
| 10771 * Set opened to true to show the collapse element and to false to hide it
. |
| 10772 * |
| 10773 * @attribute opened |
| 10774 */ |
| 10775 opened: { |
| 10776 type: Boolean, |
| 10777 value: false, |
| 10778 notify: true, |
| 10779 observer: '_openedChanged' |
| 10780 }, |
| 10781 |
| 10782 /** |
| 10783 * Set noAnimation to true to disable animations |
| 10784 * |
| 10785 * @attribute noAnimation |
| 10786 */ |
| 10787 noAnimation: { |
| 10788 type: Boolean |
| 10789 }, |
| 10790 |
| 10791 }, |
| 10792 |
| 10793 get dimension() { |
| 10794 return this.horizontal ? 'width' : 'height'; |
| 10795 }, |
| 10796 |
| 10797 /** |
| 10798 * `maxWidth` or `maxHeight`. |
| 10799 * @private |
| 10800 */ |
| 10801 get _dimensionMax() { |
| 10802 return this.horizontal ? 'maxWidth' : 'maxHeight'; |
| 10803 }, |
| 10804 |
| 10805 /** |
| 10806 * `max-width` or `max-height`. |
| 10807 * @private |
| 10808 */ |
| 10809 get _dimensionMaxCss() { |
| 10810 return this.horizontal ? 'max-width' : 'max-height'; |
| 10811 }, |
| 10812 |
| 10813 hostAttributes: { |
| 10814 role: 'group', |
| 10815 'aria-hidden': 'true', |
| 10816 'aria-expanded': 'false' |
| 10817 }, |
| 10818 |
| 10819 listeners: { |
| 10820 transitionend: '_transitionEnd' |
| 10821 }, |
| 10822 |
| 10823 attached: function() { |
| 10824 // It will take care of setting correct classes and styles. |
| 10825 this._transitionEnd(); |
| 10826 }, |
| 10827 |
| 10828 /** |
| 10829 * Toggle the opened state. |
| 10830 * |
| 10831 * @method toggle |
| 10832 */ |
| 10833 toggle: function() { |
| 10834 this.opened = !this.opened; |
| 10835 }, |
| 10836 |
| 10837 show: function() { |
| 10838 this.opened = true; |
| 10839 }, |
| 10840 |
| 10841 hide: function() { |
| 10842 this.opened = false; |
| 10843 }, |
| 10844 |
| 10845 /** |
| 10846 * Updates the size of the element. |
| 10847 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop
erty value, usually `auto` or `0px`. |
| 10848 * @param {boolean=} animated if `true` updates the size with an animation,
otherwise without. |
| 10849 */ |
| 10850 updateSize: function(size, animated) { |
| 10851 // No change! |
| 10852 var curSize = this.style[this._dimensionMax]; |
| 10853 if (curSize === size || (size === 'auto' && !curSize)) { |
| 10854 return; |
| 10855 } |
| 10856 |
| 10857 this._updateTransition(false); |
| 10858 // If we can animate, must do some prep work. |
| 10859 if (animated && !this.noAnimation && this._isDisplayed) { |
| 10860 // Animation will start at the current size. |
| 10861 var startSize = this._calcSize(); |
| 10862 // For `auto` we must calculate what is the final size for the animation
. |
| 10863 // After the transition is done, _transitionEnd will set the size back t
o `auto`. |
| 10864 if (size === 'auto') { |
| 10865 this.style[this._dimensionMax] = ''; |
| 10866 size = this._calcSize(); |
| 10867 } |
| 10868 // Go to startSize without animation. |
| 10869 this.style[this._dimensionMax] = startSize; |
| 10870 // Force layout to ensure transition will go. Set scrollTop to itself |
| 10871 // so that compilers won't remove it. |
| 10872 this.scrollTop = this.scrollTop; |
| 10873 // Enable animation. |
| 10874 this._updateTransition(true); |
| 10875 } |
| 10876 // Set the final size. |
| 10877 if (size === 'auto') { |
| 10878 this.style[this._dimensionMax] = ''; |
| 10879 } else { |
| 10880 this.style[this._dimensionMax] = size; |
| 10881 } |
| 10882 }, |
| 10883 |
| 10884 /** |
| 10885 * enableTransition() is deprecated, but left over so it doesn't break exist
ing code. |
| 10886 * Please use `noAnimation` property instead. |
| 10887 * |
| 10888 * @method enableTransition |
| 10889 * @deprecated since version 1.0.4 |
| 10890 */ |
| 10891 enableTransition: function(enabled) { |
| 10892 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation`
instead.'); |
| 10893 this.noAnimation = !enabled; |
| 10894 }, |
| 10895 |
| 10896 _updateTransition: function(enabled) { |
| 10897 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s'
; |
| 10898 }, |
| 10899 |
| 10900 _horizontalChanged: function() { |
| 10901 this.style.transitionProperty = this._dimensionMaxCss; |
| 10902 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma
xWidth'; |
| 10903 this.style[otherDimension] = ''; |
| 10904 this.updateSize(this.opened ? 'auto' : '0px', false); |
| 10905 }, |
| 10906 |
| 10907 _openedChanged: function() { |
| 10908 this.setAttribute('aria-expanded', this.opened); |
| 10909 this.setAttribute('aria-hidden', !this.opened); |
| 10910 |
| 10911 this.toggleClass('iron-collapse-closed', false); |
| 10912 this.toggleClass('iron-collapse-opened', false); |
| 10913 this.updateSize(this.opened ? 'auto' : '0px', true); |
| 10914 |
| 10915 // Focus the current collapse. |
| 10916 if (this.opened) { |
| 10917 this.focus(); |
| 10918 } |
| 10919 if (this.noAnimation) { |
| 10920 this._transitionEnd(); |
| 10921 } |
| 10922 }, |
| 10923 |
| 10924 _transitionEnd: function() { |
| 10925 if (this.opened) { |
| 10926 this.style[this._dimensionMax] = ''; |
| 10927 } |
| 10928 this.toggleClass('iron-collapse-closed', !this.opened); |
| 10929 this.toggleClass('iron-collapse-opened', this.opened); |
| 10930 this._updateTransition(false); |
| 10931 this.notifyResize(); |
| 10932 }, |
| 10933 |
| 10934 /** |
| 10935 * Simplistic heuristic to detect if element has a parent with display: none |
| 10936 * |
| 10937 * @private |
| 10938 */ |
| 10939 get _isDisplayed() { |
| 10940 var rect = this.getBoundingClientRect(); |
| 10941 for (var prop in rect) { |
| 10942 if (rect[prop] !== 0) return true; |
| 10943 } |
| 10944 return false; |
| 10945 }, |
| 10946 |
| 10947 _calcSize: function() { |
| 10948 return this.getBoundingClientRect()[this.dimension] + 'px'; |
| 10949 } |
| 10950 |
| 10951 }); |
| 10952 /** |
| 10953 Polymer.IronFormElementBehavior enables a custom element to be included |
| 10954 in an `iron-form`. |
| 10955 |
| 10956 @demo demo/index.html |
| 10957 @polymerBehavior |
| 10958 */ |
| 10959 Polymer.IronFormElementBehavior = { |
| 10960 |
| 10961 properties: { |
| 10962 /** |
| 10963 * Fired when the element is added to an `iron-form`. |
| 10964 * |
| 10965 * @event iron-form-element-register |
| 10966 */ |
| 10967 |
| 10968 /** |
| 10969 * Fired when the element is removed from an `iron-form`. |
| 10970 * |
| 10971 * @event iron-form-element-unregister |
| 10972 */ |
| 10973 |
| 10974 /** |
| 10975 * The name of this element. |
| 10976 */ |
| 10977 name: { |
| 10978 type: String |
| 10979 }, |
| 10980 |
| 10981 /** |
| 10982 * The value for this element. |
| 10983 */ |
| 10984 value: { |
| 10985 notify: true, |
| 10986 type: String |
| 10987 }, |
| 10988 |
| 10989 /** |
| 10990 * Set to true to mark the input as required. If used in a form, a |
| 10991 * custom element that uses this behavior should also use |
| 10992 * Polymer.IronValidatableBehavior and define a custom validation method. |
| 10993 * Otherwise, a `required` element will always be considered valid. |
| 10994 * It's also strongly recommended to provide a visual style for the elemen
t |
| 10995 * when its value is invalid. |
| 10996 */ |
| 10997 required: { |
| 10998 type: Boolean, |
| 10999 value: false |
| 11000 }, |
| 11001 |
| 11002 /** |
| 11003 * The form that the element is registered to. |
| 11004 */ |
| 11005 _parentForm: { |
| 11006 type: Object |
| 11007 } |
| 11008 }, |
| 11009 |
| 11010 attached: function() { |
| 11011 // Note: the iron-form that this element belongs to will set this |
| 11012 // element's _parentForm property when handling this event. |
| 11013 this.fire('iron-form-element-register'); |
| 11014 }, |
| 11015 |
| 11016 detached: function() { |
| 11017 if (this._parentForm) { |
| 11018 this._parentForm.fire('iron-form-element-unregister', {target: this}); |
| 11019 } |
| 11020 } |
| 11021 |
| 11022 }; |
| 11023 /** |
| 11024 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element |
| 11025 * that has a `checked` property, which can be used for validation if the |
| 11026 * element is also `required`. Element instances implementing this behavior |
| 11027 * will also be registered for use in an `iron-form` element. |
| 11028 * |
| 11029 * @demo demo/index.html |
| 11030 * @polymerBehavior Polymer.IronCheckedElementBehavior |
| 11031 */ |
| 11032 Polymer.IronCheckedElementBehaviorImpl = { |
| 11033 |
| 11034 properties: { |
| 11035 /** |
| 11036 * Fired when the checked state changes. |
| 11037 * |
| 11038 * @event iron-change |
| 11039 */ |
| 11040 |
| 11041 /** |
| 11042 * Gets or sets the state, `true` is checked and `false` is unchecked. |
| 11043 */ |
| 11044 checked: { |
| 11045 type: Boolean, |
| 11046 value: false, |
| 11047 reflectToAttribute: true, |
| 11048 notify: true, |
| 11049 observer: '_checkedChanged' |
| 11050 }, |
| 11051 |
| 11052 /** |
| 11053 * If true, the button toggles the active state with each tap or press |
| 11054 * of the spacebar. |
| 11055 */ |
| 11056 toggles: { |
| 11057 type: Boolean, |
| 11058 value: true, |
| 11059 reflectToAttribute: true |
| 11060 }, |
| 11061 |
| 11062 /* Overriden from Polymer.IronFormElementBehavior */ |
| 11063 value: { |
| 11064 type: String, |
| 11065 value: 'on', |
| 11066 observer: '_valueChanged' |
| 11067 } |
| 11068 }, |
| 11069 |
| 11070 observers: [ |
| 11071 '_requiredChanged(required)' |
| 11072 ], |
| 11073 |
| 11074 created: function() { |
| 11075 // Used by `iron-form` to handle the case that an element with this behavi
or |
| 11076 // doesn't have a role of 'checkbox' or 'radio', but should still only be |
| 11077 // included when the form is serialized if `this.checked === true`. |
| 11078 this._hasIronCheckedElementBehavior = true; |
| 11079 }, |
| 11080 |
| 11081 /** |
| 11082 * Returns false if the element is required and not checked, and true otherw
ise. |
| 11083 * @param {*=} _value Ignored. |
| 11084 * @return {boolean} true if `required` is false or if `checked` is true. |
| 11085 */ |
| 11086 _getValidity: function(_value) { |
| 11087 return this.disabled || !this.required || this.checked; |
| 11088 }, |
| 11089 |
| 11090 /** |
| 11091 * Update the aria-required label when `required` is changed. |
| 11092 */ |
| 11093 _requiredChanged: function() { |
| 11094 if (this.required) { |
| 11095 this.setAttribute('aria-required', 'true'); |
| 11096 } else { |
| 11097 this.removeAttribute('aria-required'); |
| 11098 } |
| 11099 }, |
| 11100 |
| 11101 /** |
| 11102 * Fire `iron-changed` when the checked state changes. |
| 11103 */ |
| 11104 _checkedChanged: function() { |
| 11105 this.active = this.checked; |
| 11106 this.fire('iron-change'); |
| 11107 }, |
| 11108 |
| 11109 /** |
| 11110 * Reset value to 'on' if it is set to `undefined`. |
| 11111 */ |
| 11112 _valueChanged: function() { |
| 11113 if (this.value === undefined || this.value === null) { |
| 11114 this.value = 'on'; |
| 11115 } |
| 11116 } |
| 11117 }; |
| 11118 |
| 11119 /** @polymerBehavior Polymer.IronCheckedElementBehavior */ |
| 11120 Polymer.IronCheckedElementBehavior = [ |
| 11121 Polymer.IronFormElementBehavior, |
| 11122 Polymer.IronValidatableBehavior, |
| 11123 Polymer.IronCheckedElementBehaviorImpl |
| 11124 ]; |
| 11125 /** |
| 11126 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element |
| 11127 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio
r` |
| 11128 * and is compatible with having a ripple effect. |
| 11129 * @polymerBehavior Polymer.PaperCheckedElementBehavior |
| 11130 */ |
| 11131 Polymer.PaperCheckedElementBehaviorImpl = { |
| 11132 /** |
| 11133 * Synchronizes the element's checked state with its ripple effect. |
| 11134 */ |
| 11135 _checkedChanged: function() { |
| 11136 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); |
| 11137 if (this.hasRipple()) { |
| 11138 if (this.checked) { |
| 11139 this._ripple.setAttribute('checked', ''); |
| 11140 } else { |
| 11141 this._ripple.removeAttribute('checked'); |
| 11142 } |
| 11143 } |
| 11144 }, |
| 11145 |
| 11146 /** |
| 11147 * Synchronizes the element's `active` and `checked` state. |
| 11148 */ |
| 11149 _buttonStateChanged: function() { |
| 11150 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); |
| 11151 if (this.disabled) { |
| 11152 return; |
| 11153 } |
| 11154 if (this.isAttached) { |
| 11155 this.checked = this.active; |
| 11156 } |
| 11157 } |
| 11158 }; |
| 11159 |
| 11160 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */ |
| 11161 Polymer.PaperCheckedElementBehavior = [ |
| 11162 Polymer.PaperInkyFocusBehavior, |
| 11163 Polymer.IronCheckedElementBehavior, |
| 11164 Polymer.PaperCheckedElementBehaviorImpl |
| 11165 ]; |
| 11166 Polymer({ |
| 11167 is: 'paper-checkbox', |
| 11168 |
| 11169 behaviors: [ |
| 11170 Polymer.PaperCheckedElementBehavior |
| 11171 ], |
10700 | 11172 |
10701 hostAttributes: { | 11173 hostAttributes: { |
10702 role: 'button', | 11174 role: 'checkbox', |
10703 tabindex: '0' | 11175 'aria-checked': false, |
10704 }, | 11176 tabindex: 0 |
10705 | 11177 }, |
10706 behaviors: [ | |
10707 Polymer.PaperInkyFocusBehavior | |
10708 ], | |
10709 | 11178 |
10710 properties: { | 11179 properties: { |
10711 /** | 11180 /** |
10712 * The URL of an image for the icon. If the src property is specified, | 11181 * Fired when the checked state changes due to user interaction. |
10713 * the icon property should not be. | 11182 * |
| 11183 * @event change |
10714 */ | 11184 */ |
10715 src: { | |
10716 type: String | |
10717 }, | |
10718 | 11185 |
10719 /** | 11186 /** |
10720 * Specifies the icon name or index in the set of icons available in | 11187 * Fired when the checked state changes. |
10721 * the icon's icon set. If the icon property is specified, | 11188 * |
10722 * the src property should not be. | 11189 * @event iron-change |
10723 */ | 11190 */ |
10724 icon: { | 11191 ariaActiveAttribute: { |
10725 type: String | |
10726 }, | |
10727 | |
10728 /** | |
10729 * Specifies the alternate text for the button, for accessibility. | |
10730 */ | |
10731 alt: { | |
10732 type: String, | 11192 type: String, |
10733 observer: "_altChanged" | 11193 value: 'aria-checked' |
10734 } | 11194 } |
10735 }, | 11195 }, |
10736 | 11196 |
10737 _altChanged: function(newValue, oldValue) { | 11197 _computeCheckboxClass: function(checked, invalid) { |
10738 var label = this.getAttribute('aria-label'); | 11198 var className = ''; |
10739 | 11199 if (checked) { |
10740 // Don't stomp over a user-set aria-label. | 11200 className += 'checked '; |
10741 if (!label || oldValue == label) { | 11201 } |
10742 this.setAttribute('aria-label', newValue); | 11202 if (invalid) { |
| 11203 className += 'invalid'; |
| 11204 } |
| 11205 return className; |
| 11206 }, |
| 11207 |
| 11208 _computeCheckmarkClass: function(checked) { |
| 11209 return checked ? '' : 'hidden'; |
| 11210 }, |
| 11211 |
| 11212 // create ripple inside the checkboxContainer |
| 11213 _createRipple: function() { |
| 11214 this._rippleContainer = this.$.checkboxContainer; |
| 11215 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); |
| 11216 } |
| 11217 |
| 11218 }); |
| 11219 Polymer({ |
| 11220 is: 'paper-icon-button-light', |
| 11221 extends: 'button', |
| 11222 |
| 11223 behaviors: [ |
| 11224 Polymer.PaperRippleBehavior |
| 11225 ], |
| 11226 |
| 11227 listeners: { |
| 11228 'down': '_rippleDown', |
| 11229 'up': '_rippleUp', |
| 11230 'focus': '_rippleDown', |
| 11231 'blur': '_rippleUp', |
| 11232 }, |
| 11233 |
| 11234 _rippleDown: function() { |
| 11235 this.getRipple().downAction(); |
| 11236 }, |
| 11237 |
| 11238 _rippleUp: function() { |
| 11239 this.getRipple().upAction(); |
| 11240 }, |
| 11241 |
| 11242 /** |
| 11243 * @param {...*} var_args |
| 11244 */ |
| 11245 ensureRipple: function(var_args) { |
| 11246 var lastRipple = this._ripple; |
| 11247 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
| 11248 if (this._ripple && this._ripple !== lastRipple) { |
| 11249 this._ripple.center = true; |
| 11250 this._ripple.classList.add('circle'); |
10743 } | 11251 } |
10744 } | 11252 } |
10745 }); | 11253 }); |
10746 // Copyright 2016 The Chromium Authors. All rights reserved. | 11254 // Copyright 2016 The Chromium Authors. All rights reserved. |
10747 // Use of this source code is governed by a BSD-style license that can be | 11255 // Use of this source code is governed by a BSD-style license that can be |
10748 // found in the LICENSE file. | 11256 // found in the LICENSE file. |
10749 | 11257 |
| 11258 cr.define('cr.icon', function() { |
| 11259 /** |
| 11260 * @return {!Array<number>} The scale factors supported by this platform for |
| 11261 * webui resources. |
| 11262 */ |
| 11263 function getSupportedScaleFactors() { |
| 11264 var supportedScaleFactors = []; |
| 11265 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { |
| 11266 // All desktop platforms support zooming which also updates the |
| 11267 // renderer's device scale factors (a.k.a devicePixelRatio), and |
| 11268 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in |
| 11269 // image-set on these platforms so that the renderer can pick the |
| 11270 // closest image for the current device scale factor. |
| 11271 supportedScaleFactors.push(1); |
| 11272 supportedScaleFactors.push(2); |
| 11273 } else { |
| 11274 // For other platforms that use fixed device scale factor, use |
| 11275 // the window's device pixel ratio. |
| 11276 // TODO(oshima): Investigate if Android/iOS need to use image-set. |
| 11277 supportedScaleFactors.push(window.devicePixelRatio); |
| 11278 } |
| 11279 return supportedScaleFactors; |
| 11280 } |
| 11281 |
| 11282 /** |
| 11283 * Returns the URL of the image, or an image set of URLs for the profile |
| 11284 * avatar. Default avatars have resources available for multiple scalefactors, |
| 11285 * whereas the GAIA profile image only comes in one size. |
| 11286 * |
| 11287 * @param {string} path The path of the image. |
| 11288 * @return {string} The url, or an image set of URLs of the avatar image. |
| 11289 */ |
| 11290 function getProfileAvatarIcon(path) { |
| 11291 var chromeThemePath = 'chrome://theme'; |
| 11292 var isDefaultAvatar = |
| 11293 (path.slice(0, chromeThemePath.length) == chromeThemePath); |
| 11294 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); |
| 11295 } |
| 11296 |
| 11297 /** |
| 11298 * Generates a CSS -webkit-image-set for a chrome:// url. |
| 11299 * An entry in the image set is added for each of getSupportedScaleFactors(). |
| 11300 * The scale-factor-specific url is generated by replacing the first instance |
| 11301 * of 'scalefactor' in |path| with the numeric scale factor. |
| 11302 * @param {string} path The URL to generate an image set for. |
| 11303 * 'scalefactor' should be a substring of |path|. |
| 11304 * @return {string} The CSS -webkit-image-set. |
| 11305 */ |
| 11306 function imageset(path) { |
| 11307 var supportedScaleFactors = getSupportedScaleFactors(); |
| 11308 |
| 11309 var replaceStartIndex = path.indexOf('scalefactor'); |
| 11310 if (replaceStartIndex < 0) |
| 11311 return url(path); |
| 11312 |
| 11313 var s = ''; |
| 11314 for (var i = 0; i < supportedScaleFactors.length; ++i) { |
| 11315 var scaleFactor = supportedScaleFactors[i]; |
| 11316 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + |
| 11317 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length); |
| 11318 |
| 11319 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; |
| 11320 |
| 11321 if (i != supportedScaleFactors.length - 1) |
| 11322 s += ', '; |
| 11323 } |
| 11324 return '-webkit-image-set(' + s + ')'; |
| 11325 } |
| 11326 |
| 11327 /** |
| 11328 * A regular expression for identifying favicon URLs. |
| 11329 * @const {!RegExp} |
| 11330 */ |
| 11331 var FAVICON_URL_REGEX = /\.ico$/i; |
| 11332 |
| 11333 /** |
| 11334 * Creates a CSS -webkit-image-set for a favicon request. |
| 11335 * @param {string} url Either the URL of the original page or of the favicon |
| 11336 * itself. |
| 11337 * @param {number=} opt_size Optional preferred size of the favicon. |
| 11338 * @param {string=} opt_type Optional type of favicon to request. Valid values |
| 11339 * are 'favicon' and 'touch-icon'. Default is 'favicon'. |
| 11340 * @return {string} -webkit-image-set for the favicon. |
| 11341 */ |
| 11342 function getFaviconImageSet(url, opt_size, opt_type) { |
| 11343 var size = opt_size || 16; |
| 11344 var type = opt_type || 'favicon'; |
| 11345 |
| 11346 return imageset( |
| 11347 'chrome://' + type + '/size/' + size + '@scalefactorx/' + |
| 11348 // Note: Literal 'iconurl' must match |kIconURLParameter| in |
| 11349 // components/favicon_base/favicon_url_parser.cc. |
| 11350 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); |
| 11351 } |
| 11352 |
| 11353 return { |
| 11354 getSupportedScaleFactors: getSupportedScaleFactors, |
| 11355 getProfileAvatarIcon: getProfileAvatarIcon, |
| 11356 getFaviconImageSet: getFaviconImageSet, |
| 11357 }; |
| 11358 }); |
| 11359 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11360 // Use of this source code is governed by a BSD-style license that can be |
| 11361 // found in the LICENSE file. |
| 11362 |
10750 /** | 11363 /** |
10751 * Implements an incremental search field which can be shown and hidden. | 11364 * @fileoverview Defines a singleton object, md_history.BrowserService, which |
10752 * Canonical implementation is <cr-search-field>. | 11365 * provides access to chrome.send APIs. |
10753 * @polymerBehavior | |
10754 */ | 11366 */ |
10755 var CrSearchFieldBehavior = { | 11367 |
| 11368 cr.define('md_history', function() { |
| 11369 /** @constructor */ |
| 11370 function BrowserService() { |
| 11371 /** @private {Array<!HistoryEntry>} */ |
| 11372 this.pendingDeleteItems_ = null; |
| 11373 /** @private {PromiseResolver} */ |
| 11374 this.pendingDeletePromise_ = null; |
| 11375 } |
| 11376 |
| 11377 BrowserService.prototype = { |
| 11378 /** |
| 11379 * @param {!Array<!HistoryEntry>} items |
| 11380 * @return {Promise<!Array<!HistoryEntry>>} |
| 11381 */ |
| 11382 deleteItems: function(items) { |
| 11383 if (this.pendingDeleteItems_ != null) { |
| 11384 // There's already a deletion in progress, reject immediately. |
| 11385 return new Promise(function(resolve, reject) { reject(items); }); |
| 11386 } |
| 11387 |
| 11388 var removalList = items.map(function(item) { |
| 11389 return { |
| 11390 url: item.url, |
| 11391 timestamps: item.allTimestamps |
| 11392 }; |
| 11393 }); |
| 11394 |
| 11395 this.pendingDeleteItems_ = items; |
| 11396 this.pendingDeletePromise_ = new PromiseResolver(); |
| 11397 |
| 11398 chrome.send('removeVisits', removalList); |
| 11399 |
| 11400 return this.pendingDeletePromise_.promise; |
| 11401 }, |
| 11402 |
| 11403 /** |
| 11404 * @param {!string} url |
| 11405 */ |
| 11406 removeBookmark: function(url) { |
| 11407 chrome.send('removeBookmark', [url]); |
| 11408 }, |
| 11409 |
| 11410 /** |
| 11411 * @param {string} sessionTag |
| 11412 */ |
| 11413 openForeignSessionAllTabs: function(sessionTag) { |
| 11414 chrome.send('openForeignSession', [sessionTag]); |
| 11415 }, |
| 11416 |
| 11417 /** |
| 11418 * @param {string} sessionTag |
| 11419 * @param {number} windowId |
| 11420 * @param {number} tabId |
| 11421 * @param {MouseEvent} e |
| 11422 */ |
| 11423 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { |
| 11424 chrome.send('openForeignSession', [ |
| 11425 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, |
| 11426 e.ctrlKey, e.metaKey, e.shiftKey |
| 11427 ]); |
| 11428 }, |
| 11429 |
| 11430 /** |
| 11431 * @param {string} sessionTag |
| 11432 */ |
| 11433 deleteForeignSession: function(sessionTag) { |
| 11434 chrome.send('deleteForeignSession', [sessionTag]); |
| 11435 }, |
| 11436 |
| 11437 openClearBrowsingData: function() { |
| 11438 chrome.send('clearBrowsingData'); |
| 11439 }, |
| 11440 |
| 11441 /** |
| 11442 * Record an action in UMA. |
| 11443 * @param {string} actionDesc The name of the action to be logged. |
| 11444 */ |
| 11445 recordAction: function(actionDesc) { |
| 11446 chrome.send('metricsHandler:recordAction', [actionDesc]); |
| 11447 }, |
| 11448 |
| 11449 /** |
| 11450 * @param {boolean} successful |
| 11451 * @private |
| 11452 */ |
| 11453 resolveDelete_: function(successful) { |
| 11454 if (this.pendingDeleteItems_ == null || |
| 11455 this.pendingDeletePromise_ == null) { |
| 11456 return; |
| 11457 } |
| 11458 |
| 11459 if (successful) |
| 11460 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_); |
| 11461 else |
| 11462 this.pendingDeletePromise_.reject(this.pendingDeleteItems_); |
| 11463 |
| 11464 this.pendingDeleteItems_ = null; |
| 11465 this.pendingDeletePromise_ = null; |
| 11466 }, |
| 11467 }; |
| 11468 |
| 11469 cr.addSingletonGetter(BrowserService); |
| 11470 |
| 11471 return {BrowserService: BrowserService}; |
| 11472 }); |
| 11473 |
| 11474 /** |
| 11475 * Called by the history backend when deletion was succesful. |
| 11476 */ |
| 11477 function deleteComplete() { |
| 11478 md_history.BrowserService.getInstance().resolveDelete_(true); |
| 11479 } |
| 11480 |
| 11481 /** |
| 11482 * Called by the history backend when the deletion failed. |
| 11483 */ |
| 11484 function deleteFailed() { |
| 11485 md_history.BrowserService.getInstance().resolveDelete_(false); |
| 11486 }; |
| 11487 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11488 // Use of this source code is governed by a BSD-style license that can be |
| 11489 // found in the LICENSE file. |
| 11490 |
| 11491 Polymer({ |
| 11492 is: 'history-searched-label', |
| 11493 |
10756 properties: { | 11494 properties: { |
10757 label: { | 11495 // The text to show in this label. |
10758 type: String, | 11496 title: String, |
10759 value: '', | 11497 |
10760 }, | 11498 // The search term to bold within the title. |
10761 | 11499 searchTerm: String, |
10762 clearLabel: { | 11500 }, |
10763 type: String, | 11501 |
10764 value: '', | 11502 observers: ['setSearchedTextToBold_(title, searchTerm)'], |
10765 }, | 11503 |
10766 | 11504 /** |
10767 showingSearch: { | 11505 * Updates the page title. If a search term is specified, highlights any |
10768 type: Boolean, | 11506 * occurrences of the search term in bold. |
10769 value: false, | |
10770 notify: true, | |
10771 observer: 'showingSearchChanged_', | |
10772 reflectToAttribute: true | |
10773 }, | |
10774 | |
10775 /** @private */ | |
10776 lastValue_: { | |
10777 type: String, | |
10778 value: '', | |
10779 }, | |
10780 }, | |
10781 | |
10782 /** | |
10783 * @abstract | |
10784 * @return {!HTMLInputElement} The input field element the behavior should | |
10785 * use. | |
10786 */ | |
10787 getSearchInput: function() {}, | |
10788 | |
10789 /** | |
10790 * @return {string} The value of the search field. | |
10791 */ | |
10792 getValue: function() { | |
10793 return this.getSearchInput().value; | |
10794 }, | |
10795 | |
10796 /** | |
10797 * Sets the value of the search field. | |
10798 * @param {string} value | |
10799 */ | |
10800 setValue: function(value) { | |
10801 // Use bindValue when setting the input value so that changes propagate | |
10802 // correctly. | |
10803 this.getSearchInput().bindValue = value; | |
10804 this.onValueChanged_(value); | |
10805 }, | |
10806 | |
10807 showAndFocus: function() { | |
10808 this.showingSearch = true; | |
10809 this.focus_(); | |
10810 }, | |
10811 | |
10812 /** @private */ | |
10813 focus_: function() { | |
10814 this.getSearchInput().focus(); | |
10815 }, | |
10816 | |
10817 onSearchTermSearch: function() { | |
10818 this.onValueChanged_(this.getValue()); | |
10819 }, | |
10820 | |
10821 /** | |
10822 * Updates the internal state of the search field based on a change that has | |
10823 * already happened. | |
10824 * @param {string} newValue | |
10825 * @private | 11507 * @private |
10826 */ | 11508 */ |
10827 onValueChanged_: function(newValue) { | 11509 setSearchedTextToBold_: function() { |
10828 if (newValue == this.lastValue_) | 11510 var i = 0; |
10829 return; | 11511 var titleElem = this.$.container; |
10830 | 11512 var titleText = this.title; |
10831 this.fire('search-changed', newValue); | 11513 |
10832 this.lastValue_ = newValue; | 11514 if (this.searchTerm == '' || this.searchTerm == null) { |
10833 }, | 11515 titleElem.textContent = titleText; |
10834 | |
10835 onSearchTermKeydown: function(e) { | |
10836 if (e.key == 'Escape') | |
10837 this.showingSearch = false; | |
10838 }, | |
10839 | |
10840 /** @private */ | |
10841 showingSearchChanged_: function() { | |
10842 if (this.showingSearch) { | |
10843 this.focus_(); | |
10844 return; | 11516 return; |
10845 } | 11517 } |
10846 | 11518 |
10847 this.setValue(''); | 11519 var re = new RegExp(quoteString(this.searchTerm), 'gim'); |
10848 this.getSearchInput().blur(); | 11520 var match; |
10849 } | 11521 titleElem.textContent = ''; |
| 11522 while (match = re.exec(titleText)) { |
| 11523 if (match.index > i) |
| 11524 titleElem.appendChild(document.createTextNode( |
| 11525 titleText.slice(i, match.index))); |
| 11526 i = re.lastIndex; |
| 11527 // Mark the highlighted text in bold. |
| 11528 var b = document.createElement('b'); |
| 11529 b.textContent = titleText.substring(match.index, i); |
| 11530 titleElem.appendChild(b); |
| 11531 } |
| 11532 if (i < titleText.length) |
| 11533 titleElem.appendChild( |
| 11534 document.createTextNode(titleText.slice(i))); |
| 11535 }, |
| 11536 }); |
| 11537 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 11538 // Use of this source code is governed by a BSD-style license that can be |
| 11539 // found in the LICENSE file. |
| 11540 |
| 11541 cr.define('md_history', function() { |
| 11542 var HistoryItem = Polymer({ |
| 11543 is: 'history-item', |
| 11544 |
| 11545 properties: { |
| 11546 // Underlying HistoryEntry data for this item. Contains read-only fields |
| 11547 // from the history backend, as well as fields computed by history-list. |
| 11548 item: {type: Object, observer: 'showIcon_'}, |
| 11549 |
| 11550 // Search term used to obtain this history-item. |
| 11551 searchTerm: {type: String}, |
| 11552 |
| 11553 selected: {type: Boolean, notify: true}, |
| 11554 |
| 11555 isFirstItem: {type: Boolean, reflectToAttribute: true}, |
| 11556 |
| 11557 isCardStart: {type: Boolean, reflectToAttribute: true}, |
| 11558 |
| 11559 isCardEnd: {type: Boolean, reflectToAttribute: true}, |
| 11560 |
| 11561 // True if the item is being displayed embedded in another element and |
| 11562 // should not manage its own borders or size. |
| 11563 embedded: {type: Boolean, reflectToAttribute: true}, |
| 11564 |
| 11565 hasTimeGap: {type: Boolean}, |
| 11566 |
| 11567 numberOfItems: {type: Number}, |
| 11568 |
| 11569 // The path of this history item inside its parent. |
| 11570 path: String, |
| 11571 }, |
| 11572 |
| 11573 /** |
| 11574 * When a history-item is selected the toolbar is notified and increases |
| 11575 * or decreases its count of selected items accordingly. |
| 11576 * @param {MouseEvent} e |
| 11577 * @private |
| 11578 */ |
| 11579 onCheckboxSelected_: function(e) { |
| 11580 // TODO(calamity): Fire this event whenever |selected| changes. |
| 11581 this.fire('history-checkbox-select', { |
| 11582 element: this, |
| 11583 shiftKey: e.shiftKey, |
| 11584 }); |
| 11585 e.preventDefault(); |
| 11586 }, |
| 11587 |
| 11588 /** |
| 11589 * @param {MouseEvent} e |
| 11590 * @private |
| 11591 */ |
| 11592 onCheckboxMousedown_: function(e) { |
| 11593 // Prevent shift clicking a checkbox from selecting text. |
| 11594 if (e.shiftKey) |
| 11595 e.preventDefault(); |
| 11596 }, |
| 11597 |
| 11598 /** |
| 11599 * Remove bookmark of current item when bookmark-star is clicked. |
| 11600 * @private |
| 11601 */ |
| 11602 onRemoveBookmarkTap_: function() { |
| 11603 if (!this.item.starred) |
| 11604 return; |
| 11605 |
| 11606 if (this.$$('#bookmark-star') == this.root.activeElement) |
| 11607 this.$['menu-button'].focus(); |
| 11608 |
| 11609 md_history.BrowserService.getInstance() |
| 11610 .removeBookmark(this.item.url); |
| 11611 this.fire('remove-bookmark-stars', this.item.url); |
| 11612 }, |
| 11613 |
| 11614 /** |
| 11615 * Fires a custom event when the menu button is clicked. Sends the details |
| 11616 * of the history item and where the menu should appear. |
| 11617 */ |
| 11618 onMenuButtonTap_: function(e) { |
| 11619 this.fire('toggle-menu', { |
| 11620 target: Polymer.dom(e).localTarget, |
| 11621 item: this.item, |
| 11622 path: this.path, |
| 11623 }); |
| 11624 |
| 11625 // Stops the 'tap' event from closing the menu when it opens. |
| 11626 e.stopPropagation(); |
| 11627 }, |
| 11628 |
| 11629 /** |
| 11630 * Set the favicon image, based on the URL of the history item. |
| 11631 * @private |
| 11632 */ |
| 11633 showIcon_: function() { |
| 11634 this.$.icon.style.backgroundImage = |
| 11635 cr.icon.getFaviconImageSet(this.item.url); |
| 11636 }, |
| 11637 |
| 11638 selectionNotAllowed_: function() { |
| 11639 return !loadTimeData.getBoolean('allowDeletingHistory'); |
| 11640 }, |
| 11641 |
| 11642 /** |
| 11643 * Generates the title for this history card. |
| 11644 * @param {number} numberOfItems The number of items in the card. |
| 11645 * @param {string} search The search term associated with these results. |
| 11646 * @private |
| 11647 */ |
| 11648 cardTitle_: function(numberOfItems, historyDate, search) { |
| 11649 if (!search) |
| 11650 return this.item.dateRelativeDay; |
| 11651 |
| 11652 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; |
| 11653 return loadTimeData.getStringF('foundSearchResults', numberOfItems, |
| 11654 loadTimeData.getString(resultId), search); |
| 11655 }, |
| 11656 |
| 11657 /** |
| 11658 * Crop long item titles to reduce their effect on layout performance. See |
| 11659 * crbug.com/621347. |
| 11660 * @param {string} title |
| 11661 * @return {string} |
| 11662 */ |
| 11663 cropItemTitle_: function(title) { |
| 11664 return (title.length > TITLE_MAX_LENGTH) ? |
| 11665 title.substr(0, TITLE_MAX_LENGTH) : |
| 11666 title; |
| 11667 } |
| 11668 }); |
| 11669 |
| 11670 /** |
| 11671 * Check whether the time difference between the given history item and the |
| 11672 * next one is large enough for a spacer to be required. |
| 11673 * @param {Array<HistoryEntry>} visits |
| 11674 * @param {number} currentIndex |
| 11675 * @param {string} searchedTerm |
| 11676 * @return {boolean} Whether or not time gap separator is required. |
| 11677 * @private |
| 11678 */ |
| 11679 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { |
| 11680 if (currentIndex >= visits.length - 1 || visits.length == 0) |
| 11681 return false; |
| 11682 |
| 11683 var currentItem = visits[currentIndex]; |
| 11684 var nextItem = visits[currentIndex + 1]; |
| 11685 |
| 11686 if (searchedTerm) |
| 11687 return currentItem.dateShort != nextItem.dateShort; |
| 11688 |
| 11689 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && |
| 11690 currentItem.dateRelativeDay == nextItem.dateRelativeDay; |
| 11691 }; |
| 11692 |
| 11693 return { HistoryItem: HistoryItem }; |
| 11694 }); |
| 11695 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11696 // Use of this source code is governed by a BSD-style license that can be |
| 11697 // found in the LICENSE file. |
| 11698 |
| 11699 /** |
| 11700 * @constructor |
| 11701 * @param {string} currentPath |
| 11702 */ |
| 11703 var SelectionTreeNode = function(currentPath) { |
| 11704 /** @type {string} */ |
| 11705 this.currentPath = currentPath; |
| 11706 /** @type {boolean} */ |
| 11707 this.leaf = false; |
| 11708 /** @type {Array<number>} */ |
| 11709 this.indexes = []; |
| 11710 /** @type {Array<SelectionTreeNode>} */ |
| 11711 this.children = []; |
10850 }; | 11712 }; |
| 11713 |
| 11714 /** |
| 11715 * @param {number} index |
| 11716 * @param {string} path |
| 11717 */ |
| 11718 SelectionTreeNode.prototype.addChild = function(index, path) { |
| 11719 this.indexes.push(index); |
| 11720 this.children[index] = new SelectionTreeNode(path); |
| 11721 }; |
| 11722 |
| 11723 /** @polymerBehavior */ |
| 11724 var HistoryListBehavior = { |
| 11725 properties: { |
| 11726 /** |
| 11727 * Polymer paths to the history items contained in this list. |
| 11728 * @type {!Set<string>} selectedPaths |
| 11729 */ |
| 11730 selectedPaths: { |
| 11731 type: Object, |
| 11732 value: /** @return {!Set<string>} */ function() { return new Set(); } |
| 11733 }, |
| 11734 |
| 11735 lastSelectedPath: String, |
| 11736 }, |
| 11737 |
| 11738 listeners: { |
| 11739 'history-checkbox-select': 'itemSelected_', |
| 11740 }, |
| 11741 |
| 11742 /** |
| 11743 * @param {number} historyDataLength |
| 11744 * @return {boolean} |
| 11745 * @private |
| 11746 */ |
| 11747 hasResults: function(historyDataLength) { return historyDataLength > 0; }, |
| 11748 |
| 11749 /** |
| 11750 * @param {string} searchedTerm |
| 11751 * @param {boolean} isLoading |
| 11752 * @return {string} |
| 11753 * @private |
| 11754 */ |
| 11755 noResultsMessage: function(searchedTerm, isLoading) { |
| 11756 if (isLoading) |
| 11757 return ''; |
| 11758 |
| 11759 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
| 11760 return loadTimeData.getString(messageId); |
| 11761 }, |
| 11762 |
| 11763 /** |
| 11764 * Deselect each item in |selectedPaths|. |
| 11765 */ |
| 11766 unselectAllItems: function() { |
| 11767 this.selectedPaths.forEach(function(path) { |
| 11768 this.set(path + '.selected', false); |
| 11769 }.bind(this)); |
| 11770 |
| 11771 this.selectedPaths.clear(); |
| 11772 }, |
| 11773 |
| 11774 /** |
| 11775 * Performs a request to the backend to delete all selected items. If |
| 11776 * successful, removes them from the view. Does not prompt the user before |
| 11777 * deleting -- see <history-list-container> for a version of this method which |
| 11778 * does prompt. |
| 11779 */ |
| 11780 deleteSelected: function() { |
| 11781 var toBeRemoved = |
| 11782 Array.from(this.selectedPaths.values()).map(function(path) { |
| 11783 return this.get(path); |
| 11784 }.bind(this)); |
| 11785 |
| 11786 md_history.BrowserService.getInstance() |
| 11787 .deleteItems(toBeRemoved) |
| 11788 .then(function() { |
| 11789 this.removeItemsByPath(Array.from(this.selectedPaths)); |
| 11790 this.fire('unselect-all'); |
| 11791 }.bind(this)); |
| 11792 }, |
| 11793 |
| 11794 /** |
| 11795 * Removes the history items in |paths|. Assumes paths are of a.0.b.0... |
| 11796 * structure. |
| 11797 * |
| 11798 * We want to use notifySplices to update the arrays for performance reasons |
| 11799 * which requires manually batching and sending the notifySplices for each |
| 11800 * level. To do this, we build a tree where each node is an array and then |
| 11801 * depth traverse it to remove items. Each time a node has all children |
| 11802 * deleted, we can also remove the node. |
| 11803 * |
| 11804 * @param {Array<string>} paths |
| 11805 * @private |
| 11806 */ |
| 11807 removeItemsByPath: function(paths) { |
| 11808 if (paths.length == 0) |
| 11809 return; |
| 11810 |
| 11811 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); |
| 11812 }, |
| 11813 |
| 11814 /** |
| 11815 * Creates the tree to traverse in order to remove |paths| from this list. |
| 11816 * Assumes paths are of a.0.b.0... |
| 11817 * structure. |
| 11818 * |
| 11819 * @param {Array<string>} paths |
| 11820 * @return {SelectionTreeNode} |
| 11821 * @private |
| 11822 */ |
| 11823 buildRemovalTree_: function(paths) { |
| 11824 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); |
| 11825 |
| 11826 // Build a tree to each history item specified in |paths|. |
| 11827 paths.forEach(function(path) { |
| 11828 var components = path.split('.'); |
| 11829 var node = rootNode; |
| 11830 components.shift(); |
| 11831 while (components.length > 1) { |
| 11832 var index = Number(components.shift()); |
| 11833 var arrayName = components.shift(); |
| 11834 |
| 11835 if (!node.children[index]) |
| 11836 node.addChild(index, [node.currentPath, index, arrayName].join('.')); |
| 11837 |
| 11838 node = node.children[index]; |
| 11839 } |
| 11840 node.leaf = true; |
| 11841 node.indexes.push(Number(components.shift())); |
| 11842 }); |
| 11843 |
| 11844 return rootNode; |
| 11845 }, |
| 11846 |
| 11847 /** |
| 11848 * Removes the history items underneath |node| and deletes container arrays as |
| 11849 * they become empty. |
| 11850 * @param {SelectionTreeNode} node |
| 11851 * @return {boolean} Whether this node's array should be deleted. |
| 11852 * @private |
| 11853 */ |
| 11854 removeItemsBeneathNode_: function(node) { |
| 11855 var array = this.get(node.currentPath); |
| 11856 var splices = []; |
| 11857 |
| 11858 node.indexes.sort(function(a, b) { return b - a; }); |
| 11859 node.indexes.forEach(function(index) { |
| 11860 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { |
| 11861 var item = array.splice(index, 1); |
| 11862 splices.push({ |
| 11863 index: index, |
| 11864 removed: [item], |
| 11865 addedCount: 0, |
| 11866 object: array, |
| 11867 type: 'splice' |
| 11868 }); |
| 11869 } |
| 11870 }.bind(this)); |
| 11871 |
| 11872 if (array.length == 0) |
| 11873 return true; |
| 11874 |
| 11875 // notifySplices gives better performance than individually splicing as it |
| 11876 // batches all of the updates together. |
| 11877 this.notifySplices(node.currentPath, splices); |
| 11878 return false; |
| 11879 }, |
| 11880 |
| 11881 /** |
| 11882 * @param {Event} e |
| 11883 * @private |
| 11884 */ |
| 11885 itemSelected_: function(e) { |
| 11886 var item = e.detail.element; |
| 11887 var paths = []; |
| 11888 var itemPath = item.path; |
| 11889 |
| 11890 // Handle shift selection. Change the selection state of all items between |
| 11891 // |path| and |lastSelected| to the selection state of |item|. |
| 11892 if (e.detail.shiftKey && this.lastSelectedPath) { |
| 11893 var itemPathComponents = itemPath.split('.'); |
| 11894 var itemIndex = Number(itemPathComponents.pop()); |
| 11895 var itemArrayPath = itemPathComponents.join('.'); |
| 11896 |
| 11897 var lastItemPathComponents = this.lastSelectedPath.split('.'); |
| 11898 var lastItemIndex = Number(lastItemPathComponents.pop()); |
| 11899 if (itemArrayPath == lastItemPathComponents.join('.')) { |
| 11900 for (var i = Math.min(itemIndex, lastItemIndex); |
| 11901 i <= Math.max(itemIndex, lastItemIndex); i++) { |
| 11902 paths.push(itemArrayPath + '.' + i); |
| 11903 } |
| 11904 } |
| 11905 } |
| 11906 |
| 11907 if (paths.length == 0) |
| 11908 paths.push(item.path); |
| 11909 |
| 11910 paths.forEach(function(path) { |
| 11911 this.set(path + '.selected', item.selected); |
| 11912 |
| 11913 if (item.selected) { |
| 11914 this.selectedPaths.add(path); |
| 11915 return; |
| 11916 } |
| 11917 |
| 11918 this.selectedPaths.delete(path); |
| 11919 }.bind(this)); |
| 11920 |
| 11921 this.lastSelectedPath = itemPath; |
| 11922 }, |
| 11923 }; |
| 11924 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 11925 // Use of this source code is governed by a BSD-style license that can be |
| 11926 // found in the LICENSE file. |
| 11927 |
| 11928 /** |
| 11929 * @typedef {{domain: string, |
| 11930 * visits: !Array<HistoryEntry>, |
| 11931 * rendered: boolean, |
| 11932 * expanded: boolean}} |
| 11933 */ |
| 11934 var HistoryDomain; |
| 11935 |
| 11936 /** |
| 11937 * @typedef {{title: string, |
| 11938 * domains: !Array<HistoryDomain>}} |
| 11939 */ |
| 11940 var HistoryGroup; |
| 11941 |
| 11942 Polymer({ |
| 11943 is: 'history-grouped-list', |
| 11944 |
| 11945 behaviors: [HistoryListBehavior], |
| 11946 |
| 11947 properties: { |
| 11948 // An array of history entries in reverse chronological order. |
| 11949 historyData: { |
| 11950 type: Array, |
| 11951 }, |
| 11952 |
| 11953 /** |
| 11954 * @type {Array<HistoryGroup>} |
| 11955 */ |
| 11956 groupedHistoryData_: { |
| 11957 type: Array, |
| 11958 }, |
| 11959 |
| 11960 searchedTerm: { |
| 11961 type: String, |
| 11962 value: '' |
| 11963 }, |
| 11964 |
| 11965 range: { |
| 11966 type: Number, |
| 11967 }, |
| 11968 |
| 11969 queryStartTime: String, |
| 11970 queryEndTime: String, |
| 11971 }, |
| 11972 |
| 11973 observers: [ |
| 11974 'updateGroupedHistoryData_(range, historyData)' |
| 11975 ], |
| 11976 |
| 11977 /** |
| 11978 * Make a list of domains from visits. |
| 11979 * @param {!Array<!HistoryEntry>} visits |
| 11980 * @return {!Array<!HistoryDomain>} |
| 11981 */ |
| 11982 createHistoryDomains_: function(visits) { |
| 11983 var domainIndexes = {}; |
| 11984 var domains = []; |
| 11985 |
| 11986 // Group the visits into a dictionary and generate a list of domains. |
| 11987 for (var i = 0, visit; visit = visits[i]; i++) { |
| 11988 var domain = visit.domain; |
| 11989 if (domainIndexes[domain] == undefined) { |
| 11990 domainIndexes[domain] = domains.length; |
| 11991 domains.push({ |
| 11992 domain: domain, |
| 11993 visits: [], |
| 11994 expanded: false, |
| 11995 rendered: false, |
| 11996 }); |
| 11997 } |
| 11998 domains[domainIndexes[domain]].visits.push(visit); |
| 11999 } |
| 12000 var sortByVisits = function(a, b) { |
| 12001 return b.visits.length - a.visits.length; |
| 12002 }; |
| 12003 domains.sort(sortByVisits); |
| 12004 |
| 12005 return domains; |
| 12006 }, |
| 12007 |
| 12008 updateGroupedHistoryData_: function() { |
| 12009 if (this.historyData.length == 0) { |
| 12010 this.groupedHistoryData_ = []; |
| 12011 return; |
| 12012 } |
| 12013 |
| 12014 if (this.range == HistoryRange.WEEK) { |
| 12015 // Group each day into a list of results. |
| 12016 var days = []; |
| 12017 var currentDayVisits = [this.historyData[0]]; |
| 12018 |
| 12019 var pushCurrentDay = function() { |
| 12020 days.push({ |
| 12021 title: this.searchedTerm ? currentDayVisits[0].dateShort : |
| 12022 currentDayVisits[0].dateRelativeDay, |
| 12023 domains: this.createHistoryDomains_(currentDayVisits), |
| 12024 }); |
| 12025 }.bind(this); |
| 12026 |
| 12027 var visitsSameDay = function(a, b) { |
| 12028 if (this.searchedTerm) |
| 12029 return a.dateShort == b.dateShort; |
| 12030 |
| 12031 return a.dateRelativeDay == b.dateRelativeDay; |
| 12032 }.bind(this); |
| 12033 |
| 12034 for (var i = 1; i < this.historyData.length; i++) { |
| 12035 var visit = this.historyData[i]; |
| 12036 if (!visitsSameDay(visit, currentDayVisits[0])) { |
| 12037 pushCurrentDay(); |
| 12038 currentDayVisits = []; |
| 12039 } |
| 12040 currentDayVisits.push(visit); |
| 12041 } |
| 12042 pushCurrentDay(); |
| 12043 |
| 12044 this.groupedHistoryData_ = days; |
| 12045 } else if (this.range == HistoryRange.MONTH) { |
| 12046 // Group each all visits into a single list. |
| 12047 this.groupedHistoryData_ = [{ |
| 12048 title: this.queryStartTime + ' – ' + this.queryEndTime, |
| 12049 domains: this.createHistoryDomains_(this.historyData) |
| 12050 }]; |
| 12051 } |
| 12052 }, |
| 12053 |
| 12054 /** |
| 12055 * @param {{model:Object, currentTarget:IronCollapseElement}} e |
| 12056 */ |
| 12057 toggleDomainExpanded_: function(e) { |
| 12058 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); |
| 12059 e.model.set('domain.rendered', true); |
| 12060 |
| 12061 // Give the history-items time to render. |
| 12062 setTimeout(function() { collapse.toggle() }, 0); |
| 12063 }, |
| 12064 |
| 12065 /** |
| 12066 * Check whether the time difference between the given history item and the |
| 12067 * next one is large enough for a spacer to be required. |
| 12068 * @param {number} groupIndex |
| 12069 * @param {number} domainIndex |
| 12070 * @param {number} itemIndex |
| 12071 * @return {boolean} Whether or not time gap separator is required. |
| 12072 * @private |
| 12073 */ |
| 12074 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { |
| 12075 var visits = |
| 12076 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; |
| 12077 |
| 12078 return md_history.HistoryItem.needsTimeGap( |
| 12079 visits, itemIndex, this.searchedTerm); |
| 12080 }, |
| 12081 |
| 12082 /** |
| 12083 * @param {number} groupIndex |
| 12084 * @param {number} domainIndex |
| 12085 * @param {number} itemIndex |
| 12086 * @return {string} |
| 12087 * @private |
| 12088 */ |
| 12089 pathForItem_: function(groupIndex, domainIndex, itemIndex) { |
| 12090 return [ |
| 12091 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', |
| 12092 itemIndex |
| 12093 ].join('.'); |
| 12094 }, |
| 12095 |
| 12096 /** |
| 12097 * @param {HistoryDomain} domain |
| 12098 * @return {string} |
| 12099 * @private |
| 12100 */ |
| 12101 getWebsiteIconStyle_: function(domain) { |
| 12102 return 'background-image: ' + |
| 12103 cr.icon.getFaviconImageSet(domain.visits[0].url); |
| 12104 }, |
| 12105 |
| 12106 /** |
| 12107 * @param {boolean} expanded |
| 12108 * @return {string} |
| 12109 * @private |
| 12110 */ |
| 12111 getDropdownIcon_: function(expanded) { |
| 12112 return expanded ? 'cr:expand-less' : 'cr:expand-more'; |
| 12113 }, |
| 12114 }); |
| 12115 /** |
| 12116 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a |
| 12117 * designated scroll target. |
| 12118 * |
| 12119 * Elements that consume this behavior can override the `_scrollHandler` |
| 12120 * method to add logic on the scroll event. |
| 12121 * |
| 12122 * @demo demo/scrolling-region.html Scrolling Region |
| 12123 * @demo demo/document.html Document Element |
| 12124 * @polymerBehavior |
| 12125 */ |
| 12126 Polymer.IronScrollTargetBehavior = { |
| 12127 |
| 12128 properties: { |
| 12129 |
| 12130 /** |
| 12131 * Specifies the element that will handle the scroll event |
| 12132 * on the behalf of the current element. This is typically a reference to
an element, |
| 12133 * but there are a few more posibilities: |
| 12134 * |
| 12135 * ### Elements id |
| 12136 * |
| 12137 *```html |
| 12138 * <div id="scrollable-element" style="overflow: auto;"> |
| 12139 * <x-element scroll-target="scrollable-element"> |
| 12140 * \x3c!-- Content--\x3e |
| 12141 * </x-element> |
| 12142 * </div> |
| 12143 *``` |
| 12144 * In this case, the `scrollTarget` will point to the outer div element. |
| 12145 * |
| 12146 * ### Document scrolling |
| 12147 * |
| 12148 * For document scrolling, you can use the reserved word `document`: |
| 12149 * |
| 12150 *```html |
| 12151 * <x-element scroll-target="document"> |
| 12152 * \x3c!-- Content --\x3e |
| 12153 * </x-element> |
| 12154 *``` |
| 12155 * |
| 12156 * ### Elements reference |
| 12157 * |
| 12158 *```js |
| 12159 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); |
| 12160 *``` |
| 12161 * |
| 12162 * @type {HTMLElement} |
| 12163 */ |
| 12164 scrollTarget: { |
| 12165 type: HTMLElement, |
| 12166 value: function() { |
| 12167 return this._defaultScrollTarget; |
| 12168 } |
| 12169 } |
| 12170 }, |
| 12171 |
| 12172 observers: [ |
| 12173 '_scrollTargetChanged(scrollTarget, isAttached)' |
| 12174 ], |
| 12175 |
| 12176 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 12177 var eventTarget; |
| 12178 |
| 12179 if (this._oldScrollTarget) { |
| 12180 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; |
| 12181 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 12182 this._oldScrollTarget = null; |
| 12183 } |
| 12184 |
| 12185 if (!isAttached) { |
| 12186 return; |
| 12187 } |
| 12188 // Support element id references |
| 12189 if (scrollTarget === 'document') { |
| 12190 |
| 12191 this.scrollTarget = this._doc; |
| 12192 |
| 12193 } else if (typeof scrollTarget === 'string') { |
| 12194 |
| 12195 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : |
| 12196 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 12197 |
| 12198 } else if (this._isValidScrollTarget()) { |
| 12199 |
| 12200 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 12201 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); |
| 12202 this._oldScrollTarget = scrollTarget; |
| 12203 |
| 12204 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 12205 } |
| 12206 }, |
| 12207 |
| 12208 /** |
| 12209 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. |
| 12210 * |
| 12211 * @protected |
| 12212 */ |
| 12213 _scrollHandler: function scrollHandler() {}, |
| 12214 |
| 12215 /** |
| 12216 * The default scroll target. Consumers of this behavior may want to customi
ze |
| 12217 * the default scroll target. |
| 12218 * |
| 12219 * @type {Element} |
| 12220 */ |
| 12221 get _defaultScrollTarget() { |
| 12222 return this._doc; |
| 12223 }, |
| 12224 |
| 12225 /** |
| 12226 * Shortcut for the document element |
| 12227 * |
| 12228 * @type {Element} |
| 12229 */ |
| 12230 get _doc() { |
| 12231 return this.ownerDocument.documentElement; |
| 12232 }, |
| 12233 |
| 12234 /** |
| 12235 * Gets the number of pixels that the content of an element is scrolled upwa
rd. |
| 12236 * |
| 12237 * @type {number} |
| 12238 */ |
| 12239 get _scrollTop() { |
| 12240 if (this._isValidScrollTarget()) { |
| 12241 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; |
| 12242 } |
| 12243 return 0; |
| 12244 }, |
| 12245 |
| 12246 /** |
| 12247 * Gets the number of pixels that the content of an element is scrolled to t
he left. |
| 12248 * |
| 12249 * @type {number} |
| 12250 */ |
| 12251 get _scrollLeft() { |
| 12252 if (this._isValidScrollTarget()) { |
| 12253 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; |
| 12254 } |
| 12255 return 0; |
| 12256 }, |
| 12257 |
| 12258 /** |
| 12259 * Sets the number of pixels that the content of an element is scrolled upwa
rd. |
| 12260 * |
| 12261 * @type {number} |
| 12262 */ |
| 12263 set _scrollTop(top) { |
| 12264 if (this.scrollTarget === this._doc) { |
| 12265 window.scrollTo(window.pageXOffset, top); |
| 12266 } else if (this._isValidScrollTarget()) { |
| 12267 this.scrollTarget.scrollTop = top; |
| 12268 } |
| 12269 }, |
| 12270 |
| 12271 /** |
| 12272 * Sets the number of pixels that the content of an element is scrolled to t
he left. |
| 12273 * |
| 12274 * @type {number} |
| 12275 */ |
| 12276 set _scrollLeft(left) { |
| 12277 if (this.scrollTarget === this._doc) { |
| 12278 window.scrollTo(left, window.pageYOffset); |
| 12279 } else if (this._isValidScrollTarget()) { |
| 12280 this.scrollTarget.scrollLeft = left; |
| 12281 } |
| 12282 }, |
| 12283 |
| 12284 /** |
| 12285 * Scrolls the content to a particular place. |
| 12286 * |
| 12287 * @method scroll |
| 12288 * @param {number} left The left position |
| 12289 * @param {number} top The top position |
| 12290 */ |
| 12291 scroll: function(left, top) { |
| 12292 if (this.scrollTarget === this._doc) { |
| 12293 window.scrollTo(left, top); |
| 12294 } else if (this._isValidScrollTarget()) { |
| 12295 this.scrollTarget.scrollLeft = left; |
| 12296 this.scrollTarget.scrollTop = top; |
| 12297 } |
| 12298 }, |
| 12299 |
| 12300 /** |
| 12301 * Gets the width of the scroll target. |
| 12302 * |
| 12303 * @type {number} |
| 12304 */ |
| 12305 get _scrollTargetWidth() { |
| 12306 if (this._isValidScrollTarget()) { |
| 12307 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; |
| 12308 } |
| 12309 return 0; |
| 12310 }, |
| 12311 |
| 12312 /** |
| 12313 * Gets the height of the scroll target. |
| 12314 * |
| 12315 * @type {number} |
| 12316 */ |
| 12317 get _scrollTargetHeight() { |
| 12318 if (this._isValidScrollTarget()) { |
| 12319 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; |
| 12320 } |
| 12321 return 0; |
| 12322 }, |
| 12323 |
| 12324 /** |
| 12325 * Returns true if the scroll target is a valid HTMLElement. |
| 12326 * |
| 12327 * @return {boolean} |
| 12328 */ |
| 12329 _isValidScrollTarget: function() { |
| 12330 return this.scrollTarget instanceof HTMLElement; |
| 12331 } |
| 12332 }; |
10851 (function() { | 12333 (function() { |
10852 'use strict'; | 12334 |
10853 | 12335 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
10854 Polymer.IronA11yAnnouncer = Polymer({ | 12336 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
10855 is: 'iron-a11y-announcer', | 12337 var DEFAULT_PHYSICAL_COUNT = 3; |
10856 | 12338 var HIDDEN_Y = '-10000px'; |
10857 properties: { | 12339 var DEFAULT_GRID_SIZE = 200; |
10858 | 12340 var SECRET_TABINDEX = -100; |
10859 /** | 12341 |
10860 * The value of mode is used to set the `aria-live` attribute | 12342 Polymer({ |
10861 * for the element that will be announced. Valid values are: `off`, | 12343 |
10862 * `polite` and `assertive`. | 12344 is: 'iron-list', |
10863 */ | |
10864 mode: { | |
10865 type: String, | |
10866 value: 'polite' | |
10867 }, | |
10868 | |
10869 _text: { | |
10870 type: String, | |
10871 value: '' | |
10872 } | |
10873 }, | |
10874 | |
10875 created: function() { | |
10876 if (!Polymer.IronA11yAnnouncer.instance) { | |
10877 Polymer.IronA11yAnnouncer.instance = this; | |
10878 } | |
10879 | |
10880 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
10881 }, | |
10882 | |
10883 /** | |
10884 * Cause a text string to be announced by screen readers. | |
10885 * | |
10886 * @param {string} text The text that should be announced. | |
10887 */ | |
10888 announce: function(text) { | |
10889 this._text = ''; | |
10890 this.async(function() { | |
10891 this._text = text; | |
10892 }, 100); | |
10893 }, | |
10894 | |
10895 _onIronAnnounce: function(event) { | |
10896 if (event.detail && event.detail.text) { | |
10897 this.announce(event.detail.text); | |
10898 } | |
10899 } | |
10900 }); | |
10901 | |
10902 Polymer.IronA11yAnnouncer.instance = null; | |
10903 | |
10904 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
10905 if (!Polymer.IronA11yAnnouncer.instance) { | |
10906 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
10907 } | |
10908 | |
10909 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
10910 }; | |
10911 })(); | |
10912 /** | |
10913 * Singleton IronMeta instance. | |
10914 */ | |
10915 Polymer.IronValidatableBehaviorMeta = null; | |
10916 | |
10917 /** | |
10918 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
10919 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
10920 * | |
10921 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
10922 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
10923 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
10924 * custom element will get a public `validate()`, which | |
10925 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
10926 * which can be used for styling. | |
10927 * | |
10928 * To implement the custom validation logic of your element, you must override | |
10929 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
10930 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
10931 * for an example. | |
10932 * | |
10933 * ### Accessibility | |
10934 * | |
10935 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
10936 * `aria-invalid` attribute. | |
10937 * | |
10938 * @demo demo/index.html | |
10939 * @polymerBehavior | |
10940 */ | |
10941 Polymer.IronValidatableBehavior = { | |
10942 | 12345 |
10943 properties: { | 12346 properties: { |
10944 | 12347 |
10945 /** | 12348 /** |
10946 * Name of the validator to use. | 12349 * An array containing items determining how many instances of the templat
e |
| 12350 * to stamp and that that each template instance should bind to. |
10947 */ | 12351 */ |
10948 validator: { | 12352 items: { |
10949 type: String | 12353 type: Array |
10950 }, | 12354 }, |
10951 | 12355 |
10952 /** | 12356 /** |
10953 * True if the last call to `validate` is invalid. | 12357 * The max count of physical items the pool can extend to. |
10954 */ | 12358 */ |
10955 invalid: { | 12359 maxPhysicalCount: { |
10956 notify: true, | 12360 type: Number, |
10957 reflectToAttribute: true, | 12361 value: 500 |
| 12362 }, |
| 12363 |
| 12364 /** |
| 12365 * The name of the variable to add to the binding scope for the array |
| 12366 * element associated with a given template instance. |
| 12367 */ |
| 12368 as: { |
| 12369 type: String, |
| 12370 value: 'item' |
| 12371 }, |
| 12372 |
| 12373 /** |
| 12374 * The name of the variable to add to the binding scope with the index |
| 12375 * for the row. |
| 12376 */ |
| 12377 indexAs: { |
| 12378 type: String, |
| 12379 value: 'index' |
| 12380 }, |
| 12381 |
| 12382 /** |
| 12383 * The name of the variable to add to the binding scope to indicate |
| 12384 * if the row is selected. |
| 12385 */ |
| 12386 selectedAs: { |
| 12387 type: String, |
| 12388 value: 'selected' |
| 12389 }, |
| 12390 |
| 12391 /** |
| 12392 * When true, the list is rendered as a grid. Grid items must have |
| 12393 * fixed width and height set via CSS. e.g. |
| 12394 * |
| 12395 * ```html |
| 12396 * <iron-list grid> |
| 12397 * <template> |
| 12398 * <div style="width: 100px; height: 100px;"> 100x100 </div> |
| 12399 * </template> |
| 12400 * </iron-list> |
| 12401 * ``` |
| 12402 */ |
| 12403 grid: { |
| 12404 type: Boolean, |
| 12405 value: false, |
| 12406 reflectToAttribute: true |
| 12407 }, |
| 12408 |
| 12409 /** |
| 12410 * When true, tapping a row will select the item, placing its data model |
| 12411 * in the set of selected items retrievable via the selection property. |
| 12412 * |
| 12413 * Note that tapping focusable elements within the list item will not |
| 12414 * result in selection, since they are presumed to have their * own action
. |
| 12415 */ |
| 12416 selectionEnabled: { |
10958 type: Boolean, | 12417 type: Boolean, |
10959 value: false | 12418 value: false |
10960 }, | 12419 }, |
10961 | 12420 |
10962 /** | 12421 /** |
10963 * This property is deprecated and should not be used. Use the global | 12422 * When `multiSelection` is false, this is the currently selected item, or
`null` |
10964 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | 12423 * if no item is selected. |
10965 */ | 12424 */ |
10966 _validatorMeta: { | 12425 selectedItem: { |
10967 type: Object | 12426 type: Object, |
| 12427 notify: true |
10968 }, | 12428 }, |
10969 | 12429 |
10970 /** | 12430 /** |
10971 * Namespace for this validator. This property is deprecated and should | 12431 * When `multiSelection` is true, this is an array that contains the selec
ted items. |
10972 * not be used. For all intents and purposes, please consider it a | |
10973 * read-only, config-time property. | |
10974 */ | 12432 */ |
10975 validatorType: { | 12433 selectedItems: { |
10976 type: String, | |
10977 value: 'validator' | |
10978 }, | |
10979 | |
10980 _validator: { | |
10981 type: Object, | 12434 type: Object, |
10982 computed: '__computeValidator(validator)' | 12435 notify: true |
10983 } | 12436 }, |
10984 }, | |
10985 | |
10986 observers: [ | |
10987 '_invalidChanged(invalid)' | |
10988 ], | |
10989 | |
10990 registered: function() { | |
10991 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
10992 }, | |
10993 | |
10994 _invalidChanged: function() { | |
10995 if (this.invalid) { | |
10996 this.setAttribute('aria-invalid', 'true'); | |
10997 } else { | |
10998 this.removeAttribute('aria-invalid'); | |
10999 } | |
11000 }, | |
11001 | |
11002 /** | |
11003 * @return {boolean} True if the validator `validator` exists. | |
11004 */ | |
11005 hasValidator: function() { | |
11006 return this._validator != null; | |
11007 }, | |
11008 | |
11009 /** | |
11010 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
11011 * your element to have custom validation logic, do not override this method
; | |
11012 * override `_getValidity(value)` instead. | |
11013 | |
11014 * @param {Object} value The value to be validated. By default, it is passed | |
11015 * to the validator's `validate()` function, if a validator is set. | |
11016 * @return {boolean} True if `value` is valid. | |
11017 */ | |
11018 validate: function(value) { | |
11019 this.invalid = !this._getValidity(value); | |
11020 return !this.invalid; | |
11021 }, | |
11022 | |
11023 /** | |
11024 * Returns true if `value` is valid. By default, it is passed | |
11025 * to the validator's `validate()` function, if a validator is set. You | |
11026 * should override this method if you want to implement custom validity | |
11027 * logic for your element. | |
11028 * | |
11029 * @param {Object} value The value to be validated. | |
11030 * @return {boolean} True if `value` is valid. | |
11031 */ | |
11032 | |
11033 _getValidity: function(value) { | |
11034 if (this.hasValidator()) { | |
11035 return this._validator.validate(value); | |
11036 } | |
11037 return true; | |
11038 }, | |
11039 | |
11040 __computeValidator: function() { | |
11041 return Polymer.IronValidatableBehaviorMeta && | |
11042 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
11043 } | |
11044 }; | |
11045 /* | |
11046 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
11047 to `<input>`. | |
11048 | |
11049 ### Two-way binding | |
11050 | |
11051 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
11052 | |
11053 <input value="{{myValue::input}}"> | |
11054 | |
11055 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
11056 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
11057 | |
11058 <input is="iron-input" bind-value="{{myValue}}"> | |
11059 | |
11060 ### Custom validators | |
11061 | |
11062 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
11063 | |
11064 <input is="iron-input" validator="my-custom-validator"> | |
11065 | |
11066 ### Stopping invalid input | |
11067 | |
11068 It may be desirable to only allow users to enter certain characters. You can use
the | |
11069 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
11070 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
11071 | |
11072 \x3c!-- only allow characters that match [0-9] --\x3e | |
11073 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
11074 | |
11075 @hero hero.svg | |
11076 @demo demo/index.html | |
11077 */ | |
11078 | |
11079 Polymer({ | |
11080 | |
11081 is: 'iron-input', | |
11082 | |
11083 extends: 'input', | |
11084 | |
11085 behaviors: [ | |
11086 Polymer.IronValidatableBehavior | |
11087 ], | |
11088 | |
11089 properties: { | |
11090 | 12437 |
11091 /** | 12438 /** |
11092 * Use this property instead of `value` for two-way data binding. | 12439 * When `true`, multiple items may be selected at once (in this case, |
| 12440 * `selected` is an array of currently selected items). When `false`, |
| 12441 * only one item may be selected at a time. |
11093 */ | 12442 */ |
11094 bindValue: { | 12443 multiSelection: { |
11095 observer: '_bindValueChanged', | |
11096 type: String | |
11097 }, | |
11098 | |
11099 /** | |
11100 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
11101 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
11102 * Pasted input will have each character checked individually; if any char
acter | |
11103 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
11104 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
11105 */ | |
11106 preventInvalidInput: { | |
11107 type: Boolean | |
11108 }, | |
11109 | |
11110 /** | |
11111 * Regular expression that list the characters allowed as input. | |
11112 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
11113 * each individual character will be checked against the pattern (rather t
han checking | |
11114 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
11115 * for example, `[a-zA-Z0-9.+-!;:]` | |
11116 */ | |
11117 allowedPattern: { | |
11118 type: String, | |
11119 observer: "_allowedPatternChanged" | |
11120 }, | |
11121 | |
11122 _previousValidInput: { | |
11123 type: String, | |
11124 value: '' | |
11125 }, | |
11126 | |
11127 _patternAlreadyChecked: { | |
11128 type: Boolean, | 12444 type: Boolean, |
11129 value: false | 12445 value: false |
11130 } | 12446 } |
11131 | 12447 }, |
11132 }, | 12448 |
11133 | 12449 observers: [ |
11134 listeners: { | 12450 '_itemsChanged(items.*)', |
11135 'input': '_onInput', | 12451 '_selectionEnabledChanged(selectionEnabled)', |
11136 'keypress': '_onKeypress' | 12452 '_multiSelectionChanged(multiSelection)', |
11137 }, | 12453 '_setOverflow(scrollTarget)' |
11138 | 12454 ], |
11139 /** @suppress {checkTypes} */ | 12455 |
11140 registered: function() { | 12456 behaviors: [ |
11141 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | 12457 Polymer.Templatizer, |
11142 if (!this._canDispatchEventOnDisabled()) { | 12458 Polymer.IronResizableBehavior, |
11143 this._origDispatchEvent = this.dispatchEvent; | 12459 Polymer.IronA11yKeysBehavior, |
11144 this.dispatchEvent = this._dispatchEventFirefoxIE; | 12460 Polymer.IronScrollTargetBehavior |
11145 } | 12461 ], |
11146 }, | 12462 |
11147 | 12463 keyBindings: { |
11148 created: function() { | 12464 'up': '_didMoveUp', |
11149 Polymer.IronA11yAnnouncer.requestAvailability(); | 12465 'down': '_didMoveDown', |
11150 }, | 12466 'enter': '_didEnter' |
11151 | 12467 }, |
11152 _canDispatchEventOnDisabled: function() { | 12468 |
11153 var input = document.createElement('input'); | 12469 /** |
11154 var canDispatch = false; | 12470 * The ratio of hidden tiles that should remain in the scroll direction. |
11155 input.disabled = true; | 12471 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. |
11156 | 12472 */ |
11157 input.addEventListener('feature-check-dispatch-event', function() { | 12473 _ratio: 0.5, |
11158 canDispatch = true; | 12474 |
11159 }); | 12475 /** |
11160 | 12476 * The padding-top value for the list. |
11161 try { | 12477 */ |
11162 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 12478 _scrollerPaddingTop: 0, |
11163 } catch(e) {} | 12479 |
11164 | 12480 /** |
11165 return canDispatch; | 12481 * This value is the same as `scrollTop`. |
11166 }, | 12482 */ |
11167 | 12483 _scrollPosition: 0, |
11168 _dispatchEventFirefoxIE: function() { | 12484 |
11169 // Due to Firefox bug, events fired on disabled form controls can throw | 12485 /** |
11170 // errors; furthermore, neither IE nor Firefox will actually dispatch | 12486 * The sum of the heights of all the tiles in the DOM. |
11171 // events from disabled form controls; as such, we toggle disable around | 12487 */ |
11172 // the dispatch to allow notifying properties to notify | 12488 _physicalSize: 0, |
11173 // See issue #47 for details | 12489 |
11174 var disabled = this.disabled; | 12490 /** |
11175 this.disabled = false; | 12491 * The average `offsetHeight` of the tiles observed till now. |
11176 this._origDispatchEvent.apply(this, arguments); | 12492 */ |
11177 this.disabled = disabled; | 12493 _physicalAverage: 0, |
11178 }, | 12494 |
11179 | 12495 /** |
11180 get _patternRegExp() { | 12496 * The number of tiles which `offsetHeight` > 0 observed until now. |
11181 var pattern; | 12497 */ |
11182 if (this.allowedPattern) { | 12498 _physicalAverageCount: 0, |
11183 pattern = new RegExp(this.allowedPattern); | 12499 |
| 12500 /** |
| 12501 * The Y position of the item rendered in the `_physicalStart` |
| 12502 * tile relative to the scrolling list. |
| 12503 */ |
| 12504 _physicalTop: 0, |
| 12505 |
| 12506 /** |
| 12507 * The number of items in the list. |
| 12508 */ |
| 12509 _virtualCount: 0, |
| 12510 |
| 12511 /** |
| 12512 * A map between an item key and its physical item index |
| 12513 */ |
| 12514 _physicalIndexForKey: null, |
| 12515 |
| 12516 /** |
| 12517 * The estimated scroll height based on `_physicalAverage` |
| 12518 */ |
| 12519 _estScrollHeight: 0, |
| 12520 |
| 12521 /** |
| 12522 * The scroll height of the dom node |
| 12523 */ |
| 12524 _scrollHeight: 0, |
| 12525 |
| 12526 /** |
| 12527 * The height of the list. This is referred as the viewport in the context o
f list. |
| 12528 */ |
| 12529 _viewportHeight: 0, |
| 12530 |
| 12531 /** |
| 12532 * The width of the list. This is referred as the viewport in the context of
list. |
| 12533 */ |
| 12534 _viewportWidth: 0, |
| 12535 |
| 12536 /** |
| 12537 * An array of DOM nodes that are currently in the tree |
| 12538 * @type {?Array<!TemplatizerNode>} |
| 12539 */ |
| 12540 _physicalItems: null, |
| 12541 |
| 12542 /** |
| 12543 * An array of heights for each item in `_physicalItems` |
| 12544 * @type {?Array<number>} |
| 12545 */ |
| 12546 _physicalSizes: null, |
| 12547 |
| 12548 /** |
| 12549 * A cached value for the first visible index. |
| 12550 * See `firstVisibleIndex` |
| 12551 * @type {?number} |
| 12552 */ |
| 12553 _firstVisibleIndexVal: null, |
| 12554 |
| 12555 /** |
| 12556 * A cached value for the last visible index. |
| 12557 * See `lastVisibleIndex` |
| 12558 * @type {?number} |
| 12559 */ |
| 12560 _lastVisibleIndexVal: null, |
| 12561 |
| 12562 /** |
| 12563 * A Polymer collection for the items. |
| 12564 * @type {?Polymer.Collection} |
| 12565 */ |
| 12566 _collection: null, |
| 12567 |
| 12568 /** |
| 12569 * True if the current item list was rendered for the first time |
| 12570 * after attached. |
| 12571 */ |
| 12572 _itemsRendered: false, |
| 12573 |
| 12574 /** |
| 12575 * The page that is currently rendered. |
| 12576 */ |
| 12577 _lastPage: null, |
| 12578 |
| 12579 /** |
| 12580 * The max number of pages to render. One page is equivalent to the height o
f the list. |
| 12581 */ |
| 12582 _maxPages: 3, |
| 12583 |
| 12584 /** |
| 12585 * The currently focused physical item. |
| 12586 */ |
| 12587 _focusedItem: null, |
| 12588 |
| 12589 /** |
| 12590 * The index of the `_focusedItem`. |
| 12591 */ |
| 12592 _focusedIndex: -1, |
| 12593 |
| 12594 /** |
| 12595 * The the item that is focused if it is moved offscreen. |
| 12596 * @private {?TemplatizerNode} |
| 12597 */ |
| 12598 _offscreenFocusedItem: null, |
| 12599 |
| 12600 /** |
| 12601 * The item that backfills the `_offscreenFocusedItem` in the physical items |
| 12602 * list when that item is moved offscreen. |
| 12603 */ |
| 12604 _focusBackfillItem: null, |
| 12605 |
| 12606 /** |
| 12607 * The maximum items per row |
| 12608 */ |
| 12609 _itemsPerRow: 1, |
| 12610 |
| 12611 /** |
| 12612 * The width of each grid item |
| 12613 */ |
| 12614 _itemWidth: 0, |
| 12615 |
| 12616 /** |
| 12617 * The height of the row in grid layout. |
| 12618 */ |
| 12619 _rowHeight: 0, |
| 12620 |
| 12621 /** |
| 12622 * The bottom of the physical content. |
| 12623 */ |
| 12624 get _physicalBottom() { |
| 12625 return this._physicalTop + this._physicalSize; |
| 12626 }, |
| 12627 |
| 12628 /** |
| 12629 * The bottom of the scroll. |
| 12630 */ |
| 12631 get _scrollBottom() { |
| 12632 return this._scrollPosition + this._viewportHeight; |
| 12633 }, |
| 12634 |
| 12635 /** |
| 12636 * The n-th item rendered in the last physical item. |
| 12637 */ |
| 12638 get _virtualEnd() { |
| 12639 return this._virtualStart + this._physicalCount - 1; |
| 12640 }, |
| 12641 |
| 12642 /** |
| 12643 * The height of the physical content that isn't on the screen. |
| 12644 */ |
| 12645 get _hiddenContentSize() { |
| 12646 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
| 12647 return size - this._viewportHeight; |
| 12648 }, |
| 12649 |
| 12650 /** |
| 12651 * The maximum scroll top value. |
| 12652 */ |
| 12653 get _maxScrollTop() { |
| 12654 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
| 12655 }, |
| 12656 |
| 12657 /** |
| 12658 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. |
| 12659 */ |
| 12660 _minVirtualStart: 0, |
| 12661 |
| 12662 /** |
| 12663 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. |
| 12664 */ |
| 12665 get _maxVirtualStart() { |
| 12666 return Math.max(0, this._virtualCount - this._physicalCount); |
| 12667 }, |
| 12668 |
| 12669 /** |
| 12670 * The n-th item rendered in the `_physicalStart` tile. |
| 12671 */ |
| 12672 _virtualStartVal: 0, |
| 12673 |
| 12674 set _virtualStart(val) { |
| 12675 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
| 12676 }, |
| 12677 |
| 12678 get _virtualStart() { |
| 12679 return this._virtualStartVal || 0; |
| 12680 }, |
| 12681 |
| 12682 /** |
| 12683 * The k-th tile that is at the top of the scrolling list. |
| 12684 */ |
| 12685 _physicalStartVal: 0, |
| 12686 |
| 12687 set _physicalStart(val) { |
| 12688 this._physicalStartVal = val % this._physicalCount; |
| 12689 if (this._physicalStartVal < 0) { |
| 12690 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
| 12691 } |
| 12692 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12693 }, |
| 12694 |
| 12695 get _physicalStart() { |
| 12696 return this._physicalStartVal || 0; |
| 12697 }, |
| 12698 |
| 12699 /** |
| 12700 * The number of tiles in the DOM. |
| 12701 */ |
| 12702 _physicalCountVal: 0, |
| 12703 |
| 12704 set _physicalCount(val) { |
| 12705 this._physicalCountVal = val; |
| 12706 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 12707 }, |
| 12708 |
| 12709 get _physicalCount() { |
| 12710 return this._physicalCountVal; |
| 12711 }, |
| 12712 |
| 12713 /** |
| 12714 * The k-th tile that is at the bottom of the scrolling list. |
| 12715 */ |
| 12716 _physicalEnd: 0, |
| 12717 |
| 12718 /** |
| 12719 * An optimal physical size such that we will have enough physical items |
| 12720 * to fill up the viewport and recycle when the user scrolls. |
| 12721 * |
| 12722 * This default value assumes that we will at least have the equivalent |
| 12723 * to a viewport of physical items above and below the user's viewport. |
| 12724 */ |
| 12725 get _optPhysicalSize() { |
| 12726 if (this.grid) { |
| 12727 return this._estRowsInView * this._rowHeight * this._maxPages; |
| 12728 } |
| 12729 return this._viewportHeight * this._maxPages; |
| 12730 }, |
| 12731 |
| 12732 get _optPhysicalCount() { |
| 12733 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
| 12734 }, |
| 12735 |
| 12736 /** |
| 12737 * True if the current list is visible. |
| 12738 */ |
| 12739 get _isVisible() { |
| 12740 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
| 12741 }, |
| 12742 |
| 12743 /** |
| 12744 * Gets the index of the first visible item in the viewport. |
| 12745 * |
| 12746 * @type {number} |
| 12747 */ |
| 12748 get firstVisibleIndex() { |
| 12749 if (this._firstVisibleIndexVal === null) { |
| 12750 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
| 12751 |
| 12752 this._firstVisibleIndexVal = this._iterateItems( |
| 12753 function(pidx, vidx) { |
| 12754 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12755 |
| 12756 if (physicalOffset > this._scrollPosition) { |
| 12757 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; |
| 12758 } |
| 12759 // Handle a partially rendered final row in grid mode |
| 12760 if (this.grid && this._virtualCount - 1 === vidx) { |
| 12761 return vidx - (vidx % this._itemsPerRow); |
| 12762 } |
| 12763 }) || 0; |
| 12764 } |
| 12765 return this._firstVisibleIndexVal; |
| 12766 }, |
| 12767 |
| 12768 /** |
| 12769 * Gets the index of the last visible item in the viewport. |
| 12770 * |
| 12771 * @type {number} |
| 12772 */ |
| 12773 get lastVisibleIndex() { |
| 12774 if (this._lastVisibleIndexVal === null) { |
| 12775 if (this.grid) { |
| 12776 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
| 12777 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
| 12778 } else { |
| 12779 var physicalOffset = this._physicalTop; |
| 12780 this._iterateItems(function(pidx, vidx) { |
| 12781 if (physicalOffset < this._scrollBottom) { |
| 12782 this._lastVisibleIndexVal = vidx; |
| 12783 } else { |
| 12784 // Break _iterateItems |
| 12785 return true; |
| 12786 } |
| 12787 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| 12788 }); |
| 12789 } |
| 12790 } |
| 12791 return this._lastVisibleIndexVal; |
| 12792 }, |
| 12793 |
| 12794 get _defaultScrollTarget() { |
| 12795 return this; |
| 12796 }, |
| 12797 get _virtualRowCount() { |
| 12798 return Math.ceil(this._virtualCount / this._itemsPerRow); |
| 12799 }, |
| 12800 |
| 12801 get _estRowsInView() { |
| 12802 return Math.ceil(this._viewportHeight / this._rowHeight); |
| 12803 }, |
| 12804 |
| 12805 get _physicalRows() { |
| 12806 return Math.ceil(this._physicalCount / this._itemsPerRow); |
| 12807 }, |
| 12808 |
| 12809 ready: function() { |
| 12810 this.addEventListener('focus', this._didFocus.bind(this), true); |
| 12811 }, |
| 12812 |
| 12813 attached: function() { |
| 12814 this.updateViewportBoundaries(); |
| 12815 this._render(); |
| 12816 // `iron-resize` is fired when the list is attached if the event is added |
| 12817 // before attached causing unnecessary work. |
| 12818 this.listen(this, 'iron-resize', '_resizeHandler'); |
| 12819 }, |
| 12820 |
| 12821 detached: function() { |
| 12822 this._itemsRendered = false; |
| 12823 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
| 12824 }, |
| 12825 |
| 12826 /** |
| 12827 * Set the overflow property if this element has its own scrolling region |
| 12828 */ |
| 12829 _setOverflow: function(scrollTarget) { |
| 12830 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
| 12831 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 12832 }, |
| 12833 |
| 12834 /** |
| 12835 * Invoke this method if you dynamically update the viewport's |
| 12836 * size or CSS padding. |
| 12837 * |
| 12838 * @method updateViewportBoundaries |
| 12839 */ |
| 12840 updateViewportBoundaries: function() { |
| 12841 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : |
| 12842 parseInt(window.getComputedStyle(this)['padding-top'], 10); |
| 12843 |
| 12844 this._viewportHeight = this._scrollTargetHeight; |
| 12845 if (this.grid) { |
| 12846 this._updateGridMetrics(); |
| 12847 } |
| 12848 }, |
| 12849 |
| 12850 /** |
| 12851 * Update the models, the position of the |
| 12852 * items in the viewport and recycle tiles as needed. |
| 12853 */ |
| 12854 _scrollHandler: function() { |
| 12855 // clamp the `scrollTop` value |
| 12856 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
| 12857 var delta = scrollTop - this._scrollPosition; |
| 12858 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
| 12859 var ratio = this._ratio; |
| 12860 var recycledTiles = 0; |
| 12861 var hiddenContentSize = this._hiddenContentSize; |
| 12862 var currentRatio = ratio; |
| 12863 var movingUp = []; |
| 12864 |
| 12865 // track the last `scrollTop` |
| 12866 this._scrollPosition = scrollTop; |
| 12867 |
| 12868 // clear cached visible indexes |
| 12869 this._firstVisibleIndexVal = null; |
| 12870 this._lastVisibleIndexVal = null; |
| 12871 |
| 12872 scrollBottom = this._scrollBottom; |
| 12873 physicalBottom = this._physicalBottom; |
| 12874 |
| 12875 // random access |
| 12876 if (Math.abs(delta) > this._physicalSize) { |
| 12877 this._physicalTop += delta; |
| 12878 recycledTiles = Math.round(delta / this._physicalAverage); |
| 12879 } |
| 12880 // scroll up |
| 12881 else if (delta < 0) { |
| 12882 var topSpace = scrollTop - this._physicalTop; |
| 12883 var virtualStart = this._virtualStart; |
| 12884 |
| 12885 recycledTileSet = []; |
| 12886 |
| 12887 kth = this._physicalEnd; |
| 12888 currentRatio = topSpace / hiddenContentSize; |
| 12889 |
| 12890 // move tiles from bottom to top |
| 12891 while ( |
| 12892 // approximate `currentRatio` to `ratio` |
| 12893 currentRatio < ratio && |
| 12894 // recycle less physical items than the total |
| 12895 recycledTiles < this._physicalCount && |
| 12896 // ensure that these recycled tiles are needed |
| 12897 virtualStart - recycledTiles > 0 && |
| 12898 // ensure that the tile is not visible |
| 12899 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom |
| 12900 ) { |
| 12901 |
| 12902 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12903 currentRatio += tileHeight / hiddenContentSize; |
| 12904 physicalBottom -= tileHeight; |
| 12905 recycledTileSet.push(kth); |
| 12906 recycledTiles++; |
| 12907 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; |
| 12908 } |
| 12909 |
| 12910 movingUp = recycledTileSet; |
| 12911 recycledTiles = -recycledTiles; |
| 12912 } |
| 12913 // scroll down |
| 12914 else if (delta > 0) { |
| 12915 var bottomSpace = physicalBottom - scrollBottom; |
| 12916 var virtualEnd = this._virtualEnd; |
| 12917 var lastVirtualItemIndex = this._virtualCount-1; |
| 12918 |
| 12919 recycledTileSet = []; |
| 12920 |
| 12921 kth = this._physicalStart; |
| 12922 currentRatio = bottomSpace / hiddenContentSize; |
| 12923 |
| 12924 // move tiles from top to bottom |
| 12925 while ( |
| 12926 // approximate `currentRatio` to `ratio` |
| 12927 currentRatio < ratio && |
| 12928 // recycle less physical items than the total |
| 12929 recycledTiles < this._physicalCount && |
| 12930 // ensure that these recycled tiles are needed |
| 12931 virtualEnd + recycledTiles < lastVirtualItemIndex && |
| 12932 // ensure that the tile is not visible |
| 12933 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop |
| 12934 ) { |
| 12935 |
| 12936 tileHeight = this._getPhysicalSizeIncrement(kth); |
| 12937 currentRatio += tileHeight / hiddenContentSize; |
| 12938 |
| 12939 this._physicalTop += tileHeight; |
| 12940 recycledTileSet.push(kth); |
| 12941 recycledTiles++; |
| 12942 kth = (kth + 1) % this._physicalCount; |
| 12943 } |
| 12944 } |
| 12945 |
| 12946 if (recycledTiles === 0) { |
| 12947 // Try to increase the pool if the list's client height isn't filled up
with physical items |
| 12948 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
| 12949 this._increasePoolIfNeeded(); |
| 12950 } |
11184 } else { | 12951 } else { |
11185 switch (this.type) { | 12952 this._virtualStart = this._virtualStart + recycledTiles; |
11186 case 'number': | 12953 this._physicalStart = this._physicalStart + recycledTiles; |
11187 pattern = /[0-9.,e-]/; | 12954 this._update(recycledTileSet, movingUp); |
11188 break; | 12955 } |
11189 } | 12956 }, |
11190 } | 12957 |
11191 return pattern; | 12958 /** |
11192 }, | 12959 * Update the list of items, starting from the `_virtualStart` item. |
11193 | 12960 * @param {!Array<number>=} itemSet |
11194 ready: function() { | 12961 * @param {!Array<number>=} movingUp |
11195 this.bindValue = this.value; | 12962 */ |
11196 }, | 12963 _update: function(itemSet, movingUp) { |
11197 | 12964 // manage focus |
11198 /** | 12965 this._manageFocus(); |
11199 * @suppress {checkTypes} | 12966 // update models |
11200 */ | 12967 this._assignModels(itemSet); |
11201 _bindValueChanged: function() { | 12968 // measure heights |
11202 if (this.value !== this.bindValue) { | 12969 this._updateMetrics(itemSet); |
11203 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | 12970 // adjust offset after measuring |
11204 } | 12971 if (movingUp) { |
11205 // manually notify because we don't want to notify until after setting val
ue | 12972 while (movingUp.length) { |
11206 this.fire('bind-value-changed', {value: this.bindValue}); | 12973 var idx = movingUp.pop(); |
11207 }, | 12974 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
11208 | 12975 } |
11209 _allowedPatternChanged: function() { | 12976 } |
11210 // Force to prevent invalid input when an `allowed-pattern` is set | 12977 // update the position of the items |
11211 this.preventInvalidInput = this.allowedPattern ? true : false; | 12978 this._positionItems(); |
11212 }, | 12979 // set the scroller size |
11213 | 12980 this._updateScrollerSize(); |
11214 _onInput: function() { | 12981 // increase the pool of physical items |
11215 // Need to validate each of the characters pasted if they haven't | 12982 this._increasePoolIfNeeded(); |
11216 // been validated inside `_onKeypress` already. | 12983 }, |
11217 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | 12984 |
11218 var valid = this._checkPatternValidity(); | 12985 /** |
11219 if (!valid) { | 12986 * Creates a pool of DOM elements and attaches them to the local dom. |
11220 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | 12987 */ |
11221 this.value = this._previousValidInput; | 12988 _createPool: function(size) { |
11222 } | 12989 var physicalItems = new Array(size); |
11223 } | 12990 |
11224 | 12991 this._ensureTemplatized(); |
11225 this.bindValue = this.value; | 12992 |
11226 this._previousValidInput = this.value; | 12993 for (var i = 0; i < size; i++) { |
11227 this._patternAlreadyChecked = false; | 12994 var inst = this.stamp(null); |
11228 }, | 12995 // First element child is item; Safari doesn't support children[0] |
11229 | 12996 // on a doc fragment |
11230 _isPrintable: function(event) { | 12997 physicalItems[i] = inst.root.querySelector('*'); |
11231 // What a control/printable character is varies wildly based on the browse
r. | 12998 Polymer.dom(this).appendChild(inst.root); |
11232 // - most control characters (arrows, backspace) do not send a `keypress`
event | 12999 } |
11233 // in Chrome, but the *do* on Firefox | 13000 return physicalItems; |
11234 // - in Firefox, when they do send a `keypress` event, control chars have | 13001 }, |
11235 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | 13002 |
11236 // - printable characters always send a keypress event. | 13003 /** |
11237 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | 13004 * Increases the pool of physical items only if needed. |
11238 // always matches the charCode. | 13005 * |
11239 // None of this makes any sense. | 13006 * @return {boolean} True if the pool was increased. |
11240 | 13007 */ |
11241 // For these keys, ASCII code == browser keycode. | 13008 _increasePoolIfNeeded: function() { |
11242 var anyNonPrintable = | 13009 // Base case 1: the list has no height. |
11243 (event.keyCode == 8) || // backspace | 13010 if (this._viewportHeight === 0) { |
11244 (event.keyCode == 9) || // tab | 13011 return false; |
11245 (event.keyCode == 13) || // enter | 13012 } |
11246 (event.keyCode == 27); // escape | 13013 // Base case 2: If the physical size is optimal and the list's client heig
ht is full |
11247 | 13014 // with physical items, don't increase the pool. |
11248 // For these keys, make sure it's a browser keycode and not an ASCII code. | 13015 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
11249 var mozNonPrintable = | 13016 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
11250 (event.keyCode == 19) || // pause | 13017 return false; |
11251 (event.keyCode == 20) || // caps lock | 13018 } |
11252 (event.keyCode == 45) || // insert | 13019 // this value should range between [0 <= `currentPage` <= `_maxPages`] |
11253 (event.keyCode == 46) || // delete | 13020 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
11254 (event.keyCode == 144) || // num lock | 13021 |
11255 (event.keyCode == 145) || // scroll lock | 13022 if (currentPage === 0) { |
11256 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | 13023 // fill the first page |
11257 (event.keyCode > 111 && event.keyCode < 124); // fn keys | 13024 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); |
11258 | 13025 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
11259 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | 13026 // paint the page and defer the next increase |
11260 }, | 13027 // wait 16ms which is rough enough to get paint cycle. |
11261 | 13028 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
11262 _onKeypress: function(event) { | 13029 } else { |
11263 if (!this.preventInvalidInput && this.type !== 'number') { | 13030 // fill the rest of the pages |
| 13031 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
| 13032 } |
| 13033 |
| 13034 this._lastPage = currentPage; |
| 13035 |
| 13036 return true; |
| 13037 }, |
| 13038 |
| 13039 /** |
| 13040 * Increases the pool size. |
| 13041 */ |
| 13042 _increasePool: function(missingItems) { |
| 13043 var nextPhysicalCount = Math.min( |
| 13044 this._physicalCount + missingItems, |
| 13045 this._virtualCount - this._virtualStart, |
| 13046 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) |
| 13047 ); |
| 13048 var prevPhysicalCount = this._physicalCount; |
| 13049 var delta = nextPhysicalCount - prevPhysicalCount; |
| 13050 |
| 13051 if (delta <= 0) { |
11264 return; | 13052 return; |
11265 } | 13053 } |
11266 var regexp = this._patternRegExp; | 13054 |
11267 if (!regexp) { | 13055 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 13056 [].push.apply(this._physicalSizes, new Array(delta)); |
| 13057 |
| 13058 this._physicalCount = prevPhysicalCount + delta; |
| 13059 |
| 13060 // update the physical start if we need to preserve the model of the focus
ed item. |
| 13061 // In this situation, the focused item is currently rendered and its model
would |
| 13062 // have changed after increasing the pool if the physical start remained u
nchanged. |
| 13063 if (this._physicalStart > this._physicalEnd && |
| 13064 this._isIndexRendered(this._focusedIndex) && |
| 13065 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { |
| 13066 this._physicalStart = this._physicalStart + delta; |
| 13067 } |
| 13068 this._update(); |
| 13069 }, |
| 13070 |
| 13071 /** |
| 13072 * Render a new list of items. This method does exactly the same as `update`
, |
| 13073 * but it also ensures that only one `update` cycle is created. |
| 13074 */ |
| 13075 _render: function() { |
| 13076 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
| 13077 |
| 13078 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 13079 this._lastPage = 0; |
| 13080 this._update(); |
| 13081 this._itemsRendered = true; |
| 13082 } |
| 13083 }, |
| 13084 |
| 13085 /** |
| 13086 * Templetizes the user template. |
| 13087 */ |
| 13088 _ensureTemplatized: function() { |
| 13089 if (!this.ctor) { |
| 13090 // Template instance props that should be excluded from forwarding |
| 13091 var props = {}; |
| 13092 props.__key__ = true; |
| 13093 props[this.as] = true; |
| 13094 props[this.indexAs] = true; |
| 13095 props[this.selectedAs] = true; |
| 13096 props.tabIndex = true; |
| 13097 |
| 13098 this._instanceProps = props; |
| 13099 this._userTemplate = Polymer.dom(this).querySelector('template'); |
| 13100 |
| 13101 if (this._userTemplate) { |
| 13102 this.templatize(this._userTemplate); |
| 13103 } else { |
| 13104 console.warn('iron-list requires a template to be provided in light-do
m'); |
| 13105 } |
| 13106 } |
| 13107 }, |
| 13108 |
| 13109 /** |
| 13110 * Implements extension point from Templatizer mixin. |
| 13111 */ |
| 13112 _getStampedChildren: function() { |
| 13113 return this._physicalItems; |
| 13114 }, |
| 13115 |
| 13116 /** |
| 13117 * Implements extension point from Templatizer |
| 13118 * Called as a side effect of a template instance path change, responsible |
| 13119 * for notifying items.<key-for-instance>.<path> change up to host. |
| 13120 */ |
| 13121 _forwardInstancePath: function(inst, path, value) { |
| 13122 if (path.indexOf(this.as + '.') === 0) { |
| 13123 this.notifyPath('items.' + inst.__key__ + '.' + |
| 13124 path.slice(this.as.length + 1), value); |
| 13125 } |
| 13126 }, |
| 13127 |
| 13128 /** |
| 13129 * Implements extension point from Templatizer mixin |
| 13130 * Called as side-effect of a host property change, responsible for |
| 13131 * notifying parent path change on each row. |
| 13132 */ |
| 13133 _forwardParentProp: function(prop, value) { |
| 13134 if (this._physicalItems) { |
| 13135 this._physicalItems.forEach(function(item) { |
| 13136 item._templateInstance[prop] = value; |
| 13137 }, this); |
| 13138 } |
| 13139 }, |
| 13140 |
| 13141 /** |
| 13142 * Implements extension point from Templatizer |
| 13143 * Called as side-effect of a host path change, responsible for |
| 13144 * notifying parent.<path> path change on each row. |
| 13145 */ |
| 13146 _forwardParentPath: function(path, value) { |
| 13147 if (this._physicalItems) { |
| 13148 this._physicalItems.forEach(function(item) { |
| 13149 item._templateInstance.notifyPath(path, value, true); |
| 13150 }, this); |
| 13151 } |
| 13152 }, |
| 13153 |
| 13154 /** |
| 13155 * Called as a side effect of a host items.<key>.<path> path change, |
| 13156 * responsible for notifying item.<path> changes. |
| 13157 */ |
| 13158 _forwardItemPath: function(path, value) { |
| 13159 if (!this._physicalIndexForKey) { |
11268 return; | 13160 return; |
11269 } | 13161 } |
11270 | 13162 var dot = path.indexOf('.'); |
11271 // Handle special keys and backspace | 13163 var key = path.substring(0, dot < 0 ? path.length : dot); |
11272 if (event.metaKey || event.ctrlKey || event.altKey) | 13164 var idx = this._physicalIndexForKey[key]; |
| 13165 var offscreenItem = this._offscreenFocusedItem; |
| 13166 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? |
| 13167 offscreenItem : this._physicalItems[idx]; |
| 13168 |
| 13169 if (!el || el._templateInstance.__key__ !== key) { |
11273 return; | 13170 return; |
11274 | 13171 } |
11275 // Check the pattern either here or in `_onInput`, but not in both. | 13172 if (dot >= 0) { |
11276 this._patternAlreadyChecked = true; | 13173 path = this.as + '.' + path.substring(dot+1); |
11277 | 13174 el._templateInstance.notifyPath(path, value, true); |
11278 var thisChar = String.fromCharCode(event.charCode); | 13175 } else { |
11279 if (this._isPrintable(event) && !regexp.test(thisChar)) { | 13176 // Update selection if needed |
11280 event.preventDefault(); | 13177 var currentItem = el._templateInstance[this.as]; |
11281 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | 13178 if (Array.isArray(this.selectedItems)) { |
11282 } | 13179 for (var i = 0; i < this.selectedItems.length; i++) { |
11283 }, | 13180 if (this.selectedItems[i] === currentItem) { |
11284 | 13181 this.set('selectedItems.' + i, value); |
11285 _checkPatternValidity: function() { | 13182 break; |
11286 var regexp = this._patternRegExp; | 13183 } |
11287 if (!regexp) { | 13184 } |
11288 return true; | 13185 } else if (this.selectedItem === currentItem) { |
11289 } | 13186 this.set('selectedItem', value); |
11290 for (var i = 0; i < this.value.length; i++) { | 13187 } |
11291 if (!regexp.test(this.value[i])) { | 13188 el._templateInstance[this.as] = value; |
11292 return false; | 13189 } |
11293 } | 13190 }, |
11294 } | 13191 |
11295 return true; | 13192 /** |
11296 }, | 13193 * Called when the items have changed. That is, ressignments |
11297 | 13194 * to `items`, splices or updates to a single item. |
11298 /** | 13195 */ |
11299 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | 13196 _itemsChanged: function(change) { |
11300 * then any constraints. | 13197 if (change.path === 'items') { |
11301 * @return {boolean} True if the value is valid. | 13198 // reset items |
11302 */ | 13199 this._virtualStart = 0; |
11303 validate: function() { | 13200 this._physicalTop = 0; |
11304 // First, check what the browser thinks. Some inputs (like type=number) | 13201 this._virtualCount = this.items ? this.items.length : 0; |
11305 // behave weirdly and will set the value to "" if something invalid is | 13202 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
11306 // entered, but will set the validity correctly. | 13203 this._physicalIndexForKey = {}; |
11307 var valid = this.checkValidity(); | 13204 this._firstVisibleIndexVal = null; |
11308 | 13205 this._lastVisibleIndexVal = null; |
11309 // Only do extra checking if the browser thought this was valid. | 13206 |
11310 if (valid) { | 13207 this._resetScrollPosition(0); |
11311 // Empty, required input is invalid | 13208 this._removeFocusedItem(); |
11312 if (this.required && this.value === '') { | 13209 // create the initial physical items |
11313 valid = false; | 13210 if (!this._physicalItems) { |
11314 } else if (this.hasValidator()) { | 13211 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
11315 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | 13212 this._physicalItems = this._createPool(this._physicalCount); |
11316 } | 13213 this._physicalSizes = new Array(this._physicalCount); |
11317 } | 13214 } |
11318 | 13215 |
11319 this.invalid = !valid; | 13216 this._physicalStart = 0; |
11320 this.fire('iron-input-validate'); | 13217 |
11321 return valid; | 13218 } else if (change.path === 'items.splices') { |
11322 }, | 13219 |
11323 | 13220 this._adjustVirtualIndex(change.value.indexSplices); |
11324 _announceInvalidCharacter: function(message) { | 13221 this._virtualCount = this.items ? this.items.length : 0; |
11325 this.fire('iron-announce', { text: message }); | 13222 |
| 13223 } else { |
| 13224 // update a single item |
| 13225 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
| 13226 return; |
| 13227 } |
| 13228 |
| 13229 this._itemsRendered = false; |
| 13230 this._debounceTemplate(this._render); |
| 13231 }, |
| 13232 |
| 13233 /** |
| 13234 * @param {!Array<!PolymerSplice>} splices |
| 13235 */ |
| 13236 _adjustVirtualIndex: function(splices) { |
| 13237 splices.forEach(function(splice) { |
| 13238 // deselect removed items |
| 13239 splice.removed.forEach(this._removeItem, this); |
| 13240 // We only need to care about changes happening above the current positi
on |
| 13241 if (splice.index < this._virtualStart) { |
| 13242 var delta = Math.max( |
| 13243 splice.addedCount - splice.removed.length, |
| 13244 splice.index - this._virtualStart); |
| 13245 |
| 13246 this._virtualStart = this._virtualStart + delta; |
| 13247 |
| 13248 if (this._focusedIndex >= 0) { |
| 13249 this._focusedIndex = this._focusedIndex + delta; |
| 13250 } |
| 13251 } |
| 13252 }, this); |
| 13253 }, |
| 13254 |
| 13255 _removeItem: function(item) { |
| 13256 this.$.selector.deselect(item); |
| 13257 // remove the current focused item |
| 13258 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
| 13259 this._removeFocusedItem(); |
| 13260 } |
| 13261 }, |
| 13262 |
| 13263 /** |
| 13264 * Executes a provided function per every physical index in `itemSet` |
| 13265 * `itemSet` default value is equivalent to the entire set of physical index
es. |
| 13266 * |
| 13267 * @param {!function(number, number)} fn |
| 13268 * @param {!Array<number>=} itemSet |
| 13269 */ |
| 13270 _iterateItems: function(fn, itemSet) { |
| 13271 var pidx, vidx, rtn, i; |
| 13272 |
| 13273 if (arguments.length === 2 && itemSet) { |
| 13274 for (i = 0; i < itemSet.length; i++) { |
| 13275 pidx = itemSet[i]; |
| 13276 vidx = this._computeVidx(pidx); |
| 13277 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13278 return rtn; |
| 13279 } |
| 13280 } |
| 13281 } else { |
| 13282 pidx = this._physicalStart; |
| 13283 vidx = this._virtualStart; |
| 13284 |
| 13285 for (; pidx < this._physicalCount; pidx++, vidx++) { |
| 13286 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13287 return rtn; |
| 13288 } |
| 13289 } |
| 13290 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
| 13291 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
| 13292 return rtn; |
| 13293 } |
| 13294 } |
| 13295 } |
| 13296 }, |
| 13297 |
| 13298 /** |
| 13299 * Returns the virtual index for a given physical index |
| 13300 * |
| 13301 * @param {number} pidx Physical index |
| 13302 * @return {number} |
| 13303 */ |
| 13304 _computeVidx: function(pidx) { |
| 13305 if (pidx >= this._physicalStart) { |
| 13306 return this._virtualStart + (pidx - this._physicalStart); |
| 13307 } |
| 13308 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
| 13309 }, |
| 13310 |
| 13311 /** |
| 13312 * Assigns the data models to a given set of items. |
| 13313 * @param {!Array<number>=} itemSet |
| 13314 */ |
| 13315 _assignModels: function(itemSet) { |
| 13316 this._iterateItems(function(pidx, vidx) { |
| 13317 var el = this._physicalItems[pidx]; |
| 13318 var inst = el._templateInstance; |
| 13319 var item = this.items && this.items[vidx]; |
| 13320 |
| 13321 if (item != null) { |
| 13322 inst[this.as] = item; |
| 13323 inst.__key__ = this._collection.getKey(item); |
| 13324 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); |
| 13325 inst[this.indexAs] = vidx; |
| 13326 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
| 13327 this._physicalIndexForKey[inst.__key__] = pidx; |
| 13328 el.removeAttribute('hidden'); |
| 13329 } else { |
| 13330 inst.__key__ = null; |
| 13331 el.setAttribute('hidden', ''); |
| 13332 } |
| 13333 }, itemSet); |
| 13334 }, |
| 13335 |
| 13336 /** |
| 13337 * Updates the height for a given set of items. |
| 13338 * |
| 13339 * @param {!Array<number>=} itemSet |
| 13340 */ |
| 13341 _updateMetrics: function(itemSet) { |
| 13342 // Make sure we distributed all the physical items |
| 13343 // so we can measure them |
| 13344 Polymer.dom.flush(); |
| 13345 |
| 13346 var newPhysicalSize = 0; |
| 13347 var oldPhysicalSize = 0; |
| 13348 var prevAvgCount = this._physicalAverageCount; |
| 13349 var prevPhysicalAvg = this._physicalAverage; |
| 13350 |
| 13351 this._iterateItems(function(pidx, vidx) { |
| 13352 |
| 13353 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
| 13354 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
| 13355 newPhysicalSize += this._physicalSizes[pidx]; |
| 13356 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| 13357 |
| 13358 }, itemSet); |
| 13359 |
| 13360 this._viewportHeight = this._scrollTargetHeight; |
| 13361 if (this.grid) { |
| 13362 this._updateGridMetrics(); |
| 13363 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
| 13364 } else { |
| 13365 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
| 13366 } |
| 13367 |
| 13368 // update the average if we measured something |
| 13369 if (this._physicalAverageCount !== prevAvgCount) { |
| 13370 this._physicalAverage = Math.round( |
| 13371 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
| 13372 this._physicalAverageCount); |
| 13373 } |
| 13374 }, |
| 13375 |
| 13376 _updateGridMetrics: function() { |
| 13377 this._viewportWidth = this.$.items.offsetWidth; |
| 13378 // Set item width to the value of the _physicalItems offsetWidth |
| 13379 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
| 13380 // Set row height to the value of the _physicalItems offsetHeight |
| 13381 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
| 13382 // If in grid mode compute how many items with exist in each row |
| 13383 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
| 13384 }, |
| 13385 |
| 13386 /** |
| 13387 * Updates the position of the physical items. |
| 13388 */ |
| 13389 _positionItems: function() { |
| 13390 this._adjustScrollPosition(); |
| 13391 |
| 13392 var y = this._physicalTop; |
| 13393 |
| 13394 if (this.grid) { |
| 13395 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
| 13396 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
| 13397 |
| 13398 this._iterateItems(function(pidx, vidx) { |
| 13399 |
| 13400 var modulus = vidx % this._itemsPerRow; |
| 13401 var x = Math.floor((modulus * this._itemWidth) + rowOffset); |
| 13402 |
| 13403 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
| 13404 |
| 13405 if (this._shouldRenderNextRow(vidx)) { |
| 13406 y += this._rowHeight; |
| 13407 } |
| 13408 |
| 13409 }); |
| 13410 } else { |
| 13411 this._iterateItems(function(pidx, vidx) { |
| 13412 |
| 13413 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
| 13414 y += this._physicalSizes[pidx]; |
| 13415 |
| 13416 }); |
| 13417 } |
| 13418 }, |
| 13419 |
| 13420 _getPhysicalSizeIncrement: function(pidx) { |
| 13421 if (!this.grid) { |
| 13422 return this._physicalSizes[pidx]; |
| 13423 } |
| 13424 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
| 13425 return 0; |
| 13426 } |
| 13427 return this._rowHeight; |
| 13428 }, |
| 13429 |
| 13430 /** |
| 13431 * Returns, based on the current index, |
| 13432 * whether or not the next index will need |
| 13433 * to be rendered on a new row. |
| 13434 * |
| 13435 * @param {number} vidx Virtual index |
| 13436 * @return {boolean} |
| 13437 */ |
| 13438 _shouldRenderNextRow: function(vidx) { |
| 13439 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
| 13440 }, |
| 13441 |
| 13442 /** |
| 13443 * Adjusts the scroll position when it was overestimated. |
| 13444 */ |
| 13445 _adjustScrollPosition: function() { |
| 13446 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
| 13447 Math.min(this._scrollPosition + this._physicalTop, 0); |
| 13448 |
| 13449 if (deltaHeight) { |
| 13450 this._physicalTop = this._physicalTop - deltaHeight; |
| 13451 // juking scroll position during interial scrolling on iOS is no bueno |
| 13452 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
| 13453 this._resetScrollPosition(this._scrollTop - deltaHeight); |
| 13454 } |
| 13455 } |
| 13456 }, |
| 13457 |
| 13458 /** |
| 13459 * Sets the position of the scroll. |
| 13460 */ |
| 13461 _resetScrollPosition: function(pos) { |
| 13462 if (this.scrollTarget) { |
| 13463 this._scrollTop = pos; |
| 13464 this._scrollPosition = this._scrollTop; |
| 13465 } |
| 13466 }, |
| 13467 |
| 13468 /** |
| 13469 * Sets the scroll height, that's the height of the content, |
| 13470 * |
| 13471 * @param {boolean=} forceUpdate If true, updates the height no matter what. |
| 13472 */ |
| 13473 _updateScrollerSize: function(forceUpdate) { |
| 13474 if (this.grid) { |
| 13475 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
| 13476 } else { |
| 13477 this._estScrollHeight = (this._physicalBottom + |
| 13478 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); |
| 13479 } |
| 13480 |
| 13481 forceUpdate = forceUpdate || this._scrollHeight === 0; |
| 13482 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
| 13483 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
| 13484 |
| 13485 // amortize height adjustment, so it won't trigger repaints very often |
| 13486 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
| 13487 this.$.items.style.height = this._estScrollHeight + 'px'; |
| 13488 this._scrollHeight = this._estScrollHeight; |
| 13489 } |
| 13490 }, |
| 13491 |
| 13492 /** |
| 13493 * Scroll to a specific item in the virtual list regardless |
| 13494 * of the physical items in the DOM tree. |
| 13495 * |
| 13496 * @method scrollToItem |
| 13497 * @param {(Object)} item The item to be scrolled to |
| 13498 */ |
| 13499 scrollToItem: function(item){ |
| 13500 return this.scrollToIndex(this.items.indexOf(item)); |
| 13501 }, |
| 13502 |
| 13503 /** |
| 13504 * Scroll to a specific index in the virtual list regardless |
| 13505 * of the physical items in the DOM tree. |
| 13506 * |
| 13507 * @method scrollToIndex |
| 13508 * @param {number} idx The index of the item |
| 13509 */ |
| 13510 scrollToIndex: function(idx) { |
| 13511 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
| 13512 return; |
| 13513 } |
| 13514 |
| 13515 Polymer.dom.flush(); |
| 13516 |
| 13517 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
| 13518 // update the virtual start only when needed |
| 13519 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
| 13520 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); |
| 13521 } |
| 13522 // manage focus |
| 13523 this._manageFocus(); |
| 13524 // assign new models |
| 13525 this._assignModels(); |
| 13526 // measure the new sizes |
| 13527 this._updateMetrics(); |
| 13528 |
| 13529 // estimate new physical offset |
| 13530 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
| 13531 this._physicalTop = estPhysicalTop; |
| 13532 |
| 13533 var currentTopItem = this._physicalStart; |
| 13534 var currentVirtualItem = this._virtualStart; |
| 13535 var targetOffsetTop = 0; |
| 13536 var hiddenContentSize = this._hiddenContentSize; |
| 13537 |
| 13538 // scroll to the item as much as we can |
| 13539 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
| 13540 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
| 13541 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
| 13542 currentVirtualItem++; |
| 13543 } |
| 13544 // update the scroller size |
| 13545 this._updateScrollerSize(true); |
| 13546 // update the position of the items |
| 13547 this._positionItems(); |
| 13548 // set the new scroll position |
| 13549 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
| 13550 // increase the pool of physical items if needed |
| 13551 this._increasePoolIfNeeded(); |
| 13552 // clear cached visible index |
| 13553 this._firstVisibleIndexVal = null; |
| 13554 this._lastVisibleIndexVal = null; |
| 13555 }, |
| 13556 |
| 13557 /** |
| 13558 * Reset the physical average and the average count. |
| 13559 */ |
| 13560 _resetAverage: function() { |
| 13561 this._physicalAverage = 0; |
| 13562 this._physicalAverageCount = 0; |
| 13563 }, |
| 13564 |
| 13565 /** |
| 13566 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` |
| 13567 * when the element is resized. |
| 13568 */ |
| 13569 _resizeHandler: function() { |
| 13570 // iOS fires the resize event when the address bar slides up |
| 13571 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
| 13572 return; |
| 13573 } |
| 13574 // In Desktop Safari 9.0.3, if the scroll bars are always shown, |
| 13575 // changing the scroll position from a resize handler would result in |
| 13576 // the scroll position being reset. Waiting 1ms fixes the issue. |
| 13577 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
| 13578 this.updateViewportBoundaries(); |
| 13579 this._render(); |
| 13580 |
| 13581 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
| 13582 this._resetAverage(); |
| 13583 this.scrollToIndex(this.firstVisibleIndex); |
| 13584 } |
| 13585 }.bind(this), 1)); |
| 13586 }, |
| 13587 |
| 13588 _getModelFromItem: function(item) { |
| 13589 var key = this._collection.getKey(item); |
| 13590 var pidx = this._physicalIndexForKey[key]; |
| 13591 |
| 13592 if (pidx != null) { |
| 13593 return this._physicalItems[pidx]._templateInstance; |
| 13594 } |
| 13595 return null; |
| 13596 }, |
| 13597 |
| 13598 /** |
| 13599 * Gets a valid item instance from its index or the object value. |
| 13600 * |
| 13601 * @param {(Object|number)} item The item object or its index |
| 13602 */ |
| 13603 _getNormalizedItem: function(item) { |
| 13604 if (this._collection.getKey(item) === undefined) { |
| 13605 if (typeof item === 'number') { |
| 13606 item = this.items[item]; |
| 13607 if (!item) { |
| 13608 throw new RangeError('<item> not found'); |
| 13609 } |
| 13610 return item; |
| 13611 } |
| 13612 throw new TypeError('<item> should be a valid item'); |
| 13613 } |
| 13614 return item; |
| 13615 }, |
| 13616 |
| 13617 /** |
| 13618 * Select the list item at the given index. |
| 13619 * |
| 13620 * @method selectItem |
| 13621 * @param {(Object|number)} item The item object or its index |
| 13622 */ |
| 13623 selectItem: function(item) { |
| 13624 item = this._getNormalizedItem(item); |
| 13625 var model = this._getModelFromItem(item); |
| 13626 |
| 13627 if (!this.multiSelection && this.selectedItem) { |
| 13628 this.deselectItem(this.selectedItem); |
| 13629 } |
| 13630 if (model) { |
| 13631 model[this.selectedAs] = true; |
| 13632 } |
| 13633 this.$.selector.select(item); |
| 13634 this.updateSizeForItem(item); |
| 13635 }, |
| 13636 |
| 13637 /** |
| 13638 * Deselects the given item list if it is already selected. |
| 13639 * |
| 13640 |
| 13641 * @method deselect |
| 13642 * @param {(Object|number)} item The item object or its index |
| 13643 */ |
| 13644 deselectItem: function(item) { |
| 13645 item = this._getNormalizedItem(item); |
| 13646 var model = this._getModelFromItem(item); |
| 13647 |
| 13648 if (model) { |
| 13649 model[this.selectedAs] = false; |
| 13650 } |
| 13651 this.$.selector.deselect(item); |
| 13652 this.updateSizeForItem(item); |
| 13653 }, |
| 13654 |
| 13655 /** |
| 13656 * Select or deselect a given item depending on whether the item |
| 13657 * has already been selected. |
| 13658 * |
| 13659 * @method toggleSelectionForItem |
| 13660 * @param {(Object|number)} item The item object or its index |
| 13661 */ |
| 13662 toggleSelectionForItem: function(item) { |
| 13663 item = this._getNormalizedItem(item); |
| 13664 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { |
| 13665 this.deselectItem(item); |
| 13666 } else { |
| 13667 this.selectItem(item); |
| 13668 } |
| 13669 }, |
| 13670 |
| 13671 /** |
| 13672 * Clears the current selection state of the list. |
| 13673 * |
| 13674 * @method clearSelection |
| 13675 */ |
| 13676 clearSelection: function() { |
| 13677 function unselect(item) { |
| 13678 var model = this._getModelFromItem(item); |
| 13679 if (model) { |
| 13680 model[this.selectedAs] = false; |
| 13681 } |
| 13682 } |
| 13683 |
| 13684 if (Array.isArray(this.selectedItems)) { |
| 13685 this.selectedItems.forEach(unselect, this); |
| 13686 } else if (this.selectedItem) { |
| 13687 unselect.call(this, this.selectedItem); |
| 13688 } |
| 13689 |
| 13690 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
| 13691 }, |
| 13692 |
| 13693 /** |
| 13694 * Add an event listener to `tap` if `selectionEnabled` is true, |
| 13695 * it will remove the listener otherwise. |
| 13696 */ |
| 13697 _selectionEnabledChanged: function(selectionEnabled) { |
| 13698 var handler = selectionEnabled ? this.listen : this.unlisten; |
| 13699 handler.call(this, this, 'tap', '_selectionHandler'); |
| 13700 }, |
| 13701 |
| 13702 /** |
| 13703 * Select an item from an event object. |
| 13704 */ |
| 13705 _selectionHandler: function(e) { |
| 13706 var model = this.modelForElement(e.target); |
| 13707 if (!model) { |
| 13708 return; |
| 13709 } |
| 13710 var modelTabIndex, activeElTabIndex; |
| 13711 var target = Polymer.dom(e).path[0]; |
| 13712 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
| 13713 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
| 13714 // Safari does not focus certain form controls via mouse |
| 13715 // https://bugs.webkit.org/show_bug.cgi?id=118043 |
| 13716 if (target.localName === 'input' || |
| 13717 target.localName === 'button' || |
| 13718 target.localName === 'select') { |
| 13719 return; |
| 13720 } |
| 13721 // Set a temporary tabindex |
| 13722 modelTabIndex = model.tabIndex; |
| 13723 model.tabIndex = SECRET_TABINDEX; |
| 13724 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
| 13725 model.tabIndex = modelTabIndex; |
| 13726 // Only select the item if the tap wasn't on a focusable child |
| 13727 // or the element bound to `tabIndex` |
| 13728 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
| 13729 return; |
| 13730 } |
| 13731 this.toggleSelectionForItem(model[this.as]); |
| 13732 }, |
| 13733 |
| 13734 _multiSelectionChanged: function(multiSelection) { |
| 13735 this.clearSelection(); |
| 13736 this.$.selector.multi = multiSelection; |
| 13737 }, |
| 13738 |
| 13739 /** |
| 13740 * Updates the size of an item. |
| 13741 * |
| 13742 * @method updateSizeForItem |
| 13743 * @param {(Object|number)} item The item object or its index |
| 13744 */ |
| 13745 updateSizeForItem: function(item) { |
| 13746 item = this._getNormalizedItem(item); |
| 13747 var key = this._collection.getKey(item); |
| 13748 var pidx = this._physicalIndexForKey[key]; |
| 13749 |
| 13750 if (pidx != null) { |
| 13751 this._updateMetrics([pidx]); |
| 13752 this._positionItems(); |
| 13753 } |
| 13754 }, |
| 13755 |
| 13756 /** |
| 13757 * Creates a temporary backfill item in the rendered pool of physical items |
| 13758 * to replace the main focused item. The focused item has tabIndex = 0 |
| 13759 * and might be currently focused by the user. |
| 13760 * |
| 13761 * This dynamic replacement helps to preserve the focus state. |
| 13762 */ |
| 13763 _manageFocus: function() { |
| 13764 var fidx = this._focusedIndex; |
| 13765 |
| 13766 if (fidx >= 0 && fidx < this._virtualCount) { |
| 13767 // if it's a valid index, check if that index is rendered |
| 13768 // in a physical item. |
| 13769 if (this._isIndexRendered(fidx)) { |
| 13770 this._restoreFocusedItem(); |
| 13771 } else { |
| 13772 this._createFocusBackfillItem(); |
| 13773 } |
| 13774 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
| 13775 // otherwise, assign the initial focused index. |
| 13776 this._focusedIndex = this._virtualStart; |
| 13777 this._focusedItem = this._physicalItems[this._physicalStart]; |
| 13778 } |
| 13779 }, |
| 13780 |
| 13781 _isIndexRendered: function(idx) { |
| 13782 return idx >= this._virtualStart && idx <= this._virtualEnd; |
| 13783 }, |
| 13784 |
| 13785 _isIndexVisible: function(idx) { |
| 13786 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
| 13787 }, |
| 13788 |
| 13789 _getPhysicalIndex: function(idx) { |
| 13790 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
| 13791 }, |
| 13792 |
| 13793 _focusPhysicalItem: function(idx) { |
| 13794 if (idx < 0 || idx >= this._virtualCount) { |
| 13795 return; |
| 13796 } |
| 13797 this._restoreFocusedItem(); |
| 13798 // scroll to index to make sure it's rendered |
| 13799 if (!this._isIndexRendered(idx)) { |
| 13800 this.scrollToIndex(idx); |
| 13801 } |
| 13802 |
| 13803 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
| 13804 var model = physicalItem._templateInstance; |
| 13805 var focusable; |
| 13806 |
| 13807 // set a secret tab index |
| 13808 model.tabIndex = SECRET_TABINDEX; |
| 13809 // check if focusable element is the physical item |
| 13810 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
| 13811 focusable = physicalItem; |
| 13812 } |
| 13813 // search for the element which tabindex is bound to the secret tab index |
| 13814 if (!focusable) { |
| 13815 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
| 13816 } |
| 13817 // restore the tab index |
| 13818 model.tabIndex = 0; |
| 13819 // focus the focusable element |
| 13820 this._focusedIndex = idx; |
| 13821 focusable && focusable.focus(); |
| 13822 }, |
| 13823 |
| 13824 _removeFocusedItem: function() { |
| 13825 if (this._offscreenFocusedItem) { |
| 13826 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
| 13827 } |
| 13828 this._offscreenFocusedItem = null; |
| 13829 this._focusBackfillItem = null; |
| 13830 this._focusedItem = null; |
| 13831 this._focusedIndex = -1; |
| 13832 }, |
| 13833 |
| 13834 _createFocusBackfillItem: function() { |
| 13835 var pidx, fidx = this._focusedIndex; |
| 13836 if (this._offscreenFocusedItem || fidx < 0) { |
| 13837 return; |
| 13838 } |
| 13839 if (!this._focusBackfillItem) { |
| 13840 // create a physical item, so that it backfills the focused item. |
| 13841 var stampedTemplate = this.stamp(null); |
| 13842 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
| 13843 Polymer.dom(this).appendChild(stampedTemplate.root); |
| 13844 } |
| 13845 // get the physical index for the focused index |
| 13846 pidx = this._getPhysicalIndex(fidx); |
| 13847 |
| 13848 if (pidx != null) { |
| 13849 // set the offcreen focused physical item |
| 13850 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 13851 // backfill the focused physical item |
| 13852 this._physicalItems[pidx] = this._focusBackfillItem; |
| 13853 // hide the focused physical |
| 13854 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 13855 } |
| 13856 }, |
| 13857 |
| 13858 _restoreFocusedItem: function() { |
| 13859 var pidx, fidx = this._focusedIndex; |
| 13860 |
| 13861 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
| 13862 return; |
| 13863 } |
| 13864 // assign models to the focused index |
| 13865 this._assignModels(); |
| 13866 // get the new physical index for the focused index |
| 13867 pidx = this._getPhysicalIndex(fidx); |
| 13868 |
| 13869 if (pidx != null) { |
| 13870 // flip the focus backfill |
| 13871 this._focusBackfillItem = this._physicalItems[pidx]; |
| 13872 // restore the focused physical item |
| 13873 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 13874 // reset the offscreen focused item |
| 13875 this._offscreenFocusedItem = null; |
| 13876 // hide the physical item that backfills |
| 13877 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
| 13878 } |
| 13879 }, |
| 13880 |
| 13881 _didFocus: function(e) { |
| 13882 var targetModel = this.modelForElement(e.target); |
| 13883 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
| 13884 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
| 13885 var fidx = this._focusedIndex; |
| 13886 |
| 13887 if (!targetModel || !focusedModel) { |
| 13888 return; |
| 13889 } |
| 13890 if (focusedModel === targetModel) { |
| 13891 // if the user focused the same item, then bring it into view if it's no
t visible |
| 13892 if (!this._isIndexVisible(fidx)) { |
| 13893 this.scrollToIndex(fidx); |
| 13894 } |
| 13895 } else { |
| 13896 this._restoreFocusedItem(); |
| 13897 // restore tabIndex for the currently focused item |
| 13898 focusedModel.tabIndex = -1; |
| 13899 // set the tabIndex for the next focused item |
| 13900 targetModel.tabIndex = 0; |
| 13901 fidx = targetModel[this.indexAs]; |
| 13902 this._focusedIndex = fidx; |
| 13903 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
| 13904 |
| 13905 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
| 13906 this._update(); |
| 13907 } |
| 13908 } |
| 13909 }, |
| 13910 |
| 13911 _didMoveUp: function() { |
| 13912 this._focusPhysicalItem(this._focusedIndex - 1); |
| 13913 }, |
| 13914 |
| 13915 _didMoveDown: function(e) { |
| 13916 // disable scroll when pressing the down key |
| 13917 e.detail.keyboardEvent.preventDefault(); |
| 13918 this._focusPhysicalItem(this._focusedIndex + 1); |
| 13919 }, |
| 13920 |
| 13921 _didEnter: function(e) { |
| 13922 this._focusPhysicalItem(this._focusedIndex); |
| 13923 this._selectionHandler(e.detail.keyboardEvent); |
11326 } | 13924 } |
11327 }); | 13925 }); |
11328 | 13926 |
11329 /* | 13927 })(); |
11330 The `iron-input-validate` event is fired whenever `validate()` is called. | |
11331 @event iron-input-validate | |
11332 */ | |
11333 Polymer({ | 13928 Polymer({ |
11334 is: 'paper-input-container', | 13929 |
| 13930 is: 'iron-scroll-threshold', |
11335 | 13931 |
11336 properties: { | 13932 properties: { |
| 13933 |
11337 /** | 13934 /** |
11338 * Set to true to disable the floating label. The label disappears when th
e input value is | 13935 * Distance from the top (or left, for horizontal) bound of the scroller |
11339 * not null. | 13936 * where the "upper trigger" will fire. |
11340 */ | 13937 */ |
11341 noLabelFloat: { | 13938 upperThreshold: { |
| 13939 type: Number, |
| 13940 value: 100 |
| 13941 }, |
| 13942 |
| 13943 /** |
| 13944 * Distance from the bottom (or right, for horizontal) bound of the scroll
er |
| 13945 * where the "lower trigger" will fire. |
| 13946 */ |
| 13947 lowerThreshold: { |
| 13948 type: Number, |
| 13949 value: 100 |
| 13950 }, |
| 13951 |
| 13952 /** |
| 13953 * Read-only value that tracks the triggered state of the upper threshold. |
| 13954 */ |
| 13955 upperTriggered: { |
| 13956 type: Boolean, |
| 13957 value: false, |
| 13958 notify: true, |
| 13959 readOnly: true |
| 13960 }, |
| 13961 |
| 13962 /** |
| 13963 * Read-only value that tracks the triggered state of the lower threshold. |
| 13964 */ |
| 13965 lowerTriggered: { |
| 13966 type: Boolean, |
| 13967 value: false, |
| 13968 notify: true, |
| 13969 readOnly: true |
| 13970 }, |
| 13971 |
| 13972 /** |
| 13973 * True if the orientation of the scroller is horizontal. |
| 13974 */ |
| 13975 horizontal: { |
11342 type: Boolean, | 13976 type: Boolean, |
11343 value: false | 13977 value: false |
11344 }, | 13978 } |
11345 | 13979 }, |
11346 /** | 13980 |
11347 * Set to true to always float the floating label. | 13981 behaviors: [ |
11348 */ | 13982 Polymer.IronScrollTargetBehavior |
11349 alwaysFloatLabel: { | 13983 ], |
11350 type: Boolean, | 13984 |
11351 value: false | 13985 observers: [ |
11352 }, | 13986 '_setOverflow(scrollTarget)', |
11353 | 13987 '_initCheck(horizontal, isAttached)' |
11354 /** | 13988 ], |
11355 * The attribute to listen for value changes on. | 13989 |
11356 */ | 13990 get _defaultScrollTarget() { |
11357 attrForValue: { | 13991 return this; |
11358 type: String, | 13992 }, |
11359 value: 'bind-value' | 13993 |
11360 }, | 13994 _setOverflow: function(scrollTarget) { |
11361 | 13995 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
11362 /** | 13996 }, |
11363 * Set to true to auto-validate the input value when it changes. | 13997 |
11364 */ | 13998 _scrollHandler: function() { |
11365 autoValidate: { | 13999 // throttle the work on the scroll event |
11366 type: Boolean, | 14000 var THROTTLE_THRESHOLD = 200; |
11367 value: false | 14001 if (!this.isDebouncerActive('_checkTheshold')) { |
11368 }, | 14002 this.debounce('_checkTheshold', function() { |
11369 | 14003 this.checkScrollThesholds(); |
11370 /** | 14004 }, THROTTLE_THRESHOLD); |
11371 * True if the input is invalid. This property is set automatically when t
he input value | 14005 } |
11372 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | 14006 }, |
11373 */ | 14007 |
11374 invalid: { | 14008 _initCheck: function(horizontal, isAttached) { |
11375 observer: '_invalidChanged', | 14009 if (isAttached) { |
11376 type: Boolean, | 14010 this.debounce('_init', function() { |
11377 value: false | 14011 this.clearTriggers(); |
11378 }, | 14012 this.checkScrollThesholds(); |
11379 | 14013 }); |
11380 /** | 14014 } |
11381 * True if the input has focus. | 14015 }, |
11382 */ | 14016 |
11383 focused: { | 14017 /** |
11384 readOnly: true, | 14018 * Checks the scroll thresholds. |
11385 type: Boolean, | 14019 * This method is automatically called by iron-scroll-threshold. |
11386 value: false, | 14020 * |
11387 notify: true | 14021 * @method checkScrollThesholds |
11388 }, | 14022 */ |
11389 | 14023 checkScrollThesholds: function() { |
11390 _addons: { | 14024 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) { |
11391 type: Array | 14025 return; |
11392 // do not set a default value here intentionally - it will be initialize
d lazily when a | 14026 } |
11393 // distributed child is attached, which may occur before configuration f
or this element | 14027 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo
p; |
11394 // in polyfill. | 14028 var lowerScrollValue = this.horizontal ? |
11395 }, | 14029 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll
Left : |
11396 | 14030 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._
scrollTop; |
11397 _inputHasContent: { | 14031 |
11398 type: Boolean, | 14032 // Detect upper threshold |
11399 value: false | 14033 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { |
11400 }, | 14034 this._setUpperTriggered(true); |
11401 | 14035 this.fire('upper-threshold'); |
11402 _inputSelector: { | 14036 } |
11403 type: String, | 14037 // Detect lower threshold |
11404 value: 'input,textarea,.paper-input-input' | 14038 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { |
11405 }, | 14039 this._setLowerTriggered(true); |
11406 | 14040 this.fire('lower-threshold'); |
11407 _boundOnFocus: { | 14041 } |
11408 type: Function, | 14042 }, |
11409 value: function() { | 14043 |
11410 return this._onFocus.bind(this); | 14044 /** |
11411 } | 14045 * Clear the upper and lower threshold states. |
11412 }, | 14046 * |
11413 | 14047 * @method clearTriggers |
11414 _boundOnBlur: { | 14048 */ |
11415 type: Function, | 14049 clearTriggers: function() { |
11416 value: function() { | 14050 this._setUpperTriggered(false); |
11417 return this._onBlur.bind(this); | 14051 this._setLowerTriggered(false); |
11418 } | |
11419 }, | |
11420 | |
11421 _boundOnInput: { | |
11422 type: Function, | |
11423 value: function() { | |
11424 return this._onInput.bind(this); | |
11425 } | |
11426 }, | |
11427 | |
11428 _boundValueChanged: { | |
11429 type: Function, | |
11430 value: function() { | |
11431 return this._onValueChanged.bind(this); | |
11432 } | |
11433 } | |
11434 }, | |
11435 | |
11436 listeners: { | |
11437 'addon-attached': '_onAddonAttached', | |
11438 'iron-input-validate': '_onIronInputValidate' | |
11439 }, | |
11440 | |
11441 get _valueChangedEvent() { | |
11442 return this.attrForValue + '-changed'; | |
11443 }, | |
11444 | |
11445 get _propertyForValue() { | |
11446 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
11447 }, | |
11448 | |
11449 get _inputElement() { | |
11450 return Polymer.dom(this).querySelector(this._inputSelector); | |
11451 }, | |
11452 | |
11453 get _inputElementValue() { | |
11454 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
11455 }, | |
11456 | |
11457 ready: function() { | |
11458 if (!this._addons) { | |
11459 this._addons = []; | |
11460 } | |
11461 this.addEventListener('focus', this._boundOnFocus, true); | |
11462 this.addEventListener('blur', this._boundOnBlur, true); | |
11463 }, | |
11464 | |
11465 attached: function() { | |
11466 if (this.attrForValue) { | |
11467 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
11468 } else { | |
11469 this.addEventListener('input', this._onInput); | |
11470 } | |
11471 | |
11472 // Only validate when attached if the input already has a value. | |
11473 if (this._inputElementValue != '') { | |
11474 this._handleValueAndAutoValidate(this._inputElement); | |
11475 } else { | |
11476 this._handleValue(this._inputElement); | |
11477 } | |
11478 }, | |
11479 | |
11480 _onAddonAttached: function(event) { | |
11481 if (!this._addons) { | |
11482 this._addons = []; | |
11483 } | |
11484 var target = event.target; | |
11485 if (this._addons.indexOf(target) === -1) { | |
11486 this._addons.push(target); | |
11487 if (this.isAttached) { | |
11488 this._handleValue(this._inputElement); | |
11489 } | |
11490 } | |
11491 }, | |
11492 | |
11493 _onFocus: function() { | |
11494 this._setFocused(true); | |
11495 }, | |
11496 | |
11497 _onBlur: function() { | |
11498 this._setFocused(false); | |
11499 this._handleValueAndAutoValidate(this._inputElement); | |
11500 }, | |
11501 | |
11502 _onInput: function(event) { | |
11503 this._handleValueAndAutoValidate(event.target); | |
11504 }, | |
11505 | |
11506 _onValueChanged: function(event) { | |
11507 this._handleValueAndAutoValidate(event.target); | |
11508 }, | |
11509 | |
11510 _handleValue: function(inputElement) { | |
11511 var value = this._inputElementValue; | |
11512 | |
11513 // type="number" hack needed because this.value is empty until it's valid | |
11514 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
11515 this._inputHasContent = true; | |
11516 } else { | |
11517 this._inputHasContent = false; | |
11518 } | |
11519 | |
11520 this.updateAddons({ | |
11521 inputElement: inputElement, | |
11522 value: value, | |
11523 invalid: this.invalid | |
11524 }); | |
11525 }, | |
11526 | |
11527 _handleValueAndAutoValidate: function(inputElement) { | |
11528 if (this.autoValidate) { | |
11529 var valid; | |
11530 if (inputElement.validate) { | |
11531 valid = inputElement.validate(this._inputElementValue); | |
11532 } else { | |
11533 valid = inputElement.checkValidity(); | |
11534 } | |
11535 this.invalid = !valid; | |
11536 } | |
11537 | |
11538 // Call this last to notify the add-ons. | |
11539 this._handleValue(inputElement); | |
11540 }, | |
11541 | |
11542 _onIronInputValidate: function(event) { | |
11543 this.invalid = this._inputElement.invalid; | |
11544 }, | |
11545 | |
11546 _invalidChanged: function() { | |
11547 if (this._addons) { | |
11548 this.updateAddons({invalid: this.invalid}); | |
11549 } | |
11550 }, | |
11551 | |
11552 /** | |
11553 * Call this to update the state of add-ons. | |
11554 * @param {Object} state Add-on state. | |
11555 */ | |
11556 updateAddons: function(state) { | |
11557 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
11558 addon.update(state); | |
11559 } | |
11560 }, | |
11561 | |
11562 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
11563 var cls = 'input-content'; | |
11564 if (!noLabelFloat) { | |
11565 var label = this.querySelector('label'); | |
11566 | |
11567 if (alwaysFloatLabel || _inputHasContent) { | |
11568 cls += ' label-is-floating'; | |
11569 // If the label is floating, ignore any offsets that may have been | |
11570 // applied from a prefix element. | |
11571 this.$.labelAndInputContainer.style.position = 'static'; | |
11572 | |
11573 if (invalid) { | |
11574 cls += ' is-invalid'; | |
11575 } else if (focused) { | |
11576 cls += " label-is-highlighted"; | |
11577 } | |
11578 } else { | |
11579 // When the label is not floating, it should overlap the input element
. | |
11580 if (label) { | |
11581 this.$.labelAndInputContainer.style.position = 'relative'; | |
11582 } | |
11583 } | |
11584 } else { | |
11585 if (_inputHasContent) { | |
11586 cls += ' label-is-hidden'; | |
11587 } | |
11588 } | |
11589 return cls; | |
11590 }, | |
11591 | |
11592 _computeUnderlineClass: function(focused, invalid) { | |
11593 var cls = 'underline'; | |
11594 if (invalid) { | |
11595 cls += ' is-invalid'; | |
11596 } else if (focused) { | |
11597 cls += ' is-highlighted' | |
11598 } | |
11599 return cls; | |
11600 }, | |
11601 | |
11602 _computeAddOnContentClass: function(focused, invalid) { | |
11603 var cls = 'add-on-content'; | |
11604 if (invalid) { | |
11605 cls += ' is-invalid'; | |
11606 } else if (focused) { | |
11607 cls += ' is-highlighted' | |
11608 } | |
11609 return cls; | |
11610 } | 14052 } |
| 14053 |
| 14054 /** |
| 14055 * Fires when the lower threshold has been reached. |
| 14056 * |
| 14057 * @event lower-threshold |
| 14058 */ |
| 14059 |
| 14060 /** |
| 14061 * Fires when the upper threshold has been reached. |
| 14062 * |
| 14063 * @event upper-threshold |
| 14064 */ |
| 14065 |
11611 }); | 14066 }); |
11612 // Copyright 2015 The Chromium Authors. All rights reserved. | 14067 // Copyright 2015 The Chromium Authors. All rights reserved. |
11613 // Use of this source code is governed by a BSD-style license that can be | 14068 // Use of this source code is governed by a BSD-style license that can be |
11614 // found in the LICENSE file. | 14069 // found in the LICENSE file. |
11615 | 14070 |
11616 var SearchField = Polymer({ | 14071 Polymer({ |
11617 is: 'cr-search-field', | 14072 is: 'history-list', |
11618 | 14073 |
11619 behaviors: [CrSearchFieldBehavior], | 14074 behaviors: [HistoryListBehavior], |
11620 | 14075 |
11621 properties: { | 14076 properties: { |
11622 value_: String, | 14077 // The search term for the current query. Set when the query returns. |
11623 }, | 14078 searchedTerm: { |
11624 | 14079 type: String, |
11625 /** @return {!HTMLInputElement} */ | 14080 value: '', |
11626 getSearchInput: function() { | 14081 }, |
11627 return this.$.searchInput; | 14082 |
11628 }, | 14083 lastSearchedTerm_: String, |
11629 | 14084 |
11630 /** @private */ | 14085 querying: Boolean, |
11631 clearSearch_: function() { | 14086 |
11632 this.setValue(''); | 14087 // An array of history entries in reverse chronological order. |
11633 this.getSearchInput().focus(); | 14088 historyData_: Array, |
11634 }, | 14089 |
11635 | 14090 resultLoadingDisabled_: { |
11636 /** @private */ | 14091 type: Boolean, |
11637 toggleShowingSearch_: function() { | 14092 value: false, |
11638 this.showingSearch = !this.showingSearch; | 14093 }, |
| 14094 }, |
| 14095 |
| 14096 listeners: { |
| 14097 'scroll': 'notifyListScroll_', |
| 14098 'remove-bookmark-stars': 'removeBookmarkStars_', |
| 14099 }, |
| 14100 |
| 14101 /** @override */ |
| 14102 attached: function() { |
| 14103 // It is possible (eg, when middle clicking the reload button) for all other |
| 14104 // resize events to fire before the list is attached and can be measured. |
| 14105 // Adding another resize here ensures it will get sized correctly. |
| 14106 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize(); |
| 14107 this.$['infinite-list'].scrollTarget = this; |
| 14108 this.$['scroll-threshold'].scrollTarget = this; |
| 14109 }, |
| 14110 |
| 14111 /** |
| 14112 * Remove bookmark star for history items with matching URLs. |
| 14113 * @param {{detail: !string}} e |
| 14114 * @private |
| 14115 */ |
| 14116 removeBookmarkStars_: function(e) { |
| 14117 var url = e.detail; |
| 14118 |
| 14119 if (this.historyData_ === undefined) |
| 14120 return; |
| 14121 |
| 14122 for (var i = 0; i < this.historyData_.length; i++) { |
| 14123 if (this.historyData_[i].url == url) |
| 14124 this.set('historyData_.' + i + '.starred', false); |
| 14125 } |
| 14126 }, |
| 14127 |
| 14128 /** |
| 14129 * Disables history result loading when there are no more history results. |
| 14130 */ |
| 14131 disableResultLoading: function() { |
| 14132 this.resultLoadingDisabled_ = true; |
| 14133 }, |
| 14134 |
| 14135 /** |
| 14136 * Adds the newly updated history results into historyData_. Adds new fields |
| 14137 * for each result. |
| 14138 * @param {!Array<!HistoryEntry>} historyResults The new history results. |
| 14139 */ |
| 14140 addNewResults: function(historyResults) { |
| 14141 var results = historyResults.slice(); |
| 14142 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) |
| 14143 .clearTriggers(); |
| 14144 |
| 14145 if (this.lastSearchedTerm_ != this.searchedTerm) { |
| 14146 this.resultLoadingDisabled_ = false; |
| 14147 if (this.historyData_) |
| 14148 this.splice('historyData_', 0, this.historyData_.length); |
| 14149 this.fire('unselect-all'); |
| 14150 this.lastSearchedTerm_ = this.searchedTerm; |
| 14151 } |
| 14152 |
| 14153 if (this.historyData_) { |
| 14154 // If we have previously received data, push the new items onto the |
| 14155 // existing array. |
| 14156 results.unshift('historyData_'); |
| 14157 this.push.apply(this, results); |
| 14158 } else { |
| 14159 // The first time we receive data, use set() to ensure the iron-list is |
| 14160 // initialized correctly. |
| 14161 this.set('historyData_', results); |
| 14162 } |
| 14163 }, |
| 14164 |
| 14165 /** |
| 14166 * Called when the page is scrolled to near the bottom of the list. |
| 14167 * @private |
| 14168 */ |
| 14169 loadMoreData_: function() { |
| 14170 if (this.resultLoadingDisabled_ || this.querying) |
| 14171 return; |
| 14172 |
| 14173 this.fire('load-more-history'); |
| 14174 }, |
| 14175 |
| 14176 /** |
| 14177 * Check whether the time difference between the given history item and the |
| 14178 * next one is large enough for a spacer to be required. |
| 14179 * @param {HistoryEntry} item |
| 14180 * @param {number} index The index of |item| in |historyData_|. |
| 14181 * @param {number} length The length of |historyData_|. |
| 14182 * @return {boolean} Whether or not time gap separator is required. |
| 14183 * @private |
| 14184 */ |
| 14185 needsTimeGap_: function(item, index, length) { |
| 14186 return md_history.HistoryItem.needsTimeGap( |
| 14187 this.historyData_, index, this.searchedTerm); |
| 14188 }, |
| 14189 |
| 14190 /** |
| 14191 * True if the given item is the beginning of a new card. |
| 14192 * @param {HistoryEntry} item |
| 14193 * @param {number} i Index of |item| within |historyData_|. |
| 14194 * @param {number} length |
| 14195 * @return {boolean} |
| 14196 * @private |
| 14197 */ |
| 14198 isCardStart_: function(item, i, length) { |
| 14199 if (length == 0 || i > length - 1) |
| 14200 return false; |
| 14201 return i == 0 || |
| 14202 this.historyData_[i].dateRelativeDay != |
| 14203 this.historyData_[i - 1].dateRelativeDay; |
| 14204 }, |
| 14205 |
| 14206 /** |
| 14207 * True if the given item is the end of a card. |
| 14208 * @param {HistoryEntry} item |
| 14209 * @param {number} i Index of |item| within |historyData_|. |
| 14210 * @param {number} length |
| 14211 * @return {boolean} |
| 14212 * @private |
| 14213 */ |
| 14214 isCardEnd_: function(item, i, length) { |
| 14215 if (length == 0 || i > length - 1) |
| 14216 return false; |
| 14217 return i == length - 1 || |
| 14218 this.historyData_[i].dateRelativeDay != |
| 14219 this.historyData_[i + 1].dateRelativeDay; |
| 14220 }, |
| 14221 |
| 14222 /** |
| 14223 * @param {number} index |
| 14224 * @return {boolean} |
| 14225 * @private |
| 14226 */ |
| 14227 isFirstItem_: function(index) { |
| 14228 return index == 0; |
| 14229 }, |
| 14230 |
| 14231 /** |
| 14232 * @private |
| 14233 */ |
| 14234 notifyListScroll_: function() { |
| 14235 this.fire('history-list-scrolled'); |
| 14236 }, |
| 14237 |
| 14238 /** |
| 14239 * @param {number} index |
| 14240 * @return {string} |
| 14241 * @private |
| 14242 */ |
| 14243 pathForItem_: function(index) { |
| 14244 return 'historyData_.' + index; |
11639 }, | 14245 }, |
11640 }); | 14246 }); |
11641 // Copyright 2015 The Chromium Authors. All rights reserved. | 14247 // Copyright 2016 The Chromium Authors. All rights reserved. |
11642 // Use of this source code is governed by a BSD-style license that can be | 14248 // Use of this source code is governed by a BSD-style license that can be |
11643 // found in the LICENSE file. | 14249 // found in the LICENSE file. |
11644 | 14250 |
11645 cr.define('downloads', function() { | 14251 /** |
11646 var Toolbar = Polymer({ | 14252 * @fileoverview |
11647 is: 'downloads-toolbar', | 14253 * history-lazy-render is a simple variant of dom-if designed for lazy rendering |
11648 | 14254 * of elements that are accessed imperatively. |
11649 attached: function() { | 14255 * Usage: |
11650 // isRTL() only works after i18n_template.js runs to set <html dir>. | 14256 * <template is="history-lazy-render" id="menu"> |
11651 this.overflowAlign_ = isRTL() ? 'left' : 'right'; | 14257 * <heavy-menu></heavy-menu> |
11652 }, | 14258 * </template> |
11653 | 14259 * |
11654 properties: { | 14260 * this.$.menu.get().then(function(menu) { |
11655 downloadsShowing: { | 14261 * menu.show(); |
11656 reflectToAttribute: true, | 14262 * }); |
11657 type: Boolean, | 14263 */ |
11658 value: false, | 14264 |
11659 observer: 'downloadsShowingChanged_', | 14265 Polymer({ |
11660 }, | 14266 is: 'history-lazy-render', |
11661 | 14267 extends: 'template', |
11662 overflowAlign_: { | 14268 |
11663 type: String, | 14269 behaviors: [ |
11664 value: 'right', | 14270 Polymer.Templatizer |
11665 }, | 14271 ], |
11666 }, | 14272 |
11667 | 14273 /** @private {Promise<Element>} */ |
11668 listeners: { | 14274 _renderPromise: null, |
11669 'paper-dropdown-close': 'onPaperDropdownClose_', | 14275 |
11670 'paper-dropdown-open': 'onPaperDropdownOpen_', | 14276 /** @private {TemplateInstance} */ |
11671 }, | 14277 _instance: null, |
11672 | 14278 |
11673 /** @return {boolean} Whether removal can be undone. */ | 14279 /** |
11674 canUndo: function() { | 14280 * Stamp the template into the DOM tree asynchronously |
11675 return this.$['search-input'] != this.shadowRoot.activeElement; | 14281 * @return {Promise<Element>} Promise which resolves when the template has |
11676 }, | 14282 * been stamped. |
11677 | 14283 */ |
11678 /** @return {boolean} Whether "Clear all" should be allowed. */ | 14284 get: function() { |
11679 canClearAll: function() { | 14285 if (!this._renderPromise) { |
11680 return !this.$['search-input'].getValue() && this.downloadsShowing; | 14286 this._renderPromise = new Promise(function(resolve) { |
11681 }, | 14287 this._debounceTemplate(function() { |
11682 | 14288 this._render(); |
11683 onFindCommand: function() { | 14289 this._renderPromise = null; |
11684 this.$['search-input'].showAndFocus(); | 14290 resolve(this.getIfExists()); |
11685 }, | 14291 }.bind(this)); |
11686 | 14292 }.bind(this)); |
11687 /** @private */ | 14293 } |
11688 closeMoreActions_: function() { | 14294 return this._renderPromise; |
11689 this.$.more.close(); | 14295 }, |
11690 }, | 14296 |
11691 | 14297 /** |
11692 /** @private */ | 14298 * @return {?Element} The element contained in the template, if it has |
11693 downloadsShowingChanged_: function() { | 14299 * already been stamped. |
11694 this.updateClearAll_(); | 14300 */ |
11695 }, | 14301 getIfExists: function() { |
11696 | 14302 if (this._instance) { |
11697 /** @private */ | 14303 var children = this._instance._children; |
11698 onClearAllTap_: function() { | 14304 |
11699 assert(this.canClearAll()); | 14305 for (var i = 0; i < children.length; i++) { |
11700 downloads.ActionService.getInstance().clearAll(); | 14306 if (children[i].nodeType == Node.ELEMENT_NODE) |
11701 }, | 14307 return children[i]; |
11702 | 14308 } |
11703 /** @private */ | 14309 } |
11704 onPaperDropdownClose_: function() { | 14310 return null; |
11705 window.removeEventListener('resize', assert(this.boundClose_)); | 14311 }, |
11706 }, | 14312 |
11707 | 14313 _render: function() { |
11708 /** | 14314 if (!this.ctor) |
11709 * @param {!Event} e | 14315 this.templatize(this); |
11710 * @private | 14316 var parentNode = this.parentNode; |
11711 */ | 14317 if (parentNode && !this._instance) { |
11712 onItemBlur_: function(e) { | 14318 this._instance = /** @type {TemplateInstance} */(this.stamp({})); |
11713 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); | 14319 var root = this._instance.root; |
11714 if (menu.items.indexOf(e.relatedTarget) >= 0) | 14320 parentNode.insertBefore(root, this); |
11715 return; | 14321 } |
11716 | 14322 }, |
11717 this.$.more.restoreFocusOnClose = false; | 14323 |
11718 this.closeMoreActions_(); | 14324 /** |
11719 this.$.more.restoreFocusOnClose = true; | 14325 * @param {string} prop |
11720 }, | 14326 * @param {Object} value |
11721 | 14327 */ |
11722 /** @private */ | 14328 _forwardParentProp: function(prop, value) { |
11723 onPaperDropdownOpen_: function() { | 14329 if (this._instance) |
11724 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); | 14330 this._instance.__setProperty(prop, value, true); |
11725 window.addEventListener('resize', this.boundClose_); | 14331 }, |
11726 }, | 14332 |
11727 | 14333 /** |
11728 /** | 14334 * @param {string} path |
11729 * @param {!CustomEvent} event | 14335 * @param {Object} value |
11730 * @private | 14336 */ |
11731 */ | 14337 _forwardParentPath: function(path, value) { |
11732 onSearchChanged_: function(event) { | 14338 if (this._instance) |
11733 downloads.ActionService.getInstance().search( | 14339 this._instance._notifyPath(path, value, true); |
11734 /** @type {string} */ (event.detail)); | 14340 } |
11735 this.updateClearAll_(); | |
11736 }, | |
11737 | |
11738 /** @private */ | |
11739 onOpenDownloadsFolderTap_: function() { | |
11740 downloads.ActionService.getInstance().openDownloadsFolder(); | |
11741 }, | |
11742 | |
11743 /** @private */ | |
11744 updateClearAll_: function() { | |
11745 this.$$('#actions .clear-all').hidden = !this.canClearAll(); | |
11746 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); | |
11747 }, | |
11748 }); | |
11749 | |
11750 return {Toolbar: Toolbar}; | |
11751 }); | 14341 }); |
11752 // Copyright 2015 The Chromium Authors. All rights reserved. | 14342 // Copyright 2016 The Chromium Authors. All rights reserved. |
11753 // Use of this source code is governed by a BSD-style license that can be | 14343 // Use of this source code is governed by a BSD-style license that can be |
11754 // found in the LICENSE file. | 14344 // found in the LICENSE file. |
11755 | 14345 |
11756 cr.define('downloads', function() { | 14346 Polymer({ |
11757 var Manager = Polymer({ | 14347 is: 'history-list-container', |
11758 is: 'downloads-manager', | 14348 |
11759 | 14349 properties: { |
11760 properties: { | 14350 // The path of the currently selected page. |
11761 hasDownloads_: { | 14351 selectedPage_: String, |
11762 observer: 'hasDownloadsChanged_', | 14352 |
11763 type: Boolean, | 14353 // Whether domain-grouped history is enabled. |
11764 }, | 14354 grouped: Boolean, |
11765 | 14355 |
11766 items_: { | 14356 /** @type {!QueryState} */ |
11767 type: Array, | 14357 queryState: Object, |
11768 value: function() { return []; }, | 14358 |
11769 }, | 14359 /** @type {!QueryResult} */ |
11770 }, | 14360 queryResult: Object, |
11771 | 14361 }, |
11772 hostAttributes: { | 14362 |
11773 loading: true, | 14363 observers: [ |
11774 }, | 14364 'groupedRangeChanged_(queryState.range)', |
11775 | 14365 ], |
11776 listeners: { | 14366 |
11777 'downloads-list.scroll': 'onListScroll_', | 14367 listeners: { |
11778 }, | 14368 'history-list-scrolled': 'closeMenu_', |
11779 | 14369 'load-more-history': 'loadMoreHistory_', |
11780 observers: [ | 14370 'toggle-menu': 'toggleMenu_', |
11781 'itemsChanged_(items_.*)', | 14371 }, |
11782 ], | 14372 |
11783 | 14373 /** |
11784 /** @private */ | 14374 * @param {HistoryQuery} info An object containing information about the |
11785 clearAll_: function() { | 14375 * query. |
11786 this.set('items_', []); | 14376 * @param {!Array<HistoryEntry>} results A list of results. |
11787 }, | 14377 */ |
11788 | 14378 historyResult: function(info, results) { |
11789 /** @private */ | 14379 this.initializeResults_(info, results); |
11790 hasDownloadsChanged_: function() { | 14380 this.closeMenu_(); |
11791 if (loadTimeData.getBoolean('allowDeletingHistory')) | 14381 |
11792 this.$.toolbar.downloadsShowing = this.hasDownloads_; | 14382 if (this.selectedPage_ == 'grouped-list') { |
11793 | 14383 this.$$('#grouped-list').historyData = results; |
11794 if (this.hasDownloads_) { | 14384 return; |
11795 this.$['downloads-list'].fire('iron-resize'); | 14385 } |
11796 } else { | 14386 |
11797 var isSearching = downloads.ActionService.getInstance().isSearching(); | 14387 var list = /** @type {HistoryListElement} */(this.$['infinite-list']); |
11798 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; | 14388 list.addNewResults(results); |
11799 this.$['no-downloads'].querySelector('span').textContent = | 14389 if (info.finished) |
11800 loadTimeData.getString(messageToShow); | 14390 list.disableResultLoading(); |
11801 } | 14391 }, |
11802 }, | 14392 |
11803 | 14393 /** |
11804 /** | 14394 * Queries the history backend for results based on queryState. |
11805 * @param {number} index | 14395 * @param {boolean} incremental Whether the new query should continue where |
11806 * @param {!Array<!downloads.Data>} list | 14396 * the previous query stopped. |
11807 * @private | 14397 */ |
11808 */ | 14398 queryHistory: function(incremental) { |
11809 insertItems_: function(index, list) { | 14399 var queryState = this.queryState; |
11810 this.splice.apply(this, ['items_', index, 0].concat(list)); | 14400 // Disable querying until the first set of results have been returned. If |
11811 this.updateHideDates_(index, index + list.length); | 14401 // there is a search, query immediately to support search query params from |
11812 this.removeAttribute('loading'); | 14402 // the URL. |
11813 }, | 14403 var noResults = !this.queryResult || this.queryResult.results == null; |
11814 | 14404 if (queryState.queryingDisabled || |
11815 /** @private */ | 14405 (!this.queryState.searchTerm && noResults)) { |
11816 itemsChanged_: function() { | 14406 return; |
11817 this.hasDownloads_ = this.items_.length > 0; | 14407 } |
11818 }, | 14408 |
11819 | 14409 // Close any open dialog if a new query is initiated. |
11820 /** | 14410 var dialog = this.$.dialog.getIfExists(); |
11821 * @param {Event} e | 14411 if (!incremental && dialog && dialog.open) |
11822 * @private | 14412 dialog.close(); |
11823 */ | 14413 |
11824 onCanExecute_: function(e) { | 14414 this.set('queryState.querying', true); |
11825 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 14415 this.set('queryState.incremental', incremental); |
11826 switch (e.command.id) { | 14416 |
11827 case 'undo-command': | 14417 var lastVisitTime = 0; |
11828 e.canExecute = this.$.toolbar.canUndo(); | 14418 if (incremental) { |
11829 break; | 14419 var lastVisit = this.queryResult.results.slice(-1)[0]; |
11830 case 'clear-all-command': | 14420 lastVisitTime = lastVisit ? lastVisit.time : 0; |
11831 e.canExecute = this.$.toolbar.canClearAll(); | 14421 } |
11832 break; | 14422 |
11833 case 'find-command': | 14423 var maxResults = |
11834 e.canExecute = true; | 14424 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0; |
11835 break; | 14425 chrome.send('queryHistory', [ |
11836 } | 14426 queryState.searchTerm, queryState.groupedOffset, queryState.range, |
11837 }, | 14427 lastVisitTime, maxResults |
11838 | 14428 ]); |
11839 /** | 14429 }, |
11840 * @param {Event} e | 14430 |
11841 * @private | 14431 /** @return {number} */ |
11842 */ | 14432 getSelectedItemCount: function() { |
11843 onCommand_: function(e) { | 14433 return this.getSelectedList_().selectedPaths.size; |
11844 if (e.command.id == 'clear-all-command') | 14434 }, |
11845 downloads.ActionService.getInstance().clearAll(); | 14435 |
11846 else if (e.command.id == 'undo-command') | 14436 unselectAllItems: function(count) { |
11847 downloads.ActionService.getInstance().undo(); | 14437 this.getSelectedList_().unselectAllItems(count); |
11848 else if (e.command.id == 'find-command') | 14438 }, |
11849 this.$.toolbar.onFindCommand(); | 14439 |
11850 }, | 14440 /** |
11851 | 14441 * Delete all the currently selected history items. Will prompt the user with |
11852 /** @private */ | 14442 * a dialog to confirm that the deletion should be performed. |
11853 onListScroll_: function() { | 14443 */ |
11854 var list = this.$['downloads-list']; | 14444 deleteSelectedWithPrompt: function() { |
11855 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { | 14445 if (!loadTimeData.getBoolean('allowDeletingHistory')) |
11856 // Approaching the end of the scrollback. Attempt to load more items. | 14446 return; |
11857 downloads.ActionService.getInstance().loadMore(); | 14447 this.$.dialog.get().then(function(dialog) { |
11858 } | 14448 dialog.showModal(); |
11859 }, | 14449 }); |
11860 | 14450 }, |
11861 /** @private */ | 14451 |
11862 onLoad_: function() { | 14452 /** |
11863 cr.ui.decorate('command', cr.ui.Command); | 14453 * @param {HistoryRange} range |
11864 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 14454 * @private |
11865 document.addEventListener('command', this.onCommand_.bind(this)); | 14455 */ |
11866 | 14456 groupedRangeChanged_: function(range) { |
11867 downloads.ActionService.getInstance().loadMore(); | 14457 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? |
11868 }, | 14458 'infinite-list' : 'grouped-list'; |
11869 | 14459 |
11870 /** | 14460 this.queryHistory(false); |
11871 * @param {number} index | 14461 }, |
11872 * @private | 14462 |
11873 */ | 14463 /** @private */ |
11874 removeItem_: function(index) { | 14464 loadMoreHistory_: function() { this.queryHistory(true); }, |
11875 this.splice('items_', index, 1); | 14465 |
11876 this.updateHideDates_(index, index); | 14466 /** |
11877 this.onListScroll_(); | 14467 * @param {HistoryQuery} info |
11878 }, | 14468 * @param {!Array<HistoryEntry>} results |
11879 | 14469 * @private |
11880 /** | 14470 */ |
11881 * @param {number} start | 14471 initializeResults_: function(info, results) { |
11882 * @param {number} end | 14472 if (results.length == 0) |
11883 * @private | 14473 return; |
11884 */ | 14474 |
11885 updateHideDates_: function(start, end) { | 14475 var currentDate = results[0].dateRelativeDay; |
11886 for (var i = start; i <= end; ++i) { | 14476 |
11887 var current = this.items_[i]; | 14477 for (var i = 0; i < results.length; i++) { |
11888 if (!current) | 14478 // Sets the default values for these fields to prevent undefined types. |
11889 continue; | 14479 results[i].selected = false; |
11890 var prev = this.items_[i - 1]; | 14480 results[i].readableTimestamp = |
11891 current.hideDate = !!prev && prev.date_string == current.date_string; | 14481 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; |
11892 } | 14482 |
11893 }, | 14483 if (results[i].dateRelativeDay != currentDate) { |
11894 | 14484 currentDate = results[i].dateRelativeDay; |
11895 /** | 14485 } |
11896 * @param {number} index | 14486 } |
11897 * @param {!downloads.Data} data | 14487 }, |
11898 * @private | 14488 |
11899 */ | 14489 /** @private */ |
11900 updateItem_: function(index, data) { | 14490 onDialogConfirmTap_: function() { |
11901 this.set('items_.' + index, data); | 14491 this.getSelectedList_().deleteSelected(); |
11902 this.updateHideDates_(index, index); | 14492 var dialog = assert(this.$.dialog.getIfExists()); |
11903 var list = /** @type {!IronListElement} */(this.$['downloads-list']); | 14493 dialog.close(); |
11904 list.updateSizeForItem(index); | 14494 }, |
11905 }, | 14495 |
11906 }); | 14496 /** @private */ |
11907 | 14497 onDialogCancelTap_: function() { |
11908 Manager.clearAll = function() { | 14498 var dialog = assert(this.$.dialog.getIfExists()); |
11909 Manager.get().clearAll_(); | 14499 dialog.close(); |
11910 }; | 14500 }, |
11911 | 14501 |
11912 /** @return {!downloads.Manager} */ | 14502 /** |
11913 Manager.get = function() { | 14503 * Closes the overflow menu. |
11914 return /** @type {!downloads.Manager} */( | 14504 * @private |
11915 queryRequiredElement('downloads-manager')); | 14505 */ |
11916 }; | 14506 closeMenu_: function() { |
11917 | 14507 var menu = this.$.sharedMenu.getIfExists(); |
11918 Manager.insertItems = function(index, list) { | 14508 if (menu) |
11919 Manager.get().insertItems_(index, list); | 14509 menu.closeMenu(); |
11920 }; | 14510 }, |
11921 | 14511 |
11922 Manager.onLoad = function() { | 14512 /** |
11923 Manager.get().onLoad_(); | 14513 * Opens the overflow menu unless the menu is already open and the same button |
11924 }; | 14514 * is pressed. |
11925 | 14515 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e |
11926 Manager.removeItem = function(index) { | 14516 * @return {Promise<Element>} |
11927 Manager.get().removeItem_(index); | 14517 * @private |
11928 }; | 14518 */ |
11929 | 14519 toggleMenu_: function(e) { |
11930 Manager.updateItem = function(index, data) { | 14520 var target = e.detail.target; |
11931 Manager.get().updateItem_(index, data); | 14521 return this.$.sharedMenu.get().then(function(menu) { |
11932 }; | 14522 /** @type {CrSharedMenuElement} */(menu).toggleMenu( |
11933 | 14523 target, e.detail); |
11934 return {Manager: Manager}; | 14524 }); |
| 14525 }, |
| 14526 |
| 14527 /** @private */ |
| 14528 onMoreFromSiteTap_: function() { |
| 14529 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 14530 this.fire('search-domain', {domain: menu.itemData.item.domain}); |
| 14531 menu.closeMenu(); |
| 14532 }, |
| 14533 |
| 14534 /** @private */ |
| 14535 onRemoveFromHistoryTap_: function() { |
| 14536 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 14537 var itemData = menu.itemData; |
| 14538 md_history.BrowserService.getInstance() |
| 14539 .deleteItems([itemData.item]) |
| 14540 .then(function(items) { |
| 14541 this.getSelectedList_().removeItemsByPath([itemData.path]); |
| 14542 // This unselect-all is to reset the toolbar when deleting a selected |
| 14543 // item. TODO(tsergeant): Make this automatic based on observing list |
| 14544 // modifications. |
| 14545 this.fire('unselect-all'); |
| 14546 }.bind(this)); |
| 14547 menu.closeMenu(); |
| 14548 }, |
| 14549 |
| 14550 /** |
| 14551 * @return {HTMLElement} |
| 14552 * @private |
| 14553 */ |
| 14554 getSelectedList_: function() { |
| 14555 return this.$.content.selectedItem; |
| 14556 }, |
11935 }); | 14557 }); |
11936 // Copyright 2015 The Chromium Authors. All rights reserved. | 14558 // Copyright 2016 The Chromium Authors. All rights reserved. |
11937 // Use of this source code is governed by a BSD-style license that can be | 14559 // Use of this source code is governed by a BSD-style license that can be |
11938 // found in the LICENSE file. | 14560 // found in the LICENSE file. |
11939 | 14561 |
11940 window.addEventListener('load', downloads.Manager.onLoad); | 14562 Polymer({ |
| 14563 is: 'history-synced-device-card', |
| 14564 |
| 14565 properties: { |
| 14566 // Name of the synced device. |
| 14567 device: String, |
| 14568 |
| 14569 // When the device information was last updated. |
| 14570 lastUpdateTime: String, |
| 14571 |
| 14572 /** |
| 14573 * The list of tabs open for this device. |
| 14574 * @type {!Array<!ForeignSessionTab>} |
| 14575 */ |
| 14576 tabs: { |
| 14577 type: Array, |
| 14578 value: function() { return []; }, |
| 14579 observer: 'updateIcons_' |
| 14580 }, |
| 14581 |
| 14582 /** |
| 14583 * The indexes where a window separator should be shown. The use of a |
| 14584 * separate array here is necessary for window separators to appear |
| 14585 * correctly in search. See http://crrev.com/2022003002 for more details. |
| 14586 * @type {!Array<number>} |
| 14587 */ |
| 14588 separatorIndexes: Array, |
| 14589 |
| 14590 // Whether the card is open. |
| 14591 cardOpen_: {type: Boolean, value: true}, |
| 14592 |
| 14593 searchTerm: String, |
| 14594 |
| 14595 // Internal identifier for the device. |
| 14596 sessionTag: String, |
| 14597 }, |
| 14598 |
| 14599 /** |
| 14600 * Open a single synced tab. Listens to 'click' rather than 'tap' |
| 14601 * to determine what modifier keys were pressed. |
| 14602 * @param {DomRepeatClickEvent} e |
| 14603 * @private |
| 14604 */ |
| 14605 openTab_: function(e) { |
| 14606 var tab = /** @type {ForeignSessionTab} */(e.model.tab); |
| 14607 md_history.BrowserService.getInstance().openForeignSessionTab( |
| 14608 this.sessionTag, tab.windowId, tab.sessionId, e); |
| 14609 e.preventDefault(); |
| 14610 }, |
| 14611 |
| 14612 /** |
| 14613 * Toggles the dropdown display of synced tabs for each device card. |
| 14614 */ |
| 14615 toggleTabCard: function() { |
| 14616 this.$.collapse.toggle(); |
| 14617 this.$['dropdown-indicator'].icon = |
| 14618 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; |
| 14619 }, |
| 14620 |
| 14621 /** |
| 14622 * When the synced tab information is set, the icon associated with the tab |
| 14623 * website is also set. |
| 14624 * @private |
| 14625 */ |
| 14626 updateIcons_: function() { |
| 14627 this.async(function() { |
| 14628 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); |
| 14629 |
| 14630 for (var i = 0; i < this.tabs.length; i++) { |
| 14631 icons[i].style.backgroundImage = |
| 14632 cr.icon.getFaviconImageSet(this.tabs[i].url); |
| 14633 } |
| 14634 }); |
| 14635 }, |
| 14636 |
| 14637 /** @private */ |
| 14638 isWindowSeparatorIndex_: function(index, separatorIndexes) { |
| 14639 return this.separatorIndexes.indexOf(index) != -1; |
| 14640 }, |
| 14641 |
| 14642 /** |
| 14643 * @param {boolean} cardOpen |
| 14644 * @return {string} |
| 14645 */ |
| 14646 getCollapseTitle_: function(cardOpen) { |
| 14647 return cardOpen ? loadTimeData.getString('collapseSessionButton') : |
| 14648 loadTimeData.getString('expandSessionButton'); |
| 14649 }, |
| 14650 |
| 14651 /** |
| 14652 * @param {CustomEvent} e |
| 14653 * @private |
| 14654 */ |
| 14655 onMenuButtonTap_: function(e) { |
| 14656 this.fire('toggle-menu', { |
| 14657 target: Polymer.dom(e).localTarget, |
| 14658 tag: this.sessionTag |
| 14659 }); |
| 14660 e.stopPropagation(); // Prevent iron-collapse. |
| 14661 }, |
| 14662 }); |
| 14663 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14664 // Use of this source code is governed by a BSD-style license that can be |
| 14665 // found in the LICENSE file. |
| 14666 |
| 14667 /** |
| 14668 * @typedef {{device: string, |
| 14669 * lastUpdateTime: string, |
| 14670 * separatorIndexes: !Array<number>, |
| 14671 * timestamp: number, |
| 14672 * tabs: !Array<!ForeignSessionTab>, |
| 14673 * tag: string}} |
| 14674 */ |
| 14675 var ForeignDeviceInternal; |
| 14676 |
| 14677 Polymer({ |
| 14678 is: 'history-synced-device-manager', |
| 14679 |
| 14680 properties: { |
| 14681 /** |
| 14682 * @type {?Array<!ForeignSession>} |
| 14683 */ |
| 14684 sessionList: { |
| 14685 type: Array, |
| 14686 observer: 'updateSyncedDevices' |
| 14687 }, |
| 14688 |
| 14689 searchTerm: { |
| 14690 type: String, |
| 14691 observer: 'searchTermChanged' |
| 14692 }, |
| 14693 |
| 14694 /** |
| 14695 * An array of synced devices with synced tab data. |
| 14696 * @type {!Array<!ForeignDeviceInternal>} |
| 14697 */ |
| 14698 syncedDevices_: { |
| 14699 type: Array, |
| 14700 value: function() { return []; } |
| 14701 }, |
| 14702 |
| 14703 /** @private */ |
| 14704 signInState_: { |
| 14705 type: Boolean, |
| 14706 value: loadTimeData.getBoolean('isUserSignedIn'), |
| 14707 }, |
| 14708 |
| 14709 /** @private */ |
| 14710 guestSession_: { |
| 14711 type: Boolean, |
| 14712 value: loadTimeData.getBoolean('isGuestSession'), |
| 14713 }, |
| 14714 |
| 14715 /** @private */ |
| 14716 fetchingSyncedTabs_: { |
| 14717 type: Boolean, |
| 14718 value: false, |
| 14719 } |
| 14720 }, |
| 14721 |
| 14722 listeners: { |
| 14723 'toggle-menu': 'onToggleMenu_', |
| 14724 }, |
| 14725 |
| 14726 /** @override */ |
| 14727 attached: function() { |
| 14728 // Update the sign in state. |
| 14729 chrome.send('otherDevicesInitialized'); |
| 14730 }, |
| 14731 |
| 14732 /** |
| 14733 * @param {!ForeignSession} session |
| 14734 * @return {!ForeignDeviceInternal} |
| 14735 */ |
| 14736 createInternalDevice_: function(session) { |
| 14737 var tabs = []; |
| 14738 var separatorIndexes = []; |
| 14739 for (var i = 0; i < session.windows.length; i++) { |
| 14740 var windowId = session.windows[i].sessionId; |
| 14741 var newTabs = session.windows[i].tabs; |
| 14742 if (newTabs.length == 0) |
| 14743 continue; |
| 14744 |
| 14745 newTabs.forEach(function(tab) { |
| 14746 tab.windowId = windowId; |
| 14747 }); |
| 14748 |
| 14749 var windowAdded = false; |
| 14750 if (!this.searchTerm) { |
| 14751 // Add all the tabs if there is no search term. |
| 14752 tabs = tabs.concat(newTabs); |
| 14753 windowAdded = true; |
| 14754 } else { |
| 14755 var searchText = this.searchTerm.toLowerCase(); |
| 14756 for (var j = 0; j < newTabs.length; j++) { |
| 14757 var tab = newTabs[j]; |
| 14758 if (tab.title.toLowerCase().indexOf(searchText) != -1) { |
| 14759 tabs.push(tab); |
| 14760 windowAdded = true; |
| 14761 } |
| 14762 } |
| 14763 } |
| 14764 if (windowAdded && i != session.windows.length - 1) |
| 14765 separatorIndexes.push(tabs.length - 1); |
| 14766 } |
| 14767 return { |
| 14768 device: session.name, |
| 14769 lastUpdateTime: '– ' + session.modifiedTime, |
| 14770 separatorIndexes: separatorIndexes, |
| 14771 timestamp: session.timestamp, |
| 14772 tabs: tabs, |
| 14773 tag: session.tag, |
| 14774 }; |
| 14775 }, |
| 14776 |
| 14777 onSignInTap_: function() { |
| 14778 chrome.send('startSignInFlow'); |
| 14779 }, |
| 14780 |
| 14781 onToggleMenu_: function(e) { |
| 14782 this.$.menu.get().then(function(menu) { |
| 14783 menu.toggleMenu(e.detail.target, e.detail.tag); |
| 14784 }); |
| 14785 }, |
| 14786 |
| 14787 onOpenAllTap_: function() { |
| 14788 var menu = assert(this.$.menu.getIfExists()); |
| 14789 md_history.BrowserService.getInstance().openForeignSessionAllTabs( |
| 14790 menu.itemData); |
| 14791 menu.closeMenu(); |
| 14792 }, |
| 14793 |
| 14794 onDeleteSessionTap_: function() { |
| 14795 var menu = assert(this.$.menu.getIfExists()); |
| 14796 md_history.BrowserService.getInstance().deleteForeignSession( |
| 14797 menu.itemData); |
| 14798 menu.closeMenu(); |
| 14799 }, |
| 14800 |
| 14801 /** @private */ |
| 14802 clearDisplayedSyncedDevices_: function() { |
| 14803 this.syncedDevices_ = []; |
| 14804 }, |
| 14805 |
| 14806 /** |
| 14807 * Decide whether or not should display no synced tabs message. |
| 14808 * @param {boolean} signInState |
| 14809 * @param {number} syncedDevicesLength |
| 14810 * @param {boolean} guestSession |
| 14811 * @return {boolean} |
| 14812 */ |
| 14813 showNoSyncedMessage: function( |
| 14814 signInState, syncedDevicesLength, guestSession) { |
| 14815 if (guestSession) |
| 14816 return true; |
| 14817 |
| 14818 return signInState && syncedDevicesLength == 0; |
| 14819 }, |
| 14820 |
| 14821 /** |
| 14822 * Shows the signin guide when the user is not signed in and not in a guest |
| 14823 * session. |
| 14824 * @param {boolean} signInState |
| 14825 * @param {boolean} guestSession |
| 14826 * @return {boolean} |
| 14827 */ |
| 14828 showSignInGuide: function(signInState, guestSession) { |
| 14829 var show = !signInState && !guestSession; |
| 14830 if (show) { |
| 14831 md_history.BrowserService.getInstance().recordAction( |
| 14832 'Signin_Impression_FromRecentTabs'); |
| 14833 } |
| 14834 |
| 14835 return show; |
| 14836 }, |
| 14837 |
| 14838 /** |
| 14839 * Decide what message should be displayed when user is logged in and there |
| 14840 * are no synced tabs. |
| 14841 * @param {boolean} fetchingSyncedTabs |
| 14842 * @return {string} |
| 14843 */ |
| 14844 noSyncedTabsMessage: function(fetchingSyncedTabs) { |
| 14845 return loadTimeData.getString( |
| 14846 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); |
| 14847 }, |
| 14848 |
| 14849 /** |
| 14850 * Replaces the currently displayed synced tabs with |sessionList|. It is |
| 14851 * common for only a single session within the list to have changed, We try to |
| 14852 * avoid doing extra work in this case. The logic could be more intelligent |
| 14853 * about updating individual tabs rather than replacing whole sessions, but |
| 14854 * this approach seems to have acceptable performance. |
| 14855 * @param {?Array<!ForeignSession>} sessionList |
| 14856 */ |
| 14857 updateSyncedDevices: function(sessionList) { |
| 14858 this.fetchingSyncedTabs_ = false; |
| 14859 |
| 14860 if (!sessionList) |
| 14861 return; |
| 14862 |
| 14863 // First, update any existing devices that have changed. |
| 14864 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); |
| 14865 for (var i = 0; i < updateCount; i++) { |
| 14866 var oldDevice = this.syncedDevices_[i]; |
| 14867 if (oldDevice.tag != sessionList[i].tag || |
| 14868 oldDevice.timestamp != sessionList[i].timestamp) { |
| 14869 this.splice( |
| 14870 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); |
| 14871 } |
| 14872 } |
| 14873 |
| 14874 // Then, append any new devices. |
| 14875 for (var i = updateCount; i < sessionList.length; i++) { |
| 14876 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); |
| 14877 } |
| 14878 }, |
| 14879 |
| 14880 /** |
| 14881 * End fetching synced tabs when sync is disabled. |
| 14882 */ |
| 14883 tabSyncDisabled: function() { |
| 14884 this.fetchingSyncedTabs_ = false; |
| 14885 this.clearDisplayedSyncedDevices_(); |
| 14886 }, |
| 14887 |
| 14888 /** |
| 14889 * Get called when user's sign in state changes, this will affect UI of synced |
| 14890 * tabs page. Sign in promo gets displayed when user is signed out, and |
| 14891 * different messages are shown when there are no synced tabs. |
| 14892 * @param {boolean} isUserSignedIn |
| 14893 */ |
| 14894 updateSignInState: function(isUserSignedIn) { |
| 14895 // If user's sign in state didn't change, then don't change message or |
| 14896 // update UI. |
| 14897 if (this.signInState_ == isUserSignedIn) |
| 14898 return; |
| 14899 |
| 14900 this.signInState_ = isUserSignedIn; |
| 14901 |
| 14902 // User signed out, clear synced device list and show the sign in promo. |
| 14903 if (!isUserSignedIn) { |
| 14904 this.clearDisplayedSyncedDevices_(); |
| 14905 return; |
| 14906 } |
| 14907 // User signed in, show the loading message when querying for synced |
| 14908 // devices. |
| 14909 this.fetchingSyncedTabs_ = true; |
| 14910 }, |
| 14911 |
| 14912 searchTermChanged: function(searchTerm) { |
| 14913 this.clearDisplayedSyncedDevices_(); |
| 14914 this.updateSyncedDevices(this.sessionList); |
| 14915 } |
| 14916 }); |
| 14917 /** |
| 14918 `iron-selector` is an element which can be used to manage a list of elements |
| 14919 that can be selected. Tapping on the item will make the item selected. The `
selected` indicates |
| 14920 which item is being selected. The default is to use the index of the item. |
| 14921 |
| 14922 Example: |
| 14923 |
| 14924 <iron-selector selected="0"> |
| 14925 <div>Item 1</div> |
| 14926 <div>Item 2</div> |
| 14927 <div>Item 3</div> |
| 14928 </iron-selector> |
| 14929 |
| 14930 If you want to use the attribute value of an element for `selected` instead of
the index, |
| 14931 set `attrForSelected` to the name of the attribute. For example, if you want
to select item by |
| 14932 `name`, set `attrForSelected` to `name`. |
| 14933 |
| 14934 Example: |
| 14935 |
| 14936 <iron-selector attr-for-selected="name" selected="foo"> |
| 14937 <div name="foo">Foo</div> |
| 14938 <div name="bar">Bar</div> |
| 14939 <div name="zot">Zot</div> |
| 14940 </iron-selector> |
| 14941 |
| 14942 You can specify a default fallback with `fallbackSelection` in case the `selec
ted` attribute does |
| 14943 not match the `attrForSelected` attribute of any elements. |
| 14944 |
| 14945 Example: |
| 14946 |
| 14947 <iron-selector attr-for-selected="name" selected="non-existing" |
| 14948 fallback-selection="default"> |
| 14949 <div name="foo">Foo</div> |
| 14950 <div name="bar">Bar</div> |
| 14951 <div name="default">Default</div> |
| 14952 </iron-selector> |
| 14953 |
| 14954 Note: When the selector is multi, the selection will set to `fallbackSelection
` iff |
| 14955 the number of matching elements is zero. |
| 14956 |
| 14957 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the
selected element. |
| 14958 |
| 14959 Example: |
| 14960 |
| 14961 <style> |
| 14962 .iron-selected { |
| 14963 background: #eee; |
| 14964 } |
| 14965 </style> |
| 14966 |
| 14967 ... |
| 14968 |
| 14969 <iron-selector selected="0"> |
| 14970 <div>Item 1</div> |
| 14971 <div>Item 2</div> |
| 14972 <div>Item 3</div> |
| 14973 </iron-selector> |
| 14974 |
| 14975 @demo demo/index.html |
| 14976 */ |
| 14977 |
| 14978 Polymer({ |
| 14979 |
| 14980 is: 'iron-selector', |
| 14981 |
| 14982 behaviors: [ |
| 14983 Polymer.IronMultiSelectableBehavior |
| 14984 ] |
| 14985 |
| 14986 }); |
| 14987 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 14988 // Use of this source code is governed by a BSD-style license that can be |
| 14989 // found in the LICENSE file. |
| 14990 |
| 14991 Polymer({ |
| 14992 is: 'history-side-bar', |
| 14993 |
| 14994 properties: { |
| 14995 selectedPage: { |
| 14996 type: String, |
| 14997 notify: true |
| 14998 }, |
| 14999 |
| 15000 route: Object, |
| 15001 |
| 15002 showFooter: Boolean, |
| 15003 |
| 15004 // If true, the sidebar is contained within an app-drawer. |
| 15005 drawer: { |
| 15006 type: Boolean, |
| 15007 reflectToAttribute: true |
| 15008 }, |
| 15009 }, |
| 15010 |
| 15011 /** @private */ |
| 15012 onSelectorActivate_: function() { |
| 15013 this.fire('history-close-drawer'); |
| 15014 }, |
| 15015 |
| 15016 /** |
| 15017 * Relocates the user to the clear browsing data section of the settings page. |
| 15018 * @param {Event} e |
| 15019 * @private |
| 15020 */ |
| 15021 onClearBrowsingDataTap_: function(e) { |
| 15022 md_history.BrowserService.getInstance().openClearBrowsingData(); |
| 15023 e.preventDefault(); |
| 15024 }, |
| 15025 |
| 15026 /** |
| 15027 * @param {Object} route |
| 15028 * @private |
| 15029 */ |
| 15030 getQueryString_: function(route) { |
| 15031 return window.location.search; |
| 15032 } |
| 15033 }); |
| 15034 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 15035 // Use of this source code is governed by a BSD-style license that can be |
| 15036 // found in the LICENSE file. |
| 15037 |
| 15038 Polymer({ |
| 15039 is: 'history-app', |
| 15040 |
| 15041 properties: { |
| 15042 showSidebarFooter: Boolean, |
| 15043 |
| 15044 // The id of the currently selected page. |
| 15045 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, |
| 15046 |
| 15047 // Whether domain-grouped history is enabled. |
| 15048 grouped_: {type: Boolean, reflectToAttribute: true}, |
| 15049 |
| 15050 /** @type {!QueryState} */ |
| 15051 queryState_: { |
| 15052 type: Object, |
| 15053 value: function() { |
| 15054 return { |
| 15055 // Whether the most recent query was incremental. |
| 15056 incremental: false, |
| 15057 // A query is initiated by page load. |
| 15058 querying: true, |
| 15059 queryingDisabled: false, |
| 15060 _range: HistoryRange.ALL_TIME, |
| 15061 searchTerm: '', |
| 15062 // TODO(calamity): Make history toolbar buttons change the offset |
| 15063 groupedOffset: 0, |
| 15064 |
| 15065 set range(val) { this._range = Number(val); }, |
| 15066 get range() { return this._range; }, |
| 15067 }; |
| 15068 } |
| 15069 }, |
| 15070 |
| 15071 /** @type {!QueryResult} */ |
| 15072 queryResult_: { |
| 15073 type: Object, |
| 15074 value: function() { |
| 15075 return { |
| 15076 info: null, |
| 15077 results: null, |
| 15078 sessionList: null, |
| 15079 }; |
| 15080 } |
| 15081 }, |
| 15082 |
| 15083 // Route data for the current page. |
| 15084 routeData_: Object, |
| 15085 |
| 15086 // The query params for the page. |
| 15087 queryParams_: Object, |
| 15088 |
| 15089 // True if the window is narrow enough for the page to have a drawer. |
| 15090 hasDrawer_: Boolean, |
| 15091 }, |
| 15092 |
| 15093 observers: [ |
| 15094 // routeData_.page <=> selectedPage |
| 15095 'routeDataChanged_(routeData_.page)', |
| 15096 'selectedPageChanged_(selectedPage_)', |
| 15097 |
| 15098 // queryParams_.q <=> queryState.searchTerm |
| 15099 'searchTermChanged_(queryState_.searchTerm)', |
| 15100 'searchQueryParamChanged_(queryParams_.q)', |
| 15101 |
| 15102 ], |
| 15103 |
| 15104 // TODO(calamity): Replace these event listeners with data bound properties. |
| 15105 listeners: { |
| 15106 'cr-menu-tap': 'onMenuTap_', |
| 15107 'history-checkbox-select': 'checkboxSelected', |
| 15108 'unselect-all': 'unselectAll', |
| 15109 'delete-selected': 'deleteSelected', |
| 15110 'search-domain': 'searchDomain_', |
| 15111 'history-close-drawer': 'closeDrawer_', |
| 15112 }, |
| 15113 |
| 15114 /** @override */ |
| 15115 ready: function() { |
| 15116 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); |
| 15117 |
| 15118 cr.ui.decorate('command', cr.ui.Command); |
| 15119 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
| 15120 document.addEventListener('command', this.onCommand_.bind(this)); |
| 15121 |
| 15122 // Redirect legacy search URLs to URLs compatible with material history. |
| 15123 if (window.location.hash) { |
| 15124 window.location.href = window.location.href.split('#')[0] + '?' + |
| 15125 window.location.hash.substr(1); |
| 15126 } |
| 15127 }, |
| 15128 |
| 15129 /** @private */ |
| 15130 onMenuTap_: function() { |
| 15131 var drawer = this.$$('#drawer'); |
| 15132 if (drawer) |
| 15133 drawer.toggle(); |
| 15134 }, |
| 15135 |
| 15136 /** |
| 15137 * Listens for history-item being selected or deselected (through checkbox) |
| 15138 * and changes the view of the top toolbar. |
| 15139 * @param {{detail: {countAddition: number}}} e |
| 15140 */ |
| 15141 checkboxSelected: function(e) { |
| 15142 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); |
| 15143 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history) |
| 15144 .getSelectedItemCount(); |
| 15145 }, |
| 15146 |
| 15147 /** |
| 15148 * Listens for call to cancel selection and loops through all items to set |
| 15149 * checkbox to be unselected. |
| 15150 * @private |
| 15151 */ |
| 15152 unselectAll: function() { |
| 15153 var listContainer = |
| 15154 /** @type {HistoryListContainerElement} */ (this.$.history); |
| 15155 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); |
| 15156 listContainer.unselectAllItems(toolbar.count); |
| 15157 toolbar.count = 0; |
| 15158 }, |
| 15159 |
| 15160 deleteSelected: function() { |
| 15161 this.$.history.deleteSelectedWithPrompt(); |
| 15162 }, |
| 15163 |
| 15164 /** |
| 15165 * @param {HistoryQuery} info An object containing information about the |
| 15166 * query. |
| 15167 * @param {!Array<HistoryEntry>} results A list of results. |
| 15168 */ |
| 15169 historyResult: function(info, results) { |
| 15170 this.set('queryState_.querying', false); |
| 15171 this.set('queryResult_.info', info); |
| 15172 this.set('queryResult_.results', results); |
| 15173 var listContainer = |
| 15174 /** @type {HistoryListContainerElement} */ (this.$['history']); |
| 15175 listContainer.historyResult(info, results); |
| 15176 }, |
| 15177 |
| 15178 /** |
| 15179 * Fired when the user presses 'More from this site'. |
| 15180 * @param {{detail: {domain: string}}} e |
| 15181 */ |
| 15182 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); }, |
| 15183 |
| 15184 /** |
| 15185 * @param {Event} e |
| 15186 * @private |
| 15187 */ |
| 15188 onCanExecute_: function(e) { |
| 15189 e = /** @type {cr.ui.CanExecuteEvent} */(e); |
| 15190 switch (e.command.id) { |
| 15191 case 'find-command': |
| 15192 e.canExecute = true; |
| 15193 break; |
| 15194 case 'slash-command': |
| 15195 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); |
| 15196 break; |
| 15197 case 'delete-command': |
| 15198 e.canExecute = this.$.toolbar.count > 0; |
| 15199 break; |
| 15200 } |
| 15201 }, |
| 15202 |
| 15203 /** |
| 15204 * @param {string} searchTerm |
| 15205 * @private |
| 15206 */ |
| 15207 searchTermChanged_: function(searchTerm) { |
| 15208 this.set('queryParams_.q', searchTerm || null); |
| 15209 this.$['history'].queryHistory(false); |
| 15210 }, |
| 15211 |
| 15212 /** |
| 15213 * @param {string} searchQuery |
| 15214 * @private |
| 15215 */ |
| 15216 searchQueryParamChanged_: function(searchQuery) { |
| 15217 this.$.toolbar.setSearchTerm(searchQuery || ''); |
| 15218 }, |
| 15219 |
| 15220 /** |
| 15221 * @param {Event} e |
| 15222 * @private |
| 15223 */ |
| 15224 onCommand_: function(e) { |
| 15225 if (e.command.id == 'find-command' || e.command.id == 'slash-command') |
| 15226 this.$.toolbar.showSearchField(); |
| 15227 if (e.command.id == 'delete-command') |
| 15228 this.deleteSelected(); |
| 15229 }, |
| 15230 |
| 15231 /** |
| 15232 * @param {!Array<!ForeignSession>} sessionList Array of objects describing |
| 15233 * the sessions from other devices. |
| 15234 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile? |
| 15235 */ |
| 15236 setForeignSessions: function(sessionList, isTabSyncEnabled) { |
| 15237 if (!isTabSyncEnabled) { |
| 15238 var syncedDeviceManagerElem = |
| 15239 /** @type {HistorySyncedDeviceManagerElement} */this |
| 15240 .$$('history-synced-device-manager'); |
| 15241 if (syncedDeviceManagerElem) |
| 15242 syncedDeviceManagerElem.tabSyncDisabled(); |
| 15243 return; |
| 15244 } |
| 15245 |
| 15246 this.set('queryResult_.sessionList', sessionList); |
| 15247 }, |
| 15248 |
| 15249 /** |
| 15250 * Update sign in state of synced device manager after user logs in or out. |
| 15251 * @param {boolean} isUserSignedIn |
| 15252 */ |
| 15253 updateSignInState: function(isUserSignedIn) { |
| 15254 var syncedDeviceManagerElem = |
| 15255 /** @type {HistorySyncedDeviceManagerElement} */this |
| 15256 .$$('history-synced-device-manager'); |
| 15257 if (syncedDeviceManagerElem) |
| 15258 syncedDeviceManagerElem.updateSignInState(isUserSignedIn); |
| 15259 }, |
| 15260 |
| 15261 /** |
| 15262 * @param {string} selectedPage |
| 15263 * @return {boolean} |
| 15264 * @private |
| 15265 */ |
| 15266 syncedTabsSelected_: function(selectedPage) { |
| 15267 return selectedPage == 'syncedTabs'; |
| 15268 }, |
| 15269 |
| 15270 /** |
| 15271 * @param {boolean} querying |
| 15272 * @param {boolean} incremental |
| 15273 * @param {string} searchTerm |
| 15274 * @return {boolean} Whether a loading spinner should be shown (implies the |
| 15275 * backend is querying a new search term). |
| 15276 * @private |
| 15277 */ |
| 15278 shouldShowSpinner_: function(querying, incremental, searchTerm) { |
| 15279 return querying && !incremental && searchTerm != ''; |
| 15280 }, |
| 15281 |
| 15282 /** |
| 15283 * @param {string} page |
| 15284 * @private |
| 15285 */ |
| 15286 routeDataChanged_: function(page) { |
| 15287 this.selectedPage_ = page; |
| 15288 }, |
| 15289 |
| 15290 /** |
| 15291 * @param {string} selectedPage |
| 15292 * @private |
| 15293 */ |
| 15294 selectedPageChanged_: function(selectedPage) { |
| 15295 this.set('routeData_.page', selectedPage); |
| 15296 }, |
| 15297 |
| 15298 /** |
| 15299 * This computed binding is needed to make the iron-pages selector update when |
| 15300 * the synced-device-manager is instantiated for the first time. Otherwise the |
| 15301 * fallback selection will continue to be used after the corresponding item is |
| 15302 * added as a child of iron-pages. |
| 15303 * @param {string} selectedPage |
| 15304 * @param {Array} items |
| 15305 * @return {string} |
| 15306 * @private |
| 15307 */ |
| 15308 getSelectedPage_: function(selectedPage, items) { |
| 15309 return selectedPage; |
| 15310 }, |
| 15311 |
| 15312 /** @private */ |
| 15313 closeDrawer_: function() { |
| 15314 var drawer = this.$$('#drawer'); |
| 15315 if (drawer) |
| 15316 drawer.close(); |
| 15317 }, |
| 15318 }); |
OLD | NEW |