| 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 |