OLD | NEW |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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); | |
OLD | NEW |