Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(692)

Side by Side Diff: chrome/browser/resources/md_history/app.crisper.js

Issue 2224003003: Vulcanize MD History to improve page-load performance (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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, '&amp;') 2030 /**
1514 .replace(/</g, '&lt;') 2031 * A model representing the deserialized path through the route tree, as
1515 .replace(/>/g, '&gt;') 2032 * well as the current queryParams.
1516 .replace(/"/g, '&quot;') 2033 */
1517 .replace(/'/g, '&#39;'); 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
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
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
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
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
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 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/md_history/app.js ('k') | chrome/browser/resources/md_history/app.vulcanized.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698