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

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

Issue 2235593002: Vulcanize MD History (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: 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. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview Assertion support. 6 * @fileoverview Assertion support.
7 */ 7 */
8 8
9 /** 9 /**
10 * Verify |condition| is truthy and return |condition| if so. 10 * Verify |condition| is truthy and return |condition| if so.
(...skipping 590 matching lines...) Expand 10 before | Expand all | Expand 10 after
601 /** Whether this is on iOS. */ 601 /** Whether this is on iOS. */
602 get isIOS() { 602 get isIOS() {
603 return /iPad|iPhone|iPod/.test(navigator.platform); 603 return /iPad|iPhone|iPod/.test(navigator.platform);
604 } 604 }
605 }; 605 };
606 }(); 606 }();
607 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 607 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
608 // Use of this source code is governed by a BSD-style license that can be 608 // Use of this source code is governed by a BSD-style license that can be
609 // found in the LICENSE file. 609 // found in the LICENSE file.
610 610
611 cr.define('cr.ui', function() {
612
613 /**
614 * Decorates elements as an instance of a class.
615 * @param {string|!Element} source The way to find the element(s) to decorate.
616 * If this is a string then {@code querySeletorAll} is used to find the
617 * elements to decorate.
618 * @param {!Function} constr The constructor to decorate with. The constr
619 * needs to have a {@code decorate} function.
620 */
621 function decorate(source, constr) {
622 var elements;
623 if (typeof source == 'string')
624 elements = cr.doc.querySelectorAll(source);
625 else
626 elements = [source];
627
628 for (var i = 0, el; el = elements[i]; i++) {
629 if (!(el instanceof constr))
630 constr.decorate(el);
631 }
632 }
633
634 /**
635 * Helper function for creating new element for define.
636 */
637 function createElementHelper(tagName, opt_bag) {
638 // Allow passing in ownerDocument to create in a different document.
639 var doc;
640 if (opt_bag && opt_bag.ownerDocument)
641 doc = opt_bag.ownerDocument;
642 else
643 doc = cr.doc;
644 return doc.createElement(tagName);
645 }
646
647 /**
648 * Creates the constructor for a UI element class.
649 *
650 * Usage:
651 * <pre>
652 * var List = cr.ui.define('list');
653 * List.prototype = {
654 * __proto__: HTMLUListElement.prototype,
655 * decorate: function() {
656 * ...
657 * },
658 * ...
659 * };
660 * </pre>
661 *
662 * @param {string|Function} tagNameOrFunction The tagName or
663 * function to use for newly created elements. If this is a function it
664 * needs to return a new element when called.
665 * @return {function(Object=):Element} The constructor function which takes
666 * an optional property bag. The function also has a static
667 * {@code decorate} method added to it.
668 */
669 function define(tagNameOrFunction) {
670 var createFunction, tagName;
671 if (typeof tagNameOrFunction == 'function') {
672 createFunction = tagNameOrFunction;
673 tagName = '';
674 } else {
675 createFunction = createElementHelper;
676 tagName = tagNameOrFunction;
677 }
678
679 /**
680 * Creates a new UI element constructor.
681 * @param {Object=} opt_propertyBag Optional bag of properties to set on the
682 * object after created. The property {@code ownerDocument} is special
683 * cased and it allows you to create the element in a different
684 * document than the default.
685 * @constructor
686 */
687 function f(opt_propertyBag) {
688 var el = createFunction(tagName, opt_propertyBag);
689 f.decorate(el);
690 for (var propertyName in opt_propertyBag) {
691 el[propertyName] = opt_propertyBag[propertyName];
692 }
693 return el;
694 }
695
696 /**
697 * Decorates an element as a UI element class.
698 * @param {!Element} el The element to decorate.
699 */
700 f.decorate = function(el) {
701 el.__proto__ = f.prototype;
702 el.decorate();
703 };
704
705 return f;
706 }
707
708 /**
709 * Input elements do not grow and shrink with their content. This is a simple
710 * (and not very efficient) way of handling shrinking to content with support
711 * for min width and limited by the width of the parent element.
712 * @param {!HTMLElement} el The element to limit the width for.
713 * @param {!HTMLElement} parentEl The parent element that should limit the
714 * size.
715 * @param {number} min The minimum width.
716 * @param {number=} opt_scale Optional scale factor to apply to the width.
717 */
718 function limitInputWidth(el, parentEl, min, opt_scale) {
719 // Needs a size larger than borders
720 el.style.width = '10px';
721 var doc = el.ownerDocument;
722 var win = doc.defaultView;
723 var computedStyle = win.getComputedStyle(el);
724 var parentComputedStyle = win.getComputedStyle(parentEl);
725 var rtl = computedStyle.direction == 'rtl';
726
727 // To get the max width we get the width of the treeItem minus the position
728 // of the input.
729 var inputRect = el.getBoundingClientRect(); // box-sizing
730 var parentRect = parentEl.getBoundingClientRect();
731 var startPos = rtl ? parentRect.right - inputRect.right :
732 inputRect.left - parentRect.left;
733
734 // Add up border and padding of the input.
735 var inner = parseInt(computedStyle.borderLeftWidth, 10) +
736 parseInt(computedStyle.paddingLeft, 10) +
737 parseInt(computedStyle.paddingRight, 10) +
738 parseInt(computedStyle.borderRightWidth, 10);
739
740 // We also need to subtract the padding of parent to prevent it to overflow.
741 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
742 parseInt(parentComputedStyle.paddingRight, 10);
743
744 var max = parentEl.clientWidth - startPos - inner - parentPadding;
745 if (opt_scale)
746 max *= opt_scale;
747
748 function limit() {
749 if (el.scrollWidth > max) {
750 el.style.width = max + 'px';
751 } else {
752 el.style.width = 0;
753 var sw = el.scrollWidth;
754 if (sw < min) {
755 el.style.width = min + 'px';
756 } else {
757 el.style.width = sw + 'px';
758 }
759 }
760 }
761
762 el.addEventListener('input', limit);
763 limit();
764 }
765
766 /**
767 * Takes a number and spits out a value CSS will be happy with. To avoid
768 * subpixel layout issues, the value is rounded to the nearest integral value.
769 * @param {number} pixels The number of pixels.
770 * @return {string} e.g. '16px'.
771 */
772 function toCssPx(pixels) {
773 if (!window.isFinite(pixels))
774 console.error('Pixel value is not a number: ' + pixels);
775 return Math.round(pixels) + 'px';
776 }
777
778 /**
779 * Users complain they occasionaly use doubleclicks instead of clicks
780 * (http://crbug.com/140364). To fix it we freeze click handling for
781 * the doubleclick time interval.
782 * @param {MouseEvent} e Initial click event.
783 */
784 function swallowDoubleClick(e) {
785 var doc = e.target.ownerDocument;
786 var counter = Math.min(1, e.detail);
787 function swallow(e) {
788 e.stopPropagation();
789 e.preventDefault();
790 }
791 function onclick(e) {
792 if (e.detail > counter) {
793 counter = e.detail;
794 // Swallow the click since it's a click inside the doubleclick timeout.
795 swallow(e);
796 } else {
797 // Stop tracking clicks and let regular handling.
798 doc.removeEventListener('dblclick', swallow, true);
799 doc.removeEventListener('click', onclick, true);
800 }
801 }
802 // The following 'click' event (if e.type == 'mouseup') mustn't be taken
803 // into account (it mustn't stop tracking clicks). Start event listening
804 // after zero timeout.
805 setTimeout(function() {
806 doc.addEventListener('click', onclick, true);
807 doc.addEventListener('dblclick', swallow, true);
808 }, 0);
809 }
810
811 return {
812 decorate: decorate,
813 define: define,
814 limitInputWidth: limitInputWidth,
815 toCssPx: toCssPx,
816 swallowDoubleClick: swallowDoubleClick
817 };
818 });
819 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
820 // Use of this source code is governed by a BSD-style license that can be
821 // found in the LICENSE file.
822
823 /**
824 * @fileoverview A command is an abstraction of an action a user can do in the
825 * UI.
826 *
827 * When the focus changes in the document for each command a canExecute event
828 * is dispatched on the active element. By listening to this event you can
829 * enable and disable the command by setting the event.canExecute property.
830 *
831 * When a command is executed a command event is dispatched on the active
832 * element. Note that you should stop the propagation after you have handled the
833 * command if there might be other command listeners higher up in the DOM tree.
834 */
835
836 cr.define('cr.ui', function() {
837
838 /**
839 * This is used to identify keyboard shortcuts.
840 * @param {string} shortcut The text used to describe the keys for this
841 * keyboard shortcut.
842 * @constructor
843 */
844 function KeyboardShortcut(shortcut) {
845 var mods = {};
846 var ident = '';
847 shortcut.split('|').forEach(function(part) {
848 var partLc = part.toLowerCase();
849 switch (partLc) {
850 case 'alt':
851 case 'ctrl':
852 case 'meta':
853 case 'shift':
854 mods[partLc + 'Key'] = true;
855 break;
856 default:
857 if (ident)
858 throw Error('Invalid shortcut');
859 ident = part;
860 }
861 });
862
863 this.ident_ = ident;
864 this.mods_ = mods;
865 }
866
867 KeyboardShortcut.prototype = {
868 /**
869 * Whether the keyboard shortcut object matches a keyboard event.
870 * @param {!Event} e The keyboard event object.
871 * @return {boolean} Whether we found a match or not.
872 */
873 matchesEvent: function(e) {
874 if (e.key == this.ident_) {
875 // All keyboard modifiers needs to match.
876 var mods = this.mods_;
877 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
878 return e[k] == !!mods[k];
879 });
880 }
881 return false;
882 }
883 };
884
885 /**
886 * Creates a new command element.
887 * @constructor
888 * @extends {HTMLElement}
889 */
890 var Command = cr.ui.define('command');
891
892 Command.prototype = {
893 __proto__: HTMLElement.prototype,
894
895 /**
896 * Initializes the command.
897 */
898 decorate: function() {
899 CommandManager.init(assert(this.ownerDocument));
900
901 if (this.hasAttribute('shortcut'))
902 this.shortcut = this.getAttribute('shortcut');
903 },
904
905 /**
906 * Executes the command by dispatching a command event on the given element.
907 * If |element| isn't given, the active element is used instead.
908 * If the command is {@code disabled} this does nothing.
909 * @param {HTMLElement=} opt_element Optional element to dispatch event on.
910 */
911 execute: function(opt_element) {
912 if (this.disabled)
913 return;
914 var doc = this.ownerDocument;
915 if (doc.activeElement) {
916 var e = new Event('command', {bubbles: true});
917 e.command = this;
918
919 (opt_element || doc.activeElement).dispatchEvent(e);
920 }
921 },
922
923 /**
924 * Call this when there have been changes that might change whether the
925 * command can be executed or not.
926 * @param {Node=} opt_node Node for which to actuate command state.
927 */
928 canExecuteChange: function(opt_node) {
929 dispatchCanExecuteEvent(this,
930 opt_node || this.ownerDocument.activeElement);
931 },
932
933 /**
934 * The keyboard shortcut that triggers the command. This is a string
935 * consisting of a key (as reported by WebKit in keydown) as
936 * well as optional key modifiers joinded with a '|'.
937 *
938 * Multiple keyboard shortcuts can be provided by separating them by
939 * whitespace.
940 *
941 * For example:
942 * "F1"
943 * "Backspace|Meta" for Apple command backspace.
944 * "a|Ctrl" for Control A
945 * "Delete Backspace|Meta" for Delete and Command Backspace
946 *
947 * @type {string}
948 */
949 shortcut_: '',
950 get shortcut() {
951 return this.shortcut_;
952 },
953 set shortcut(shortcut) {
954 var oldShortcut = this.shortcut_;
955 if (shortcut !== oldShortcut) {
956 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
957 return new KeyboardShortcut(shortcut);
958 });
959
960 // Set this after the keyboardShortcuts_ since that might throw.
961 this.shortcut_ = shortcut;
962 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
963 oldShortcut);
964 }
965 },
966
967 /**
968 * Whether the event object matches the shortcut for this command.
969 * @param {!Event} e The key event object.
970 * @return {boolean} Whether it matched or not.
971 */
972 matchesEvent: function(e) {
973 if (!this.keyboardShortcuts_)
974 return false;
975
976 return this.keyboardShortcuts_.some(function(keyboardShortcut) {
977 return keyboardShortcut.matchesEvent(e);
978 });
979 },
980 };
981
982 /**
983 * The label of the command.
984 */
985 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
986
987 /**
988 * Whether the command is disabled or not.
989 */
990 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
991
992 /**
993 * Whether the command is hidden or not.
994 */
995 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
996
997 /**
998 * Whether the command is checked or not.
999 */
1000 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
1001
1002 /**
1003 * The flag that prevents the shortcut text from being displayed on menu.
1004 *
1005 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
1006 * is displayed in menu when the command is assosiated with a menu item.
1007 * Otherwise, no text is displayed.
1008 */
1009 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
1010
1011 /**
1012 * Dispatches a canExecute event on the target.
1013 * @param {!cr.ui.Command} command The command that we are testing for.
1014 * @param {EventTarget} target The target element to dispatch the event on.
1015 */
1016 function dispatchCanExecuteEvent(command, target) {
1017 var e = new CanExecuteEvent(command);
1018 target.dispatchEvent(e);
1019 command.disabled = !e.canExecute;
1020 }
1021
1022 /**
1023 * The command managers for different documents.
1024 */
1025 var commandManagers = {};
1026
1027 /**
1028 * Keeps track of the focused element and updates the commands when the focus
1029 * changes.
1030 * @param {!Document} doc The document that we are managing the commands for.
1031 * @constructor
1032 */
1033 function CommandManager(doc) {
1034 doc.addEventListener('focus', this.handleFocus_.bind(this), true);
1035 // Make sure we add the listener to the bubbling phase so that elements can
1036 // prevent the command.
1037 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
1038 }
1039
1040 /**
1041 * Initializes a command manager for the document as needed.
1042 * @param {!Document} doc The document to manage the commands for.
1043 */
1044 CommandManager.init = function(doc) {
1045 var uid = cr.getUid(doc);
1046 if (!(uid in commandManagers)) {
1047 commandManagers[uid] = new CommandManager(doc);
1048 }
1049 };
1050
1051 CommandManager.prototype = {
1052
1053 /**
1054 * Handles focus changes on the document.
1055 * @param {Event} e The focus event object.
1056 * @private
1057 * @suppress {checkTypes}
1058 * TODO(vitalyp): remove the suppression.
1059 */
1060 handleFocus_: function(e) {
1061 var target = e.target;
1062
1063 // Ignore focus on a menu button or command item.
1064 if (target.menu || target.command)
1065 return;
1066
1067 var commands = Array.prototype.slice.call(
1068 target.ownerDocument.querySelectorAll('command'));
1069
1070 commands.forEach(function(command) {
1071 dispatchCanExecuteEvent(command, target);
1072 });
1073 },
1074
1075 /**
1076 * Handles the keydown event and routes it to the right command.
1077 * @param {!Event} e The keydown event.
1078 */
1079 handleKeyDown_: function(e) {
1080 var target = e.target;
1081 var commands = Array.prototype.slice.call(
1082 target.ownerDocument.querySelectorAll('command'));
1083
1084 for (var i = 0, command; command = commands[i]; i++) {
1085 if (command.matchesEvent(e)) {
1086 // When invoking a command via a shortcut, we have to manually check
1087 // if it can be executed, since focus might not have been changed
1088 // what would have updated the command's state.
1089 command.canExecuteChange();
1090
1091 if (!command.disabled) {
1092 e.preventDefault();
1093 // We do not want any other element to handle this.
1094 e.stopPropagation();
1095 command.execute();
1096 return;
1097 }
1098 }
1099 }
1100 }
1101 };
1102
1103 /**
1104 * The event type used for canExecute events.
1105 * @param {!cr.ui.Command} command The command that we are evaluating.
1106 * @extends {Event}
1107 * @constructor
1108 * @class
1109 */
1110 function CanExecuteEvent(command) {
1111 var e = new Event('canExecute', {bubbles: true, cancelable: true});
1112 e.__proto__ = CanExecuteEvent.prototype;
1113 e.command = command;
1114 return e;
1115 }
1116
1117 CanExecuteEvent.prototype = {
1118 __proto__: Event.prototype,
1119
1120 /**
1121 * The current command
1122 * @type {cr.ui.Command}
1123 */
1124 command: null,
1125
1126 /**
1127 * Whether the target can execute the command. Setting this also stops the
1128 * propagation and prevents the default. Callers can tell if an event has
1129 * been handled via |this.defaultPrevented|.
1130 * @type {boolean}
1131 */
1132 canExecute_: false,
1133 get canExecute() {
1134 return this.canExecute_;
1135 },
1136 set canExecute(canExecute) {
1137 this.canExecute_ = !!canExecute;
1138 this.stopPropagation();
1139 this.preventDefault();
1140 }
1141 };
1142
1143 // Export
1144 return {
1145 Command: Command,
1146 CanExecuteEvent: CanExecuteEvent
1147 };
1148 });
1149 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
1150 // Use of this source code is governed by a BSD-style license that can be
1151 // found in the LICENSE file.
1152
1153 // <include src="../../../../ui/webui/resources/js/assert.js"> 611 // <include src="../../../../ui/webui/resources/js/assert.js">
1154 612
1155 /** 613 /**
1156 * Alias for document.getElementById. Found elements must be HTMLElements. 614 * Alias for document.getElementById. Found elements must be HTMLElements.
1157 * @param {string} id The ID of the element to find. 615 * @param {string} id The ID of the element to find.
1158 * @return {HTMLElement} The found element or null if not found. 616 * @return {HTMLElement} The found element or null if not found.
1159 */ 617 */
1160 function $(id) { 618 function $(id) {
1161 var el = document.getElementById(id); 619 var el = document.getElementById(id);
1162 return el ? assertInstanceof(el, HTMLElement) : null; 620 return el ? assertInstanceof(el, HTMLElement) : null;
(...skipping 417 matching lines...) Expand 10 before | Expand all | Expand 10 after
1580 case 0xdb: return '['; 1038 case 0xdb: return '[';
1581 case 0xdd: return ']'; 1039 case 0xdd: return ']';
1582 } 1040 }
1583 return 'Unidentified'; 1041 return 'Unidentified';
1584 } 1042 }
1585 }); 1043 });
1586 } else { 1044 } else {
1587 window.console.log("KeyboardEvent.Key polyfill not required"); 1045 window.console.log("KeyboardEvent.Key polyfill not required");
1588 } 1046 }
1589 // </if> /* is_ios */ 1047 // </if> /* is_ios */
1048 // Copyright 2016 The Chromium Authors. All rights reserved.
1049 // Use of this source code is governed by a BSD-style license that can be
1050 // found in the LICENSE file.
1051
1052 // Globals:
1053 /** @const */ var RESULTS_PER_PAGE = 150;
1054
1055 /**
1056 * Amount of time between pageviews that we consider a 'break' in browsing,
1057 * measured in milliseconds.
1058 * @const
1059 */
1060 var BROWSING_GAP_TIME = 15 * 60 * 1000;
1061
1062 /**
1063 * Maximum length of a history item title. Anything longer than this will be
1064 * cropped to fit within this limit. This value is large enough that it will not
1065 * be noticeable in a 960px wide history-item.
1066 * @const
1067 */
1068 var TITLE_MAX_LENGTH = 300;
1069
1070 /**
1071 * @enum {number}
1072 */
1073 var HistoryRange = {
1074 ALL_TIME: 0,
1075 WEEK: 1,
1076 MONTH: 2
1077 };
1078
1079 // Types:
1080 /**
1081 * @typedef {{groupedOffset: number,
1082 * incremental: boolean,
1083 * querying: boolean,
1084 * range: HistoryRange,
1085 * searchTerm: string}}
1086 */
1087 var QueryState;
1088
1089 /**
1090 * @typedef {{info: ?HistoryQuery,
1091 * results: ?Array<!HistoryEntry>,
1092 * sessionList: ?Array<!ForeignSession>}}
1093 */
1094 var QueryResult;
1095
1096 /** @constructor
1097 * @extends {MouseEvent} */
1098 var DomRepeatClickEvent = function() {
1099 this.model = null;
1100 };
1101 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
1102 // Use of this source code is governed by a BSD-style license that can be
1103 // found in the LICENSE file.
1104
1105 cr.define('cr.ui', function() {
1106
1107 /**
1108 * Decorates elements as an instance of a class.
1109 * @param {string|!Element} source The way to find the element(s) to decorate.
1110 * If this is a string then {@code querySeletorAll} is used to find the
1111 * elements to decorate.
1112 * @param {!Function} constr The constructor to decorate with. The constr
1113 * needs to have a {@code decorate} function.
1114 */
1115 function decorate(source, constr) {
1116 var elements;
1117 if (typeof source == 'string')
1118 elements = cr.doc.querySelectorAll(source);
1119 else
1120 elements = [source];
1121
1122 for (var i = 0, el; el = elements[i]; i++) {
1123 if (!(el instanceof constr))
1124 constr.decorate(el);
1125 }
1126 }
1127
1128 /**
1129 * Helper function for creating new element for define.
1130 */
1131 function createElementHelper(tagName, opt_bag) {
1132 // Allow passing in ownerDocument to create in a different document.
1133 var doc;
1134 if (opt_bag && opt_bag.ownerDocument)
1135 doc = opt_bag.ownerDocument;
1136 else
1137 doc = cr.doc;
1138 return doc.createElement(tagName);
1139 }
1140
1141 /**
1142 * Creates the constructor for a UI element class.
1143 *
1144 * Usage:
1145 * <pre>
1146 * var List = cr.ui.define('list');
1147 * List.prototype = {
1148 * __proto__: HTMLUListElement.prototype,
1149 * decorate: function() {
1150 * ...
1151 * },
1152 * ...
1153 * };
1154 * </pre>
1155 *
1156 * @param {string|Function} tagNameOrFunction The tagName or
1157 * function to use for newly created elements. If this is a function it
1158 * needs to return a new element when called.
1159 * @return {function(Object=):Element} The constructor function which takes
1160 * an optional property bag. The function also has a static
1161 * {@code decorate} method added to it.
1162 */
1163 function define(tagNameOrFunction) {
1164 var createFunction, tagName;
1165 if (typeof tagNameOrFunction == 'function') {
1166 createFunction = tagNameOrFunction;
1167 tagName = '';
1168 } else {
1169 createFunction = createElementHelper;
1170 tagName = tagNameOrFunction;
1171 }
1172
1173 /**
1174 * Creates a new UI element constructor.
1175 * @param {Object=} opt_propertyBag Optional bag of properties to set on the
1176 * object after created. The property {@code ownerDocument} is special
1177 * cased and it allows you to create the element in a different
1178 * document than the default.
1179 * @constructor
1180 */
1181 function f(opt_propertyBag) {
1182 var el = createFunction(tagName, opt_propertyBag);
1183 f.decorate(el);
1184 for (var propertyName in opt_propertyBag) {
1185 el[propertyName] = opt_propertyBag[propertyName];
1186 }
1187 return el;
1188 }
1189
1190 /**
1191 * Decorates an element as a UI element class.
1192 * @param {!Element} el The element to decorate.
1193 */
1194 f.decorate = function(el) {
1195 el.__proto__ = f.prototype;
1196 el.decorate();
1197 };
1198
1199 return f;
1200 }
1201
1202 /**
1203 * Input elements do not grow and shrink with their content. This is a simple
1204 * (and not very efficient) way of handling shrinking to content with support
1205 * for min width and limited by the width of the parent element.
1206 * @param {!HTMLElement} el The element to limit the width for.
1207 * @param {!HTMLElement} parentEl The parent element that should limit the
1208 * size.
1209 * @param {number} min The minimum width.
1210 * @param {number=} opt_scale Optional scale factor to apply to the width.
1211 */
1212 function limitInputWidth(el, parentEl, min, opt_scale) {
1213 // Needs a size larger than borders
1214 el.style.width = '10px';
1215 var doc = el.ownerDocument;
1216 var win = doc.defaultView;
1217 var computedStyle = win.getComputedStyle(el);
1218 var parentComputedStyle = win.getComputedStyle(parentEl);
1219 var rtl = computedStyle.direction == 'rtl';
1220
1221 // To get the max width we get the width of the treeItem minus the position
1222 // of the input.
1223 var inputRect = el.getBoundingClientRect(); // box-sizing
1224 var parentRect = parentEl.getBoundingClientRect();
1225 var startPos = rtl ? parentRect.right - inputRect.right :
1226 inputRect.left - parentRect.left;
1227
1228 // Add up border and padding of the input.
1229 var inner = parseInt(computedStyle.borderLeftWidth, 10) +
1230 parseInt(computedStyle.paddingLeft, 10) +
1231 parseInt(computedStyle.paddingRight, 10) +
1232 parseInt(computedStyle.borderRightWidth, 10);
1233
1234 // We also need to subtract the padding of parent to prevent it to overflow.
1235 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
1236 parseInt(parentComputedStyle.paddingRight, 10);
1237
1238 var max = parentEl.clientWidth - startPos - inner - parentPadding;
1239 if (opt_scale)
1240 max *= opt_scale;
1241
1242 function limit() {
1243 if (el.scrollWidth > max) {
1244 el.style.width = max + 'px';
1245 } else {
1246 el.style.width = 0;
1247 var sw = el.scrollWidth;
1248 if (sw < min) {
1249 el.style.width = min + 'px';
1250 } else {
1251 el.style.width = sw + 'px';
1252 }
1253 }
1254 }
1255
1256 el.addEventListener('input', limit);
1257 limit();
1258 }
1259
1260 /**
1261 * Takes a number and spits out a value CSS will be happy with. To avoid
1262 * subpixel layout issues, the value is rounded to the nearest integral value.
1263 * @param {number} pixels The number of pixels.
1264 * @return {string} e.g. '16px'.
1265 */
1266 function toCssPx(pixels) {
1267 if (!window.isFinite(pixels))
1268 console.error('Pixel value is not a number: ' + pixels);
1269 return Math.round(pixels) + 'px';
1270 }
1271
1272 /**
1273 * Users complain they occasionaly use doubleclicks instead of clicks
1274 * (http://crbug.com/140364). To fix it we freeze click handling for
1275 * the doubleclick time interval.
1276 * @param {MouseEvent} e Initial click event.
1277 */
1278 function swallowDoubleClick(e) {
1279 var doc = e.target.ownerDocument;
1280 var counter = Math.min(1, e.detail);
1281 function swallow(e) {
1282 e.stopPropagation();
1283 e.preventDefault();
1284 }
1285 function onclick(e) {
1286 if (e.detail > counter) {
1287 counter = e.detail;
1288 // Swallow the click since it's a click inside the doubleclick timeout.
1289 swallow(e);
1290 } else {
1291 // Stop tracking clicks and let regular handling.
1292 doc.removeEventListener('dblclick', swallow, true);
1293 doc.removeEventListener('click', onclick, true);
1294 }
1295 }
1296 // The following 'click' event (if e.type == 'mouseup') mustn't be taken
1297 // into account (it mustn't stop tracking clicks). Start event listening
1298 // after zero timeout.
1299 setTimeout(function() {
1300 doc.addEventListener('click', onclick, true);
1301 doc.addEventListener('dblclick', swallow, true);
1302 }, 0);
1303 }
1304
1305 return {
1306 decorate: decorate,
1307 define: define,
1308 limitInputWidth: limitInputWidth,
1309 toCssPx: toCssPx,
1310 swallowDoubleClick: swallowDoubleClick
1311 };
1312 });
1313 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
1314 // Use of this source code is governed by a BSD-style license that can be
1315 // found in the LICENSE file.
1316
1317 /**
1318 * @fileoverview A command is an abstraction of an action a user can do in the
1319 * UI.
1320 *
1321 * When the focus changes in the document for each command a canExecute event
1322 * is dispatched on the active element. By listening to this event you can
1323 * enable and disable the command by setting the event.canExecute property.
1324 *
1325 * When a command is executed a command event is dispatched on the active
1326 * element. Note that you should stop the propagation after you have handled the
1327 * command if there might be other command listeners higher up in the DOM tree.
1328 */
1329
1330 cr.define('cr.ui', function() {
1331
1332 /**
1333 * This is used to identify keyboard shortcuts.
1334 * @param {string} shortcut The text used to describe the keys for this
1335 * keyboard shortcut.
1336 * @constructor
1337 */
1338 function KeyboardShortcut(shortcut) {
1339 var mods = {};
1340 var ident = '';
1341 shortcut.split('|').forEach(function(part) {
1342 var partLc = part.toLowerCase();
1343 switch (partLc) {
1344 case 'alt':
1345 case 'ctrl':
1346 case 'meta':
1347 case 'shift':
1348 mods[partLc + 'Key'] = true;
1349 break;
1350 default:
1351 if (ident)
1352 throw Error('Invalid shortcut');
1353 ident = part;
1354 }
1355 });
1356
1357 this.ident_ = ident;
1358 this.mods_ = mods;
1359 }
1360
1361 KeyboardShortcut.prototype = {
1362 /**
1363 * Whether the keyboard shortcut object matches a keyboard event.
1364 * @param {!Event} e The keyboard event object.
1365 * @return {boolean} Whether we found a match or not.
1366 */
1367 matchesEvent: function(e) {
1368 if (e.key == this.ident_) {
1369 // All keyboard modifiers needs to match.
1370 var mods = this.mods_;
1371 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
1372 return e[k] == !!mods[k];
1373 });
1374 }
1375 return false;
1376 }
1377 };
1378
1379 /**
1380 * Creates a new command element.
1381 * @constructor
1382 * @extends {HTMLElement}
1383 */
1384 var Command = cr.ui.define('command');
1385
1386 Command.prototype = {
1387 __proto__: HTMLElement.prototype,
1388
1389 /**
1390 * Initializes the command.
1391 */
1392 decorate: function() {
1393 CommandManager.init(assert(this.ownerDocument));
1394
1395 if (this.hasAttribute('shortcut'))
1396 this.shortcut = this.getAttribute('shortcut');
1397 },
1398
1399 /**
1400 * Executes the command by dispatching a command event on the given element.
1401 * If |element| isn't given, the active element is used instead.
1402 * If the command is {@code disabled} this does nothing.
1403 * @param {HTMLElement=} opt_element Optional element to dispatch event on.
1404 */
1405 execute: function(opt_element) {
1406 if (this.disabled)
1407 return;
1408 var doc = this.ownerDocument;
1409 if (doc.activeElement) {
1410 var e = new Event('command', {bubbles: true});
1411 e.command = this;
1412
1413 (opt_element || doc.activeElement).dispatchEvent(e);
1414 }
1415 },
1416
1417 /**
1418 * Call this when there have been changes that might change whether the
1419 * command can be executed or not.
1420 * @param {Node=} opt_node Node for which to actuate command state.
1421 */
1422 canExecuteChange: function(opt_node) {
1423 dispatchCanExecuteEvent(this,
1424 opt_node || this.ownerDocument.activeElement);
1425 },
1426
1427 /**
1428 * The keyboard shortcut that triggers the command. This is a string
1429 * consisting of a key (as reported by WebKit in keydown) as
1430 * well as optional key modifiers joinded with a '|'.
1431 *
1432 * Multiple keyboard shortcuts can be provided by separating them by
1433 * whitespace.
1434 *
1435 * For example:
1436 * "F1"
1437 * "Backspace|Meta" for Apple command backspace.
1438 * "a|Ctrl" for Control A
1439 * "Delete Backspace|Meta" for Delete and Command Backspace
1440 *
1441 * @type {string}
1442 */
1443 shortcut_: '',
1444 get shortcut() {
1445 return this.shortcut_;
1446 },
1447 set shortcut(shortcut) {
1448 var oldShortcut = this.shortcut_;
1449 if (shortcut !== oldShortcut) {
1450 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
1451 return new KeyboardShortcut(shortcut);
1452 });
1453
1454 // Set this after the keyboardShortcuts_ since that might throw.
1455 this.shortcut_ = shortcut;
1456 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
1457 oldShortcut);
1458 }
1459 },
1460
1461 /**
1462 * Whether the event object matches the shortcut for this command.
1463 * @param {!Event} e The key event object.
1464 * @return {boolean} Whether it matched or not.
1465 */
1466 matchesEvent: function(e) {
1467 if (!this.keyboardShortcuts_)
1468 return false;
1469
1470 return this.keyboardShortcuts_.some(function(keyboardShortcut) {
1471 return keyboardShortcut.matchesEvent(e);
1472 });
1473 },
1474 };
1475
1476 /**
1477 * The label of the command.
1478 */
1479 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
1480
1481 /**
1482 * Whether the command is disabled or not.
1483 */
1484 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
1485
1486 /**
1487 * Whether the command is hidden or not.
1488 */
1489 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
1490
1491 /**
1492 * Whether the command is checked or not.
1493 */
1494 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
1495
1496 /**
1497 * The flag that prevents the shortcut text from being displayed on menu.
1498 *
1499 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
1500 * is displayed in menu when the command is assosiated with a menu item.
1501 * Otherwise, no text is displayed.
1502 */
1503 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
1504
1505 /**
1506 * Dispatches a canExecute event on the target.
1507 * @param {!cr.ui.Command} command The command that we are testing for.
1508 * @param {EventTarget} target The target element to dispatch the event on.
1509 */
1510 function dispatchCanExecuteEvent(command, target) {
1511 var e = new CanExecuteEvent(command);
1512 target.dispatchEvent(e);
1513 command.disabled = !e.canExecute;
1514 }
1515
1516 /**
1517 * The command managers for different documents.
1518 */
1519 var commandManagers = {};
1520
1521 /**
1522 * Keeps track of the focused element and updates the commands when the focus
1523 * changes.
1524 * @param {!Document} doc The document that we are managing the commands for.
1525 * @constructor
1526 */
1527 function CommandManager(doc) {
1528 doc.addEventListener('focus', this.handleFocus_.bind(this), true);
1529 // Make sure we add the listener to the bubbling phase so that elements can
1530 // prevent the command.
1531 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
1532 }
1533
1534 /**
1535 * Initializes a command manager for the document as needed.
1536 * @param {!Document} doc The document to manage the commands for.
1537 */
1538 CommandManager.init = function(doc) {
1539 var uid = cr.getUid(doc);
1540 if (!(uid in commandManagers)) {
1541 commandManagers[uid] = new CommandManager(doc);
1542 }
1543 };
1544
1545 CommandManager.prototype = {
1546
1547 /**
1548 * Handles focus changes on the document.
1549 * @param {Event} e The focus event object.
1550 * @private
1551 * @suppress {checkTypes}
1552 * TODO(vitalyp): remove the suppression.
1553 */
1554 handleFocus_: function(e) {
1555 var target = e.target;
1556
1557 // Ignore focus on a menu button or command item.
1558 if (target.menu || target.command)
1559 return;
1560
1561 var commands = Array.prototype.slice.call(
1562 target.ownerDocument.querySelectorAll('command'));
1563
1564 commands.forEach(function(command) {
1565 dispatchCanExecuteEvent(command, target);
1566 });
1567 },
1568
1569 /**
1570 * Handles the keydown event and routes it to the right command.
1571 * @param {!Event} e The keydown event.
1572 */
1573 handleKeyDown_: function(e) {
1574 var target = e.target;
1575 var commands = Array.prototype.slice.call(
1576 target.ownerDocument.querySelectorAll('command'));
1577
1578 for (var i = 0, command; command = commands[i]; i++) {
1579 if (command.matchesEvent(e)) {
1580 // When invoking a command via a shortcut, we have to manually check
1581 // if it can be executed, since focus might not have been changed
1582 // what would have updated the command's state.
1583 command.canExecuteChange();
1584
1585 if (!command.disabled) {
1586 e.preventDefault();
1587 // We do not want any other element to handle this.
1588 e.stopPropagation();
1589 command.execute();
1590 return;
1591 }
1592 }
1593 }
1594 }
1595 };
1596
1597 /**
1598 * The event type used for canExecute events.
1599 * @param {!cr.ui.Command} command The command that we are evaluating.
1600 * @extends {Event}
1601 * @constructor
1602 * @class
1603 */
1604 function CanExecuteEvent(command) {
1605 var e = new Event('canExecute', {bubbles: true, cancelable: true});
1606 e.__proto__ = CanExecuteEvent.prototype;
1607 e.command = command;
1608 return e;
1609 }
1610
1611 CanExecuteEvent.prototype = {
1612 __proto__: Event.prototype,
1613
1614 /**
1615 * The current command
1616 * @type {cr.ui.Command}
1617 */
1618 command: null,
1619
1620 /**
1621 * Whether the target can execute the command. Setting this also stops the
1622 * propagation and prevents the default. Callers can tell if an event has
1623 * been handled via |this.defaultPrevented|.
1624 * @type {boolean}
1625 */
1626 canExecute_: false,
1627 get canExecute() {
1628 return this.canExecute_;
1629 },
1630 set canExecute(canExecute) {
1631 this.canExecute_ = !!canExecute;
1632 this.stopPropagation();
1633 this.preventDefault();
1634 }
1635 };
1636
1637 // Export
1638 return {
1639 Command: Command,
1640 CanExecuteEvent: CanExecuteEvent
1641 };
1642 });
1643 Polymer({
1644 is: 'app-drawer',
1645
1646 properties: {
1647 /**
1648 * The opened state of the drawer.
1649 */
1650 opened: {
1651 type: Boolean,
1652 value: false,
1653 notify: true,
1654 reflectToAttribute: true
1655 },
1656
1657 /**
1658 * The drawer does not have a scrim and cannot be swiped close.
1659 */
1660 persistent: {
1661 type: Boolean,
1662 value: false,
1663 reflectToAttribute: true
1664 },
1665
1666 /**
1667 * The alignment of the drawer on the screen ('left', 'right', 'start' o r 'end').
1668 * 'start' computes to left and 'end' to right in LTR layout and vice ve rsa in RTL
1669 * layout.
1670 */
1671 align: {
1672 type: String,
1673 value: 'left'
1674 },
1675
1676 /**
1677 * The computed, read-only position of the drawer on the screen ('left' or 'right').
1678 */
1679 position: {
1680 type: String,
1681 readOnly: true,
1682 value: 'left',
1683 reflectToAttribute: true
1684 },
1685
1686 /**
1687 * Create an area at the edge of the screen to swipe open the drawer.
1688 */
1689 swipeOpen: {
1690 type: Boolean,
1691 value: false,
1692 reflectToAttribute: true
1693 },
1694
1695 /**
1696 * Trap keyboard focus when the drawer is opened and not persistent.
1697 */
1698 noFocusTrap: {
1699 type: Boolean,
1700 value: false
1701 }
1702 },
1703
1704 observers: [
1705 'resetLayout(position)',
1706 '_resetPosition(align, isAttached)'
1707 ],
1708
1709 _translateOffset: 0,
1710
1711 _trackDetails: null,
1712
1713 _drawerState: 0,
1714
1715 _boundEscKeydownHandler: null,
1716
1717 _firstTabStop: null,
1718
1719 _lastTabStop: null,
1720
1721 ready: function() {
1722 // Set the scroll direction so you can vertically scroll inside the draw er.
1723 this.setScrollDirection('y');
1724
1725 // Only transition the drawer after its first render (e.g. app-drawer-la yout
1726 // may need to set the initial opened state which should not be transiti oned).
1727 this._setTransitionDuration('0s');
1728 },
1729
1730 attached: function() {
1731 // Only transition the drawer after its first render (e.g. app-drawer-la yout
1732 // may need to set the initial opened state which should not be transiti oned).
1733 Polymer.RenderStatus.afterNextRender(this, function() {
1734 this._setTransitionDuration('');
1735 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
1736 this._resetDrawerState();
1737
1738 this.listen(this, 'track', '_track');
1739 this.addEventListener('transitionend', this._transitionend.bind(this)) ;
1740 this.addEventListener('keydown', this._tabKeydownHandler.bind(this))
1741 });
1742 },
1743
1744 detached: function() {
1745 document.removeEventListener('keydown', this._boundEscKeydownHandler);
1746 },
1747
1748 /**
1749 * Opens the drawer.
1750 */
1751 open: function() {
1752 this.opened = true;
1753 },
1754
1755 /**
1756 * Closes the drawer.
1757 */
1758 close: function() {
1759 this.opened = false;
1760 },
1761
1762 /**
1763 * Toggles the drawer open and close.
1764 */
1765 toggle: function() {
1766 this.opened = !this.opened;
1767 },
1768
1769 /**
1770 * Gets the width of the drawer.
1771 *
1772 * @return {number} The width of the drawer in pixels.
1773 */
1774 getWidth: function() {
1775 return this.$.contentContainer.offsetWidth;
1776 },
1777
1778 /**
1779 * Resets the layout. If you changed the size of app-header via CSS
1780 * you can notify the changes by either firing the `iron-resize` event
1781 * or calling `resetLayout` directly.
1782 *
1783 * @method resetLayout
1784 */
1785 resetLayout: function() {
1786 this.debounce('_resetLayout', function() {
1787 this.fire('app-drawer-reset-layout');
1788 }, 1);
1789 },
1790
1791 _isRTL: function() {
1792 return window.getComputedStyle(this).direction === 'rtl';
1793 },
1794
1795 _resetPosition: function() {
1796 switch (this.align) {
1797 case 'start':
1798 this._setPosition(this._isRTL() ? 'right' : 'left');
1799 return;
1800 case 'end':
1801 this._setPosition(this._isRTL() ? 'left' : 'right');
1802 return;
1803 }
1804 this._setPosition(this.align);
1805 },
1806
1807 _escKeydownHandler: function(event) {
1808 var ESC_KEYCODE = 27;
1809 if (event.keyCode === ESC_KEYCODE) {
1810 // Prevent any side effects if app-drawer closes.
1811 event.preventDefault();
1812 this.close();
1813 }
1814 },
1815
1816 _track: function(event) {
1817 if (this.persistent) {
1818 return;
1819 }
1820
1821 // Disable user selection on desktop.
1822 event.preventDefault();
1823
1824 switch (event.detail.state) {
1825 case 'start':
1826 this._trackStart(event);
1827 break;
1828 case 'track':
1829 this._trackMove(event);
1830 break;
1831 case 'end':
1832 this._trackEnd(event);
1833 break;
1834 }
1835 },
1836
1837 _trackStart: function(event) {
1838 this._drawerState = this._DRAWER_STATE.TRACKING;
1839
1840 // Disable transitions since style attributes will reflect user track ev ents.
1841 this._setTransitionDuration('0s');
1842 this.style.visibility = 'visible';
1843
1844 var rect = this.$.contentContainer.getBoundingClientRect();
1845 if (this.position === 'left') {
1846 this._translateOffset = rect.left;
1847 } else {
1848 this._translateOffset = rect.right - window.innerWidth;
1849 }
1850
1851 this._trackDetails = [];
1852 },
1853
1854 _trackMove: function(event) {
1855 this._translateDrawer(event.detail.dx + this._translateOffset);
1856
1857 // Use Date.now() since event.timeStamp is inconsistent across browsers (e.g. most
1858 // browsers use milliseconds but FF 44 uses microseconds).
1859 this._trackDetails.push({
1860 dx: event.detail.dx,
1861 timeStamp: Date.now()
1862 });
1863 },
1864
1865 _trackEnd: function(event) {
1866 var x = event.detail.dx + this._translateOffset;
1867 var drawerWidth = this.getWidth();
1868 var isPositionLeft = this.position === 'left';
1869 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
1870 (x <= 0 || x >= drawerWidth);
1871
1872 if (!isInEndState) {
1873 // No longer need the track events after this method returns - allow t hem to be GC'd.
1874 var trackDetails = this._trackDetails;
1875 this._trackDetails = null;
1876
1877 this._flingDrawer(event, trackDetails);
1878 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1879 return;
1880 }
1881 }
1882
1883 // If the drawer is not flinging, toggle the opened state based on the p osition of
1884 // the drawer.
1885 var halfWidth = drawerWidth / 2;
1886 if (event.detail.dx < -halfWidth) {
1887 this.opened = this.position === 'right';
1888 } else if (event.detail.dx > halfWidth) {
1889 this.opened = this.position === 'left';
1890 }
1891
1892 // Trigger app-drawer-transitioned now since there will be no transition end event.
1893 if (isInEndState) {
1894 this._resetDrawerState();
1895 }
1896
1897 this._setTransitionDuration('');
1898 this._resetDrawerTranslate();
1899 this.style.visibility = '';
1900 },
1901
1902 _calculateVelocity: function(event, trackDetails) {
1903 // Find the oldest track event that is within 100ms using binary search.
1904 var now = Date.now();
1905 var timeLowerBound = now - 100;
1906 var trackDetail;
1907 var min = 0;
1908 var max = trackDetails.length - 1;
1909
1910 while (min <= max) {
1911 // Floor of average of min and max.
1912 var mid = (min + max) >> 1;
1913 var d = trackDetails[mid];
1914 if (d.timeStamp >= timeLowerBound) {
1915 trackDetail = d;
1916 max = mid - 1;
1917 } else {
1918 min = mid + 1;
1919 }
1920 }
1921
1922 if (trackDetail) {
1923 var dx = event.detail.dx - trackDetail.dx;
1924 var dt = (now - trackDetail.timeStamp) || 1;
1925 return dx / dt;
1926 }
1927 return 0;
1928 },
1929
1930 _flingDrawer: function(event, trackDetails) {
1931 var velocity = this._calculateVelocity(event, trackDetails);
1932
1933 // Do not fling if velocity is not above a threshold.
1934 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
1935 return;
1936 }
1937
1938 this._drawerState = this._DRAWER_STATE.FLINGING;
1939
1940 var x = event.detail.dx + this._translateOffset;
1941 var drawerWidth = this.getWidth();
1942 var isPositionLeft = this.position === 'left';
1943 var isVelocityPositive = velocity > 0;
1944 var isClosingLeft = !isVelocityPositive && isPositionLeft;
1945 var isClosingRight = isVelocityPositive && !isPositionLeft;
1946 var dx;
1947 if (isClosingLeft) {
1948 dx = -(x + drawerWidth);
1949 } else if (isClosingRight) {
1950 dx = (drawerWidth - x);
1951 } else {
1952 dx = -x;
1953 }
1954
1955 // Enforce a minimum transition velocity to make the drawer feel snappy.
1956 if (isVelocityPositive) {
1957 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
1958 this.opened = this.position === 'left';
1959 } else {
1960 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
1961 this.opened = this.position === 'right';
1962 }
1963
1964 // Calculate the amount of time needed to finish the transition based on the
1965 // initial slope of the timing function.
1966 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms');
1967 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);
1968
1969 this._resetDrawerTranslate();
1970 },
1971
1972 _transitionend: function(event) {
1973 // contentContainer will transition on opened state changed, and scrim w ill
1974 // transition on persistent state changed when opened - these are the
1975 // transitions we are interested in.
1976 var target = Polymer.dom(event).rootTarget;
1977 if (target === this.$.contentContainer || target === this.$.scrim) {
1978
1979 // If the drawer was flinging, we need to reset the style attributes.
1980 if (this._drawerState === this._DRAWER_STATE.FLINGING) {
1981 this._setTransitionDuration('');
1982 this._setTransitionTimingFunction('');
1983 this.style.visibility = '';
1984 }
1985
1986 this._resetDrawerState();
1987 }
1988 },
1989
1990 _setTransitionDuration: function(duration) {
1991 this.$.contentContainer.style.transitionDuration = duration;
1992 this.$.scrim.style.transitionDuration = duration;
1993 },
1994
1995 _setTransitionTimingFunction: function(timingFunction) {
1996 this.$.contentContainer.style.transitionTimingFunction = timingFunction;
1997 this.$.scrim.style.transitionTimingFunction = timingFunction;
1998 },
1999
2000 _translateDrawer: function(x) {
2001 var drawerWidth = this.getWidth();
2002
2003 if (this.position === 'left') {
2004 x = Math.max(-drawerWidth, Math.min(x, 0));
2005 this.$.scrim.style.opacity = 1 + x / drawerWidth;
2006 } else {
2007 x = Math.max(0, Math.min(x, drawerWidth));
2008 this.$.scrim.style.opacity = 1 - x / drawerWidth;
2009 }
2010
2011 this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
2012 },
2013
2014 _resetDrawerTranslate: function() {
2015 this.$.scrim.style.opacity = '';
2016 this.transform('', this.$.contentContainer);
2017 },
2018
2019 _resetDrawerState: function() {
2020 var oldState = this._drawerState;
2021 if (this.opened) {
2022 this._drawerState = this.persistent ?
2023 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED;
2024 } else {
2025 this._drawerState = this._DRAWER_STATE.CLOSED;
2026 }
2027
2028 if (oldState !== this._drawerState) {
2029 if (this._drawerState === this._DRAWER_STATE.OPENED) {
2030 this._setKeyboardFocusTrap();
2031 document.addEventListener('keydown', this._boundEscKeydownHandler);
2032 document.body.style.overflow = 'hidden';
2033 } else {
2034 document.removeEventListener('keydown', this._boundEscKeydownHandler );
2035 document.body.style.overflow = '';
2036 }
2037
2038 // Don't fire the event on initial load.
2039 if (oldState !== this._DRAWER_STATE.INIT) {
2040 this.fire('app-drawer-transitioned');
2041 }
2042 }
2043 },
2044
2045 _setKeyboardFocusTrap: function() {
2046 if (this.noFocusTrap) {
2047 return;
2048 }
2049
2050 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated) , this will
2051 // not select focusable elements inside shadow roots.
2052 var focusableElementsSelector = [
2053 'a[href]:not([tabindex="-1"])',
2054 'area[href]:not([tabindex="-1"])',
2055 'input:not([disabled]):not([tabindex="-1"])',
2056 'select:not([disabled]):not([tabindex="-1"])',
2057 'textarea:not([disabled]):not([tabindex="-1"])',
2058 'button:not([disabled]):not([tabindex="-1"])',
2059 'iframe:not([tabindex="-1"])',
2060 '[tabindex]:not([tabindex="-1"])',
2061 '[contentEditable=true]:not([tabindex="-1"])'
2062 ].join(',');
2063 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem entsSelector);
2064
2065 if (focusableElements.length > 0) {
2066 this._firstTabStop = focusableElements[0];
2067 this._lastTabStop = focusableElements[focusableElements.length - 1];
2068 } else {
2069 // Reset saved tab stops when there are no focusable elements in the d rawer.
2070 this._firstTabStop = null;
2071 this._lastTabStop = null;
2072 }
2073
2074 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the first focusable
2075 // element in the drawer, if it exists. Use the tabindex attribute since the this.tabIndex
2076 // property in IE/Edge returns 0 (instead of -1) when the attribute is n ot set.
2077 var tabindex = this.getAttribute('tabindex');
2078 if (tabindex && parseInt(tabindex, 10) > -1) {
2079 this.focus();
2080 } else if (this._firstTabStop) {
2081 this._firstTabStop.focus();
2082 }
2083 },
2084
2085 _tabKeydownHandler: function(event) {
2086 if (this.noFocusTrap) {
2087 return;
2088 }
2089
2090 var TAB_KEYCODE = 9;
2091 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB_KEYCODE) {
2092 if (event.shiftKey) {
2093 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f irstTabStop) {
2094 event.preventDefault();
2095 this._lastTabStop.focus();
2096 }
2097 } else {
2098 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la stTabStop) {
2099 event.preventDefault();
2100 this._firstTabStop.focus();
2101 }
2102 }
2103 }
2104 },
2105
2106 _MIN_FLING_THRESHOLD: 0.2,
2107
2108 _MIN_TRANSITION_VELOCITY: 1.2,
2109
2110 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
2111
2112 _FLING_INITIAL_SLOPE: 1.5,
2113
2114 _DRAWER_STATE: {
2115 INIT: 0,
2116 OPENED: 1,
2117 OPENED_PERSISTENT: 2,
2118 CLOSED: 3,
2119 TRACKING: 4,
2120 FLINGING: 5
2121 }
2122
2123 /**
2124 * Fired when the layout of app-drawer has changed.
2125 *
2126 * @event app-drawer-reset-layout
2127 */
2128
2129 /**
2130 * Fired when app-drawer has finished transitioning.
2131 *
2132 * @event app-drawer-transitioned
2133 */
2134 });
2135 (function() {
2136 'use strict';
2137
2138 Polymer({
2139 is: 'iron-location',
2140 properties: {
2141 /**
2142 * The pathname component of the URL.
2143 */
2144 path: {
2145 type: String,
2146 notify: true,
2147 value: function() {
2148 return window.decodeURIComponent(window.location.pathname);
2149 }
2150 },
2151 /**
2152 * The query string portion of the URL.
2153 */
2154 query: {
2155 type: String,
2156 notify: true,
2157 value: function() {
2158 return window.decodeURIComponent(window.location.search.slice(1));
2159 }
2160 },
2161 /**
2162 * The hash component of the URL.
2163 */
2164 hash: {
2165 type: String,
2166 notify: true,
2167 value: function() {
2168 return window.decodeURIComponent(window.location.hash.slice(1));
2169 }
2170 },
2171 /**
2172 * If the user was on a URL for less than `dwellTime` milliseconds, it
2173 * won't be added to the browser's history, but instead will be replaced
2174 * by the next entry.
2175 *
2176 * This is to prevent large numbers of entries from clogging up the user 's
2177 * browser history. Disable by setting to a negative number.
2178 */
2179 dwellTime: {
2180 type: Number,
2181 value: 2000
2182 },
2183
2184 /**
2185 * A regexp that defines the set of URLs that should be considered part
2186 * of this web app.
2187 *
2188 * Clicking on a link that matches this regex won't result in a full pag e
2189 * navigation, but will instead just update the URL state in place.
2190 *
2191 * This regexp is given everything after the origin in an absolute
2192 * URL. So to match just URLs that start with /search/ do:
2193 * url-space-regex="^/search/"
2194 *
2195 * @type {string|RegExp}
2196 */
2197 urlSpaceRegex: {
2198 type: String,
2199 value: ''
2200 },
2201
2202 /**
2203 * urlSpaceRegex, but coerced into a regexp.
2204 *
2205 * @type {RegExp}
2206 */
2207 _urlSpaceRegExp: {
2208 computed: '_makeRegExp(urlSpaceRegex)'
2209 },
2210
2211 _lastChangedAt: {
2212 type: Number
2213 },
2214
2215 _initialized: {
2216 type: Boolean,
2217 value: false
2218 }
2219 },
2220 hostAttributes: {
2221 hidden: true
2222 },
2223 observers: [
2224 '_updateUrl(path, query, hash)'
2225 ],
2226 attached: function() {
2227 this.listen(window, 'hashchange', '_hashChanged');
2228 this.listen(window, 'location-changed', '_urlChanged');
2229 this.listen(window, 'popstate', '_urlChanged');
2230 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g lobalOnClick');
2231 // Give a 200ms grace period to make initial redirects without any
2232 // additions to the user's history.
2233 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
2234
2235 this._initialized = true;
2236 this._urlChanged();
2237 },
2238 detached: function() {
2239 this.unlisten(window, 'hashchange', '_hashChanged');
2240 this.unlisten(window, 'location-changed', '_urlChanged');
2241 this.unlisten(window, 'popstate', '_urlChanged');
2242 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', ' _globalOnClick');
2243 this._initialized = false;
2244 },
2245 _hashChanged: function() {
2246 this.hash = window.decodeURIComponent(window.location.hash.substring(1)) ;
2247 },
2248 _urlChanged: function() {
2249 // We want to extract all info out of the updated URL before we
2250 // try to write anything back into it.
2251 //
2252 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
2253 // one when we set this.hash. Likewise for query.
2254 this._dontUpdateUrl = true;
2255 this._hashChanged();
2256 this.path = window.decodeURIComponent(window.location.pathname);
2257 this.query = window.decodeURIComponent(
2258 window.location.search.substring(1));
2259 this._dontUpdateUrl = false;
2260 this._updateUrl();
2261 },
2262 _getUrl: function() {
2263 var partiallyEncodedPath = window.encodeURI(
2264 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
2265 var partiallyEncodedQuery = '';
2266 if (this.query) {
2267 partiallyEncodedQuery = '?' + window.encodeURI(
2268 this.query).replace(/\#/g, '%23');
2269 }
2270 var partiallyEncodedHash = '';
2271 if (this.hash) {
2272 partiallyEncodedHash = '#' + window.encodeURI(this.hash);
2273 }
2274 return (
2275 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash) ;
2276 },
2277 _updateUrl: function() {
2278 if (this._dontUpdateUrl || !this._initialized) {
2279 return;
2280 }
2281 if (this.path === window.decodeURIComponent(window.location.pathname) &&
2282 this.query === window.decodeURIComponent(
2283 window.location.search.substring(1)) &&
2284 this.hash === window.decodeURIComponent(
2285 window.location.hash.substring(1))) {
2286 // Nothing to do, the current URL is a representation of our propertie s.
2287 return;
2288 }
2289 var newUrl = this._getUrl();
2290 // Need to use a full URL in case the containing page has a base URI.
2291 var fullNewUrl = new URL(
2292 newUrl, window.location.protocol + '//' + window.location.host).href ;
2293 var now = window.performance.now();
2294 var shouldReplace =
2295 this._lastChangedAt + this.dwellTime > now;
2296 this._lastChangedAt = now;
2297 if (shouldReplace) {
2298 window.history.replaceState({}, '', fullNewUrl);
2299 } else {
2300 window.history.pushState({}, '', fullNewUrl);
2301 }
2302 this.fire('location-changed', {}, {node: window});
2303 },
2304 /**
2305 * A necessary evil so that links work as expected. Does its best to
2306 * bail out early if possible.
2307 *
2308 * @param {MouseEvent} event .
2309 */
2310 _globalOnClick: function(event) {
2311 // If another event handler has stopped this event then there's nothing
2312 // for us to do. This can happen e.g. when there are multiple
2313 // iron-location elements in a page.
2314 if (event.defaultPrevented) {
2315 return;
2316 }
2317 var href = this._getSameOriginLinkHref(event);
2318 if (!href) {
2319 return;
2320 }
2321 event.preventDefault();
2322 // If the navigation is to the current page we shouldn't add a history
2323 // entry or fire a change event.
2324 if (href === window.location.href) {
2325 return;
2326 }
2327 window.history.pushState({}, '', href);
2328 this.fire('location-changed', {}, {node: window});
2329 },
2330 /**
2331 * Returns the absolute URL of the link (if any) that this click event
2332 * is clicking on, if we can and should override the resulting full
2333 * page navigation. Returns null otherwise.
2334 *
2335 * @param {MouseEvent} event .
2336 * @return {string?} .
2337 */
2338 _getSameOriginLinkHref: function(event) {
2339 // We only care about left-clicks.
2340 if (event.button !== 0) {
2341 return null;
2342 }
2343 // We don't want modified clicks, where the intent is to open the page
2344 // in a new tab.
2345 if (event.metaKey || event.ctrlKey) {
2346 return null;
2347 }
2348 var eventPath = Polymer.dom(event).path;
2349 var anchor = null;
2350 for (var i = 0; i < eventPath.length; i++) {
2351 var element = eventPath[i];
2352 if (element.tagName === 'A' && element.href) {
2353 anchor = element;
2354 break;
2355 }
2356 }
2357
2358 // If there's no link there's nothing to do.
2359 if (!anchor) {
2360 return null;
2361 }
2362
2363 // Target blank is a new tab, don't intercept.
2364 if (anchor.target === '_blank') {
2365 return null;
2366 }
2367 // If the link is for an existing parent frame, don't intercept.
2368 if ((anchor.target === '_top' ||
2369 anchor.target === '_parent') &&
2370 window.top !== window) {
2371 return null;
2372 }
2373
2374 var href = anchor.href;
2375
2376 // It only makes sense for us to intercept same-origin navigations.
2377 // pushState/replaceState don't work with cross-origin links.
2378 var url;
2379 if (document.baseURI != null) {
2380 url = new URL(href, /** @type {string} */(document.baseURI));
2381 } else {
2382 url = new URL(href);
2383 }
2384
2385 var origin;
2386
2387 // IE Polyfill
2388 if (window.location.origin) {
2389 origin = window.location.origin;
2390 } else {
2391 origin = window.location.protocol + '//' + window.location.hostname;
2392
2393 if (window.location.port) {
2394 origin += ':' + window.location.port;
2395 }
2396 }
2397
2398 if (url.origin !== origin) {
2399 return null;
2400 }
2401 var normalizedHref = url.pathname + url.search + url.hash;
2402
2403 // If we've been configured not to handle this url... don't handle it!
2404 if (this._urlSpaceRegExp &&
2405 !this._urlSpaceRegExp.test(normalizedHref)) {
2406 return null;
2407 }
2408 // Need to use a full URL in case the containing page has a base URI.
2409 var fullNormalizedHref = new URL(
2410 normalizedHref, window.location.href).href;
2411 return fullNormalizedHref;
2412 },
2413 _makeRegExp: function(urlSpaceRegex) {
2414 return RegExp(urlSpaceRegex);
2415 }
2416 });
2417 })();
2418 'use strict';
2419
2420 Polymer({
2421 is: 'iron-query-params',
2422 properties: {
2423 paramsString: {
2424 type: String,
2425 notify: true,
2426 observer: 'paramsStringChanged',
2427 },
2428 paramsObject: {
2429 type: Object,
2430 notify: true,
2431 value: function() {
2432 return {};
2433 }
2434 },
2435 _dontReact: {
2436 type: Boolean,
2437 value: false
2438 }
2439 },
2440 hostAttributes: {
2441 hidden: true
2442 },
2443 observers: [
2444 'paramsObjectChanged(paramsObject.*)'
2445 ],
2446 paramsStringChanged: function() {
2447 this._dontReact = true;
2448 this.paramsObject = this._decodeParams(this.paramsString);
2449 this._dontReact = false;
2450 },
2451 paramsObjectChanged: function() {
2452 if (this._dontReact) {
2453 return;
2454 }
2455 this.paramsString = this._encodeParams(this.paramsObject);
2456 },
2457 _encodeParams: function(params) {
2458 var encodedParams = [];
2459 for (var key in params) {
2460 var value = params[key];
2461 if (value === '') {
2462 encodedParams.push(encodeURIComponent(key));
2463 } else if (value) {
2464 encodedParams.push(
2465 encodeURIComponent(key) +
2466 '=' +
2467 encodeURIComponent(value.toString())
2468 );
2469 }
2470 }
2471 return encodedParams.join('&');
2472 },
2473 _decodeParams: function(paramString) {
2474 var params = {};
2475
2476 // Work around a bug in decodeURIComponent where + is not
2477 // converted to spaces:
2478 paramString = (paramString || '').replace(/\+/g, '%20');
2479
2480 var paramList = paramString.split('&');
2481 for (var i = 0; i < paramList.length; i++) {
2482 var param = paramList[i].split('=');
2483 if (param[0]) {
2484 params[decodeURIComponent(param[0])] =
2485 decodeURIComponent(param[1] || '');
2486 }
2487 }
2488 return params;
2489 }
2490 });
2491 'use strict';
2492
2493 /**
2494 * Provides bidirectional mapping between `path` and `queryParams` and a
2495 * app-route compatible `route` object.
2496 *
2497 * For more information, see the docs for `app-route-converter`.
2498 *
2499 * @polymerBehavior
2500 */
2501 Polymer.AppRouteConverterBehavior = {
2502 properties: {
2503 /**
2504 * A model representing the deserialized path through the route tree, as
2505 * well as the current queryParams.
2506 *
2507 * A route object is the kernel of the routing system. It is intended to
2508 * be fed into consuming elements such as `app-route`.
2509 *
2510 * @type {?Object}
2511 */
2512 route: {
2513 type: Object,
2514 notify: true
2515 },
2516
2517 /**
2518 * A set of key/value pairs that are universally accessible to branches of
2519 * the route tree.
2520 *
2521 * @type {?Object}
2522 */
2523 queryParams: {
2524 type: Object,
2525 notify: true
2526 },
2527
2528 /**
2529 * The serialized path through the route tree. This corresponds to the
2530 * `window.location.pathname` value, and will update to reflect changes
2531 * to that value.
2532 */
2533 path: {
2534 type: String,
2535 notify: true,
2536 }
2537 },
2538
2539 observers: [
2540 '_locationChanged(path, queryParams)',
2541 '_routeChanged(route.prefix, route.path)',
2542 '_routeQueryParamsChanged(route.__queryParams)'
2543 ],
2544
2545 created: function() {
2546 this.linkPaths('route.__queryParams', 'queryParams');
2547 this.linkPaths('queryParams', 'route.__queryParams');
2548 },
2549
2550 /**
2551 * Handler called when the path or queryParams change.
2552 */
2553 _locationChanged: function() {
2554 if (this.route &&
2555 this.route.path === this.path &&
2556 this.queryParams === this.route.__queryParams) {
2557 return;
2558 }
2559 this.route = {
2560 prefix: '',
2561 path: this.path,
2562 __queryParams: this.queryParams
2563 };
2564 },
2565
2566 /**
2567 * Handler called when the route prefix and route path change.
2568 */
2569 _routeChanged: function() {
2570 if (!this.route) {
2571 return;
2572 }
2573
2574 this.path = this.route.prefix + this.route.path;
2575 },
2576
2577 /**
2578 * Handler called when the route queryParams change.
2579 *
2580 * @param {Object} queryParams A set of key/value pairs that are
2581 * universally accessible to branches of the route tree.
2582 */
2583 _routeQueryParamsChanged: function(queryParams) {
2584 if (!this.route) {
2585 return;
2586 }
2587 this.queryParams = queryParams;
2588 }
2589 };
2590 'use strict';
2591
2592 Polymer({
2593 is: 'app-location',
2594
2595 properties: {
2596 /**
2597 * A model representing the deserialized path through the route tree, as
2598 * well as the current queryParams.
2599 */
2600 route: {
2601 type: Object,
2602 notify: true
2603 },
2604
2605 /**
2606 * In many scenarios, it is convenient to treat the `hash` as a stand-in
2607 * alternative to the `path`. For example, if deploying an app to a stat ic
2608 * web server (e.g., Github Pages) - where one does not have control ove r
2609 * server-side routing - it is usually a better experience to use the ha sh
2610 * to represent paths through one's app.
2611 *
2612 * When this property is set to true, the `hash` will be used in place o f
2613
2614 * the `path` for generating a `route`.
2615 */
2616 useHashAsPath: {
2617 type: Boolean,
2618 value: false
2619 },
2620
2621 /**
2622 * A regexp that defines the set of URLs that should be considered part
2623 * of this web app.
2624 *
2625 * Clicking on a link that matches this regex won't result in a full pag e
2626 * navigation, but will instead just update the URL state in place.
2627 *
2628 * This regexp is given everything after the origin in an absolute
2629 * URL. So to match just URLs that start with /search/ do:
2630 * url-space-regex="^/search/"
2631 *
2632 * @type {string|RegExp}
2633 */
2634 urlSpaceRegex: {
2635 type: String,
2636 notify: true
2637 },
2638
2639 /**
2640 * A set of key/value pairs that are universally accessible to branches
2641 * of the route tree.
2642 */
2643 __queryParams: {
2644 type: Object
2645 },
2646
2647 /**
2648 * The pathname component of the current URL.
2649 */
2650 __path: {
2651 type: String
2652 },
2653
2654 /**
2655 * The query string portion of the current URL.
2656 */
2657 __query: {
2658 type: String
2659 },
2660
2661 /**
2662 * The hash portion of the current URL.
2663 */
2664 __hash: {
2665 type: String
2666 },
2667
2668 /**
2669 * The route path, which will be either the hash or the path, depending
2670 * on useHashAsPath.
2671 */
2672 path: {
2673 type: String,
2674 observer: '__onPathChanged'
2675 }
2676 },
2677
2678 behaviors: [Polymer.AppRouteConverterBehavior],
2679
2680 observers: [
2681 '__computeRoutePath(useHashAsPath, __hash, __path)'
2682 ],
2683
2684 __computeRoutePath: function() {
2685 this.path = this.useHashAsPath ? this.__hash : this.__path;
2686 },
2687
2688 __onPathChanged: function() {
2689 if (!this._readied) {
2690 return;
2691 }
2692
2693 if (this.useHashAsPath) {
2694 this.__hash = this.path;
2695 } else {
2696 this.__path = this.path;
2697 }
2698 }
2699 });
2700 'use strict';
2701
2702 Polymer({
2703 is: 'app-route',
2704
2705 properties: {
2706 /**
2707 * The URL component managed by this element.
2708 */
2709 route: {
2710 type: Object,
2711 notify: true
2712 },
2713
2714 /**
2715 * The pattern of slash-separated segments to match `path` against.
2716 *
2717 * For example the pattern "/foo" will match "/foo" or "/foo/bar"
2718 * but not "/foobar".
2719 *
2720 * Path segments like `/:named` are mapped to properties on the `data` obj ect.
2721 */
2722 pattern: {
2723 type: String
2724 },
2725
2726 /**
2727 * The parameterized values that are extracted from the route as
2728 * described by `pattern`.
2729 */
2730 data: {
2731 type: Object,
2732 value: function() {return {};},
2733 notify: true
2734 },
2735
2736 /**
2737 * @type {?Object}
2738 */
2739 queryParams: {
2740 type: Object,
2741 value: function() {
2742 return {};
2743 },
2744 notify: true
2745 },
2746
2747 /**
2748 * The part of `path` NOT consumed by `pattern`.
2749 */
2750 tail: {
2751 type: Object,
2752 value: function() {return {path: null, prefix: null, __queryParams: null };},
2753 notify: true
2754 },
2755
2756 active: {
2757 type: Boolean,
2758 notify: true,
2759 readOnly: true
2760 },
2761
2762 _queryParamsUpdating: {
2763 type: Boolean,
2764 value: false
2765 },
2766 /**
2767 * @type {?string}
2768 */
2769 _matched: {
2770 type: String,
2771 value: ''
2772 }
2773 },
2774
2775 observers: [
2776 '__tryToMatch(route.path, pattern)',
2777 '__updatePathOnDataChange(data.*)',
2778 '__tailPathChanged(tail.path)',
2779 '__routeQueryParamsChanged(route.__queryParams)',
2780 '__tailQueryParamsChanged(tail.__queryParams)',
2781 '__queryParamsChanged(queryParams.*)'
2782 ],
2783
2784 created: function() {
2785 this.linkPaths('route.__queryParams', 'tail.__queryParams');
2786 this.linkPaths('tail.__queryParams', 'route.__queryParams');
2787 },
2788
2789 /**
2790 * Deal with the query params object being assigned to wholesale.
2791 * @export
2792 */
2793 __routeQueryParamsChanged: function(queryParams) {
2794 if (queryParams && this.tail) {
2795 this.set('tail.__queryParams', queryParams);
2796
2797 if (!this.active || this._queryParamsUpdating) {
2798 return;
2799 }
2800
2801 // Copy queryParams and track whether there are any differences compared
2802 // to the existing query params.
2803 var copyOfQueryParams = {};
2804 var anythingChanged = false;
2805 for (var key in queryParams) {
2806 copyOfQueryParams[key] = queryParams[key];
2807 if (anythingChanged ||
2808 !this.queryParams ||
2809 queryParams[key] !== this.queryParams[key]) {
2810 anythingChanged = true;
2811 }
2812 }
2813 // Need to check whether any keys were deleted
2814 for (var key in this.queryParams) {
2815 if (anythingChanged || !(key in queryParams)) {
2816 anythingChanged = true;
2817 break;
2818 }
2819 }
2820
2821 if (!anythingChanged) {
2822 return;
2823 }
2824 this._queryParamsUpdating = true;
2825 this.set('queryParams', copyOfQueryParams);
2826 this._queryParamsUpdating = false;
2827 }
2828 },
2829
2830 /**
2831 * @export
2832 */
2833 __tailQueryParamsChanged: function(queryParams) {
2834 if (queryParams && this.route) {
2835 this.set('route.__queryParams', queryParams);
2836 }
2837 },
2838
2839 /**
2840 * @export
2841 */
2842 __queryParamsChanged: function(changes) {
2843 if (!this.active || this._queryParamsUpdating) {
2844 return;
2845 }
2846
2847 this.set('route.__' + changes.path, changes.value);
2848 },
2849
2850 __resetProperties: function() {
2851 this._setActive(false);
2852 this._matched = null;
2853 //this.tail = { path: null, prefix: null, queryParams: null };
2854 //this.data = {};
2855 },
2856
2857 /**
2858 * @export
2859 */
2860 __tryToMatch: function() {
2861 if (!this.route) {
2862 return;
2863 }
2864 var path = this.route.path;
2865 var pattern = this.pattern;
2866 if (!pattern) {
2867 return;
2868 }
2869
2870 if (!path) {
2871 this.__resetProperties();
2872 return;
2873 }
2874
2875 var remainingPieces = path.split('/');
2876 var patternPieces = pattern.split('/');
2877
2878 var matched = [];
2879 var namedMatches = {};
2880
2881 for (var i=0; i < patternPieces.length; i++) {
2882 var patternPiece = patternPieces[i];
2883 if (!patternPiece && patternPiece !== '') {
2884 break;
2885 }
2886 var pathPiece = remainingPieces.shift();
2887
2888 // We don't match this path.
2889 if (!pathPiece && pathPiece !== '') {
2890 this.__resetProperties();
2891 return;
2892 }
2893 matched.push(pathPiece);
2894
2895 if (patternPiece.charAt(0) == ':') {
2896 namedMatches[patternPiece.slice(1)] = pathPiece;
2897 } else if (patternPiece !== pathPiece) {
2898 this.__resetProperties();
2899 return;
2900 }
2901 }
2902
2903 this._matched = matched.join('/');
2904
2905 // Properties that must be updated atomically.
2906 var propertyUpdates = {};
2907
2908 //this.active
2909 if (!this.active) {
2910 propertyUpdates.active = true;
2911 }
2912
2913 // this.tail
2914 var tailPrefix = this.route.prefix + this._matched;
2915 var tailPath = remainingPieces.join('/');
2916 if (remainingPieces.length > 0) {
2917 tailPath = '/' + tailPath;
2918 }
2919 if (!this.tail ||
2920 this.tail.prefix !== tailPrefix ||
2921 this.tail.path !== tailPath) {
2922 propertyUpdates.tail = {
2923 prefix: tailPrefix,
2924 path: tailPath,
2925 __queryParams: this.route.__queryParams
2926 };
2927 }
2928
2929 // this.data
2930 propertyUpdates.data = namedMatches;
2931 this._dataInUrl = {};
2932 for (var key in namedMatches) {
2933 this._dataInUrl[key] = namedMatches[key];
2934 }
2935
2936 this.__setMulti(propertyUpdates);
2937 },
2938
2939 /**
2940 * @export
2941 */
2942 __tailPathChanged: function() {
2943 if (!this.active) {
2944 return;
2945 }
2946 var tailPath = this.tail.path;
2947 var newPath = this._matched;
2948 if (tailPath) {
2949 if (tailPath.charAt(0) !== '/') {
2950 tailPath = '/' + tailPath;
2951 }
2952 newPath += tailPath;
2953 }
2954 this.set('route.path', newPath);
2955 },
2956
2957 /**
2958 * @export
2959 */
2960 __updatePathOnDataChange: function() {
2961 if (!this.route || !this.active) {
2962 return;
2963 }
2964 var newPath = this.__getLink({});
2965 var oldPath = this.__getLink(this._dataInUrl);
2966 if (newPath === oldPath) {
2967 return;
2968 }
2969 this.set('route.path', newPath);
2970 },
2971
2972 __getLink: function(overrideValues) {
2973 var values = {tail: null};
2974 for (var key in this.data) {
2975 values[key] = this.data[key];
2976 }
2977 for (var key in overrideValues) {
2978 values[key] = overrideValues[key];
2979 }
2980 var patternPieces = this.pattern.split('/');
2981 var interp = patternPieces.map(function(value) {
2982 if (value[0] == ':') {
2983 value = values[value.slice(1)];
2984 }
2985 return value;
2986 }, this);
2987 if (values.tail && values.tail.path) {
2988 if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
2989 interp.push(values.tail.path.slice(1));
2990 } else {
2991 interp.push(values.tail.path);
2992 }
2993 }
2994 return interp.join('/');
2995 },
2996
2997 __setMulti: function(setObj) {
2998 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at
2999 // internal data structures. I would not advise that you copy this
3000 // example.
3001 //
3002 // In the future this will be a feature of Polymer itself.
3003 // See: https://github.com/Polymer/polymer/issues/3640
3004 //
3005 // Hacking around with private methods like this is juggling footguns,
3006 // and is likely to have unexpected and unsupported rough edges.
3007 //
3008 // Be ye so warned.
3009 for (var property in setObj) {
3010 this._propertySetter(property, setObj[property]);
3011 }
3012
3013 for (var property in setObj) {
3014 this._pathEffector(property, this[property]);
3015 this._notifyPathUp(property, this[property]);
3016 }
3017 }
3018 });
3019 Polymer({
3020
3021 is: 'iron-media-query',
3022
3023 properties: {
3024
3025 /**
3026 * The Boolean return value of the media query.
3027 */
3028 queryMatches: {
3029 type: Boolean,
3030 value: false,
3031 readOnly: true,
3032 notify: true
3033 },
3034
3035 /**
3036 * The CSS media query to evaluate.
3037 */
3038 query: {
3039 type: String,
3040 observer: 'queryChanged'
3041 },
3042
3043 /**
3044 * If true, the query attribute is assumed to be a complete media query
3045 * string rather than a single media feature.
3046 */
3047 full: {
3048 type: Boolean,
3049 value: false
3050 },
3051
3052 /**
3053 * @type {function(MediaQueryList)}
3054 */
3055 _boundMQHandler: {
3056 value: function() {
3057 return this.queryHandler.bind(this);
3058 }
3059 },
3060
3061 /**
3062 * @type {MediaQueryList}
3063 */
3064 _mq: {
3065 value: null
3066 }
3067 },
3068
3069 attached: function() {
3070 this.style.display = 'none';
3071 this.queryChanged();
3072 },
3073
3074 detached: function() {
3075 this._remove();
3076 },
3077
3078 _add: function() {
3079 if (this._mq) {
3080 this._mq.addListener(this._boundMQHandler);
3081 }
3082 },
3083
3084 _remove: function() {
3085 if (this._mq) {
3086 this._mq.removeListener(this._boundMQHandler);
3087 }
3088 this._mq = null;
3089 },
3090
3091 queryChanged: function() {
3092 this._remove();
3093 var query = this.query;
3094 if (!query) {
3095 return;
3096 }
3097 if (!this.full && query[0] !== '(') {
3098 query = '(' + query + ')';
3099 }
3100 this._mq = window.matchMedia(query);
3101 this._add();
3102 this.queryHandler(this._mq);
3103 },
3104
3105 queryHandler: function(mq) {
3106 this._setQueryMatches(mq.matches);
3107 }
3108
3109 });
1590 /** 3110 /**
1591 * `IronResizableBehavior` is a behavior that can be used in Polymer elements to 3111 * `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 3112 * 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 3113 * 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 3114 * notified when they are resized or un-hidden by their parents in order to ta ke
1595 * action on their new measurements). 3115 * action on their new measurements).
1596 * 3116 *
1597 * Elements that perform measurement should add the `IronResizableBehavior` be havior to 3117 * 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. 3118 * 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, 3119 * 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). 3280 // else they will get redundantly notified when the parent attaches).
1761 if (!this.isAttached) { 3281 if (!this.isAttached) {
1762 return; 3282 return;
1763 } 3283 }
1764 3284
1765 this._notifyingDescendant = true; 3285 this._notifyingDescendant = true;
1766 descendant.notifyResize(); 3286 descendant.notifyResize();
1767 this._notifyingDescendant = false; 3287 this._notifyingDescendant = false;
1768 } 3288 }
1769 }; 3289 };
1770 (function() { 3290 /**
1771 'use strict'; 3291 * @param {!Function} selectCallback
1772 3292 * @constructor
1773 /** 3293 */
1774 * Chrome uses an older version of DOM Level 3 Keyboard Events 3294 Polymer.IronSelection = function(selectCallback) {
3295 this.selection = [];
3296 this.selectCallback = selectCallback;
3297 };
3298
3299 Polymer.IronSelection.prototype = {
3300
3301 /**
3302 * Retrieves the selected item(s).
1775 * 3303 *
1776 * Most keys are labeled as text, but some are Unicode codepoints. 3304 * @method get
1777 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set 3305 * @returns Returns the selected item(s). If the multi property is true,
1778 */ 3306 * `get` will return an array, otherwise it will return
1779 var KEY_IDENTIFIER = { 3307 * the selected item or undefined if there is no selection.
1780 'U+0008': 'backspace', 3308 */
1781 'U+0009': 'tab', 3309 get: function() {
1782 'U+001B': 'esc', 3310 return this.multi ? this.selection.slice() : this.selection[0];
1783 'U+0020': 'space', 3311 },
1784 'U+007F': 'del' 3312
1785 }; 3313 /**
1786 3314 * Clears all the selection except the ones indicated.
1787 /**
1788 * Special table for KeyboardEvent.keyCode.
1789 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er
1790 * than that.
1791 * 3315 *
1792 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode 3316 * @method clear
1793 */ 3317 * @param {Array} excludes items to be excluded.
1794 var KEY_CODE = { 3318 */
1795 8: 'backspace', 3319 clear: function(excludes) {
1796 9: 'tab', 3320 this.selection.slice().forEach(function(item) {
1797 13: 'enter', 3321 if (!excludes || excludes.indexOf(item) < 0) {
1798 27: 'esc', 3322 this.setItemSelected(item, false);
1799 33: 'pageup', 3323 }
1800 34: 'pagedown', 3324 }, this);
1801 35: 'end', 3325 },
1802 36: 'home', 3326
1803 32: 'space', 3327 /**
1804 37: 'left', 3328 * Indicates if a given item is selected.
1805 38: 'up',
1806 39: 'right',
1807 40: 'down',
1808 46: 'del',
1809 106: '*'
1810 };
1811
1812 /**
1813 * MODIFIER_KEYS maps the short name for modifier keys used in a key
1814 * combo string to the property name that references those same keys
1815 * in a KeyboardEvent instance.
1816 */
1817 var MODIFIER_KEYS = {
1818 'shift': 'shiftKey',
1819 'ctrl': 'ctrlKey',
1820 'alt': 'altKey',
1821 'meta': 'metaKey'
1822 };
1823
1824 /**
1825 * KeyboardEvent.key is mostly represented by printable character made by
1826 * the keyboard, with unprintable keys labeled nicely.
1827 * 3329 *
1828 * However, on OS X, Alt+char can make a Unicode character that follows an 3330 * @method isSelected
1829 * Apple-specific mapping. In this case, we fall back to .keyCode. 3331 * @param {*} item The item whose selection state should be checked.
1830 */ 3332 * @returns Returns true if `item` is selected.
1831 var KEY_CHAR = /[a-z0-9*]/; 3333 */
1832 3334 isSelected: function(item) {
1833 /** 3335 return this.selection.indexOf(item) >= 0;
1834 * Matches a keyIdentifier string. 3336 },
1835 */ 3337
1836 var IDENT_CHAR = /U\+/; 3338 /**
1837 3339 * Sets the selection state for a given item to either selected or deselecte d.
1838 /**
1839 * Matches arrow keys in Gecko 27.0+
1840 */
1841 var ARROW_KEY = /^arrow/;
1842
1843 /**
1844 * Matches space keys everywhere (notably including IE10's exceptional name
1845 * `spacebar`).
1846 */
1847 var SPACE_KEY = /^space(bar)?/;
1848
1849 /**
1850 * Matches ESC key.
1851 * 3340 *
1852 * Value from: http://w3c.github.io/uievents-key/#key-Escape 3341 * @method setItemSelected
1853 */ 3342 * @param {*} item The item to select.
1854 var ESC_KEY = /^escape$/; 3343 * @param {boolean} isSelected True for selected, false for deselected.
1855 3344 */
1856 /** 3345 setItemSelected: function(item, isSelected) {
1857 * Transforms the key. 3346 if (item != null) {
1858 * @param {string} key The KeyBoardEvent.key 3347 if (isSelected !== this.isSelected(item)) {
1859 * @param {Boolean} [noSpecialChars] Limits the transformation to 3348 // proceed to update selection only if requested state differs from cu rrent
1860 * alpha-numeric characters. 3349 if (isSelected) {
1861 */ 3350 this.selection.push(item);
1862 function transformKey(key, noSpecialChars) { 3351 } else {
1863 var validKey = ''; 3352 var i = this.selection.indexOf(item);
1864 if (key) { 3353 if (i >= 0) {
1865 var lKey = key.toLowerCase(); 3354 this.selection.splice(i, 1);
1866 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
1867 validKey = 'space';
1868 } else if (ESC_KEY.test(lKey)) {
1869 validKey = 'esc';
1870 } else if (lKey.length == 1) {
1871 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
1872 validKey = lKey;
1873 }
1874 } else if (ARROW_KEY.test(lKey)) {
1875 validKey = lKey.replace('arrow', '');
1876 } else if (lKey == 'multiply') {
1877 // numpad '*' can map to Multiply on IE/Windows
1878 validKey = '*';
1879 } else {
1880 validKey = lKey;
1881 }
1882 }
1883 return validKey;
1884 }
1885
1886 function transformKeyIdentifier(keyIdent) {
1887 var validKey = '';
1888 if (keyIdent) {
1889 if (keyIdent in KEY_IDENTIFIER) {
1890 validKey = KEY_IDENTIFIER[keyIdent];
1891 } else if (IDENT_CHAR.test(keyIdent)) {
1892 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
1893 validKey = String.fromCharCode(keyIdent).toLowerCase();
1894 } else {
1895 validKey = keyIdent.toLowerCase();
1896 }
1897 }
1898 return validKey;
1899 }
1900
1901 function transformKeyCode(keyCode) {
1902 var validKey = '';
1903 if (Number(keyCode)) {
1904 if (keyCode >= 65 && keyCode <= 90) {
1905 // ascii a-z
1906 // lowercase is 32 offset from uppercase
1907 validKey = String.fromCharCode(32 + keyCode);
1908 } else if (keyCode >= 112 && keyCode <= 123) {
1909 // function keys f1-f12
1910 validKey = 'f' + (keyCode - 112);
1911 } else if (keyCode >= 48 && keyCode <= 57) {
1912 // top 0-9 keys
1913 validKey = String(keyCode - 48);
1914 } else if (keyCode >= 96 && keyCode <= 105) {
1915 // num pad 0-9
1916 validKey = String(keyCode - 96);
1917 } else {
1918 validKey = KEY_CODE[keyCode];
1919 }
1920 }
1921 return validKey;
1922 }
1923
1924 /**
1925 * Calculates the normalized key for a KeyboardEvent.
1926 * @param {KeyboardEvent} keyEvent
1927 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
1928 * transformation to alpha-numeric chars. This is useful with key
1929 * combinations like shift + 2, which on FF for MacOS produces
1930 * keyEvent.key = @
1931 * To get 2 returned, set noSpecialChars = true
1932 * To get @ returned, set noSpecialChars = false
1933 */
1934 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
1935 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
1936 // .detail.key to support artificial keyboard events.
1937 return transformKey(keyEvent.key, noSpecialChars) ||
1938 transformKeyIdentifier(keyEvent.keyIdentifier) ||
1939 transformKeyCode(keyEvent.keyCode) ||
1940 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
1941 }
1942
1943 function keyComboMatchesEvent(keyCombo, event) {
1944 // For combos with modifiers we support only alpha-numeric keys
1945 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
1946 return keyEvent === keyCombo.key &&
1947 (!keyCombo.hasModifiers || (
1948 !!event.shiftKey === !!keyCombo.shiftKey &&
1949 !!event.ctrlKey === !!keyCombo.ctrlKey &&
1950 !!event.altKey === !!keyCombo.altKey &&
1951 !!event.metaKey === !!keyCombo.metaKey)
1952 );
1953 }
1954
1955 function parseKeyComboString(keyComboString) {
1956 if (keyComboString.length === 1) {
1957 return {
1958 combo: keyComboString,
1959 key: keyComboString,
1960 event: 'keydown'
1961 };
1962 }
1963 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP art) {
1964 var eventParts = keyComboPart.split(':');
1965 var keyName = eventParts[0];
1966 var event = eventParts[1];
1967
1968 if (keyName in MODIFIER_KEYS) {
1969 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
1970 parsedKeyCombo.hasModifiers = true;
1971 } else {
1972 parsedKeyCombo.key = keyName;
1973 parsedKeyCombo.event = event || 'keydown';
1974 }
1975
1976 return parsedKeyCombo;
1977 }, {
1978 combo: keyComboString.split(':').shift()
1979 });
1980 }
1981
1982 function parseEventString(eventString) {
1983 return eventString.trim().split(' ').map(function(keyComboString) {
1984 return parseKeyComboString(keyComboString);
1985 });
1986 }
1987
1988 /**
1989 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
1990 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
1991 * The element takes care of browser differences with respect to Keyboard ev ents
1992 * and uses an expressive syntax to filter key presses.
1993 *
1994 * Use the `keyBindings` prototype property to express what combination of k eys
1995 * will trigger the callback. A key binding has the format
1996 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
1997 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
1998 *
1999 * keyBindings: {
2000 * 'space': '_onKeydown', // same as 'space:keydown'
2001 * 'shift+tab': '_onKeydown',
2002 * 'enter:keypress': '_onKeypress',
2003 * 'esc:keyup': '_onKeyup'
2004 * }
2005 *
2006 * The callback will receive with an event containing the following informat ion in `event.detail`:
2007 *
2008 * _onKeydown: function(event) {
2009 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
2010 * console.log(event.detail.key); // KEY only, e.g. "tab"
2011 * console.log(event.detail.event); // EVENT, e.g. "keydown"
2012 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
2013 * }
2014 *
2015 * Use the `keyEventTarget` attribute to set up event handlers on a specific
2016 * node.
2017 *
2018 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
2019 * for an example.
2020 *
2021 * @demo demo/index.html
2022 * @polymerBehavior
2023 */
2024 Polymer.IronA11yKeysBehavior = {
2025 properties: {
2026 /**
2027 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
2028 * `null` to disable the listeners.
2029 * @type {?EventTarget}
2030 */
2031 keyEventTarget: {
2032 type: Object,
2033 value: function() {
2034 return this;
2035 }
2036 },
2037
2038 /**
2039 * If true, this property will cause the implementing element to
2040 * automatically stop propagation on any handled KeyboardEvents.
2041 */
2042 stopKeyboardEventPropagation: {
2043 type: Boolean,
2044 value: false
2045 },
2046
2047 _boundKeyHandlers: {
2048 type: Array,
2049 value: function() {
2050 return [];
2051 }
2052 },
2053
2054 // We use this due to a limitation in IE10 where instances will have
2055 // own properties of everything on the "prototype".
2056 _imperativeKeyBindings: {
2057 type: Object,
2058 value: function() {
2059 return {};
2060 }
2061 }
2062 },
2063
2064 observers: [
2065 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
2066 ],
2067
2068
2069 /**
2070 * To be used to express what combination of keys will trigger the relati ve
2071 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
2072 * @type {Object}
2073 */
2074 keyBindings: {},
2075
2076 registered: function() {
2077 this._prepKeyBindings();
2078 },
2079
2080 attached: function() {
2081 this._listenKeyEventListeners();
2082 },
2083
2084 detached: function() {
2085 this._unlistenKeyEventListeners();
2086 },
2087
2088 /**
2089 * Can be used to imperatively add a key binding to the implementing
2090 * element. This is the imperative equivalent of declaring a keybinding
2091 * in the `keyBindings` prototype property.
2092 */
2093 addOwnKeyBinding: function(eventString, handlerName) {
2094 this._imperativeKeyBindings[eventString] = handlerName;
2095 this._prepKeyBindings();
2096 this._resetKeyEventListeners();
2097 },
2098
2099 /**
2100 * When called, will remove all imperatively-added key bindings.
2101 */
2102 removeOwnKeyBindings: function() {
2103 this._imperativeKeyBindings = {};
2104 this._prepKeyBindings();
2105 this._resetKeyEventListeners();
2106 },
2107
2108 /**
2109 * Returns true if a keyboard event matches `eventString`.
2110 *
2111 * @param {KeyboardEvent} event
2112 * @param {string} eventString
2113 * @return {boolean}
2114 */
2115 keyboardEventMatchesKeys: function(event, eventString) {
2116 var keyCombos = parseEventString(eventString);
2117 for (var i = 0; i < keyCombos.length; ++i) {
2118 if (keyComboMatchesEvent(keyCombos[i], event)) {
2119 return true;
2120 }
2121 }
2122 return false;
2123 },
2124
2125 _collectKeyBindings: function() {
2126 var keyBindings = this.behaviors.map(function(behavior) {
2127 return behavior.keyBindings;
2128 });
2129
2130 if (keyBindings.indexOf(this.keyBindings) === -1) {
2131 keyBindings.push(this.keyBindings);
2132 }
2133
2134 return keyBindings;
2135 },
2136
2137 _prepKeyBindings: function() {
2138 this._keyBindings = {};
2139
2140 this._collectKeyBindings().forEach(function(keyBindings) {
2141 for (var eventString in keyBindings) {
2142 this._addKeyBinding(eventString, keyBindings[eventString]);
2143 }
2144 }, this);
2145
2146 for (var eventString in this._imperativeKeyBindings) {
2147 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
2148 }
2149
2150 // Give precedence to combos with modifiers to be checked first.
2151 for (var eventName in this._keyBindings) {
2152 this._keyBindings[eventName].sort(function (kb1, kb2) {
2153 var b1 = kb1[0].hasModifiers;
2154 var b2 = kb2[0].hasModifiers;
2155 return (b1 === b2) ? 0 : b1 ? -1 : 1;
2156 })
2157 }
2158 },
2159
2160 _addKeyBinding: function(eventString, handlerName) {
2161 parseEventString(eventString).forEach(function(keyCombo) {
2162 this._keyBindings[keyCombo.event] =
2163 this._keyBindings[keyCombo.event] || [];
2164
2165 this._keyBindings[keyCombo.event].push([
2166 keyCombo,
2167 handlerName
2168 ]);
2169 }, this);
2170 },
2171
2172 _resetKeyEventListeners: function() {
2173 this._unlistenKeyEventListeners();
2174
2175 if (this.isAttached) {
2176 this._listenKeyEventListeners();
2177 }
2178 },
2179
2180 _listenKeyEventListeners: function() {
2181 if (!this.keyEventTarget) {
2182 return;
2183 }
2184 Object.keys(this._keyBindings).forEach(function(eventName) {
2185 var keyBindings = this._keyBindings[eventName];
2186 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
2187
2188 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH andler]);
2189
2190 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
2191 }, this);
2192 },
2193
2194 _unlistenKeyEventListeners: function() {
2195 var keyHandlerTuple;
2196 var keyEventTarget;
2197 var eventName;
2198 var boundKeyHandler;
2199
2200 while (this._boundKeyHandlers.length) {
2201 // My kingdom for block-scope binding and destructuring assignment..
2202 keyHandlerTuple = this._boundKeyHandlers.pop();
2203 keyEventTarget = keyHandlerTuple[0];
2204 eventName = keyHandlerTuple[1];
2205 boundKeyHandler = keyHandlerTuple[2];
2206
2207 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
2208 }
2209 },
2210
2211 _onKeyBindingEvent: function(keyBindings, event) {
2212 if (this.stopKeyboardEventPropagation) {
2213 event.stopPropagation();
2214 }
2215
2216 // if event has been already prevented, don't do anything
2217 if (event.defaultPrevented) {
2218 return;
2219 }
2220
2221 for (var i = 0; i < keyBindings.length; i++) {
2222 var keyCombo = keyBindings[i][0];
2223 var handlerName = keyBindings[i][1];
2224 if (keyComboMatchesEvent(keyCombo, event)) {
2225 this._triggerKeyHandler(keyCombo, handlerName, event);
2226 // exit the loop if eventDefault was prevented
2227 if (event.defaultPrevented) {
2228 return;
2229 } 3355 }
2230 } 3356 }
2231 } 3357 if (this.selectCallback) {
2232 }, 3358 this.selectCallback(item, isSelected);
2233 3359 }
2234 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { 3360 }
2235 var detail = Object.create(keyCombo); 3361 }
2236 detail.keyboardEvent = keyboardEvent; 3362 },
2237 var event = new CustomEvent(keyCombo.event, { 3363
2238 detail: detail, 3364 /**
2239 cancelable: true 3365 * Sets the selection state for a given item. If the `multi` property
2240 }); 3366 * is true, then the selected state of `item` will be toggled; otherwise
2241 this[handlerName].call(this, event); 3367 * the `item` will be selected.
2242 if (event.defaultPrevented) { 3368 *
2243 keyboardEvent.preventDefault(); 3369 * @method select
2244 } 3370 * @param {*} item The item to select.
2245 } 3371 */
2246 }; 3372 select: function(item) {
2247 })(); 3373 if (this.multi) {
2248 /** 3374 this.toggle(item);
2249 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a 3375 } else if (this.get() !== item) {
2250 * designated scroll target. 3376 this.setItemSelected(this.get(), false);
2251 * 3377 this.setItemSelected(item, true);
2252 * Elements that consume this behavior can override the `_scrollHandler` 3378 }
2253 * method to add logic on the scroll event. 3379 },
2254 * 3380
2255 * @demo demo/scrolling-region.html Scrolling Region 3381 /**
2256 * @demo demo/document.html Document Element 3382 * Toggles the selection state for `item`.
2257 * @polymerBehavior 3383 *
2258 */ 3384 * @method toggle
2259 Polymer.IronScrollTargetBehavior = { 3385 * @param {*} item The item to toggle.
3386 */
3387 toggle: function(item) {
3388 this.setItemSelected(item, !this.isSelected(item));
3389 }
3390
3391 };
3392 /** @polymerBehavior */
3393 Polymer.IronSelectableBehavior = {
3394
3395 /**
3396 * Fired when iron-selector is activated (selected or deselected).
3397 * It is fired before the selected items are changed.
3398 * Cancel the event to abort selection.
3399 *
3400 * @event iron-activate
3401 */
3402
3403 /**
3404 * Fired when an item is selected
3405 *
3406 * @event iron-select
3407 */
3408
3409 /**
3410 * Fired when an item is deselected
3411 *
3412 * @event iron-deselect
3413 */
3414
3415 /**
3416 * Fired when the list of selectable items changes (e.g., items are
3417 * added or removed). The detail of the event is a mutation record that
3418 * describes what changed.
3419 *
3420 * @event iron-items-changed
3421 */
2260 3422
2261 properties: { 3423 properties: {
2262 3424
2263 /** 3425 /**
2264 * Specifies the element that will handle the scroll event 3426 * If you want to use an attribute value or property of an element for
2265 * on the behalf of the current element. This is typically a reference to an element, 3427 * `selected` instead of the index, set this to the name of the attribute
2266 * but there are a few more posibilities: 3428 * or property. Hyphenated values are converted to camel case when used to
3429 * look up the property of a selectable element. Camel cased values are
3430 * *not* converted to hyphenated values for attribute lookup. It's
3431 * recommended that you provide the hyphenated form of the name so that
3432 * selection works in both cases. (Use `attr-or-property-name` instead of
3433 * `attrOrPropertyName`.)
3434 */
3435 attrForSelected: {
3436 type: String,
3437 value: null
3438 },
3439
3440 /**
3441 * Gets or sets the selected element. The default is to use the index of t he item.
3442 * @type {string|number}
3443 */
3444 selected: {
3445 type: String,
3446 notify: true
3447 },
3448
3449 /**
3450 * Returns the currently selected item.
2267 * 3451 *
2268 * ### Elements id 3452 * @type {?Object}
2269 *
2270 *```html
2271 * <div id="scrollable-element" style="overflow: auto;">
2272 * <x-element scroll-target="scrollable-element">
2273 * \x3c!-- Content--\x3e
2274 * </x-element>
2275 * </div>
2276 *```
2277 * In this case, the `scrollTarget` will point to the outer div element.
2278 *
2279 * ### Document scrolling
2280 *
2281 * For document scrolling, you can use the reserved word `document`:
2282 *
2283 *```html
2284 * <x-element scroll-target="document">
2285 * \x3c!-- Content --\x3e
2286 * </x-element>
2287 *```
2288 *
2289 * ### Elements reference
2290 *
2291 *```js
2292 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
2293 *```
2294 *
2295 * @type {HTMLElement}
2296 */
2297 scrollTarget: {
2298 type: HTMLElement,
2299 value: function() {
2300 return this._defaultScrollTarget;
2301 }
2302 }
2303 },
2304
2305 observers: [
2306 '_scrollTargetChanged(scrollTarget, isAttached)'
2307 ],
2308
2309 _scrollTargetChanged: function(scrollTarget, isAttached) {
2310 var eventTarget;
2311
2312 if (this._oldScrollTarget) {
2313 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
2314 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
2315 this._oldScrollTarget = null;
2316 }
2317
2318 if (!isAttached) {
2319 return;
2320 }
2321 // Support element id references
2322 if (scrollTarget === 'document') {
2323
2324 this.scrollTarget = this._doc;
2325
2326 } else if (typeof scrollTarget === 'string') {
2327
2328 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
2329 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
2330
2331 } else if (this._isValidScrollTarget()) {
2332
2333 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
2334 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
2335 this._oldScrollTarget = scrollTarget;
2336
2337 eventTarget.addEventListener('scroll', this._boundScrollHandler);
2338 }
2339 },
2340
2341 /**
2342 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
2343 *
2344 * @protected
2345 */
2346 _scrollHandler: function scrollHandler() {},
2347
2348 /**
2349 * The default scroll target. Consumers of this behavior may want to customi ze
2350 * the default scroll target.
2351 *
2352 * @type {Element}
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 }
2465 };
2466 (function() {
2467
2468 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
2469 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
2470 var DEFAULT_PHYSICAL_COUNT = 3;
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
2479 properties: {
2480
2481 /**
2482 * An array containing items determining how many instances of the templat e
2483 * to stamp and that that each template instance should bind to.
2484 */
2485 items: {
2486 type: Array
2487 },
2488
2489 /**
2490 * The max count of physical items the pool can extend to.
2491 */
2492 maxPhysicalCount: {
2493 type: Number,
2494 value: 500
2495 },
2496
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,
2538 value: false,
2539 reflectToAttribute: true
2540 },
2541
2542 /**
2543 * When true, tapping a row will select the item, placing its data model
2544 * in the set of selected items retrievable via the selection property.
2545 *
2546 * Note that tapping focusable elements within the list item will not
2547 * result in selection, since they are presumed to have their * own action .
2548 */
2549 selectionEnabled: {
2550 type: Boolean,
2551 value: false
2552 },
2553
2554 /**
2555 * When `multiSelection` is false, this is the currently selected item, or `null`
2556 * if no item is selected.
2557 */ 3453 */
2558 selectedItem: { 3454 selectedItem: {
2559 type: Object, 3455 type: Object,
3456 readOnly: true,
2560 notify: true 3457 notify: true
2561 }, 3458 },
2562 3459
2563 /** 3460 /**
2564 * When `multiSelection` is true, this is an array that contains the selec ted items. 3461 * The event that fires from items when they are selected. Selectable
2565 */ 3462 * will listen for this event from items and update the selection state.
2566 selectedItems: { 3463 * Set to empty string to listen to no events.
3464 */
3465 activateEvent: {
3466 type: String,
3467 value: 'tap',
3468 observer: '_activateEventChanged'
3469 },
3470
3471 /**
3472 * This is a CSS selector string. If this is set, only items that match t he CSS selector
3473 * are selectable.
3474 */
3475 selectable: String,
3476
3477 /**
3478 * The class to set on elements when selected.
3479 */
3480 selectedClass: {
3481 type: String,
3482 value: 'iron-selected'
3483 },
3484
3485 /**
3486 * The attribute to set on elements when selected.
3487 */
3488 selectedAttribute: {
3489 type: String,
3490 value: null
3491 },
3492
3493 /**
3494 * Default fallback if the selection based on selected with `attrForSelect ed`
3495 * is not found.
3496 */
3497 fallbackSelection: {
3498 type: String,
3499 value: null
3500 },
3501
3502 /**
3503 * The list of items from which a selection can be made.
3504 */
3505 items: {
3506 type: Array,
3507 readOnly: true,
3508 notify: true,
3509 value: function() {
3510 return [];
3511 }
3512 },
3513
3514 /**
3515 * The set of excluded elements where the key is the `localName`
3516 * of the element that will be ignored from the item list.
3517 *
3518 * @default {template: 1}
3519 */
3520 _excludedLocalNames: {
2567 type: Object, 3521 type: Object,
2568 notify: true 3522 value: function() {
2569 }, 3523 return {
2570 3524 'template': 1
2571 /** 3525 };
2572 * When `true`, multiple items may be selected at once (in this case, 3526 }
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 } 3527 }
2580 }, 3528 },
2581 3529
2582 observers: [ 3530 observers: [
2583 '_itemsChanged(items.*)', 3531 '_updateAttrForSelected(attrForSelected)',
2584 '_selectionEnabledChanged(selectionEnabled)', 3532 '_updateSelected(selected)',
2585 '_multiSelectionChanged(multiSelection)', 3533 '_checkFallback(fallbackSelection)'
2586 '_setOverflow(scrollTarget)'
2587 ], 3534 ],
2588 3535
2589 behaviors: [ 3536 created: function() {
2590 Polymer.Templatizer, 3537 this._bindFilterItem = this._filterItem.bind(this);
2591 Polymer.IronResizableBehavior, 3538 this._selection = new Polymer.IronSelection(this._applySelection.bind(this ));
2592 Polymer.IronA11yKeysBehavior, 3539 },
2593 Polymer.IronScrollTargetBehavior 3540
2594 ], 3541 attached: function() {
2595 3542 this._observer = this._observeItems(this);
2596 keyBindings: { 3543 this._updateItems();
2597 'up': '_didMoveUp', 3544 if (!this._shouldUpdateSelection) {
2598 'down': '_didMoveDown', 3545 this._updateSelected();
2599 'enter': '_didEnter' 3546 }
2600 }, 3547 this._addListener(this.activateEvent);
2601 3548 },
2602 /** 3549
2603 * The ratio of hidden tiles that should remain in the scroll direction. 3550 detached: function() {
2604 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons. 3551 if (this._observer) {
2605 */ 3552 Polymer.dom(this).unobserveNodes(this._observer);
2606 _ratio: 0.5, 3553 }
2607 3554 this._removeListener(this.activateEvent);
2608 /** 3555 },
2609 * The padding-top value for the list. 3556
2610 */ 3557 /**
2611 _scrollerPaddingTop: 0, 3558 * Returns the index of the given item.
2612
2613 /**
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 * 3559 *
2855 * This default value assumes that we will at least have the equivalent 3560 * @method indexOf
2856 * to a viewport of physical items above and below the user's viewport. 3561 * @param {Object} item
2857 */ 3562 * @returns Returns the index of the item
2858 get _optPhysicalSize() { 3563 */
2859 if (this.grid) { 3564 indexOf: function(item) {
2860 return this._estRowsInView * this._rowHeight * this._maxPages; 3565 return this.items.indexOf(item);
2861 } 3566 },
2862 return this._viewportHeight * this._maxPages; 3567
2863 }, 3568 /**
2864 3569 * Selects the given value.
2865 get _optPhysicalCount() {
2866 return this._estRowsInView * this._itemsPerRow * this._maxPages;
2867 },
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 * 3570 *
2879 * @type {number} 3571 * @method select
2880 */ 3572 * @param {string|number} value the value to select.
2881 get firstVisibleIndex() { 3573 */
2882 if (this._firstVisibleIndexVal === null) { 3574 select: function(value) {
2883 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop); 3575 this.selected = value;
2884 3576 },
2885 this._firstVisibleIndexVal = this._iterateItems( 3577
2886 function(pidx, vidx) { 3578 /**
2887 physicalOffset += this._getPhysicalSizeIncrement(pidx); 3579 * Selects the previous item.
2888
2889 if (physicalOffset > this._scrollPosition) {
2890 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
2891 }
2892 // Handle a partially rendered final row in grid mode
2893 if (this.grid && this._virtualCount - 1 === vidx) {
2894 return vidx - (vidx % this._itemsPerRow);
2895 }
2896 }) || 0;
2897 }
2898 return this._firstVisibleIndexVal;
2899 },
2900
2901 /**
2902 * Gets the index of the last visible item in the viewport.
2903 * 3580 *
2904 * @type {number} 3581 * @method selectPrevious
2905 */ 3582 */
2906 get lastVisibleIndex() { 3583 selectPrevious: function() {
2907 if (this._lastVisibleIndexVal === null) { 3584 var length = this.items.length;
2908 if (this.grid) { 3585 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len gth;
2909 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1; 3586 this.selected = this._indexToValue(index);
2910 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); 3587 },
2911 } else { 3588
2912 var physicalOffset = this._physicalTop; 3589 /**
2913 this._iterateItems(function(pidx, vidx) { 3590 * Selects the next item.
2914 if (physicalOffset < this._scrollBottom) {
2915 this._lastVisibleIndexVal = vidx;
2916 } else {
2917 // Break _iterateItems
2918 return true;
2919 }
2920 physicalOffset += this._getPhysicalSizeIncrement(pidx);
2921 });
2922 }
2923 }
2924 return this._lastVisibleIndexVal;
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 * 3591 *
2971 * @method updateViewportBoundaries 3592 * @method selectNext
2972 */ 3593 */
2973 updateViewportBoundaries: function() { 3594 selectNext: function() {
2974 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : 3595 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l ength;
2975 parseInt(window.getComputedStyle(this)['padding-top'], 10); 3596 this.selected = this._indexToValue(index);
2976 3597 },
2977 this._viewportHeight = this._scrollTargetHeight; 3598
2978 if (this.grid) { 3599 /**
2979 this._updateGridMetrics(); 3600 * Selects the item at the given index.
2980 } 3601 *
2981 }, 3602 * @method selectIndex
2982 3603 */
2983 /** 3604 selectIndex: function(index) {
2984 * Update the models, the position of the 3605 this.select(this._indexToValue(index));
2985 * items in the viewport and recycle tiles as needed. 3606 },
2986 */ 3607
2987 _scrollHandler: function() { 3608 /**
2988 // clamp the `scrollTop` value 3609 * Force a synchronous update of the `items` property.
2989 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; 3610 *
2990 var delta = scrollTop - this._scrollPosition; 3611 * NOTE: Consider listening for the `iron-items-changed` event to respond to
2991 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; 3612 * updates to the set of selectable items after updates to the DOM list and
2992 var ratio = this._ratio; 3613 * selection state have been made.
2993 var recycledTiles = 0; 3614 *
2994 var hiddenContentSize = this._hiddenContentSize; 3615 * WARNING: If you are using this method, you should probably consider an
2995 var currentRatio = ratio; 3616 * alternate approach. Synchronously querying for items is potentially
2996 var movingUp = []; 3617 * slow for many use cases. The `items` property will update asynchronously
2997 3618 * on its own to reflect selectable items in the DOM.
2998 // track the last `scrollTop` 3619 */
2999 this._scrollPosition = scrollTop; 3620 forceSynchronousItemUpdate: function() {
3000 3621 this._updateItems();
3001 // clear cached visible indexes 3622 },
3002 this._firstVisibleIndexVal = null; 3623
3003 this._lastVisibleIndexVal = null; 3624 get _shouldUpdateSelection() {
3004 3625 return this.selected != null;
3005 scrollBottom = this._scrollBottom; 3626 },
3006 physicalBottom = this._physicalBottom; 3627
3007 3628 _checkFallback: function() {
3008 // random access 3629 if (this._shouldUpdateSelection) {
3009 if (Math.abs(delta) > this._physicalSize) { 3630 this._updateSelected();
3010 this._physicalTop += delta; 3631 }
3011 recycledTiles = Math.round(delta / this._physicalAverage); 3632 },
3012 } 3633
3013 // scroll up 3634 _addListener: function(eventName) {
3014 else if (delta < 0) { 3635 this.listen(this, eventName, '_activateHandler');
3015 var topSpace = scrollTop - this._physicalTop; 3636 },
3016 var virtualStart = this._virtualStart; 3637
3017 3638 _removeListener: function(eventName) {
3018 recycledTileSet = []; 3639 this.unlisten(this, eventName, '_activateHandler');
3019 3640 },
3020 kth = this._physicalEnd; 3641
3021 currentRatio = topSpace / hiddenContentSize; 3642 _activateEventChanged: function(eventName, old) {
3022 3643 this._removeListener(old);
3023 // move tiles from bottom to top 3644 this._addListener(eventName);
3024 while ( 3645 },
3025 // approximate `currentRatio` to `ratio` 3646
3026 currentRatio < ratio && 3647 _updateItems: function() {
3027 // recycle less physical items than the total 3648 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
3028 recycledTiles < this._physicalCount && 3649 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
3029 // ensure that these recycled tiles are needed 3650 this._setItems(nodes);
3030 virtualStart - recycledTiles > 0 && 3651 },
3031 // ensure that the tile is not visible 3652
3032 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom 3653 _updateAttrForSelected: function() {
3033 ) { 3654 if (this._shouldUpdateSelection) {
3034 3655 this.selected = this._indexToValue(this.indexOf(this.selectedItem));
3035 tileHeight = this._getPhysicalSizeIncrement(kth); 3656 }
3036 currentRatio += tileHeight / hiddenContentSize; 3657 },
3037 physicalBottom -= tileHeight; 3658
3038 recycledTileSet.push(kth); 3659 _updateSelected: function() {
3039 recycledTiles++; 3660 this._selectSelected(this.selected);
3040 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; 3661 },
3041 } 3662
3042 3663 _selectSelected: function(selected) {
3043 movingUp = recycledTileSet; 3664 this._selection.select(this._valueToItem(this.selected));
3044 recycledTiles = -recycledTiles; 3665 // Check for items, since this array is populated only when attached
3045 } 3666 // Since Number(0) is falsy, explicitly check for undefined
3046 // scroll down 3667 if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
3047 else if (delta > 0) { 3668 this.selected = this.fallbackSelection;
3048 var bottomSpace = physicalBottom - scrollBottom; 3669 }
3049 var virtualEnd = this._virtualEnd; 3670 },
3050 var lastVirtualItemIndex = this._virtualCount-1; 3671
3051 3672 _filterItem: function(node) {
3052 recycledTileSet = []; 3673 return !this._excludedLocalNames[node.localName];
3053 3674 },
3054 kth = this._physicalStart; 3675
3055 currentRatio = bottomSpace / hiddenContentSize; 3676 _valueToItem: function(value) {
3056 3677 return (value == null) ? null : this.items[this._valueToIndex(value)];
3057 // move tiles from top to bottom 3678 },
3058 while ( 3679
3059 // approximate `currentRatio` to `ratio` 3680 _valueToIndex: function(value) {
3060 currentRatio < ratio && 3681 if (this.attrForSelected) {
3061 // recycle less physical items than the total 3682 for (var i = 0, item; item = this.items[i]; i++) {
3062 recycledTiles < this._physicalCount && 3683 if (this._valueForItem(item) == value) {
3063 // ensure that these recycled tiles are needed 3684 return i;
3064 virtualEnd + recycledTiles < lastVirtualItemIndex && 3685 }
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 } 3686 }
3084 } else { 3687 } else {
3085 this._virtualStart = this._virtualStart + recycledTiles; 3688 return Number(value);
3086 this._physicalStart = this._physicalStart + recycledTiles; 3689 }
3087 this._update(recycledTileSet, movingUp); 3690 },
3088 } 3691
3089 }, 3692 _indexToValue: function(index) {
3090 3693 if (this.attrForSelected) {
3091 /** 3694 var item = this.items[index];
3092 * Update the list of items, starting from the `_virtualStart` item. 3695 if (item) {
3093 * @param {!Array<number>=} itemSet 3696 return this._valueForItem(item);
3094 * @param {!Array<number>=} movingUp 3697 }
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 { 3698 } else {
3163 // fill the rest of the pages 3699 return index;
3164 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ; 3700 }
3165 } 3701 },
3166 3702
3167 this._lastPage = currentPage; 3703 _valueForItem: function(item) {
3168 3704 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected) ];
3169 return true; 3705 return propValue != undefined ? propValue : item.getAttribute(this.attrFor Selected);
3170 }, 3706 },
3171 3707
3172 /** 3708 _applySelection: function(item, isSelected) {
3173 * Increases the pool size. 3709 if (this.selectedClass) {
3174 */ 3710 this.toggleClass(this.selectedClass, isSelected, item);
3175 _increasePool: function(missingItems) { 3711 }
3176 var nextPhysicalCount = Math.min( 3712 if (this.selectedAttribute) {
3177 this._physicalCount + missingItems, 3713 this.toggleAttribute(this.selectedAttribute, isSelected, item);
3178 this._virtualCount - this._virtualStart, 3714 }
3179 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) 3715 this._selectionChange();
3180 ); 3716 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
3181 var prevPhysicalCount = this._physicalCount; 3717 },
3182 var delta = nextPhysicalCount - prevPhysicalCount; 3718
3183 3719 _selectionChange: function() {
3184 if (delta <= 0) { 3720 this._setSelectedItem(this._selection.get());
3185 return; 3721 },
3186 } 3722
3187 3723 // observe items change under the given node.
3188 [].push.apply(this._physicalItems, this._createPool(delta)); 3724 _observeItems: function(node) {
3189 [].push.apply(this._physicalSizes, new Array(delta)); 3725 return Polymer.dom(node).observeNodes(function(mutation) {
3190 3726 this._updateItems();
3191 this._physicalCount = prevPhysicalCount + delta; 3727
3192 3728 if (this._shouldUpdateSelection) {
3193 // update the physical start if we need to preserve the model of the focus ed item. 3729 this._updateSelected();
3194 // In this situation, the focused item is currently rendered and its model would 3730 }
3195 // have changed after increasing the pool if the physical start remained u nchanged. 3731
3196 if (this._physicalStart > this._physicalEnd && 3732 // Let other interested parties know about the change so that
3197 this._isIndexRendered(this._focusedIndex) && 3733 // we don't have to recreate mutation observers everywhere.
3198 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { 3734 this.fire('iron-items-changed', mutation, {
3199 this._physicalStart = this._physicalStart + delta; 3735 bubbles: false,
3200 } 3736 cancelable: false
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 }); 3737 });
3543 } else { 3738 });
3544 this._iterateItems(function(pidx, vidx) { 3739 },
3545 3740
3546 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); 3741 _activateHandler: function(e) {
3547 y += this._physicalSizes[pidx]; 3742 var t = e.target;
3548 3743 var items = this.items;
3549 }); 3744 while (t && t != this) {
3550 } 3745 var i = items.indexOf(t);
3551 }, 3746 if (i >= 0) {
3552 3747 var value = this._indexToValue(i);
3553 _getPhysicalSizeIncrement: function(pidx) { 3748 this._itemActivate(value, t);
3554 if (!this.grid) { 3749 return;
3555 return this._physicalSizes[pidx]; 3750 }
3556 } 3751 t = t.parentNode;
3557 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) { 3752 }
3558 return 0; 3753 },
3559 } 3754
3560 return this._rowHeight; 3755 _itemActivate: function(value, item) {
3561 }, 3756 if (!this.fire('iron-activate',
3562 3757 {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
3563 /** 3758 this.select(value);
3564 * Returns, based on the current index, 3759 }
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 } 3760 }
4058 }); 3761
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 }; 3762 };
4100 3763 Polymer({
4101 ActionService.prototype = { 3764
4102 /** @param {string} id ID of the download to cancel. */ 3765 is: 'iron-pages',
4103 cancel: chromeSendWithId('cancel'), 3766
4104 3767 behaviors: [
4105 /** Instructs the browser to clear all finished downloads. */ 3768 Polymer.IronResizableBehavior,
4106 clearAll: function() { 3769 Polymer.IronSelectableBehavior
4107 if (loadTimeData.getBoolean('allowDeletingHistory')) { 3770 ],
4108 chrome.send('clearAll'); 3771
4109 this.search(''); 3772 properties: {
4110 } 3773
4111 }, 3774 // as the selected page is the only one visible, activateEvent
4112 3775 // is both non-sensical and problematic; e.g. in cases where a user
4113 /** @param {string} id ID of the dangerous download to discard. */ 3776 // handler attempts to change the page and the activateEvent
4114 discardDangerous: chromeSendWithId('discardDangerous'), 3777 // handler immediately changes it back
4115 3778 activateEvent: {
4116 /** @param {string} url URL of a file to download. */ 3779 type: String,
4117 download: function(url) { 3780 value: null
4118 var a = document.createElement('a'); 3781 }
4119 a.href = url; 3782
4120 a.setAttribute('download', ''); 3783 },
4121 a.click(); 3784
4122 }, 3785 observers: [
4123 3786 '_selectedPageChanged(selected)'
4124 /** @param {string} id ID of the download that the user started dragging. */ 3787 ],
4125 drag: chromeSendWithId('drag'), 3788
4126 3789 _selectedPageChanged: function(selected, old) {
4127 /** Loads more downloads with the current search terms. */ 3790 this.async(this.notifyResize);
4128 loadMore: function() { 3791 }
4129 chrome.send('getDownloads', this.searchTerms_); 3792 });
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() { 3793 (function() {
4346 3794
4347 // monostate data 3795 // monostate data
4348 var metaDatas = {}; 3796 var metaDatas = {};
4349 var metaArrays = {}; 3797 var metaArrays = {};
4350 var singleton = null; 3798 var singleton = null;
4351 3799
4352 Polymer.IronMeta = Polymer({ 3800 Polymer.IronMeta = Polymer({
4353 3801
4354 is: 'iron-meta', 3802 is: 'iron-meta',
(...skipping 355 matching lines...) Expand 10 before | Expand all | Expand 10 after
4710 this._img.style.height = '100%'; 4158 this._img.style.height = '100%';
4711 this._img.draggable = false; 4159 this._img.draggable = false;
4712 } 4160 }
4713 this._img.src = this.src; 4161 this._img.src = this.src;
4714 Polymer.dom(this.root).appendChild(this._img); 4162 Polymer.dom(this.root).appendChild(this._img);
4715 } 4163 }
4716 } 4164 }
4717 4165
4718 }); 4166 });
4719 /** 4167 /**
4720 * @demo demo/index.html
4721 * @polymerBehavior
4722 */
4723 Polymer.IronControlState = {
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: [
4763 '_changedControlState(focused, disabled)'
4764 ],
4765
4766 ready: function() {
4767 this.addEventListener('focus', this._boundFocusBlurHandler, true);
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();
5664 }
5665 },
5666
5667 /**
5668 * In addition to the functionality provided in `IronButtonState`, ensures
5669 * a ripple effect is created when the element is in a `pressed` state.
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() {
5730 return /** @type {!PaperRippleElement} */ (
5731 document.createElement('paper-ripple'));
5732 },
5733
5734 _noinkChanged: function(noink) {
5735 if (this.hasRipple()) {
5736 this._ripple.noink = noink;
5737 }
5738 }
5739 };
5740 /** @polymerBehavior Polymer.PaperButtonBehavior */
5741 Polymer.PaperButtonBehaviorImpl = {
5742 properties: {
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,
5817 Polymer.IronControlState,
5818 Polymer.PaperRippleBehavior,
5819 Polymer.PaperButtonBehaviorImpl
5820 ];
5821 Polymer({
5822 is: 'paper-button',
5823
5824 behaviors: [
5825 Polymer.PaperButtonBehavior
5826 ],
5827
5828 properties: {
5829 /**
5830 * If true, the button should be styled with a shadow.
5831 */
5832 raised: {
5833 type: Boolean,
5834 reflectToAttribute: true,
5835 value: false,
5836 observer: '_calculateElevation'
5837 }
5838 },
5839
5840 _calculateElevation: function() {
5841 if (!this.raised) {
5842 this._setElevation(0);
5843 } else {
5844 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5845 }
5846 }
5847
5848 /**
5849 Fired when the animation finishes.
5850 This is useful if you want to wait until
5851 the ripple animation finishes to perform some action.
5852
5853 @event transitionend
5854 Event param: {{node: Object}} detail Contains the animated node.
5855 */
5856 });
5857 Polymer({
5858 is: 'paper-icon-button-light',
5859 extends: 'button',
5860
5861 behaviors: [
5862 Polymer.PaperRippleBehavior
5863 ],
5864
5865 listeners: {
5866 'down': '_rippleDown',
5867 'up': '_rippleUp',
5868 'focus': '_rippleDown',
5869 'blur': '_rippleUp',
5870 },
5871
5872 _rippleDown: function() {
5873 this.getRipple().downAction();
5874 },
5875
5876 _rippleUp: function() {
5877 this.getRipple().upAction();
5878 },
5879
5880 /**
5881 * @param {...*} var_args
5882 */
5883 ensureRipple: function(var_args) {
5884 var lastRipple = this._ripple;
5885 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
5886 if (this._ripple && this._ripple !== lastRipple) {
5887 this._ripple.center = true;
5888 this._ripple.classList.add('circle');
5889 }
5890 }
5891 });
5892 /**
5893 * `iron-range-behavior` provides the behavior for something with a minimum to m aximum range.
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: {
6006 /**
6007 * The number that represents the current secondary progress.
6008 */
6009 secondaryProgress: {
6010 type: Number,
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,
6028 value: false,
6029 observer: '_toggleIndeterminate'
6030 },
6031
6032 /**
6033 * True if the progress is disabled.
6034 */
6035 disabled: {
6036 type: Boolean,
6037 value: false,
6038 reflectToAttribute: true,
6039 observer: '_disabledChanged'
6040 }
6041 },
6042
6043 observers: [
6044 '_progressChanged(secondaryProgress, value, min, max)'
6045 ],
6046
6047 hostAttributes: {
6048 role: 'progressbar'
6049 },
6050
6051 _toggleIndeterminate: function(indeterminate) {
6052 // If we use attribute/class binding, the animation sometimes doesn't tran slate properly
6053 // on Safari 7.1. So instead, we toggle the class here in the update metho d.
6054 this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
6055 },
6056
6057 _transformProgress: function(progress, ratio) {
6058 var transform = 'scaleX(' + (ratio / 100) + ')';
6059 progress.style.transform = progress.style.webkitTransform = transform;
6060 },
6061
6062 _mainRatioChanged: function(ratio) {
6063 this._transformProgress(this.$.primaryProgress, ratio);
6064 },
6065
6066 _progressChanged: function(secondaryProgress, value, min, max) {
6067 secondaryProgress = this._clampValue(secondaryProgress);
6068 value = this._clampValue(value);
6069
6070 var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
6071 var mainRatio = this._calcRatio(value) * 100;
6072
6073 this._setSecondaryRatio(secondaryRatio);
6074 this._transformProgress(this.$.secondaryProgress, secondaryRatio);
6075 this._transformProgress(this.$.primaryProgress, mainRatio);
6076
6077 this.secondaryProgress = secondaryProgress;
6078
6079 this.setAttribute('aria-valuenow', value);
6080 this.setAttribute('aria-valuemin', min);
6081 this.setAttribute('aria-valuemax', max);
6082 },
6083
6084 _disabledChanged: function(disabled) {
6085 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
6086 },
6087
6088 _hideSecondaryProgress: function(secondaryRatio) {
6089 return secondaryRatio === 0;
6090 }
6091 });
6092 /**
6093 * The `iron-iconset-svg` element allows users to define their own icon sets 4168 * 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 4169 * 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. 4170 * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
6096 * 4171 *
6097 * Using svg elements to create icons has a few advantages over traditional 4172 * 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 4173 * 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 4174 * 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. 4175 * are stylable via css. Icons can be themed, colorized, and even animated.
6101 * 4176 *
6102 * Example: 4177 * Example:
(...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after
6261 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/ 370136 4336 // 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 4337 // 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%;'; 4338 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
6264 svg.appendChild(content).removeAttribute('id'); 4339 svg.appendChild(content).removeAttribute('id');
6265 return svg; 4340 return svg;
6266 } 4341 }
6267 return null; 4342 return null;
6268 } 4343 }
6269 4344
6270 }); 4345 });
6271 // Copyright 2015 The Chromium Authors. All rights reserved. 4346 (function() {
6272 // Use of this source code is governed by a BSD-style license that can be 4347 'use strict';
6273 // found in the LICENSE file. 4348
6274 4349 /**
6275 cr.define('downloads', function() { 4350 * Chrome uses an older version of DOM Level 3 Keyboard Events
6276 var Item = Polymer({ 4351 *
6277 is: 'downloads-item', 4352 * Most keys are labeled as text, but some are Unicode codepoints.
4353 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712 21/keyset.html#KeySet-Set
4354 */
4355 var KEY_IDENTIFIER = {
4356 'U+0008': 'backspace',
4357 'U+0009': 'tab',
4358 'U+001B': 'esc',
4359 'U+0020': 'space',
4360 'U+007F': 'del'
4361 };
4362
4363 /**
4364 * Special table for KeyboardEvent.keyCode.
4365 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett er
4366 * than that.
4367 *
4368 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve nt.keyCode#Value_of_keyCode
4369 */
4370 var KEY_CODE = {
4371 8: 'backspace',
4372 9: 'tab',
4373 13: 'enter',
4374 27: 'esc',
4375 33: 'pageup',
4376 34: 'pagedown',
4377 35: 'end',
4378 36: 'home',
4379 32: 'space',
4380 37: 'left',
4381 38: 'up',
4382 39: 'right',
4383 40: 'down',
4384 46: 'del',
4385 106: '*'
4386 };
4387
4388 /**
4389 * MODIFIER_KEYS maps the short name for modifier keys used in a key
4390 * combo string to the property name that references those same keys
4391 * in a KeyboardEvent instance.
4392 */
4393 var MODIFIER_KEYS = {
4394 'shift': 'shiftKey',
4395 'ctrl': 'ctrlKey',
4396 'alt': 'altKey',
4397 'meta': 'metaKey'
4398 };
4399
4400 /**
4401 * KeyboardEvent.key is mostly represented by printable character made by
4402 * the keyboard, with unprintable keys labeled nicely.
4403 *
4404 * However, on OS X, Alt+char can make a Unicode character that follows an
4405 * Apple-specific mapping. In this case, we fall back to .keyCode.
4406 */
4407 var KEY_CHAR = /[a-z0-9*]/;
4408
4409 /**
4410 * Matches a keyIdentifier string.
4411 */
4412 var IDENT_CHAR = /U\+/;
4413
4414 /**
4415 * Matches arrow keys in Gecko 27.0+
4416 */
4417 var ARROW_KEY = /^arrow/;
4418
4419 /**
4420 * Matches space keys everywhere (notably including IE10's exceptional name
4421 * `spacebar`).
4422 */
4423 var SPACE_KEY = /^space(bar)?/;
4424
4425 /**
4426 * Matches ESC key.
4427 *
4428 * Value from: http://w3c.github.io/uievents-key/#key-Escape
4429 */
4430 var ESC_KEY = /^escape$/;
4431
4432 /**
4433 * Transforms the key.
4434 * @param {string} key The KeyBoardEvent.key
4435 * @param {Boolean} [noSpecialChars] Limits the transformation to
4436 * alpha-numeric characters.
4437 */
4438 function transformKey(key, noSpecialChars) {
4439 var validKey = '';
4440 if (key) {
4441 var lKey = key.toLowerCase();
4442 if (lKey === ' ' || SPACE_KEY.test(lKey)) {
4443 validKey = 'space';
4444 } else if (ESC_KEY.test(lKey)) {
4445 validKey = 'esc';
4446 } else if (lKey.length == 1) {
4447 if (!noSpecialChars || KEY_CHAR.test(lKey)) {
4448 validKey = lKey;
4449 }
4450 } else if (ARROW_KEY.test(lKey)) {
4451 validKey = lKey.replace('arrow', '');
4452 } else if (lKey == 'multiply') {
4453 // numpad '*' can map to Multiply on IE/Windows
4454 validKey = '*';
4455 } else {
4456 validKey = lKey;
4457 }
4458 }
4459 return validKey;
4460 }
4461
4462 function transformKeyIdentifier(keyIdent) {
4463 var validKey = '';
4464 if (keyIdent) {
4465 if (keyIdent in KEY_IDENTIFIER) {
4466 validKey = KEY_IDENTIFIER[keyIdent];
4467 } else if (IDENT_CHAR.test(keyIdent)) {
4468 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
4469 validKey = String.fromCharCode(keyIdent).toLowerCase();
4470 } else {
4471 validKey = keyIdent.toLowerCase();
4472 }
4473 }
4474 return validKey;
4475 }
4476
4477 function transformKeyCode(keyCode) {
4478 var validKey = '';
4479 if (Number(keyCode)) {
4480 if (keyCode >= 65 && keyCode <= 90) {
4481 // ascii a-z
4482 // lowercase is 32 offset from uppercase
4483 validKey = String.fromCharCode(32 + keyCode);
4484 } else if (keyCode >= 112 && keyCode <= 123) {
4485 // function keys f1-f12
4486 validKey = 'f' + (keyCode - 112);
4487 } else if (keyCode >= 48 && keyCode <= 57) {
4488 // top 0-9 keys
4489 validKey = String(keyCode - 48);
4490 } else if (keyCode >= 96 && keyCode <= 105) {
4491 // num pad 0-9
4492 validKey = String(keyCode - 96);
4493 } else {
4494 validKey = KEY_CODE[keyCode];
4495 }
4496 }
4497 return validKey;
4498 }
4499
4500 /**
4501 * Calculates the normalized key for a KeyboardEvent.
4502 * @param {KeyboardEvent} keyEvent
4503 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
4504 * transformation to alpha-numeric chars. This is useful with key
4505 * combinations like shift + 2, which on FF for MacOS produces
4506 * keyEvent.key = @
4507 * To get 2 returned, set noSpecialChars = true
4508 * To get @ returned, set noSpecialChars = false
4509 */
4510 function normalizedKeyForEvent(keyEvent, noSpecialChars) {
4511 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
4512 // .detail.key to support artificial keyboard events.
4513 return transformKey(keyEvent.key, noSpecialChars) ||
4514 transformKeyIdentifier(keyEvent.keyIdentifier) ||
4515 transformKeyCode(keyEvent.keyCode) ||
4516 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no SpecialChars) || '';
4517 }
4518
4519 function keyComboMatchesEvent(keyCombo, event) {
4520 // For combos with modifiers we support only alpha-numeric keys
4521 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
4522 return keyEvent === keyCombo.key &&
4523 (!keyCombo.hasModifiers || (
4524 !!event.shiftKey === !!keyCombo.shiftKey &&
4525 !!event.ctrlKey === !!keyCombo.ctrlKey &&
4526 !!event.altKey === !!keyCombo.altKey &&
4527 !!event.metaKey === !!keyCombo.metaKey)
4528 );
4529 }
4530
4531 function parseKeyComboString(keyComboString) {
4532 if (keyComboString.length === 1) {
4533 return {
4534 combo: keyComboString,
4535 key: keyComboString,
4536 event: 'keydown'
4537 };
4538 }
4539 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP art) {
4540 var eventParts = keyComboPart.split(':');
4541 var keyName = eventParts[0];
4542 var event = eventParts[1];
4543
4544 if (keyName in MODIFIER_KEYS) {
4545 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
4546 parsedKeyCombo.hasModifiers = true;
4547 } else {
4548 parsedKeyCombo.key = keyName;
4549 parsedKeyCombo.event = event || 'keydown';
4550 }
4551
4552 return parsedKeyCombo;
4553 }, {
4554 combo: keyComboString.split(':').shift()
4555 });
4556 }
4557
4558 function parseEventString(eventString) {
4559 return eventString.trim().split(' ').map(function(keyComboString) {
4560 return parseKeyComboString(keyComboString);
4561 });
4562 }
4563
4564 /**
4565 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces sing
4566 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3 .org/TR/wai-aria-practices/#kbd_general_binding).
4567 * The element takes care of browser differences with respect to Keyboard ev ents
4568 * and uses an expressive syntax to filter key presses.
4569 *
4570 * Use the `keyBindings` prototype property to express what combination of k eys
4571 * will trigger the callback. A key binding has the format
4572 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
4573 * `"KEY:EVENT": "callback"` are valid as well). Some examples:
4574 *
4575 * keyBindings: {
4576 * 'space': '_onKeydown', // same as 'space:keydown'
4577 * 'shift+tab': '_onKeydown',
4578 * 'enter:keypress': '_onKeypress',
4579 * 'esc:keyup': '_onKeyup'
4580 * }
4581 *
4582 * The callback will receive with an event containing the following informat ion in `event.detail`:
4583 *
4584 * _onKeydown: function(event) {
4585 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
4586 * console.log(event.detail.key); // KEY only, e.g. "tab"
4587 * console.log(event.detail.event); // EVENT, e.g. "keydown"
4588 * console.log(event.detail.keyboardEvent); // the original KeyboardE vent
4589 * }
4590 *
4591 * Use the `keyEventTarget` attribute to set up event handlers on a specific
4592 * node.
4593 *
4594 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k eys-behavior/blob/master/demo/x-key-aware.html)
4595 * for an example.
4596 *
4597 * @demo demo/index.html
4598 * @polymerBehavior
4599 */
4600 Polymer.IronA11yKeysBehavior = {
4601 properties: {
4602 /**
4603 * The EventTarget that will be firing relevant KeyboardEvents. Set it t o
4604 * `null` to disable the listeners.
4605 * @type {?EventTarget}
4606 */
4607 keyEventTarget: {
4608 type: Object,
4609 value: function() {
4610 return this;
4611 }
4612 },
4613
4614 /**
4615 * If true, this property will cause the implementing element to
4616 * automatically stop propagation on any handled KeyboardEvents.
4617 */
4618 stopKeyboardEventPropagation: {
4619 type: Boolean,
4620 value: false
4621 },
4622
4623 _boundKeyHandlers: {
4624 type: Array,
4625 value: function() {
4626 return [];
4627 }
4628 },
4629
4630 // We use this due to a limitation in IE10 where instances will have
4631 // own properties of everything on the "prototype".
4632 _imperativeKeyBindings: {
4633 type: Object,
4634 value: function() {
4635 return {};
4636 }
4637 }
4638 },
4639
4640 observers: [
4641 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
4642 ],
4643
4644
4645 /**
4646 * To be used to express what combination of keys will trigger the relati ve
4647 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
4648 * @type {Object}
4649 */
4650 keyBindings: {},
4651
4652 registered: function() {
4653 this._prepKeyBindings();
4654 },
4655
4656 attached: function() {
4657 this._listenKeyEventListeners();
4658 },
4659
4660 detached: function() {
4661 this._unlistenKeyEventListeners();
4662 },
4663
4664 /**
4665 * Can be used to imperatively add a key binding to the implementing
4666 * element. This is the imperative equivalent of declaring a keybinding
4667 * in the `keyBindings` prototype property.
4668 */
4669 addOwnKeyBinding: function(eventString, handlerName) {
4670 this._imperativeKeyBindings[eventString] = handlerName;
4671 this._prepKeyBindings();
4672 this._resetKeyEventListeners();
4673 },
4674
4675 /**
4676 * When called, will remove all imperatively-added key bindings.
4677 */
4678 removeOwnKeyBindings: function() {
4679 this._imperativeKeyBindings = {};
4680 this._prepKeyBindings();
4681 this._resetKeyEventListeners();
4682 },
4683
4684 /**
4685 * Returns true if a keyboard event matches `eventString`.
4686 *
4687 * @param {KeyboardEvent} event
4688 * @param {string} eventString
4689 * @return {boolean}
4690 */
4691 keyboardEventMatchesKeys: function(event, eventString) {
4692 var keyCombos = parseEventString(eventString);
4693 for (var i = 0; i < keyCombos.length; ++i) {
4694 if (keyComboMatchesEvent(keyCombos[i], event)) {
4695 return true;
4696 }
4697 }
4698 return false;
4699 },
4700
4701 _collectKeyBindings: function() {
4702 var keyBindings = this.behaviors.map(function(behavior) {
4703 return behavior.keyBindings;
4704 });
4705
4706 if (keyBindings.indexOf(this.keyBindings) === -1) {
4707 keyBindings.push(this.keyBindings);
4708 }
4709
4710 return keyBindings;
4711 },
4712
4713 _prepKeyBindings: function() {
4714 this._keyBindings = {};
4715
4716 this._collectKeyBindings().forEach(function(keyBindings) {
4717 for (var eventString in keyBindings) {
4718 this._addKeyBinding(eventString, keyBindings[eventString]);
4719 }
4720 }, this);
4721
4722 for (var eventString in this._imperativeKeyBindings) {
4723 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri ng]);
4724 }
4725
4726 // Give precedence to combos with modifiers to be checked first.
4727 for (var eventName in this._keyBindings) {
4728 this._keyBindings[eventName].sort(function (kb1, kb2) {
4729 var b1 = kb1[0].hasModifiers;
4730 var b2 = kb2[0].hasModifiers;
4731 return (b1 === b2) ? 0 : b1 ? -1 : 1;
4732 })
4733 }
4734 },
4735
4736 _addKeyBinding: function(eventString, handlerName) {
4737 parseEventString(eventString).forEach(function(keyCombo) {
4738 this._keyBindings[keyCombo.event] =
4739 this._keyBindings[keyCombo.event] || [];
4740
4741 this._keyBindings[keyCombo.event].push([
4742 keyCombo,
4743 handlerName
4744 ]);
4745 }, this);
4746 },
4747
4748 _resetKeyEventListeners: function() {
4749 this._unlistenKeyEventListeners();
4750
4751 if (this.isAttached) {
4752 this._listenKeyEventListeners();
4753 }
4754 },
4755
4756 _listenKeyEventListeners: function() {
4757 if (!this.keyEventTarget) {
4758 return;
4759 }
4760 Object.keys(this._keyBindings).forEach(function(eventName) {
4761 var keyBindings = this._keyBindings[eventName];
4762 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
4763
4764 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH andler]);
4765
4766 this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
4767 }, this);
4768 },
4769
4770 _unlistenKeyEventListeners: function() {
4771 var keyHandlerTuple;
4772 var keyEventTarget;
4773 var eventName;
4774 var boundKeyHandler;
4775
4776 while (this._boundKeyHandlers.length) {
4777 // My kingdom for block-scope binding and destructuring assignment..
4778 keyHandlerTuple = this._boundKeyHandlers.pop();
4779 keyEventTarget = keyHandlerTuple[0];
4780 eventName = keyHandlerTuple[1];
4781 boundKeyHandler = keyHandlerTuple[2];
4782
4783 keyEventTarget.removeEventListener(eventName, boundKeyHandler);
4784 }
4785 },
4786
4787 _onKeyBindingEvent: function(keyBindings, event) {
4788 if (this.stopKeyboardEventPropagation) {
4789 event.stopPropagation();
4790 }
4791
4792 // if event has been already prevented, don't do anything
4793 if (event.defaultPrevented) {
4794 return;
4795 }
4796
4797 for (var i = 0; i < keyBindings.length; i++) {
4798 var keyCombo = keyBindings[i][0];
4799 var handlerName = keyBindings[i][1];
4800 if (keyComboMatchesEvent(keyCombo, event)) {
4801 this._triggerKeyHandler(keyCombo, handlerName, event);
4802 // exit the loop if eventDefault was prevented
4803 if (event.defaultPrevented) {
4804 return;
4805 }
4806 }
4807 }
4808 },
4809
4810 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
4811 var detail = Object.create(keyCombo);
4812 detail.keyboardEvent = keyboardEvent;
4813 var event = new CustomEvent(keyCombo.event, {
4814 detail: detail,
4815 cancelable: true
4816 });
4817 this[handlerName].call(this, event);
4818 if (event.defaultPrevented) {
4819 keyboardEvent.preventDefault();
4820 }
4821 }
4822 };
4823 })();
4824 /**
4825 * @demo demo/index.html
4826 * @polymerBehavior
4827 */
4828 Polymer.IronControlState = {
6278 4829
6279 properties: { 4830 properties: {
6280 data: { 4831
6281 type: Object, 4832 /**
6282 }, 4833 * If true, the element currently has focus.
6283 4834 */
6284 completelyOnDisk_: { 4835 focused: {
6285 computed: 'computeCompletelyOnDisk_(' +
6286 'data.state, data.file_externally_removed)',
6287 type: Boolean,
6288 value: true,
6289 },
6290
6291 controlledBy_: {
6292 computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
6293 type: String,
6294 value: '',
6295 },
6296
6297 isActive_: {
6298 computed: 'computeIsActive_(' +
6299 'data.state, data.file_externally_removed)',
6300 type: Boolean,
6301 value: true,
6302 },
6303
6304 isDangerous_: {
6305 computed: 'computeIsDangerous_(data.state)',
6306 type: Boolean, 4836 type: Boolean,
6307 value: false, 4837 value: false,
6308 }, 4838 notify: true,
6309 4839 readOnly: true,
6310 isMalware_: { 4840 reflectToAttribute: true
6311 computed: 'computeIsMalware_(isDangerous_, data.danger_type)', 4841 },
4842
4843 /**
4844 * If true, the user cannot interact with this element.
4845 */
4846 disabled: {
6312 type: Boolean, 4847 type: Boolean,
6313 value: false, 4848 value: false,
6314 }, 4849 notify: true,
6315 4850 observer: '_disabledChanged',
6316 isInProgress_: { 4851 reflectToAttribute: true
6317 computed: 'computeIsInProgress_(data.state)', 4852 },
4853
4854 _oldTabIndex: {
4855 type: Number
4856 },
4857
4858 _boundFocusBlurHandler: {
4859 type: Function,
4860 value: function() {
4861 return this._focusBlurHandler.bind(this);
4862 }
4863 }
4864
4865 },
4866
4867 observers: [
4868 '_changedControlState(focused, disabled)'
4869 ],
4870
4871 ready: function() {
4872 this.addEventListener('focus', this._boundFocusBlurHandler, true);
4873 this.addEventListener('blur', this._boundFocusBlurHandler, true);
4874 },
4875
4876 _focusBlurHandler: function(event) {
4877 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
4878 // eventually become `this` due to retargeting; if we are not in
4879 // ShadowDOM land, `event.target` will eventually become `this` due
4880 // to the second conditional which fires a synthetic event (that is also
4881 // handled). In either case, we can disregard `event.path`.
4882
4883 if (event.target === this) {
4884 this._setFocused(event.type === 'focus');
4885 } else if (!this.shadowRoot) {
4886 var target = /** @type {Node} */(Polymer.dom(event).localTarget);
4887 if (!this.isLightDescendant(target)) {
4888 this.fire(event.type, {sourceEvent: event}, {
4889 node: this,
4890 bubbles: event.bubbles,
4891 cancelable: event.cancelable
4892 });
4893 }
4894 }
4895 },
4896
4897 _disabledChanged: function(disabled, old) {
4898 this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
4899 this.style.pointerEvents = disabled ? 'none' : '';
4900 if (disabled) {
4901 this._oldTabIndex = this.tabIndex;
4902 this._setFocused(false);
4903 this.tabIndex = -1;
4904 this.blur();
4905 } else if (this._oldTabIndex !== undefined) {
4906 this.tabIndex = this._oldTabIndex;
4907 }
4908 },
4909
4910 _changedControlState: function() {
4911 // _controlStateChanged is abstract, follow-on behaviors may implement it
4912 if (this._controlStateChanged) {
4913 this._controlStateChanged();
4914 }
4915 }
4916
4917 };
4918 /**
4919 * @demo demo/index.html
4920 * @polymerBehavior Polymer.IronButtonState
4921 */
4922 Polymer.IronButtonStateImpl = {
4923
4924 properties: {
4925
4926 /**
4927 * If true, the user is currently holding down the button.
4928 */
4929 pressed: {
4930 type: Boolean,
4931 readOnly: true,
4932 value: false,
4933 reflectToAttribute: true,
4934 observer: '_pressedChanged'
4935 },
4936
4937 /**
4938 * If true, the button toggles the active state with each tap or press
4939 * of the spacebar.
4940 */
4941 toggles: {
6318 type: Boolean, 4942 type: Boolean,
6319 value: false, 4943 value: false,
6320 }, 4944 reflectToAttribute: true
6321 4945 },
6322 pauseOrResumeText_: { 4946
6323 computed: 'computePauseOrResumeText_(isInProgress_, data.resume)', 4947 /**
6324 type: String, 4948 * If true, the button is a toggle and is currently in the active state.
6325 }, 4949 */
6326 4950 active: {
6327 showCancel_: {
6328 computed: 'computeShowCancel_(data.state)',
6329 type: Boolean, 4951 type: Boolean,
6330 value: false, 4952 value: false,
6331 }, 4953 notify: true,
6332 4954 reflectToAttribute: true
6333 showProgress_: { 4955 },
6334 computed: 'computeShowProgress_(showCancel_, data.percent)', 4956
4957 /**
4958 * True if the element is currently being pressed by a "pointer," which
4959 * is loosely defined as mouse or touch input (but specifically excluding
4960 * keyboard input).
4961 */
4962 pointerDown: {
6335 type: Boolean, 4963 type: Boolean,
6336 value: false, 4964 readOnly: true,
6337 }, 4965 value: false
4966 },
4967
4968 /**
4969 * True if the input device that caused the element to receive focus
4970 * was a keyboard.
4971 */
4972 receivedFocusFromKeyboard: {
4973 type: Boolean,
4974 readOnly: true
4975 },
4976
4977 /**
4978 * The aria attribute to be set if the button is a toggle and in the
4979 * active state.
4980 */
4981 ariaActiveAttribute: {
4982 type: String,
4983 value: 'aria-pressed',
4984 observer: '_ariaActiveAttributeChanged'
4985 }
4986 },
4987
4988 listeners: {
4989 down: '_downHandler',
4990 up: '_upHandler',
4991 tap: '_tapHandler'
6338 }, 4992 },
6339 4993
6340 observers: [ 4994 observers: [
6341 // TODO(dbeam): this gets called way more when I observe data.by_ext_id 4995 '_detectKeyboardFocus(focused)',
6342 // and data.by_ext_name directly. Why? 4996 '_activeChanged(active, ariaActiveAttribute)'
6343 'observeControlledBy_(controlledBy_)',
6344 'observeIsDangerous_(isDangerous_, data)',
6345 ], 4997 ],
6346 4998
6347 ready: function() { 4999 keyBindings: {
6348 this.content = this.$.content; 5000 'enter:keydown': '_asyncClick',
6349 }, 5001 'space:keydown': '_spaceKeyDownHandler',
6350 5002 'space:keyup': '_spaceKeyUpHandler',
6351 /** @private */ 5003 },
6352 computeClass_: function() { 5004
6353 var classes = []; 5005 _mouseEventRe: /^mouse/,
6354 5006
6355 if (this.isActive_) 5007 _tapHandler: function() {
6356 classes.push('is-active'); 5008 if (this.toggles) {
6357 5009 // a tap is needed to toggle the active state
6358 if (this.isDangerous_) 5010 this._userActivate(!this.active);
6359 classes.push('dangerous'); 5011 } else {
6360 5012 this.active = false;
6361 if (this.showProgress_) 5013 }
6362 classes.push('show-progress'); 5014 },
6363 5015
6364 return classes.join(' '); 5016 _detectKeyboardFocus: function(focused) {
6365 }, 5017 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
6366 5018 },
6367 /** @private */ 5019
6368 computeCompletelyOnDisk_: function() { 5020 // to emulate native checkbox, (de-)activations from a user interaction fire
6369 return this.data.state == downloads.States.COMPLETE && 5021 // 'change' events
6370 !this.data.file_externally_removed; 5022 _userActivate: function(active) {
6371 }, 5023 if (this.active !== active) {
6372 5024 this.active = active;
6373 /** @private */ 5025 this.fire('change');
6374 computeControlledBy_: function() { 5026 }
6375 if (!this.data.by_ext_id || !this.data.by_ext_name) 5027 },
6376 return ''; 5028
6377 5029 _downHandler: function(event) {
6378 var url = 'chrome://extensions#' + this.data.by_ext_id; 5030 this._setPointerDown(true);
6379 var name = this.data.by_ext_name; 5031 this._setPressed(true);
6380 return loadTimeData.getStringF('controlledByUrl', url, name); 5032 this._setReceivedFocusFromKeyboard(false);
6381 }, 5033 },
6382 5034
6383 /** @private */ 5035 _upHandler: function() {
6384 computeDangerIcon_: function() { 5036 this._setPointerDown(false);
6385 if (!this.isDangerous_) 5037 this._setPressed(false);
6386 return ''; 5038 },
6387 5039
6388 switch (this.data.danger_type) { 5040 /**
6389 case downloads.DangerType.DANGEROUS_CONTENT: 5041 * @param {!KeyboardEvent} event .
6390 case downloads.DangerType.DANGEROUS_HOST: 5042 */
6391 case downloads.DangerType.DANGEROUS_URL: 5043 _spaceKeyDownHandler: function(event) {
6392 case downloads.DangerType.POTENTIALLY_UNWANTED: 5044 var keyboardEvent = event.detail.keyboardEvent;
6393 case downloads.DangerType.UNCOMMON_CONTENT: 5045 var target = Polymer.dom(keyboardEvent).localTarget;
6394 return 'downloads:remove-circle'; 5046
6395 default: 5047 // Ignore the event if this is coming from a focused light child, since th at
6396 return 'cr:warning'; 5048 // element will deal with it.
6397 } 5049 if (this.isLightDescendant(/** @type {Node} */(target)))
6398 }, 5050 return;
6399 5051
6400 /** @private */ 5052 keyboardEvent.preventDefault();
6401 computeDate_: function() { 5053 keyboardEvent.stopImmediatePropagation();
6402 assert(typeof this.data.hideDate == 'boolean'); 5054 this._setPressed(true);
6403 if (this.data.hideDate) 5055 },
6404 return ''; 5056
6405 return assert(this.data.since_string || this.data.date_string); 5057 /**
6406 }, 5058 * @param {!KeyboardEvent} event .
6407 5059 */
6408 /** @private */ 5060 _spaceKeyUpHandler: function(event) {
6409 computeDescription_: function() { 5061 var keyboardEvent = event.detail.keyboardEvent;
6410 var data = this.data; 5062 var target = Polymer.dom(keyboardEvent).localTarget;
6411 5063
6412 switch (data.state) { 5064 // Ignore the event if this is coming from a focused light child, since th at
6413 case downloads.States.DANGEROUS: 5065 // element will deal with it.
6414 var fileName = data.file_name; 5066 if (this.isLightDescendant(/** @type {Node} */(target)))
6415 switch (data.danger_type) { 5067 return;
6416 case downloads.DangerType.DANGEROUS_FILE: 5068
6417 return loadTimeData.getStringF('dangerFileDesc', fileName); 5069 if (this.pressed) {
6418 case downloads.DangerType.DANGEROUS_URL: 5070 this._asyncClick();
6419 return loadTimeData.getString('dangerUrlDesc'); 5071 }
6420 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through. 5072 this._setPressed(false);
6421 case downloads.DangerType.DANGEROUS_HOST: 5073 },
6422 return loadTimeData.getStringF('dangerContentDesc', fileName); 5074
6423 case downloads.DangerType.UNCOMMON_CONTENT: 5075 // trigger click asynchronously, the asynchrony is useful to allow one
6424 return loadTimeData.getStringF('dangerUncommonDesc', fileName); 5076 // event handler to unwind before triggering another event
6425 case downloads.DangerType.POTENTIALLY_UNWANTED: 5077 _asyncClick: function() {
6426 return loadTimeData.getStringF('dangerSettingsDesc', fileName); 5078 this.async(function() {
5079 this.click();
5080 }, 1);
5081 },
5082
5083 // any of these changes are considered a change to button state
5084
5085 _pressedChanged: function(pressed) {
5086 this._changedButtonState();
5087 },
5088
5089 _ariaActiveAttributeChanged: function(value, oldValue) {
5090 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
5091 this.removeAttribute(oldValue);
5092 }
5093 },
5094
5095 _activeChanged: function(active, ariaActiveAttribute) {
5096 if (this.toggles) {
5097 this.setAttribute(this.ariaActiveAttribute,
5098 active ? 'true' : 'false');
5099 } else {
5100 this.removeAttribute(this.ariaActiveAttribute);
5101 }
5102 this._changedButtonState();
5103 },
5104
5105 _controlStateChanged: function() {
5106 if (this.disabled) {
5107 this._setPressed(false);
5108 } else {
5109 this._changedButtonState();
5110 }
5111 },
5112
5113 // provide hook for follow-on behaviors to react to button-state
5114
5115 _changedButtonState: function() {
5116 if (this._buttonStateChanged) {
5117 this._buttonStateChanged(); // abstract
5118 }
5119 }
5120
5121 };
5122
5123 /** @polymerBehavior */
5124 Polymer.IronButtonState = [
5125 Polymer.IronA11yKeysBehavior,
5126 Polymer.IronButtonStateImpl
5127 ];
5128 (function() {
5129 var Utility = {
5130 distance: function(x1, y1, x2, y2) {
5131 var xDelta = (x1 - x2);
5132 var yDelta = (y1 - y2);
5133
5134 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
5135 },
5136
5137 now: window.performance && window.performance.now ?
5138 window.performance.now.bind(window.performance) : Date.now
5139 };
5140
5141 /**
5142 * @param {HTMLElement} element
5143 * @constructor
5144 */
5145 function ElementMetrics(element) {
5146 this.element = element;
5147 this.width = this.boundingRect.width;
5148 this.height = this.boundingRect.height;
5149
5150 this.size = Math.max(this.width, this.height);
5151 }
5152
5153 ElementMetrics.prototype = {
5154 get boundingRect () {
5155 return this.element.getBoundingClientRect();
5156 },
5157
5158 furthestCornerDistanceFrom: function(x, y) {
5159 var topLeft = Utility.distance(x, y, 0, 0);
5160 var topRight = Utility.distance(x, y, this.width, 0);
5161 var bottomLeft = Utility.distance(x, y, 0, this.height);
5162 var bottomRight = Utility.distance(x, y, this.width, this.height);
5163
5164 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
5165 }
5166 };
5167
5168 /**
5169 * @param {HTMLElement} element
5170 * @constructor
5171 */
5172 function Ripple(element) {
5173 this.element = element;
5174 this.color = window.getComputedStyle(element).color;
5175
5176 this.wave = document.createElement('div');
5177 this.waveContainer = document.createElement('div');
5178 this.wave.style.backgroundColor = this.color;
5179 this.wave.classList.add('wave');
5180 this.waveContainer.classList.add('wave-container');
5181 Polymer.dom(this.waveContainer).appendChild(this.wave);
5182
5183 this.resetInteractionState();
5184 }
5185
5186 Ripple.MAX_RADIUS = 300;
5187
5188 Ripple.prototype = {
5189 get recenters() {
5190 return this.element.recenters;
5191 },
5192
5193 get center() {
5194 return this.element.center;
5195 },
5196
5197 get mouseDownElapsed() {
5198 var elapsed;
5199
5200 if (!this.mouseDownStart) {
5201 return 0;
5202 }
5203
5204 elapsed = Utility.now() - this.mouseDownStart;
5205
5206 if (this.mouseUpStart) {
5207 elapsed -= this.mouseUpElapsed;
5208 }
5209
5210 return elapsed;
5211 },
5212
5213 get mouseUpElapsed() {
5214 return this.mouseUpStart ?
5215 Utility.now () - this.mouseUpStart : 0;
5216 },
5217
5218 get mouseDownElapsedSeconds() {
5219 return this.mouseDownElapsed / 1000;
5220 },
5221
5222 get mouseUpElapsedSeconds() {
5223 return this.mouseUpElapsed / 1000;
5224 },
5225
5226 get mouseInteractionSeconds() {
5227 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
5228 },
5229
5230 get initialOpacity() {
5231 return this.element.initialOpacity;
5232 },
5233
5234 get opacityDecayVelocity() {
5235 return this.element.opacityDecayVelocity;
5236 },
5237
5238 get radius() {
5239 var width2 = this.containerMetrics.width * this.containerMetrics.width;
5240 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
5241 var waveRadius = Math.min(
5242 Math.sqrt(width2 + height2),
5243 Ripple.MAX_RADIUS
5244 ) * 1.1 + 5;
5245
5246 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
5247 var timeNow = this.mouseInteractionSeconds / duration;
5248 var size = waveRadius * (1 - Math.pow(80, -timeNow));
5249
5250 return Math.abs(size);
5251 },
5252
5253 get opacity() {
5254 if (!this.mouseUpStart) {
5255 return this.initialOpacity;
5256 }
5257
5258 return Math.max(
5259 0,
5260 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
5261 );
5262 },
5263
5264 get outerOpacity() {
5265 // Linear increase in background opacity, capped at the opacity
5266 // of the wavefront (waveOpacity).
5267 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
5268 var waveOpacity = this.opacity;
5269
5270 return Math.max(
5271 0,
5272 Math.min(outerOpacity, waveOpacity)
5273 );
5274 },
5275
5276 get isOpacityFullyDecayed() {
5277 return this.opacity < 0.01 &&
5278 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5279 },
5280
5281 get isRestingAtMaxRadius() {
5282 return this.opacity >= this.initialOpacity &&
5283 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
5284 },
5285
5286 get isAnimationComplete() {
5287 return this.mouseUpStart ?
5288 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
5289 },
5290
5291 get translationFraction() {
5292 return Math.min(
5293 1,
5294 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
5295 );
5296 },
5297
5298 get xNow() {
5299 if (this.xEnd) {
5300 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
5301 }
5302
5303 return this.xStart;
5304 },
5305
5306 get yNow() {
5307 if (this.yEnd) {
5308 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
5309 }
5310
5311 return this.yStart;
5312 },
5313
5314 get isMouseDown() {
5315 return this.mouseDownStart && !this.mouseUpStart;
5316 },
5317
5318 resetInteractionState: function() {
5319 this.maxRadius = 0;
5320 this.mouseDownStart = 0;
5321 this.mouseUpStart = 0;
5322
5323 this.xStart = 0;
5324 this.yStart = 0;
5325 this.xEnd = 0;
5326 this.yEnd = 0;
5327 this.slideDistance = 0;
5328
5329 this.containerMetrics = new ElementMetrics(this.element);
5330 },
5331
5332 draw: function() {
5333 var scale;
5334 var translateString;
5335 var dx;
5336 var dy;
5337
5338 this.wave.style.opacity = this.opacity;
5339
5340 scale = this.radius / (this.containerMetrics.size / 2);
5341 dx = this.xNow - (this.containerMetrics.width / 2);
5342 dy = this.yNow - (this.containerMetrics.height / 2);
5343
5344
5345 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
5346 // https://bugs.webkit.org/show_bug.cgi?id=98538
5347 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
5348 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
5349 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
5350 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
5351 },
5352
5353 /** @param {Event=} event */
5354 downAction: function(event) {
5355 var xCenter = this.containerMetrics.width / 2;
5356 var yCenter = this.containerMetrics.height / 2;
5357
5358 this.resetInteractionState();
5359 this.mouseDownStart = Utility.now();
5360
5361 if (this.center) {
5362 this.xStart = xCenter;
5363 this.yStart = yCenter;
5364 this.slideDistance = Utility.distance(
5365 this.xStart, this.yStart, this.xEnd, this.yEnd
5366 );
5367 } else {
5368 this.xStart = event ?
5369 event.detail.x - this.containerMetrics.boundingRect.left :
5370 this.containerMetrics.width / 2;
5371 this.yStart = event ?
5372 event.detail.y - this.containerMetrics.boundingRect.top :
5373 this.containerMetrics.height / 2;
5374 }
5375
5376 if (this.recenters) {
5377 this.xEnd = xCenter;
5378 this.yEnd = yCenter;
5379 this.slideDistance = Utility.distance(
5380 this.xStart, this.yStart, this.xEnd, this.yEnd
5381 );
5382 }
5383
5384 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
5385 this.xStart,
5386 this.yStart
5387 );
5388
5389 this.waveContainer.style.top =
5390 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
5391 this.waveContainer.style.left =
5392 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
5393
5394 this.waveContainer.style.width = this.containerMetrics.size + 'px';
5395 this.waveContainer.style.height = this.containerMetrics.size + 'px';
5396 },
5397
5398 /** @param {Event=} event */
5399 upAction: function(event) {
5400 if (!this.isMouseDown) {
5401 return;
5402 }
5403
5404 this.mouseUpStart = Utility.now();
5405 },
5406
5407 remove: function() {
5408 Polymer.dom(this.waveContainer.parentNode).removeChild(
5409 this.waveContainer
5410 );
5411 }
5412 };
5413
5414 Polymer({
5415 is: 'paper-ripple',
5416
5417 behaviors: [
5418 Polymer.IronA11yKeysBehavior
5419 ],
5420
5421 properties: {
5422 /**
5423 * The initial opacity set on the wave.
5424 *
5425 * @attribute initialOpacity
5426 * @type number
5427 * @default 0.25
5428 */
5429 initialOpacity: {
5430 type: Number,
5431 value: 0.25
5432 },
5433
5434 /**
5435 * How fast (opacity per second) the wave fades out.
5436 *
5437 * @attribute opacityDecayVelocity
5438 * @type number
5439 * @default 0.8
5440 */
5441 opacityDecayVelocity: {
5442 type: Number,
5443 value: 0.8
5444 },
5445
5446 /**
5447 * If true, ripples will exhibit a gravitational pull towards
5448 * the center of their container as they fade away.
5449 *
5450 * @attribute recenters
5451 * @type boolean
5452 * @default false
5453 */
5454 recenters: {
5455 type: Boolean,
5456 value: false
5457 },
5458
5459 /**
5460 * If true, ripples will center inside its container
5461 *
5462 * @attribute recenters
5463 * @type boolean
5464 * @default false
5465 */
5466 center: {
5467 type: Boolean,
5468 value: false
5469 },
5470
5471 /**
5472 * A list of the visual ripples.
5473 *
5474 * @attribute ripples
5475 * @type Array
5476 * @default []
5477 */
5478 ripples: {
5479 type: Array,
5480 value: function() {
5481 return [];
6427 } 5482 }
6428 break; 5483 },
6429 5484
6430 case downloads.States.IN_PROGRESS: 5485 /**
6431 case downloads.States.PAUSED: // Fallthrough. 5486 * True when there are visible ripples animating within the
6432 return data.progress_status_text; 5487 * element.
6433 } 5488 */
6434 5489 animating: {
6435 return ''; 5490 type: Boolean,
6436 }, 5491 readOnly: true,
6437 5492 reflectToAttribute: true,
6438 /** @private */ 5493 value: false
6439 computeIsActive_: function() { 5494 },
6440 return this.data.state != downloads.States.CANCELLED && 5495
6441 this.data.state != downloads.States.INTERRUPTED && 5496 /**
6442 !this.data.file_externally_removed; 5497 * If true, the ripple will remain in the "down" state until `holdDown`
6443 }, 5498 * is set to false again.
6444 5499 */
6445 /** @private */ 5500 holdDown: {
6446 computeIsDangerous_: function() { 5501 type: Boolean,
6447 return this.data.state == downloads.States.DANGEROUS; 5502 value: false,
6448 }, 5503 observer: '_holdDownChanged'
6449 5504 },
6450 /** @private */ 5505
6451 computeIsInProgress_: function() { 5506 /**
6452 return this.data.state == downloads.States.IN_PROGRESS; 5507 * If true, the ripple will not generate a ripple effect
6453 }, 5508 * via pointer interaction.
6454 5509 * Calling ripple's imperative api like `simulatedRipple` will
6455 /** @private */ 5510 * still generate the ripple effect.
6456 computeIsMalware_: function() { 5511 */
6457 return this.isDangerous_ && 5512 noink: {
6458 (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT || 5513 type: Boolean,
6459 this.data.danger_type == downloads.DangerType.DANGEROUS_HOST || 5514 value: false
6460 this.data.danger_type == downloads.DangerType.DANGEROUS_URL || 5515 },
6461 this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED); 5516
6462 }, 5517 _animating: {
6463 5518 type: Boolean
6464 /** @private */ 5519 },
6465 computePauseOrResumeText_: function() { 5520
6466 if (this.isInProgress_) 5521 _boundAnimate: {
6467 return loadTimeData.getString('controlPause'); 5522 type: Function,
6468 if (this.data.resume) 5523 value: function() {
6469 return loadTimeData.getString('controlResume'); 5524 return this.animate.bind(this);
6470 return ''; 5525 }
6471 }, 5526 }
6472 5527 },
6473 /** @private */ 5528
6474 computeRemoveStyle_: function() { 5529 get target () {
6475 var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); 5530 return this.keyEventTarget;
6476 var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; 5531 },
6477 return hideRemove ? 'visibility: hidden' : ''; 5532
6478 }, 5533 keyBindings: {
6479 5534 'enter:keydown': '_onEnterKeydown',
6480 /** @private */ 5535 'space:keydown': '_onSpaceKeydown',
6481 computeShowCancel_: function() { 5536 'space:keyup': '_onSpaceKeyup'
6482 return this.data.state == downloads.States.IN_PROGRESS || 5537 },
6483 this.data.state == downloads.States.PAUSED; 5538
6484 }, 5539 attached: function() {
6485 5540 // Set up a11yKeysBehavior to listen to key events on the target,
6486 /** @private */ 5541 // so that space and enter activate the ripple even if the target doesn' t
6487 computeShowProgress_: function() { 5542 // handle key events. The key handlers deal with `noink` themselves.
6488 return this.showCancel_ && this.data.percent >= -1; 5543 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
6489 }, 5544 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
6490 5545 } else {
6491 /** @private */ 5546 this.keyEventTarget = this.parentNode;
6492 computeTag_: function() { 5547 }
6493 switch (this.data.state) { 5548 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
6494 case downloads.States.CANCELLED: 5549 this.listen(keyEventTarget, 'up', 'uiUpAction');
6495 return loadTimeData.getString('statusCancelled'); 5550 this.listen(keyEventTarget, 'down', 'uiDownAction');
6496 5551 },
6497 case downloads.States.INTERRUPTED: 5552
6498 return this.data.last_reason_text; 5553 detached: function() {
6499 5554 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
6500 case downloads.States.COMPLETE: 5555 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
6501 return this.data.file_externally_removed ? 5556 this.keyEventTarget = null;
6502 loadTimeData.getString('statusRemoved') : ''; 5557 },
6503 } 5558
6504 5559 get shouldKeepAnimating () {
6505 return ''; 5560 for (var index = 0; index < this.ripples.length; ++index) {
6506 }, 5561 if (!this.ripples[index].isAnimationComplete) {
6507 5562 return true;
6508 /** @private */ 5563 }
6509 isIndeterminate_: function() { 5564 }
6510 return this.data.percent == -1; 5565
6511 }, 5566 return false;
6512 5567 },
6513 /** @private */ 5568
6514 observeControlledBy_: function() { 5569 simulatedRipple: function() {
6515 this.$['controlled-by'].innerHTML = this.controlledBy_; 5570 this.downAction(null);
6516 }, 5571
6517 5572 // Please see polymer/polymer#1305
6518 /** @private */ 5573 this.async(function() {
6519 observeIsDangerous_: function() { 5574 this.upAction();
6520 if (!this.data) 5575 }, 1);
6521 return; 5576 },
6522 5577
6523 if (this.isDangerous_) { 5578 /**
6524 this.$.url.removeAttribute('href'); 5579 * Provokes a ripple down effect via a UI event,
6525 } else { 5580 * respecting the `noink` property.
6526 this.$.url.href = assert(this.data.url); 5581 * @param {Event=} event
6527 var filePath = encodeURIComponent(this.data.file_path); 5582 */
6528 var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; 5583 uiDownAction: function(event) {
6529 this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; 5584 if (!this.noink) {
6530 } 5585 this.downAction(event);
6531 }, 5586 }
6532 5587 },
6533 /** @private */ 5588
6534 onCancelTap_: function() { 5589 /**
6535 downloads.ActionService.getInstance().cancel(this.data.id); 5590 * Provokes a ripple down effect via a UI event,
6536 }, 5591 * *not* respecting the `noink` property.
6537 5592 * @param {Event=} event
6538 /** @private */ 5593 */
6539 onDiscardDangerousTap_: function() { 5594 downAction: function(event) {
6540 downloads.ActionService.getInstance().discardDangerous(this.data.id); 5595 if (this.holdDown && this.ripples.length > 0) {
6541 }, 5596 return;
6542 5597 }
6543 /** 5598
6544 * @private 5599 var ripple = this.addRipple();
6545 * @param {Event} e 5600
6546 */ 5601 ripple.downAction(event);
6547 onDragStart_: function(e) { 5602
6548 e.preventDefault(); 5603 if (!this._animating) {
6549 downloads.ActionService.getInstance().drag(this.data.id); 5604 this._animating = true;
6550 }, 5605 this.animate();
6551 5606 }
6552 /** 5607 },
6553 * @param {Event} e 5608
6554 * @private 5609 /**
6555 */ 5610 * Provokes a ripple up effect via a UI event,
6556 onFileLinkTap_: function(e) { 5611 * respecting the `noink` property.
6557 e.preventDefault(); 5612 * @param {Event=} event
6558 downloads.ActionService.getInstance().openFile(this.data.id); 5613 */
6559 }, 5614 uiUpAction: function(event) {
6560 5615 if (!this.noink) {
6561 /** @private */ 5616 this.upAction(event);
6562 onPauseOrResumeTap_: function() { 5617 }
6563 if (this.isInProgress_) 5618 },
6564 downloads.ActionService.getInstance().pause(this.data.id); 5619
6565 else 5620 /**
6566 downloads.ActionService.getInstance().resume(this.data.id); 5621 * Provokes a ripple up effect via a UI event,
6567 }, 5622 * *not* respecting the `noink` property.
6568 5623 * @param {Event=} event
6569 /** @private */ 5624 */
6570 onRemoveTap_: function() { 5625 upAction: function(event) {
6571 downloads.ActionService.getInstance().remove(this.data.id); 5626 if (this.holdDown) {
6572 }, 5627 return;
6573 5628 }
6574 /** @private */ 5629
6575 onRetryTap_: function() { 5630 this.ripples.forEach(function(ripple) {
6576 downloads.ActionService.getInstance().download(this.data.url); 5631 ripple.upAction(event);
6577 }, 5632 });
6578 5633
6579 /** @private */ 5634 this._animating = true;
6580 onSaveDangerousTap_: function() { 5635 this.animate();
6581 downloads.ActionService.getInstance().saveDangerous(this.data.id); 5636 },
6582 }, 5637
6583 5638 onAnimationComplete: function() {
6584 /** @private */ 5639 this._animating = false;
6585 onShowTap_: function() { 5640 this.$.background.style.backgroundColor = null;
6586 downloads.ActionService.getInstance().show(this.data.id); 5641 this.fire('transitionend');
6587 }, 5642 },
6588 }); 5643
6589 5644 addRipple: function() {
6590 return {Item: Item}; 5645 var ripple = new Ripple(this);
6591 }); 5646
6592 /** @polymerBehavior Polymer.PaperItemBehavior */ 5647 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
6593 Polymer.PaperItemBehaviorImpl = { 5648 this.$.background.style.backgroundColor = ripple.color;
6594 hostAttributes: { 5649 this.ripples.push(ripple);
6595 role: 'option', 5650
6596 tabindex: '0' 5651 this._setAnimating(true);
5652
5653 return ripple;
5654 },
5655
5656 removeRipple: function(ripple) {
5657 var rippleIndex = this.ripples.indexOf(ripple);
5658
5659 if (rippleIndex < 0) {
5660 return;
5661 }
5662
5663 this.ripples.splice(rippleIndex, 1);
5664
5665 ripple.remove();
5666
5667 if (!this.ripples.length) {
5668 this._setAnimating(false);
5669 }
5670 },
5671
5672 animate: function() {
5673 if (!this._animating) {
5674 return;
5675 }
5676 var index;
5677 var ripple;
5678
5679 for (index = 0; index < this.ripples.length; ++index) {
5680 ripple = this.ripples[index];
5681
5682 ripple.draw();
5683
5684 this.$.background.style.opacity = ripple.outerOpacity;
5685
5686 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
5687 this.removeRipple(ripple);
5688 }
5689 }
5690
5691 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
5692 this.onAnimationComplete();
5693 } else {
5694 window.requestAnimationFrame(this._boundAnimate);
5695 }
5696 },
5697
5698 _onEnterKeydown: function() {
5699 this.uiDownAction();
5700 this.async(this.uiUpAction, 1);
5701 },
5702
5703 _onSpaceKeydown: function() {
5704 this.uiDownAction();
5705 },
5706
5707 _onSpaceKeyup: function() {
5708 this.uiUpAction();
5709 },
5710
5711 // note: holdDown does not respect noink since it can be a focus based
5712 // effect.
5713 _holdDownChanged: function(newVal, oldVal) {
5714 if (oldVal === undefined) {
5715 return;
5716 }
5717 if (newVal) {
5718 this.downAction();
5719 } else {
5720 this.upAction();
5721 }
5722 }
5723
5724 /**
5725 Fired when the animation finishes.
5726 This is useful if you want to wait until
5727 the ripple animation finishes to perform some action.
5728
5729 @event transitionend
5730 @param {{node: Object}} detail Contains the animated node.
5731 */
5732 });
5733 })();
5734 /**
5735 * `Polymer.PaperRippleBehavior` dynamically implements a ripple
5736 * when the element has focus via pointer or keyboard.
5737 *
5738 * NOTE: This behavior is intended to be used in conjunction with and after
5739 * `Polymer.IronButtonState` and `Polymer.IronControlState`.
5740 *
5741 * @polymerBehavior Polymer.PaperRippleBehavior
5742 */
5743 Polymer.PaperRippleBehavior = {
5744 properties: {
5745 /**
5746 * If true, the element will not produce a ripple effect when interacted
5747 * with via the pointer.
5748 */
5749 noink: {
5750 type: Boolean,
5751 observer: '_noinkChanged'
5752 },
5753
5754 /**
5755 * @type {Element|undefined}
5756 */
5757 _rippleContainer: {
5758 type: Object,
5759 }
5760 },
5761
5762 /**
5763 * Ensures a `<paper-ripple>` element is available when the element is
5764 * focused.
5765 */
5766 _buttonStateChanged: function() {
5767 if (this.focused) {
5768 this.ensureRipple();
5769 }
5770 },
5771
5772 /**
5773 * In addition to the functionality provided in `IronButtonState`, ensures
5774 * a ripple effect is created when the element is in a `pressed` state.
5775 */
5776 _downHandler: function(event) {
5777 Polymer.IronButtonStateImpl._downHandler.call(this, event);
5778 if (this.pressed) {
5779 this.ensureRipple(event);
5780 }
5781 },
5782
5783 /**
5784 * Ensures this element contains a ripple effect. For startup efficiency
5785 * the ripple effect is dynamically on demand when needed.
5786 * @param {!Event=} optTriggeringEvent (optional) event that triggered the
5787 * ripple.
5788 */
5789 ensureRipple: function(optTriggeringEvent) {
5790 if (!this.hasRipple()) {
5791 this._ripple = this._createRipple();
5792 this._ripple.noink = this.noink;
5793 var rippleContainer = this._rippleContainer || this.root;
5794 if (rippleContainer) {
5795 Polymer.dom(rippleContainer).appendChild(this._ripple);
5796 }
5797 if (optTriggeringEvent) {
5798 // Check if the event happened inside of the ripple container
5799 // Fall back to host instead of the root because distributed text
5800 // nodes are not valid event targets
5801 var domContainer = Polymer.dom(this._rippleContainer || this);
5802 var target = Polymer.dom(optTriggeringEvent).rootTarget;
5803 if (domContainer.deepContains( /** @type {Node} */(target))) {
5804 this._ripple.uiDownAction(optTriggeringEvent);
5805 }
5806 }
5807 }
5808 },
5809
5810 /**
5811 * Returns the `<paper-ripple>` element used by this element to create
5812 * ripple effects. The element's ripple is created on demand, when
5813 * necessary, and calling this method will force the
5814 * ripple to be created.
5815 */
5816 getRipple: function() {
5817 this.ensureRipple();
5818 return this._ripple;
5819 },
5820
5821 /**
5822 * Returns true if this element currently contains a ripple effect.
5823 * @return {boolean}
5824 */
5825 hasRipple: function() {
5826 return Boolean(this._ripple);
5827 },
5828
5829 /**
5830 * Create the element's ripple effect via creating a `<paper-ripple>`.
5831 * Override this method to customize the ripple element.
5832 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
5833 */
5834 _createRipple: function() {
5835 return /** @type {!PaperRippleElement} */ (
5836 document.createElement('paper-ripple'));
5837 },
5838
5839 _noinkChanged: function(noink) {
5840 if (this.hasRipple()) {
5841 this._ripple.noink = noink;
5842 }
6597 } 5843 }
6598 }; 5844 };
5845 /** @polymerBehavior Polymer.PaperButtonBehavior */
5846 Polymer.PaperButtonBehaviorImpl = {
5847 properties: {
5848 /**
5849 * The z-depth of this element, from 0-5. Setting to 0 will remove the
5850 * shadow, and each increasing number greater than 0 will be "deeper"
5851 * than the last.
5852 *
5853 * @attribute elevation
5854 * @type number
5855 * @default 1
5856 */
5857 elevation: {
5858 type: Number,
5859 reflectToAttribute: true,
5860 readOnly: true
5861 }
5862 },
5863
5864 observers: [
5865 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom Keyboard)',
5866 '_computeKeyboardClass(receivedFocusFromKeyboard)'
5867 ],
5868
5869 hostAttributes: {
5870 role: 'button',
5871 tabindex: '0',
5872 animated: true
5873 },
5874
5875 _calculateElevation: function() {
5876 var e = 1;
5877 if (this.disabled) {
5878 e = 0;
5879 } else if (this.active || this.pressed) {
5880 e = 4;
5881 } else if (this.receivedFocusFromKeyboard) {
5882 e = 3;
5883 }
5884 this._setElevation(e);
5885 },
5886
5887 _computeKeyboardClass: function(receivedFocusFromKeyboard) {
5888 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
5889 },
5890
5891 /**
5892 * In addition to `IronButtonState` behavior, when space key goes down,
5893 * create a ripple down effect.
5894 *
5895 * @param {!KeyboardEvent} event .
5896 */
5897 _spaceKeyDownHandler: function(event) {
5898 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
5899 // Ensure that there is at most one ripple when the space key is held down .
5900 if (this.hasRipple() && this.getRipple().ripples.length < 1) {
5901 this._ripple.uiDownAction();
5902 }
5903 },
5904
5905 /**
5906 * In addition to `IronButtonState` behavior, when space key goes up,
5907 * create a ripple up effect.
5908 *
5909 * @param {!KeyboardEvent} event .
5910 */
5911 _spaceKeyUpHandler: function(event) {
5912 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
5913 if (this.hasRipple()) {
5914 this._ripple.uiUpAction();
5915 }
5916 }
5917 };
6599 5918
6600 /** @polymerBehavior */ 5919 /** @polymerBehavior */
6601 Polymer.PaperItemBehavior = [ 5920 Polymer.PaperButtonBehavior = [
6602 Polymer.IronButtonState, 5921 Polymer.IronButtonState,
6603 Polymer.IronControlState, 5922 Polymer.IronControlState,
6604 Polymer.PaperItemBehaviorImpl 5923 Polymer.PaperRippleBehavior,
5924 Polymer.PaperButtonBehaviorImpl
6605 ]; 5925 ];
6606 Polymer({ 5926 Polymer({
6607 is: 'paper-item', 5927 is: 'paper-button',
6608 5928
6609 behaviors: [ 5929 behaviors: [
6610 Polymer.PaperItemBehavior 5930 Polymer.PaperButtonBehavior
6611 ] 5931 ],
5932
5933 properties: {
5934 /**
5935 * If true, the button should be styled with a shadow.
5936 */
5937 raised: {
5938 type: Boolean,
5939 reflectToAttribute: true,
5940 value: false,
5941 observer: '_calculateElevation'
5942 }
5943 },
5944
5945 _calculateElevation: function() {
5946 if (!this.raised) {
5947 this._setElevation(0);
5948 } else {
5949 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
5950 }
5951 }
5952
5953 /**
5954 Fired when the animation finishes.
5955 This is useful if you want to wait until
5956 the ripple animation finishes to perform some action.
5957
5958 @event transitionend
5959 Event param: {{node: Object}} detail Contains the animated node.
5960 */
6612 }); 5961 });
6613 /** 5962 /**
6614 * @param {!Function} selectCallback 5963 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus.
6615 * @constructor 5964 *
5965 * @polymerBehavior Polymer.PaperInkyFocusBehavior
6616 */ 5966 */
6617 Polymer.IronSelection = function(selectCallback) { 5967 Polymer.PaperInkyFocusBehaviorImpl = {
6618 this.selection = []; 5968 observers: [
6619 this.selectCallback = selectCallback; 5969 '_focusedChanged(receivedFocusFromKeyboard)'
5970 ],
5971
5972 _focusedChanged: function(receivedFocusFromKeyboard) {
5973 if (receivedFocusFromKeyboard) {
5974 this.ensureRipple();
5975 }
5976 if (this.hasRipple()) {
5977 this._ripple.holdDown = receivedFocusFromKeyboard;
5978 }
5979 },
5980
5981 _createRipple: function() {
5982 var ripple = Polymer.PaperRippleBehavior._createRipple();
5983 ripple.id = 'ink';
5984 ripple.setAttribute('center', '');
5985 ripple.classList.add('circle');
5986 return ripple;
5987 }
6620 }; 5988 };
6621 5989
6622 Polymer.IronSelection.prototype = { 5990 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
6623 5991 Polymer.PaperInkyFocusBehavior = [
6624 /** 5992 Polymer.IronButtonState,
6625 * Retrieves the selected item(s). 5993 Polymer.IronControlState,
6626 * 5994 Polymer.PaperRippleBehavior,
6627 * @method get 5995 Polymer.PaperInkyFocusBehaviorImpl
6628 * @returns Returns the selected item(s). If the multi property is true, 5996 ];
6629 * `get` will return an array, otherwise it will return 5997 Polymer({
6630 * the selected item or undefined if there is no selection. 5998 is: 'paper-icon-button',
6631 */ 5999
6632 get: function() { 6000 hostAttributes: {
6633 return this.multi ? this.selection.slice() : this.selection[0]; 6001 role: 'button',
6634 }, 6002 tabindex: '0'
6635 6003 },
6636 /** 6004
6637 * Clears all the selection except the ones indicated. 6005 behaviors: [
6638 * 6006 Polymer.PaperInkyFocusBehavior
6639 * @method clear 6007 ],
6640 * @param {Array} excludes items to be excluded. 6008
6641 */ 6009 properties: {
6642 clear: function(excludes) { 6010 /**
6643 this.selection.slice().forEach(function(item) { 6011 * The URL of an image for the icon. If the src property is specified,
6644 if (!excludes || excludes.indexOf(item) < 0) { 6012 * the icon property should not be.
6645 this.setItemSelected(item, false); 6013 */
6646 } 6014 src: {
6647 }, this); 6015 type: String
6648 }, 6016 },
6649 6017
6650 /** 6018 /**
6651 * Indicates if a given item is selected. 6019 * Specifies the icon name or index in the set of icons available in
6652 * 6020 * the icon's icon set. If the icon property is specified,
6653 * @method isSelected 6021 * the src property should not be.
6654 * @param {*} item The item whose selection state should be checked. 6022 */
6655 * @returns Returns true if `item` is selected. 6023 icon: {
6656 */ 6024 type: String
6657 isSelected: function(item) { 6025 },
6658 return this.selection.indexOf(item) >= 0; 6026
6659 }, 6027 /**
6660 6028 * Specifies the alternate text for the button, for accessibility.
6661 /** 6029 */
6662 * Sets the selection state for a given item to either selected or deselecte d. 6030 alt: {
6663 * 6031 type: String,
6664 * @method setItemSelected 6032 observer: "_altChanged"
6665 * @param {*} item The item to select. 6033 }
6666 * @param {boolean} isSelected True for selected, false for deselected. 6034 },
6667 */ 6035
6668 setItemSelected: function(item, isSelected) { 6036 _altChanged: function(newValue, oldValue) {
6669 if (item != null) { 6037 var label = this.getAttribute('aria-label');
6670 if (isSelected !== this.isSelected(item)) { 6038
6671 // proceed to update selection only if requested state differs from cu rrent 6039 // Don't stomp over a user-set aria-label.
6672 if (isSelected) { 6040 if (!label || oldValue == label) {
6673 this.selection.push(item); 6041 this.setAttribute('aria-label', newValue);
6674 } else { 6042 }
6675 var i = this.selection.indexOf(item); 6043 }
6676 if (i >= 0) { 6044 });
6677 this.selection.splice(i, 1); 6045 Polymer({
6678 } 6046 is: 'paper-tab',
6047
6048 behaviors: [
6049 Polymer.IronControlState,
6050 Polymer.IronButtonState,
6051 Polymer.PaperRippleBehavior
6052 ],
6053
6054 properties: {
6055
6056 /**
6057 * If true, the tab will forward keyboard clicks (enter/space) to
6058 * the first anchor element found in its descendants
6059 */
6060 link: {
6061 type: Boolean,
6062 value: false,
6063 reflectToAttribute: true
6064 }
6065
6066 },
6067
6068 hostAttributes: {
6069 role: 'tab'
6070 },
6071
6072 listeners: {
6073 down: '_updateNoink',
6074 tap: '_onTap'
6075 },
6076
6077 attached: function() {
6078 this._updateNoink();
6079 },
6080
6081 get _parentNoink () {
6082 var parent = Polymer.dom(this).parentNode;
6083 return !!parent && !!parent.noink;
6084 },
6085
6086 _updateNoink: function() {
6087 this.noink = !!this.noink || !!this._parentNoink;
6088 },
6089
6090 _onTap: function(event) {
6091 if (this.link) {
6092 var anchor = this.queryEffectiveChildren('a');
6093
6094 if (!anchor) {
6095 return;
6679 } 6096 }
6680 if (this.selectCallback) { 6097
6681 this.selectCallback(item, isSelected); 6098 // Don't get stuck in a loop delegating
6099 // the listener from the child anchor
6100 if (event.target === anchor) {
6101 return;
6682 } 6102 }
6683 } 6103
6684 } 6104 anchor.click();
6685 }, 6105 }
6686 6106 }
6687 /** 6107
6688 * Sets the selection state for a given item. If the `multi` property 6108 });
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 */ 6109 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */
7087 Polymer.IronMultiSelectableBehaviorImpl = { 6110 Polymer.IronMultiSelectableBehaviorImpl = {
7088 properties: { 6111 properties: {
7089 6112
7090 /** 6113 /**
7091 * If true, multiple selections are allowed. 6114 * If true, multiple selections are allowed.
7092 */ 6115 */
7093 multi: { 6116 multi: {
7094 type: Boolean, 6117 type: Boolean,
7095 value: false, 6118 value: false,
(...skipping 436 matching lines...) Expand 10 before | Expand all | Expand 10 after
7532 }; 6555 };
7533 6556
7534 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; 6557 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
7535 6558
7536 /** @polymerBehavior Polymer.IronMenuBehavior */ 6559 /** @polymerBehavior Polymer.IronMenuBehavior */
7537 Polymer.IronMenuBehavior = [ 6560 Polymer.IronMenuBehavior = [
7538 Polymer.IronMultiSelectableBehavior, 6561 Polymer.IronMultiSelectableBehavior,
7539 Polymer.IronA11yKeysBehavior, 6562 Polymer.IronA11yKeysBehavior,
7540 Polymer.IronMenuBehaviorImpl 6563 Polymer.IronMenuBehaviorImpl
7541 ]; 6564 ];
6565 /**
6566 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior.
6567 *
6568 * @polymerBehavior Polymer.IronMenubarBehavior
6569 */
6570 Polymer.IronMenubarBehaviorImpl = {
6571
6572 hostAttributes: {
6573 'role': 'menubar'
6574 },
6575
6576 keyBindings: {
6577 'left': '_onLeftKey',
6578 'right': '_onRightKey'
6579 },
6580
6581 _onUpKey: function(event) {
6582 this.focusedItem.click();
6583 event.detail.keyboardEvent.preventDefault();
6584 },
6585
6586 _onDownKey: function(event) {
6587 this.focusedItem.click();
6588 event.detail.keyboardEvent.preventDefault();
6589 },
6590
6591 get _isRTL() {
6592 return window.getComputedStyle(this)['direction'] === 'rtl';
6593 },
6594
6595 _onLeftKey: function(event) {
6596 if (this._isRTL) {
6597 this._focusNext();
6598 } else {
6599 this._focusPrevious();
6600 }
6601 event.detail.keyboardEvent.preventDefault();
6602 },
6603
6604 _onRightKey: function(event) {
6605 if (this._isRTL) {
6606 this._focusPrevious();
6607 } else {
6608 this._focusNext();
6609 }
6610 event.detail.keyboardEvent.preventDefault();
6611 },
6612
6613 _onKeydown: function(event) {
6614 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
6615 return;
6616 }
6617
6618 // all other keys focus the menu item starting with that character
6619 this._focusWithKeyboardEvent(event);
6620 }
6621
6622 };
6623
6624 /** @polymerBehavior Polymer.IronMenubarBehavior */
6625 Polymer.IronMenubarBehavior = [
6626 Polymer.IronMenuBehavior,
6627 Polymer.IronMenubarBehaviorImpl
6628 ];
6629 Polymer({
6630 is: 'paper-tabs',
6631
6632 behaviors: [
6633 Polymer.IronResizableBehavior,
6634 Polymer.IronMenubarBehavior
6635 ],
6636
6637 properties: {
6638 /**
6639 * If true, ink ripple effect is disabled. When this property is changed ,
6640 * all descendant `<paper-tab>` elements have their `noink` property
6641 * changed to the new value as well.
6642 */
6643 noink: {
6644 type: Boolean,
6645 value: false,
6646 observer: '_noinkChanged'
6647 },
6648
6649 /**
6650 * If true, the bottom bar to indicate the selected tab will not be show n.
6651 */
6652 noBar: {
6653 type: Boolean,
6654 value: false
6655 },
6656
6657 /**
6658 * If true, the slide effect for the bottom bar is disabled.
6659 */
6660 noSlide: {
6661 type: Boolean,
6662 value: false
6663 },
6664
6665 /**
6666 * If true, tabs are scrollable and the tab width is based on the label width.
6667 */
6668 scrollable: {
6669 type: Boolean,
6670 value: false
6671 },
6672
6673 /**
6674 * If true, tabs expand to fit their container. This currently only appl ies when
6675 * scrollable is true.
6676 */
6677 fitContainer: {
6678 type: Boolean,
6679 value: false
6680 },
6681
6682 /**
6683 * If true, dragging on the tabs to scroll is disabled.
6684 */
6685 disableDrag: {
6686 type: Boolean,
6687 value: false
6688 },
6689
6690 /**
6691 * If true, scroll buttons (left/right arrow) will be hidden for scrolla ble tabs.
6692 */
6693 hideScrollButtons: {
6694 type: Boolean,
6695 value: false
6696 },
6697
6698 /**
6699 * If true, the tabs are aligned to bottom (the selection bar appears at the top).
6700 */
6701 alignBottom: {
6702 type: Boolean,
6703 value: false
6704 },
6705
6706 selectable: {
6707 type: String,
6708 value: 'paper-tab'
6709 },
6710
6711 /**
6712 * If true, tabs are automatically selected when focused using the
6713 * keyboard.
6714 */
6715 autoselect: {
6716 type: Boolean,
6717 value: false
6718 },
6719
6720 /**
6721 * The delay (in milliseconds) between when the user stops interacting
6722 * with the tabs through the keyboard and when the focused item is
6723 * automatically selected (if `autoselect` is true).
6724 */
6725 autoselectDelay: {
6726 type: Number,
6727 value: 0
6728 },
6729
6730 _step: {
6731 type: Number,
6732 value: 10
6733 },
6734
6735 _holdDelay: {
6736 type: Number,
6737 value: 1
6738 },
6739
6740 _leftHidden: {
6741 type: Boolean,
6742 value: false
6743 },
6744
6745 _rightHidden: {
6746 type: Boolean,
6747 value: false
6748 },
6749
6750 _previousTab: {
6751 type: Object
6752 }
6753 },
6754
6755 hostAttributes: {
6756 role: 'tablist'
6757 },
6758
6759 listeners: {
6760 'iron-resize': '_onTabSizingChanged',
6761 'iron-items-changed': '_onTabSizingChanged',
6762 'iron-select': '_onIronSelect',
6763 'iron-deselect': '_onIronDeselect'
6764 },
6765
6766 keyBindings: {
6767 'left:keyup right:keyup': '_onArrowKeyup'
6768 },
6769
6770 created: function() {
6771 this._holdJob = null;
6772 this._pendingActivationItem = undefined;
6773 this._pendingActivationTimeout = undefined;
6774 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind (this);
6775 this.addEventListener('blur', this._onBlurCapture.bind(this), true);
6776 },
6777
6778 ready: function() {
6779 this.setScrollDirection('y', this.$.tabsContainer);
6780 },
6781
6782 detached: function() {
6783 this._cancelPendingActivation();
6784 },
6785
6786 _noinkChanged: function(noink) {
6787 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab');
6788 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt ribute);
6789 },
6790
6791 _setNoinkAttribute: function(element) {
6792 element.setAttribute('noink', '');
6793 },
6794
6795 _removeNoinkAttribute: function(element) {
6796 element.removeAttribute('noink');
6797 },
6798
6799 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll Buttons) {
6800 if (!scrollable || hideScrollButtons) {
6801 return 'hidden';
6802 }
6803
6804 if (hideThisButton) {
6805 return 'not-visible';
6806 }
6807
6808 return '';
6809 },
6810
6811 _computeTabsContentClass: function(scrollable, fitContainer) {
6812 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '' ) : ' fit-container';
6813 },
6814
6815 _computeSelectionBarClass: function(noBar, alignBottom) {
6816 if (noBar) {
6817 return 'hidden';
6818 } else if (alignBottom) {
6819 return 'align-bottom';
6820 }
6821
6822 return '';
6823 },
6824
6825 // TODO(cdata): Add `track` response back in when gesture lands.
6826
6827 _onTabSizingChanged: function() {
6828 this.debounce('_onTabSizingChanged', function() {
6829 this._scroll();
6830 this._tabChanged(this.selectedItem);
6831 }, 10);
6832 },
6833
6834 _onIronSelect: function(event) {
6835 this._tabChanged(event.detail.item, this._previousTab);
6836 this._previousTab = event.detail.item;
6837 this.cancelDebouncer('tab-changed');
6838 },
6839
6840 _onIronDeselect: function(event) {
6841 this.debounce('tab-changed', function() {
6842 this._tabChanged(null, this._previousTab);
6843 this._previousTab = null;
6844 // See polymer/polymer#1305
6845 }, 1);
6846 },
6847
6848 _activateHandler: function() {
6849 // Cancel item activations scheduled by keyboard events when any other
6850 // action causes an item to be activated (e.g. clicks).
6851 this._cancelPendingActivation();
6852
6853 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
6854 },
6855
6856 /**
6857 * Activates an item after a delay (in milliseconds).
6858 */
6859 _scheduleActivation: function(item, delay) {
6860 this._pendingActivationItem = item;
6861 this._pendingActivationTimeout = this.async(
6862 this._bindDelayedActivationHandler, delay);
6863 },
6864
6865 /**
6866 * Activates the last item given to `_scheduleActivation`.
6867 */
6868 _delayedActivationHandler: function() {
6869 var item = this._pendingActivationItem;
6870 this._pendingActivationItem = undefined;
6871 this._pendingActivationTimeout = undefined;
6872 item.fire(this.activateEvent, null, {
6873 bubbles: true,
6874 cancelable: true
6875 });
6876 },
6877
6878 /**
6879 * Cancels a previously scheduled item activation made with
6880 * `_scheduleActivation`.
6881 */
6882 _cancelPendingActivation: function() {
6883 if (this._pendingActivationTimeout !== undefined) {
6884 this.cancelAsync(this._pendingActivationTimeout);
6885 this._pendingActivationItem = undefined;
6886 this._pendingActivationTimeout = undefined;
6887 }
6888 },
6889
6890 _onArrowKeyup: function(event) {
6891 if (this.autoselect) {
6892 this._scheduleActivation(this.focusedItem, this.autoselectDelay);
6893 }
6894 },
6895
6896 _onBlurCapture: function(event) {
6897 // Cancel a scheduled item activation (if any) when that item is
6898 // blurred.
6899 if (event.target === this._pendingActivationItem) {
6900 this._cancelPendingActivation();
6901 }
6902 },
6903
6904 get _tabContainerScrollSize () {
6905 return Math.max(
6906 0,
6907 this.$.tabsContainer.scrollWidth -
6908 this.$.tabsContainer.offsetWidth
6909 );
6910 },
6911
6912 _scroll: function(e, detail) {
6913 if (!this.scrollable) {
6914 return;
6915 }
6916
6917 var ddx = (detail && -detail.ddx) || 0;
6918 this._affectScroll(ddx);
6919 },
6920
6921 _down: function(e) {
6922 // go one beat async to defeat IronMenuBehavior
6923 // autorefocus-on-no-selection timeout
6924 this.async(function() {
6925 if (this._defaultFocusAsync) {
6926 this.cancelAsync(this._defaultFocusAsync);
6927 this._defaultFocusAsync = null;
6928 }
6929 }, 1);
6930 },
6931
6932 _affectScroll: function(dx) {
6933 this.$.tabsContainer.scrollLeft += dx;
6934
6935 var scrollLeft = this.$.tabsContainer.scrollLeft;
6936
6937 this._leftHidden = scrollLeft === 0;
6938 this._rightHidden = scrollLeft === this._tabContainerScrollSize;
6939 },
6940
6941 _onLeftScrollButtonDown: function() {
6942 this._scrollToLeft();
6943 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel ay);
6944 },
6945
6946 _onRightScrollButtonDown: function() {
6947 this._scrollToRight();
6948 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe lay);
6949 },
6950
6951 _onScrollButtonUp: function() {
6952 clearInterval(this._holdJob);
6953 this._holdJob = null;
6954 },
6955
6956 _scrollToLeft: function() {
6957 this._affectScroll(-this._step);
6958 },
6959
6960 _scrollToRight: function() {
6961 this._affectScroll(this._step);
6962 },
6963
6964 _tabChanged: function(tab, old) {
6965 if (!tab) {
6966 // Remove the bar without animation.
6967 this.$.selectionBar.classList.remove('expand');
6968 this.$.selectionBar.classList.remove('contract');
6969 this._positionBar(0, 0);
6970 return;
6971 }
6972
6973 var r = this.$.tabsContent.getBoundingClientRect();
6974 var w = r.width;
6975 var tabRect = tab.getBoundingClientRect();
6976 var tabOffsetLeft = tabRect.left - r.left;
6977
6978 this._pos = {
6979 width: this._calcPercent(tabRect.width, w),
6980 left: this._calcPercent(tabOffsetLeft, w)
6981 };
6982
6983 if (this.noSlide || old == null) {
6984 // Position the bar without animation.
6985 this.$.selectionBar.classList.remove('expand');
6986 this.$.selectionBar.classList.remove('contract');
6987 this._positionBar(this._pos.width, this._pos.left);
6988 return;
6989 }
6990
6991 var oldRect = old.getBoundingClientRect();
6992 var oldIndex = this.items.indexOf(old);
6993 var index = this.items.indexOf(tab);
6994 var m = 5;
6995
6996 // bar animation: expand
6997 this.$.selectionBar.classList.add('expand');
6998
6999 var moveRight = oldIndex < index;
7000 var isRTL = this._isRTL;
7001 if (isRTL) {
7002 moveRight = !moveRight;
7003 }
7004
7005 if (moveRight) {
7006 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old Rect.left, w) - m,
7007 this._left);
7008 } else {
7009 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab Rect.left, w) - m,
7010 this._calcPercent(tabOffsetLeft, w) + m);
7011 }
7012
7013 if (this.scrollable) {
7014 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft);
7015 }
7016 },
7017
7018 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) {
7019 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft;
7020 if (l < 0) {
7021 this.$.tabsContainer.scrollLeft += l;
7022 } else {
7023 l += (tabWidth - this.$.tabsContainer.offsetWidth);
7024 if (l > 0) {
7025 this.$.tabsContainer.scrollLeft += l;
7026 }
7027 }
7028 },
7029
7030 _calcPercent: function(w, w0) {
7031 return 100 * w / w0;
7032 },
7033
7034 _positionBar: function(width, left) {
7035 width = width || 0;
7036 left = left || 0;
7037
7038 this._width = width;
7039 this._left = left;
7040 this.transform(
7041 'translateX(' + left + '%) scaleX(' + (width / 100) + ')',
7042 this.$.selectionBar);
7043 },
7044
7045 _onBarTransitionEnd: function(e) {
7046 var cl = this.$.selectionBar.classList;
7047 // bar animation: expand -> contract
7048 if (cl.contains('expand')) {
7049 cl.remove('expand');
7050 cl.add('contract');
7051 this._positionBar(this._pos.width, this._pos.left);
7052 // bar animation done
7053 } else if (cl.contains('contract')) {
7054 cl.remove('contract');
7055 }
7056 }
7057 });
7542 (function() { 7058 (function() {
7543 Polymer({ 7059 'use strict';
7544 is: 'paper-menu', 7060
7545 7061 Polymer.IronA11yAnnouncer = Polymer({
7546 behaviors: [ 7062 is: 'iron-a11y-announcer',
7547 Polymer.IronMenuBehavior 7063
7548 ] 7064 properties: {
7065
7066 /**
7067 * The value of mode is used to set the `aria-live` attribute
7068 * for the element that will be announced. Valid values are: `off`,
7069 * `polite` and `assertive`.
7070 */
7071 mode: {
7072 type: String,
7073 value: 'polite'
7074 },
7075
7076 _text: {
7077 type: String,
7078 value: ''
7079 }
7080 },
7081
7082 created: function() {
7083 if (!Polymer.IronA11yAnnouncer.instance) {
7084 Polymer.IronA11yAnnouncer.instance = this;
7085 }
7086
7087 document.body.addEventListener('iron-announce', this._onIronAnnounce.b ind(this));
7088 },
7089
7090 /**
7091 * Cause a text string to be announced by screen readers.
7092 *
7093 * @param {string} text The text that should be announced.
7094 */
7095 announce: function(text) {
7096 this._text = '';
7097 this.async(function() {
7098 this._text = text;
7099 }, 100);
7100 },
7101
7102 _onIronAnnounce: function(event) {
7103 if (event.detail && event.detail.text) {
7104 this.announce(event.detail.text);
7105 }
7106 }
7549 }); 7107 });
7108
7109 Polymer.IronA11yAnnouncer.instance = null;
7110
7111 Polymer.IronA11yAnnouncer.requestAvailability = function() {
7112 if (!Polymer.IronA11yAnnouncer.instance) {
7113 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y -announcer');
7114 }
7115
7116 document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
7117 };
7550 })(); 7118 })();
7551 /** 7119 /**
7120 * Singleton IronMeta instance.
7121 */
7122 Polymer.IronValidatableBehaviorMeta = null;
7123
7124 /**
7125 * `Use Polymer.IronValidatableBehavior` to implement an element that validate s user input.
7126 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo gic to an iron-input.
7127 *
7128 * By default, an `<iron-form>` element validates its fields when the user pre sses the submit button.
7129 * To validate a form imperatively, call the form's `validate()` method, which in turn will
7130 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh avior`, your
7131 * custom element will get a public `validate()`, which
7132 * will return the validity of the element, and a corresponding `invalid` attr ibute,
7133 * which can be used for styling.
7134 *
7135 * To implement the custom validation logic of your element, you must override
7136 * the protected `_getValidity()` method of this behaviour, rather than `valid ate()`.
7137 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si mple-element.html)
7138 * for an example.
7139 *
7140 * ### Accessibility
7141 *
7142 * Changing the `invalid` property, either manually or by calling `validate()` will update the
7143 * `aria-invalid` attribute.
7144 *
7145 * @demo demo/index.html
7146 * @polymerBehavior
7147 */
7148 Polymer.IronValidatableBehavior = {
7149
7150 properties: {
7151
7152 /**
7153 * Name of the validator to use.
7154 */
7155 validator: {
7156 type: String
7157 },
7158
7159 /**
7160 * True if the last call to `validate` is invalid.
7161 */
7162 invalid: {
7163 notify: true,
7164 reflectToAttribute: true,
7165 type: Boolean,
7166 value: false
7167 },
7168
7169 /**
7170 * This property is deprecated and should not be used. Use the global
7171 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead .
7172 */
7173 _validatorMeta: {
7174 type: Object
7175 },
7176
7177 /**
7178 * Namespace for this validator. This property is deprecated and should
7179 * not be used. For all intents and purposes, please consider it a
7180 * read-only, config-time property.
7181 */
7182 validatorType: {
7183 type: String,
7184 value: 'validator'
7185 },
7186
7187 _validator: {
7188 type: Object,
7189 computed: '__computeValidator(validator)'
7190 }
7191 },
7192
7193 observers: [
7194 '_invalidChanged(invalid)'
7195 ],
7196
7197 registered: function() {
7198 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat or'});
7199 },
7200
7201 _invalidChanged: function() {
7202 if (this.invalid) {
7203 this.setAttribute('aria-invalid', 'true');
7204 } else {
7205 this.removeAttribute('aria-invalid');
7206 }
7207 },
7208
7209 /**
7210 * @return {boolean} True if the validator `validator` exists.
7211 */
7212 hasValidator: function() {
7213 return this._validator != null;
7214 },
7215
7216 /**
7217 * Returns true if the `value` is valid, and updates `invalid`. If you want
7218 * your element to have custom validation logic, do not override this method ;
7219 * override `_getValidity(value)` instead.
7220
7221 * @param {Object} value The value to be validated. By default, it is passed
7222 * to the validator's `validate()` function, if a validator is set.
7223 * @return {boolean} True if `value` is valid.
7224 */
7225 validate: function(value) {
7226 this.invalid = !this._getValidity(value);
7227 return !this.invalid;
7228 },
7229
7230 /**
7231 * Returns true if `value` is valid. By default, it is passed
7232 * to the validator's `validate()` function, if a validator is set. You
7233 * should override this method if you want to implement custom validity
7234 * logic for your element.
7235 *
7236 * @param {Object} value The value to be validated.
7237 * @return {boolean} True if `value` is valid.
7238 */
7239
7240 _getValidity: function(value) {
7241 if (this.hasValidator()) {
7242 return this._validator.validate(value);
7243 }
7244 return true;
7245 },
7246
7247 __computeValidator: function() {
7248 return Polymer.IronValidatableBehaviorMeta &&
7249 Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
7250 }
7251 };
7252 /*
7253 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal idatorBehavior`
7254 to `<input>`.
7255
7256 ### Two-way binding
7257
7258 By default you can only get notified of changes to an `input`'s `value` due to u ser input:
7259
7260 <input value="{{myValue::input}}">
7261
7262 `iron-input` adds the `bind-value` property that mirrors the `value` property, a nd can be used
7263 for two-way data binding. `bind-value` will notify if it is changed either by us er input or by script.
7264
7265 <input is="iron-input" bind-value="{{myValue}}">
7266
7267 ### Custom validators
7268
7269 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit h `<iron-input>`.
7270
7271 <input is="iron-input" validator="my-custom-validator">
7272
7273 ### Stopping invalid input
7274
7275 It may be desirable to only allow users to enter certain characters. You can use the
7276 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
7277 is separate from validation, and `allowed-pattern` does not affect how the input is validated.
7278
7279 \x3c!-- only allow characters that match [0-9] --\x3e
7280 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
7281
7282 @hero hero.svg
7283 @demo demo/index.html
7284 */
7285
7286 Polymer({
7287
7288 is: 'iron-input',
7289
7290 extends: 'input',
7291
7292 behaviors: [
7293 Polymer.IronValidatableBehavior
7294 ],
7295
7296 properties: {
7297
7298 /**
7299 * Use this property instead of `value` for two-way data binding.
7300 */
7301 bindValue: {
7302 observer: '_bindValueChanged',
7303 type: String
7304 },
7305
7306 /**
7307 * Set to true to prevent the user from entering invalid input. If `allowe dPattern` is set,
7308 * any character typed by the user will be matched against that pattern, a nd rejected if it's not a match.
7309 * Pasted input will have each character checked individually; if any char acter
7310 * doesn't match `allowedPattern`, the entire pasted string will be reject ed.
7311 * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
7312 */
7313 preventInvalidInput: {
7314 type: Boolean
7315 },
7316
7317 /**
7318 * Regular expression that list the characters allowed as input.
7319 * This pattern represents the allowed characters for the field; as the us er inputs text,
7320 * each individual character will be checked against the pattern (rather t han checking
7321 * the entire value as a whole). The recommended format should be a list o f allowed characters;
7322 * for example, `[a-zA-Z0-9.+-!;:]`
7323 */
7324 allowedPattern: {
7325 type: String,
7326 observer: "_allowedPatternChanged"
7327 },
7328
7329 _previousValidInput: {
7330 type: String,
7331 value: ''
7332 },
7333
7334 _patternAlreadyChecked: {
7335 type: Boolean,
7336 value: false
7337 }
7338
7339 },
7340
7341 listeners: {
7342 'input': '_onInput',
7343 'keypress': '_onKeypress'
7344 },
7345
7346 /** @suppress {checkTypes} */
7347 registered: function() {
7348 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E).
7349 if (!this._canDispatchEventOnDisabled()) {
7350 this._origDispatchEvent = this.dispatchEvent;
7351 this.dispatchEvent = this._dispatchEventFirefoxIE;
7352 }
7353 },
7354
7355 created: function() {
7356 Polymer.IronA11yAnnouncer.requestAvailability();
7357 },
7358
7359 _canDispatchEventOnDisabled: function() {
7360 var input = document.createElement('input');
7361 var canDispatch = false;
7362 input.disabled = true;
7363
7364 input.addEventListener('feature-check-dispatch-event', function() {
7365 canDispatch = true;
7366 });
7367
7368 try {
7369 input.dispatchEvent(new Event('feature-check-dispatch-event'));
7370 } catch(e) {}
7371
7372 return canDispatch;
7373 },
7374
7375 _dispatchEventFirefoxIE: function() {
7376 // Due to Firefox bug, events fired on disabled form controls can throw
7377 // errors; furthermore, neither IE nor Firefox will actually dispatch
7378 // events from disabled form controls; as such, we toggle disable around
7379 // the dispatch to allow notifying properties to notify
7380 // See issue #47 for details
7381 var disabled = this.disabled;
7382 this.disabled = false;
7383 this._origDispatchEvent.apply(this, arguments);
7384 this.disabled = disabled;
7385 },
7386
7387 get _patternRegExp() {
7388 var pattern;
7389 if (this.allowedPattern) {
7390 pattern = new RegExp(this.allowedPattern);
7391 } else {
7392 switch (this.type) {
7393 case 'number':
7394 pattern = /[0-9.,e-]/;
7395 break;
7396 }
7397 }
7398 return pattern;
7399 },
7400
7401 ready: function() {
7402 this.bindValue = this.value;
7403 },
7404
7405 /**
7406 * @suppress {checkTypes}
7407 */
7408 _bindValueChanged: function() {
7409 if (this.value !== this.bindValue) {
7410 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
7411 }
7412 // manually notify because we don't want to notify until after setting val ue
7413 this.fire('bind-value-changed', {value: this.bindValue});
7414 },
7415
7416 _allowedPatternChanged: function() {
7417 // Force to prevent invalid input when an `allowed-pattern` is set
7418 this.preventInvalidInput = this.allowedPattern ? true : false;
7419 },
7420
7421 _onInput: function() {
7422 // Need to validate each of the characters pasted if they haven't
7423 // been validated inside `_onKeypress` already.
7424 if (this.preventInvalidInput && !this._patternAlreadyChecked) {
7425 var valid = this._checkPatternValidity();
7426 if (!valid) {
7427 this._announceInvalidCharacter('Invalid string of characters not enter ed.');
7428 this.value = this._previousValidInput;
7429 }
7430 }
7431
7432 this.bindValue = this.value;
7433 this._previousValidInput = this.value;
7434 this._patternAlreadyChecked = false;
7435 },
7436
7437 _isPrintable: function(event) {
7438 // What a control/printable character is varies wildly based on the browse r.
7439 // - most control characters (arrows, backspace) do not send a `keypress` event
7440 // in Chrome, but the *do* on Firefox
7441 // - in Firefox, when they do send a `keypress` event, control chars have
7442 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
7443 // - printable characters always send a keypress event.
7444 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
7445 // always matches the charCode.
7446 // None of this makes any sense.
7447
7448 // For these keys, ASCII code == browser keycode.
7449 var anyNonPrintable =
7450 (event.keyCode == 8) || // backspace
7451 (event.keyCode == 9) || // tab
7452 (event.keyCode == 13) || // enter
7453 (event.keyCode == 27); // escape
7454
7455 // For these keys, make sure it's a browser keycode and not an ASCII code.
7456 var mozNonPrintable =
7457 (event.keyCode == 19) || // pause
7458 (event.keyCode == 20) || // caps lock
7459 (event.keyCode == 45) || // insert
7460 (event.keyCode == 46) || // delete
7461 (event.keyCode == 144) || // num lock
7462 (event.keyCode == 145) || // scroll lock
7463 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows
7464 (event.keyCode > 111 && event.keyCode < 124); // fn keys
7465
7466 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
7467 },
7468
7469 _onKeypress: function(event) {
7470 if (!this.preventInvalidInput && this.type !== 'number') {
7471 return;
7472 }
7473 var regexp = this._patternRegExp;
7474 if (!regexp) {
7475 return;
7476 }
7477
7478 // Handle special keys and backspace
7479 if (event.metaKey || event.ctrlKey || event.altKey)
7480 return;
7481
7482 // Check the pattern either here or in `_onInput`, but not in both.
7483 this._patternAlreadyChecked = true;
7484
7485 var thisChar = String.fromCharCode(event.charCode);
7486 if (this._isPrintable(event) && !regexp.test(thisChar)) {
7487 event.preventDefault();
7488 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.');
7489 }
7490 },
7491
7492 _checkPatternValidity: function() {
7493 var regexp = this._patternRegExp;
7494 if (!regexp) {
7495 return true;
7496 }
7497 for (var i = 0; i < this.value.length; i++) {
7498 if (!regexp.test(this.value[i])) {
7499 return false;
7500 }
7501 }
7502 return true;
7503 },
7504
7505 /**
7506 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first,
7507 * then any constraints.
7508 * @return {boolean} True if the value is valid.
7509 */
7510 validate: function() {
7511 // First, check what the browser thinks. Some inputs (like type=number)
7512 // behave weirdly and will set the value to "" if something invalid is
7513 // entered, but will set the validity correctly.
7514 var valid = this.checkValidity();
7515
7516 // Only do extra checking if the browser thought this was valid.
7517 if (valid) {
7518 // Empty, required input is invalid
7519 if (this.required && this.value === '') {
7520 valid = false;
7521 } else if (this.hasValidator()) {
7522 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value );
7523 }
7524 }
7525
7526 this.invalid = !valid;
7527 this.fire('iron-input-validate');
7528 return valid;
7529 },
7530
7531 _announceInvalidCharacter: function(message) {
7532 this.fire('iron-announce', { text: message });
7533 }
7534 });
7535
7536 /*
7537 The `iron-input-validate` event is fired whenever `validate()` is called.
7538 @event iron-input-validate
7539 */
7540 Polymer({
7541 is: 'paper-input-container',
7542
7543 properties: {
7544 /**
7545 * Set to true to disable the floating label. The label disappears when th e input value is
7546 * not null.
7547 */
7548 noLabelFloat: {
7549 type: Boolean,
7550 value: false
7551 },
7552
7553 /**
7554 * Set to true to always float the floating label.
7555 */
7556 alwaysFloatLabel: {
7557 type: Boolean,
7558 value: false
7559 },
7560
7561 /**
7562 * The attribute to listen for value changes on.
7563 */
7564 attrForValue: {
7565 type: String,
7566 value: 'bind-value'
7567 },
7568
7569 /**
7570 * Set to true to auto-validate the input value when it changes.
7571 */
7572 autoValidate: {
7573 type: Boolean,
7574 value: false
7575 },
7576
7577 /**
7578 * True if the input is invalid. This property is set automatically when t he input value
7579 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
7580 */
7581 invalid: {
7582 observer: '_invalidChanged',
7583 type: Boolean,
7584 value: false
7585 },
7586
7587 /**
7588 * True if the input has focus.
7589 */
7590 focused: {
7591 readOnly: true,
7592 type: Boolean,
7593 value: false,
7594 notify: true
7595 },
7596
7597 _addons: {
7598 type: Array
7599 // do not set a default value here intentionally - it will be initialize d lazily when a
7600 // distributed child is attached, which may occur before configuration f or this element
7601 // in polyfill.
7602 },
7603
7604 _inputHasContent: {
7605 type: Boolean,
7606 value: false
7607 },
7608
7609 _inputSelector: {
7610 type: String,
7611 value: 'input,textarea,.paper-input-input'
7612 },
7613
7614 _boundOnFocus: {
7615 type: Function,
7616 value: function() {
7617 return this._onFocus.bind(this);
7618 }
7619 },
7620
7621 _boundOnBlur: {
7622 type: Function,
7623 value: function() {
7624 return this._onBlur.bind(this);
7625 }
7626 },
7627
7628 _boundOnInput: {
7629 type: Function,
7630 value: function() {
7631 return this._onInput.bind(this);
7632 }
7633 },
7634
7635 _boundValueChanged: {
7636 type: Function,
7637 value: function() {
7638 return this._onValueChanged.bind(this);
7639 }
7640 }
7641 },
7642
7643 listeners: {
7644 'addon-attached': '_onAddonAttached',
7645 'iron-input-validate': '_onIronInputValidate'
7646 },
7647
7648 get _valueChangedEvent() {
7649 return this.attrForValue + '-changed';
7650 },
7651
7652 get _propertyForValue() {
7653 return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
7654 },
7655
7656 get _inputElement() {
7657 return Polymer.dom(this).querySelector(this._inputSelector);
7658 },
7659
7660 get _inputElementValue() {
7661 return this._inputElement[this._propertyForValue] || this._inputElement.va lue;
7662 },
7663
7664 ready: function() {
7665 if (!this._addons) {
7666 this._addons = [];
7667 }
7668 this.addEventListener('focus', this._boundOnFocus, true);
7669 this.addEventListener('blur', this._boundOnBlur, true);
7670 },
7671
7672 attached: function() {
7673 if (this.attrForValue) {
7674 this._inputElement.addEventListener(this._valueChangedEvent, this._bound ValueChanged);
7675 } else {
7676 this.addEventListener('input', this._onInput);
7677 }
7678
7679 // Only validate when attached if the input already has a value.
7680 if (this._inputElementValue != '') {
7681 this._handleValueAndAutoValidate(this._inputElement);
7682 } else {
7683 this._handleValue(this._inputElement);
7684 }
7685 },
7686
7687 _onAddonAttached: function(event) {
7688 if (!this._addons) {
7689 this._addons = [];
7690 }
7691 var target = event.target;
7692 if (this._addons.indexOf(target) === -1) {
7693 this._addons.push(target);
7694 if (this.isAttached) {
7695 this._handleValue(this._inputElement);
7696 }
7697 }
7698 },
7699
7700 _onFocus: function() {
7701 this._setFocused(true);
7702 },
7703
7704 _onBlur: function() {
7705 this._setFocused(false);
7706 this._handleValueAndAutoValidate(this._inputElement);
7707 },
7708
7709 _onInput: function(event) {
7710 this._handleValueAndAutoValidate(event.target);
7711 },
7712
7713 _onValueChanged: function(event) {
7714 this._handleValueAndAutoValidate(event.target);
7715 },
7716
7717 _handleValue: function(inputElement) {
7718 var value = this._inputElementValue;
7719
7720 // type="number" hack needed because this.value is empty until it's valid
7721 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme nt.checkValidity())) {
7722 this._inputHasContent = true;
7723 } else {
7724 this._inputHasContent = false;
7725 }
7726
7727 this.updateAddons({
7728 inputElement: inputElement,
7729 value: value,
7730 invalid: this.invalid
7731 });
7732 },
7733
7734 _handleValueAndAutoValidate: function(inputElement) {
7735 if (this.autoValidate) {
7736 var valid;
7737 if (inputElement.validate) {
7738 valid = inputElement.validate(this._inputElementValue);
7739 } else {
7740 valid = inputElement.checkValidity();
7741 }
7742 this.invalid = !valid;
7743 }
7744
7745 // Call this last to notify the add-ons.
7746 this._handleValue(inputElement);
7747 },
7748
7749 _onIronInputValidate: function(event) {
7750 this.invalid = this._inputElement.invalid;
7751 },
7752
7753 _invalidChanged: function() {
7754 if (this._addons) {
7755 this.updateAddons({invalid: this.invalid});
7756 }
7757 },
7758
7759 /**
7760 * Call this to update the state of add-ons.
7761 * @param {Object} state Add-on state.
7762 */
7763 updateAddons: function(state) {
7764 for (var addon, index = 0; addon = this._addons[index]; index++) {
7765 addon.update(state);
7766 }
7767 },
7768
7769 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
7770 var cls = 'input-content';
7771 if (!noLabelFloat) {
7772 var label = this.querySelector('label');
7773
7774 if (alwaysFloatLabel || _inputHasContent) {
7775 cls += ' label-is-floating';
7776 // If the label is floating, ignore any offsets that may have been
7777 // applied from a prefix element.
7778 this.$.labelAndInputContainer.style.position = 'static';
7779
7780 if (invalid) {
7781 cls += ' is-invalid';
7782 } else if (focused) {
7783 cls += " label-is-highlighted";
7784 }
7785 } else {
7786 // When the label is not floating, it should overlap the input element .
7787 if (label) {
7788 this.$.labelAndInputContainer.style.position = 'relative';
7789 }
7790 }
7791 } else {
7792 if (_inputHasContent) {
7793 cls += ' label-is-hidden';
7794 }
7795 }
7796 return cls;
7797 },
7798
7799 _computeUnderlineClass: function(focused, invalid) {
7800 var cls = 'underline';
7801 if (invalid) {
7802 cls += ' is-invalid';
7803 } else if (focused) {
7804 cls += ' is-highlighted'
7805 }
7806 return cls;
7807 },
7808
7809 _computeAddOnContentClass: function(focused, invalid) {
7810 var cls = 'add-on-content';
7811 if (invalid) {
7812 cls += ' is-invalid';
7813 } else if (focused) {
7814 cls += ' is-highlighted'
7815 }
7816 return cls;
7817 }
7818 });
7819 /** @polymerBehavior */
7820 Polymer.PaperSpinnerBehavior = {
7821
7822 listeners: {
7823 'animationend': '__reset',
7824 'webkitAnimationEnd': '__reset'
7825 },
7826
7827 properties: {
7828 /**
7829 * Displays the spinner.
7830 */
7831 active: {
7832 type: Boolean,
7833 value: false,
7834 reflectToAttribute: true,
7835 observer: '__activeChanged'
7836 },
7837
7838 /**
7839 * Alternative text content for accessibility support.
7840 * If alt is present, it will add an aria-label whose content matches alt when active.
7841 * If alt is not present, it will default to 'loading' as the alt value.
7842 */
7843 alt: {
7844 type: String,
7845 value: 'loading',
7846 observer: '__altChanged'
7847 },
7848
7849 __coolingDown: {
7850 type: Boolean,
7851 value: false
7852 }
7853 },
7854
7855 __computeContainerClasses: function(active, coolingDown) {
7856 return [
7857 active || coolingDown ? 'active' : '',
7858 coolingDown ? 'cooldown' : ''
7859 ].join(' ');
7860 },
7861
7862 __activeChanged: function(active, old) {
7863 this.__setAriaHidden(!active);
7864 this.__coolingDown = !active && old;
7865 },
7866
7867 __altChanged: function(alt) {
7868 // user-provided `aria-label` takes precedence over prototype default
7869 if (alt === this.getPropertyInfo('alt').value) {
7870 this.alt = this.getAttribute('aria-label') || alt;
7871 } else {
7872 this.__setAriaHidden(alt==='');
7873 this.setAttribute('aria-label', alt);
7874 }
7875 },
7876
7877 __setAriaHidden: function(hidden) {
7878 var attr = 'aria-hidden';
7879 if (hidden) {
7880 this.setAttribute(attr, 'true');
7881 } else {
7882 this.removeAttribute(attr);
7883 }
7884 },
7885
7886 __reset: function() {
7887 this.active = false;
7888 this.__coolingDown = false;
7889 }
7890 };
7891 Polymer({
7892 is: 'paper-spinner-lite',
7893
7894 behaviors: [
7895 Polymer.PaperSpinnerBehavior
7896 ]
7897 });
7898 // Copyright 2016 The Chromium Authors. All rights reserved.
7899 // Use of this source code is governed by a BSD-style license that can be
7900 // found in the LICENSE file.
7901
7902 /**
7903 * Implements an incremental search field which can be shown and hidden.
7904 * Canonical implementation is <cr-search-field>.
7905 * @polymerBehavior
7906 */
7907 var CrSearchFieldBehavior = {
7908 properties: {
7909 label: {
7910 type: String,
7911 value: '',
7912 },
7913
7914 clearLabel: {
7915 type: String,
7916 value: '',
7917 },
7918
7919 showingSearch: {
7920 type: Boolean,
7921 value: false,
7922 notify: true,
7923 observer: 'showingSearchChanged_',
7924 reflectToAttribute: true
7925 },
7926
7927 /** @private */
7928 lastValue_: {
7929 type: String,
7930 value: '',
7931 },
7932 },
7933
7934 /**
7935 * @abstract
7936 * @return {!HTMLInputElement} The input field element the behavior should
7937 * use.
7938 */
7939 getSearchInput: function() {},
7940
7941 /**
7942 * @return {string} The value of the search field.
7943 */
7944 getValue: function() {
7945 return this.getSearchInput().value;
7946 },
7947
7948 /**
7949 * Sets the value of the search field.
7950 * @param {string} value
7951 */
7952 setValue: function(value) {
7953 // Use bindValue when setting the input value so that changes propagate
7954 // correctly.
7955 this.getSearchInput().bindValue = value;
7956 this.onValueChanged_(value);
7957 },
7958
7959 showAndFocus: function() {
7960 this.showingSearch = true;
7961 this.focus_();
7962 },
7963
7964 /** @private */
7965 focus_: function() {
7966 this.getSearchInput().focus();
7967 },
7968
7969 onSearchTermSearch: function() {
7970 this.onValueChanged_(this.getValue());
7971 },
7972
7973 /**
7974 * Updates the internal state of the search field based on a change that has
7975 * already happened.
7976 * @param {string} newValue
7977 * @private
7978 */
7979 onValueChanged_: function(newValue) {
7980 if (newValue == this.lastValue_)
7981 return;
7982
7983 this.fire('search-changed', newValue);
7984 this.lastValue_ = newValue;
7985 },
7986
7987 onSearchTermKeydown: function(e) {
7988 if (e.key == 'Escape')
7989 this.showingSearch = false;
7990 },
7991
7992 /** @private */
7993 showingSearchChanged_: function() {
7994 if (this.showingSearch) {
7995 this.focus_();
7996 return;
7997 }
7998
7999 this.setValue('');
8000 this.getSearchInput().blur();
8001 }
8002 };
8003 // Copyright 2016 The Chromium Authors. All rights reserved.
8004 // Use of this source code is governed by a BSD-style license that can be
8005 // found in the LICENSE file.
8006
8007 // TODO(tsergeant): Add tests for cr-toolbar-search-field.
8008 Polymer({
8009 is: 'cr-toolbar-search-field',
8010
8011 behaviors: [CrSearchFieldBehavior],
8012
8013 properties: {
8014 narrow: {
8015 type: Boolean,
8016 reflectToAttribute: true,
8017 },
8018
8019 // Prompt text to display in the search field.
8020 label: String,
8021
8022 // Tooltip to display on the clear search button.
8023 clearLabel: String,
8024
8025 // When true, show a loading spinner to indicate that the backend is
8026 // processing the search. Will only show if the search field is open.
8027 spinnerActive: {
8028 type: Boolean,
8029 reflectToAttribute: true
8030 },
8031
8032 /** @private */
8033 hasSearchText_: Boolean,
8034 },
8035
8036 listeners: {
8037 'tap': 'showSearch_',
8038 'searchInput.bind-value-changed': 'onBindValueChanged_',
8039 },
8040
8041 /** @return {!HTMLInputElement} */
8042 getSearchInput: function() {
8043 return this.$.searchInput;
8044 },
8045
8046 /** @return {boolean} */
8047 isSearchFocused: function() {
8048 return this.$.searchTerm.focused;
8049 },
8050
8051 /**
8052 * @param {boolean} narrow
8053 * @return {number}
8054 * @private
8055 */
8056 computeIconTabIndex_: function(narrow) {
8057 return narrow ? 0 : -1;
8058 },
8059
8060 /**
8061 * @param {boolean} spinnerActive
8062 * @param {boolean} showingSearch
8063 * @return {boolean}
8064 * @private
8065 */
8066 isSpinnerShown_: function(spinnerActive, showingSearch) {
8067 return spinnerActive && showingSearch;
8068 },
8069
8070 /** @private */
8071 onInputBlur_: function() {
8072 if (!this.hasSearchText_)
8073 this.showingSearch = false;
8074 },
8075
8076 /**
8077 * Update the state of the search field whenever the underlying input value
8078 * changes. Unlike onsearch or onkeypress, this is reliably called immediately
8079 * after any change, whether the result of user input or JS modification.
8080 * @private
8081 */
8082 onBindValueChanged_: function() {
8083 var newValue = this.$.searchInput.bindValue;
8084 this.hasSearchText_ = newValue != '';
8085 if (newValue != '')
8086 this.showingSearch = true;
8087 },
8088
8089 /**
8090 * @param {Event} e
8091 * @private
8092 */
8093 showSearch_: function(e) {
8094 if (e.target != this.$.clearSearch)
8095 this.showingSearch = true;
8096 },
8097
8098 /**
8099 * @param {Event} e
8100 * @private
8101 */
8102 hideSearch_: function(e) {
8103 this.showingSearch = false;
8104 e.stopPropagation();
8105 }
8106 });
8107 // Copyright 2016 The Chromium Authors. All rights reserved.
8108 // Use of this source code is governed by a BSD-style license that can be
8109 // found in the LICENSE file.
8110
8111 Polymer({
8112 is: 'cr-toolbar',
8113
8114 properties: {
8115 // Name to display in the toolbar, in titlecase.
8116 pageName: String,
8117
8118 // Prompt text to display in the search field.
8119 searchPrompt: String,
8120
8121 // Tooltip to display on the clear search button.
8122 clearLabel: String,
8123
8124 // Value is proxied through to cr-toolbar-search-field. When true,
8125 // the search field will show a processing spinner.
8126 spinnerActive: Boolean,
8127
8128 // Controls whether the menu button is shown at the start of the menu.
8129 showMenu: {
8130 type: Boolean,
8131 reflectToAttribute: true,
8132 value: true
8133 },
8134
8135 /** @private */
8136 narrow_: {
8137 type: Boolean,
8138 reflectToAttribute: true
8139 },
8140
8141 /** @private */
8142 showingSearch_: {
8143 type: Boolean,
8144 reflectToAttribute: true,
8145 },
8146 },
8147
8148 /** @return {!CrToolbarSearchFieldElement} */
8149 getSearchField: function() {
8150 return this.$.search;
8151 },
8152
8153 /** @private */
8154 onMenuTap_: function(e) {
8155 this.fire('cr-menu-tap');
8156 }
8157 });
8158 // Copyright 2015 The Chromium Authors. All rights reserved.
8159 // Use of this source code is governed by a BSD-style license that can be
8160 // found in the LICENSE file.
8161
8162 Polymer({
8163 is: 'history-toolbar',
8164 properties: {
8165 // Number of history items currently selected.
8166 count: {
8167 type: Number,
8168 value: 0,
8169 observer: 'changeToolbarView_'
8170 },
8171
8172 // True if 1 or more history items are selected. When this value changes
8173 // the background colour changes.
8174 itemsSelected_: {
8175 type: Boolean,
8176 value: false,
8177 reflectToAttribute: true
8178 },
8179
8180 // The most recent term entered in the search field. Updated incrementally
8181 // as the user types.
8182 searchTerm: {
8183 type: String,
8184 notify: true,
8185 },
8186
8187 // True if the backend is processing and a spinner should be shown in the
8188 // toolbar.
8189 spinnerActive: {
8190 type: Boolean,
8191 value: false
8192 },
8193
8194 hasDrawer: {
8195 type: Boolean,
8196 observer: 'hasDrawerChanged_',
8197 reflectToAttribute: true,
8198 },
8199
8200 // Whether domain-grouped history is enabled.
8201 isGroupedMode: {
8202 type: Boolean,
8203 reflectToAttribute: true,
8204 },
8205
8206 // The period to search over. Matches BrowsingHistoryHandler::Range.
8207 groupedRange: {
8208 type: Number,
8209 value: 0,
8210 reflectToAttribute: true,
8211 notify: true
8212 },
8213
8214 // The start time of the query range.
8215 queryStartTime: String,
8216
8217 // The end time of the query range.
8218 queryEndTime: String,
8219 },
8220
8221 /**
8222 * Changes the toolbar background color depending on whether any history items
8223 * are currently selected.
8224 * @private
8225 */
8226 changeToolbarView_: function() {
8227 this.itemsSelected_ = this.count > 0;
8228 },
8229
8230 /**
8231 * When changing the search term externally, update the search field to
8232 * reflect the new search term.
8233 * @param {string} search
8234 */
8235 setSearchTerm: function(search) {
8236 if (this.searchTerm == search)
8237 return;
8238
8239 this.searchTerm = search;
8240 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
8241 .getSearchField();
8242 searchField.showAndFocus();
8243 searchField.setValue(search);
8244 },
8245
8246 /**
8247 * @param {!CustomEvent} event
8248 * @private
8249 */
8250 onSearchChanged_: function(event) {
8251 this.searchTerm = /** @type {string} */ (event.detail);
8252 },
8253
8254 onClearSelectionTap_: function() {
8255 this.fire('unselect-all');
8256 },
8257
8258 onDeleteTap_: function() {
8259 this.fire('delete-selected');
8260 },
8261
8262 get searchBar() {
8263 return this.$['main-toolbar'].getSearchField();
8264 },
8265
8266 showSearchField: function() {
8267 /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
8268 .getSearchField()
8269 .showAndFocus();
8270 },
8271
8272 /**
8273 * If the user is a supervised user the delete button is not shown.
8274 * @private
8275 */
8276 deletingAllowed_: function() {
8277 return loadTimeData.getBoolean('allowDeletingHistory');
8278 },
8279
8280 numberOfItemsSelected_: function(count) {
8281 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : '';
8282 },
8283
8284 getHistoryInterval_: function(queryStartTime, queryEndTime) {
8285 // TODO(calamity): Fix the format of these dates.
8286 return loadTimeData.getStringF(
8287 'historyInterval', queryStartTime, queryEndTime);
8288 },
8289
8290 /** @private */
8291 hasDrawerChanged_: function() {
8292 this.updateStyles();
8293 },
8294 });
8295 // Copyright 2016 The Chromium Authors. All rights reserved.
8296 // Use of this source code is governed by a BSD-style license that can be
8297 // found in the LICENSE file.
8298
8299 /**
8300 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the
8301 * dialog is closed via close(), a 'close' event is fired. If the dialog is
8302 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event.
8303 * Additionally clients can inspect the dialog's |returnValue| property inside
8304 * the 'close' event listener to determine whether it was canceled or just
8305 * closed, where a truthy value means success, and a falsy value means it was
8306 * canceled.
8307 */
8308 Polymer({
8309 is: 'cr-dialog',
8310 extends: 'dialog',
8311
8312 cancel: function() {
8313 this.fire('cancel');
8314 HTMLDialogElement.prototype.close.call(this, '');
8315 },
8316
8317 /**
8318 * @param {string=} opt_returnValue
8319 * @override
8320 */
8321 close: function(opt_returnValue) {
8322 HTMLDialogElement.prototype.close.call(this, 'success');
8323 },
8324
8325 /** @return {!PaperIconButtonElement} */
8326 getCloseButton: function() {
8327 return this.$.close;
8328 },
8329 });
8330 /**
7552 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and 8331 `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. 8332 optionally centers it in the window or another element.
7554 8333
7555 The element will only be sized and/or positioned if it has not already been size d and/or positioned 8334 The element will only be sized and/or positioned if it has not already been size d and/or positioned
7556 by CSS. 8335 by CSS.
7557 8336
7558 CSS properties | Action 8337 CSS properties | Action
7559 -----------------------------|------------------------------------------- 8338 -----------------------------|-------------------------------------------
7560 `position` set | Element is not centered horizontally or verticall y 8339 `position` set | Element is not centered horizontally or verticall y
7561 `top` or `bottom` set | Element is not vertically centered 8340 `top` or `bottom` set | Element is not vertically centered
(...skipping 2733 matching lines...) Expand 10 before | Expand all | Expand 10 after
10295 height: height + 'px', 11074 height: height + 'px',
10296 transform: 'translateY(0)' 11075 transform: 'translateY(0)'
10297 }, { 11076 }, {
10298 height: height / 2 + 'px', 11077 height: height / 2 + 'px',
10299 transform: 'translateY(-20px)' 11078 transform: 'translateY(-20px)'
10300 }], this.timingFromConfig(config)); 11079 }], this.timingFromConfig(config));
10301 11080
10302 return this._effect; 11081 return this._effect;
10303 } 11082 }
10304 }); 11083 });
10305 (function() { 11084 // Copyright 2016 The Chromium Authors. All rights reserved.
10306 'use strict'; 11085 // Use of this source code is governed by a BSD-style license that can be
10307 11086 // found in the LICENSE file.
10308 var config = { 11087
10309 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', 11088 /** Same as paper-menu-button's custom easing cubic-bezier param. */
10310 MAX_ANIMATION_TIME_MS: 400 11089 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)';
10311 }; 11090
10312 11091 Polymer({
10313 var PaperMenuButton = Polymer({ 11092 is: 'cr-shared-menu',
10314 is: 'paper-menu-button', 11093
10315 11094 behaviors: [Polymer.IronA11yKeysBehavior],
10316 /** 11095
10317 * Fired when the dropdown opens. 11096 properties: {
10318 * 11097 menuOpen: {
10319 * @event paper-dropdown-open 11098 type: Boolean,
10320 */ 11099 observer: 'menuOpenChanged_',
10321 11100 value: false,
10322 /** 11101 },
10323 * Fired when the dropdown closes. 11102
10324 * 11103 /**
10325 * @event paper-dropdown-close 11104 * The contextual item that this menu was clicked for.
10326 */ 11105 * e.g. the data used to render an item in an <iron-list> or <dom-repeat>
10327 11106 * @type {?Object}
10328 behaviors: [ 11107 */
10329 Polymer.IronA11yKeysBehavior, 11108 itemData: {
10330 Polymer.IronControlState 11109 type: Object,
10331 ], 11110 value: null,
10332 11111 },
10333 properties: { 11112
10334 /** 11113 /** @override */
10335 * True if the content is currently displayed. 11114 keyEventTarget: {
10336 */ 11115 type: Object,
10337 opened: { 11116 value: function() {
10338 type: Boolean, 11117 return this.$.menu;
10339 value: false, 11118 }
10340 notify: true, 11119 },
10341 observer: '_openedChanged' 11120
10342 }, 11121 openAnimationConfig: {
10343 11122 type: Object,
10344 /** 11123 value: function() {
10345 * The orientation against which to align the menu dropdown 11124 return [{
10346 * horizontally relative to the dropdown trigger. 11125 name: 'fade-in-animation',
10347 */ 11126 timing: {
10348 horizontalAlign: { 11127 delay: 50,
10349 type: String, 11128 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 } 11129 }
10517 }, 11130 }, {
10518 11131 name: 'paper-menu-grow-width-animation',
10519 hostAttributes: { 11132 timing: {
10520 role: 'group', 11133 delay: 50,
10521 'aria-haspopup': 'true' 11134 duration: 150,
10522 }, 11135 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 } 11136 }
10545 }, 11137 }, {
10546 11138 name: 'paper-menu-grow-height-animation',
10547 /** 11139 timing: {
10548 * Make the dropdown content appear as an overlay positioned relative 11140 delay: 100,
10549 * to the dropdown trigger. 11141 duration: 275,
10550 */ 11142 easing: SLIDE_CUBIC_BEZIER
10551 open: function() {
10552 if (this.disabled) {
10553 return;
10554 } 11143 }
10555 11144 }];
10556 this.$.dropdown.open(); 11145 }
10557 }, 11146 },
10558 11147
10559 /** 11148 closeAnimationConfig: {
10560 * Hide the dropdown content. 11149 type: Object,
10561 */ 11150 value: function() {
10562 close: function() { 11151 return [{
10563 this.$.dropdown.close(); 11152 name: 'fade-out-animation',
10564 }, 11153 timing: {
10565 11154 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 } 11155 }
10577 }, 11156 }];
10578 11157 }
10579 /** 11158 }
10580 * Closes the dropdown when an `iron-activate` event is received if 11159 },
10581 * `closeOnActivate` is true. 11160
10582 * 11161 keyBindings: {
10583 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. 11162 'tab': 'onTabPressed_',
10584 */ 11163 },
10585 _onIronActivate: function(event) { 11164
10586 if (this.closeOnActivate) { 11165 listeners: {
10587 this.close(); 11166 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_',
10588 } 11167 },
10589 }, 11168
10590 11169 /**
10591 /** 11170 * 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`. 11171 * @private {?Element}
10593 * When the dropdown closes, the `paper-menu-button` fires `paper-close` . 11172 */
10594 * 11173 lastAnchor_: null,
10595 * @param {boolean} opened True if the dropdown is opened, otherwise fal se. 11174
10596 * @param {boolean} oldOpened The previous value of `opened`. 11175 /**
10597 */ 11176 * The first focusable child in the menu's light DOM.
10598 _openedChanged: function(opened, oldOpened) { 11177 * @private {?Element}
10599 if (opened) { 11178 */
10600 // TODO(cdata): Update this when we can measure changes in distribut ed 11179 firstFocus_: null,
10601 // children in an idiomatic way. 11180
10602 // We poke this property in case the element has changed. This will 11181 /**
10603 // cause the focus target for the `iron-dropdown` to be updated as 11182 * The last focusable child in the menu's light DOM.
10604 // necessary: 11183 * @private {?Element}
10605 this._dropdownContent = this.contentElement; 11184 */
10606 this.fire('paper-dropdown-open'); 11185 lastFocus_: null,
10607 } else if (oldOpened != null) { 11186
10608 this.fire('paper-dropdown-close'); 11187 /** @override */
10609 } 11188 attached: function() {
10610 }, 11189 window.addEventListener('resize', this.closeMenu.bind(this));
10611 11190 },
10612 /** 11191
10613 * If the dropdown is open when disabled becomes true, close the 11192 /** Closes the menu. */
10614 * dropdown. 11193 closeMenu: function() {
10615 * 11194 if (this.root.activeElement == null) {
10616 * @param {boolean} disabled True if disabled, otherwise false. 11195 // Something else has taken focus away from the menu. Do not attempt to
10617 */ 11196 // restore focus to the button which opened the menu.
10618 _disabledChanged: function(disabled) { 11197 this.$.dropdown.restoreFocusOnClose = false;
10619 Polymer.IronControlState._disabledChanged.apply(this, arguments); 11198 }
10620 if (disabled && this.opened) { 11199 this.menuOpen = false;
10621 this.close(); 11200 },
10622 } 11201
10623 }, 11202 /**
10624 11203 * Opens the menu at the anchor location.
10625 __onIronOverlayCanceled: function(event) { 11204 * @param {!Element} anchor The location to display the menu.
10626 var uiEvent = event.detail; 11205 * @param {!Object} itemData The contextual item's data.
10627 var target = Polymer.dom(uiEvent).rootTarget; 11206 */
10628 var trigger = this.$.trigger; 11207 openMenu: function(anchor, itemData) {
10629 var path = Polymer.dom(uiEvent).path; 11208 if (this.lastAnchor_ == anchor && this.menuOpen)
10630 11209 return;
10631 if (path.indexOf(trigger) > -1) { 11210
10632 event.preventDefault(); 11211 if (this.menuOpen)
10633 } 11212 this.closeMenu();
10634 } 11213
10635 }); 11214 this.itemData = itemData;
10636 11215 this.lastAnchor_ = anchor;
10637 Object.keys(config).forEach(function (key) { 11216 this.$.dropdown.restoreFocusOnClose = true;
10638 PaperMenuButton[key] = config[key]; 11217
10639 }); 11218 var focusableChildren = Polymer.dom(this).querySelectorAll(
10640 11219 '[tabindex]:not([hidden]),button:not([hidden])');
10641 Polymer.PaperMenuButton = PaperMenuButton; 11220 if (focusableChildren.length > 0) {
10642 })(); 11221 this.$.dropdown.focusTarget = focusableChildren[0];
10643 /** 11222 this.firstFocus_ = focusableChildren[0];
10644 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k eyboard focus. 11223 this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
10645 * 11224 }
10646 * @polymerBehavior Polymer.PaperInkyFocusBehavior 11225
10647 */ 11226 // Move the menu to the anchor.
10648 Polymer.PaperInkyFocusBehaviorImpl = { 11227 this.$.dropdown.positionTarget = anchor;
10649 observers: [ 11228 this.menuOpen = true;
10650 '_focusedChanged(receivedFocusFromKeyboard)' 11229 },
10651 ], 11230
10652 11231 /**
10653 _focusedChanged: function(receivedFocusFromKeyboard) { 11232 * Toggles the menu for the anchor that is passed in.
10654 if (receivedFocusFromKeyboard) { 11233 * @param {!Element} anchor The location to display the menu.
10655 this.ensureRipple(); 11234 * @param {!Object} itemData The contextual item's data.
10656 } 11235 */
10657 if (this.hasRipple()) { 11236 toggleMenu: function(anchor, itemData) {
10658 this._ripple.holdDown = receivedFocusFromKeyboard; 11237 if (anchor == this.lastAnchor_ && this.menuOpen)
10659 } 11238 this.closeMenu();
10660 }, 11239 else
10661 11240 this.openMenu(anchor, itemData);
10662 _createRipple: function() { 11241 },
10663 var ripple = Polymer.PaperRippleBehavior._createRipple(); 11242
10664 ripple.id = 'ink'; 11243 /**
10665 ripple.setAttribute('center', ''); 11244 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
10666 ripple.classList.add('circle'); 11245 * the first element with a nonzero tabindex to the last such element.
10667 return ripple; 11246 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
11247 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
11248 * @param {CustomEvent} e
11249 */
11250 onTabPressed_: function(e) {
11251 if (!this.firstFocus_ || !this.lastFocus_)
11252 return;
11253
11254 var toFocus;
11255 var keyEvent = e.detail.keyboardEvent;
11256 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_)
11257 toFocus = this.lastFocus_;
11258 else if (keyEvent.target == this.lastFocus_)
11259 toFocus = this.firstFocus_;
11260
11261 if (!toFocus)
11262 return;
11263
11264 e.preventDefault();
11265 toFocus.focus();
11266 },
11267
11268 /**
11269 * Ensure the menu is reset properly when it is closed by the dropdown (eg,
11270 * clicking outside).
11271 * @private
11272 */
11273 menuOpenChanged_: function() {
11274 if (!this.menuOpen) {
11275 this.itemData = null;
11276 this.lastAnchor_ = null;
11277 }
11278 },
11279
11280 /**
11281 * Prevent focus restoring when tapping outside the menu. This stops the
11282 * focus moving around unexpectedly when closing the menu with the mouse.
11283 * @param {CustomEvent} e
11284 * @private
11285 */
11286 onOverlayCanceled_: function(e) {
11287 if (e.detail.type == 'tap')
11288 this.$.dropdown.restoreFocusOnClose = false;
11289 },
11290 });
11291 /** @polymerBehavior Polymer.PaperItemBehavior */
11292 Polymer.PaperItemBehaviorImpl = {
11293 hostAttributes: {
11294 role: 'option',
11295 tabindex: '0'
10668 } 11296 }
10669 }; 11297 };
10670 11298
10671 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ 11299 /** @polymerBehavior */
10672 Polymer.PaperInkyFocusBehavior = [ 11300 Polymer.PaperItemBehavior = [
10673 Polymer.IronButtonState, 11301 Polymer.IronButtonState,
10674 Polymer.IronControlState, 11302 Polymer.IronControlState,
10675 Polymer.PaperRippleBehavior, 11303 Polymer.PaperItemBehaviorImpl
10676 Polymer.PaperInkyFocusBehaviorImpl
10677 ]; 11304 ];
10678 Polymer({ 11305 Polymer({
10679 is: 'paper-icon-button', 11306 is: 'paper-item',
11307
11308 behaviors: [
11309 Polymer.PaperItemBehavior
11310 ]
11311 });
11312 Polymer({
11313
11314 is: 'iron-collapse',
11315
11316 behaviors: [
11317 Polymer.IronResizableBehavior
11318 ],
11319
11320 properties: {
11321
11322 /**
11323 * If true, the orientation is horizontal; otherwise is vertical.
11324 *
11325 * @attribute horizontal
11326 */
11327 horizontal: {
11328 type: Boolean,
11329 value: false,
11330 observer: '_horizontalChanged'
11331 },
11332
11333 /**
11334 * Set opened to true to show the collapse element and to false to hide it .
11335 *
11336 * @attribute opened
11337 */
11338 opened: {
11339 type: Boolean,
11340 value: false,
11341 notify: true,
11342 observer: '_openedChanged'
11343 },
11344
11345 /**
11346 * Set noAnimation to true to disable animations
11347 *
11348 * @attribute noAnimation
11349 */
11350 noAnimation: {
11351 type: Boolean
11352 },
11353
11354 },
11355
11356 get dimension() {
11357 return this.horizontal ? 'width' : 'height';
11358 },
11359
11360 /**
11361 * `maxWidth` or `maxHeight`.
11362 * @private
11363 */
11364 get _dimensionMax() {
11365 return this.horizontal ? 'maxWidth' : 'maxHeight';
11366 },
11367
11368 /**
11369 * `max-width` or `max-height`.
11370 * @private
11371 */
11372 get _dimensionMaxCss() {
11373 return this.horizontal ? 'max-width' : 'max-height';
11374 },
11375
11376 hostAttributes: {
11377 role: 'group',
11378 'aria-hidden': 'true',
11379 'aria-expanded': 'false'
11380 },
11381
11382 listeners: {
11383 transitionend: '_transitionEnd'
11384 },
11385
11386 attached: function() {
11387 // It will take care of setting correct classes and styles.
11388 this._transitionEnd();
11389 },
11390
11391 /**
11392 * Toggle the opened state.
11393 *
11394 * @method toggle
11395 */
11396 toggle: function() {
11397 this.opened = !this.opened;
11398 },
11399
11400 show: function() {
11401 this.opened = true;
11402 },
11403
11404 hide: function() {
11405 this.opened = false;
11406 },
11407
11408 /**
11409 * Updates the size of the element.
11410 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop erty value, usually `auto` or `0px`.
11411 * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
11412 */
11413 updateSize: function(size, animated) {
11414 // No change!
11415 var curSize = this.style[this._dimensionMax];
11416 if (curSize === size || (size === 'auto' && !curSize)) {
11417 return;
11418 }
11419
11420 this._updateTransition(false);
11421 // If we can animate, must do some prep work.
11422 if (animated && !this.noAnimation && this._isDisplayed) {
11423 // Animation will start at the current size.
11424 var startSize = this._calcSize();
11425 // For `auto` we must calculate what is the final size for the animation .
11426 // After the transition is done, _transitionEnd will set the size back t o `auto`.
11427 if (size === 'auto') {
11428 this.style[this._dimensionMax] = '';
11429 size = this._calcSize();
11430 }
11431 // Go to startSize without animation.
11432 this.style[this._dimensionMax] = startSize;
11433 // Force layout to ensure transition will go. Set scrollTop to itself
11434 // so that compilers won't remove it.
11435 this.scrollTop = this.scrollTop;
11436 // Enable animation.
11437 this._updateTransition(true);
11438 }
11439 // Set the final size.
11440 if (size === 'auto') {
11441 this.style[this._dimensionMax] = '';
11442 } else {
11443 this.style[this._dimensionMax] = size;
11444 }
11445 },
11446
11447 /**
11448 * enableTransition() is deprecated, but left over so it doesn't break exist ing code.
11449 * Please use `noAnimation` property instead.
11450 *
11451 * @method enableTransition
11452 * @deprecated since version 1.0.4
11453 */
11454 enableTransition: function(enabled) {
11455 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.');
11456 this.noAnimation = !enabled;
11457 },
11458
11459 _updateTransition: function(enabled) {
11460 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s' ;
11461 },
11462
11463 _horizontalChanged: function() {
11464 this.style.transitionProperty = this._dimensionMaxCss;
11465 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma xWidth';
11466 this.style[otherDimension] = '';
11467 this.updateSize(this.opened ? 'auto' : '0px', false);
11468 },
11469
11470 _openedChanged: function() {
11471 this.setAttribute('aria-expanded', this.opened);
11472 this.setAttribute('aria-hidden', !this.opened);
11473
11474 this.toggleClass('iron-collapse-closed', false);
11475 this.toggleClass('iron-collapse-opened', false);
11476 this.updateSize(this.opened ? 'auto' : '0px', true);
11477
11478 // Focus the current collapse.
11479 if (this.opened) {
11480 this.focus();
11481 }
11482 if (this.noAnimation) {
11483 this._transitionEnd();
11484 }
11485 },
11486
11487 _transitionEnd: function() {
11488 if (this.opened) {
11489 this.style[this._dimensionMax] = '';
11490 }
11491 this.toggleClass('iron-collapse-closed', !this.opened);
11492 this.toggleClass('iron-collapse-opened', this.opened);
11493 this._updateTransition(false);
11494 this.notifyResize();
11495 },
11496
11497 /**
11498 * Simplistic heuristic to detect if element has a parent with display: none
11499 *
11500 * @private
11501 */
11502 get _isDisplayed() {
11503 var rect = this.getBoundingClientRect();
11504 for (var prop in rect) {
11505 if (rect[prop] !== 0) return true;
11506 }
11507 return false;
11508 },
11509
11510 _calcSize: function() {
11511 return this.getBoundingClientRect()[this.dimension] + 'px';
11512 }
11513
11514 });
11515 /**
11516 Polymer.IronFormElementBehavior enables a custom element to be included
11517 in an `iron-form`.
11518
11519 @demo demo/index.html
11520 @polymerBehavior
11521 */
11522 Polymer.IronFormElementBehavior = {
11523
11524 properties: {
11525 /**
11526 * Fired when the element is added to an `iron-form`.
11527 *
11528 * @event iron-form-element-register
11529 */
11530
11531 /**
11532 * Fired when the element is removed from an `iron-form`.
11533 *
11534 * @event iron-form-element-unregister
11535 */
11536
11537 /**
11538 * The name of this element.
11539 */
11540 name: {
11541 type: String
11542 },
11543
11544 /**
11545 * The value for this element.
11546 */
11547 value: {
11548 notify: true,
11549 type: String
11550 },
11551
11552 /**
11553 * Set to true to mark the input as required. If used in a form, a
11554 * custom element that uses this behavior should also use
11555 * Polymer.IronValidatableBehavior and define a custom validation method.
11556 * Otherwise, a `required` element will always be considered valid.
11557 * It's also strongly recommended to provide a visual style for the elemen t
11558 * when its value is invalid.
11559 */
11560 required: {
11561 type: Boolean,
11562 value: false
11563 },
11564
11565 /**
11566 * The form that the element is registered to.
11567 */
11568 _parentForm: {
11569 type: Object
11570 }
11571 },
11572
11573 attached: function() {
11574 // Note: the iron-form that this element belongs to will set this
11575 // element's _parentForm property when handling this event.
11576 this.fire('iron-form-element-register');
11577 },
11578
11579 detached: function() {
11580 if (this._parentForm) {
11581 this._parentForm.fire('iron-form-element-unregister', {target: this});
11582 }
11583 }
11584
11585 };
11586 /**
11587 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element
11588 * that has a `checked` property, which can be used for validation if the
11589 * element is also `required`. Element instances implementing this behavior
11590 * will also be registered for use in an `iron-form` element.
11591 *
11592 * @demo demo/index.html
11593 * @polymerBehavior Polymer.IronCheckedElementBehavior
11594 */
11595 Polymer.IronCheckedElementBehaviorImpl = {
11596
11597 properties: {
11598 /**
11599 * Fired when the checked state changes.
11600 *
11601 * @event iron-change
11602 */
11603
11604 /**
11605 * Gets or sets the state, `true` is checked and `false` is unchecked.
11606 */
11607 checked: {
11608 type: Boolean,
11609 value: false,
11610 reflectToAttribute: true,
11611 notify: true,
11612 observer: '_checkedChanged'
11613 },
11614
11615 /**
11616 * If true, the button toggles the active state with each tap or press
11617 * of the spacebar.
11618 */
11619 toggles: {
11620 type: Boolean,
11621 value: true,
11622 reflectToAttribute: true
11623 },
11624
11625 /* Overriden from Polymer.IronFormElementBehavior */
11626 value: {
11627 type: String,
11628 value: 'on',
11629 observer: '_valueChanged'
11630 }
11631 },
11632
11633 observers: [
11634 '_requiredChanged(required)'
11635 ],
11636
11637 created: function() {
11638 // Used by `iron-form` to handle the case that an element with this behavi or
11639 // doesn't have a role of 'checkbox' or 'radio', but should still only be
11640 // included when the form is serialized if `this.checked === true`.
11641 this._hasIronCheckedElementBehavior = true;
11642 },
11643
11644 /**
11645 * Returns false if the element is required and not checked, and true otherw ise.
11646 * @param {*=} _value Ignored.
11647 * @return {boolean} true if `required` is false or if `checked` is true.
11648 */
11649 _getValidity: function(_value) {
11650 return this.disabled || !this.required || this.checked;
11651 },
11652
11653 /**
11654 * Update the aria-required label when `required` is changed.
11655 */
11656 _requiredChanged: function() {
11657 if (this.required) {
11658 this.setAttribute('aria-required', 'true');
11659 } else {
11660 this.removeAttribute('aria-required');
11661 }
11662 },
11663
11664 /**
11665 * Fire `iron-changed` when the checked state changes.
11666 */
11667 _checkedChanged: function() {
11668 this.active = this.checked;
11669 this.fire('iron-change');
11670 },
11671
11672 /**
11673 * Reset value to 'on' if it is set to `undefined`.
11674 */
11675 _valueChanged: function() {
11676 if (this.value === undefined || this.value === null) {
11677 this.value = 'on';
11678 }
11679 }
11680 };
11681
11682 /** @polymerBehavior Polymer.IronCheckedElementBehavior */
11683 Polymer.IronCheckedElementBehavior = [
11684 Polymer.IronFormElementBehavior,
11685 Polymer.IronValidatableBehavior,
11686 Polymer.IronCheckedElementBehaviorImpl
11687 ];
11688 /**
11689 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element
11690 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio r`
11691 * and is compatible with having a ripple effect.
11692 * @polymerBehavior Polymer.PaperCheckedElementBehavior
11693 */
11694 Polymer.PaperCheckedElementBehaviorImpl = {
11695 /**
11696 * Synchronizes the element's checked state with its ripple effect.
11697 */
11698 _checkedChanged: function() {
11699 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
11700 if (this.hasRipple()) {
11701 if (this.checked) {
11702 this._ripple.setAttribute('checked', '');
11703 } else {
11704 this._ripple.removeAttribute('checked');
11705 }
11706 }
11707 },
11708
11709 /**
11710 * Synchronizes the element's `active` and `checked` state.
11711 */
11712 _buttonStateChanged: function() {
11713 Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
11714 if (this.disabled) {
11715 return;
11716 }
11717 if (this.isAttached) {
11718 this.checked = this.active;
11719 }
11720 }
11721 };
11722
11723 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */
11724 Polymer.PaperCheckedElementBehavior = [
11725 Polymer.PaperInkyFocusBehavior,
11726 Polymer.IronCheckedElementBehavior,
11727 Polymer.PaperCheckedElementBehaviorImpl
11728 ];
11729 Polymer({
11730 is: 'paper-checkbox',
11731
11732 behaviors: [
11733 Polymer.PaperCheckedElementBehavior
11734 ],
10680 11735
10681 hostAttributes: { 11736 hostAttributes: {
10682 role: 'button', 11737 role: 'checkbox',
10683 tabindex: '0' 11738 'aria-checked': false,
10684 }, 11739 tabindex: 0
10685 11740 },
10686 behaviors: [
10687 Polymer.PaperInkyFocusBehavior
10688 ],
10689 11741
10690 properties: { 11742 properties: {
10691 /** 11743 /**
10692 * The URL of an image for the icon. If the src property is specified, 11744 * Fired when the checked state changes due to user interaction.
10693 * the icon property should not be. 11745 *
11746 * @event change
10694 */ 11747 */
10695 src: {
10696 type: String
10697 },
10698 11748
10699 /** 11749 /**
10700 * Specifies the icon name or index in the set of icons available in 11750 * Fired when the checked state changes.
10701 * the icon's icon set. If the icon property is specified, 11751 *
10702 * the src property should not be. 11752 * @event iron-change
10703 */ 11753 */
10704 icon: { 11754 ariaActiveAttribute: {
10705 type: String
10706 },
10707
10708 /**
10709 * Specifies the alternate text for the button, for accessibility.
10710 */
10711 alt: {
10712 type: String, 11755 type: String,
10713 observer: "_altChanged" 11756 value: 'aria-checked'
10714 } 11757 }
10715 }, 11758 },
10716 11759
10717 _altChanged: function(newValue, oldValue) { 11760 _computeCheckboxClass: function(checked, invalid) {
10718 var label = this.getAttribute('aria-label'); 11761 var className = '';
10719 11762 if (checked) {
10720 // Don't stomp over a user-set aria-label. 11763 className += 'checked ';
10721 if (!label || oldValue == label) { 11764 }
10722 this.setAttribute('aria-label', newValue); 11765 if (invalid) {
11766 className += 'invalid';
11767 }
11768 return className;
11769 },
11770
11771 _computeCheckmarkClass: function(checked) {
11772 return checked ? '' : 'hidden';
11773 },
11774
11775 // create ripple inside the checkboxContainer
11776 _createRipple: function() {
11777 this._rippleContainer = this.$.checkboxContainer;
11778 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
11779 }
11780
11781 });
11782 Polymer({
11783 is: 'paper-icon-button-light',
11784 extends: 'button',
11785
11786 behaviors: [
11787 Polymer.PaperRippleBehavior
11788 ],
11789
11790 listeners: {
11791 'down': '_rippleDown',
11792 'up': '_rippleUp',
11793 'focus': '_rippleDown',
11794 'blur': '_rippleUp',
11795 },
11796
11797 _rippleDown: function() {
11798 this.getRipple().downAction();
11799 },
11800
11801 _rippleUp: function() {
11802 this.getRipple().upAction();
11803 },
11804
11805 /**
11806 * @param {...*} var_args
11807 */
11808 ensureRipple: function(var_args) {
11809 var lastRipple = this._ripple;
11810 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
11811 if (this._ripple && this._ripple !== lastRipple) {
11812 this._ripple.center = true;
11813 this._ripple.classList.add('circle');
10723 } 11814 }
10724 } 11815 }
10725 }); 11816 });
10726 // Copyright 2016 The Chromium Authors. All rights reserved. 11817 // Copyright 2016 The Chromium Authors. All rights reserved.
10727 // Use of this source code is governed by a BSD-style license that can be 11818 // Use of this source code is governed by a BSD-style license that can be
10728 // found in the LICENSE file. 11819 // found in the LICENSE file.
10729 11820
11821 cr.define('cr.icon', function() {
11822 /**
11823 * @return {!Array<number>} The scale factors supported by this platform for
11824 * webui resources.
11825 */
11826 function getSupportedScaleFactors() {
11827 var supportedScaleFactors = [];
11828 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
11829 // All desktop platforms support zooming which also updates the
11830 // renderer's device scale factors (a.k.a devicePixelRatio), and
11831 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
11832 // image-set on these platforms so that the renderer can pick the
11833 // closest image for the current device scale factor.
11834 supportedScaleFactors.push(1);
11835 supportedScaleFactors.push(2);
11836 } else {
11837 // For other platforms that use fixed device scale factor, use
11838 // the window's device pixel ratio.
11839 // TODO(oshima): Investigate if Android/iOS need to use image-set.
11840 supportedScaleFactors.push(window.devicePixelRatio);
11841 }
11842 return supportedScaleFactors;
11843 }
11844
11845 /**
11846 * Returns the URL of the image, or an image set of URLs for the profile
11847 * avatar. Default avatars have resources available for multiple scalefactors,
11848 * whereas the GAIA profile image only comes in one size.
11849 *
11850 * @param {string} path The path of the image.
11851 * @return {string} The url, or an image set of URLs of the avatar image.
11852 */
11853 function getProfileAvatarIcon(path) {
11854 var chromeThemePath = 'chrome://theme';
11855 var isDefaultAvatar =
11856 (path.slice(0, chromeThemePath.length) == chromeThemePath);
11857 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path);
11858 }
11859
11860 /**
11861 * Generates a CSS -webkit-image-set for a chrome:// url.
11862 * An entry in the image set is added for each of getSupportedScaleFactors().
11863 * The scale-factor-specific url is generated by replacing the first instance
11864 * of 'scalefactor' in |path| with the numeric scale factor.
11865 * @param {string} path The URL to generate an image set for.
11866 * 'scalefactor' should be a substring of |path|.
11867 * @return {string} The CSS -webkit-image-set.
11868 */
11869 function imageset(path) {
11870 var supportedScaleFactors = getSupportedScaleFactors();
11871
11872 var replaceStartIndex = path.indexOf('scalefactor');
11873 if (replaceStartIndex < 0)
11874 return url(path);
11875
11876 var s = '';
11877 for (var i = 0; i < supportedScaleFactors.length; ++i) {
11878 var scaleFactor = supportedScaleFactors[i];
11879 var pathWithScaleFactor = path.substr(0, replaceStartIndex) +
11880 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
11881
11882 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
11883
11884 if (i != supportedScaleFactors.length - 1)
11885 s += ', ';
11886 }
11887 return '-webkit-image-set(' + s + ')';
11888 }
11889
11890 /**
11891 * A regular expression for identifying favicon URLs.
11892 * @const {!RegExp}
11893 */
11894 var FAVICON_URL_REGEX = /\.ico$/i;
11895
11896 /**
11897 * Creates a CSS -webkit-image-set for a favicon request.
11898 * @param {string} url Either the URL of the original page or of the favicon
11899 * itself.
11900 * @param {number=} opt_size Optional preferred size of the favicon.
11901 * @param {string=} opt_type Optional type of favicon to request. Valid values
11902 * are 'favicon' and 'touch-icon'. Default is 'favicon'.
11903 * @return {string} -webkit-image-set for the favicon.
11904 */
11905 function getFaviconImageSet(url, opt_size, opt_type) {
11906 var size = opt_size || 16;
11907 var type = opt_type || 'favicon';
11908
11909 return imageset(
11910 'chrome://' + type + '/size/' + size + '@scalefactorx/' +
11911 // Note: Literal 'iconurl' must match |kIconURLParameter| in
11912 // components/favicon_base/favicon_url_parser.cc.
11913 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
11914 }
11915
11916 return {
11917 getSupportedScaleFactors: getSupportedScaleFactors,
11918 getProfileAvatarIcon: getProfileAvatarIcon,
11919 getFaviconImageSet: getFaviconImageSet,
11920 };
11921 });
11922 // Copyright 2016 The Chromium Authors. All rights reserved.
11923 // Use of this source code is governed by a BSD-style license that can be
11924 // found in the LICENSE file.
11925
10730 /** 11926 /**
10731 * Implements an incremental search field which can be shown and hidden. 11927 * @fileoverview Defines a singleton object, md_history.BrowserService, which
10732 * Canonical implementation is <cr-search-field>. 11928 * provides access to chrome.send APIs.
10733 * @polymerBehavior
10734 */ 11929 */
10735 var CrSearchFieldBehavior = { 11930
11931 cr.define('md_history', function() {
11932 /** @constructor */
11933 function BrowserService() {
11934 /** @private {Array<!HistoryEntry>} */
11935 this.pendingDeleteItems_ = null;
11936 /** @private {PromiseResolver} */
11937 this.pendingDeletePromise_ = null;
11938 }
11939
11940 BrowserService.prototype = {
11941 /**
11942 * @param {!Array<!HistoryEntry>} items
11943 * @return {Promise<!Array<!HistoryEntry>>}
11944 */
11945 deleteItems: function(items) {
11946 if (this.pendingDeleteItems_ != null) {
11947 // There's already a deletion in progress, reject immediately.
11948 return new Promise(function(resolve, reject) { reject(items); });
11949 }
11950
11951 var removalList = items.map(function(item) {
11952 return {
11953 url: item.url,
11954 timestamps: item.allTimestamps
11955 };
11956 });
11957
11958 this.pendingDeleteItems_ = items;
11959 this.pendingDeletePromise_ = new PromiseResolver();
11960
11961 chrome.send('removeVisits', removalList);
11962
11963 return this.pendingDeletePromise_.promise;
11964 },
11965
11966 /**
11967 * @param {!string} url
11968 */
11969 removeBookmark: function(url) {
11970 chrome.send('removeBookmark', [url]);
11971 },
11972
11973 /**
11974 * @param {string} sessionTag
11975 */
11976 openForeignSessionAllTabs: function(sessionTag) {
11977 chrome.send('openForeignSession', [sessionTag]);
11978 },
11979
11980 /**
11981 * @param {string} sessionTag
11982 * @param {number} windowId
11983 * @param {number} tabId
11984 * @param {MouseEvent} e
11985 */
11986 openForeignSessionTab: function(sessionTag, windowId, tabId, e) {
11987 chrome.send('openForeignSession', [
11988 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey,
11989 e.ctrlKey, e.metaKey, e.shiftKey
11990 ]);
11991 },
11992
11993 /**
11994 * @param {string} sessionTag
11995 */
11996 deleteForeignSession: function(sessionTag) {
11997 chrome.send('deleteForeignSession', [sessionTag]);
11998 },
11999
12000 openClearBrowsingData: function() {
12001 chrome.send('clearBrowsingData');
12002 },
12003
12004 /**
12005 * @param {boolean} successful
12006 * @private
12007 */
12008 resolveDelete_: function(successful) {
12009 if (this.pendingDeleteItems_ == null ||
12010 this.pendingDeletePromise_ == null) {
12011 return;
12012 }
12013
12014 if (successful)
12015 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_);
12016 else
12017 this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
12018
12019 this.pendingDeleteItems_ = null;
12020 this.pendingDeletePromise_ = null;
12021 },
12022 };
12023
12024 cr.addSingletonGetter(BrowserService);
12025
12026 return {BrowserService: BrowserService};
12027 });
12028
12029 /**
12030 * Called by the history backend when deletion was succesful.
12031 */
12032 function deleteComplete() {
12033 md_history.BrowserService.getInstance().resolveDelete_(true);
12034 }
12035
12036 /**
12037 * Called by the history backend when the deletion failed.
12038 */
12039 function deleteFailed() {
12040 md_history.BrowserService.getInstance().resolveDelete_(false);
12041 };
12042 // Copyright 2016 The Chromium Authors. All rights reserved.
12043 // Use of this source code is governed by a BSD-style license that can be
12044 // found in the LICENSE file.
12045
12046 Polymer({
12047 is: 'history-searched-label',
12048
10736 properties: { 12049 properties: {
10737 label: { 12050 // The text to show in this label.
10738 type: String, 12051 title: String,
10739 value: '', 12052
10740 }, 12053 // The search term to bold within the title.
10741 12054 searchTerm: String,
10742 clearLabel: { 12055 },
10743 type: String, 12056
10744 value: '', 12057 observers: ['setSearchedTextToBold_(title, searchTerm)'],
10745 },
10746
10747 showingSearch: {
10748 type: Boolean,
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 12058
10762 /** 12059 /**
10763 * @abstract 12060 * Updates the page title. If a search term is specified, highlights any
10764 * @return {!HTMLInputElement} The input field element the behavior should 12061 * occurrences of the search term in bold.
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 12062 * @private
10806 */ 12063 */
10807 onValueChanged_: function(newValue) { 12064 setSearchedTextToBold_: function() {
10808 if (newValue == this.lastValue_) 12065 var i = 0;
10809 return; 12066 var titleElem = this.$.container;
10810 12067 var titleText = this.title;
10811 this.fire('search-changed', newValue); 12068
10812 this.lastValue_ = newValue; 12069 if (this.searchTerm == '' || this.searchTerm == null) {
10813 }, 12070 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; 12071 return;
10825 } 12072 }
10826 12073
10827 this.setValue(''); 12074 var re = new RegExp(quoteString(this.searchTerm), 'gim');
10828 this.getSearchInput().blur(); 12075 var match;
10829 } 12076 titleElem.textContent = '';
10830 }; 12077 while (match = re.exec(titleText)) {
12078 if (match.index > i)
12079 titleElem.appendChild(document.createTextNode(
12080 titleText.slice(i, match.index)));
12081 i = re.lastIndex;
12082 // Mark the highlighted text in bold.
12083 var b = document.createElement('b');
12084 b.textContent = titleText.substring(match.index, i);
12085 titleElem.appendChild(b);
12086 }
12087 if (i < titleText.length)
12088 titleElem.appendChild(
12089 document.createTextNode(titleText.slice(i)));
12090 },
12091 });
12092 // Copyright 2015 The Chromium Authors. All rights reserved.
12093 // Use of this source code is governed by a BSD-style license that can be
12094 // found in the LICENSE file.
12095
12096 cr.define('md_history', function() {
12097 var HistoryItem = Polymer({
12098 is: 'history-item',
12099
12100 properties: {
12101 // Underlying HistoryEntry data for this item. Contains read-only fields
12102 // from the history backend, as well as fields computed by history-list.
12103 item: {type: Object, observer: 'showIcon_'},
12104
12105 // Search term used to obtain this history-item.
12106 searchTerm: {type: String},
12107
12108 selected: {type: Boolean, notify: true},
12109
12110 isFirstItem: {type: Boolean, reflectToAttribute: true},
12111
12112 isCardStart: {type: Boolean, reflectToAttribute: true},
12113
12114 isCardEnd: {type: Boolean, reflectToAttribute: true},
12115
12116 // True if the item is being displayed embedded in another element and
12117 // should not manage its own borders or size.
12118 embedded: {type: Boolean, reflectToAttribute: true},
12119
12120 hasTimeGap: {type: Boolean},
12121
12122 numberOfItems: {type: Number}
12123 },
12124
12125 /**
12126 * When a history-item is selected the toolbar is notified and increases
12127 * or decreases its count of selected items accordingly.
12128 * @private
12129 */
12130 onCheckboxSelected_: function() {
12131 this.fire('history-checkbox-select', {
12132 countAddition: this.$.checkbox.checked ? 1 : -1
12133 });
12134 },
12135
12136 /**
12137 * Remove bookmark of current item when bookmark-star is clicked.
12138 * @private
12139 */
12140 onRemoveBookmarkTap_: function() {
12141 if (!this.item.starred)
12142 return;
12143
12144 if (this.$$('#bookmark-star') == this.root.activeElement)
12145 this.$['menu-button'].focus();
12146
12147 md_history.BrowserService.getInstance()
12148 .removeBookmark(this.item.url);
12149 this.fire('remove-bookmark-stars', this.item.url);
12150 },
12151
12152 /**
12153 * Fires a custom event when the menu button is clicked. Sends the details
12154 * of the history item and where the menu should appear.
12155 */
12156 onMenuButtonTap_: function(e) {
12157 this.fire('toggle-menu', {
12158 target: Polymer.dom(e).localTarget,
12159 item: this.item,
12160 });
12161
12162 // Stops the 'tap' event from closing the menu when it opens.
12163 e.stopPropagation();
12164 },
12165
12166 /**
12167 * Set the favicon image, based on the URL of the history item.
12168 * @private
12169 */
12170 showIcon_: function() {
12171 this.$.icon.style.backgroundImage =
12172 cr.icon.getFaviconImageSet(this.item.url);
12173 },
12174
12175 selectionNotAllowed_: function() {
12176 return !loadTimeData.getBoolean('allowDeletingHistory');
12177 },
12178
12179 /**
12180 * Generates the title for this history card.
12181 * @param {number} numberOfItems The number of items in the card.
12182 * @param {string} search The search term associated with these results.
12183 * @private
12184 */
12185 cardTitle_: function(numberOfItems, historyDate, search) {
12186 if (!search)
12187 return this.item.dateRelativeDay;
12188
12189 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults';
12190 return loadTimeData.getStringF('foundSearchResults', numberOfItems,
12191 loadTimeData.getString(resultId), search);
12192 },
12193
12194 /**
12195 * Crop long item titles to reduce their effect on layout performance. See
12196 * crbug.com/621347.
12197 * @param {string} title
12198 * @return {string}
12199 */
12200 cropItemTitle_: function(title) {
12201 return (title.length > TITLE_MAX_LENGTH) ?
12202 title.substr(0, TITLE_MAX_LENGTH) :
12203 title;
12204 }
12205 });
12206
12207 /**
12208 * Check whether the time difference between the given history item and the
12209 * next one is large enough for a spacer to be required.
12210 * @param {Array<HistoryEntry>} visits
12211 * @param {number} currentIndex
12212 * @param {string} searchedTerm
12213 * @return {boolean} Whether or not time gap separator is required.
12214 * @private
12215 */
12216 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) {
12217 if (currentIndex >= visits.length - 1 || visits.length == 0)
12218 return false;
12219
12220 var currentItem = visits[currentIndex];
12221 var nextItem = visits[currentIndex + 1];
12222
12223 if (searchedTerm)
12224 return currentItem.dateShort != nextItem.dateShort;
12225
12226 return currentItem.time - nextItem.time > BROWSING_GAP_TIME &&
12227 currentItem.dateRelativeDay == nextItem.dateRelativeDay;
12228 };
12229
12230 return { HistoryItem: HistoryItem };
12231 });
12232 // Copyright 2016 The Chromium Authors. All rights reserved.
12233 // Use of this source code is governed by a BSD-style license that can be
12234 // found in the LICENSE file.
12235
12236 /**
12237 * @typedef {{domain: string,
12238 * visits: !Array<HistoryEntry>,
12239 * rendered: boolean,
12240 * expanded: boolean}}
12241 */
12242 var HistoryDomain;
12243
12244 /**
12245 * @typedef {{title: string,
12246 * domains: !Array<HistoryDomain>}}
12247 */
12248 var HistoryGroup;
12249
12250 // TODO(calamity): Support selection by refactoring selection out of
12251 // history-list and into history-app.
12252 Polymer({
12253 is: 'history-grouped-list',
12254
12255 properties: {
12256 // An array of history entries in reverse chronological order.
12257 historyData: {
12258 type: Array,
12259 },
12260
12261 /**
12262 * @type {Array<HistoryGroup>}
12263 */
12264 groupedHistoryData_: {
12265 type: Array,
12266 },
12267
12268 searchedTerm: {
12269 type: String,
12270 value: ''
12271 },
12272
12273 range: {
12274 type: Number,
12275 },
12276
12277 queryStartTime: String,
12278 queryEndTime: String,
12279 },
12280
12281 observers: [
12282 'updateGroupedHistoryData_(range, historyData)'
12283 ],
12284
12285 /**
12286 * Make a list of domains from visits.
12287 * @param {!Array<!HistoryEntry>} visits
12288 * @return {!Array<!HistoryDomain>}
12289 */
12290 createHistoryDomains_: function(visits) {
12291 var domainIndexes = {};
12292 var domains = [];
12293
12294 // Group the visits into a dictionary and generate a list of domains.
12295 for (var i = 0, visit; visit = visits[i]; i++) {
12296 var domain = visit.domain;
12297 if (domainIndexes[domain] == undefined) {
12298 domainIndexes[domain] = domains.length;
12299 domains.push({
12300 domain: domain,
12301 visits: [],
12302 expanded: false,
12303 rendered: false,
12304 });
12305 }
12306 domains[domainIndexes[domain]].visits.push(visit);
12307 }
12308 var sortByVisits = function(a, b) {
12309 return b.visits.length - a.visits.length;
12310 };
12311 domains.sort(sortByVisits);
12312
12313 return domains;
12314 },
12315
12316 updateGroupedHistoryData_: function() {
12317 if (this.historyData.length == 0) {
12318 this.groupedHistoryData_ = [];
12319 return;
12320 }
12321
12322 if (this.range == HistoryRange.WEEK) {
12323 // Group each day into a list of results.
12324 var days = [];
12325 var currentDayVisits = [this.historyData[0]];
12326
12327 var pushCurrentDay = function() {
12328 days.push({
12329 title: this.searchedTerm ? currentDayVisits[0].dateShort :
12330 currentDayVisits[0].dateRelativeDay,
12331 domains: this.createHistoryDomains_(currentDayVisits),
12332 });
12333 }.bind(this);
12334
12335 var visitsSameDay = function(a, b) {
12336 if (this.searchedTerm)
12337 return a.dateShort == b.dateShort;
12338
12339 return a.dateRelativeDay == b.dateRelativeDay;
12340 }.bind(this);
12341
12342 for (var i = 1; i < this.historyData.length; i++) {
12343 var visit = this.historyData[i];
12344 if (!visitsSameDay(visit, currentDayVisits[0])) {
12345 pushCurrentDay();
12346 currentDayVisits = [];
12347 }
12348 currentDayVisits.push(visit);
12349 }
12350 pushCurrentDay();
12351
12352 this.groupedHistoryData_ = days;
12353 } else if (this.range == HistoryRange.MONTH) {
12354 // Group each all visits into a single list.
12355 this.groupedHistoryData_ = [{
12356 title: this.queryStartTime + ' – ' + this.queryEndTime,
12357 domains: this.createHistoryDomains_(this.historyData)
12358 }];
12359 }
12360 },
12361
12362 /**
12363 * @param {{model:Object, currentTarget:IronCollapseElement}} e
12364 */
12365 toggleDomainExpanded_: function(e) {
12366 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse');
12367 e.model.set('domain.rendered', true);
12368
12369 // Give the history-items time to render.
12370 setTimeout(function() { collapse.toggle() }, 0);
12371 },
12372
12373 /**
12374 * Check whether the time difference between the given history item and the
12375 * next one is large enough for a spacer to be required.
12376 * @param {number} groupIndex
12377 * @param {number} domainIndex
12378 * @param {number} itemIndex
12379 * @return {boolean} Whether or not time gap separator is required.
12380 * @private
12381 */
12382 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) {
12383 var visits =
12384 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits;
12385
12386 return md_history.HistoryItem.needsTimeGap(
12387 visits, itemIndex, this.searchedTerm);
12388 },
12389
12390 hasResults_: function(historyDataLength) {
12391 return historyDataLength > 0;
12392 },
12393
12394 getWebsiteIconStyle_: function(domain) {
12395 return 'background-image: ' +
12396 cr.icon.getFaviconImageSet(domain.visits[0].url);
12397 },
12398
12399 getDropdownIcon_: function(expanded) {
12400 return expanded ? 'cr:expand-less' : 'cr:expand-more';
12401 },
12402
12403 noResultsMessage_: function(searchedTerm) {
12404 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
12405 return loadTimeData.getString(messageId);
12406 },
12407 });
12408 /**
12409 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e vents from a
12410 * designated scroll target.
12411 *
12412 * Elements that consume this behavior can override the `_scrollHandler`
12413 * method to add logic on the scroll event.
12414 *
12415 * @demo demo/scrolling-region.html Scrolling Region
12416 * @demo demo/document.html Document Element
12417 * @polymerBehavior
12418 */
12419 Polymer.IronScrollTargetBehavior = {
12420
12421 properties: {
12422
12423 /**
12424 * Specifies the element that will handle the scroll event
12425 * on the behalf of the current element. This is typically a reference to an element,
12426 * but there are a few more posibilities:
12427 *
12428 * ### Elements id
12429 *
12430 *```html
12431 * <div id="scrollable-element" style="overflow: auto;">
12432 * <x-element scroll-target="scrollable-element">
12433 * \x3c!-- Content--\x3e
12434 * </x-element>
12435 * </div>
12436 *```
12437 * In this case, the `scrollTarget` will point to the outer div element.
12438 *
12439 * ### Document scrolling
12440 *
12441 * For document scrolling, you can use the reserved word `document`:
12442 *
12443 *```html
12444 * <x-element scroll-target="document">
12445 * \x3c!-- Content --\x3e
12446 * </x-element>
12447 *```
12448 *
12449 * ### Elements reference
12450 *
12451 *```js
12452 * appHeader.scrollTarget = document.querySelector('#scrollable-element');
12453 *```
12454 *
12455 * @type {HTMLElement}
12456 */
12457 scrollTarget: {
12458 type: HTMLElement,
12459 value: function() {
12460 return this._defaultScrollTarget;
12461 }
12462 }
12463 },
12464
12465 observers: [
12466 '_scrollTargetChanged(scrollTarget, isAttached)'
12467 ],
12468
12469 _scrollTargetChanged: function(scrollTarget, isAttached) {
12470 var eventTarget;
12471
12472 if (this._oldScrollTarget) {
12473 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc rollTarget;
12474 eventTarget.removeEventListener('scroll', this._boundScrollHandler);
12475 this._oldScrollTarget = null;
12476 }
12477
12478 if (!isAttached) {
12479 return;
12480 }
12481 // Support element id references
12482 if (scrollTarget === 'document') {
12483
12484 this.scrollTarget = this._doc;
12485
12486 } else if (typeof scrollTarget === 'string') {
12487
12488 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
12489 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
12490
12491 } else if (this._isValidScrollTarget()) {
12492
12493 eventTarget = scrollTarget === this._doc ? window : scrollTarget;
12494 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl er.bind(this);
12495 this._oldScrollTarget = scrollTarget;
12496
12497 eventTarget.addEventListener('scroll', this._boundScrollHandler);
12498 }
12499 },
12500
12501 /**
12502 * Runs on every scroll event. Consumer of this behavior may override this m ethod.
12503 *
12504 * @protected
12505 */
12506 _scrollHandler: function scrollHandler() {},
12507
12508 /**
12509 * The default scroll target. Consumers of this behavior may want to customi ze
12510 * the default scroll target.
12511 *
12512 * @type {Element}
12513 */
12514 get _defaultScrollTarget() {
12515 return this._doc;
12516 },
12517
12518 /**
12519 * Shortcut for the document element
12520 *
12521 * @type {Element}
12522 */
12523 get _doc() {
12524 return this.ownerDocument.documentElement;
12525 },
12526
12527 /**
12528 * Gets the number of pixels that the content of an element is scrolled upwa rd.
12529 *
12530 * @type {number}
12531 */
12532 get _scrollTop() {
12533 if (this._isValidScrollTarget()) {
12534 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol lTarget.scrollTop;
12535 }
12536 return 0;
12537 },
12538
12539 /**
12540 * Gets the number of pixels that the content of an element is scrolled to t he left.
12541 *
12542 * @type {number}
12543 */
12544 get _scrollLeft() {
12545 if (this._isValidScrollTarget()) {
12546 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol lTarget.scrollLeft;
12547 }
12548 return 0;
12549 },
12550
12551 /**
12552 * Sets the number of pixels that the content of an element is scrolled upwa rd.
12553 *
12554 * @type {number}
12555 */
12556 set _scrollTop(top) {
12557 if (this.scrollTarget === this._doc) {
12558 window.scrollTo(window.pageXOffset, top);
12559 } else if (this._isValidScrollTarget()) {
12560 this.scrollTarget.scrollTop = top;
12561 }
12562 },
12563
12564 /**
12565 * Sets the number of pixels that the content of an element is scrolled to t he left.
12566 *
12567 * @type {number}
12568 */
12569 set _scrollLeft(left) {
12570 if (this.scrollTarget === this._doc) {
12571 window.scrollTo(left, window.pageYOffset);
12572 } else if (this._isValidScrollTarget()) {
12573 this.scrollTarget.scrollLeft = left;
12574 }
12575 },
12576
12577 /**
12578 * Scrolls the content to a particular place.
12579 *
12580 * @method scroll
12581 * @param {number} left The left position
12582 * @param {number} top The top position
12583 */
12584 scroll: function(left, top) {
12585 if (this.scrollTarget === this._doc) {
12586 window.scrollTo(left, top);
12587 } else if (this._isValidScrollTarget()) {
12588 this.scrollTarget.scrollLeft = left;
12589 this.scrollTarget.scrollTop = top;
12590 }
12591 },
12592
12593 /**
12594 * Gets the width of the scroll target.
12595 *
12596 * @type {number}
12597 */
12598 get _scrollTargetWidth() {
12599 if (this._isValidScrollTarget()) {
12600 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll Target.offsetWidth;
12601 }
12602 return 0;
12603 },
12604
12605 /**
12606 * Gets the height of the scroll target.
12607 *
12608 * @type {number}
12609 */
12610 get _scrollTargetHeight() {
12611 if (this._isValidScrollTarget()) {
12612 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol lTarget.offsetHeight;
12613 }
12614 return 0;
12615 },
12616
12617 /**
12618 * Returns true if the scroll target is a valid HTMLElement.
12619 *
12620 * @return {boolean}
12621 */
12622 _isValidScrollTarget: function() {
12623 return this.scrollTarget instanceof HTMLElement;
12624 }
12625 };
10831 (function() { 12626 (function() {
10832 'use strict'; 12627
10833 12628 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
10834 Polymer.IronA11yAnnouncer = Polymer({ 12629 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
10835 is: 'iron-a11y-announcer', 12630 var DEFAULT_PHYSICAL_COUNT = 3;
10836 12631 var HIDDEN_Y = '-10000px';
10837 properties: { 12632 var DEFAULT_GRID_SIZE = 200;
10838 12633 var SECRET_TABINDEX = -100;
10839 /** 12634
10840 * The value of mode is used to set the `aria-live` attribute 12635 Polymer({
10841 * for the element that will be announced. Valid values are: `off`, 12636
10842 * `polite` and `assertive`. 12637 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 12638
10923 properties: { 12639 properties: {
10924 12640
10925 /** 12641 /**
10926 * Name of the validator to use. 12642 * An array containing items determining how many instances of the templat e
12643 * to stamp and that that each template instance should bind to.
10927 */ 12644 */
10928 validator: { 12645 items: {
10929 type: String 12646 type: Array
10930 }, 12647 },
10931 12648
10932 /** 12649 /**
10933 * True if the last call to `validate` is invalid. 12650 * The max count of physical items the pool can extend to.
10934 */ 12651 */
10935 invalid: { 12652 maxPhysicalCount: {
10936 notify: true, 12653 type: Number,
10937 reflectToAttribute: true, 12654 value: 500
12655 },
12656
12657 /**
12658 * The name of the variable to add to the binding scope for the array
12659 * element associated with a given template instance.
12660 */
12661 as: {
12662 type: String,
12663 value: 'item'
12664 },
12665
12666 /**
12667 * The name of the variable to add to the binding scope with the index
12668 * for the row.
12669 */
12670 indexAs: {
12671 type: String,
12672 value: 'index'
12673 },
12674
12675 /**
12676 * The name of the variable to add to the binding scope to indicate
12677 * if the row is selected.
12678 */
12679 selectedAs: {
12680 type: String,
12681 value: 'selected'
12682 },
12683
12684 /**
12685 * When true, the list is rendered as a grid. Grid items must have
12686 * fixed width and height set via CSS. e.g.
12687 *
12688 * ```html
12689 * <iron-list grid>
12690 * <template>
12691 * <div style="width: 100px; height: 100px;"> 100x100 </div>
12692 * </template>
12693 * </iron-list>
12694 * ```
12695 */
12696 grid: {
12697 type: Boolean,
12698 value: false,
12699 reflectToAttribute: true
12700 },
12701
12702 /**
12703 * When true, tapping a row will select the item, placing its data model
12704 * in the set of selected items retrievable via the selection property.
12705 *
12706 * Note that tapping focusable elements within the list item will not
12707 * result in selection, since they are presumed to have their * own action .
12708 */
12709 selectionEnabled: {
10938 type: Boolean, 12710 type: Boolean,
10939 value: false 12711 value: false
10940 }, 12712 },
10941 12713
10942 /** 12714 /**
10943 * This property is deprecated and should not be used. Use the global 12715 * When `multiSelection` is false, this is the currently selected item, or `null`
10944 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead . 12716 * if no item is selected.
10945 */ 12717 */
10946 _validatorMeta: { 12718 selectedItem: {
10947 type: Object 12719 type: Object,
12720 notify: true
10948 }, 12721 },
10949 12722
10950 /** 12723 /**
10951 * Namespace for this validator. This property is deprecated and should 12724 * 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 */ 12725 */
10955 validatorType: { 12726 selectedItems: {
10956 type: String,
10957 value: 'validator'
10958 },
10959
10960 _validator: {
10961 type: Object, 12727 type: Object,
10962 computed: '__computeValidator(validator)' 12728 notify: true
10963 } 12729 },
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 12730
11071 /** 12731 /**
11072 * Use this property instead of `value` for two-way data binding. 12732 * When `true`, multiple items may be selected at once (in this case,
12733 * `selected` is an array of currently selected items). When `false`,
12734 * only one item may be selected at a time.
11073 */ 12735 */
11074 bindValue: { 12736 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, 12737 type: Boolean,
11109 value: false 12738 value: false
11110 } 12739 }
11111 12740 },
11112 }, 12741
11113 12742 observers: [
11114 listeners: { 12743 '_itemsChanged(items.*)',
11115 'input': '_onInput', 12744 '_selectionEnabledChanged(selectionEnabled)',
11116 'keypress': '_onKeypress' 12745 '_multiSelectionChanged(multiSelection)',
11117 }, 12746 '_setOverflow(scrollTarget)'
11118 12747 ],
11119 /** @suppress {checkTypes} */ 12748
11120 registered: function() { 12749 behaviors: [
11121 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I E). 12750 Polymer.Templatizer,
11122 if (!this._canDispatchEventOnDisabled()) { 12751 Polymer.IronResizableBehavior,
11123 this._origDispatchEvent = this.dispatchEvent; 12752 Polymer.IronA11yKeysBehavior,
11124 this.dispatchEvent = this._dispatchEventFirefoxIE; 12753 Polymer.IronScrollTargetBehavior
11125 } 12754 ],
11126 }, 12755
11127 12756 keyBindings: {
11128 created: function() { 12757 'up': '_didMoveUp',
11129 Polymer.IronA11yAnnouncer.requestAvailability(); 12758 'down': '_didMoveDown',
11130 }, 12759 'enter': '_didEnter'
11131 12760 },
11132 _canDispatchEventOnDisabled: function() { 12761
11133 var input = document.createElement('input'); 12762 /**
11134 var canDispatch = false; 12763 * The ratio of hidden tiles that should remain in the scroll direction.
11135 input.disabled = true; 12764 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
11136 12765 */
11137 input.addEventListener('feature-check-dispatch-event', function() { 12766 _ratio: 0.5,
11138 canDispatch = true; 12767
11139 }); 12768 /**
11140 12769 * The padding-top value for the list.
11141 try { 12770 */
11142 input.dispatchEvent(new Event('feature-check-dispatch-event')); 12771 _scrollerPaddingTop: 0,
11143 } catch(e) {} 12772
11144 12773 /**
11145 return canDispatch; 12774 * This value is the same as `scrollTop`.
11146 }, 12775 */
11147 12776 _scrollPosition: 0,
11148 _dispatchEventFirefoxIE: function() { 12777
11149 // Due to Firefox bug, events fired on disabled form controls can throw 12778 /**
11150 // errors; furthermore, neither IE nor Firefox will actually dispatch 12779 * The sum of the heights of all the tiles in the DOM.
11151 // events from disabled form controls; as such, we toggle disable around 12780 */
11152 // the dispatch to allow notifying properties to notify 12781 _physicalSize: 0,
11153 // See issue #47 for details 12782
11154 var disabled = this.disabled; 12783 /**
11155 this.disabled = false; 12784 * The average `offsetHeight` of the tiles observed till now.
11156 this._origDispatchEvent.apply(this, arguments); 12785 */
11157 this.disabled = disabled; 12786 _physicalAverage: 0,
11158 }, 12787
11159 12788 /**
11160 get _patternRegExp() { 12789 * The number of tiles which `offsetHeight` > 0 observed until now.
11161 var pattern; 12790 */
11162 if (this.allowedPattern) { 12791 _physicalAverageCount: 0,
11163 pattern = new RegExp(this.allowedPattern); 12792
12793 /**
12794 * The Y position of the item rendered in the `_physicalStart`
12795 * tile relative to the scrolling list.
12796 */
12797 _physicalTop: 0,
12798
12799 /**
12800 * The number of items in the list.
12801 */
12802 _virtualCount: 0,
12803
12804 /**
12805 * A map between an item key and its physical item index
12806 */
12807 _physicalIndexForKey: null,
12808
12809 /**
12810 * The estimated scroll height based on `_physicalAverage`
12811 */
12812 _estScrollHeight: 0,
12813
12814 /**
12815 * The scroll height of the dom node
12816 */
12817 _scrollHeight: 0,
12818
12819 /**
12820 * The height of the list. This is referred as the viewport in the context o f list.
12821 */
12822 _viewportHeight: 0,
12823
12824 /**
12825 * The width of the list. This is referred as the viewport in the context of list.
12826 */
12827 _viewportWidth: 0,
12828
12829 /**
12830 * An array of DOM nodes that are currently in the tree
12831 * @type {?Array<!TemplatizerNode>}
12832 */
12833 _physicalItems: null,
12834
12835 /**
12836 * An array of heights for each item in `_physicalItems`
12837 * @type {?Array<number>}
12838 */
12839 _physicalSizes: null,
12840
12841 /**
12842 * A cached value for the first visible index.
12843 * See `firstVisibleIndex`
12844 * @type {?number}
12845 */
12846 _firstVisibleIndexVal: null,
12847
12848 /**
12849 * A cached value for the last visible index.
12850 * See `lastVisibleIndex`
12851 * @type {?number}
12852 */
12853 _lastVisibleIndexVal: null,
12854
12855 /**
12856 * A Polymer collection for the items.
12857 * @type {?Polymer.Collection}
12858 */
12859 _collection: null,
12860
12861 /**
12862 * True if the current item list was rendered for the first time
12863 * after attached.
12864 */
12865 _itemsRendered: false,
12866
12867 /**
12868 * The page that is currently rendered.
12869 */
12870 _lastPage: null,
12871
12872 /**
12873 * The max number of pages to render. One page is equivalent to the height o f the list.
12874 */
12875 _maxPages: 3,
12876
12877 /**
12878 * The currently focused physical item.
12879 */
12880 _focusedItem: null,
12881
12882 /**
12883 * The index of the `_focusedItem`.
12884 */
12885 _focusedIndex: -1,
12886
12887 /**
12888 * The the item that is focused if it is moved offscreen.
12889 * @private {?TemplatizerNode}
12890 */
12891 _offscreenFocusedItem: null,
12892
12893 /**
12894 * The item that backfills the `_offscreenFocusedItem` in the physical items
12895 * list when that item is moved offscreen.
12896 */
12897 _focusBackfillItem: null,
12898
12899 /**
12900 * The maximum items per row
12901 */
12902 _itemsPerRow: 1,
12903
12904 /**
12905 * The width of each grid item
12906 */
12907 _itemWidth: 0,
12908
12909 /**
12910 * The height of the row in grid layout.
12911 */
12912 _rowHeight: 0,
12913
12914 /**
12915 * The bottom of the physical content.
12916 */
12917 get _physicalBottom() {
12918 return this._physicalTop + this._physicalSize;
12919 },
12920
12921 /**
12922 * The bottom of the scroll.
12923 */
12924 get _scrollBottom() {
12925 return this._scrollPosition + this._viewportHeight;
12926 },
12927
12928 /**
12929 * The n-th item rendered in the last physical item.
12930 */
12931 get _virtualEnd() {
12932 return this._virtualStart + this._physicalCount - 1;
12933 },
12934
12935 /**
12936 * The height of the physical content that isn't on the screen.
12937 */
12938 get _hiddenContentSize() {
12939 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic alSize;
12940 return size - this._viewportHeight;
12941 },
12942
12943 /**
12944 * The maximum scroll top value.
12945 */
12946 get _maxScrollTop() {
12947 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin gTop;
12948 },
12949
12950 /**
12951 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
12952 */
12953 _minVirtualStart: 0,
12954
12955 /**
12956 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
12957 */
12958 get _maxVirtualStart() {
12959 return Math.max(0, this._virtualCount - this._physicalCount);
12960 },
12961
12962 /**
12963 * The n-th item rendered in the `_physicalStart` tile.
12964 */
12965 _virtualStartVal: 0,
12966
12967 set _virtualStart(val) {
12968 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
12969 },
12970
12971 get _virtualStart() {
12972 return this._virtualStartVal || 0;
12973 },
12974
12975 /**
12976 * The k-th tile that is at the top of the scrolling list.
12977 */
12978 _physicalStartVal: 0,
12979
12980 set _physicalStart(val) {
12981 this._physicalStartVal = val % this._physicalCount;
12982 if (this._physicalStartVal < 0) {
12983 this._physicalStartVal = this._physicalCount + this._physicalStartVal;
12984 }
12985 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
12986 },
12987
12988 get _physicalStart() {
12989 return this._physicalStartVal || 0;
12990 },
12991
12992 /**
12993 * The number of tiles in the DOM.
12994 */
12995 _physicalCountVal: 0,
12996
12997 set _physicalCount(val) {
12998 this._physicalCountVal = val;
12999 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
13000 },
13001
13002 get _physicalCount() {
13003 return this._physicalCountVal;
13004 },
13005
13006 /**
13007 * The k-th tile that is at the bottom of the scrolling list.
13008 */
13009 _physicalEnd: 0,
13010
13011 /**
13012 * An optimal physical size such that we will have enough physical items
13013 * to fill up the viewport and recycle when the user scrolls.
13014 *
13015 * This default value assumes that we will at least have the equivalent
13016 * to a viewport of physical items above and below the user's viewport.
13017 */
13018 get _optPhysicalSize() {
13019 if (this.grid) {
13020 return this._estRowsInView * this._rowHeight * this._maxPages;
13021 }
13022 return this._viewportHeight * this._maxPages;
13023 },
13024
13025 get _optPhysicalCount() {
13026 return this._estRowsInView * this._itemsPerRow * this._maxPages;
13027 },
13028
13029 /**
13030 * True if the current list is visible.
13031 */
13032 get _isVisible() {
13033 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight);
13034 },
13035
13036 /**
13037 * Gets the index of the first visible item in the viewport.
13038 *
13039 * @type {number}
13040 */
13041 get firstVisibleIndex() {
13042 if (this._firstVisibleIndexVal === null) {
13043 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin gTop);
13044
13045 this._firstVisibleIndexVal = this._iterateItems(
13046 function(pidx, vidx) {
13047 physicalOffset += this._getPhysicalSizeIncrement(pidx);
13048
13049 if (physicalOffset > this._scrollPosition) {
13050 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
13051 }
13052 // Handle a partially rendered final row in grid mode
13053 if (this.grid && this._virtualCount - 1 === vidx) {
13054 return vidx - (vidx % this._itemsPerRow);
13055 }
13056 }) || 0;
13057 }
13058 return this._firstVisibleIndexVal;
13059 },
13060
13061 /**
13062 * Gets the index of the last visible item in the viewport.
13063 *
13064 * @type {number}
13065 */
13066 get lastVisibleIndex() {
13067 if (this._lastVisibleIndexVal === null) {
13068 if (this.grid) {
13069 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i temsPerRow - 1;
13070 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
13071 } else {
13072 var physicalOffset = this._physicalTop;
13073 this._iterateItems(function(pidx, vidx) {
13074 if (physicalOffset < this._scrollBottom) {
13075 this._lastVisibleIndexVal = vidx;
13076 } else {
13077 // Break _iterateItems
13078 return true;
13079 }
13080 physicalOffset += this._getPhysicalSizeIncrement(pidx);
13081 });
13082 }
13083 }
13084 return this._lastVisibleIndexVal;
13085 },
13086
13087 get _defaultScrollTarget() {
13088 return this;
13089 },
13090 get _virtualRowCount() {
13091 return Math.ceil(this._virtualCount / this._itemsPerRow);
13092 },
13093
13094 get _estRowsInView() {
13095 return Math.ceil(this._viewportHeight / this._rowHeight);
13096 },
13097
13098 get _physicalRows() {
13099 return Math.ceil(this._physicalCount / this._itemsPerRow);
13100 },
13101
13102 ready: function() {
13103 this.addEventListener('focus', this._didFocus.bind(this), true);
13104 },
13105
13106 attached: function() {
13107 this.updateViewportBoundaries();
13108 this._render();
13109 // `iron-resize` is fired when the list is attached if the event is added
13110 // before attached causing unnecessary work.
13111 this.listen(this, 'iron-resize', '_resizeHandler');
13112 },
13113
13114 detached: function() {
13115 this._itemsRendered = false;
13116 this.unlisten(this, 'iron-resize', '_resizeHandler');
13117 },
13118
13119 /**
13120 * Set the overflow property if this element has its own scrolling region
13121 */
13122 _setOverflow: function(scrollTarget) {
13123 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
13124 this.style.overflow = scrollTarget === this ? 'auto' : '';
13125 },
13126
13127 /**
13128 * Invoke this method if you dynamically update the viewport's
13129 * size or CSS padding.
13130 *
13131 * @method updateViewportBoundaries
13132 */
13133 updateViewportBoundaries: function() {
13134 this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
13135 parseInt(window.getComputedStyle(this)['padding-top'], 10);
13136
13137 this._viewportHeight = this._scrollTargetHeight;
13138 if (this.grid) {
13139 this._updateGridMetrics();
13140 }
13141 },
13142
13143 /**
13144 * Update the models, the position of the
13145 * items in the viewport and recycle tiles as needed.
13146 */
13147 _scrollHandler: function() {
13148 // clamp the `scrollTop` value
13149 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ;
13150 var delta = scrollTop - this._scrollPosition;
13151 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m;
13152 var ratio = this._ratio;
13153 var recycledTiles = 0;
13154 var hiddenContentSize = this._hiddenContentSize;
13155 var currentRatio = ratio;
13156 var movingUp = [];
13157
13158 // track the last `scrollTop`
13159 this._scrollPosition = scrollTop;
13160
13161 // clear cached visible indexes
13162 this._firstVisibleIndexVal = null;
13163 this._lastVisibleIndexVal = null;
13164
13165 scrollBottom = this._scrollBottom;
13166 physicalBottom = this._physicalBottom;
13167
13168 // random access
13169 if (Math.abs(delta) > this._physicalSize) {
13170 this._physicalTop += delta;
13171 recycledTiles = Math.round(delta / this._physicalAverage);
13172 }
13173 // scroll up
13174 else if (delta < 0) {
13175 var topSpace = scrollTop - this._physicalTop;
13176 var virtualStart = this._virtualStart;
13177
13178 recycledTileSet = [];
13179
13180 kth = this._physicalEnd;
13181 currentRatio = topSpace / hiddenContentSize;
13182
13183 // move tiles from bottom to top
13184 while (
13185 // approximate `currentRatio` to `ratio`
13186 currentRatio < ratio &&
13187 // recycle less physical items than the total
13188 recycledTiles < this._physicalCount &&
13189 // ensure that these recycled tiles are needed
13190 virtualStart - recycledTiles > 0 &&
13191 // ensure that the tile is not visible
13192 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
13193 ) {
13194
13195 tileHeight = this._getPhysicalSizeIncrement(kth);
13196 currentRatio += tileHeight / hiddenContentSize;
13197 physicalBottom -= tileHeight;
13198 recycledTileSet.push(kth);
13199 recycledTiles++;
13200 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
13201 }
13202
13203 movingUp = recycledTileSet;
13204 recycledTiles = -recycledTiles;
13205 }
13206 // scroll down
13207 else if (delta > 0) {
13208 var bottomSpace = physicalBottom - scrollBottom;
13209 var virtualEnd = this._virtualEnd;
13210 var lastVirtualItemIndex = this._virtualCount-1;
13211
13212 recycledTileSet = [];
13213
13214 kth = this._physicalStart;
13215 currentRatio = bottomSpace / hiddenContentSize;
13216
13217 // move tiles from top to bottom
13218 while (
13219 // approximate `currentRatio` to `ratio`
13220 currentRatio < ratio &&
13221 // recycle less physical items than the total
13222 recycledTiles < this._physicalCount &&
13223 // ensure that these recycled tiles are needed
13224 virtualEnd + recycledTiles < lastVirtualItemIndex &&
13225 // ensure that the tile is not visible
13226 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
13227 ) {
13228
13229 tileHeight = this._getPhysicalSizeIncrement(kth);
13230 currentRatio += tileHeight / hiddenContentSize;
13231
13232 this._physicalTop += tileHeight;
13233 recycledTileSet.push(kth);
13234 recycledTiles++;
13235 kth = (kth + 1) % this._physicalCount;
13236 }
13237 }
13238
13239 if (recycledTiles === 0) {
13240 // Try to increase the pool if the list's client height isn't filled up with physical items
13241 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
13242 this._increasePoolIfNeeded();
13243 }
11164 } else { 13244 } else {
11165 switch (this.type) { 13245 this._virtualStart = this._virtualStart + recycledTiles;
11166 case 'number': 13246 this._physicalStart = this._physicalStart + recycledTiles;
11167 pattern = /[0-9.,e-]/; 13247 this._update(recycledTileSet, movingUp);
11168 break; 13248 }
11169 } 13249 },
11170 } 13250
11171 return pattern; 13251 /**
11172 }, 13252 * Update the list of items, starting from the `_virtualStart` item.
11173 13253 * @param {!Array<number>=} itemSet
11174 ready: function() { 13254 * @param {!Array<number>=} movingUp
11175 this.bindValue = this.value; 13255 */
11176 }, 13256 _update: function(itemSet, movingUp) {
11177 13257 // manage focus
11178 /** 13258 this._manageFocus();
11179 * @suppress {checkTypes} 13259 // update models
11180 */ 13260 this._assignModels(itemSet);
11181 _bindValueChanged: function() { 13261 // measure heights
11182 if (this.value !== this.bindValue) { 13262 this._updateMetrics(itemSet);
11183 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue; 13263 // adjust offset after measuring
11184 } 13264 if (movingUp) {
11185 // manually notify because we don't want to notify until after setting val ue 13265 while (movingUp.length) {
11186 this.fire('bind-value-changed', {value: this.bindValue}); 13266 var idx = movingUp.pop();
11187 }, 13267 this._physicalTop -= this._getPhysicalSizeIncrement(idx);
11188 13268 }
11189 _allowedPatternChanged: function() { 13269 }
11190 // Force to prevent invalid input when an `allowed-pattern` is set 13270 // update the position of the items
11191 this.preventInvalidInput = this.allowedPattern ? true : false; 13271 this._positionItems();
11192 }, 13272 // set the scroller size
11193 13273 this._updateScrollerSize();
11194 _onInput: function() { 13274 // increase the pool of physical items
11195 // Need to validate each of the characters pasted if they haven't 13275 this._increasePoolIfNeeded();
11196 // been validated inside `_onKeypress` already. 13276 },
11197 if (this.preventInvalidInput && !this._patternAlreadyChecked) { 13277
11198 var valid = this._checkPatternValidity(); 13278 /**
11199 if (!valid) { 13279 * Creates a pool of DOM elements and attaches them to the local dom.
11200 this._announceInvalidCharacter('Invalid string of characters not enter ed.'); 13280 */
11201 this.value = this._previousValidInput; 13281 _createPool: function(size) {
11202 } 13282 var physicalItems = new Array(size);
11203 } 13283
11204 13284 this._ensureTemplatized();
11205 this.bindValue = this.value; 13285
11206 this._previousValidInput = this.value; 13286 for (var i = 0; i < size; i++) {
11207 this._patternAlreadyChecked = false; 13287 var inst = this.stamp(null);
11208 }, 13288 // First element child is item; Safari doesn't support children[0]
11209 13289 // on a doc fragment
11210 _isPrintable: function(event) { 13290 physicalItems[i] = inst.root.querySelector('*');
11211 // What a control/printable character is varies wildly based on the browse r. 13291 Polymer.dom(this).appendChild(inst.root);
11212 // - most control characters (arrows, backspace) do not send a `keypress` event 13292 }
11213 // in Chrome, but the *do* on Firefox 13293 return physicalItems;
11214 // - in Firefox, when they do send a `keypress` event, control chars have 13294 },
11215 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) 13295
11216 // - printable characters always send a keypress event. 13296 /**
11217 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode 13297 * Increases the pool of physical items only if needed.
11218 // always matches the charCode. 13298 *
11219 // None of this makes any sense. 13299 * @return {boolean} True if the pool was increased.
11220 13300 */
11221 // For these keys, ASCII code == browser keycode. 13301 _increasePoolIfNeeded: function() {
11222 var anyNonPrintable = 13302 // Base case 1: the list has no height.
11223 (event.keyCode == 8) || // backspace 13303 if (this._viewportHeight === 0) {
11224 (event.keyCode == 9) || // tab 13304 return false;
11225 (event.keyCode == 13) || // enter 13305 }
11226 (event.keyCode == 27); // escape 13306 // Base case 2: If the physical size is optimal and the list's client heig ht is full
11227 13307 // with physical items, don't increase the pool.
11228 // For these keys, make sure it's a browser keycode and not an ASCII code. 13308 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi s._physicalTop <= this._scrollPosition;
11229 var mozNonPrintable = 13309 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
11230 (event.keyCode == 19) || // pause 13310 return false;
11231 (event.keyCode == 20) || // caps lock 13311 }
11232 (event.keyCode == 45) || // insert 13312 // this value should range between [0 <= `currentPage` <= `_maxPages`]
11233 (event.keyCode == 46) || // delete 13313 var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
11234 (event.keyCode == 144) || // num lock 13314
11235 (event.keyCode == 145) || // scroll lock 13315 if (currentPage === 0) {
11236 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho me, arrows 13316 // fill the first page
11237 (event.keyCode > 111 && event.keyCode < 124); // fn keys 13317 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5)));
11238 13318 } else if (this._lastPage !== currentPage && isClientHeightFull) {
11239 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); 13319 // paint the page and defer the next increase
11240 }, 13320 // wait 16ms which is rough enough to get paint cycle.
11241 13321 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, this._itemsPerRow), 16));
11242 _onKeypress: function(event) { 13322 } else {
11243 if (!this.preventInvalidInput && this.type !== 'number') { 13323 // fill the rest of the pages
13324 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow)) ;
13325 }
13326
13327 this._lastPage = currentPage;
13328
13329 return true;
13330 },
13331
13332 /**
13333 * Increases the pool size.
13334 */
13335 _increasePool: function(missingItems) {
13336 var nextPhysicalCount = Math.min(
13337 this._physicalCount + missingItems,
13338 this._virtualCount - this._virtualStart,
13339 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
13340 );
13341 var prevPhysicalCount = this._physicalCount;
13342 var delta = nextPhysicalCount - prevPhysicalCount;
13343
13344 if (delta <= 0) {
11244 return; 13345 return;
11245 } 13346 }
11246 var regexp = this._patternRegExp; 13347
11247 if (!regexp) { 13348 [].push.apply(this._physicalItems, this._createPool(delta));
13349 [].push.apply(this._physicalSizes, new Array(delta));
13350
13351 this._physicalCount = prevPhysicalCount + delta;
13352
13353 // update the physical start if we need to preserve the model of the focus ed item.
13354 // In this situation, the focused item is currently rendered and its model would
13355 // have changed after increasing the pool if the physical start remained u nchanged.
13356 if (this._physicalStart > this._physicalEnd &&
13357 this._isIndexRendered(this._focusedIndex) &&
13358 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
13359 this._physicalStart = this._physicalStart + delta;
13360 }
13361 this._update();
13362 },
13363
13364 /**
13365 * Render a new list of items. This method does exactly the same as `update` ,
13366 * but it also ensures that only one `update` cycle is created.
13367 */
13368 _render: function() {
13369 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
13370
13371 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
13372 this._lastPage = 0;
13373 this._update();
13374 this._itemsRendered = true;
13375 }
13376 },
13377
13378 /**
13379 * Templetizes the user template.
13380 */
13381 _ensureTemplatized: function() {
13382 if (!this.ctor) {
13383 // Template instance props that should be excluded from forwarding
13384 var props = {};
13385 props.__key__ = true;
13386 props[this.as] = true;
13387 props[this.indexAs] = true;
13388 props[this.selectedAs] = true;
13389 props.tabIndex = true;
13390
13391 this._instanceProps = props;
13392 this._userTemplate = Polymer.dom(this).querySelector('template');
13393
13394 if (this._userTemplate) {
13395 this.templatize(this._userTemplate);
13396 } else {
13397 console.warn('iron-list requires a template to be provided in light-do m');
13398 }
13399 }
13400 },
13401
13402 /**
13403 * Implements extension point from Templatizer mixin.
13404 */
13405 _getStampedChildren: function() {
13406 return this._physicalItems;
13407 },
13408
13409 /**
13410 * Implements extension point from Templatizer
13411 * Called as a side effect of a template instance path change, responsible
13412 * for notifying items.<key-for-instance>.<path> change up to host.
13413 */
13414 _forwardInstancePath: function(inst, path, value) {
13415 if (path.indexOf(this.as + '.') === 0) {
13416 this.notifyPath('items.' + inst.__key__ + '.' +
13417 path.slice(this.as.length + 1), value);
13418 }
13419 },
13420
13421 /**
13422 * Implements extension point from Templatizer mixin
13423 * Called as side-effect of a host property change, responsible for
13424 * notifying parent path change on each row.
13425 */
13426 _forwardParentProp: function(prop, value) {
13427 if (this._physicalItems) {
13428 this._physicalItems.forEach(function(item) {
13429 item._templateInstance[prop] = value;
13430 }, this);
13431 }
13432 },
13433
13434 /**
13435 * Implements extension point from Templatizer
13436 * Called as side-effect of a host path change, responsible for
13437 * notifying parent.<path> path change on each row.
13438 */
13439 _forwardParentPath: function(path, value) {
13440 if (this._physicalItems) {
13441 this._physicalItems.forEach(function(item) {
13442 item._templateInstance.notifyPath(path, value, true);
13443 }, this);
13444 }
13445 },
13446
13447 /**
13448 * Called as a side effect of a host items.<key>.<path> path change,
13449 * responsible for notifying item.<path> changes.
13450 */
13451 _forwardItemPath: function(path, value) {
13452 if (!this._physicalIndexForKey) {
11248 return; 13453 return;
11249 } 13454 }
11250 13455 var dot = path.indexOf('.');
11251 // Handle special keys and backspace 13456 var key = path.substring(0, dot < 0 ? path.length : dot);
11252 if (event.metaKey || event.ctrlKey || event.altKey) 13457 var idx = this._physicalIndexForKey[key];
13458 var offscreenItem = this._offscreenFocusedItem;
13459 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
13460 offscreenItem : this._physicalItems[idx];
13461
13462 if (!el || el._templateInstance.__key__ !== key) {
11253 return; 13463 return;
11254 13464 }
11255 // Check the pattern either here or in `_onInput`, but not in both. 13465 if (dot >= 0) {
11256 this._patternAlreadyChecked = true; 13466 path = this.as + '.' + path.substring(dot+1);
11257 13467 el._templateInstance.notifyPath(path, value, true);
11258 var thisChar = String.fromCharCode(event.charCode); 13468 } else {
11259 if (this._isPrintable(event) && !regexp.test(thisChar)) { 13469 // Update selection if needed
11260 event.preventDefault(); 13470 var currentItem = el._templateInstance[this.as];
11261 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e ntered.'); 13471 if (Array.isArray(this.selectedItems)) {
11262 } 13472 for (var i = 0; i < this.selectedItems.length; i++) {
11263 }, 13473 if (this.selectedItems[i] === currentItem) {
11264 13474 this.set('selectedItems.' + i, value);
11265 _checkPatternValidity: function() { 13475 break;
11266 var regexp = this._patternRegExp; 13476 }
11267 if (!regexp) { 13477 }
11268 return true; 13478 } else if (this.selectedItem === currentItem) {
11269 } 13479 this.set('selectedItem', value);
11270 for (var i = 0; i < this.value.length; i++) { 13480 }
11271 if (!regexp.test(this.value[i])) { 13481 el._templateInstance[this.as] = value;
11272 return false; 13482 }
11273 } 13483 },
11274 } 13484
11275 return true; 13485 /**
11276 }, 13486 * Called when the items have changed. That is, ressignments
11277 13487 * to `items`, splices or updates to a single item.
11278 /** 13488 */
11279 * Returns true if `value` is valid. The validator provided in `validator` w ill be used first, 13489 _itemsChanged: function(change) {
11280 * then any constraints. 13490 if (change.path === 'items') {
11281 * @return {boolean} True if the value is valid. 13491 // reset items
11282 */ 13492 this._virtualStart = 0;
11283 validate: function() { 13493 this._physicalTop = 0;
11284 // First, check what the browser thinks. Some inputs (like type=number) 13494 this._virtualCount = this.items ? this.items.length : 0;
11285 // behave weirdly and will set the value to "" if something invalid is 13495 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
11286 // entered, but will set the validity correctly. 13496 this._physicalIndexForKey = {};
11287 var valid = this.checkValidity(); 13497 this._firstVisibleIndexVal = null;
11288 13498 this._lastVisibleIndexVal = null;
11289 // Only do extra checking if the browser thought this was valid. 13499
11290 if (valid) { 13500 this._resetScrollPosition(0);
11291 // Empty, required input is invalid 13501 this._removeFocusedItem();
11292 if (this.required && this.value === '') { 13502 // create the initial physical items
11293 valid = false; 13503 if (!this._physicalItems) {
11294 } else if (this.hasValidator()) { 13504 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
11295 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value ); 13505 this._physicalItems = this._createPool(this._physicalCount);
11296 } 13506 this._physicalSizes = new Array(this._physicalCount);
11297 } 13507 }
11298 13508
11299 this.invalid = !valid; 13509 this._physicalStart = 0;
11300 this.fire('iron-input-validate'); 13510
11301 return valid; 13511 } else if (change.path === 'items.splices') {
11302 }, 13512
11303 13513 this._adjustVirtualIndex(change.value.indexSplices);
11304 _announceInvalidCharacter: function(message) { 13514 this._virtualCount = this.items ? this.items.length : 0;
11305 this.fire('iron-announce', { text: message }); 13515
13516 } else {
13517 // update a single item
13518 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
13519 return;
13520 }
13521
13522 this._itemsRendered = false;
13523 this._debounceTemplate(this._render);
13524 },
13525
13526 /**
13527 * @param {!Array<!PolymerSplice>} splices
13528 */
13529 _adjustVirtualIndex: function(splices) {
13530 splices.forEach(function(splice) {
13531 // deselect removed items
13532 splice.removed.forEach(this._removeItem, this);
13533 // We only need to care about changes happening above the current positi on
13534 if (splice.index < this._virtualStart) {
13535 var delta = Math.max(
13536 splice.addedCount - splice.removed.length,
13537 splice.index - this._virtualStart);
13538
13539 this._virtualStart = this._virtualStart + delta;
13540
13541 if (this._focusedIndex >= 0) {
13542 this._focusedIndex = this._focusedIndex + delta;
13543 }
13544 }
13545 }, this);
13546 },
13547
13548 _removeItem: function(item) {
13549 this.$.selector.deselect(item);
13550 // remove the current focused item
13551 if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
13552 this._removeFocusedItem();
13553 }
13554 },
13555
13556 /**
13557 * Executes a provided function per every physical index in `itemSet`
13558 * `itemSet` default value is equivalent to the entire set of physical index es.
13559 *
13560 * @param {!function(number, number)} fn
13561 * @param {!Array<number>=} itemSet
13562 */
13563 _iterateItems: function(fn, itemSet) {
13564 var pidx, vidx, rtn, i;
13565
13566 if (arguments.length === 2 && itemSet) {
13567 for (i = 0; i < itemSet.length; i++) {
13568 pidx = itemSet[i];
13569 vidx = this._computeVidx(pidx);
13570 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13571 return rtn;
13572 }
13573 }
13574 } else {
13575 pidx = this._physicalStart;
13576 vidx = this._virtualStart;
13577
13578 for (; pidx < this._physicalCount; pidx++, vidx++) {
13579 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13580 return rtn;
13581 }
13582 }
13583 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
13584 if ((rtn = fn.call(this, pidx, vidx)) != null) {
13585 return rtn;
13586 }
13587 }
13588 }
13589 },
13590
13591 /**
13592 * Returns the virtual index for a given physical index
13593 *
13594 * @param {number} pidx Physical index
13595 * @return {number}
13596 */
13597 _computeVidx: function(pidx) {
13598 if (pidx >= this._physicalStart) {
13599 return this._virtualStart + (pidx - this._physicalStart);
13600 }
13601 return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
13602 },
13603
13604 /**
13605 * Assigns the data models to a given set of items.
13606 * @param {!Array<number>=} itemSet
13607 */
13608 _assignModels: function(itemSet) {
13609 this._iterateItems(function(pidx, vidx) {
13610 var el = this._physicalItems[pidx];
13611 var inst = el._templateInstance;
13612 var item = this.items && this.items[vidx];
13613
13614 if (item != null) {
13615 inst[this.as] = item;
13616 inst.__key__ = this._collection.getKey(item);
13617 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item);
13618 inst[this.indexAs] = vidx;
13619 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
13620 this._physicalIndexForKey[inst.__key__] = pidx;
13621 el.removeAttribute('hidden');
13622 } else {
13623 inst.__key__ = null;
13624 el.setAttribute('hidden', '');
13625 }
13626 }, itemSet);
13627 },
13628
13629 /**
13630 * Updates the height for a given set of items.
13631 *
13632 * @param {!Array<number>=} itemSet
13633 */
13634 _updateMetrics: function(itemSet) {
13635 // Make sure we distributed all the physical items
13636 // so we can measure them
13637 Polymer.dom.flush();
13638
13639 var newPhysicalSize = 0;
13640 var oldPhysicalSize = 0;
13641 var prevAvgCount = this._physicalAverageCount;
13642 var prevPhysicalAvg = this._physicalAverage;
13643
13644 this._iterateItems(function(pidx, vidx) {
13645
13646 oldPhysicalSize += this._physicalSizes[pidx] || 0;
13647 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
13648 newPhysicalSize += this._physicalSizes[pidx];
13649 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
13650
13651 }, itemSet);
13652
13653 this._viewportHeight = this._scrollTargetHeight;
13654 if (this.grid) {
13655 this._updateGridMetrics();
13656 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
13657 } else {
13658 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS ize;
13659 }
13660
13661 // update the average if we measured something
13662 if (this._physicalAverageCount !== prevAvgCount) {
13663 this._physicalAverage = Math.round(
13664 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
13665 this._physicalAverageCount);
13666 }
13667 },
13668
13669 _updateGridMetrics: function() {
13670 this._viewportWidth = this.$.items.offsetWidth;
13671 // Set item width to the value of the _physicalItems offsetWidth
13672 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun dingClientRect().width : DEFAULT_GRID_SIZE;
13673 // Set row height to the value of the _physicalItems offsetHeight
13674 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH eight : DEFAULT_GRID_SIZE;
13675 // If in grid mode compute how many items with exist in each row
13676 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi s._itemWidth) : this._itemsPerRow;
13677 },
13678
13679 /**
13680 * Updates the position of the physical items.
13681 */
13682 _positionItems: function() {
13683 this._adjustScrollPosition();
13684
13685 var y = this._physicalTop;
13686
13687 if (this.grid) {
13688 var totalItemWidth = this._itemsPerRow * this._itemWidth;
13689 var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
13690
13691 this._iterateItems(function(pidx, vidx) {
13692
13693 var modulus = vidx % this._itemsPerRow;
13694 var x = Math.floor((modulus * this._itemWidth) + rowOffset);
13695
13696 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
13697
13698 if (this._shouldRenderNextRow(vidx)) {
13699 y += this._rowHeight;
13700 }
13701
13702 });
13703 } else {
13704 this._iterateItems(function(pidx, vidx) {
13705
13706 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
13707 y += this._physicalSizes[pidx];
13708
13709 });
13710 }
13711 },
13712
13713 _getPhysicalSizeIncrement: function(pidx) {
13714 if (!this.grid) {
13715 return this._physicalSizes[pidx];
13716 }
13717 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
13718 return 0;
13719 }
13720 return this._rowHeight;
13721 },
13722
13723 /**
13724 * Returns, based on the current index,
13725 * whether or not the next index will need
13726 * to be rendered on a new row.
13727 *
13728 * @param {number} vidx Virtual index
13729 * @return {boolean}
13730 */
13731 _shouldRenderNextRow: function(vidx) {
13732 return vidx % this._itemsPerRow === this._itemsPerRow - 1;
13733 },
13734
13735 /**
13736 * Adjusts the scroll position when it was overestimated.
13737 */
13738 _adjustScrollPosition: function() {
13739 var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
13740 Math.min(this._scrollPosition + this._physicalTop, 0);
13741
13742 if (deltaHeight) {
13743 this._physicalTop = this._physicalTop - deltaHeight;
13744 // juking scroll position during interial scrolling on iOS is no bueno
13745 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
13746 this._resetScrollPosition(this._scrollTop - deltaHeight);
13747 }
13748 }
13749 },
13750
13751 /**
13752 * Sets the position of the scroll.
13753 */
13754 _resetScrollPosition: function(pos) {
13755 if (this.scrollTarget) {
13756 this._scrollTop = pos;
13757 this._scrollPosition = this._scrollTop;
13758 }
13759 },
13760
13761 /**
13762 * Sets the scroll height, that's the height of the content,
13763 *
13764 * @param {boolean=} forceUpdate If true, updates the height no matter what.
13765 */
13766 _updateScrollerSize: function(forceUpdate) {
13767 if (this.grid) {
13768 this._estScrollHeight = this._virtualRowCount * this._rowHeight;
13769 } else {
13770 this._estScrollHeight = (this._physicalBottom +
13771 Math.max(this._virtualCount - this._physicalCount - this._virtualSta rt, 0) * this._physicalAverage);
13772 }
13773
13774 forceUpdate = forceUpdate || this._scrollHeight === 0;
13775 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
13776 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this ._estScrollHeight;
13777
13778 // amortize height adjustment, so it won't trigger repaints very often
13779 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
13780 this.$.items.style.height = this._estScrollHeight + 'px';
13781 this._scrollHeight = this._estScrollHeight;
13782 }
13783 },
13784
13785 /**
13786 * Scroll to a specific item in the virtual list regardless
13787 * of the physical items in the DOM tree.
13788 *
13789 * @method scrollToItem
13790 * @param {(Object)} item The item to be scrolled to
13791 */
13792 scrollToItem: function(item){
13793 return this.scrollToIndex(this.items.indexOf(item));
13794 },
13795
13796 /**
13797 * Scroll to a specific index in the virtual list regardless
13798 * of the physical items in the DOM tree.
13799 *
13800 * @method scrollToIndex
13801 * @param {number} idx The index of the item
13802 */
13803 scrollToIndex: function(idx) {
13804 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
13805 return;
13806 }
13807
13808 Polymer.dom.flush();
13809
13810 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
13811 // update the virtual start only when needed
13812 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
13813 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
13814 }
13815 // manage focus
13816 this._manageFocus();
13817 // assign new models
13818 this._assignModels();
13819 // measure the new sizes
13820 this._updateMetrics();
13821
13822 // estimate new physical offset
13823 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
13824 this._physicalTop = estPhysicalTop;
13825
13826 var currentTopItem = this._physicalStart;
13827 var currentVirtualItem = this._virtualStart;
13828 var targetOffsetTop = 0;
13829 var hiddenContentSize = this._hiddenContentSize;
13830
13831 // scroll to the item as much as we can
13832 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
13833 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre ntTopItem);
13834 currentTopItem = (currentTopItem + 1) % this._physicalCount;
13835 currentVirtualItem++;
13836 }
13837 // update the scroller size
13838 this._updateScrollerSize(true);
13839 // update the position of the items
13840 this._positionItems();
13841 // set the new scroll position
13842 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop);
13843 // increase the pool of physical items if needed
13844 this._increasePoolIfNeeded();
13845 // clear cached visible index
13846 this._firstVisibleIndexVal = null;
13847 this._lastVisibleIndexVal = null;
13848 },
13849
13850 /**
13851 * Reset the physical average and the average count.
13852 */
13853 _resetAverage: function() {
13854 this._physicalAverage = 0;
13855 this._physicalAverageCount = 0;
13856 },
13857
13858 /**
13859 * A handler for the `iron-resize` event triggered by `IronResizableBehavior `
13860 * when the element is resized.
13861 */
13862 _resizeHandler: function() {
13863 // iOS fires the resize event when the address bar slides up
13864 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100 ) {
13865 return;
13866 }
13867 // In Desktop Safari 9.0.3, if the scroll bars are always shown,
13868 // changing the scroll position from a resize handler would result in
13869 // the scroll position being reset. Waiting 1ms fixes the issue.
13870 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
13871 this.updateViewportBoundaries();
13872 this._render();
13873
13874 if (this._itemsRendered && this._physicalItems && this._isVisible) {
13875 this._resetAverage();
13876 this.scrollToIndex(this.firstVisibleIndex);
13877 }
13878 }.bind(this), 1));
13879 },
13880
13881 _getModelFromItem: function(item) {
13882 var key = this._collection.getKey(item);
13883 var pidx = this._physicalIndexForKey[key];
13884
13885 if (pidx != null) {
13886 return this._physicalItems[pidx]._templateInstance;
13887 }
13888 return null;
13889 },
13890
13891 /**
13892 * Gets a valid item instance from its index or the object value.
13893 *
13894 * @param {(Object|number)} item The item object or its index
13895 */
13896 _getNormalizedItem: function(item) {
13897 if (this._collection.getKey(item) === undefined) {
13898 if (typeof item === 'number') {
13899 item = this.items[item];
13900 if (!item) {
13901 throw new RangeError('<item> not found');
13902 }
13903 return item;
13904 }
13905 throw new TypeError('<item> should be a valid item');
13906 }
13907 return item;
13908 },
13909
13910 /**
13911 * Select the list item at the given index.
13912 *
13913 * @method selectItem
13914 * @param {(Object|number)} item The item object or its index
13915 */
13916 selectItem: function(item) {
13917 item = this._getNormalizedItem(item);
13918 var model = this._getModelFromItem(item);
13919
13920 if (!this.multiSelection && this.selectedItem) {
13921 this.deselectItem(this.selectedItem);
13922 }
13923 if (model) {
13924 model[this.selectedAs] = true;
13925 }
13926 this.$.selector.select(item);
13927 this.updateSizeForItem(item);
13928 },
13929
13930 /**
13931 * Deselects the given item list if it is already selected.
13932 *
13933
13934 * @method deselect
13935 * @param {(Object|number)} item The item object or its index
13936 */
13937 deselectItem: function(item) {
13938 item = this._getNormalizedItem(item);
13939 var model = this._getModelFromItem(item);
13940
13941 if (model) {
13942 model[this.selectedAs] = false;
13943 }
13944 this.$.selector.deselect(item);
13945 this.updateSizeForItem(item);
13946 },
13947
13948 /**
13949 * Select or deselect a given item depending on whether the item
13950 * has already been selected.
13951 *
13952 * @method toggleSelectionForItem
13953 * @param {(Object|number)} item The item object or its index
13954 */
13955 toggleSelectionForItem: function(item) {
13956 item = this._getNormalizedItem(item);
13957 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item )) {
13958 this.deselectItem(item);
13959 } else {
13960 this.selectItem(item);
13961 }
13962 },
13963
13964 /**
13965 * Clears the current selection state of the list.
13966 *
13967 * @method clearSelection
13968 */
13969 clearSelection: function() {
13970 function unselect(item) {
13971 var model = this._getModelFromItem(item);
13972 if (model) {
13973 model[this.selectedAs] = false;
13974 }
13975 }
13976
13977 if (Array.isArray(this.selectedItems)) {
13978 this.selectedItems.forEach(unselect, this);
13979 } else if (this.selectedItem) {
13980 unselect.call(this, this.selectedItem);
13981 }
13982
13983 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
13984 },
13985
13986 /**
13987 * Add an event listener to `tap` if `selectionEnabled` is true,
13988 * it will remove the listener otherwise.
13989 */
13990 _selectionEnabledChanged: function(selectionEnabled) {
13991 var handler = selectionEnabled ? this.listen : this.unlisten;
13992 handler.call(this, this, 'tap', '_selectionHandler');
13993 },
13994
13995 /**
13996 * Select an item from an event object.
13997 */
13998 _selectionHandler: function(e) {
13999 var model = this.modelForElement(e.target);
14000 if (!model) {
14001 return;
14002 }
14003 var modelTabIndex, activeElTabIndex;
14004 var target = Polymer.dom(e).path[0];
14005 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac tiveElement;
14006 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i ndexAs])];
14007 // Safari does not focus certain form controls via mouse
14008 // https://bugs.webkit.org/show_bug.cgi?id=118043
14009 if (target.localName === 'input' ||
14010 target.localName === 'button' ||
14011 target.localName === 'select') {
14012 return;
14013 }
14014 // Set a temporary tabindex
14015 modelTabIndex = model.tabIndex;
14016 model.tabIndex = SECRET_TABINDEX;
14017 activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
14018 model.tabIndex = modelTabIndex;
14019 // Only select the item if the tap wasn't on a focusable child
14020 // or the element bound to `tabIndex`
14021 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE CRET_TABINDEX) {
14022 return;
14023 }
14024 this.toggleSelectionForItem(model[this.as]);
14025 },
14026
14027 _multiSelectionChanged: function(multiSelection) {
14028 this.clearSelection();
14029 this.$.selector.multi = multiSelection;
14030 },
14031
14032 /**
14033 * Updates the size of an item.
14034 *
14035 * @method updateSizeForItem
14036 * @param {(Object|number)} item The item object or its index
14037 */
14038 updateSizeForItem: function(item) {
14039 item = this._getNormalizedItem(item);
14040 var key = this._collection.getKey(item);
14041 var pidx = this._physicalIndexForKey[key];
14042
14043 if (pidx != null) {
14044 this._updateMetrics([pidx]);
14045 this._positionItems();
14046 }
14047 },
14048
14049 /**
14050 * Creates a temporary backfill item in the rendered pool of physical items
14051 * to replace the main focused item. The focused item has tabIndex = 0
14052 * and might be currently focused by the user.
14053 *
14054 * This dynamic replacement helps to preserve the focus state.
14055 */
14056 _manageFocus: function() {
14057 var fidx = this._focusedIndex;
14058
14059 if (fidx >= 0 && fidx < this._virtualCount) {
14060 // if it's a valid index, check if that index is rendered
14061 // in a physical item.
14062 if (this._isIndexRendered(fidx)) {
14063 this._restoreFocusedItem();
14064 } else {
14065 this._createFocusBackfillItem();
14066 }
14067 } else if (this._virtualCount > 0 && this._physicalCount > 0) {
14068 // otherwise, assign the initial focused index.
14069 this._focusedIndex = this._virtualStart;
14070 this._focusedItem = this._physicalItems[this._physicalStart];
14071 }
14072 },
14073
14074 _isIndexRendered: function(idx) {
14075 return idx >= this._virtualStart && idx <= this._virtualEnd;
14076 },
14077
14078 _isIndexVisible: function(idx) {
14079 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
14080 },
14081
14082 _getPhysicalIndex: function(idx) {
14083 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz edItem(idx))];
14084 },
14085
14086 _focusPhysicalItem: function(idx) {
14087 if (idx < 0 || idx >= this._virtualCount) {
14088 return;
14089 }
14090 this._restoreFocusedItem();
14091 // scroll to index to make sure it's rendered
14092 if (!this._isIndexRendered(idx)) {
14093 this.scrollToIndex(idx);
14094 }
14095
14096 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
14097 var model = physicalItem._templateInstance;
14098 var focusable;
14099
14100 // set a secret tab index
14101 model.tabIndex = SECRET_TABINDEX;
14102 // check if focusable element is the physical item
14103 if (physicalItem.tabIndex === SECRET_TABINDEX) {
14104 focusable = physicalItem;
14105 }
14106 // search for the element which tabindex is bound to the secret tab index
14107 if (!focusable) {
14108 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET_TABINDEX + '"]');
14109 }
14110 // restore the tab index
14111 model.tabIndex = 0;
14112 // focus the focusable element
14113 this._focusedIndex = idx;
14114 focusable && focusable.focus();
14115 },
14116
14117 _removeFocusedItem: function() {
14118 if (this._offscreenFocusedItem) {
14119 Polymer.dom(this).removeChild(this._offscreenFocusedItem);
14120 }
14121 this._offscreenFocusedItem = null;
14122 this._focusBackfillItem = null;
14123 this._focusedItem = null;
14124 this._focusedIndex = -1;
14125 },
14126
14127 _createFocusBackfillItem: function() {
14128 var pidx, fidx = this._focusedIndex;
14129 if (this._offscreenFocusedItem || fidx < 0) {
14130 return;
14131 }
14132 if (!this._focusBackfillItem) {
14133 // create a physical item, so that it backfills the focused item.
14134 var stampedTemplate = this.stamp(null);
14135 this._focusBackfillItem = stampedTemplate.root.querySelector('*');
14136 Polymer.dom(this).appendChild(stampedTemplate.root);
14137 }
14138 // get the physical index for the focused index
14139 pidx = this._getPhysicalIndex(fidx);
14140
14141 if (pidx != null) {
14142 // set the offcreen focused physical item
14143 this._offscreenFocusedItem = this._physicalItems[pidx];
14144 // backfill the focused physical item
14145 this._physicalItems[pidx] = this._focusBackfillItem;
14146 // hide the focused physical
14147 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
14148 }
14149 },
14150
14151 _restoreFocusedItem: function() {
14152 var pidx, fidx = this._focusedIndex;
14153
14154 if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
14155 return;
14156 }
14157 // assign models to the focused index
14158 this._assignModels();
14159 // get the new physical index for the focused index
14160 pidx = this._getPhysicalIndex(fidx);
14161
14162 if (pidx != null) {
14163 // flip the focus backfill
14164 this._focusBackfillItem = this._physicalItems[pidx];
14165 // restore the focused physical item
14166 this._physicalItems[pidx] = this._offscreenFocusedItem;
14167 // reset the offscreen focused item
14168 this._offscreenFocusedItem = null;
14169 // hide the physical item that backfills
14170 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
14171 }
14172 },
14173
14174 _didFocus: function(e) {
14175 var targetModel = this.modelForElement(e.target);
14176 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
14177 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
14178 var fidx = this._focusedIndex;
14179
14180 if (!targetModel || !focusedModel) {
14181 return;
14182 }
14183 if (focusedModel === targetModel) {
14184 // if the user focused the same item, then bring it into view if it's no t visible
14185 if (!this._isIndexVisible(fidx)) {
14186 this.scrollToIndex(fidx);
14187 }
14188 } else {
14189 this._restoreFocusedItem();
14190 // restore tabIndex for the currently focused item
14191 focusedModel.tabIndex = -1;
14192 // set the tabIndex for the next focused item
14193 targetModel.tabIndex = 0;
14194 fidx = targetModel[this.indexAs];
14195 this._focusedIndex = fidx;
14196 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
14197
14198 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
14199 this._update();
14200 }
14201 }
14202 },
14203
14204 _didMoveUp: function() {
14205 this._focusPhysicalItem(this._focusedIndex - 1);
14206 },
14207
14208 _didMoveDown: function(e) {
14209 // disable scroll when pressing the down key
14210 e.detail.keyboardEvent.preventDefault();
14211 this._focusPhysicalItem(this._focusedIndex + 1);
14212 },
14213
14214 _didEnter: function(e) {
14215 this._focusPhysicalItem(this._focusedIndex);
14216 this._selectionHandler(e.detail.keyboardEvent);
11306 } 14217 }
11307 }); 14218 });
11308 14219
11309 /* 14220 })();
11310 The `iron-input-validate` event is fired whenever `validate()` is called.
11311 @event iron-input-validate
11312 */
11313 Polymer({ 14221 Polymer({
11314 is: 'paper-input-container', 14222
14223 is: 'iron-scroll-threshold',
11315 14224
11316 properties: { 14225 properties: {
14226
11317 /** 14227 /**
11318 * Set to true to disable the floating label. The label disappears when th e input value is 14228 * Distance from the top (or left, for horizontal) bound of the scroller
11319 * not null. 14229 * where the "upper trigger" will fire.
11320 */ 14230 */
11321 noLabelFloat: { 14231 upperThreshold: {
14232 type: Number,
14233 value: 100
14234 },
14235
14236 /**
14237 * Distance from the bottom (or right, for horizontal) bound of the scroll er
14238 * where the "lower trigger" will fire.
14239 */
14240 lowerThreshold: {
14241 type: Number,
14242 value: 100
14243 },
14244
14245 /**
14246 * Read-only value that tracks the triggered state of the upper threshold.
14247 */
14248 upperTriggered: {
14249 type: Boolean,
14250 value: false,
14251 notify: true,
14252 readOnly: true
14253 },
14254
14255 /**
14256 * Read-only value that tracks the triggered state of the lower threshold.
14257 */
14258 lowerTriggered: {
14259 type: Boolean,
14260 value: false,
14261 notify: true,
14262 readOnly: true
14263 },
14264
14265 /**
14266 * True if the orientation of the scroller is horizontal.
14267 */
14268 horizontal: {
11322 type: Boolean, 14269 type: Boolean,
11323 value: false 14270 value: false
11324 }, 14271 }
11325 14272 },
11326 /** 14273
11327 * Set to true to always float the floating label. 14274 behaviors: [
11328 */ 14275 Polymer.IronScrollTargetBehavior
11329 alwaysFloatLabel: { 14276 ],
11330 type: Boolean, 14277
11331 value: false 14278 observers: [
11332 }, 14279 '_setOverflow(scrollTarget)',
11333 14280 '_initCheck(horizontal, isAttached)'
11334 /** 14281 ],
11335 * The attribute to listen for value changes on. 14282
11336 */ 14283 get _defaultScrollTarget() {
11337 attrForValue: { 14284 return this;
11338 type: String, 14285 },
11339 value: 'bind-value' 14286
11340 }, 14287 _setOverflow: function(scrollTarget) {
11341 14288 this.style.overflow = scrollTarget === this ? 'auto' : '';
11342 /** 14289 },
11343 * Set to true to auto-validate the input value when it changes. 14290
11344 */ 14291 _scrollHandler: function() {
11345 autoValidate: { 14292 // throttle the work on the scroll event
11346 type: Boolean, 14293 var THROTTLE_THRESHOLD = 200;
11347 value: false 14294 if (!this.isDebouncerActive('_checkTheshold')) {
11348 }, 14295 this.debounce('_checkTheshold', function() {
11349 14296 this.checkScrollThesholds();
11350 /** 14297 }, THROTTLE_THRESHOLD);
11351 * True if the input is invalid. This property is set automatically when t he input value 14298 }
11352 * changes if auto-validating, or when the `iron-input-validate` event is heard from a child. 14299 },
11353 */ 14300
11354 invalid: { 14301 _initCheck: function(horizontal, isAttached) {
11355 observer: '_invalidChanged', 14302 if (isAttached) {
11356 type: Boolean, 14303 this.debounce('_init', function() {
11357 value: false 14304 this.clearTriggers();
11358 }, 14305 this.checkScrollThesholds();
11359 14306 });
11360 /** 14307 }
11361 * True if the input has focus. 14308 },
11362 */ 14309
11363 focused: { 14310 /**
11364 readOnly: true, 14311 * Checks the scroll thresholds.
11365 type: Boolean, 14312 * This method is automatically called by iron-scroll-threshold.
11366 value: false, 14313 *
11367 notify: true 14314 * @method checkScrollThesholds
11368 }, 14315 */
11369 14316 checkScrollThesholds: function() {
11370 _addons: { 14317 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
11371 type: Array 14318 return;
11372 // do not set a default value here intentionally - it will be initialize d lazily when a 14319 }
11373 // distributed child is attached, which may occur before configuration f or this element 14320 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo p;
11374 // in polyfill. 14321 var lowerScrollValue = this.horizontal ?
11375 }, 14322 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll Left :
11376 14323 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._ scrollTop;
11377 _inputHasContent: { 14324
11378 type: Boolean, 14325 // Detect upper threshold
11379 value: false 14326 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
11380 }, 14327 this._setUpperTriggered(true);
11381 14328 this.fire('upper-threshold');
11382 _inputSelector: { 14329 }
11383 type: String, 14330 // Detect lower threshold
11384 value: 'input,textarea,.paper-input-input' 14331 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
11385 }, 14332 this._setLowerTriggered(true);
11386 14333 this.fire('lower-threshold');
11387 _boundOnFocus: { 14334 }
11388 type: Function, 14335 },
11389 value: function() { 14336
11390 return this._onFocus.bind(this); 14337 /**
11391 } 14338 * Clear the upper and lower threshold states.
11392 }, 14339 *
11393 14340 * @method clearTriggers
11394 _boundOnBlur: { 14341 */
11395 type: Function, 14342 clearTriggers: function() {
11396 value: function() { 14343 this._setUpperTriggered(false);
11397 return this._onBlur.bind(this); 14344 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 } 14345 }
14346
14347 /**
14348 * Fires when the lower threshold has been reached.
14349 *
14350 * @event lower-threshold
14351 */
14352
14353 /**
14354 * Fires when the upper threshold has been reached.
14355 *
14356 * @event upper-threshold
14357 */
14358
11591 }); 14359 });
11592 // Copyright 2015 The Chromium Authors. All rights reserved. 14360 // Copyright 2015 The Chromium Authors. All rights reserved.
11593 // Use of this source code is governed by a BSD-style license that can be 14361 // Use of this source code is governed by a BSD-style license that can be
11594 // found in the LICENSE file. 14362 // found in the LICENSE file.
11595 14363
11596 var SearchField = Polymer({ 14364 Polymer({
11597 is: 'cr-search-field', 14365 is: 'history-list',
11598
11599 behaviors: [CrSearchFieldBehavior],
11600 14366
11601 properties: { 14367 properties: {
11602 value_: String, 14368 // The search term for the current query. Set when the query returns.
11603 }, 14369 searchedTerm: {
11604 14370 type: String,
11605 /** @return {!HTMLInputElement} */ 14371 value: '',
11606 getSearchInput: function() { 14372 },
11607 return this.$.searchInput; 14373
14374 lastSearchedTerm_: String,
14375
14376 querying: Boolean,
14377
14378 // An array of history entries in reverse chronological order.
14379 historyData_: Array,
14380
14381 resultLoadingDisabled_: {
14382 type: Boolean,
14383 value: false,
14384 },
14385 },
14386
14387 listeners: {
14388 'infinite-list.scroll': 'notifyListScroll_',
14389 'remove-bookmark-stars': 'removeBookmarkStars_',
14390 },
14391
14392 /** @override */
14393 attached: function() {
14394 // It is possible (eg, when middle clicking the reload button) for all other
14395 // resize events to fire before the list is attached and can be measured.
14396 // Adding another resize here ensures it will get sized correctly.
14397 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize();
14398 },
14399
14400 /**
14401 * Remove bookmark star for history items with matching URLs.
14402 * @param {{detail: !string}} e
14403 * @private
14404 */
14405 removeBookmarkStars_: function(e) {
14406 var url = e.detail;
14407
14408 if (this.historyData_ === undefined)
14409 return;
14410
14411 for (var i = 0; i < this.historyData_.length; i++) {
14412 if (this.historyData_[i].url == url)
14413 this.set('historyData_.' + i + '.starred', false);
14414 }
14415 },
14416
14417 /**
14418 * Disables history result loading when there are no more history results.
14419 */
14420 disableResultLoading: function() {
14421 this.resultLoadingDisabled_ = true;
14422 },
14423
14424 /**
14425 * Adds the newly updated history results into historyData_. Adds new fields
14426 * for each result.
14427 * @param {!Array<!HistoryEntry>} historyResults The new history results.
14428 */
14429 addNewResults: function(historyResults) {
14430 var results = historyResults.slice();
14431 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold'])
14432 .clearTriggers();
14433
14434 if (this.lastSearchedTerm_ != this.searchedTerm) {
14435 this.resultLoadingDisabled_ = false;
14436 if (this.historyData_)
14437 this.splice('historyData_', 0, this.historyData_.length);
14438 this.fire('unselect-all');
14439 this.lastSearchedTerm_ = this.searchedTerm;
14440 }
14441
14442 if (this.historyData_) {
14443 // If we have previously received data, push the new items onto the
14444 // existing array.
14445 results.unshift('historyData_');
14446 this.push.apply(this, results);
14447 } else {
14448 // The first time we receive data, use set() to ensure the iron-list is
14449 // initialized correctly.
14450 this.set('historyData_', results);
14451 }
14452 },
14453
14454 /**
14455 * Cycle through each entry in historyData_ and set all items to be
14456 * unselected.
14457 * @param {number} overallItemCount The number of checkboxes selected.
14458 */
14459 unselectAllItems: function(overallItemCount) {
14460 if (this.historyData_ === undefined)
14461 return;
14462
14463 for (var i = 0; i < this.historyData_.length; i++) {
14464 if (this.historyData_[i].selected) {
14465 this.set('historyData_.' + i + '.selected', false);
14466 overallItemCount--;
14467 if (overallItemCount == 0)
14468 break;
14469 }
14470 }
14471 },
14472
14473 /**
14474 * Remove the given |items| from the list. Expected to be called after the
14475 * items are removed from the backend.
14476 * @param {!Array<!HistoryEntry>} removalList
14477 * @private
14478 */
14479 removeDeletedHistory_: function(removalList) {
14480 // This set is only for speed. Note that set inclusion for objects is by
14481 // reference, so this relies on the HistoryEntry objects never being copied.
14482 var deletedItems = new Set(removalList);
14483 var splices = [];
14484
14485 for (var i = this.historyData_.length - 1; i >= 0; i--) {
14486 var item = this.historyData_[i];
14487 if (deletedItems.has(item)) {
14488 // Removes the selected item from historyData_. Use unshift so
14489 // |splices| ends up in index order.
14490 splices.unshift({
14491 index: i,
14492 removed: [item],
14493 addedCount: 0,
14494 object: this.historyData_,
14495 type: 'splice'
14496 });
14497 this.historyData_.splice(i, 1);
14498 }
14499 }
14500 // notifySplices gives better performance than individually splicing as it
14501 // batches all of the updates together.
14502 this.notifySplices('historyData_', splices);
14503 },
14504
14505 /**
14506 * Performs a request to the backend to delete all selected items. If
14507 * successful, removes them from the view. Does not prompt the user before
14508 * deleting -- see <history-list-container> for a version of this method which
14509 * does prompt.
14510 */
14511 deleteSelected: function() {
14512 var toBeRemoved = this.historyData_.filter(function(item) {
14513 return item.selected;
14514 });
14515 md_history.BrowserService.getInstance()
14516 .deleteItems(toBeRemoved)
14517 .then(function(items) {
14518 this.removeDeletedHistory_(items);
14519 this.fire('unselect-all');
14520 }.bind(this));
14521 },
14522
14523 /**
14524 * Called when the page is scrolled to near the bottom of the list.
14525 * @private
14526 */
14527 loadMoreData_: function() {
14528 if (this.resultLoadingDisabled_ || this.querying)
14529 return;
14530
14531 this.fire('load-more-history');
14532 },
14533
14534 /**
14535 * Check whether the time difference between the given history item and the
14536 * next one is large enough for a spacer to be required.
14537 * @param {HistoryEntry} item
14538 * @param {number} index The index of |item| in |historyData_|.
14539 * @param {number} length The length of |historyData_|.
14540 * @return {boolean} Whether or not time gap separator is required.
14541 * @private
14542 */
14543 needsTimeGap_: function(item, index, length) {
14544 return md_history.HistoryItem.needsTimeGap(
14545 this.historyData_, index, this.searchedTerm);
14546 },
14547
14548 hasResults: function(historyDataLength) {
14549 return historyDataLength > 0;
14550 },
14551
14552 noResultsMessage_: function(searchedTerm, isLoading) {
14553 if (isLoading)
14554 return '';
14555 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
14556 return loadTimeData.getString(messageId);
14557 },
14558
14559 /**
14560 * True if the given item is the beginning of a new card.
14561 * @param {HistoryEntry} item
14562 * @param {number} i Index of |item| within |historyData_|.
14563 * @param {number} length
14564 * @return {boolean}
14565 * @private
14566 */
14567 isCardStart_: function(item, i, length) {
14568 if (length == 0 || i > length - 1)
14569 return false;
14570 return i == 0 ||
14571 this.historyData_[i].dateRelativeDay !=
14572 this.historyData_[i - 1].dateRelativeDay;
14573 },
14574
14575 /**
14576 * True if the given item is the end of a card.
14577 * @param {HistoryEntry} item
14578 * @param {number} i Index of |item| within |historyData_|.
14579 * @param {number} length
14580 * @return {boolean}
14581 * @private
14582 */
14583 isCardEnd_: function(item, i, length) {
14584 if (length == 0 || i > length - 1)
14585 return false;
14586 return i == length - 1 ||
14587 this.historyData_[i].dateRelativeDay !=
14588 this.historyData_[i + 1].dateRelativeDay;
14589 },
14590
14591 /**
14592 * @param {number} index
14593 * @return {boolean}
14594 * @private
14595 */
14596 isFirstItem_: function(index) {
14597 return index == 0;
14598 },
14599
14600 /**
14601 * @private
14602 */
14603 notifyListScroll_: function() {
14604 this.fire('history-list-scrolled');
14605 },
14606 });
14607 // Copyright 2016 The Chromium Authors. All rights reserved.
14608 // Use of this source code is governed by a BSD-style license that can be
14609 // found in the LICENSE file.
14610
14611 Polymer({
14612 is: 'history-list-container',
14613
14614 properties: {
14615 // The path of the currently selected page.
14616 selectedPage_: String,
14617
14618 // Whether domain-grouped history is enabled.
14619 grouped: Boolean,
14620
14621 /** @type {!QueryState} */
14622 queryState: Object,
14623
14624 /** @type {!QueryResult} */
14625 queryResult: Object,
14626 },
14627
14628 observers: [
14629 'groupedRangeChanged_(queryState.range)',
14630 ],
14631
14632 listeners: {
14633 'history-list-scrolled': 'closeMenu_',
14634 'load-more-history': 'loadMoreHistory_',
14635 'toggle-menu': 'toggleMenu_',
14636 },
14637
14638 /**
14639 * @param {HistoryQuery} info An object containing information about the
14640 * query.
14641 * @param {!Array<HistoryEntry>} results A list of results.
14642 */
14643 historyResult: function(info, results) {
14644 this.initializeResults_(info, results);
14645 this.closeMenu_();
14646
14647 if (this.selectedPage_ == 'grouped-list') {
14648 this.$$('#grouped-list').historyData = results;
14649 return;
14650 }
14651
14652 var list = /** @type {HistoryListElement} */(this.$['infinite-list']);
14653 list.addNewResults(results);
14654 if (info.finished)
14655 list.disableResultLoading();
14656 },
14657
14658 /**
14659 * Queries the history backend for results based on queryState.
14660 * @param {boolean} incremental Whether the new query should continue where
14661 * the previous query stopped.
14662 */
14663 queryHistory: function(incremental) {
14664 var queryState = this.queryState;
14665 // Disable querying until the first set of results have been returned. If
14666 // there is a search, query immediately to support search query params from
14667 // the URL.
14668 var noResults = !this.queryResult || this.queryResult.results == null;
14669 if (queryState.queryingDisabled ||
14670 (!this.queryState.searchTerm && noResults)) {
14671 return;
14672 }
14673
14674 // Close any open dialog if a new query is initiated.
14675 if (!incremental && this.$.dialog.open)
14676 this.$.dialog.close();
14677
14678 this.set('queryState.querying', true);
14679 this.set('queryState.incremental', incremental);
14680
14681 var lastVisitTime = 0;
14682 if (incremental) {
14683 var lastVisit = this.queryResult.results.slice(-1)[0];
14684 lastVisitTime = lastVisit ? lastVisit.time : 0;
14685 }
14686
14687 var maxResults =
14688 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0;
14689 chrome.send('queryHistory', [
14690 queryState.searchTerm, queryState.groupedOffset, queryState.range,
14691 lastVisitTime, maxResults
14692 ]);
14693 },
14694
14695 unselectAllItems: function(count) {
14696 /** @type {HistoryListElement} */ (this.$['infinite-list'])
14697 .unselectAllItems(count);
14698 },
14699
14700 /**
14701 * Delete all the currently selected history items. Will prompt the user with
14702 * a dialog to confirm that the deletion should be performed.
14703 */
14704 deleteSelectedWithPrompt: function() {
14705 if (!loadTimeData.getBoolean('allowDeletingHistory'))
14706 return;
14707
14708 this.$.dialog.showModal();
14709 },
14710
14711 /**
14712 * @param {HistoryRange} range
14713 * @private
14714 */
14715 groupedRangeChanged_: function(range) {
14716 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ?
14717 'infinite-list' : 'grouped-list';
14718
14719 this.queryHistory(false);
11608 }, 14720 },
11609 14721
11610 /** @private */ 14722 /** @private */
11611 clearSearch_: function() { 14723 loadMoreHistory_: function() { this.queryHistory(true); },
11612 this.setValue(''); 14724
11613 this.getSearchInput().focus(); 14725 /**
14726 * @param {HistoryQuery} info
14727 * @param {!Array<HistoryEntry>} results
14728 * @private
14729 */
14730 initializeResults_: function(info, results) {
14731 if (results.length == 0)
14732 return;
14733
14734 var currentDate = results[0].dateRelativeDay;
14735
14736 for (var i = 0; i < results.length; i++) {
14737 // Sets the default values for these fields to prevent undefined types.
14738 results[i].selected = false;
14739 results[i].readableTimestamp =
14740 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
14741
14742 if (results[i].dateRelativeDay != currentDate) {
14743 currentDate = results[i].dateRelativeDay;
14744 }
14745 }
11614 }, 14746 },
11615 14747
11616 /** @private */ 14748 /** @private */
11617 toggleShowingSearch_: function() { 14749 onDialogConfirmTap_: function() {
11618 this.showingSearch = !this.showingSearch; 14750 this.$['infinite-list'].deleteSelected();
14751 this.$.dialog.close();
14752 },
14753
14754 /** @private */
14755 onDialogCancelTap_: function() {
14756 this.$.dialog.close();
14757 },
14758
14759 /**
14760 * Closes the overflow menu.
14761 * @private
14762 */
14763 closeMenu_: function() {
14764 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).closeMenu();
14765 },
14766
14767 /**
14768 * Opens the overflow menu unless the menu is already open and the same button
14769 * is pressed.
14770 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e
14771 * @private
14772 */
14773 toggleMenu_: function(e) {
14774 var target = e.detail.target;
14775 /** @type {CrSharedMenuElement} */(this.$.sharedMenu).toggleMenu(
14776 target, e.detail.item);
14777 },
14778
14779 /** @private */
14780 onMoreFromSiteTap_: function() {
14781 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
14782 this.fire('search-domain', {domain: menu.itemData.domain});
14783 menu.closeMenu();
14784 },
14785
14786 /** @private */
14787 onRemoveFromHistoryTap_: function() {
14788 var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
14789 md_history.BrowserService.getInstance()
14790 .deleteItems([menu.itemData])
14791 .then(function(items) {
14792 this.$['infinite-list'].removeDeletedHistory_(items);
14793 // This unselect-all is to reset the toolbar when deleting a selected
14794 // item. TODO(tsergeant): Make this automatic based on observing list
14795 // modifications.
14796 this.fire('unselect-all');
14797 }.bind(this));
14798 menu.closeMenu();
14799 },
14800 });
14801 // Copyright 2016 The Chromium Authors. All rights reserved.
14802 // Use of this source code is governed by a BSD-style license that can be
14803 // found in the LICENSE file.
14804
14805 Polymer({
14806 is: 'history-synced-device-card',
14807
14808 properties: {
14809 // Name of the synced device.
14810 device: String,
14811
14812 // When the device information was last updated.
14813 lastUpdateTime: String,
14814
14815 /**
14816 * The list of tabs open for this device.
14817 * @type {!Array<!ForeignSessionTab>}
14818 */
14819 tabs: {
14820 type: Array,
14821 value: function() { return []; },
14822 observer: 'updateIcons_'
14823 },
14824
14825 /**
14826 * The indexes where a window separator should be shown. The use of a
14827 * separate array here is necessary for window separators to appear
14828 * correctly in search. See http://crrev.com/2022003002 for more details.
14829 * @type {!Array<number>}
14830 */
14831 separatorIndexes: Array,
14832
14833 // Whether the card is open.
14834 cardOpen_: {type: Boolean, value: true},
14835
14836 searchTerm: String,
14837
14838 // Internal identifier for the device.
14839 sessionTag: String,
14840 },
14841
14842 /**
14843 * Open a single synced tab. Listens to 'click' rather than 'tap'
14844 * to determine what modifier keys were pressed.
14845 * @param {DomRepeatClickEvent} e
14846 * @private
14847 */
14848 openTab_: function(e) {
14849 var tab = /** @type {ForeignSessionTab} */(e.model.tab);
14850 md_history.BrowserService.getInstance().openForeignSessionTab(
14851 this.sessionTag, tab.windowId, tab.sessionId, e);
14852 e.preventDefault();
14853 },
14854
14855 /**
14856 * Toggles the dropdown display of synced tabs for each device card.
14857 */
14858 toggleTabCard: function() {
14859 this.$.collapse.toggle();
14860 this.$['dropdown-indicator'].icon =
14861 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more';
14862 },
14863
14864 /**
14865 * When the synced tab information is set, the icon associated with the tab
14866 * website is also set.
14867 * @private
14868 */
14869 updateIcons_: function() {
14870 this.async(function() {
14871 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon');
14872
14873 for (var i = 0; i < this.tabs.length; i++) {
14874 icons[i].style.backgroundImage =
14875 cr.icon.getFaviconImageSet(this.tabs[i].url);
14876 }
14877 });
14878 },
14879
14880 /** @private */
14881 isWindowSeparatorIndex_: function(index, separatorIndexes) {
14882 return this.separatorIndexes.indexOf(index) != -1;
14883 },
14884
14885 /**
14886 * @param {boolean} cardOpen
14887 * @return {string}
14888 */
14889 getCollapseTitle_: function(cardOpen) {
14890 return cardOpen ? loadTimeData.getString('collapseSessionButton') :
14891 loadTimeData.getString('expandSessionButton');
14892 },
14893
14894 /**
14895 * @param {CustomEvent} e
14896 * @private
14897 */
14898 onMenuButtonTap_: function(e) {
14899 this.fire('toggle-menu', {
14900 target: Polymer.dom(e).localTarget,
14901 tag: this.sessionTag
14902 });
14903 e.stopPropagation(); // Prevent iron-collapse.
14904 },
14905 });
14906 // Copyright 2016 The Chromium Authors. All rights reserved.
14907 // Use of this source code is governed by a BSD-style license that can be
14908 // found in the LICENSE file.
14909
14910 /**
14911 * @typedef {{device: string,
14912 * lastUpdateTime: string,
14913 * separatorIndexes: !Array<number>,
14914 * timestamp: number,
14915 * tabs: !Array<!ForeignSessionTab>,
14916 * tag: string}}
14917 */
14918 var ForeignDeviceInternal;
14919
14920 Polymer({
14921 is: 'history-synced-device-manager',
14922
14923 properties: {
14924 /**
14925 * @type {?Array<!ForeignSession>}
14926 */
14927 sessionList: {
14928 type: Array,
14929 observer: 'updateSyncedDevices'
14930 },
14931
14932 searchTerm: {
14933 type: String,
14934 observer: 'searchTermChanged'
14935 },
14936
14937 /**
14938 * An array of synced devices with synced tab data.
14939 * @type {!Array<!ForeignDeviceInternal>}
14940 */
14941 syncedDevices_: {
14942 type: Array,
14943 value: function() { return []; }
14944 },
14945
14946 /** @private */
14947 signInState_: {
14948 type: Boolean,
14949 value: loadTimeData.getBoolean('isUserSignedIn'),
14950 },
14951
14952 /** @private */
14953 guestSession_: {
14954 type: Boolean,
14955 value: loadTimeData.getBoolean('isGuestSession'),
14956 },
14957
14958 /** @private */
14959 fetchingSyncedTabs_: {
14960 type: Boolean,
14961 value: false,
14962 }
14963 },
14964
14965 listeners: {
14966 'toggle-menu': 'onToggleMenu_',
14967 },
14968
14969 /** @override */
14970 attached: function() {
14971 // Update the sign in state.
14972 chrome.send('otherDevicesInitialized');
14973 },
14974
14975 /**
14976 * @param {!ForeignSession} session
14977 * @return {!ForeignDeviceInternal}
14978 */
14979 createInternalDevice_: function(session) {
14980 var tabs = [];
14981 var separatorIndexes = [];
14982 for (var i = 0; i < session.windows.length; i++) {
14983 var windowId = session.windows[i].sessionId;
14984 var newTabs = session.windows[i].tabs;
14985 if (newTabs.length == 0)
14986 continue;
14987
14988 newTabs.forEach(function(tab) {
14989 tab.windowId = windowId;
14990 });
14991
14992 var windowAdded = false;
14993 if (!this.searchTerm) {
14994 // Add all the tabs if there is no search term.
14995 tabs = tabs.concat(newTabs);
14996 windowAdded = true;
14997 } else {
14998 var searchText = this.searchTerm.toLowerCase();
14999 for (var j = 0; j < newTabs.length; j++) {
15000 var tab = newTabs[j];
15001 if (tab.title.toLowerCase().indexOf(searchText) != -1) {
15002 tabs.push(tab);
15003 windowAdded = true;
15004 }
15005 }
15006 }
15007 if (windowAdded && i != session.windows.length - 1)
15008 separatorIndexes.push(tabs.length - 1);
15009 }
15010 return {
15011 device: session.name,
15012 lastUpdateTime: '– ' + session.modifiedTime,
15013 separatorIndexes: separatorIndexes,
15014 timestamp: session.timestamp,
15015 tabs: tabs,
15016 tag: session.tag,
15017 };
15018 },
15019
15020 onSignInTap_: function() {
15021 chrome.send('SyncSetupShowSetupUI');
15022 chrome.send('SyncSetupStartSignIn', [false]);
15023 },
15024
15025 onToggleMenu_: function(e) {
15026 this.$.menu.toggleMenu(e.detail.target, e.detail.tag);
15027 },
15028
15029 onOpenAllTap_: function() {
15030 md_history.BrowserService.getInstance().openForeignSessionAllTabs(
15031 this.$.menu.itemData);
15032 this.$.menu.closeMenu();
15033 },
15034
15035 onDeleteSessionTap_: function() {
15036 md_history.BrowserService.getInstance().deleteForeignSession(
15037 this.$.menu.itemData);
15038 this.$.menu.closeMenu();
15039 },
15040
15041 /** @private */
15042 clearDisplayedSyncedDevices_: function() {
15043 this.syncedDevices_ = [];
15044 },
15045
15046 /**
15047 * Decide whether or not should display no synced tabs message.
15048 * @param {boolean} signInState
15049 * @param {number} syncedDevicesLength
15050 * @param {boolean} guestSession
15051 * @return {boolean}
15052 */
15053 showNoSyncedMessage: function(
15054 signInState, syncedDevicesLength, guestSession) {
15055 if (guestSession)
15056 return true;
15057
15058 return signInState && syncedDevicesLength == 0;
15059 },
15060
15061 /**
15062 * Shows the signin guide when the user is not signed in and not in a guest
15063 * session.
15064 * @param {boolean} signInState
15065 * @param {boolean} guestSession
15066 * @return {boolean}
15067 */
15068 showSignInGuide: function(signInState, guestSession) {
15069 return !signInState && !guestSession;
15070 },
15071
15072 /**
15073 * Decide what message should be displayed when user is logged in and there
15074 * are no synced tabs.
15075 * @param {boolean} fetchingSyncedTabs
15076 * @return {string}
15077 */
15078 noSyncedTabsMessage: function(fetchingSyncedTabs) {
15079 return loadTimeData.getString(
15080 fetchingSyncedTabs ? 'loading' : 'noSyncedResults');
15081 },
15082
15083 /**
15084 * Replaces the currently displayed synced tabs with |sessionList|. It is
15085 * common for only a single session within the list to have changed, We try to
15086 * avoid doing extra work in this case. The logic could be more intelligent
15087 * about updating individual tabs rather than replacing whole sessions, but
15088 * this approach seems to have acceptable performance.
15089 * @param {?Array<!ForeignSession>} sessionList
15090 */
15091 updateSyncedDevices: function(sessionList) {
15092 this.fetchingSyncedTabs_ = false;
15093
15094 if (!sessionList)
15095 return;
15096
15097 // First, update any existing devices that have changed.
15098 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length);
15099 for (var i = 0; i < updateCount; i++) {
15100 var oldDevice = this.syncedDevices_[i];
15101 if (oldDevice.tag != sessionList[i].tag ||
15102 oldDevice.timestamp != sessionList[i].timestamp) {
15103 this.splice(
15104 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i]));
15105 }
15106 }
15107
15108 // Then, append any new devices.
15109 for (var i = updateCount; i < sessionList.length; i++) {
15110 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i]));
15111 }
15112 },
15113
15114 /**
15115 * Get called when user's sign in state changes, this will affect UI of synced
15116 * tabs page. Sign in promo gets displayed when user is signed out, and
15117 * different messages are shown when there are no synced tabs.
15118 * @param {boolean} isUserSignedIn
15119 */
15120 updateSignInState: function(isUserSignedIn) {
15121 // If user's sign in state didn't change, then don't change message or
15122 // update UI.
15123 if (this.signInState_ == isUserSignedIn)
15124 return;
15125
15126 this.signInState_ = isUserSignedIn;
15127
15128 // User signed out, clear synced device list and show the sign in promo.
15129 if (!isUserSignedIn) {
15130 this.clearDisplayedSyncedDevices_();
15131 return;
15132 }
15133 // User signed in, show the loading message when querying for synced
15134 // devices.
15135 this.fetchingSyncedTabs_ = true;
15136 },
15137
15138 searchTermChanged: function(searchTerm) {
15139 this.clearDisplayedSyncedDevices_();
15140 this.updateSyncedDevices(this.sessionList);
15141 }
15142 });
15143 /**
15144 `iron-selector` is an element which can be used to manage a list of elements
15145 that can be selected. Tapping on the item will make the item selected. The ` selected` indicates
15146 which item is being selected. The default is to use the index of the item.
15147
15148 Example:
15149
15150 <iron-selector selected="0">
15151 <div>Item 1</div>
15152 <div>Item 2</div>
15153 <div>Item 3</div>
15154 </iron-selector>
15155
15156 If you want to use the attribute value of an element for `selected` instead of the index,
15157 set `attrForSelected` to the name of the attribute. For example, if you want to select item by
15158 `name`, set `attrForSelected` to `name`.
15159
15160 Example:
15161
15162 <iron-selector attr-for-selected="name" selected="foo">
15163 <div name="foo">Foo</div>
15164 <div name="bar">Bar</div>
15165 <div name="zot">Zot</div>
15166 </iron-selector>
15167
15168 You can specify a default fallback with `fallbackSelection` in case the `selec ted` attribute does
15169 not match the `attrForSelected` attribute of any elements.
15170
15171 Example:
15172
15173 <iron-selector attr-for-selected="name" selected="non-existing"
15174 fallback-selection="default">
15175 <div name="foo">Foo</div>
15176 <div name="bar">Bar</div>
15177 <div name="default">Default</div>
15178 </iron-selector>
15179
15180 Note: When the selector is multi, the selection will set to `fallbackSelection ` iff
15181 the number of matching elements is zero.
15182
15183 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the selected element.
15184
15185 Example:
15186
15187 <style>
15188 .iron-selected {
15189 background: #eee;
15190 }
15191 </style>
15192
15193 ...
15194
15195 <iron-selector selected="0">
15196 <div>Item 1</div>
15197 <div>Item 2</div>
15198 <div>Item 3</div>
15199 </iron-selector>
15200
15201 @demo demo/index.html
15202 */
15203
15204 Polymer({
15205
15206 is: 'iron-selector',
15207
15208 behaviors: [
15209 Polymer.IronMultiSelectableBehavior
15210 ]
15211
15212 });
15213 // Copyright 2016 The Chromium Authors. All rights reserved.
15214 // Use of this source code is governed by a BSD-style license that can be
15215 // found in the LICENSE file.
15216
15217 Polymer({
15218 is: 'history-side-bar',
15219
15220 properties: {
15221 selectedPage: {
15222 type: String,
15223 notify: true
15224 },
15225
15226 route: Object,
15227
15228 showFooter: Boolean,
15229
15230 // If true, the sidebar is contained within an app-drawer.
15231 drawer: {
15232 type: Boolean,
15233 reflectToAttribute: true
15234 },
15235 },
15236
15237 /** @private */
15238 onSelectorActivate_: function() {
15239 this.fire('history-close-drawer');
15240 },
15241
15242 /**
15243 * Relocates the user to the clear browsing data section of the settings page.
15244 * @param {Event} e
15245 * @private
15246 */
15247 onClearBrowsingDataTap_: function(e) {
15248 md_history.BrowserService.getInstance().openClearBrowsingData();
15249 e.preventDefault();
15250 },
15251
15252 /**
15253 * @param {Object} route
15254 * @private
15255 */
15256 getQueryString_: function(route) {
15257 return window.location.search;
15258 }
15259 });
15260 // Copyright 2016 The Chromium Authors. All rights reserved.
15261 // Use of this source code is governed by a BSD-style license that can be
15262 // found in the LICENSE file.
15263
15264 Polymer({
15265 is: 'history-app',
15266
15267 properties: {
15268 showSidebarFooter: Boolean,
15269
15270 // The id of the currently selected page.
15271 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'},
15272
15273 // Whether domain-grouped history is enabled.
15274 grouped_: {type: Boolean, reflectToAttribute: true},
15275
15276 /** @type {!QueryState} */
15277 queryState_: {
15278 type: Object,
15279 value: function() {
15280 return {
15281 // Whether the most recent query was incremental.
15282 incremental: false,
15283 // A query is initiated by page load.
15284 querying: true,
15285 queryingDisabled: false,
15286 _range: HistoryRange.ALL_TIME,
15287 searchTerm: '',
15288 // TODO(calamity): Make history toolbar buttons change the offset
15289 groupedOffset: 0,
15290
15291 set range(val) { this._range = Number(val); },
15292 get range() { return this._range; },
15293 };
15294 }
15295 },
15296
15297 /** @type {!QueryResult} */
15298 queryResult_: {
15299 type: Object,
15300 value: function() {
15301 return {
15302 info: null,
15303 results: null,
15304 sessionList: null,
15305 };
15306 }
15307 },
15308
15309 // Route data for the current page.
15310 routeData_: Object,
15311
15312 // The query params for the page.
15313 queryParams_: Object,
15314
15315 // True if the window is narrow enough for the page to have a drawer.
15316 hasDrawer_: Boolean,
15317 },
15318
15319 observers: [
15320 // routeData_.page <=> selectedPage
15321 'routeDataChanged_(routeData_.page)',
15322 'selectedPageChanged_(selectedPage_)',
15323
15324 // queryParams_.q <=> queryState.searchTerm
15325 'searchTermChanged_(queryState_.searchTerm)',
15326 'searchQueryParamChanged_(queryParams_.q)',
15327
15328 ],
15329
15330 // TODO(calamity): Replace these event listeners with data bound properties.
15331 listeners: {
15332 'cr-menu-tap': 'onMenuTap_',
15333 'history-checkbox-select': 'checkboxSelected',
15334 'unselect-all': 'unselectAll',
15335 'delete-selected': 'deleteSelected',
15336 'search-domain': 'searchDomain_',
15337 'history-close-drawer': 'closeDrawer_',
15338 },
15339
15340 /** @override */
15341 ready: function() {
15342 this.grouped_ = loadTimeData.getBoolean('groupByDomain');
15343
15344 cr.ui.decorate('command', cr.ui.Command);
15345 document.addEventListener('canExecute', this.onCanExecute_.bind(this));
15346 document.addEventListener('command', this.onCommand_.bind(this));
15347
15348 // Redirect legacy search URLs to URLs compatible with material history.
15349 if (window.location.hash) {
15350 window.location.href = window.location.href.split('#')[0] + '?' +
15351 window.location.hash.substr(1);
15352 }
15353 },
15354
15355 /** @private */
15356 onMenuTap_: function() {
15357 var drawer = this.$$('#drawer');
15358 if (drawer)
15359 drawer.toggle();
15360 },
15361
15362 /**
15363 * Listens for history-item being selected or deselected (through checkbox)
15364 * and changes the view of the top toolbar.
15365 * @param {{detail: {countAddition: number}}} e
15366 */
15367 checkboxSelected: function(e) {
15368 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
15369 toolbar.count += e.detail.countAddition;
15370 },
15371
15372 /**
15373 * Listens for call to cancel selection and loops through all items to set
15374 * checkbox to be unselected.
15375 * @private
15376 */
15377 unselectAll: function() {
15378 var listContainer =
15379 /** @type {HistoryListContainerElement} */ (this.$['history']);
15380 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
15381 listContainer.unselectAllItems(toolbar.count);
15382 toolbar.count = 0;
15383 },
15384
15385 deleteSelected: function() {
15386 this.$.history.deleteSelectedWithPrompt();
15387 },
15388
15389 /**
15390 * @param {HistoryQuery} info An object containing information about the
15391 * query.
15392 * @param {!Array<HistoryEntry>} results A list of results.
15393 */
15394 historyResult: function(info, results) {
15395 this.set('queryState_.querying', false);
15396 this.set('queryResult_.info', info);
15397 this.set('queryResult_.results', results);
15398 var listContainer =
15399 /** @type {HistoryListContainerElement} */ (this.$['history']);
15400 listContainer.historyResult(info, results);
15401 },
15402
15403 /**
15404 * Fired when the user presses 'More from this site'.
15405 * @param {{detail: {domain: string}}} e
15406 */
15407 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); },
15408
15409 /**
15410 * @param {Event} e
15411 * @private
15412 */
15413 onCanExecute_: function(e) {
15414 e = /** @type {cr.ui.CanExecuteEvent} */(e);
15415 switch (e.command.id) {
15416 case 'find-command':
15417 e.canExecute = true;
15418 break;
15419 case 'slash-command':
15420 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused();
15421 break;
15422 case 'delete-command':
15423 e.canExecute = this.$.toolbar.count > 0;
15424 break;
15425 }
15426 },
15427
15428 /**
15429 * @param {string} searchTerm
15430 * @private
15431 */
15432 searchTermChanged_: function(searchTerm) {
15433 this.set('queryParams_.q', searchTerm || null);
15434 this.$['history'].queryHistory(false);
15435 },
15436
15437 /**
15438 * @param {string} searchQuery
15439 * @private
15440 */
15441 searchQueryParamChanged_: function(searchQuery) {
15442 this.$.toolbar.setSearchTerm(searchQuery || '');
15443 },
15444
15445 /**
15446 * @param {Event} e
15447 * @private
15448 */
15449 onCommand_: function(e) {
15450 if (e.command.id == 'find-command' || e.command.id == 'slash-command')
15451 this.$.toolbar.showSearchField();
15452 if (e.command.id == 'delete-command')
15453 this.deleteSelected();
15454 },
15455
15456 /**
15457 * @param {!Array<!ForeignSession>} sessionList Array of objects describing
15458 * the sessions from other devices.
15459 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
15460 */
15461 setForeignSessions: function(sessionList, isTabSyncEnabled) {
15462 if (!isTabSyncEnabled)
15463 return;
15464
15465 this.set('queryResult_.sessionList', sessionList);
15466 },
15467
15468 /**
15469 * Update sign in state of synced device manager after user logs in or out.
15470 * @param {boolean} isUserSignedIn
15471 */
15472 updateSignInState: function(isUserSignedIn) {
15473 var syncedDeviceManagerElem =
15474 /** @type {HistorySyncedDeviceManagerElement} */this
15475 .$$('history-synced-device-manager');
15476 if (syncedDeviceManagerElem)
15477 syncedDeviceManagerElem.updateSignInState(isUserSignedIn);
15478 },
15479
15480 /**
15481 * @param {string} selectedPage
15482 * @return {boolean}
15483 * @private
15484 */
15485 syncedTabsSelected_: function(selectedPage) {
15486 return selectedPage == 'syncedTabs';
15487 },
15488
15489 /**
15490 * @param {boolean} querying
15491 * @param {boolean} incremental
15492 * @param {string} searchTerm
15493 * @return {boolean} Whether a loading spinner should be shown (implies the
15494 * backend is querying a new search term).
15495 * @private
15496 */
15497 shouldShowSpinner_: function(querying, incremental, searchTerm) {
15498 return querying && !incremental && searchTerm != '';
15499 },
15500
15501 /**
15502 * @param {string} page
15503 * @private
15504 */
15505 routeDataChanged_: function(page) {
15506 this.selectedPage_ = page;
15507 },
15508
15509 /**
15510 * @param {string} selectedPage
15511 * @private
15512 */
15513 selectedPageChanged_: function(selectedPage) {
15514 this.set('routeData_.page', selectedPage);
15515 },
15516
15517 /**
15518 * This computed binding is needed to make the iron-pages selector update when
15519 * the synced-device-manager is instantiated for the first time. Otherwise the
15520 * fallback selection will continue to be used after the corresponding item is
15521 * added as a child of iron-pages.
15522 * @param {string} selectedPage
15523 * @param {Array} items
15524 * @return {string}
15525 * @private
15526 */
15527 getSelectedPage_(selectedPage, items) {
15528 return selectedPage;
15529 },
15530
15531 /** @private */
15532 closeDrawer_: function() {
15533 var drawer = this.$$('#drawer');
15534 if (drawer)
15535 drawer.close();
11619 }, 15536 },
11620 }); 15537 });
11621 // Copyright 2015 The Chromium Authors. All rights reserved. 15538 // Copyright 2015 The Chromium Authors. All rights reserved.
11622 // Use of this source code is governed by a BSD-style license that can be 15539 // Use of this source code is governed by a BSD-style license that can be
11623 // found in the LICENSE file. 15540 // found in the LICENSE file.
11624 15541
11625 cr.define('downloads', function() { 15542 // Send the history query immediately. This allows the query to process during
11626 var Toolbar = Polymer({ 15543 // the initial page startup.
11627 is: 'downloads-toolbar', 15544 chrome.send('queryHistory', ['', 0, 0, 0, RESULTS_PER_PAGE]);
11628 15545 chrome.send('getForeignSessions');
11629 attached: function() { 15546
11630 // isRTL() only works after i18n_template.js runs to set <html dir>. 15547 /** @type {Promise} */
11631 this.overflowAlign_ = isRTL() ? 'left' : 'right'; 15548 var upgradePromise = null;
11632 }, 15549 /** @type {boolean} */
11633 15550 var resultsRendered = false;
11634 properties: { 15551
11635 downloadsShowing: { 15552 /**
11636 reflectToAttribute: true, 15553 * @return {!Promise<!HistoryAppElement>} Resolves once the history-app has been
11637 type: Boolean, 15554 * fully upgraded.
11638 value: false, 15555 */
11639 observer: 'downloadsShowingChanged_', 15556 function waitForHistoryApp() {
11640 }, 15557 if (!upgradePromise) {
11641 15558 upgradePromise = new Promise(function(resolve, reject) {
11642 overflowAlign_: { 15559 if (window.Polymer && Polymer.isInstance &&
11643 type: String, 15560 Polymer.isInstance(document.querySelector('history-app'))) {
11644 value: 'right', 15561 resolve(/** @type {!HistoryAppElement} */(
11645 }, 15562 document.querySelector('history-app')));
11646 }, 15563 } else {
11647 15564 $('bundle').addEventListener('load', function() {
11648 listeners: { 15565 resolve(/** @type {!HistoryAppElement} */(
11649 'paper-dropdown-close': 'onPaperDropdownClose_', 15566 document.querySelector('history-app')));
11650 'paper-dropdown-open': 'onPaperDropdownOpen_', 15567 });
11651 }, 15568 }
11652 15569 });
11653 /** @return {boolean} Whether removal can be undone. */ 15570 }
11654 canUndo: function() { 15571 return upgradePromise;
11655 return this.$['search-input'] != this.shadowRoot.activeElement; 15572 }
11656 }, 15573
11657 15574 // Chrome Callbacks-------------------------------------------------------------
11658 /** @return {boolean} Whether "Clear all" should be allowed. */ 15575
11659 canClearAll: function() { 15576 /**
11660 return !this.$['search-input'].getValue() && this.downloadsShowing; 15577 * Our history system calls this function with results from searches.
11661 }, 15578 * @param {HistoryQuery} info An object containing information about the query.
11662 15579 * @param {!Array<HistoryEntry>} results A list of results.
11663 onFindCommand: function() { 15580 */
11664 this.$['search-input'].showAndFocus(); 15581 function historyResult(info, results) {
11665 }, 15582 waitForHistoryApp().then(function(historyApp) {
11666 15583 historyApp.historyResult(info, results);
11667 /** @private */ 15584 document.body.classList.remove('loading');
11668 closeMoreActions_: function() { 15585
11669 this.$.more.close(); 15586 if (!resultsRendered) {
11670 }, 15587 resultsRendered = true;
11671 15588 // requestAnimationFrame allows measurement immediately before the next
11672 /** @private */ 15589 // repaint, but after the first page of <iron-list> items has stamped.
11673 downloadsShowingChanged_: function() { 15590 requestAnimationFrame(function() {
11674 this.updateClearAll_(); 15591 chrome.send(
11675 }, 15592 'metricsHandler:recordTime',
11676 15593 ['History.ResultsRenderedTime', window.performance.now()]);
11677 /** @private */ 15594 });
11678 onClearAllTap_: function() { 15595 }
11679 assert(this.canClearAll());
11680 downloads.ActionService.getInstance().clearAll();
11681 },
11682
11683 /** @private */
11684 onPaperDropdownClose_: function() {
11685 window.removeEventListener('resize', assert(this.boundClose_));
11686 },
11687
11688 /**
11689 * @param {!Event} e
11690 * @private
11691 */
11692 onItemBlur_: function(e) {
11693 var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu'));
11694 if (menu.items.indexOf(e.relatedTarget) >= 0)
11695 return;
11696
11697 this.$.more.restoreFocusOnClose = false;
11698 this.closeMoreActions_();
11699 this.$.more.restoreFocusOnClose = true;
11700 },
11701
11702 /** @private */
11703 onPaperDropdownOpen_: function() {
11704 this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this);
11705 window.addEventListener('resize', this.boundClose_);
11706 },
11707
11708 /**
11709 * @param {!CustomEvent} event
11710 * @private
11711 */
11712 onSearchChanged_: function(event) {
11713 downloads.ActionService.getInstance().search(
11714 /** @type {string} */ (event.detail));
11715 this.updateClearAll_();
11716 },
11717
11718 /** @private */
11719 onOpenDownloadsFolderTap_: function() {
11720 downloads.ActionService.getInstance().openDownloadsFolder();
11721 },
11722
11723 /** @private */
11724 updateClearAll_: function() {
11725 this.$$('#actions .clear-all').hidden = !this.canClearAll();
11726 this.$$('paper-menu .clear-all').hidden = !this.canClearAll();
11727 },
11728 }); 15596 });
11729 15597 }
11730 return {Toolbar: Toolbar}; 15598
11731 }); 15599 /**
11732 // Copyright 2015 The Chromium Authors. All rights reserved. 15600 * Called by the history backend after receiving results and after discovering
11733 // Use of this source code is governed by a BSD-style license that can be 15601 * the existence of other forms of browsing history.
11734 // found in the LICENSE file. 15602 * @param {boolean} hasSyncedResults Whether there are synced results.
11735 15603 * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
11736 cr.define('downloads', function() { 15604 * a sentence about the existence of other forms of browsing history.
11737 var Manager = Polymer({ 15605 */
11738 is: 'downloads-manager', 15606 function showNotification(
11739 15607 hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
11740 properties: { 15608 // TODO(msramek): |hasSyncedResults| was used in the old WebUI to show
11741 hasDownloads_: { 15609 // the message about other signed-in devices. This message does not exist
11742 observer: 'hasDownloadsChanged_', 15610 // in the MD history anymore, so the parameter is not needed. Remove it
11743 type: Boolean, 15611 // when WebUI is removed and this becomes the only client of
11744 }, 15612 // BrowsingHistoryHandler.
11745 15613 waitForHistoryApp().then(function(historyApp) {
11746 items_: { 15614 historyApp.showSidebarFooter = includeOtherFormsOfBrowsingHistory;
11747 type: Array,
11748 value: function() { return []; },
11749 },
11750 },
11751
11752 hostAttributes: {
11753 loading: true,
11754 },
11755
11756 listeners: {
11757 'downloads-list.scroll': 'onListScroll_',
11758 },
11759
11760 observers: [
11761 'itemsChanged_(items_.*)',
11762 ],
11763
11764 /** @private */
11765 clearAll_: function() {
11766 this.set('items_', []);
11767 },
11768
11769 /** @private */
11770 hasDownloadsChanged_: function() {
11771 if (loadTimeData.getBoolean('allowDeletingHistory'))
11772 this.$.toolbar.downloadsShowing = this.hasDownloads_;
11773
11774 if (this.hasDownloads_) {
11775 this.$['downloads-list'].fire('iron-resize');
11776 } else {
11777 var isSearching = downloads.ActionService.getInstance().isSearching();
11778 var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads';
11779 this.$['no-downloads'].querySelector('span').textContent =
11780 loadTimeData.getString(messageToShow);
11781 }
11782 },
11783
11784 /**
11785 * @param {number} index
11786 * @param {!Array<!downloads.Data>} list
11787 * @private
11788 */
11789 insertItems_: function(index, list) {
11790 this.splice.apply(this, ['items_', index, 0].concat(list));
11791 this.updateHideDates_(index, index + list.length);
11792 this.removeAttribute('loading');
11793 },
11794
11795 /** @private */
11796 itemsChanged_: function() {
11797 this.hasDownloads_ = this.items_.length > 0;
11798 },
11799
11800 /**
11801 * @param {Event} e
11802 * @private
11803 */
11804 onCanExecute_: function(e) {
11805 e = /** @type {cr.ui.CanExecuteEvent} */(e);
11806 switch (e.command.id) {
11807 case 'undo-command':
11808 e.canExecute = this.$.toolbar.canUndo();
11809 break;
11810 case 'clear-all-command':
11811 e.canExecute = this.$.toolbar.canClearAll();
11812 break;
11813 case 'find-command':
11814 e.canExecute = true;
11815 break;
11816 }
11817 },
11818
11819 /**
11820 * @param {Event} e
11821 * @private
11822 */
11823 onCommand_: function(e) {
11824 if (e.command.id == 'clear-all-command')
11825 downloads.ActionService.getInstance().clearAll();
11826 else if (e.command.id == 'undo-command')
11827 downloads.ActionService.getInstance().undo();
11828 else if (e.command.id == 'find-command')
11829 this.$.toolbar.onFindCommand();
11830 },
11831
11832 /** @private */
11833 onListScroll_: function() {
11834 var list = this.$['downloads-list'];
11835 if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) {
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 }); 15615 });
11887 15616 }
11888 Manager.clearAll = function() { 15617
11889 Manager.get().clearAll_(); 15618 /**
11890 }; 15619 * Receives the synced history data. An empty list means that either there are
11891 15620 * no foreign sessions, or tab sync is disabled for this profile.
11892 /** @return {!downloads.Manager} */ 15621 * |isTabSyncEnabled| makes it possible to distinguish between the cases.
11893 Manager.get = function() { 15622 *
11894 return /** @type {!downloads.Manager} */( 15623 * @param {!Array<!ForeignSession>} sessionList Array of objects describing the
11895 queryRequiredElement('downloads-manager')); 15624 * sessions from other devices.
11896 }; 15625 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
11897 15626 */
11898 Manager.insertItems = function(index, list) { 15627 function setForeignSessions(sessionList, isTabSyncEnabled) {
11899 Manager.get().insertItems_(index, list); 15628 waitForHistoryApp().then(function(historyApp) {
11900 }; 15629 historyApp.setForeignSessions(sessionList, isTabSyncEnabled);
11901 15630 });
11902 Manager.onLoad = function() { 15631 }
11903 Manager.get().onLoad_(); 15632
11904 }; 15633 /**
11905 15634 * Called when the history is deleted by someone else.
11906 Manager.removeItem = function(index) { 15635 */
11907 Manager.get().removeItem_(index); 15636 function historyDeleted() {
11908 }; 15637 }
11909 15638
11910 Manager.updateItem = function(index, data) { 15639 /**
11911 Manager.get().updateItem_(index, data); 15640 * Called by the history backend after user's sign in state changes.
11912 }; 15641 * @param {boolean} isUserSignedIn Whether user is signed in or not now.
11913 15642 */
11914 return {Manager: Manager}; 15643 function updateSignInState(isUserSignedIn) {
11915 }); 15644 waitForHistoryApp().then(function(historyApp) {
11916 // Copyright 2015 The Chromium Authors. All rights reserved. 15645 historyApp.updateSignInState(isUserSignedIn);
11917 // Use of this source code is governed by a BSD-style license that can be 15646 });
11918 // found in the LICENSE file. 15647 };
11919
11920 window.addEventListener('load', downloads.Manager.onLoad);
OLDNEW
« no previous file with comments | « chrome/browser/resources/md_downloads/vulcanize_readme.md ('k') | chrome/browser/resources/md_history/history.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698