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

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: Change to keyword arguments 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 document.addEventListener('click', function(e) { 1612 */
1364 if (e.defaultPrevented) 1613 dwellTime: {
1365 return; 1614 type: Number,
1366 1615 value: 2000
1367 var el = e.target; 1616 },
1368 if (el.nodeType == Node.ELEMENT_NODE && 1617
1369 el.webkitMatchesSelector('A, A *')) { 1618 /**
1370 while (el.tagName != 'A') { 1619 * A regexp that defines the set of URLs that should be considered part
1371 el = el.parentElement; 1620 * of this web app.
1372 } 1621 *
1373 1622 * Clicking on a link that matches this regex won't result in a full pag e
1374 if ((el.protocol == 'file:' || el.protocol == 'about:') && 1623 * navigation, but will instead just update the URL state in place.
1375 (e.button == 0 || e.button == 1)) { 1624 *
1376 chrome.send('navigateToUrl', [ 1625 * This regexp is given everything after the origin in an absolute
1377 el.href, 1626 * URL. So to match just URLs that start with /search/ do:
1378 el.target, 1627 * url-space-regex="^/search/"
1379 e.button, 1628 *
1380 e.altKey, 1629 * @type {string|RegExp}
1381 e.ctrlKey, 1630 */
1382 e.metaKey, 1631 urlSpaceRegex: {
1383 e.shiftKey 1632 type: String,
1384 ]); 1633 value: ''
1385 e.preventDefault(); 1634 },
1386 } 1635
1387 } 1636 /**
1388 }); 1637 * urlSpaceRegex, but coerced into a regexp.
1389 1638 *
1390 /** 1639 * @type {RegExp}
1391 * Creates a new URL which is the old URL with a GET param of key=value. 1640 */
1392 * @param {string} url The base URL. There is not sanity checking on the URL so 1641 _urlSpaceRegExp: {
1393 * it must be passed in a proper format. 1642 computed: '_makeRegExp(urlSpaceRegex)'
1394 * @param {string} key The key of the param. 1643 },
1395 * @param {string} value The value of the param. 1644
1396 * @return {string} The new URL. 1645 _lastChangedAt: {
1397 */ 1646 type: Number
1398 function appendParam(url, key, value) { 1647 },
1399 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); 1648
1400 1649 _initialized: {
1401 if (url.indexOf('?') == -1) 1650 type: Boolean,
1402 return url + '?' + param; 1651 value: false
1403 return url + '&' + param; 1652 }
1404 } 1653 },
1405 1654 hostAttributes: {
1406 /** 1655 hidden: true
1407 * Creates an element of a specified type with a specified class name. 1656 },
1408 * @param {string} type The node type. 1657 observers: [
1409 * @param {string} className The class name to use. 1658 '_updateUrl(path, query, hash)'
1410 * @return {Element} The created element. 1659 ],
1411 */ 1660 attached: function() {
1412 function createElementWithClassName(type, className) { 1661 this.listen(window, 'hashchange', '_hashChanged');
1413 var elm = document.createElement(type); 1662 this.listen(window, 'location-changed', '_urlChanged');
1414 elm.className = className; 1663 this.listen(window, 'popstate', '_urlChanged');
1415 return elm; 1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g lobalOnClick');
1416 } 1665 // Give a 200ms grace period to make initial redirects without any
1417 1666 // additions to the user's history.
1418 /** 1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
1419 * webkitTransitionEnd does not always fire (e.g. when animation is aborted 1668
1420 * or when no paint happens during the animation). This function sets up 1669 this._initialized = true;
1421 * a timer and emulate the event if it is not fired when the timer expires. 1670 this._urlChanged();
1422 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. 1671 },
1423 * @param {number=} opt_timeOut The maximum wait time in milliseconds for the 1672 detached: function() {
1424 * webkitTransitionEnd to happen. If not specified, it is fetched from |el| 1673 this.unlisten(window, 'hashchange', '_hashChanged');
1425 * using the transitionDuration style value. 1674 this.unlisten(window, 'location-changed', '_urlChanged');
1426 */ 1675 this.unlisten(window, 'popstate', '_urlChanged');
1427 function ensureTransitionEndEvent(el, opt_timeOut) { 1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', ' _globalOnClick');
1428 if (opt_timeOut === undefined) { 1677 this._initialized = false;
1429 var style = getComputedStyle(el); 1678 },
1430 opt_timeOut = parseFloat(style.transitionDuration) * 1000; 1679 _hashChanged: function() {
1431 1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1)) ;
1432 // Give an additional 50ms buffer for the animation to complete. 1681 },
1433 opt_timeOut += 50; 1682 _urlChanged: function() {
1434 } 1683 // We want to extract all info out of the updated URL before we
1435 1684 // try to write anything back into it.
1436 var fired = false; 1685 //
1437 el.addEventListener('webkitTransitionEnd', function f(e) { 1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
1438 el.removeEventListener('webkitTransitionEnd', f); 1687 // one when we set this.hash. Likewise for query.
1439 fired = true; 1688 this._dontUpdateUrl = true;
1440 }); 1689 this._hashChanged();
1441 window.setTimeout(function() { 1690 this.path = window.decodeURIComponent(window.location.pathname);
1442 if (!fired) 1691 this.query = window.decodeURIComponent(
1443 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); 1692 window.location.search.substring(1));
1444 }, opt_timeOut); 1693 this._dontUpdateUrl = false;
1445 } 1694 this._updateUrl();
1446 1695 },
1447 /** 1696 _getUrl: function() {
1448 * Alias for document.scrollTop getter. 1697 var partiallyEncodedPath = window.encodeURI(
1449 * @param {!HTMLDocument} doc The document node where information will be 1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
1450 * queried from. 1699 var partiallyEncodedQuery = '';
1451 * @return {number} The Y document scroll offset. 1700 if (this.query) {
1452 */ 1701 partiallyEncodedQuery = '?' + window.encodeURI(
1453 function scrollTopForDocument(doc) { 1702 this.query).replace(/\#/g, '%23');
1454 return doc.documentElement.scrollTop || doc.body.scrollTop; 1703 }
1455 } 1704 var partiallyEncodedHash = '';
1456 1705 if (this.hash) {
1457 /** 1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash);
1458 * Alias for document.scrollTop setter. 1707 }
1459 * @param {!HTMLDocument} doc The document node where information will be 1708 return (
1460 * queried from. 1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash) ;
1461 * @param {number} value The target Y scroll offset. 1710 },
1462 */ 1711 _updateUrl: function() {
1463 function setScrollTopForDocument(doc, value) { 1712 if (this._dontUpdateUrl || !this._initialized) {
1464 doc.documentElement.scrollTop = doc.body.scrollTop = value; 1713 return;
1465 } 1714 }
1466 1715 if (this.path === window.decodeURIComponent(window.location.pathname) &&
1467 /** 1716 this.query === window.decodeURIComponent(
1468 * Alias for document.scrollLeft getter. 1717 window.location.search.substring(1)) &&
1469 * @param {!HTMLDocument} doc The document node where information will be 1718 this.hash === window.decodeURIComponent(
1470 * queried from. 1719 window.location.hash.substring(1))) {
1471 * @return {number} The X document scroll offset. 1720 // Nothing to do, the current URL is a representation of our propertie s.
1472 */ 1721 return;
1473 function scrollLeftForDocument(doc) { 1722 }
1474 return doc.documentElement.scrollLeft || doc.body.scrollLeft; 1723 var newUrl = this._getUrl();
1475 } 1724 // Need to use a full URL in case the containing page has a base URI.
1476 1725 var fullNewUrl = new URL(
1477 /** 1726 newUrl, window.location.protocol + '//' + window.location.host).href ;
1478 * Alias for document.scrollLeft setter. 1727 var now = window.performance.now();
1479 * @param {!HTMLDocument} doc The document node where information will be 1728 var shouldReplace =
1480 * queried from. 1729 this._lastChangedAt + this.dwellTime > now;
1481 * @param {number} value The target X scroll offset. 1730 this._lastChangedAt = now;
1482 */ 1731 if (shouldReplace) {
1483 function setScrollLeftForDocument(doc, value) { 1732 window.history.replaceState({}, '', fullNewUrl);
1484 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; 1733 } else {
1485 } 1734 window.history.pushState({}, '', fullNewUrl);
1486 1735 }
1487 /** 1736 this.fire('location-changed', {}, {node: window});
1488 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. 1737 },
1489 * @param {string} original The original string. 1738 /**
1490 * @return {string} The string with all the characters mentioned above replaced. 1739 * A necessary evil so that links work as expected. Does its best to
1491 */ 1740 * bail out early if possible.
1492 function HTMLEscape(original) { 1741 *
1493 return original.replace(/&/g, '&amp;') 1742 * @param {MouseEvent} event .
1494 .replace(/</g, '&lt;') 1743 */
1495 .replace(/>/g, '&gt;') 1744 _globalOnClick: function(event) {
1496 .replace(/"/g, '&quot;') 1745 // If another event handler has stopped this event then there's nothing
1497 .replace(/'/g, '&#39;'); 1746 // for us to do. This can happen e.g. when there are multiple
1498 } 1747 // iron-location elements in a page.
1499 1748 if (event.defaultPrevented) {
1500 /** 1749 return;
1501 * Shortens the provided string (if necessary) to a string of length at most 1750 }
1502 * |maxLength|. 1751 var href = this._getSameOriginLinkHref(event);
1503 * @param {string} original The original string. 1752 if (!href) {
1504 * @param {number} maxLength The maximum length allowed for the string. 1753 return;
1505 * @return {string} The original string if its length does not exceed 1754 }
1506 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' 1755 event.preventDefault();
1507 * appended. 1756 // If the navigation is to the current page we shouldn't add a history
1508 */ 1757 // entry or fire a change event.
1509 function elide(original, maxLength) { 1758 if (href === window.location.href) {
1510 if (original.length <= maxLength) 1759 return;
1511 return original; 1760 }
1512 return original.substring(0, maxLength - 1) + '\u2026'; 1761 window.history.pushState({}, '', href);
1513 } 1762 this.fire('location-changed', {}, {node: window});
1514 1763 },
1515 /** 1764 /**
1516 * Quote a string so it can be used in a regular expression. 1765 * Returns the absolute URL of the link (if any) that this click event
1517 * @param {string} str The source string. 1766 * is clicking on, if we can and should override the resulting full
1518 * @return {string} The escaped string. 1767 * page navigation. Returns null otherwise.
1519 */ 1768 *
1520 function quoteString(str) { 1769 * @param {MouseEvent} event .
1521 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); 1770 * @return {string?} .
1522 } 1771 */
1523 1772 _getSameOriginLinkHref: function(event) {
1524 // <if expr="is_ios"> 1773 // We only care about left-clicks.
1525 // Polyfill 'key' in KeyboardEvent for iOS. 1774 if (event.button !== 0) {
1526 // This function is not intended to be complete but should 1775 return null;
1527 // be sufficient enough to have iOS work correctly while 1776 }
1528 // it does not support key yet. 1777 // We don't want modified clicks, where the intent is to open the page
1529 if (!('key' in KeyboardEvent.prototype)) { 1778 // in a new tab.
1530 Object.defineProperty(KeyboardEvent.prototype, 'key', { 1779 if (event.metaKey || event.ctrlKey) {
1531 /** @this {KeyboardEvent} */ 1780 return null;
1532 get: function () { 1781 }
1533 // 0-9 1782 var eventPath = Polymer.dom(event).path;
1534 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) 1783 var anchor = null;
1535 return String.fromCharCode(this.keyCode); 1784 for (var i = 0; i < eventPath.length; i++) {
1536 1785 var element = eventPath[i];
1537 // A-Z 1786 if (element.tagName === 'A' && element.href) {
1538 if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { 1787 anchor = element;
1539 var result = String.fromCharCode(this.keyCode).toLowerCase(); 1788 break;
1540 if (this.shiftKey) 1789 }
1541 result = result.toUpperCase(); 1790 }
1542 return result; 1791
1543 } 1792 // If there's no link there's nothing to do.
1544 1793 if (!anchor) {
1545 // Special characters 1794 return null;
1546 switch(this.keyCode) { 1795 }
1547 case 0x08: return 'Backspace'; 1796
1548 case 0x09: return 'Tab'; 1797 // Target blank is a new tab, don't intercept.
1549 case 0x0d: return 'Enter'; 1798 if (anchor.target === '_blank') {
1550 case 0x10: return 'Shift'; 1799 return null;
1551 case 0x11: return 'Control'; 1800 }
1552 case 0x12: return 'Alt'; 1801 // If the link is for an existing parent frame, don't intercept.
1553 case 0x1b: return 'Escape'; 1802 if ((anchor.target === '_top' ||
1554 case 0x20: return ' '; 1803 anchor.target === '_parent') &&
1555 case 0x21: return 'PageUp'; 1804 window.top !== window) {
1556 case 0x22: return 'PageDown'; 1805 return null;
1557 case 0x23: return 'End'; 1806 }
1558 case 0x24: return 'Home'; 1807
1559 case 0x25: return 'ArrowLeft'; 1808 var href = anchor.href;
1560 case 0x26: return 'ArrowUp'; 1809
1561 case 0x27: return 'ArrowRight'; 1810 // It only makes sense for us to intercept same-origin navigations.
1562 case 0x28: return 'ArrowDown'; 1811 // pushState/replaceState don't work with cross-origin links.
1563 case 0x2d: return 'Insert'; 1812 var url;
1564 case 0x2e: return 'Delete'; 1813 if (document.baseURI != null) {
1565 case 0x5b: return 'Meta'; 1814 url = new URL(href, /** @type {string} */(document.baseURI));
1566 case 0x70: return 'F1'; 1815 } else {
1567 case 0x71: return 'F2'; 1816 url = new URL(href);
1568 case 0x72: return 'F3'; 1817 }
1569 case 0x73: return 'F4'; 1818
1570 case 0x74: return 'F5'; 1819 var origin;
1571 case 0x75: return 'F6'; 1820
1572 case 0x76: return 'F7'; 1821 // IE Polyfill
1573 case 0x77: return 'F8'; 1822 if (window.location.origin) {
1574 case 0x78: return 'F9'; 1823 origin = window.location.origin;
1575 case 0x79: return 'F10'; 1824 } else {
1576 case 0x7a: return 'F11'; 1825 origin = window.location.protocol + '//' + window.location.hostname;
1577 case 0x7b: return 'F12'; 1826
1578 case 0xbb: return '='; 1827 if (window.location.port) {
1579 case 0xbd: return '-'; 1828 origin += ':' + window.location.port;
1580 case 0xdb: return '['; 1829 }
1581 case 0xdd: return ']'; 1830 }
1582 } 1831
1583 return 'Unidentified'; 1832 if (url.origin !== origin) {
1833 return null;
1834 }
1835 var normalizedHref = url.pathname + url.search + url.hash;
1836
1837 // If we've been configured not to handle this url... don't handle it!
1838 if (this._urlSpaceRegExp &&
1839 !this._urlSpaceRegExp.test(normalizedHref)) {
1840 return null;
1841 }
1842 // Need to use a full URL in case the containing page has a base URI.
1843 var fullNormalizedHref = new URL(
1844 normalizedHref, window.location.href).href;
1845 return fullNormalizedHref;
1846 },
1847 _makeRegExp: function(urlSpaceRegex) {
1848 return RegExp(urlSpaceRegex);
1849 }
1850 });
1851 })();
1852 'use strict';
1853
1854 Polymer({
1855 is: 'iron-query-params',
1856 properties: {
1857 paramsString: {
1858 type: String,
1859 notify: true,
1860 observer: 'paramsStringChanged',
1861 },
1862 paramsObject: {
1863 type: Object,
1864 notify: true,
1865 value: function() {
1866 return {};
1867 }
1868 },
1869 _dontReact: {
1870 type: Boolean,
1871 value: false
1872 }
1873 },
1874 hostAttributes: {
1875 hidden: true
1876 },
1877 observers: [
1878 'paramsObjectChanged(paramsObject.*)'
1879 ],
1880 paramsStringChanged: function() {
1881 this._dontReact = true;
1882 this.paramsObject = this._decodeParams(this.paramsString);
1883 this._dontReact = false;
1884 },
1885 paramsObjectChanged: function() {
1886 if (this._dontReact) {
1887 return;
1888 }
1889 this.paramsString = this._encodeParams(this.paramsObject);
1890 },
1891 _encodeParams: function(params) {
1892 var encodedParams = [];
1893 for (var key in params) {
1894 var value = params[key];
1895 if (value === '') {
1896 encodedParams.push(encodeURIComponent(key));
1897 } else if (value) {
1898 encodedParams.push(
1899 encodeURIComponent(key) +
1900 '=' +
1901 encodeURIComponent(value.toString())
1902 );
1903 }
1904 }
1905 return encodedParams.join('&');
1906 },
1907 _decodeParams: function(paramString) {
1908 var params = {};
1909
1910 // Work around a bug in decodeURIComponent where + is not
1911 // converted to spaces:
1912 paramString = (paramString || '').replace(/\+/g, '%20');
1913
1914 var paramList = paramString.split('&');
1915 for (var i = 0; i < paramList.length; i++) {
1916 var param = paramList[i].split('=');
1917 if (param[0]) {
1918 params[decodeURIComponent(param[0])] =
1919 decodeURIComponent(param[1] || '');
1920 }
1921 }
1922 return params;
1584 } 1923 }
1585 }); 1924 });
1586 } else { 1925 'use strict';
1587 window.console.log("KeyboardEvent.Key polyfill not required"); 1926
1588 } 1927 /**
1589 // </if> /* is_ios */ 1928 * Provides bidirectional mapping between `path` and `queryParams` and a
1929 * app-route compatible `route` object.
1930 *
1931 * For more information, see the docs for `app-route-converter`.
1932 *
1933 * @polymerBehavior
1934 */
1935 Polymer.AppRouteConverterBehavior = {
1936 properties: {
1937 /**
1938 * A model representing the deserialized path through the route tree, as
1939 * well as the current queryParams.
1940 *
1941 * A route object is the kernel of the routing system. It is intended to
1942 * be fed into consuming elements such as `app-route`.
1943 *
1944 * @type {?Object}
1945 */
1946 route: {
1947 type: Object,
1948 notify: true
1949 },
1950
1951 /**
1952 * A set of key/value pairs that are universally accessible to branches of
1953 * the route tree.
1954 *
1955 * @type {?Object}
1956 */
1957 queryParams: {
1958 type: Object,
1959 notify: true
1960 },
1961
1962 /**
1963 * The serialized path through the route tree. This corresponds to the
1964 * `window.location.pathname` value, and will update to reflect changes
1965 * to that value.
1966 */
1967 path: {
1968 type: String,
1969 notify: true,
1970 }
1971 },
1972
1973 observers: [
1974 '_locationChanged(path, queryParams)',
1975 '_routeChanged(route.prefix, route.path)',
1976 '_routeQueryParamsChanged(route.__queryParams)'
1977 ],
1978
1979 created: function() {
1980 this.linkPaths('route.__queryParams', 'queryParams');
1981 this.linkPaths('queryParams', 'route.__queryParams');
1982 },
1983
1984 /**
1985 * Handler called when the path or queryParams change.
1986 */
1987 _locationChanged: function() {
1988 if (this.route &&
1989 this.route.path === this.path &&
1990 this.queryParams === this.route.__queryParams) {
1991 return;
1992 }
1993 this.route = {
1994 prefix: '',
1995 path: this.path,
1996 __queryParams: this.queryParams
1997 };
1998 },
1999
2000 /**
2001 * Handler called when the route prefix and route path change.
2002 */
2003 _routeChanged: function() {
2004 if (!this.route) {
2005 return;
2006 }
2007
2008 this.path = this.route.prefix + this.route.path;
2009 },
2010
2011 /**
2012 * Handler called when the route queryParams change.
2013 *
2014 * @param {Object} queryParams A set of key/value pairs that are
2015 * universally accessible to branches of the route tree.
2016 */
2017 _routeQueryParamsChanged: function(queryParams) {
2018 if (!this.route) {
2019 return;
2020 }
2021 this.queryParams = queryParams;
2022 }
2023 };
2024 'use strict';
2025
2026 Polymer({
2027 is: 'app-location',
2028
2029 properties: {
2030 /**
2031 * A model representing the deserialized path through the route tree, as
2032 * well as the current queryParams.
2033 */
2034 route: {
2035 type: Object,
2036 notify: true
2037 },
2038
2039 /**
2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in
2041 * alternative to the `path`. For example, if deploying an app to a stat ic
2042 * web server (e.g., Github Pages) - where one does not have control ove r
2043 * server-side routing - it is usually a better experience to use the ha sh
2044 * to represent paths through one's app.
2045 *
2046 * When this property is set to true, the `hash` will be used in place o f
2047
2048 * the `path` for generating a `route`.
2049 */
2050 useHashAsPath: {
2051 type: Boolean,
2052 value: false
2053 },
2054
2055 /**
2056 * A regexp that defines the set of URLs that should be considered part
2057 * of this web app.
2058 *
2059 * Clicking on a link that matches this regex won't result in a full pag e
2060 * navigation, but will instead just update the URL state in place.
2061 *
2062 * This regexp is given everything after the origin in an absolute
2063 * URL. So to match just URLs that start with /search/ do:
2064 * url-space-regex="^/search/"
2065 *
2066 * @type {string|RegExp}
2067 */
2068 urlSpaceRegex: {
2069 type: String,
2070 notify: true
2071 },
2072
2073 /**
2074 * A set of key/value pairs that are universally accessible to branches
2075 * of the route tree.
2076 */
2077 __queryParams: {
2078 type: Object
2079 },
2080
2081 /**
2082 * The pathname component of the current URL.
2083 */
2084 __path: {
2085 type: String
2086 },
2087
2088 /**
2089 * The query string portion of the current URL.
2090 */
2091 __query: {
2092 type: String
2093 },
2094
2095 /**
2096 * The hash portion of the current URL.
2097 */
2098 __hash: {
2099 type: String
2100 },
2101
2102 /**
2103 * The route path, which will be either the hash or the path, depending
2104 * on useHashAsPath.
2105 */
2106 path: {
2107 type: String,
2108 observer: '__onPathChanged'
2109 }
2110 },
2111
2112 behaviors: [Polymer.AppRouteConverterBehavior],
2113
2114 observers: [
2115 '__computeRoutePath(useHashAsPath, __hash, __path)'
2116 ],
2117
2118 __computeRoutePath: function() {
2119 this.path = this.useHashAsPath ? this.__hash : this.__path;
2120 },
2121
2122 __onPathChanged: function() {
2123 if (!this._readied) {
2124 return;
2125 }
2126
2127 if (this.useHashAsPath) {
2128 this.__hash = this.path;
2129 } else {
2130 this.__path = this.path;
2131 }
2132 }
2133 });
2134 'use strict';
2135
2136 Polymer({
2137 is: 'app-route',
2138
2139 properties: {
2140 /**
2141 * The URL component managed by this element.
2142 */
2143 route: {
2144 type: Object,
2145 notify: true
2146 },
2147
2148 /**
2149 * The pattern of slash-separated segments to match `path` against.
2150 *
2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar"
2152 * but not "/foobar".
2153 *
2154 * Path segments like `/:named` are mapped to properties on the `data` obj ect.
2155 */
2156 pattern: {
2157 type: String
2158 },
2159
2160 /**
2161 * The parameterized values that are extracted from the route as
2162 * described by `pattern`.
2163 */
2164 data: {
2165 type: Object,
2166 value: function() {return {};},
2167 notify: true
2168 },
2169
2170 /**
2171 * @type {?Object}
2172 */
2173 queryParams: {
2174 type: Object,
2175 value: function() {
2176 return {};
2177 },
2178 notify: true
2179 },
2180
2181 /**
2182 * The part of `path` NOT consumed by `pattern`.
2183 */
2184 tail: {
2185 type: Object,
2186 value: function() {return {path: null, prefix: null, __queryParams: null };},
2187 notify: true
2188 },
2189
2190 active: {
2191 type: Boolean,
2192 notify: true,
2193 readOnly: true
2194 },
2195
2196 _queryParamsUpdating: {
2197 type: Boolean,
2198 value: false
2199 },
2200 /**
2201 * @type {?string}
2202 */
2203 _matched: {
2204 type: String,
2205 value: ''
2206 }
2207 },
2208
2209 observers: [
2210 '__tryToMatch(route.path, pattern)',
2211 '__updatePathOnDataChange(data.*)',
2212 '__tailPathChanged(tail.path)',
2213 '__routeQueryParamsChanged(route.__queryParams)',
2214 '__tailQueryParamsChanged(tail.__queryParams)',
2215 '__queryParamsChanged(queryParams.*)'
2216 ],
2217
2218 created: function() {
2219 this.linkPaths('route.__queryParams', 'tail.__queryParams');
2220 this.linkPaths('tail.__queryParams', 'route.__queryParams');
2221 },
2222
2223 /**
2224 * Deal with the query params object being assigned to wholesale.
2225 * @export
2226 */
2227 __routeQueryParamsChanged: function(queryParams) {
2228 if (queryParams && this.tail) {
2229 this.set('tail.__queryParams', queryParams);
2230
2231 if (!this.active || this._queryParamsUpdating) {
2232 return;
2233 }
2234
2235 // Copy queryParams and track whether there are any differences compared
2236 // to the existing query params.
2237 var copyOfQueryParams = {};
2238 var anythingChanged = false;
2239 for (var key in queryParams) {
2240 copyOfQueryParams[key] = queryParams[key];
2241 if (anythingChanged ||
2242 !this.queryParams ||
2243 queryParams[key] !== this.queryParams[key]) {
2244 anythingChanged = true;
2245 }
2246 }
2247 // Need to check whether any keys were deleted
2248 for (var key in this.queryParams) {
2249 if (anythingChanged || !(key in queryParams)) {
2250 anythingChanged = true;
2251 break;
2252 }
2253 }
2254
2255 if (!anythingChanged) {
2256 return;
2257 }
2258 this._queryParamsUpdating = true;
2259 this.set('queryParams', copyOfQueryParams);
2260 this._queryParamsUpdating = false;
2261 }
2262 },
2263
2264 /**
2265 * @export
2266 */
2267 __tailQueryParamsChanged: function(queryParams) {
2268 if (queryParams && this.route) {
2269 this.set('route.__queryParams', queryParams);
2270 }
2271 },
2272
2273 /**
2274 * @export
2275 */
2276 __queryParamsChanged: function(changes) {
2277 if (!this.active || this._queryParamsUpdating) {
2278 return;
2279 }
2280
2281 this.set('route.__' + changes.path, changes.value);
2282 },
2283
2284 __resetProperties: function() {
2285 this._setActive(false);
2286 this._matched = null;
2287 //this.tail = { path: null, prefix: null, queryParams: null };
2288 //this.data = {};
2289 },
2290
2291 /**
2292 * @export
2293 */
2294 __tryToMatch: function() {
2295 if (!this.route) {
2296 return;
2297 }
2298 var path = this.route.path;
2299 var pattern = this.pattern;
2300 if (!pattern) {
2301 return;
2302 }
2303
2304 if (!path) {
2305 this.__resetProperties();
2306 return;
2307 }
2308
2309 var remainingPieces = path.split('/');
2310 var patternPieces = pattern.split('/');
2311
2312 var matched = [];
2313 var namedMatches = {};
2314
2315 for (var i=0; i < patternPieces.length; i++) {
2316 var patternPiece = patternPieces[i];
2317 if (!patternPiece && patternPiece !== '') {
2318 break;
2319 }
2320 var pathPiece = remainingPieces.shift();
2321
2322 // We don't match this path.
2323 if (!pathPiece && pathPiece !== '') {
2324 this.__resetProperties();
2325 return;
2326 }
2327 matched.push(pathPiece);
2328
2329 if (patternPiece.charAt(0) == ':') {
2330 namedMatches[patternPiece.slice(1)] = pathPiece;
2331 } else if (patternPiece !== pathPiece) {
2332 this.__resetProperties();
2333 return;
2334 }
2335 }
2336
2337 this._matched = matched.join('/');
2338
2339 // Properties that must be updated atomically.
2340 var propertyUpdates = {};
2341
2342 //this.active
2343 if (!this.active) {
2344 propertyUpdates.active = true;
2345 }
2346
2347 // this.tail
2348 var tailPrefix = this.route.prefix + this._matched;
2349 var tailPath = remainingPieces.join('/');
2350 if (remainingPieces.length > 0) {
2351 tailPath = '/' + tailPath;
2352 }
2353 if (!this.tail ||
2354 this.tail.prefix !== tailPrefix ||
2355 this.tail.path !== tailPath) {
2356 propertyUpdates.tail = {
2357 prefix: tailPrefix,
2358 path: tailPath,
2359 __queryParams: this.route.__queryParams
2360 };
2361 }
2362
2363 // this.data
2364 propertyUpdates.data = namedMatches;
2365 this._dataInUrl = {};
2366 for (var key in namedMatches) {
2367 this._dataInUrl[key] = namedMatches[key];
2368 }
2369
2370 this.__setMulti(propertyUpdates);
2371 },
2372
2373 /**
2374 * @export
2375 */
2376 __tailPathChanged: function() {
2377 if (!this.active) {
2378 return;
2379 }
2380 var tailPath = this.tail.path;
2381 var newPath = this._matched;
2382 if (tailPath) {
2383 if (tailPath.charAt(0) !== '/') {
2384 tailPath = '/' + tailPath;
2385 }
2386 newPath += tailPath;
2387 }
2388 this.set('route.path', newPath);
2389 },
2390
2391 /**
2392 * @export
2393 */
2394 __updatePathOnDataChange: function() {
2395 if (!this.route || !this.active) {
2396 return;
2397 }
2398 var newPath = this.__getLink({});
2399 var oldPath = this.__getLink(this._dataInUrl);
2400 if (newPath === oldPath) {
2401 return;
2402 }
2403 this.set('route.path', newPath);
2404 },
2405
2406 __getLink: function(overrideValues) {
2407 var values = {tail: null};
2408 for (var key in this.data) {
2409 values[key] = this.data[key];
2410 }
2411 for (var key in overrideValues) {
2412 values[key] = overrideValues[key];
2413 }
2414 var patternPieces = this.pattern.split('/');
2415 var interp = patternPieces.map(function(value) {
2416 if (value[0] == ':') {
2417 value = values[value.slice(1)];
2418 }
2419 return value;
2420 }, this);
2421 if (values.tail && values.tail.path) {
2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
2423 interp.push(values.tail.path.slice(1));
2424 } else {
2425 interp.push(values.tail.path);
2426 }
2427 }
2428 return interp.join('/');
2429 },
2430
2431 __setMulti: function(setObj) {
2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at
2433 // internal data structures. I would not advise that you copy this
2434 // example.
2435 //
2436 // In the future this will be a feature of Polymer itself.
2437 // See: https://github.com/Polymer/polymer/issues/3640
2438 //
2439 // Hacking around with private methods like this is juggling footguns,
2440 // and is likely to have unexpected and unsupported rough edges.
2441 //
2442 // Be ye so warned.
2443 for (var property in setObj) {
2444 this._propertySetter(property, setObj[property]);
2445 }
2446
2447 for (var property in setObj) {
2448 this._pathEffector(property, this[property]);
2449 this._notifyPathUp(property, this[property]);
2450 }
2451 }
2452 });
2453 Polymer({
2454
2455 is: 'iron-media-query',
2456
2457 properties: {
2458
2459 /**
2460 * The Boolean return value of the media query.
2461 */
2462 queryMatches: {
2463 type: Boolean,
2464 value: false,
2465 readOnly: true,
2466 notify: true
2467 },
2468
2469 /**
2470 * The CSS media query to evaluate.
2471 */
2472 query: {
2473 type: String,
2474 observer: 'queryChanged'
2475 },
2476
2477 /**
2478 * If true, the query attribute is assumed to be a complete media query
2479 * string rather than a single media feature.
2480 */
2481 full: {
2482 type: Boolean,
2483 value: false
2484 },
2485
2486 /**
2487 * @type {function(MediaQueryList)}
2488 */
2489 _boundMQHandler: {
2490 value: function() {
2491 return this.queryHandler.bind(this);
2492 }
2493 },
2494
2495 /**
2496 * @type {MediaQueryList}
2497 */
2498 _mq: {
2499 value: null
2500 }
2501 },
2502
2503 attached: function() {
2504 this.style.display = 'none';
2505 this.queryChanged();
2506 },
2507
2508 detached: function() {
2509 this._remove();
2510 },
2511
2512 _add: function() {
2513 if (this._mq) {
2514 this._mq.addListener(this._boundMQHandler);
2515 }
2516 },
2517
2518 _remove: function() {
2519 if (this._mq) {
2520 this._mq.removeListener(this._boundMQHandler);
2521 }
2522 this._mq = null;
2523 },
2524
2525 queryChanged: function() {
2526 this._remove();
2527 var query = this.query;
2528 if (!query) {
2529 return;
2530 }
2531 if (!this.full && query[0] !== '(') {
2532 query = '(' + query + ')';
2533 }
2534 this._mq = window.matchMedia(query);
2535 this._add();
2536 this.queryHandler(this._mq);
2537 },
2538
2539 queryHandler: function(mq) {
2540 this._setQueryMatches(mq.matches);
2541 }
2542
2543 });
1590 /** 2544 /**
1591 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to 2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
1592 * coordinate the flow of resize events between "resizers" (elements that cont rol the 2546 * coordinate the flow of resize events between "resizers" (elements that cont rol the
1593 * size or hidden state of their children) and "resizables" (elements that nee d to be 2547 * size or hidden state of their children) and "resizables" (elements that nee d to be
1594 * notified when they are resized or un-hidden by their parents in order to ta ke 2548 * notified when they are resized or un-hidden by their parents in order to ta ke
1595 * action on their new measurements). 2549 * action on their new measurements).
1596 * 2550 *
1597 * Elements that perform measurement should add the `IronResizableBehavior` be havior to 2551 * Elements that perform measurement should add the `IronResizableBehavior` be havior to
1598 * their element definition and listen for the `iron-resize` event on themselv es. 2552 * their element definition and listen for the `iron-resize` event on themselv es.
1599 * This event will be fired when they become showing after having been hidden, 2553 * This event will be fired when they become showing after having been hidden,
(...skipping 160 matching lines...) Expand 10 before | Expand all | Expand 10 after
1760 // else they will get redundantly notified when the parent attaches). 2714 // else they will get redundantly notified when the parent attaches).
1761 if (!this.isAttached) { 2715 if (!this.isAttached) {
1762 return; 2716 return;
1763 } 2717 }
1764 2718
1765 this._notifyingDescendant = true; 2719 this._notifyingDescendant = true;
1766 descendant.notifyResize(); 2720 descendant.notifyResize();
1767 this._notifyingDescendant = false; 2721 this._notifyingDescendant = false;
1768 } 2722 }
1769 }; 2723 };
2724 /**
2725 * @param {!Function} selectCallback
2726 * @constructor
2727 */
2728 Polymer.IronSelection = function(selectCallback) {
2729 this.selection = [];
2730 this.selectCallback = selectCallback;
2731 };
2732
2733 Polymer.IronSelection.prototype = {
2734
2735 /**
2736 * Retrieves the selected item(s).
2737 *
2738 * @method get
2739 * @returns Returns the selected item(s). If the multi property is true,
2740 * `get` will return an array, otherwise it will return
2741 * the selected item or undefined if there is no selection.
2742 */
2743 get: function() {
2744 return this.multi ? this.selection.slice() : this.selection[0];
2745 },
2746
2747 /**
2748 * Clears all the selection except the ones indicated.
2749 *
2750 * @method clear
2751 * @param {Array} excludes items to be excluded.
2752 */
2753 clear: function(excludes) {
2754 this.selection.slice().forEach(function(item) {
2755 if (!excludes || excludes.indexOf(item) < 0) {
2756 this.setItemSelected(item, false);
2757 }
2758 }, this);
2759 },
2760
2761 /**
2762 * Indicates if a given item is selected.
2763 *
2764 * @method isSelected
2765 * @param {*} item The item whose selection state should be checked.
2766 * @returns Returns true if `item` is selected.
2767 */
2768 isSelected: function(item) {
2769 return this.selection.indexOf(item) >= 0;
2770 },
2771
2772 /**
2773 * Sets the selection state for a given item to either selected or deselecte d.
2774 *
2775 * @method setItemSelected
2776 * @param {*} item The item to select.
2777 * @param {boolean} isSelected True for selected, false for deselected.
2778 */
2779 setItemSelected: function(item, isSelected) {
2780 if (item != null) {
2781 if (isSelected !== this.isSelected(item)) {
2782 // proceed to update selection only if requested state differs from cu rrent
2783 if (isSelected) {
2784 this.selection.push(item);
2785 } else {
2786 var i = this.selection.indexOf(item);
2787 if (i >= 0) {
2788 this.selection.splice(i, 1);
2789 }
2790 }
2791 if (this.selectCallback) {
2792 this.selectCallback(item, isSelected);
2793 }
2794 }
2795 }
2796 },
2797
2798 /**
2799 * Sets the selection state for a given item. If the `multi` property
2800 * is true, then the selected state of `item` will be toggled; otherwise
2801 * the `item` will be selected.
2802 *
2803 * @method select
2804 * @param {*} item The item to select.
2805 */
2806 select: function(item) {
2807 if (this.multi) {
2808 this.toggle(item);
2809 } else if (this.get() !== item) {
2810 this.setItemSelected(this.get(), false);
2811 this.setItemSelected(item, true);
2812 }
2813 },
2814
2815 /**
2816 * Toggles the selection state for `item`.
2817 *
2818 * @method toggle
2819 * @param {*} item The item to toggle.
2820 */
2821 toggle: function(item) {
2822 this.setItemSelected(item, !this.isSelected(item));
2823 }
2824
2825 };
2826 /** @polymerBehavior */
2827 Polymer.IronSelectableBehavior = {
2828
2829 /**
2830 * Fired when iron-selector is activated (selected or deselected).
2831 * It is fired before the selected items are changed.
2832 * Cancel the event to abort selection.
2833 *
2834 * @event iron-activate
2835 */
2836
2837 /**
2838 * Fired when an item is selected
2839 *
2840 * @event iron-select
2841 */
2842
2843 /**
2844 * Fired when an item is deselected
2845 *
2846 * @event iron-deselect
2847 */
2848
2849 /**
2850 * Fired when the list of selectable items changes (e.g., items are
2851 * added or removed). The detail of the event is a mutation record that
2852 * describes what changed.
2853 *
2854 * @event iron-items-changed
2855 */
2856
2857 properties: {
2858
2859 /**
2860 * If you want to use an attribute value or property of an element for
2861 * `selected` instead of the index, set this to the name of the attribute
2862 * or property. Hyphenated values are converted to camel case when used to
2863 * look up the property of a selectable element. Camel cased values are
2864 * *not* converted to hyphenated values for attribute lookup. It's
2865 * recommended that you provide the hyphenated form of the name so that
2866 * selection works in both cases. (Use `attr-or-property-name` instead of
2867 * `attrOrPropertyName`.)
2868 */
2869 attrForSelected: {
2870 type: String,
2871 value: null
2872 },
2873
2874 /**
2875 * Gets or sets the selected element. The default is to use the index of t he item.
2876 * @type {string|number}
2877 */
2878 selected: {
2879 type: String,
2880 notify: true
2881 },
2882
2883 /**
2884 * Returns the currently selected item.
2885 *
2886 * @type {?Object}
2887 */
2888 selectedItem: {
2889 type: Object,
2890 readOnly: true,
2891 notify: true
2892 },
2893
2894 /**
2895 * The event that fires from items when they are selected. Selectable
2896 * will listen for this event from items and update the selection state.
2897 * Set to empty string to listen to no events.
2898 */
2899 activateEvent: {
2900 type: String,
2901 value: 'tap',
2902 observer: '_activateEventChanged'
2903 },
2904
2905 /**
2906 * This is a CSS selector string. If this is set, only items that match t he CSS selector
2907 * are selectable.
2908 */
2909 selectable: String,
2910
2911 /**
2912 * The class to set on elements when selected.
2913 */
2914 selectedClass: {
2915 type: String,
2916 value: 'iron-selected'
2917 },
2918
2919 /**
2920 * The attribute to set on elements when selected.
2921 */
2922 selectedAttribute: {
2923 type: String,
2924 value: null
2925 },
2926
2927 /**
2928 * Default fallback if the selection based on selected with `attrForSelect ed`
2929 * is not found.
2930 */
2931 fallbackSelection: {
2932 type: String,
2933 value: null
2934 },
2935
2936 /**
2937 * The list of items from which a selection can be made.
2938 */
2939 items: {
2940 type: Array,
2941 readOnly: true,
2942 notify: true,
2943 value: function() {
2944 return [];
2945 }
2946 },
2947
2948 /**
2949 * The set of excluded elements where the key is the `localName`
2950 * of the element that will be ignored from the item list.
2951 *
2952 * @default {template: 1}
2953 */
2954 _excludedLocalNames: {
2955 type: Object,
2956 value: function() {
2957 return {
2958 'template': 1
2959 };
2960 }
2961 }
2962 },
2963
2964 observers: [
2965 '_updateAttrForSelected(attrForSelected)',
2966 '_updateSelected(selected)',
2967 '_checkFallback(fallbackSelection)'
2968 ],
2969
2970 created: function() {
2971 this._bindFilterItem = this._filterItem.bind(this);
2972 this._selection = new Polymer.IronSelection(this._applySelection.bind(this ));
2973 },
2974
2975 attached: function() {
2976 this._observer = this._observeItems(this);
2977 this._updateItems();
2978 if (!this._shouldUpdateSelection) {
2979 this._updateSelected();
2980 }
2981 this._addListener(this.activateEvent);
2982 },
2983
2984 detached: function() {
2985 if (this._observer) {
2986 Polymer.dom(this).unobserveNodes(this._observer);
2987 }
2988 this._removeListener(this.activateEvent);
2989 },
2990
2991 /**
2992 * Returns the index of the given item.
2993 *
2994 * @method indexOf
2995 * @param {Object} item
2996 * @returns Returns the index of the item
2997 */
2998 indexOf: function(item) {
2999 return this.items.indexOf(item);
3000 },
3001
3002 /**
3003 * Selects the given value.
3004 *
3005 * @method select
3006 * @param {string|number} value the value to select.
3007 */
3008 select: function(value) {
3009 this.selected = value;
3010 },
3011
3012 /**
3013 * Selects the previous item.
3014 *
3015 * @method selectPrevious
3016 */
3017 selectPrevious: function() {
3018 var length = this.items.length;
3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
3020 this.selected = this._indexToValue(index);
3021 },
3022
3023 /**
3024 * Selects the next item.
3025 *
3026 * @method selectNext
3027 */
3028 selectNext: function() {
3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
3030 this.selected = this._indexToValue(index);
3031 },
3032
3033 /**
3034 * Selects the item at the given index.
3035 *
3036 * @method selectIndex
3037 */
3038 selectIndex: function(index) {
3039 this.select(this._indexToValue(index));
3040 },
3041
3042 /**
3043 * Force a synchronous update of the `items` property.
3044 *
3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to
3046 * updates to the set of selectable items after updates to the DOM list and
3047 * selection state have been made.
3048 *
3049 * WARNING: If you are using this method, you should probably consider an
3050 * alternate approach. Synchronously querying for items is potentially
3051 * slow for many use cases. The `items` property will update asynchronously
3052 * on its own to reflect selectable items in the DOM.
3053 */
3054 forceSynchronousItemUpdate: function() {
3055 this._updateItems();
3056 },
3057
3058 get _shouldUpdateSelection() {
3059 return this.selected != null;
3060 },
3061
3062 _checkFallback: function() {
3063 if (this._shouldUpdateSelection) {
3064 this._updateSelected();
3065 }
3066 },
3067
3068 _addListener: function(eventName) {
3069 this.listen(this, eventName, '_activateHandler');
3070 },
3071
3072 _removeListener: function(eventName) {
3073 this.unlisten(this, eventName, '_activateHandler');
3074 },
3075
3076 _activateEventChanged: function(eventName, old) {
3077 this._removeListener(old);
3078 this._addListener(eventName);
3079 },
3080
3081 _updateItems: function() {
3082 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
3083 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
3084 this._setItems(nodes);
3085 },
3086
3087 _updateAttrForSelected: function() {
3088 if (this._shouldUpdateSelection) {
3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3090 }
3091 },
3092
3093 _updateSelected: function() {
3094 this._selectSelected(this.selected);
3095 },
3096
3097 _selectSelected: function(selected) {
3098 this._selection.select(this._valueToItem(this.selected));
3099 // Check for items, since this array is populated only when attached
3100 // Since Number(0) is falsy, explicitly check for undefined
3101 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
3102 this.selected = this.fallbackSelection;
3103 }
3104 },
3105
3106 _filterItem: function(node) {
3107 return !this._excludedLocalNames[node.localName];
3108 },
3109
3110 _valueToItem: function(value) {
3111 return (value == null) ? null : this.items[this._valueToIndex(value)];
3112 },
3113
3114 _valueToIndex: function(value) {
3115 if (this.attrForSelected) {
3116 for (var i = 0, item; item = this.items[i]; i++) {
3117 if (this._valueForItem(item) == value) {
3118 return i;
3119 }
3120 }
3121 } else {
3122 return Number(value);
3123 }
3124 },
3125
3126 _indexToValue: function(index) {
3127 if (this.attrForSelected) {
3128 var item = this.items[index];
3129 if (item) {
3130 return this._valueForItem(item);
3131 }
3132 } else {
3133 return index;
3134 }
3135 },
3136
3137 _valueForItem: function(item) {
3138 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ];
3139 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected);
3140 },
3141
3142 _applySelection: function(item, isSelected) {
3143 if (this.selectedClass) {
3144 this.toggleClass(this.selectedClass, isSelected, item);
3145 }
3146 if (this.selectedAttribute) {
3147 this.toggleAttribute(this.selectedAttribute, isSelected, item);
3148 }
3149 this._selectionChange();
3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
3151 },
3152
3153 _selectionChange: function() {
3154 this._setSelectedItem(this._selection.get());
3155 },
3156
3157 // observe items change under the given node.
3158 _observeItems: function(node) {
3159 return Polymer.dom(node).observeNodes(function(mutation) {
3160 this._updateItems();
3161
3162 if (this._shouldUpdateSelection) {
3163 this._updateSelected();
3164 }
3165
3166 // Let other interested parties know about the change so that
3167 // we don't have to recreate mutation observers everywhere.
3168 this.fire('iron-items-changed', mutation, {
3169 bubbles: false,
3170 cancelable: false
3171 });
3172 });
3173 },
3174
3175 _activateHandler: function(e) {
3176 var t = e.target;
3177 var items = this.items;
3178 while (t && t != this) {
3179 var i = items.indexOf(t);
3180 if (i >= 0) {
3181 var value = this._indexToValue(i);
3182 this._itemActivate(value, t);
3183 return;
3184 }
3185 t = t.parentNode;
3186 }
3187 },
3188
3189 _itemActivate: function(value, item) {
3190 if (!this.fire('iron-activate',
3191 {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
3192 this.select(value);
3193 }
3194 }
3195
3196 };
3197 Polymer({
3198
3199 is: 'iron-pages',
3200
3201 behaviors: [
3202 Polymer.IronResizableBehavior,
3203 Polymer.IronSelectableBehavior
3204 ],
3205
3206 properties: {
3207
3208 // as the selected page is the only one visible, activateEvent
3209 // is both non-sensical and problematic; e.g. in cases where a user
3210 // handler attempts to change the page and the activateEvent
3211 // handler immediately changes it back
3212 activateEvent: {
3213 type: String,
3214 value: null
3215 }
3216
3217 },
3218
3219 observers: [
3220 '_selectedPageChanged(selected)'
3221 ],
3222
3223 _selectedPageChanged: function(selected, old) {
3224 this.async(this.notifyResize);
3225 }
3226 });
1770 (function() { 3227 (function() {
1771 'use strict'; 3228 'use strict';
1772 3229
1773 /** 3230 /**
1774 * Chrome uses an older version of DOM Level 3 Keyboard Events 3231 * Chrome uses an older version of DOM Level 3 Keyboard Events
1775 * 3232 *
1776 * Most keys are labeled as text, but some are Unicode codepoints. 3233 * Most keys are labeled as text, but some are Unicode codepoints.
1777 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set 3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set
1778 */ 3235 */
1779 var KEY_IDENTIFIER = { 3236 var KEY_IDENTIFIER = {
(...skipping 459 matching lines...) Expand 10 before | Expand all | Expand 10 after
2239 cancelable: true 3696 cancelable: true
2240 }); 3697 });
2241 this[handlerName].call(this, event); 3698 this[handlerName].call(this, event);
2242 if (event.defaultPrevented) { 3699 if (event.defaultPrevented) {
2243 keyboardEvent.preventDefault(); 3700 keyboardEvent.preventDefault();
2244 } 3701 }
2245 } 3702 }
2246 }; 3703 };
2247 })(); 3704 })();
2248 /** 3705 /**
2249 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a 3706 * @demo demo/index.html
2250 * designated scroll target.
2251 *
2252 * Elements that consume this behavior can override the `_scrollHandler`
2253 * method to add logic on the scroll event.
2254 *
2255 * @demo demo/scrolling-region.html Scrolling Region
2256 * @demo demo/document.html Document Element
2257 * @polymerBehavior 3707 * @polymerBehavior
2258 */ 3708 */
2259 Polymer.IronScrollTargetBehavior = { 3709 Polymer.IronControlState = {
2260 3710
2261 properties: { 3711 properties: {
2262 3712
2263 /** 3713 /**
2264 * Specifies the element that will handle the scroll event 3714 * If true, the element currently has focus.
2265 * on the behalf of the current element. This is typically a reference to an element, 3715 */
2266 * but there are a few more posibilities: 3716 focused: {
2267 * 3717 type: Boolean,
2268 * ### Elements id 3718 value: false,
2269 * 3719 notify: true,
2270 *```html 3720 readOnly: true,
2271 * <div id="scrollable-element" style="overflow: auto;"> 3721 reflectToAttribute: true
2272 * <x-element scroll-target="scrollable-element"> 3722 },
2273 * \x3c!-- Content--\x3e 3723
2274 * </x-element> 3724 /**
2275 * </div> 3725 * If true, the user cannot interact with this element.
2276 *``` 3726 */
2277 * In this case, the `scrollTarget` will point to the outer div element. 3727 disabled: {
2278 * 3728 type: Boolean,
2279 * ### Document scrolling 3729 value: false,
2280 * 3730 notify: true,
2281 * For document scrolling, you can use the reserved word `document`: 3731 observer: '_disabledChanged',
2282 * 3732 reflectToAttribute: true
2283 *```html 3733 },
2284 * <x-element scroll-target="document"> 3734
2285 * \x3c!-- Content --\x3e 3735 _oldTabIndex: {
2286 * </x-element> 3736 type: Number
2287 *``` 3737 },
2288 * 3738
2289 * ### Elements reference 3739 _boundFocusBlurHandler: {
2290 * 3740 type: Function,
2291 *```js
2292 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
2293 *```
2294 *
2295 * @type {HTMLElement}
2296 */
2297 scrollTarget: {
2298 type: HTMLElement,
2299 value: function() { 3741 value: function() {
2300 return this._defaultScrollTarget; 3742 return this._focusBlurHandler.bind(this);
2301 } 3743 }
2302 } 3744 }
3745
2303 }, 3746 },
2304 3747
2305 observers: [ 3748 observers: [
2306 '_scrollTargetChanged(scrollTarget, isAttached)' 3749 '_changedControlState(focused, disabled)'
2307 ], 3750 ],
2308 3751
2309 _scrollTargetChanged: function(scrollTarget, isAttached) { 3752 ready: function() {
2310 var eventTarget; 3753 this.addEventListener('focus', this._boundFocusBlurHandler, true);
2311 3754 this.addEventListener('blur', this._boundFocusBlurHandler, true);
2312 if (this._oldScrollTarget) { 3755 },
2313 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget; 3756
2314 eventTarget.removeEventListener('scroll', this._boundScrollHandler); 3757 _focusBlurHandler: function(event) {
2315 this._oldScrollTarget = null; 3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
2316 } 3759 // eventually become `this` due to retargeting; if we are not in
2317 3760 // ShadowDOM land, `event.target` will eventually become `this` due
2318 if (!isAttached) { 3761 // to the second conditional which fires a synthetic event (that is also
2319 return; 3762 // handled). In either case, we can disregard `event.path`.
2320 } 3763
2321 // Support element id references 3764 if (event.target === this) {
2322 if (scrollTarget === 'document') { 3765 this._setFocused(event.type === 'focus');
2323 3766 } else if (!this.shadowRoot) {
2324 this.scrollTarget = this._doc; 3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget);
2325 3768 if (!this.isLightDescendant(target)) {
2326 } else if (typeof scrollTarget === 'string') { 3769 this.fire(event.type, {sourceEvent: event}, {
2327 3770 node: this,
2328 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : 3771 bubbles: event.bubbles,
2329 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); 3772 cancelable: event.cancelable
2330 3773 });
2331 } else if (this._isValidScrollTarget()) { 3774 }
2332 3775 }
2333 eventTarget = scrollTarget === this._doc ? window : scrollTarget; 3776 },
2334 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this); 3777
2335 this._oldScrollTarget = scrollTarget; 3778 _disabledChanged: function(disabled, old) {
2336 3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
2337 eventTarget.addEventListener('scroll', this._boundScrollHandler); 3780 this.style.pointerEvents = disabled ? 'none' : '';
2338 } 3781 if (disabled) {
2339 }, 3782 this._oldTabIndex = this.tabIndex;
2340 3783 this._setFocused(false);
2341 /** 3784 this.tabIndex = -1;
2342 * Runs on every scroll event. Consumer of this behavior may override this m ethod. 3785 this.blur();
2343 * 3786 } else if (this._oldTabIndex !== undefined) {
2344 * @protected 3787 this.tabIndex = this._oldTabIndex;
2345 */ 3788 }
2346 _scrollHandler: function scrollHandler() {}, 3789 },
2347 3790
2348 /** 3791 _changedControlState: function() {
2349 * The default scroll target. Consumers of this behavior may want to customi ze 3792 // _controlStateChanged is abstract, follow-on behaviors may implement it
2350 * the default scroll target. 3793 if (this._controlStateChanged) {
2351 * 3794 this._controlStateChanged();
2352 * @type {Element} 3795 }
2353 */
2354 get _defaultScrollTarget() {
2355 return this._doc;
2356 },
2357
2358 /**
2359 * Shortcut for the document element
2360 *
2361 * @type {Element}
2362 */
2363 get _doc() {
2364 return this.ownerDocument.documentElement;
2365 },
2366
2367 /**
2368 * Gets the number of pixels that the content of an element is scrolled upwa rd.
2369 *
2370 * @type {number}
2371 */
2372 get _scrollTop() {
2373 if (this._isValidScrollTarget()) {
2374 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
2375 }
2376 return 0;
2377 },
2378
2379 /**
2380 * Gets the number of pixels that the content of an element is scrolled to t he left.
2381 *
2382 * @type {number}
2383 */
2384 get _scrollLeft() {
2385 if (this._isValidScrollTarget()) {
2386 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
2387 }
2388 return 0;
2389 },
2390
2391 /**
2392 * Sets the number of pixels that the content of an element is scrolled upwa rd.
2393 *
2394 * @type {number}
2395 */
2396 set _scrollTop(top) {
2397 if (this.scrollTarget === this._doc) {
2398 window.scrollTo(window.pageXOffset, top);
2399 } else if (this._isValidScrollTarget()) {
2400 this.scrollTarget.scrollTop = top;
2401 }
2402 },
2403
2404 /**
2405 * Sets the number of pixels that the content of an element is scrolled to t he left.
2406 *
2407 * @type {number}
2408 */
2409 set _scrollLeft(left) {
2410 if (this.scrollTarget === this._doc) {
2411 window.scrollTo(left, window.pageYOffset);
2412 } else if (this._isValidScrollTarget()) {
2413 this.scrollTarget.scrollLeft = left;
2414 }
2415 },
2416
2417 /**
2418 * Scrolls the content to a particular place.
2419 *
2420 * @method scroll
2421 * @param {number} left The left position
2422 * @param {number} top The top position
2423 */
2424 scroll: function(left, top) {
2425 if (this.scrollTarget === this._doc) {
2426 window.scrollTo(left, top);
2427 } else if (this._isValidScrollTarget()) {
2428 this.scrollTarget.scrollLeft = left;
2429 this.scrollTarget.scrollTop = top;
2430 }
2431 },
2432
2433 /**
2434 * Gets the width of the scroll target.
2435 *
2436 * @type {number}
2437 */
2438 get _scrollTargetWidth() {
2439 if (this._isValidScrollTarget()) {
2440 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
2441 }
2442 return 0;
2443 },
2444
2445 /**
2446 * Gets the height of the scroll target.
2447 *
2448 * @type {number}
2449 */
2450 get _scrollTargetHeight() {
2451 if (this._isValidScrollTarget()) {
2452 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
2453 }
2454 return 0;
2455 },
2456
2457 /**
2458 * Returns true if the scroll target is a valid HTMLElement.
2459 *
2460 * @return {boolean}
2461 */
2462 _isValidScrollTarget: function() {
2463 return this.scrollTarget instanceof HTMLElement;
2464 } 3796 }
3797
2465 }; 3798 };
2466 (function() { 3799 /**
2467 3800 * @demo demo/index.html
2468 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); 3801 * @polymerBehavior Polymer.IronButtonState
2469 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; 3802 */
2470 var DEFAULT_PHYSICAL_COUNT = 3; 3803 Polymer.IronButtonStateImpl = {
2471 var HIDDEN_Y = '-10000px';
2472 var DEFAULT_GRID_SIZE = 200;
2473 var SECRET_TABINDEX = -100;
2474
2475 Polymer({
2476
2477 is: 'iron-list',
2478 3804
2479 properties: { 3805 properties: {
2480 3806
2481 /** 3807 /**
2482 * An array containing items determining how many instances of the templat e 3808 * If true, the user is currently holding down the button.
2483 * to stamp and that that each template instance should bind to. 3809 */
2484 */ 3810 pressed: {
2485 items: { 3811 type: Boolean,
2486 type: Array 3812 readOnly: true,
2487 }, 3813 value: false,
2488 3814 reflectToAttribute: true,
2489 /** 3815 observer: '_pressedChanged'
2490 * The max count of physical items the pool can extend to. 3816 },
2491 */ 3817
2492 maxPhysicalCount: { 3818 /**
2493 type: Number, 3819 * If true, the button toggles the active state with each tap or press
2494 value: 500 3820 * of the spacebar.
2495 }, 3821 */
2496 3822 toggles: {
2497 /**
2498 * The name of the variable to add to the binding scope for the array
2499 * element associated with a given template instance.
2500 */
2501 as: {
2502 type: String,
2503 value: 'item'
2504 },
2505
2506 /**
2507 * The name of the variable to add to the binding scope with the index
2508 * for the row.
2509 */
2510 indexAs: {
2511 type: String,
2512 value: 'index'
2513 },
2514
2515 /**
2516 * The name of the variable to add to the binding scope to indicate
2517 * if the row is selected.
2518 */
2519 selectedAs: {
2520 type: String,
2521 value: 'selected'
2522 },
2523
2524 /**
2525 * When true, the list is rendered as a grid. Grid items must have
2526 * fixed width and height set via CSS. e.g.
2527 *
2528 * ```html
2529 * <iron-list grid>
2530 * <template>
2531 * <div style="width: 100px; height: 100px;"> 100x100 </div>
2532 * </template>
2533 * </iron-list>
2534 * ```
2535 */
2536 grid: {
2537 type: Boolean, 3823 type: Boolean,
2538 value: false, 3824 value: false,
2539 reflectToAttribute: true 3825 reflectToAttribute: true
2540 }, 3826 },
2541 3827
2542 /** 3828 /**
2543 * When true, tapping a row will select the item, placing its data model 3829 * If true, the button is a toggle and is currently in the active state.
2544 * in the set of selected items retrievable via the selection property. 3830 */
3831 active: {
3832 type: Boolean,
3833 value: false,
3834 notify: true,
3835 reflectToAttribute: true
3836 },
3837
3838 /**
3839 * True if the element is currently being pressed by a "pointer," which
3840 * is loosely defined as mouse or touch input (but specifically excluding
3841 * keyboard input).
3842 */
3843 pointerDown: {
3844 type: Boolean,
3845 readOnly: true,
3846 value: false
3847 },
3848
3849 /**
3850 * True if the input device that caused the element to receive focus
3851 * was a keyboard.
3852 */
3853 receivedFocusFromKeyboard: {
3854 type: Boolean,
3855 readOnly: true
3856 },
3857
3858 /**
3859 * The aria attribute to be set if the button is a toggle and in the
3860 * active state.
3861 */
3862 ariaActiveAttribute: {
3863 type: String,
3864 value: 'aria-pressed',
3865 observer: '_ariaActiveAttributeChanged'
3866 }
3867 },
3868
3869 listeners: {
3870 down: '_downHandler',
3871 up: '_upHandler',
3872 tap: '_tapHandler'
3873 },
3874
3875 observers: [
3876 '_detectKeyboardFocus(focused)',
3877 '_activeChanged(active, ariaActiveAttribute)'
3878 ],
3879
3880 keyBindings: {
3881 'enter:keydown': '_asyncClick',
3882 'space:keydown': '_spaceKeyDownHandler',
3883 'space:keyup': '_spaceKeyUpHandler',
3884 },
3885
3886 _mouseEventRe: /^mouse/,
3887
3888 _tapHandler: function() {
3889 if (this.toggles) {
3890 // a tap is needed to toggle the active state
3891 this._userActivate(!this.active);
3892 } else {
3893 this.active = false;
3894 }
3895 },
3896
3897 _detectKeyboardFocus: function(focused) {
3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
3899 },
3900
3901 // to emulate native checkbox, (de-)activations from a user interaction fire
3902 // 'change' events
3903 _userActivate: function(active) {
3904 if (this.active !== active) {
3905 this.active = active;
3906 this.fire('change');
3907 }
3908 },
3909
3910 _downHandler: function(event) {
3911 this._setPointerDown(true);
3912 this._setPressed(true);
3913 this._setReceivedFocusFromKeyboard(false);
3914 },
3915
3916 _upHandler: function() {
3917 this._setPointerDown(false);
3918 this._setPressed(false);
3919 },
3920
3921 /**
3922 * @param {!KeyboardEvent} event .
3923 */
3924 _spaceKeyDownHandler: function(event) {
3925 var keyboardEvent = event.detail.keyboardEvent;
3926 var target = Polymer.dom(keyboardEvent).localTarget;
3927
3928 // Ignore the event if this is coming from a focused light child, since th at
3929 // element will deal with it.
3930 if (this.isLightDescendant(/** @type {Node} */(target)))
3931 return;
3932
3933 keyboardEvent.preventDefault();
3934 keyboardEvent.stopImmediatePropagation();
3935 this._setPressed(true);
3936 },
3937
3938 /**
3939 * @param {!KeyboardEvent} event .
3940 */
3941 _spaceKeyUpHandler: function(event) {
3942 var keyboardEvent = event.detail.keyboardEvent;
3943 var target = Polymer.dom(keyboardEvent).localTarget;
3944
3945 // Ignore the event if this is coming from a focused light child, since th at
3946 // element will deal with it.
3947 if (this.isLightDescendant(/** @type {Node} */(target)))
3948 return;
3949
3950 if (this.pressed) {
3951 this._asyncClick();
3952 }
3953 this._setPressed(false);
3954 },
3955
3956 // trigger click asynchronously, the asynchrony is useful to allow one
3957 // event handler to unwind before triggering another event
3958 _asyncClick: function() {
3959 this.async(function() {
3960 this.click();
3961 }, 1);
3962 },
3963
3964 // any of these changes are considered a change to button state
3965
3966 _pressedChanged: function(pressed) {
3967 this._changedButtonState();
3968 },
3969
3970 _ariaActiveAttributeChanged: function(value, oldValue) {
3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
3972 this.removeAttribute(oldValue);
3973 }
3974 },
3975
3976 _activeChanged: function(active, ariaActiveAttribute) {
3977 if (this.toggles) {
3978 this.setAttribute(this.ariaActiveAttribute,
3979 active ? 'true' : 'false');
3980 } else {
3981 this.removeAttribute(this.ariaActiveAttribute);
3982 }
3983 this._changedButtonState();
3984 },
3985
3986 _controlStateChanged: function() {
3987 if (this.disabled) {
3988 this._setPressed(false);
3989 } else {
3990 this._changedButtonState();
3991 }
3992 },
3993
3994 // provide hook for follow-on behaviors to react to button-state
3995
3996 _changedButtonState: function() {
3997 if (this._buttonStateChanged) {
3998 this._buttonStateChanged(); // abstract
3999 }
4000 }
4001
4002 };
4003
4004 /** @polymerBehavior */
4005 Polymer.IronButtonState = [
4006 Polymer.IronA11yKeysBehavior,
4007 Polymer.IronButtonStateImpl
4008 ];
4009 (function() {
4010 var Utility = {
4011 distance: function(x1, y1, x2, y2) {
4012 var xDelta = (x1 - x2);
4013 var yDelta = (y1 - y2);
4014
4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
4016 },
4017
4018 now: window.performance && window.performance.now ?
4019 window.performance.now.bind(window.performance) : Date.now
4020 };
4021
4022 /**
4023 * @param {HTMLElement} element
4024 * @constructor
4025 */
4026 function ElementMetrics(element) {
4027 this.element = element;
4028 this.width = this.boundingRect.width;
4029 this.height = this.boundingRect.height;
4030
4031 this.size = Math.max(this.width, this.height);
4032 }
4033
4034 ElementMetrics.prototype = {
4035 get boundingRect () {
4036 return this.element.getBoundingClientRect();
4037 },
4038
4039 furthestCornerDistanceFrom: function(x, y) {
4040 var topLeft = Utility.distance(x, y, 0, 0);
4041 var topRight = Utility.distance(x, y, this.width, 0);
4042 var bottomLeft = Utility.distance(x, y, 0, this.height);
4043 var bottomRight = Utility.distance(x, y, this.width, this.height);
4044
4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
4046 }
4047 };
4048
4049 /**
4050 * @param {HTMLElement} element
4051 * @constructor
4052 */
4053 function Ripple(element) {
4054 this.element = element;
4055 this.color = window.getComputedStyle(element).color;
4056
4057 this.wave = document.createElement('div');
4058 this.waveContainer = document.createElement('div');
4059 this.wave.style.backgroundColor = this.color;
4060 this.wave.classList.add('wave');
4061 this.waveContainer.classList.add('wave-container');
4062 Polymer.dom(this.waveContainer).appendChild(this.wave);
4063
4064 this.resetInteractionState();
4065 }
4066
4067 Ripple.MAX_RADIUS = 300;
4068
4069 Ripple.prototype = {
4070 get recenters() {
4071 return this.element.recenters;
4072 },
4073
4074 get center() {
4075 return this.element.center;
4076 },
4077
4078 get mouseDownElapsed() {
4079 var elapsed;
4080
4081 if (!this.mouseDownStart) {
4082 return 0;
4083 }
4084
4085 elapsed = Utility.now() - this.mouseDownStart;
4086
4087 if (this.mouseUpStart) {
4088 elapsed -= this.mouseUpElapsed;
4089 }
4090
4091 return elapsed;
4092 },
4093
4094 get mouseUpElapsed() {
4095 return this.mouseUpStart ?
4096 Utility.now () - this.mouseUpStart : 0;
4097 },
4098
4099 get mouseDownElapsedSeconds() {
4100 return this.mouseDownElapsed / 1000;
4101 },
4102
4103 get mouseUpElapsedSeconds() {
4104 return this.mouseUpElapsed / 1000;
4105 },
4106
4107 get mouseInteractionSeconds() {
4108 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
4109 },
4110
4111 get initialOpacity() {
4112 return this.element.initialOpacity;
4113 },
4114
4115 get opacityDecayVelocity() {
4116 return this.element.opacityDecayVelocity;
4117 },
4118
4119 get radius() {
4120 var width2 = this.containerMetrics.width * this.containerMetrics.width;
4121 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
4122 var waveRadius = Math.min(
4123 Math.sqrt(width2 + height2),
4124 Ripple.MAX_RADIUS
4125 ) * 1.1 + 5;
4126
4127 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
4128 var timeNow = this.mouseInteractionSeconds / duration;
4129 var size = waveRadius * (1 - Math.pow(80, -timeNow));
4130
4131 return Math.abs(size);
4132 },
4133
4134 get opacity() {
4135 if (!this.mouseUpStart) {
4136 return this.initialOpacity;
4137 }
4138
4139 return Math.max(
4140 0,
4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
4142 );
4143 },
4144
4145 get outerOpacity() {
4146 // Linear increase in background opacity, capped at the opacity
4147 // of the wavefront (waveOpacity).
4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
4149 var waveOpacity = this.opacity;
4150
4151 return Math.max(
4152 0,
4153 Math.min(outerOpacity, waveOpacity)
4154 );
4155 },
4156
4157 get isOpacityFullyDecayed() {
4158 return this.opacity < 0.01 &&
4159 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
4160 },
4161
4162 get isRestingAtMaxRadius() {
4163 return this.opacity >= this.initialOpacity &&
4164 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
4165 },
4166
4167 get isAnimationComplete() {
4168 return this.mouseUpStart ?
4169 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
4170 },
4171
4172 get translationFraction() {
4173 return Math.min(
4174 1,
4175 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
4176 );
4177 },
4178
4179 get xNow() {
4180 if (this.xEnd) {
4181 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
4182 }
4183
4184 return this.xStart;
4185 },
4186
4187 get yNow() {
4188 if (this.yEnd) {
4189 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
4190 }
4191
4192 return this.yStart;
4193 },
4194
4195 get isMouseDown() {
4196 return this.mouseDownStart && !this.mouseUpStart;
4197 },
4198
4199 resetInteractionState: function() {
4200 this.maxRadius = 0;
4201 this.mouseDownStart = 0;
4202 this.mouseUpStart = 0;
4203
4204 this.xStart = 0;
4205 this.yStart = 0;
4206 this.xEnd = 0;
4207 this.yEnd = 0;
4208 this.slideDistance = 0;
4209
4210 this.containerMetrics = new ElementMetrics(this.element);
4211 },
4212
4213 draw: function() {
4214 var scale;
4215 var translateString;
4216 var dx;
4217 var dy;
4218
4219 this.wave.style.opacity = this.opacity;
4220
4221 scale = this.radius / (this.containerMetrics.size / 2);
4222 dx = this.xNow - (this.containerMetrics.width / 2);
4223 dy = this.yNow - (this.containerMetrics.height / 2);
4224
4225
4226 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
4227 // https://bugs.webkit.org/show_bug.cgi?id=98538
4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
4232 },
4233
4234 /** @param {Event=} event */
4235 downAction: function(event) {
4236 var xCenter = this.containerMetrics.width / 2;
4237 var yCenter = this.containerMetrics.height / 2;
4238
4239 this.resetInteractionState();
4240 this.mouseDownStart = Utility.now();
4241
4242 if (this.center) {
4243 this.xStart = xCenter;
4244 this.yStart = yCenter;
4245 this.slideDistance = Utility.distance(
4246 this.xStart, this.yStart, this.xEnd, this.yEnd
4247 );
4248 } else {
4249 this.xStart = event ?
4250 event.detail.x - this.containerMetrics.boundingRect.left :
4251 this.containerMetrics.width / 2;
4252 this.yStart = event ?
4253 event.detail.y - this.containerMetrics.boundingRect.top :
4254 this.containerMetrics.height / 2;
4255 }
4256
4257 if (this.recenters) {
4258 this.xEnd = xCenter;
4259 this.yEnd = yCenter;
4260 this.slideDistance = Utility.distance(
4261 this.xStart, this.yStart, this.xEnd, this.yEnd
4262 );
4263 }
4264
4265 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
4266 this.xStart,
4267 this.yStart
4268 );
4269
4270 this.waveContainer.style.top =
4271 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
4272 this.waveContainer.style.left =
4273 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
4274
4275 this.waveContainer.style.width = this.containerMetrics.size + 'px';
4276 this.waveContainer.style.height = this.containerMetrics.size + 'px';
4277 },
4278
4279 /** @param {Event=} event */
4280 upAction: function(event) {
4281 if (!this.isMouseDown) {
4282 return;
4283 }
4284
4285 this.mouseUpStart = Utility.now();
4286 },
4287
4288 remove: function() {
4289 Polymer.dom(this.waveContainer.parentNode).removeChild(
4290 this.waveContainer
4291 );
4292 }
4293 };
4294
4295 Polymer({
4296 is: 'paper-ripple',
4297
4298 behaviors: [
4299 Polymer.IronA11yKeysBehavior
4300 ],
4301
4302 properties: {
4303 /**
4304 * The initial opacity set on the wave.
4305 *
4306 * @attribute initialOpacity
4307 * @type number
4308 * @default 0.25
4309 */
4310 initialOpacity: {
4311 type: Number,
4312 value: 0.25
4313 },
4314
4315 /**
4316 * How fast (opacity per second) the wave fades out.
4317 *
4318 * @attribute opacityDecayVelocity
4319 * @type number
4320 * @default 0.8
4321 */
4322 opacityDecayVelocity: {
4323 type: Number,
4324 value: 0.8
4325 },
4326
4327 /**
4328 * If true, ripples will exhibit a gravitational pull towards
4329 * the center of their container as they fade away.
4330 *
4331 * @attribute recenters
4332 * @type boolean
4333 * @default false
4334 */
4335 recenters: {
4336 type: Boolean,
4337 value: false
4338 },
4339
4340 /**
4341 * If true, ripples will center inside its container
4342 *
4343 * @attribute recenters
4344 * @type boolean
4345 * @default false
4346 */
4347 center: {
4348 type: Boolean,
4349 value: false
4350 },
4351
4352 /**
4353 * A list of the visual ripples.
4354 *
4355 * @attribute ripples
4356 * @type Array
4357 * @default []
4358 */
4359 ripples: {
4360 type: Array,
4361 value: function() {
4362 return [];
4363 }
4364 },
4365
4366 /**
4367 * True when there are visible ripples animating within the
4368 * element.
4369 */
4370 animating: {
4371 type: Boolean,
4372 readOnly: true,
4373 reflectToAttribute: true,
4374 value: false
4375 },
4376
4377 /**
4378 * If true, the ripple will remain in the "down" state until `holdDown`
4379 * is set to false again.
4380 */
4381 holdDown: {
4382 type: Boolean,
4383 value: false,
4384 observer: '_holdDownChanged'
4385 },
4386
4387 /**
4388 * If true, the ripple will not generate a ripple effect
4389 * via pointer interaction.
4390 * Calling ripple's imperative api like `simulatedRipple` will
4391 * still generate the ripple effect.
4392 */
4393 noink: {
4394 type: Boolean,
4395 value: false
4396 },
4397
4398 _animating: {
4399 type: Boolean
4400 },
4401
4402 _boundAnimate: {
4403 type: Function,
4404 value: function() {
4405 return this.animate.bind(this);
4406 }
4407 }
4408 },
4409
4410 get target () {
4411 return this.keyEventTarget;
4412 },
4413
4414 keyBindings: {
4415 'enter:keydown': '_onEnterKeydown',
4416 'space:keydown': '_onSpaceKeydown',
4417 'space:keyup': '_onSpaceKeyup'
4418 },
4419
4420 attached: function() {
4421 // Set up a11yKeysBehavior to listen to key events on the target,
4422 // so that space and enter activate the ripple even if the target doesn' t
4423 // handle key events. The key handlers deal with `noink` themselves.
4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
4426 } else {
4427 this.keyEventTarget = this.parentNode;
4428 }
4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
4430 this.listen(keyEventTarget, 'up', 'uiUpAction');
4431 this.listen(keyEventTarget, 'down', 'uiDownAction');
4432 },
4433
4434 detached: function() {
4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
4437 this.keyEventTarget = null;
4438 },
4439
4440 get shouldKeepAnimating () {
4441 for (var index = 0; index < this.ripples.length; ++index) {
4442 if (!this.ripples[index].isAnimationComplete) {
4443 return true;
4444 }
4445 }
4446
4447 return false;
4448 },
4449
4450 simulatedRipple: function() {
4451 this.downAction(null);
4452
4453 // Please see polymer/polymer#1305
4454 this.async(function() {
4455 this.upAction();
4456 }, 1);
4457 },
4458
4459 /**
4460 * Provokes a ripple down effect via a UI event,
4461 * respecting the `noink` property.
4462 * @param {Event=} event
4463 */
4464 uiDownAction: function(event) {
4465 if (!this.noink) {
4466 this.downAction(event);
4467 }
4468 },
4469
4470 /**
4471 * Provokes a ripple down effect via a UI event,
4472 * *not* respecting the `noink` property.
4473 * @param {Event=} event
4474 */
4475 downAction: function(event) {
4476 if (this.holdDown && this.ripples.length > 0) {
4477 return;
4478 }
4479
4480 var ripple = this.addRipple();
4481
4482 ripple.downAction(event);
4483
4484 if (!this._animating) {
4485 this._animating = true;
4486 this.animate();
4487 }
4488 },
4489
4490 /**
4491 * Provokes a ripple up effect via a UI event,
4492 * respecting the `noink` property.
4493 * @param {Event=} event
4494 */
4495 uiUpAction: function(event) {
4496 if (!this.noink) {
4497 this.upAction(event);
4498 }
4499 },
4500
4501 /**
4502 * Provokes a ripple up effect via a UI event,
4503 * *not* respecting the `noink` property.
4504 * @param {Event=} event
4505 */
4506 upAction: function(event) {
4507 if (this.holdDown) {
4508 return;
4509 }
4510
4511 this.ripples.forEach(function(ripple) {
4512 ripple.upAction(event);
4513 });
4514
4515 this._animating = true;
4516 this.animate();
4517 },
4518
4519 onAnimationComplete: function() {
4520 this._animating = false;
4521 this.$.background.style.backgroundColor = null;
4522 this.fire('transitionend');
4523 },
4524
4525 addRipple: function() {
4526 var ripple = new Ripple(this);
4527
4528 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
4529 this.$.background.style.backgroundColor = ripple.color;
4530 this.ripples.push(ripple);
4531
4532 this._setAnimating(true);
4533
4534 return ripple;
4535 },
4536
4537 removeRipple: function(ripple) {
4538 var rippleIndex = this.ripples.indexOf(ripple);
4539
4540 if (rippleIndex < 0) {
4541 return;
4542 }
4543
4544 this.ripples.splice(rippleIndex, 1);
4545
4546 ripple.remove();
4547
4548 if (!this.ripples.length) {
4549 this._setAnimating(false);
4550 }
4551 },
4552
4553 animate: function() {
4554 if (!this._animating) {
4555 return;
4556 }
4557 var index;
4558 var ripple;
4559
4560 for (index = 0; index < this.ripples.length; ++index) {
4561 ripple = this.ripples[index];
4562
4563 ripple.draw();
4564
4565 this.$.background.style.opacity = ripple.outerOpacity;
4566
4567 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
4568 this.removeRipple(ripple);
4569 }
4570 }
4571
4572 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
4573 this.onAnimationComplete();
4574 } else {
4575 window.requestAnimationFrame(this._boundAnimate);
4576 }
4577 },
4578
4579 _onEnterKeydown: function() {
4580 this.uiDownAction();
4581 this.async(this.uiUpAction, 1);
4582 },
4583
4584 _onSpaceKeydown: function() {
4585 this.uiDownAction();
4586 },
4587
4588 _onSpaceKeyup: function() {
4589 this.uiUpAction();
4590 },
4591
4592 // note: holdDown does not respect noink since it can be a focus based
4593 // effect.
4594 _holdDownChanged: function(newVal, oldVal) {
4595 if (oldVal === undefined) {
4596 return;
4597 }
4598 if (newVal) {
4599 this.downAction();
4600 } else {
4601 this.upAction();
4602 }
4603 }
4604
4605 /**
4606 Fired when the animation finishes.
4607 This is useful if you want to wait until
4608 the ripple animation finishes to perform some action.
4609
4610 @event transitionend
4611 @param {{node: Object}} detail Contains the animated node.
4612 */
4613 });
4614 })();
4615 /**
4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
4617 * when the element has focus via pointer or keyboard.
4618 *
4619 * NOTE: This behavior is intended to be used in conjunction with and after
4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
4621 *
4622 * @polymerBehavior Polymer.PaperRippleBehavior
4623 */
4624 Polymer.PaperRippleBehavior = {
4625 properties: {
4626 /**
4627 * If true, the element will not produce a ripple effect when interacted
4628 * with via the pointer.
4629 */
4630 noink: {
4631 type: Boolean,
4632 observer: '_noinkChanged'
4633 },
4634
4635 /**
4636 * @type {Element|undefined}
4637 */
4638 _rippleContainer: {
4639 type: Object,
4640 }
4641 },
4642
4643 /**
4644 * Ensures a `<paper-ripple>` element is available when the element is
4645 * focused.
4646 */
4647 _buttonStateChanged: function() {
4648 if (this.focused) {
4649 this.ensureRipple();
4650 }
4651 },
4652
4653 /**
4654 * In addition to the functionality provided in `IronButtonState`, ensures
4655 * a ripple effect is created when the element is in a `pressed` state.
4656 */
4657 _downHandler: function(event) {
4658 Polymer.IronButtonStateImpl._downHandler.call(this, event);
4659 if (this.pressed) {
4660 this.ensureRipple(event);
4661 }
4662 },
4663
4664 /**
4665 * Ensures this element contains a ripple effect. For startup efficiency
4666 * the ripple effect is dynamically on demand when needed.
4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
4668 * ripple.
4669 */
4670 ensureRipple: function(optTriggeringEvent) {
4671 if (!this.hasRipple()) {
4672 this._ripple = this._createRipple();
4673 this._ripple.noink = this.noink;
4674 var rippleContainer = this._rippleContainer || this.root;
4675 if (rippleContainer) {
4676 Polymer.dom(rippleContainer).appendChild(this._ripple);
4677 }
4678 if (optTriggeringEvent) {
4679 // Check if the event happened inside of the ripple container
4680 // Fall back to host instead of the root because distributed text
4681 // nodes are not valid event targets
4682 var domContainer = Polymer.dom(this._rippleContainer || this);
4683 var target = Polymer.dom(optTriggeringEvent).rootTarget;
4684 if (domContainer.deepContains( /** @type {Node} */(target))) {
4685 this._ripple.uiDownAction(optTriggeringEvent);
4686 }
4687 }
4688 }
4689 },
4690
4691 /**
4692 * Returns the `<paper-ripple>` element used by this element to create
4693 * ripple effects. The element's ripple is created on demand, when
4694 * necessary, and calling this method will force the
4695 * ripple to be created.
4696 */
4697 getRipple: function() {
4698 this.ensureRipple();
4699 return this._ripple;
4700 },
4701
4702 /**
4703 * Returns true if this element currently contains a ripple effect.
4704 * @return {boolean}
4705 */
4706 hasRipple: function() {
4707 return Boolean(this._ripple);
4708 },
4709
4710 /**
4711 * Create the element's ripple effect via creating a `<paper-ripple>`.
4712 * Override this method to customize the ripple element.
4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
4714 */
4715 _createRipple: function() {
4716 return /** @type {!PaperRippleElement} */ (
4717 document.createElement('paper-ripple'));
4718 },
4719
4720 _noinkChanged: function(noink) {
4721 if (this.hasRipple()) {
4722 this._ripple.noink = noink;
4723 }
4724 }
4725 };
4726 /** @polymerBehavior Polymer.PaperButtonBehavior */
4727 Polymer.PaperButtonBehaviorImpl = {
4728 properties: {
4729 /**
4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the
4731 * shadow, and each increasing number greater than 0 will be "deeper"
4732 * than the last.
2545 * 4733 *
2546 * Note that tapping focusable elements within the list item will not 4734 * @attribute elevation
2547 * result in selection, since they are presumed to have their * own action . 4735 * @type number
2548 */ 4736 * @default 1
2549 selectionEnabled: { 4737 */
2550 type: Boolean, 4738 elevation: {
2551 value: false 4739 type: Number,
2552 }, 4740 reflectToAttribute: true,
2553 4741 readOnly: true
2554 /**
2555 * When `multiSelection` is false, this is the currently selected item, or `null`
2556 * if no item is selected.
2557 */
2558 selectedItem: {
2559 type: Object,
2560 notify: true
2561 },
2562
2563 /**
2564 * When `multiSelection` is true, this is an array that contains the selec ted items.
2565 */
2566 selectedItems: {
2567 type: Object,
2568 notify: true
2569 },
2570
2571 /**
2572 * When `true`, multiple items may be selected at once (in this case,
2573 * `selected` is an array of currently selected items). When `false`,
2574 * only one item may be selected at a time.
2575 */
2576 multiSelection: {
2577 type: Boolean,
2578 value: false
2579 } 4742 }
2580 }, 4743 },
2581 4744
2582 observers: [ 4745 observers: [
2583 '_itemsChanged(items.*)', 4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
2584 '_selectionEnabledChanged(selectionEnabled)', 4747 '_computeKeyboardClass(receivedFocusFromKeyboard)'
2585 '_multiSelectionChanged(multiSelection)',
2586 '_setOverflow(scrollTarget)'
2587 ], 4748 ],
2588 4749
2589 behaviors: [ 4750 hostAttributes: {
2590 Polymer.Templatizer, 4751 role: 'button',
2591 Polymer.IronResizableBehavior, 4752 tabindex: '0',
2592 Polymer.IronA11yKeysBehavior, 4753 animated: true
2593 Polymer.IronScrollTargetBehavior 4754 },
2594 ], 4755
2595 4756 _calculateElevation: function() {
2596 keyBindings: { 4757 var e = 1;
2597 'up': '_didMoveUp', 4758 if (this.disabled) {
2598 'down': '_didMoveDown', 4759 e = 0;
2599 'enter': '_didEnter' 4760 } else if (this.active || this.pressed) {
2600 }, 4761 e = 4;
2601 4762 } else if (this.receivedFocusFromKeyboard) {
2602 /** 4763 e = 3;
2603 * The ratio of hidden tiles that should remain in the scroll direction. 4764 }
2604 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons. 4765 this._setElevation(e);
2605 */ 4766 },
2606 _ratio: 0.5, 4767
2607 4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
2608 /** 4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
2609 * The padding-top value for the list. 4770 },
2610 */ 4771
2611 _scrollerPaddingTop: 0, 4772 /**
2612 4773 * In addition to `IronButtonState` behavior, when space key goes down,
2613 /** 4774 * create a ripple down effect.
2614 * This value is the same as `scrollTop`.
2615 */
2616 _scrollPosition: 0,
2617
2618 /**
2619 * The sum of the heights of all the tiles in the DOM.
2620 */
2621 _physicalSize: 0,
2622
2623 /**
2624 * The average `offsetHeight` of the tiles observed till now.
2625 */
2626 _physicalAverage: 0,
2627
2628 /**
2629 * The number of tiles which `offsetHeight` > 0 observed until now.
2630 */
2631 _physicalAverageCount: 0,
2632
2633 /**
2634 * The Y position of the item rendered in the `_physicalStart`
2635 * tile relative to the scrolling list.
2636 */
2637 _physicalTop: 0,
2638
2639 /**
2640 * The number of items in the list.
2641 */
2642 _virtualCount: 0,
2643
2644 /**
2645 * A map between an item key and its physical item index
2646 */
2647 _physicalIndexForKey: null,
2648
2649 /**
2650 * The estimated scroll height based on `_physicalAverage`
2651 */
2652 _estScrollHeight: 0,
2653
2654 /**
2655 * The scroll height of the dom node
2656 */
2657 _scrollHeight: 0,
2658
2659 /**
2660 * The height of the list. This is referred as the viewport in the context o f list.
2661 */
2662 _viewportHeight: 0,
2663
2664 /**
2665 * The width of the list. This is referred as the viewport in the context of list.
2666 */
2667 _viewportWidth: 0,
2668
2669 /**
2670 * An array of DOM nodes that are currently in the tree
2671 * @type {?Array<!TemplatizerNode>}
2672 */
2673 _physicalItems: null,
2674
2675 /**
2676 * An array of heights for each item in `_physicalItems`
2677 * @type {?Array<number>}
2678 */
2679 _physicalSizes: null,
2680
2681 /**
2682 * A cached value for the first visible index.
2683 * See `firstVisibleIndex`
2684 * @type {?number}
2685 */
2686 _firstVisibleIndexVal: null,
2687
2688 /**
2689 * A cached value for the last visible index.
2690 * See `lastVisibleIndex`
2691 * @type {?number}
2692 */
2693 _lastVisibleIndexVal: null,
2694
2695 /**
2696 * A Polymer collection for the items.
2697 * @type {?Polymer.Collection}
2698 */
2699 _collection: null,
2700
2701 /**
2702 * True if the current item list was rendered for the first time
2703 * after attached.
2704 */
2705 _itemsRendered: false,
2706
2707 /**
2708 * The page that is currently rendered.
2709 */
2710 _lastPage: null,
2711
2712 /**
2713 * The max number of pages to render. One page is equivalent to the height o f the list.
2714 */
2715 _maxPages: 3,
2716
2717 /**
2718 * The currently focused physical item.
2719 */
2720 _focusedItem: null,
2721
2722 /**
2723 * The index of the `_focusedItem`.
2724 */
2725 _focusedIndex: -1,
2726
2727 /**
2728 * The the item that is focused if it is moved offscreen.
2729 * @private {?TemplatizerNode}
2730 */
2731 _offscreenFocusedItem: null,
2732
2733 /**
2734 * The item that backfills the `_offscreenFocusedItem` in the physical items
2735 * list when that item is moved offscreen.
2736 */
2737 _focusBackfillItem: null,
2738
2739 /**
2740 * The maximum items per row
2741 */
2742 _itemsPerRow: 1,
2743
2744 /**
2745 * The width of each grid item
2746 */
2747 _itemWidth: 0,
2748
2749 /**
2750 * The height of the row in grid layout.
2751 */
2752 _rowHeight: 0,
2753
2754 /**
2755 * The bottom of the physical content.
2756 */
2757 get _physicalBottom() {
2758 return this._physicalTop + this._physicalSize;
2759 },
2760
2761 /**
2762 * The bottom of the scroll.
2763 */
2764 get _scrollBottom() {
2765 return this._scrollPosition + this._viewportHeight;
2766 },
2767
2768 /**
2769 * The n-th item rendered in the last physical item.
2770 */
2771 get _virtualEnd() {
2772 return this._virtualStart + this._physicalCount - 1;
2773 },
2774
2775 /**
2776 * The height of the physical content that isn't on the screen.
2777 */
2778 get _hiddenContentSize() {
2779 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
2780 return size - this._viewportHeight;
2781 },
2782
2783 /**
2784 * The maximum scroll top value.
2785 */
2786 get _maxScrollTop() {
2787 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
2788 },
2789
2790 /**
2791 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
2792 */
2793 _minVirtualStart: 0,
2794
2795 /**
2796 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
2797 */
2798 get _maxVirtualStart() {
2799 return Math.max(0, this._virtualCount - this._physicalCount);
2800 },
2801
2802 /**
2803 * The n-th item rendered in the `_physicalStart` tile.
2804 */
2805 _virtualStartVal: 0,
2806
2807 set _virtualStart(val) {
2808 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
2809 },
2810
2811 get _virtualStart() {
2812 return this._virtualStartVal || 0;
2813 },
2814
2815 /**
2816 * The k-th tile that is at the top of the scrolling list.
2817 */
2818 _physicalStartVal: 0,
2819
2820 set _physicalStart(val) {
2821 this._physicalStartVal = val % this._physicalCount;
2822 if (this._physicalStartVal < 0) {
2823 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
2824 }
2825 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2826 },
2827
2828 get _physicalStart() {
2829 return this._physicalStartVal || 0;
2830 },
2831
2832 /**
2833 * The number of tiles in the DOM.
2834 */
2835 _physicalCountVal: 0,
2836
2837 set _physicalCount(val) {
2838 this._physicalCountVal = val;
2839 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
2840 },
2841
2842 get _physicalCount() {
2843 return this._physicalCountVal;
2844 },
2845
2846 /**
2847 * The k-th tile that is at the bottom of the scrolling list.
2848 */
2849 _physicalEnd: 0,
2850
2851 /**
2852 * An optimal physical size such that we will have enough physical items
2853 * to fill up the viewport and recycle when the user scrolls.
2854 * 4775 *
2855 * This default value assumes that we will at least have the equivalent 4776 * @param {!KeyboardEvent} event .
2856 * to a viewport of physical items above and below the user's viewport. 4777 */
2857 */ 4778 _spaceKeyDownHandler: function(event) {
2858 get _optPhysicalSize() { 4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
2859 if (this.grid) { 4780 // Ensure that there is at most one ripple when the space key is held down .
2860 return this._estRowsInView * this._rowHeight * this._maxPages; 4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
2861 } 4782 this._ripple.uiDownAction();
2862 return this._viewportHeight * this._maxPages; 4783 }
2863 }, 4784 },
2864 4785
2865 get _optPhysicalCount() { 4786 /**
2866 return this._estRowsInView * this._itemsPerRow * this._maxPages; 4787 * In addition to `IronButtonState` behavior, when space key goes up,
2867 }, 4788 * create a ripple up effect.
2868
2869 /**
2870 * True if the current list is visible.
2871 */
2872 get _isVisible() {
2873 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
2874 },
2875
2876 /**
2877 * Gets the index of the first visible item in the viewport.
2878 * 4789 *
2879 * @type {number} 4790 * @param {!KeyboardEvent} event .
2880 */ 4791 */
2881 get firstVisibleIndex() { 4792 _spaceKeyUpHandler: function(event) {
2882 if (this._firstVisibleIndexVal === null) { 4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
2883 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 4794 if (this.hasRipple()) {
2884 4795 this._ripple.uiUpAction();
2885 this._firstVisibleIndexVal = this._iterateItems( 4796 }
2886 function(pidx, vidx) { 4797 }
2887 physicalOffset += this._getPhysicalSizeIncrement(pidx); 4798 };
2888 4799
2889 if (physicalOffset > this._scrollPosition) { 4800 /** @polymerBehavior */
2890 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; 4801 Polymer.PaperButtonBehavior = [
2891 } 4802 Polymer.IronButtonState,
2892 // Handle a partially rendered final row in grid mode 4803 Polymer.IronControlState,
2893 if (this.grid && this._virtualCount - 1 === vidx) { 4804 Polymer.PaperRippleBehavior,
2894 return vidx - (vidx % this._itemsPerRow); 4805 Polymer.PaperButtonBehaviorImpl
2895 } 4806 ];
2896 }) || 0; 4807 Polymer({
2897 } 4808 is: 'paper-button',
2898 return this._firstVisibleIndexVal; 4809
2899 }, 4810 behaviors: [
2900 4811 Polymer.PaperButtonBehavior
2901 /** 4812 ],
2902 * Gets the index of the last visible item in the viewport. 4813
2903 * 4814 properties: {
2904 * @type {number} 4815 /**
2905 */ 4816 * If true, the button should be styled with a shadow.
2906 get lastVisibleIndex() { 4817 */
2907 if (this._lastVisibleIndexVal === null) { 4818 raised: {
2908 if (this.grid) { 4819 type: Boolean,
2909 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 4820 reflectToAttribute: true,
2910 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 4821 value: false,
4822 observer: '_calculateElevation'
4823 }
4824 },
4825
4826 _calculateElevation: function() {
4827 if (!this.raised) {
4828 this._setElevation(0);
2911 } else { 4829 } else {
2912 var physicalOffset = this._physicalTop; 4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
2913 this._iterateItems(function(pidx, vidx) { 4831 }
2914 if (physicalOffset < this._scrollBottom) { 4832 }
2915 this._lastVisibleIndexVal = vidx; 4833
2916 } else { 4834 /**
2917 // Break _iterateItems 4835 Fired when the animation finishes.
2918 return true; 4836 This is useful if you want to wait until
2919 } 4837 the ripple animation finishes to perform some action.
2920 physicalOffset += this._getPhysicalSizeIncrement(pidx); 4838
2921 }); 4839 @event transitionend
2922 } 4840 Event param: {{node: Object}} detail Contains the animated node.
2923 } 4841 */
2924 return this._lastVisibleIndexVal; 4842 });
2925 },
2926
2927 get _defaultScrollTarget() {
2928 return this;
2929 },
2930 get _virtualRowCount() {
2931 return Math.ceil(this._virtualCount / this._itemsPerRow);
2932 },
2933
2934 get _estRowsInView() {
2935 return Math.ceil(this._viewportHeight / this._rowHeight);
2936 },
2937
2938 get _physicalRows() {
2939 return Math.ceil(this._physicalCount / this._itemsPerRow);
2940 },
2941
2942 ready: function() {
2943 this.addEventListener('focus', this._didFocus.bind(this), true);
2944 },
2945
2946 attached: function() {
2947 this.updateViewportBoundaries();
2948 this._render();
2949 // `iron-resize` is fired when the list is attached if the event is added
2950 // before attached causing unnecessary work.
2951 this.listen(this, 'iron-resize', '_resizeHandler');
2952 },
2953
2954 detached: function() {
2955 this._itemsRendered = false;
2956 this.unlisten(this, 'iron-resize', '_resizeHandler');
2957 },
2958
2959 /**
2960 * Set the overflow property if this element has its own scrolling region
2961 */
2962 _setOverflow: function(scrollTarget) {
2963 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
2964 this.style.overflow = scrollTarget === this ? 'auto' : '';
2965 },
2966
2967 /**
2968 * Invoke this method if you dynamically update the viewport's
2969 * size or CSS padding.
2970 *
2971 * @method updateViewportBoundaries
2972 */
2973 updateViewportBoundaries: function() {
2974 this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
2975 parseInt(window.getComputedStyle(this)['padding-top'], 10);
2976
2977 this._viewportHeight = this._scrollTargetHeight;
2978 if (this.grid) {
2979 this._updateGridMetrics();
2980 }
2981 },
2982
2983 /**
2984 * Update the models, the position of the
2985 * items in the viewport and recycle tiles as needed.
2986 */
2987 _scrollHandler: function() {
2988 // clamp the `scrollTop` value
2989 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
2990 var delta = scrollTop - this._scrollPosition;
2991 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
2992 var ratio = this._ratio;
2993 var recycledTiles = 0;
2994 var hiddenContentSize = this._hiddenContentSize;
2995 var currentRatio = ratio;
2996 var movingUp = [];
2997
2998 // track the last `scrollTop`
2999 this._scrollPosition = scrollTop;
3000
3001 // clear cached visible indexes
3002 this._firstVisibleIndexVal = null;
3003 this._lastVisibleIndexVal = null;
3004
3005 scrollBottom = this._scrollBottom;
3006 physicalBottom = this._physicalBottom;
3007
3008 // random access
3009 if (Math.abs(delta) > this._physicalSize) {
3010 this._physicalTop += delta;
3011 recycledTiles = Math.round(delta / this._physicalAverage);
3012 }
3013 // scroll up
3014 else if (delta < 0) {
3015 var topSpace = scrollTop - this._physicalTop;
3016 var virtualStart = this._virtualStart;
3017
3018 recycledTileSet = [];
3019
3020 kth = this._physicalEnd;
3021 currentRatio = topSpace / hiddenContentSize;
3022
3023 // move tiles from bottom to top
3024 while (
3025 // approximate `currentRatio` to `ratio`
3026 currentRatio < ratio &&
3027 // recycle less physical items than the total
3028 recycledTiles < this._physicalCount &&
3029 // ensure that these recycled tiles are needed
3030 virtualStart - recycledTiles > 0 &&
3031 // ensure that the tile is not visible
3032 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
3033 ) {
3034
3035 tileHeight = this._getPhysicalSizeIncrement(kth);
3036 currentRatio += tileHeight / hiddenContentSize;
3037 physicalBottom -= tileHeight;
3038 recycledTileSet.push(kth);
3039 recycledTiles++;
3040 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
3041 }
3042
3043 movingUp = recycledTileSet;
3044 recycledTiles = -recycledTiles;
3045 }
3046 // scroll down
3047 else if (delta > 0) {
3048 var bottomSpace = physicalBottom - scrollBottom;
3049 var virtualEnd = this._virtualEnd;
3050 var lastVirtualItemIndex = this._virtualCount-1;
3051
3052 recycledTileSet = [];
3053
3054 kth = this._physicalStart;
3055 currentRatio = bottomSpace / hiddenContentSize;
3056
3057 // move tiles from top to bottom
3058 while (
3059 // approximate `currentRatio` to `ratio`
3060 currentRatio < ratio &&
3061 // recycle less physical items than the total
3062 recycledTiles < this._physicalCount &&
3063 // ensure that these recycled tiles are needed
3064 virtualEnd + recycledTiles < lastVirtualItemIndex &&
3065 // ensure that the tile is not visible
3066 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
3067 ) {
3068
3069 tileHeight = this._getPhysicalSizeIncrement(kth);
3070 currentRatio += tileHeight / hiddenContentSize;
3071
3072 this._physicalTop += tileHeight;
3073 recycledTileSet.push(kth);
3074 recycledTiles++;
3075 kth = (kth + 1) % this._physicalCount;
3076 }
3077 }
3078
3079 if (recycledTiles === 0) {
3080 // Try to increase the pool if the list's client height isn't filled up with physical items
3081 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
3082 this._increasePoolIfNeeded();
3083 }
3084 } else {
3085 this._virtualStart = this._virtualStart + recycledTiles;
3086 this._physicalStart = this._physicalStart + recycledTiles;
3087 this._update(recycledTileSet, movingUp);
3088 }
3089 },
3090
3091 /**
3092 * Update the list of items, starting from the `_virtualStart` item.
3093 * @param {!Array<number>=} itemSet
3094 * @param {!Array<number>=} movingUp
3095 */
3096 _update: function(itemSet, movingUp) {
3097 // manage focus
3098 this._manageFocus();
3099 // update models
3100 this._assignModels(itemSet);
3101 // measure heights
3102 this._updateMetrics(itemSet);
3103 // adjust offset after measuring
3104 if (movingUp) {
3105 while (movingUp.length) {
3106 var idx = movingUp.pop();
3107 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
3108 }
3109 }
3110 // update the position of the items
3111 this._positionItems();
3112 // set the scroller size
3113 this._updateScrollerSize();
3114 // increase the pool of physical items
3115 this._increasePoolIfNeeded();
3116 },
3117
3118 /**
3119 * Creates a pool of DOM elements and attaches them to the local dom.
3120 */
3121 _createPool: function(size) {
3122 var physicalItems = new Array(size);
3123
3124 this._ensureTemplatized();
3125
3126 for (var i = 0; i < size; i++) {
3127 var inst = this.stamp(null);
3128 // First element child is item; Safari doesn't support children[0]
3129 // on a doc fragment
3130 physicalItems[i] = inst.root.querySelector('*');
3131 Polymer.dom(this).appendChild(inst.root);
3132 }
3133 return physicalItems;
3134 },
3135
3136 /**
3137 * Increases the pool of physical items only if needed.
3138 *
3139 * @return {boolean} True if the pool was increased.
3140 */
3141 _increasePoolIfNeeded: function() {
3142 // Base case 1: the list has no height.
3143 if (this._viewportHeight === 0) {
3144 return false;
3145 }
3146 // Base case 2: If the physical size is optimal and the list's client heig ht is full
3147 // with physical items, don't increase the pool.
3148 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
3149 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
3150 return false;
3151 }
3152 // this value should range between [0 <= `currentPage` <= `_maxPages`]
3153 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
3154
3155 if (currentPage === 0) {
3156 // fill the first page
3157 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
3158 } else if (this._lastPage !== currentPage && isClientHeightFull) {
3159 // paint the page and defer the next increase
3160 // wait 16ms which is rough enough to get paint cycle.
3161 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
3162 } else {
3163 // fill the rest of the pages
3164 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
3165 }
3166
3167 this._lastPage = currentPage;
3168
3169 return true;
3170 },
3171
3172 /**
3173 * Increases the pool size.
3174 */
3175 _increasePool: function(missingItems) {
3176 var nextPhysicalCount = Math.min(
3177 this._physicalCount + missingItems,
3178 this._virtualCount - this._virtualStart,
3179 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
3180 );
3181 var prevPhysicalCount = this._physicalCount;
3182 var delta = nextPhysicalCount - prevPhysicalCount;
3183
3184 if (delta <= 0) {
3185 return;
3186 }
3187
3188 [].push.apply(this._physicalItems, this._createPool(delta));
3189 [].push.apply(this._physicalSizes, new Array(delta));
3190
3191 this._physicalCount = prevPhysicalCount + delta;
3192
3193 // update the physical start if we need to preserve the model of the focus ed item.
3194 // In this situation, the focused item is currently rendered and its model would
3195 // have changed after increasing the pool if the physical start remained u nchanged.
3196 if (this._physicalStart > this._physicalEnd &&
3197 this._isIndexRendered(this._focusedIndex) &&
3198 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
3199 this._physicalStart = this._physicalStart + delta;
3200 }
3201 this._update();
3202 },
3203
3204 /**
3205 * Render a new list of items. This method does exactly the same as `update` ,
3206 * but it also ensures that only one `update` cycle is created.
3207 */
3208 _render: function() {
3209 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
3210
3211 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
3212 this._lastPage = 0;
3213 this._update();
3214 this._itemsRendered = true;
3215 }
3216 },
3217
3218 /**
3219 * Templetizes the user template.
3220 */
3221 _ensureTemplatized: function() {
3222 if (!this.ctor) {
3223 // Template instance props that should be excluded from forwarding
3224 var props = {};
3225 props.__key__ = true;
3226 props[this.as] = true;
3227 props[this.indexAs] = true;
3228 props[this.selectedAs] = true;
3229 props.tabIndex = true;
3230
3231 this._instanceProps = props;
3232 this._userTemplate = Polymer.dom(this).querySelector('template');
3233
3234 if (this._userTemplate) {
3235 this.templatize(this._userTemplate);
3236 } else {
3237 console.warn('iron-list requires a template to be provided in light-do m');
3238 }
3239 }
3240 },
3241
3242 /**
3243 * Implements extension point from Templatizer mixin.
3244 */
3245 _getStampedChildren: function() {
3246 return this._physicalItems;
3247 },
3248
3249 /**
3250 * Implements extension point from Templatizer
3251 * Called as a side effect of a template instance path change, responsible
3252 * for notifying items.<key-for-instance>.<path> change up to host.
3253 */
3254 _forwardInstancePath: function(inst, path, value) {
3255 if (path.indexOf(this.as + '.') === 0) {
3256 this.notifyPath('items.' + inst.__key__ + '.' +
3257 path.slice(this.as.length + 1), value);
3258 }
3259 },
3260
3261 /**
3262 * Implements extension point from Templatizer mixin
3263 * Called as side-effect of a host property change, responsible for
3264 * notifying parent path change on each row.
3265 */
3266 _forwardParentProp: function(prop, value) {
3267 if (this._physicalItems) {
3268 this._physicalItems.forEach(function(item) {
3269 item._templateInstance[prop] = value;
3270 }, this);
3271 }
3272 },
3273
3274 /**
3275 * Implements extension point from Templatizer
3276 * Called as side-effect of a host path change, responsible for
3277 * notifying parent.<path> path change on each row.
3278 */
3279 _forwardParentPath: function(path, value) {
3280 if (this._physicalItems) {
3281 this._physicalItems.forEach(function(item) {
3282 item._templateInstance.notifyPath(path, value, true);
3283 }, this);
3284 }
3285 },
3286
3287 /**
3288 * Called as a side effect of a host items.<key>.<path> path change,
3289 * responsible for notifying item.<path> changes.
3290 */
3291 _forwardItemPath: function(path, value) {
3292 if (!this._physicalIndexForKey) {
3293 return;
3294 }
3295 var dot = path.indexOf('.');
3296 var key = path.substring(0, dot < 0 ? path.length : dot);
3297 var idx = this._physicalIndexForKey[key];
3298 var offscreenItem = this._offscreenFocusedItem;
3299 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
3300 offscreenItem : this._physicalItems[idx];
3301
3302 if (!el || el._templateInstance.__key__ !== key) {
3303 return;
3304 }
3305 if (dot >= 0) {
3306 path = this.as + '.' + path.substring(dot+1);
3307 el._templateInstance.notifyPath(path, value, true);
3308 } else {
3309 // Update selection if needed
3310 var currentItem = el._templateInstance[this.as];
3311 if (Array.isArray(this.selectedItems)) {
3312 for (var i = 0; i < this.selectedItems.length; i++) {
3313 if (this.selectedItems[i] === currentItem) {
3314 this.set('selectedItems.' + i, value);
3315 break;
3316 }
3317 }
3318 } else if (this.selectedItem === currentItem) {
3319 this.set('selectedItem', value);
3320 }
3321 el._templateInstance[this.as] = value;
3322 }
3323 },
3324
3325 /**
3326 * Called when the items have changed. That is, ressignments
3327 * to `items`, splices or updates to a single item.
3328 */
3329 _itemsChanged: function(change) {
3330 if (change.path === 'items') {
3331 // reset items
3332 this._virtualStart = 0;
3333 this._physicalTop = 0;
3334 this._virtualCount = this.items ? this.items.length : 0;
3335 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
3336 this._physicalIndexForKey = {};
3337 this._firstVisibleIndexVal = null;
3338 this._lastVisibleIndexVal = null;
3339
3340 this._resetScrollPosition(0);
3341 this._removeFocusedItem();
3342 // create the initial physical items
3343 if (!this._physicalItems) {
3344 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
3345 this._physicalItems = this._createPool(this._physicalCount);
3346 this._physicalSizes = new Array(this._physicalCount);
3347 }
3348
3349 this._physicalStart = 0;
3350
3351 } else if (change.path === 'items.splices') {
3352
3353 this._adjustVirtualIndex(change.value.indexSplices);
3354 this._virtualCount = this.items ? this.items.length : 0;
3355
3356 } else {
3357 // update a single item
3358 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
3359 return;
3360 }
3361
3362 this._itemsRendered = false;
3363 this._debounceTemplate(this._render);
3364 },
3365
3366 /**
3367 * @param {!Array<!PolymerSplice>} splices
3368 */
3369 _adjustVirtualIndex: function(splices) {
3370 splices.forEach(function(splice) {
3371 // deselect removed items
3372 splice.removed.forEach(this._removeItem, this);
3373 // We only need to care about changes happening above the current positi on
3374 if (splice.index < this._virtualStart) {
3375 var delta = Math.max(
3376 splice.addedCount - splice.removed.length,
3377 splice.index - this._virtualStart);
3378
3379 this._virtualStart = this._virtualStart + delta;
3380
3381 if (this._focusedIndex >= 0) {
3382 this._focusedIndex = this._focusedIndex + delta;
3383 }
3384 }
3385 }, this);
3386 },
3387
3388 _removeItem: function(item) {
3389 this.$.selector.deselect(item);
3390 // remove the current focused item
3391 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
3392 this._removeFocusedItem();
3393 }
3394 },
3395
3396 /**
3397 * Executes a provided function per every physical index in `itemSet`
3398 * `itemSet` default value is equivalent to the entire set of physical index es.
3399 *
3400 * @param {!function(number, number)} fn
3401 * @param {!Array<number>=} itemSet
3402 */
3403 _iterateItems: function(fn, itemSet) {
3404 var pidx, vidx, rtn, i;
3405
3406 if (arguments.length === 2 && itemSet) {
3407 for (i = 0; i < itemSet.length; i++) {
3408 pidx = itemSet[i];
3409 vidx = this._computeVidx(pidx);
3410 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3411 return rtn;
3412 }
3413 }
3414 } else {
3415 pidx = this._physicalStart;
3416 vidx = this._virtualStart;
3417
3418 for (; pidx < this._physicalCount; pidx++, vidx++) {
3419 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3420 return rtn;
3421 }
3422 }
3423 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
3424 if ((rtn = fn.call(this, pidx, vidx)) != null) {
3425 return rtn;
3426 }
3427 }
3428 }
3429 },
3430
3431 /**
3432 * Returns the virtual index for a given physical index
3433 *
3434 * @param {number} pidx Physical index
3435 * @return {number}
3436 */
3437 _computeVidx: function(pidx) {
3438 if (pidx >= this._physicalStart) {
3439 return this._virtualStart + (pidx - this._physicalStart);
3440 }
3441 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
3442 },
3443
3444 /**
3445 * Assigns the data models to a given set of items.
3446 * @param {!Array<number>=} itemSet
3447 */
3448 _assignModels: function(itemSet) {
3449 this._iterateItems(function(pidx, vidx) {
3450 var el = this._physicalItems[pidx];
3451 var inst = el._templateInstance;
3452 var item = this.items && this.items[vidx];
3453
3454 if (item != null) {
3455 inst[this.as] = item;
3456 inst.__key__ = this._collection.getKey(item);
3457 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item);
3458 inst[this.indexAs] = vidx;
3459 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
3460 this._physicalIndexForKey[inst.__key__] = pidx;
3461 el.removeAttribute('hidden');
3462 } else {
3463 inst.__key__ = null;
3464 el.setAttribute('hidden', '');
3465 }
3466 }, itemSet);
3467 },
3468
3469 /**
3470 * Updates the height for a given set of items.
3471 *
3472 * @param {!Array<number>=} itemSet
3473 */
3474 _updateMetrics: function(itemSet) {
3475 // Make sure we distributed all the physical items
3476 // so we can measure them
3477 Polymer.dom.flush();
3478
3479 var newPhysicalSize = 0;
3480 var oldPhysicalSize = 0;
3481 var prevAvgCount = this._physicalAverageCount;
3482 var prevPhysicalAvg = this._physicalAverage;
3483
3484 this._iterateItems(function(pidx, vidx) {
3485
3486 oldPhysicalSize += this._physicalSizes[pidx] || 0;
3487 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
3488 newPhysicalSize += this._physicalSizes[pidx];
3489 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
3490
3491 }, itemSet);
3492
3493 this._viewportHeight = this._scrollTargetHeight;
3494 if (this.grid) {
3495 this._updateGridMetrics();
3496 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
3497 } else {
3498 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
3499 }
3500
3501 // update the average if we measured something
3502 if (this._physicalAverageCount !== prevAvgCount) {
3503 this._physicalAverage = Math.round(
3504 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
3505 this._physicalAverageCount);
3506 }
3507 },
3508
3509 _updateGridMetrics: function() {
3510 this._viewportWidth = this.$.items.offsetWidth;
3511 // Set item width to the value of the _physicalItems offsetWidth
3512 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
3513 // Set row height to the value of the _physicalItems offsetHeight
3514 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
3515 // If in grid mode compute how many items with exist in each row
3516 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
3517 },
3518
3519 /**
3520 * Updates the position of the physical items.
3521 */
3522 _positionItems: function() {
3523 this._adjustScrollPosition();
3524
3525 var y = this._physicalTop;
3526
3527 if (this.grid) {
3528 var totalItemWidth = this._itemsPerRow * this._itemWidth;
3529 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
3530
3531 this._iterateItems(function(pidx, vidx) {
3532
3533 var modulus = vidx % this._itemsPerRow;
3534 var x = Math.floor((modulus * this._itemWidth) + rowOffset);
3535
3536 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
3537
3538 if (this._shouldRenderNextRow(vidx)) {
3539 y += this._rowHeight;
3540 }
3541
3542 });
3543 } else {
3544 this._iterateItems(function(pidx, vidx) {
3545
3546 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
3547 y += this._physicalSizes[pidx];
3548
3549 });
3550 }
3551 },
3552
3553 _getPhysicalSizeIncrement: function(pidx) {
3554 if (!this.grid) {
3555 return this._physicalSizes[pidx];
3556 }
3557 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
3558 return 0;
3559 }
3560 return this._rowHeight;
3561 },
3562
3563 /**
3564 * Returns, based on the current index,
3565 * whether or not the next index will need
3566 * to be rendered on a new row.
3567 *
3568 * @param {number} vidx Virtual index
3569 * @return {boolean}
3570 */
3571 _shouldRenderNextRow: function(vidx) {
3572 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
3573 },
3574
3575 /**
3576 * Adjusts the scroll position when it was overestimated.
3577 */
3578 _adjustScrollPosition: function() {
3579 var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
3580 Math.min(this._scrollPosition + this._physicalTop, 0);
3581
3582 if (deltaHeight) {
3583 this._physicalTop = this._physicalTop - deltaHeight;
3584 // juking scroll position during interial scrolling on iOS is no bueno
3585 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
3586 this._resetScrollPosition(this._scrollTop - deltaHeight);
3587 }
3588 }
3589 },
3590
3591 /**
3592 * Sets the position of the scroll.
3593 */
3594 _resetScrollPosition: function(pos) {
3595 if (this.scrollTarget) {
3596 this._scrollTop = pos;
3597 this._scrollPosition = this._scrollTop;
3598 }
3599 },
3600
3601 /**
3602 * Sets the scroll height, that's the height of the content,
3603 *
3604 * @param {boolean=} forceUpdate If true, updates the height no matter what.
3605 */
3606 _updateScrollerSize: function(forceUpdate) {
3607 if (this.grid) {
3608 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
3609 } else {
3610 this._estScrollHeight = (this._physicalBottom +
3611 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
3612 }
3613
3614 forceUpdate = forceUpdate || this._scrollHeight === 0;
3615 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
3616 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
3617
3618 // amortize height adjustment, so it won't trigger repaints very often
3619 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
3620 this.$.items.style.height = this._estScrollHeight + 'px';
3621 this._scrollHeight = this._estScrollHeight;
3622 }
3623 },
3624
3625 /**
3626 * Scroll to a specific item in the virtual list regardless
3627 * of the physical items in the DOM tree.
3628 *
3629 * @method scrollToItem
3630 * @param {(Object)} item The item to be scrolled to
3631 */
3632 scrollToItem: function(item){
3633 return this.scrollToIndex(this.items.indexOf(item));
3634 },
3635
3636 /**
3637 * Scroll to a specific index in the virtual list regardless
3638 * of the physical items in the DOM tree.
3639 *
3640 * @method scrollToIndex
3641 * @param {number} idx The index of the item
3642 */
3643 scrollToIndex: function(idx) {
3644 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
3645 return;
3646 }
3647
3648 Polymer.dom.flush();
3649
3650 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
3651 // update the virtual start only when needed
3652 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
3653 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
3654 }
3655 // manage focus
3656 this._manageFocus();
3657 // assign new models
3658 this._assignModels();
3659 // measure the new sizes
3660 this._updateMetrics();
3661
3662 // estimate new physical offset
3663 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
3664 this._physicalTop = estPhysicalTop;
3665
3666 var currentTopItem = this._physicalStart;
3667 var currentVirtualItem = this._virtualStart;
3668 var targetOffsetTop = 0;
3669 var hiddenContentSize = this._hiddenContentSize;
3670
3671 // scroll to the item as much as we can
3672 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
3673 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
3674 currentTopItem = (currentTopItem + 1) % this._physicalCount;
3675 currentVirtualItem++;
3676 }
3677 // update the scroller size
3678 this._updateScrollerSize(true);
3679 // update the position of the items
3680 this._positionItems();
3681 // set the new scroll position
3682 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
3683 // increase the pool of physical items if needed
3684 this._increasePoolIfNeeded();
3685 // clear cached visible index
3686 this._firstVisibleIndexVal = null;
3687 this._lastVisibleIndexVal = null;
3688 },
3689
3690 /**
3691 * Reset the physical average and the average count.
3692 */
3693 _resetAverage: function() {
3694 this._physicalAverage = 0;
3695 this._physicalAverageCount = 0;
3696 },
3697
3698 /**
3699 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
3700 * when the element is resized.
3701 */
3702 _resizeHandler: function() {
3703 // iOS fires the resize event when the address bar slides up
3704 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
3705 return;
3706 }
3707 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
3708 // changing the scroll position from a resize handler would result in
3709 // the scroll position being reset. Waiting 1ms fixes the issue.
3710 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
3711 this.updateViewportBoundaries();
3712 this._render();
3713
3714 if (this._itemsRendered && this._physicalItems && this._isVisible) {
3715 this._resetAverage();
3716 this.scrollToIndex(this.firstVisibleIndex);
3717 }
3718 }.bind(this), 1));
3719 },
3720
3721 _getModelFromItem: function(item) {
3722 var key = this._collection.getKey(item);
3723 var pidx = this._physicalIndexForKey[key];
3724
3725 if (pidx != null) {
3726 return this._physicalItems[pidx]._templateInstance;
3727 }
3728 return null;
3729 },
3730
3731 /**
3732 * Gets a valid item instance from its index or the object value.
3733 *
3734 * @param {(Object|number)} item The item object or its index
3735 */
3736 _getNormalizedItem: function(item) {
3737 if (this._collection.getKey(item) === undefined) {
3738 if (typeof item === 'number') {
3739 item = this.items[item];
3740 if (!item) {
3741 throw new RangeError('<item> not found');
3742 }
3743 return item;
3744 }
3745 throw new TypeError('<item> should be a valid item');
3746 }
3747 return item;
3748 },
3749
3750 /**
3751 * Select the list item at the given index.
3752 *
3753 * @method selectItem
3754 * @param {(Object|number)} item The item object or its index
3755 */
3756 selectItem: function(item) {
3757 item = this._getNormalizedItem(item);
3758 var model = this._getModelFromItem(item);
3759
3760 if (!this.multiSelection && this.selectedItem) {
3761 this.deselectItem(this.selectedItem);
3762 }
3763 if (model) {
3764 model[this.selectedAs] = true;
3765 }
3766 this.$.selector.select(item);
3767 this.updateSizeForItem(item);
3768 },
3769
3770 /**
3771 * Deselects the given item list if it is already selected.
3772 *
3773
3774 * @method deselect
3775 * @param {(Object|number)} item The item object or its index
3776 */
3777 deselectItem: function(item) {
3778 item = this._getNormalizedItem(item);
3779 var model = this._getModelFromItem(item);
3780
3781 if (model) {
3782 model[this.selectedAs] = false;
3783 }
3784 this.$.selector.deselect(item);
3785 this.updateSizeForItem(item);
3786 },
3787
3788 /**
3789 * Select or deselect a given item depending on whether the item
3790 * has already been selected.
3791 *
3792 * @method toggleSelectionForItem
3793 * @param {(Object|number)} item The item object or its index
3794 */
3795 toggleSelectionForItem: function(item) {
3796 item = this._getNormalizedItem(item);
3797 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) {
3798 this.deselectItem(item);
3799 } else {
3800 this.selectItem(item);
3801 }
3802 },
3803
3804 /**
3805 * Clears the current selection state of the list.
3806 *
3807 * @method clearSelection
3808 */
3809 clearSelection: function() {
3810 function unselect(item) {
3811 var model = this._getModelFromItem(item);
3812 if (model) {
3813 model[this.selectedAs] = false;
3814 }
3815 }
3816
3817 if (Array.isArray(this.selectedItems)) {
3818 this.selectedItems.forEach(unselect, this);
3819 } else if (this.selectedItem) {
3820 unselect.call(this, this.selectedItem);
3821 }
3822
3823 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
3824 },
3825
3826 /**
3827 * Add an event listener to `tap` if `selectionEnabled` is true,
3828 * it will remove the listener otherwise.
3829 */
3830 _selectionEnabledChanged: function(selectionEnabled) {
3831 var handler = selectionEnabled ? this.listen : this.unlisten;
3832 handler.call(this, this, 'tap', '_selectionHandler');
3833 },
3834
3835 /**
3836 * Select an item from an event object.
3837 */
3838 _selectionHandler: function(e) {
3839 var model = this.modelForElement(e.target);
3840 if (!model) {
3841 return;
3842 }
3843 var modelTabIndex, activeElTabIndex;
3844 var target = Polymer.dom(e).path[0];
3845 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
3846 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
3847 // Safari does not focus certain form controls via mouse
3848 // https://bugs.webkit.org/show_bug.cgi?id=118043
3849 if (target.localName === 'input' ||
3850 target.localName === 'button' ||
3851 target.localName === 'select') {
3852 return;
3853 }
3854 // Set a temporary tabindex
3855 modelTabIndex = model.tabIndex;
3856 model.tabIndex = SECRET_TABINDEX;
3857 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
3858 model.tabIndex = modelTabIndex;
3859 // Only select the item if the tap wasn't on a focusable child
3860 // or the element bound to `tabIndex`
3861 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
3862 return;
3863 }
3864 this.toggleSelectionForItem(model[this.as]);
3865 },
3866
3867 _multiSelectionChanged: function(multiSelection) {
3868 this.clearSelection();
3869 this.$.selector.multi = multiSelection;
3870 },
3871
3872 /**
3873 * Updates the size of an item.
3874 *
3875 * @method updateSizeForItem
3876 * @param {(Object|number)} item The item object or its index
3877 */
3878 updateSizeForItem: function(item) {
3879 item = this._getNormalizedItem(item);
3880 var key = this._collection.getKey(item);
3881 var pidx = this._physicalIndexForKey[key];
3882
3883 if (pidx != null) {
3884 this._updateMetrics([pidx]);
3885 this._positionItems();
3886 }
3887 },
3888
3889 /**
3890 * Creates a temporary backfill item in the rendered pool of physical items
3891 * to replace the main focused item. The focused item has tabIndex = 0
3892 * and might be currently focused by the user.
3893 *
3894 * This dynamic replacement helps to preserve the focus state.
3895 */
3896 _manageFocus: function() {
3897 var fidx = this._focusedIndex;
3898
3899 if (fidx >= 0 && fidx < this._virtualCount) {
3900 // if it's a valid index, check if that index is rendered
3901 // in a physical item.
3902 if (this._isIndexRendered(fidx)) {
3903 this._restoreFocusedItem();
3904 } else {
3905 this._createFocusBackfillItem();
3906 }
3907 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
3908 // otherwise, assign the initial focused index.
3909 this._focusedIndex = this._virtualStart;
3910 this._focusedItem = this._physicalItems[this._physicalStart];
3911 }
3912 },
3913
3914 _isIndexRendered: function(idx) {
3915 return idx >= this._virtualStart && idx <= this._virtualEnd;
3916 },
3917
3918 _isIndexVisible: function(idx) {
3919 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
3920 },
3921
3922 _getPhysicalIndex: function(idx) {
3923 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
3924 },
3925
3926 _focusPhysicalItem: function(idx) {
3927 if (idx < 0 || idx >= this._virtualCount) {
3928 return;
3929 }
3930 this._restoreFocusedItem();
3931 // scroll to index to make sure it's rendered
3932 if (!this._isIndexRendered(idx)) {
3933 this.scrollToIndex(idx);
3934 }
3935
3936 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
3937 var model = physicalItem._templateInstance;
3938 var focusable;
3939
3940 // set a secret tab index
3941 model.tabIndex = SECRET_TABINDEX;
3942 // check if focusable element is the physical item
3943 if (physicalItem.tabIndex === SECRET_TABINDEX) {
3944 focusable = physicalItem;
3945 }
3946 // search for the element which tabindex is bound to the secret tab index
3947 if (!focusable) {
3948 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
3949 }
3950 // restore the tab index
3951 model.tabIndex = 0;
3952 // focus the focusable element
3953 this._focusedIndex = idx;
3954 focusable && focusable.focus();
3955 },
3956
3957 _removeFocusedItem: function() {
3958 if (this._offscreenFocusedItem) {
3959 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
3960 }
3961 this._offscreenFocusedItem = null;
3962 this._focusBackfillItem = null;
3963 this._focusedItem = null;
3964 this._focusedIndex = -1;
3965 },
3966
3967 _createFocusBackfillItem: function() {
3968 var pidx, fidx = this._focusedIndex;
3969 if (this._offscreenFocusedItem || fidx < 0) {
3970 return;
3971 }
3972 if (!this._focusBackfillItem) {
3973 // create a physical item, so that it backfills the focused item.
3974 var stampedTemplate = this.stamp(null);
3975 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
3976 Polymer.dom(this).appendChild(stampedTemplate.root);
3977 }
3978 // get the physical index for the focused index
3979 pidx = this._getPhysicalIndex(fidx);
3980
3981 if (pidx != null) {
3982 // set the offcreen focused physical item
3983 this._offscreenFocusedItem = this._physicalItems[pidx];
3984 // backfill the focused physical item
3985 this._physicalItems[pidx] = this._focusBackfillItem;
3986 // hide the focused physical
3987 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
3988 }
3989 },
3990
3991 _restoreFocusedItem: function() {
3992 var pidx, fidx = this._focusedIndex;
3993
3994 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
3995 return;
3996 }
3997 // assign models to the focused index
3998 this._assignModels();
3999 // get the new physical index for the focused index
4000 pidx = this._getPhysicalIndex(fidx);
4001
4002 if (pidx != null) {
4003 // flip the focus backfill
4004 this._focusBackfillItem = this._physicalItems[pidx];
4005 // restore the focused physical item
4006 this._physicalItems[pidx] = this._offscreenFocusedItem;
4007 // reset the offscreen focused item
4008 this._offscreenFocusedItem = null;
4009 // hide the physical item that backfills
4010 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
4011 }
4012 },
4013
4014 _didFocus: function(e) {
4015 var targetModel = this.modelForElement(e.target);
4016 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
4017 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
4018 var fidx = this._focusedIndex;
4019
4020 if (!targetModel || !focusedModel) {
4021 return;
4022 }
4023 if (focusedModel === targetModel) {
4024 // if the user focused the same item, then bring it into view if it's no t visible
4025 if (!this._isIndexVisible(fidx)) {
4026 this.scrollToIndex(fidx);
4027 }
4028 } else {
4029 this._restoreFocusedItem();
4030 // restore tabIndex for the currently focused item
4031 focusedModel.tabIndex = -1;
4032 // set the tabIndex for the next focused item
4033 targetModel.tabIndex = 0;
4034 fidx = targetModel[this.indexAs];
4035 this._focusedIndex = fidx;
4036 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
4037
4038 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
4039 this._update();
4040 }
4041 }
4042 },
4043
4044 _didMoveUp: function() {
4045 this._focusPhysicalItem(this._focusedIndex - 1);
4046 },
4047
4048 _didMoveDown: function(e) {
4049 // disable scroll when pressing the down key
4050 e.detail.keyboardEvent.preventDefault();
4051 this._focusPhysicalItem(this._focusedIndex + 1);
4052 },
4053
4054 _didEnter: function(e) {
4055 this._focusPhysicalItem(this._focusedIndex);
4056 this._selectionHandler(e.detail.keyboardEvent);
4057 }
4058 });
4059
4060 })();
4061 // Copyright 2015 The Chromium Authors. All rights reserved.
4062 // Use of this source code is governed by a BSD-style license that can be
4063 // found in the LICENSE file.
4064
4065 cr.define('downloads', function() {
4066 /**
4067 * @param {string} chromeSendName
4068 * @return {function(string):void} A chrome.send() callback with curried name.
4069 */
4070 function chromeSendWithId(chromeSendName) {
4071 return function(id) { chrome.send(chromeSendName, [id]); };
4072 }
4073
4074 /** @constructor */
4075 function ActionService() {
4076 /** @private {Array<string>} */
4077 this.searchTerms_ = [];
4078 }
4079
4080 /**
4081 * @param {string} s
4082 * @return {string} |s| without whitespace at the beginning or end.
4083 */
4084 function trim(s) { return s.trim(); }
4085
4086 /**
4087 * @param {string|undefined} value
4088 * @return {boolean} Whether |value| is truthy.
4089 */
4090 function truthy(value) { return !!value; }
4091
4092 /**
4093 * @param {string} searchText Input typed by the user into a search box.
4094 * @return {Array<string>} A list of terms extracted from |searchText|.
4095 */
4096 ActionService.splitTerms = function(searchText) {
4097 // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
4098 return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
4099 };
4100
4101 ActionService.prototype = {
4102 /** @param {string} id ID of the download to cancel. */
4103 cancel: chromeSendWithId('cancel'),
4104
4105 /** Instructs the browser to clear all finished downloads. */
4106 clearAll: function() {
4107 if (loadTimeData.getBoolean('allowDeletingHistory')) {
4108 chrome.send('clearAll');
4109 this.search('');
4110 }
4111 },
4112
4113 /** @param {string} id ID of the dangerous download to discard. */
4114 discardDangerous: chromeSendWithId('discardDangerous'),
4115
4116 /** @param {string} url URL of a file to download. */
4117 download: function(url) {
4118 var a = document.createElement('a');
4119 a.href = url;
4120 a.setAttribute('download', '');
4121 a.click();
4122 },
4123
4124 /** @param {string} id ID of the download that the user started dragging. */
4125 drag: chromeSendWithId('drag'),
4126
4127 /** Loads more downloads with the current search terms. */
4128 loadMore: function() {
4129 chrome.send('getDownloads', this.searchTerms_);
4130 },
4131
4132 /**
4133 * @return {boolean} Whether the user is currently searching for downloads
4134 * (i.e. has a non-empty search term).
4135 */
4136 isSearching: function() {
4137 return this.searchTerms_.length > 0;
4138 },
4139
4140 /** Opens the current local destination for downloads. */
4141 openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
4142
4143 /**
4144 * @param {string} id ID of the download to run locally on the user's box.
4145 */
4146 openFile: chromeSendWithId('openFile'),
4147
4148 /** @param {string} id ID the of the progressing download to pause. */
4149 pause: chromeSendWithId('pause'),
4150
4151 /** @param {string} id ID of the finished download to remove. */
4152 remove: chromeSendWithId('remove'),
4153
4154 /** @param {string} id ID of the paused download to resume. */
4155 resume: chromeSendWithId('resume'),
4156
4157 /**
4158 * @param {string} id ID of the dangerous download to save despite
4159 * warnings.
4160 */
4161 saveDangerous: chromeSendWithId('saveDangerous'),
4162
4163 /** @param {string} searchText What to search for. */
4164 search: function(searchText) {
4165 var searchTerms = ActionService.splitTerms(searchText);
4166 var sameTerms = searchTerms.length == this.searchTerms_.length;
4167
4168 for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
4169 if (searchTerms[i] != this.searchTerms_[i])
4170 sameTerms = false;
4171 }
4172
4173 if (sameTerms)
4174 return;
4175
4176 this.searchTerms_ = searchTerms;
4177 this.loadMore();
4178 },
4179
4180 /**
4181 * Shows the local folder a finished download resides in.
4182 * @param {string} id ID of the download to show.
4183 */
4184 show: chromeSendWithId('show'),
4185
4186 /** Undo download removal. */
4187 undo: chrome.send.bind(chrome, 'undo'),
4188 };
4189
4190 cr.addSingletonGetter(ActionService);
4191
4192 return {ActionService: ActionService};
4193 });
4194 // Copyright 2015 The Chromium Authors. All rights reserved.
4195 // Use of this source code is governed by a BSD-style license that can be
4196 // found in the LICENSE file.
4197
4198 cr.define('downloads', function() {
4199 /**
4200 * Explains why a download is in DANGEROUS state.
4201 * @enum {string}
4202 */
4203 var DangerType = {
4204 NOT_DANGEROUS: 'NOT_DANGEROUS',
4205 DANGEROUS_FILE: 'DANGEROUS_FILE',
4206 DANGEROUS_URL: 'DANGEROUS_URL',
4207 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
4208 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
4209 DANGEROUS_HOST: 'DANGEROUS_HOST',
4210 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
4211 };
4212
4213 /**
4214 * The states a download can be in. These correspond to states defined in
4215 * DownloadsDOMHandler::CreateDownloadItemValue
4216 * @enum {string}
4217 */
4218 var States = {
4219 IN_PROGRESS: 'IN_PROGRESS',
4220 CANCELLED: 'CANCELLED',
4221 COMPLETE: 'COMPLETE',
4222 PAUSED: 'PAUSED',
4223 DANGEROUS: 'DANGEROUS',
4224 INTERRUPTED: 'INTERRUPTED',
4225 };
4226
4227 return {
4228 DangerType: DangerType,
4229 States: States,
4230 };
4231 });
4232 // Copyright 2014 The Chromium Authors. All rights reserved.
4233 // Use of this source code is governed by a BSD-style license that can be
4234 // found in the LICENSE file.
4235
4236 // Action links are elements that are used to perform an in-page navigation or
4237 // action (e.g. showing a dialog).
4238 //
4239 // They look like normal anchor (<a>) tags as their text color is blue. However,
4240 // they're subtly different as they're not initially underlined (giving users a
4241 // clue that underlined links navigate while action links don't).
4242 //
4243 // Action links look very similar to normal links when hovered (hand cursor,
4244 // underlined). This gives the user an idea that clicking this link will do
4245 // something similar to navigation but in the same page.
4246 //
4247 // They can be created in JavaScript like this:
4248 //
4249 // var link = document.createElement('a', 'action-link'); // Note second arg.
4250 //
4251 // or with a constructor like this:
4252 //
4253 // var link = new ActionLink();
4254 //
4255 // They can be used easily from HTML as well, like so:
4256 //
4257 // <a is="action-link">Click me!</a>
4258 //
4259 // NOTE: <action-link> and document.createElement('action-link') don't work.
4260
4261 /**
4262 * @constructor
4263 * @extends {HTMLAnchorElement}
4264 */
4265 var ActionLink = document.registerElement('action-link', {
4266 prototype: {
4267 __proto__: HTMLAnchorElement.prototype,
4268
4269 /** @this {ActionLink} */
4270 createdCallback: function() {
4271 // Action links can start disabled (e.g. <a is="action-link" disabled>).
4272 this.tabIndex = this.disabled ? -1 : 0;
4273
4274 if (!this.hasAttribute('role'))
4275 this.setAttribute('role', 'link');
4276
4277 this.addEventListener('keydown', function(e) {
4278 if (!this.disabled && e.key == 'Enter' && !this.href) {
4279 // Schedule a click asynchronously because other 'keydown' handlers
4280 // may still run later (e.g. document.addEventListener('keydown')).
4281 // Specifically options dialogs break when this timeout isn't here.
4282 // NOTE: this affects the "trusted" state of the ensuing click. I
4283 // haven't found anything that breaks because of this (yet).
4284 window.setTimeout(this.click.bind(this), 0);
4285 }
4286 });
4287
4288 function preventDefault(e) {
4289 e.preventDefault();
4290 }
4291
4292 function removePreventDefault() {
4293 document.removeEventListener('selectstart', preventDefault);
4294 document.removeEventListener('mouseup', removePreventDefault);
4295 }
4296
4297 this.addEventListener('mousedown', function() {
4298 // This handlers strives to match the behavior of <a href="...">.
4299
4300 // While the mouse is down, prevent text selection from dragging.
4301 document.addEventListener('selectstart', preventDefault);
4302 document.addEventListener('mouseup', removePreventDefault);
4303
4304 // If focus started via mouse press, don't show an outline.
4305 if (document.activeElement != this)
4306 this.classList.add('no-outline');
4307 });
4308
4309 this.addEventListener('blur', function() {
4310 this.classList.remove('no-outline');
4311 });
4312 },
4313
4314 /** @type {boolean} */
4315 set disabled(disabled) {
4316 if (disabled)
4317 HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
4318 else
4319 HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
4320 this.tabIndex = disabled ? -1 : 0;
4321 },
4322 get disabled() {
4323 return this.hasAttribute('disabled');
4324 },
4325
4326 /** @override */
4327 setAttribute: function(attr, val) {
4328 if (attr.toLowerCase() == 'disabled')
4329 this.disabled = true;
4330 else
4331 HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
4332 },
4333
4334 /** @override */
4335 removeAttribute: function(attr) {
4336 if (attr.toLowerCase() == 'disabled')
4337 this.disabled = false;
4338 else
4339 HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments);
4340 },
4341 },
4342
4343 extends: 'a',
4344 });
4345 (function() { 4843 (function() {
4346 4844
4347 // monostate data 4845 // monostate data
4348 var metaDatas = {}; 4846 var metaDatas = {};
4349 var metaArrays = {}; 4847 var metaArrays = {};
4350 var singleton = null; 4848 var singleton = null;
4351 4849
4352 Polymer.IronMeta = Polymer({ 4850 Polymer.IronMeta = Polymer({
4353 4851
4354 is: 'iron-meta', 4852 is: 'iron-meta',
(...skipping 355 matching lines...) Expand 10 before | Expand all | Expand 10 after
4710 this._img.style.height = '100%'; 5208 this._img.style.height = '100%';
4711 this._img.draggable = false; 5209 this._img.draggable = false;
4712 } 5210 }
4713 this._img.src = this.src; 5211 this._img.src = this.src;
4714 Polymer.dom(this.root).appendChild(this._img); 5212 Polymer.dom(this.root).appendChild(this._img);
4715 } 5213 }
4716 } 5214 }
4717 5215
4718 }); 5216 });
4719 /** 5217 /**
4720 * @demo demo/index.html 5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus.
4721 * @polymerBehavior 5219 *
5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior
4722 */ 5221 */
4723 Polymer.IronControlState = { 5222 Polymer.PaperInkyFocusBehaviorImpl = {
4724
4725 properties: {
4726
4727 /**
4728 * If true, the element currently has focus.
4729 */
4730 focused: {
4731 type: Boolean,
4732 value: false,
4733 notify: true,
4734 readOnly: true,
4735 reflectToAttribute: true
4736 },
4737
4738 /**
4739 * If true, the user cannot interact with this element.
4740 */
4741 disabled: {
4742 type: Boolean,
4743 value: false,
4744 notify: true,
4745 observer: '_disabledChanged',
4746 reflectToAttribute: true
4747 },
4748
4749 _oldTabIndex: {
4750 type: Number
4751 },
4752
4753 _boundFocusBlurHandler: {
4754 type: Function,
4755 value: function() {
4756 return this._focusBlurHandler.bind(this);
4757 }
4758 }
4759
4760 },
4761
4762 observers: [ 5223 observers: [
4763 '_changedControlState(focused, disabled)' 5224 '_focusedChanged(receivedFocusFromKeyboard)'
4764 ], 5225 ],
4765 5226
4766 ready: function() { 5227 _focusedChanged: function(receivedFocusFromKeyboard) {
4767 this.addEventListener('focus', this._boundFocusBlurHandler, true); 5228 if (receivedFocusFromKeyboard) {
4768 this.addEventListener('blur', this._boundFocusBlurHandler, true);
4769 },
4770
4771 _focusBlurHandler: function(event) {
4772 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
4773 // eventually become `this` due to retargeting; if we are not in
4774 // ShadowDOM land, `event.target` will eventually become `this` due
4775 // to the second conditional which fires a synthetic event (that is also
4776 // handled). In either case, we can disregard `event.path`.
4777
4778 if (event.target === this) {
4779 this._setFocused(event.type === 'focus');
4780 } else if (!this.shadowRoot) {
4781 var target = /** @type {Node} */(Polymer.dom(event).localTarget);
4782 if (!this.isLightDescendant(target)) {
4783 this.fire(event.type, {sourceEvent: event}, {
4784 node: this,
4785 bubbles: event.bubbles,
4786 cancelable: event.cancelable
4787 });
4788 }
4789 }
4790 },
4791
4792 _disabledChanged: function(disabled, old) {
4793 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
4794 this.style.pointerEvents = disabled ? 'none' : '';
4795 if (disabled) {
4796 this._oldTabIndex = this.tabIndex;
4797 this._setFocused(false);
4798 this.tabIndex = -1;
4799 this.blur();
4800 } else if (this._oldTabIndex !== undefined) {
4801 this.tabIndex = this._oldTabIndex;
4802 }
4803 },
4804
4805 _changedControlState: function() {
4806 // _controlStateChanged is abstract, follow-on behaviors may implement it
4807 if (this._controlStateChanged) {
4808 this._controlStateChanged();
4809 }
4810 }
4811
4812 };
4813 /**
4814 * @demo demo/index.html
4815 * @polymerBehavior Polymer.IronButtonState
4816 */
4817 Polymer.IronButtonStateImpl = {
4818
4819 properties: {
4820
4821 /**
4822 * If true, the user is currently holding down the button.
4823 */
4824 pressed: {
4825 type: Boolean,
4826 readOnly: true,
4827 value: false,
4828 reflectToAttribute: true,
4829 observer: '_pressedChanged'
4830 },
4831
4832 /**
4833 * If true, the button toggles the active state with each tap or press
4834 * of the spacebar.
4835 */
4836 toggles: {
4837 type: Boolean,
4838 value: false,
4839 reflectToAttribute: true
4840 },
4841
4842 /**
4843 * If true, the button is a toggle and is currently in the active state.
4844 */
4845 active: {
4846 type: Boolean,
4847 value: false,
4848 notify: true,
4849 reflectToAttribute: true
4850 },
4851
4852 /**
4853 * True if the element is currently being pressed by a "pointer," which
4854 * is loosely defined as mouse or touch input (but specifically excluding
4855 * keyboard input).
4856 */
4857 pointerDown: {
4858 type: Boolean,
4859 readOnly: true,
4860 value: false
4861 },
4862
4863 /**
4864 * True if the input device that caused the element to receive focus
4865 * was a keyboard.
4866 */
4867 receivedFocusFromKeyboard: {
4868 type: Boolean,
4869 readOnly: true
4870 },
4871
4872 /**
4873 * The aria attribute to be set if the button is a toggle and in the
4874 * active state.
4875 */
4876 ariaActiveAttribute: {
4877 type: String,
4878 value: 'aria-pressed',
4879 observer: '_ariaActiveAttributeChanged'
4880 }
4881 },
4882
4883 listeners: {
4884 down: '_downHandler',
4885 up: '_upHandler',
4886 tap: '_tapHandler'
4887 },
4888
4889 observers: [
4890 '_detectKeyboardFocus(focused)',
4891 '_activeChanged(active, ariaActiveAttribute)'
4892 ],
4893
4894 keyBindings: {
4895 'enter:keydown': '_asyncClick',
4896 'space:keydown': '_spaceKeyDownHandler',
4897 'space:keyup': '_spaceKeyUpHandler',
4898 },
4899
4900 _mouseEventRe: /^mouse/,
4901
4902 _tapHandler: function() {
4903 if (this.toggles) {
4904 // a tap is needed to toggle the active state
4905 this._userActivate(!this.active);
4906 } else {
4907 this.active = false;
4908 }
4909 },
4910
4911 _detectKeyboardFocus: function(focused) {
4912 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
4913 },
4914
4915 // to emulate native checkbox, (de-)activations from a user interaction fire
4916 // 'change' events
4917 _userActivate: function(active) {
4918 if (this.active !== active) {
4919 this.active = active;
4920 this.fire('change');
4921 }
4922 },
4923
4924 _downHandler: function(event) {
4925 this._setPointerDown(true);
4926 this._setPressed(true);
4927 this._setReceivedFocusFromKeyboard(false);
4928 },
4929
4930 _upHandler: function() {
4931 this._setPointerDown(false);
4932 this._setPressed(false);
4933 },
4934
4935 /**
4936 * @param {!KeyboardEvent} event .
4937 */
4938 _spaceKeyDownHandler: function(event) {
4939 var keyboardEvent = event.detail.keyboardEvent;
4940 var target = Polymer.dom(keyboardEvent).localTarget;
4941
4942 // Ignore the event if this is coming from a focused light child, since th at
4943 // element will deal with it.
4944 if (this.isLightDescendant(/** @type {Node} */(target)))
4945 return;
4946
4947 keyboardEvent.preventDefault();
4948 keyboardEvent.stopImmediatePropagation();
4949 this._setPressed(true);
4950 },
4951
4952 /**
4953 * @param {!KeyboardEvent} event .
4954 */
4955 _spaceKeyUpHandler: function(event) {
4956 var keyboardEvent = event.detail.keyboardEvent;
4957 var target = Polymer.dom(keyboardEvent).localTarget;
4958
4959 // Ignore the event if this is coming from a focused light child, since th at
4960 // element will deal with it.
4961 if (this.isLightDescendant(/** @type {Node} */(target)))
4962 return;
4963
4964 if (this.pressed) {
4965 this._asyncClick();
4966 }
4967 this._setPressed(false);
4968 },
4969
4970 // trigger click asynchronously, the asynchrony is useful to allow one
4971 // event handler to unwind before triggering another event
4972 _asyncClick: function() {
4973 this.async(function() {
4974 this.click();
4975 }, 1);
4976 },
4977
4978 // any of these changes are considered a change to button state
4979
4980 _pressedChanged: function(pressed) {
4981 this._changedButtonState();
4982 },
4983
4984 _ariaActiveAttributeChanged: function(value, oldValue) {
4985 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
4986 this.removeAttribute(oldValue);
4987 }
4988 },
4989
4990 _activeChanged: function(active, ariaActiveAttribute) {
4991 if (this.toggles) {
4992 this.setAttribute(this.ariaActiveAttribute,
4993 active ? 'true' : 'false');
4994 } else {
4995 this.removeAttribute(this.ariaActiveAttribute);
4996 }
4997 this._changedButtonState();
4998 },
4999
5000 _controlStateChanged: function() {
5001 if (this.disabled) {
5002 this._setPressed(false);
5003 } else {
5004 this._changedButtonState();
5005 }
5006 },
5007
5008 // provide hook for follow-on behaviors to react to button-state
5009
5010 _changedButtonState: function() {
5011 if (this._buttonStateChanged) {
5012 this._buttonStateChanged(); // abstract
5013 }
5014 }
5015
5016 };
5017
5018 /** @polymerBehavior */
5019 Polymer.IronButtonState = [
5020 Polymer.IronA11yKeysBehavior,
5021 Polymer.IronButtonStateImpl
5022 ];
5023 (function() {
5024 var Utility = {
5025 distance: function(x1, y1, x2, y2) {
5026 var xDelta = (x1 - x2);
5027 var yDelta = (y1 - y2);
5028
5029 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
5030 },
5031
5032 now: window.performance && window.performance.now ?
5033 window.performance.now.bind(window.performance) : Date.now
5034 };
5035
5036 /**
5037 * @param {HTMLElement} element
5038 * @constructor
5039 */
5040 function ElementMetrics(element) {
5041 this.element = element;
5042 this.width = this.boundingRect.width;
5043 this.height = this.boundingRect.height;
5044
5045 this.size = Math.max(this.width, this.height);
5046 }
5047
5048 ElementMetrics.prototype = {
5049 get boundingRect () {
5050 return this.element.getBoundingClientRect();
5051 },
5052
5053 furthestCornerDistanceFrom: function(x, y) {
5054 var topLeft = Utility.distance(x, y, 0, 0);
5055 var topRight = Utility.distance(x, y, this.width, 0);
5056 var bottomLeft = Utility.distance(x, y, 0, this.height);
5057 var bottomRight = Utility.distance(x, y, this.width, this.height);
5058
5059 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
5060 }
5061 };
5062
5063 /**
5064 * @param {HTMLElement} element
5065 * @constructor
5066 */
5067 function Ripple(element) {
5068 this.element = element;
5069 this.color = window.getComputedStyle(element).color;
5070
5071 this.wave = document.createElement('div');
5072 this.waveContainer = document.createElement('div');
5073 this.wave.style.backgroundColor = this.color;
5074 this.wave.classList.add('wave');
5075 this.waveContainer.classList.add('wave-container');
5076 Polymer.dom(this.waveContainer).appendChild(this.wave);
5077
5078 this.resetInteractionState();
5079 }
5080
5081 Ripple.MAX_RADIUS = 300;
5082
5083 Ripple.prototype = {
5084 get recenters() {
5085 return this.element.recenters;
5086 },
5087
5088 get center() {
5089 return this.element.center;
5090 },
5091
5092 get mouseDownElapsed() {
5093 var elapsed;
5094
5095 if (!this.mouseDownStart) {
5096 return 0;
5097 }
5098
5099 elapsed = Utility.now() - this.mouseDownStart;
5100
5101 if (this.mouseUpStart) {
5102 elapsed -= this.mouseUpElapsed;
5103 }
5104
5105 return elapsed;
5106 },
5107
5108 get mouseUpElapsed() {
5109 return this.mouseUpStart ?
5110 Utility.now () - this.mouseUpStart : 0;
5111 },
5112
5113 get mouseDownElapsedSeconds() {
5114 return this.mouseDownElapsed / 1000;
5115 },
5116
5117 get mouseUpElapsedSeconds() {
5118 return this.mouseUpElapsed / 1000;
5119 },
5120
5121 get mouseInteractionSeconds() {
5122 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
5123 },
5124
5125 get initialOpacity() {
5126 return this.element.initialOpacity;
5127 },
5128
5129 get opacityDecayVelocity() {
5130 return this.element.opacityDecayVelocity;
5131 },
5132
5133 get radius() {
5134 var width2 = this.containerMetrics.width * this.containerMetrics.width;
5135 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
5136 var waveRadius = Math.min(
5137 Math.sqrt(width2 + height2),
5138 Ripple.MAX_RADIUS
5139 ) * 1.1 + 5;
5140
5141 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
5142 var timeNow = this.mouseInteractionSeconds / duration;
5143 var size = waveRadius * (1 - Math.pow(80, -timeNow));
5144
5145 return Math.abs(size);
5146 },
5147
5148 get opacity() {
5149 if (!this.mouseUpStart) {
5150 return this.initialOpacity;
5151 }
5152
5153 return Math.max(
5154 0,
5155 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
5156 );
5157 },
5158
5159 get outerOpacity() {
5160 // Linear increase in background opacity, capped at the opacity
5161 // of the wavefront (waveOpacity).
5162 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
5163 var waveOpacity = this.opacity;
5164
5165 return Math.max(
5166 0,
5167 Math.min(outerOpacity, waveOpacity)
5168 );
5169 },
5170
5171 get isOpacityFullyDecayed() {
5172 return this.opacity < 0.01 &&
5173 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5174 },
5175
5176 get isRestingAtMaxRadius() {
5177 return this.opacity >= this.initialOpacity &&
5178 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5179 },
5180
5181 get isAnimationComplete() {
5182 return this.mouseUpStart ?
5183 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
5184 },
5185
5186 get translationFraction() {
5187 return Math.min(
5188 1,
5189 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
5190 );
5191 },
5192
5193 get xNow() {
5194 if (this.xEnd) {
5195 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
5196 }
5197
5198 return this.xStart;
5199 },
5200
5201 get yNow() {
5202 if (this.yEnd) {
5203 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
5204 }
5205
5206 return this.yStart;
5207 },
5208
5209 get isMouseDown() {
5210 return this.mouseDownStart && !this.mouseUpStart;
5211 },
5212
5213 resetInteractionState: function() {
5214 this.maxRadius = 0;
5215 this.mouseDownStart = 0;
5216 this.mouseUpStart = 0;
5217
5218 this.xStart = 0;
5219 this.yStart = 0;
5220 this.xEnd = 0;
5221 this.yEnd = 0;
5222 this.slideDistance = 0;
5223
5224 this.containerMetrics = new ElementMetrics(this.element);
5225 },
5226
5227 draw: function() {
5228 var scale;
5229 var translateString;
5230 var dx;
5231 var dy;
5232
5233 this.wave.style.opacity = this.opacity;
5234
5235 scale = this.radius / (this.containerMetrics.size / 2);
5236 dx = this.xNow - (this.containerMetrics.width / 2);
5237 dy = this.yNow - (this.containerMetrics.height / 2);
5238
5239
5240 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
5241 // https://bugs.webkit.org/show_bug.cgi?id=98538
5242 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
5243 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
5244 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
5245 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
5246 },
5247
5248 /** @param {Event=} event */
5249 downAction: function(event) {
5250 var xCenter = this.containerMetrics.width / 2;
5251 var yCenter = this.containerMetrics.height / 2;
5252
5253 this.resetInteractionState();
5254 this.mouseDownStart = Utility.now();
5255
5256 if (this.center) {
5257 this.xStart = xCenter;
5258 this.yStart = yCenter;
5259 this.slideDistance = Utility.distance(
5260 this.xStart, this.yStart, this.xEnd, this.yEnd
5261 );
5262 } else {
5263 this.xStart = event ?
5264 event.detail.x - this.containerMetrics.boundingRect.left :
5265 this.containerMetrics.width / 2;
5266 this.yStart = event ?
5267 event.detail.y - this.containerMetrics.boundingRect.top :
5268 this.containerMetrics.height / 2;
5269 }
5270
5271 if (this.recenters) {
5272 this.xEnd = xCenter;
5273 this.yEnd = yCenter;
5274 this.slideDistance = Utility.distance(
5275 this.xStart, this.yStart, this.xEnd, this.yEnd
5276 );
5277 }
5278
5279 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
5280 this.xStart,
5281 this.yStart
5282 );
5283
5284 this.waveContainer.style.top =
5285 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
5286 this.waveContainer.style.left =
5287 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
5288
5289 this.waveContainer.style.width = this.containerMetrics.size + 'px';
5290 this.waveContainer.style.height = this.containerMetrics.size + 'px';
5291 },
5292
5293 /** @param {Event=} event */
5294 upAction: function(event) {
5295 if (!this.isMouseDown) {
5296 return;
5297 }
5298
5299 this.mouseUpStart = Utility.now();
5300 },
5301
5302 remove: function() {
5303 Polymer.dom(this.waveContainer.parentNode).removeChild(
5304 this.waveContainer
5305 );
5306 }
5307 };
5308
5309 Polymer({
5310 is: 'paper-ripple',
5311
5312 behaviors: [
5313 Polymer.IronA11yKeysBehavior
5314 ],
5315
5316 properties: {
5317 /**
5318 * The initial opacity set on the wave.
5319 *
5320 * @attribute initialOpacity
5321 * @type number
5322 * @default 0.25
5323 */
5324 initialOpacity: {
5325 type: Number,
5326 value: 0.25
5327 },
5328
5329 /**
5330 * How fast (opacity per second) the wave fades out.
5331 *
5332 * @attribute opacityDecayVelocity
5333 * @type number
5334 * @default 0.8
5335 */
5336 opacityDecayVelocity: {
5337 type: Number,
5338 value: 0.8
5339 },
5340
5341 /**
5342 * If true, ripples will exhibit a gravitational pull towards
5343 * the center of their container as they fade away.
5344 *
5345 * @attribute recenters
5346 * @type boolean
5347 * @default false
5348 */
5349 recenters: {
5350 type: Boolean,
5351 value: false
5352 },
5353
5354 /**
5355 * If true, ripples will center inside its container
5356 *
5357 * @attribute recenters
5358 * @type boolean
5359 * @default false
5360 */
5361 center: {
5362 type: Boolean,
5363 value: false
5364 },
5365
5366 /**
5367 * A list of the visual ripples.
5368 *
5369 * @attribute ripples
5370 * @type Array
5371 * @default []
5372 */
5373 ripples: {
5374 type: Array,
5375 value: function() {
5376 return [];
5377 }
5378 },
5379
5380 /**
5381 * True when there are visible ripples animating within the
5382 * element.
5383 */
5384 animating: {
5385 type: Boolean,
5386 readOnly: true,
5387 reflectToAttribute: true,
5388 value: false
5389 },
5390
5391 /**
5392 * If true, the ripple will remain in the "down" state until `holdDown`
5393 * is set to false again.
5394 */
5395 holdDown: {
5396 type: Boolean,
5397 value: false,
5398 observer: '_holdDownChanged'
5399 },
5400
5401 /**
5402 * If true, the ripple will not generate a ripple effect
5403 * via pointer interaction.
5404 * Calling ripple's imperative api like `simulatedRipple` will
5405 * still generate the ripple effect.
5406 */
5407 noink: {
5408 type: Boolean,
5409 value: false
5410 },
5411
5412 _animating: {
5413 type: Boolean
5414 },
5415
5416 _boundAnimate: {
5417 type: Function,
5418 value: function() {
5419 return this.animate.bind(this);
5420 }
5421 }
5422 },
5423
5424 get target () {
5425 return this.keyEventTarget;
5426 },
5427
5428 keyBindings: {
5429 'enter:keydown': '_onEnterKeydown',
5430 'space:keydown': '_onSpaceKeydown',
5431 'space:keyup': '_onSpaceKeyup'
5432 },
5433
5434 attached: function() {
5435 // Set up a11yKeysBehavior to listen to key events on the target,
5436 // so that space and enter activate the ripple even if the target doesn' t
5437 // handle key events. The key handlers deal with `noink` themselves.
5438 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
5439 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
5440 } else {
5441 this.keyEventTarget = this.parentNode;
5442 }
5443 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
5444 this.listen(keyEventTarget, 'up', 'uiUpAction');
5445 this.listen(keyEventTarget, 'down', 'uiDownAction');
5446 },
5447
5448 detached: function() {
5449 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
5450 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
5451 this.keyEventTarget = null;
5452 },
5453
5454 get shouldKeepAnimating () {
5455 for (var index = 0; index < this.ripples.length; ++index) {
5456 if (!this.ripples[index].isAnimationComplete) {
5457 return true;
5458 }
5459 }
5460
5461 return false;
5462 },
5463
5464 simulatedRipple: function() {
5465 this.downAction(null);
5466
5467 // Please see polymer/polymer#1305
5468 this.async(function() {
5469 this.upAction();
5470 }, 1);
5471 },
5472
5473 /**
5474 * Provokes a ripple down effect via a UI event,
5475 * respecting the `noink` property.
5476 * @param {Event=} event
5477 */
5478 uiDownAction: function(event) {
5479 if (!this.noink) {
5480 this.downAction(event);
5481 }
5482 },
5483
5484 /**
5485 * Provokes a ripple down effect via a UI event,
5486 * *not* respecting the `noink` property.
5487 * @param {Event=} event
5488 */
5489 downAction: function(event) {
5490 if (this.holdDown && this.ripples.length > 0) {
5491 return;
5492 }
5493
5494 var ripple = this.addRipple();
5495
5496 ripple.downAction(event);
5497
5498 if (!this._animating) {
5499 this._animating = true;
5500 this.animate();
5501 }
5502 },
5503
5504 /**
5505 * Provokes a ripple up effect via a UI event,
5506 * respecting the `noink` property.
5507 * @param {Event=} event
5508 */
5509 uiUpAction: function(event) {
5510 if (!this.noink) {
5511 this.upAction(event);
5512 }
5513 },
5514
5515 /**
5516 * Provokes a ripple up effect via a UI event,
5517 * *not* respecting the `noink` property.
5518 * @param {Event=} event
5519 */
5520 upAction: function(event) {
5521 if (this.holdDown) {
5522 return;
5523 }
5524
5525 this.ripples.forEach(function(ripple) {
5526 ripple.upAction(event);
5527 });
5528
5529 this._animating = true;
5530 this.animate();
5531 },
5532
5533 onAnimationComplete: function() {
5534 this._animating = false;
5535 this.$.background.style.backgroundColor = null;
5536 this.fire('transitionend');
5537 },
5538
5539 addRipple: function() {
5540 var ripple = new Ripple(this);
5541
5542 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
5543 this.$.background.style.backgroundColor = ripple.color;
5544 this.ripples.push(ripple);
5545
5546 this._setAnimating(true);
5547
5548 return ripple;
5549 },
5550
5551 removeRipple: function(ripple) {
5552 var rippleIndex = this.ripples.indexOf(ripple);
5553
5554 if (rippleIndex < 0) {
5555 return;
5556 }
5557
5558 this.ripples.splice(rippleIndex, 1);
5559
5560 ripple.remove();
5561
5562 if (!this.ripples.length) {
5563 this._setAnimating(false);
5564 }
5565 },
5566
5567 animate: function() {
5568 if (!this._animating) {
5569 return;
5570 }
5571 var index;
5572 var ripple;
5573
5574 for (index = 0; index < this.ripples.length; ++index) {
5575 ripple = this.ripples[index];
5576
5577 ripple.draw();
5578
5579 this.$.background.style.opacity = ripple.outerOpacity;
5580
5581 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
5582 this.removeRipple(ripple);
5583 }
5584 }
5585
5586 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
5587 this.onAnimationComplete();
5588 } else {
5589 window.requestAnimationFrame(this._boundAnimate);
5590 }
5591 },
5592
5593 _onEnterKeydown: function() {
5594 this.uiDownAction();
5595 this.async(this.uiUpAction, 1);
5596 },
5597
5598 _onSpaceKeydown: function() {
5599 this.uiDownAction();
5600 },
5601
5602 _onSpaceKeyup: function() {
5603 this.uiUpAction();
5604 },
5605
5606 // note: holdDown does not respect noink since it can be a focus based
5607 // effect.
5608 _holdDownChanged: function(newVal, oldVal) {
5609 if (oldVal === undefined) {
5610 return;
5611 }
5612 if (newVal) {
5613 this.downAction();
5614 } else {
5615 this.upAction();
5616 }
5617 }
5618
5619 /**
5620 Fired when the animation finishes.
5621 This is useful if you want to wait until
5622 the ripple animation finishes to perform some action.
5623
5624 @event transitionend
5625 @param {{node: Object}} detail Contains the animated node.
5626 */
5627 });
5628 })();
5629 /**
5630 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
5631 * when the element has focus via pointer or keyboard.
5632 *
5633 * NOTE: This behavior is intended to be used in conjunction with and after
5634 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
5635 *
5636 * @polymerBehavior Polymer.PaperRippleBehavior
5637 */
5638 Polymer.PaperRippleBehavior = {
5639 properties: {
5640 /**
5641 * If true, the element will not produce a ripple effect when interacted
5642 * with via the pointer.
5643 */
5644 noink: {
5645 type: Boolean,
5646 observer: '_noinkChanged'
5647 },
5648
5649 /**
5650 * @type {Element|undefined}
5651 */
5652 _rippleContainer: {
5653 type: Object,
5654 }
5655 },
5656
5657 /**
5658 * Ensures a `<paper-ripple>` element is available when the element is
5659 * focused.
5660 */
5661 _buttonStateChanged: function() {
5662 if (this.focused) {
5663 this.ensureRipple(); 5229 this.ensureRipple();
5664 } 5230 }
5665 }, 5231 if (this.hasRipple()) {
5666 5232 this._ripple.holdDown = receivedFocusFromKeyboard;
5667 /** 5233 }
5668 * In addition to the functionality provided in `IronButtonState`, ensures 5234 },
5669 * a ripple effect is created when the element is in a `pressed` state. 5235
5670 */
5671 _downHandler: function(event) {
5672 Polymer.IronButtonStateImpl._downHandler.call(this, event);
5673 if (this.pressed) {
5674 this.ensureRipple(event);
5675 }
5676 },
5677
5678 /**
5679 * Ensures this element contains a ripple effect. For startup efficiency
5680 * the ripple effect is dynamically on demand when needed.
5681 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
5682 * ripple.
5683 */
5684 ensureRipple: function(optTriggeringEvent) {
5685 if (!this.hasRipple()) {
5686 this._ripple = this._createRipple();
5687 this._ripple.noink = this.noink;
5688 var rippleContainer = this._rippleContainer || this.root;
5689 if (rippleContainer) {
5690 Polymer.dom(rippleContainer).appendChild(this._ripple);
5691 }
5692 if (optTriggeringEvent) {
5693 // Check if the event happened inside of the ripple container
5694 // Fall back to host instead of the root because distributed text
5695 // nodes are not valid event targets
5696 var domContainer = Polymer.dom(this._rippleContainer || this);
5697 var target = Polymer.dom(optTriggeringEvent).rootTarget;
5698 if (domContainer.deepContains( /** @type {Node} */(target))) {
5699 this._ripple.uiDownAction(optTriggeringEvent);
5700 }
5701 }
5702 }
5703 },
5704
5705 /**
5706 * Returns the `<paper-ripple>` element used by this element to create
5707 * ripple effects. The element's ripple is created on demand, when
5708 * necessary, and calling this method will force the
5709 * ripple to be created.
5710 */
5711 getRipple: function() {
5712 this.ensureRipple();
5713 return this._ripple;
5714 },
5715
5716 /**
5717 * Returns true if this element currently contains a ripple effect.
5718 * @return {boolean}
5719 */
5720 hasRipple: function() {
5721 return Boolean(this._ripple);
5722 },
5723
5724 /**
5725 * Create the element's ripple effect via creating a `<paper-ripple>`.
5726 * Override this method to customize the ripple element.
5727 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
5728 */
5729 _createRipple: function() { 5236 _createRipple: function() {
5730 return /** @type {!PaperRippleElement} */ ( 5237 var ripple = Polymer.PaperRippleBehavior._createRipple();
5731 document.createElement('paper-ripple')); 5238 ripple.id = 'ink';
5732 }, 5239 ripple.setAttribute('center', '');
5733 5240 ripple.classList.add('circle');
5734 _noinkChanged: function(noink) { 5241 return ripple;
5735 if (this.hasRipple()) {
5736 this._ripple.noink = noink;
5737 }
5738 } 5242 }
5739 }; 5243 };
5740 /** @polymerBehavior Polymer.PaperButtonBehavior */ 5244
5741 Polymer.PaperButtonBehaviorImpl = { 5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
5742 properties: { 5246 Polymer.PaperInkyFocusBehavior = [
5743 /**
5744 * The z-depth of this element, from 0-5. Setting to 0 will remove the
5745 * shadow, and each increasing number greater than 0 will be "deeper"
5746 * than the last.
5747 *
5748 * @attribute elevation
5749 * @type number
5750 * @default 1
5751 */
5752 elevation: {
5753 type: Number,
5754 reflectToAttribute: true,
5755 readOnly: true
5756 }
5757 },
5758
5759 observers: [
5760 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
5761 '_computeKeyboardClass(receivedFocusFromKeyboard)'
5762 ],
5763
5764 hostAttributes: {
5765 role: 'button',
5766 tabindex: '0',
5767 animated: true
5768 },
5769
5770 _calculateElevation: function() {
5771 var e = 1;
5772 if (this.disabled) {
5773 e = 0;
5774 } else if (this.active || this.pressed) {
5775 e = 4;
5776 } else if (this.receivedFocusFromKeyboard) {
5777 e = 3;
5778 }
5779 this._setElevation(e);
5780 },
5781
5782 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5783 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5784 },
5785
5786 /**
5787 * In addition to `IronButtonState` behavior, when space key goes down,
5788 * create a ripple down effect.
5789 *
5790 * @param {!KeyboardEvent} event .
5791 */
5792 _spaceKeyDownHandler: function(event) {
5793 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5794 // Ensure that there is at most one ripple when the space key is held down .
5795 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5796 this._ripple.uiDownAction();
5797 }
5798 },
5799
5800 /**
5801 * In addition to `IronButtonState` behavior, when space key goes up,
5802 * create a ripple up effect.
5803 *
5804 * @param {!KeyboardEvent} event .
5805 */
5806 _spaceKeyUpHandler: function(event) {
5807 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5808 if (this.hasRipple()) {
5809 this._ripple.uiUpAction();
5810 }
5811 }
5812 };
5813
5814 /** @polymerBehavior */
5815 Polymer.PaperButtonBehavior = [
5816 Polymer.IronButtonState, 5247 Polymer.IronButtonState,
5817 Polymer.IronControlState, 5248 Polymer.IronControlState,
5818 Polymer.PaperRippleBehavior, 5249 Polymer.PaperRippleBehavior,
5819 Polymer.PaperButtonBehaviorImpl 5250 Polymer.PaperInkyFocusBehaviorImpl
5820 ]; 5251 ];
5821 Polymer({ 5252 Polymer({
5822 is: 'paper-button', 5253 is: 'paper-icon-button',
5254
5255 hostAttributes: {
5256 role: 'button',
5257 tabindex: '0'
5258 },
5823 5259
5824 behaviors: [ 5260 behaviors: [
5825 Polymer.PaperButtonBehavior 5261 Polymer.PaperInkyFocusBehavior
5826 ], 5262 ],
5827 5263
5828 properties: { 5264 properties: {
5829 /** 5265 /**
5830 * If true, the button should be styled with a shadow. 5266 * The URL of an image for the icon. If the src property is specified,
5267 * the icon property should not be.
5831 */ 5268 */
5832 raised: { 5269 src: {
5833 type: Boolean, 5270 type: String
5834 reflectToAttribute: true, 5271 },
5835 value: false, 5272
5836 observer: '_calculateElevation' 5273 /**
5837 } 5274 * Specifies the icon name or index in the set of icons available in
5838 }, 5275 * the icon's icon set. If the icon property is specified,
5839 5276 * the src property should not be.
5840 _calculateElevation: function() { 5277 */
5841 if (!this.raised) { 5278 icon: {
5842 this._setElevation(0); 5279 type: String
5843 } else { 5280 },
5844 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); 5281
5845 } 5282 /**
5846 } 5283 * Specifies the alternate text for the button, for accessibility.
5847 5284 */
5848 /** 5285 alt: {
5849 Fired when the animation finishes. 5286 type: String,
5850 This is useful if you want to wait until 5287 observer: "_altChanged"
5851 the ripple animation finishes to perform some action. 5288 }
5852 5289 },
5853 @event transitionend 5290
5854 Event param: {{node: Object}} detail Contains the animated node. 5291 _altChanged: function(newValue, oldValue) {
5855 */ 5292 var label = this.getAttribute('aria-label');
5293
5294 // Don't stomp over a user-set aria-label.
5295 if (!label || oldValue == label) {
5296 this.setAttribute('aria-label', newValue);
5297 }
5298 }
5856 }); 5299 });
5857 Polymer({ 5300 Polymer({
5858 is: 'paper-icon-button-light', 5301 is: 'paper-tab',
5859 extends: 'button',
5860 5302
5861 behaviors: [ 5303 behaviors: [
5304 Polymer.IronControlState,
5305 Polymer.IronButtonState,
5862 Polymer.PaperRippleBehavior 5306 Polymer.PaperRippleBehavior
5863 ], 5307 ],
5864 5308
5309 properties: {
5310
5311 /**
5312 * If true, the tab will forward keyboard clicks (enter/space) to
5313 * the first anchor element found in its descendants
5314 */
5315 link: {
5316 type: Boolean,
5317 value: false,
5318 reflectToAttribute: true
5319 }
5320
5321 },
5322
5323 hostAttributes: {
5324 role: 'tab'
5325 },
5326
5865 listeners: { 5327 listeners: {
5866 'down': '_rippleDown', 5328 down: '_updateNoink',
5867 'up': '_rippleUp', 5329 tap: '_onTap'
5868 'focus': '_rippleDown', 5330 },
5869 'blur': '_rippleUp', 5331
5870 }, 5332 attached: function() {
5871 5333 this._updateNoink();
5872 _rippleDown: function() { 5334 },
5873 this.getRipple().downAction(); 5335
5874 }, 5336 get _parentNoink () {
5875 5337 var parent = Polymer.dom(this).parentNode;
5876 _rippleUp: function() { 5338 return !!parent && !!parent.noink;
5877 this.getRipple().upAction(); 5339 },
5878 }, 5340
5879 5341 _updateNoink: function() {
5880 /** 5342 this.noink = !!this.noink || !!this._parentNoink;
5881 * @param {...*} var_args 5343 },
5882 */ 5344
5883 ensureRipple: function(var_args) { 5345 _onTap: function(event) {
5884 var lastRipple = this._ripple; 5346 if (this.link) {
5885 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); 5347 var anchor = this.queryEffectiveChildren('a');
5886 if (this._ripple && this._ripple !== lastRipple) { 5348
5887 this._ripple.center = true; 5349 if (!anchor) {
5888 this._ripple.classList.add('circle'); 5350 return;
5889 } 5351 }
5890 } 5352
5353 // Don't get stuck in a loop delegating
5354 // the listener from the child anchor
5355 if (event.target === anchor) {
5356 return;
5357 }
5358
5359 anchor.click();
5360 }
5361 }
5362
5891 }); 5363 });
5892 /** 5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
5893 * `iron-range-behavior` provides the behavior for something with a minimum to m aximum range. 5365 Polymer.IronMultiSelectableBehaviorImpl = {
5894 *
5895 * @demo demo/index.html
5896 * @polymerBehavior
5897 */
5898 Polymer.IronRangeBehavior = {
5899
5900 properties: {
5901
5902 /**
5903 * The number that represents the current value.
5904 */
5905 value: {
5906 type: Number,
5907 value: 0,
5908 notify: true,
5909 reflectToAttribute: true
5910 },
5911
5912 /**
5913 * The number that indicates the minimum value of the range.
5914 */
5915 min: {
5916 type: Number,
5917 value: 0,
5918 notify: true
5919 },
5920
5921 /**
5922 * The number that indicates the maximum value of the range.
5923 */
5924 max: {
5925 type: Number,
5926 value: 100,
5927 notify: true
5928 },
5929
5930 /**
5931 * Specifies the value granularity of the range's value.
5932 */
5933 step: {
5934 type: Number,
5935 value: 1,
5936 notify: true
5937 },
5938
5939 /**
5940 * Returns the ratio of the value.
5941 */
5942 ratio: {
5943 type: Number,
5944 value: 0,
5945 readOnly: true,
5946 notify: true
5947 },
5948 },
5949
5950 observers: [
5951 '_update(value, min, max, step)'
5952 ],
5953
5954 _calcRatio: function(value) {
5955 return (this._clampValue(value) - this.min) / (this.max - this.min);
5956 },
5957
5958 _clampValue: function(value) {
5959 return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
5960 },
5961
5962 _calcStep: function(value) {
5963 // polymer/issues/2493
5964 value = parseFloat(value);
5965
5966 if (!this.step) {
5967 return value;
5968 }
5969
5970 var numSteps = Math.round((value - this.min) / this.step);
5971 if (this.step < 1) {
5972 /**
5973 * For small values of this.step, if we calculate the step using
5974 * `Math.round(value / step) * step` we may hit a precision point issue
5975 * eg. 0.1 * 0.2 = 0.020000000000000004
5976 * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
5977 *
5978 * as a work around we can divide by the reciprocal of `step`
5979 */
5980 return numSteps / (1 / this.step) + this.min;
5981 } else {
5982 return numSteps * this.step + this.min;
5983 }
5984 },
5985
5986 _validateValue: function() {
5987 var v = this._clampValue(this.value);
5988 this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
5989 return this.value !== v;
5990 },
5991
5992 _update: function() {
5993 this._validateValue();
5994 this._setRatio(this._calcRatio(this.value) * 100);
5995 }
5996
5997 };
5998 Polymer({
5999 is: 'paper-progress',
6000
6001 behaviors: [
6002 Polymer.IronRangeBehavior
6003 ],
6004
6005 properties: { 5366 properties: {
6006 /** 5367
6007 * The number that represents the current secondary progress. 5368 /**
6008 */ 5369 * If true, multiple selections are allowed.
6009 secondaryProgress: { 5370 */
6010 type: Number, 5371 multi: {
6011 value: 0
6012 },
6013
6014 /**
6015 * The secondary ratio
6016 */
6017 secondaryRatio: {
6018 type: Number,
6019 value: 0,
6020 readOnly: true
6021 },
6022
6023 /**
6024 * Use an indeterminate progress indicator.
6025 */
6026 indeterminate: {
6027 type: Boolean, 5372 type: Boolean,
6028 value: false, 5373 value: false,
6029 observer: '_toggleIndeterminate' 5374 observer: 'multiChanged'
6030 }, 5375 },
6031 5376
6032 /** 5377 /**
6033 * True if the progress is disabled. 5378 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
6034 */ 5379 * is true.
6035 disabled: { 5380 */
6036 type: Boolean, 5381 selectedValues: {
6037 value: false, 5382 type: Array,
6038 reflectToAttribute: true, 5383 notify: true
6039 observer: '_disabledChanged' 5384 },
6040 } 5385
5386 /**
5387 * Returns an array of currently selected items.
5388 */
5389 selectedItems: {
5390 type: Array,
5391 readOnly: true,
5392 notify: true
5393 },
5394
6041 }, 5395 },
6042 5396
6043 observers: [ 5397 observers: [
6044 '_progressChanged(secondaryProgress, value, min, max)' 5398 '_updateSelected(selectedValues.splices)'
6045 ], 5399 ],
6046 5400
5401 /**
5402 * Selects the given value. If the `multi` property is true, then the select ed state of the
5403 * `value` will be toggled; otherwise the `value` will be selected.
5404 *
5405 * @method select
5406 * @param {string|number} value the value to select.
5407 */
5408 select: function(value) {
5409 if (this.multi) {
5410 if (this.selectedValues) {
5411 this._toggleSelected(value);
5412 } else {
5413 this.selectedValues = [value];
5414 }
5415 } else {
5416 this.selected = value;
5417 }
5418 },
5419
5420 multiChanged: function(multi) {
5421 this._selection.multi = multi;
5422 },
5423
5424 get _shouldUpdateSelection() {
5425 return this.selected != null ||
5426 (this.selectedValues != null && this.selectedValues.length);
5427 },
5428
5429 _updateAttrForSelected: function() {
5430 if (!this.multi) {
5431 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
5432 } else if (this._shouldUpdateSelection) {
5433 this.selectedValues = this.selectedItems.map(function(selectedItem) {
5434 return this._indexToValue(this.indexOf(selectedItem));
5435 }, this).filter(function(unfilteredValue) {
5436 return unfilteredValue != null;
5437 }, this);
5438 }
5439 },
5440
5441 _updateSelected: function() {
5442 if (this.multi) {
5443 this._selectMulti(this.selectedValues);
5444 } else {
5445 this._selectSelected(this.selected);
5446 }
5447 },
5448
5449 _selectMulti: function(values) {
5450 if (values) {
5451 var selectedItems = this._valuesToItems(values);
5452 // clear all but the current selected items
5453 this._selection.clear(selectedItems);
5454 // select only those not selected yet
5455 for (var i = 0; i < selectedItems.length; i++) {
5456 this._selection.setItemSelected(selectedItems[i], true);
5457 }
5458 // Check for items, since this array is populated only when attached
5459 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
5460 var fallback = this._valueToItem(this.fallbackSelection);
5461 if (fallback) {
5462 this.selectedValues = [this.fallbackSelection];
5463 }
5464 }
5465 } else {
5466 this._selection.clear();
5467 }
5468 },
5469
5470 _selectionChange: function() {
5471 var s = this._selection.get();
5472 if (this.multi) {
5473 this._setSelectedItems(s);
5474 } else {
5475 this._setSelectedItems([s]);
5476 this._setSelectedItem(s);
5477 }
5478 },
5479
5480 _toggleSelected: function(value) {
5481 var i = this.selectedValues.indexOf(value);
5482 var unselected = i < 0;
5483 if (unselected) {
5484 this.push('selectedValues',value);
5485 } else {
5486 this.splice('selectedValues',i,1);
5487 }
5488 },
5489
5490 _valuesToItems: function(values) {
5491 return (values == null) ? null : values.map(function(value) {
5492 return this._valueToItem(value);
5493 }, this);
5494 }
5495 };
5496
5497 /** @polymerBehavior */
5498 Polymer.IronMultiSelectableBehavior = [
5499 Polymer.IronSelectableBehavior,
5500 Polymer.IronMultiSelectableBehaviorImpl
5501 ];
5502 /**
5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
5504 *
5505 * @demo demo/index.html
5506 * @polymerBehavior Polymer.IronMenuBehavior
5507 */
5508 Polymer.IronMenuBehaviorImpl = {
5509
5510 properties: {
5511
5512 /**
5513 * Returns the currently focused item.
5514 * @type {?Object}
5515 */
5516 focusedItem: {
5517 observer: '_focusedItemChanged',
5518 readOnly: true,
5519 type: Object
5520 },
5521
5522 /**
5523 * The attribute to use on menu items to look up the item title. Typing th e first
5524 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
5525 * will be used.
5526 */
5527 attrForItemTitle: {
5528 type: String
5529 }
5530 },
5531
6047 hostAttributes: { 5532 hostAttributes: {
6048 role: 'progressbar' 5533 'role': 'menu',
6049 }, 5534 'tabindex': '0'
6050 5535 },
6051 _toggleIndeterminate: function(indeterminate) { 5536
6052 // If we use attribute/class binding, the animation sometimes doesn't tran slate properly 5537 observers: [
6053 // on Safari 7.1. So instead, we toggle the class here in the update metho d. 5538 '_updateMultiselectable(multi)'
6054 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress); 5539 ],
6055 }, 5540
6056 5541 listeners: {
6057 _transformProgress: function(progress, ratio) { 5542 'focus': '_onFocus',
6058 var transform = 'scaleX(' + (ratio / 100) + ')'; 5543 'keydown': '_onKeydown',
6059 progress.style.transform = progress.style.webkitTransform = transform; 5544 'iron-items-changed': '_onIronItemsChanged'
6060 }, 5545 },
6061 5546
6062 _mainRatioChanged: function(ratio) { 5547 keyBindings: {
6063 this._transformProgress(this.$.primaryProgress, ratio); 5548 'up': '_onUpKey',
6064 }, 5549 'down': '_onDownKey',
6065 5550 'esc': '_onEscKey',
6066 _progressChanged: function(secondaryProgress, value, min, max) { 5551 'shift+tab:keydown': '_onShiftTabDown'
6067 secondaryProgress = this._clampValue(secondaryProgress); 5552 },
6068 value = this._clampValue(value); 5553
6069 5554 attached: function() {
6070 var secondaryRatio = this._calcRatio(secondaryProgress) * 100; 5555 this._resetTabindices();
6071 var mainRatio = this._calcRatio(value) * 100; 5556 },
6072 5557
6073 this._setSecondaryRatio(secondaryRatio); 5558 /**
6074 this._transformProgress(this.$.secondaryProgress, secondaryRatio); 5559 * Selects the given value. If the `multi` property is true, then the select ed state of the
6075 this._transformProgress(this.$.primaryProgress, mainRatio); 5560 * `value` will be toggled; otherwise the `value` will be selected.
6076 5561 *
6077 this.secondaryProgress = secondaryProgress; 5562 * @param {string|number} value the value to select.
6078 5563 */
6079 this.setAttribute('aria-valuenow', value); 5564 select: function(value) {
6080 this.setAttribute('aria-valuemin', min); 5565 // Cancel automatically focusing a default item if the menu received focus
6081 this.setAttribute('aria-valuemax', max); 5566 // through a user action selecting a particular item.
6082 }, 5567 if (this._defaultFocusAsync) {
6083 5568 this.cancelAsync(this._defaultFocusAsync);
6084 _disabledChanged: function(disabled) { 5569 this._defaultFocusAsync = null;
6085 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); 5570 }
6086 }, 5571 var item = this._valueToItem(value);
6087 5572 if (item && item.hasAttribute('disabled')) return;
6088 _hideSecondaryProgress: function(secondaryRatio) { 5573 this._setFocusedItem(item);
6089 return secondaryRatio === 0; 5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
5575 },
5576
5577 /**
5578 * Resets all tabindex attributes to the appropriate value based on the
5579 * current selection state. The appropriate value is `0` (focusable) for
5580 * the default selected item, and `-1` (not keyboard focusable) for all
5581 * other items.
5582 */
5583 _resetTabindices: function() {
5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem;
5585
5586 this.items.forEach(function(item) {
5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
5588 }, this);
5589 },
5590
5591 /**
5592 * Sets appropriate ARIA based on whether or not the menu is meant to be
5593 * multi-selectable.
5594 *
5595 * @param {boolean} multi True if the menu should be multi-selectable.
5596 */
5597 _updateMultiselectable: function(multi) {
5598 if (multi) {
5599 this.setAttribute('aria-multiselectable', 'true');
5600 } else {
5601 this.removeAttribute('aria-multiselectable');
5602 }
5603 },
5604
5605 /**
5606 * Given a KeyboardEvent, this method will focus the appropriate item in the
5607 * menu (if there is a relevant item, and it is possible to focus it).
5608 *
5609 * @param {KeyboardEvent} event A KeyboardEvent.
5610 */
5611 _focusWithKeyboardEvent: function(event) {
5612 for (var i = 0, item; item = this.items[i]; i++) {
5613 var attr = this.attrForItemTitle || 'textContent';
5614 var title = item[attr] || item.getAttribute(attr);
5615
5616 if (!item.hasAttribute('disabled') && title &&
5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) {
5618 this._setFocusedItem(item);
5619 break;
5620 }
5621 }
5622 },
5623
5624 /**
5625 * Focuses the previous item (relative to the currently focused item) in the
5626 * menu, disabled items will be skipped.
5627 * Loop until length + 1 to handle case of single item in menu.
5628 */
5629 _focusPrevious: function() {
5630 var length = this.items.length;
5631 var curFocusIndex = Number(this.indexOf(this.focusedItem));
5632 for (var i = 1; i < length + 1; i++) {
5633 var item = this.items[(curFocusIndex - i + length) % length];
5634 if (!item.hasAttribute('disabled')) {
5635 this._setFocusedItem(item);
5636 return;
5637 }
5638 }
5639 },
5640
5641 /**
5642 * Focuses the next item (relative to the currently focused item) in the
5643 * menu, disabled items will be skipped.
5644 * Loop until length + 1 to handle case of single item in menu.
5645 */
5646 _focusNext: function() {
5647 var length = this.items.length;
5648 var curFocusIndex = Number(this.indexOf(this.focusedItem));
5649 for (var i = 1; i < length + 1; i++) {
5650 var item = this.items[(curFocusIndex + i) % length];
5651 if (!item.hasAttribute('disabled')) {
5652 this._setFocusedItem(item);
5653 return;
5654 }
5655 }
5656 },
5657
5658 /**
5659 * Mutates items in the menu based on provided selection details, so that
5660 * all items correctly reflect selection state.
5661 *
5662 * @param {Element} item An item in the menu.
5663 * @param {boolean} isSelected True if the item should be shown in a
5664 * selected state, otherwise false.
5665 */
5666 _applySelection: function(item, isSelected) {
5667 if (isSelected) {
5668 item.setAttribute('aria-selected', 'true');
5669 } else {
5670 item.removeAttribute('aria-selected');
5671 }
5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
5673 },
5674
5675 /**
5676 * Discretely updates tabindex values among menu items as the focused item
5677 * changes.
5678 *
5679 * @param {Element} focusedItem The element that is currently focused.
5680 * @param {?Element} old The last element that was considered focused, if
5681 * applicable.
5682 */
5683 _focusedItemChanged: function(focusedItem, old) {
5684 old && old.setAttribute('tabindex', '-1');
5685 if (focusedItem) {
5686 focusedItem.setAttribute('tabindex', '0');
5687 focusedItem.focus();
5688 }
5689 },
5690
5691 /**
5692 * A handler that responds to mutation changes related to the list of items
5693 * in the menu.
5694 *
5695 * @param {CustomEvent} event An event containing mutation records as its
5696 * detail.
5697 */
5698 _onIronItemsChanged: function(event) {
5699 if (event.detail.addedNodes.length) {
5700 this._resetTabindices();
5701 }
5702 },
5703
5704 /**
5705 * Handler that is called when a shift+tab keypress is detected by the menu.
5706 *
5707 * @param {CustomEvent} event A key combination event.
5708 */
5709 _onShiftTabDown: function(event) {
5710 var oldTabIndex = this.getAttribute('tabindex');
5711
5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
5713
5714 this._setFocusedItem(null);
5715
5716 this.setAttribute('tabindex', '-1');
5717
5718 this.async(function() {
5719 this.setAttribute('tabindex', oldTabIndex);
5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5721 // NOTE(cdata): polymer/polymer#1305
5722 }, 1);
5723 },
5724
5725 /**
5726 * Handler that is called when the menu receives focus.
5727 *
5728 * @param {FocusEvent} event A focus event.
5729 */
5730 _onFocus: function(event) {
5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
5732 // do not focus the menu itself
5733 return;
5734 }
5735
5736 // Do not focus the selected tab if the deepest target is part of the
5737 // menu element's local DOM and is focusable.
5738 var rootTarget = /** @type {?HTMLElement} */(
5739 Polymer.dom(event).rootTarget);
5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
5741 return;
5742 }
5743
5744 // clear the cached focus item
5745 this._defaultFocusAsync = this.async(function() {
5746 // focus the selected item when the menu receives focus, or the first it em
5747 // if no item is selected
5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
5749
5750 this._setFocusedItem(null);
5751
5752 if (selectedItem) {
5753 this._setFocusedItem(selectedItem);
5754 } else if (this.items[0]) {
5755 // We find the first none-disabled item (if one exists)
5756 this._focusNext();
5757 }
5758 });
5759 },
5760
5761 /**
5762 * Handler that is called when the up key is pressed.
5763 *
5764 * @param {CustomEvent} event A key combination event.
5765 */
5766 _onUpKey: function(event) {
5767 // up and down arrows moves the focus
5768 this._focusPrevious();
5769 event.detail.keyboardEvent.preventDefault();
5770 },
5771
5772 /**
5773 * Handler that is called when the down key is pressed.
5774 *
5775 * @param {CustomEvent} event A key combination event.
5776 */
5777 _onDownKey: function(event) {
5778 this._focusNext();
5779 event.detail.keyboardEvent.preventDefault();
5780 },
5781
5782 /**
5783 * Handler that is called when the esc key is pressed.
5784 *
5785 * @param {CustomEvent} event A key combination event.
5786 */
5787 _onEscKey: function(event) {
5788 // esc blurs the control
5789 this.focusedItem.blur();
5790 },
5791
5792 /**
5793 * Handler that is called when a keydown event is detected.
5794 *
5795 * @param {KeyboardEvent} event A keyboard event.
5796 */
5797 _onKeydown: function(event) {
5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
5799 // all other keys focus the menu item starting with that character
5800 this._focusWithKeyboardEvent(event);
5801 }
5802 event.stopPropagation();
5803 },
5804
5805 // override _activateHandler
5806 _activateHandler: function(event) {
5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
5808 event.stopPropagation();
6090 } 5809 }
6091 }); 5810 };
5811
5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
5813
5814 /** @polymerBehavior Polymer.IronMenuBehavior */
5815 Polymer.IronMenuBehavior = [
5816 Polymer.IronMultiSelectableBehavior,
5817 Polymer.IronA11yKeysBehavior,
5818 Polymer.IronMenuBehaviorImpl
5819 ];
5820 /**
5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior.
5822 *
5823 * @polymerBehavior Polymer.IronMenubarBehavior
5824 */
5825 Polymer.IronMenubarBehaviorImpl = {
5826
5827 hostAttributes: {
5828 'role': 'menubar'
5829 },
5830
5831 keyBindings: {
5832 'left': '_onLeftKey',
5833 'right': '_onRightKey'
5834 },
5835
5836 _onUpKey: function(event) {
5837 this.focusedItem.click();
5838 event.detail.keyboardEvent.preventDefault();
5839 },
5840
5841 _onDownKey: function(event) {
5842 this.focusedItem.click();
5843 event.detail.keyboardEvent.preventDefault();
5844 },
5845
5846 get _isRTL() {
5847 return window.getComputedStyle(this)['direction'] === 'rtl';
5848 },
5849
5850 _onLeftKey: function(event) {
5851 if (this._isRTL) {
5852 this._focusNext();
5853 } else {
5854 this._focusPrevious();
5855 }
5856 event.detail.keyboardEvent.preventDefault();
5857 },
5858
5859 _onRightKey: function(event) {
5860 if (this._isRTL) {
5861 this._focusPrevious();
5862 } else {
5863 this._focusNext();
5864 }
5865 event.detail.keyboardEvent.preventDefault();
5866 },
5867
5868 _onKeydown: function(event) {
5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
5870 return;
5871 }
5872
5873 // all other keys focus the menu item starting with that character
5874 this._focusWithKeyboardEvent(event);
5875 }
5876
5877 };
5878
5879 /** @polymerBehavior Polymer.IronMenubarBehavior */
5880 Polymer.IronMenubarBehavior = [
5881 Polymer.IronMenuBehavior,
5882 Polymer.IronMenubarBehaviorImpl
5883 ];
6092 /** 5884 /**
6093 * The `iron-iconset-svg` element allows users to define their own icon sets 5885 * The `iron-iconset-svg` element allows users to define their own icon sets
6094 * that contain svg icons. The svg icon elements should be children of the 5886 * that contain svg icons. The svg icon elements should be children of the
6095 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. 5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
6096 * 5888 *
6097 * Using svg elements to create icons has a few advantages over traditional 5889 * Using svg elements to create icons has a few advantages over traditional
6098 * bitmap graphics like jpg or png. Icons that use svg are vector based so 5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so
6099 * they are resolution independent and should look good on any device. They 5891 * they are resolution independent and should look good on any device. They
6100 * are stylable via css. Icons can be themed, colorized, and even animated. 5892 * are stylable via css. Icons can be themed, colorized, and even animated.
6101 * 5893 *
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after
6261 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136 6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136
6262 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root 6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
6263 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;'; 6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6264 svg.appendChild(content).removeAttribute('id'); 6056 svg.appendChild(content).removeAttribute('id');
6265 return svg; 6057 return svg;
6266 } 6058 }
6267 return null; 6059 return null;
6268 } 6060 }
6269 6061
6270 }); 6062 });
6063 Polymer({
6064 is: 'paper-tabs',
6065
6066 behaviors: [
6067 Polymer.IronResizableBehavior,
6068 Polymer.IronMenubarBehavior
6069 ],
6070
6071 properties: {
6072 /**
6073 * If true, ink ripple effect is disabled. When this property is changed ,
6074 * all descendant `<paper-tab>` elements have their `noink` property
6075 * changed to the new value as well.
6076 */
6077 noink: {
6078 type: Boolean,
6079 value: false,
6080 observer: '_noinkChanged'
6081 },
6082
6083 /**
6084 * If true, the bottom bar to indicate the selected tab will not be show n.
6085 */
6086 noBar: {
6087 type: Boolean,
6088 value: false
6089 },
6090
6091 /**
6092 * If true, the slide effect for the bottom bar is disabled.
6093 */
6094 noSlide: {
6095 type: Boolean,
6096 value: false
6097 },
6098
6099 /**
6100 * If true, tabs are scrollable and the tab width is based on the label width.
6101 */
6102 scrollable: {
6103 type: Boolean,
6104 value: false
6105 },
6106
6107 /**
6108 * If true, tabs expand to fit their container. This currently only appl ies when
6109 * scrollable is true.
6110 */
6111 fitContainer: {
6112 type: Boolean,
6113 value: false
6114 },
6115
6116 /**
6117 * If true, dragging on the tabs to scroll is disabled.
6118 */
6119 disableDrag: {
6120 type: Boolean,
6121 value: false
6122 },
6123
6124 /**
6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla ble tabs.
6126 */
6127 hideScrollButtons: {
6128 type: Boolean,
6129 value: false
6130 },
6131
6132 /**
6133 * If true, the tabs are aligned to bottom (the selection bar appears at the top).
6134 */
6135 alignBottom: {
6136 type: Boolean,
6137 value: false
6138 },
6139
6140 selectable: {
6141 type: String,
6142 value: 'paper-tab'
6143 },
6144
6145 /**
6146 * If true, tabs are automatically selected when focused using the
6147 * keyboard.
6148 */
6149 autoselect: {
6150 type: Boolean,
6151 value: false
6152 },
6153
6154 /**
6155 * The delay (in milliseconds) between when the user stops interacting
6156 * with the tabs through the keyboard and when the focused item is
6157 * automatically selected (if `autoselect` is true).
6158 */
6159 autoselectDelay: {
6160 type: Number,
6161 value: 0
6162 },
6163
6164 _step: {
6165 type: Number,
6166 value: 10
6167 },
6168
6169 _holdDelay: {
6170 type: Number,
6171 value: 1
6172 },
6173
6174 _leftHidden: {
6175 type: Boolean,
6176 value: false
6177 },
6178
6179 _rightHidden: {
6180 type: Boolean,
6181 value: false
6182 },
6183
6184 _previousTab: {
6185 type: Object
6186 }
6187 },
6188
6189 hostAttributes: {
6190 role: 'tablist'
6191 },
6192
6193 listeners: {
6194 'iron-resize': '_onTabSizingChanged',
6195 'iron-items-changed': '_onTabSizingChanged',
6196 'iron-select': '_onIronSelect',
6197 'iron-deselect': '_onIronDeselect'
6198 },
6199
6200 keyBindings: {
6201 'left:keyup right:keyup': '_onArrowKeyup'
6202 },
6203
6204 created: function() {
6205 this._holdJob = null;
6206 this._pendingActivationItem = undefined;
6207 this._pendingActivationTimeout = undefined;
6208 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind (this);
6209 this.addEventListener('blur', this._onBlurCapture.bind(this), true);
6210 },
6211
6212 ready: function() {
6213 this.setScrollDirection('y', this.$.tabsContainer);
6214 },
6215
6216 detached: function() {
6217 this._cancelPendingActivation();
6218 },
6219
6220 _noinkChanged: function(noink) {
6221 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab');
6222 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt ribute);
6223 },
6224
6225 _setNoinkAttribute: function(element) {
6226 element.setAttribute('noink', '');
6227 },
6228
6229 _removeNoinkAttribute: function(element) {
6230 element.removeAttribute('noink');
6231 },
6232
6233 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll Buttons) {
6234 if (!scrollable || hideScrollButtons) {
6235 return 'hidden';
6236 }
6237
6238 if (hideThisButton) {
6239 return 'not-visible';
6240 }
6241
6242 return '';
6243 },
6244
6245 _computeTabsContentClass: function(scrollable, fitContainer) {
6246 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '' ) : ' fit-container';
6247 },
6248
6249 _computeSelectionBarClass: function(noBar, alignBottom) {
6250 if (noBar) {
6251 return 'hidden';
6252 } else if (alignBottom) {
6253 return 'align-bottom';
6254 }
6255
6256 return '';
6257 },
6258
6259 // TODO(cdata): Add `track` response back in when gesture lands.
6260
6261 _onTabSizingChanged: function() {
6262 this.debounce('_onTabSizingChanged', function() {
6263 this._scroll();
6264 this._tabChanged(this.selectedItem);
6265 }, 10);
6266 },
6267
6268 _onIronSelect: function(event) {
6269 this._tabChanged(event.detail.item, this._previousTab);
6270 this._previousTab = event.detail.item;
6271 this.cancelDebouncer('tab-changed');
6272 },
6273
6274 _onIronDeselect: function(event) {
6275 this.debounce('tab-changed', function() {
6276 this._tabChanged(null, this._previousTab);
6277 this._previousTab = null;
6278 // See polymer/polymer#1305
6279 }, 1);
6280 },
6281
6282 _activateHandler: function() {
6283 // Cancel item activations scheduled by keyboard events when any other
6284 // action causes an item to be activated (e.g. clicks).
6285 this._cancelPendingActivation();
6286
6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
6288 },
6289
6290 /**
6291 * Activates an item after a delay (in milliseconds).
6292 */
6293 _scheduleActivation: function(item, delay) {
6294 this._pendingActivationItem = item;
6295 this._pendingActivationTimeout = this.async(
6296 this._bindDelayedActivationHandler, delay);
6297 },
6298
6299 /**
6300 * Activates the last item given to `_scheduleActivation`.
6301 */
6302 _delayedActivationHandler: function() {
6303 var item = this._pendingActivationItem;
6304 this._pendingActivationItem = undefined;
6305 this._pendingActivationTimeout = undefined;
6306 item.fire(this.activateEvent, null, {
6307 bubbles: true,
6308 cancelable: true
6309 });
6310 },
6311
6312 /**
6313 * Cancels a previously scheduled item activation made with
6314 * `_scheduleActivation`.
6315 */
6316 _cancelPendingActivation: function() {
6317 if (this._pendingActivationTimeout !== undefined) {
6318 this.cancelAsync(this._pendingActivationTimeout);
6319 this._pendingActivationItem = undefined;
6320 this._pendingActivationTimeout = undefined;
6321 }
6322 },
6323
6324 _onArrowKeyup: function(event) {
6325 if (this.autoselect) {
6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay);
6327 }
6328 },
6329
6330 _onBlurCapture: function(event) {
6331 // Cancel a scheduled item activation (if any) when that item is
6332 // blurred.
6333 if (event.target === this._pendingActivationItem) {
6334 this._cancelPendingActivation();
6335 }
6336 },
6337
6338 get _tabContainerScrollSize () {
6339 return Math.max(
6340 0,
6341 this.$.tabsContainer.scrollWidth -
6342 this.$.tabsContainer.offsetWidth
6343 );
6344 },
6345
6346 _scroll: function(e, detail) {
6347 if (!this.scrollable) {
6348 return;
6349 }
6350
6351 var ddx = (detail && -detail.ddx) || 0;
6352 this._affectScroll(ddx);
6353 },
6354
6355 _down: function(e) {
6356 // go one beat async to defeat IronMenuBehavior
6357 // autorefocus-on-no-selection timeout
6358 this.async(function() {
6359 if (this._defaultFocusAsync) {
6360 this.cancelAsync(this._defaultFocusAsync);
6361 this._defaultFocusAsync = null;
6362 }
6363 }, 1);
6364 },
6365
6366 _affectScroll: function(dx) {
6367 this.$.tabsContainer.scrollLeft += dx;
6368
6369 var scrollLeft = this.$.tabsContainer.scrollLeft;
6370
6371 this._leftHidden = scrollLeft === 0;
6372 this._rightHidden = scrollLeft === this._tabContainerScrollSize;
6373 },
6374
6375 _onLeftScrollButtonDown: function() {
6376 this._scrollToLeft();
6377 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel ay);
6378 },
6379
6380 _onRightScrollButtonDown: function() {
6381 this._scrollToRight();
6382 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe lay);
6383 },
6384
6385 _onScrollButtonUp: function() {
6386 clearInterval(this._holdJob);
6387 this._holdJob = null;
6388 },
6389
6390 _scrollToLeft: function() {
6391 this._affectScroll(-this._step);
6392 },
6393
6394 _scrollToRight: function() {
6395 this._affectScroll(this._step);
6396 },
6397
6398 _tabChanged: function(tab, old) {
6399 if (!tab) {
6400 // Remove the bar without animation.
6401 this.$.selectionBar.classList.remove('expand');
6402 this.$.selectionBar.classList.remove('contract');
6403 this._positionBar(0, 0);
6404 return;
6405 }
6406
6407 var r = this.$.tabsContent.getBoundingClientRect();
6408 var w = r.width;
6409 var tabRect = tab.getBoundingClientRect();
6410 var tabOffsetLeft = tabRect.left - r.left;
6411
6412 this._pos = {
6413 width: this._calcPercent(tabRect.width, w),
6414 left: this._calcPercent(tabOffsetLeft, w)
6415 };
6416
6417 if (this.noSlide || old == null) {
6418 // Position the bar without animation.
6419 this.$.selectionBar.classList.remove('expand');
6420 this.$.selectionBar.classList.remove('contract');
6421 this._positionBar(this._pos.width, this._pos.left);
6422 return;
6423 }
6424
6425 var oldRect = old.getBoundingClientRect();
6426 var oldIndex = this.items.indexOf(old);
6427 var index = this.items.indexOf(tab);
6428 var m = 5;
6429
6430 // bar animation: expand
6431 this.$.selectionBar.classList.add('expand');
6432
6433 var moveRight = oldIndex < index;
6434 var isRTL = this._isRTL;
6435 if (isRTL) {
6436 moveRight = !moveRight;
6437 }
6438
6439 if (moveRight) {
6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old Rect.left, w) - m,
6441 this._left);
6442 } else {
6443 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab Rect.left, w) - m,
6444 this._calcPercent(tabOffsetLeft, w) + m);
6445 }
6446
6447 if (this.scrollable) {
6448 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft);
6449 }
6450 },
6451
6452 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) {
6453 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft;
6454 if (l < 0) {
6455 this.$.tabsContainer.scrollLeft += l;
6456 } else {
6457 l += (tabWidth - this.$.tabsContainer.offsetWidth);
6458 if (l > 0) {
6459 this.$.tabsContainer.scrollLeft += l;
6460 }
6461 }
6462 },
6463
6464 _calcPercent: function(w, w0) {
6465 return 100 * w / w0;
6466 },
6467
6468 _positionBar: function(width, left) {
6469 width = width || 0;
6470 left = left || 0;
6471
6472 this._width = width;
6473 this._left = left;
6474 this.transform(
6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')',
6476 this.$.selectionBar);
6477 },
6478
6479 _onBarTransitionEnd: function(e) {
6480 var cl = this.$.selectionBar.classList;
6481 // bar animation: expand -> contract
6482 if (cl.contains('expand')) {
6483 cl.remove('expand');
6484 cl.add('contract');
6485 this._positionBar(this._pos.width, this._pos.left);
6486 // bar animation done
6487 } else if (cl.contains('contract')) {
6488 cl.remove('contract');
6489 }
6490 }
6491 });
6492 (function() {
6493 'use strict';
6494
6495 Polymer.IronA11yAnnouncer = Polymer({
6496 is: 'iron-a11y-announcer',
6497
6498 properties: {
6499
6500 /**
6501 * The value of mode is used to set the `aria-live` attribute
6502 * for the element that will be announced. Valid values are: `off`,
6503 * `polite` and `assertive`.
6504 */
6505 mode: {
6506 type: String,
6507 value: 'polite'
6508 },
6509
6510 _text: {
6511 type: String,
6512 value: ''
6513 }
6514 },
6515
6516 created: function() {
6517 if (!Polymer.IronA11yAnnouncer.instance) {
6518 Polymer.IronA11yAnnouncer.instance = this;
6519 }
6520
6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
6522 },
6523
6524 /**
6525 * Cause a text string to be announced by screen readers.
6526 *
6527 * @param {string} text The text that should be announced.
6528 */
6529 announce: function(text) {
6530 this._text = '';
6531 this.async(function() {
6532 this._text = text;
6533 }, 100);
6534 },
6535
6536 _onIronAnnounce: function(event) {
6537 if (event.detail && event.detail.text) {
6538 this.announce(event.detail.text);
6539 }
6540 }
6541 });
6542
6543 Polymer.IronA11yAnnouncer.instance = null;
6544
6545 Polymer.IronA11yAnnouncer.requestAvailability = function() {
6546 if (!Polymer.IronA11yAnnouncer.instance) {
6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
6548 }
6549
6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
6551 };
6552 })();
6553 /**
6554 * Singleton IronMeta instance.
6555 */
6556 Polymer.IronValidatableBehaviorMeta = null;
6557
6558 /**
6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
6561 *
6562 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
6563 * To validate a form imperatively, call the form's `validate()` method, which in turn will
6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
6565 * custom element will get a public `validate()`, which
6566 * will return the validity of the element, and a corresponding `invalid` attr ibute,
6567 * which can be used for styling.
6568 *
6569 * To implement the custom validation logic of your element, you must override
6570 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
6572 * for an example.
6573 *
6574 * ### Accessibility
6575 *
6576 * Changing the `invalid` property, either manually or by calling `validate()` will update the
6577 * `aria-invalid` attribute.
6578 *
6579 * @demo demo/index.html
6580 * @polymerBehavior
6581 */
6582 Polymer.IronValidatableBehavior = {
6583
6584 properties: {
6585
6586 /**
6587 * Name of the validator to use.
6588 */
6589 validator: {
6590 type: String
6591 },
6592
6593 /**
6594 * True if the last call to `validate` is invalid.
6595 */
6596 invalid: {
6597 notify: true,
6598 reflectToAttribute: true,
6599 type: Boolean,
6600 value: false
6601 },
6602
6603 /**
6604 * This property is deprecated and should not be used. Use the global
6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
6606 */
6607 _validatorMeta: {
6608 type: Object
6609 },
6610
6611 /**
6612 * Namespace for this validator. This property is deprecated and should
6613 * not be used. For all intents and purposes, please consider it a
6614 * read-only, config-time property.
6615 */
6616 validatorType: {
6617 type: String,
6618 value: 'validator'
6619 },
6620
6621 _validator: {
6622 type: Object,
6623 computed: '__computeValidator(validator)'
6624 }
6625 },
6626
6627 observers: [
6628 '_invalidChanged(invalid)'
6629 ],
6630
6631 registered: function() {
6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
6633 },
6634
6635 _invalidChanged: function() {
6636 if (this.invalid) {
6637 this.setAttribute('aria-invalid', 'true');
6638 } else {
6639 this.removeAttribute('aria-invalid');
6640 }
6641 },
6642
6643 /**
6644 * @return {boolean} True if the validator `validator` exists.
6645 */
6646 hasValidator: function() {
6647 return this._validator != null;
6648 },
6649
6650 /**
6651 * Returns true if the `value` is valid, and updates `invalid`. If you want
6652 * your element to have custom validation logic, do not override this method ;
6653 * override `_getValidity(value)` instead.
6654
6655 * @param {Object} value The value to be validated. By default, it is passed
6656 * to the validator's `validate()` function, if a validator is set.
6657 * @return {boolean} True if `value` is valid.
6658 */
6659 validate: function(value) {
6660 this.invalid = !this._getValidity(value);
6661 return !this.invalid;
6662 },
6663
6664 /**
6665 * Returns true if `value` is valid. By default, it is passed
6666 * to the validator's `validate()` function, if a validator is set. You
6667 * should override this method if you want to implement custom validity
6668 * logic for your element.
6669 *
6670 * @param {Object} value The value to be validated.
6671 * @return {boolean} True if `value` is valid.
6672 */
6673
6674 _getValidity: function(value) {
6675 if (this.hasValidator()) {
6676 return this._validator.validate(value);
6677 }
6678 return true;
6679 },
6680
6681 __computeValidator: function() {
6682 return Polymer.IronValidatableBehaviorMeta &&
6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
6684 }
6685 };
6686 /*
6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
6688 to `<input>`.
6689
6690 ### Two-way binding
6691
6692 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
6693
6694 <input value="{{myValue::input}}">
6695
6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
6697 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
6698
6699 <input is="iron-input" bind-value="{{myValue}}">
6700
6701 ### Custom validators
6702
6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
6704
6705 <input is="iron-input" validator="my-custom-validator">
6706
6707 ### Stopping invalid input
6708
6709 It may be desirable to only allow users to enter certain characters. You can use the
6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
6711 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
6712
6713 \x3c!-- only allow characters that match [0-9] --\x3e
6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
6715
6716 @hero hero.svg
6717 @demo demo/index.html
6718 */
6719
6720 Polymer({
6721
6722 is: 'iron-input',
6723
6724 extends: 'input',
6725
6726 behaviors: [
6727 Polymer.IronValidatableBehavior
6728 ],
6729
6730 properties: {
6731
6732 /**
6733 * Use this property instead of `value` for two-way data binding.
6734 */
6735 bindValue: {
6736 observer: '_bindValueChanged',
6737 type: String
6738 },
6739
6740 /**
6741 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
6742 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
6743 * Pasted input will have each character checked individually; if any char acter
6744 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
6745 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
6746 */
6747 preventInvalidInput: {
6748 type: Boolean
6749 },
6750
6751 /**
6752 * Regular expression that list the characters allowed as input.
6753 * This pattern represents the allowed characters for the field; as the us er inputs text,
6754 * each individual character will be checked against the pattern (rather t han checking
6755 * the entire value as a whole). The recommended format should be a list o f allowed characters;
6756 * for example, `[a-zA-Z0-9.+-!;:]`
6757 */
6758 allowedPattern: {
6759 type: String,
6760 observer: "_allowedPatternChanged"
6761 },
6762
6763 _previousValidInput: {
6764 type: String,
6765 value: ''
6766 },
6767
6768 _patternAlreadyChecked: {
6769 type: Boolean,
6770 value: false
6771 }
6772
6773 },
6774
6775 listeners: {
6776 'input': '_onInput',
6777 'keypress': '_onKeypress'
6778 },
6779
6780 /** @suppress {checkTypes} */
6781 registered: function() {
6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
6783 if (!this._canDispatchEventOnDisabled()) {
6784 this._origDispatchEvent = this.dispatchEvent;
6785 this.dispatchEvent = this._dispatchEventFirefoxIE;
6786 }
6787 },
6788
6789 created: function() {
6790 Polymer.IronA11yAnnouncer.requestAvailability();
6791 },
6792
6793 _canDispatchEventOnDisabled: function() {
6794 var input = document.createElement('input');
6795 var canDispatch = false;
6796 input.disabled = true;
6797
6798 input.addEventListener('feature-check-dispatch-event', function() {
6799 canDispatch = true;
6800 });
6801
6802 try {
6803 input.dispatchEvent(new Event('feature-check-dispatch-event'));
6804 } catch(e) {}
6805
6806 return canDispatch;
6807 },
6808
6809 _dispatchEventFirefoxIE: function() {
6810 // Due to Firefox bug, events fired on disabled form controls can throw
6811 // errors; furthermore, neither IE nor Firefox will actually dispatch
6812 // events from disabled form controls; as such, we toggle disable around
6813 // the dispatch to allow notifying properties to notify
6814 // See issue #47 for details
6815 var disabled = this.disabled;
6816 this.disabled = false;
6817 this._origDispatchEvent.apply(this, arguments);
6818 this.disabled = disabled;
6819 },
6820
6821 get _patternRegExp() {
6822 var pattern;
6823 if (this.allowedPattern) {
6824 pattern = new RegExp(this.allowedPattern);
6825 } else {
6826 switch (this.type) {
6827 case 'number':
6828 pattern = /[0-9.,e-]/;
6829 break;
6830 }
6831 }
6832 return pattern;
6833 },
6834
6835 ready: function() {
6836 this.bindValue = this.value;
6837 },
6838
6839 /**
6840 * @suppress {checkTypes}
6841 */
6842 _bindValueChanged: function() {
6843 if (this.value !== this.bindValue) {
6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
6845 }
6846 // manually notify because we don't want to notify until after setting val ue
6847 this.fire('bind-value-changed', {value: this.bindValue});
6848 },
6849
6850 _allowedPatternChanged: function() {
6851 // Force to prevent invalid input when an `allowed-pattern` is set
6852 this.preventInvalidInput = this.allowedPattern ? true : false;
6853 },
6854
6855 _onInput: function() {
6856 // Need to validate each of the characters pasted if they haven't
6857 // been validated inside `_onKeypress` already.
6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
6859 var valid = this._checkPatternValidity();
6860 if (!valid) {
6861 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
6862 this.value = this._previousValidInput;
6863 }
6864 }
6865
6866 this.bindValue = this.value;
6867 this._previousValidInput = this.value;
6868 this._patternAlreadyChecked = false;
6869 },
6870
6871 _isPrintable: function(event) {
6872 // What a control/printable character is varies wildly based on the browse r.
6873 // - most control characters (arrows, backspace) do not send a `keypress` event
6874 // in Chrome, but the *do* on Firefox
6875 // - in Firefox, when they do send a `keypress` event, control chars have
6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
6877 // - printable characters always send a keypress event.
6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
6879 // always matches the charCode.
6880 // None of this makes any sense.
6881
6882 // For these keys, ASCII code == browser keycode.
6883 var anyNonPrintable =
6884 (event.keyCode == 8) || // backspace
6885 (event.keyCode == 9) || // tab
6886 (event.keyCode == 13) || // enter
6887 (event.keyCode == 27); // escape
6888
6889 // For these keys, make sure it's a browser keycode and not an ASCII code.
6890 var mozNonPrintable =
6891 (event.keyCode == 19) || // pause
6892 (event.keyCode == 20) || // caps lock
6893 (event.keyCode == 45) || // insert
6894 (event.keyCode == 46) || // delete
6895 (event.keyCode == 144) || // num lock
6896 (event.keyCode == 145) || // scroll lock
6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys
6899
6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
6901 },
6902
6903 _onKeypress: function(event) {
6904 if (!this.preventInvalidInput && this.type !== 'number') {
6905 return;
6906 }
6907 var regexp = this._patternRegExp;
6908 if (!regexp) {
6909 return;
6910 }
6911
6912 // Handle special keys and backspace
6913 if (event.metaKey || event.ctrlKey || event.altKey)
6914 return;
6915
6916 // Check the pattern either here or in `_onInput`, but not in both.
6917 this._patternAlreadyChecked = true;
6918
6919 var thisChar = String.fromCharCode(event.charCode);
6920 if (this._isPrintable(event) && !regexp.test(thisChar)) {
6921 event.preventDefault();
6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
6923 }
6924 },
6925
6926 _checkPatternValidity: function() {
6927 var regexp = this._patternRegExp;
6928 if (!regexp) {
6929 return true;
6930 }
6931 for (var i = 0; i < this.value.length; i++) {
6932 if (!regexp.test(this.value[i])) {
6933 return false;
6934 }
6935 }
6936 return true;
6937 },
6938
6939 /**
6940 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
6941 * then any constraints.
6942 * @return {boolean} True if the value is valid.
6943 */
6944 validate: function() {
6945 // First, check what the browser thinks. Some inputs (like type=number)
6946 // behave weirdly and will set the value to "" if something invalid is
6947 // entered, but will set the validity correctly.
6948 var valid = this.checkValidity();
6949
6950 // Only do extra checking if the browser thought this was valid.
6951 if (valid) {
6952 // Empty, required input is invalid
6953 if (this.required && this.value === '') {
6954 valid = false;
6955 } else if (this.hasValidator()) {
6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
6957 }
6958 }
6959
6960 this.invalid = !valid;
6961 this.fire('iron-input-validate');
6962 return valid;
6963 },
6964
6965 _announceInvalidCharacter: function(message) {
6966 this.fire('iron-announce', { text: message });
6967 }
6968 });
6969
6970 /*
6971 The `iron-input-validate` event is fired whenever `validate()` is called.
6972 @event iron-input-validate
6973 */
6974 Polymer({
6975 is: 'paper-input-container',
6976
6977 properties: {
6978 /**
6979 * Set to true to disable the floating label. The label disappears when th e input value is
6980 * not null.
6981 */
6982 noLabelFloat: {
6983 type: Boolean,
6984 value: false
6985 },
6986
6987 /**
6988 * Set to true to always float the floating label.
6989 */
6990 alwaysFloatLabel: {
6991 type: Boolean,
6992 value: false
6993 },
6994
6995 /**
6996 * The attribute to listen for value changes on.
6997 */
6998 attrForValue: {
6999 type: String,
7000 value: 'bind-value'
7001 },
7002
7003 /**
7004 * Set to true to auto-validate the input value when it changes.
7005 */
7006 autoValidate: {
7007 type: Boolean,
7008 value: false
7009 },
7010
7011 /**
7012 * True if the input is invalid. This property is set automatically when t he input value
7013 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
7014 */
7015 invalid: {
7016 observer: '_invalidChanged',
7017 type: Boolean,
7018 value: false
7019 },
7020
7021 /**
7022 * True if the input has focus.
7023 */
7024 focused: {
7025 readOnly: true,
7026 type: Boolean,
7027 value: false,
7028 notify: true
7029 },
7030
7031 _addons: {
7032 type: Array
7033 // do not set a default value here intentionally - it will be initialize d lazily when a
7034 // distributed child is attached, which may occur before configuration f or this element
7035 // in polyfill.
7036 },
7037
7038 _inputHasContent: {
7039 type: Boolean,
7040 value: false
7041 },
7042
7043 _inputSelector: {
7044 type: String,
7045 value: 'input,textarea,.paper-input-input'
7046 },
7047
7048 _boundOnFocus: {
7049 type: Function,
7050 value: function() {
7051 return this._onFocus.bind(this);
7052 }
7053 },
7054
7055 _boundOnBlur: {
7056 type: Function,
7057 value: function() {
7058 return this._onBlur.bind(this);
7059 }
7060 },
7061
7062 _boundOnInput: {
7063 type: Function,
7064 value: function() {
7065 return this._onInput.bind(this);
7066 }
7067 },
7068
7069 _boundValueChanged: {
7070 type: Function,
7071 value: function() {
7072 return this._onValueChanged.bind(this);
7073 }
7074 }
7075 },
7076
7077 listeners: {
7078 'addon-attached': '_onAddonAttached',
7079 'iron-input-validate': '_onIronInputValidate'
7080 },
7081
7082 get _valueChangedEvent() {
7083 return this.attrForValue + '-changed';
7084 },
7085
7086 get _propertyForValue() {
7087 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
7088 },
7089
7090 get _inputElement() {
7091 return Polymer.dom(this).querySelector(this._inputSelector);
7092 },
7093
7094 get _inputElementValue() {
7095 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
7096 },
7097
7098 ready: function() {
7099 if (!this._addons) {
7100 this._addons = [];
7101 }
7102 this.addEventListener('focus', this._boundOnFocus, true);
7103 this.addEventListener('blur', this._boundOnBlur, true);
7104 },
7105
7106 attached: function() {
7107 if (this.attrForValue) {
7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
7109 } else {
7110 this.addEventListener('input', this._onInput);
7111 }
7112
7113 // Only validate when attached if the input already has a value.
7114 if (this._inputElementValue != '') {
7115 this._handleValueAndAutoValidate(this._inputElement);
7116 } else {
7117 this._handleValue(this._inputElement);
7118 }
7119 },
7120
7121 _onAddonAttached: function(event) {
7122 if (!this._addons) {
7123 this._addons = [];
7124 }
7125 var target = event.target;
7126 if (this._addons.indexOf(target) === -1) {
7127 this._addons.push(target);
7128 if (this.isAttached) {
7129 this._handleValue(this._inputElement);
7130 }
7131 }
7132 },
7133
7134 _onFocus: function() {
7135 this._setFocused(true);
7136 },
7137
7138 _onBlur: function() {
7139 this._setFocused(false);
7140 this._handleValueAndAutoValidate(this._inputElement);
7141 },
7142
7143 _onInput: function(event) {
7144 this._handleValueAndAutoValidate(event.target);
7145 },
7146
7147 _onValueChanged: function(event) {
7148 this._handleValueAndAutoValidate(event.target);
7149 },
7150
7151 _handleValue: function(inputElement) {
7152 var value = this._inputElementValue;
7153
7154 // type="number" hack needed because this.value is empty until it's valid
7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
7156 this._inputHasContent = true;
7157 } else {
7158 this._inputHasContent = false;
7159 }
7160
7161 this.updateAddons({
7162 inputElement: inputElement,
7163 value: value,
7164 invalid: this.invalid
7165 });
7166 },
7167
7168 _handleValueAndAutoValidate: function(inputElement) {
7169 if (this.autoValidate) {
7170 var valid;
7171 if (inputElement.validate) {
7172 valid = inputElement.validate(this._inputElementValue);
7173 } else {
7174 valid = inputElement.checkValidity();
7175 }
7176 this.invalid = !valid;
7177 }
7178
7179 // Call this last to notify the add-ons.
7180 this._handleValue(inputElement);
7181 },
7182
7183 _onIronInputValidate: function(event) {
7184 this.invalid = this._inputElement.invalid;
7185 },
7186
7187 _invalidChanged: function() {
7188 if (this._addons) {
7189 this.updateAddons({invalid: this.invalid});
7190 }
7191 },
7192
7193 /**
7194 * Call this to update the state of add-ons.
7195 * @param {Object} state Add-on state.
7196 */
7197 updateAddons: function(state) {
7198 for (var addon, index = 0; addon = this._addons[index]; index++) {
7199 addon.update(state);
7200 }
7201 },
7202
7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
7204 var cls = 'input-content';
7205 if (!noLabelFloat) {
7206 var label = this.querySelector('label');
7207
7208 if (alwaysFloatLabel || _inputHasContent) {
7209 cls += ' label-is-floating';
7210 // If the label is floating, ignore any offsets that may have been
7211 // applied from a prefix element.
7212 this.$.labelAndInputContainer.style.position = 'static';
7213
7214 if (invalid) {
7215 cls += ' is-invalid';
7216 } else if (focused) {
7217 cls += " label-is-highlighted";
7218 }
7219 } else {
7220 // When the label is not floating, it should overlap the input element .
7221 if (label) {
7222 this.$.labelAndInputContainer.style.position = 'relative';
7223 }
7224 }
7225 } else {
7226 if (_inputHasContent) {
7227 cls += ' label-is-hidden';
7228 }
7229 }
7230 return cls;
7231 },
7232
7233 _computeUnderlineClass: function(focused, invalid) {
7234 var cls = 'underline';
7235 if (invalid) {
7236 cls += ' is-invalid';
7237 } else if (focused) {
7238 cls += ' is-highlighted'
7239 }
7240 return cls;
7241 },
7242
7243 _computeAddOnContentClass: function(focused, invalid) {
7244 var cls = 'add-on-content';
7245 if (invalid) {
7246 cls += ' is-invalid';
7247 } else if (focused) {
7248 cls += ' is-highlighted'
7249 }
7250 return cls;
7251 }
7252 });
7253 /** @polymerBehavior */
7254 Polymer.PaperSpinnerBehavior = {
7255
7256 listeners: {
7257 'animationend': '__reset',
7258 'webkitAnimationEnd': '__reset'
7259 },
7260
7261 properties: {
7262 /**
7263 * Displays the spinner.
7264 */
7265 active: {
7266 type: Boolean,
7267 value: false,
7268 reflectToAttribute: true,
7269 observer: '__activeChanged'
7270 },
7271
7272 /**
7273 * Alternative text content for accessibility support.
7274 * If alt is present, it will add an aria-label whose content matches alt when active.
7275 * If alt is not present, it will default to 'loading' as the alt value.
7276 */
7277 alt: {
7278 type: String,
7279 value: 'loading',
7280 observer: '__altChanged'
7281 },
7282
7283 __coolingDown: {
7284 type: Boolean,
7285 value: false
7286 }
7287 },
7288
7289 __computeContainerClasses: function(active, coolingDown) {
7290 return [
7291 active || coolingDown ? 'active' : '',
7292 coolingDown ? 'cooldown' : ''
7293 ].join(' ');
7294 },
7295
7296 __activeChanged: function(active, old) {
7297 this.__setAriaHidden(!active);
7298 this.__coolingDown = !active && old;
7299 },
7300
7301 __altChanged: function(alt) {
7302 // user-provided `aria-label` takes precedence over prototype default
7303 if (alt === this.getPropertyInfo('alt').value) {
7304 this.alt = this.getAttribute('aria-label') || alt;
7305 } else {
7306 this.__setAriaHidden(alt==='');
7307 this.setAttribute('aria-label', alt);
7308 }
7309 },
7310
7311 __setAriaHidden: function(hidden) {
7312 var attr = 'aria-hidden';
7313 if (hidden) {
7314 this.setAttribute(attr, 'true');
7315 } else {
7316 this.removeAttribute(attr);
7317 }
7318 },
7319
7320 __reset: function() {
7321 this.active = false;
7322 this.__coolingDown = false;
7323 }
7324 };
7325 Polymer({
7326 is: 'paper-spinner-lite',
7327
7328 behaviors: [
7329 Polymer.PaperSpinnerBehavior
7330 ]
7331 });
7332 // Copyright 2016 The Chromium Authors. All rights reserved.
7333 // Use of this source code is governed by a BSD-style license that can be
7334 // found in the LICENSE file.
7335
7336 /**
7337 * Implements an incremental search field which can be shown and hidden.
7338 * Canonical implementation is <cr-search-field>.
7339 * @polymerBehavior
7340 */
7341 var CrSearchFieldBehavior = {
7342 properties: {
7343 label: {
7344 type: String,
7345 value: '',
7346 },
7347
7348 clearLabel: {
7349 type: String,
7350 value: '',
7351 },
7352
7353 showingSearch: {
7354 type: Boolean,
7355 value: false,
7356 notify: true,
7357 observer: 'showingSearchChanged_',
7358 reflectToAttribute: true
7359 },
7360
7361 /** @private */
7362 lastValue_: {
7363 type: String,
7364 value: '',
7365 },
7366 },
7367
7368 /**
7369 * @abstract
7370 * @return {!HTMLInputElement} The input field element the behavior should
7371 * use.
7372 */
7373 getSearchInput: function() {},
7374
7375 /**
7376 * @return {string} The value of the search field.
7377 */
7378 getValue: function() {
7379 return this.getSearchInput().value;
7380 },
7381
7382 /**
7383 * Sets the value of the search field.
7384 * @param {string} value
7385 */
7386 setValue: function(value) {
7387 // Use bindValue when setting the input value so that changes propagate
7388 // correctly.
7389 this.getSearchInput().bindValue = value;
7390 this.onValueChanged_(value);
7391 },
7392
7393 showAndFocus: function() {
7394 this.showingSearch = true;
7395 this.focus_();
7396 },
7397
7398 /** @private */
7399 focus_: function() {
7400 this.getSearchInput().focus();
7401 },
7402
7403 onSearchTermSearch: function() {
7404 this.onValueChanged_(this.getValue());
7405 },
7406
7407 /**
7408 * Updates the internal state of the search field based on a change that has
7409 * already happened.
7410 * @param {string} newValue
7411 * @private
7412 */
7413 onValueChanged_: function(newValue) {
7414 if (newValue == this.lastValue_)
7415 return;
7416
7417 this.fire('search-changed', newValue);
7418 this.lastValue_ = newValue;
7419 },
7420
7421 onSearchTermKeydown: function(e) {
7422 if (e.key == 'Escape')
7423 this.showingSearch = false;
7424 },
7425
7426 /** @private */
7427 showingSearchChanged_: function() {
7428 if (this.showingSearch) {
7429 this.focus_();
7430 return;
7431 }
7432
7433 this.setValue('');
7434 this.getSearchInput().blur();
7435 }
7436 };
7437 // Copyright 2016 The Chromium Authors. All rights reserved.
7438 // Use of this source code is governed by a BSD-style license that can be
7439 // found in the LICENSE file.
7440
7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field.
7442 Polymer({
7443 is: 'cr-toolbar-search-field',
7444
7445 behaviors: [CrSearchFieldBehavior],
7446
7447 properties: {
7448 narrow: {
7449 type: Boolean,
7450 reflectToAttribute: true,
7451 },
7452
7453 // Prompt text to display in the search field.
7454 label: String,
7455
7456 // Tooltip to display on the clear search button.
7457 clearLabel: String,
7458
7459 // When true, show a loading spinner to indicate that the backend is
7460 // processing the search. Will only show if the search field is open.
7461 spinnerActive: {
7462 type: Boolean,
7463 reflectToAttribute: true
7464 },
7465
7466 /** @private */
7467 hasSearchText_: Boolean,
7468 },
7469
7470 listeners: {
7471 'tap': 'showSearch_',
7472 'searchInput.bind-value-changed': 'onBindValueChanged_',
7473 },
7474
7475 /** @return {!HTMLInputElement} */
7476 getSearchInput: function() {
7477 return this.$.searchInput;
7478 },
7479
7480 /** @return {boolean} */
7481 isSearchFocused: function() {
7482 return this.$.searchTerm.focused;
7483 },
7484
7485 /**
7486 * @param {boolean} narrow
7487 * @return {number}
7488 * @private
7489 */
7490 computeIconTabIndex_: function(narrow) {
7491 return narrow ? 0 : -1;
7492 },
7493
7494 /**
7495 * @param {boolean} spinnerActive
7496 * @param {boolean} showingSearch
7497 * @return {boolean}
7498 * @private
7499 */
7500 isSpinnerShown_: function(spinnerActive, showingSearch) {
7501 return spinnerActive && showingSearch;
7502 },
7503
7504 /** @private */
7505 onInputBlur_: function() {
7506 if (!this.hasSearchText_)
7507 this.showingSearch = false;
7508 },
7509
7510 /**
7511 * Update the state of the search field whenever the underlying input value
7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately
7513 * after any change, whether the result of user input or JS modification.
7514 * @private
7515 */
7516 onBindValueChanged_: function() {
7517 var newValue = this.$.searchInput.bindValue;
7518 this.hasSearchText_ = newValue != '';
7519 if (newValue != '')
7520 this.showingSearch = true;
7521 },
7522
7523 /**
7524 * @param {Event} e
7525 * @private
7526 */
7527 showSearch_: function(e) {
7528 if (e.target != this.$.clearSearch)
7529 this.showingSearch = true;
7530 },
7531
7532 /**
7533 * @param {Event} e
7534 * @private
7535 */
7536 hideSearch_: function(e) {
7537 this.showingSearch = false;
7538 e.stopPropagation();
7539 }
7540 });
7541 // Copyright 2016 The Chromium Authors. All rights reserved.
7542 // Use of this source code is governed by a BSD-style license that can be
7543 // found in the LICENSE file.
7544
7545 Polymer({
7546 is: 'cr-toolbar',
7547
7548 properties: {
7549 // Name to display in the toolbar, in titlecase.
7550 pageName: String,
7551
7552 // Prompt text to display in the search field.
7553 searchPrompt: String,
7554
7555 // Tooltip to display on the clear search button.
7556 clearLabel: String,
7557
7558 // Value is proxied through to cr-toolbar-search-field. When true,
7559 // the search field will show a processing spinner.
7560 spinnerActive: Boolean,
7561
7562 // Controls whether the menu button is shown at the start of the menu.
7563 showMenu: {
7564 type: Boolean,
7565 reflectToAttribute: true,
7566 value: true
7567 },
7568
7569 /** @private */
7570 narrow_: {
7571 type: Boolean,
7572 reflectToAttribute: true
7573 },
7574
7575 /** @private */
7576 showingSearch_: {
7577 type: Boolean,
7578 reflectToAttribute: true,
7579 },
7580 },
7581
7582 /** @return {!CrToolbarSearchFieldElement} */
7583 getSearchField: function() {
7584 return this.$.search;
7585 },
7586
7587 /** @private */
7588 onMenuTap_: function(e) {
7589 this.fire('cr-menu-tap');
7590 }
7591 });
6271 // Copyright 2015 The Chromium Authors. All rights reserved. 7592 // Copyright 2015 The Chromium Authors. All rights reserved.
6272 // Use of this source code is governed by a BSD-style license that can be 7593 // Use of this source code is governed by a BSD-style license that can be
6273 // found in the LICENSE file. 7594 // found in the LICENSE file.
6274 7595
6275 cr.define('downloads', function() { 7596 Polymer({
6276 var Item = Polymer({ 7597 is: 'history-toolbar',
6277 is: 'downloads-item', 7598 properties: {
6278 7599 // Number of history items currently selected.
6279 properties: { 7600 // TODO(calamity): bind this to
6280 data: { 7601 // listContainer.selectedItem.selectedPaths.length.
6281 type: Object, 7602 count: {
6282 }, 7603 type: Number,
6283 7604 value: 0,
6284 completelyOnDisk_: { 7605 observer: 'changeToolbarView_'
6285 computed: 'computeCompletelyOnDisk_(' + 7606 },
6286 'data.state, data.file_externally_removed)', 7607
6287 type: Boolean, 7608 // True if 1 or more history items are selected. When this value changes
6288 value: true, 7609 // the background colour changes.
6289 }, 7610 itemsSelected_: {
6290 7611 type: Boolean,
6291 controlledBy_: { 7612 value: false,
6292 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', 7613 reflectToAttribute: true
6293 type: String, 7614 },
6294 value: '', 7615
6295 }, 7616 // The most recent term entered in the search field. Updated incrementally
6296 7617 // as the user types.
6297 isActive_: { 7618 searchTerm: {
6298 computed: 'computeIsActive_(' + 7619 type: String,
6299 'data.state, data.file_externally_removed)', 7620 notify: true,
6300 type: Boolean, 7621 },
6301 value: true, 7622
6302 }, 7623 // True if the backend is processing and a spinner should be shown in the
6303 7624 // toolbar.
6304 isDangerous_: { 7625 spinnerActive: {
6305 computed: 'computeIsDangerous_(data.state)', 7626 type: Boolean,
6306 type: Boolean, 7627 value: false
6307 value: false, 7628 },
6308 }, 7629
6309 7630 hasDrawer: {
6310 isMalware_: { 7631 type: Boolean,
6311 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', 7632 observer: 'hasDrawerChanged_',
6312 type: Boolean, 7633 reflectToAttribute: true,
6313 value: false, 7634 },
6314 }, 7635
6315 7636 // Whether domain-grouped history is enabled.
6316 isInProgress_: { 7637 isGroupedMode: {
6317 computed: 'computeIsInProgress_(data.state)', 7638 type: Boolean,
6318 type: Boolean, 7639 reflectToAttribute: true,
6319 value: false, 7640 },
6320 }, 7641
6321 7642 // The period to search over. Matches BrowsingHistoryHandler::Range.
6322 pauseOrResumeText_: { 7643 groupedRange: {
6323 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', 7644 type: Number,
6324 type: String, 7645 value: 0,
6325 }, 7646 reflectToAttribute: true,
6326 7647 notify: true
6327 showCancel_: { 7648 },
6328 computed: 'computeShowCancel_(data.state)', 7649
6329 type: Boolean, 7650 // The start time of the query range.
6330 value: false, 7651 queryStartTime: String,
6331 }, 7652
6332 7653 // The end time of the query range.
6333 showProgress_: { 7654 queryEndTime: String,
6334 computed: 'computeShowProgress_(showCancel_, data.percent)', 7655 },
6335 type: Boolean, 7656
6336 value: false, 7657 /**
6337 }, 7658 * Changes the toolbar background color depending on whether any history items
6338 }, 7659 * are currently selected.
6339 7660 * @private
6340 observers: [ 7661 */
6341 // TODO(dbeam): this gets called way more when I observe data.by_ext_id 7662 changeToolbarView_: function() {
6342 // and data.by_ext_name directly. Why? 7663 this.itemsSelected_ = this.count > 0;
6343 'observeControlledBy_(controlledBy_)', 7664 },
6344 'observeIsDangerous_(isDangerous_, data)', 7665
6345 ], 7666 /**
6346 7667 * When changing the search term externally, update the search field to
6347 ready: function() { 7668 * reflect the new search term.
6348 this.content = this.$.content; 7669 * @param {string} search
6349 }, 7670 */
6350 7671 setSearchTerm: function(search) {
6351 /** @private */ 7672 if (this.searchTerm == search)
6352 computeClass_: function() { 7673 return;
6353 var classes = []; 7674
6354 7675 this.searchTerm = search;
6355 if (this.isActive_) 7676 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
6356 classes.push('is-active'); 7677 .getSearchField();
6357 7678 searchField.showAndFocus();
6358 if (this.isDangerous_) 7679 searchField.setValue(search);
6359 classes.push('dangerous'); 7680 },
6360 7681
6361 if (this.showProgress_) 7682 /**
6362 classes.push('show-progress'); 7683 * @param {!CustomEvent} event
6363 7684 * @private
6364 return classes.join(' '); 7685 */
6365 }, 7686 onSearchChanged_: function(event) {
6366 7687 this.searchTerm = /** @type {string} */ (event.detail);
6367 /** @private */ 7688 },
6368 computeCompletelyOnDisk_: function() { 7689
6369 return this.data.state == downloads.States.COMPLETE && 7690 onClearSelectionTap_: function() {
6370 !this.data.file_externally_removed; 7691 this.fire('unselect-all');
6371 }, 7692 },
6372 7693
6373 /** @private */ 7694 onDeleteTap_: function() {
6374 computeControlledBy_: function() { 7695 this.fire('delete-selected');
6375 if (!this.data.by_ext_id || !this.data.by_ext_name) 7696 },
6376 return ''; 7697
6377 7698 get searchBar() {
6378 var url = 'chrome://extensions#' + this.data.by_ext_id; 7699 return this.$['main-toolbar'].getSearchField();
6379 var name = this.data.by_ext_name; 7700 },
6380 return loadTimeData.getStringF('controlledByUrl', url, name); 7701
6381 }, 7702 showSearchField: function() {
6382 7703 /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
6383 /** @private */ 7704 .getSearchField()
6384 computeDangerIcon_: function() { 7705 .showAndFocus();
6385 if (!this.isDangerous_) 7706 },
6386 return ''; 7707
6387 7708 /**
6388 switch (this.data.danger_type) { 7709 * If the user is a supervised user the delete button is not shown.
6389 case downloads.DangerType.DANGEROUS_CONTENT: 7710 * @private
6390 case downloads.DangerType.DANGEROUS_HOST: 7711 */
6391 case downloads.DangerType.DANGEROUS_URL: 7712 deletingAllowed_: function() {
6392 case downloads.DangerType.POTENTIALLY_UNWANTED: 7713 return loadTimeData.getBoolean('allowDeletingHistory');
6393 case downloads.DangerType.UNCOMMON_CONTENT: 7714 },
6394 return 'downloads:remove-circle'; 7715
6395 default: 7716 numberOfItemsSelected_: function(count) {
6396 return 'cr:warning'; 7717 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : '';
6397 } 7718 },
6398 }, 7719
6399 7720 getHistoryInterval_: function(queryStartTime, queryEndTime) {
6400 /** @private */ 7721 // TODO(calamity): Fix the format of these dates.
6401 computeDate_: function() { 7722 return loadTimeData.getStringF(
6402 assert(typeof this.data.hideDate == 'boolean'); 7723 'historyInterval', queryStartTime, queryEndTime);
6403 if (this.data.hideDate) 7724 },
6404 return ''; 7725
6405 return assert(this.data.since_string || this.data.date_string); 7726 /** @private */
6406 }, 7727 hasDrawerChanged_: function() {
6407 7728 this.updateStyles();
6408 /** @private */ 7729 },
6409 computeDescription_: function() {
6410 var data = this.data;
6411
6412 switch (data.state) {
6413 case downloads.States.DANGEROUS:
6414 var fileName = data.file_name;
6415 switch (data.danger_type) {
6416 case downloads.DangerType.DANGEROUS_FILE:
6417 return loadTimeData.getStringF('dangerFileDesc', fileName);
6418 case downloads.DangerType.DANGEROUS_URL:
6419 return loadTimeData.getString('dangerUrlDesc');
6420 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through.
6421 case downloads.DangerType.DANGEROUS_HOST:
6422 return loadTimeData.getStringF('dangerContentDesc', fileName);
6423 case downloads.DangerType.UNCOMMON_CONTENT:
6424 return loadTimeData.getStringF('dangerUncommonDesc', fileName);
6425 case downloads.DangerType.POTENTIALLY_UNWANTED:
6426 return loadTimeData.getStringF('dangerSettingsDesc', fileName);
6427 }
6428 break;
6429
6430 case downloads.States.IN_PROGRESS:
6431 case downloads.States.PAUSED: // Fallthrough.
6432 return data.progress_status_text;
6433 }
6434
6435 return '';
6436 },
6437
6438 /** @private */
6439 computeIsActive_: function() {
6440 return this.data.state != downloads.States.CANCELLED &&
6441 this.data.state != downloads.States.INTERRUPTED &&
6442 !this.data.file_externally_removed;
6443 },
6444
6445 /** @private */
6446 computeIsDangerous_: function() {
6447 return this.data.state == downloads.States.DANGEROUS;
6448 },
6449
6450 /** @private */
6451 computeIsInProgress_: function() {
6452 return this.data.state == downloads.States.IN_PROGRESS;
6453 },
6454
6455 /** @private */
6456 computeIsMalware_: function() {
6457 return this.isDangerous_ &&
6458 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT ||
6459 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
6460 this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
6461 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
6462 },
6463
6464 /** @private */
6465 computePauseOrResumeText_: function() {
6466 if (this.isInProgress_)
6467 return loadTimeData.getString('controlPause');
6468 if (this.data.resume)
6469 return loadTimeData.getString('controlResume');
6470 return '';
6471 },
6472
6473 /** @private */
6474 computeRemoveStyle_: function() {
6475 var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
6476 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
6477 return hideRemove ? 'visibility: hidden' : '';
6478 },
6479
6480 /** @private */
6481 computeShowCancel_: function() {
6482 return this.data.state == downloads.States.IN_PROGRESS ||
6483 this.data.state == downloads.States.PAUSED;
6484 },
6485
6486 /** @private */
6487 computeShowProgress_: function() {
6488 return this.showCancel_ && this.data.percent >= -1;
6489 },
6490
6491 /** @private */
6492 computeTag_: function() {
6493 switch (this.data.state) {
6494 case downloads.States.CANCELLED:
6495 return loadTimeData.getString('statusCancelled');
6496
6497 case downloads.States.INTERRUPTED:
6498 return this.data.last_reason_text;
6499
6500 case downloads.States.COMPLETE:
6501 return this.data.file_externally_removed ?
6502 loadTimeData.getString('statusRemoved') : '';
6503 }
6504
6505 return '';
6506 },
6507
6508 /** @private */
6509 isIndeterminate_: function() {
6510 return this.data.percent == -1;
6511 },
6512
6513 /** @private */
6514 observeControlledBy_: function() {
6515 this.$['controlled-by'].innerHTML = this.controlledBy_;
6516 },
6517
6518 /** @private */
6519 observeIsDangerous_: function() {
6520 if (!this.data)
6521 return;
6522
6523 if (this.isDangerous_) {
6524 this.$.url.removeAttribute('href');
6525 } else {
6526 this.$.url.href = assert(this.data.url);
6527 var filePath = encodeURIComponent(this.data.file_path);
6528 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x';
6529 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor;
6530 }
6531 },
6532
6533 /** @private */
6534 onCancelTap_: function() {
6535 downloads.ActionService.getInstance().cancel(this.data.id);
6536 },
6537
6538 /** @private */
6539 onDiscardDangerousTap_: function() {
6540 downloads.ActionService.getInstance().discardDangerous(this.data.id);
6541 },
6542
6543 /**
6544 * @private
6545 * @param {Event} e
6546 */
6547 onDragStart_: function(e) {
6548 e.preventDefault();
6549 downloads.ActionService.getInstance().drag(this.data.id);
6550 },
6551
6552 /**
6553 * @param {Event} e
6554 * @private
6555 */
6556 onFileLinkTap_: function(e) {
6557 e.preventDefault();
6558 downloads.ActionService.getInstance().openFile(this.data.id);
6559 },
6560
6561 /** @private */
6562 onPauseOrResumeTap_: function() {
6563 if (this.isInProgress_)
6564 downloads.ActionService.getInstance().pause(this.data.id);
6565 else
6566 downloads.ActionService.getInstance().resume(this.data.id);
6567 },
6568
6569 /** @private */
6570 onRemoveTap_: function() {
6571 downloads.ActionService.getInstance().remove(this.data.id);
6572 },
6573
6574 /** @private */
6575 onRetryTap_: function() {
6576 downloads.ActionService.getInstance().download(this.data.url);
6577 },
6578
6579 /** @private */
6580 onSaveDangerousTap_: function() {
6581 downloads.ActionService.getInstance().saveDangerous(this.data.id);
6582 },
6583
6584 /** @private */
6585 onShowTap_: function() {
6586 downloads.ActionService.getInstance().show(this.data.id);
6587 },
6588 });
6589
6590 return {Item: Item};
6591 }); 7730 });
6592 /** @polymerBehavior Polymer.PaperItemBehavior */ 7731 // Copyright 2016 The Chromium Authors. All rights reserved.
6593 Polymer.PaperItemBehaviorImpl = { 7732 // Use of this source code is governed by a BSD-style license that can be
6594 hostAttributes: { 7733 // found in the LICENSE file.
6595 role: 'option', 7734
6596 tabindex: '0' 7735 /**
6597 } 7736 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the
6598 }; 7737 * dialog is closed via close(), a 'close' event is fired. If the dialog is
6599 7738 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event.
6600 /** @polymerBehavior */ 7739 * Additionally clients can inspect the dialog's |returnValue| property inside
6601 Polymer.PaperItemBehavior = [ 7740 * the 'close' event listener to determine whether it was canceled or just
6602 Polymer.IronButtonState, 7741 * closed, where a truthy value means success, and a falsy value means it was
6603 Polymer.IronControlState, 7742 * canceled.
6604 Polymer.PaperItemBehaviorImpl 7743 */
6605 ];
6606 Polymer({ 7744 Polymer({
6607 is: 'paper-item', 7745 is: 'cr-dialog',
6608 7746 extends: 'dialog',
6609 behaviors: [ 7747
6610 Polymer.PaperItemBehavior 7748 cancel: function() {
6611 ] 7749 this.fire('cancel');
6612 }); 7750 HTMLDialogElement.prototype.close.call(this, '');
6613 /** 7751 },
6614 * @param {!Function} selectCallback 7752
6615 * @constructor 7753 /**
6616 */ 7754 * @param {string=} opt_returnValue
6617 Polymer.IronSelection = function(selectCallback) { 7755 * @override
6618 this.selection = []; 7756 */
6619 this.selectCallback = selectCallback; 7757 close: function(opt_returnValue) {
6620 }; 7758 HTMLDialogElement.prototype.close.call(this, 'success');
6621 7759 },
6622 Polymer.IronSelection.prototype = { 7760
6623 7761 /** @return {!PaperIconButtonElement} */
6624 /** 7762 getCloseButton: function() {
6625 * Retrieves the selected item(s). 7763 return this.$.close;
6626 * 7764 },
6627 * @method get 7765 });
6628 * @returns Returns the selected item(s). If the multi property is true,
6629 * `get` will return an array, otherwise it will return
6630 * the selected item or undefined if there is no selection.
6631 */
6632 get: function() {
6633 return this.multi ? this.selection.slice() : this.selection[0];
6634 },
6635
6636 /**
6637 * Clears all the selection except the ones indicated.
6638 *
6639 * @method clear
6640 * @param {Array} excludes items to be excluded.
6641 */
6642 clear: function(excludes) {
6643 this.selection.slice().forEach(function(item) {
6644 if (!excludes || excludes.indexOf(item) < 0) {
6645 this.setItemSelected(item, false);
6646 }
6647 }, this);
6648 },
6649
6650 /**
6651 * Indicates if a given item is selected.
6652 *
6653 * @method isSelected
6654 * @param {*} item The item whose selection state should be checked.
6655 * @returns Returns true if `item` is selected.
6656 */
6657 isSelected: function(item) {
6658 return this.selection.indexOf(item) >= 0;
6659 },
6660
6661 /**
6662 * Sets the selection state for a given item to either selected or deselecte d.
6663 *
6664 * @method setItemSelected
6665 * @param {*} item The item to select.
6666 * @param {boolean} isSelected True for selected, false for deselected.
6667 */
6668 setItemSelected: function(item, isSelected) {
6669 if (item != null) {
6670 if (isSelected !== this.isSelected(item)) {
6671 // proceed to update selection only if requested state differs from cu rrent
6672 if (isSelected) {
6673 this.selection.push(item);
6674 } else {
6675 var i = this.selection.indexOf(item);
6676 if (i >= 0) {
6677 this.selection.splice(i, 1);
6678 }
6679 }
6680 if (this.selectCallback) {
6681 this.selectCallback(item, isSelected);
6682 }
6683 }
6684 }
6685 },
6686
6687 /**
6688 * Sets the selection state for a given item. If the `multi` property
6689 * is true, then the selected state of `item` will be toggled; otherwise
6690 * the `item` will be selected.
6691 *
6692 * @method select
6693 * @param {*} item The item to select.
6694 */
6695 select: function(item) {
6696 if (this.multi) {
6697 this.toggle(item);
6698 } else if (this.get() !== item) {
6699 this.setItemSelected(this.get(), false);
6700 this.setItemSelected(item, true);
6701 }
6702 },
6703
6704 /**
6705 * Toggles the selection state for `item`.
6706 *
6707 * @method toggle
6708 * @param {*} item The item to toggle.
6709 */
6710 toggle: function(item) {
6711 this.setItemSelected(item, !this.isSelected(item));
6712 }
6713
6714 };
6715 /** @polymerBehavior */
6716 Polymer.IronSelectableBehavior = {
6717
6718 /**
6719 * Fired when iron-selector is activated (selected or deselected).
6720 * It is fired before the selected items are changed.
6721 * Cancel the event to abort selection.
6722 *
6723 * @event iron-activate
6724 */
6725
6726 /**
6727 * Fired when an item is selected
6728 *
6729 * @event iron-select
6730 */
6731
6732 /**
6733 * Fired when an item is deselected
6734 *
6735 * @event iron-deselect
6736 */
6737
6738 /**
6739 * Fired when the list of selectable items changes (e.g., items are
6740 * added or removed). The detail of the event is a mutation record that
6741 * describes what changed.
6742 *
6743 * @event iron-items-changed
6744 */
6745
6746 properties: {
6747
6748 /**
6749 * If you want to use an attribute value or property of an element for
6750 * `selected` instead of the index, set this to the name of the attribute
6751 * or property. Hyphenated values are converted to camel case when used to
6752 * look up the property of a selectable element. Camel cased values are
6753 * *not* converted to hyphenated values for attribute lookup. It's
6754 * recommended that you provide the hyphenated form of the name so that
6755 * selection works in both cases. (Use `attr-or-property-name` instead of
6756 * `attrOrPropertyName`.)
6757 */
6758 attrForSelected: {
6759 type: String,
6760 value: null
6761 },
6762
6763 /**
6764 * Gets or sets the selected element. The default is to use the index of t he item.
6765 * @type {string|number}
6766 */
6767 selected: {
6768 type: String,
6769 notify: true
6770 },
6771
6772 /**
6773 * Returns the currently selected item.
6774 *
6775 * @type {?Object}
6776 */
6777 selectedItem: {
6778 type: Object,
6779 readOnly: true,
6780 notify: true
6781 },
6782
6783 /**
6784 * The event that fires from items when they are selected. Selectable
6785 * will listen for this event from items and update the selection state.
6786 * Set to empty string to listen to no events.
6787 */
6788 activateEvent: {
6789 type: String,
6790 value: 'tap',
6791 observer: '_activateEventChanged'
6792 },
6793
6794 /**
6795 * This is a CSS selector string. If this is set, only items that match t he CSS selector
6796 * are selectable.
6797 */
6798 selectable: String,
6799
6800 /**
6801 * The class to set on elements when selected.
6802 */
6803 selectedClass: {
6804 type: String,
6805 value: 'iron-selected'
6806 },
6807
6808 /**
6809 * The attribute to set on elements when selected.
6810 */
6811 selectedAttribute: {
6812 type: String,
6813 value: null
6814 },
6815
6816 /**
6817 * Default fallback if the selection based on selected with `attrForSelect ed`
6818 * is not found.
6819 */
6820 fallbackSelection: {
6821 type: String,
6822 value: null
6823 },
6824
6825 /**
6826 * The list of items from which a selection can be made.
6827 */
6828 items: {
6829 type: Array,
6830 readOnly: true,
6831 notify: true,
6832 value: function() {
6833 return [];
6834 }
6835 },
6836
6837 /**
6838 * The set of excluded elements where the key is the `localName`
6839 * of the element that will be ignored from the item list.
6840 *
6841 * @default {template: 1}
6842 */
6843 _excludedLocalNames: {
6844 type: Object,
6845 value: function() {
6846 return {
6847 'template': 1
6848 };
6849 }
6850 }
6851 },
6852
6853 observers: [
6854 '_updateAttrForSelected(attrForSelected)',
6855 '_updateSelected(selected)',
6856 '_checkFallback(fallbackSelection)'
6857 ],
6858
6859 created: function() {
6860 this._bindFilterItem = this._filterItem.bind(this);
6861 this._selection = new Polymer.IronSelection(this._applySelection.bind(this ));
6862 },
6863
6864 attached: function() {
6865 this._observer = this._observeItems(this);
6866 this._updateItems();
6867 if (!this._shouldUpdateSelection) {
6868 this._updateSelected();
6869 }
6870 this._addListener(this.activateEvent);
6871 },
6872
6873 detached: function() {
6874 if (this._observer) {
6875 Polymer.dom(this).unobserveNodes(this._observer);
6876 }
6877 this._removeListener(this.activateEvent);
6878 },
6879
6880 /**
6881 * Returns the index of the given item.
6882 *
6883 * @method indexOf
6884 * @param {Object} item
6885 * @returns Returns the index of the item
6886 */
6887 indexOf: function(item) {
6888 return this.items.indexOf(item);
6889 },
6890
6891 /**
6892 * Selects the given value.
6893 *
6894 * @method select
6895 * @param {string|number} value the value to select.
6896 */
6897 select: function(value) {
6898 this.selected = value;
6899 },
6900
6901 /**
6902 * Selects the previous item.
6903 *
6904 * @method selectPrevious
6905 */
6906 selectPrevious: function() {
6907 var length = this.items.length;
6908 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
6909 this.selected = this._indexToValue(index);
6910 },
6911
6912 /**
6913 * Selects the next item.
6914 *
6915 * @method selectNext
6916 */
6917 selectNext: function() {
6918 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
6919 this.selected = this._indexToValue(index);
6920 },
6921
6922 /**
6923 * Selects the item at the given index.
6924 *
6925 * @method selectIndex
6926 */
6927 selectIndex: function(index) {
6928 this.select(this._indexToValue(index));
6929 },
6930
6931 /**
6932 * Force a synchronous update of the `items` property.
6933 *
6934 * NOTE: Consider listening for the `iron-items-changed` event to respond to
6935 * updates to the set of selectable items after updates to the DOM list and
6936 * selection state have been made.
6937 *
6938 * WARNING: If you are using this method, you should probably consider an
6939 * alternate approach. Synchronously querying for items is potentially
6940 * slow for many use cases. The `items` property will update asynchronously
6941 * on its own to reflect selectable items in the DOM.
6942 */
6943 forceSynchronousItemUpdate: function() {
6944 this._updateItems();
6945 },
6946
6947 get _shouldUpdateSelection() {
6948 return this.selected != null;
6949 },
6950
6951 _checkFallback: function() {
6952 if (this._shouldUpdateSelection) {
6953 this._updateSelected();
6954 }
6955 },
6956
6957 _addListener: function(eventName) {
6958 this.listen(this, eventName, '_activateHandler');
6959 },
6960
6961 _removeListener: function(eventName) {
6962 this.unlisten(this, eventName, '_activateHandler');
6963 },
6964
6965 _activateEventChanged: function(eventName, old) {
6966 this._removeListener(old);
6967 this._addListener(eventName);
6968 },
6969
6970 _updateItems: function() {
6971 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
6972 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
6973 this._setItems(nodes);
6974 },
6975
6976 _updateAttrForSelected: function() {
6977 if (this._shouldUpdateSelection) {
6978 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
6979 }
6980 },
6981
6982 _updateSelected: function() {
6983 this._selectSelected(this.selected);
6984 },
6985
6986 _selectSelected: function(selected) {
6987 this._selection.select(this._valueToItem(this.selected));
6988 // Check for items, since this array is populated only when attached
6989 // Since Number(0) is falsy, explicitly check for undefined
6990 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
6991 this.selected = this.fallbackSelection;
6992 }
6993 },
6994
6995 _filterItem: function(node) {
6996 return !this._excludedLocalNames[node.localName];
6997 },
6998
6999 _valueToItem: function(value) {
7000 return (value == null) ? null : this.items[this._valueToIndex(value)];
7001 },
7002
7003 _valueToIndex: function(value) {
7004 if (this.attrForSelected) {
7005 for (var i = 0, item; item = this.items[i]; i++) {
7006 if (this._valueForItem(item) == value) {
7007 return i;
7008 }
7009 }
7010 } else {
7011 return Number(value);
7012 }
7013 },
7014
7015 _indexToValue: function(index) {
7016 if (this.attrForSelected) {
7017 var item = this.items[index];
7018 if (item) {
7019 return this._valueForItem(item);
7020 }
7021 } else {
7022 return index;
7023 }
7024 },
7025
7026 _valueForItem: function(item) {
7027 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ];
7028 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected);
7029 },
7030
7031 _applySelection: function(item, isSelected) {
7032 if (this.selectedClass) {
7033 this.toggleClass(this.selectedClass, isSelected, item);
7034 }
7035 if (this.selectedAttribute) {
7036 this.toggleAttribute(this.selectedAttribute, isSelected, item);
7037 }
7038 this._selectionChange();
7039 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
7040 },
7041
7042 _selectionChange: function() {
7043 this._setSelectedItem(this._selection.get());
7044 },
7045
7046 // observe items change under the given node.
7047 _observeItems: function(node) {
7048 return Polymer.dom(node).observeNodes(function(mutation) {
7049 this._updateItems();
7050
7051 if (this._shouldUpdateSelection) {
7052 this._updateSelected();
7053 }
7054
7055 // Let other interested parties know about the change so that
7056 // we don't have to recreate mutation observers everywhere.
7057 this.fire('iron-items-changed', mutation, {
7058 bubbles: false,
7059 cancelable: false
7060 });
7061 });
7062 },
7063
7064 _activateHandler: function(e) {
7065 var t = e.target;
7066 var items = this.items;
7067 while (t && t != this) {
7068 var i = items.indexOf(t);
7069 if (i >= 0) {
7070 var value = this._indexToValue(i);
7071 this._itemActivate(value, t);
7072 return;
7073 }
7074 t = t.parentNode;
7075 }
7076 },
7077
7078 _itemActivate: function(value, item) {
7079 if (!this.fire('iron-activate',
7080 {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
7081 this.select(value);
7082 }
7083 }
7084
7085 };
7086 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
7087 Polymer.IronMultiSelectableBehaviorImpl = {
7088 properties: {
7089
7090 /**
7091 * If true, multiple selections are allowed.
7092 */
7093 multi: {
7094 type: Boolean,
7095 value: false,
7096 observer: 'multiChanged'
7097 },
7098
7099 /**
7100 * Gets or sets the selected elements. This is used instead of `selected` when `multi`
7101 * is true.
7102 */
7103 selectedValues: {
7104 type: Array,
7105 notify: true
7106 },
7107
7108 /**
7109 * Returns an array of currently selected items.
7110 */
7111 selectedItems: {
7112 type: Array,
7113 readOnly: true,
7114 notify: true
7115 },
7116
7117 },
7118
7119 observers: [
7120 '_updateSelected(selectedValues.splices)'
7121 ],
7122
7123 /**
7124 * Selects the given value. If the `multi` property is true, then the select ed state of the
7125 * `value` will be toggled; otherwise the `value` will be selected.
7126 *
7127 * @method select
7128 * @param {string|number} value the value to select.
7129 */
7130 select: function(value) {
7131 if (this.multi) {
7132 if (this.selectedValues) {
7133 this._toggleSelected(value);
7134 } else {
7135 this.selectedValues = [value];
7136 }
7137 } else {
7138 this.selected = value;
7139 }
7140 },
7141
7142 multiChanged: function(multi) {
7143 this._selection.multi = multi;
7144 },
7145
7146 get _shouldUpdateSelection() {
7147 return this.selected != null ||
7148 (this.selectedValues != null && this.selectedValues.length);
7149 },
7150
7151 _updateAttrForSelected: function() {
7152 if (!this.multi) {
7153 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
7154 } else if (this._shouldUpdateSelection) {
7155 this.selectedValues = this.selectedItems.map(function(selectedItem) {
7156 return this._indexToValue(this.indexOf(selectedItem));
7157 }, this).filter(function(unfilteredValue) {
7158 return unfilteredValue != null;
7159 }, this);
7160 }
7161 },
7162
7163 _updateSelected: function() {
7164 if (this.multi) {
7165 this._selectMulti(this.selectedValues);
7166 } else {
7167 this._selectSelected(this.selected);
7168 }
7169 },
7170
7171 _selectMulti: function(values) {
7172 if (values) {
7173 var selectedItems = this._valuesToItems(values);
7174 // clear all but the current selected items
7175 this._selection.clear(selectedItems);
7176 // select only those not selected yet
7177 for (var i = 0; i < selectedItems.length; i++) {
7178 this._selection.setItemSelected(selectedItems[i], true);
7179 }
7180 // Check for items, since this array is populated only when attached
7181 if (this.fallbackSelection && this.items.length && !this._selection.get( ).length) {
7182 var fallback = this._valueToItem(this.fallbackSelection);
7183 if (fallback) {
7184 this.selectedValues = [this.fallbackSelection];
7185 }
7186 }
7187 } else {
7188 this._selection.clear();
7189 }
7190 },
7191
7192 _selectionChange: function() {
7193 var s = this._selection.get();
7194 if (this.multi) {
7195 this._setSelectedItems(s);
7196 } else {
7197 this._setSelectedItems([s]);
7198 this._setSelectedItem(s);
7199 }
7200 },
7201
7202 _toggleSelected: function(value) {
7203 var i = this.selectedValues.indexOf(value);
7204 var unselected = i < 0;
7205 if (unselected) {
7206 this.push('selectedValues',value);
7207 } else {
7208 this.splice('selectedValues',i,1);
7209 }
7210 },
7211
7212 _valuesToItems: function(values) {
7213 return (values == null) ? null : values.map(function(value) {
7214 return this._valueToItem(value);
7215 }, this);
7216 }
7217 };
7218
7219 /** @polymerBehavior */
7220 Polymer.IronMultiSelectableBehavior = [
7221 Polymer.IronSelectableBehavior,
7222 Polymer.IronMultiSelectableBehaviorImpl
7223 ];
7224 /**
7225 * `Polymer.IronMenuBehavior` implements accessible menu behavior.
7226 *
7227 * @demo demo/index.html
7228 * @polymerBehavior Polymer.IronMenuBehavior
7229 */
7230 Polymer.IronMenuBehaviorImpl = {
7231
7232 properties: {
7233
7234 /**
7235 * Returns the currently focused item.
7236 * @type {?Object}
7237 */
7238 focusedItem: {
7239 observer: '_focusedItemChanged',
7240 readOnly: true,
7241 type: Object
7242 },
7243
7244 /**
7245 * The attribute to use on menu items to look up the item title. Typing th e first
7246 * letter of an item when the menu is open focuses that item. If unset, `t extContent`
7247 * will be used.
7248 */
7249 attrForItemTitle: {
7250 type: String
7251 }
7252 },
7253
7254 hostAttributes: {
7255 'role': 'menu',
7256 'tabindex': '0'
7257 },
7258
7259 observers: [
7260 '_updateMultiselectable(multi)'
7261 ],
7262
7263 listeners: {
7264 'focus': '_onFocus',
7265 'keydown': '_onKeydown',
7266 'iron-items-changed': '_onIronItemsChanged'
7267 },
7268
7269 keyBindings: {
7270 'up': '_onUpKey',
7271 'down': '_onDownKey',
7272 'esc': '_onEscKey',
7273 'shift+tab:keydown': '_onShiftTabDown'
7274 },
7275
7276 attached: function() {
7277 this._resetTabindices();
7278 },
7279
7280 /**
7281 * Selects the given value. If the `multi` property is true, then the select ed state of the
7282 * `value` will be toggled; otherwise the `value` will be selected.
7283 *
7284 * @param {string|number} value the value to select.
7285 */
7286 select: function(value) {
7287 // Cancel automatically focusing a default item if the menu received focus
7288 // through a user action selecting a particular item.
7289 if (this._defaultFocusAsync) {
7290 this.cancelAsync(this._defaultFocusAsync);
7291 this._defaultFocusAsync = null;
7292 }
7293 var item = this._valueToItem(value);
7294 if (item && item.hasAttribute('disabled')) return;
7295 this._setFocusedItem(item);
7296 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
7297 },
7298
7299 /**
7300 * Resets all tabindex attributes to the appropriate value based on the
7301 * current selection state. The appropriate value is `0` (focusable) for
7302 * the default selected item, and `-1` (not keyboard focusable) for all
7303 * other items.
7304 */
7305 _resetTabindices: function() {
7306 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[ 0]) : this.selectedItem;
7307
7308 this.items.forEach(function(item) {
7309 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
7310 }, this);
7311 },
7312
7313 /**
7314 * Sets appropriate ARIA based on whether or not the menu is meant to be
7315 * multi-selectable.
7316 *
7317 * @param {boolean} multi True if the menu should be multi-selectable.
7318 */
7319 _updateMultiselectable: function(multi) {
7320 if (multi) {
7321 this.setAttribute('aria-multiselectable', 'true');
7322 } else {
7323 this.removeAttribute('aria-multiselectable');
7324 }
7325 },
7326
7327 /**
7328 * Given a KeyboardEvent, this method will focus the appropriate item in the
7329 * menu (if there is a relevant item, and it is possible to focus it).
7330 *
7331 * @param {KeyboardEvent} event A KeyboardEvent.
7332 */
7333 _focusWithKeyboardEvent: function(event) {
7334 for (var i = 0, item; item = this.items[i]; i++) {
7335 var attr = this.attrForItemTitle || 'textContent';
7336 var title = item[attr] || item.getAttribute(attr);
7337
7338 if (!item.hasAttribute('disabled') && title &&
7339 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k eyCode).toLowerCase()) {
7340 this._setFocusedItem(item);
7341 break;
7342 }
7343 }
7344 },
7345
7346 /**
7347 * Focuses the previous item (relative to the currently focused item) in the
7348 * menu, disabled items will be skipped.
7349 * Loop until length + 1 to handle case of single item in menu.
7350 */
7351 _focusPrevious: function() {
7352 var length = this.items.length;
7353 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7354 for (var i = 1; i < length + 1; i++) {
7355 var item = this.items[(curFocusIndex - i + length) % length];
7356 if (!item.hasAttribute('disabled')) {
7357 this._setFocusedItem(item);
7358 return;
7359 }
7360 }
7361 },
7362
7363 /**
7364 * Focuses the next item (relative to the currently focused item) in the
7365 * menu, disabled items will be skipped.
7366 * Loop until length + 1 to handle case of single item in menu.
7367 */
7368 _focusNext: function() {
7369 var length = this.items.length;
7370 var curFocusIndex = Number(this.indexOf(this.focusedItem));
7371 for (var i = 1; i < length + 1; i++) {
7372 var item = this.items[(curFocusIndex + i) % length];
7373 if (!item.hasAttribute('disabled')) {
7374 this._setFocusedItem(item);
7375 return;
7376 }
7377 }
7378 },
7379
7380 /**
7381 * Mutates items in the menu based on provided selection details, so that
7382 * all items correctly reflect selection state.
7383 *
7384 * @param {Element} item An item in the menu.
7385 * @param {boolean} isSelected True if the item should be shown in a
7386 * selected state, otherwise false.
7387 */
7388 _applySelection: function(item, isSelected) {
7389 if (isSelected) {
7390 item.setAttribute('aria-selected', 'true');
7391 } else {
7392 item.removeAttribute('aria-selected');
7393 }
7394 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
7395 },
7396
7397 /**
7398 * Discretely updates tabindex values among menu items as the focused item
7399 * changes.
7400 *
7401 * @param {Element} focusedItem The element that is currently focused.
7402 * @param {?Element} old The last element that was considered focused, if
7403 * applicable.
7404 */
7405 _focusedItemChanged: function(focusedItem, old) {
7406 old && old.setAttribute('tabindex', '-1');
7407 if (focusedItem) {
7408 focusedItem.setAttribute('tabindex', '0');
7409 focusedItem.focus();
7410 }
7411 },
7412
7413 /**
7414 * A handler that responds to mutation changes related to the list of items
7415 * in the menu.
7416 *
7417 * @param {CustomEvent} event An event containing mutation records as its
7418 * detail.
7419 */
7420 _onIronItemsChanged: function(event) {
7421 if (event.detail.addedNodes.length) {
7422 this._resetTabindices();
7423 }
7424 },
7425
7426 /**
7427 * Handler that is called when a shift+tab keypress is detected by the menu.
7428 *
7429 * @param {CustomEvent} event A key combination event.
7430 */
7431 _onShiftTabDown: function(event) {
7432 var oldTabIndex = this.getAttribute('tabindex');
7433
7434 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
7435
7436 this._setFocusedItem(null);
7437
7438 this.setAttribute('tabindex', '-1');
7439
7440 this.async(function() {
7441 this.setAttribute('tabindex', oldTabIndex);
7442 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7443 // NOTE(cdata): polymer/polymer#1305
7444 }, 1);
7445 },
7446
7447 /**
7448 * Handler that is called when the menu receives focus.
7449 *
7450 * @param {FocusEvent} event A focus event.
7451 */
7452 _onFocus: function(event) {
7453 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
7454 // do not focus the menu itself
7455 return;
7456 }
7457
7458 // Do not focus the selected tab if the deepest target is part of the
7459 // menu element's local DOM and is focusable.
7460 var rootTarget = /** @type {?HTMLElement} */(
7461 Polymer.dom(event).rootTarget);
7462 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && ! this.isLightDescendant(rootTarget)) {
7463 return;
7464 }
7465
7466 // clear the cached focus item
7467 this._defaultFocusAsync = this.async(function() {
7468 // focus the selected item when the menu receives focus, or the first it em
7469 // if no item is selected
7470 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem s[0]) : this.selectedItem;
7471
7472 this._setFocusedItem(null);
7473
7474 if (selectedItem) {
7475 this._setFocusedItem(selectedItem);
7476 } else if (this.items[0]) {
7477 // We find the first none-disabled item (if one exists)
7478 this._focusNext();
7479 }
7480 });
7481 },
7482
7483 /**
7484 * Handler that is called when the up key is pressed.
7485 *
7486 * @param {CustomEvent} event A key combination event.
7487 */
7488 _onUpKey: function(event) {
7489 // up and down arrows moves the focus
7490 this._focusPrevious();
7491 event.detail.keyboardEvent.preventDefault();
7492 },
7493
7494 /**
7495 * Handler that is called when the down key is pressed.
7496 *
7497 * @param {CustomEvent} event A key combination event.
7498 */
7499 _onDownKey: function(event) {
7500 this._focusNext();
7501 event.detail.keyboardEvent.preventDefault();
7502 },
7503
7504 /**
7505 * Handler that is called when the esc key is pressed.
7506 *
7507 * @param {CustomEvent} event A key combination event.
7508 */
7509 _onEscKey: function(event) {
7510 // esc blurs the control
7511 this.focusedItem.blur();
7512 },
7513
7514 /**
7515 * Handler that is called when a keydown event is detected.
7516 *
7517 * @param {KeyboardEvent} event A keyboard event.
7518 */
7519 _onKeydown: function(event) {
7520 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
7521 // all other keys focus the menu item starting with that character
7522 this._focusWithKeyboardEvent(event);
7523 }
7524 event.stopPropagation();
7525 },
7526
7527 // override _activateHandler
7528 _activateHandler: function(event) {
7529 Polymer.IronSelectableBehavior._activateHandler.call(this, event);
7530 event.stopPropagation();
7531 }
7532 };
7533
7534 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7535
7536 /** @polymerBehavior Polymer.IronMenuBehavior */
7537 Polymer.IronMenuBehavior = [
7538 Polymer.IronMultiSelectableBehavior,
7539 Polymer.IronA11yKeysBehavior,
7540 Polymer.IronMenuBehaviorImpl
7541 ];
7542 (function() {
7543 Polymer({
7544 is: 'paper-menu',
7545
7546 behaviors: [
7547 Polymer.IronMenuBehavior
7548 ]
7549 });
7550 })();
7551 /** 7766 /**
7552 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and 7767 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
7553 optionally centers it in the window or another element. 7768 optionally centers it in the window or another element.
7554 7769
7555 The element will only be sized and/or positioned if it has not already been size d and/or positioned 7770 The element will only be sized and/or positioned if it has not already been size d and/or positioned
7556 by CSS. 7771 by CSS.
7557 7772
7558 CSS properties | Action 7773 CSS properties | Action
7559 -----------------------------|------------------------------------------- 7774 -----------------------------|-------------------------------------------
7560 `position` set | Element is not centered horizontally or verticall y 7775 `position` set | Element is not centered horizontally or verticall y
(...skipping 2734 matching lines...) Expand 10 before | Expand all | Expand 10 after
10295 height: height + 'px', 10510 height: height + 'px',
10296 transform: 'translateY(0)' 10511 transform: 'translateY(0)'
10297 }, { 10512 }, {
10298 height: height / 2 + 'px', 10513 height: height / 2 + 'px',
10299 transform: 'translateY(-20px)' 10514 transform: 'translateY(-20px)'
10300 }], this.timingFromConfig(config)); 10515 }], this.timingFromConfig(config));
10301 10516
10302 return this._effect; 10517 return this._effect;
10303 } 10518 }
10304 }); 10519 });
10305 (function() { 10520 // Copyright 2016 The Chromium Authors. All rights reserved.
10306 'use strict'; 10521 // Use of this source code is governed by a BSD-style license that can be
10307 10522 // found in the LICENSE file.
10308 var config = { 10523
10309 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', 10524 /** Same as paper-menu-button's custom easing cubic-bezier param. */
10310 MAX_ANIMATION_TIME_MS: 400 10525 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)';
10311 }; 10526
10312 10527 Polymer({
10313 var PaperMenuButton = Polymer({ 10528 is: 'cr-shared-menu',
10314 is: 'paper-menu-button', 10529
10315 10530 behaviors: [Polymer.IronA11yKeysBehavior],
10316 /** 10531
10317 * Fired when the dropdown opens. 10532 properties: {
10318 * 10533 menuOpen: {
10319 * @event paper-dropdown-open 10534 type: Boolean,
10320 */ 10535 observer: 'menuOpenChanged_',
10321 10536 value: false,
10322 /** 10537 },
10323 * Fired when the dropdown closes. 10538
10324 * 10539 /**
10325 * @event paper-dropdown-close 10540 * The contextual item that this menu was clicked for.
10326 */ 10541 * e.g. the data used to render an item in an <iron-list> or <dom-repeat>
10327 10542 * @type {?Object}
10328 behaviors: [ 10543 */
10329 Polymer.IronA11yKeysBehavior, 10544 itemData: {
10330 Polymer.IronControlState 10545 type: Object,
10331 ], 10546 value: null,
10332 10547 },
10333 properties: { 10548
10334 /** 10549 /** @override */
10335 * True if the content is currently displayed. 10550 keyEventTarget: {
10336 */ 10551 type: Object,
10337 opened: { 10552 value: function() {
10338 type: Boolean, 10553 return this.$.menu;
10339 value: false, 10554 }
10340 notify: true, 10555 },
10341 observer: '_openedChanged' 10556
10342 }, 10557 openAnimationConfig: {
10343 10558 type: Object,
10344 /** 10559 value: function() {
10345 * The orientation against which to align the menu dropdown 10560 return [{
10346 * horizontally relative to the dropdown trigger. 10561 name: 'fade-in-animation',
10347 */ 10562 timing: {
10348 horizontalAlign: { 10563 delay: 50,
10349 type: String, 10564 duration: 200
10350 value: 'left',
10351 reflectToAttribute: true
10352 },
10353
10354 /**
10355 * The orientation against which to align the menu dropdown
10356 * vertically relative to the dropdown trigger.
10357 */
10358 verticalAlign: {
10359 type: String,
10360 value: 'top',
10361 reflectToAttribute: true
10362 },
10363
10364 /**
10365 * If true, the `horizontalAlign` and `verticalAlign` properties will
10366 * be considered preferences instead of strict requirements when
10367 * positioning the dropdown and may be changed if doing so reduces
10368 * the area of the dropdown falling outside of `fitInto`.
10369 */
10370 dynamicAlign: {
10371 type: Boolean
10372 },
10373
10374 /**
10375 * A pixel value that will be added to the position calculated for the
10376 * given `horizontalAlign`. Use a negative value to offset to the
10377 * left, or a positive value to offset to the right.
10378 */
10379 horizontalOffset: {
10380 type: Number,
10381 value: 0,
10382 notify: true
10383 },
10384
10385 /**
10386 * A pixel value that will be added to the position calculated for the
10387 * given `verticalAlign`. Use a negative value to offset towards the
10388 * top, or a positive value to offset towards the bottom.
10389 */
10390 verticalOffset: {
10391 type: Number,
10392 value: 0,
10393 notify: true
10394 },
10395
10396 /**
10397 * If true, the dropdown will be positioned so that it doesn't overlap
10398 * the button.
10399 */
10400 noOverlap: {
10401 type: Boolean
10402 },
10403
10404 /**
10405 * Set to true to disable animations when opening and closing the
10406 * dropdown.
10407 */
10408 noAnimations: {
10409 type: Boolean,
10410 value: false
10411 },
10412
10413 /**
10414 * Set to true to disable automatically closing the dropdown after
10415 * a selection has been made.
10416 */
10417 ignoreSelect: {
10418 type: Boolean,
10419 value: false
10420 },
10421
10422 /**
10423 * Set to true to enable automatically closing the dropdown after an
10424 * item has been activated, even if the selection did not change.
10425 */
10426 closeOnActivate: {
10427 type: Boolean,
10428 value: false
10429 },
10430
10431 /**
10432 * An animation config. If provided, this will be used to animate the
10433 * opening of the dropdown.
10434 */
10435 openAnimationConfig: {
10436 type: Object,
10437 value: function() {
10438 return [{
10439 name: 'fade-in-animation',
10440 timing: {
10441 delay: 100,
10442 duration: 200
10443 }
10444 }, {
10445 name: 'paper-menu-grow-width-animation',
10446 timing: {
10447 delay: 100,
10448 duration: 150,
10449 easing: config.ANIMATION_CUBIC_BEZIER
10450 }
10451 }, {
10452 name: 'paper-menu-grow-height-animation',
10453 timing: {
10454 delay: 100,
10455 duration: 275,
10456 easing: config.ANIMATION_CUBIC_BEZIER
10457 }
10458 }];
10459 }
10460 },
10461
10462 /**
10463 * An animation config. If provided, this will be used to animate the
10464 * closing of the dropdown.
10465 */
10466 closeAnimationConfig: {
10467 type: Object,
10468 value: function() {
10469 return [{
10470 name: 'fade-out-animation',
10471 timing: {
10472 duration: 150
10473 }
10474 }, {
10475 name: 'paper-menu-shrink-width-animation',
10476 timing: {
10477 delay: 100,
10478 duration: 50,
10479 easing: config.ANIMATION_CUBIC_BEZIER
10480 }
10481 }, {
10482 name: 'paper-menu-shrink-height-animation',
10483 timing: {
10484 duration: 200,
10485 easing: 'ease-in'
10486 }
10487 }];
10488 }
10489 },
10490
10491 /**
10492 * By default, the dropdown will constrain scrolling on the page
10493 * to itself when opened.
10494 * Set to true in order to prevent scroll from being constrained
10495 * to the dropdown when it opens.
10496 */
10497 allowOutsideScroll: {
10498 type: Boolean,
10499 value: false
10500 },
10501
10502 /**
10503 * Whether focus should be restored to the button when the menu closes .
10504 */
10505 restoreFocusOnClose: {
10506 type: Boolean,
10507 value: true
10508 },
10509
10510 /**
10511 * This is the element intended to be bound as the focus target
10512 * for the `iron-dropdown` contained by `paper-menu-button`.
10513 */
10514 _dropdownContent: {
10515 type: Object
10516 } 10565 }
10517 }, 10566 }, {
10518 10567 name: 'paper-menu-grow-width-animation',
10519 hostAttributes: { 10568 timing: {
10520 role: 'group', 10569 delay: 50,
10521 'aria-haspopup': 'true' 10570 duration: 150,
10522 }, 10571 easing: SLIDE_CUBIC_BEZIER
10523
10524 listeners: {
10525 'iron-activate': '_onIronActivate',
10526 'iron-select': '_onIronSelect'
10527 },
10528
10529 /**
10530 * The content element that is contained by the menu button, if any.
10531 */
10532 get contentElement() {
10533 return Polymer.dom(this.$.content).getDistributedNodes()[0];
10534 },
10535
10536 /**
10537 * Toggles the drowpdown content between opened and closed.
10538 */
10539 toggle: function() {
10540 if (this.opened) {
10541 this.close();
10542 } else {
10543 this.open();
10544 } 10572 }
10545 }, 10573 }, {
10546 10574 name: 'paper-menu-grow-height-animation',
10547 /** 10575 timing: {
10548 * Make the dropdown content appear as an overlay positioned relative 10576 delay: 100,
10549 * to the dropdown trigger. 10577 duration: 275,
10550 */ 10578 easing: SLIDE_CUBIC_BEZIER
10551 open: function() {
10552 if (this.disabled) {
10553 return;
10554 } 10579 }
10555 10580 }];
10556 this.$.dropdown.open(); 10581 }
10557 }, 10582 },
10558 10583
10559 /** 10584 closeAnimationConfig: {
10560 * Hide the dropdown content. 10585 type: Object,
10561 */ 10586 value: function() {
10562 close: function() { 10587 return [{
10563 this.$.dropdown.close(); 10588 name: 'fade-out-animation',
10564 }, 10589 timing: {
10565 10590 duration: 150
10566 /**
10567 * When an `iron-select` event is received, the dropdown should
10568 * automatically close on the assumption that a value has been chosen.
10569 *
10570 * @param {CustomEvent} event A CustomEvent instance with type
10571 * set to `"iron-select"`.
10572 */
10573 _onIronSelect: function(event) {
10574 if (!this.ignoreSelect) {
10575 this.close();
10576 } 10591 }
10577 }, 10592 }];
10578 10593 }
10579 /** 10594 }
10580 * Closes the dropdown when an `iron-activate` event is received if 10595 },
10581 * `closeOnActivate` is true. 10596
10582 * 10597 keyBindings: {
10583 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. 10598 'tab': 'onTabPressed_',
10584 */ 10599 },
10585 _onIronActivate: function(event) { 10600
10586 if (this.closeOnActivate) { 10601 listeners: {
10587 this.close(); 10602 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_',
10588 } 10603 },
10589 }, 10604
10590 10605 /**
10591 /** 10606 * The last anchor that was used to open a menu. It's necessary for toggling.
10592 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. 10607 * @private {?Element}
10593 * When the dropdown closes, the `paper-menu-button` fires `paper-close` . 10608 */
10594 * 10609 lastAnchor_: null,
10595 * @param {boolean} opened True if the dropdown is opened, otherwise fal se. 10610
10596 * @param {boolean} oldOpened The previous value of `opened`. 10611 /**
10597 */ 10612 * The first focusable child in the menu's light DOM.
10598 _openedChanged: function(opened, oldOpened) { 10613 * @private {?Element}
10599 if (opened) { 10614 */
10600 // TODO(cdata): Update this when we can measure changes in distribut ed 10615 firstFocus_: null,
10601 // children in an idiomatic way. 10616
10602 // We poke this property in case the element has changed. This will 10617 /**
10603 // cause the focus target for the `iron-dropdown` to be updated as 10618 * The last focusable child in the menu's light DOM.
10604 // necessary: 10619 * @private {?Element}
10605 this._dropdownContent = this.contentElement; 10620 */
10606 this.fire('paper-dropdown-open'); 10621 lastFocus_: null,
10607 } else if (oldOpened != null) { 10622
10608 this.fire('paper-dropdown-close'); 10623 /** @override */
10609 } 10624 attached: function() {
10610 }, 10625 window.addEventListener('resize', this.closeMenu.bind(this));
10611 10626 },
10612 /** 10627
10613 * If the dropdown is open when disabled becomes true, close the 10628 /** Closes the menu. */
10614 * dropdown. 10629 closeMenu: function() {
10615 * 10630 if (this.root.activeElement == null) {
10616 * @param {boolean} disabled True if disabled, otherwise false. 10631 // Something else has taken focus away from the menu. Do not attempt to
10617 */ 10632 // restore focus to the button which opened the menu.
10618 _disabledChanged: function(disabled) { 10633 this.$.dropdown.restoreFocusOnClose = false;
10619 Polymer.IronControlState._disabledChanged.apply(this, arguments); 10634 }
10620 if (disabled && this.opened) { 10635 this.menuOpen = false;
10621 this.close(); 10636 },
10622 } 10637
10623 }, 10638 /**
10624 10639 * Opens the menu at the anchor location.
10625 __onIronOverlayCanceled: function(event) { 10640 * @param {!Element} anchor The location to display the menu.
10626 var uiEvent = event.detail; 10641 * @param {!Object} itemData The contextual item's data.
10627 var target = Polymer.dom(uiEvent).rootTarget; 10642 */
10628 var trigger = this.$.trigger; 10643 openMenu: function(anchor, itemData) {
10629 var path = Polymer.dom(uiEvent).path; 10644 if (this.lastAnchor_ == anchor && this.menuOpen)
10630 10645 return;
10631 if (path.indexOf(trigger) > -1) { 10646
10632 event.preventDefault(); 10647 if (this.menuOpen)
10633 } 10648 this.closeMenu();
10634 } 10649
10635 }); 10650 this.itemData = itemData;
10636 10651 this.lastAnchor_ = anchor;
10637 Object.keys(config).forEach(function (key) { 10652 this.$.dropdown.restoreFocusOnClose = true;
10638 PaperMenuButton[key] = config[key]; 10653
10639 }); 10654 var focusableChildren = Polymer.dom(this).querySelectorAll(
10640 10655 '[tabindex]:not([hidden]),button:not([hidden])');
10641 Polymer.PaperMenuButton = PaperMenuButton; 10656 if (focusableChildren.length > 0) {
10642 })(); 10657 this.$.dropdown.focusTarget = focusableChildren[0];
10643 /** 10658 this.firstFocus_ = focusableChildren[0];
10644 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus. 10659 this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
10645 * 10660 }
10646 * @polymerBehavior Polymer.PaperInkyFocusBehavior 10661
10647 */ 10662 // Move the menu to the anchor.
10648 Polymer.PaperInkyFocusBehaviorImpl = { 10663 this.$.dropdown.positionTarget = anchor;
10649 observers: [ 10664 this.menuOpen = true;
10650 '_focusedChanged(receivedFocusFromKeyboard)' 10665 },
10651 ], 10666
10652 10667 /**
10653 _focusedChanged: function(receivedFocusFromKeyboard) { 10668 * Toggles the menu for the anchor that is passed in.
10654 if (receivedFocusFromKeyboard) { 10669 * @param {!Element} anchor The location to display the menu.
10655 this.ensureRipple(); 10670 * @param {!Object} itemData The contextual item's data.
10656 } 10671 */
10657 if (this.hasRipple()) { 10672 toggleMenu: function(anchor, itemData) {
10658 this._ripple.holdDown = receivedFocusFromKeyboard; 10673 if (anchor == this.lastAnchor_ && this.menuOpen)
10659 } 10674 this.closeMenu();
10660 }, 10675 else
10661 10676 this.openMenu(anchor, itemData);
10662 _createRipple: function() { 10677 },
10663 var ripple = Polymer.PaperRippleBehavior._createRipple(); 10678
10664 ripple.id = 'ink'; 10679 /**
10665 ripple.setAttribute('center', ''); 10680 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
10666 ripple.classList.add('circle'); 10681 * the first element with a nonzero tabindex to the last such element.
10667 return ripple; 10682 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
10683 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
10684 * @param {CustomEvent} e
10685 */
10686 onTabPressed_: function(e) {
10687 if (!this.firstFocus_ || !this.lastFocus_)
10688 return;
10689
10690 var toFocus;
10691 var keyEvent = e.detail.keyboardEvent;
10692 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_)
10693 toFocus = this.lastFocus_;
10694 else if (keyEvent.target == this.lastFocus_)
10695 toFocus = this.firstFocus_;
10696
10697 if (!toFocus)
10698 return;
10699
10700 e.preventDefault();
10701 toFocus.focus();
10702 },
10703
10704 /**
10705 * Ensure the menu is reset properly when it is closed by the dropdown (eg,
10706 * clicking outside).
10707 * @private
10708 */
10709 menuOpenChanged_: function() {
10710 if (!this.menuOpen) {
10711 this.itemData = null;
10712 this.lastAnchor_ = null;
10713 }
10714 },
10715
10716 /**
10717 * Prevent focus restoring when tapping outside the menu. This stops the
10718 * focus moving around unexpectedly when closing the menu with the mouse.
10719 * @param {CustomEvent} e
10720 * @private
10721 */
10722 onOverlayCanceled_: function(e) {
10723 if (e.detail.type == 'tap')
10724 this.$.dropdown.restoreFocusOnClose = false;
10725 },
10726 });
10727 /** @polymerBehavior Polymer.PaperItemBehavior */
10728 Polymer.PaperItemBehaviorImpl = {
10729 hostAttributes: {
10730 role: 'option',
10731 tabindex: '0'
10668 } 10732 }
10669 }; 10733 };
10670 10734
10671 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ 10735 /** @polymerBehavior */
10672 Polymer.PaperInkyFocusBehavior = [ 10736 Polymer.PaperItemBehavior = [
10673 Polymer.IronButtonState, 10737 Polymer.IronButtonState,
10674 Polymer.IronControlState, 10738 Polymer.IronControlState,
10675 Polymer.PaperRippleBehavior, 10739 Polymer.PaperItemBehaviorImpl
10676 Polymer.PaperInkyFocusBehaviorImpl
10677 ]; 10740 ];
10678 Polymer({ 10741 Polymer({
10679 is: 'paper-icon-button', 10742 is: 'paper-item',
10743
10744 behaviors: [
10745 Polymer.PaperItemBehavior
10746 ]
10747 });
10748 Polymer({
10749
10750 is: 'iron-collapse',
10751
10752 behaviors: [
10753 Polymer.IronResizableBehavior
10754 ],
10755
10756 properties: {
10757
10758 /**
10759 * If true, the orientation is horizontal; otherwise is vertical.
10760 *
10761 * @attribute horizontal
10762 */
10763 horizontal: {
10764 type: Boolean,
10765 value: false,
10766 observer: '_horizontalChanged'
10767 },
10768
10769 /**
10770 * Set opened to true to show the collapse element and to false to hide it .
10771 *
10772 * @attribute opened
10773 */
10774 opened: {
10775 type: Boolean,
10776 value: false,
10777 notify: true,
10778 observer: '_openedChanged'
10779 },
10780
10781 /**
10782 * Set noAnimation to true to disable animations
10783 *
10784 * @attribute noAnimation
10785 */
10786 noAnimation: {
10787 type: Boolean
10788 },
10789
10790 },
10791
10792 get dimension() {
10793 return this.horizontal ? 'width' : 'height';
10794 },
10795
10796 /**
10797 * `maxWidth` or `maxHeight`.
10798 * @private
10799 */
10800 get _dimensionMax() {
10801 return this.horizontal ? 'maxWidth' : 'maxHeight';
10802 },
10803
10804 /**
10805 * `max-width` or `max-height`.
10806 * @private
10807 */
10808 get _dimensionMaxCss() {
10809 return this.horizontal ? 'max-width' : 'max-height';
10810 },
10811
10812 hostAttributes: {
10813 role: 'group',
10814 'aria-hidden': 'true',
10815 'aria-expanded': 'false'
10816 },
10817
10818 listeners: {
10819 transitionend: '_transitionEnd'
10820 },
10821
10822 attached: function() {
10823 // It will take care of setting correct classes and styles.
10824 this._transitionEnd();
10825 },
10826
10827 /**
10828 * Toggle the opened state.
10829 *
10830 * @method toggle
10831 */
10832 toggle: function() {
10833 this.opened = !this.opened;
10834 },
10835
10836 show: function() {
10837 this.opened = true;
10838 },
10839
10840 hide: function() {
10841 this.opened = false;
10842 },
10843
10844 /**
10845 * Updates the size of the element.
10846 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop erty value, usually `auto` or `0px`.
10847 * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
10848 */
10849 updateSize: function(size, animated) {
10850 // No change!
10851 var curSize = this.style[this._dimensionMax];
10852 if (curSize === size || (size === 'auto' && !curSize)) {
10853 return;
10854 }
10855
10856 this._updateTransition(false);
10857 // If we can animate, must do some prep work.
10858 if (animated && !this.noAnimation && this._isDisplayed) {
10859 // Animation will start at the current size.
10860 var startSize = this._calcSize();
10861 // For `auto` we must calculate what is the final size for the animation .
10862 // After the transition is done, _transitionEnd will set the size back t o `auto`.
10863 if (size === 'auto') {
10864 this.style[this._dimensionMax] = '';
10865 size = this._calcSize();
10866 }
10867 // Go to startSize without animation.
10868 this.style[this._dimensionMax] = startSize;
10869 // Force layout to ensure transition will go. Set scrollTop to itself
10870 // so that compilers won't remove it.
10871 this.scrollTop = this.scrollTop;
10872 // Enable animation.
10873 this._updateTransition(true);
10874 }
10875 // Set the final size.
10876 if (size === 'auto') {
10877 this.style[this._dimensionMax] = '';
10878 } else {
10879 this.style[this._dimensionMax] = size;
10880 }
10881 },
10882
10883 /**
10884 * enableTransition() is deprecated, but left over so it doesn't break exist ing code.
10885 * Please use `noAnimation` property instead.
10886 *
10887 * @method enableTransition
10888 * @deprecated since version 1.0.4
10889 */
10890 enableTransition: function(enabled) {
10891 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.');
10892 this.noAnimation = !enabled;
10893 },
10894
10895 _updateTransition: function(enabled) {
10896 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s' ;
10897 },
10898
10899 _horizontalChanged: function() {
10900 this.style.transitionProperty = this._dimensionMaxCss;
10901 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma xWidth';
10902 this.style[otherDimension] = '';
10903 this.updateSize(this.opened ? 'auto' : '0px', false);
10904 },
10905
10906 _openedChanged: function() {
10907 this.setAttribute('aria-expanded', this.opened);
10908 this.setAttribute('aria-hidden', !this.opened);
10909
10910 this.toggleClass('iron-collapse-closed', false);
10911 this.toggleClass('iron-collapse-opened', false);
10912 this.updateSize(this.opened ? 'auto' : '0px', true);
10913
10914 // Focus the current collapse.
10915 if (this.opened) {
10916 this.focus();
10917 }
10918 if (this.noAnimation) {
10919 this._transitionEnd();
10920 }
10921 },
10922
10923 _transitionEnd: function() {
10924 if (this.opened) {
10925 this.style[this._dimensionMax] = '';
10926 }
10927 this.toggleClass('iron-collapse-closed', !this.opened);
10928 this.toggleClass('iron-collapse-opened', this.opened);
10929 this._updateTransition(false);
10930 this.notifyResize();
10931 },
10932
10933 /**
10934 * Simplistic heuristic to detect if element has a parent with display: none
10935 *
10936 * @private
10937 */
10938 get _isDisplayed() {
10939 var rect = this.getBoundingClientRect();
10940 for (var prop in rect) {
10941 if (rect[prop] !== 0) return true;
10942 }
10943 return false;
10944 },
10945
10946 _calcSize: function() {
10947 return this.getBoundingClientRect()[this.dimension] + 'px';
10948 }
10949
10950 });
10951 /**
10952 Polymer.IronFormElementBehavior enables a custom element to be included
10953 in an `iron-form`.
10954
10955 @demo demo/index.html
10956 @polymerBehavior
10957 */
10958 Polymer.IronFormElementBehavior = {
10959
10960 properties: {
10961 /**
10962 * Fired when the element is added to an `iron-form`.
10963 *
10964 * @event iron-form-element-register
10965 */
10966
10967 /**
10968 * Fired when the element is removed from an `iron-form`.
10969 *
10970 * @event iron-form-element-unregister
10971 */
10972
10973 /**
10974 * The name of this element.
10975 */
10976 name: {
10977 type: String
10978 },
10979
10980 /**
10981 * The value for this element.
10982 */
10983 value: {
10984 notify: true,
10985 type: String
10986 },
10987
10988 /**
10989 * Set to true to mark the input as required. If used in a form, a
10990 * custom element that uses this behavior should also use
10991 * Polymer.IronValidatableBehavior and define a custom validation method.
10992 * Otherwise, a `required` element will always be considered valid.
10993 * It's also strongly recommended to provide a visual style for the elemen t
10994 * when its value is invalid.
10995 */
10996 required: {
10997 type: Boolean,
10998 value: false
10999 },
11000
11001 /**
11002 * The form that the element is registered to.
11003 */
11004 _parentForm: {
11005 type: Object
11006 }
11007 },
11008
11009 attached: function() {
11010 // Note: the iron-form that this element belongs to will set this
11011 // element's _parentForm property when handling this event.
11012 this.fire('iron-form-element-register');
11013 },
11014
11015 detached: function() {
11016 if (this._parentForm) {
11017 this._parentForm.fire('iron-form-element-unregister', {target: this});
11018 }
11019 }
11020
11021 };
11022 /**
11023 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element
11024 * that has a `checked` property, which can be used for validation if the
11025 * element is also `required`. Element instances implementing this behavior
11026 * will also be registered for use in an `iron-form` element.
11027 *
11028 * @demo demo/index.html
11029 * @polymerBehavior Polymer.IronCheckedElementBehavior
11030 */
11031 Polymer.IronCheckedElementBehaviorImpl = {
11032
11033 properties: {
11034 /**
11035 * Fired when the checked state changes.
11036 *
11037 * @event iron-change
11038 */
11039
11040 /**
11041 * Gets or sets the state, `true` is checked and `false` is unchecked.
11042 */
11043 checked: {
11044 type: Boolean,
11045 value: false,
11046 reflectToAttribute: true,
11047 notify: true,
11048 observer: '_checkedChanged'
11049 },
11050
11051 /**
11052 * If true, the button toggles the active state with each tap or press
11053 * of the spacebar.
11054 */
11055 toggles: {
11056 type: Boolean,
11057 value: true,
11058 reflectToAttribute: true
11059 },
11060
11061 /* Overriden from Polymer.IronFormElementBehavior */
11062 value: {
11063 type: String,
11064 value: 'on',
11065 observer: '_valueChanged'
11066 }
11067 },
11068
11069 observers: [
11070 '_requiredChanged(required)'
11071 ],
11072
11073 created: function() {
11074 // Used by `iron-form` to handle the case that an element with this behavi or
11075 // doesn't have a role of 'checkbox' or 'radio', but should still only be
11076 // included when the form is serialized if `this.checked === true`.
11077 this._hasIronCheckedElementBehavior = true;
11078 },
11079
11080 /**
11081 * Returns false if the element is required and not checked, and true otherw ise.
11082 * @param {*=} _value Ignored.
11083 * @return {boolean} true if `required` is false or if `checked` is true.
11084 */
11085 _getValidity: function(_value) {
11086 return this.disabled || !this.required || this.checked;
11087 },
11088
11089 /**
11090 * Update the aria-required label when `required` is changed.
11091 */
11092 _requiredChanged: function() {
11093 if (this.required) {
11094 this.setAttribute('aria-required', 'true');
11095 } else {
11096 this.removeAttribute('aria-required');
11097 }
11098 },
11099
11100 /**
11101 * Fire `iron-changed` when the checked state changes.
11102 */
11103 _checkedChanged: function() {
11104 this.active = this.checked;
11105 this.fire('iron-change');
11106 },
11107
11108 /**
11109 * Reset value to 'on' if it is set to `undefined`.
11110 */
11111 _valueChanged: function() {
11112 if (this.value === undefined || this.value === null) {
11113 this.value = 'on';
11114 }
11115 }
11116 };
11117
11118 /** @polymerBehavior Polymer.IronCheckedElementBehavior */
11119 Polymer.IronCheckedElementBehavior = [
11120 Polymer.IronFormElementBehavior,
11121 Polymer.IronValidatableBehavior,
11122 Polymer.IronCheckedElementBehaviorImpl
11123 ];
11124 /**
11125 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element
11126 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio r`
11127 * and is compatible with having a ripple effect.
11128 * @polymerBehavior Polymer.PaperCheckedElementBehavior
11129 */
11130 Polymer.PaperCheckedElementBehaviorImpl = {
11131 /**
11132 * Synchronizes the element's checked state with its ripple effect.
11133 */
11134 _checkedChanged: function() {
11135 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
11136 if (this.hasRipple()) {
11137 if (this.checked) {
11138 this._ripple.setAttribute('checked', '');
11139 } else {
11140 this._ripple.removeAttribute('checked');
11141 }
11142 }
11143 },
11144
11145 /**
11146 * Synchronizes the element's `active` and `checked` state.
11147 */
11148 _buttonStateChanged: function() {
11149 Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
11150 if (this.disabled) {
11151 return;
11152 }
11153 if (this.isAttached) {
11154 this.checked = this.active;
11155 }
11156 }
11157 };
11158
11159 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */
11160 Polymer.PaperCheckedElementBehavior = [
11161 Polymer.PaperInkyFocusBehavior,
11162 Polymer.IronCheckedElementBehavior,
11163 Polymer.PaperCheckedElementBehaviorImpl
11164 ];
11165 Polymer({
11166 is: 'paper-checkbox',
11167
11168 behaviors: [
11169 Polymer.PaperCheckedElementBehavior
11170 ],
10680 11171
10681 hostAttributes: { 11172 hostAttributes: {
10682 role: 'button', 11173 role: 'checkbox',
10683 tabindex: '0' 11174 'aria-checked': false,
10684 }, 11175 tabindex: 0
10685 11176 },
10686 behaviors: [
10687 Polymer.PaperInkyFocusBehavior
10688 ],
10689 11177
10690 properties: { 11178 properties: {
10691 /** 11179 /**
10692 * The URL of an image for the icon. If the src property is specified, 11180 * Fired when the checked state changes due to user interaction.
10693 * the icon property should not be. 11181 *
11182 * @event change
10694 */ 11183 */
10695 src: {
10696 type: String
10697 },
10698 11184
10699 /** 11185 /**
10700 * Specifies the icon name or index in the set of icons available in 11186 * Fired when the checked state changes.
10701 * the icon's icon set. If the icon property is specified, 11187 *
10702 * the src property should not be. 11188 * @event iron-change
10703 */ 11189 */
10704 icon: { 11190 ariaActiveAttribute: {
10705 type: String
10706 },
10707
10708 /**
10709 * Specifies the alternate text for the button, for accessibility.
10710 */
10711 alt: {
10712 type: String, 11191 type: String,
10713 observer: "_altChanged" 11192 value: 'aria-checked'
10714 } 11193 }
10715 }, 11194 },
10716 11195
10717 _altChanged: function(newValue, oldValue) { 11196 _computeCheckboxClass: function(checked, invalid) {
10718 var label = this.getAttribute('aria-label'); 11197 var className = '';
10719 11198 if (checked) {
10720 // Don't stomp over a user-set aria-label. 11199 className += 'checked ';
10721 if (!label || oldValue == label) { 11200 }
10722 this.setAttribute('aria-label', newValue); 11201 if (invalid) {
11202 className += 'invalid';
11203 }
11204 return className;
11205 },
11206
11207 _computeCheckmarkClass: function(checked) {
11208 return checked ? '' : 'hidden';
11209 },
11210
11211 // create ripple inside the checkboxContainer
11212 _createRipple: function() {
11213 this._rippleContainer = this.$.checkboxContainer;
11214 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
11215 }
11216
11217 });
11218 Polymer({
11219 is: 'paper-icon-button-light',
11220 extends: 'button',
11221
11222 behaviors: [
11223 Polymer.PaperRippleBehavior
11224 ],
11225
11226 listeners: {
11227 'down': '_rippleDown',
11228 'up': '_rippleUp',
11229 'focus': '_rippleDown',
11230 'blur': '_rippleUp',
11231 },
11232
11233 _rippleDown: function() {
11234 this.getRipple().downAction();
11235 },
11236
11237 _rippleUp: function() {
11238 this.getRipple().upAction();
11239 },
11240
11241 /**
11242 * @param {...*} var_args
11243 */
11244 ensureRipple: function(var_args) {
11245 var lastRipple = this._ripple;
11246 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
11247 if (this._ripple && this._ripple !== lastRipple) {
11248 this._ripple.center = true;
11249 this._ripple.classList.add('circle');
10723 } 11250 }
10724 } 11251 }
10725 }); 11252 });
10726 // Copyright 2016 The Chromium Authors. All rights reserved. 11253 // Copyright 2016 The Chromium Authors. All rights reserved.
10727 // Use of this source code is governed by a BSD-style license that can be 11254 // Use of this source code is governed by a BSD-style license that can be
10728 // found in the LICENSE file. 11255 // found in the LICENSE file.
10729 11256
11257 cr.define('cr.icon', function() {
11258 /**
11259 * @return {!Array<number>} The scale factors supported by this platform for
11260 * webui resources.
11261 */
11262 function getSupportedScaleFactors() {
11263 var supportedScaleFactors = [];
11264 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
11265 // All desktop platforms support zooming which also updates the
11266 // renderer's device scale factors (a.k.a devicePixelRatio), and
11267 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
11268 // image-set on these platforms so that the renderer can pick the
11269 // closest image for the current device scale factor.
11270 supportedScaleFactors.push(1);
11271 supportedScaleFactors.push(2);
11272 } else {
11273 // For other platforms that use fixed device scale factor, use
11274 // the window's device pixel ratio.
11275 // TODO(oshima): Investigate if Android/iOS need to use image-set.
11276 supportedScaleFactors.push(window.devicePixelRatio);
11277 }
11278 return supportedScaleFactors;
11279 }
11280
11281 /**
11282 * Returns the URL of the image, or an image set of URLs for the profile
11283 * avatar. Default avatars have resources available for multiple scalefactors,
11284 * whereas the GAIA profile image only comes in one size.
11285 *
11286 * @param {string} path The path of the image.
11287 * @return {string} The url, or an image set of URLs of the avatar image.
11288 */
11289 function getProfileAvatarIcon(path) {
11290 var chromeThemePath = 'chrome://theme';
11291 var isDefaultAvatar =
11292 (path.slice(0, chromeThemePath.length) == chromeThemePath);
11293 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path);
11294 }
11295
11296 /**
11297 * Generates a CSS -webkit-image-set for a chrome:// url.
11298 * An entry in the image set is added for each of getSupportedScaleFactors().
11299 * The scale-factor-specific url is generated by replacing the first instance
11300 * of 'scalefactor' in |path| with the numeric scale factor.
11301 * @param {string} path The URL to generate an image set for.
11302 * 'scalefactor' should be a substring of |path|.
11303 * @return {string} The CSS -webkit-image-set.
11304 */
11305 function imageset(path) {
11306 var supportedScaleFactors = getSupportedScaleFactors();
11307
11308 var replaceStartIndex = path.indexOf('scalefactor');
11309 if (replaceStartIndex < 0)
11310 return url(path);
11311
11312 var s = '';
11313 for (var i = 0; i < supportedScaleFactors.length; ++i) {
11314 var scaleFactor = supportedScaleFactors[i];
11315 var pathWithScaleFactor = path.substr(0, replaceStartIndex) +
11316 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
11317
11318 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
11319
11320 if (i != supportedScaleFactors.length - 1)
11321 s += ', ';
11322 }
11323 return '-webkit-image-set(' + s + ')';
11324 }
11325
11326 /**
11327 * A regular expression for identifying favicon URLs.
11328 * @const {!RegExp}
11329 */
11330 var FAVICON_URL_REGEX = /\.ico$/i;
11331
11332 /**
11333 * Creates a CSS -webkit-image-set for a favicon request.
11334 * @param {string} url Either the URL of the original page or of the favicon
11335 * itself.
11336 * @param {number=} opt_size Optional preferred size of the favicon.
11337 * @param {string=} opt_type Optional type of favicon to request. Valid values
11338 * are 'favicon' and 'touch-icon'. Default is 'favicon'.
11339 * @return {string} -webkit-image-set for the favicon.
11340 */
11341 function getFaviconImageSet(url, opt_size, opt_type) {
11342 var size = opt_size || 16;
11343 var type = opt_type || 'favicon';
11344
11345 return imageset(
11346 'chrome://' + type + '/size/' + size + '@scalefactorx/' +
11347 // Note: Literal 'iconurl' must match |kIconURLParameter| in
11348 // components/favicon_base/favicon_url_parser.cc.
11349 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
11350 }
11351
11352 return {
11353 getSupportedScaleFactors: getSupportedScaleFactors,
11354 getProfileAvatarIcon: getProfileAvatarIcon,
11355 getFaviconImageSet: getFaviconImageSet,
11356 };
11357 });
11358 // Copyright 2016 The Chromium Authors. All rights reserved.
11359 // Use of this source code is governed by a BSD-style license that can be
11360 // found in the LICENSE file.
11361
10730 /** 11362 /**
10731 * Implements an incremental search field which can be shown and hidden. 11363 * @fileoverview Defines a singleton object, md_history.BrowserService, which
10732 * Canonical implementation is <cr-search-field>. 11364 * provides access to chrome.send APIs.
10733 * @polymerBehavior
10734 */ 11365 */
10735 var CrSearchFieldBehavior = { 11366
11367 cr.define('md_history', function() {
11368 /** @constructor */
11369 function BrowserService() {
11370 /** @private {Array<!HistoryEntry>} */
11371 this.pendingDeleteItems_ = null;
11372 /** @private {PromiseResolver} */
11373 this.pendingDeletePromise_ = null;
11374 }
11375
11376 BrowserService.prototype = {
11377 /**
11378 * @param {!Array<!HistoryEntry>} items
11379 * @return {Promise<!Array<!HistoryEntry>>}
11380 */
11381 deleteItems: function(items) {
11382 if (this.pendingDeleteItems_ != null) {
11383 // There's already a deletion in progress, reject immediately.
11384 return new Promise(function(resolve, reject) { reject(items); });
11385 }
11386
11387 var removalList = items.map(function(item) {
11388 return {
11389 url: item.url,
11390 timestamps: item.allTimestamps
11391 };
11392 });
11393
11394 this.pendingDeleteItems_ = items;
11395 this.pendingDeletePromise_ = new PromiseResolver();
11396
11397 chrome.send('removeVisits', removalList);
11398
11399 return this.pendingDeletePromise_.promise;
11400 },
11401
11402 /**
11403 * @param {!string} url
11404 */
11405 removeBookmark: function(url) {
11406 chrome.send('removeBookmark', [url]);
11407 },
11408
11409 /**
11410 * @param {string} sessionTag
11411 */
11412 openForeignSessionAllTabs: function(sessionTag) {
11413 chrome.send('openForeignSession', [sessionTag]);
11414 },
11415
11416 /**
11417 * @param {string} sessionTag
11418 * @param {number} windowId
11419 * @param {number} tabId
11420 * @param {MouseEvent} e
11421 */
11422 openForeignSessionTab: function(sessionTag, windowId, tabId, e) {
11423 chrome.send('openForeignSession', [
11424 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey,
11425 e.ctrlKey, e.metaKey, e.shiftKey
11426 ]);
11427 },
11428
11429 /**
11430 * @param {string} sessionTag
11431 */
11432 deleteForeignSession: function(sessionTag) {
11433 chrome.send('deleteForeignSession', [sessionTag]);
11434 },
11435
11436 openClearBrowsingData: function() {
11437 chrome.send('clearBrowsingData');
11438 },
11439
11440 /**
11441 * @param {boolean} successful
11442 * @private
11443 */
11444 resolveDelete_: function(successful) {
11445 if (this.pendingDeleteItems_ == null ||
11446 this.pendingDeletePromise_ == null) {
11447 return;
11448 }
11449
11450 if (successful)
11451 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_);
11452 else
11453 this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
11454
11455 this.pendingDeleteItems_ = null;
11456 this.pendingDeletePromise_ = null;
11457 },
11458 };
11459
11460 cr.addSingletonGetter(BrowserService);
11461
11462 return {BrowserService: BrowserService};
11463 });
11464
11465 /**
11466 * Called by the history backend when deletion was succesful.
11467 */
11468 function deleteComplete() {
11469 md_history.BrowserService.getInstance().resolveDelete_(true);
11470 }
11471
11472 /**
11473 * Called by the history backend when the deletion failed.
11474 */
11475 function deleteFailed() {
11476 md_history.BrowserService.getInstance().resolveDelete_(false);
11477 };
11478 // Copyright 2016 The Chromium Authors. All rights reserved.
11479 // Use of this source code is governed by a BSD-style license that can be
11480 // found in the LICENSE file.
11481
11482 Polymer({
11483 is: 'history-searched-label',
11484
10736 properties: { 11485 properties: {
10737 label: { 11486 // The text to show in this label.
10738 type: String, 11487 title: String,
10739 value: '', 11488
10740 }, 11489 // The search term to bold within the title.
10741 11490 searchTerm: String,
10742 clearLabel: { 11491 },
10743 type: String, 11492
10744 value: '', 11493 observers: ['setSearchedTextToBold_(title, searchTerm)'],
10745 }, 11494
10746 11495 /**
10747 showingSearch: { 11496 * Updates the page title. If a search term is specified, highlights any
10748 type: Boolean, 11497 * occurrences of the search term in bold.
10749 value: false,
10750 notify: true,
10751 observer: 'showingSearchChanged_',
10752 reflectToAttribute: true
10753 },
10754
10755 /** @private */
10756 lastValue_: {
10757 type: String,
10758 value: '',
10759 },
10760 },
10761
10762 /**
10763 * @abstract
10764 * @return {!HTMLInputElement} The input field element the behavior should
10765 * use.
10766 */
10767 getSearchInput: function() {},
10768
10769 /**
10770 * @return {string} The value of the search field.
10771 */
10772 getValue: function() {
10773 return this.getSearchInput().value;
10774 },
10775
10776 /**
10777 * Sets the value of the search field.
10778 * @param {string} value
10779 */
10780 setValue: function(value) {
10781 // Use bindValue when setting the input value so that changes propagate
10782 // correctly.
10783 this.getSearchInput().bindValue = value;
10784 this.onValueChanged_(value);
10785 },
10786
10787 showAndFocus: function() {
10788 this.showingSearch = true;
10789 this.focus_();
10790 },
10791
10792 /** @private */
10793 focus_: function() {
10794 this.getSearchInput().focus();
10795 },
10796
10797 onSearchTermSearch: function() {
10798 this.onValueChanged_(this.getValue());
10799 },
10800
10801 /**
10802 * Updates the internal state of the search field based on a change that has
10803 * already happened.
10804 * @param {string} newValue
10805 * @private 11498 * @private
10806 */ 11499 */
10807 onValueChanged_: function(newValue) { 11500 setSearchedTextToBold_: function() {
10808 if (newValue == this.lastValue_) 11501 var i = 0;
10809 return; 11502 var titleElem = this.$.container;
10810 11503 var titleText = this.title;
10811 this.fire('search-changed', newValue); 11504
10812 this.lastValue_ = newValue; 11505 if (this.searchTerm == '' || this.searchTerm == null) {
10813 }, 11506 titleElem.textContent = titleText;
10814
10815 onSearchTermKeydown: function(e) {
10816 if (e.key == 'Escape')
10817 this.showingSearch = false;
10818 },
10819
10820 /** @private */
10821 showingSearchChanged_: function() {
10822 if (this.showingSearch) {
10823 this.focus_();
10824 return; 11507 return;
10825 } 11508 }
10826 11509
10827 this.setValue(''); 11510 var re = new RegExp(quoteString(this.searchTerm), 'gim');
10828 this.getSearchInput().blur(); 11511 var match;
10829 } 11512 titleElem.textContent = '';
11513 while (match = re.exec(titleText)) {
11514 if (match.index > i)
11515 titleElem.appendChild(document.createTextNode(
11516 titleText.slice(i, match.index)));
11517 i = re.lastIndex;
11518 // Mark the highlighted text in bold.
11519 var b = document.createElement('b');
11520 b.textContent = titleText.substring(match.index, i);
11521 titleElem.appendChild(b);
11522 }
11523 if (i < titleText.length)
11524 titleElem.appendChild(
11525 document.createTextNode(titleText.slice(i)));
11526 },
11527 });
11528 // Copyright 2015 The Chromium Authors. All rights reserved.
11529 // Use of this source code is governed by a BSD-style license that can be
11530 // found in the LICENSE file.
11531
11532 cr.define('md_history', function() {
11533 var HistoryItem = Polymer({
11534 is: 'history-item',
11535
11536 properties: {
11537 // Underlying HistoryEntry data for this item. Contains read-only fields
11538 // from the history backend, as well as fields computed by history-list.
11539 item: {type: Object, observer: 'showIcon_'},
11540
11541 // Search term used to obtain this history-item.
11542 searchTerm: {type: String},
11543
11544 selected: {type: Boolean, notify: true},
11545
11546 isFirstItem: {type: Boolean, reflectToAttribute: true},
11547
11548 isCardStart: {type: Boolean, reflectToAttribute: true},
11549
11550 isCardEnd: {type: Boolean, reflectToAttribute: true},
11551
11552 // True if the item is being displayed embedded in another element and
11553 // should not manage its own borders or size.
11554 embedded: {type: Boolean, reflectToAttribute: true},
11555
11556 hasTimeGap: {type: Boolean},
11557
11558 numberOfItems: {type: Number},
11559
11560 // The path of this history item inside its parent.
11561 path: String,
11562 },
11563
11564 /**
11565 * When a history-item is selected the toolbar is notified and increases
11566 * or decreases its count of selected items accordingly.
11567 * @private
11568 */
11569 onCheckboxSelected_: function() {
11570 // TODO(calamity): Fire this event whenever |selected| changes.
11571 this.fire('history-checkbox-select', {
11572 element: this,
11573 countAddition: this.$.checkbox.checked ? 1 : -1
11574 });
11575 },
11576
11577 /**
11578 * Remove bookmark of current item when bookmark-star is clicked.
11579 * @private
11580 */
11581 onRemoveBookmarkTap_: function() {
11582 if (!this.item.starred)
11583 return;
11584
11585 if (this.$$('#bookmark-star') == this.root.activeElement)
11586 this.$['menu-button'].focus();
11587
11588 md_history.BrowserService.getInstance()
11589 .removeBookmark(this.item.url);
11590 this.fire('remove-bookmark-stars', this.item.url);
11591 },
11592
11593 /**
11594 * Fires a custom event when the menu button is clicked. Sends the details
11595 * of the history item and where the menu should appear.
11596 */
11597 onMenuButtonTap_: function(e) {
11598 this.fire('toggle-menu', {
11599 target: Polymer.dom(e).localTarget,
11600 item: this.item,
11601 });
11602
11603 // Stops the 'tap' event from closing the menu when it opens.
11604 e.stopPropagation();
11605 },
11606
11607 /**
11608 * Set the favicon image, based on the URL of the history item.
11609 * @private
11610 */
11611 showIcon_: function() {
11612 this.$.icon.style.backgroundImage =
11613 cr.icon.getFaviconImageSet(this.item.url);
11614 },
11615
11616 selectionNotAllowed_: function() {
11617 return !loadTimeData.getBoolean('allowDeletingHistory');
11618 },
11619
11620 /**
11621 * Generates the title for this history card.
11622 * @param {number} numberOfItems The number of items in the card.
11623 * @param {string} search The search term associated with these results.
11624 * @private
11625 */
11626 cardTitle_: function(numberOfItems, historyDate, search) {
11627 if (!search)
11628 return this.item.dateRelativeDay;
11629
11630 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults';
11631 return loadTimeData.getStringF('foundSearchResults', numberOfItems,
11632 loadTimeData.getString(resultId), search);
11633 },
11634
11635 /**
11636 * Crop long item titles to reduce their effect on layout performance. See
11637 * crbug.com/621347.
11638 * @param {string} title
11639 * @return {string}
11640 */
11641 cropItemTitle_: function(title) {
11642 return (title.length > TITLE_MAX_LENGTH) ?
11643 title.substr(0, TITLE_MAX_LENGTH) :
11644 title;
11645 }
11646 });
11647
11648 /**
11649 * Check whether the time difference between the given history item and the
11650 * next one is large enough for a spacer to be required.
11651 * @param {Array<HistoryEntry>} visits
11652 * @param {number} currentIndex
11653 * @param {string} searchedTerm
11654 * @return {boolean} Whether or not time gap separator is required.
11655 * @private
11656 */
11657 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) {
11658 if (currentIndex >= visits.length - 1 || visits.length == 0)
11659 return false;
11660
11661 var currentItem = visits[currentIndex];
11662 var nextItem = visits[currentIndex + 1];
11663
11664 if (searchedTerm)
11665 return currentItem.dateShort != nextItem.dateShort;
11666
11667 return currentItem.time - nextItem.time > BROWSING_GAP_TIME &&
11668 currentItem.dateRelativeDay == nextItem.dateRelativeDay;
11669 };
11670
11671 return { HistoryItem: HistoryItem };
11672 });
11673 // Copyright 2016 The Chromium Authors. All rights reserved.
11674 // Use of this source code is governed by a BSD-style license that can be
11675 // found in the LICENSE file.
11676
11677 /**
11678 * @constructor
11679 * @param {string} currentPath
11680 */
11681 var SelectionTreeNode = function(currentPath) {
11682 /** @type {string} */
11683 this.currentPath = currentPath;
11684 /** @type {boolean} */
11685 this.leaf = false;
11686 /** @type {Array<number>} */
11687 this.indexes = [];
11688 /** @type {Array<SelectionTreeNode>} */
11689 this.children = [];
10830 }; 11690 };
11691
11692 /**
11693 * @param {number} index
11694 * @param {string} path
11695 */
11696 SelectionTreeNode.prototype.addChild = function(index, path) {
11697 this.indexes.push(index);
11698 this.children[index] = new SelectionTreeNode(path);
11699 };
11700
11701 /** @polymerBehavior */
11702 var HistoryListBehavior = {
11703 properties: {
11704 /**
11705 * Polymer paths to the history items contained in this list.
11706 * @type {Array<string>} selectedPaths
11707 */
11708 selectedPaths: {
11709 type: Array,
11710 value: /** @return {Array<string>} */ function() { return []; }
11711 },
11712 },
11713
11714 listeners: {
11715 'history-checkbox-select': 'itemSelected_',
11716 },
11717
11718 /**
11719 * @param {number} historyDataLength
11720 * @return {boolean}
11721 * @private
11722 */
11723 hasResults: function(historyDataLength) { return historyDataLength > 0; },
11724
11725 /**
11726 * @param {string} searchedTerm
11727 * @param {boolean} isLoading
11728 * @return {string}
11729 * @private
11730 */
11731 noResultsMessage: function(searchedTerm, isLoading) {
11732 if (isLoading)
11733 return '';
11734
11735 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
11736 return loadTimeData.getString(messageId);
11737 },
11738
11739 /**
11740 * Deselect each item in |selectedPaths|.
11741 */
11742 unselectAllItems: function() {
11743 this.selectedPaths.forEach(function(path) {
11744 this.set(path + '.selected', false);
11745 }.bind(this));
11746
11747 this.selectedPaths = [];
11748 },
11749
11750 /**
11751 * Performs a request to the backend to delete all selected items. If
11752 * successful, removes them from the view. Does not prompt the user before
11753 * deleting -- see <history-list-container> for a version of this method which
11754 * does prompt.
11755 */
11756 deleteSelected: function() {
11757 var toBeRemoved = this.selectedPaths.map(function(path) {
11758 return this.get(path);
11759 }.bind(this));
11760 md_history.BrowserService.getInstance()
11761 .deleteItems(toBeRemoved)
11762 .then(function() {
11763 this.removeItemsByPath(this.selectedPaths);
11764 this.fire('unselect-all');
11765 }.bind(this));
11766 },
11767
11768 /**
11769 * Removes the history items in |paths|. Assumes paths are of a.0.b.0...
11770 * structure.
11771 *
11772 * We want to use notifySplices to update the arrays for performance reasons
11773 * which requires manually batching and sending the notifySplices for each
11774 * level. To do this, we build a tree where each node is an array and then
11775 * depth traverse it to remove items. Each time a node has all children
11776 * deleted, we can also remove the node.
11777 *
11778 * @param {Array<string>} paths
11779 * @private
11780 */
11781 removeItemsByPath: function(paths) {
11782 if (paths.length == 0)
11783 return;
11784
11785 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths));
11786 },
11787
11788 /**
11789 * Creates the tree to traverse in order to remove |paths| from this list.
11790 * Assumes paths are of a.0.b.0...
11791 * structure.
11792 *
11793 * @param {Array<string>} paths
11794 * @return {SelectionTreeNode}
11795 * @private
11796 */
11797 buildRemovalTree_: function(paths) {
11798 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]);
11799
11800 // Build a tree to each history item specified in |paths|.
11801 paths.forEach(function(path) {
11802 var components = path.split('.');
11803 var node = rootNode;
11804 components.shift();
11805 while (components.length > 1) {
11806 var index = Number(components.shift());
11807 var arrayName = components.shift();
11808
11809 if (!node.children[index])
11810 node.addChild(index, [node.currentPath, index, arrayName].join('.'));
11811
11812 node = node.children[index];
11813 }
11814 node.leaf = true;
11815 node.indexes.push(Number(components.shift()));
11816 });
11817
11818 return rootNode;
11819 },
11820
11821 /**
11822 * Removes the history items underneath |node| and deletes container arrays as
11823 * they become empty.
11824 * @param {SelectionTreeNode} node
11825 * @return {boolean} Whether this node's array should be deleted.
11826 * @private
11827 */
11828 removeItemsBeneathNode_: function(node) {
11829 var array = this.get(node.currentPath);
11830 var splices = [];
11831
11832 node.indexes.sort(function(a, b) { return b - a; });
11833 node.indexes.forEach(function(index) {
11834 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) {
11835 var item = array.splice(index, 1);
11836 splices.push({
11837 index: index,
11838 removed: [item],
11839 addedCount: 0,
11840 object: array,
11841 type: 'splice'
11842 });
11843 }
11844 }.bind(this));
11845
11846 if (array.length == 0)
11847 return true;
11848
11849 // notifySplices gives better performance than individually splicing as it
11850 // batches all of the updates together.
11851 this.notifySplices(node.currentPath, splices);
11852 return false;
11853 },
11854
11855 /**
11856 * @param {Event} e
11857 * @private
11858 */
11859 itemSelected_: function(e) {
11860 var item = e.detail.element;
11861 var path = item.path;
11862 if (item.selected) {
11863 this.push('selectedPaths', path);
11864 return;
11865 }
11866
11867 var index = this.selectedPaths.indexOf(path);
11868 if (index != -1)
11869 this.splice('selectedPaths', index, 1);
11870 },
11871 };
11872 // Copyright 2016 The Chromium Authors. All rights reserved.
11873 // Use of this source code is governed by a BSD-style license that can be
11874 // found in the LICENSE file.
11875
11876 /**
11877 * @typedef {{domain: string,
11878 * visits: !Array<HistoryEntry>,
11879 * rendered: boolean,
11880 * expanded: boolean}}
11881 */
11882 var HistoryDomain;
11883
11884 /**
11885 * @typedef {{title: string,
11886 * domains: !Array<HistoryDomain>}}
11887 */
11888 var HistoryGroup;
11889
11890 Polymer({
11891 is: 'history-grouped-list',
11892
11893 behaviors: [HistoryListBehavior],
11894
11895 properties: {
11896 // An array of history entries in reverse chronological order.
11897 historyData: {
11898 type: Array,
11899 },
11900
11901 /**
11902 * @type {Array<HistoryGroup>}
11903 */
11904 groupedHistoryData_: {
11905 type: Array,
11906 },
11907
11908 searchedTerm: {
11909 type: String,
11910 value: ''
11911 },
11912
11913 range: {
11914 type: Number,
11915 },
11916
11917 queryStartTime: String,
11918 queryEndTime: String,
11919 },
11920
11921 observers: [
11922 'updateGroupedHistoryData_(range, historyData)'
11923 ],
11924
11925 /**
11926 * Make a list of domains from visits.
11927 * @param {!Array<!HistoryEntry>} visits
11928 * @return {!Array<!HistoryDomain>}
11929 */
11930 createHistoryDomains_: function(visits) {
11931 var domainIndexes = {};
11932 var domains = [];
11933
11934 // Group the visits into a dictionary and generate a list of domains.
11935 for (var i = 0, visit; visit = visits[i]; i++) {
11936 var domain = visit.domain;
11937 if (domainIndexes[domain] == undefined) {
11938 domainIndexes[domain] = domains.length;
11939 domains.push({
11940 domain: domain,
11941 visits: [],
11942 expanded: false,
11943 rendered: false,
11944 });
11945 }
11946 domains[domainIndexes[domain]].visits.push(visit);
11947 }
11948 var sortByVisits = function(a, b) {
11949 return b.visits.length - a.visits.length;
11950 };
11951 domains.sort(sortByVisits);
11952
11953 return domains;
11954 },
11955
11956 updateGroupedHistoryData_: function() {
11957 if (this.historyData.length == 0) {
11958 this.groupedHistoryData_ = [];
11959 return;
11960 }
11961
11962 if (this.range == HistoryRange.WEEK) {
11963 // Group each day into a list of results.
11964 var days = [];
11965 var currentDayVisits = [this.historyData[0]];
11966
11967 var pushCurrentDay = function() {
11968 days.push({
11969 title: this.searchedTerm ? currentDayVisits[0].dateShort :
11970 currentDayVisits[0].dateRelativeDay,
11971 domains: this.createHistoryDomains_(currentDayVisits),
11972 });
11973 }.bind(this);
11974
11975 var visitsSameDay = function(a, b) {
11976 if (this.searchedTerm)
11977 return a.dateShort == b.dateShort;
11978
11979 return a.dateRelativeDay == b.dateRelativeDay;
11980 }.bind(this);
11981
11982 for (var i = 1; i < this.historyData.length; i++) {
11983 var visit = this.historyData[i];
11984 if (!visitsSameDay(visit, currentDayVisits[0])) {
11985 pushCurrentDay();
11986 currentDayVisits = [];
11987 }
11988 currentDayVisits.push(visit);
11989 }
11990 pushCurrentDay();
11991
11992 this.groupedHistoryData_ = days;
11993 } else if (this.range == HistoryRange.MONTH) {
11994 // Group each all visits into a single list.
11995 this.groupedHistoryData_ = [{
11996 title: this.queryStartTime + ' – ' + this.queryEndTime,
11997 domains: this.createHistoryDomains_(this.historyData)
11998 }];
11999 }
12000 },
12001
12002 /**
12003 * @param {{model:Object, currentTarget:IronCollapseElement}} e
12004 */
12005 toggleDomainExpanded_: function(e) {
12006 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse');
12007 e.model.set('domain.rendered', true);
12008
12009 // Give the history-items time to render.
12010 setTimeout(function() { collapse.toggle() }, 0);
12011 },
12012
12013 /**
12014 * Check whether the time difference between the given history item and the
12015 * next one is large enough for a spacer to be required.
12016 * @param {number} groupIndex
12017 * @param {number} domainIndex
12018 * @param {number} itemIndex
12019 * @return {boolean} Whether or not time gap separator is required.
12020 * @private
12021 */
12022 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) {
12023 var visits =
12024 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits;
12025
12026 return md_history.HistoryItem.needsTimeGap(
12027 visits, itemIndex, this.searchedTerm);
12028 },
12029
12030 /**
12031 * @param {number} groupIndex
12032 * @param {number} domainIndex
12033 * @param {number} itemIndex
12034 * @return {string}
12035 * @private
12036 */
12037 pathForItem_: function(groupIndex, domainIndex, itemIndex) {
12038 return [
12039 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits',
12040 itemIndex
12041 ].join('.');
12042 },
12043
12044 /**
12045 * @param {HistoryDomain} domain
12046 * @return {string}
12047 * @private
12048 */
12049 getWebsiteIconStyle_: function(domain) {
12050 return 'background-image: ' +
12051 cr.icon.getFaviconImageSet(domain.visits[0].url);
12052 },
12053
12054 /**
12055 * @param {boolean} expanded
12056 * @return {string}
12057 * @private
12058 */
12059 getDropdownIcon_: function(expanded) {
12060 return expanded ? 'cr:expand-less' : 'cr:expand-more';
12061 },
12062 });
12063 /**
12064 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
12065 * designated scroll target.
12066 *
12067 * Elements that consume this behavior can override the `_scrollHandler`
12068 * method to add logic on the scroll event.
12069 *
12070 * @demo demo/scrolling-region.html Scrolling Region
12071 * @demo demo/document.html Document Element
12072 * @polymerBehavior
12073 */
12074 Polymer.IronScrollTargetBehavior = {
12075
12076 properties: {
12077
12078 /**
12079 * Specifies the element that will handle the scroll event
12080 * on the behalf of the current element. This is typically a reference to an element,
12081 * but there are a few more posibilities:
12082 *
12083 * ### Elements id
12084 *
12085 *```html
12086 * <div id="scrollable-element" style="overflow: auto;">
12087 * <x-element scroll-target="scrollable-element">
12088 * \x3c!-- Content--\x3e
12089 * </x-element>
12090 * </div>
12091 *```
12092 * In this case, the `scrollTarget` will point to the outer div element.
12093 *
12094 * ### Document scrolling
12095 *
12096 * For document scrolling, you can use the reserved word `document`:
12097 *
12098 *```html
12099 * <x-element scroll-target="document">
12100 * \x3c!-- Content --\x3e
12101 * </x-element>
12102 *```
12103 *
12104 * ### Elements reference
12105 *
12106 *```js
12107 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
12108 *```
12109 *
12110 * @type {HTMLElement}
12111 */
12112 scrollTarget: {
12113 type: HTMLElement,
12114 value: function() {
12115 return this._defaultScrollTarget;
12116 }
12117 }
12118 },
12119
12120 observers: [
12121 '_scrollTargetChanged(scrollTarget, isAttached)'
12122 ],
12123
12124 _scrollTargetChanged: function(scrollTarget, isAttached) {
12125 var eventTarget;
12126
12127 if (this._oldScrollTarget) {
12128 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
12129 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
12130 this._oldScrollTarget = null;
12131 }
12132
12133 if (!isAttached) {
12134 return;
12135 }
12136 // Support element id references
12137 if (scrollTarget === 'document') {
12138
12139 this.scrollTarget = this._doc;
12140
12141 } else if (typeof scrollTarget === 'string') {
12142
12143 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
12144 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
12145
12146 } else if (this._isValidScrollTarget()) {
12147
12148 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
12149 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
12150 this._oldScrollTarget = scrollTarget;
12151
12152 eventTarget.addEventListener('scroll', this._boundScrollHandler);
12153 }
12154 },
12155
12156 /**
12157 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
12158 *
12159 * @protected
12160 */
12161 _scrollHandler: function scrollHandler() {},
12162
12163 /**
12164 * The default scroll target. Consumers of this behavior may want to customi ze
12165 * the default scroll target.
12166 *
12167 * @type {Element}
12168 */
12169 get _defaultScrollTarget() {
12170 return this._doc;
12171 },
12172
12173 /**
12174 * Shortcut for the document element
12175 *
12176 * @type {Element}
12177 */
12178 get _doc() {
12179 return this.ownerDocument.documentElement;
12180 },
12181
12182 /**
12183 * Gets the number of pixels that the content of an element is scrolled upwa rd.
12184 *
12185 * @type {number}
12186 */
12187 get _scrollTop() {
12188 if (this._isValidScrollTarget()) {
12189 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
12190 }
12191 return 0;
12192 },
12193
12194 /**
12195 * Gets the number of pixels that the content of an element is scrolled to t he left.
12196 *
12197 * @type {number}
12198 */
12199 get _scrollLeft() {
12200 if (this._isValidScrollTarget()) {
12201 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
12202 }
12203 return 0;
12204 },
12205
12206 /**
12207 * Sets the number of pixels that the content of an element is scrolled upwa rd.
12208 *
12209 * @type {number}
12210 */
12211 set _scrollTop(top) {
12212 if (this.scrollTarget === this._doc) {
12213 window.scrollTo(window.pageXOffset, top);
12214 } else if (this._isValidScrollTarget()) {
12215 this.scrollTarget.scrollTop = top;
12216 }
12217 },
12218
12219 /**
12220 * Sets the number of pixels that the content of an element is scrolled to t he left.
12221 *
12222 * @type {number}
12223 */
12224 set _scrollLeft(left) {
12225 if (this.scrollTarget === this._doc) {
12226 window.scrollTo(left, window.pageYOffset);
12227 } else if (this._isValidScrollTarget()) {
12228 this.scrollTarget.scrollLeft = left;
12229 }
12230 },
12231
12232 /**
12233 * Scrolls the content to a particular place.
12234 *
12235 * @method scroll
12236 * @param {number} left The left position
12237 * @param {number} top The top position
12238 */
12239 scroll: function(left, top) {
12240 if (this.scrollTarget === this._doc) {
12241 window.scrollTo(left, top);
12242 } else if (this._isValidScrollTarget()) {
12243 this.scrollTarget.scrollLeft = left;
12244 this.scrollTarget.scrollTop = top;
12245 }
12246 },
12247
12248 /**
12249 * Gets the width of the scroll target.
12250 *
12251 * @type {number}
12252 */
12253 get _scrollTargetWidth() {
12254 if (this._isValidScrollTarget()) {
12255 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
12256 }
12257 return 0;
12258 },
12259
12260 /**
12261 * Gets the height of the scroll target.
12262 *
12263 * @type {number}
12264 */
12265 get _scrollTargetHeight() {
12266 if (this._isValidScrollTarget()) {
12267 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
12268 }
12269 return 0;
12270 },
12271
12272 /**
12273 * Returns true if the scroll target is a valid HTMLElement.
12274 *
12275 * @return {boolean}
12276 */
12277 _isValidScrollTarget: function() {
12278 return this.scrollTarget instanceof HTMLElement;
12279 }
12280 };
10831 (function() { 12281 (function() {
10832 'use strict'; 12282
10833 12283 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
10834 Polymer.IronA11yAnnouncer = Polymer({ 12284 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
10835 is: 'iron-a11y-announcer', 12285 var DEFAULT_PHYSICAL_COUNT = 3;
10836 12286 var HIDDEN_Y = '-10000px';
10837 properties: { 12287 var DEFAULT_GRID_SIZE = 200;
10838 12288 var SECRET_TABINDEX = -100;
10839 /** 12289
10840 * The value of mode is used to set the `aria-live` attribute 12290 Polymer({
10841 * for the element that will be announced. Valid values are: `off`, 12291
10842 * `polite` and `assertive`. 12292 is: 'iron-list',
10843 */
10844 mode: {
10845 type: String,
10846 value: 'polite'
10847 },
10848
10849 _text: {
10850 type: String,
10851 value: ''
10852 }
10853 },
10854
10855 created: function() {
10856 if (!Polymer.IronA11yAnnouncer.instance) {
10857 Polymer.IronA11yAnnouncer.instance = this;
10858 }
10859
10860 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
10861 },
10862
10863 /**
10864 * Cause a text string to be announced by screen readers.
10865 *
10866 * @param {string} text The text that should be announced.
10867 */
10868 announce: function(text) {
10869 this._text = '';
10870 this.async(function() {
10871 this._text = text;
10872 }, 100);
10873 },
10874
10875 _onIronAnnounce: function(event) {
10876 if (event.detail && event.detail.text) {
10877 this.announce(event.detail.text);
10878 }
10879 }
10880 });
10881
10882 Polymer.IronA11yAnnouncer.instance = null;
10883
10884 Polymer.IronA11yAnnouncer.requestAvailability = function() {
10885 if (!Polymer.IronA11yAnnouncer.instance) {
10886 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
10887 }
10888
10889 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
10890 };
10891 })();
10892 /**
10893 * Singleton IronMeta instance.
10894 */
10895 Polymer.IronValidatableBehaviorMeta = null;
10896
10897 /**
10898 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
10899 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
10900 *
10901 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
10902 * To validate a form imperatively, call the form's `validate()` method, which in turn will
10903 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
10904 * custom element will get a public `validate()`, which
10905 * will return the validity of the element, and a corresponding `invalid` attr ibute,
10906 * which can be used for styling.
10907 *
10908 * To implement the custom validation logic of your element, you must override
10909 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
10910 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
10911 * for an example.
10912 *
10913 * ### Accessibility
10914 *
10915 * Changing the `invalid` property, either manually or by calling `validate()` will update the
10916 * `aria-invalid` attribute.
10917 *
10918 * @demo demo/index.html
10919 * @polymerBehavior
10920 */
10921 Polymer.IronValidatableBehavior = {
10922 12293
10923 properties: { 12294 properties: {
10924 12295
10925 /** 12296 /**
10926 * Name of the validator to use. 12297 * An array containing items determining how many instances of the templat e
12298 * to stamp and that that each template instance should bind to.
10927 */ 12299 */
10928 validator: { 12300 items: {
10929 type: String 12301 type: Array
10930 }, 12302 },
10931 12303
10932 /** 12304 /**
10933 * True if the last call to `validate` is invalid. 12305 * The max count of physical items the pool can extend to.
10934 */ 12306 */
10935 invalid: { 12307 maxPhysicalCount: {
10936 notify: true, 12308 type: Number,
10937 reflectToAttribute: true, 12309 value: 500
12310 },
12311
12312 /**
12313 * The name of the variable to add to the binding scope for the array
12314 * element associated with a given template instance.
12315 */
12316 as: {
12317 type: String,
12318 value: 'item'
12319 },
12320
12321 /**
12322 * The name of the variable to add to the binding scope with the index
12323 * for the row.
12324 */
12325 indexAs: {
12326 type: String,
12327 value: 'index'
12328 },
12329
12330 /**
12331 * The name of the variable to add to the binding scope to indicate
12332 * if the row is selected.
12333 */
12334 selectedAs: {
12335 type: String,
12336 value: 'selected'
12337 },
12338
12339 /**
12340 * When true, the list is rendered as a grid. Grid items must have
12341 * fixed width and height set via CSS. e.g.
12342 *
12343 * ```html
12344 * <iron-list grid>
12345 * <template>
12346 * <div style="width: 100px; height: 100px;"> 100x100 </div>
12347 * </template>
12348 * </iron-list>
12349 * ```
12350 */
12351 grid: {
12352 type: Boolean,
12353 value: false,
12354 reflectToAttribute: true
12355 },
12356
12357 /**
12358 * When true, tapping a row will select the item, placing its data model
12359 * in the set of selected items retrievable via the selection property.
12360 *
12361 * Note that tapping focusable elements within the list item will not
12362 * result in selection, since they are presumed to have their * own action .
12363 */
12364 selectionEnabled: {
10938 type: Boolean, 12365 type: Boolean,
10939 value: false 12366 value: false
10940 }, 12367 },
10941 12368
10942 /** 12369 /**
10943 * This property is deprecated and should not be used. Use the global 12370 * When `multiSelection` is false, this is the currently selected item, or `null`
10944 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead . 12371 * if no item is selected.
10945 */ 12372 */
10946 _validatorMeta: { 12373 selectedItem: {
10947 type: Object 12374 type: Object,
12375 notify: true
10948 }, 12376 },
10949 12377
10950 /** 12378 /**
10951 * Namespace for this validator. This property is deprecated and should 12379 * When `multiSelection` is true, this is an array that contains the selec ted items.
10952 * not be used. For all intents and purposes, please consider it a
10953 * read-only, config-time property.
10954 */ 12380 */
10955 validatorType: { 12381 selectedItems: {
10956 type: String,
10957 value: 'validator'
10958 },
10959
10960 _validator: {
10961 type: Object, 12382 type: Object,
10962 computed: '__computeValidator(validator)' 12383 notify: true
10963 } 12384 },
10964 },
10965
10966 observers: [
10967 '_invalidChanged(invalid)'
10968 ],
10969
10970 registered: function() {
10971 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
10972 },
10973
10974 _invalidChanged: function() {
10975 if (this.invalid) {
10976 this.setAttribute('aria-invalid', 'true');
10977 } else {
10978 this.removeAttribute('aria-invalid');
10979 }
10980 },
10981
10982 /**
10983 * @return {boolean} True if the validator `validator` exists.
10984 */
10985 hasValidator: function() {
10986 return this._validator != null;
10987 },
10988
10989 /**
10990 * Returns true if the `value` is valid, and updates `invalid`. If you want
10991 * your element to have custom validation logic, do not override this method ;
10992 * override `_getValidity(value)` instead.
10993
10994 * @param {Object} value The value to be validated. By default, it is passed
10995 * to the validator's `validate()` function, if a validator is set.
10996 * @return {boolean} True if `value` is valid.
10997 */
10998 validate: function(value) {
10999 this.invalid = !this._getValidity(value);
11000 return !this.invalid;
11001 },
11002
11003 /**
11004 * Returns true if `value` is valid. By default, it is passed
11005 * to the validator's `validate()` function, if a validator is set. You
11006 * should override this method if you want to implement custom validity
11007 * logic for your element.
11008 *
11009 * @param {Object} value The value to be validated.
11010 * @return {boolean} True if `value` is valid.
11011 */
11012
11013 _getValidity: function(value) {
11014 if (this.hasValidator()) {
11015 return this._validator.validate(value);
11016 }
11017 return true;
11018 },
11019
11020 __computeValidator: function() {
11021 return Polymer.IronValidatableBehaviorMeta &&
11022 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
11023 }
11024 };
11025 /*
11026 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
11027 to `<input>`.
11028
11029 ### Two-way binding
11030
11031 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
11032
11033 <input value="{{myValue::input}}">
11034
11035 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
11036 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
11037
11038 <input is="iron-input" bind-value="{{myValue}}">
11039
11040 ### Custom validators
11041
11042 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
11043
11044 <input is="iron-input" validator="my-custom-validator">
11045
11046 ### Stopping invalid input
11047
11048 It may be desirable to only allow users to enter certain characters. You can use the
11049 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
11050 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
11051
11052 \x3c!-- only allow characters that match [0-9] --\x3e
11053 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
11054
11055 @hero hero.svg
11056 @demo demo/index.html
11057 */
11058
11059 Polymer({
11060
11061 is: 'iron-input',
11062
11063 extends: 'input',
11064
11065 behaviors: [
11066 Polymer.IronValidatableBehavior
11067 ],
11068
11069 properties: {
11070 12385
11071 /** 12386 /**
11072 * Use this property instead of `value` for two-way data binding. 12387 * When `true`, multiple items may be selected at once (in this case,
12388 * `selected` is an array of currently selected items). When `false`,
12389 * only one item may be selected at a time.
11073 */ 12390 */
11074 bindValue: { 12391 multiSelection: {
11075 observer: '_bindValueChanged',
11076 type: String
11077 },
11078
11079 /**
11080 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
11081 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
11082 * Pasted input will have each character checked individually; if any char acter
11083 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
11084 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
11085 */
11086 preventInvalidInput: {
11087 type: Boolean
11088 },
11089
11090 /**
11091 * Regular expression that list the characters allowed as input.
11092 * This pattern represents the allowed characters for the field; as the us er inputs text,
11093 * each individual character will be checked against the pattern (rather t han checking
11094 * the entire value as a whole). The recommended format should be a list o f allowed characters;
11095 * for example, `[a-zA-Z0-9.+-!;:]`
11096 */
11097 allowedPattern: {
11098 type: String,
11099 observer: "_allowedPatternChanged"
11100 },
11101
11102 _previousValidInput: {
11103 type: String,
11104 value: ''
11105 },
11106
11107 _patternAlreadyChecked: {
11108 type: Boolean, 12392 type: Boolean,
11109 value: false 12393 value: false
11110 } 12394 }
11111 12395 },
11112 }, 12396
11113 12397 observers: [
11114 listeners: { 12398 '_itemsChanged(items.*)',
11115 'input': '_onInput', 12399 '_selectionEnabledChanged(selectionEnabled)',
11116 'keypress': '_onKeypress' 12400 '_multiSelectionChanged(multiSelection)',
11117 }, 12401 '_setOverflow(scrollTarget)'
11118 12402 ],
11119 /** @suppress {checkTypes} */ 12403
11120 registered: function() { 12404 behaviors: [
11121 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E). 12405 Polymer.Templatizer,
11122 if (!this._canDispatchEventOnDisabled()) { 12406 Polymer.IronResizableBehavior,
11123 this._origDispatchEvent = this.dispatchEvent; 12407 Polymer.IronA11yKeysBehavior,
11124 this.dispatchEvent = this._dispatchEventFirefoxIE; 12408 Polymer.IronScrollTargetBehavior
11125 } 12409 ],
11126 }, 12410
11127 12411 keyBindings: {
11128 created: function() { 12412 'up': '_didMoveUp',
11129 Polymer.IronA11yAnnouncer.requestAvailability(); 12413 'down': '_didMoveDown',
11130 }, 12414 'enter': '_didEnter'
11131 12415 },
11132 _canDispatchEventOnDisabled: function() { 12416
11133 var input = document.createElement('input'); 12417 /**
11134 var canDispatch = false; 12418 * The ratio of hidden tiles that should remain in the scroll direction.
11135 input.disabled = true; 12419 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
11136 12420 */
11137 input.addEventListener('feature-check-dispatch-event', function() { 12421 _ratio: 0.5,
11138 canDispatch = true; 12422
11139 }); 12423 /**
11140 12424 * The padding-top value for the list.
11141 try { 12425 */
11142 input.dispatchEvent(new Event('feature-check-dispatch-event')); 12426 _scrollerPaddingTop: 0,
11143 } catch(e) {} 12427
11144 12428 /**
11145 return canDispatch; 12429 * This value is the same as `scrollTop`.
11146 }, 12430 */
11147 12431 _scrollPosition: 0,
11148 _dispatchEventFirefoxIE: function() { 12432
11149 // Due to Firefox bug, events fired on disabled form controls can throw 12433 /**
11150 // errors; furthermore, neither IE nor Firefox will actually dispatch 12434 * The sum of the heights of all the tiles in the DOM.
11151 // events from disabled form controls; as such, we toggle disable around 12435 */
11152 // the dispatch to allow notifying properties to notify 12436 _physicalSize: 0,
11153 // See issue #47 for details 12437
11154 var disabled = this.disabled; 12438 /**
11155 this.disabled = false; 12439 * The average `offsetHeight` of the tiles observed till now.
11156 this._origDispatchEvent.apply(this, arguments); 12440 */
11157 this.disabled = disabled; 12441 _physicalAverage: 0,
11158 }, 12442
11159 12443 /**
11160 get _patternRegExp() { 12444 * The number of tiles which `offsetHeight` > 0 observed until now.
11161 var pattern; 12445 */
11162 if (this.allowedPattern) { 12446 _physicalAverageCount: 0,
11163 pattern = new RegExp(this.allowedPattern); 12447
12448 /**
12449 * The Y position of the item rendered in the `_physicalStart`
12450 * tile relative to the scrolling list.
12451 */
12452 _physicalTop: 0,
12453
12454 /**
12455 * The number of items in the list.
12456 */
12457 _virtualCount: 0,
12458
12459 /**
12460 * A map between an item key and its physical item index
12461 */
12462 _physicalIndexForKey: null,
12463
12464 /**
12465 * The estimated scroll height based on `_physicalAverage`
12466 */
12467 _estScrollHeight: 0,
12468
12469 /**
12470 * The scroll height of the dom node
12471 */
12472 _scrollHeight: 0,
12473
12474 /**
12475 * The height of the list. This is referred as the viewport in the context o f list.
12476 */
12477 _viewportHeight: 0,
12478
12479 /**
12480 * The width of the list. This is referred as the viewport in the context of list.
12481 */
12482 _viewportWidth: 0,
12483
12484 /**
12485 * An array of DOM nodes that are currently in the tree
12486 * @type {?Array<!TemplatizerNode>}
12487 */
12488 _physicalItems: null,
12489
12490 /**
12491 * An array of heights for each item in `_physicalItems`
12492 * @type {?Array<number>}
12493 */
12494 _physicalSizes: null,
12495
12496 /**
12497 * A cached value for the first visible index.
12498 * See `firstVisibleIndex`
12499 * @type {?number}
12500 */
12501 _firstVisibleIndexVal: null,
12502
12503 /**
12504 * A cached value for the last visible index.
12505 * See `lastVisibleIndex`
12506 * @type {?number}
12507 */
12508 _lastVisibleIndexVal: null,
12509
12510 /**
12511 * A Polymer collection for the items.
12512 * @type {?Polymer.Collection}
12513 */
12514 _collection: null,
12515
12516 /**
12517 * True if the current item list was rendered for the first time
12518 * after attached.
12519 */
12520 _itemsRendered: false,
12521
12522 /**
12523 * The page that is currently rendered.
12524 */
12525 _lastPage: null,
12526
12527 /**
12528 * The max number of pages to render. One page is equivalent to the height o f the list.
12529 */
12530 _maxPages: 3,
12531
12532 /**
12533 * The currently focused physical item.
12534 */
12535 _focusedItem: null,
12536
12537 /**
12538 * The index of the `_focusedItem`.
12539 */
12540 _focusedIndex: -1,
12541
12542 /**
12543 * The the item that is focused if it is moved offscreen.
12544 * @private {?TemplatizerNode}
12545 */
12546 _offscreenFocusedItem: null,
12547
12548 /**
12549 * The item that backfills the `_offscreenFocusedItem` in the physical items
12550 * list when that item is moved offscreen.
12551 */
12552 _focusBackfillItem: null,
12553
12554 /**
12555 * The maximum items per row
12556 */
12557 _itemsPerRow: 1,
12558
12559 /**
12560 * The width of each grid item
12561 */
12562 _itemWidth: 0,
12563
12564 /**
12565 * The height of the row in grid layout.
12566 */
12567 _rowHeight: 0,
12568
12569 /**
12570 * The bottom of the physical content.
12571 */
12572 get _physicalBottom() {
12573 return this._physicalTop + this._physicalSize;
12574 },
12575
12576 /**
12577 * The bottom of the scroll.
12578 */
12579 get _scrollBottom() {
12580 return this._scrollPosition + this._viewportHeight;
12581 },
12582
12583 /**
12584 * The n-th item rendered in the last physical item.
12585 */
12586 get _virtualEnd() {
12587 return this._virtualStart + this._physicalCount - 1;
12588 },
12589
12590 /**
12591 * The height of the physical content that isn't on the screen.
12592 */
12593 get _hiddenContentSize() {
12594 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
12595 return size - this._viewportHeight;
12596 },
12597
12598 /**
12599 * The maximum scroll top value.
12600 */
12601 get _maxScrollTop() {
12602 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
12603 },
12604
12605 /**
12606 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
12607 */
12608 _minVirtualStart: 0,
12609
12610 /**
12611 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
12612 */
12613 get _maxVirtualStart() {
12614 return Math.max(0, this._virtualCount - this._physicalCount);
12615 },
12616
12617 /**
12618 * The n-th item rendered in the `_physicalStart` tile.
12619 */
12620 _virtualStartVal: 0,
12621
12622 set _virtualStart(val) {
12623 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
12624 },
12625
12626 get _virtualStart() {
12627 return this._virtualStartVal || 0;
12628 },
12629
12630 /**
12631 * The k-th tile that is at the top of the scrolling list.
12632 */
12633 _physicalStartVal: 0,
12634
12635 set _physicalStart(val) {
12636 this._physicalStartVal = val % this._physicalCount;
12637 if (this._physicalStartVal < 0) {
12638 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
12639 }
12640 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12641 },
12642
12643 get _physicalStart() {
12644 return this._physicalStartVal || 0;
12645 },
12646
12647 /**
12648 * The number of tiles in the DOM.
12649 */
12650 _physicalCountVal: 0,
12651
12652 set _physicalCount(val) {
12653 this._physicalCountVal = val;
12654 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12655 },
12656
12657 get _physicalCount() {
12658 return this._physicalCountVal;
12659 },
12660
12661 /**
12662 * The k-th tile that is at the bottom of the scrolling list.
12663 */
12664 _physicalEnd: 0,
12665
12666 /**
12667 * An optimal physical size such that we will have enough physical items
12668 * to fill up the viewport and recycle when the user scrolls.
12669 *
12670 * This default value assumes that we will at least have the equivalent
12671 * to a viewport of physical items above and below the user's viewport.
12672 */
12673 get _optPhysicalSize() {
12674 if (this.grid) {
12675 return this._estRowsInView * this._rowHeight * this._maxPages;
12676 }
12677 return this._viewportHeight * this._maxPages;
12678 },
12679
12680 get _optPhysicalCount() {
12681 return this._estRowsInView * this._itemsPerRow * this._maxPages;
12682 },
12683
12684 /**
12685 * True if the current list is visible.
12686 */
12687 get _isVisible() {
12688 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
12689 },
12690
12691 /**
12692 * Gets the index of the first visible item in the viewport.
12693 *
12694 * @type {number}
12695 */
12696 get firstVisibleIndex() {
12697 if (this._firstVisibleIndexVal === null) {
12698 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
12699
12700 this._firstVisibleIndexVal = this._iterateItems(
12701 function(pidx, vidx) {
12702 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12703
12704 if (physicalOffset > this._scrollPosition) {
12705 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
12706 }
12707 // Handle a partially rendered final row in grid mode
12708 if (this.grid && this._virtualCount - 1 === vidx) {
12709 return vidx - (vidx % this._itemsPerRow);
12710 }
12711 }) || 0;
12712 }
12713 return this._firstVisibleIndexVal;
12714 },
12715
12716 /**
12717 * Gets the index of the last visible item in the viewport.
12718 *
12719 * @type {number}
12720 */
12721 get lastVisibleIndex() {
12722 if (this._lastVisibleIndexVal === null) {
12723 if (this.grid) {
12724 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
12725 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
12726 } else {
12727 var physicalOffset = this._physicalTop;
12728 this._iterateItems(function(pidx, vidx) {
12729 if (physicalOffset < this._scrollBottom) {
12730 this._lastVisibleIndexVal = vidx;
12731 } else {
12732 // Break _iterateItems
12733 return true;
12734 }
12735 physicalOffset += this._getPhysicalSizeIncrement(pidx);
12736 });
12737 }
12738 }
12739 return this._lastVisibleIndexVal;
12740 },
12741
12742 get _defaultScrollTarget() {
12743 return this;
12744 },
12745 get _virtualRowCount() {
12746 return Math.ceil(this._virtualCount / this._itemsPerRow);
12747 },
12748
12749 get _estRowsInView() {
12750 return Math.ceil(this._viewportHeight / this._rowHeight);
12751 },
12752
12753 get _physicalRows() {
12754 return Math.ceil(this._physicalCount / this._itemsPerRow);
12755 },
12756
12757 ready: function() {
12758 this.addEventListener('focus', this._didFocus.bind(this), true);
12759 },
12760
12761 attached: function() {
12762 this.updateViewportBoundaries();
12763 this._render();
12764 // `iron-resize` is fired when the list is attached if the event is added
12765 // before attached causing unnecessary work.
12766 this.listen(this, 'iron-resize', '_resizeHandler');
12767 },
12768
12769 detached: function() {
12770 this._itemsRendered = false;
12771 this.unlisten(this, 'iron-resize', '_resizeHandler');
12772 },
12773
12774 /**
12775 * Set the overflow property if this element has its own scrolling region
12776 */
12777 _setOverflow: function(scrollTarget) {
12778 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
12779 this.style.overflow = scrollTarget === this ? 'auto' : '';
12780 },
12781
12782 /**
12783 * Invoke this method if you dynamically update the viewport's
12784 * size or CSS padding.
12785 *
12786 * @method updateViewportBoundaries
12787 */
12788 updateViewportBoundaries: function() {
12789 this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
12790 parseInt(window.getComputedStyle(this)['padding-top'], 10);
12791
12792 this._viewportHeight = this._scrollTargetHeight;
12793 if (this.grid) {
12794 this._updateGridMetrics();
12795 }
12796 },
12797
12798 /**
12799 * Update the models, the position of the
12800 * items in the viewport and recycle tiles as needed.
12801 */
12802 _scrollHandler: function() {
12803 // clamp the `scrollTop` value
12804 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
12805 var delta = scrollTop - this._scrollPosition;
12806 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
12807 var ratio = this._ratio;
12808 var recycledTiles = 0;
12809 var hiddenContentSize = this._hiddenContentSize;
12810 var currentRatio = ratio;
12811 var movingUp = [];
12812
12813 // track the last `scrollTop`
12814 this._scrollPosition = scrollTop;
12815
12816 // clear cached visible indexes
12817 this._firstVisibleIndexVal = null;
12818 this._lastVisibleIndexVal = null;
12819
12820 scrollBottom = this._scrollBottom;
12821 physicalBottom = this._physicalBottom;
12822
12823 // random access
12824 if (Math.abs(delta) > this._physicalSize) {
12825 this._physicalTop += delta;
12826 recycledTiles = Math.round(delta / this._physicalAverage);
12827 }
12828 // scroll up
12829 else if (delta < 0) {
12830 var topSpace = scrollTop - this._physicalTop;
12831 var virtualStart = this._virtualStart;
12832
12833 recycledTileSet = [];
12834
12835 kth = this._physicalEnd;
12836 currentRatio = topSpace / hiddenContentSize;
12837
12838 // move tiles from bottom to top
12839 while (
12840 // approximate `currentRatio` to `ratio`
12841 currentRatio < ratio &&
12842 // recycle less physical items than the total
12843 recycledTiles < this._physicalCount &&
12844 // ensure that these recycled tiles are needed
12845 virtualStart - recycledTiles > 0 &&
12846 // ensure that the tile is not visible
12847 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
12848 ) {
12849
12850 tileHeight = this._getPhysicalSizeIncrement(kth);
12851 currentRatio += tileHeight / hiddenContentSize;
12852 physicalBottom -= tileHeight;
12853 recycledTileSet.push(kth);
12854 recycledTiles++;
12855 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
12856 }
12857
12858 movingUp = recycledTileSet;
12859 recycledTiles = -recycledTiles;
12860 }
12861 // scroll down
12862 else if (delta > 0) {
12863 var bottomSpace = physicalBottom - scrollBottom;
12864 var virtualEnd = this._virtualEnd;
12865 var lastVirtualItemIndex = this._virtualCount-1;
12866
12867 recycledTileSet = [];
12868
12869 kth = this._physicalStart;
12870 currentRatio = bottomSpace / hiddenContentSize;
12871
12872 // move tiles from top to bottom
12873 while (
12874 // approximate `currentRatio` to `ratio`
12875 currentRatio < ratio &&
12876 // recycle less physical items than the total
12877 recycledTiles < this._physicalCount &&
12878 // ensure that these recycled tiles are needed
12879 virtualEnd + recycledTiles < lastVirtualItemIndex &&
12880 // ensure that the tile is not visible
12881 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
12882 ) {
12883
12884 tileHeight = this._getPhysicalSizeIncrement(kth);
12885 currentRatio += tileHeight / hiddenContentSize;
12886
12887 this._physicalTop += tileHeight;
12888 recycledTileSet.push(kth);
12889 recycledTiles++;
12890 kth = (kth + 1) % this._physicalCount;
12891 }
12892 }
12893
12894 if (recycledTiles === 0) {
12895 // Try to increase the pool if the list's client height isn't filled up with physical items
12896 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
12897 this._increasePoolIfNeeded();
12898 }
11164 } else { 12899 } else {
11165 switch (this.type) { 12900 this._virtualStart = this._virtualStart + recycledTiles;
11166 case 'number': 12901 this._physicalStart = this._physicalStart + recycledTiles;
11167 pattern = /[0-9.,e-]/; 12902 this._update(recycledTileSet, movingUp);
11168 break; 12903 }
11169 } 12904 },
11170 } 12905
11171 return pattern; 12906 /**
11172 }, 12907 * Update the list of items, starting from the `_virtualStart` item.
11173 12908 * @param {!Array<number>=} itemSet
11174 ready: function() { 12909 * @param {!Array<number>=} movingUp
11175 this.bindValue = this.value; 12910 */
11176 }, 12911 _update: function(itemSet, movingUp) {
11177 12912 // manage focus
11178 /** 12913 this._manageFocus();
11179 * @suppress {checkTypes} 12914 // update models
11180 */ 12915 this._assignModels(itemSet);
11181 _bindValueChanged: function() { 12916 // measure heights
11182 if (this.value !== this.bindValue) { 12917 this._updateMetrics(itemSet);
11183 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue; 12918 // adjust offset after measuring
11184 } 12919 if (movingUp) {
11185 // manually notify because we don't want to notify until after setting val ue 12920 while (movingUp.length) {
11186 this.fire('bind-value-changed', {value: this.bindValue}); 12921 var idx = movingUp.pop();
11187 }, 12922 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
11188 12923 }
11189 _allowedPatternChanged: function() { 12924 }
11190 // Force to prevent invalid input when an `allowed-pattern` is set 12925 // update the position of the items
11191 this.preventInvalidInput = this.allowedPattern ? true : false; 12926 this._positionItems();
11192 }, 12927 // set the scroller size
11193 12928 this._updateScrollerSize();
11194 _onInput: function() { 12929 // increase the pool of physical items
11195 // Need to validate each of the characters pasted if they haven't 12930 this._increasePoolIfNeeded();
11196 // been validated inside `_onKeypress` already. 12931 },
11197 if (this.preventInvalidInput && !this._patternAlreadyChecked) { 12932
11198 var valid = this._checkPatternValidity(); 12933 /**
11199 if (!valid) { 12934 * Creates a pool of DOM elements and attaches them to the local dom.
11200 this._announceInvalidCharacter('Invalid string of characters not enter ed.'); 12935 */
11201 this.value = this._previousValidInput; 12936 _createPool: function(size) {
11202 } 12937 var physicalItems = new Array(size);
11203 } 12938
11204 12939 this._ensureTemplatized();
11205 this.bindValue = this.value; 12940
11206 this._previousValidInput = this.value; 12941 for (var i = 0; i < size; i++) {
11207 this._patternAlreadyChecked = false; 12942 var inst = this.stamp(null);
11208 }, 12943 // First element child is item; Safari doesn't support children[0]
11209 12944 // on a doc fragment
11210 _isPrintable: function(event) { 12945 physicalItems[i] = inst.root.querySelector('*');
11211 // What a control/printable character is varies wildly based on the browse r. 12946 Polymer.dom(this).appendChild(inst.root);
11212 // - most control characters (arrows, backspace) do not send a `keypress` event 12947 }
11213 // in Chrome, but the *do* on Firefox 12948 return physicalItems;
11214 // - in Firefox, when they do send a `keypress` event, control chars have 12949 },
11215 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) 12950
11216 // - printable characters always send a keypress event. 12951 /**
11217 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode 12952 * Increases the pool of physical items only if needed.
11218 // always matches the charCode. 12953 *
11219 // None of this makes any sense. 12954 * @return {boolean} True if the pool was increased.
11220 12955 */
11221 // For these keys, ASCII code == browser keycode. 12956 _increasePoolIfNeeded: function() {
11222 var anyNonPrintable = 12957 // Base case 1: the list has no height.
11223 (event.keyCode == 8) || // backspace 12958 if (this._viewportHeight === 0) {
11224 (event.keyCode == 9) || // tab 12959 return false;
11225 (event.keyCode == 13) || // enter 12960 }
11226 (event.keyCode == 27); // escape 12961 // Base case 2: If the physical size is optimal and the list's client heig ht is full
11227 12962 // with physical items, don't increase the pool.
11228 // For these keys, make sure it's a browser keycode and not an ASCII code. 12963 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
11229 var mozNonPrintable = 12964 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
11230 (event.keyCode == 19) || // pause 12965 return false;
11231 (event.keyCode == 20) || // caps lock 12966 }
11232 (event.keyCode == 45) || // insert 12967 // this value should range between [0 <= `currentPage` <= `_maxPages`]
11233 (event.keyCode == 46) || // delete 12968 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
11234 (event.keyCode == 144) || // num lock 12969
11235 (event.keyCode == 145) || // scroll lock 12970 if (currentPage === 0) {
11236 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows 12971 // fill the first page
11237 (event.keyCode > 111 && event.keyCode < 124); // fn keys 12972 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
11238 12973 } else if (this._lastPage !== currentPage && isClientHeightFull) {
11239 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); 12974 // paint the page and defer the next increase
11240 }, 12975 // wait 16ms which is rough enough to get paint cycle.
11241 12976 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
11242 _onKeypress: function(event) { 12977 } else {
11243 if (!this.preventInvalidInput && this.type !== 'number') { 12978 // fill the rest of the pages
12979 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
12980 }
12981
12982 this._lastPage = currentPage;
12983
12984 return true;
12985 },
12986
12987 /**
12988 * Increases the pool size.
12989 */
12990 _increasePool: function(missingItems) {
12991 var nextPhysicalCount = Math.min(
12992 this._physicalCount + missingItems,
12993 this._virtualCount - this._virtualStart,
12994 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
12995 );
12996 var prevPhysicalCount = this._physicalCount;
12997 var delta = nextPhysicalCount - prevPhysicalCount;
12998
12999 if (delta <= 0) {
11244 return; 13000 return;
11245 } 13001 }
11246 var regexp = this._patternRegExp; 13002
11247 if (!regexp) { 13003 [].push.apply(this._physicalItems, this._createPool(delta));
13004 [].push.apply(this._physicalSizes, new Array(delta));
13005
13006 this._physicalCount = prevPhysicalCount + delta;
13007
13008 // update the physical start if we need to preserve the model of the focus ed item.
13009 // In this situation, the focused item is currently rendered and its model would
13010 // have changed after increasing the pool if the physical start remained u nchanged.
13011 if (this._physicalStart > this._physicalEnd &&
13012 this._isIndexRendered(this._focusedIndex) &&
13013 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
13014 this._physicalStart = this._physicalStart + delta;
13015 }
13016 this._update();
13017 },
13018
13019 /**
13020 * Render a new list of items. This method does exactly the same as `update` ,
13021 * but it also ensures that only one `update` cycle is created.
13022 */
13023 _render: function() {
13024 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
13025
13026 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
13027 this._lastPage = 0;
13028 this._update();
13029 this._itemsRendered = true;
13030 }
13031 },
13032
13033 /**
13034 * Templetizes the user template.
13035 */
13036 _ensureTemplatized: function() {
13037 if (!this.ctor) {
13038 // Template instance props that should be excluded from forwarding
13039 var props = {};
13040 props.__key__ = true;
13041 props[this.as] = true;
13042 props[this.indexAs] = true;
13043 props[this.selectedAs] = true;
13044 props.tabIndex = true;
13045
13046 this._instanceProps = props;
13047 this._userTemplate = Polymer.dom(this).querySelector('template');
13048
13049 if (this._userTemplate) {
13050 this.templatize(this._userTemplate);
13051 } else {
13052 console.warn('iron-list requires a template to be provided in light-do m');
13053 }
13054 }
13055 },
13056
13057 /**
13058 * Implements extension point from Templatizer mixin.
13059 */
13060 _getStampedChildren: function() {
13061 return this._physicalItems;
13062 },
13063
13064 /**
13065 * Implements extension point from Templatizer
13066 * Called as a side effect of a template instance path change, responsible
13067 * for notifying items.<key-for-instance>.<path> change up to host.
13068 */
13069 _forwardInstancePath: function(inst, path, value) {
13070 if (path.indexOf(this.as + '.') === 0) {
13071 this.notifyPath('items.' + inst.__key__ + '.' +
13072 path.slice(this.as.length + 1), value);
13073 }
13074 },
13075
13076 /**
13077 * Implements extension point from Templatizer mixin
13078 * Called as side-effect of a host property change, responsible for
13079 * notifying parent path change on each row.
13080 */
13081 _forwardParentProp: function(prop, value) {
13082 if (this._physicalItems) {
13083 this._physicalItems.forEach(function(item) {
13084 item._templateInstance[prop] = value;
13085 }, this);
13086 }
13087 },
13088
13089 /**
13090 * Implements extension point from Templatizer
13091 * Called as side-effect of a host path change, responsible for
13092 * notifying parent.<path> path change on each row.
13093 */
13094 _forwardParentPath: function(path, value) {
13095 if (this._physicalItems) {
13096 this._physicalItems.forEach(function(item) {
13097 item._templateInstance.notifyPath(path, value, true);
13098 }, this);
13099 }
13100 },
13101
13102 /**
13103 * Called as a side effect of a host items.<key>.<path> path change,
13104 * responsible for notifying item.<path> changes.
13105 */
13106 _forwardItemPath: function(path, value) {
13107 if (!this._physicalIndexForKey) {
11248 return; 13108 return;
11249 } 13109 }
11250 13110 var dot = path.indexOf('.');
11251 // Handle special keys and backspace 13111 var key = path.substring(0, dot < 0 ? path.length : dot);
11252 if (event.metaKey || event.ctrlKey || event.altKey) 13112 var idx = this._physicalIndexForKey[key];
13113 var offscreenItem = this._offscreenFocusedItem;
13114 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
13115 offscreenItem : this._physicalItems[idx];
13116
13117 if (!el || el._templateInstance.__key__ !== key) {
11253 return; 13118 return;
11254 13119 }
11255 // Check the pattern either here or in `_onInput`, but not in both. 13120 if (dot >= 0) {
11256 this._patternAlreadyChecked = true; 13121 path = this.as + '.' + path.substring(dot+1);
11257 13122 el._templateInstance.notifyPath(path, value, true);
11258 var thisChar = String.fromCharCode(event.charCode); 13123 } else {
11259 if (this._isPrintable(event) && !regexp.test(thisChar)) { 13124 // Update selection if needed
11260 event.preventDefault(); 13125 var currentItem = el._templateInstance[this.as];
11261 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.'); 13126 if (Array.isArray(this.selectedItems)) {
11262 } 13127 for (var i = 0; i < this.selectedItems.length; i++) {
11263 }, 13128 if (this.selectedItems[i] === currentItem) {
11264 13129 this.set('selectedItems.' + i, value);
11265 _checkPatternValidity: function() { 13130 break;
11266 var regexp = this._patternRegExp; 13131 }
11267 if (!regexp) { 13132 }
11268 return true; 13133 } else if (this.selectedItem === currentItem) {
11269 } 13134 this.set('selectedItem', value);
11270 for (var i = 0; i < this.value.length; i++) { 13135 }
11271 if (!regexp.test(this.value[i])) { 13136 el._templateInstance[this.as] = value;
11272 return false; 13137 }
11273 } 13138 },
11274 } 13139
11275 return true; 13140 /**
11276 }, 13141 * Called when the items have changed. That is, ressignments
11277 13142 * to `items`, splices or updates to a single item.
11278 /** 13143 */
11279 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first, 13144 _itemsChanged: function(change) {
11280 * then any constraints. 13145 if (change.path === 'items') {
11281 * @return {boolean} True if the value is valid. 13146 // reset items
11282 */ 13147 this._virtualStart = 0;
11283 validate: function() { 13148 this._physicalTop = 0;
11284 // First, check what the browser thinks. Some inputs (like type=number) 13149 this._virtualCount = this.items ? this.items.length : 0;
11285 // behave weirdly and will set the value to "" if something invalid is 13150 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
11286 // entered, but will set the validity correctly. 13151 this._physicalIndexForKey = {};
11287 var valid = this.checkValidity(); 13152 this._firstVisibleIndexVal = null;
11288 13153 this._lastVisibleIndexVal = null;
11289 // Only do extra checking if the browser thought this was valid. 13154
11290 if (valid) { 13155 this._resetScrollPosition(0);
11291 // Empty, required input is invalid 13156 this._removeFocusedItem();
11292 if (this.required && this.value === '') { 13157 // create the initial physical items
11293 valid = false; 13158 if (!this._physicalItems) {
11294 } else if (this.hasValidator()) { 13159 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
11295 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value ); 13160 this._physicalItems = this._createPool(this._physicalCount);
11296 } 13161 this._physicalSizes = new Array(this._physicalCount);
11297 } 13162 }
11298 13163
11299 this.invalid = !valid; 13164 this._physicalStart = 0;
11300 this.fire('iron-input-validate'); 13165
11301 return valid; 13166 } else if (change.path === 'items.splices') {
11302 }, 13167
11303 13168 this._adjustVirtualIndex(change.value.indexSplices);
11304 _announceInvalidCharacter: function(message) { 13169 this._virtualCount = this.items ? this.items.length : 0;
11305 this.fire('iron-announce', { text: message }); 13170
13171 } else {
13172 // update a single item
13173 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
13174 return;
13175 }
13176
13177 this._itemsRendered = false;
13178 this._debounceTemplate(this._render);
13179 },
13180
13181 /**
13182 * @param {!Array<!PolymerSplice>} splices
13183 */
13184 _adjustVirtualIndex: function(splices) {
13185 splices.forEach(function(splice) {
13186 // deselect removed items
13187 splice.removed.forEach(this._removeItem, this);
13188 // We only need to care about changes happening above the current positi on
13189 if (splice.index < this._virtualStart) {
13190 var delta = Math.max(
13191 splice.addedCount - splice.removed.length,
13192 splice.index - this._virtualStart);
13193
13194 this._virtualStart = this._virtualStart + delta;
13195
13196 if (this._focusedIndex >= 0) {
13197 this._focusedIndex = this._focusedIndex + delta;
13198 }
13199 }
13200 }, this);
13201 },
13202
13203 _removeItem: function(item) {
13204 this.$.selector.deselect(item);
13205 // remove the current focused item
13206 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
13207 this._removeFocusedItem();
13208 }
13209 },
13210
13211 /**
13212 * Executes a provided function per every physical index in `itemSet`
13213 * `itemSet` default value is equivalent to the entire set of physical index es.
13214 *
13215 * @param {!function(number, number)} fn
13216 * @param {!Array<number>=} itemSet
13217 */
13218 _iterateItems: function(fn, itemSet) {
13219 var pidx, vidx, rtn, i;
13220
13221 if (arguments.length === 2 && itemSet) {
13222 for (i = 0; i < itemSet.length; i++) {
13223 pidx = itemSet[i];
13224 vidx = this._computeVidx(pidx);
13225 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13226 return rtn;
13227 }
13228 }
13229 } else {
13230 pidx = this._physicalStart;
13231 vidx = this._virtualStart;
13232
13233 for (; pidx < this._physicalCount; pidx++, vidx++) {
13234 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13235 return rtn;
13236 }
13237 }
13238 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
13239 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13240 return rtn;
13241 }
13242 }
13243 }
13244 },
13245
13246 /**
13247 * Returns the virtual index for a given physical index
13248 *
13249 * @param {number} pidx Physical index
13250 * @return {number}
13251 */
13252 _computeVidx: function(pidx) {
13253 if (pidx >= this._physicalStart) {
13254 return this._virtualStart + (pidx - this._physicalStart);
13255 }
13256 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
13257 },
13258
13259 /**
13260 * Assigns the data models to a given set of items.
13261 * @param {!Array<number>=} itemSet
13262 */
13263 _assignModels: function(itemSet) {
13264 this._iterateItems(function(pidx, vidx) {
13265 var el = this._physicalItems[pidx];
13266 var inst = el._templateInstance;
13267 var item = this.items && this.items[vidx];
13268
13269 if (item != null) {
13270 inst[this.as] = item;
13271 inst.__key__ = this._collection.getKey(item);
13272 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item);
13273 inst[this.indexAs] = vidx;
13274 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
13275 this._physicalIndexForKey[inst.__key__] = pidx;
13276 el.removeAttribute('hidden');
13277 } else {
13278 inst.__key__ = null;
13279 el.setAttribute('hidden', '');
13280 }
13281 }, itemSet);
13282 },
13283
13284 /**
13285 * Updates the height for a given set of items.
13286 *
13287 * @param {!Array<number>=} itemSet
13288 */
13289 _updateMetrics: function(itemSet) {
13290 // Make sure we distributed all the physical items
13291 // so we can measure them
13292 Polymer.dom.flush();
13293
13294 var newPhysicalSize = 0;
13295 var oldPhysicalSize = 0;
13296 var prevAvgCount = this._physicalAverageCount;
13297 var prevPhysicalAvg = this._physicalAverage;
13298
13299 this._iterateItems(function(pidx, vidx) {
13300
13301 oldPhysicalSize += this._physicalSizes[pidx] || 0;
13302 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
13303 newPhysicalSize += this._physicalSizes[pidx];
13304 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
13305
13306 }, itemSet);
13307
13308 this._viewportHeight = this._scrollTargetHeight;
13309 if (this.grid) {
13310 this._updateGridMetrics();
13311 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
13312 } else {
13313 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
13314 }
13315
13316 // update the average if we measured something
13317 if (this._physicalAverageCount !== prevAvgCount) {
13318 this._physicalAverage = Math.round(
13319 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
13320 this._physicalAverageCount);
13321 }
13322 },
13323
13324 _updateGridMetrics: function() {
13325 this._viewportWidth = this.$.items.offsetWidth;
13326 // Set item width to the value of the _physicalItems offsetWidth
13327 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
13328 // Set row height to the value of the _physicalItems offsetHeight
13329 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
13330 // If in grid mode compute how many items with exist in each row
13331 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
13332 },
13333
13334 /**
13335 * Updates the position of the physical items.
13336 */
13337 _positionItems: function() {
13338 this._adjustScrollPosition();
13339
13340 var y = this._physicalTop;
13341
13342 if (this.grid) {
13343 var totalItemWidth = this._itemsPerRow * this._itemWidth;
13344 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
13345
13346 this._iterateItems(function(pidx, vidx) {
13347
13348 var modulus = vidx % this._itemsPerRow;
13349 var x = Math.floor((modulus * this._itemWidth) + rowOffset);
13350
13351 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
13352
13353 if (this._shouldRenderNextRow(vidx)) {
13354 y += this._rowHeight;
13355 }
13356
13357 });
13358 } else {
13359 this._iterateItems(function(pidx, vidx) {
13360
13361 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
13362 y += this._physicalSizes[pidx];
13363
13364 });
13365 }
13366 },
13367
13368 _getPhysicalSizeIncrement: function(pidx) {
13369 if (!this.grid) {
13370 return this._physicalSizes[pidx];
13371 }
13372 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
13373 return 0;
13374 }
13375 return this._rowHeight;
13376 },
13377
13378 /**
13379 * Returns, based on the current index,
13380 * whether or not the next index will need
13381 * to be rendered on a new row.
13382 *
13383 * @param {number} vidx Virtual index
13384 * @return {boolean}
13385 */
13386 _shouldRenderNextRow: function(vidx) {
13387 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
13388 },
13389
13390 /**
13391 * Adjusts the scroll position when it was overestimated.
13392 */
13393 _adjustScrollPosition: function() {
13394 var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
13395 Math.min(this._scrollPosition + this._physicalTop, 0);
13396
13397 if (deltaHeight) {
13398 this._physicalTop = this._physicalTop - deltaHeight;
13399 // juking scroll position during interial scrolling on iOS is no bueno
13400 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
13401 this._resetScrollPosition(this._scrollTop - deltaHeight);
13402 }
13403 }
13404 },
13405
13406 /**
13407 * Sets the position of the scroll.
13408 */
13409 _resetScrollPosition: function(pos) {
13410 if (this.scrollTarget) {
13411 this._scrollTop = pos;
13412 this._scrollPosition = this._scrollTop;
13413 }
13414 },
13415
13416 /**
13417 * Sets the scroll height, that's the height of the content,
13418 *
13419 * @param {boolean=} forceUpdate If true, updates the height no matter what.
13420 */
13421 _updateScrollerSize: function(forceUpdate) {
13422 if (this.grid) {
13423 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
13424 } else {
13425 this._estScrollHeight = (this._physicalBottom +
13426 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
13427 }
13428
13429 forceUpdate = forceUpdate || this._scrollHeight === 0;
13430 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
13431 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
13432
13433 // amortize height adjustment, so it won't trigger repaints very often
13434 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
13435 this.$.items.style.height = this._estScrollHeight + 'px';
13436 this._scrollHeight = this._estScrollHeight;
13437 }
13438 },
13439
13440 /**
13441 * Scroll to a specific item in the virtual list regardless
13442 * of the physical items in the DOM tree.
13443 *
13444 * @method scrollToItem
13445 * @param {(Object)} item The item to be scrolled to
13446 */
13447 scrollToItem: function(item){
13448 return this.scrollToIndex(this.items.indexOf(item));
13449 },
13450
13451 /**
13452 * Scroll to a specific index in the virtual list regardless
13453 * of the physical items in the DOM tree.
13454 *
13455 * @method scrollToIndex
13456 * @param {number} idx The index of the item
13457 */
13458 scrollToIndex: function(idx) {
13459 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
13460 return;
13461 }
13462
13463 Polymer.dom.flush();
13464
13465 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
13466 // update the virtual start only when needed
13467 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
13468 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
13469 }
13470 // manage focus
13471 this._manageFocus();
13472 // assign new models
13473 this._assignModels();
13474 // measure the new sizes
13475 this._updateMetrics();
13476
13477 // estimate new physical offset
13478 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
13479 this._physicalTop = estPhysicalTop;
13480
13481 var currentTopItem = this._physicalStart;
13482 var currentVirtualItem = this._virtualStart;
13483 var targetOffsetTop = 0;
13484 var hiddenContentSize = this._hiddenContentSize;
13485
13486 // scroll to the item as much as we can
13487 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
13488 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
13489 currentTopItem = (currentTopItem + 1) % this._physicalCount;
13490 currentVirtualItem++;
13491 }
13492 // update the scroller size
13493 this._updateScrollerSize(true);
13494 // update the position of the items
13495 this._positionItems();
13496 // set the new scroll position
13497 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
13498 // increase the pool of physical items if needed
13499 this._increasePoolIfNeeded();
13500 // clear cached visible index
13501 this._firstVisibleIndexVal = null;
13502 this._lastVisibleIndexVal = null;
13503 },
13504
13505 /**
13506 * Reset the physical average and the average count.
13507 */
13508 _resetAverage: function() {
13509 this._physicalAverage = 0;
13510 this._physicalAverageCount = 0;
13511 },
13512
13513 /**
13514 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
13515 * when the element is resized.
13516 */
13517 _resizeHandler: function() {
13518 // iOS fires the resize event when the address bar slides up
13519 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
13520 return;
13521 }
13522 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
13523 // changing the scroll position from a resize handler would result in
13524 // the scroll position being reset. Waiting 1ms fixes the issue.
13525 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
13526 this.updateViewportBoundaries();
13527 this._render();
13528
13529 if (this._itemsRendered && this._physicalItems && this._isVisible) {
13530 this._resetAverage();
13531 this.scrollToIndex(this.firstVisibleIndex);
13532 }
13533 }.bind(this), 1));
13534 },
13535
13536 _getModelFromItem: function(item) {
13537 var key = this._collection.getKey(item);
13538 var pidx = this._physicalIndexForKey[key];
13539
13540 if (pidx != null) {
13541 return this._physicalItems[pidx]._templateInstance;
13542 }
13543 return null;
13544 },
13545
13546 /**
13547 * Gets a valid item instance from its index or the object value.
13548 *
13549 * @param {(Object|number)} item The item object or its index
13550 */
13551 _getNormalizedItem: function(item) {
13552 if (this._collection.getKey(item) === undefined) {
13553 if (typeof item === 'number') {
13554 item = this.items[item];
13555 if (!item) {
13556 throw new RangeError('<item> not found');
13557 }
13558 return item;
13559 }
13560 throw new TypeError('<item> should be a valid item');
13561 }
13562 return item;
13563 },
13564
13565 /**
13566 * Select the list item at the given index.
13567 *
13568 * @method selectItem
13569 * @param {(Object|number)} item The item object or its index
13570 */
13571 selectItem: function(item) {
13572 item = this._getNormalizedItem(item);
13573 var model = this._getModelFromItem(item);
13574
13575 if (!this.multiSelection && this.selectedItem) {
13576 this.deselectItem(this.selectedItem);
13577 }
13578 if (model) {
13579 model[this.selectedAs] = true;
13580 }
13581 this.$.selector.select(item);
13582 this.updateSizeForItem(item);
13583 },
13584
13585 /**
13586 * Deselects the given item list if it is already selected.
13587 *
13588
13589 * @method deselect
13590 * @param {(Object|number)} item The item object or its index
13591 */
13592 deselectItem: function(item) {
13593 item = this._getNormalizedItem(item);
13594 var model = this._getModelFromItem(item);
13595
13596 if (model) {
13597 model[this.selectedAs] = false;
13598 }
13599 this.$.selector.deselect(item);
13600 this.updateSizeForItem(item);
13601 },
13602
13603 /**
13604 * Select or deselect a given item depending on whether the item
13605 * has already been selected.
13606 *
13607 * @method toggleSelectionForItem
13608 * @param {(Object|number)} item The item object or its index
13609 */
13610 toggleSelectionForItem: function(item) {
13611 item = this._getNormalizedItem(item);
13612 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) {
13613 this.deselectItem(item);
13614 } else {
13615 this.selectItem(item);
13616 }
13617 },
13618
13619 /**
13620 * Clears the current selection state of the list.
13621 *
13622 * @method clearSelection
13623 */
13624 clearSelection: function() {
13625 function unselect(item) {
13626 var model = this._getModelFromItem(item);
13627 if (model) {
13628 model[this.selectedAs] = false;
13629 }
13630 }
13631
13632 if (Array.isArray(this.selectedItems)) {
13633 this.selectedItems.forEach(unselect, this);
13634 } else if (this.selectedItem) {
13635 unselect.call(this, this.selectedItem);
13636 }
13637
13638 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
13639 },
13640
13641 /**
13642 * Add an event listener to `tap` if `selectionEnabled` is true,
13643 * it will remove the listener otherwise.
13644 */
13645 _selectionEnabledChanged: function(selectionEnabled) {
13646 var handler = selectionEnabled ? this.listen : this.unlisten;
13647 handler.call(this, this, 'tap', '_selectionHandler');
13648 },
13649
13650 /**
13651 * Select an item from an event object.
13652 */
13653 _selectionHandler: function(e) {
13654 var model = this.modelForElement(e.target);
13655 if (!model) {
13656 return;
13657 }
13658 var modelTabIndex, activeElTabIndex;
13659 var target = Polymer.dom(e).path[0];
13660 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
13661 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
13662 // Safari does not focus certain form controls via mouse
13663 // https://bugs.webkit.org/show_bug.cgi?id=118043
13664 if (target.localName === 'input' ||
13665 target.localName === 'button' ||
13666 target.localName === 'select') {
13667 return;
13668 }
13669 // Set a temporary tabindex
13670 modelTabIndex = model.tabIndex;
13671 model.tabIndex = SECRET_TABINDEX;
13672 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
13673 model.tabIndex = modelTabIndex;
13674 // Only select the item if the tap wasn't on a focusable child
13675 // or the element bound to `tabIndex`
13676 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
13677 return;
13678 }
13679 this.toggleSelectionForItem(model[this.as]);
13680 },
13681
13682 _multiSelectionChanged: function(multiSelection) {
13683 this.clearSelection();
13684 this.$.selector.multi = multiSelection;
13685 },
13686
13687 /**
13688 * Updates the size of an item.
13689 *
13690 * @method updateSizeForItem
13691 * @param {(Object|number)} item The item object or its index
13692 */
13693 updateSizeForItem: function(item) {
13694 item = this._getNormalizedItem(item);
13695 var key = this._collection.getKey(item);
13696 var pidx = this._physicalIndexForKey[key];
13697
13698 if (pidx != null) {
13699 this._updateMetrics([pidx]);
13700 this._positionItems();
13701 }
13702 },
13703
13704 /**
13705 * Creates a temporary backfill item in the rendered pool of physical items
13706 * to replace the main focused item. The focused item has tabIndex = 0
13707 * and might be currently focused by the user.
13708 *
13709 * This dynamic replacement helps to preserve the focus state.
13710 */
13711 _manageFocus: function() {
13712 var fidx = this._focusedIndex;
13713
13714 if (fidx >= 0 && fidx < this._virtualCount) {
13715 // if it's a valid index, check if that index is rendered
13716 // in a physical item.
13717 if (this._isIndexRendered(fidx)) {
13718 this._restoreFocusedItem();
13719 } else {
13720 this._createFocusBackfillItem();
13721 }
13722 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
13723 // otherwise, assign the initial focused index.
13724 this._focusedIndex = this._virtualStart;
13725 this._focusedItem = this._physicalItems[this._physicalStart];
13726 }
13727 },
13728
13729 _isIndexRendered: function(idx) {
13730 return idx >= this._virtualStart && idx <= this._virtualEnd;
13731 },
13732
13733 _isIndexVisible: function(idx) {
13734 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
13735 },
13736
13737 _getPhysicalIndex: function(idx) {
13738 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
13739 },
13740
13741 _focusPhysicalItem: function(idx) {
13742 if (idx < 0 || idx >= this._virtualCount) {
13743 return;
13744 }
13745 this._restoreFocusedItem();
13746 // scroll to index to make sure it's rendered
13747 if (!this._isIndexRendered(idx)) {
13748 this.scrollToIndex(idx);
13749 }
13750
13751 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
13752 var model = physicalItem._templateInstance;
13753 var focusable;
13754
13755 // set a secret tab index
13756 model.tabIndex = SECRET_TABINDEX;
13757 // check if focusable element is the physical item
13758 if (physicalItem.tabIndex === SECRET_TABINDEX) {
13759 focusable = physicalItem;
13760 }
13761 // search for the element which tabindex is bound to the secret tab index
13762 if (!focusable) {
13763 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
13764 }
13765 // restore the tab index
13766 model.tabIndex = 0;
13767 // focus the focusable element
13768 this._focusedIndex = idx;
13769 focusable && focusable.focus();
13770 },
13771
13772 _removeFocusedItem: function() {
13773 if (this._offscreenFocusedItem) {
13774 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
13775 }
13776 this._offscreenFocusedItem = null;
13777 this._focusBackfillItem = null;
13778 this._focusedItem = null;
13779 this._focusedIndex = -1;
13780 },
13781
13782 _createFocusBackfillItem: function() {
13783 var pidx, fidx = this._focusedIndex;
13784 if (this._offscreenFocusedItem || fidx < 0) {
13785 return;
13786 }
13787 if (!this._focusBackfillItem) {
13788 // create a physical item, so that it backfills the focused item.
13789 var stampedTemplate = this.stamp(null);
13790 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
13791 Polymer.dom(this).appendChild(stampedTemplate.root);
13792 }
13793 // get the physical index for the focused index
13794 pidx = this._getPhysicalIndex(fidx);
13795
13796 if (pidx != null) {
13797 // set the offcreen focused physical item
13798 this._offscreenFocusedItem = this._physicalItems[pidx];
13799 // backfill the focused physical item
13800 this._physicalItems[pidx] = this._focusBackfillItem;
13801 // hide the focused physical
13802 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
13803 }
13804 },
13805
13806 _restoreFocusedItem: function() {
13807 var pidx, fidx = this._focusedIndex;
13808
13809 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
13810 return;
13811 }
13812 // assign models to the focused index
13813 this._assignModels();
13814 // get the new physical index for the focused index
13815 pidx = this._getPhysicalIndex(fidx);
13816
13817 if (pidx != null) {
13818 // flip the focus backfill
13819 this._focusBackfillItem = this._physicalItems[pidx];
13820 // restore the focused physical item
13821 this._physicalItems[pidx] = this._offscreenFocusedItem;
13822 // reset the offscreen focused item
13823 this._offscreenFocusedItem = null;
13824 // hide the physical item that backfills
13825 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
13826 }
13827 },
13828
13829 _didFocus: function(e) {
13830 var targetModel = this.modelForElement(e.target);
13831 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
13832 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
13833 var fidx = this._focusedIndex;
13834
13835 if (!targetModel || !focusedModel) {
13836 return;
13837 }
13838 if (focusedModel === targetModel) {
13839 // if the user focused the same item, then bring it into view if it's no t visible
13840 if (!this._isIndexVisible(fidx)) {
13841 this.scrollToIndex(fidx);
13842 }
13843 } else {
13844 this._restoreFocusedItem();
13845 // restore tabIndex for the currently focused item
13846 focusedModel.tabIndex = -1;
13847 // set the tabIndex for the next focused item
13848 targetModel.tabIndex = 0;
13849 fidx = targetModel[this.indexAs];
13850 this._focusedIndex = fidx;
13851 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
13852
13853 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
13854 this._update();
13855 }
13856 }
13857 },
13858
13859 _didMoveUp: function() {
13860 this._focusPhysicalItem(this._focusedIndex - 1);
13861 },
13862
13863 _didMoveDown: function(e) {
13864 // disable scroll when pressing the down key
13865 e.detail.keyboardEvent.preventDefault();
13866 this._focusPhysicalItem(this._focusedIndex + 1);
13867 },
13868
13869 _didEnter: function(e) {
13870 this._focusPhysicalItem(this._focusedIndex);
13871 this._selectionHandler(e.detail.keyboardEvent);
11306 } 13872 }
11307 }); 13873 });
11308 13874
11309 /* 13875 })();
11310 The `iron-input-validate` event is fired whenever `validate()` is called.
11311 @event iron-input-validate
11312 */
11313 Polymer({ 13876 Polymer({
11314 is: 'paper-input-container', 13877
13878 is: 'iron-scroll-threshold',
11315 13879
11316 properties: { 13880 properties: {
13881
11317 /** 13882 /**
11318 * Set to true to disable the floating label. The label disappears when th e input value is 13883 * Distance from the top (or left, for horizontal) bound of the scroller
11319 * not null. 13884 * where the "upper trigger" will fire.
11320 */ 13885 */
11321 noLabelFloat: { 13886 upperThreshold: {
13887 type: Number,
13888 value: 100
13889 },
13890
13891 /**
13892 * Distance from the bottom (or right, for horizontal) bound of the scroll er
13893 * where the "lower trigger" will fire.
13894 */
13895 lowerThreshold: {
13896 type: Number,
13897 value: 100
13898 },
13899
13900 /**
13901 * Read-only value that tracks the triggered state of the upper threshold.
13902 */
13903 upperTriggered: {
13904 type: Boolean,
13905 value: false,
13906 notify: true,
13907 readOnly: true
13908 },
13909
13910 /**
13911 * Read-only value that tracks the triggered state of the lower threshold.
13912 */
13913 lowerTriggered: {
13914 type: Boolean,
13915 value: false,
13916 notify: true,
13917 readOnly: true
13918 },
13919
13920 /**
13921 * True if the orientation of the scroller is horizontal.
13922 */
13923 horizontal: {
11322 type: Boolean, 13924 type: Boolean,
11323 value: false 13925 value: false
11324 }, 13926 }
11325 13927 },
11326 /** 13928
11327 * Set to true to always float the floating label. 13929 behaviors: [
11328 */ 13930 Polymer.IronScrollTargetBehavior
11329 alwaysFloatLabel: { 13931 ],
11330 type: Boolean, 13932
11331 value: false 13933 observers: [
11332 }, 13934 '_setOverflow(scrollTarget)',
11333 13935 '_initCheck(horizontal, isAttached)'
11334 /** 13936 ],
11335 * The attribute to listen for value changes on. 13937
11336 */ 13938 get _defaultScrollTarget() {
11337 attrForValue: { 13939 return this;
11338 type: String, 13940 },
11339 value: 'bind-value' 13941
11340 }, 13942 _setOverflow: function(scrollTarget) {
11341 13943 this.style.overflow = scrollTarget === this ? 'auto' : '';
11342 /** 13944 },
11343 * Set to true to auto-validate the input value when it changes. 13945
11344 */ 13946 _scrollHandler: function() {
11345 autoValidate: { 13947 // throttle the work on the scroll event
11346 type: Boolean, 13948 var THROTTLE_THRESHOLD = 200;
11347 value: false 13949 if (!this.isDebouncerActive('_checkTheshold')) {
11348 }, 13950 this.debounce('_checkTheshold', function() {
11349 13951 this.checkScrollThesholds();
11350 /** 13952 }, THROTTLE_THRESHOLD);
11351 * True if the input is invalid. This property is set automatically when t he input value 13953 }
11352 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child. 13954 },
11353 */ 13955
11354 invalid: { 13956 _initCheck: function(horizontal, isAttached) {
11355 observer: '_invalidChanged', 13957 if (isAttached) {
11356 type: Boolean, 13958 this.debounce('_init', function() {
11357 value: false 13959 this.clearTriggers();
11358 }, 13960 this.checkScrollThesholds();
11359 13961 });
11360 /** 13962 }
11361 * True if the input has focus. 13963 },
11362 */ 13964
11363 focused: { 13965 /**
11364 readOnly: true, 13966 * Checks the scroll thresholds.
11365 type: Boolean, 13967 * This method is automatically called by iron-scroll-threshold.
11366 value: false, 13968 *
11367 notify: true 13969 * @method checkScrollThesholds
11368 }, 13970 */
11369 13971 checkScrollThesholds: function() {
11370 _addons: { 13972 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
11371 type: Array 13973 return;
11372 // do not set a default value here intentionally - it will be initialize d lazily when a 13974 }
11373 // distributed child is attached, which may occur before configuration f or this element 13975 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo p;
11374 // in polyfill. 13976 var lowerScrollValue = this.horizontal ?
11375 }, 13977 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll Left :
11376 13978 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._ scrollTop;
11377 _inputHasContent: { 13979
11378 type: Boolean, 13980 // Detect upper threshold
11379 value: false 13981 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
11380 }, 13982 this._setUpperTriggered(true);
11381 13983 this.fire('upper-threshold');
11382 _inputSelector: { 13984 }
11383 type: String, 13985 // Detect lower threshold
11384 value: 'input,textarea,.paper-input-input' 13986 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
11385 }, 13987 this._setLowerTriggered(true);
11386 13988 this.fire('lower-threshold');
11387 _boundOnFocus: { 13989 }
11388 type: Function, 13990 },
11389 value: function() { 13991
11390 return this._onFocus.bind(this); 13992 /**
11391 } 13993 * Clear the upper and lower threshold states.
11392 }, 13994 *
11393 13995 * @method clearTriggers
11394 _boundOnBlur: { 13996 */
11395 type: Function, 13997 clearTriggers: function() {
11396 value: function() { 13998 this._setUpperTriggered(false);
11397 return this._onBlur.bind(this); 13999 this._setLowerTriggered(false);
11398 }
11399 },
11400
11401 _boundOnInput: {
11402 type: Function,
11403 value: function() {
11404 return this._onInput.bind(this);
11405 }
11406 },
11407
11408 _boundValueChanged: {
11409 type: Function,
11410 value: function() {
11411 return this._onValueChanged.bind(this);
11412 }
11413 }
11414 },
11415
11416 listeners: {
11417 'addon-attached': '_onAddonAttached',
11418 'iron-input-validate': '_onIronInputValidate'
11419 },
11420
11421 get _valueChangedEvent() {
11422 return this.attrForValue + '-changed';
11423 },
11424
11425 get _propertyForValue() {
11426 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
11427 },
11428
11429 get _inputElement() {
11430 return Polymer.dom(this).querySelector(this._inputSelector);
11431 },
11432
11433 get _inputElementValue() {
11434 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
11435 },
11436
11437 ready: function() {
11438 if (!this._addons) {
11439 this._addons = [];
11440 }
11441 this.addEventListener('focus', this._boundOnFocus, true);
11442 this.addEventListener('blur', this._boundOnBlur, true);
11443 },
11444
11445 attached: function() {
11446 if (this.attrForValue) {
11447 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
11448 } else {
11449 this.addEventListener('input', this._onInput);
11450 }
11451
11452 // Only validate when attached if the input already has a value.
11453 if (this._inputElementValue != '') {
11454 this._handleValueAndAutoValidate(this._inputElement);
11455 } else {
11456 this._handleValue(this._inputElement);
11457 }
11458 },
11459
11460 _onAddonAttached: function(event) {
11461 if (!this._addons) {
11462 this._addons = [];
11463 }
11464 var target = event.target;
11465 if (this._addons.indexOf(target) === -1) {
11466 this._addons.push(target);
11467 if (this.isAttached) {
11468 this._handleValue(this._inputElement);
11469 }
11470 }
11471 },
11472
11473 _onFocus: function() {
11474 this._setFocused(true);
11475 },
11476
11477 _onBlur: function() {
11478 this._setFocused(false);
11479 this._handleValueAndAutoValidate(this._inputElement);
11480 },
11481
11482 _onInput: function(event) {
11483 this._handleValueAndAutoValidate(event.target);
11484 },
11485
11486 _onValueChanged: function(event) {
11487 this._handleValueAndAutoValidate(event.target);
11488 },
11489
11490 _handleValue: function(inputElement) {
11491 var value = this._inputElementValue;
11492
11493 // type="number" hack needed because this.value is empty until it's valid
11494 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
11495 this._inputHasContent = true;
11496 } else {
11497 this._inputHasContent = false;
11498 }
11499
11500 this.updateAddons({
11501 inputElement: inputElement,
11502 value: value,
11503 invalid: this.invalid
11504 });
11505 },
11506
11507 _handleValueAndAutoValidate: function(inputElement) {
11508 if (this.autoValidate) {
11509 var valid;
11510 if (inputElement.validate) {
11511 valid = inputElement.validate(this._inputElementValue);
11512 } else {
11513 valid = inputElement.checkValidity();
11514 }
11515 this.invalid = !valid;
11516 }
11517
11518 // Call this last to notify the add-ons.
11519 this._handleValue(inputElement);
11520 },
11521
11522 _onIronInputValidate: function(event) {
11523 this.invalid = this._inputElement.invalid;
11524 },
11525
11526 _invalidChanged: function() {
11527 if (this._addons) {
11528 this.updateAddons({invalid: this.invalid});
11529 }
11530 },
11531
11532 /**
11533 * Call this to update the state of add-ons.
11534 * @param {Object} state Add-on state.
11535 */
11536 updateAddons: function(state) {
11537 for (var addon, index = 0; addon = this._addons[index]; index++) {
11538 addon.update(state);
11539 }
11540 },
11541
11542 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
11543 var cls = 'input-content';
11544 if (!noLabelFloat) {
11545 var label = this.querySelector('label');
11546
11547 if (alwaysFloatLabel || _inputHasContent) {
11548 cls += ' label-is-floating';
11549 // If the label is floating, ignore any offsets that may have been
11550 // applied from a prefix element.
11551 this.$.labelAndInputContainer.style.position = 'static';
11552
11553 if (invalid) {
11554 cls += ' is-invalid';
11555 } else if (focused) {
11556 cls += " label-is-highlighted";
11557 }
11558 } else {
11559 // When the label is not floating, it should overlap the input element .
11560 if (label) {
11561 this.$.labelAndInputContainer.style.position = 'relative';
11562 }
11563 }
11564 } else {
11565 if (_inputHasContent) {
11566 cls += ' label-is-hidden';
11567 }
11568 }
11569 return cls;
11570 },
11571
11572 _computeUnderlineClass: function(focused, invalid) {
11573 var cls = 'underline';
11574 if (invalid) {
11575 cls += ' is-invalid';
11576 } else if (focused) {
11577 cls += ' is-highlighted'
11578 }
11579 return cls;
11580 },
11581
11582 _computeAddOnContentClass: function(focused, invalid) {
11583 var cls = 'add-on-content';
11584 if (invalid) {
11585 cls += ' is-invalid';
11586 } else if (focused) {
11587 cls += ' is-highlighted'
11588 }
11589 return cls;
11590 } 14000 }
14001
14002 /**
14003 * Fires when the lower threshold has been reached.
14004 *
14005 * @event lower-threshold
14006 */
14007
14008 /**
14009 * Fires when the upper threshold has been reached.
14010 *
14011 * @event upper-threshold
14012 */
14013
11591 }); 14014 });
11592 // Copyright 2015 The Chromium Authors. All rights reserved. 14015 // Copyright 2015 The Chromium Authors. All rights reserved.
11593 // Use of this source code is governed by a BSD-style license that can be 14016 // Use of this source code is governed by a BSD-style license that can be
11594 // found in the LICENSE file. 14017 // found in the LICENSE file.
11595 14018
11596 var SearchField = Polymer({ 14019 Polymer({
11597 is: 'cr-search-field', 14020 is: 'history-list',
11598 14021
11599 behaviors: [CrSearchFieldBehavior], 14022 behaviors: [HistoryListBehavior],
11600 14023
11601 properties: { 14024 properties: {
11602 value_: String, 14025 // The search term for the current query. Set when the query returns.
11603 }, 14026 searchedTerm: {
11604 14027 type: String,
11605 /** @return {!HTMLInputElement} */ 14028 value: '',
11606 getSearchInput: function() { 14029 },
11607 return this.$.searchInput; 14030
11608 }, 14031 lastSearchedTerm_: String,
11609 14032
11610 /** @private */ 14033 querying: Boolean,
11611 clearSearch_: function() { 14034
11612 this.setValue(''); 14035 // An array of history entries in reverse chronological order.
11613 this.getSearchInput().focus(); 14036 historyData_: Array,
11614 }, 14037
11615 14038 resultLoadingDisabled_: {
11616 /** @private */ 14039 type: Boolean,
11617 toggleShowingSearch_: function() { 14040 value: false,
11618 this.showingSearch = !this.showingSearch; 14041 },
14042 },
14043
14044 listeners: {
14045 'infinite-list.scroll': 'notifyListScroll_',
14046 'remove-bookmark-stars': 'removeBookmarkStars_',
14047 },
14048
14049 /** @override */
14050 attached: function() {
14051 // It is possible (eg, when middle clicking the reload button) for all other
14052 // resize events to fire before the list is attached and can be measured.
14053 // Adding another resize here ensures it will get sized correctly.
14054 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize();
14055 },
14056
14057 /**
14058 * Remove bookmark star for history items with matching URLs.
14059 * @param {{detail: !string}} e
14060 * @private
14061 */
14062 removeBookmarkStars_: function(e) {
14063 var url = e.detail;
14064
14065 if (this.historyData_ === undefined)
14066 return;
14067
14068 for (var i = 0; i < this.historyData_.length; i++) {
14069 if (this.historyData_[i].url == url)
14070 this.set('historyData_.' + i + '.starred', false);
14071 }
14072 },
14073
14074 /**
14075 * Disables history result loading when there are no more history results.
14076 */
14077 disableResultLoading: function() {
14078 this.resultLoadingDisabled_ = true;
14079 },
14080
14081 /**
14082 * Adds the newly updated history results into historyData_. Adds new fields
14083 * for each result.
14084 * @param {!Array<!HistoryEntry>} historyResults The new history results.
14085 */
14086 addNewResults: function(historyResults) {
14087 var results = historyResults.slice();
14088 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold'])
14089 .clearTriggers();
14090
14091 if (this.lastSearchedTerm_ != this.searchedTerm) {
14092 this.resultLoadingDisabled_ = false;
14093 if (this.historyData_)
14094 this.splice('historyData_', 0, this.historyData_.length);
14095 this.fire('unselect-all');
14096 this.lastSearchedTerm_ = this.searchedTerm;
14097 }
14098
14099 if (this.historyData_) {
14100 // If we have previously received data, push the new items onto the
14101 // existing array.
14102 results.unshift('historyData_');
14103 this.push.apply(this, results);
14104 } else {
14105 // The first time we receive data, use set() to ensure the iron-list is
14106 // initialized correctly.
14107 this.set('historyData_', results);
14108 }
14109 },
14110
14111 /**
14112 * Called when the page is scrolled to near the bottom of the list.
14113 * @private
14114 */
14115 loadMoreData_: function() {
14116 if (this.resultLoadingDisabled_ || this.querying)
14117 return;
14118
14119 this.fire('load-more-history');
14120 },
14121
14122 /**
14123 * Check whether the time difference between the given history item and the
14124 * next one is large enough for a spacer to be required.
14125 * @param {HistoryEntry} item
14126 * @param {number} index The index of |item| in |historyData_|.
14127 * @param {number} length The length of |historyData_|.
14128 * @return {boolean} Whether or not time gap separator is required.
14129 * @private
14130 */
14131 needsTimeGap_: function(item, index, length) {
14132 return md_history.HistoryItem.needsTimeGap(
14133 this.historyData_, index, this.searchedTerm);
14134 },
14135
14136 /**
14137 * True if the given item is the beginning of a new card.
14138 * @param {HistoryEntry} item
14139 * @param {number} i Index of |item| within |historyData_|.
14140 * @param {number} length
14141 * @return {boolean}
14142 * @private
14143 */
14144 isCardStart_: function(item, i, length) {
14145 if (length == 0 || i > length - 1)
14146 return false;
14147 return i == 0 ||
14148 this.historyData_[i].dateRelativeDay !=
14149 this.historyData_[i - 1].dateRelativeDay;
14150 },
14151
14152 /**
14153 * True if the given item is the end of a card.
14154 * @param {HistoryEntry} item
14155 * @param {number} i Index of |item| within |historyData_|.
14156 * @param {number} length
14157 * @return {boolean}
14158 * @private
14159 */
14160 isCardEnd_: function(item, i, length) {
14161 if (length == 0 || i > length - 1)
14162 return false;
14163 return i == length - 1 ||
14164 this.historyData_[i].dateRelativeDay !=
14165 this.historyData_[i + 1].dateRelativeDay;
14166 },
14167
14168 /**
14169 * @param {number} index
14170 * @return {boolean}
14171 * @private
14172 */
14173 isFirstItem_: function(index) {
14174 return index == 0;
14175 },
14176
14177 /**
14178 * @private
14179 */
14180 notifyListScroll_: function() {
14181 this.fire('history-list-scrolled');
14182 },
14183
14184 /**
14185 * @param {number} index
14186 * @return {string}
14187 * @private
14188 */
14189 pathForItem_: function(index) {
14190 return 'historyData_.' + index;
11619 }, 14191 },
11620 }); 14192 });
11621 // Copyright 2015 The Chromium Authors. All rights reserved. 14193 // Copyright 2016 The Chromium Authors. All rights reserved.
11622 // Use of this source code is governed by a BSD-style license that can be 14194 // Use of this source code is governed by a BSD-style license that can be
11623 // found in the LICENSE file. 14195 // found in the LICENSE file.
11624 14196
11625 cr.define('downloads', function() { 14197 Polymer({
11626 var Toolbar = Polymer({ 14198 is: 'history-list-container',
11627 is: 'downloads-toolbar', 14199
11628 14200 properties: {
11629 attached: function() { 14201 // The path of the currently selected page.
11630 // isRTL() only works after i18n_template.js runs to set <html dir>. 14202 selectedPage_: String,
11631 this.overflowAlign_ = isRTL() ? 'left' : 'right'; 14203
11632 }, 14204 // Whether domain-grouped history is enabled.
11633 14205 grouped: Boolean,
11634 properties: { 14206
11635 downloadsShowing: { 14207 /** @type {!QueryState} */
11636 reflectToAttribute: true, 14208 queryState: Object,
11637 type: Boolean, 14209
11638 value: false, 14210 /** @type {!QueryResult} */
11639 observer: 'downloadsShowingChanged_', 14211 queryResult: Object,
11640 }, 14212 },
11641 14213
11642 overflowAlign_: { 14214 observers: [
11643 type: String, 14215 'groupedRangeChanged_(queryState.range)',
11644 value: 'right', 14216 ],
11645 }, 14217
11646 }, 14218 listeners: {
11647 14219 'history-list-scrolled': 'closeMenu_',
11648 listeners: { 14220 'load-more-history': 'loadMoreHistory_',
11649 'paper-dropdown-close': 'onPaperDropdownClose_', 14221 'toggle-menu': 'toggleMenu_',
11650 'paper-dropdown-open': 'onPaperDropdownOpen_', 14222 },
11651 }, 14223
11652 14224 /**
11653 /** @return {boolean} Whether removal can be undone. */ 14225 * @param {HistoryQuery} info An object containing information about the
11654 canUndo: function() { 14226 * query.
11655 return this.$['search-input'] != this.shadowRoot.activeElement; 14227 * @param {!Array<HistoryEntry>} results A list of results.
11656 }, 14228 */
11657 14229 historyResult: function(info, results) {
11658 /** @return {boolean} Whether "Clear all" should be allowed. */ 14230 this.initializeResults_(info, results);
11659 canClearAll: function() { 14231 this.closeMenu_();
11660 return !this.$['search-input'].getValue() && this.downloadsShowing; 14232
11661 }, 14233 if (this.selectedPage_ == 'grouped-list') {
11662 14234 this.$$('#grouped-list').historyData = results;
11663 onFindCommand: function() { 14235 return;
11664 this.$['search-input'].showAndFocus(); 14236 }
11665 }, 14237
11666 14238 var list = /** @type {HistoryListElement} */(this.$['infinite-list']);
11667 /** @private */ 14239 list.addNewResults(results);
11668 closeMoreActions_: function() { 14240 if (info.finished)
11669 this.$.more.close(); 14241 list.disableResultLoading();
11670 }, 14242 },
11671 14243
11672 /** @private */ 14244 /**
11673 downloadsShowingChanged_: function() { 14245 * Queries the history backend for results based on queryState.
11674 this.updateClearAll_(); 14246 * @param {boolean} incremental Whether the new query should continue where
11675 }, 14247 * the previous query stopped.
11676 14248 */
11677 /** @private */ 14249 queryHistory: function(incremental) {
11678 onClearAllTap_: function() { 14250 var queryState = this.queryState;
11679 assert(this.canClearAll()); 14251 // Disable querying until the first set of results have been returned. If
11680 downloads.ActionService.getInstance().clearAll(); 14252 // there is a search, query immediately to support search query params from
11681 }, 14253 // the URL.
11682 14254 var noResults = !this.queryResult || this.queryResult.results == null;
11683 /** @private */ 14255 if (queryState.queryingDisabled ||
11684 onPaperDropdownClose_: function() { 14256 (!this.queryState.searchTerm && noResults)) {
11685 window.removeEventListener('resize', assert(this.boundClose_)); 14257 return;
11686 }, 14258 }
11687 14259
11688 /** 14260 // Close any open dialog if a new query is initiated.
11689 * @param {!Event} e 14261 if (!incremental && this.$.dialog.open)
11690 * @private 14262 this.$.dialog.close();
11691 */ 14263
11692 onItemBlur_: function(e) { 14264 this.set('queryState.querying', true);
11693 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu')); 14265 this.set('queryState.incremental', incremental);
11694 if (menu.items.indexOf(e.relatedTarget) >= 0) 14266
11695 return; 14267 var lastVisitTime = 0;
11696 14268 if (incremental) {
11697 this.$.more.restoreFocusOnClose = false; 14269 var lastVisit = this.queryResult.results.slice(-1)[0];
11698 this.closeMoreActions_(); 14270 lastVisitTime = lastVisit ? lastVisit.time : 0;
11699 this.$.more.restoreFocusOnClose = true; 14271 }
11700 }, 14272
11701 14273 var maxResults =
11702 /** @private */ 14274 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0;
11703 onPaperDropdownOpen_: function() { 14275 chrome.send('queryHistory', [
11704 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this); 14276 queryState.searchTerm, queryState.groupedOffset, queryState.range,
11705 window.addEventListener('resize', this.boundClose_); 14277 lastVisitTime, maxResults
11706 }, 14278 ]);
11707 14279 },
11708 /** 14280
11709 * @param {!CustomEvent} event 14281 unselectAllItems: function(count) {
11710 * @private 14282 this.getSelectedList_().unselectAllItems(count);
11711 */ 14283 },
11712 onSearchChanged_: function(event) { 14284
11713 downloads.ActionService.getInstance().search( 14285 /**
11714 /** @type {string} */ (event.detail)); 14286 * Delete all the currently selected history items. Will prompt the user with
11715 this.updateClearAll_(); 14287 * a dialog to confirm that the deletion should be performed.
11716 }, 14288 */
11717 14289 deleteSelectedWithPrompt: function() {
11718 /** @private */ 14290 if (!loadTimeData.getBoolean('allowDeletingHistory'))
11719 onOpenDownloadsFolderTap_: function() { 14291 return;
11720 downloads.ActionService.getInstance().openDownloadsFolder(); 14292
11721 }, 14293 this.$.dialog.showModal();
11722 14294 },
11723 /** @private */ 14295
11724 updateClearAll_: function() { 14296 /**
11725 this.$$('#actions .clear-all').hidden = !this.canClearAll(); 14297 * @param {HistoryRange} range
11726 this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); 14298 * @private
11727 }, 14299 */
11728 }); 14300 groupedRangeChanged_: function(range) {
11729 14301 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ?
11730 return {Toolbar: Toolbar}; 14302 'infinite-list' : 'grouped-list';
14303
14304 this.queryHistory(false);
14305 },
14306
14307 /** @private */
14308 loadMoreHistory_: function() { this.queryHistory(true); },
14309
14310 /**
14311 * @param {HistoryQuery} info
14312 * @param {!Array<HistoryEntry>} results
14313 * @private
14314 */
14315 initializeResults_: function(info, results) {
14316 if (results.length == 0)
14317 return;
14318
14319 var currentDate = results[0].dateRelativeDay;
14320
14321 for (var i = 0; i < results.length; i++) {
14322 // Sets the default values for these fields to prevent undefined types.
14323 results[i].selected = false;
14324 results[i].readableTimestamp =
14325 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
14326
14327 if (results[i].dateRelativeDay != currentDate) {
14328 currentDate = results[i].dateRelativeDay;
14329 }
14330 }
14331 },
14332
14333 /** @private */
14334 onDialogConfirmTap_: function() {
14335 this.getSelectedList_().deleteSelected();
14336 this.$.dialog.close();
14337 },
14338
14339 /** @private */
14340 onDialogCancelTap_: function() {
14341 this.$.dialog.close();
14342 },
14343
14344 /**
14345 * Closes the overflow menu.
14346 * @private
14347 */
14348 closeMenu_: function() {
14349 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).closeMenu();
14350 },
14351
14352 /**
14353 * Opens the overflow menu unless the menu is already open and the same button
14354 * is pressed.
14355 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e
14356 * @private
14357 */
14358 toggleMenu_: function(e) {
14359 var target = e.detail.target;
14360 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).toggleMenu(
14361 target, e.detail.item);
14362 },
14363
14364 /** @private */
14365 onMoreFromSiteTap_: function() {
14366 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
14367 this.fire('search-domain', {domain: menu.itemData.domain});
14368 menu.closeMenu();
14369 },
14370
14371 /** @private */
14372 onRemoveFromHistoryTap_: function() {
14373 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
14374 md_history.BrowserService.getInstance()
14375 .deleteItems([menu.itemData])
14376 .then(function(items) {
14377 this.getSelectedList_().removeItemsByPath(items[0].path);
14378 // This unselect-all is to reset the toolbar when deleting a selected
14379 // item. TODO(tsergeant): Make this automatic based on observing list
14380 // modifications.
14381 this.fire('unselect-all');
14382 }.bind(this));
14383 menu.closeMenu();
14384 },
14385
14386 /**
14387 * @return {HTMLElement}
14388 * @private
14389 */
14390 getSelectedList_: function() {
14391 return this.$.content.selectedItem;
14392 },
11731 }); 14393 });
11732 // Copyright 2015 The Chromium Authors. All rights reserved. 14394 // Copyright 2016 The Chromium Authors. All rights reserved.
11733 // Use of this source code is governed by a BSD-style license that can be 14395 // Use of this source code is governed by a BSD-style license that can be
11734 // found in the LICENSE file. 14396 // found in the LICENSE file.
11735 14397
11736 cr.define('downloads', function() { 14398 Polymer({
11737 var Manager = Polymer({ 14399 is: 'history-synced-device-card',
11738 is: 'downloads-manager', 14400
11739 14401 properties: {
11740 properties: { 14402 // Name of the synced device.
11741 hasDownloads_: { 14403 device: String,
11742 observer: 'hasDownloadsChanged_', 14404
11743 type: Boolean, 14405 // When the device information was last updated.
11744 }, 14406 lastUpdateTime: String,
11745 14407
11746 items_: { 14408 /**
11747 type: Array, 14409 * The list of tabs open for this device.
11748 value: function() { return []; }, 14410 * @type {!Array<!ForeignSessionTab>}
11749 }, 14411 */
11750 }, 14412 tabs: {
11751 14413 type: Array,
11752 hostAttributes: { 14414 value: function() { return []; },
11753 loading: true, 14415 observer: 'updateIcons_'
11754 }, 14416 },
11755 14417
11756 listeners: { 14418 /**
11757 'downloads-list.scroll': 'onListScroll_', 14419 * The indexes where a window separator should be shown. The use of a
11758 }, 14420 * separate array here is necessary for window separators to appear
11759 14421 * correctly in search. See http://crrev.com/2022003002 for more details.
11760 observers: [ 14422 * @type {!Array<number>}
11761 'itemsChanged_(items_.*)', 14423 */
11762 ], 14424 separatorIndexes: Array,
11763 14425
11764 /** @private */ 14426 // Whether the card is open.
11765 clearAll_: function() { 14427 cardOpen_: {type: Boolean, value: true},
11766 this.set('items_', []); 14428
11767 }, 14429 searchTerm: String,
11768 14430
11769 /** @private */ 14431 // Internal identifier for the device.
11770 hasDownloadsChanged_: function() { 14432 sessionTag: String,
11771 if (loadTimeData.getBoolean('allowDeletingHistory')) 14433 },
11772 this.$.toolbar.downloadsShowing = this.hasDownloads_; 14434
11773 14435 /**
11774 if (this.hasDownloads_) { 14436 * Open a single synced tab. Listens to 'click' rather than 'tap'
11775 this.$['downloads-list'].fire('iron-resize'); 14437 * to determine what modifier keys were pressed.
11776 } else { 14438 * @param {DomRepeatClickEvent} e
11777 var isSearching = downloads.ActionService.getInstance().isSearching(); 14439 * @private
11778 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads'; 14440 */
11779 this.$['no-downloads'].querySelector('span').textContent = 14441 openTab_: function(e) {
11780 loadTimeData.getString(messageToShow); 14442 var tab = /** @type {ForeignSessionTab} */(e.model.tab);
11781 } 14443 md_history.BrowserService.getInstance().openForeignSessionTab(
11782 }, 14444 this.sessionTag, tab.windowId, tab.sessionId, e);
11783 14445 e.preventDefault();
11784 /** 14446 },
11785 * @param {number} index 14447
11786 * @param {!Array<!downloads.Data>} list 14448 /**
11787 * @private 14449 * Toggles the dropdown display of synced tabs for each device card.
11788 */ 14450 */
11789 insertItems_: function(index, list) { 14451 toggleTabCard: function() {
11790 this.splice.apply(this, ['items_', index, 0].concat(list)); 14452 this.$.collapse.toggle();
11791 this.updateHideDates_(index, index + list.length); 14453 this.$['dropdown-indicator'].icon =
11792 this.removeAttribute('loading'); 14454 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more';
11793 }, 14455 },
11794 14456
11795 /** @private */ 14457 /**
11796 itemsChanged_: function() { 14458 * When the synced tab information is set, the icon associated with the tab
11797 this.hasDownloads_ = this.items_.length > 0; 14459 * website is also set.
11798 }, 14460 * @private
11799 14461 */
11800 /** 14462 updateIcons_: function() {
11801 * @param {Event} e 14463 this.async(function() {
11802 * @private 14464 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon');
11803 */ 14465
11804 onCanExecute_: function(e) { 14466 for (var i = 0; i < this.tabs.length; i++) {
11805 e = /** @type {cr.ui.CanExecuteEvent} */(e); 14467 icons[i].style.backgroundImage =
11806 switch (e.command.id) { 14468 cr.icon.getFaviconImageSet(this.tabs[i].url);
11807 case 'undo-command': 14469 }
11808 e.canExecute = this.$.toolbar.canUndo(); 14470 });
11809 break; 14471 },
11810 case 'clear-all-command': 14472
11811 e.canExecute = this.$.toolbar.canClearAll(); 14473 /** @private */
11812 break; 14474 isWindowSeparatorIndex_: function(index, separatorIndexes) {
11813 case 'find-command': 14475 return this.separatorIndexes.indexOf(index) != -1;
11814 e.canExecute = true; 14476 },
11815 break; 14477
11816 } 14478 /**
11817 }, 14479 * @param {boolean} cardOpen
11818 14480 * @return {string}
11819 /** 14481 */
11820 * @param {Event} e 14482 getCollapseTitle_: function(cardOpen) {
11821 * @private 14483 return cardOpen ? loadTimeData.getString('collapseSessionButton') :
11822 */ 14484 loadTimeData.getString('expandSessionButton');
11823 onCommand_: function(e) { 14485 },
11824 if (e.command.id == 'clear-all-command') 14486
11825 downloads.ActionService.getInstance().clearAll(); 14487 /**
11826 else if (e.command.id == 'undo-command') 14488 * @param {CustomEvent} e
11827 downloads.ActionService.getInstance().undo(); 14489 * @private
11828 else if (e.command.id == 'find-command') 14490 */
11829 this.$.toolbar.onFindCommand(); 14491 onMenuButtonTap_: function(e) {
11830 }, 14492 this.fire('toggle-menu', {
11831 14493 target: Polymer.dom(e).localTarget,
11832 /** @private */ 14494 tag: this.sessionTag
11833 onListScroll_: function() { 14495 });
11834 var list = this.$['downloads-list']; 14496 e.stopPropagation(); // Prevent iron-collapse.
11835 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) { 14497 },
11836 // Approaching the end of the scrollback. Attempt to load more items.
11837 downloads.ActionService.getInstance().loadMore();
11838 }
11839 },
11840
11841 /** @private */
11842 onLoad_: function() {
11843 cr.ui.decorate('command', cr.ui.Command);
11844 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
11845 document.addEventListener('command', this.onCommand_.bind(this));
11846
11847 downloads.ActionService.getInstance().loadMore();
11848 },
11849
11850 /**
11851 * @param {number} index
11852 * @private
11853 */
11854 removeItem_: function(index) {
11855 this.splice('items_', index, 1);
11856 this.updateHideDates_(index, index);
11857 this.onListScroll_();
11858 },
11859
11860 /**
11861 * @param {number} start
11862 * @param {number} end
11863 * @private
11864 */
11865 updateHideDates_: function(start, end) {
11866 for (var i = start; i <= end; ++i) {
11867 var current = this.items_[i];
11868 if (!current)
11869 continue;
11870 var prev = this.items_[i - 1];
11871 current.hideDate = !!prev && prev.date_string == current.date_string;
11872 }
11873 },
11874
11875 /**
11876 * @param {number} index
11877 * @param {!downloads.Data} data
11878 * @private
11879 */
11880 updateItem_: function(index, data) {
11881 this.set('items_.' + index, data);
11882 this.updateHideDates_(index, index);
11883 var list = /** @type {!IronListElement} */(this.$['downloads-list']);
11884 list.updateSizeForItem(index);
11885 },
11886 });
11887
11888 Manager.clearAll = function() {
11889 Manager.get().clearAll_();
11890 };
11891
11892 /** @return {!downloads.Manager} */
11893 Manager.get = function() {
11894 return /** @type {!downloads.Manager} */(
11895 queryRequiredElement('downloads-manager'));
11896 };
11897
11898 Manager.insertItems = function(index, list) {
11899 Manager.get().insertItems_(index, list);
11900 };
11901
11902 Manager.onLoad = function() {
11903 Manager.get().onLoad_();
11904 };
11905
11906 Manager.removeItem = function(index) {
11907 Manager.get().removeItem_(index);
11908 };
11909
11910 Manager.updateItem = function(index, data) {
11911 Manager.get().updateItem_(index, data);
11912 };
11913
11914 return {Manager: Manager};
11915 }); 14498 });
11916 // Copyright 2015 The Chromium Authors. All rights reserved. 14499 // Copyright 2016 The Chromium Authors. All rights reserved.
11917 // Use of this source code is governed by a BSD-style license that can be 14500 // Use of this source code is governed by a BSD-style license that can be
11918 // found in the LICENSE file. 14501 // found in the LICENSE file.
11919 14502
11920 window.addEventListener('load', downloads.Manager.onLoad); 14503 /**
14504 * @typedef {{device: string,
14505 * lastUpdateTime: string,
14506 * separatorIndexes: !Array<number>,
14507 * timestamp: number,
14508 * tabs: !Array<!ForeignSessionTab>,
14509 * tag: string}}
14510 */
14511 var ForeignDeviceInternal;
14512
14513 Polymer({
14514 is: 'history-synced-device-manager',
14515
14516 properties: {
14517 /**
14518 * @type {?Array<!ForeignSession>}
14519 */
14520 sessionList: {
14521 type: Array,
14522 observer: 'updateSyncedDevices'
14523 },
14524
14525 searchTerm: {
14526 type: String,
14527 observer: 'searchTermChanged'
14528 },
14529
14530 /**
14531 * An array of synced devices with synced tab data.
14532 * @type {!Array<!ForeignDeviceInternal>}
14533 */
14534 syncedDevices_: {
14535 type: Array,
14536 value: function() { return []; }
14537 },
14538
14539 /** @private */
14540 signInState_: {
14541 type: Boolean,
14542 value: loadTimeData.getBoolean('isUserSignedIn'),
14543 },
14544
14545 /** @private */
14546 guestSession_: {
14547 type: Boolean,
14548 value: loadTimeData.getBoolean('isGuestSession'),
14549 },
14550
14551 /** @private */
14552 fetchingSyncedTabs_: {
14553 type: Boolean,
14554 value: false,
14555 }
14556 },
14557
14558 listeners: {
14559 'toggle-menu': 'onToggleMenu_',
14560 },
14561
14562 /** @override */
14563 attached: function() {
14564 // Update the sign in state.
14565 chrome.send('otherDevicesInitialized');
14566 },
14567
14568 /**
14569 * @param {!ForeignSession} session
14570 * @return {!ForeignDeviceInternal}
14571 */
14572 createInternalDevice_: function(session) {
14573 var tabs = [];
14574 var separatorIndexes = [];
14575 for (var i = 0; i < session.windows.length; i++) {
14576 var windowId = session.windows[i].sessionId;
14577 var newTabs = session.windows[i].tabs;
14578 if (newTabs.length == 0)
14579 continue;
14580
14581 newTabs.forEach(function(tab) {
14582 tab.windowId = windowId;
14583 });
14584
14585 var windowAdded = false;
14586 if (!this.searchTerm) {
14587 // Add all the tabs if there is no search term.
14588 tabs = tabs.concat(newTabs);
14589 windowAdded = true;
14590 } else {
14591 var searchText = this.searchTerm.toLowerCase();
14592 for (var j = 0; j < newTabs.length; j++) {
14593 var tab = newTabs[j];
14594 if (tab.title.toLowerCase().indexOf(searchText) != -1) {
14595 tabs.push(tab);
14596 windowAdded = true;
14597 }
14598 }
14599 }
14600 if (windowAdded && i != session.windows.length - 1)
14601 separatorIndexes.push(tabs.length - 1);
14602 }
14603 return {
14604 device: session.name,
14605 lastUpdateTime: '– ' + session.modifiedTime,
14606 separatorIndexes: separatorIndexes,
14607 timestamp: session.timestamp,
14608 tabs: tabs,
14609 tag: session.tag,
14610 };
14611 },
14612
14613 onSignInTap_: function() {
14614 chrome.send('SyncSetupShowSetupUI');
14615 chrome.send('SyncSetupStartSignIn', [false]);
14616 },
14617
14618 onToggleMenu_: function(e) {
14619 this.$.menu.toggleMenu(e.detail.target, e.detail.tag);
14620 },
14621
14622 onOpenAllTap_: function() {
14623 md_history.BrowserService.getInstance().openForeignSessionAllTabs(
14624 this.$.menu.itemData);
14625 this.$.menu.closeMenu();
14626 },
14627
14628 onDeleteSessionTap_: function() {
14629 md_history.BrowserService.getInstance().deleteForeignSession(
14630 this.$.menu.itemData);
14631 this.$.menu.closeMenu();
14632 },
14633
14634 /** @private */
14635 clearDisplayedSyncedDevices_: function() {
14636 this.syncedDevices_ = [];
14637 },
14638
14639 /**
14640 * Decide whether or not should display no synced tabs message.
14641 * @param {boolean} signInState
14642 * @param {number} syncedDevicesLength
14643 * @param {boolean} guestSession
14644 * @return {boolean}
14645 */
14646 showNoSyncedMessage: function(
14647 signInState, syncedDevicesLength, guestSession) {
14648 if (guestSession)
14649 return true;
14650
14651 return signInState && syncedDevicesLength == 0;
14652 },
14653
14654 /**
14655 * Shows the signin guide when the user is not signed in and not in a guest
14656 * session.
14657 * @param {boolean} signInState
14658 * @param {boolean} guestSession
14659 * @return {boolean}
14660 */
14661 showSignInGuide: function(signInState, guestSession) {
14662 return !signInState && !guestSession;
14663 },
14664
14665 /**
14666 * Decide what message should be displayed when user is logged in and there
14667 * are no synced tabs.
14668 * @param {boolean} fetchingSyncedTabs
14669 * @return {string}
14670 */
14671 noSyncedTabsMessage: function(fetchingSyncedTabs) {
14672 return loadTimeData.getString(
14673 fetchingSyncedTabs ? 'loading' : 'noSyncedResults');
14674 },
14675
14676 /**
14677 * Replaces the currently displayed synced tabs with |sessionList|. It is
14678 * common for only a single session within the list to have changed, We try to
14679 * avoid doing extra work in this case. The logic could be more intelligent
14680 * about updating individual tabs rather than replacing whole sessions, but
14681 * this approach seems to have acceptable performance.
14682 * @param {?Array<!ForeignSession>} sessionList
14683 */
14684 updateSyncedDevices: function(sessionList) {
14685 this.fetchingSyncedTabs_ = false;
14686
14687 if (!sessionList)
14688 return;
14689
14690 // First, update any existing devices that have changed.
14691 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length);
14692 for (var i = 0; i < updateCount; i++) {
14693 var oldDevice = this.syncedDevices_[i];
14694 if (oldDevice.tag != sessionList[i].tag ||
14695 oldDevice.timestamp != sessionList[i].timestamp) {
14696 this.splice(
14697 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i]));
14698 }
14699 }
14700
14701 // Then, append any new devices.
14702 for (var i = updateCount; i < sessionList.length; i++) {
14703 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i]));
14704 }
14705 },
14706
14707 /**
14708 * Get called when user's sign in state changes, this will affect UI of synced
14709 * tabs page. Sign in promo gets displayed when user is signed out, and
14710 * different messages are shown when there are no synced tabs.
14711 * @param {boolean} isUserSignedIn
14712 */
14713 updateSignInState: function(isUserSignedIn) {
14714 // If user's sign in state didn't change, then don't change message or
14715 // update UI.
14716 if (this.signInState_ == isUserSignedIn)
14717 return;
14718
14719 this.signInState_ = isUserSignedIn;
14720
14721 // User signed out, clear synced device list and show the sign in promo.
14722 if (!isUserSignedIn) {
14723 this.clearDisplayedSyncedDevices_();
14724 return;
14725 }
14726 // User signed in, show the loading message when querying for synced
14727 // devices.
14728 this.fetchingSyncedTabs_ = true;
14729 },
14730
14731 searchTermChanged: function(searchTerm) {
14732 this.clearDisplayedSyncedDevices_();
14733 this.updateSyncedDevices(this.sessionList);
14734 }
14735 });
14736 /**
14737 `iron-selector` is an element which can be used to manage a list of elements
14738 that can be selected. Tapping on the item will make the item selected. The ` selected` indicates
14739 which item is being selected. The default is to use the index of the item.
14740
14741 Example:
14742
14743 <iron-selector selected="0">
14744 <div>Item 1</div>
14745 <div>Item 2</div>
14746 <div>Item 3</div>
14747 </iron-selector>
14748
14749 If you want to use the attribute value of an element for `selected` instead of the index,
14750 set `attrForSelected` to the name of the attribute. For example, if you want to select item by
14751 `name`, set `attrForSelected` to `name`.
14752
14753 Example:
14754
14755 <iron-selector attr-for-selected="name" selected="foo">
14756 <div name="foo">Foo</div>
14757 <div name="bar">Bar</div>
14758 <div name="zot">Zot</div>
14759 </iron-selector>
14760
14761 You can specify a default fallback with `fallbackSelection` in case the `selec ted` attribute does
14762 not match the `attrForSelected` attribute of any elements.
14763
14764 Example:
14765
14766 <iron-selector attr-for-selected="name" selected="non-existing"
14767 fallback-selection="default">
14768 <div name="foo">Foo</div>
14769 <div name="bar">Bar</div>
14770 <div name="default">Default</div>
14771 </iron-selector>
14772
14773 Note: When the selector is multi, the selection will set to `fallbackSelection ` iff
14774 the number of matching elements is zero.
14775
14776 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the selected element.
14777
14778 Example:
14779
14780 <style>
14781 .iron-selected {
14782 background: #eee;
14783 }
14784 </style>
14785
14786 ...
14787
14788 <iron-selector selected="0">
14789 <div>Item 1</div>
14790 <div>Item 2</div>
14791 <div>Item 3</div>
14792 </iron-selector>
14793
14794 @demo demo/index.html
14795 */
14796
14797 Polymer({
14798
14799 is: 'iron-selector',
14800
14801 behaviors: [
14802 Polymer.IronMultiSelectableBehavior
14803 ]
14804
14805 });
14806 // Copyright 2016 The Chromium Authors. All rights reserved.
14807 // Use of this source code is governed by a BSD-style license that can be
14808 // found in the LICENSE file.
14809
14810 Polymer({
14811 is: 'history-side-bar',
14812
14813 properties: {
14814 selectedPage: {
14815 type: String,
14816 notify: true
14817 },
14818
14819 route: Object,
14820
14821 showFooter: Boolean,
14822
14823 // If true, the sidebar is contained within an app-drawer.
14824 drawer: {
14825 type: Boolean,
14826 reflectToAttribute: true
14827 },
14828 },
14829
14830 /** @private */
14831 onSelectorActivate_: function() {
14832 this.fire('history-close-drawer');
14833 },
14834
14835 /**
14836 * Relocates the user to the clear browsing data section of the settings page.
14837 * @param {Event} e
14838 * @private
14839 */
14840 onClearBrowsingDataTap_: function(e) {
14841 md_history.BrowserService.getInstance().openClearBrowsingData();
14842 e.preventDefault();
14843 },
14844
14845 /**
14846 * @param {Object} route
14847 * @private
14848 */
14849 getQueryString_: function(route) {
14850 return window.location.search;
14851 }
14852 });
14853 // Copyright 2016 The Chromium Authors. All rights reserved.
14854 // Use of this source code is governed by a BSD-style license that can be
14855 // found in the LICENSE file.
14856
14857 Polymer({
14858 is: 'history-app',
14859
14860 properties: {
14861 showSidebarFooter: Boolean,
14862
14863 // The id of the currently selected page.
14864 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'},
14865
14866 // Whether domain-grouped history is enabled.
14867 grouped_: {type: Boolean, reflectToAttribute: true},
14868
14869 /** @type {!QueryState} */
14870 queryState_: {
14871 type: Object,
14872 value: function() {
14873 return {
14874 // Whether the most recent query was incremental.
14875 incremental: false,
14876 // A query is initiated by page load.
14877 querying: true,
14878 queryingDisabled: false,
14879 _range: HistoryRange.ALL_TIME,
14880 searchTerm: '',
14881 // TODO(calamity): Make history toolbar buttons change the offset
14882 groupedOffset: 0,
14883
14884 set range(val) { this._range = Number(val); },
14885 get range() { return this._range; },
14886 };
14887 }
14888 },
14889
14890 /** @type {!QueryResult} */
14891 queryResult_: {
14892 type: Object,
14893 value: function() {
14894 return {
14895 info: null,
14896 results: null,
14897 sessionList: null,
14898 };
14899 }
14900 },
14901
14902 // Route data for the current page.
14903 routeData_: Object,
14904
14905 // The query params for the page.
14906 queryParams_: Object,
14907
14908 // True if the window is narrow enough for the page to have a drawer.
14909 hasDrawer_: Boolean,
14910 },
14911
14912 observers: [
14913 // routeData_.page <=> selectedPage
14914 'routeDataChanged_(routeData_.page)',
14915 'selectedPageChanged_(selectedPage_)',
14916
14917 // queryParams_.q <=> queryState.searchTerm
14918 'searchTermChanged_(queryState_.searchTerm)',
14919 'searchQueryParamChanged_(queryParams_.q)',
14920
14921 ],
14922
14923 // TODO(calamity): Replace these event listeners with data bound properties.
14924 listeners: {
14925 'cr-menu-tap': 'onMenuTap_',
14926 'history-checkbox-select': 'checkboxSelected',
14927 'unselect-all': 'unselectAll',
14928 'delete-selected': 'deleteSelected',
14929 'search-domain': 'searchDomain_',
14930 'history-close-drawer': 'closeDrawer_',
14931 },
14932
14933 /** @override */
14934 ready: function() {
14935 this.grouped_ = loadTimeData.getBoolean('groupByDomain');
14936
14937 cr.ui.decorate('command', cr.ui.Command);
14938 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
14939 document.addEventListener('command', this.onCommand_.bind(this));
14940
14941 // Redirect legacy search URLs to URLs compatible with material history.
14942 if (window.location.hash) {
14943 window.location.href = window.location.href.split('#')[0] + '?' +
14944 window.location.hash.substr(1);
14945 }
14946 },
14947
14948 /** @private */
14949 onMenuTap_: function() {
14950 var drawer = this.$$('#drawer');
14951 if (drawer)
14952 drawer.toggle();
14953 },
14954
14955 /**
14956 * Listens for history-item being selected or deselected (through checkbox)
14957 * and changes the view of the top toolbar.
14958 * @param {{detail: {countAddition: number}}} e
14959 */
14960 checkboxSelected: function(e) {
14961 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
14962 toolbar.count += e.detail.countAddition;
14963 },
14964
14965 /**
14966 * Listens for call to cancel selection and loops through all items to set
14967 * checkbox to be unselected.
14968 * @private
14969 */
14970 unselectAll: function() {
14971 var listContainer =
14972 /** @type {HistoryListContainerElement} */ (this.$['history']);
14973 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
14974 listContainer.unselectAllItems(toolbar.count);
14975 toolbar.count = 0;
14976 },
14977
14978 deleteSelected: function() {
14979 this.$.history.deleteSelectedWithPrompt();
14980 },
14981
14982 /**
14983 * @param {HistoryQuery} info An object containing information about the
14984 * query.
14985 * @param {!Array<HistoryEntry>} results A list of results.
14986 */
14987 historyResult: function(info, results) {
14988 this.set('queryState_.querying', false);
14989 this.set('queryResult_.info', info);
14990 this.set('queryResult_.results', results);
14991 var listContainer =
14992 /** @type {HistoryListContainerElement} */ (this.$['history']);
14993 listContainer.historyResult(info, results);
14994 },
14995
14996 /**
14997 * Fired when the user presses 'More from this site'.
14998 * @param {{detail: {domain: string}}} e
14999 */
15000 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); },
15001
15002 /**
15003 * @param {Event} e
15004 * @private
15005 */
15006 onCanExecute_: function(e) {
15007 e = /** @type {cr.ui.CanExecuteEvent} */(e);
15008 switch (e.command.id) {
15009 case 'find-command':
15010 e.canExecute = true;
15011 break;
15012 case 'slash-command':
15013 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused();
15014 break;
15015 case 'delete-command':
15016 e.canExecute = this.$.toolbar.count > 0;
15017 break;
15018 }
15019 },
15020
15021 /**
15022 * @param {string} searchTerm
15023 * @private
15024 */
15025 searchTermChanged_: function(searchTerm) {
15026 this.set('queryParams_.q', searchTerm || null);
15027 this.$['history'].queryHistory(false);
15028 },
15029
15030 /**
15031 * @param {string} searchQuery
15032 * @private
15033 */
15034 searchQueryParamChanged_: function(searchQuery) {
15035 this.$.toolbar.setSearchTerm(searchQuery || '');
15036 },
15037
15038 /**
15039 * @param {Event} e
15040 * @private
15041 */
15042 onCommand_: function(e) {
15043 if (e.command.id == 'find-command' || e.command.id == 'slash-command')
15044 this.$.toolbar.showSearchField();
15045 if (e.command.id == 'delete-command')
15046 this.deleteSelected();
15047 },
15048
15049 /**
15050 * @param {!Array<!ForeignSession>} sessionList Array of objects describing
15051 * the sessions from other devices.
15052 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
15053 */
15054 setForeignSessions: function(sessionList, isTabSyncEnabled) {
15055 if (!isTabSyncEnabled)
15056 return;
15057
15058 this.set('queryResult_.sessionList', sessionList);
15059 },
15060
15061 /**
15062 * Update sign in state of synced device manager after user logs in or out.
15063 * @param {boolean} isUserSignedIn
15064 */
15065 updateSignInState: function(isUserSignedIn) {
15066 var syncedDeviceManagerElem =
15067 /** @type {HistorySyncedDeviceManagerElement} */this
15068 .$$('history-synced-device-manager');
15069 if (syncedDeviceManagerElem)
15070 syncedDeviceManagerElem.updateSignInState(isUserSignedIn);
15071 },
15072
15073 /**
15074 * @param {string} selectedPage
15075 * @return {boolean}
15076 * @private
15077 */
15078 syncedTabsSelected_: function(selectedPage) {
15079 return selectedPage == 'syncedTabs';
15080 },
15081
15082 /**
15083 * @param {boolean} querying
15084 * @param {boolean} incremental
15085 * @param {string} searchTerm
15086 * @return {boolean} Whether a loading spinner should be shown (implies the
15087 * backend is querying a new search term).
15088 * @private
15089 */
15090 shouldShowSpinner_: function(querying, incremental, searchTerm) {
15091 return querying && !incremental && searchTerm != '';
15092 },
15093
15094 /**
15095 * @param {string} page
15096 * @private
15097 */
15098 routeDataChanged_: function(page) {
15099 this.selectedPage_ = page;
15100 },
15101
15102 /**
15103 * @param {string} selectedPage
15104 * @private
15105 */
15106 selectedPageChanged_: function(selectedPage) {
15107 this.set('routeData_.page', selectedPage);
15108 },
15109
15110 /**
15111 * This computed binding is needed to make the iron-pages selector update when
15112 * the synced-device-manager is instantiated for the first time. Otherwise the
15113 * fallback selection will continue to be used after the corresponding item is
15114 * added as a child of iron-pages.
15115 * @param {string} selectedPage
15116 * @param {Array} items
15117 * @return {string}
15118 * @private
15119 */
15120 getSelectedPage_: function(selectedPage, items) {
15121 return selectedPage;
15122 },
15123
15124 /** @private */
15125 closeDrawer_: function() {
15126 var drawer = this.$$('#drawer');
15127 if (drawer)
15128 drawer.close();
15129 },
15130 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698