| Index: chrome/browser/resources/md_history/crisper.js
|
| diff --git a/chrome/browser/resources/md_downloads/crisper.js b/chrome/browser/resources/md_history/crisper.js
|
| similarity index 69%
|
| copy from chrome/browser/resources/md_downloads/crisper.js
|
| copy to chrome/browser/resources/md_history/crisper.js
|
| index 2d17f3a17ee86921ff069832066d8986932d700a..7cc12a4a7bf6d6fa6477725e07b1d50d512dddee 100644
|
| --- a/chrome/browser/resources/md_downloads/crisper.js
|
| +++ b/chrome/browser/resources/md_history/crisper.js
|
| @@ -608,3750 +608,3198 @@ var cr = cr || function() {
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
|
|
| -cr.define('cr.ui', function() {
|
| -
|
| - /**
|
| - * Decorates elements as an instance of a class.
|
| - * @param {string|!Element} source The way to find the element(s) to decorate.
|
| - * If this is a string then {@code querySeletorAll} is used to find the
|
| - * elements to decorate.
|
| - * @param {!Function} constr The constructor to decorate with. The constr
|
| - * needs to have a {@code decorate} function.
|
| - */
|
| - function decorate(source, constr) {
|
| - var elements;
|
| - if (typeof source == 'string')
|
| - elements = cr.doc.querySelectorAll(source);
|
| - else
|
| - elements = [source];
|
| -
|
| - for (var i = 0, el; el = elements[i]; i++) {
|
| - if (!(el instanceof constr))
|
| - constr.decorate(el);
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Helper function for creating new element for define.
|
| - */
|
| - function createElementHelper(tagName, opt_bag) {
|
| - // Allow passing in ownerDocument to create in a different document.
|
| - var doc;
|
| - if (opt_bag && opt_bag.ownerDocument)
|
| - doc = opt_bag.ownerDocument;
|
| - else
|
| - doc = cr.doc;
|
| - return doc.createElement(tagName);
|
| - }
|
| +// <include src="../../../../ui/webui/resources/js/assert.js">
|
|
|
| - /**
|
| - * Creates the constructor for a UI element class.
|
| - *
|
| - * Usage:
|
| - * <pre>
|
| - * var List = cr.ui.define('list');
|
| - * List.prototype = {
|
| - * __proto__: HTMLUListElement.prototype,
|
| - * decorate: function() {
|
| - * ...
|
| - * },
|
| - * ...
|
| - * };
|
| - * </pre>
|
| - *
|
| - * @param {string|Function} tagNameOrFunction The tagName or
|
| - * function to use for newly created elements. If this is a function it
|
| - * needs to return a new element when called.
|
| - * @return {function(Object=):Element} The constructor function which takes
|
| - * an optional property bag. The function also has a static
|
| - * {@code decorate} method added to it.
|
| - */
|
| - function define(tagNameOrFunction) {
|
| - var createFunction, tagName;
|
| - if (typeof tagNameOrFunction == 'function') {
|
| - createFunction = tagNameOrFunction;
|
| - tagName = '';
|
| - } else {
|
| - createFunction = createElementHelper;
|
| - tagName = tagNameOrFunction;
|
| - }
|
| +/**
|
| + * Alias for document.getElementById. Found elements must be HTMLElements.
|
| + * @param {string} id The ID of the element to find.
|
| + * @return {HTMLElement} The found element or null if not found.
|
| + */
|
| +function $(id) {
|
| + var el = document.getElementById(id);
|
| + return el ? assertInstanceof(el, HTMLElement) : null;
|
| +}
|
|
|
| - /**
|
| - * Creates a new UI element constructor.
|
| - * @param {Object=} opt_propertyBag Optional bag of properties to set on the
|
| - * object after created. The property {@code ownerDocument} is special
|
| - * cased and it allows you to create the element in a different
|
| - * document than the default.
|
| - * @constructor
|
| - */
|
| - function f(opt_propertyBag) {
|
| - var el = createFunction(tagName, opt_propertyBag);
|
| - f.decorate(el);
|
| - for (var propertyName in opt_propertyBag) {
|
| - el[propertyName] = opt_propertyBag[propertyName];
|
| - }
|
| - return el;
|
| - }
|
| +// TODO(devlin): This should return SVGElement, but closure compiler is missing
|
| +// those externs.
|
| +/**
|
| + * Alias for document.getElementById. Found elements must be SVGElements.
|
| + * @param {string} id The ID of the element to find.
|
| + * @return {Element} The found element or null if not found.
|
| + */
|
| +function getSVGElement(id) {
|
| + var el = document.getElementById(id);
|
| + return el ? assertInstanceof(el, Element) : null;
|
| +}
|
|
|
| - /**
|
| - * Decorates an element as a UI element class.
|
| - * @param {!Element} el The element to decorate.
|
| - */
|
| - f.decorate = function(el) {
|
| - el.__proto__ = f.prototype;
|
| - el.decorate();
|
| - };
|
| +/**
|
| + * Add an accessible message to the page that will be announced to
|
| + * users who have spoken feedback on, but will be invisible to all
|
| + * other users. It's removed right away so it doesn't clutter the DOM.
|
| + * @param {string} msg The text to be pronounced.
|
| + */
|
| +function announceAccessibleMessage(msg) {
|
| + var element = document.createElement('div');
|
| + element.setAttribute('aria-live', 'polite');
|
| + element.style.position = 'relative';
|
| + element.style.left = '-9999px';
|
| + element.style.height = '0px';
|
| + element.innerText = msg;
|
| + document.body.appendChild(element);
|
| + window.setTimeout(function() {
|
| + document.body.removeChild(element);
|
| + }, 0);
|
| +}
|
|
|
| - return f;
|
| +/**
|
| + * Generates a CSS url string.
|
| + * @param {string} s The URL to generate the CSS url for.
|
| + * @return {string} The CSS url string.
|
| + */
|
| +function url(s) {
|
| + // http://www.w3.org/TR/css3-values/#uris
|
| + // Parentheses, commas, whitespace characters, single quotes (') and double
|
| + // quotes (") appearing in a URI must be escaped with a backslash
|
| + var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
|
| + // WebKit has a bug when it comes to URLs that end with \
|
| + // https://bugs.webkit.org/show_bug.cgi?id=28885
|
| + if (/\\\\$/.test(s2)) {
|
| + // Add a space to work around the WebKit bug.
|
| + s2 += ' ';
|
| }
|
| + return 'url("' + s2 + '")';
|
| +}
|
|
|
| - /**
|
| - * Input elements do not grow and shrink with their content. This is a simple
|
| - * (and not very efficient) way of handling shrinking to content with support
|
| - * for min width and limited by the width of the parent element.
|
| - * @param {!HTMLElement} el The element to limit the width for.
|
| - * @param {!HTMLElement} parentEl The parent element that should limit the
|
| - * size.
|
| - * @param {number} min The minimum width.
|
| - * @param {number=} opt_scale Optional scale factor to apply to the width.
|
| - */
|
| - function limitInputWidth(el, parentEl, min, opt_scale) {
|
| - // Needs a size larger than borders
|
| - el.style.width = '10px';
|
| - var doc = el.ownerDocument;
|
| - var win = doc.defaultView;
|
| - var computedStyle = win.getComputedStyle(el);
|
| - var parentComputedStyle = win.getComputedStyle(parentEl);
|
| - var rtl = computedStyle.direction == 'rtl';
|
| -
|
| - // To get the max width we get the width of the treeItem minus the position
|
| - // of the input.
|
| - var inputRect = el.getBoundingClientRect(); // box-sizing
|
| - var parentRect = parentEl.getBoundingClientRect();
|
| - var startPos = rtl ? parentRect.right - inputRect.right :
|
| - inputRect.left - parentRect.left;
|
| +/**
|
| + * Parses query parameters from Location.
|
| + * @param {Location} location The URL to generate the CSS url for.
|
| + * @return {Object} Dictionary containing name value pairs for URL
|
| + */
|
| +function parseQueryParams(location) {
|
| + var params = {};
|
| + var query = unescape(location.search.substring(1));
|
| + var vars = query.split('&');
|
| + for (var i = 0; i < vars.length; i++) {
|
| + var pair = vars[i].split('=');
|
| + params[pair[0]] = pair[1];
|
| + }
|
| + return params;
|
| +}
|
|
|
| - // Add up border and padding of the input.
|
| - var inner = parseInt(computedStyle.borderLeftWidth, 10) +
|
| - parseInt(computedStyle.paddingLeft, 10) +
|
| - parseInt(computedStyle.paddingRight, 10) +
|
| - parseInt(computedStyle.borderRightWidth, 10);
|
| +/**
|
| + * Creates a new URL by appending or replacing the given query key and value.
|
| + * Not supporting URL with username and password.
|
| + * @param {Location} location The original URL.
|
| + * @param {string} key The query parameter name.
|
| + * @param {string} value The query parameter value.
|
| + * @return {string} The constructed new URL.
|
| + */
|
| +function setQueryParam(location, key, value) {
|
| + var query = parseQueryParams(location);
|
| + query[encodeURIComponent(key)] = encodeURIComponent(value);
|
|
|
| - // We also need to subtract the padding of parent to prevent it to overflow.
|
| - var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
|
| - parseInt(parentComputedStyle.paddingRight, 10);
|
| + var newQuery = '';
|
| + for (var q in query) {
|
| + newQuery += (newQuery ? '&' : '?') + q + '=' + query[q];
|
| + }
|
|
|
| - var max = parentEl.clientWidth - startPos - inner - parentPadding;
|
| - if (opt_scale)
|
| - max *= opt_scale;
|
| + return location.origin + location.pathname + newQuery + location.hash;
|
| +}
|
|
|
| - function limit() {
|
| - if (el.scrollWidth > max) {
|
| - el.style.width = max + 'px';
|
| - } else {
|
| - el.style.width = 0;
|
| - var sw = el.scrollWidth;
|
| - if (sw < min) {
|
| - el.style.width = min + 'px';
|
| - } else {
|
| - el.style.width = sw + 'px';
|
| - }
|
| - }
|
| - }
|
| +/**
|
| + * @param {Node} el A node to search for ancestors with |className|.
|
| + * @param {string} className A class to search for.
|
| + * @return {Element} A node with class of |className| or null if none is found.
|
| + */
|
| +function findAncestorByClass(el, className) {
|
| + return /** @type {Element} */(findAncestor(el, function(el) {
|
| + return el.classList && el.classList.contains(className);
|
| + }));
|
| +}
|
|
|
| - el.addEventListener('input', limit);
|
| - limit();
|
| +/**
|
| + * Return the first ancestor for which the {@code predicate} returns true.
|
| + * @param {Node} node The node to check.
|
| + * @param {function(Node):boolean} predicate The function that tests the
|
| + * nodes.
|
| + * @return {Node} The found ancestor or null if not found.
|
| + */
|
| +function findAncestor(node, predicate) {
|
| + var last = false;
|
| + while (node != null && !(last = predicate(node))) {
|
| + node = node.parentNode;
|
| }
|
| + return last ? node : null;
|
| +}
|
|
|
| - /**
|
| - * Takes a number and spits out a value CSS will be happy with. To avoid
|
| - * subpixel layout issues, the value is rounded to the nearest integral value.
|
| - * @param {number} pixels The number of pixels.
|
| - * @return {string} e.g. '16px'.
|
| - */
|
| - function toCssPx(pixels) {
|
| - if (!window.isFinite(pixels))
|
| - console.error('Pixel value is not a number: ' + pixels);
|
| - return Math.round(pixels) + 'px';
|
| +function swapDomNodes(a, b) {
|
| + var afterA = a.nextSibling;
|
| + if (afterA == b) {
|
| + swapDomNodes(b, a);
|
| + return;
|
| }
|
| + var aParent = a.parentNode;
|
| + b.parentNode.replaceChild(a, b);
|
| + aParent.insertBefore(b, afterA);
|
| +}
|
|
|
| - /**
|
| - * Users complain they occasionaly use doubleclicks instead of clicks
|
| - * (http://crbug.com/140364). To fix it we freeze click handling for
|
| - * the doubleclick time interval.
|
| - * @param {MouseEvent} e Initial click event.
|
| - */
|
| - function swallowDoubleClick(e) {
|
| - var doc = e.target.ownerDocument;
|
| - var counter = Math.min(1, e.detail);
|
| - function swallow(e) {
|
| - e.stopPropagation();
|
| +/**
|
| + * Disables text selection and dragging, with optional whitelist callbacks.
|
| + * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
|
| + * is defined and returns true, the onselectionstart event will be
|
| + * surpressed.
|
| + * @param {function(Event):boolean=} opt_allowDragStart Unless this function
|
| + * is defined and returns true, the ondragstart event will be surpressed.
|
| + */
|
| +function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
|
| + // Disable text selection.
|
| + document.onselectstart = function(e) {
|
| + if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e)))
|
| e.preventDefault();
|
| - }
|
| - function onclick(e) {
|
| - if (e.detail > counter) {
|
| - counter = e.detail;
|
| - // Swallow the click since it's a click inside the doubleclick timeout.
|
| - swallow(e);
|
| - } else {
|
| - // Stop tracking clicks and let regular handling.
|
| - doc.removeEventListener('dblclick', swallow, true);
|
| - doc.removeEventListener('click', onclick, true);
|
| - }
|
| - }
|
| - // The following 'click' event (if e.type == 'mouseup') mustn't be taken
|
| - // into account (it mustn't stop tracking clicks). Start event listening
|
| - // after zero timeout.
|
| - setTimeout(function() {
|
| - doc.addEventListener('click', onclick, true);
|
| - doc.addEventListener('dblclick', swallow, true);
|
| - }, 0);
|
| - }
|
| + };
|
|
|
| - return {
|
| - decorate: decorate,
|
| - define: define,
|
| - limitInputWidth: limitInputWidth,
|
| - toCssPx: toCssPx,
|
| - swallowDoubleClick: swallowDoubleClick
|
| + // Disable dragging.
|
| + document.ondragstart = function(e) {
|
| + if (!(opt_allowDragStart && opt_allowDragStart.call(this, e)))
|
| + e.preventDefault();
|
| };
|
| -});
|
| -// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| +}
|
|
|
| /**
|
| - * @fileoverview A command is an abstraction of an action a user can do in the
|
| - * UI.
|
| - *
|
| - * When the focus changes in the document for each command a canExecute event
|
| - * is dispatched on the active element. By listening to this event you can
|
| - * enable and disable the command by setting the event.canExecute property.
|
| - *
|
| - * When a command is executed a command event is dispatched on the active
|
| - * element. Note that you should stop the propagation after you have handled the
|
| - * command if there might be other command listeners higher up in the DOM tree.
|
| + * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead.
|
| + * Call this to stop clicks on <a href="#"> links from scrolling to the top of
|
| + * the page (and possibly showing a # in the link).
|
| */
|
| -
|
| -cr.define('cr.ui', function() {
|
| -
|
| - /**
|
| - * This is used to identify keyboard shortcuts.
|
| - * @param {string} shortcut The text used to describe the keys for this
|
| - * keyboard shortcut.
|
| - * @constructor
|
| - */
|
| - function KeyboardShortcut(shortcut) {
|
| - var mods = {};
|
| - var ident = '';
|
| - shortcut.split('|').forEach(function(part) {
|
| - var partLc = part.toLowerCase();
|
| - switch (partLc) {
|
| - case 'alt':
|
| - case 'ctrl':
|
| - case 'meta':
|
| - case 'shift':
|
| - mods[partLc + 'Key'] = true;
|
| - break;
|
| - default:
|
| - if (ident)
|
| - throw Error('Invalid shortcut');
|
| - ident = part;
|
| - }
|
| +function preventDefaultOnPoundLinkClicks() {
|
| + document.addEventListener('click', function(e) {
|
| + var anchor = findAncestor(/** @type {Node} */(e.target), function(el) {
|
| + return el.tagName == 'A';
|
| });
|
| + // Use getAttribute() to prevent URL normalization.
|
| + if (anchor && anchor.getAttribute('href') == '#')
|
| + e.preventDefault();
|
| + });
|
| +}
|
|
|
| - this.ident_ = ident;
|
| - this.mods_ = mods;
|
| - }
|
| +/**
|
| + * Check the directionality of the page.
|
| + * @return {boolean} True if Chrome is running an RTL UI.
|
| + */
|
| +function isRTL() {
|
| + return document.documentElement.dir == 'rtl';
|
| +}
|
|
|
| - KeyboardShortcut.prototype = {
|
| - /**
|
| - * Whether the keyboard shortcut object matches a keyboard event.
|
| - * @param {!Event} e The keyboard event object.
|
| - * @return {boolean} Whether we found a match or not.
|
| - */
|
| - matchesEvent: function(e) {
|
| - if (e.key == this.ident_) {
|
| - // All keyboard modifiers needs to match.
|
| - var mods = this.mods_;
|
| - return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
|
| - return e[k] == !!mods[k];
|
| - });
|
| - }
|
| - return false;
|
| - }
|
| - };
|
| +/**
|
| + * Get an element that's known to exist by its ID. We use this instead of just
|
| + * calling getElementById and not checking the result because this lets us
|
| + * satisfy the JSCompiler type system.
|
| + * @param {string} id The identifier name.
|
| + * @return {!HTMLElement} the Element.
|
| + */
|
| +function getRequiredElement(id) {
|
| + return assertInstanceof($(id), HTMLElement,
|
| + 'Missing required element: ' + id);
|
| +}
|
|
|
| - /**
|
| - * Creates a new command element.
|
| - * @constructor
|
| - * @extends {HTMLElement}
|
| - */
|
| - var Command = cr.ui.define('command');
|
| +/**
|
| + * Query an element that's known to exist by a selector. We use this instead of
|
| + * just calling querySelector and not checking the result because this lets us
|
| + * satisfy the JSCompiler type system.
|
| + * @param {string} selectors CSS selectors to query the element.
|
| + * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional
|
| + * context object for querySelector.
|
| + * @return {!HTMLElement} the Element.
|
| + */
|
| +function queryRequiredElement(selectors, opt_context) {
|
| + var element = (opt_context || document).querySelector(selectors);
|
| + return assertInstanceof(element, HTMLElement,
|
| + 'Missing required element: ' + selectors);
|
| +}
|
|
|
| - Command.prototype = {
|
| - __proto__: HTMLElement.prototype,
|
| +// Handle click on a link. If the link points to a chrome: or file: url, then
|
| +// call into the browser to do the navigation.
|
| +document.addEventListener('click', function(e) {
|
| + if (e.defaultPrevented)
|
| + return;
|
|
|
| - /**
|
| - * Initializes the command.
|
| - */
|
| - decorate: function() {
|
| - CommandManager.init(assert(this.ownerDocument));
|
| + var el = e.target;
|
| + if (el.nodeType == Node.ELEMENT_NODE &&
|
| + el.webkitMatchesSelector('A, A *')) {
|
| + while (el.tagName != 'A') {
|
| + el = el.parentElement;
|
| + }
|
|
|
| - if (this.hasAttribute('shortcut'))
|
| - this.shortcut = this.getAttribute('shortcut');
|
| - },
|
| + if ((el.protocol == 'file:' || el.protocol == 'about:') &&
|
| + (e.button == 0 || e.button == 1)) {
|
| + chrome.send('navigateToUrl', [
|
| + el.href,
|
| + el.target,
|
| + e.button,
|
| + e.altKey,
|
| + e.ctrlKey,
|
| + e.metaKey,
|
| + e.shiftKey
|
| + ]);
|
| + e.preventDefault();
|
| + }
|
| + }
|
| +});
|
|
|
| - /**
|
| - * Executes the command by dispatching a command event on the given element.
|
| - * If |element| isn't given, the active element is used instead.
|
| - * If the command is {@code disabled} this does nothing.
|
| - * @param {HTMLElement=} opt_element Optional element to dispatch event on.
|
| - */
|
| - execute: function(opt_element) {
|
| - if (this.disabled)
|
| - return;
|
| - var doc = this.ownerDocument;
|
| - if (doc.activeElement) {
|
| - var e = new Event('command', {bubbles: true});
|
| - e.command = this;
|
| +/**
|
| + * Creates a new URL which is the old URL with a GET param of key=value.
|
| + * @param {string} url The base URL. There is not sanity checking on the URL so
|
| + * it must be passed in a proper format.
|
| + * @param {string} key The key of the param.
|
| + * @param {string} value The value of the param.
|
| + * @return {string} The new URL.
|
| + */
|
| +function appendParam(url, key, value) {
|
| + var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
|
|
| - (opt_element || doc.activeElement).dispatchEvent(e);
|
| - }
|
| - },
|
| + if (url.indexOf('?') == -1)
|
| + return url + '?' + param;
|
| + return url + '&' + param;
|
| +}
|
|
|
| - /**
|
| - * Call this when there have been changes that might change whether the
|
| - * command can be executed or not.
|
| - * @param {Node=} opt_node Node for which to actuate command state.
|
| - */
|
| - canExecuteChange: function(opt_node) {
|
| - dispatchCanExecuteEvent(this,
|
| - opt_node || this.ownerDocument.activeElement);
|
| - },
|
| -
|
| - /**
|
| - * The keyboard shortcut that triggers the command. This is a string
|
| - * consisting of a key (as reported by WebKit in keydown) as
|
| - * well as optional key modifiers joinded with a '|'.
|
| - *
|
| - * Multiple keyboard shortcuts can be provided by separating them by
|
| - * whitespace.
|
| - *
|
| - * For example:
|
| - * "F1"
|
| - * "Backspace|Meta" for Apple command backspace.
|
| - * "a|Ctrl" for Control A
|
| - * "Delete Backspace|Meta" for Delete and Command Backspace
|
| - *
|
| - * @type {string}
|
| - */
|
| - shortcut_: '',
|
| - get shortcut() {
|
| - return this.shortcut_;
|
| - },
|
| - set shortcut(shortcut) {
|
| - var oldShortcut = this.shortcut_;
|
| - if (shortcut !== oldShortcut) {
|
| - this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
|
| - return new KeyboardShortcut(shortcut);
|
| - });
|
| -
|
| - // Set this after the keyboardShortcuts_ since that might throw.
|
| - this.shortcut_ = shortcut;
|
| - cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
|
| - oldShortcut);
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Whether the event object matches the shortcut for this command.
|
| - * @param {!Event} e The key event object.
|
| - * @return {boolean} Whether it matched or not.
|
| - */
|
| - matchesEvent: function(e) {
|
| - if (!this.keyboardShortcuts_)
|
| - return false;
|
| -
|
| - return this.keyboardShortcuts_.some(function(keyboardShortcut) {
|
| - return keyboardShortcut.matchesEvent(e);
|
| - });
|
| - },
|
| - };
|
| -
|
| - /**
|
| - * The label of the command.
|
| - */
|
| - cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
|
| -
|
| - /**
|
| - * Whether the command is disabled or not.
|
| - */
|
| - cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
|
| +/**
|
| + * Creates an element of a specified type with a specified class name.
|
| + * @param {string} type The node type.
|
| + * @param {string} className The class name to use.
|
| + * @return {Element} The created element.
|
| + */
|
| +function createElementWithClassName(type, className) {
|
| + var elm = document.createElement(type);
|
| + elm.className = className;
|
| + return elm;
|
| +}
|
|
|
| - /**
|
| - * Whether the command is hidden or not.
|
| - */
|
| - cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
|
| +/**
|
| + * webkitTransitionEnd does not always fire (e.g. when animation is aborted
|
| + * or when no paint happens during the animation). This function sets up
|
| + * a timer and emulate the event if it is not fired when the timer expires.
|
| + * @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
|
| + * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
|
| + * webkitTransitionEnd to happen. If not specified, it is fetched from |el|
|
| + * using the transitionDuration style value.
|
| + */
|
| +function ensureTransitionEndEvent(el, opt_timeOut) {
|
| + if (opt_timeOut === undefined) {
|
| + var style = getComputedStyle(el);
|
| + opt_timeOut = parseFloat(style.transitionDuration) * 1000;
|
|
|
| - /**
|
| - * Whether the command is checked or not.
|
| - */
|
| - cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
|
| + // Give an additional 50ms buffer for the animation to complete.
|
| + opt_timeOut += 50;
|
| + }
|
|
|
| - /**
|
| - * The flag that prevents the shortcut text from being displayed on menu.
|
| - *
|
| - * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
|
| - * is displayed in menu when the command is assosiated with a menu item.
|
| - * Otherwise, no text is displayed.
|
| - */
|
| - cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
|
| + var fired = false;
|
| + el.addEventListener('webkitTransitionEnd', function f(e) {
|
| + el.removeEventListener('webkitTransitionEnd', f);
|
| + fired = true;
|
| + });
|
| + window.setTimeout(function() {
|
| + if (!fired)
|
| + cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
|
| + }, opt_timeOut);
|
| +}
|
|
|
| - /**
|
| - * Dispatches a canExecute event on the target.
|
| - * @param {!cr.ui.Command} command The command that we are testing for.
|
| - * @param {EventTarget} target The target element to dispatch the event on.
|
| - */
|
| - function dispatchCanExecuteEvent(command, target) {
|
| - var e = new CanExecuteEvent(command);
|
| - target.dispatchEvent(e);
|
| - command.disabled = !e.canExecute;
|
| - }
|
| +/**
|
| + * Alias for document.scrollTop getter.
|
| + * @param {!HTMLDocument} doc The document node where information will be
|
| + * queried from.
|
| + * @return {number} The Y document scroll offset.
|
| + */
|
| +function scrollTopForDocument(doc) {
|
| + return doc.documentElement.scrollTop || doc.body.scrollTop;
|
| +}
|
|
|
| - /**
|
| - * The command managers for different documents.
|
| - */
|
| - var commandManagers = {};
|
| +/**
|
| + * Alias for document.scrollTop setter.
|
| + * @param {!HTMLDocument} doc The document node where information will be
|
| + * queried from.
|
| + * @param {number} value The target Y scroll offset.
|
| + */
|
| +function setScrollTopForDocument(doc, value) {
|
| + doc.documentElement.scrollTop = doc.body.scrollTop = value;
|
| +}
|
|
|
| - /**
|
| - * Keeps track of the focused element and updates the commands when the focus
|
| - * changes.
|
| - * @param {!Document} doc The document that we are managing the commands for.
|
| - * @constructor
|
| - */
|
| - function CommandManager(doc) {
|
| - doc.addEventListener('focus', this.handleFocus_.bind(this), true);
|
| - // Make sure we add the listener to the bubbling phase so that elements can
|
| - // prevent the command.
|
| - doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
|
| - }
|
| +/**
|
| + * Alias for document.scrollLeft getter.
|
| + * @param {!HTMLDocument} doc The document node where information will be
|
| + * queried from.
|
| + * @return {number} The X document scroll offset.
|
| + */
|
| +function scrollLeftForDocument(doc) {
|
| + return doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
| +}
|
|
|
| - /**
|
| - * Initializes a command manager for the document as needed.
|
| - * @param {!Document} doc The document to manage the commands for.
|
| - */
|
| - CommandManager.init = function(doc) {
|
| - var uid = cr.getUid(doc);
|
| - if (!(uid in commandManagers)) {
|
| - commandManagers[uid] = new CommandManager(doc);
|
| - }
|
| - };
|
| +/**
|
| + * Alias for document.scrollLeft setter.
|
| + * @param {!HTMLDocument} doc The document node where information will be
|
| + * queried from.
|
| + * @param {number} value The target X scroll offset.
|
| + */
|
| +function setScrollLeftForDocument(doc, value) {
|
| + doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
|
| +}
|
|
|
| - CommandManager.prototype = {
|
| +/**
|
| + * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
|
| + * @param {string} original The original string.
|
| + * @return {string} The string with all the characters mentioned above replaced.
|
| + */
|
| +function HTMLEscape(original) {
|
| + return original.replace(/&/g, '&')
|
| + .replace(/</g, '<')
|
| + .replace(/>/g, '>')
|
| + .replace(/"/g, '"')
|
| + .replace(/'/g, ''');
|
| +}
|
|
|
| - /**
|
| - * Handles focus changes on the document.
|
| - * @param {Event} e The focus event object.
|
| - * @private
|
| - * @suppress {checkTypes}
|
| - * TODO(vitalyp): remove the suppression.
|
| - */
|
| - handleFocus_: function(e) {
|
| - var target = e.target;
|
| +/**
|
| + * Shortens the provided string (if necessary) to a string of length at most
|
| + * |maxLength|.
|
| + * @param {string} original The original string.
|
| + * @param {number} maxLength The maximum length allowed for the string.
|
| + * @return {string} The original string if its length does not exceed
|
| + * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
|
| + * appended.
|
| + */
|
| +function elide(original, maxLength) {
|
| + if (original.length <= maxLength)
|
| + return original;
|
| + return original.substring(0, maxLength - 1) + '\u2026';
|
| +}
|
|
|
| - // Ignore focus on a menu button or command item.
|
| - if (target.menu || target.command)
|
| - return;
|
| +/**
|
| + * Quote a string so it can be used in a regular expression.
|
| + * @param {string} str The source string.
|
| + * @return {string} The escaped string.
|
| + */
|
| +function quoteString(str) {
|
| + return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
|
| +}
|
|
|
| - var commands = Array.prototype.slice.call(
|
| - target.ownerDocument.querySelectorAll('command'));
|
| +// <if expr="is_ios">
|
| +// Polyfill 'key' in KeyboardEvent for iOS.
|
| +// This function is not intended to be complete but should
|
| +// be sufficient enough to have iOS work correctly while
|
| +// it does not support key yet.
|
| +if (!('key' in KeyboardEvent.prototype)) {
|
| + Object.defineProperty(KeyboardEvent.prototype, 'key', {
|
| + /** @this {KeyboardEvent} */
|
| + get: function () {
|
| + // 0-9
|
| + if (this.keyCode >= 0x30 && this.keyCode <= 0x39)
|
| + return String.fromCharCode(this.keyCode);
|
|
|
| - commands.forEach(function(command) {
|
| - dispatchCanExecuteEvent(command, target);
|
| - });
|
| - },
|
| + // A-Z
|
| + if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) {
|
| + var result = String.fromCharCode(this.keyCode).toLowerCase();
|
| + if (this.shiftKey)
|
| + result = result.toUpperCase();
|
| + return result;
|
| + }
|
|
|
| - /**
|
| - * Handles the keydown event and routes it to the right command.
|
| - * @param {!Event} e The keydown event.
|
| - */
|
| - handleKeyDown_: function(e) {
|
| - var target = e.target;
|
| - var commands = Array.prototype.slice.call(
|
| - target.ownerDocument.querySelectorAll('command'));
|
| -
|
| - for (var i = 0, command; command = commands[i]; i++) {
|
| - if (command.matchesEvent(e)) {
|
| - // When invoking a command via a shortcut, we have to manually check
|
| - // if it can be executed, since focus might not have been changed
|
| - // what would have updated the command's state.
|
| - command.canExecuteChange();
|
| -
|
| - if (!command.disabled) {
|
| - e.preventDefault();
|
| - // We do not want any other element to handle this.
|
| - e.stopPropagation();
|
| - command.execute();
|
| - return;
|
| - }
|
| - }
|
| + // Special characters
|
| + switch(this.keyCode) {
|
| + case 0x08: return 'Backspace';
|
| + case 0x09: return 'Tab';
|
| + case 0x0d: return 'Enter';
|
| + case 0x10: return 'Shift';
|
| + case 0x11: return 'Control';
|
| + case 0x12: return 'Alt';
|
| + case 0x1b: return 'Escape';
|
| + case 0x20: return ' ';
|
| + case 0x21: return 'PageUp';
|
| + case 0x22: return 'PageDown';
|
| + case 0x23: return 'End';
|
| + case 0x24: return 'Home';
|
| + case 0x25: return 'ArrowLeft';
|
| + case 0x26: return 'ArrowUp';
|
| + case 0x27: return 'ArrowRight';
|
| + case 0x28: return 'ArrowDown';
|
| + case 0x2d: return 'Insert';
|
| + case 0x2e: return 'Delete';
|
| + case 0x5b: return 'Meta';
|
| + case 0x70: return 'F1';
|
| + case 0x71: return 'F2';
|
| + case 0x72: return 'F3';
|
| + case 0x73: return 'F4';
|
| + case 0x74: return 'F5';
|
| + case 0x75: return 'F6';
|
| + case 0x76: return 'F7';
|
| + case 0x77: return 'F8';
|
| + case 0x78: return 'F9';
|
| + case 0x79: return 'F10';
|
| + case 0x7a: return 'F11';
|
| + case 0x7b: return 'F12';
|
| + case 0xbb: return '=';
|
| + case 0xbd: return '-';
|
| + case 0xdb: return '[';
|
| + case 0xdd: return ']';
|
| }
|
| + return 'Unidentified';
|
| }
|
| - };
|
| -
|
| - /**
|
| - * The event type used for canExecute events.
|
| - * @param {!cr.ui.Command} command The command that we are evaluating.
|
| - * @extends {Event}
|
| - * @constructor
|
| - * @class
|
| - */
|
| - function CanExecuteEvent(command) {
|
| - var e = new Event('canExecute', {bubbles: true, cancelable: true});
|
| - e.__proto__ = CanExecuteEvent.prototype;
|
| - e.command = command;
|
| - return e;
|
| - }
|
| -
|
| - CanExecuteEvent.prototype = {
|
| - __proto__: Event.prototype,
|
| -
|
| - /**
|
| - * The current command
|
| - * @type {cr.ui.Command}
|
| - */
|
| - command: null,
|
| -
|
| - /**
|
| - * Whether the target can execute the command. Setting this also stops the
|
| - * propagation and prevents the default. Callers can tell if an event has
|
| - * been handled via |this.defaultPrevented|.
|
| - * @type {boolean}
|
| - */
|
| - canExecute_: false,
|
| - get canExecute() {
|
| - return this.canExecute_;
|
| - },
|
| - set canExecute(canExecute) {
|
| - this.canExecute_ = !!canExecute;
|
| - this.stopPropagation();
|
| - this.preventDefault();
|
| - }
|
| - };
|
| -
|
| - // Export
|
| - return {
|
| - Command: Command,
|
| - CanExecuteEvent: CanExecuteEvent
|
| - };
|
| -});
|
| -// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| + });
|
| +} else {
|
| + window.console.log("KeyboardEvent.Key polyfill not required");
|
| +}
|
| +// </if> /* is_ios */
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
|
|
| -// <include src="../../../../ui/webui/resources/js/assert.js">
|
| -
|
| -/**
|
| - * Alias for document.getElementById. Found elements must be HTMLElements.
|
| - * @param {string} id The ID of the element to find.
|
| - * @return {HTMLElement} The found element or null if not found.
|
| - */
|
| -function $(id) {
|
| - var el = document.getElementById(id);
|
| - return el ? assertInstanceof(el, HTMLElement) : null;
|
| -}
|
| +// Globals:
|
| +/** @const */ var RESULTS_PER_PAGE = 150;
|
|
|
| -// TODO(devlin): This should return SVGElement, but closure compiler is missing
|
| -// those externs.
|
| /**
|
| - * Alias for document.getElementById. Found elements must be SVGElements.
|
| - * @param {string} id The ID of the element to find.
|
| - * @return {Element} The found element or null if not found.
|
| + * Amount of time between pageviews that we consider a 'break' in browsing,
|
| + * measured in milliseconds.
|
| + * @const
|
| */
|
| -function getSVGElement(id) {
|
| - var el = document.getElementById(id);
|
| - return el ? assertInstanceof(el, Element) : null;
|
| -}
|
| +var BROWSING_GAP_TIME = 15 * 60 * 1000;
|
|
|
| /**
|
| - * Add an accessible message to the page that will be announced to
|
| - * users who have spoken feedback on, but will be invisible to all
|
| - * other users. It's removed right away so it doesn't clutter the DOM.
|
| - * @param {string} msg The text to be pronounced.
|
| + * Maximum length of a history item title. Anything longer than this will be
|
| + * cropped to fit within this limit. This value is large enough that it will not
|
| + * be noticeable in a 960px wide history-item.
|
| + * @const
|
| */
|
| -function announceAccessibleMessage(msg) {
|
| - var element = document.createElement('div');
|
| - element.setAttribute('aria-live', 'polite');
|
| - element.style.position = 'relative';
|
| - element.style.left = '-9999px';
|
| - element.style.height = '0px';
|
| - element.innerText = msg;
|
| - document.body.appendChild(element);
|
| - window.setTimeout(function() {
|
| - document.body.removeChild(element);
|
| - }, 0);
|
| -}
|
| +var TITLE_MAX_LENGTH = 300;
|
|
|
| /**
|
| - * Generates a CSS url string.
|
| - * @param {string} s The URL to generate the CSS url for.
|
| - * @return {string} The CSS url string.
|
| + * @enum {number}
|
| */
|
| -function url(s) {
|
| - // http://www.w3.org/TR/css3-values/#uris
|
| - // Parentheses, commas, whitespace characters, single quotes (') and double
|
| - // quotes (") appearing in a URI must be escaped with a backslash
|
| - var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
|
| - // WebKit has a bug when it comes to URLs that end with \
|
| - // https://bugs.webkit.org/show_bug.cgi?id=28885
|
| - if (/\\\\$/.test(s2)) {
|
| - // Add a space to work around the WebKit bug.
|
| - s2 += ' ';
|
| - }
|
| - return 'url("' + s2 + '")';
|
| -}
|
| +var HistoryRange = {
|
| + ALL_TIME: 0,
|
| + WEEK: 1,
|
| + MONTH: 2
|
| +};
|
|
|
| +// Types:
|
| /**
|
| - * Parses query parameters from Location.
|
| - * @param {Location} location The URL to generate the CSS url for.
|
| - * @return {Object} Dictionary containing name value pairs for URL
|
| + * @typedef {{groupedOffset: number,
|
| + * incremental: boolean,
|
| + * querying: boolean,
|
| + * range: HistoryRange,
|
| + * searchTerm: string}}
|
| */
|
| -function parseQueryParams(location) {
|
| - var params = {};
|
| - var query = unescape(location.search.substring(1));
|
| - var vars = query.split('&');
|
| - for (var i = 0; i < vars.length; i++) {
|
| - var pair = vars[i].split('=');
|
| - params[pair[0]] = pair[1];
|
| - }
|
| - return params;
|
| -}
|
| +var QueryState;
|
|
|
| /**
|
| - * Creates a new URL by appending or replacing the given query key and value.
|
| - * Not supporting URL with username and password.
|
| - * @param {Location} location The original URL.
|
| - * @param {string} key The query parameter name.
|
| - * @param {string} value The query parameter value.
|
| - * @return {string} The constructed new URL.
|
| + * @typedef {{info: ?HistoryQuery,
|
| + * results: ?Array<!HistoryEntry>,
|
| + * sessionList: ?Array<!ForeignSession>}}
|
| */
|
| -function setQueryParam(location, key, value) {
|
| - var query = parseQueryParams(location);
|
| - query[encodeURIComponent(key)] = encodeURIComponent(value);
|
| +var QueryResult;
|
|
|
| - var newQuery = '';
|
| - for (var q in query) {
|
| - newQuery += (newQuery ? '&' : '?') + q + '=' + query[q];
|
| - }
|
| +/** @constructor
|
| + * @extends {MouseEvent} */
|
| +var DomRepeatClickEvent = function() {
|
| + this.model = null;
|
| +};
|
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - return location.origin + location.pathname + newQuery + location.hash;
|
| -}
|
| +cr.define('cr.ui', function() {
|
|
|
| -/**
|
| - * @param {Node} el A node to search for ancestors with |className|.
|
| - * @param {string} className A class to search for.
|
| - * @return {Element} A node with class of |className| or null if none is found.
|
| - */
|
| -function findAncestorByClass(el, className) {
|
| - return /** @type {Element} */(findAncestor(el, function(el) {
|
| - return el.classList && el.classList.contains(className);
|
| - }));
|
| -}
|
| + /**
|
| + * Decorates elements as an instance of a class.
|
| + * @param {string|!Element} source The way to find the element(s) to decorate.
|
| + * If this is a string then {@code querySeletorAll} is used to find the
|
| + * elements to decorate.
|
| + * @param {!Function} constr The constructor to decorate with. The constr
|
| + * needs to have a {@code decorate} function.
|
| + */
|
| + function decorate(source, constr) {
|
| + var elements;
|
| + if (typeof source == 'string')
|
| + elements = cr.doc.querySelectorAll(source);
|
| + else
|
| + elements = [source];
|
|
|
| -/**
|
| - * Return the first ancestor for which the {@code predicate} returns true.
|
| - * @param {Node} node The node to check.
|
| - * @param {function(Node):boolean} predicate The function that tests the
|
| - * nodes.
|
| - * @return {Node} The found ancestor or null if not found.
|
| - */
|
| -function findAncestor(node, predicate) {
|
| - var last = false;
|
| - while (node != null && !(last = predicate(node))) {
|
| - node = node.parentNode;
|
| + for (var i = 0, el; el = elements[i]; i++) {
|
| + if (!(el instanceof constr))
|
| + constr.decorate(el);
|
| + }
|
| }
|
| - return last ? node : null;
|
| -}
|
|
|
| -function swapDomNodes(a, b) {
|
| - var afterA = a.nextSibling;
|
| - if (afterA == b) {
|
| - swapDomNodes(b, a);
|
| - return;
|
| - }
|
| - var aParent = a.parentNode;
|
| - b.parentNode.replaceChild(a, b);
|
| - aParent.insertBefore(b, afterA);
|
| -}
|
| + /**
|
| + * Helper function for creating new element for define.
|
| + */
|
| + function createElementHelper(tagName, opt_bag) {
|
| + // Allow passing in ownerDocument to create in a different document.
|
| + var doc;
|
| + if (opt_bag && opt_bag.ownerDocument)
|
| + doc = opt_bag.ownerDocument;
|
| + else
|
| + doc = cr.doc;
|
| + return doc.createElement(tagName);
|
| + }
|
|
|
| -/**
|
| - * Disables text selection and dragging, with optional whitelist callbacks.
|
| - * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
|
| - * is defined and returns true, the onselectionstart event will be
|
| - * surpressed.
|
| - * @param {function(Event):boolean=} opt_allowDragStart Unless this function
|
| - * is defined and returns true, the ondragstart event will be surpressed.
|
| - */
|
| -function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
|
| - // Disable text selection.
|
| - document.onselectstart = function(e) {
|
| - if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e)))
|
| - e.preventDefault();
|
| - };
|
| + /**
|
| + * Creates the constructor for a UI element class.
|
| + *
|
| + * Usage:
|
| + * <pre>
|
| + * var List = cr.ui.define('list');
|
| + * List.prototype = {
|
| + * __proto__: HTMLUListElement.prototype,
|
| + * decorate: function() {
|
| + * ...
|
| + * },
|
| + * ...
|
| + * };
|
| + * </pre>
|
| + *
|
| + * @param {string|Function} tagNameOrFunction The tagName or
|
| + * function to use for newly created elements. If this is a function it
|
| + * needs to return a new element when called.
|
| + * @return {function(Object=):Element} The constructor function which takes
|
| + * an optional property bag. The function also has a static
|
| + * {@code decorate} method added to it.
|
| + */
|
| + function define(tagNameOrFunction) {
|
| + var createFunction, tagName;
|
| + if (typeof tagNameOrFunction == 'function') {
|
| + createFunction = tagNameOrFunction;
|
| + tagName = '';
|
| + } else {
|
| + createFunction = createElementHelper;
|
| + tagName = tagNameOrFunction;
|
| + }
|
|
|
| - // Disable dragging.
|
| - document.ondragstart = function(e) {
|
| - if (!(opt_allowDragStart && opt_allowDragStart.call(this, e)))
|
| - e.preventDefault();
|
| - };
|
| -}
|
| + /**
|
| + * Creates a new UI element constructor.
|
| + * @param {Object=} opt_propertyBag Optional bag of properties to set on the
|
| + * object after created. The property {@code ownerDocument} is special
|
| + * cased and it allows you to create the element in a different
|
| + * document than the default.
|
| + * @constructor
|
| + */
|
| + function f(opt_propertyBag) {
|
| + var el = createFunction(tagName, opt_propertyBag);
|
| + f.decorate(el);
|
| + for (var propertyName in opt_propertyBag) {
|
| + el[propertyName] = opt_propertyBag[propertyName];
|
| + }
|
| + return el;
|
| + }
|
|
|
| -/**
|
| - * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead.
|
| - * Call this to stop clicks on <a href="#"> links from scrolling to the top of
|
| - * the page (and possibly showing a # in the link).
|
| - */
|
| -function preventDefaultOnPoundLinkClicks() {
|
| - document.addEventListener('click', function(e) {
|
| - var anchor = findAncestor(/** @type {Node} */(e.target), function(el) {
|
| - return el.tagName == 'A';
|
| - });
|
| - // Use getAttribute() to prevent URL normalization.
|
| - if (anchor && anchor.getAttribute('href') == '#')
|
| - e.preventDefault();
|
| - });
|
| -}
|
| + /**
|
| + * Decorates an element as a UI element class.
|
| + * @param {!Element} el The element to decorate.
|
| + */
|
| + f.decorate = function(el) {
|
| + el.__proto__ = f.prototype;
|
| + el.decorate();
|
| + };
|
|
|
| -/**
|
| - * Check the directionality of the page.
|
| - * @return {boolean} True if Chrome is running an RTL UI.
|
| - */
|
| -function isRTL() {
|
| - return document.documentElement.dir == 'rtl';
|
| -}
|
| + return f;
|
| + }
|
|
|
| -/**
|
| - * Get an element that's known to exist by its ID. We use this instead of just
|
| - * calling getElementById and not checking the result because this lets us
|
| - * satisfy the JSCompiler type system.
|
| - * @param {string} id The identifier name.
|
| - * @return {!HTMLElement} the Element.
|
| - */
|
| -function getRequiredElement(id) {
|
| - return assertInstanceof($(id), HTMLElement,
|
| - 'Missing required element: ' + id);
|
| -}
|
| + /**
|
| + * Input elements do not grow and shrink with their content. This is a simple
|
| + * (and not very efficient) way of handling shrinking to content with support
|
| + * for min width and limited by the width of the parent element.
|
| + * @param {!HTMLElement} el The element to limit the width for.
|
| + * @param {!HTMLElement} parentEl The parent element that should limit the
|
| + * size.
|
| + * @param {number} min The minimum width.
|
| + * @param {number=} opt_scale Optional scale factor to apply to the width.
|
| + */
|
| + function limitInputWidth(el, parentEl, min, opt_scale) {
|
| + // Needs a size larger than borders
|
| + el.style.width = '10px';
|
| + var doc = el.ownerDocument;
|
| + var win = doc.defaultView;
|
| + var computedStyle = win.getComputedStyle(el);
|
| + var parentComputedStyle = win.getComputedStyle(parentEl);
|
| + var rtl = computedStyle.direction == 'rtl';
|
|
|
| -/**
|
| - * Query an element that's known to exist by a selector. We use this instead of
|
| - * just calling querySelector and not checking the result because this lets us
|
| - * satisfy the JSCompiler type system.
|
| - * @param {string} selectors CSS selectors to query the element.
|
| - * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional
|
| - * context object for querySelector.
|
| - * @return {!HTMLElement} the Element.
|
| - */
|
| -function queryRequiredElement(selectors, opt_context) {
|
| - var element = (opt_context || document).querySelector(selectors);
|
| - return assertInstanceof(element, HTMLElement,
|
| - 'Missing required element: ' + selectors);
|
| -}
|
| + // To get the max width we get the width of the treeItem minus the position
|
| + // of the input.
|
| + var inputRect = el.getBoundingClientRect(); // box-sizing
|
| + var parentRect = parentEl.getBoundingClientRect();
|
| + var startPos = rtl ? parentRect.right - inputRect.right :
|
| + inputRect.left - parentRect.left;
|
|
|
| -// Handle click on a link. If the link points to a chrome: or file: url, then
|
| -// call into the browser to do the navigation.
|
| -document.addEventListener('click', function(e) {
|
| - if (e.defaultPrevented)
|
| - return;
|
| + // Add up border and padding of the input.
|
| + var inner = parseInt(computedStyle.borderLeftWidth, 10) +
|
| + parseInt(computedStyle.paddingLeft, 10) +
|
| + parseInt(computedStyle.paddingRight, 10) +
|
| + parseInt(computedStyle.borderRightWidth, 10);
|
|
|
| - var el = e.target;
|
| - if (el.nodeType == Node.ELEMENT_NODE &&
|
| - el.webkitMatchesSelector('A, A *')) {
|
| - while (el.tagName != 'A') {
|
| - el = el.parentElement;
|
| + // We also need to subtract the padding of parent to prevent it to overflow.
|
| + var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
|
| + parseInt(parentComputedStyle.paddingRight, 10);
|
| +
|
| + var max = parentEl.clientWidth - startPos - inner - parentPadding;
|
| + if (opt_scale)
|
| + max *= opt_scale;
|
| +
|
| + function limit() {
|
| + if (el.scrollWidth > max) {
|
| + el.style.width = max + 'px';
|
| + } else {
|
| + el.style.width = 0;
|
| + var sw = el.scrollWidth;
|
| + if (sw < min) {
|
| + el.style.width = min + 'px';
|
| + } else {
|
| + el.style.width = sw + 'px';
|
| + }
|
| + }
|
| }
|
|
|
| - if ((el.protocol == 'file:' || el.protocol == 'about:') &&
|
| - (e.button == 0 || e.button == 1)) {
|
| - chrome.send('navigateToUrl', [
|
| - el.href,
|
| - el.target,
|
| - e.button,
|
| - e.altKey,
|
| - e.ctrlKey,
|
| - e.metaKey,
|
| - e.shiftKey
|
| - ]);
|
| + el.addEventListener('input', limit);
|
| + limit();
|
| + }
|
| +
|
| + /**
|
| + * Takes a number and spits out a value CSS will be happy with. To avoid
|
| + * subpixel layout issues, the value is rounded to the nearest integral value.
|
| + * @param {number} pixels The number of pixels.
|
| + * @return {string} e.g. '16px'.
|
| + */
|
| + function toCssPx(pixels) {
|
| + if (!window.isFinite(pixels))
|
| + console.error('Pixel value is not a number: ' + pixels);
|
| + return Math.round(pixels) + 'px';
|
| + }
|
| +
|
| + /**
|
| + * Users complain they occasionaly use doubleclicks instead of clicks
|
| + * (http://crbug.com/140364). To fix it we freeze click handling for
|
| + * the doubleclick time interval.
|
| + * @param {MouseEvent} e Initial click event.
|
| + */
|
| + function swallowDoubleClick(e) {
|
| + var doc = e.target.ownerDocument;
|
| + var counter = Math.min(1, e.detail);
|
| + function swallow(e) {
|
| + e.stopPropagation();
|
| e.preventDefault();
|
| }
|
| + function onclick(e) {
|
| + if (e.detail > counter) {
|
| + counter = e.detail;
|
| + // Swallow the click since it's a click inside the doubleclick timeout.
|
| + swallow(e);
|
| + } else {
|
| + // Stop tracking clicks and let regular handling.
|
| + doc.removeEventListener('dblclick', swallow, true);
|
| + doc.removeEventListener('click', onclick, true);
|
| + }
|
| + }
|
| + // The following 'click' event (if e.type == 'mouseup') mustn't be taken
|
| + // into account (it mustn't stop tracking clicks). Start event listening
|
| + // after zero timeout.
|
| + setTimeout(function() {
|
| + doc.addEventListener('click', onclick, true);
|
| + doc.addEventListener('dblclick', swallow, true);
|
| + }, 0);
|
| }
|
| -});
|
|
|
| -/**
|
| - * Creates a new URL which is the old URL with a GET param of key=value.
|
| - * @param {string} url The base URL. There is not sanity checking on the URL so
|
| - * it must be passed in a proper format.
|
| - * @param {string} key The key of the param.
|
| - * @param {string} value The value of the param.
|
| - * @return {string} The new URL.
|
| - */
|
| -function appendParam(url, key, value) {
|
| - var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
| -
|
| - if (url.indexOf('?') == -1)
|
| - return url + '?' + param;
|
| - return url + '&' + param;
|
| -}
|
| -
|
| -/**
|
| - * Creates an element of a specified type with a specified class name.
|
| - * @param {string} type The node type.
|
| - * @param {string} className The class name to use.
|
| - * @return {Element} The created element.
|
| - */
|
| -function createElementWithClassName(type, className) {
|
| - var elm = document.createElement(type);
|
| - elm.className = className;
|
| - return elm;
|
| -}
|
| + return {
|
| + decorate: decorate,
|
| + define: define,
|
| + limitInputWidth: limitInputWidth,
|
| + toCssPx: toCssPx,
|
| + swallowDoubleClick: swallowDoubleClick
|
| + };
|
| +});
|
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| /**
|
| - * webkitTransitionEnd does not always fire (e.g. when animation is aborted
|
| - * or when no paint happens during the animation). This function sets up
|
| - * a timer and emulate the event if it is not fired when the timer expires.
|
| - * @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
|
| - * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
|
| - * webkitTransitionEnd to happen. If not specified, it is fetched from |el|
|
| - * using the transitionDuration style value.
|
| + * @fileoverview A command is an abstraction of an action a user can do in the
|
| + * UI.
|
| + *
|
| + * When the focus changes in the document for each command a canExecute event
|
| + * is dispatched on the active element. By listening to this event you can
|
| + * enable and disable the command by setting the event.canExecute property.
|
| + *
|
| + * When a command is executed a command event is dispatched on the active
|
| + * element. Note that you should stop the propagation after you have handled the
|
| + * command if there might be other command listeners higher up in the DOM tree.
|
| */
|
| -function ensureTransitionEndEvent(el, opt_timeOut) {
|
| - if (opt_timeOut === undefined) {
|
| - var style = getComputedStyle(el);
|
| - opt_timeOut = parseFloat(style.transitionDuration) * 1000;
|
| -
|
| - // Give an additional 50ms buffer for the animation to complete.
|
| - opt_timeOut += 50;
|
| - }
|
|
|
| - var fired = false;
|
| - el.addEventListener('webkitTransitionEnd', function f(e) {
|
| - el.removeEventListener('webkitTransitionEnd', f);
|
| - fired = true;
|
| - });
|
| - window.setTimeout(function() {
|
| - if (!fired)
|
| - cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
|
| - }, opt_timeOut);
|
| -}
|
| +cr.define('cr.ui', function() {
|
|
|
| -/**
|
| - * Alias for document.scrollTop getter.
|
| - * @param {!HTMLDocument} doc The document node where information will be
|
| - * queried from.
|
| - * @return {number} The Y document scroll offset.
|
| - */
|
| -function scrollTopForDocument(doc) {
|
| - return doc.documentElement.scrollTop || doc.body.scrollTop;
|
| -}
|
| + /**
|
| + * This is used to identify keyboard shortcuts.
|
| + * @param {string} shortcut The text used to describe the keys for this
|
| + * keyboard shortcut.
|
| + * @constructor
|
| + */
|
| + function KeyboardShortcut(shortcut) {
|
| + var mods = {};
|
| + var ident = '';
|
| + shortcut.split('|').forEach(function(part) {
|
| + var partLc = part.toLowerCase();
|
| + switch (partLc) {
|
| + case 'alt':
|
| + case 'ctrl':
|
| + case 'meta':
|
| + case 'shift':
|
| + mods[partLc + 'Key'] = true;
|
| + break;
|
| + default:
|
| + if (ident)
|
| + throw Error('Invalid shortcut');
|
| + ident = part;
|
| + }
|
| + });
|
|
|
| -/**
|
| - * Alias for document.scrollTop setter.
|
| - * @param {!HTMLDocument} doc The document node where information will be
|
| - * queried from.
|
| - * @param {number} value The target Y scroll offset.
|
| - */
|
| -function setScrollTopForDocument(doc, value) {
|
| - doc.documentElement.scrollTop = doc.body.scrollTop = value;
|
| -}
|
| + this.ident_ = ident;
|
| + this.mods_ = mods;
|
| + }
|
|
|
| -/**
|
| - * Alias for document.scrollLeft getter.
|
| - * @param {!HTMLDocument} doc The document node where information will be
|
| - * queried from.
|
| - * @return {number} The X document scroll offset.
|
| - */
|
| -function scrollLeftForDocument(doc) {
|
| - return doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
| -}
|
| + KeyboardShortcut.prototype = {
|
| + /**
|
| + * Whether the keyboard shortcut object matches a keyboard event.
|
| + * @param {!Event} e The keyboard event object.
|
| + * @return {boolean} Whether we found a match or not.
|
| + */
|
| + matchesEvent: function(e) {
|
| + if (e.key == this.ident_) {
|
| + // All keyboard modifiers needs to match.
|
| + var mods = this.mods_;
|
| + return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
|
| + return e[k] == !!mods[k];
|
| + });
|
| + }
|
| + return false;
|
| + }
|
| + };
|
|
|
| -/**
|
| - * Alias for document.scrollLeft setter.
|
| - * @param {!HTMLDocument} doc The document node where information will be
|
| - * queried from.
|
| - * @param {number} value The target X scroll offset.
|
| - */
|
| -function setScrollLeftForDocument(doc, value) {
|
| - doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
|
| -}
|
| + /**
|
| + * Creates a new command element.
|
| + * @constructor
|
| + * @extends {HTMLElement}
|
| + */
|
| + var Command = cr.ui.define('command');
|
|
|
| -/**
|
| - * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
|
| - * @param {string} original The original string.
|
| - * @return {string} The string with all the characters mentioned above replaced.
|
| - */
|
| -function HTMLEscape(original) {
|
| - return original.replace(/&/g, '&')
|
| - .replace(/</g, '<')
|
| - .replace(/>/g, '>')
|
| - .replace(/"/g, '"')
|
| - .replace(/'/g, ''');
|
| -}
|
| + Command.prototype = {
|
| + __proto__: HTMLElement.prototype,
|
|
|
| -/**
|
| - * Shortens the provided string (if necessary) to a string of length at most
|
| - * |maxLength|.
|
| - * @param {string} original The original string.
|
| - * @param {number} maxLength The maximum length allowed for the string.
|
| - * @return {string} The original string if its length does not exceed
|
| - * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
|
| - * appended.
|
| - */
|
| -function elide(original, maxLength) {
|
| - if (original.length <= maxLength)
|
| - return original;
|
| - return original.substring(0, maxLength - 1) + '\u2026';
|
| -}
|
| + /**
|
| + * Initializes the command.
|
| + */
|
| + decorate: function() {
|
| + CommandManager.init(assert(this.ownerDocument));
|
|
|
| -/**
|
| - * Quote a string so it can be used in a regular expression.
|
| - * @param {string} str The source string.
|
| - * @return {string} The escaped string.
|
| - */
|
| -function quoteString(str) {
|
| - return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
|
| -}
|
| + if (this.hasAttribute('shortcut'))
|
| + this.shortcut = this.getAttribute('shortcut');
|
| + },
|
|
|
| -// <if expr="is_ios">
|
| -// Polyfill 'key' in KeyboardEvent for iOS.
|
| -// This function is not intended to be complete but should
|
| -// be sufficient enough to have iOS work correctly while
|
| -// it does not support key yet.
|
| -if (!('key' in KeyboardEvent.prototype)) {
|
| - Object.defineProperty(KeyboardEvent.prototype, 'key', {
|
| - /** @this {KeyboardEvent} */
|
| - get: function () {
|
| - // 0-9
|
| - if (this.keyCode >= 0x30 && this.keyCode <= 0x39)
|
| - return String.fromCharCode(this.keyCode);
|
| + /**
|
| + * Executes the command by dispatching a command event on the given element.
|
| + * If |element| isn't given, the active element is used instead.
|
| + * If the command is {@code disabled} this does nothing.
|
| + * @param {HTMLElement=} opt_element Optional element to dispatch event on.
|
| + */
|
| + execute: function(opt_element) {
|
| + if (this.disabled)
|
| + return;
|
| + var doc = this.ownerDocument;
|
| + if (doc.activeElement) {
|
| + var e = new Event('command', {bubbles: true});
|
| + e.command = this;
|
|
|
| - // A-Z
|
| - if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) {
|
| - var result = String.fromCharCode(this.keyCode).toLowerCase();
|
| - if (this.shiftKey)
|
| - result = result.toUpperCase();
|
| - return result;
|
| + (opt_element || doc.activeElement).dispatchEvent(e);
|
| }
|
| -
|
| - // Special characters
|
| - switch(this.keyCode) {
|
| - case 0x08: return 'Backspace';
|
| - case 0x09: return 'Tab';
|
| - case 0x0d: return 'Enter';
|
| - case 0x10: return 'Shift';
|
| - case 0x11: return 'Control';
|
| - case 0x12: return 'Alt';
|
| - case 0x1b: return 'Escape';
|
| - case 0x20: return ' ';
|
| - case 0x21: return 'PageUp';
|
| - case 0x22: return 'PageDown';
|
| - case 0x23: return 'End';
|
| - case 0x24: return 'Home';
|
| - case 0x25: return 'ArrowLeft';
|
| - case 0x26: return 'ArrowUp';
|
| - case 0x27: return 'ArrowRight';
|
| - case 0x28: return 'ArrowDown';
|
| - case 0x2d: return 'Insert';
|
| - case 0x2e: return 'Delete';
|
| - case 0x5b: return 'Meta';
|
| - case 0x70: return 'F1';
|
| - case 0x71: return 'F2';
|
| - case 0x72: return 'F3';
|
| - case 0x73: return 'F4';
|
| - case 0x74: return 'F5';
|
| - case 0x75: return 'F6';
|
| - case 0x76: return 'F7';
|
| - case 0x77: return 'F8';
|
| - case 0x78: return 'F9';
|
| - case 0x79: return 'F10';
|
| - case 0x7a: return 'F11';
|
| - case 0x7b: return 'F12';
|
| - case 0xbb: return '=';
|
| - case 0xbd: return '-';
|
| - case 0xdb: return '[';
|
| - case 0xdd: return ']';
|
| - }
|
| - return 'Unidentified';
|
| - }
|
| - });
|
| -} else {
|
| - window.console.log("KeyboardEvent.Key polyfill not required");
|
| -}
|
| -// </if> /* is_ios */
|
| -/**
|
| - * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
|
| - * coordinate the flow of resize events between "resizers" (elements that control the
|
| - * size or hidden state of their children) and "resizables" (elements that need to be
|
| - * notified when they are resized or un-hidden by their parents in order to take
|
| - * action on their new measurements).
|
| - *
|
| - * Elements that perform measurement should add the `IronResizableBehavior` behavior to
|
| - * their element definition and listen for the `iron-resize` event on themselves.
|
| - * This event will be fired when they become showing after having been hidden,
|
| - * when they are resized explicitly by another resizable, or when the window has been
|
| - * resized.
|
| - *
|
| - * Note, the `iron-resize` event is non-bubbling.
|
| - *
|
| - * @polymerBehavior Polymer.IronResizableBehavior
|
| - * @demo demo/index.html
|
| - **/
|
| - Polymer.IronResizableBehavior = {
|
| - properties: {
|
| - /**
|
| - * The closest ancestor element that implements `IronResizableBehavior`.
|
| - */
|
| - _parentResizable: {
|
| - type: Object,
|
| - observer: '_parentResizableChanged'
|
| - },
|
| -
|
| - /**
|
| - * True if this element is currently notifying its descedant elements of
|
| - * resize.
|
| - */
|
| - _notifyingDescendant: {
|
| - type: Boolean,
|
| - value: false
|
| - }
|
| - },
|
| -
|
| - listeners: {
|
| - 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
|
| - },
|
| -
|
| - created: function() {
|
| - // We don't really need property effects on these, and also we want them
|
| - // to be created before the `_parentResizable` observer fires:
|
| - this._interestedResizables = [];
|
| - this._boundNotifyResize = this.notifyResize.bind(this);
|
| - },
|
| -
|
| - attached: function() {
|
| - this.fire('iron-request-resize-notifications', null, {
|
| - node: this,
|
| - bubbles: true,
|
| - cancelable: true
|
| - });
|
| -
|
| - if (!this._parentResizable) {
|
| - window.addEventListener('resize', this._boundNotifyResize);
|
| - this.notifyResize();
|
| - }
|
| - },
|
| -
|
| - detached: function() {
|
| - if (this._parentResizable) {
|
| - this._parentResizable.stopResizeNotificationsFor(this);
|
| - } else {
|
| - window.removeEventListener('resize', this._boundNotifyResize);
|
| - }
|
| -
|
| - this._parentResizable = null;
|
| - },
|
| + },
|
|
|
| /**
|
| - * Can be called to manually notify a resizable and its descendant
|
| - * resizables of a resize change.
|
| + * Call this when there have been changes that might change whether the
|
| + * command can be executed or not.
|
| + * @param {Node=} opt_node Node for which to actuate command state.
|
| */
|
| - notifyResize: function() {
|
| - if (!this.isAttached) {
|
| - return;
|
| - }
|
| -
|
| - this._interestedResizables.forEach(function(resizable) {
|
| - if (this.resizerShouldNotify(resizable)) {
|
| - this._notifyDescendant(resizable);
|
| - }
|
| - }, this);
|
| -
|
| - this._fireResize();
|
| + canExecuteChange: function(opt_node) {
|
| + dispatchCanExecuteEvent(this,
|
| + opt_node || this.ownerDocument.activeElement);
|
| },
|
|
|
| /**
|
| - * Used to assign the closest resizable ancestor to this resizable
|
| - * if the ancestor detects a request for notifications.
|
| + * The keyboard shortcut that triggers the command. This is a string
|
| + * consisting of a key (as reported by WebKit in keydown) as
|
| + * well as optional key modifiers joinded with a '|'.
|
| + *
|
| + * Multiple keyboard shortcuts can be provided by separating them by
|
| + * whitespace.
|
| + *
|
| + * For example:
|
| + * "F1"
|
| + * "Backspace|Meta" for Apple command backspace.
|
| + * "a|Ctrl" for Control A
|
| + * "Delete Backspace|Meta" for Delete and Command Backspace
|
| + *
|
| + * @type {string}
|
| */
|
| - assignParentResizable: function(parentResizable) {
|
| - this._parentResizable = parentResizable;
|
| + shortcut_: '',
|
| + get shortcut() {
|
| + return this.shortcut_;
|
| },
|
| + set shortcut(shortcut) {
|
| + var oldShortcut = this.shortcut_;
|
| + if (shortcut !== oldShortcut) {
|
| + this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
|
| + return new KeyboardShortcut(shortcut);
|
| + });
|
|
|
| - /**
|
| - * Used to remove a resizable descendant from the list of descendants
|
| - * that should be notified of a resize change.
|
| - */
|
| - stopResizeNotificationsFor: function(target) {
|
| - var index = this._interestedResizables.indexOf(target);
|
| -
|
| - if (index > -1) {
|
| - this._interestedResizables.splice(index, 1);
|
| - this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
|
| + // Set this after the keyboardShortcuts_ since that might throw.
|
| + this.shortcut_ = shortcut;
|
| + cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
|
| + oldShortcut);
|
| }
|
| },
|
|
|
| /**
|
| - * This method can be overridden to filter nested elements that should or
|
| - * should not be notified by the current element. Return true if an element
|
| - * should be notified, or false if it should not be notified.
|
| - *
|
| - * @param {HTMLElement} element A candidate descendant element that
|
| - * implements `IronResizableBehavior`.
|
| - * @return {boolean} True if the `element` should be notified of resize.
|
| + * Whether the event object matches the shortcut for this command.
|
| + * @param {!Event} e The key event object.
|
| + * @return {boolean} Whether it matched or not.
|
| */
|
| - resizerShouldNotify: function(element) { return true; },
|
| -
|
| - _onDescendantIronResize: function(event) {
|
| - if (this._notifyingDescendant) {
|
| - event.stopPropagation();
|
| - return;
|
| - }
|
| -
|
| - // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
|
| - // otherwise non-bubbling event "just work." We do it manually here for
|
| - // the case where Polymer is not using shadow roots for whatever reason:
|
| - if (!Polymer.Settings.useShadow) {
|
| - this._fireResize();
|
| - }
|
| - },
|
| + matchesEvent: function(e) {
|
| + if (!this.keyboardShortcuts_)
|
| + return false;
|
|
|
| - _fireResize: function() {
|
| - this.fire('iron-resize', null, {
|
| - node: this,
|
| - bubbles: false
|
| + return this.keyboardShortcuts_.some(function(keyboardShortcut) {
|
| + return keyboardShortcut.matchesEvent(e);
|
| });
|
| },
|
| + };
|
|
|
| - _onIronRequestResizeNotifications: function(event) {
|
| - var target = event.path ? event.path[0] : event.target;
|
| + /**
|
| + * The label of the command.
|
| + */
|
| + cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
|
|
|
| - if (target === this) {
|
| - return;
|
| - }
|
| + /**
|
| + * Whether the command is disabled or not.
|
| + */
|
| + cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
|
|
|
| - if (this._interestedResizables.indexOf(target) === -1) {
|
| - this._interestedResizables.push(target);
|
| - this.listen(target, 'iron-resize', '_onDescendantIronResize');
|
| - }
|
| + /**
|
| + * Whether the command is hidden or not.
|
| + */
|
| + cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
|
|
|
| - target.assignParentResizable(this);
|
| - this._notifyDescendant(target);
|
| + /**
|
| + * Whether the command is checked or not.
|
| + */
|
| + cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
|
|
|
| - event.stopPropagation();
|
| - },
|
| + /**
|
| + * The flag that prevents the shortcut text from being displayed on menu.
|
| + *
|
| + * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
|
| + * is displayed in menu when the command is assosiated with a menu item.
|
| + * Otherwise, no text is displayed.
|
| + */
|
| + cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
|
|
|
| - _parentResizableChanged: function(parentResizable) {
|
| - if (parentResizable) {
|
| - window.removeEventListener('resize', this._boundNotifyResize);
|
| - }
|
| - },
|
| + /**
|
| + * Dispatches a canExecute event on the target.
|
| + * @param {!cr.ui.Command} command The command that we are testing for.
|
| + * @param {EventTarget} target The target element to dispatch the event on.
|
| + */
|
| + function dispatchCanExecuteEvent(command, target) {
|
| + var e = new CanExecuteEvent(command);
|
| + target.dispatchEvent(e);
|
| + command.disabled = !e.canExecute;
|
| + }
|
|
|
| - _notifyDescendant: function(descendant) {
|
| - // NOTE(cdata): In IE10, attached is fired on children first, so it's
|
| - // important not to notify them if the parent is not attached yet (or
|
| - // else they will get redundantly notified when the parent attaches).
|
| - if (!this.isAttached) {
|
| - return;
|
| - }
|
| + /**
|
| + * The command managers for different documents.
|
| + */
|
| + var commandManagers = {};
|
|
|
| - this._notifyingDescendant = true;
|
| - descendant.notifyResize();
|
| - this._notifyingDescendant = false;
|
| + /**
|
| + * Keeps track of the focused element and updates the commands when the focus
|
| + * changes.
|
| + * @param {!Document} doc The document that we are managing the commands for.
|
| + * @constructor
|
| + */
|
| + function CommandManager(doc) {
|
| + doc.addEventListener('focus', this.handleFocus_.bind(this), true);
|
| + // Make sure we add the listener to the bubbling phase so that elements can
|
| + // prevent the command.
|
| + doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
|
| + }
|
| +
|
| + /**
|
| + * Initializes a command manager for the document as needed.
|
| + * @param {!Document} doc The document to manage the commands for.
|
| + */
|
| + CommandManager.init = function(doc) {
|
| + var uid = cr.getUid(doc);
|
| + if (!(uid in commandManagers)) {
|
| + commandManagers[uid] = new CommandManager(doc);
|
| }
|
| };
|
| -(function() {
|
| - 'use strict';
|
| -
|
| - /**
|
| - * Chrome uses an older version of DOM Level 3 Keyboard Events
|
| - *
|
| - * Most keys are labeled as text, but some are Unicode codepoints.
|
| - * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
|
| - */
|
| - var KEY_IDENTIFIER = {
|
| - 'U+0008': 'backspace',
|
| - 'U+0009': 'tab',
|
| - 'U+001B': 'esc',
|
| - 'U+0020': 'space',
|
| - 'U+007F': 'del'
|
| - };
|
|
|
| - /**
|
| - * Special table for KeyboardEvent.keyCode.
|
| - * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
|
| - * than that.
|
| - *
|
| - * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
|
| - */
|
| - var KEY_CODE = {
|
| - 8: 'backspace',
|
| - 9: 'tab',
|
| - 13: 'enter',
|
| - 27: 'esc',
|
| - 33: 'pageup',
|
| - 34: 'pagedown',
|
| - 35: 'end',
|
| - 36: 'home',
|
| - 32: 'space',
|
| - 37: 'left',
|
| - 38: 'up',
|
| - 39: 'right',
|
| - 40: 'down',
|
| - 46: 'del',
|
| - 106: '*'
|
| - };
|
| + CommandManager.prototype = {
|
|
|
| /**
|
| - * MODIFIER_KEYS maps the short name for modifier keys used in a key
|
| - * combo string to the property name that references those same keys
|
| - * in a KeyboardEvent instance.
|
| + * Handles focus changes on the document.
|
| + * @param {Event} e The focus event object.
|
| + * @private
|
| + * @suppress {checkTypes}
|
| + * TODO(vitalyp): remove the suppression.
|
| */
|
| - var MODIFIER_KEYS = {
|
| - 'shift': 'shiftKey',
|
| - 'ctrl': 'ctrlKey',
|
| - 'alt': 'altKey',
|
| - 'meta': 'metaKey'
|
| - };
|
| + handleFocus_: function(e) {
|
| + var target = e.target;
|
|
|
| - /**
|
| - * KeyboardEvent.key is mostly represented by printable character made by
|
| - * the keyboard, with unprintable keys labeled nicely.
|
| - *
|
| - * However, on OS X, Alt+char can make a Unicode character that follows an
|
| - * Apple-specific mapping. In this case, we fall back to .keyCode.
|
| - */
|
| - var KEY_CHAR = /[a-z0-9*]/;
|
| + // Ignore focus on a menu button or command item.
|
| + if (target.menu || target.command)
|
| + return;
|
|
|
| - /**
|
| - * Matches a keyIdentifier string.
|
| - */
|
| - var IDENT_CHAR = /U\+/;
|
| + var commands = Array.prototype.slice.call(
|
| + target.ownerDocument.querySelectorAll('command'));
|
|
|
| - /**
|
| - * Matches arrow keys in Gecko 27.0+
|
| - */
|
| - var ARROW_KEY = /^arrow/;
|
| + commands.forEach(function(command) {
|
| + dispatchCanExecuteEvent(command, target);
|
| + });
|
| + },
|
|
|
| /**
|
| - * Matches space keys everywhere (notably including IE10's exceptional name
|
| - * `spacebar`).
|
| + * Handles the keydown event and routes it to the right command.
|
| + * @param {!Event} e The keydown event.
|
| */
|
| - var SPACE_KEY = /^space(bar)?/;
|
| + handleKeyDown_: function(e) {
|
| + var target = e.target;
|
| + var commands = Array.prototype.slice.call(
|
| + target.ownerDocument.querySelectorAll('command'));
|
|
|
| - /**
|
| - * Matches ESC key.
|
| - *
|
| - * Value from: http://w3c.github.io/uievents-key/#key-Escape
|
| - */
|
| - var ESC_KEY = /^escape$/;
|
| + for (var i = 0, command; command = commands[i]; i++) {
|
| + if (command.matchesEvent(e)) {
|
| + // When invoking a command via a shortcut, we have to manually check
|
| + // if it can be executed, since focus might not have been changed
|
| + // what would have updated the command's state.
|
| + command.canExecuteChange();
|
|
|
| - /**
|
| - * Transforms the key.
|
| - * @param {string} key The KeyBoardEvent.key
|
| - * @param {Boolean} [noSpecialChars] Limits the transformation to
|
| - * alpha-numeric characters.
|
| - */
|
| - function transformKey(key, noSpecialChars) {
|
| - var validKey = '';
|
| - if (key) {
|
| - var lKey = key.toLowerCase();
|
| - if (lKey === ' ' || SPACE_KEY.test(lKey)) {
|
| - validKey = 'space';
|
| - } else if (ESC_KEY.test(lKey)) {
|
| - validKey = 'esc';
|
| - } else if (lKey.length == 1) {
|
| - if (!noSpecialChars || KEY_CHAR.test(lKey)) {
|
| - validKey = lKey;
|
| + if (!command.disabled) {
|
| + e.preventDefault();
|
| + // We do not want any other element to handle this.
|
| + e.stopPropagation();
|
| + command.execute();
|
| + return;
|
| }
|
| - } else if (ARROW_KEY.test(lKey)) {
|
| - validKey = lKey.replace('arrow', '');
|
| - } else if (lKey == 'multiply') {
|
| - // numpad '*' can map to Multiply on IE/Windows
|
| - validKey = '*';
|
| - } else {
|
| - validKey = lKey;
|
| }
|
| }
|
| - return validKey;
|
| }
|
| + };
|
|
|
| - function transformKeyIdentifier(keyIdent) {
|
| - var validKey = '';
|
| - if (keyIdent) {
|
| - if (keyIdent in KEY_IDENTIFIER) {
|
| - validKey = KEY_IDENTIFIER[keyIdent];
|
| - } else if (IDENT_CHAR.test(keyIdent)) {
|
| - keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
|
| - validKey = String.fromCharCode(keyIdent).toLowerCase();
|
| - } else {
|
| - validKey = keyIdent.toLowerCase();
|
| - }
|
| - }
|
| - return validKey;
|
| - }
|
| + /**
|
| + * The event type used for canExecute events.
|
| + * @param {!cr.ui.Command} command The command that we are evaluating.
|
| + * @extends {Event}
|
| + * @constructor
|
| + * @class
|
| + */
|
| + function CanExecuteEvent(command) {
|
| + var e = new Event('canExecute', {bubbles: true, cancelable: true});
|
| + e.__proto__ = CanExecuteEvent.prototype;
|
| + e.command = command;
|
| + return e;
|
| + }
|
|
|
| - function transformKeyCode(keyCode) {
|
| - var validKey = '';
|
| - if (Number(keyCode)) {
|
| - if (keyCode >= 65 && keyCode <= 90) {
|
| - // ascii a-z
|
| - // lowercase is 32 offset from uppercase
|
| - validKey = String.fromCharCode(32 + keyCode);
|
| - } else if (keyCode >= 112 && keyCode <= 123) {
|
| - // function keys f1-f12
|
| - validKey = 'f' + (keyCode - 112);
|
| - } else if (keyCode >= 48 && keyCode <= 57) {
|
| - // top 0-9 keys
|
| - validKey = String(keyCode - 48);
|
| - } else if (keyCode >= 96 && keyCode <= 105) {
|
| - // num pad 0-9
|
| - validKey = String(keyCode - 96);
|
| - } else {
|
| - validKey = KEY_CODE[keyCode];
|
| - }
|
| - }
|
| - return validKey;
|
| - }
|
| + CanExecuteEvent.prototype = {
|
| + __proto__: Event.prototype,
|
|
|
| /**
|
| - * Calculates the normalized key for a KeyboardEvent.
|
| - * @param {KeyboardEvent} keyEvent
|
| - * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
|
| - * transformation to alpha-numeric chars. This is useful with key
|
| - * combinations like shift + 2, which on FF for MacOS produces
|
| - * keyEvent.key = @
|
| - * To get 2 returned, set noSpecialChars = true
|
| - * To get @ returned, set noSpecialChars = false
|
| + * The current command
|
| + * @type {cr.ui.Command}
|
| */
|
| - function normalizedKeyForEvent(keyEvent, noSpecialChars) {
|
| - // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
|
| - // .detail.key to support artificial keyboard events.
|
| - return transformKey(keyEvent.key, noSpecialChars) ||
|
| - transformKeyIdentifier(keyEvent.keyIdentifier) ||
|
| - transformKeyCode(keyEvent.keyCode) ||
|
| - transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
|
| - }
|
| + command: null,
|
|
|
| - function keyComboMatchesEvent(keyCombo, event) {
|
| - // For combos with modifiers we support only alpha-numeric keys
|
| - var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
|
| - return keyEvent === keyCombo.key &&
|
| - (!keyCombo.hasModifiers || (
|
| - !!event.shiftKey === !!keyCombo.shiftKey &&
|
| - !!event.ctrlKey === !!keyCombo.ctrlKey &&
|
| - !!event.altKey === !!keyCombo.altKey &&
|
| - !!event.metaKey === !!keyCombo.metaKey)
|
| - );
|
| + /**
|
| + * Whether the target can execute the command. Setting this also stops the
|
| + * propagation and prevents the default. Callers can tell if an event has
|
| + * been handled via |this.defaultPrevented|.
|
| + * @type {boolean}
|
| + */
|
| + canExecute_: false,
|
| + get canExecute() {
|
| + return this.canExecute_;
|
| + },
|
| + set canExecute(canExecute) {
|
| + this.canExecute_ = !!canExecute;
|
| + this.stopPropagation();
|
| + this.preventDefault();
|
| }
|
| + };
|
|
|
| - function parseKeyComboString(keyComboString) {
|
| - if (keyComboString.length === 1) {
|
| - return {
|
| - combo: keyComboString,
|
| - key: keyComboString,
|
| - event: 'keydown'
|
| - };
|
| - }
|
| - return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
|
| - var eventParts = keyComboPart.split(':');
|
| - var keyName = eventParts[0];
|
| - var event = eventParts[1];
|
| -
|
| - if (keyName in MODIFIER_KEYS) {
|
| - parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
|
| - parsedKeyCombo.hasModifiers = true;
|
| - } else {
|
| - parsedKeyCombo.key = keyName;
|
| - parsedKeyCombo.event = event || 'keydown';
|
| - }
|
| -
|
| - return parsedKeyCombo;
|
| - }, {
|
| - combo: keyComboString.split(':').shift()
|
| - });
|
| - }
|
| -
|
| - function parseEventString(eventString) {
|
| - return eventString.trim().split(' ').map(function(keyComboString) {
|
| - return parseKeyComboString(keyComboString);
|
| - });
|
| - }
|
| + // Export
|
| + return {
|
| + Command: Command,
|
| + CanExecuteEvent: CanExecuteEvent
|
| + };
|
| +});
|
| +Polymer({
|
| + is: 'app-drawer',
|
|
|
| - /**
|
| - * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
|
| - * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
|
| - * The element takes care of browser differences with respect to Keyboard events
|
| - * and uses an expressive syntax to filter key presses.
|
| - *
|
| - * Use the `keyBindings` prototype property to express what combination of keys
|
| - * will trigger the callback. A key binding has the format
|
| - * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
|
| - * `"KEY:EVENT": "callback"` are valid as well). Some examples:
|
| - *
|
| - * keyBindings: {
|
| - * 'space': '_onKeydown', // same as 'space:keydown'
|
| - * 'shift+tab': '_onKeydown',
|
| - * 'enter:keypress': '_onKeypress',
|
| - * 'esc:keyup': '_onKeyup'
|
| - * }
|
| - *
|
| - * The callback will receive with an event containing the following information in `event.detail`:
|
| - *
|
| - * _onKeydown: function(event) {
|
| - * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
|
| - * console.log(event.detail.key); // KEY only, e.g. "tab"
|
| - * console.log(event.detail.event); // EVENT, e.g. "keydown"
|
| - * console.log(event.detail.keyboardEvent); // the original KeyboardEvent
|
| - * }
|
| - *
|
| - * Use the `keyEventTarget` attribute to set up event handlers on a specific
|
| - * node.
|
| - *
|
| - * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
|
| - * for an example.
|
| - *
|
| - * @demo demo/index.html
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.IronA11yKeysBehavior = {
|
| properties: {
|
| /**
|
| - * The EventTarget that will be firing relevant KeyboardEvents. Set it to
|
| - * `null` to disable the listeners.
|
| - * @type {?EventTarget}
|
| + * The opened state of the drawer.
|
| */
|
| - keyEventTarget: {
|
| - type: Object,
|
| - value: function() {
|
| - return this;
|
| - }
|
| + opened: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + reflectToAttribute: true
|
| },
|
|
|
| /**
|
| - * If true, this property will cause the implementing element to
|
| - * automatically stop propagation on any handled KeyboardEvents.
|
| + * The drawer does not have a scrim and cannot be swiped close.
|
| */
|
| - stopKeyboardEventPropagation: {
|
| + persistent: {
|
| type: Boolean,
|
| - value: false
|
| + value: false,
|
| + reflectToAttribute: true
|
| },
|
|
|
| - _boundKeyHandlers: {
|
| - type: Array,
|
| - value: function() {
|
| - return [];
|
| - }
|
| + /**
|
| + * The alignment of the drawer on the screen ('left', 'right', 'start' or 'end').
|
| + * 'start' computes to left and 'end' to right in LTR layout and vice versa in RTL
|
| + * layout.
|
| + */
|
| + align: {
|
| + type: String,
|
| + value: 'left'
|
| },
|
|
|
| - // We use this due to a limitation in IE10 where instances will have
|
| - // own properties of everything on the "prototype".
|
| - _imperativeKeyBindings: {
|
| - type: Object,
|
| - value: function() {
|
| - return {};
|
| - }
|
| + /**
|
| + * The computed, read-only position of the drawer on the screen ('left' or 'right').
|
| + */
|
| + position: {
|
| + type: String,
|
| + readOnly: true,
|
| + value: 'left',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * Create an area at the edge of the screen to swipe open the drawer.
|
| + */
|
| + swipeOpen: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * Trap keyboard focus when the drawer is opened and not persistent.
|
| + */
|
| + noFocusTrap: {
|
| + type: Boolean,
|
| + value: false
|
| }
|
| },
|
|
|
| observers: [
|
| - '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
|
| + 'resetLayout(position)',
|
| + '_resetPosition(align, isAttached)'
|
| ],
|
|
|
| + _translateOffset: 0,
|
|
|
| - /**
|
| - * To be used to express what combination of keys will trigger the relative
|
| - * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
|
| - * @type {Object}
|
| - */
|
| - keyBindings: {},
|
| + _trackDetails: null,
|
|
|
| - registered: function() {
|
| - this._prepKeyBindings();
|
| + _drawerState: 0,
|
| +
|
| + _boundEscKeydownHandler: null,
|
| +
|
| + _firstTabStop: null,
|
| +
|
| + _lastTabStop: null,
|
| +
|
| + ready: function() {
|
| + // Set the scroll direction so you can vertically scroll inside the drawer.
|
| + this.setScrollDirection('y');
|
| +
|
| + // Only transition the drawer after its first render (e.g. app-drawer-layout
|
| + // may need to set the initial opened state which should not be transitioned).
|
| + this._setTransitionDuration('0s');
|
| },
|
|
|
| attached: function() {
|
| - this._listenKeyEventListeners();
|
| + // Only transition the drawer after its first render (e.g. app-drawer-layout
|
| + // may need to set the initial opened state which should not be transitioned).
|
| + Polymer.RenderStatus.afterNextRender(this, function() {
|
| + this._setTransitionDuration('');
|
| + this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
|
| + this._resetDrawerState();
|
| +
|
| + this.listen(this, 'track', '_track');
|
| + this.addEventListener('transitionend', this._transitionend.bind(this));
|
| + this.addEventListener('keydown', this._tabKeydownHandler.bind(this))
|
| + });
|
| },
|
|
|
| detached: function() {
|
| - this._unlistenKeyEventListeners();
|
| + document.removeEventListener('keydown', this._boundEscKeydownHandler);
|
| },
|
|
|
| /**
|
| - * Can be used to imperatively add a key binding to the implementing
|
| - * element. This is the imperative equivalent of declaring a keybinding
|
| - * in the `keyBindings` prototype property.
|
| + * Opens the drawer.
|
| */
|
| - addOwnKeyBinding: function(eventString, handlerName) {
|
| - this._imperativeKeyBindings[eventString] = handlerName;
|
| - this._prepKeyBindings();
|
| - this._resetKeyEventListeners();
|
| + open: function() {
|
| + this.opened = true;
|
| },
|
|
|
| /**
|
| - * When called, will remove all imperatively-added key bindings.
|
| + * Closes the drawer.
|
| */
|
| - removeOwnKeyBindings: function() {
|
| - this._imperativeKeyBindings = {};
|
| - this._prepKeyBindings();
|
| - this._resetKeyEventListeners();
|
| + close: function() {
|
| + this.opened = false;
|
| },
|
|
|
| /**
|
| - * Returns true if a keyboard event matches `eventString`.
|
| + * Toggles the drawer open and close.
|
| + */
|
| + toggle: function() {
|
| + this.opened = !this.opened;
|
| + },
|
| +
|
| + /**
|
| + * Gets the width of the drawer.
|
| *
|
| - * @param {KeyboardEvent} event
|
| - * @param {string} eventString
|
| - * @return {boolean}
|
| + * @return {number} The width of the drawer in pixels.
|
| */
|
| - keyboardEventMatchesKeys: function(event, eventString) {
|
| - var keyCombos = parseEventString(eventString);
|
| - for (var i = 0; i < keyCombos.length; ++i) {
|
| - if (keyComboMatchesEvent(keyCombos[i], event)) {
|
| - return true;
|
| - }
|
| - }
|
| - return false;
|
| + getWidth: function() {
|
| + return this.$.contentContainer.offsetWidth;
|
| },
|
|
|
| - _collectKeyBindings: function() {
|
| - var keyBindings = this.behaviors.map(function(behavior) {
|
| - return behavior.keyBindings;
|
| - });
|
| + /**
|
| + * Resets the layout. If you changed the size of app-header via CSS
|
| + * you can notify the changes by either firing the `iron-resize` event
|
| + * or calling `resetLayout` directly.
|
| + *
|
| + * @method resetLayout
|
| + */
|
| + resetLayout: function() {
|
| + this.debounce('_resetLayout', function() {
|
| + this.fire('app-drawer-reset-layout');
|
| + }, 1);
|
| + },
|
|
|
| - if (keyBindings.indexOf(this.keyBindings) === -1) {
|
| - keyBindings.push(this.keyBindings);
|
| + _isRTL: function() {
|
| + return window.getComputedStyle(this).direction === 'rtl';
|
| + },
|
| +
|
| + _resetPosition: function() {
|
| + switch (this.align) {
|
| + case 'start':
|
| + this._setPosition(this._isRTL() ? 'right' : 'left');
|
| + return;
|
| + case 'end':
|
| + this._setPosition(this._isRTL() ? 'left' : 'right');
|
| + return;
|
| }
|
| + this._setPosition(this.align);
|
| + },
|
|
|
| - return keyBindings;
|
| + _escKeydownHandler: function(event) {
|
| + var ESC_KEYCODE = 27;
|
| + if (event.keyCode === ESC_KEYCODE) {
|
| + // Prevent any side effects if app-drawer closes.
|
| + event.preventDefault();
|
| + this.close();
|
| + }
|
| },
|
|
|
| - _prepKeyBindings: function() {
|
| - this._keyBindings = {};
|
| + _track: function(event) {
|
| + if (this.persistent) {
|
| + return;
|
| + }
|
|
|
| - this._collectKeyBindings().forEach(function(keyBindings) {
|
| - for (var eventString in keyBindings) {
|
| - this._addKeyBinding(eventString, keyBindings[eventString]);
|
| - }
|
| - }, this);
|
| + // Disable user selection on desktop.
|
| + event.preventDefault();
|
|
|
| - for (var eventString in this._imperativeKeyBindings) {
|
| - this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
|
| + switch (event.detail.state) {
|
| + case 'start':
|
| + this._trackStart(event);
|
| + break;
|
| + case 'track':
|
| + this._trackMove(event);
|
| + break;
|
| + case 'end':
|
| + this._trackEnd(event);
|
| + break;
|
| }
|
| + },
|
|
|
| - // Give precedence to combos with modifiers to be checked first.
|
| - for (var eventName in this._keyBindings) {
|
| - this._keyBindings[eventName].sort(function (kb1, kb2) {
|
| - var b1 = kb1[0].hasModifiers;
|
| - var b2 = kb2[0].hasModifiers;
|
| - return (b1 === b2) ? 0 : b1 ? -1 : 1;
|
| - })
|
| + _trackStart: function(event) {
|
| + this._drawerState = this._DRAWER_STATE.TRACKING;
|
| +
|
| + // Disable transitions since style attributes will reflect user track events.
|
| + this._setTransitionDuration('0s');
|
| + this.style.visibility = 'visible';
|
| +
|
| + var rect = this.$.contentContainer.getBoundingClientRect();
|
| + if (this.position === 'left') {
|
| + this._translateOffset = rect.left;
|
| + } else {
|
| + this._translateOffset = rect.right - window.innerWidth;
|
| }
|
| +
|
| + this._trackDetails = [];
|
| },
|
|
|
| - _addKeyBinding: function(eventString, handlerName) {
|
| - parseEventString(eventString).forEach(function(keyCombo) {
|
| - this._keyBindings[keyCombo.event] =
|
| - this._keyBindings[keyCombo.event] || [];
|
| + _trackMove: function(event) {
|
| + this._translateDrawer(event.detail.dx + this._translateOffset);
|
|
|
| - this._keyBindings[keyCombo.event].push([
|
| - keyCombo,
|
| - handlerName
|
| - ]);
|
| - }, this);
|
| + // Use Date.now() since event.timeStamp is inconsistent across browsers (e.g. most
|
| + // browsers use milliseconds but FF 44 uses microseconds).
|
| + this._trackDetails.push({
|
| + dx: event.detail.dx,
|
| + timeStamp: Date.now()
|
| + });
|
| },
|
|
|
| - _resetKeyEventListeners: function() {
|
| - this._unlistenKeyEventListeners();
|
| + _trackEnd: function(event) {
|
| + var x = event.detail.dx + this._translateOffset;
|
| + var drawerWidth = this.getWidth();
|
| + var isPositionLeft = this.position === 'left';
|
| + var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
|
| + (x <= 0 || x >= drawerWidth);
|
|
|
| - if (this.isAttached) {
|
| - this._listenKeyEventListeners();
|
| + if (!isInEndState) {
|
| + // No longer need the track events after this method returns - allow them to be GC'd.
|
| + var trackDetails = this._trackDetails;
|
| + this._trackDetails = null;
|
| +
|
| + this._flingDrawer(event, trackDetails);
|
| + if (this._drawerState === this._DRAWER_STATE.FLINGING) {
|
| + return;
|
| + }
|
| }
|
| - },
|
|
|
| - _listenKeyEventListeners: function() {
|
| - if (!this.keyEventTarget) {
|
| - return;
|
| + // If the drawer is not flinging, toggle the opened state based on the position of
|
| + // the drawer.
|
| + var halfWidth = drawerWidth / 2;
|
| + if (event.detail.dx < -halfWidth) {
|
| + this.opened = this.position === 'right';
|
| + } else if (event.detail.dx > halfWidth) {
|
| + this.opened = this.position === 'left';
|
| }
|
| - Object.keys(this._keyBindings).forEach(function(eventName) {
|
| - var keyBindings = this._keyBindings[eventName];
|
| - var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
|
|
|
| - this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
|
| + // Trigger app-drawer-transitioned now since there will be no transitionend event.
|
| + if (isInEndState) {
|
| + this._resetDrawerState();
|
| + }
|
|
|
| - this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
|
| - }, this);
|
| + this._setTransitionDuration('');
|
| + this._resetDrawerTranslate();
|
| + this.style.visibility = '';
|
| },
|
|
|
| - _unlistenKeyEventListeners: function() {
|
| - var keyHandlerTuple;
|
| - var keyEventTarget;
|
| - var eventName;
|
| - var boundKeyHandler;
|
| + _calculateVelocity: function(event, trackDetails) {
|
| + // Find the oldest track event that is within 100ms using binary search.
|
| + var now = Date.now();
|
| + var timeLowerBound = now - 100;
|
| + var trackDetail;
|
| + var min = 0;
|
| + var max = trackDetails.length - 1;
|
|
|
| - while (this._boundKeyHandlers.length) {
|
| - // My kingdom for block-scope binding and destructuring assignment..
|
| - keyHandlerTuple = this._boundKeyHandlers.pop();
|
| - keyEventTarget = keyHandlerTuple[0];
|
| - eventName = keyHandlerTuple[1];
|
| - boundKeyHandler = keyHandlerTuple[2];
|
| + while (min <= max) {
|
| + // Floor of average of min and max.
|
| + var mid = (min + max) >> 1;
|
| + var d = trackDetails[mid];
|
| + if (d.timeStamp >= timeLowerBound) {
|
| + trackDetail = d;
|
| + max = mid - 1;
|
| + } else {
|
| + min = mid + 1;
|
| + }
|
| + }
|
|
|
| - keyEventTarget.removeEventListener(eventName, boundKeyHandler);
|
| + if (trackDetail) {
|
| + var dx = event.detail.dx - trackDetail.dx;
|
| + var dt = (now - trackDetail.timeStamp) || 1;
|
| + return dx / dt;
|
| }
|
| + return 0;
|
| },
|
|
|
| - _onKeyBindingEvent: function(keyBindings, event) {
|
| - if (this.stopKeyboardEventPropagation) {
|
| - event.stopPropagation();
|
| - }
|
| + _flingDrawer: function(event, trackDetails) {
|
| + var velocity = this._calculateVelocity(event, trackDetails);
|
|
|
| - // if event has been already prevented, don't do anything
|
| - if (event.defaultPrevented) {
|
| + // Do not fling if velocity is not above a threshold.
|
| + if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
|
| return;
|
| }
|
|
|
| - for (var i = 0; i < keyBindings.length; i++) {
|
| - var keyCombo = keyBindings[i][0];
|
| - var handlerName = keyBindings[i][1];
|
| - if (keyComboMatchesEvent(keyCombo, event)) {
|
| - this._triggerKeyHandler(keyCombo, handlerName, event);
|
| - // exit the loop if eventDefault was prevented
|
| - if (event.defaultPrevented) {
|
| - return;
|
| - }
|
| - }
|
| + this._drawerState = this._DRAWER_STATE.FLINGING;
|
| +
|
| + var x = event.detail.dx + this._translateOffset;
|
| + var drawerWidth = this.getWidth();
|
| + var isPositionLeft = this.position === 'left';
|
| + var isVelocityPositive = velocity > 0;
|
| + var isClosingLeft = !isVelocityPositive && isPositionLeft;
|
| + var isClosingRight = isVelocityPositive && !isPositionLeft;
|
| + var dx;
|
| + if (isClosingLeft) {
|
| + dx = -(x + drawerWidth);
|
| + } else if (isClosingRight) {
|
| + dx = (drawerWidth - x);
|
| + } else {
|
| + dx = -x;
|
| }
|
| - },
|
|
|
| - _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
|
| - var detail = Object.create(keyCombo);
|
| - detail.keyboardEvent = keyboardEvent;
|
| - var event = new CustomEvent(keyCombo.event, {
|
| - detail: detail,
|
| - cancelable: true
|
| - });
|
| - this[handlerName].call(this, event);
|
| - if (event.defaultPrevented) {
|
| - keyboardEvent.preventDefault();
|
| + // Enforce a minimum transition velocity to make the drawer feel snappy.
|
| + if (isVelocityPositive) {
|
| + velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
|
| + this.opened = this.position === 'left';
|
| + } else {
|
| + velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
|
| + this.opened = this.position === 'right';
|
| }
|
| - }
|
| - };
|
| - })();
|
| -/**
|
| - * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll events from a
|
| - * designated scroll target.
|
| - *
|
| - * Elements that consume this behavior can override the `_scrollHandler`
|
| - * method to add logic on the scroll event.
|
| - *
|
| - * @demo demo/scrolling-region.html Scrolling Region
|
| - * @demo demo/document.html Document Element
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.IronScrollTargetBehavior = {
|
|
|
| - properties: {
|
| + // Calculate the amount of time needed to finish the transition based on the
|
| + // initial slope of the timing function.
|
| + this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms');
|
| + this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);
|
|
|
| - /**
|
| - * Specifies the element that will handle the scroll event
|
| - * on the behalf of the current element. This is typically a reference to an element,
|
| - * but there are a few more posibilities:
|
| - *
|
| - * ### Elements id
|
| - *
|
| - *```html
|
| - * <div id="scrollable-element" style="overflow: auto;">
|
| - * <x-element scroll-target="scrollable-element">
|
| - * \x3c!-- Content--\x3e
|
| - * </x-element>
|
| - * </div>
|
| - *```
|
| - * In this case, the `scrollTarget` will point to the outer div element.
|
| - *
|
| - * ### Document scrolling
|
| - *
|
| - * For document scrolling, you can use the reserved word `document`:
|
| - *
|
| - *```html
|
| - * <x-element scroll-target="document">
|
| - * \x3c!-- Content --\x3e
|
| - * </x-element>
|
| - *```
|
| - *
|
| - * ### Elements reference
|
| - *
|
| - *```js
|
| - * appHeader.scrollTarget = document.querySelector('#scrollable-element');
|
| - *```
|
| - *
|
| - * @type {HTMLElement}
|
| - */
|
| - scrollTarget: {
|
| - type: HTMLElement,
|
| - value: function() {
|
| - return this._defaultScrollTarget;
|
| + this._resetDrawerTranslate();
|
| + },
|
| +
|
| + _transitionend: function(event) {
|
| + // contentContainer will transition on opened state changed, and scrim will
|
| + // transition on persistent state changed when opened - these are the
|
| + // transitions we are interested in.
|
| + var target = Polymer.dom(event).rootTarget;
|
| + if (target === this.$.contentContainer || target === this.$.scrim) {
|
| +
|
| + // If the drawer was flinging, we need to reset the style attributes.
|
| + if (this._drawerState === this._DRAWER_STATE.FLINGING) {
|
| + this._setTransitionDuration('');
|
| + this._setTransitionTimingFunction('');
|
| + this.style.visibility = '';
|
| + }
|
| +
|
| + this._resetDrawerState();
|
| }
|
| - }
|
| - },
|
| + },
|
|
|
| - observers: [
|
| - '_scrollTargetChanged(scrollTarget, isAttached)'
|
| - ],
|
| + _setTransitionDuration: function(duration) {
|
| + this.$.contentContainer.style.transitionDuration = duration;
|
| + this.$.scrim.style.transitionDuration = duration;
|
| + },
|
|
|
| - _scrollTargetChanged: function(scrollTarget, isAttached) {
|
| - var eventTarget;
|
| + _setTransitionTimingFunction: function(timingFunction) {
|
| + this.$.contentContainer.style.transitionTimingFunction = timingFunction;
|
| + this.$.scrim.style.transitionTimingFunction = timingFunction;
|
| + },
|
|
|
| - if (this._oldScrollTarget) {
|
| - eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScrollTarget;
|
| - eventTarget.removeEventListener('scroll', this._boundScrollHandler);
|
| - this._oldScrollTarget = null;
|
| - }
|
| + _translateDrawer: function(x) {
|
| + var drawerWidth = this.getWidth();
|
|
|
| - if (!isAttached) {
|
| - return;
|
| - }
|
| - // Support element id references
|
| - if (scrollTarget === 'document') {
|
| + if (this.position === 'left') {
|
| + x = Math.max(-drawerWidth, Math.min(x, 0));
|
| + this.$.scrim.style.opacity = 1 + x / drawerWidth;
|
| + } else {
|
| + x = Math.max(0, Math.min(x, drawerWidth));
|
| + this.$.scrim.style.opacity = 1 - x / drawerWidth;
|
| + }
|
|
|
| - this.scrollTarget = this._doc;
|
| + this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
|
| + },
|
|
|
| - } else if (typeof scrollTarget === 'string') {
|
| + _resetDrawerTranslate: function() {
|
| + this.$.scrim.style.opacity = '';
|
| + this.transform('', this.$.contentContainer);
|
| + },
|
|
|
| - this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
|
| - Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
| + _resetDrawerState: function() {
|
| + var oldState = this._drawerState;
|
| + if (this.opened) {
|
| + this._drawerState = this.persistent ?
|
| + this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED;
|
| + } else {
|
| + this._drawerState = this._DRAWER_STATE.CLOSED;
|
| + }
|
|
|
| - } else if (this._isValidScrollTarget()) {
|
| + if (oldState !== this._drawerState) {
|
| + if (this._drawerState === this._DRAWER_STATE.OPENED) {
|
| + this._setKeyboardFocusTrap();
|
| + document.addEventListener('keydown', this._boundEscKeydownHandler);
|
| + document.body.style.overflow = 'hidden';
|
| + } else {
|
| + document.removeEventListener('keydown', this._boundEscKeydownHandler);
|
| + document.body.style.overflow = '';
|
| + }
|
|
|
| - eventTarget = scrollTarget === this._doc ? window : scrollTarget;
|
| - this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
|
| - this._oldScrollTarget = scrollTarget;
|
| + // Don't fire the event on initial load.
|
| + if (oldState !== this._DRAWER_STATE.INIT) {
|
| + this.fire('app-drawer-transitioned');
|
| + }
|
| + }
|
| + },
|
|
|
| - eventTarget.addEventListener('scroll', this._boundScrollHandler);
|
| - }
|
| - },
|
| + _setKeyboardFocusTrap: function() {
|
| + if (this.noFocusTrap) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Runs on every scroll event. Consumer of this behavior may override this method.
|
| - *
|
| - * @protected
|
| - */
|
| - _scrollHandler: function scrollHandler() {},
|
| + // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated), this will
|
| + // not select focusable elements inside shadow roots.
|
| + var focusableElementsSelector = [
|
| + 'a[href]:not([tabindex="-1"])',
|
| + 'area[href]:not([tabindex="-1"])',
|
| + 'input:not([disabled]):not([tabindex="-1"])',
|
| + 'select:not([disabled]):not([tabindex="-1"])',
|
| + 'textarea:not([disabled]):not([tabindex="-1"])',
|
| + 'button:not([disabled]):not([tabindex="-1"])',
|
| + 'iframe:not([tabindex="-1"])',
|
| + '[tabindex]:not([tabindex="-1"])',
|
| + '[contentEditable=true]:not([tabindex="-1"])'
|
| + ].join(',');
|
| + var focusableElements = Polymer.dom(this).querySelectorAll(focusableElementsSelector);
|
| +
|
| + if (focusableElements.length > 0) {
|
| + this._firstTabStop = focusableElements[0];
|
| + this._lastTabStop = focusableElements[focusableElements.length - 1];
|
| + } else {
|
| + // Reset saved tab stops when there are no focusable elements in the drawer.
|
| + this._firstTabStop = null;
|
| + this._lastTabStop = null;
|
| + }
|
|
|
| - /**
|
| - * The default scroll target. Consumers of this behavior may want to customize
|
| - * the default scroll target.
|
| - *
|
| - * @type {Element}
|
| - */
|
| - get _defaultScrollTarget() {
|
| - return this._doc;
|
| - },
|
| + // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the first focusable
|
| + // element in the drawer, if it exists. Use the tabindex attribute since the this.tabIndex
|
| + // property in IE/Edge returns 0 (instead of -1) when the attribute is not set.
|
| + var tabindex = this.getAttribute('tabindex');
|
| + if (tabindex && parseInt(tabindex, 10) > -1) {
|
| + this.focus();
|
| + } else if (this._firstTabStop) {
|
| + this._firstTabStop.focus();
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Shortcut for the document element
|
| - *
|
| - * @type {Element}
|
| - */
|
| - get _doc() {
|
| - return this.ownerDocument.documentElement;
|
| - },
|
| + _tabKeydownHandler: function(event) {
|
| + if (this.noFocusTrap) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Gets the number of pixels that the content of an element is scrolled upward.
|
| - *
|
| - * @type {number}
|
| - */
|
| - get _scrollTop() {
|
| - if (this._isValidScrollTarget()) {
|
| - return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop;
|
| - }
|
| - return 0;
|
| - },
|
| + var TAB_KEYCODE = 9;
|
| + if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB_KEYCODE) {
|
| + if (event.shiftKey) {
|
| + if (this._firstTabStop && Polymer.dom(event).localTarget === this._firstTabStop) {
|
| + event.preventDefault();
|
| + this._lastTabStop.focus();
|
| + }
|
| + } else {
|
| + if (this._lastTabStop && Polymer.dom(event).localTarget === this._lastTabStop) {
|
| + event.preventDefault();
|
| + this._firstTabStop.focus();
|
| + }
|
| + }
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Gets the number of pixels that the content of an element is scrolled to the left.
|
| - *
|
| - * @type {number}
|
| - */
|
| - get _scrollLeft() {
|
| - if (this._isValidScrollTarget()) {
|
| - return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft;
|
| - }
|
| - return 0;
|
| - },
|
| + _MIN_FLING_THRESHOLD: 0.2,
|
|
|
| - /**
|
| - * Sets the number of pixels that the content of an element is scrolled upward.
|
| - *
|
| - * @type {number}
|
| - */
|
| - set _scrollTop(top) {
|
| - if (this.scrollTarget === this._doc) {
|
| - window.scrollTo(window.pageXOffset, top);
|
| - } else if (this._isValidScrollTarget()) {
|
| - this.scrollTarget.scrollTop = top;
|
| - }
|
| - },
|
| + _MIN_TRANSITION_VELOCITY: 1.2,
|
|
|
| - /**
|
| - * Sets the number of pixels that the content of an element is scrolled to the left.
|
| - *
|
| - * @type {number}
|
| - */
|
| - set _scrollLeft(left) {
|
| - if (this.scrollTarget === this._doc) {
|
| - window.scrollTo(left, window.pageYOffset);
|
| - } else if (this._isValidScrollTarget()) {
|
| - this.scrollTarget.scrollLeft = left;
|
| - }
|
| - },
|
| + _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
|
|
|
| - /**
|
| - * Scrolls the content to a particular place.
|
| - *
|
| - * @method scroll
|
| - * @param {number} left The left position
|
| - * @param {number} top The top position
|
| - */
|
| - scroll: function(left, top) {
|
| - if (this.scrollTarget === this._doc) {
|
| - window.scrollTo(left, top);
|
| - } else if (this._isValidScrollTarget()) {
|
| - this.scrollTarget.scrollLeft = left;
|
| - this.scrollTarget.scrollTop = top;
|
| - }
|
| - },
|
| + _FLING_INITIAL_SLOPE: 1.5,
|
|
|
| - /**
|
| - * Gets the width of the scroll target.
|
| - *
|
| - * @type {number}
|
| - */
|
| - get _scrollTargetWidth() {
|
| - if (this._isValidScrollTarget()) {
|
| - return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth;
|
| + _DRAWER_STATE: {
|
| + INIT: 0,
|
| + OPENED: 1,
|
| + OPENED_PERSISTENT: 2,
|
| + CLOSED: 3,
|
| + TRACKING: 4,
|
| + FLINGING: 5
|
| }
|
| - return 0;
|
| - },
|
|
|
| - /**
|
| - * Gets the height of the scroll target.
|
| - *
|
| - * @type {number}
|
| - */
|
| - get _scrollTargetHeight() {
|
| - if (this._isValidScrollTarget()) {
|
| - return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight;
|
| - }
|
| - return 0;
|
| - },
|
| + /**
|
| + * Fired when the layout of app-drawer has changed.
|
| + *
|
| + * @event app-drawer-reset-layout
|
| + */
|
|
|
| - /**
|
| - * Returns true if the scroll target is a valid HTMLElement.
|
| - *
|
| - * @return {boolean}
|
| - */
|
| - _isValidScrollTarget: function() {
|
| - return this.scrollTarget instanceof HTMLElement;
|
| - }
|
| - };
|
| + /**
|
| + * Fired when app-drawer has finished transitioning.
|
| + *
|
| + * @event app-drawer-transitioned
|
| + */
|
| + });
|
| (function() {
|
| + 'use strict';
|
|
|
| - var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
|
| - var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
|
| - var DEFAULT_PHYSICAL_COUNT = 3;
|
| - var HIDDEN_Y = '-10000px';
|
| - var DEFAULT_GRID_SIZE = 200;
|
| - var SECRET_TABINDEX = -100;
|
| + Polymer({
|
| + is: 'iron-location',
|
| + properties: {
|
| + /**
|
| + * The pathname component of the URL.
|
| + */
|
| + path: {
|
| + type: String,
|
| + notify: true,
|
| + value: function() {
|
| + return window.decodeURIComponent(window.location.pathname);
|
| + }
|
| + },
|
| + /**
|
| + * The query string portion of the URL.
|
| + */
|
| + query: {
|
| + type: String,
|
| + notify: true,
|
| + value: function() {
|
| + return window.decodeURIComponent(window.location.search.slice(1));
|
| + }
|
| + },
|
| + /**
|
| + * The hash component of the URL.
|
| + */
|
| + hash: {
|
| + type: String,
|
| + notify: true,
|
| + value: function() {
|
| + return window.decodeURIComponent(window.location.hash.slice(1));
|
| + }
|
| + },
|
| + /**
|
| + * If the user was on a URL for less than `dwellTime` milliseconds, it
|
| + * won't be added to the browser's history, but instead will be replaced
|
| + * by the next entry.
|
| + *
|
| + * This is to prevent large numbers of entries from clogging up the user's
|
| + * browser history. Disable by setting to a negative number.
|
| + */
|
| + dwellTime: {
|
| + type: Number,
|
| + value: 2000
|
| + },
|
|
|
| - Polymer({
|
| + /**
|
| + * A regexp that defines the set of URLs that should be considered part
|
| + * of this web app.
|
| + *
|
| + * Clicking on a link that matches this regex won't result in a full page
|
| + * navigation, but will instead just update the URL state in place.
|
| + *
|
| + * This regexp is given everything after the origin in an absolute
|
| + * URL. So to match just URLs that start with /search/ do:
|
| + * url-space-regex="^/search/"
|
| + *
|
| + * @type {string|RegExp}
|
| + */
|
| + urlSpaceRegex: {
|
| + type: String,
|
| + value: ''
|
| + },
|
|
|
| - is: 'iron-list',
|
| + /**
|
| + * urlSpaceRegex, but coerced into a regexp.
|
| + *
|
| + * @type {RegExp}
|
| + */
|
| + _urlSpaceRegExp: {
|
| + computed: '_makeRegExp(urlSpaceRegex)'
|
| + },
|
|
|
| - properties: {
|
| + _lastChangedAt: {
|
| + type: Number
|
| + },
|
|
|
| - /**
|
| - * An array containing items determining how many instances of the template
|
| - * to stamp and that that each template instance should bind to.
|
| - */
|
| - items: {
|
| - type: Array
|
| + _initialized: {
|
| + type: Boolean,
|
| + value: false
|
| + }
|
| },
|
| -
|
| - /**
|
| - * The max count of physical items the pool can extend to.
|
| - */
|
| - maxPhysicalCount: {
|
| - type: Number,
|
| - value: 500
|
| + hostAttributes: {
|
| + hidden: true
|
| },
|
| + observers: [
|
| + '_updateUrl(path, query, hash)'
|
| + ],
|
| + attached: function() {
|
| + this.listen(window, 'hashchange', '_hashChanged');
|
| + this.listen(window, 'location-changed', '_urlChanged');
|
| + this.listen(window, 'popstate', '_urlChanged');
|
| + this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_globalOnClick');
|
| + // Give a 200ms grace period to make initial redirects without any
|
| + // additions to the user's history.
|
| + this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
|
|
|
| - /**
|
| - * The name of the variable to add to the binding scope for the array
|
| - * element associated with a given template instance.
|
| - */
|
| - as: {
|
| - type: String,
|
| - value: 'item'
|
| + this._initialized = true;
|
| + this._urlChanged();
|
| },
|
| -
|
| - /**
|
| - * The name of the variable to add to the binding scope with the index
|
| - * for the row.
|
| - */
|
| - indexAs: {
|
| - type: String,
|
| - value: 'index'
|
| + detached: function() {
|
| + this.unlisten(window, 'hashchange', '_hashChanged');
|
| + this.unlisten(window, 'location-changed', '_urlChanged');
|
| + this.unlisten(window, 'popstate', '_urlChanged');
|
| + this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '_globalOnClick');
|
| + this._initialized = false;
|
| + },
|
| + _hashChanged: function() {
|
| + this.hash = window.decodeURIComponent(window.location.hash.substring(1));
|
| + },
|
| + _urlChanged: function() {
|
| + // We want to extract all info out of the updated URL before we
|
| + // try to write anything back into it.
|
| + //
|
| + // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
|
| + // one when we set this.hash. Likewise for query.
|
| + this._dontUpdateUrl = true;
|
| + this._hashChanged();
|
| + this.path = window.decodeURIComponent(window.location.pathname);
|
| + this.query = window.decodeURIComponent(
|
| + window.location.search.substring(1));
|
| + this._dontUpdateUrl = false;
|
| + this._updateUrl();
|
| + },
|
| + _getUrl: function() {
|
| + var partiallyEncodedPath = window.encodeURI(
|
| + this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
|
| + var partiallyEncodedQuery = '';
|
| + if (this.query) {
|
| + partiallyEncodedQuery = '?' + window.encodeURI(
|
| + this.query).replace(/\#/g, '%23');
|
| + }
|
| + var partiallyEncodedHash = '';
|
| + if (this.hash) {
|
| + partiallyEncodedHash = '#' + window.encodeURI(this.hash);
|
| + }
|
| + return (
|
| + partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash);
|
| + },
|
| + _updateUrl: function() {
|
| + if (this._dontUpdateUrl || !this._initialized) {
|
| + return;
|
| + }
|
| + if (this.path === window.decodeURIComponent(window.location.pathname) &&
|
| + this.query === window.decodeURIComponent(
|
| + window.location.search.substring(1)) &&
|
| + this.hash === window.decodeURIComponent(
|
| + window.location.hash.substring(1))) {
|
| + // Nothing to do, the current URL is a representation of our properties.
|
| + return;
|
| + }
|
| + var newUrl = this._getUrl();
|
| + // Need to use a full URL in case the containing page has a base URI.
|
| + var fullNewUrl = new URL(
|
| + newUrl, window.location.protocol + '//' + window.location.host).href;
|
| + var now = window.performance.now();
|
| + var shouldReplace =
|
| + this._lastChangedAt + this.dwellTime > now;
|
| + this._lastChangedAt = now;
|
| + if (shouldReplace) {
|
| + window.history.replaceState({}, '', fullNewUrl);
|
| + } else {
|
| + window.history.pushState({}, '', fullNewUrl);
|
| + }
|
| + this.fire('location-changed', {}, {node: window});
|
| },
|
| -
|
| /**
|
| - * The name of the variable to add to the binding scope to indicate
|
| - * if the row is selected.
|
| + * A necessary evil so that links work as expected. Does its best to
|
| + * bail out early if possible.
|
| + *
|
| + * @param {MouseEvent} event .
|
| */
|
| - selectedAs: {
|
| - type: String,
|
| - value: 'selected'
|
| + _globalOnClick: function(event) {
|
| + // If another event handler has stopped this event then there's nothing
|
| + // for us to do. This can happen e.g. when there are multiple
|
| + // iron-location elements in a page.
|
| + if (event.defaultPrevented) {
|
| + return;
|
| + }
|
| + var href = this._getSameOriginLinkHref(event);
|
| + if (!href) {
|
| + return;
|
| + }
|
| + event.preventDefault();
|
| + // If the navigation is to the current page we shouldn't add a history
|
| + // entry or fire a change event.
|
| + if (href === window.location.href) {
|
| + return;
|
| + }
|
| + window.history.pushState({}, '', href);
|
| + this.fire('location-changed', {}, {node: window});
|
| },
|
| -
|
| /**
|
| - * When true, the list is rendered as a grid. Grid items must have
|
| - * fixed width and height set via CSS. e.g.
|
| + * Returns the absolute URL of the link (if any) that this click event
|
| + * is clicking on, if we can and should override the resulting full
|
| + * page navigation. Returns null otherwise.
|
| *
|
| - * ```html
|
| - * <iron-list grid>
|
| - * <template>
|
| - * <div style="width: 100px; height: 100px;"> 100x100 </div>
|
| - * </template>
|
| - * </iron-list>
|
| - * ```
|
| + * @param {MouseEvent} event .
|
| + * @return {string?} .
|
| */
|
| - grid: {
|
| - type: Boolean,
|
| - value: false,
|
| - reflectToAttribute: true
|
| + _getSameOriginLinkHref: function(event) {
|
| + // We only care about left-clicks.
|
| + if (event.button !== 0) {
|
| + return null;
|
| + }
|
| + // We don't want modified clicks, where the intent is to open the page
|
| + // in a new tab.
|
| + if (event.metaKey || event.ctrlKey) {
|
| + return null;
|
| + }
|
| + var eventPath = Polymer.dom(event).path;
|
| + var anchor = null;
|
| + for (var i = 0; i < eventPath.length; i++) {
|
| + var element = eventPath[i];
|
| + if (element.tagName === 'A' && element.href) {
|
| + anchor = element;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + // If there's no link there's nothing to do.
|
| + if (!anchor) {
|
| + return null;
|
| + }
|
| +
|
| + // Target blank is a new tab, don't intercept.
|
| + if (anchor.target === '_blank') {
|
| + return null;
|
| + }
|
| + // If the link is for an existing parent frame, don't intercept.
|
| + if ((anchor.target === '_top' ||
|
| + anchor.target === '_parent') &&
|
| + window.top !== window) {
|
| + return null;
|
| + }
|
| +
|
| + var href = anchor.href;
|
| +
|
| + // It only makes sense for us to intercept same-origin navigations.
|
| + // pushState/replaceState don't work with cross-origin links.
|
| + var url;
|
| + if (document.baseURI != null) {
|
| + url = new URL(href, /** @type {string} */(document.baseURI));
|
| + } else {
|
| + url = new URL(href);
|
| + }
|
| +
|
| + var origin;
|
| +
|
| + // IE Polyfill
|
| + if (window.location.origin) {
|
| + origin = window.location.origin;
|
| + } else {
|
| + origin = window.location.protocol + '//' + window.location.hostname;
|
| +
|
| + if (window.location.port) {
|
| + origin += ':' + window.location.port;
|
| + }
|
| + }
|
| +
|
| + if (url.origin !== origin) {
|
| + return null;
|
| + }
|
| + var normalizedHref = url.pathname + url.search + url.hash;
|
| +
|
| + // If we've been configured not to handle this url... don't handle it!
|
| + if (this._urlSpaceRegExp &&
|
| + !this._urlSpaceRegExp.test(normalizedHref)) {
|
| + return null;
|
| + }
|
| + // Need to use a full URL in case the containing page has a base URI.
|
| + var fullNormalizedHref = new URL(
|
| + normalizedHref, window.location.href).href;
|
| + return fullNormalizedHref;
|
| },
|
| + _makeRegExp: function(urlSpaceRegex) {
|
| + return RegExp(urlSpaceRegex);
|
| + }
|
| + });
|
| + })();
|
| +'use strict';
|
|
|
| - /**
|
| - * When true, tapping a row will select the item, placing its data model
|
| - * in the set of selected items retrievable via the selection property.
|
| - *
|
| - * Note that tapping focusable elements within the list item will not
|
| - * result in selection, since they are presumed to have their * own action.
|
| - */
|
| - selectionEnabled: {
|
| + Polymer({
|
| + is: 'iron-query-params',
|
| + properties: {
|
| + paramsString: {
|
| + type: String,
|
| + notify: true,
|
| + observer: 'paramsStringChanged',
|
| + },
|
| + paramsObject: {
|
| + type: Object,
|
| + notify: true,
|
| + value: function() {
|
| + return {};
|
| + }
|
| + },
|
| + _dontReact: {
|
| type: Boolean,
|
| value: false
|
| - },
|
| + }
|
| + },
|
| + hostAttributes: {
|
| + hidden: true
|
| + },
|
| + observers: [
|
| + 'paramsObjectChanged(paramsObject.*)'
|
| + ],
|
| + paramsStringChanged: function() {
|
| + this._dontReact = true;
|
| + this.paramsObject = this._decodeParams(this.paramsString);
|
| + this._dontReact = false;
|
| + },
|
| + paramsObjectChanged: function() {
|
| + if (this._dontReact) {
|
| + return;
|
| + }
|
| + this.paramsString = this._encodeParams(this.paramsObject);
|
| + },
|
| + _encodeParams: function(params) {
|
| + var encodedParams = [];
|
| + for (var key in params) {
|
| + var value = params[key];
|
| + if (value === '') {
|
| + encodedParams.push(encodeURIComponent(key));
|
| + } else if (value) {
|
| + encodedParams.push(
|
| + encodeURIComponent(key) +
|
| + '=' +
|
| + encodeURIComponent(value.toString())
|
| + );
|
| + }
|
| + }
|
| + return encodedParams.join('&');
|
| + },
|
| + _decodeParams: function(paramString) {
|
| + var params = {};
|
| +
|
| + // Work around a bug in decodeURIComponent where + is not
|
| + // converted to spaces:
|
| + paramString = (paramString || '').replace(/\+/g, '%20');
|
| +
|
| + var paramList = paramString.split('&');
|
| + for (var i = 0; i < paramList.length; i++) {
|
| + var param = paramList[i].split('=');
|
| + if (param[0]) {
|
| + params[decodeURIComponent(param[0])] =
|
| + decodeURIComponent(param[1] || '');
|
| + }
|
| + }
|
| + return params;
|
| + }
|
| + });
|
| +'use strict';
|
|
|
| + /**
|
| + * Provides bidirectional mapping between `path` and `queryParams` and a
|
| + * app-route compatible `route` object.
|
| + *
|
| + * For more information, see the docs for `app-route-converter`.
|
| + *
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.AppRouteConverterBehavior = {
|
| + properties: {
|
| /**
|
| - * When `multiSelection` is false, this is the currently selected item, or `null`
|
| - * if no item is selected.
|
| + * A model representing the deserialized path through the route tree, as
|
| + * well as the current queryParams.
|
| + *
|
| + * A route object is the kernel of the routing system. It is intended to
|
| + * be fed into consuming elements such as `app-route`.
|
| + *
|
| + * @type {?Object}
|
| */
|
| - selectedItem: {
|
| + route: {
|
| type: Object,
|
| notify: true
|
| },
|
|
|
| /**
|
| - * When `multiSelection` is true, this is an array that contains the selected items.
|
| + * A set of key/value pairs that are universally accessible to branches of
|
| + * the route tree.
|
| + *
|
| + * @type {?Object}
|
| */
|
| - selectedItems: {
|
| + queryParams: {
|
| type: Object,
|
| notify: true
|
| },
|
|
|
| /**
|
| - * When `true`, multiple items may be selected at once (in this case,
|
| - * `selected` is an array of currently selected items). When `false`,
|
| - * only one item may be selected at a time.
|
| + * The serialized path through the route tree. This corresponds to the
|
| + * `window.location.pathname` value, and will update to reflect changes
|
| + * to that value.
|
| */
|
| - multiSelection: {
|
| - type: Boolean,
|
| - value: false
|
| + path: {
|
| + type: String,
|
| + notify: true,
|
| }
|
| },
|
|
|
| observers: [
|
| - '_itemsChanged(items.*)',
|
| - '_selectionEnabledChanged(selectionEnabled)',
|
| - '_multiSelectionChanged(multiSelection)',
|
| - '_setOverflow(scrollTarget)'
|
| + '_locationChanged(path, queryParams)',
|
| + '_routeChanged(route.prefix, route.path)',
|
| + '_routeQueryParamsChanged(route.__queryParams)'
|
| ],
|
|
|
| - behaviors: [
|
| - Polymer.Templatizer,
|
| - Polymer.IronResizableBehavior,
|
| - Polymer.IronA11yKeysBehavior,
|
| - Polymer.IronScrollTargetBehavior
|
| - ],
|
| -
|
| - keyBindings: {
|
| - 'up': '_didMoveUp',
|
| - 'down': '_didMoveDown',
|
| - 'enter': '_didEnter'
|
| + created: function() {
|
| + this.linkPaths('route.__queryParams', 'queryParams');
|
| + this.linkPaths('queryParams', 'route.__queryParams');
|
| },
|
|
|
| /**
|
| - * The ratio of hidden tiles that should remain in the scroll direction.
|
| - * Recommended value ~0.5, so it will distribute tiles evely in both directions.
|
| + * Handler called when the path or queryParams change.
|
| */
|
| - _ratio: 0.5,
|
| + _locationChanged: function() {
|
| + if (this.route &&
|
| + this.route.path === this.path &&
|
| + this.queryParams === this.route.__queryParams) {
|
| + return;
|
| + }
|
| + this.route = {
|
| + prefix: '',
|
| + path: this.path,
|
| + __queryParams: this.queryParams
|
| + };
|
| + },
|
|
|
| /**
|
| - * The padding-top value for the list.
|
| + * Handler called when the route prefix and route path change.
|
| */
|
| - _scrollerPaddingTop: 0,
|
| + _routeChanged: function() {
|
| + if (!this.route) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * This value is the same as `scrollTop`.
|
| - */
|
| - _scrollPosition: 0,
|
| + this.path = this.route.prefix + this.route.path;
|
| + },
|
|
|
| /**
|
| - * The sum of the heights of all the tiles in the DOM.
|
| + * Handler called when the route queryParams change.
|
| + *
|
| + * @param {Object} queryParams A set of key/value pairs that are
|
| + * universally accessible to branches of the route tree.
|
| */
|
| - _physicalSize: 0,
|
| -
|
| - /**
|
| - * The average `offsetHeight` of the tiles observed till now.
|
| - */
|
| - _physicalAverage: 0,
|
| + _routeQueryParamsChanged: function(queryParams) {
|
| + if (!this.route) {
|
| + return;
|
| + }
|
| + this.queryParams = queryParams;
|
| + }
|
| + };
|
| +'use strict';
|
|
|
| - /**
|
| - * The number of tiles which `offsetHeight` > 0 observed until now.
|
| - */
|
| - _physicalAverageCount: 0,
|
| + Polymer({
|
| + is: 'app-location',
|
|
|
| - /**
|
| - * The Y position of the item rendered in the `_physicalStart`
|
| - * tile relative to the scrolling list.
|
| - */
|
| - _physicalTop: 0,
|
| + properties: {
|
| + /**
|
| + * A model representing the deserialized path through the route tree, as
|
| + * well as the current queryParams.
|
| + */
|
| + route: {
|
| + type: Object,
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The number of items in the list.
|
| - */
|
| - _virtualCount: 0,
|
| + /**
|
| + * In many scenarios, it is convenient to treat the `hash` as a stand-in
|
| + * alternative to the `path`. For example, if deploying an app to a static
|
| + * web server (e.g., Github Pages) - where one does not have control over
|
| + * server-side routing - it is usually a better experience to use the hash
|
| + * to represent paths through one's app.
|
| + *
|
| + * When this property is set to true, the `hash` will be used in place of
|
|
|
| - /**
|
| - * A map between an item key and its physical item index
|
| - */
|
| - _physicalIndexForKey: null,
|
| + * the `path` for generating a `route`.
|
| + */
|
| + useHashAsPath: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
|
|
| - /**
|
| - * The estimated scroll height based on `_physicalAverage`
|
| - */
|
| - _estScrollHeight: 0,
|
| + /**
|
| + * A regexp that defines the set of URLs that should be considered part
|
| + * of this web app.
|
| + *
|
| + * Clicking on a link that matches this regex won't result in a full page
|
| + * navigation, but will instead just update the URL state in place.
|
| + *
|
| + * This regexp is given everything after the origin in an absolute
|
| + * URL. So to match just URLs that start with /search/ do:
|
| + * url-space-regex="^/search/"
|
| + *
|
| + * @type {string|RegExp}
|
| + */
|
| + urlSpaceRegex: {
|
| + type: String,
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The scroll height of the dom node
|
| - */
|
| - _scrollHeight: 0,
|
| + /**
|
| + * A set of key/value pairs that are universally accessible to branches
|
| + * of the route tree.
|
| + */
|
| + __queryParams: {
|
| + type: Object
|
| + },
|
|
|
| - /**
|
| - * The height of the list. This is referred as the viewport in the context of list.
|
| - */
|
| - _viewportHeight: 0,
|
| + /**
|
| + * The pathname component of the current URL.
|
| + */
|
| + __path: {
|
| + type: String
|
| + },
|
|
|
| - /**
|
| - * The width of the list. This is referred as the viewport in the context of list.
|
| - */
|
| - _viewportWidth: 0,
|
| + /**
|
| + * The query string portion of the current URL.
|
| + */
|
| + __query: {
|
| + type: String
|
| + },
|
|
|
| - /**
|
| - * An array of DOM nodes that are currently in the tree
|
| - * @type {?Array<!TemplatizerNode>}
|
| - */
|
| - _physicalItems: null,
|
| + /**
|
| + * The hash portion of the current URL.
|
| + */
|
| + __hash: {
|
| + type: String
|
| + },
|
|
|
| - /**
|
| - * An array of heights for each item in `_physicalItems`
|
| - * @type {?Array<number>}
|
| - */
|
| - _physicalSizes: null,
|
| + /**
|
| + * The route path, which will be either the hash or the path, depending
|
| + * on useHashAsPath.
|
| + */
|
| + path: {
|
| + type: String,
|
| + observer: '__onPathChanged'
|
| + }
|
| + },
|
|
|
| - /**
|
| - * A cached value for the first visible index.
|
| - * See `firstVisibleIndex`
|
| - * @type {?number}
|
| - */
|
| - _firstVisibleIndexVal: null,
|
| + behaviors: [Polymer.AppRouteConverterBehavior],
|
|
|
| - /**
|
| - * A cached value for the last visible index.
|
| - * See `lastVisibleIndex`
|
| - * @type {?number}
|
| - */
|
| - _lastVisibleIndexVal: null,
|
| + observers: [
|
| + '__computeRoutePath(useHashAsPath, __hash, __path)'
|
| + ],
|
|
|
| - /**
|
| - * A Polymer collection for the items.
|
| - * @type {?Polymer.Collection}
|
| - */
|
| - _collection: null,
|
| + __computeRoutePath: function() {
|
| + this.path = this.useHashAsPath ? this.__hash : this.__path;
|
| + },
|
|
|
| - /**
|
| - * True if the current item list was rendered for the first time
|
| - * after attached.
|
| - */
|
| - _itemsRendered: false,
|
| + __onPathChanged: function() {
|
| + if (!this._readied) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * The page that is currently rendered.
|
| - */
|
| - _lastPage: null,
|
| + if (this.useHashAsPath) {
|
| + this.__hash = this.path;
|
| + } else {
|
| + this.__path = this.path;
|
| + }
|
| + }
|
| + });
|
| +'use strict';
|
|
|
| - /**
|
| - * The max number of pages to render. One page is equivalent to the height of the list.
|
| - */
|
| - _maxPages: 3,
|
| + Polymer({
|
| + is: 'app-route',
|
|
|
| - /**
|
| - * The currently focused physical item.
|
| - */
|
| - _focusedItem: null,
|
| + properties: {
|
| + /**
|
| + * The URL component managed by this element.
|
| + */
|
| + route: {
|
| + type: Object,
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The index of the `_focusedItem`.
|
| - */
|
| - _focusedIndex: -1,
|
| + /**
|
| + * The pattern of slash-separated segments to match `path` against.
|
| + *
|
| + * For example the pattern "/foo" will match "/foo" or "/foo/bar"
|
| + * but not "/foobar".
|
| + *
|
| + * Path segments like `/:named` are mapped to properties on the `data` object.
|
| + */
|
| + pattern: {
|
| + type: String
|
| + },
|
|
|
| - /**
|
| - * The the item that is focused if it is moved offscreen.
|
| - * @private {?TemplatizerNode}
|
| - */
|
| - _offscreenFocusedItem: null,
|
| + /**
|
| + * The parameterized values that are extracted from the route as
|
| + * described by `pattern`.
|
| + */
|
| + data: {
|
| + type: Object,
|
| + value: function() {return {};},
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The item that backfills the `_offscreenFocusedItem` in the physical items
|
| - * list when that item is moved offscreen.
|
| - */
|
| - _focusBackfillItem: null,
|
| + /**
|
| + * @type {?Object}
|
| + */
|
| + queryParams: {
|
| + type: Object,
|
| + value: function() {
|
| + return {};
|
| + },
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The maximum items per row
|
| - */
|
| - _itemsPerRow: 1,
|
| + /**
|
| + * The part of `path` NOT consumed by `pattern`.
|
| + */
|
| + tail: {
|
| + type: Object,
|
| + value: function() {return {path: null, prefix: null, __queryParams: null};},
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * The width of each grid item
|
| - */
|
| - _itemWidth: 0,
|
| + active: {
|
| + type: Boolean,
|
| + notify: true,
|
| + readOnly: true
|
| + },
|
|
|
| - /**
|
| - * The height of the row in grid layout.
|
| - */
|
| - _rowHeight: 0,
|
| + _queryParamsUpdating: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| + /**
|
| + * @type {?string}
|
| + */
|
| + _matched: {
|
| + type: String,
|
| + value: ''
|
| + }
|
| + },
|
|
|
| - /**
|
| - * The bottom of the physical content.
|
| - */
|
| - get _physicalBottom() {
|
| - return this._physicalTop + this._physicalSize;
|
| + observers: [
|
| + '__tryToMatch(route.path, pattern)',
|
| + '__updatePathOnDataChange(data.*)',
|
| + '__tailPathChanged(tail.path)',
|
| + '__routeQueryParamsChanged(route.__queryParams)',
|
| + '__tailQueryParamsChanged(tail.__queryParams)',
|
| + '__queryParamsChanged(queryParams.*)'
|
| + ],
|
| +
|
| + created: function() {
|
| + this.linkPaths('route.__queryParams', 'tail.__queryParams');
|
| + this.linkPaths('tail.__queryParams', 'route.__queryParams');
|
| },
|
|
|
| /**
|
| - * The bottom of the scroll.
|
| + * Deal with the query params object being assigned to wholesale.
|
| + * @export
|
| */
|
| - get _scrollBottom() {
|
| - return this._scrollPosition + this._viewportHeight;
|
| + __routeQueryParamsChanged: function(queryParams) {
|
| + if (queryParams && this.tail) {
|
| + this.set('tail.__queryParams', queryParams);
|
| +
|
| + if (!this.active || this._queryParamsUpdating) {
|
| + return;
|
| + }
|
| +
|
| + // Copy queryParams and track whether there are any differences compared
|
| + // to the existing query params.
|
| + var copyOfQueryParams = {};
|
| + var anythingChanged = false;
|
| + for (var key in queryParams) {
|
| + copyOfQueryParams[key] = queryParams[key];
|
| + if (anythingChanged ||
|
| + !this.queryParams ||
|
| + queryParams[key] !== this.queryParams[key]) {
|
| + anythingChanged = true;
|
| + }
|
| + }
|
| + // Need to check whether any keys were deleted
|
| + for (var key in this.queryParams) {
|
| + if (anythingChanged || !(key in queryParams)) {
|
| + anythingChanged = true;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + if (!anythingChanged) {
|
| + return;
|
| + }
|
| + this._queryParamsUpdating = true;
|
| + this.set('queryParams', copyOfQueryParams);
|
| + this._queryParamsUpdating = false;
|
| + }
|
| },
|
|
|
| /**
|
| - * The n-th item rendered in the last physical item.
|
| + * @export
|
| */
|
| - get _virtualEnd() {
|
| - return this._virtualStart + this._physicalCount - 1;
|
| + __tailQueryParamsChanged: function(queryParams) {
|
| + if (queryParams && this.route) {
|
| + this.set('route.__queryParams', queryParams);
|
| + }
|
| },
|
|
|
| /**
|
| - * The height of the physical content that isn't on the screen.
|
| + * @export
|
| */
|
| - get _hiddenContentSize() {
|
| - var size = this.grid ? this._physicalRows * this._rowHeight : this._physicalSize;
|
| - return size - this._viewportHeight;
|
| + __queryParamsChanged: function(changes) {
|
| + if (!this.active || this._queryParamsUpdating) {
|
| + return;
|
| + }
|
| +
|
| + this.set('route.__' + changes.path, changes.value);
|
| },
|
|
|
| - /**
|
| - * The maximum scroll top value.
|
| - */
|
| - get _maxScrollTop() {
|
| - return this._estScrollHeight - this._viewportHeight + this._scrollerPaddingTop;
|
| + __resetProperties: function() {
|
| + this._setActive(false);
|
| + this._matched = null;
|
| + //this.tail = { path: null, prefix: null, queryParams: null };
|
| + //this.data = {};
|
| },
|
|
|
| /**
|
| - * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
|
| + * @export
|
| */
|
| - _minVirtualStart: 0,
|
| + __tryToMatch: function() {
|
| + if (!this.route) {
|
| + return;
|
| + }
|
| + var path = this.route.path;
|
| + var pattern = this.pattern;
|
| + if (!pattern) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * The largest n-th value for an item such that it can be rendered in `_physicalStart`.
|
| - */
|
| - get _maxVirtualStart() {
|
| - return Math.max(0, this._virtualCount - this._physicalCount);
|
| - },
|
| + if (!path) {
|
| + this.__resetProperties();
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * The n-th item rendered in the `_physicalStart` tile.
|
| - */
|
| - _virtualStartVal: 0,
|
| + var remainingPieces = path.split('/');
|
| + var patternPieces = pattern.split('/');
|
|
|
| - set _virtualStart(val) {
|
| - this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
|
| - },
|
| + var matched = [];
|
| + var namedMatches = {};
|
|
|
| - get _virtualStart() {
|
| - return this._virtualStartVal || 0;
|
| - },
|
| + for (var i=0; i < patternPieces.length; i++) {
|
| + var patternPiece = patternPieces[i];
|
| + if (!patternPiece && patternPiece !== '') {
|
| + break;
|
| + }
|
| + var pathPiece = remainingPieces.shift();
|
|
|
| - /**
|
| - * The k-th tile that is at the top of the scrolling list.
|
| - */
|
| - _physicalStartVal: 0,
|
| + // We don't match this path.
|
| + if (!pathPiece && pathPiece !== '') {
|
| + this.__resetProperties();
|
| + return;
|
| + }
|
| + matched.push(pathPiece);
|
|
|
| - set _physicalStart(val) {
|
| - this._physicalStartVal = val % this._physicalCount;
|
| - if (this._physicalStartVal < 0) {
|
| - this._physicalStartVal = this._physicalCount + this._physicalStartVal;
|
| + if (patternPiece.charAt(0) == ':') {
|
| + namedMatches[patternPiece.slice(1)] = pathPiece;
|
| + } else if (patternPiece !== pathPiece) {
|
| + this.__resetProperties();
|
| + return;
|
| + }
|
| }
|
| - this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| - },
|
| -
|
| - get _physicalStart() {
|
| - return this._physicalStartVal || 0;
|
| - },
|
| -
|
| - /**
|
| - * The number of tiles in the DOM.
|
| - */
|
| - _physicalCountVal: 0,
|
|
|
| - set _physicalCount(val) {
|
| - this._physicalCountVal = val;
|
| - this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| - },
|
| + this._matched = matched.join('/');
|
|
|
| - get _physicalCount() {
|
| - return this._physicalCountVal;
|
| - },
|
| + // Properties that must be updated atomically.
|
| + var propertyUpdates = {};
|
|
|
| - /**
|
| - * The k-th tile that is at the bottom of the scrolling list.
|
| - */
|
| - _physicalEnd: 0,
|
| + //this.active
|
| + if (!this.active) {
|
| + propertyUpdates.active = true;
|
| + }
|
|
|
| - /**
|
| - * An optimal physical size such that we will have enough physical items
|
| - * to fill up the viewport and recycle when the user scrolls.
|
| - *
|
| - * This default value assumes that we will at least have the equivalent
|
| - * to a viewport of physical items above and below the user's viewport.
|
| - */
|
| - get _optPhysicalSize() {
|
| - if (this.grid) {
|
| - return this._estRowsInView * this._rowHeight * this._maxPages;
|
| + // this.tail
|
| + var tailPrefix = this.route.prefix + this._matched;
|
| + var tailPath = remainingPieces.join('/');
|
| + if (remainingPieces.length > 0) {
|
| + tailPath = '/' + tailPath;
|
| + }
|
| + if (!this.tail ||
|
| + this.tail.prefix !== tailPrefix ||
|
| + this.tail.path !== tailPath) {
|
| + propertyUpdates.tail = {
|
| + prefix: tailPrefix,
|
| + path: tailPath,
|
| + __queryParams: this.route.__queryParams
|
| + };
|
| }
|
| - return this._viewportHeight * this._maxPages;
|
| - },
|
|
|
| - get _optPhysicalCount() {
|
| - return this._estRowsInView * this._itemsPerRow * this._maxPages;
|
| - },
|
| + // this.data
|
| + propertyUpdates.data = namedMatches;
|
| + this._dataInUrl = {};
|
| + for (var key in namedMatches) {
|
| + this._dataInUrl[key] = namedMatches[key];
|
| + }
|
|
|
| - /**
|
| - * True if the current list is visible.
|
| - */
|
| - get _isVisible() {
|
| - return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight);
|
| + this.__setMulti(propertyUpdates);
|
| },
|
|
|
| /**
|
| - * Gets the index of the first visible item in the viewport.
|
| - *
|
| - * @type {number}
|
| + * @export
|
| */
|
| - get firstVisibleIndex() {
|
| - if (this._firstVisibleIndexVal === null) {
|
| - var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddingTop);
|
| -
|
| - this._firstVisibleIndexVal = this._iterateItems(
|
| - function(pidx, vidx) {
|
| - physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
| -
|
| - if (physicalOffset > this._scrollPosition) {
|
| - return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
|
| - }
|
| - // Handle a partially rendered final row in grid mode
|
| - if (this.grid && this._virtualCount - 1 === vidx) {
|
| - return vidx - (vidx % this._itemsPerRow);
|
| - }
|
| - }) || 0;
|
| + __tailPathChanged: function() {
|
| + if (!this.active) {
|
| + return;
|
| }
|
| - return this._firstVisibleIndexVal;
|
| + var tailPath = this.tail.path;
|
| + var newPath = this._matched;
|
| + if (tailPath) {
|
| + if (tailPath.charAt(0) !== '/') {
|
| + tailPath = '/' + tailPath;
|
| + }
|
| + newPath += tailPath;
|
| + }
|
| + this.set('route.path', newPath);
|
| },
|
|
|
| /**
|
| - * Gets the index of the last visible item in the viewport.
|
| - *
|
| - * @type {number}
|
| + * @export
|
| */
|
| - get lastVisibleIndex() {
|
| - if (this._lastVisibleIndexVal === null) {
|
| - if (this.grid) {
|
| - var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._itemsPerRow - 1;
|
| - this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
|
| + __updatePathOnDataChange: function() {
|
| + if (!this.route || !this.active) {
|
| + return;
|
| + }
|
| + var newPath = this.__getLink({});
|
| + var oldPath = this.__getLink(this._dataInUrl);
|
| + if (newPath === oldPath) {
|
| + return;
|
| + }
|
| + this.set('route.path', newPath);
|
| + },
|
| +
|
| + __getLink: function(overrideValues) {
|
| + var values = {tail: null};
|
| + for (var key in this.data) {
|
| + values[key] = this.data[key];
|
| + }
|
| + for (var key in overrideValues) {
|
| + values[key] = overrideValues[key];
|
| + }
|
| + var patternPieces = this.pattern.split('/');
|
| + var interp = patternPieces.map(function(value) {
|
| + if (value[0] == ':') {
|
| + value = values[value.slice(1)];
|
| + }
|
| + return value;
|
| + }, this);
|
| + if (values.tail && values.tail.path) {
|
| + if (interp.length > 0 && values.tail.path.charAt(0) === '/') {
|
| + interp.push(values.tail.path.slice(1));
|
| } else {
|
| - var physicalOffset = this._physicalTop;
|
| - this._iterateItems(function(pidx, vidx) {
|
| - if (physicalOffset < this._scrollBottom) {
|
| - this._lastVisibleIndexVal = vidx;
|
| - } else {
|
| - // Break _iterateItems
|
| - return true;
|
| - }
|
| - physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
| - });
|
| + interp.push(values.tail.path);
|
| }
|
| }
|
| - return this._lastVisibleIndexVal;
|
| + return interp.join('/');
|
| },
|
|
|
| - get _defaultScrollTarget() {
|
| - return this;
|
| - },
|
| - get _virtualRowCount() {
|
| - return Math.ceil(this._virtualCount / this._itemsPerRow);
|
| - },
|
| + __setMulti: function(setObj) {
|
| + // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at
|
| + // internal data structures. I would not advise that you copy this
|
| + // example.
|
| + //
|
| + // In the future this will be a feature of Polymer itself.
|
| + // See: https://github.com/Polymer/polymer/issues/3640
|
| + //
|
| + // Hacking around with private methods like this is juggling footguns,
|
| + // and is likely to have unexpected and unsupported rough edges.
|
| + //
|
| + // Be ye so warned.
|
| + for (var property in setObj) {
|
| + this._propertySetter(property, setObj[property]);
|
| + }
|
|
|
| - get _estRowsInView() {
|
| - return Math.ceil(this._viewportHeight / this._rowHeight);
|
| - },
|
| + for (var property in setObj) {
|
| + this._pathEffector(property, this[property]);
|
| + this._notifyPathUp(property, this[property]);
|
| + }
|
| + }
|
| + });
|
| +Polymer({
|
|
|
| - get _physicalRows() {
|
| - return Math.ceil(this._physicalCount / this._itemsPerRow);
|
| - },
|
| + is: 'iron-media-query',
|
|
|
| - ready: function() {
|
| - this.addEventListener('focus', this._didFocus.bind(this), true);
|
| + properties: {
|
| +
|
| + /**
|
| + * The Boolean return value of the media query.
|
| + */
|
| + queryMatches: {
|
| + type: Boolean,
|
| + value: false,
|
| + readOnly: true,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * The CSS media query to evaluate.
|
| + */
|
| + query: {
|
| + type: String,
|
| + observer: 'queryChanged'
|
| + },
|
| +
|
| + /**
|
| + * If true, the query attribute is assumed to be a complete media query
|
| + * string rather than a single media feature.
|
| + */
|
| + full: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * @type {function(MediaQueryList)}
|
| + */
|
| + _boundMQHandler: {
|
| + value: function() {
|
| + return this.queryHandler.bind(this);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @type {MediaQueryList}
|
| + */
|
| + _mq: {
|
| + value: null
|
| + }
|
| },
|
|
|
| attached: function() {
|
| - this.updateViewportBoundaries();
|
| - this._render();
|
| - // `iron-resize` is fired when the list is attached if the event is added
|
| - // before attached causing unnecessary work.
|
| - this.listen(this, 'iron-resize', '_resizeHandler');
|
| + this.style.display = 'none';
|
| + this.queryChanged();
|
| },
|
|
|
| detached: function() {
|
| - this._itemsRendered = false;
|
| - this.unlisten(this, 'iron-resize', '_resizeHandler');
|
| + this._remove();
|
| },
|
|
|
| - /**
|
| - * Set the overflow property if this element has its own scrolling region
|
| - */
|
| - _setOverflow: function(scrollTarget) {
|
| - this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
|
| - this.style.overflow = scrollTarget === this ? 'auto' : '';
|
| + _add: function() {
|
| + if (this._mq) {
|
| + this._mq.addListener(this._boundMQHandler);
|
| + }
|
| },
|
|
|
| - /**
|
| - * Invoke this method if you dynamically update the viewport's
|
| - * size or CSS padding.
|
| - *
|
| - * @method updateViewportBoundaries
|
| - */
|
| - updateViewportBoundaries: function() {
|
| - this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
|
| - parseInt(window.getComputedStyle(this)['padding-top'], 10);
|
| -
|
| - this._viewportHeight = this._scrollTargetHeight;
|
| - if (this.grid) {
|
| - this._updateGridMetrics();
|
| + _remove: function() {
|
| + if (this._mq) {
|
| + this._mq.removeListener(this._boundMQHandler);
|
| }
|
| + this._mq = null;
|
| },
|
|
|
| - /**
|
| - * Update the models, the position of the
|
| - * items in the viewport and recycle tiles as needed.
|
| - */
|
| - _scrollHandler: function() {
|
| - // clamp the `scrollTop` value
|
| - var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
|
| - var delta = scrollTop - this._scrollPosition;
|
| - var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
|
| - var ratio = this._ratio;
|
| - var recycledTiles = 0;
|
| - var hiddenContentSize = this._hiddenContentSize;
|
| - var currentRatio = ratio;
|
| - var movingUp = [];
|
| -
|
| - // track the last `scrollTop`
|
| - this._scrollPosition = scrollTop;
|
| -
|
| - // clear cached visible indexes
|
| - this._firstVisibleIndexVal = null;
|
| - this._lastVisibleIndexVal = null;
|
| -
|
| - scrollBottom = this._scrollBottom;
|
| - physicalBottom = this._physicalBottom;
|
| -
|
| - // random access
|
| - if (Math.abs(delta) > this._physicalSize) {
|
| - this._physicalTop += delta;
|
| - recycledTiles = Math.round(delta / this._physicalAverage);
|
| + queryChanged: function() {
|
| + this._remove();
|
| + var query = this.query;
|
| + if (!query) {
|
| + return;
|
| }
|
| - // scroll up
|
| - else if (delta < 0) {
|
| - var topSpace = scrollTop - this._physicalTop;
|
| - var virtualStart = this._virtualStart;
|
| -
|
| - recycledTileSet = [];
|
| + if (!this.full && query[0] !== '(') {
|
| + query = '(' + query + ')';
|
| + }
|
| + this._mq = window.matchMedia(query);
|
| + this._add();
|
| + this.queryHandler(this._mq);
|
| + },
|
|
|
| - kth = this._physicalEnd;
|
| - currentRatio = topSpace / hiddenContentSize;
|
| + queryHandler: function(mq) {
|
| + this._setQueryMatches(mq.matches);
|
| + }
|
|
|
| - // move tiles from bottom to top
|
| - while (
|
| - // approximate `currentRatio` to `ratio`
|
| - currentRatio < ratio &&
|
| - // recycle less physical items than the total
|
| - recycledTiles < this._physicalCount &&
|
| - // ensure that these recycled tiles are needed
|
| - virtualStart - recycledTiles > 0 &&
|
| - // ensure that the tile is not visible
|
| - physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
|
| - ) {
|
| -
|
| - tileHeight = this._getPhysicalSizeIncrement(kth);
|
| - currentRatio += tileHeight / hiddenContentSize;
|
| - physicalBottom -= tileHeight;
|
| - recycledTileSet.push(kth);
|
| - recycledTiles++;
|
| - kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
|
| - }
|
| + });
|
| +/**
|
| + * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
|
| + * coordinate the flow of resize events between "resizers" (elements that control the
|
| + * size or hidden state of their children) and "resizables" (elements that need to be
|
| + * notified when they are resized or un-hidden by their parents in order to take
|
| + * action on their new measurements).
|
| + *
|
| + * Elements that perform measurement should add the `IronResizableBehavior` behavior to
|
| + * their element definition and listen for the `iron-resize` event on themselves.
|
| + * This event will be fired when they become showing after having been hidden,
|
| + * when they are resized explicitly by another resizable, or when the window has been
|
| + * resized.
|
| + *
|
| + * Note, the `iron-resize` event is non-bubbling.
|
| + *
|
| + * @polymerBehavior Polymer.IronResizableBehavior
|
| + * @demo demo/index.html
|
| + **/
|
| + Polymer.IronResizableBehavior = {
|
| + properties: {
|
| + /**
|
| + * The closest ancestor element that implements `IronResizableBehavior`.
|
| + */
|
| + _parentResizable: {
|
| + type: Object,
|
| + observer: '_parentResizableChanged'
|
| + },
|
|
|
| - movingUp = recycledTileSet;
|
| - recycledTiles = -recycledTiles;
|
| + /**
|
| + * True if this element is currently notifying its descedant elements of
|
| + * resize.
|
| + */
|
| + _notifyingDescendant: {
|
| + type: Boolean,
|
| + value: false
|
| }
|
| - // scroll down
|
| - else if (delta > 0) {
|
| - var bottomSpace = physicalBottom - scrollBottom;
|
| - var virtualEnd = this._virtualEnd;
|
| - var lastVirtualItemIndex = this._virtualCount-1;
|
| -
|
| - recycledTileSet = [];
|
| + },
|
|
|
| - kth = this._physicalStart;
|
| - currentRatio = bottomSpace / hiddenContentSize;
|
| + listeners: {
|
| + 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
|
| + },
|
|
|
| - // move tiles from top to bottom
|
| - while (
|
| - // approximate `currentRatio` to `ratio`
|
| - currentRatio < ratio &&
|
| - // recycle less physical items than the total
|
| - recycledTiles < this._physicalCount &&
|
| - // ensure that these recycled tiles are needed
|
| - virtualEnd + recycledTiles < lastVirtualItemIndex &&
|
| - // ensure that the tile is not visible
|
| - this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
|
| - ) {
|
| + created: function() {
|
| + // We don't really need property effects on these, and also we want them
|
| + // to be created before the `_parentResizable` observer fires:
|
| + this._interestedResizables = [];
|
| + this._boundNotifyResize = this.notifyResize.bind(this);
|
| + },
|
|
|
| - tileHeight = this._getPhysicalSizeIncrement(kth);
|
| - currentRatio += tileHeight / hiddenContentSize;
|
| + attached: function() {
|
| + this.fire('iron-request-resize-notifications', null, {
|
| + node: this,
|
| + bubbles: true,
|
| + cancelable: true
|
| + });
|
|
|
| - this._physicalTop += tileHeight;
|
| - recycledTileSet.push(kth);
|
| - recycledTiles++;
|
| - kth = (kth + 1) % this._physicalCount;
|
| - }
|
| + if (!this._parentResizable) {
|
| + window.addEventListener('resize', this._boundNotifyResize);
|
| + this.notifyResize();
|
| }
|
| + },
|
|
|
| - if (recycledTiles === 0) {
|
| - // Try to increase the pool if the list's client height isn't filled up with physical items
|
| - if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
|
| - this._increasePoolIfNeeded();
|
| - }
|
| + detached: function() {
|
| + if (this._parentResizable) {
|
| + this._parentResizable.stopResizeNotificationsFor(this);
|
| } else {
|
| - this._virtualStart = this._virtualStart + recycledTiles;
|
| - this._physicalStart = this._physicalStart + recycledTiles;
|
| - this._update(recycledTileSet, movingUp);
|
| + window.removeEventListener('resize', this._boundNotifyResize);
|
| }
|
| +
|
| + this._parentResizable = null;
|
| },
|
|
|
| /**
|
| - * Update the list of items, starting from the `_virtualStart` item.
|
| - * @param {!Array<number>=} itemSet
|
| - * @param {!Array<number>=} movingUp
|
| + * Can be called to manually notify a resizable and its descendant
|
| + * resizables of a resize change.
|
| */
|
| - _update: function(itemSet, movingUp) {
|
| - // manage focus
|
| - this._manageFocus();
|
| - // update models
|
| - this._assignModels(itemSet);
|
| - // measure heights
|
| - this._updateMetrics(itemSet);
|
| - // adjust offset after measuring
|
| - if (movingUp) {
|
| - while (movingUp.length) {
|
| - var idx = movingUp.pop();
|
| - this._physicalTop -= this._getPhysicalSizeIncrement(idx);
|
| - }
|
| + notifyResize: function() {
|
| + if (!this.isAttached) {
|
| + return;
|
| }
|
| - // update the position of the items
|
| - this._positionItems();
|
| - // set the scroller size
|
| - this._updateScrollerSize();
|
| - // increase the pool of physical items
|
| - this._increasePoolIfNeeded();
|
| +
|
| + this._interestedResizables.forEach(function(resizable) {
|
| + if (this.resizerShouldNotify(resizable)) {
|
| + this._notifyDescendant(resizable);
|
| + }
|
| + }, this);
|
| +
|
| + this._fireResize();
|
| },
|
|
|
| /**
|
| - * Creates a pool of DOM elements and attaches them to the local dom.
|
| + * Used to assign the closest resizable ancestor to this resizable
|
| + * if the ancestor detects a request for notifications.
|
| */
|
| - _createPool: function(size) {
|
| - var physicalItems = new Array(size);
|
| + assignParentResizable: function(parentResizable) {
|
| + this._parentResizable = parentResizable;
|
| + },
|
|
|
| - this._ensureTemplatized();
|
| + /**
|
| + * Used to remove a resizable descendant from the list of descendants
|
| + * that should be notified of a resize change.
|
| + */
|
| + stopResizeNotificationsFor: function(target) {
|
| + var index = this._interestedResizables.indexOf(target);
|
|
|
| - for (var i = 0; i < size; i++) {
|
| - var inst = this.stamp(null);
|
| - // First element child is item; Safari doesn't support children[0]
|
| - // on a doc fragment
|
| - physicalItems[i] = inst.root.querySelector('*');
|
| - Polymer.dom(this).appendChild(inst.root);
|
| + if (index > -1) {
|
| + this._interestedResizables.splice(index, 1);
|
| + this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
|
| }
|
| - return physicalItems;
|
| },
|
|
|
| /**
|
| - * Increases the pool of physical items only if needed.
|
| + * This method can be overridden to filter nested elements that should or
|
| + * should not be notified by the current element. Return true if an element
|
| + * should be notified, or false if it should not be notified.
|
| *
|
| - * @return {boolean} True if the pool was increased.
|
| + * @param {HTMLElement} element A candidate descendant element that
|
| + * implements `IronResizableBehavior`.
|
| + * @return {boolean} True if the `element` should be notified of resize.
|
| */
|
| - _increasePoolIfNeeded: function() {
|
| - // Base case 1: the list has no height.
|
| - if (this._viewportHeight === 0) {
|
| - return false;
|
| - }
|
| - // Base case 2: If the physical size is optimal and the list's client height is full
|
| - // with physical items, don't increase the pool.
|
| - var isClientHeightFull = this._physicalBottom >= this._scrollBottom && this._physicalTop <= this._scrollPosition;
|
| - if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
|
| - return false;
|
| - }
|
| - // this value should range between [0 <= `currentPage` <= `_maxPages`]
|
| - var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
|
| + resizerShouldNotify: function(element) { return true; },
|
|
|
| - if (currentPage === 0) {
|
| - // fill the first page
|
| - this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
|
| - } else if (this._lastPage !== currentPage && isClientHeightFull) {
|
| - // paint the page and defer the next increase
|
| - // wait 16ms which is rough enough to get paint cycle.
|
| - Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, this._itemsPerRow), 16));
|
| - } else {
|
| - // fill the rest of the pages
|
| - this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow));
|
| + _onDescendantIronResize: function(event) {
|
| + if (this._notifyingDescendant) {
|
| + event.stopPropagation();
|
| + return;
|
| }
|
|
|
| - this._lastPage = currentPage;
|
| + // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
|
| + // otherwise non-bubbling event "just work." We do it manually here for
|
| + // the case where Polymer is not using shadow roots for whatever reason:
|
| + if (!Polymer.Settings.useShadow) {
|
| + this._fireResize();
|
| + }
|
| + },
|
|
|
| - return true;
|
| + _fireResize: function() {
|
| + this.fire('iron-resize', null, {
|
| + node: this,
|
| + bubbles: false
|
| + });
|
| },
|
|
|
| - /**
|
| - * Increases the pool size.
|
| - */
|
| - _increasePool: function(missingItems) {
|
| - var nextPhysicalCount = Math.min(
|
| - this._physicalCount + missingItems,
|
| - this._virtualCount - this._virtualStart,
|
| - Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
|
| - );
|
| - var prevPhysicalCount = this._physicalCount;
|
| - var delta = nextPhysicalCount - prevPhysicalCount;
|
| + _onIronRequestResizeNotifications: function(event) {
|
| + var target = event.path ? event.path[0] : event.target;
|
|
|
| - if (delta <= 0) {
|
| + if (target === this) {
|
| return;
|
| }
|
|
|
| - [].push.apply(this._physicalItems, this._createPool(delta));
|
| - [].push.apply(this._physicalSizes, new Array(delta));
|
| + if (this._interestedResizables.indexOf(target) === -1) {
|
| + this._interestedResizables.push(target);
|
| + this.listen(target, 'iron-resize', '_onDescendantIronResize');
|
| + }
|
|
|
| - this._physicalCount = prevPhysicalCount + delta;
|
| + target.assignParentResizable(this);
|
| + this._notifyDescendant(target);
|
|
|
| - // update the physical start if we need to preserve the model of the focused item.
|
| - // In this situation, the focused item is currently rendered and its model would
|
| - // have changed after increasing the pool if the physical start remained unchanged.
|
| - if (this._physicalStart > this._physicalEnd &&
|
| - this._isIndexRendered(this._focusedIndex) &&
|
| - this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
|
| - this._physicalStart = this._physicalStart + delta;
|
| - }
|
| - this._update();
|
| + event.stopPropagation();
|
| },
|
|
|
| - /**
|
| - * Render a new list of items. This method does exactly the same as `update`,
|
| - * but it also ensures that only one `update` cycle is created.
|
| - */
|
| - _render: function() {
|
| - var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
|
| -
|
| - if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
|
| - this._lastPage = 0;
|
| - this._update();
|
| - this._itemsRendered = true;
|
| + _parentResizableChanged: function(parentResizable) {
|
| + if (parentResizable) {
|
| + window.removeEventListener('resize', this._boundNotifyResize);
|
| }
|
| },
|
|
|
| - /**
|
| - * Templetizes the user template.
|
| - */
|
| - _ensureTemplatized: function() {
|
| - if (!this.ctor) {
|
| - // Template instance props that should be excluded from forwarding
|
| - var props = {};
|
| - props.__key__ = true;
|
| - props[this.as] = true;
|
| - props[this.indexAs] = true;
|
| - props[this.selectedAs] = true;
|
| - props.tabIndex = true;
|
| -
|
| - this._instanceProps = props;
|
| - this._userTemplate = Polymer.dom(this).querySelector('template');
|
| -
|
| - if (this._userTemplate) {
|
| - this.templatize(this._userTemplate);
|
| - } else {
|
| - console.warn('iron-list requires a template to be provided in light-dom');
|
| - }
|
| + _notifyDescendant: function(descendant) {
|
| + // NOTE(cdata): In IE10, attached is fired on children first, so it's
|
| + // important not to notify them if the parent is not attached yet (or
|
| + // else they will get redundantly notified when the parent attaches).
|
| + if (!this.isAttached) {
|
| + return;
|
| }
|
| - },
|
|
|
| - /**
|
| - * Implements extension point from Templatizer mixin.
|
| - */
|
| - _getStampedChildren: function() {
|
| - return this._physicalItems;
|
| - },
|
| + this._notifyingDescendant = true;
|
| + descendant.notifyResize();
|
| + this._notifyingDescendant = false;
|
| + }
|
| + };
|
| +/**
|
| + * @param {!Function} selectCallback
|
| + * @constructor
|
| + */
|
| + Polymer.IronSelection = function(selectCallback) {
|
| + this.selection = [];
|
| + this.selectCallback = selectCallback;
|
| + };
|
| +
|
| + Polymer.IronSelection.prototype = {
|
|
|
| /**
|
| - * Implements extension point from Templatizer
|
| - * Called as a side effect of a template instance path change, responsible
|
| - * for notifying items.<key-for-instance>.<path> change up to host.
|
| + * Retrieves the selected item(s).
|
| + *
|
| + * @method get
|
| + * @returns Returns the selected item(s). If the multi property is true,
|
| + * `get` will return an array, otherwise it will return
|
| + * the selected item or undefined if there is no selection.
|
| */
|
| - _forwardInstancePath: function(inst, path, value) {
|
| - if (path.indexOf(this.as + '.') === 0) {
|
| - this.notifyPath('items.' + inst.__key__ + '.' +
|
| - path.slice(this.as.length + 1), value);
|
| - }
|
| + get: function() {
|
| + return this.multi ? this.selection.slice() : this.selection[0];
|
| },
|
|
|
| /**
|
| - * Implements extension point from Templatizer mixin
|
| - * Called as side-effect of a host property change, responsible for
|
| - * notifying parent path change on each row.
|
| + * Clears all the selection except the ones indicated.
|
| + *
|
| + * @method clear
|
| + * @param {Array} excludes items to be excluded.
|
| */
|
| - _forwardParentProp: function(prop, value) {
|
| - if (this._physicalItems) {
|
| - this._physicalItems.forEach(function(item) {
|
| - item._templateInstance[prop] = value;
|
| - }, this);
|
| - }
|
| + clear: function(excludes) {
|
| + this.selection.slice().forEach(function(item) {
|
| + if (!excludes || excludes.indexOf(item) < 0) {
|
| + this.setItemSelected(item, false);
|
| + }
|
| + }, this);
|
| },
|
|
|
| /**
|
| - * Implements extension point from Templatizer
|
| - * Called as side-effect of a host path change, responsible for
|
| - * notifying parent.<path> path change on each row.
|
| + * Indicates if a given item is selected.
|
| + *
|
| + * @method isSelected
|
| + * @param {*} item The item whose selection state should be checked.
|
| + * @returns Returns true if `item` is selected.
|
| */
|
| - _forwardParentPath: function(path, value) {
|
| - if (this._physicalItems) {
|
| - this._physicalItems.forEach(function(item) {
|
| - item._templateInstance.notifyPath(path, value, true);
|
| - }, this);
|
| - }
|
| + isSelected: function(item) {
|
| + return this.selection.indexOf(item) >= 0;
|
| },
|
|
|
| /**
|
| - * Called as a side effect of a host items.<key>.<path> path change,
|
| - * responsible for notifying item.<path> changes.
|
| + * Sets the selection state for a given item to either selected or deselected.
|
| + *
|
| + * @method setItemSelected
|
| + * @param {*} item The item to select.
|
| + * @param {boolean} isSelected True for selected, false for deselected.
|
| */
|
| - _forwardItemPath: function(path, value) {
|
| - if (!this._physicalIndexForKey) {
|
| - return;
|
| - }
|
| - var dot = path.indexOf('.');
|
| - var key = path.substring(0, dot < 0 ? path.length : dot);
|
| - var idx = this._physicalIndexForKey[key];
|
| - var offscreenItem = this._offscreenFocusedItem;
|
| - var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
|
| - offscreenItem : this._physicalItems[idx];
|
| -
|
| - if (!el || el._templateInstance.__key__ !== key) {
|
| - return;
|
| - }
|
| - if (dot >= 0) {
|
| - path = this.as + '.' + path.substring(dot+1);
|
| - el._templateInstance.notifyPath(path, value, true);
|
| - } else {
|
| - // Update selection if needed
|
| - var currentItem = el._templateInstance[this.as];
|
| - if (Array.isArray(this.selectedItems)) {
|
| - for (var i = 0; i < this.selectedItems.length; i++) {
|
| - if (this.selectedItems[i] === currentItem) {
|
| - this.set('selectedItems.' + i, value);
|
| - break;
|
| + setItemSelected: function(item, isSelected) {
|
| + if (item != null) {
|
| + if (isSelected !== this.isSelected(item)) {
|
| + // proceed to update selection only if requested state differs from current
|
| + if (isSelected) {
|
| + this.selection.push(item);
|
| + } else {
|
| + var i = this.selection.indexOf(item);
|
| + if (i >= 0) {
|
| + this.selection.splice(i, 1);
|
| }
|
| }
|
| - } else if (this.selectedItem === currentItem) {
|
| - this.set('selectedItem', value);
|
| + if (this.selectCallback) {
|
| + this.selectCallback(item, isSelected);
|
| + }
|
| }
|
| - el._templateInstance[this.as] = value;
|
| }
|
| },
|
|
|
| /**
|
| - * Called when the items have changed. That is, ressignments
|
| - * to `items`, splices or updates to a single item.
|
| + * Sets the selection state for a given item. If the `multi` property
|
| + * is true, then the selected state of `item` will be toggled; otherwise
|
| + * the `item` will be selected.
|
| + *
|
| + * @method select
|
| + * @param {*} item The item to select.
|
| */
|
| - _itemsChanged: function(change) {
|
| - if (change.path === 'items') {
|
| - // reset items
|
| - this._virtualStart = 0;
|
| - this._physicalTop = 0;
|
| - this._virtualCount = this.items ? this.items.length : 0;
|
| - this._collection = this.items ? Polymer.Collection.get(this.items) : null;
|
| - this._physicalIndexForKey = {};
|
| - this._firstVisibleIndexVal = null;
|
| - this._lastVisibleIndexVal = null;
|
| -
|
| - this._resetScrollPosition(0);
|
| - this._removeFocusedItem();
|
| - // create the initial physical items
|
| - if (!this._physicalItems) {
|
| - this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, this._virtualCount));
|
| - this._physicalItems = this._createPool(this._physicalCount);
|
| - this._physicalSizes = new Array(this._physicalCount);
|
| - }
|
| -
|
| - this._physicalStart = 0;
|
| -
|
| - } else if (change.path === 'items.splices') {
|
| -
|
| - this._adjustVirtualIndex(change.value.indexSplices);
|
| - this._virtualCount = this.items ? this.items.length : 0;
|
| -
|
| - } else {
|
| - // update a single item
|
| - this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
|
| - return;
|
| + select: function(item) {
|
| + if (this.multi) {
|
| + this.toggle(item);
|
| + } else if (this.get() !== item) {
|
| + this.setItemSelected(this.get(), false);
|
| + this.setItemSelected(item, true);
|
| }
|
| -
|
| - this._itemsRendered = false;
|
| - this._debounceTemplate(this._render);
|
| },
|
|
|
| /**
|
| - * @param {!Array<!PolymerSplice>} splices
|
| + * Toggles the selection state for `item`.
|
| + *
|
| + * @method toggle
|
| + * @param {*} item The item to toggle.
|
| */
|
| - _adjustVirtualIndex: function(splices) {
|
| - splices.forEach(function(splice) {
|
| - // deselect removed items
|
| - splice.removed.forEach(this._removeItem, this);
|
| - // We only need to care about changes happening above the current position
|
| - if (splice.index < this._virtualStart) {
|
| - var delta = Math.max(
|
| - splice.addedCount - splice.removed.length,
|
| - splice.index - this._virtualStart);
|
| + toggle: function(item) {
|
| + this.setItemSelected(item, !this.isSelected(item));
|
| + }
|
|
|
| - this._virtualStart = this._virtualStart + delta;
|
| + };
|
| +/** @polymerBehavior */
|
| + Polymer.IronSelectableBehavior = {
|
|
|
| - if (this._focusedIndex >= 0) {
|
| - this._focusedIndex = this._focusedIndex + delta;
|
| - }
|
| - }
|
| - }, this);
|
| - },
|
| + /**
|
| + * Fired when iron-selector is activated (selected or deselected).
|
| + * It is fired before the selected items are changed.
|
| + * Cancel the event to abort selection.
|
| + *
|
| + * @event iron-activate
|
| + */
|
|
|
| - _removeItem: function(item) {
|
| - this.$.selector.deselect(item);
|
| - // remove the current focused item
|
| - if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
|
| - this._removeFocusedItem();
|
| - }
|
| - },
|
| + /**
|
| + * Fired when an item is selected
|
| + *
|
| + * @event iron-select
|
| + */
|
|
|
| - /**
|
| - * Executes a provided function per every physical index in `itemSet`
|
| - * `itemSet` default value is equivalent to the entire set of physical indexes.
|
| - *
|
| - * @param {!function(number, number)} fn
|
| - * @param {!Array<number>=} itemSet
|
| - */
|
| - _iterateItems: function(fn, itemSet) {
|
| - var pidx, vidx, rtn, i;
|
| + /**
|
| + * Fired when an item is deselected
|
| + *
|
| + * @event iron-deselect
|
| + */
|
|
|
| - if (arguments.length === 2 && itemSet) {
|
| - for (i = 0; i < itemSet.length; i++) {
|
| - pidx = itemSet[i];
|
| - vidx = this._computeVidx(pidx);
|
| - if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| - return rtn;
|
| - }
|
| - }
|
| - } else {
|
| - pidx = this._physicalStart;
|
| - vidx = this._virtualStart;
|
| + /**
|
| + * Fired when the list of selectable items changes (e.g., items are
|
| + * added or removed). The detail of the event is a mutation record that
|
| + * describes what changed.
|
| + *
|
| + * @event iron-items-changed
|
| + */
|
|
|
| - for (; pidx < this._physicalCount; pidx++, vidx++) {
|
| - if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| - return rtn;
|
| - }
|
| - }
|
| - for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
|
| - if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| - return rtn;
|
| - }
|
| - }
|
| - }
|
| - },
|
| + properties: {
|
|
|
| - /**
|
| - * Returns the virtual index for a given physical index
|
| - *
|
| - * @param {number} pidx Physical index
|
| - * @return {number}
|
| - */
|
| - _computeVidx: function(pidx) {
|
| - if (pidx >= this._physicalStart) {
|
| - return this._virtualStart + (pidx - this._physicalStart);
|
| - }
|
| - return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
|
| - },
|
| + /**
|
| + * If you want to use an attribute value or property of an element for
|
| + * `selected` instead of the index, set this to the name of the attribute
|
| + * or property. Hyphenated values are converted to camel case when used to
|
| + * look up the property of a selectable element. Camel cased values are
|
| + * *not* converted to hyphenated values for attribute lookup. It's
|
| + * recommended that you provide the hyphenated form of the name so that
|
| + * selection works in both cases. (Use `attr-or-property-name` instead of
|
| + * `attrOrPropertyName`.)
|
| + */
|
| + attrForSelected: {
|
| + type: String,
|
| + value: null
|
| + },
|
|
|
| - /**
|
| - * Assigns the data models to a given set of items.
|
| - * @param {!Array<number>=} itemSet
|
| - */
|
| - _assignModels: function(itemSet) {
|
| - this._iterateItems(function(pidx, vidx) {
|
| - var el = this._physicalItems[pidx];
|
| - var inst = el._templateInstance;
|
| - var item = this.items && this.items[vidx];
|
| + /**
|
| + * Gets or sets the selected element. The default is to use the index of the item.
|
| + * @type {string|number}
|
| + */
|
| + selected: {
|
| + type: String,
|
| + notify: true
|
| + },
|
|
|
| - if (item != null) {
|
| - inst[this.as] = item;
|
| - inst.__key__ = this._collection.getKey(item);
|
| - inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
|
| - inst[this.indexAs] = vidx;
|
| - inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
|
| - this._physicalIndexForKey[inst.__key__] = pidx;
|
| - el.removeAttribute('hidden');
|
| - } else {
|
| - inst.__key__ = null;
|
| - el.setAttribute('hidden', '');
|
| - }
|
| - }, itemSet);
|
| - },
|
| + /**
|
| + * Returns the currently selected item.
|
| + *
|
| + * @type {?Object}
|
| + */
|
| + selectedItem: {
|
| + type: Object,
|
| + readOnly: true,
|
| + notify: true
|
| + },
|
|
|
| - /**
|
| - * Updates the height for a given set of items.
|
| - *
|
| - * @param {!Array<number>=} itemSet
|
| - */
|
| - _updateMetrics: function(itemSet) {
|
| - // Make sure we distributed all the physical items
|
| - // so we can measure them
|
| - Polymer.dom.flush();
|
| + /**
|
| + * The event that fires from items when they are selected. Selectable
|
| + * will listen for this event from items and update the selection state.
|
| + * Set to empty string to listen to no events.
|
| + */
|
| + activateEvent: {
|
| + type: String,
|
| + value: 'tap',
|
| + observer: '_activateEventChanged'
|
| + },
|
|
|
| - var newPhysicalSize = 0;
|
| - var oldPhysicalSize = 0;
|
| - var prevAvgCount = this._physicalAverageCount;
|
| - var prevPhysicalAvg = this._physicalAverage;
|
| + /**
|
| + * This is a CSS selector string. If this is set, only items that match the CSS selector
|
| + * are selectable.
|
| + */
|
| + selectable: String,
|
|
|
| - this._iterateItems(function(pidx, vidx) {
|
| + /**
|
| + * The class to set on elements when selected.
|
| + */
|
| + selectedClass: {
|
| + type: String,
|
| + value: 'iron-selected'
|
| + },
|
|
|
| - oldPhysicalSize += this._physicalSizes[pidx] || 0;
|
| - this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
|
| - newPhysicalSize += this._physicalSizes[pidx];
|
| - this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
|
| + /**
|
| + * The attribute to set on elements when selected.
|
| + */
|
| + selectedAttribute: {
|
| + type: String,
|
| + value: null
|
| + },
|
|
|
| - }, itemSet);
|
| + /**
|
| + * Default fallback if the selection based on selected with `attrForSelected`
|
| + * is not found.
|
| + */
|
| + fallbackSelection: {
|
| + type: String,
|
| + value: null
|
| + },
|
|
|
| - this._viewportHeight = this._scrollTargetHeight;
|
| - if (this.grid) {
|
| - this._updateGridMetrics();
|
| - this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
|
| - } else {
|
| - this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
|
| + /**
|
| + * The list of items from which a selection can be made.
|
| + */
|
| + items: {
|
| + type: Array,
|
| + readOnly: true,
|
| + notify: true,
|
| + value: function() {
|
| + return [];
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * The set of excluded elements where the key is the `localName`
|
| + * of the element that will be ignored from the item list.
|
| + *
|
| + * @default {template: 1}
|
| + */
|
| + _excludedLocalNames: {
|
| + type: Object,
|
| + value: function() {
|
| + return {
|
| + 'template': 1
|
| + };
|
| + }
|
| }
|
| + },
|
|
|
| - // update the average if we measured something
|
| - if (this._physicalAverageCount !== prevAvgCount) {
|
| - this._physicalAverage = Math.round(
|
| - ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
|
| - this._physicalAverageCount);
|
| + observers: [
|
| + '_updateAttrForSelected(attrForSelected)',
|
| + '_updateSelected(selected)',
|
| + '_checkFallback(fallbackSelection)'
|
| + ],
|
| +
|
| + created: function() {
|
| + this._bindFilterItem = this._filterItem.bind(this);
|
| + this._selection = new Polymer.IronSelection(this._applySelection.bind(this));
|
| + },
|
| +
|
| + attached: function() {
|
| + this._observer = this._observeItems(this);
|
| + this._updateItems();
|
| + if (!this._shouldUpdateSelection) {
|
| + this._updateSelected();
|
| }
|
| + this._addListener(this.activateEvent);
|
| },
|
|
|
| - _updateGridMetrics: function() {
|
| - this._viewportWidth = this.$.items.offsetWidth;
|
| - // Set item width to the value of the _physicalItems offsetWidth
|
| - this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoundingClientRect().width : DEFAULT_GRID_SIZE;
|
| - // Set row height to the value of the _physicalItems offsetHeight
|
| - this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetHeight : DEFAULT_GRID_SIZE;
|
| - // If in grid mode compute how many items with exist in each row
|
| - this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / this._itemWidth) : this._itemsPerRow;
|
| + detached: function() {
|
| + if (this._observer) {
|
| + Polymer.dom(this).unobserveNodes(this._observer);
|
| + }
|
| + this._removeListener(this.activateEvent);
|
| },
|
|
|
| /**
|
| - * Updates the position of the physical items.
|
| + * Returns the index of the given item.
|
| + *
|
| + * @method indexOf
|
| + * @param {Object} item
|
| + * @returns Returns the index of the item
|
| */
|
| - _positionItems: function() {
|
| - this._adjustScrollPosition();
|
| + indexOf: function(item) {
|
| + return this.items.indexOf(item);
|
| + },
|
|
|
| - var y = this._physicalTop;
|
| -
|
| - if (this.grid) {
|
| - var totalItemWidth = this._itemsPerRow * this._itemWidth;
|
| - var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
|
| -
|
| - this._iterateItems(function(pidx, vidx) {
|
| -
|
| - var modulus = vidx % this._itemsPerRow;
|
| - var x = Math.floor((modulus * this._itemWidth) + rowOffset);
|
| -
|
| - this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
|
| -
|
| - if (this._shouldRenderNextRow(vidx)) {
|
| - y += this._rowHeight;
|
| - }
|
| -
|
| - });
|
| - } else {
|
| - this._iterateItems(function(pidx, vidx) {
|
| -
|
| - this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
|
| - y += this._physicalSizes[pidx];
|
| -
|
| - });
|
| - }
|
| - },
|
| -
|
| - _getPhysicalSizeIncrement: function(pidx) {
|
| - if (!this.grid) {
|
| - return this._physicalSizes[pidx];
|
| - }
|
| - if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
|
| - return 0;
|
| - }
|
| - return this._rowHeight;
|
| - },
|
| + /**
|
| + * Selects the given value.
|
| + *
|
| + * @method select
|
| + * @param {string|number} value the value to select.
|
| + */
|
| + select: function(value) {
|
| + this.selected = value;
|
| + },
|
|
|
| /**
|
| - * Returns, based on the current index,
|
| - * whether or not the next index will need
|
| - * to be rendered on a new row.
|
| + * Selects the previous item.
|
| *
|
| - * @param {number} vidx Virtual index
|
| - * @return {boolean}
|
| + * @method selectPrevious
|
| */
|
| - _shouldRenderNextRow: function(vidx) {
|
| - return vidx % this._itemsPerRow === this._itemsPerRow - 1;
|
| + selectPrevious: function() {
|
| + var length = this.items.length;
|
| + var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length;
|
| + this.selected = this._indexToValue(index);
|
| },
|
|
|
| /**
|
| - * Adjusts the scroll position when it was overestimated.
|
| + * Selects the next item.
|
| + *
|
| + * @method selectNext
|
| */
|
| - _adjustScrollPosition: function() {
|
| - var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
|
| - Math.min(this._scrollPosition + this._physicalTop, 0);
|
| -
|
| - if (deltaHeight) {
|
| - this._physicalTop = this._physicalTop - deltaHeight;
|
| - // juking scroll position during interial scrolling on iOS is no bueno
|
| - if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
|
| - this._resetScrollPosition(this._scrollTop - deltaHeight);
|
| - }
|
| - }
|
| + selectNext: function() {
|
| + var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.length;
|
| + this.selected = this._indexToValue(index);
|
| },
|
|
|
| /**
|
| - * Sets the position of the scroll.
|
| + * Selects the item at the given index.
|
| + *
|
| + * @method selectIndex
|
| */
|
| - _resetScrollPosition: function(pos) {
|
| - if (this.scrollTarget) {
|
| - this._scrollTop = pos;
|
| - this._scrollPosition = this._scrollTop;
|
| - }
|
| + selectIndex: function(index) {
|
| + this.select(this._indexToValue(index));
|
| },
|
|
|
| /**
|
| - * Sets the scroll height, that's the height of the content,
|
| + * Force a synchronous update of the `items` property.
|
| *
|
| - * @param {boolean=} forceUpdate If true, updates the height no matter what.
|
| + * NOTE: Consider listening for the `iron-items-changed` event to respond to
|
| + * updates to the set of selectable items after updates to the DOM list and
|
| + * selection state have been made.
|
| + *
|
| + * WARNING: If you are using this method, you should probably consider an
|
| + * alternate approach. Synchronously querying for items is potentially
|
| + * slow for many use cases. The `items` property will update asynchronously
|
| + * on its own to reflect selectable items in the DOM.
|
| */
|
| - _updateScrollerSize: function(forceUpdate) {
|
| - if (this.grid) {
|
| - this._estScrollHeight = this._virtualRowCount * this._rowHeight;
|
| - } else {
|
| - this._estScrollHeight = (this._physicalBottom +
|
| - Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage);
|
| - }
|
| -
|
| - forceUpdate = forceUpdate || this._scrollHeight === 0;
|
| - forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
|
| - forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this._estScrollHeight;
|
| -
|
| - // amortize height adjustment, so it won't trigger repaints very often
|
| - if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
|
| - this.$.items.style.height = this._estScrollHeight + 'px';
|
| - this._scrollHeight = this._estScrollHeight;
|
| - }
|
| + forceSynchronousItemUpdate: function() {
|
| + this._updateItems();
|
| },
|
|
|
| - /**
|
| - * Scroll to a specific item in the virtual list regardless
|
| - * of the physical items in the DOM tree.
|
| - *
|
| - * @method scrollToItem
|
| - * @param {(Object)} item The item to be scrolled to
|
| - */
|
| - scrollToItem: function(item){
|
| - return this.scrollToIndex(this.items.indexOf(item));
|
| + get _shouldUpdateSelection() {
|
| + return this.selected != null;
|
| },
|
|
|
| - /**
|
| - * Scroll to a specific index in the virtual list regardless
|
| - * of the physical items in the DOM tree.
|
| - *
|
| - * @method scrollToIndex
|
| - * @param {number} idx The index of the item
|
| - */
|
| - scrollToIndex: function(idx) {
|
| - if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
|
| - return;
|
| + _checkFallback: function() {
|
| + if (this._shouldUpdateSelection) {
|
| + this._updateSelected();
|
| }
|
| + },
|
|
|
| - Polymer.dom.flush();
|
| + _addListener: function(eventName) {
|
| + this.listen(this, eventName, '_activateHandler');
|
| + },
|
|
|
| - idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
|
| - // update the virtual start only when needed
|
| - if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
|
| - this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
|
| - }
|
| - // manage focus
|
| - this._manageFocus();
|
| - // assign new models
|
| - this._assignModels();
|
| - // measure the new sizes
|
| - this._updateMetrics();
|
| + _removeListener: function(eventName) {
|
| + this.unlisten(this, eventName, '_activateHandler');
|
| + },
|
|
|
| - // estimate new physical offset
|
| - var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
|
| - this._physicalTop = estPhysicalTop;
|
| + _activateEventChanged: function(eventName, old) {
|
| + this._removeListener(old);
|
| + this._addListener(eventName);
|
| + },
|
|
|
| - var currentTopItem = this._physicalStart;
|
| - var currentVirtualItem = this._virtualStart;
|
| - var targetOffsetTop = 0;
|
| - var hiddenContentSize = this._hiddenContentSize;
|
| + _updateItems: function() {
|
| + var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
|
| + nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
|
| + this._setItems(nodes);
|
| + },
|
|
|
| - // scroll to the item as much as we can
|
| - while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
|
| - targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(currentTopItem);
|
| - currentTopItem = (currentTopItem + 1) % this._physicalCount;
|
| - currentVirtualItem++;
|
| + _updateAttrForSelected: function() {
|
| + if (this._shouldUpdateSelection) {
|
| + this.selected = this._indexToValue(this.indexOf(this.selectedItem));
|
| }
|
| - // update the scroller size
|
| - this._updateScrollerSize(true);
|
| - // update the position of the items
|
| - this._positionItems();
|
| - // set the new scroll position
|
| - this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop);
|
| - // increase the pool of physical items if needed
|
| - this._increasePoolIfNeeded();
|
| - // clear cached visible index
|
| - this._firstVisibleIndexVal = null;
|
| - this._lastVisibleIndexVal = null;
|
| },
|
|
|
| - /**
|
| - * Reset the physical average and the average count.
|
| - */
|
| - _resetAverage: function() {
|
| - this._physicalAverage = 0;
|
| - this._physicalAverageCount = 0;
|
| + _updateSelected: function() {
|
| + this._selectSelected(this.selected);
|
| },
|
|
|
| - /**
|
| - * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
|
| - * when the element is resized.
|
| - */
|
| - _resizeHandler: function() {
|
| - // iOS fires the resize event when the address bar slides up
|
| - if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100) {
|
| - return;
|
| + _selectSelected: function(selected) {
|
| + this._selection.select(this._valueToItem(this.selected));
|
| + // Check for items, since this array is populated only when attached
|
| + // Since Number(0) is falsy, explicitly check for undefined
|
| + if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
|
| + this.selected = this.fallbackSelection;
|
| }
|
| - // In Desktop Safari 9.0.3, if the scroll bars are always shown,
|
| - // changing the scroll position from a resize handler would result in
|
| - // the scroll position being reset. Waiting 1ms fixes the issue.
|
| - Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
|
| - this.updateViewportBoundaries();
|
| - this._render();
|
| -
|
| - if (this._itemsRendered && this._physicalItems && this._isVisible) {
|
| - this._resetAverage();
|
| - this.scrollToIndex(this.firstVisibleIndex);
|
| - }
|
| - }.bind(this), 1));
|
| },
|
|
|
| - _getModelFromItem: function(item) {
|
| - var key = this._collection.getKey(item);
|
| - var pidx = this._physicalIndexForKey[key];
|
| + _filterItem: function(node) {
|
| + return !this._excludedLocalNames[node.localName];
|
| + },
|
|
|
| - if (pidx != null) {
|
| - return this._physicalItems[pidx]._templateInstance;
|
| - }
|
| - return null;
|
| + _valueToItem: function(value) {
|
| + return (value == null) ? null : this.items[this._valueToIndex(value)];
|
| },
|
|
|
| - /**
|
| - * Gets a valid item instance from its index or the object value.
|
| - *
|
| - * @param {(Object|number)} item The item object or its index
|
| - */
|
| - _getNormalizedItem: function(item) {
|
| - if (this._collection.getKey(item) === undefined) {
|
| - if (typeof item === 'number') {
|
| - item = this.items[item];
|
| - if (!item) {
|
| - throw new RangeError('<item> not found');
|
| + _valueToIndex: function(value) {
|
| + if (this.attrForSelected) {
|
| + for (var i = 0, item; item = this.items[i]; i++) {
|
| + if (this._valueForItem(item) == value) {
|
| + return i;
|
| }
|
| - return item;
|
| }
|
| - throw new TypeError('<item> should be a valid item');
|
| + } else {
|
| + return Number(value);
|
| }
|
| - return item;
|
| },
|
|
|
| - /**
|
| - * Select the list item at the given index.
|
| - *
|
| - * @method selectItem
|
| - * @param {(Object|number)} item The item object or its index
|
| - */
|
| - selectItem: function(item) {
|
| - item = this._getNormalizedItem(item);
|
| - var model = this._getModelFromItem(item);
|
| -
|
| - if (!this.multiSelection && this.selectedItem) {
|
| - this.deselectItem(this.selectedItem);
|
| - }
|
| - if (model) {
|
| - model[this.selectedAs] = true;
|
| + _indexToValue: function(index) {
|
| + if (this.attrForSelected) {
|
| + var item = this.items[index];
|
| + if (item) {
|
| + return this._valueForItem(item);
|
| + }
|
| + } else {
|
| + return index;
|
| }
|
| - this.$.selector.select(item);
|
| - this.updateSizeForItem(item);
|
| },
|
|
|
| - /**
|
| - * Deselects the given item list if it is already selected.
|
| - *
|
| -
|
| - * @method deselect
|
| - * @param {(Object|number)} item The item object or its index
|
| - */
|
| - deselectItem: function(item) {
|
| - item = this._getNormalizedItem(item);
|
| - var model = this._getModelFromItem(item);
|
| + _valueForItem: function(item) {
|
| + var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
|
| + return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected);
|
| + },
|
|
|
| - if (model) {
|
| - model[this.selectedAs] = false;
|
| + _applySelection: function(item, isSelected) {
|
| + if (this.selectedClass) {
|
| + this.toggleClass(this.selectedClass, isSelected, item);
|
| }
|
| - this.$.selector.deselect(item);
|
| - this.updateSizeForItem(item);
|
| + if (this.selectedAttribute) {
|
| + this.toggleAttribute(this.selectedAttribute, isSelected, item);
|
| + }
|
| + this._selectionChange();
|
| + this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
|
| },
|
|
|
| - /**
|
| - * Select or deselect a given item depending on whether the item
|
| - * has already been selected.
|
| - *
|
| - * @method toggleSelectionForItem
|
| - * @param {(Object|number)} item The item object or its index
|
| - */
|
| - toggleSelectionForItem: function(item) {
|
| - item = this._getNormalizedItem(item);
|
| - if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
|
| - this.deselectItem(item);
|
| - } else {
|
| - this.selectItem(item);
|
| - }
|
| + _selectionChange: function() {
|
| + this._setSelectedItem(this._selection.get());
|
| },
|
|
|
| - /**
|
| - * Clears the current selection state of the list.
|
| - *
|
| - * @method clearSelection
|
| - */
|
| - clearSelection: function() {
|
| - function unselect(item) {
|
| - var model = this._getModelFromItem(item);
|
| - if (model) {
|
| - model[this.selectedAs] = false;
|
| - }
|
| - }
|
| + // observe items change under the given node.
|
| + _observeItems: function(node) {
|
| + return Polymer.dom(node).observeNodes(function(mutation) {
|
| + this._updateItems();
|
|
|
| - if (Array.isArray(this.selectedItems)) {
|
| - this.selectedItems.forEach(unselect, this);
|
| - } else if (this.selectedItem) {
|
| - unselect.call(this, this.selectedItem);
|
| - }
|
| + if (this._shouldUpdateSelection) {
|
| + this._updateSelected();
|
| + }
|
|
|
| - /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
|
| + // Let other interested parties know about the change so that
|
| + // we don't have to recreate mutation observers everywhere.
|
| + this.fire('iron-items-changed', mutation, {
|
| + bubbles: false,
|
| + cancelable: false
|
| + });
|
| + });
|
| },
|
|
|
| - /**
|
| - * Add an event listener to `tap` if `selectionEnabled` is true,
|
| - * it will remove the listener otherwise.
|
| - */
|
| - _selectionEnabledChanged: function(selectionEnabled) {
|
| - var handler = selectionEnabled ? this.listen : this.unlisten;
|
| - handler.call(this, this, 'tap', '_selectionHandler');
|
| + _activateHandler: function(e) {
|
| + var t = e.target;
|
| + var items = this.items;
|
| + while (t && t != this) {
|
| + var i = items.indexOf(t);
|
| + if (i >= 0) {
|
| + var value = this._indexToValue(i);
|
| + this._itemActivate(value, t);
|
| + return;
|
| + }
|
| + t = t.parentNode;
|
| + }
|
| },
|
|
|
| - /**
|
| - * Select an item from an event object.
|
| - */
|
| - _selectionHandler: function(e) {
|
| - var model = this.modelForElement(e.target);
|
| - if (!model) {
|
| - return;
|
| - }
|
| - var modelTabIndex, activeElTabIndex;
|
| - var target = Polymer.dom(e).path[0];
|
| - var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).activeElement;
|
| - var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.indexAs])];
|
| - // Safari does not focus certain form controls via mouse
|
| - // https://bugs.webkit.org/show_bug.cgi?id=118043
|
| - if (target.localName === 'input' ||
|
| - target.localName === 'button' ||
|
| - target.localName === 'select') {
|
| - return;
|
| - }
|
| - // Set a temporary tabindex
|
| - modelTabIndex = model.tabIndex;
|
| - model.tabIndex = SECRET_TABINDEX;
|
| - activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
|
| - model.tabIndex = modelTabIndex;
|
| - // Only select the item if the tap wasn't on a focusable child
|
| - // or the element bound to `tabIndex`
|
| - if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SECRET_TABINDEX) {
|
| - return;
|
| + _itemActivate: function(value, item) {
|
| + if (!this.fire('iron-activate',
|
| + {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
|
| + this.select(value);
|
| }
|
| - this.toggleSelectionForItem(model[this.as]);
|
| - },
|
| + }
|
|
|
| - _multiSelectionChanged: function(multiSelection) {
|
| - this.clearSelection();
|
| - this.$.selector.multi = multiSelection;
|
| - },
|
| + };
|
| +Polymer({
|
|
|
| - /**
|
| - * Updates the size of an item.
|
| - *
|
| - * @method updateSizeForItem
|
| - * @param {(Object|number)} item The item object or its index
|
| - */
|
| - updateSizeForItem: function(item) {
|
| - item = this._getNormalizedItem(item);
|
| - var key = this._collection.getKey(item);
|
| - var pidx = this._physicalIndexForKey[key];
|
| + is: 'iron-pages',
|
|
|
| - if (pidx != null) {
|
| - this._updateMetrics([pidx]);
|
| - this._positionItems();
|
| - }
|
| - },
|
| + behaviors: [
|
| + Polymer.IronResizableBehavior,
|
| + Polymer.IronSelectableBehavior
|
| + ],
|
|
|
| - /**
|
| - * Creates a temporary backfill item in the rendered pool of physical items
|
| - * to replace the main focused item. The focused item has tabIndex = 0
|
| - * and might be currently focused by the user.
|
| - *
|
| - * This dynamic replacement helps to preserve the focus state.
|
| - */
|
| - _manageFocus: function() {
|
| - var fidx = this._focusedIndex;
|
| + properties: {
|
|
|
| - if (fidx >= 0 && fidx < this._virtualCount) {
|
| - // if it's a valid index, check if that index is rendered
|
| - // in a physical item.
|
| - if (this._isIndexRendered(fidx)) {
|
| - this._restoreFocusedItem();
|
| - } else {
|
| - this._createFocusBackfillItem();
|
| + // as the selected page is the only one visible, activateEvent
|
| + // is both non-sensical and problematic; e.g. in cases where a user
|
| + // handler attempts to change the page and the activateEvent
|
| + // handler immediately changes it back
|
| + activateEvent: {
|
| + type: String,
|
| + value: null
|
| }
|
| - } else if (this._virtualCount > 0 && this._physicalCount > 0) {
|
| - // otherwise, assign the initial focused index.
|
| - this._focusedIndex = this._virtualStart;
|
| - this._focusedItem = this._physicalItems[this._physicalStart];
|
| - }
|
| - },
|
| -
|
| - _isIndexRendered: function(idx) {
|
| - return idx >= this._virtualStart && idx <= this._virtualEnd;
|
| - },
|
|
|
| - _isIndexVisible: function(idx) {
|
| - return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
|
| - },
|
| + },
|
|
|
| - _getPhysicalIndex: function(idx) {
|
| - return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
|
| - },
|
| + observers: [
|
| + '_selectedPageChanged(selected)'
|
| + ],
|
|
|
| - _focusPhysicalItem: function(idx) {
|
| - if (idx < 0 || idx >= this._virtualCount) {
|
| - return;
|
| - }
|
| - this._restoreFocusedItem();
|
| - // scroll to index to make sure it's rendered
|
| - if (!this._isIndexRendered(idx)) {
|
| - this.scrollToIndex(idx);
|
| + _selectedPageChanged: function(selected, old) {
|
| + this.async(this.notifyResize);
|
| }
|
| + });
|
| +(function() {
|
|
|
| - var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
|
| - var model = physicalItem._templateInstance;
|
| - var focusable;
|
| + // monostate data
|
| + var metaDatas = {};
|
| + var metaArrays = {};
|
| + var singleton = null;
|
|
|
| - // set a secret tab index
|
| - model.tabIndex = SECRET_TABINDEX;
|
| - // check if focusable element is the physical item
|
| - if (physicalItem.tabIndex === SECRET_TABINDEX) {
|
| - focusable = physicalItem;
|
| - }
|
| - // search for the element which tabindex is bound to the secret tab index
|
| - if (!focusable) {
|
| - focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET_TABINDEX + '"]');
|
| - }
|
| - // restore the tab index
|
| - model.tabIndex = 0;
|
| - // focus the focusable element
|
| - this._focusedIndex = idx;
|
| - focusable && focusable.focus();
|
| - },
|
| + Polymer.IronMeta = Polymer({
|
|
|
| - _removeFocusedItem: function() {
|
| - if (this._offscreenFocusedItem) {
|
| - Polymer.dom(this).removeChild(this._offscreenFocusedItem);
|
| - }
|
| - this._offscreenFocusedItem = null;
|
| - this._focusBackfillItem = null;
|
| - this._focusedItem = null;
|
| - this._focusedIndex = -1;
|
| - },
|
| -
|
| - _createFocusBackfillItem: function() {
|
| - var pidx, fidx = this._focusedIndex;
|
| - if (this._offscreenFocusedItem || fidx < 0) {
|
| - return;
|
| - }
|
| - if (!this._focusBackfillItem) {
|
| - // create a physical item, so that it backfills the focused item.
|
| - var stampedTemplate = this.stamp(null);
|
| - this._focusBackfillItem = stampedTemplate.root.querySelector('*');
|
| - Polymer.dom(this).appendChild(stampedTemplate.root);
|
| - }
|
| - // get the physical index for the focused index
|
| - pidx = this._getPhysicalIndex(fidx);
|
| -
|
| - if (pidx != null) {
|
| - // set the offcreen focused physical item
|
| - this._offscreenFocusedItem = this._physicalItems[pidx];
|
| - // backfill the focused physical item
|
| - this._physicalItems[pidx] = this._focusBackfillItem;
|
| - // hide the focused physical
|
| - this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
|
| - }
|
| - },
|
| -
|
| - _restoreFocusedItem: function() {
|
| - var pidx, fidx = this._focusedIndex;
|
| -
|
| - if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
|
| - return;
|
| - }
|
| - // assign models to the focused index
|
| - this._assignModels();
|
| - // get the new physical index for the focused index
|
| - pidx = this._getPhysicalIndex(fidx);
|
| -
|
| - if (pidx != null) {
|
| - // flip the focus backfill
|
| - this._focusBackfillItem = this._physicalItems[pidx];
|
| - // restore the focused physical item
|
| - this._physicalItems[pidx] = this._offscreenFocusedItem;
|
| - // reset the offscreen focused item
|
| - this._offscreenFocusedItem = null;
|
| - // hide the physical item that backfills
|
| - this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
|
| - }
|
| - },
|
| -
|
| - _didFocus: function(e) {
|
| - var targetModel = this.modelForElement(e.target);
|
| - var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
|
| - var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
|
| - var fidx = this._focusedIndex;
|
| -
|
| - if (!targetModel || !focusedModel) {
|
| - return;
|
| - }
|
| - if (focusedModel === targetModel) {
|
| - // if the user focused the same item, then bring it into view if it's not visible
|
| - if (!this._isIndexVisible(fidx)) {
|
| - this.scrollToIndex(fidx);
|
| - }
|
| - } else {
|
| - this._restoreFocusedItem();
|
| - // restore tabIndex for the currently focused item
|
| - focusedModel.tabIndex = -1;
|
| - // set the tabIndex for the next focused item
|
| - targetModel.tabIndex = 0;
|
| - fidx = targetModel[this.indexAs];
|
| - this._focusedIndex = fidx;
|
| - this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
|
| -
|
| - if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
|
| - this._update();
|
| - }
|
| - }
|
| - },
|
| -
|
| - _didMoveUp: function() {
|
| - this._focusPhysicalItem(this._focusedIndex - 1);
|
| - },
|
| -
|
| - _didMoveDown: function(e) {
|
| - // disable scroll when pressing the down key
|
| - e.detail.keyboardEvent.preventDefault();
|
| - this._focusPhysicalItem(this._focusedIndex + 1);
|
| - },
|
| -
|
| - _didEnter: function(e) {
|
| - this._focusPhysicalItem(this._focusedIndex);
|
| - this._selectionHandler(e.detail.keyboardEvent);
|
| - }
|
| - });
|
| -
|
| -})();
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -cr.define('downloads', function() {
|
| - /**
|
| - * @param {string} chromeSendName
|
| - * @return {function(string):void} A chrome.send() callback with curried name.
|
| - */
|
| - function chromeSendWithId(chromeSendName) {
|
| - return function(id) { chrome.send(chromeSendName, [id]); };
|
| - }
|
| -
|
| - /** @constructor */
|
| - function ActionService() {
|
| - /** @private {Array<string>} */
|
| - this.searchTerms_ = [];
|
| - }
|
| -
|
| - /**
|
| - * @param {string} s
|
| - * @return {string} |s| without whitespace at the beginning or end.
|
| - */
|
| - function trim(s) { return s.trim(); }
|
| -
|
| - /**
|
| - * @param {string|undefined} value
|
| - * @return {boolean} Whether |value| is truthy.
|
| - */
|
| - function truthy(value) { return !!value; }
|
| -
|
| - /**
|
| - * @param {string} searchText Input typed by the user into a search box.
|
| - * @return {Array<string>} A list of terms extracted from |searchText|.
|
| - */
|
| - ActionService.splitTerms = function(searchText) {
|
| - // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
|
| - return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
|
| - };
|
| -
|
| - ActionService.prototype = {
|
| - /** @param {string} id ID of the download to cancel. */
|
| - cancel: chromeSendWithId('cancel'),
|
| -
|
| - /** Instructs the browser to clear all finished downloads. */
|
| - clearAll: function() {
|
| - if (loadTimeData.getBoolean('allowDeletingHistory')) {
|
| - chrome.send('clearAll');
|
| - this.search('');
|
| - }
|
| - },
|
| -
|
| - /** @param {string} id ID of the dangerous download to discard. */
|
| - discardDangerous: chromeSendWithId('discardDangerous'),
|
| -
|
| - /** @param {string} url URL of a file to download. */
|
| - download: function(url) {
|
| - var a = document.createElement('a');
|
| - a.href = url;
|
| - a.setAttribute('download', '');
|
| - a.click();
|
| - },
|
| -
|
| - /** @param {string} id ID of the download that the user started dragging. */
|
| - drag: chromeSendWithId('drag'),
|
| -
|
| - /** Loads more downloads with the current search terms. */
|
| - loadMore: function() {
|
| - chrome.send('getDownloads', this.searchTerms_);
|
| - },
|
| -
|
| - /**
|
| - * @return {boolean} Whether the user is currently searching for downloads
|
| - * (i.e. has a non-empty search term).
|
| - */
|
| - isSearching: function() {
|
| - return this.searchTerms_.length > 0;
|
| - },
|
| -
|
| - /** Opens the current local destination for downloads. */
|
| - openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
|
| -
|
| - /**
|
| - * @param {string} id ID of the download to run locally on the user's box.
|
| - */
|
| - openFile: chromeSendWithId('openFile'),
|
| -
|
| - /** @param {string} id ID the of the progressing download to pause. */
|
| - pause: chromeSendWithId('pause'),
|
| -
|
| - /** @param {string} id ID of the finished download to remove. */
|
| - remove: chromeSendWithId('remove'),
|
| -
|
| - /** @param {string} id ID of the paused download to resume. */
|
| - resume: chromeSendWithId('resume'),
|
| -
|
| - /**
|
| - * @param {string} id ID of the dangerous download to save despite
|
| - * warnings.
|
| - */
|
| - saveDangerous: chromeSendWithId('saveDangerous'),
|
| -
|
| - /** @param {string} searchText What to search for. */
|
| - search: function(searchText) {
|
| - var searchTerms = ActionService.splitTerms(searchText);
|
| - var sameTerms = searchTerms.length == this.searchTerms_.length;
|
| -
|
| - for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
|
| - if (searchTerms[i] != this.searchTerms_[i])
|
| - sameTerms = false;
|
| - }
|
| -
|
| - if (sameTerms)
|
| - return;
|
| -
|
| - this.searchTerms_ = searchTerms;
|
| - this.loadMore();
|
| - },
|
| -
|
| - /**
|
| - * Shows the local folder a finished download resides in.
|
| - * @param {string} id ID of the download to show.
|
| - */
|
| - show: chromeSendWithId('show'),
|
| -
|
| - /** Undo download removal. */
|
| - undo: chrome.send.bind(chrome, 'undo'),
|
| - };
|
| -
|
| - cr.addSingletonGetter(ActionService);
|
| -
|
| - return {ActionService: ActionService};
|
| -});
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -cr.define('downloads', function() {
|
| - /**
|
| - * Explains why a download is in DANGEROUS state.
|
| - * @enum {string}
|
| - */
|
| - var DangerType = {
|
| - NOT_DANGEROUS: 'NOT_DANGEROUS',
|
| - DANGEROUS_FILE: 'DANGEROUS_FILE',
|
| - DANGEROUS_URL: 'DANGEROUS_URL',
|
| - DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
|
| - UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
|
| - DANGEROUS_HOST: 'DANGEROUS_HOST',
|
| - POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
|
| - };
|
| -
|
| - /**
|
| - * The states a download can be in. These correspond to states defined in
|
| - * DownloadsDOMHandler::CreateDownloadItemValue
|
| - * @enum {string}
|
| - */
|
| - var States = {
|
| - IN_PROGRESS: 'IN_PROGRESS',
|
| - CANCELLED: 'CANCELLED',
|
| - COMPLETE: 'COMPLETE',
|
| - PAUSED: 'PAUSED',
|
| - DANGEROUS: 'DANGEROUS',
|
| - INTERRUPTED: 'INTERRUPTED',
|
| - };
|
| -
|
| - return {
|
| - DangerType: DangerType,
|
| - States: States,
|
| - };
|
| -});
|
| -// Copyright 2014 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -// Action links are elements that are used to perform an in-page navigation or
|
| -// action (e.g. showing a dialog).
|
| -//
|
| -// They look like normal anchor (<a>) tags as their text color is blue. However,
|
| -// they're subtly different as they're not initially underlined (giving users a
|
| -// clue that underlined links navigate while action links don't).
|
| -//
|
| -// Action links look very similar to normal links when hovered (hand cursor,
|
| -// underlined). This gives the user an idea that clicking this link will do
|
| -// something similar to navigation but in the same page.
|
| -//
|
| -// They can be created in JavaScript like this:
|
| -//
|
| -// var link = document.createElement('a', 'action-link'); // Note second arg.
|
| -//
|
| -// or with a constructor like this:
|
| -//
|
| -// var link = new ActionLink();
|
| -//
|
| -// They can be used easily from HTML as well, like so:
|
| -//
|
| -// <a is="action-link">Click me!</a>
|
| -//
|
| -// NOTE: <action-link> and document.createElement('action-link') don't work.
|
| -
|
| -/**
|
| - * @constructor
|
| - * @extends {HTMLAnchorElement}
|
| - */
|
| -var ActionLink = document.registerElement('action-link', {
|
| - prototype: {
|
| - __proto__: HTMLAnchorElement.prototype,
|
| -
|
| - /** @this {ActionLink} */
|
| - createdCallback: function() {
|
| - // Action links can start disabled (e.g. <a is="action-link" disabled>).
|
| - this.tabIndex = this.disabled ? -1 : 0;
|
| -
|
| - if (!this.hasAttribute('role'))
|
| - this.setAttribute('role', 'link');
|
| -
|
| - this.addEventListener('keydown', function(e) {
|
| - if (!this.disabled && e.key == 'Enter' && !this.href) {
|
| - // Schedule a click asynchronously because other 'keydown' handlers
|
| - // may still run later (e.g. document.addEventListener('keydown')).
|
| - // Specifically options dialogs break when this timeout isn't here.
|
| - // NOTE: this affects the "trusted" state of the ensuing click. I
|
| - // haven't found anything that breaks because of this (yet).
|
| - window.setTimeout(this.click.bind(this), 0);
|
| - }
|
| - });
|
| -
|
| - function preventDefault(e) {
|
| - e.preventDefault();
|
| - }
|
| -
|
| - function removePreventDefault() {
|
| - document.removeEventListener('selectstart', preventDefault);
|
| - document.removeEventListener('mouseup', removePreventDefault);
|
| - }
|
| -
|
| - this.addEventListener('mousedown', function() {
|
| - // This handlers strives to match the behavior of <a href="...">.
|
| -
|
| - // While the mouse is down, prevent text selection from dragging.
|
| - document.addEventListener('selectstart', preventDefault);
|
| - document.addEventListener('mouseup', removePreventDefault);
|
| -
|
| - // If focus started via mouse press, don't show an outline.
|
| - if (document.activeElement != this)
|
| - this.classList.add('no-outline');
|
| - });
|
| -
|
| - this.addEventListener('blur', function() {
|
| - this.classList.remove('no-outline');
|
| - });
|
| - },
|
| -
|
| - /** @type {boolean} */
|
| - set disabled(disabled) {
|
| - if (disabled)
|
| - HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
|
| - else
|
| - HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
|
| - this.tabIndex = disabled ? -1 : 0;
|
| - },
|
| - get disabled() {
|
| - return this.hasAttribute('disabled');
|
| - },
|
| -
|
| - /** @override */
|
| - setAttribute: function(attr, val) {
|
| - if (attr.toLowerCase() == 'disabled')
|
| - this.disabled = true;
|
| - else
|
| - HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
|
| - },
|
| -
|
| - /** @override */
|
| - removeAttribute: function(attr) {
|
| - if (attr.toLowerCase() == 'disabled')
|
| - this.disabled = false;
|
| - else
|
| - HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments);
|
| - },
|
| - },
|
| -
|
| - extends: 'a',
|
| -});
|
| -(function() {
|
| -
|
| - // monostate data
|
| - var metaDatas = {};
|
| - var metaArrays = {};
|
| - var singleton = null;
|
| -
|
| - Polymer.IronMeta = Polymer({
|
| -
|
| - is: 'iron-meta',
|
| + is: 'iron-meta',
|
|
|
| properties: {
|
|
|
| @@ -4717,2407 +4165,1982 @@ Polymer({
|
|
|
| });
|
| /**
|
| + * The `iron-iconset-svg` element allows users to define their own icon sets
|
| + * that contain svg icons. The svg icon elements should be children of the
|
| + * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
|
| + *
|
| + * Using svg elements to create icons has a few advantages over traditional
|
| + * bitmap graphics like jpg or png. Icons that use svg are vector based so
|
| + * they are resolution independent and should look good on any device. They
|
| + * are stylable via css. Icons can be themed, colorized, and even animated.
|
| + *
|
| + * Example:
|
| + *
|
| + * <iron-iconset-svg name="my-svg-icons" size="24">
|
| + * <svg>
|
| + * <defs>
|
| + * <g id="shape">
|
| + * <rect x="12" y="0" width="12" height="24" />
|
| + * <circle cx="12" cy="12" r="12" />
|
| + * </g>
|
| + * </defs>
|
| + * </svg>
|
| + * </iron-iconset-svg>
|
| + *
|
| + * This will automatically register the icon set "my-svg-icons" to the iconset
|
| + * database. To use these icons from within another element, make a
|
| + * `iron-iconset` element and call the `byId` method
|
| + * to retrieve a given iconset. To apply a particular icon inside an
|
| + * element use the `applyIcon` method. For example:
|
| + *
|
| + * iconset.applyIcon(iconNode, 'car');
|
| + *
|
| + * @element iron-iconset-svg
|
| * @demo demo/index.html
|
| - * @polymerBehavior
|
| + * @implements {Polymer.Iconset}
|
| */
|
| - Polymer.IronControlState = {
|
| + Polymer({
|
| + is: 'iron-iconset-svg',
|
|
|
| properties: {
|
|
|
| /**
|
| - * If true, the element currently has focus.
|
| + * The name of the iconset.
|
| */
|
| - focused: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - readOnly: true,
|
| - reflectToAttribute: true
|
| - },
|
| + name: {
|
| + type: String,
|
| + observer: '_nameChanged'
|
| + },
|
|
|
| /**
|
| - * If true, the user cannot interact with this element.
|
| + * The size of an individual icon. Note that icons must be square.
|
| */
|
| - disabled: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - observer: '_disabledChanged',
|
| - reflectToAttribute: true
|
| - },
|
| -
|
| - _oldTabIndex: {
|
| - type: Number
|
| - },
|
| -
|
| - _boundFocusBlurHandler: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._focusBlurHandler.bind(this);
|
| - }
|
| + size: {
|
| + type: Number,
|
| + value: 24
|
| }
|
|
|
| },
|
|
|
| - observers: [
|
| - '_changedControlState(focused, disabled)'
|
| - ],
|
| -
|
| - ready: function() {
|
| - this.addEventListener('focus', this._boundFocusBlurHandler, true);
|
| - this.addEventListener('blur', this._boundFocusBlurHandler, true);
|
| + attached: function() {
|
| + this.style.display = 'none';
|
| },
|
|
|
| - _focusBlurHandler: function(event) {
|
| - // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
|
| - // eventually become `this` due to retargeting; if we are not in
|
| - // ShadowDOM land, `event.target` will eventually become `this` due
|
| - // to the second conditional which fires a synthetic event (that is also
|
| - // handled). In either case, we can disregard `event.path`.
|
| -
|
| - if (event.target === this) {
|
| - this._setFocused(event.type === 'focus');
|
| - } else if (!this.shadowRoot) {
|
| - var target = /** @type {Node} */(Polymer.dom(event).localTarget);
|
| - if (!this.isLightDescendant(target)) {
|
| - this.fire(event.type, {sourceEvent: event}, {
|
| - node: this,
|
| - bubbles: event.bubbles,
|
| - cancelable: event.cancelable
|
| - });
|
| - }
|
| - }
|
| + /**
|
| + * Construct an array of all icon names in this iconset.
|
| + *
|
| + * @return {!Array} Array of icon names.
|
| + */
|
| + getIconNames: function() {
|
| + this._icons = this._createIconMap();
|
| + return Object.keys(this._icons).map(function(n) {
|
| + return this.name + ':' + n;
|
| + }, this);
|
| },
|
|
|
| - _disabledChanged: function(disabled, old) {
|
| - this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
| - this.style.pointerEvents = disabled ? 'none' : '';
|
| - if (disabled) {
|
| - this._oldTabIndex = this.tabIndex;
|
| - this._setFocused(false);
|
| - this.tabIndex = -1;
|
| - this.blur();
|
| - } else if (this._oldTabIndex !== undefined) {
|
| - this.tabIndex = this._oldTabIndex;
|
| + /**
|
| + * Applies an icon to the given element.
|
| + *
|
| + * An svg icon is prepended to the element's shadowRoot if it exists,
|
| + * otherwise to the element itself.
|
| + *
|
| + * @method applyIcon
|
| + * @param {Element} element Element to which the icon is applied.
|
| + * @param {string} iconName Name of the icon to apply.
|
| + * @return {?Element} The svg element which renders the icon.
|
| + */
|
| + applyIcon: function(element, iconName) {
|
| + // insert svg element into shadow root, if it exists
|
| + element = element.root || element;
|
| + // Remove old svg element
|
| + this.removeIcon(element);
|
| + // install new svg element
|
| + var svg = this._cloneIcon(iconName);
|
| + if (svg) {
|
| + var pde = Polymer.dom(element);
|
| + pde.insertBefore(svg, pde.childNodes[0]);
|
| + return element._svgIcon = svg;
|
| }
|
| + return null;
|
| },
|
|
|
| - _changedControlState: function() {
|
| - // _controlStateChanged is abstract, follow-on behaviors may implement it
|
| - if (this._controlStateChanged) {
|
| - this._controlStateChanged();
|
| - }
|
| - }
|
| -
|
| - };
|
| -/**
|
| - * @demo demo/index.html
|
| - * @polymerBehavior Polymer.IronButtonState
|
| - */
|
| - Polymer.IronButtonStateImpl = {
|
| -
|
| - properties: {
|
| -
|
| - /**
|
| - * If true, the user is currently holding down the button.
|
| - */
|
| - pressed: {
|
| - type: Boolean,
|
| - readOnly: true,
|
| - value: false,
|
| - reflectToAttribute: true,
|
| - observer: '_pressedChanged'
|
| - },
|
| -
|
| - /**
|
| - * If true, the button toggles the active state with each tap or press
|
| - * of the spacebar.
|
| - */
|
| - toggles: {
|
| - type: Boolean,
|
| - value: false,
|
| - reflectToAttribute: true
|
| - },
|
| -
|
| - /**
|
| - * If true, the button is a toggle and is currently in the active state.
|
| - */
|
| - active: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - reflectToAttribute: true
|
| - },
|
| -
|
| - /**
|
| - * True if the element is currently being pressed by a "pointer," which
|
| - * is loosely defined as mouse or touch input (but specifically excluding
|
| - * keyboard input).
|
| - */
|
| - pointerDown: {
|
| - type: Boolean,
|
| - readOnly: true,
|
| - value: false
|
| - },
|
| -
|
| - /**
|
| - * True if the input device that caused the element to receive focus
|
| - * was a keyboard.
|
| - */
|
| - receivedFocusFromKeyboard: {
|
| - type: Boolean,
|
| - readOnly: true
|
| - },
|
| -
|
| - /**
|
| - * The aria attribute to be set if the button is a toggle and in the
|
| - * active state.
|
| - */
|
| - ariaActiveAttribute: {
|
| - type: String,
|
| - value: 'aria-pressed',
|
| - observer: '_ariaActiveAttributeChanged'
|
| + /**
|
| + * Remove an icon from the given element by undoing the changes effected
|
| + * by `applyIcon`.
|
| + *
|
| + * @param {Element} element The element from which the icon is removed.
|
| + */
|
| + removeIcon: function(element) {
|
| + // Remove old svg element
|
| + if (element._svgIcon) {
|
| + Polymer.dom(element).removeChild(element._svgIcon);
|
| + element._svgIcon = null;
|
| }
|
| },
|
|
|
| - listeners: {
|
| - down: '_downHandler',
|
| - up: '_upHandler',
|
| - tap: '_tapHandler'
|
| - },
|
| -
|
| - observers: [
|
| - '_detectKeyboardFocus(focused)',
|
| - '_activeChanged(active, ariaActiveAttribute)'
|
| - ],
|
| -
|
| - keyBindings: {
|
| - 'enter:keydown': '_asyncClick',
|
| - 'space:keydown': '_spaceKeyDownHandler',
|
| - 'space:keyup': '_spaceKeyUpHandler',
|
| - },
|
| -
|
| - _mouseEventRe: /^mouse/,
|
| -
|
| - _tapHandler: function() {
|
| - if (this.toggles) {
|
| - // a tap is needed to toggle the active state
|
| - this._userActivate(!this.active);
|
| - } else {
|
| - this.active = false;
|
| - }
|
| + /**
|
| + *
|
| + * When name is changed, register iconset metadata
|
| + *
|
| + */
|
| + _nameChanged: function() {
|
| + new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
|
| + this.async(function() {
|
| + this.fire('iron-iconset-added', this, {node: window});
|
| + });
|
| },
|
|
|
| - _detectKeyboardFocus: function(focused) {
|
| - this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
|
| + /**
|
| + * Create a map of child SVG elements by id.
|
| + *
|
| + * @return {!Object} Map of id's to SVG elements.
|
| + */
|
| + _createIconMap: function() {
|
| + // Objects chained to Object.prototype (`{}`) have members. Specifically,
|
| + // on FF there is a `watch` method that confuses the icon map, so we
|
| + // need to use a null-based object here.
|
| + var icons = Object.create(null);
|
| + Polymer.dom(this).querySelectorAll('[id]')
|
| + .forEach(function(icon) {
|
| + icons[icon.id] = icon;
|
| + });
|
| + return icons;
|
| },
|
|
|
| - // to emulate native checkbox, (de-)activations from a user interaction fire
|
| - // 'change' events
|
| - _userActivate: function(active) {
|
| - if (this.active !== active) {
|
| - this.active = active;
|
| - this.fire('change');
|
| - }
|
| - },
|
| -
|
| - _downHandler: function(event) {
|
| - this._setPointerDown(true);
|
| - this._setPressed(true);
|
| - this._setReceivedFocusFromKeyboard(false);
|
| - },
|
| -
|
| - _upHandler: function() {
|
| - this._setPointerDown(false);
|
| - this._setPressed(false);
|
| + /**
|
| + * Produce installable clone of the SVG element matching `id` in this
|
| + * iconset, or `undefined` if there is no matching element.
|
| + *
|
| + * @return {Element} Returns an installable clone of the SVG element
|
| + * matching `id`.
|
| + */
|
| + _cloneIcon: function(id) {
|
| + // create the icon map on-demand, since the iconset itself has no discrete
|
| + // signal to know when it's children are fully parsed
|
| + this._icons = this._icons || this._createIconMap();
|
| + return this._prepareSvgClone(this._icons[id], this.size);
|
| },
|
|
|
| /**
|
| - * @param {!KeyboardEvent} event .
|
| + * @param {Element} sourceSvg
|
| + * @param {number} size
|
| + * @return {Element}
|
| */
|
| - _spaceKeyDownHandler: function(event) {
|
| - var keyboardEvent = event.detail.keyboardEvent;
|
| - var target = Polymer.dom(keyboardEvent).localTarget;
|
| + _prepareSvgClone: function(sourceSvg, size) {
|
| + if (sourceSvg) {
|
| + var content = sourceSvg.cloneNode(true),
|
| + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
|
| + viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
|
| + svg.setAttribute('viewBox', viewBox);
|
| + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
| + // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/370136
|
| + // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
|
| + svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
|
| + svg.appendChild(content).removeAttribute('id');
|
| + return svg;
|
| + }
|
| + return null;
|
| + }
|
|
|
| - // Ignore the event if this is coming from a focused light child, since that
|
| - // element will deal with it.
|
| - if (this.isLightDescendant(/** @type {Node} */(target)))
|
| - return;
|
| + });
|
| +(function() {
|
| + 'use strict';
|
|
|
| - keyboardEvent.preventDefault();
|
| - keyboardEvent.stopImmediatePropagation();
|
| - this._setPressed(true);
|
| - },
|
| + /**
|
| + * Chrome uses an older version of DOM Level 3 Keyboard Events
|
| + *
|
| + * Most keys are labeled as text, but some are Unicode codepoints.
|
| + * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
|
| + */
|
| + var KEY_IDENTIFIER = {
|
| + 'U+0008': 'backspace',
|
| + 'U+0009': 'tab',
|
| + 'U+001B': 'esc',
|
| + 'U+0020': 'space',
|
| + 'U+007F': 'del'
|
| + };
|
|
|
| /**
|
| - * @param {!KeyboardEvent} event .
|
| + * Special table for KeyboardEvent.keyCode.
|
| + * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
|
| + * than that.
|
| + *
|
| + * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
|
| */
|
| - _spaceKeyUpHandler: function(event) {
|
| - var keyboardEvent = event.detail.keyboardEvent;
|
| - var target = Polymer.dom(keyboardEvent).localTarget;
|
| + var KEY_CODE = {
|
| + 8: 'backspace',
|
| + 9: 'tab',
|
| + 13: 'enter',
|
| + 27: 'esc',
|
| + 33: 'pageup',
|
| + 34: 'pagedown',
|
| + 35: 'end',
|
| + 36: 'home',
|
| + 32: 'space',
|
| + 37: 'left',
|
| + 38: 'up',
|
| + 39: 'right',
|
| + 40: 'down',
|
| + 46: 'del',
|
| + 106: '*'
|
| + };
|
|
|
| - // Ignore the event if this is coming from a focused light child, since that
|
| - // element will deal with it.
|
| - if (this.isLightDescendant(/** @type {Node} */(target)))
|
| - return;
|
| + /**
|
| + * MODIFIER_KEYS maps the short name for modifier keys used in a key
|
| + * combo string to the property name that references those same keys
|
| + * in a KeyboardEvent instance.
|
| + */
|
| + var MODIFIER_KEYS = {
|
| + 'shift': 'shiftKey',
|
| + 'ctrl': 'ctrlKey',
|
| + 'alt': 'altKey',
|
| + 'meta': 'metaKey'
|
| + };
|
|
|
| - if (this.pressed) {
|
| - this._asyncClick();
|
| - }
|
| - this._setPressed(false);
|
| - },
|
| + /**
|
| + * KeyboardEvent.key is mostly represented by printable character made by
|
| + * the keyboard, with unprintable keys labeled nicely.
|
| + *
|
| + * However, on OS X, Alt+char can make a Unicode character that follows an
|
| + * Apple-specific mapping. In this case, we fall back to .keyCode.
|
| + */
|
| + var KEY_CHAR = /[a-z0-9*]/;
|
|
|
| - // trigger click asynchronously, the asynchrony is useful to allow one
|
| - // event handler to unwind before triggering another event
|
| - _asyncClick: function() {
|
| - this.async(function() {
|
| - this.click();
|
| - }, 1);
|
| - },
|
| + /**
|
| + * Matches a keyIdentifier string.
|
| + */
|
| + var IDENT_CHAR = /U\+/;
|
|
|
| - // any of these changes are considered a change to button state
|
| + /**
|
| + * Matches arrow keys in Gecko 27.0+
|
| + */
|
| + var ARROW_KEY = /^arrow/;
|
|
|
| - _pressedChanged: function(pressed) {
|
| - this._changedButtonState();
|
| - },
|
| + /**
|
| + * Matches space keys everywhere (notably including IE10's exceptional name
|
| + * `spacebar`).
|
| + */
|
| + var SPACE_KEY = /^space(bar)?/;
|
|
|
| - _ariaActiveAttributeChanged: function(value, oldValue) {
|
| - if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
|
| - this.removeAttribute(oldValue);
|
| - }
|
| - },
|
| + /**
|
| + * Matches ESC key.
|
| + *
|
| + * Value from: http://w3c.github.io/uievents-key/#key-Escape
|
| + */
|
| + var ESC_KEY = /^escape$/;
|
|
|
| - _activeChanged: function(active, ariaActiveAttribute) {
|
| - if (this.toggles) {
|
| - this.setAttribute(this.ariaActiveAttribute,
|
| - active ? 'true' : 'false');
|
| - } else {
|
| - this.removeAttribute(this.ariaActiveAttribute);
|
| + /**
|
| + * Transforms the key.
|
| + * @param {string} key The KeyBoardEvent.key
|
| + * @param {Boolean} [noSpecialChars] Limits the transformation to
|
| + * alpha-numeric characters.
|
| + */
|
| + function transformKey(key, noSpecialChars) {
|
| + var validKey = '';
|
| + if (key) {
|
| + var lKey = key.toLowerCase();
|
| + if (lKey === ' ' || SPACE_KEY.test(lKey)) {
|
| + validKey = 'space';
|
| + } else if (ESC_KEY.test(lKey)) {
|
| + validKey = 'esc';
|
| + } else if (lKey.length == 1) {
|
| + if (!noSpecialChars || KEY_CHAR.test(lKey)) {
|
| + validKey = lKey;
|
| + }
|
| + } else if (ARROW_KEY.test(lKey)) {
|
| + validKey = lKey.replace('arrow', '');
|
| + } else if (lKey == 'multiply') {
|
| + // numpad '*' can map to Multiply on IE/Windows
|
| + validKey = '*';
|
| + } else {
|
| + validKey = lKey;
|
| + }
|
| }
|
| - this._changedButtonState();
|
| - },
|
| + return validKey;
|
| + }
|
|
|
| - _controlStateChanged: function() {
|
| - if (this.disabled) {
|
| - this._setPressed(false);
|
| - } else {
|
| - this._changedButtonState();
|
| + function transformKeyIdentifier(keyIdent) {
|
| + var validKey = '';
|
| + if (keyIdent) {
|
| + if (keyIdent in KEY_IDENTIFIER) {
|
| + validKey = KEY_IDENTIFIER[keyIdent];
|
| + } else if (IDENT_CHAR.test(keyIdent)) {
|
| + keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
|
| + validKey = String.fromCharCode(keyIdent).toLowerCase();
|
| + } else {
|
| + validKey = keyIdent.toLowerCase();
|
| + }
|
| }
|
| - },
|
| -
|
| - // provide hook for follow-on behaviors to react to button-state
|
| + return validKey;
|
| + }
|
|
|
| - _changedButtonState: function() {
|
| - if (this._buttonStateChanged) {
|
| - this._buttonStateChanged(); // abstract
|
| + function transformKeyCode(keyCode) {
|
| + var validKey = '';
|
| + if (Number(keyCode)) {
|
| + if (keyCode >= 65 && keyCode <= 90) {
|
| + // ascii a-z
|
| + // lowercase is 32 offset from uppercase
|
| + validKey = String.fromCharCode(32 + keyCode);
|
| + } else if (keyCode >= 112 && keyCode <= 123) {
|
| + // function keys f1-f12
|
| + validKey = 'f' + (keyCode - 112);
|
| + } else if (keyCode >= 48 && keyCode <= 57) {
|
| + // top 0-9 keys
|
| + validKey = String(keyCode - 48);
|
| + } else if (keyCode >= 96 && keyCode <= 105) {
|
| + // num pad 0-9
|
| + validKey = String(keyCode - 96);
|
| + } else {
|
| + validKey = KEY_CODE[keyCode];
|
| + }
|
| }
|
| + return validKey;
|
| }
|
|
|
| - };
|
| -
|
| - /** @polymerBehavior */
|
| - Polymer.IronButtonState = [
|
| - Polymer.IronA11yKeysBehavior,
|
| - Polymer.IronButtonStateImpl
|
| - ];
|
| -(function() {
|
| - var Utility = {
|
| - distance: function(x1, y1, x2, y2) {
|
| - var xDelta = (x1 - x2);
|
| - var yDelta = (y1 - y2);
|
| -
|
| - return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
|
| - },
|
| -
|
| - now: window.performance && window.performance.now ?
|
| - window.performance.now.bind(window.performance) : Date.now
|
| - };
|
| -
|
| /**
|
| - * @param {HTMLElement} element
|
| - * @constructor
|
| + * Calculates the normalized key for a KeyboardEvent.
|
| + * @param {KeyboardEvent} keyEvent
|
| + * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
|
| + * transformation to alpha-numeric chars. This is useful with key
|
| + * combinations like shift + 2, which on FF for MacOS produces
|
| + * keyEvent.key = @
|
| + * To get 2 returned, set noSpecialChars = true
|
| + * To get @ returned, set noSpecialChars = false
|
| */
|
| - function ElementMetrics(element) {
|
| - this.element = element;
|
| - this.width = this.boundingRect.width;
|
| - this.height = this.boundingRect.height;
|
| -
|
| - this.size = Math.max(this.width, this.height);
|
| + function normalizedKeyForEvent(keyEvent, noSpecialChars) {
|
| + // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
|
| + // .detail.key to support artificial keyboard events.
|
| + return transformKey(keyEvent.key, noSpecialChars) ||
|
| + transformKeyIdentifier(keyEvent.keyIdentifier) ||
|
| + transformKeyCode(keyEvent.keyCode) ||
|
| + transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
|
| }
|
|
|
| - ElementMetrics.prototype = {
|
| - get boundingRect () {
|
| - return this.element.getBoundingClientRect();
|
| - },
|
| -
|
| - furthestCornerDistanceFrom: function(x, y) {
|
| - var topLeft = Utility.distance(x, y, 0, 0);
|
| - var topRight = Utility.distance(x, y, this.width, 0);
|
| - var bottomLeft = Utility.distance(x, y, 0, this.height);
|
| - var bottomRight = Utility.distance(x, y, this.width, this.height);
|
| + function keyComboMatchesEvent(keyCombo, event) {
|
| + // For combos with modifiers we support only alpha-numeric keys
|
| + var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
|
| + return keyEvent === keyCombo.key &&
|
| + (!keyCombo.hasModifiers || (
|
| + !!event.shiftKey === !!keyCombo.shiftKey &&
|
| + !!event.ctrlKey === !!keyCombo.ctrlKey &&
|
| + !!event.altKey === !!keyCombo.altKey &&
|
| + !!event.metaKey === !!keyCombo.metaKey)
|
| + );
|
| + }
|
|
|
| - return Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
| + function parseKeyComboString(keyComboString) {
|
| + if (keyComboString.length === 1) {
|
| + return {
|
| + combo: keyComboString,
|
| + key: keyComboString,
|
| + event: 'keydown'
|
| + };
|
| }
|
| - };
|
| -
|
| - /**
|
| - * @param {HTMLElement} element
|
| - * @constructor
|
| - */
|
| - function Ripple(element) {
|
| - this.element = element;
|
| - this.color = window.getComputedStyle(element).color;
|
| + return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
|
| + var eventParts = keyComboPart.split(':');
|
| + var keyName = eventParts[0];
|
| + var event = eventParts[1];
|
|
|
| - this.wave = document.createElement('div');
|
| - this.waveContainer = document.createElement('div');
|
| - this.wave.style.backgroundColor = this.color;
|
| - this.wave.classList.add('wave');
|
| - this.waveContainer.classList.add('wave-container');
|
| - Polymer.dom(this.waveContainer).appendChild(this.wave);
|
| + if (keyName in MODIFIER_KEYS) {
|
| + parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
|
| + parsedKeyCombo.hasModifiers = true;
|
| + } else {
|
| + parsedKeyCombo.key = keyName;
|
| + parsedKeyCombo.event = event || 'keydown';
|
| + }
|
|
|
| - this.resetInteractionState();
|
| + return parsedKeyCombo;
|
| + }, {
|
| + combo: keyComboString.split(':').shift()
|
| + });
|
| }
|
|
|
| - Ripple.MAX_RADIUS = 300;
|
| + function parseEventString(eventString) {
|
| + return eventString.trim().split(' ').map(function(keyComboString) {
|
| + return parseKeyComboString(keyComboString);
|
| + });
|
| + }
|
|
|
| - Ripple.prototype = {
|
| - get recenters() {
|
| - return this.element.recenters;
|
| - },
|
| + /**
|
| + * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
|
| + * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
|
| + * The element takes care of browser differences with respect to Keyboard events
|
| + * and uses an expressive syntax to filter key presses.
|
| + *
|
| + * Use the `keyBindings` prototype property to express what combination of keys
|
| + * will trigger the callback. A key binding has the format
|
| + * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
|
| + * `"KEY:EVENT": "callback"` are valid as well). Some examples:
|
| + *
|
| + * keyBindings: {
|
| + * 'space': '_onKeydown', // same as 'space:keydown'
|
| + * 'shift+tab': '_onKeydown',
|
| + * 'enter:keypress': '_onKeypress',
|
| + * 'esc:keyup': '_onKeyup'
|
| + * }
|
| + *
|
| + * The callback will receive with an event containing the following information in `event.detail`:
|
| + *
|
| + * _onKeydown: function(event) {
|
| + * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
|
| + * console.log(event.detail.key); // KEY only, e.g. "tab"
|
| + * console.log(event.detail.event); // EVENT, e.g. "keydown"
|
| + * console.log(event.detail.keyboardEvent); // the original KeyboardEvent
|
| + * }
|
| + *
|
| + * Use the `keyEventTarget` attribute to set up event handlers on a specific
|
| + * node.
|
| + *
|
| + * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
|
| + * for an example.
|
| + *
|
| + * @demo demo/index.html
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.IronA11yKeysBehavior = {
|
| + properties: {
|
| + /**
|
| + * The EventTarget that will be firing relevant KeyboardEvents. Set it to
|
| + * `null` to disable the listeners.
|
| + * @type {?EventTarget}
|
| + */
|
| + keyEventTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this;
|
| + }
|
| + },
|
|
|
| - get center() {
|
| - return this.element.center;
|
| - },
|
| + /**
|
| + * If true, this property will cause the implementing element to
|
| + * automatically stop propagation on any handled KeyboardEvents.
|
| + */
|
| + stopKeyboardEventPropagation: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
|
|
| - get mouseDownElapsed() {
|
| - var elapsed;
|
| + _boundKeyHandlers: {
|
| + type: Array,
|
| + value: function() {
|
| + return [];
|
| + }
|
| + },
|
|
|
| - if (!this.mouseDownStart) {
|
| - return 0;
|
| + // We use this due to a limitation in IE10 where instances will have
|
| + // own properties of everything on the "prototype".
|
| + _imperativeKeyBindings: {
|
| + type: Object,
|
| + value: function() {
|
| + return {};
|
| + }
|
| }
|
| + },
|
|
|
| - elapsed = Utility.now() - this.mouseDownStart;
|
| + observers: [
|
| + '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
|
| + ],
|
|
|
| - if (this.mouseUpStart) {
|
| - elapsed -= this.mouseUpElapsed;
|
| - }
|
|
|
| - return elapsed;
|
| - },
|
| + /**
|
| + * To be used to express what combination of keys will trigger the relative
|
| + * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
|
| + * @type {Object}
|
| + */
|
| + keyBindings: {},
|
|
|
| - get mouseUpElapsed() {
|
| - return this.mouseUpStart ?
|
| - Utility.now () - this.mouseUpStart : 0;
|
| + registered: function() {
|
| + this._prepKeyBindings();
|
| },
|
|
|
| - get mouseDownElapsedSeconds() {
|
| - return this.mouseDownElapsed / 1000;
|
| + attached: function() {
|
| + this._listenKeyEventListeners();
|
| },
|
|
|
| - get mouseUpElapsedSeconds() {
|
| - return this.mouseUpElapsed / 1000;
|
| + detached: function() {
|
| + this._unlistenKeyEventListeners();
|
| },
|
|
|
| - get mouseInteractionSeconds() {
|
| - return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
|
| + /**
|
| + * Can be used to imperatively add a key binding to the implementing
|
| + * element. This is the imperative equivalent of declaring a keybinding
|
| + * in the `keyBindings` prototype property.
|
| + */
|
| + addOwnKeyBinding: function(eventString, handlerName) {
|
| + this._imperativeKeyBindings[eventString] = handlerName;
|
| + this._prepKeyBindings();
|
| + this._resetKeyEventListeners();
|
| },
|
|
|
| - get initialOpacity() {
|
| - return this.element.initialOpacity;
|
| + /**
|
| + * When called, will remove all imperatively-added key bindings.
|
| + */
|
| + removeOwnKeyBindings: function() {
|
| + this._imperativeKeyBindings = {};
|
| + this._prepKeyBindings();
|
| + this._resetKeyEventListeners();
|
| },
|
|
|
| - get opacityDecayVelocity() {
|
| - return this.element.opacityDecayVelocity;
|
| + /**
|
| + * Returns true if a keyboard event matches `eventString`.
|
| + *
|
| + * @param {KeyboardEvent} event
|
| + * @param {string} eventString
|
| + * @return {boolean}
|
| + */
|
| + keyboardEventMatchesKeys: function(event, eventString) {
|
| + var keyCombos = parseEventString(eventString);
|
| + for (var i = 0; i < keyCombos.length; ++i) {
|
| + if (keyComboMatchesEvent(keyCombos[i], event)) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| },
|
|
|
| - get radius() {
|
| - var width2 = this.containerMetrics.width * this.containerMetrics.width;
|
| - var height2 = this.containerMetrics.height * this.containerMetrics.height;
|
| - var waveRadius = Math.min(
|
| - Math.sqrt(width2 + height2),
|
| - Ripple.MAX_RADIUS
|
| - ) * 1.1 + 5;
|
| -
|
| - var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
|
| - var timeNow = this.mouseInteractionSeconds / duration;
|
| - var size = waveRadius * (1 - Math.pow(80, -timeNow));
|
| -
|
| - return Math.abs(size);
|
| - },
|
| + _collectKeyBindings: function() {
|
| + var keyBindings = this.behaviors.map(function(behavior) {
|
| + return behavior.keyBindings;
|
| + });
|
|
|
| - get opacity() {
|
| - if (!this.mouseUpStart) {
|
| - return this.initialOpacity;
|
| + if (keyBindings.indexOf(this.keyBindings) === -1) {
|
| + keyBindings.push(this.keyBindings);
|
| }
|
|
|
| - return Math.max(
|
| - 0,
|
| - this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
|
| - );
|
| + return keyBindings;
|
| },
|
|
|
| - get outerOpacity() {
|
| - // Linear increase in background opacity, capped at the opacity
|
| - // of the wavefront (waveOpacity).
|
| - var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
|
| - var waveOpacity = this.opacity;
|
| + _prepKeyBindings: function() {
|
| + this._keyBindings = {};
|
|
|
| - return Math.max(
|
| - 0,
|
| - Math.min(outerOpacity, waveOpacity)
|
| - );
|
| - },
|
| + this._collectKeyBindings().forEach(function(keyBindings) {
|
| + for (var eventString in keyBindings) {
|
| + this._addKeyBinding(eventString, keyBindings[eventString]);
|
| + }
|
| + }, this);
|
|
|
| - get isOpacityFullyDecayed() {
|
| - return this.opacity < 0.01 &&
|
| - this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
|
| - },
|
| + for (var eventString in this._imperativeKeyBindings) {
|
| + this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
|
| + }
|
|
|
| - get isRestingAtMaxRadius() {
|
| - return this.opacity >= this.initialOpacity &&
|
| - this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
|
| + // Give precedence to combos with modifiers to be checked first.
|
| + for (var eventName in this._keyBindings) {
|
| + this._keyBindings[eventName].sort(function (kb1, kb2) {
|
| + var b1 = kb1[0].hasModifiers;
|
| + var b2 = kb2[0].hasModifiers;
|
| + return (b1 === b2) ? 0 : b1 ? -1 : 1;
|
| + })
|
| + }
|
| },
|
|
|
| - get isAnimationComplete() {
|
| - return this.mouseUpStart ?
|
| - this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
|
| - },
|
| + _addKeyBinding: function(eventString, handlerName) {
|
| + parseEventString(eventString).forEach(function(keyCombo) {
|
| + this._keyBindings[keyCombo.event] =
|
| + this._keyBindings[keyCombo.event] || [];
|
|
|
| - get translationFraction() {
|
| - return Math.min(
|
| - 1,
|
| - this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
|
| - );
|
| + this._keyBindings[keyCombo.event].push([
|
| + keyCombo,
|
| + handlerName
|
| + ]);
|
| + }, this);
|
| },
|
|
|
| - get xNow() {
|
| - if (this.xEnd) {
|
| - return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
|
| - }
|
| -
|
| - return this.xStart;
|
| - },
|
| + _resetKeyEventListeners: function() {
|
| + this._unlistenKeyEventListeners();
|
|
|
| - get yNow() {
|
| - if (this.yEnd) {
|
| - return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
|
| + if (this.isAttached) {
|
| + this._listenKeyEventListeners();
|
| }
|
| -
|
| - return this.yStart;
|
| - },
|
| -
|
| - get isMouseDown() {
|
| - return this.mouseDownStart && !this.mouseUpStart;
|
| },
|
|
|
| - resetInteractionState: function() {
|
| - this.maxRadius = 0;
|
| - this.mouseDownStart = 0;
|
| - this.mouseUpStart = 0;
|
| + _listenKeyEventListeners: function() {
|
| + if (!this.keyEventTarget) {
|
| + return;
|
| + }
|
| + Object.keys(this._keyBindings).forEach(function(eventName) {
|
| + var keyBindings = this._keyBindings[eventName];
|
| + var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
|
|
|
| - this.xStart = 0;
|
| - this.yStart = 0;
|
| - this.xEnd = 0;
|
| - this.yEnd = 0;
|
| - this.slideDistance = 0;
|
| + this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
|
|
|
| - this.containerMetrics = new ElementMetrics(this.element);
|
| + this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
|
| + }, this);
|
| },
|
|
|
| - draw: function() {
|
| - var scale;
|
| - var translateString;
|
| - var dx;
|
| - var dy;
|
| -
|
| - this.wave.style.opacity = this.opacity;
|
| -
|
| - scale = this.radius / (this.containerMetrics.size / 2);
|
| - dx = this.xNow - (this.containerMetrics.width / 2);
|
| - dy = this.yNow - (this.containerMetrics.height / 2);
|
| + _unlistenKeyEventListeners: function() {
|
| + var keyHandlerTuple;
|
| + var keyEventTarget;
|
| + var eventName;
|
| + var boundKeyHandler;
|
|
|
| + while (this._boundKeyHandlers.length) {
|
| + // My kingdom for block-scope binding and destructuring assignment..
|
| + keyHandlerTuple = this._boundKeyHandlers.pop();
|
| + keyEventTarget = keyHandlerTuple[0];
|
| + eventName = keyHandlerTuple[1];
|
| + boundKeyHandler = keyHandlerTuple[2];
|
|
|
| - // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
|
| - // https://bugs.webkit.org/show_bug.cgi?id=98538
|
| - this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
|
| - this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
|
| - this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
|
| - this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
|
| + keyEventTarget.removeEventListener(eventName, boundKeyHandler);
|
| + }
|
| },
|
|
|
| - /** @param {Event=} event */
|
| - downAction: function(event) {
|
| - var xCenter = this.containerMetrics.width / 2;
|
| - var yCenter = this.containerMetrics.height / 2;
|
| -
|
| - this.resetInteractionState();
|
| - this.mouseDownStart = Utility.now();
|
| + _onKeyBindingEvent: function(keyBindings, event) {
|
| + if (this.stopKeyboardEventPropagation) {
|
| + event.stopPropagation();
|
| + }
|
|
|
| - if (this.center) {
|
| - this.xStart = xCenter;
|
| - this.yStart = yCenter;
|
| - this.slideDistance = Utility.distance(
|
| - this.xStart, this.yStart, this.xEnd, this.yEnd
|
| - );
|
| - } else {
|
| - this.xStart = event ?
|
| - event.detail.x - this.containerMetrics.boundingRect.left :
|
| - this.containerMetrics.width / 2;
|
| - this.yStart = event ?
|
| - event.detail.y - this.containerMetrics.boundingRect.top :
|
| - this.containerMetrics.height / 2;
|
| + // if event has been already prevented, don't do anything
|
| + if (event.defaultPrevented) {
|
| + return;
|
| }
|
|
|
| - if (this.recenters) {
|
| - this.xEnd = xCenter;
|
| - this.yEnd = yCenter;
|
| - this.slideDistance = Utility.distance(
|
| - this.xStart, this.yStart, this.xEnd, this.yEnd
|
| - );
|
| + for (var i = 0; i < keyBindings.length; i++) {
|
| + var keyCombo = keyBindings[i][0];
|
| + var handlerName = keyBindings[i][1];
|
| + if (keyComboMatchesEvent(keyCombo, event)) {
|
| + this._triggerKeyHandler(keyCombo, handlerName, event);
|
| + // exit the loop if eventDefault was prevented
|
| + if (event.defaultPrevented) {
|
| + return;
|
| + }
|
| + }
|
| }
|
| + },
|
|
|
| - this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
|
| - this.xStart,
|
| - this.yStart
|
| - );
|
| + _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
|
| + var detail = Object.create(keyCombo);
|
| + detail.keyboardEvent = keyboardEvent;
|
| + var event = new CustomEvent(keyCombo.event, {
|
| + detail: detail,
|
| + cancelable: true
|
| + });
|
| + this[handlerName].call(this, event);
|
| + if (event.defaultPrevented) {
|
| + keyboardEvent.preventDefault();
|
| + }
|
| + }
|
| + };
|
| + })();
|
| +/**
|
| + * @demo demo/index.html
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.IronControlState = {
|
|
|
| - this.waveContainer.style.top =
|
| - (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
|
| - this.waveContainer.style.left =
|
| - (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
|
| + properties: {
|
|
|
| - this.waveContainer.style.width = this.containerMetrics.size + 'px';
|
| - this.waveContainer.style.height = this.containerMetrics.size + 'px';
|
| + /**
|
| + * If true, the element currently has focus.
|
| + */
|
| + focused: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + readOnly: true,
|
| + reflectToAttribute: true
|
| },
|
|
|
| - /** @param {Event=} event */
|
| - upAction: function(event) {
|
| - if (!this.isMouseDown) {
|
| - return;
|
| - }
|
| + /**
|
| + * If true, the user cannot interact with this element.
|
| + */
|
| + disabled: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + observer: '_disabledChanged',
|
| + reflectToAttribute: true
|
| + },
|
|
|
| - this.mouseUpStart = Utility.now();
|
| + _oldTabIndex: {
|
| + type: Number
|
| },
|
|
|
| - remove: function() {
|
| - Polymer.dom(this.waveContainer.parentNode).removeChild(
|
| - this.waveContainer
|
| - );
|
| + _boundFocusBlurHandler: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._focusBlurHandler.bind(this);
|
| + }
|
| }
|
| - };
|
|
|
| - Polymer({
|
| - is: 'paper-ripple',
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.IronA11yKeysBehavior
|
| - ],
|
| + observers: [
|
| + '_changedControlState(focused, disabled)'
|
| + ],
|
|
|
| - properties: {
|
| - /**
|
| - * The initial opacity set on the wave.
|
| - *
|
| - * @attribute initialOpacity
|
| - * @type number
|
| - * @default 0.25
|
| - */
|
| - initialOpacity: {
|
| - type: Number,
|
| - value: 0.25
|
| - },
|
| + ready: function() {
|
| + this.addEventListener('focus', this._boundFocusBlurHandler, true);
|
| + this.addEventListener('blur', this._boundFocusBlurHandler, true);
|
| + },
|
|
|
| - /**
|
| - * How fast (opacity per second) the wave fades out.
|
| - *
|
| - * @attribute opacityDecayVelocity
|
| - * @type number
|
| - * @default 0.8
|
| - */
|
| - opacityDecayVelocity: {
|
| - type: Number,
|
| - value: 0.8
|
| - },
|
| -
|
| - /**
|
| - * If true, ripples will exhibit a gravitational pull towards
|
| - * the center of their container as they fade away.
|
| - *
|
| - * @attribute recenters
|
| - * @type boolean
|
| - * @default false
|
| - */
|
| - recenters: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| -
|
| - /**
|
| - * If true, ripples will center inside its container
|
| - *
|
| - * @attribute recenters
|
| - * @type boolean
|
| - * @default false
|
| - */
|
| - center: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| -
|
| - /**
|
| - * A list of the visual ripples.
|
| - *
|
| - * @attribute ripples
|
| - * @type Array
|
| - * @default []
|
| - */
|
| - ripples: {
|
| - type: Array,
|
| - value: function() {
|
| - return [];
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * True when there are visible ripples animating within the
|
| - * element.
|
| - */
|
| - animating: {
|
| - type: Boolean,
|
| - readOnly: true,
|
| - reflectToAttribute: true,
|
| - value: false
|
| - },
|
| -
|
| - /**
|
| - * If true, the ripple will remain in the "down" state until `holdDown`
|
| - * is set to false again.
|
| - */
|
| - holdDown: {
|
| - type: Boolean,
|
| - value: false,
|
| - observer: '_holdDownChanged'
|
| - },
|
| -
|
| - /**
|
| - * If true, the ripple will not generate a ripple effect
|
| - * via pointer interaction.
|
| - * Calling ripple's imperative api like `simulatedRipple` will
|
| - * still generate the ripple effect.
|
| - */
|
| - noink: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| -
|
| - _animating: {
|
| - type: Boolean
|
| - },
|
| + _focusBlurHandler: function(event) {
|
| + // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
|
| + // eventually become `this` due to retargeting; if we are not in
|
| + // ShadowDOM land, `event.target` will eventually become `this` due
|
| + // to the second conditional which fires a synthetic event (that is also
|
| + // handled). In either case, we can disregard `event.path`.
|
|
|
| - _boundAnimate: {
|
| - type: Function,
|
| - value: function() {
|
| - return this.animate.bind(this);
|
| - }
|
| + if (event.target === this) {
|
| + this._setFocused(event.type === 'focus');
|
| + } else if (!this.shadowRoot) {
|
| + var target = /** @type {Node} */(Polymer.dom(event).localTarget);
|
| + if (!this.isLightDescendant(target)) {
|
| + this.fire(event.type, {sourceEvent: event}, {
|
| + node: this,
|
| + bubbles: event.bubbles,
|
| + cancelable: event.cancelable
|
| + });
|
| }
|
| - },
|
| -
|
| - get target () {
|
| - return this.keyEventTarget;
|
| - },
|
| + }
|
| + },
|
|
|
| - keyBindings: {
|
| - 'enter:keydown': '_onEnterKeydown',
|
| - 'space:keydown': '_onSpaceKeydown',
|
| - 'space:keyup': '_onSpaceKeyup'
|
| - },
|
| + _disabledChanged: function(disabled, old) {
|
| + this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
| + this.style.pointerEvents = disabled ? 'none' : '';
|
| + if (disabled) {
|
| + this._oldTabIndex = this.tabIndex;
|
| + this._setFocused(false);
|
| + this.tabIndex = -1;
|
| + this.blur();
|
| + } else if (this._oldTabIndex !== undefined) {
|
| + this.tabIndex = this._oldTabIndex;
|
| + }
|
| + },
|
|
|
| - attached: function() {
|
| - // Set up a11yKeysBehavior to listen to key events on the target,
|
| - // so that space and enter activate the ripple even if the target doesn't
|
| - // handle key events. The key handlers deal with `noink` themselves.
|
| - if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
|
| - this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
|
| - } else {
|
| - this.keyEventTarget = this.parentNode;
|
| - }
|
| - var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
|
| - this.listen(keyEventTarget, 'up', 'uiUpAction');
|
| - this.listen(keyEventTarget, 'down', 'uiDownAction');
|
| - },
|
| + _changedControlState: function() {
|
| + // _controlStateChanged is abstract, follow-on behaviors may implement it
|
| + if (this._controlStateChanged) {
|
| + this._controlStateChanged();
|
| + }
|
| + }
|
|
|
| - detached: function() {
|
| - this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
|
| - this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
|
| - this.keyEventTarget = null;
|
| - },
|
| + };
|
| +/**
|
| + * @demo demo/index.html
|
| + * @polymerBehavior Polymer.IronButtonState
|
| + */
|
| + Polymer.IronButtonStateImpl = {
|
|
|
| - get shouldKeepAnimating () {
|
| - for (var index = 0; index < this.ripples.length; ++index) {
|
| - if (!this.ripples[index].isAnimationComplete) {
|
| - return true;
|
| - }
|
| - }
|
| + properties: {
|
|
|
| - return false;
|
| + /**
|
| + * If true, the user is currently holding down the button.
|
| + */
|
| + pressed: {
|
| + type: Boolean,
|
| + readOnly: true,
|
| + value: false,
|
| + reflectToAttribute: true,
|
| + observer: '_pressedChanged'
|
| },
|
|
|
| - simulatedRipple: function() {
|
| - this.downAction(null);
|
| -
|
| - // Please see polymer/polymer#1305
|
| - this.async(function() {
|
| - this.upAction();
|
| - }, 1);
|
| + /**
|
| + * If true, the button toggles the active state with each tap or press
|
| + * of the spacebar.
|
| + */
|
| + toggles: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true
|
| },
|
|
|
| /**
|
| - * Provokes a ripple down effect via a UI event,
|
| - * respecting the `noink` property.
|
| - * @param {Event=} event
|
| + * If true, the button is a toggle and is currently in the active state.
|
| */
|
| - uiDownAction: function(event) {
|
| - if (!this.noink) {
|
| - this.downAction(event);
|
| - }
|
| + active: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + reflectToAttribute: true
|
| },
|
|
|
| /**
|
| - * Provokes a ripple down effect via a UI event,
|
| - * *not* respecting the `noink` property.
|
| - * @param {Event=} event
|
| + * True if the element is currently being pressed by a "pointer," which
|
| + * is loosely defined as mouse or touch input (but specifically excluding
|
| + * keyboard input).
|
| */
|
| - downAction: function(event) {
|
| - if (this.holdDown && this.ripples.length > 0) {
|
| - return;
|
| - }
|
| -
|
| - var ripple = this.addRipple();
|
| -
|
| - ripple.downAction(event);
|
| -
|
| - if (!this._animating) {
|
| - this._animating = true;
|
| - this.animate();
|
| - }
|
| + pointerDown: {
|
| + type: Boolean,
|
| + readOnly: true,
|
| + value: false
|
| },
|
|
|
| /**
|
| - * Provokes a ripple up effect via a UI event,
|
| - * respecting the `noink` property.
|
| - * @param {Event=} event
|
| + * True if the input device that caused the element to receive focus
|
| + * was a keyboard.
|
| */
|
| - uiUpAction: function(event) {
|
| - if (!this.noink) {
|
| - this.upAction(event);
|
| - }
|
| + receivedFocusFromKeyboard: {
|
| + type: Boolean,
|
| + readOnly: true
|
| },
|
|
|
| /**
|
| - * Provokes a ripple up effect via a UI event,
|
| - * *not* respecting the `noink` property.
|
| - * @param {Event=} event
|
| + * The aria attribute to be set if the button is a toggle and in the
|
| + * active state.
|
| */
|
| - upAction: function(event) {
|
| - if (this.holdDown) {
|
| - return;
|
| - }
|
| + ariaActiveAttribute: {
|
| + type: String,
|
| + value: 'aria-pressed',
|
| + observer: '_ariaActiveAttributeChanged'
|
| + }
|
| + },
|
|
|
| - this.ripples.forEach(function(ripple) {
|
| - ripple.upAction(event);
|
| - });
|
| + listeners: {
|
| + down: '_downHandler',
|
| + up: '_upHandler',
|
| + tap: '_tapHandler'
|
| + },
|
|
|
| - this._animating = true;
|
| - this.animate();
|
| - },
|
| + observers: [
|
| + '_detectKeyboardFocus(focused)',
|
| + '_activeChanged(active, ariaActiveAttribute)'
|
| + ],
|
|
|
| - onAnimationComplete: function() {
|
| - this._animating = false;
|
| - this.$.background.style.backgroundColor = null;
|
| - this.fire('transitionend');
|
| - },
|
| + keyBindings: {
|
| + 'enter:keydown': '_asyncClick',
|
| + 'space:keydown': '_spaceKeyDownHandler',
|
| + 'space:keyup': '_spaceKeyUpHandler',
|
| + },
|
|
|
| - addRipple: function() {
|
| - var ripple = new Ripple(this);
|
| + _mouseEventRe: /^mouse/,
|
|
|
| - Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
|
| - this.$.background.style.backgroundColor = ripple.color;
|
| - this.ripples.push(ripple);
|
| + _tapHandler: function() {
|
| + if (this.toggles) {
|
| + // a tap is needed to toggle the active state
|
| + this._userActivate(!this.active);
|
| + } else {
|
| + this.active = false;
|
| + }
|
| + },
|
|
|
| - this._setAnimating(true);
|
| + _detectKeyboardFocus: function(focused) {
|
| + this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
|
| + },
|
|
|
| - return ripple;
|
| - },
|
| + // to emulate native checkbox, (de-)activations from a user interaction fire
|
| + // 'change' events
|
| + _userActivate: function(active) {
|
| + if (this.active !== active) {
|
| + this.active = active;
|
| + this.fire('change');
|
| + }
|
| + },
|
|
|
| - removeRipple: function(ripple) {
|
| - var rippleIndex = this.ripples.indexOf(ripple);
|
| + _downHandler: function(event) {
|
| + this._setPointerDown(true);
|
| + this._setPressed(true);
|
| + this._setReceivedFocusFromKeyboard(false);
|
| + },
|
|
|
| - if (rippleIndex < 0) {
|
| - return;
|
| - }
|
| + _upHandler: function() {
|
| + this._setPointerDown(false);
|
| + this._setPressed(false);
|
| + },
|
|
|
| - this.ripples.splice(rippleIndex, 1);
|
| + /**
|
| + * @param {!KeyboardEvent} event .
|
| + */
|
| + _spaceKeyDownHandler: function(event) {
|
| + var keyboardEvent = event.detail.keyboardEvent;
|
| + var target = Polymer.dom(keyboardEvent).localTarget;
|
|
|
| - ripple.remove();
|
| + // Ignore the event if this is coming from a focused light child, since that
|
| + // element will deal with it.
|
| + if (this.isLightDescendant(/** @type {Node} */(target)))
|
| + return;
|
|
|
| - if (!this.ripples.length) {
|
| - this._setAnimating(false);
|
| - }
|
| - },
|
| + keyboardEvent.preventDefault();
|
| + keyboardEvent.stopImmediatePropagation();
|
| + this._setPressed(true);
|
| + },
|
|
|
| - animate: function() {
|
| - if (!this._animating) {
|
| - return;
|
| - }
|
| - var index;
|
| - var ripple;
|
| + /**
|
| + * @param {!KeyboardEvent} event .
|
| + */
|
| + _spaceKeyUpHandler: function(event) {
|
| + var keyboardEvent = event.detail.keyboardEvent;
|
| + var target = Polymer.dom(keyboardEvent).localTarget;
|
|
|
| - for (index = 0; index < this.ripples.length; ++index) {
|
| - ripple = this.ripples[index];
|
| + // Ignore the event if this is coming from a focused light child, since that
|
| + // element will deal with it.
|
| + if (this.isLightDescendant(/** @type {Node} */(target)))
|
| + return;
|
|
|
| - ripple.draw();
|
| + if (this.pressed) {
|
| + this._asyncClick();
|
| + }
|
| + this._setPressed(false);
|
| + },
|
|
|
| - this.$.background.style.opacity = ripple.outerOpacity;
|
| + // trigger click asynchronously, the asynchrony is useful to allow one
|
| + // event handler to unwind before triggering another event
|
| + _asyncClick: function() {
|
| + this.async(function() {
|
| + this.click();
|
| + }, 1);
|
| + },
|
|
|
| - if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
|
| - this.removeRipple(ripple);
|
| - }
|
| - }
|
| + // any of these changes are considered a change to button state
|
|
|
| - if (!this.shouldKeepAnimating && this.ripples.length === 0) {
|
| - this.onAnimationComplete();
|
| - } else {
|
| - window.requestAnimationFrame(this._boundAnimate);
|
| - }
|
| - },
|
| + _pressedChanged: function(pressed) {
|
| + this._changedButtonState();
|
| + },
|
|
|
| - _onEnterKeydown: function() {
|
| - this.uiDownAction();
|
| - this.async(this.uiUpAction, 1);
|
| - },
|
| + _ariaActiveAttributeChanged: function(value, oldValue) {
|
| + if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
|
| + this.removeAttribute(oldValue);
|
| + }
|
| + },
|
|
|
| - _onSpaceKeydown: function() {
|
| - this.uiDownAction();
|
| - },
|
| + _activeChanged: function(active, ariaActiveAttribute) {
|
| + if (this.toggles) {
|
| + this.setAttribute(this.ariaActiveAttribute,
|
| + active ? 'true' : 'false');
|
| + } else {
|
| + this.removeAttribute(this.ariaActiveAttribute);
|
| + }
|
| + this._changedButtonState();
|
| + },
|
|
|
| - _onSpaceKeyup: function() {
|
| - this.uiUpAction();
|
| - },
|
| + _controlStateChanged: function() {
|
| + if (this.disabled) {
|
| + this._setPressed(false);
|
| + } else {
|
| + this._changedButtonState();
|
| + }
|
| + },
|
|
|
| - // note: holdDown does not respect noink since it can be a focus based
|
| - // effect.
|
| - _holdDownChanged: function(newVal, oldVal) {
|
| - if (oldVal === undefined) {
|
| - return;
|
| - }
|
| - if (newVal) {
|
| - this.downAction();
|
| - } else {
|
| - this.upAction();
|
| - }
|
| + // provide hook for follow-on behaviors to react to button-state
|
| +
|
| + _changedButtonState: function() {
|
| + if (this._buttonStateChanged) {
|
| + this._buttonStateChanged(); // abstract
|
| }
|
| + }
|
|
|
| - /**
|
| - Fired when the animation finishes.
|
| - This is useful if you want to wait until
|
| - the ripple animation finishes to perform some action.
|
| + };
|
|
|
| - @event transitionend
|
| - @param {{node: Object}} detail Contains the animated node.
|
| - */
|
| - });
|
| - })();
|
| -/**
|
| - * `Polymer.PaperRippleBehavior` dynamically implements a ripple
|
| - * when the element has focus via pointer or keyboard.
|
| - *
|
| - * NOTE: This behavior is intended to be used in conjunction with and after
|
| - * `Polymer.IronButtonState` and `Polymer.IronControlState`.
|
| - *
|
| - * @polymerBehavior Polymer.PaperRippleBehavior
|
| - */
|
| - Polymer.PaperRippleBehavior = {
|
| - properties: {
|
| - /**
|
| - * If true, the element will not produce a ripple effect when interacted
|
| - * with via the pointer.
|
| - */
|
| - noink: {
|
| - type: Boolean,
|
| - observer: '_noinkChanged'
|
| + /** @polymerBehavior */
|
| + Polymer.IronButtonState = [
|
| + Polymer.IronA11yKeysBehavior,
|
| + Polymer.IronButtonStateImpl
|
| + ];
|
| +(function() {
|
| + var Utility = {
|
| + distance: function(x1, y1, x2, y2) {
|
| + var xDelta = (x1 - x2);
|
| + var yDelta = (y1 - y2);
|
| +
|
| + return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
|
| },
|
|
|
| - /**
|
| - * @type {Element|undefined}
|
| - */
|
| - _rippleContainer: {
|
| - type: Object,
|
| - }
|
| - },
|
| + now: window.performance && window.performance.now ?
|
| + window.performance.now.bind(window.performance) : Date.now
|
| + };
|
|
|
| /**
|
| - * Ensures a `<paper-ripple>` element is available when the element is
|
| - * focused.
|
| + * @param {HTMLElement} element
|
| + * @constructor
|
| */
|
| - _buttonStateChanged: function() {
|
| - if (this.focused) {
|
| - this.ensureRipple();
|
| - }
|
| - },
|
| + function ElementMetrics(element) {
|
| + this.element = element;
|
| + this.width = this.boundingRect.width;
|
| + this.height = this.boundingRect.height;
|
|
|
| - /**
|
| - * In addition to the functionality provided in `IronButtonState`, ensures
|
| - * a ripple effect is created when the element is in a `pressed` state.
|
| - */
|
| - _downHandler: function(event) {
|
| - Polymer.IronButtonStateImpl._downHandler.call(this, event);
|
| - if (this.pressed) {
|
| - this.ensureRipple(event);
|
| - }
|
| - },
|
| + this.size = Math.max(this.width, this.height);
|
| + }
|
|
|
| - /**
|
| - * Ensures this element contains a ripple effect. For startup efficiency
|
| - * the ripple effect is dynamically on demand when needed.
|
| - * @param {!Event=} optTriggeringEvent (optional) event that triggered the
|
| - * ripple.
|
| - */
|
| - ensureRipple: function(optTriggeringEvent) {
|
| - if (!this.hasRipple()) {
|
| - this._ripple = this._createRipple();
|
| - this._ripple.noink = this.noink;
|
| - var rippleContainer = this._rippleContainer || this.root;
|
| - if (rippleContainer) {
|
| - Polymer.dom(rippleContainer).appendChild(this._ripple);
|
| - }
|
| - if (optTriggeringEvent) {
|
| - // Check if the event happened inside of the ripple container
|
| - // Fall back to host instead of the root because distributed text
|
| - // nodes are not valid event targets
|
| - var domContainer = Polymer.dom(this._rippleContainer || this);
|
| - var target = Polymer.dom(optTriggeringEvent).rootTarget;
|
| - if (domContainer.deepContains( /** @type {Node} */(target))) {
|
| - this._ripple.uiDownAction(optTriggeringEvent);
|
| - }
|
| - }
|
| - }
|
| - },
|
| + ElementMetrics.prototype = {
|
| + get boundingRect () {
|
| + return this.element.getBoundingClientRect();
|
| + },
|
|
|
| - /**
|
| - * Returns the `<paper-ripple>` element used by this element to create
|
| - * ripple effects. The element's ripple is created on demand, when
|
| - * necessary, and calling this method will force the
|
| - * ripple to be created.
|
| - */
|
| - getRipple: function() {
|
| - this.ensureRipple();
|
| - return this._ripple;
|
| - },
|
| + furthestCornerDistanceFrom: function(x, y) {
|
| + var topLeft = Utility.distance(x, y, 0, 0);
|
| + var topRight = Utility.distance(x, y, this.width, 0);
|
| + var bottomLeft = Utility.distance(x, y, 0, this.height);
|
| + var bottomRight = Utility.distance(x, y, this.width, this.height);
|
|
|
| - /**
|
| - * Returns true if this element currently contains a ripple effect.
|
| - * @return {boolean}
|
| - */
|
| - hasRipple: function() {
|
| - return Boolean(this._ripple);
|
| - },
|
| + return Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
| + }
|
| + };
|
|
|
| /**
|
| - * Create the element's ripple effect via creating a `<paper-ripple>`.
|
| - * Override this method to customize the ripple element.
|
| - * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
|
| + * @param {HTMLElement} element
|
| + * @constructor
|
| */
|
| - _createRipple: function() {
|
| - return /** @type {!PaperRippleElement} */ (
|
| - document.createElement('paper-ripple'));
|
| - },
|
| -
|
| - _noinkChanged: function(noink) {
|
| - if (this.hasRipple()) {
|
| - this._ripple.noink = noink;
|
| - }
|
| - }
|
| - };
|
| -/** @polymerBehavior Polymer.PaperButtonBehavior */
|
| - Polymer.PaperButtonBehaviorImpl = {
|
| - properties: {
|
| - /**
|
| - * The z-depth of this element, from 0-5. Setting to 0 will remove the
|
| - * shadow, and each increasing number greater than 0 will be "deeper"
|
| - * than the last.
|
| - *
|
| - * @attribute elevation
|
| - * @type number
|
| - * @default 1
|
| - */
|
| - elevation: {
|
| - type: Number,
|
| - reflectToAttribute: true,
|
| - readOnly: true
|
| - }
|
| - },
|
| + function Ripple(element) {
|
| + this.element = element;
|
| + this.color = window.getComputedStyle(element).color;
|
|
|
| - observers: [
|
| - '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',
|
| - '_computeKeyboardClass(receivedFocusFromKeyboard)'
|
| - ],
|
| + this.wave = document.createElement('div');
|
| + this.waveContainer = document.createElement('div');
|
| + this.wave.style.backgroundColor = this.color;
|
| + this.wave.classList.add('wave');
|
| + this.waveContainer.classList.add('wave-container');
|
| + Polymer.dom(this.waveContainer).appendChild(this.wave);
|
|
|
| - hostAttributes: {
|
| - role: 'button',
|
| - tabindex: '0',
|
| - animated: true
|
| - },
|
| + this.resetInteractionState();
|
| + }
|
|
|
| - _calculateElevation: function() {
|
| - var e = 1;
|
| - if (this.disabled) {
|
| - e = 0;
|
| - } else if (this.active || this.pressed) {
|
| - e = 4;
|
| - } else if (this.receivedFocusFromKeyboard) {
|
| - e = 3;
|
| - }
|
| - this._setElevation(e);
|
| - },
|
| + Ripple.MAX_RADIUS = 300;
|
|
|
| - _computeKeyboardClass: function(receivedFocusFromKeyboard) {
|
| - this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
|
| - },
|
| + Ripple.prototype = {
|
| + get recenters() {
|
| + return this.element.recenters;
|
| + },
|
|
|
| - /**
|
| - * In addition to `IronButtonState` behavior, when space key goes down,
|
| - * create a ripple down effect.
|
| - *
|
| - * @param {!KeyboardEvent} event .
|
| - */
|
| - _spaceKeyDownHandler: function(event) {
|
| - Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
|
| - // Ensure that there is at most one ripple when the space key is held down.
|
| - if (this.hasRipple() && this.getRipple().ripples.length < 1) {
|
| - this._ripple.uiDownAction();
|
| - }
|
| - },
|
| + get center() {
|
| + return this.element.center;
|
| + },
|
|
|
| - /**
|
| - * In addition to `IronButtonState` behavior, when space key goes up,
|
| - * create a ripple up effect.
|
| - *
|
| - * @param {!KeyboardEvent} event .
|
| - */
|
| - _spaceKeyUpHandler: function(event) {
|
| - Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
|
| - if (this.hasRipple()) {
|
| - this._ripple.uiUpAction();
|
| - }
|
| - }
|
| - };
|
| + get mouseDownElapsed() {
|
| + var elapsed;
|
|
|
| - /** @polymerBehavior */
|
| - Polymer.PaperButtonBehavior = [
|
| - Polymer.IronButtonState,
|
| - Polymer.IronControlState,
|
| - Polymer.PaperRippleBehavior,
|
| - Polymer.PaperButtonBehaviorImpl
|
| - ];
|
| -Polymer({
|
| - is: 'paper-button',
|
| + if (!this.mouseDownStart) {
|
| + return 0;
|
| + }
|
|
|
| - behaviors: [
|
| - Polymer.PaperButtonBehavior
|
| - ],
|
| + elapsed = Utility.now() - this.mouseDownStart;
|
|
|
| - properties: {
|
| - /**
|
| - * If true, the button should be styled with a shadow.
|
| - */
|
| - raised: {
|
| - type: Boolean,
|
| - reflectToAttribute: true,
|
| - value: false,
|
| - observer: '_calculateElevation'
|
| + if (this.mouseUpStart) {
|
| + elapsed -= this.mouseUpElapsed;
|
| }
|
| +
|
| + return elapsed;
|
| },
|
|
|
| - _calculateElevation: function() {
|
| - if (!this.raised) {
|
| - this._setElevation(0);
|
| - } else {
|
| - Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
|
| - }
|
| - }
|
| + get mouseUpElapsed() {
|
| + return this.mouseUpStart ?
|
| + Utility.now () - this.mouseUpStart : 0;
|
| + },
|
|
|
| - /**
|
| - Fired when the animation finishes.
|
| - This is useful if you want to wait until
|
| - the ripple animation finishes to perform some action.
|
| + get mouseDownElapsedSeconds() {
|
| + return this.mouseDownElapsed / 1000;
|
| + },
|
|
|
| - @event transitionend
|
| - Event param: {{node: Object}} detail Contains the animated node.
|
| - */
|
| - });
|
| -Polymer({
|
| - is: 'paper-icon-button-light',
|
| - extends: 'button',
|
| + get mouseUpElapsedSeconds() {
|
| + return this.mouseUpElapsed / 1000;
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.PaperRippleBehavior
|
| - ],
|
| + get mouseInteractionSeconds() {
|
| + return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
|
| + },
|
|
|
| - listeners: {
|
| - 'down': '_rippleDown',
|
| - 'up': '_rippleUp',
|
| - 'focus': '_rippleDown',
|
| - 'blur': '_rippleUp',
|
| + get initialOpacity() {
|
| + return this.element.initialOpacity;
|
| },
|
|
|
| - _rippleDown: function() {
|
| - this.getRipple().downAction();
|
| + get opacityDecayVelocity() {
|
| + return this.element.opacityDecayVelocity;
|
| },
|
|
|
| - _rippleUp: function() {
|
| - this.getRipple().upAction();
|
| + get radius() {
|
| + var width2 = this.containerMetrics.width * this.containerMetrics.width;
|
| + var height2 = this.containerMetrics.height * this.containerMetrics.height;
|
| + var waveRadius = Math.min(
|
| + Math.sqrt(width2 + height2),
|
| + Ripple.MAX_RADIUS
|
| + ) * 1.1 + 5;
|
| +
|
| + var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
|
| + var timeNow = this.mouseInteractionSeconds / duration;
|
| + var size = waveRadius * (1 - Math.pow(80, -timeNow));
|
| +
|
| + return Math.abs(size);
|
| },
|
|
|
| - /**
|
| - * @param {...*} var_args
|
| - */
|
| - ensureRipple: function(var_args) {
|
| - var lastRipple = this._ripple;
|
| - Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
|
| - if (this._ripple && this._ripple !== lastRipple) {
|
| - this._ripple.center = true;
|
| - this._ripple.classList.add('circle');
|
| + get opacity() {
|
| + if (!this.mouseUpStart) {
|
| + return this.initialOpacity;
|
| }
|
| - }
|
| - });
|
| -/**
|
| - * `iron-range-behavior` provides the behavior for something with a minimum to maximum range.
|
| - *
|
| - * @demo demo/index.html
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.IronRangeBehavior = {
|
|
|
| - properties: {
|
| + return Math.max(
|
| + 0,
|
| + this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
|
| + );
|
| + },
|
|
|
| - /**
|
| - * The number that represents the current value.
|
| - */
|
| - value: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true,
|
| - reflectToAttribute: true
|
| - },
|
| + get outerOpacity() {
|
| + // Linear increase in background opacity, capped at the opacity
|
| + // of the wavefront (waveOpacity).
|
| + var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
|
| + var waveOpacity = this.opacity;
|
|
|
| - /**
|
| - * The number that indicates the minimum value of the range.
|
| - */
|
| - min: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| - },
|
| + return Math.max(
|
| + 0,
|
| + Math.min(outerOpacity, waveOpacity)
|
| + );
|
| + },
|
|
|
| - /**
|
| - * The number that indicates the maximum value of the range.
|
| - */
|
| - max: {
|
| - type: Number,
|
| - value: 100,
|
| - notify: true
|
| - },
|
| + get isOpacityFullyDecayed() {
|
| + return this.opacity < 0.01 &&
|
| + this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
|
| + },
|
|
|
| - /**
|
| - * Specifies the value granularity of the range's value.
|
| - */
|
| - step: {
|
| - type: Number,
|
| - value: 1,
|
| - notify: true
|
| - },
|
| + get isRestingAtMaxRadius() {
|
| + return this.opacity >= this.initialOpacity &&
|
| + this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
|
| + },
|
|
|
| - /**
|
| - * Returns the ratio of the value.
|
| - */
|
| - ratio: {
|
| - type: Number,
|
| - value: 0,
|
| - readOnly: true,
|
| - notify: true
|
| - },
|
| - },
|
| + get isAnimationComplete() {
|
| + return this.mouseUpStart ?
|
| + this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
|
| + },
|
|
|
| - observers: [
|
| - '_update(value, min, max, step)'
|
| - ],
|
| + get translationFraction() {
|
| + return Math.min(
|
| + 1,
|
| + this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
|
| + );
|
| + },
|
|
|
| - _calcRatio: function(value) {
|
| - return (this._clampValue(value) - this.min) / (this.max - this.min);
|
| - },
|
| + get xNow() {
|
| + if (this.xEnd) {
|
| + return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
|
| + }
|
|
|
| - _clampValue: function(value) {
|
| - return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
|
| - },
|
| + return this.xStart;
|
| + },
|
|
|
| - _calcStep: function(value) {
|
| - // polymer/issues/2493
|
| - value = parseFloat(value);
|
| + get yNow() {
|
| + if (this.yEnd) {
|
| + return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
|
| + }
|
|
|
| - if (!this.step) {
|
| - return value;
|
| - }
|
| + return this.yStart;
|
| + },
|
|
|
| - var numSteps = Math.round((value - this.min) / this.step);
|
| - if (this.step < 1) {
|
| - /**
|
| - * For small values of this.step, if we calculate the step using
|
| - * `Math.round(value / step) * step` we may hit a precision point issue
|
| - * eg. 0.1 * 0.2 = 0.020000000000000004
|
| - * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
| - *
|
| - * as a work around we can divide by the reciprocal of `step`
|
| - */
|
| - return numSteps / (1 / this.step) + this.min;
|
| - } else {
|
| - return numSteps * this.step + this.min;
|
| - }
|
| - },
|
| + get isMouseDown() {
|
| + return this.mouseDownStart && !this.mouseUpStart;
|
| + },
|
|
|
| - _validateValue: function() {
|
| - var v = this._clampValue(this.value);
|
| - this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
|
| - return this.value !== v;
|
| - },
|
| + resetInteractionState: function() {
|
| + this.maxRadius = 0;
|
| + this.mouseDownStart = 0;
|
| + this.mouseUpStart = 0;
|
|
|
| - _update: function() {
|
| - this._validateValue();
|
| - this._setRatio(this._calcRatio(this.value) * 100);
|
| - }
|
| + this.xStart = 0;
|
| + this.yStart = 0;
|
| + this.xEnd = 0;
|
| + this.yEnd = 0;
|
| + this.slideDistance = 0;
|
|
|
| -};
|
| -Polymer({
|
| - is: 'paper-progress',
|
| + this.containerMetrics = new ElementMetrics(this.element);
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.IronRangeBehavior
|
| - ],
|
| + draw: function() {
|
| + var scale;
|
| + var translateString;
|
| + var dx;
|
| + var dy;
|
|
|
| - properties: {
|
| - /**
|
| - * The number that represents the current secondary progress.
|
| - */
|
| - secondaryProgress: {
|
| - type: Number,
|
| - value: 0
|
| - },
|
| + this.wave.style.opacity = this.opacity;
|
|
|
| - /**
|
| - * The secondary ratio
|
| - */
|
| - secondaryRatio: {
|
| - type: Number,
|
| - value: 0,
|
| - readOnly: true
|
| - },
|
| + scale = this.radius / (this.containerMetrics.size / 2);
|
| + dx = this.xNow - (this.containerMetrics.width / 2);
|
| + dy = this.yNow - (this.containerMetrics.height / 2);
|
|
|
| - /**
|
| - * Use an indeterminate progress indicator.
|
| - */
|
| - indeterminate: {
|
| - type: Boolean,
|
| - value: false,
|
| - observer: '_toggleIndeterminate'
|
| +
|
| + // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
|
| + // https://bugs.webkit.org/show_bug.cgi?id=98538
|
| + this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
|
| + this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
|
| + this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
|
| + this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
|
| },
|
|
|
| - /**
|
| - * True if the progress is disabled.
|
| - */
|
| - disabled: {
|
| - type: Boolean,
|
| - value: false,
|
| - reflectToAttribute: true,
|
| - observer: '_disabledChanged'
|
| - }
|
| - },
|
| + /** @param {Event=} event */
|
| + downAction: function(event) {
|
| + var xCenter = this.containerMetrics.width / 2;
|
| + var yCenter = this.containerMetrics.height / 2;
|
|
|
| - observers: [
|
| - '_progressChanged(secondaryProgress, value, min, max)'
|
| - ],
|
| + this.resetInteractionState();
|
| + this.mouseDownStart = Utility.now();
|
|
|
| - hostAttributes: {
|
| - role: 'progressbar'
|
| - },
|
| + if (this.center) {
|
| + this.xStart = xCenter;
|
| + this.yStart = yCenter;
|
| + this.slideDistance = Utility.distance(
|
| + this.xStart, this.yStart, this.xEnd, this.yEnd
|
| + );
|
| + } else {
|
| + this.xStart = event ?
|
| + event.detail.x - this.containerMetrics.boundingRect.left :
|
| + this.containerMetrics.width / 2;
|
| + this.yStart = event ?
|
| + event.detail.y - this.containerMetrics.boundingRect.top :
|
| + this.containerMetrics.height / 2;
|
| + }
|
|
|
| - _toggleIndeterminate: function(indeterminate) {
|
| - // If we use attribute/class binding, the animation sometimes doesn't translate properly
|
| - // on Safari 7.1. So instead, we toggle the class here in the update method.
|
| - this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
|
| - },
|
| + if (this.recenters) {
|
| + this.xEnd = xCenter;
|
| + this.yEnd = yCenter;
|
| + this.slideDistance = Utility.distance(
|
| + this.xStart, this.yStart, this.xEnd, this.yEnd
|
| + );
|
| + }
|
|
|
| - _transformProgress: function(progress, ratio) {
|
| - var transform = 'scaleX(' + (ratio / 100) + ')';
|
| - progress.style.transform = progress.style.webkitTransform = transform;
|
| - },
|
| + this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
|
| + this.xStart,
|
| + this.yStart
|
| + );
|
|
|
| - _mainRatioChanged: function(ratio) {
|
| - this._transformProgress(this.$.primaryProgress, ratio);
|
| - },
|
| + this.waveContainer.style.top =
|
| + (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
|
| + this.waveContainer.style.left =
|
| + (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
|
|
|
| - _progressChanged: function(secondaryProgress, value, min, max) {
|
| - secondaryProgress = this._clampValue(secondaryProgress);
|
| - value = this._clampValue(value);
|
| + this.waveContainer.style.width = this.containerMetrics.size + 'px';
|
| + this.waveContainer.style.height = this.containerMetrics.size + 'px';
|
| + },
|
|
|
| - var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
|
| - var mainRatio = this._calcRatio(value) * 100;
|
| + /** @param {Event=} event */
|
| + upAction: function(event) {
|
| + if (!this.isMouseDown) {
|
| + return;
|
| + }
|
|
|
| - this._setSecondaryRatio(secondaryRatio);
|
| - this._transformProgress(this.$.secondaryProgress, secondaryRatio);
|
| - this._transformProgress(this.$.primaryProgress, mainRatio);
|
| + this.mouseUpStart = Utility.now();
|
| + },
|
|
|
| - this.secondaryProgress = secondaryProgress;
|
| + remove: function() {
|
| + Polymer.dom(this.waveContainer.parentNode).removeChild(
|
| + this.waveContainer
|
| + );
|
| + }
|
| + };
|
|
|
| - this.setAttribute('aria-valuenow', value);
|
| - this.setAttribute('aria-valuemin', min);
|
| - this.setAttribute('aria-valuemax', max);
|
| - },
|
| + Polymer({
|
| + is: 'paper-ripple',
|
|
|
| - _disabledChanged: function(disabled) {
|
| - this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
| - },
|
| + behaviors: [
|
| + Polymer.IronA11yKeysBehavior
|
| + ],
|
|
|
| - _hideSecondaryProgress: function(secondaryRatio) {
|
| - return secondaryRatio === 0;
|
| - }
|
| - });
|
| -/**
|
| - * The `iron-iconset-svg` element allows users to define their own icon sets
|
| - * that contain svg icons. The svg icon elements should be children of the
|
| - * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
|
| - *
|
| - * Using svg elements to create icons has a few advantages over traditional
|
| - * bitmap graphics like jpg or png. Icons that use svg are vector based so
|
| - * they are resolution independent and should look good on any device. They
|
| - * are stylable via css. Icons can be themed, colorized, and even animated.
|
| - *
|
| - * Example:
|
| - *
|
| - * <iron-iconset-svg name="my-svg-icons" size="24">
|
| - * <svg>
|
| - * <defs>
|
| - * <g id="shape">
|
| - * <rect x="12" y="0" width="12" height="24" />
|
| - * <circle cx="12" cy="12" r="12" />
|
| - * </g>
|
| - * </defs>
|
| - * </svg>
|
| - * </iron-iconset-svg>
|
| - *
|
| - * This will automatically register the icon set "my-svg-icons" to the iconset
|
| - * database. To use these icons from within another element, make a
|
| - * `iron-iconset` element and call the `byId` method
|
| - * to retrieve a given iconset. To apply a particular icon inside an
|
| - * element use the `applyIcon` method. For example:
|
| - *
|
| - * iconset.applyIcon(iconNode, 'car');
|
| - *
|
| - * @element iron-iconset-svg
|
| - * @demo demo/index.html
|
| - * @implements {Polymer.Iconset}
|
| - */
|
| - Polymer({
|
| - is: 'iron-iconset-svg',
|
| -
|
| - properties: {
|
| -
|
| - /**
|
| - * The name of the iconset.
|
| - */
|
| - name: {
|
| - type: String,
|
| - observer: '_nameChanged'
|
| - },
|
| -
|
| - /**
|
| - * The size of an individual icon. Note that icons must be square.
|
| - */
|
| - size: {
|
| - type: Number,
|
| - value: 24
|
| - }
|
| -
|
| - },
|
| -
|
| - attached: function() {
|
| - this.style.display = 'none';
|
| - },
|
| -
|
| - /**
|
| - * Construct an array of all icon names in this iconset.
|
| - *
|
| - * @return {!Array} Array of icon names.
|
| - */
|
| - getIconNames: function() {
|
| - this._icons = this._createIconMap();
|
| - return Object.keys(this._icons).map(function(n) {
|
| - return this.name + ':' + n;
|
| - }, this);
|
| - },
|
| -
|
| - /**
|
| - * Applies an icon to the given element.
|
| - *
|
| - * An svg icon is prepended to the element's shadowRoot if it exists,
|
| - * otherwise to the element itself.
|
| - *
|
| - * @method applyIcon
|
| - * @param {Element} element Element to which the icon is applied.
|
| - * @param {string} iconName Name of the icon to apply.
|
| - * @return {?Element} The svg element which renders the icon.
|
| - */
|
| - applyIcon: function(element, iconName) {
|
| - // insert svg element into shadow root, if it exists
|
| - element = element.root || element;
|
| - // Remove old svg element
|
| - this.removeIcon(element);
|
| - // install new svg element
|
| - var svg = this._cloneIcon(iconName);
|
| - if (svg) {
|
| - var pde = Polymer.dom(element);
|
| - pde.insertBefore(svg, pde.childNodes[0]);
|
| - return element._svgIcon = svg;
|
| - }
|
| - return null;
|
| - },
|
| + properties: {
|
| + /**
|
| + * The initial opacity set on the wave.
|
| + *
|
| + * @attribute initialOpacity
|
| + * @type number
|
| + * @default 0.25
|
| + */
|
| + initialOpacity: {
|
| + type: Number,
|
| + value: 0.25
|
| + },
|
|
|
| - /**
|
| - * Remove an icon from the given element by undoing the changes effected
|
| - * by `applyIcon`.
|
| - *
|
| - * @param {Element} element The element from which the icon is removed.
|
| - */
|
| - removeIcon: function(element) {
|
| - // Remove old svg element
|
| - if (element._svgIcon) {
|
| - Polymer.dom(element).removeChild(element._svgIcon);
|
| - element._svgIcon = null;
|
| - }
|
| - },
|
| + /**
|
| + * How fast (opacity per second) the wave fades out.
|
| + *
|
| + * @attribute opacityDecayVelocity
|
| + * @type number
|
| + * @default 0.8
|
| + */
|
| + opacityDecayVelocity: {
|
| + type: Number,
|
| + value: 0.8
|
| + },
|
|
|
| - /**
|
| - *
|
| - * When name is changed, register iconset metadata
|
| - *
|
| - */
|
| - _nameChanged: function() {
|
| - new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
|
| - this.async(function() {
|
| - this.fire('iron-iconset-added', this, {node: window});
|
| - });
|
| - },
|
| + /**
|
| + * If true, ripples will exhibit a gravitational pull towards
|
| + * the center of their container as they fade away.
|
| + *
|
| + * @attribute recenters
|
| + * @type boolean
|
| + * @default false
|
| + */
|
| + recenters: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
|
|
| - /**
|
| - * Create a map of child SVG elements by id.
|
| - *
|
| - * @return {!Object} Map of id's to SVG elements.
|
| - */
|
| - _createIconMap: function() {
|
| - // Objects chained to Object.prototype (`{}`) have members. Specifically,
|
| - // on FF there is a `watch` method that confuses the icon map, so we
|
| - // need to use a null-based object here.
|
| - var icons = Object.create(null);
|
| - Polymer.dom(this).querySelectorAll('[id]')
|
| - .forEach(function(icon) {
|
| - icons[icon.id] = icon;
|
| - });
|
| - return icons;
|
| - },
|
| + /**
|
| + * If true, ripples will center inside its container
|
| + *
|
| + * @attribute recenters
|
| + * @type boolean
|
| + * @default false
|
| + */
|
| + center: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
|
|
| - /**
|
| - * Produce installable clone of the SVG element matching `id` in this
|
| - * iconset, or `undefined` if there is no matching element.
|
| - *
|
| - * @return {Element} Returns an installable clone of the SVG element
|
| - * matching `id`.
|
| - */
|
| - _cloneIcon: function(id) {
|
| - // create the icon map on-demand, since the iconset itself has no discrete
|
| - // signal to know when it's children are fully parsed
|
| - this._icons = this._icons || this._createIconMap();
|
| - return this._prepareSvgClone(this._icons[id], this.size);
|
| - },
|
| + /**
|
| + * A list of the visual ripples.
|
| + *
|
| + * @attribute ripples
|
| + * @type Array
|
| + * @default []
|
| + */
|
| + ripples: {
|
| + type: Array,
|
| + value: function() {
|
| + return [];
|
| + }
|
| + },
|
|
|
| - /**
|
| - * @param {Element} sourceSvg
|
| - * @param {number} size
|
| - * @return {Element}
|
| - */
|
| - _prepareSvgClone: function(sourceSvg, size) {
|
| - if (sourceSvg) {
|
| - var content = sourceSvg.cloneNode(true),
|
| - svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
|
| - viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
|
| - svg.setAttribute('viewBox', viewBox);
|
| - svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
| - // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/370136
|
| - // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
|
| - svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
|
| - svg.appendChild(content).removeAttribute('id');
|
| - return svg;
|
| - }
|
| - return null;
|
| - }
|
| + /**
|
| + * True when there are visible ripples animating within the
|
| + * element.
|
| + */
|
| + animating: {
|
| + type: Boolean,
|
| + readOnly: true,
|
| + reflectToAttribute: true,
|
| + value: false
|
| + },
|
|
|
| - });
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| + /**
|
| + * If true, the ripple will remain in the "down" state until `holdDown`
|
| + * is set to false again.
|
| + */
|
| + holdDown: {
|
| + type: Boolean,
|
| + value: false,
|
| + observer: '_holdDownChanged'
|
| + },
|
|
|
| -cr.define('downloads', function() {
|
| - var Item = Polymer({
|
| - is: 'downloads-item',
|
| + /**
|
| + * If true, the ripple will not generate a ripple effect
|
| + * via pointer interaction.
|
| + * Calling ripple's imperative api like `simulatedRipple` will
|
| + * still generate the ripple effect.
|
| + */
|
| + noink: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
|
|
| - properties: {
|
| - data: {
|
| - type: Object,
|
| - },
|
| + _animating: {
|
| + type: Boolean
|
| + },
|
|
|
| - completelyOnDisk_: {
|
| - computed: 'computeCompletelyOnDisk_(' +
|
| - 'data.state, data.file_externally_removed)',
|
| - type: Boolean,
|
| - value: true,
|
| + _boundAnimate: {
|
| + type: Function,
|
| + value: function() {
|
| + return this.animate.bind(this);
|
| + }
|
| + }
|
| },
|
|
|
| - controlledBy_: {
|
| - computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
|
| - type: String,
|
| - value: '',
|
| + get target () {
|
| + return this.keyEventTarget;
|
| },
|
|
|
| - isActive_: {
|
| - computed: 'computeIsActive_(' +
|
| - 'data.state, data.file_externally_removed)',
|
| - type: Boolean,
|
| - value: true,
|
| + keyBindings: {
|
| + 'enter:keydown': '_onEnterKeydown',
|
| + 'space:keydown': '_onSpaceKeydown',
|
| + 'space:keyup': '_onSpaceKeyup'
|
| },
|
|
|
| - isDangerous_: {
|
| - computed: 'computeIsDangerous_(data.state)',
|
| - type: Boolean,
|
| - value: false,
|
| + attached: function() {
|
| + // Set up a11yKeysBehavior to listen to key events on the target,
|
| + // so that space and enter activate the ripple even if the target doesn't
|
| + // handle key events. The key handlers deal with `noink` themselves.
|
| + if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
|
| + this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
|
| + } else {
|
| + this.keyEventTarget = this.parentNode;
|
| + }
|
| + var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
|
| + this.listen(keyEventTarget, 'up', 'uiUpAction');
|
| + this.listen(keyEventTarget, 'down', 'uiDownAction');
|
| },
|
|
|
| - isMalware_: {
|
| - computed: 'computeIsMalware_(isDangerous_, data.danger_type)',
|
| - type: Boolean,
|
| - value: false,
|
| + detached: function() {
|
| + this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
|
| + this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
|
| + this.keyEventTarget = null;
|
| },
|
|
|
| - isInProgress_: {
|
| - computed: 'computeIsInProgress_(data.state)',
|
| - type: Boolean,
|
| - value: false,
|
| - },
|
| + get shouldKeepAnimating () {
|
| + for (var index = 0; index < this.ripples.length; ++index) {
|
| + if (!this.ripples[index].isAnimationComplete) {
|
| + return true;
|
| + }
|
| + }
|
|
|
| - pauseOrResumeText_: {
|
| - computed: 'computePauseOrResumeText_(isInProgress_, data.resume)',
|
| - type: String,
|
| + return false;
|
| },
|
|
|
| - showCancel_: {
|
| - computed: 'computeShowCancel_(data.state)',
|
| - type: Boolean,
|
| - value: false,
|
| - },
|
| + simulatedRipple: function() {
|
| + this.downAction(null);
|
|
|
| - showProgress_: {
|
| - computed: 'computeShowProgress_(showCancel_, data.percent)',
|
| - type: Boolean,
|
| - value: false,
|
| + // Please see polymer/polymer#1305
|
| + this.async(function() {
|
| + this.upAction();
|
| + }, 1);
|
| },
|
| - },
|
|
|
| - observers: [
|
| - // TODO(dbeam): this gets called way more when I observe data.by_ext_id
|
| - // and data.by_ext_name directly. Why?
|
| - 'observeControlledBy_(controlledBy_)',
|
| - 'observeIsDangerous_(isDangerous_, data)',
|
| - ],
|
| + /**
|
| + * Provokes a ripple down effect via a UI event,
|
| + * respecting the `noink` property.
|
| + * @param {Event=} event
|
| + */
|
| + uiDownAction: function(event) {
|
| + if (!this.noink) {
|
| + this.downAction(event);
|
| + }
|
| + },
|
|
|
| - ready: function() {
|
| - this.content = this.$.content;
|
| - },
|
| + /**
|
| + * Provokes a ripple down effect via a UI event,
|
| + * *not* respecting the `noink` property.
|
| + * @param {Event=} event
|
| + */
|
| + downAction: function(event) {
|
| + if (this.holdDown && this.ripples.length > 0) {
|
| + return;
|
| + }
|
|
|
| - /** @private */
|
| - computeClass_: function() {
|
| - var classes = [];
|
| + var ripple = this.addRipple();
|
|
|
| - if (this.isActive_)
|
| - classes.push('is-active');
|
| + ripple.downAction(event);
|
|
|
| - if (this.isDangerous_)
|
| - classes.push('dangerous');
|
| + if (!this._animating) {
|
| + this._animating = true;
|
| + this.animate();
|
| + }
|
| + },
|
|
|
| - if (this.showProgress_)
|
| - classes.push('show-progress');
|
| + /**
|
| + * Provokes a ripple up effect via a UI event,
|
| + * respecting the `noink` property.
|
| + * @param {Event=} event
|
| + */
|
| + uiUpAction: function(event) {
|
| + if (!this.noink) {
|
| + this.upAction(event);
|
| + }
|
| + },
|
|
|
| - return classes.join(' ');
|
| - },
|
| + /**
|
| + * Provokes a ripple up effect via a UI event,
|
| + * *not* respecting the `noink` property.
|
| + * @param {Event=} event
|
| + */
|
| + upAction: function(event) {
|
| + if (this.holdDown) {
|
| + return;
|
| + }
|
|
|
| - /** @private */
|
| - computeCompletelyOnDisk_: function() {
|
| - return this.data.state == downloads.States.COMPLETE &&
|
| - !this.data.file_externally_removed;
|
| - },
|
| + this.ripples.forEach(function(ripple) {
|
| + ripple.upAction(event);
|
| + });
|
|
|
| - /** @private */
|
| - computeControlledBy_: function() {
|
| - if (!this.data.by_ext_id || !this.data.by_ext_name)
|
| - return '';
|
| + this._animating = true;
|
| + this.animate();
|
| + },
|
|
|
| - var url = 'chrome://extensions#' + this.data.by_ext_id;
|
| - var name = this.data.by_ext_name;
|
| - return loadTimeData.getStringF('controlledByUrl', url, name);
|
| - },
|
| + onAnimationComplete: function() {
|
| + this._animating = false;
|
| + this.$.background.style.backgroundColor = null;
|
| + this.fire('transitionend');
|
| + },
|
|
|
| - /** @private */
|
| - computeDangerIcon_: function() {
|
| - if (!this.isDangerous_)
|
| - return '';
|
| + addRipple: function() {
|
| + var ripple = new Ripple(this);
|
|
|
| - switch (this.data.danger_type) {
|
| - case downloads.DangerType.DANGEROUS_CONTENT:
|
| - case downloads.DangerType.DANGEROUS_HOST:
|
| - case downloads.DangerType.DANGEROUS_URL:
|
| - case downloads.DangerType.POTENTIALLY_UNWANTED:
|
| - case downloads.DangerType.UNCOMMON_CONTENT:
|
| - return 'downloads:remove-circle';
|
| - default:
|
| - return 'cr:warning';
|
| - }
|
| - },
|
| + Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
|
| + this.$.background.style.backgroundColor = ripple.color;
|
| + this.ripples.push(ripple);
|
|
|
| - /** @private */
|
| - computeDate_: function() {
|
| - assert(typeof this.data.hideDate == 'boolean');
|
| - if (this.data.hideDate)
|
| - return '';
|
| - return assert(this.data.since_string || this.data.date_string);
|
| - },
|
| + this._setAnimating(true);
|
|
|
| - /** @private */
|
| - computeDescription_: function() {
|
| - var data = this.data;
|
| -
|
| - switch (data.state) {
|
| - case downloads.States.DANGEROUS:
|
| - var fileName = data.file_name;
|
| - switch (data.danger_type) {
|
| - case downloads.DangerType.DANGEROUS_FILE:
|
| - return loadTimeData.getStringF('dangerFileDesc', fileName);
|
| - case downloads.DangerType.DANGEROUS_URL:
|
| - return loadTimeData.getString('dangerUrlDesc');
|
| - case downloads.DangerType.DANGEROUS_CONTENT: // Fall through.
|
| - case downloads.DangerType.DANGEROUS_HOST:
|
| - return loadTimeData.getStringF('dangerContentDesc', fileName);
|
| - case downloads.DangerType.UNCOMMON_CONTENT:
|
| - return loadTimeData.getStringF('dangerUncommonDesc', fileName);
|
| - case downloads.DangerType.POTENTIALLY_UNWANTED:
|
| - return loadTimeData.getStringF('dangerSettingsDesc', fileName);
|
| - }
|
| - break;
|
| + return ripple;
|
| + },
|
|
|
| - case downloads.States.IN_PROGRESS:
|
| - case downloads.States.PAUSED: // Fallthrough.
|
| - return data.progress_status_text;
|
| - }
|
| + removeRipple: function(ripple) {
|
| + var rippleIndex = this.ripples.indexOf(ripple);
|
|
|
| - return '';
|
| - },
|
| + if (rippleIndex < 0) {
|
| + return;
|
| + }
|
|
|
| - /** @private */
|
| - computeIsActive_: function() {
|
| - return this.data.state != downloads.States.CANCELLED &&
|
| - this.data.state != downloads.States.INTERRUPTED &&
|
| - !this.data.file_externally_removed;
|
| - },
|
| + this.ripples.splice(rippleIndex, 1);
|
|
|
| - /** @private */
|
| - computeIsDangerous_: function() {
|
| - return this.data.state == downloads.States.DANGEROUS;
|
| - },
|
| + ripple.remove();
|
|
|
| - /** @private */
|
| - computeIsInProgress_: function() {
|
| - return this.data.state == downloads.States.IN_PROGRESS;
|
| - },
|
| + if (!this.ripples.length) {
|
| + this._setAnimating(false);
|
| + }
|
| + },
|
|
|
| - /** @private */
|
| - computeIsMalware_: function() {
|
| - return this.isDangerous_ &&
|
| - (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT ||
|
| - this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
|
| - this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
|
| - this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
|
| - },
|
| + animate: function() {
|
| + if (!this._animating) {
|
| + return;
|
| + }
|
| + var index;
|
| + var ripple;
|
|
|
| - /** @private */
|
| - computePauseOrResumeText_: function() {
|
| - if (this.isInProgress_)
|
| - return loadTimeData.getString('controlPause');
|
| - if (this.data.resume)
|
| - return loadTimeData.getString('controlResume');
|
| - return '';
|
| - },
|
| + for (index = 0; index < this.ripples.length; ++index) {
|
| + ripple = this.ripples[index];
|
|
|
| - /** @private */
|
| - computeRemoveStyle_: function() {
|
| - var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
|
| - var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
|
| - return hideRemove ? 'visibility: hidden' : '';
|
| - },
|
| + ripple.draw();
|
|
|
| - /** @private */
|
| - computeShowCancel_: function() {
|
| - return this.data.state == downloads.States.IN_PROGRESS ||
|
| - this.data.state == downloads.States.PAUSED;
|
| - },
|
| + this.$.background.style.opacity = ripple.outerOpacity;
|
|
|
| - /** @private */
|
| - computeShowProgress_: function() {
|
| - return this.showCancel_ && this.data.percent >= -1;
|
| - },
|
| + if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
|
| + this.removeRipple(ripple);
|
| + }
|
| + }
|
|
|
| - /** @private */
|
| - computeTag_: function() {
|
| - switch (this.data.state) {
|
| - case downloads.States.CANCELLED:
|
| - return loadTimeData.getString('statusCancelled');
|
| + if (!this.shouldKeepAnimating && this.ripples.length === 0) {
|
| + this.onAnimationComplete();
|
| + } else {
|
| + window.requestAnimationFrame(this._boundAnimate);
|
| + }
|
| + },
|
|
|
| - case downloads.States.INTERRUPTED:
|
| - return this.data.last_reason_text;
|
| + _onEnterKeydown: function() {
|
| + this.uiDownAction();
|
| + this.async(this.uiUpAction, 1);
|
| + },
|
|
|
| - case downloads.States.COMPLETE:
|
| - return this.data.file_externally_removed ?
|
| - loadTimeData.getString('statusRemoved') : '';
|
| - }
|
| + _onSpaceKeydown: function() {
|
| + this.uiDownAction();
|
| + },
|
|
|
| - return '';
|
| - },
|
| + _onSpaceKeyup: function() {
|
| + this.uiUpAction();
|
| + },
|
|
|
| - /** @private */
|
| - isIndeterminate_: function() {
|
| - return this.data.percent == -1;
|
| - },
|
| + // note: holdDown does not respect noink since it can be a focus based
|
| + // effect.
|
| + _holdDownChanged: function(newVal, oldVal) {
|
| + if (oldVal === undefined) {
|
| + return;
|
| + }
|
| + if (newVal) {
|
| + this.downAction();
|
| + } else {
|
| + this.upAction();
|
| + }
|
| + }
|
|
|
| - /** @private */
|
| - observeControlledBy_: function() {
|
| - this.$['controlled-by'].innerHTML = this.controlledBy_;
|
| - },
|
| + /**
|
| + Fired when the animation finishes.
|
| + This is useful if you want to wait until
|
| + the ripple animation finishes to perform some action.
|
|
|
| - /** @private */
|
| - observeIsDangerous_: function() {
|
| - if (!this.data)
|
| - return;
|
| + @event transitionend
|
| + @param {{node: Object}} detail Contains the animated node.
|
| + */
|
| + });
|
| + })();
|
| +/**
|
| + * `Polymer.PaperRippleBehavior` dynamically implements a ripple
|
| + * when the element has focus via pointer or keyboard.
|
| + *
|
| + * NOTE: This behavior is intended to be used in conjunction with and after
|
| + * `Polymer.IronButtonState` and `Polymer.IronControlState`.
|
| + *
|
| + * @polymerBehavior Polymer.PaperRippleBehavior
|
| + */
|
| + Polymer.PaperRippleBehavior = {
|
| + properties: {
|
| + /**
|
| + * If true, the element will not produce a ripple effect when interacted
|
| + * with via the pointer.
|
| + */
|
| + noink: {
|
| + type: Boolean,
|
| + observer: '_noinkChanged'
|
| + },
|
|
|
| - if (this.isDangerous_) {
|
| - this.$.url.removeAttribute('href');
|
| - } else {
|
| - this.$.url.href = assert(this.data.url);
|
| - var filePath = encodeURIComponent(this.data.file_path);
|
| - var scaleFactor = '?scale=' + window.devicePixelRatio + 'x';
|
| - this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor;
|
| + /**
|
| + * @type {Element|undefined}
|
| + */
|
| + _rippleContainer: {
|
| + type: Object,
|
| }
|
| },
|
|
|
| - /** @private */
|
| - onCancelTap_: function() {
|
| - downloads.ActionService.getInstance().cancel(this.data.id);
|
| - },
|
| -
|
| - /** @private */
|
| - onDiscardDangerousTap_: function() {
|
| - downloads.ActionService.getInstance().discardDangerous(this.data.id);
|
| + /**
|
| + * Ensures a `<paper-ripple>` element is available when the element is
|
| + * focused.
|
| + */
|
| + _buttonStateChanged: function() {
|
| + if (this.focused) {
|
| + this.ensureRipple();
|
| + }
|
| },
|
|
|
| /**
|
| - * @private
|
| - * @param {Event} e
|
| + * In addition to the functionality provided in `IronButtonState`, ensures
|
| + * a ripple effect is created when the element is in a `pressed` state.
|
| */
|
| - onDragStart_: function(e) {
|
| - e.preventDefault();
|
| - downloads.ActionService.getInstance().drag(this.data.id);
|
| + _downHandler: function(event) {
|
| + Polymer.IronButtonStateImpl._downHandler.call(this, event);
|
| + if (this.pressed) {
|
| + this.ensureRipple(event);
|
| + }
|
| },
|
|
|
| /**
|
| - * @param {Event} e
|
| - * @private
|
| + * Ensures this element contains a ripple effect. For startup efficiency
|
| + * the ripple effect is dynamically on demand when needed.
|
| + * @param {!Event=} optTriggeringEvent (optional) event that triggered the
|
| + * ripple.
|
| */
|
| - onFileLinkTap_: function(e) {
|
| - e.preventDefault();
|
| - downloads.ActionService.getInstance().openFile(this.data.id);
|
| - },
|
| -
|
| - /** @private */
|
| - onPauseOrResumeTap_: function() {
|
| - if (this.isInProgress_)
|
| - downloads.ActionService.getInstance().pause(this.data.id);
|
| - else
|
| - downloads.ActionService.getInstance().resume(this.data.id);
|
| - },
|
| -
|
| - /** @private */
|
| - onRemoveTap_: function() {
|
| - downloads.ActionService.getInstance().remove(this.data.id);
|
| + ensureRipple: function(optTriggeringEvent) {
|
| + if (!this.hasRipple()) {
|
| + this._ripple = this._createRipple();
|
| + this._ripple.noink = this.noink;
|
| + var rippleContainer = this._rippleContainer || this.root;
|
| + if (rippleContainer) {
|
| + Polymer.dom(rippleContainer).appendChild(this._ripple);
|
| + }
|
| + if (optTriggeringEvent) {
|
| + // Check if the event happened inside of the ripple container
|
| + // Fall back to host instead of the root because distributed text
|
| + // nodes are not valid event targets
|
| + var domContainer = Polymer.dom(this._rippleContainer || this);
|
| + var target = Polymer.dom(optTriggeringEvent).rootTarget;
|
| + if (domContainer.deepContains( /** @type {Node} */(target))) {
|
| + this._ripple.uiDownAction(optTriggeringEvent);
|
| + }
|
| + }
|
| + }
|
| },
|
|
|
| - /** @private */
|
| - onRetryTap_: function() {
|
| - downloads.ActionService.getInstance().download(this.data.url);
|
| + /**
|
| + * Returns the `<paper-ripple>` element used by this element to create
|
| + * ripple effects. The element's ripple is created on demand, when
|
| + * necessary, and calling this method will force the
|
| + * ripple to be created.
|
| + */
|
| + getRipple: function() {
|
| + this.ensureRipple();
|
| + return this._ripple;
|
| },
|
|
|
| - /** @private */
|
| - onSaveDangerousTap_: function() {
|
| - downloads.ActionService.getInstance().saveDangerous(this.data.id);
|
| + /**
|
| + * Returns true if this element currently contains a ripple effect.
|
| + * @return {boolean}
|
| + */
|
| + hasRipple: function() {
|
| + return Boolean(this._ripple);
|
| },
|
|
|
| - /** @private */
|
| - onShowTap_: function() {
|
| - downloads.ActionService.getInstance().show(this.data.id);
|
| + /**
|
| + * Create the element's ripple effect via creating a `<paper-ripple>`.
|
| + * Override this method to customize the ripple element.
|
| + * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
|
| + */
|
| + _createRipple: function() {
|
| + return /** @type {!PaperRippleElement} */ (
|
| + document.createElement('paper-ripple'));
|
| },
|
| - });
|
|
|
| - return {Item: Item};
|
| -});
|
| -/** @polymerBehavior Polymer.PaperItemBehavior */
|
| - Polymer.PaperItemBehaviorImpl = {
|
| - hostAttributes: {
|
| - role: 'option',
|
| - tabindex: '0'
|
| + _noinkChanged: function(noink) {
|
| + if (this.hasRipple()) {
|
| + this._ripple.noink = noink;
|
| + }
|
| }
|
| };
|
| +/** @polymerBehavior Polymer.PaperButtonBehavior */
|
| + Polymer.PaperButtonBehaviorImpl = {
|
| + properties: {
|
| + /**
|
| + * The z-depth of this element, from 0-5. Setting to 0 will remove the
|
| + * shadow, and each increasing number greater than 0 will be "deeper"
|
| + * than the last.
|
| + *
|
| + * @attribute elevation
|
| + * @type number
|
| + * @default 1
|
| + */
|
| + elevation: {
|
| + type: Number,
|
| + reflectToAttribute: true,
|
| + readOnly: true
|
| + }
|
| + },
|
|
|
| - /** @polymerBehavior */
|
| - Polymer.PaperItemBehavior = [
|
| - Polymer.IronButtonState,
|
| - Polymer.IronControlState,
|
| - Polymer.PaperItemBehaviorImpl
|
| - ];
|
| -Polymer({
|
| - is: 'paper-item',
|
| -
|
| - behaviors: [
|
| - Polymer.PaperItemBehavior
|
| - ]
|
| - });
|
| -/**
|
| - * @param {!Function} selectCallback
|
| - * @constructor
|
| - */
|
| - Polymer.IronSelection = function(selectCallback) {
|
| - this.selection = [];
|
| - this.selectCallback = selectCallback;
|
| - };
|
| -
|
| - Polymer.IronSelection.prototype = {
|
| + observers: [
|
| + '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',
|
| + '_computeKeyboardClass(receivedFocusFromKeyboard)'
|
| + ],
|
|
|
| - /**
|
| - * Retrieves the selected item(s).
|
| - *
|
| - * @method get
|
| - * @returns Returns the selected item(s). If the multi property is true,
|
| - * `get` will return an array, otherwise it will return
|
| - * the selected item or undefined if there is no selection.
|
| - */
|
| - get: function() {
|
| - return this.multi ? this.selection.slice() : this.selection[0];
|
| + hostAttributes: {
|
| + role: 'button',
|
| + tabindex: '0',
|
| + animated: true
|
| },
|
|
|
| - /**
|
| - * Clears all the selection except the ones indicated.
|
| - *
|
| - * @method clear
|
| - * @param {Array} excludes items to be excluded.
|
| - */
|
| - clear: function(excludes) {
|
| - this.selection.slice().forEach(function(item) {
|
| - if (!excludes || excludes.indexOf(item) < 0) {
|
| - this.setItemSelected(item, false);
|
| - }
|
| - }, this);
|
| + _calculateElevation: function() {
|
| + var e = 1;
|
| + if (this.disabled) {
|
| + e = 0;
|
| + } else if (this.active || this.pressed) {
|
| + e = 4;
|
| + } else if (this.receivedFocusFromKeyboard) {
|
| + e = 3;
|
| + }
|
| + this._setElevation(e);
|
| },
|
|
|
| - /**
|
| - * Indicates if a given item is selected.
|
| - *
|
| - * @method isSelected
|
| - * @param {*} item The item whose selection state should be checked.
|
| - * @returns Returns true if `item` is selected.
|
| - */
|
| - isSelected: function(item) {
|
| - return this.selection.indexOf(item) >= 0;
|
| + _computeKeyboardClass: function(receivedFocusFromKeyboard) {
|
| + this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
|
| },
|
|
|
| /**
|
| - * Sets the selection state for a given item to either selected or deselected.
|
| + * In addition to `IronButtonState` behavior, when space key goes down,
|
| + * create a ripple down effect.
|
| *
|
| - * @method setItemSelected
|
| - * @param {*} item The item to select.
|
| - * @param {boolean} isSelected True for selected, false for deselected.
|
| + * @param {!KeyboardEvent} event .
|
| */
|
| - setItemSelected: function(item, isSelected) {
|
| - if (item != null) {
|
| - if (isSelected !== this.isSelected(item)) {
|
| - // proceed to update selection only if requested state differs from current
|
| - if (isSelected) {
|
| - this.selection.push(item);
|
| - } else {
|
| - var i = this.selection.indexOf(item);
|
| - if (i >= 0) {
|
| - this.selection.splice(i, 1);
|
| - }
|
| - }
|
| - if (this.selectCallback) {
|
| - this.selectCallback(item, isSelected);
|
| - }
|
| - }
|
| + _spaceKeyDownHandler: function(event) {
|
| + Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
|
| + // Ensure that there is at most one ripple when the space key is held down.
|
| + if (this.hasRipple() && this.getRipple().ripples.length < 1) {
|
| + this._ripple.uiDownAction();
|
| }
|
| },
|
|
|
| /**
|
| - * Sets the selection state for a given item. If the `multi` property
|
| - * is true, then the selected state of `item` will be toggled; otherwise
|
| - * the `item` will be selected.
|
| + * In addition to `IronButtonState` behavior, when space key goes up,
|
| + * create a ripple up effect.
|
| *
|
| - * @method select
|
| - * @param {*} item The item to select.
|
| + * @param {!KeyboardEvent} event .
|
| */
|
| - select: function(item) {
|
| - if (this.multi) {
|
| - this.toggle(item);
|
| - } else if (this.get() !== item) {
|
| - this.setItemSelected(this.get(), false);
|
| - this.setItemSelected(item, true);
|
| + _spaceKeyUpHandler: function(event) {
|
| + Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
|
| + if (this.hasRipple()) {
|
| + this._ripple.uiUpAction();
|
| }
|
| - },
|
| -
|
| - /**
|
| - * Toggles the selection state for `item`.
|
| - *
|
| - * @method toggle
|
| - * @param {*} item The item to toggle.
|
| - */
|
| - toggle: function(item) {
|
| - this.setItemSelected(item, !this.isSelected(item));
|
| }
|
| -
|
| };
|
| -/** @polymerBehavior */
|
| - Polymer.IronSelectableBehavior = {
|
|
|
| - /**
|
| - * Fired when iron-selector is activated (selected or deselected).
|
| - * It is fired before the selected items are changed.
|
| - * Cancel the event to abort selection.
|
| - *
|
| - * @event iron-activate
|
| - */
|
| + /** @polymerBehavior */
|
| + Polymer.PaperButtonBehavior = [
|
| + Polymer.IronButtonState,
|
| + Polymer.IronControlState,
|
| + Polymer.PaperRippleBehavior,
|
| + Polymer.PaperButtonBehaviorImpl
|
| + ];
|
| +Polymer({
|
| + is: 'paper-button',
|
|
|
| - /**
|
| - * Fired when an item is selected
|
| - *
|
| - * @event iron-select
|
| - */
|
| + behaviors: [
|
| + Polymer.PaperButtonBehavior
|
| + ],
|
|
|
| - /**
|
| - * Fired when an item is deselected
|
| - *
|
| - * @event iron-deselect
|
| - */
|
| + properties: {
|
| + /**
|
| + * If true, the button should be styled with a shadow.
|
| + */
|
| + raised: {
|
| + type: Boolean,
|
| + reflectToAttribute: true,
|
| + value: false,
|
| + observer: '_calculateElevation'
|
| + }
|
| + },
|
| +
|
| + _calculateElevation: function() {
|
| + if (!this.raised) {
|
| + this._setElevation(0);
|
| + } else {
|
| + Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
|
| + }
|
| + }
|
|
|
| /**
|
| - * Fired when the list of selectable items changes (e.g., items are
|
| - * added or removed). The detail of the event is a mutation record that
|
| - * describes what changed.
|
| - *
|
| - * @event iron-items-changed
|
| - */
|
| + Fired when the animation finishes.
|
| + This is useful if you want to wait until
|
| + the ripple animation finishes to perform some action.
|
|
|
| - properties: {
|
| + @event transitionend
|
| + Event param: {{node: Object}} detail Contains the animated node.
|
| + */
|
| + });
|
| +/**
|
| + * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has keyboard focus.
|
| + *
|
| + * @polymerBehavior Polymer.PaperInkyFocusBehavior
|
| + */
|
| + Polymer.PaperInkyFocusBehaviorImpl = {
|
| + observers: [
|
| + '_focusedChanged(receivedFocusFromKeyboard)'
|
| + ],
|
|
|
| - /**
|
| - * If you want to use an attribute value or property of an element for
|
| - * `selected` instead of the index, set this to the name of the attribute
|
| - * or property. Hyphenated values are converted to camel case when used to
|
| - * look up the property of a selectable element. Camel cased values are
|
| - * *not* converted to hyphenated values for attribute lookup. It's
|
| - * recommended that you provide the hyphenated form of the name so that
|
| - * selection works in both cases. (Use `attr-or-property-name` instead of
|
| - * `attrOrPropertyName`.)
|
| - */
|
| - attrForSelected: {
|
| - type: String,
|
| - value: null
|
| + _focusedChanged: function(receivedFocusFromKeyboard) {
|
| + if (receivedFocusFromKeyboard) {
|
| + this.ensureRipple();
|
| + }
|
| + if (this.hasRipple()) {
|
| + this._ripple.holdDown = receivedFocusFromKeyboard;
|
| + }
|
| + },
|
| +
|
| + _createRipple: function() {
|
| + var ripple = Polymer.PaperRippleBehavior._createRipple();
|
| + ripple.id = 'ink';
|
| + ripple.setAttribute('center', '');
|
| + ripple.classList.add('circle');
|
| + return ripple;
|
| + }
|
| + };
|
| +
|
| + /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
|
| + Polymer.PaperInkyFocusBehavior = [
|
| + Polymer.IronButtonState,
|
| + Polymer.IronControlState,
|
| + Polymer.PaperRippleBehavior,
|
| + Polymer.PaperInkyFocusBehaviorImpl
|
| + ];
|
| +Polymer({
|
| + is: 'paper-icon-button',
|
| +
|
| + hostAttributes: {
|
| + role: 'button',
|
| + tabindex: '0'
|
| },
|
|
|
| - /**
|
| - * Gets or sets the selected element. The default is to use the index of the item.
|
| - * @type {string|number}
|
| - */
|
| - selected: {
|
| - type: String,
|
| - notify: true
|
| + behaviors: [
|
| + Polymer.PaperInkyFocusBehavior
|
| + ],
|
| +
|
| + properties: {
|
| + /**
|
| + * The URL of an image for the icon. If the src property is specified,
|
| + * the icon property should not be.
|
| + */
|
| + src: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * Specifies the icon name or index in the set of icons available in
|
| + * the icon's icon set. If the icon property is specified,
|
| + * the src property should not be.
|
| + */
|
| + icon: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * Specifies the alternate text for the button, for accessibility.
|
| + */
|
| + alt: {
|
| + type: String,
|
| + observer: "_altChanged"
|
| + }
|
| },
|
|
|
| - /**
|
| - * Returns the currently selected item.
|
| - *
|
| - * @type {?Object}
|
| - */
|
| - selectedItem: {
|
| - type: Object,
|
| - readOnly: true,
|
| - notify: true
|
| + _altChanged: function(newValue, oldValue) {
|
| + var label = this.getAttribute('aria-label');
|
| +
|
| + // Don't stomp over a user-set aria-label.
|
| + if (!label || oldValue == label) {
|
| + this.setAttribute('aria-label', newValue);
|
| + }
|
| + }
|
| + });
|
| +Polymer({
|
| + is: 'paper-tab',
|
| +
|
| + behaviors: [
|
| + Polymer.IronControlState,
|
| + Polymer.IronButtonState,
|
| + Polymer.PaperRippleBehavior
|
| + ],
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * If true, the tab will forward keyboard clicks (enter/space) to
|
| + * the first anchor element found in its descendants
|
| + */
|
| + link: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true
|
| + }
|
| +
|
| },
|
|
|
| - /**
|
| - * The event that fires from items when they are selected. Selectable
|
| - * will listen for this event from items and update the selection state.
|
| - * Set to empty string to listen to no events.
|
| - */
|
| - activateEvent: {
|
| - type: String,
|
| - value: 'tap',
|
| - observer: '_activateEventChanged'
|
| + hostAttributes: {
|
| + role: 'tab'
|
| },
|
|
|
| - /**
|
| - * This is a CSS selector string. If this is set, only items that match the CSS selector
|
| - * are selectable.
|
| - */
|
| - selectable: String,
|
| + listeners: {
|
| + down: '_updateNoink',
|
| + tap: '_onTap'
|
| + },
|
|
|
| - /**
|
| - * The class to set on elements when selected.
|
| - */
|
| - selectedClass: {
|
| - type: String,
|
| - value: 'iron-selected'
|
| + attached: function() {
|
| + this._updateNoink();
|
| + },
|
| +
|
| + get _parentNoink () {
|
| + var parent = Polymer.dom(this).parentNode;
|
| + return !!parent && !!parent.noink;
|
| + },
|
| +
|
| + _updateNoink: function() {
|
| + this.noink = !!this.noink || !!this._parentNoink;
|
| },
|
|
|
| + _onTap: function(event) {
|
| + if (this.link) {
|
| + var anchor = this.queryEffectiveChildren('a');
|
| +
|
| + if (!anchor) {
|
| + return;
|
| + }
|
| +
|
| + // Don't get stuck in a loop delegating
|
| + // the listener from the child anchor
|
| + if (event.target === anchor) {
|
| + return;
|
| + }
|
| +
|
| + anchor.click();
|
| + }
|
| + }
|
| +
|
| + });
|
| +/** @polymerBehavior Polymer.IronMultiSelectableBehavior */
|
| + Polymer.IronMultiSelectableBehaviorImpl = {
|
| + properties: {
|
| +
|
| /**
|
| - * The attribute to set on elements when selected.
|
| + * If true, multiple selections are allowed.
|
| */
|
| - selectedAttribute: {
|
| - type: String,
|
| - value: null
|
| + multi: {
|
| + type: Boolean,
|
| + value: false,
|
| + observer: 'multiChanged'
|
| },
|
|
|
| /**
|
| - * Default fallback if the selection based on selected with `attrForSelected`
|
| - * is not found.
|
| + * Gets or sets the selected elements. This is used instead of `selected` when `multi`
|
| + * is true.
|
| */
|
| - fallbackSelection: {
|
| - type: String,
|
| - value: null
|
| + selectedValues: {
|
| + type: Array,
|
| + notify: true
|
| },
|
|
|
| /**
|
| - * The list of items from which a selection can be made.
|
| + * Returns an array of currently selected items.
|
| */
|
| - items: {
|
| + selectedItems: {
|
| type: Array,
|
| readOnly: true,
|
| - notify: true,
|
| - value: function() {
|
| - return [];
|
| - }
|
| + notify: true
|
| },
|
|
|
| - /**
|
| - * The set of excluded elements where the key is the `localName`
|
| - * of the element that will be ignored from the item list.
|
| - *
|
| - * @default {template: 1}
|
| - */
|
| - _excludedLocalNames: {
|
| - type: Object,
|
| - value: function() {
|
| - return {
|
| - 'template': 1
|
| - };
|
| - }
|
| - }
|
| },
|
|
|
| observers: [
|
| - '_updateAttrForSelected(attrForSelected)',
|
| - '_updateSelected(selected)',
|
| - '_checkFallback(fallbackSelection)'
|
| - ],
|
| -
|
| - created: function() {
|
| - this._bindFilterItem = this._filterItem.bind(this);
|
| - this._selection = new Polymer.IronSelection(this._applySelection.bind(this));
|
| - },
|
| -
|
| - attached: function() {
|
| - this._observer = this._observeItems(this);
|
| - this._updateItems();
|
| - if (!this._shouldUpdateSelection) {
|
| - this._updateSelected();
|
| - }
|
| - this._addListener(this.activateEvent);
|
| - },
|
| -
|
| - detached: function() {
|
| - if (this._observer) {
|
| - Polymer.dom(this).unobserveNodes(this._observer);
|
| - }
|
| - this._removeListener(this.activateEvent);
|
| - },
|
| -
|
| - /**
|
| - * Returns the index of the given item.
|
| - *
|
| - * @method indexOf
|
| - * @param {Object} item
|
| - * @returns Returns the index of the item
|
| - */
|
| - indexOf: function(item) {
|
| - return this.items.indexOf(item);
|
| - },
|
| -
|
| - /**
|
| - * Selects the given value.
|
| - *
|
| - * @method select
|
| - * @param {string|number} value the value to select.
|
| - */
|
| - select: function(value) {
|
| - this.selected = value;
|
| - },
|
| -
|
| - /**
|
| - * Selects the previous item.
|
| - *
|
| - * @method selectPrevious
|
| - */
|
| - selectPrevious: function() {
|
| - var length = this.items.length;
|
| - var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length;
|
| - this.selected = this._indexToValue(index);
|
| - },
|
| -
|
| - /**
|
| - * Selects the next item.
|
| - *
|
| - * @method selectNext
|
| - */
|
| - selectNext: function() {
|
| - var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.length;
|
| - this.selected = this._indexToValue(index);
|
| - },
|
| -
|
| - /**
|
| - * Selects the item at the given index.
|
| - *
|
| - * @method selectIndex
|
| - */
|
| - selectIndex: function(index) {
|
| - this.select(this._indexToValue(index));
|
| - },
|
| -
|
| - /**
|
| - * Force a synchronous update of the `items` property.
|
| - *
|
| - * NOTE: Consider listening for the `iron-items-changed` event to respond to
|
| - * updates to the set of selectable items after updates to the DOM list and
|
| - * selection state have been made.
|
| - *
|
| - * WARNING: If you are using this method, you should probably consider an
|
| - * alternate approach. Synchronously querying for items is potentially
|
| - * slow for many use cases. The `items` property will update asynchronously
|
| - * on its own to reflect selectable items in the DOM.
|
| - */
|
| - forceSynchronousItemUpdate: function() {
|
| - this._updateItems();
|
| - },
|
| -
|
| - get _shouldUpdateSelection() {
|
| - return this.selected != null;
|
| - },
|
| -
|
| - _checkFallback: function() {
|
| - if (this._shouldUpdateSelection) {
|
| - this._updateSelected();
|
| - }
|
| - },
|
| -
|
| - _addListener: function(eventName) {
|
| - this.listen(this, eventName, '_activateHandler');
|
| - },
|
| -
|
| - _removeListener: function(eventName) {
|
| - this.unlisten(this, eventName, '_activateHandler');
|
| - },
|
| -
|
| - _activateEventChanged: function(eventName, old) {
|
| - this._removeListener(old);
|
| - this._addListener(eventName);
|
| - },
|
| -
|
| - _updateItems: function() {
|
| - var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
|
| - nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
|
| - this._setItems(nodes);
|
| - },
|
| -
|
| - _updateAttrForSelected: function() {
|
| - if (this._shouldUpdateSelection) {
|
| - this.selected = this._indexToValue(this.indexOf(this.selectedItem));
|
| - }
|
| - },
|
| -
|
| - _updateSelected: function() {
|
| - this._selectSelected(this.selected);
|
| - },
|
| -
|
| - _selectSelected: function(selected) {
|
| - this._selection.select(this._valueToItem(this.selected));
|
| - // Check for items, since this array is populated only when attached
|
| - // Since Number(0) is falsy, explicitly check for undefined
|
| - if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
|
| - this.selected = this.fallbackSelection;
|
| - }
|
| - },
|
| -
|
| - _filterItem: function(node) {
|
| - return !this._excludedLocalNames[node.localName];
|
| - },
|
| -
|
| - _valueToItem: function(value) {
|
| - return (value == null) ? null : this.items[this._valueToIndex(value)];
|
| - },
|
| -
|
| - _valueToIndex: function(value) {
|
| - if (this.attrForSelected) {
|
| - for (var i = 0, item; item = this.items[i]; i++) {
|
| - if (this._valueForItem(item) == value) {
|
| - return i;
|
| - }
|
| - }
|
| - } else {
|
| - return Number(value);
|
| - }
|
| - },
|
| -
|
| - _indexToValue: function(index) {
|
| - if (this.attrForSelected) {
|
| - var item = this.items[index];
|
| - if (item) {
|
| - return this._valueForItem(item);
|
| - }
|
| - } else {
|
| - return index;
|
| - }
|
| - },
|
| -
|
| - _valueForItem: function(item) {
|
| - var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
|
| - return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected);
|
| - },
|
| -
|
| - _applySelection: function(item, isSelected) {
|
| - if (this.selectedClass) {
|
| - this.toggleClass(this.selectedClass, isSelected, item);
|
| - }
|
| - if (this.selectedAttribute) {
|
| - this.toggleAttribute(this.selectedAttribute, isSelected, item);
|
| - }
|
| - this._selectionChange();
|
| - this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
|
| - },
|
| -
|
| - _selectionChange: function() {
|
| - this._setSelectedItem(this._selection.get());
|
| - },
|
| -
|
| - // observe items change under the given node.
|
| - _observeItems: function(node) {
|
| - return Polymer.dom(node).observeNodes(function(mutation) {
|
| - this._updateItems();
|
| -
|
| - if (this._shouldUpdateSelection) {
|
| - this._updateSelected();
|
| - }
|
| -
|
| - // Let other interested parties know about the change so that
|
| - // we don't have to recreate mutation observers everywhere.
|
| - this.fire('iron-items-changed', mutation, {
|
| - bubbles: false,
|
| - cancelable: false
|
| - });
|
| - });
|
| - },
|
| -
|
| - _activateHandler: function(e) {
|
| - var t = e.target;
|
| - var items = this.items;
|
| - while (t && t != this) {
|
| - var i = items.indexOf(t);
|
| - if (i >= 0) {
|
| - var value = this._indexToValue(i);
|
| - this._itemActivate(value, t);
|
| - return;
|
| - }
|
| - t = t.parentNode;
|
| - }
|
| - },
|
| -
|
| - _itemActivate: function(value, item) {
|
| - if (!this.fire('iron-activate',
|
| - {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
|
| - this.select(value);
|
| - }
|
| - }
|
| -
|
| - };
|
| -/** @polymerBehavior Polymer.IronMultiSelectableBehavior */
|
| - Polymer.IronMultiSelectableBehaviorImpl = {
|
| - properties: {
|
| -
|
| - /**
|
| - * If true, multiple selections are allowed.
|
| - */
|
| - multi: {
|
| - type: Boolean,
|
| - value: false,
|
| - observer: 'multiChanged'
|
| - },
|
| -
|
| - /**
|
| - * Gets or sets the selected elements. This is used instead of `selected` when `multi`
|
| - * is true.
|
| - */
|
| - selectedValues: {
|
| - type: Array,
|
| - notify: true
|
| - },
|
| -
|
| - /**
|
| - * Returns an array of currently selected items.
|
| - */
|
| - selectedItems: {
|
| - type: Array,
|
| - readOnly: true,
|
| - notify: true
|
| - },
|
| -
|
| - },
|
| -
|
| - observers: [
|
| - '_updateSelected(selectedValues.splices)'
|
| + '_updateSelected(selectedValues.splices)'
|
| ],
|
|
|
| /**
|
| @@ -7539,4382 +6562,9086 @@ Polymer({
|
| Polymer.IronA11yKeysBehavior,
|
| Polymer.IronMenuBehaviorImpl
|
| ];
|
| -(function() {
|
| - Polymer({
|
| - is: 'paper-menu',
|
| -
|
| - behaviors: [
|
| - Polymer.IronMenuBehavior
|
| - ]
|
| - });
|
| - })();
|
| /**
|
| -`Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
|
| -optionally centers it in the window or another element.
|
| + * `Polymer.IronMenubarBehavior` implements accessible menubar behavior.
|
| + *
|
| + * @polymerBehavior Polymer.IronMenubarBehavior
|
| + */
|
| + Polymer.IronMenubarBehaviorImpl = {
|
|
|
| -The element will only be sized and/or positioned if it has not already been sized and/or positioned
|
| -by CSS.
|
| + hostAttributes: {
|
| + 'role': 'menubar'
|
| + },
|
|
|
| -CSS properties | Action
|
| ------------------------------|-------------------------------------------
|
| -`position` set | Element is not centered horizontally or vertically
|
| -`top` or `bottom` set | Element is not vertically centered
|
| -`left` or `right` set | Element is not horizontally centered
|
| -`max-height` set | Element respects `max-height`
|
| -`max-width` set | Element respects `max-width`
|
| + keyBindings: {
|
| + 'left': '_onLeftKey',
|
| + 'right': '_onRightKey'
|
| + },
|
|
|
| -`Polymer.IronFitBehavior` can position an element into another element using
|
| -`verticalAlign` and `horizontalAlign`. This will override the element's css position.
|
| + _onUpKey: function(event) {
|
| + this.focusedItem.click();
|
| + event.detail.keyboardEvent.preventDefault();
|
| + },
|
|
|
| - <div class="container">
|
| - <iron-fit-impl vertical-align="top" horizontal-align="auto">
|
| - Positioned into the container
|
| - </iron-fit-impl>
|
| - </div>
|
| + _onDownKey: function(event) {
|
| + this.focusedItem.click();
|
| + event.detail.keyboardEvent.preventDefault();
|
| + },
|
|
|
| -Use `noOverlap` to position the element around another element without overlapping it.
|
| + get _isRTL() {
|
| + return window.getComputedStyle(this)['direction'] === 'rtl';
|
| + },
|
|
|
| - <div class="container">
|
| - <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
|
| - Positioned around the container
|
| - </iron-fit-impl>
|
| - </div>
|
| + _onLeftKey: function(event) {
|
| + if (this._isRTL) {
|
| + this._focusNext();
|
| + } else {
|
| + this._focusPrevious();
|
| + }
|
| + event.detail.keyboardEvent.preventDefault();
|
| + },
|
|
|
| -@demo demo/index.html
|
| -@polymerBehavior
|
| -*/
|
| + _onRightKey: function(event) {
|
| + if (this._isRTL) {
|
| + this._focusPrevious();
|
| + } else {
|
| + this._focusNext();
|
| + }
|
| + event.detail.keyboardEvent.preventDefault();
|
| + },
|
|
|
| - Polymer.IronFitBehavior = {
|
| + _onKeydown: function(event) {
|
| + if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) {
|
| + return;
|
| + }
|
|
|
| - properties: {
|
| + // all other keys focus the menu item starting with that character
|
| + this._focusWithKeyboardEvent(event);
|
| + }
|
|
|
| - /**
|
| - * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
|
| - * but it can be set to a child element. This is useful, for example, for implementing a
|
| - * scrolling region inside the element.
|
| - * @type {!Element}
|
| - */
|
| - sizingTarget: {
|
| - type: Object,
|
| - value: function() {
|
| - return this;
|
| + };
|
| +
|
| + /** @polymerBehavior Polymer.IronMenubarBehavior */
|
| + Polymer.IronMenubarBehavior = [
|
| + Polymer.IronMenuBehavior,
|
| + Polymer.IronMenubarBehaviorImpl
|
| + ];
|
| +Polymer({
|
| + is: 'paper-tabs',
|
| +
|
| + behaviors: [
|
| + Polymer.IronResizableBehavior,
|
| + Polymer.IronMenubarBehavior
|
| + ],
|
| +
|
| + properties: {
|
| + /**
|
| + * If true, ink ripple effect is disabled. When this property is changed,
|
| + * all descendant `<paper-tab>` elements have their `noink` property
|
| + * changed to the new value as well.
|
| + */
|
| + noink: {
|
| + type: Boolean,
|
| + value: false,
|
| + observer: '_noinkChanged'
|
| + },
|
| +
|
| + /**
|
| + * If true, the bottom bar to indicate the selected tab will not be shown.
|
| + */
|
| + noBar: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, the slide effect for the bottom bar is disabled.
|
| + */
|
| + noSlide: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, tabs are scrollable and the tab width is based on the label width.
|
| + */
|
| + scrollable: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, tabs expand to fit their container. This currently only applies when
|
| + * scrollable is true.
|
| + */
|
| + fitContainer: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, dragging on the tabs to scroll is disabled.
|
| + */
|
| + disableDrag: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, scroll buttons (left/right arrow) will be hidden for scrollable tabs.
|
| + */
|
| + hideScrollButtons: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * If true, the tabs are aligned to bottom (the selection bar appears at the top).
|
| + */
|
| + alignBottom: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + selectable: {
|
| + type: String,
|
| + value: 'paper-tab'
|
| + },
|
| +
|
| + /**
|
| + * If true, tabs are automatically selected when focused using the
|
| + * keyboard.
|
| + */
|
| + autoselect: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * The delay (in milliseconds) between when the user stops interacting
|
| + * with the tabs through the keyboard and when the focused item is
|
| + * automatically selected (if `autoselect` is true).
|
| + */
|
| + autoselectDelay: {
|
| + type: Number,
|
| + value: 0
|
| + },
|
| +
|
| + _step: {
|
| + type: Number,
|
| + value: 10
|
| + },
|
| +
|
| + _holdDelay: {
|
| + type: Number,
|
| + value: 1
|
| + },
|
| +
|
| + _leftHidden: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + _rightHidden: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + _previousTab: {
|
| + type: Object
|
| }
|
| },
|
|
|
| - /**
|
| - * The element to fit `this` into.
|
| - */
|
| - fitInto: {
|
| - type: Object,
|
| - value: window
|
| + hostAttributes: {
|
| + role: 'tablist'
|
| },
|
|
|
| - /**
|
| - * Will position the element around the positionTarget without overlapping it.
|
| - */
|
| - noOverlap: {
|
| - type: Boolean
|
| + listeners: {
|
| + 'iron-resize': '_onTabSizingChanged',
|
| + 'iron-items-changed': '_onTabSizingChanged',
|
| + 'iron-select': '_onIronSelect',
|
| + 'iron-deselect': '_onIronDeselect'
|
| },
|
|
|
| - /**
|
| - * The element that should be used to position the element. If not set, it will
|
| - * default to the parent node.
|
| - * @type {!Element}
|
| - */
|
| - positionTarget: {
|
| - type: Element
|
| + keyBindings: {
|
| + 'left:keyup right:keyup': '_onArrowKeyup'
|
| },
|
|
|
| - /**
|
| - * The orientation against which to align the element horizontally
|
| - * relative to the `positionTarget`. Possible values are "left", "right", "auto".
|
| - */
|
| - horizontalAlign: {
|
| - type: String
|
| + created: function() {
|
| + this._holdJob = null;
|
| + this._pendingActivationItem = undefined;
|
| + this._pendingActivationTimeout = undefined;
|
| + this._bindDelayedActivationHandler = this._delayedActivationHandler.bind(this);
|
| + this.addEventListener('blur', this._onBlurCapture.bind(this), true);
|
| },
|
|
|
| - /**
|
| - * The orientation against which to align the element vertically
|
| - * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
|
| - */
|
| - verticalAlign: {
|
| - type: String
|
| + ready: function() {
|
| + this.setScrollDirection('y', this.$.tabsContainer);
|
| },
|
|
|
| - /**
|
| - * If true, it will use `horizontalAlign` and `verticalAlign` values as preferred alignment
|
| - * and if there's not enough space, it will pick the values which minimize the cropping.
|
| - */
|
| - dynamicAlign: {
|
| - type: Boolean
|
| + detached: function() {
|
| + this._cancelPendingActivation();
|
| + },
|
| +
|
| + _noinkChanged: function(noink) {
|
| + var childTabs = Polymer.dom(this).querySelectorAll('paper-tab');
|
| + childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAttribute);
|
| + },
|
| +
|
| + _setNoinkAttribute: function(element) {
|
| + element.setAttribute('noink', '');
|
| + },
|
| +
|
| + _removeNoinkAttribute: function(element) {
|
| + element.removeAttribute('noink');
|
| + },
|
| +
|
| + _computeScrollButtonClass: function(hideThisButton, scrollable, hideScrollButtons) {
|
| + if (!scrollable || hideScrollButtons) {
|
| + return 'hidden';
|
| + }
|
| +
|
| + if (hideThisButton) {
|
| + return 'not-visible';
|
| + }
|
| +
|
| + return '';
|
| + },
|
| +
|
| + _computeTabsContentClass: function(scrollable, fitContainer) {
|
| + return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '') : ' fit-container';
|
| + },
|
| +
|
| + _computeSelectionBarClass: function(noBar, alignBottom) {
|
| + if (noBar) {
|
| + return 'hidden';
|
| + } else if (alignBottom) {
|
| + return 'align-bottom';
|
| + }
|
| +
|
| + return '';
|
| + },
|
| +
|
| + // TODO(cdata): Add `track` response back in when gesture lands.
|
| +
|
| + _onTabSizingChanged: function() {
|
| + this.debounce('_onTabSizingChanged', function() {
|
| + this._scroll();
|
| + this._tabChanged(this.selectedItem);
|
| + }, 10);
|
| + },
|
| +
|
| + _onIronSelect: function(event) {
|
| + this._tabChanged(event.detail.item, this._previousTab);
|
| + this._previousTab = event.detail.item;
|
| + this.cancelDebouncer('tab-changed');
|
| + },
|
| +
|
| + _onIronDeselect: function(event) {
|
| + this.debounce('tab-changed', function() {
|
| + this._tabChanged(null, this._previousTab);
|
| + this._previousTab = null;
|
| + // See polymer/polymer#1305
|
| + }, 1);
|
| + },
|
| +
|
| + _activateHandler: function() {
|
| + // Cancel item activations scheduled by keyboard events when any other
|
| + // action causes an item to be activated (e.g. clicks).
|
| + this._cancelPendingActivation();
|
| +
|
| + Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments);
|
| },
|
|
|
| /**
|
| - * The same as setting margin-left and margin-right css properties.
|
| - * @deprecated
|
| + * Activates an item after a delay (in milliseconds).
|
| */
|
| - horizontalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| + _scheduleActivation: function(item, delay) {
|
| + this._pendingActivationItem = item;
|
| + this._pendingActivationTimeout = this.async(
|
| + this._bindDelayedActivationHandler, delay);
|
| },
|
|
|
| /**
|
| - * The same as setting margin-top and margin-bottom css properties.
|
| - * @deprecated
|
| + * Activates the last item given to `_scheduleActivation`.
|
| */
|
| - verticalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| + _delayedActivationHandler: function() {
|
| + var item = this._pendingActivationItem;
|
| + this._pendingActivationItem = undefined;
|
| + this._pendingActivationTimeout = undefined;
|
| + item.fire(this.activateEvent, null, {
|
| + bubbles: true,
|
| + cancelable: true
|
| + });
|
| },
|
|
|
| /**
|
| - * Set to true to auto-fit on attach.
|
| + * Cancels a previously scheduled item activation made with
|
| + * `_scheduleActivation`.
|
| */
|
| - autoFitOnAttach: {
|
| - type: Boolean,
|
| - value: false
|
| + _cancelPendingActivation: function() {
|
| + if (this._pendingActivationTimeout !== undefined) {
|
| + this.cancelAsync(this._pendingActivationTimeout);
|
| + this._pendingActivationItem = undefined;
|
| + this._pendingActivationTimeout = undefined;
|
| + }
|
| },
|
|
|
| - /** @type {?Object} */
|
| - _fitInfo: {
|
| - type: Object
|
| - }
|
| - },
|
| + _onArrowKeyup: function(event) {
|
| + if (this.autoselect) {
|
| + this._scheduleActivation(this.focusedItem, this.autoselectDelay);
|
| + }
|
| + },
|
|
|
| - get _fitWidth() {
|
| - var fitWidth;
|
| - if (this.fitInto === window) {
|
| - fitWidth = this.fitInto.innerWidth;
|
| - } else {
|
| - fitWidth = this.fitInto.getBoundingClientRect().width;
|
| - }
|
| - return fitWidth;
|
| - },
|
| + _onBlurCapture: function(event) {
|
| + // Cancel a scheduled item activation (if any) when that item is
|
| + // blurred.
|
| + if (event.target === this._pendingActivationItem) {
|
| + this._cancelPendingActivation();
|
| + }
|
| + },
|
|
|
| - get _fitHeight() {
|
| - var fitHeight;
|
| - if (this.fitInto === window) {
|
| - fitHeight = this.fitInto.innerHeight;
|
| - } else {
|
| - fitHeight = this.fitInto.getBoundingClientRect().height;
|
| - }
|
| - return fitHeight;
|
| - },
|
| + get _tabContainerScrollSize () {
|
| + return Math.max(
|
| + 0,
|
| + this.$.tabsContainer.scrollWidth -
|
| + this.$.tabsContainer.offsetWidth
|
| + );
|
| + },
|
|
|
| - get _fitLeft() {
|
| - var fitLeft;
|
| - if (this.fitInto === window) {
|
| - fitLeft = 0;
|
| - } else {
|
| - fitLeft = this.fitInto.getBoundingClientRect().left;
|
| - }
|
| - return fitLeft;
|
| - },
|
| + _scroll: function(e, detail) {
|
| + if (!this.scrollable) {
|
| + return;
|
| + }
|
|
|
| - get _fitTop() {
|
| - var fitTop;
|
| - if (this.fitInto === window) {
|
| - fitTop = 0;
|
| - } else {
|
| - fitTop = this.fitInto.getBoundingClientRect().top;
|
| - }
|
| - return fitTop;
|
| - },
|
| + var ddx = (detail && -detail.ddx) || 0;
|
| + this._affectScroll(ddx);
|
| + },
|
|
|
| - /**
|
| - * The element that should be used to position the element,
|
| - * if no position target is configured.
|
| - */
|
| - get _defaultPositionTarget() {
|
| - var parent = Polymer.dom(this).parentNode;
|
| + _down: function(e) {
|
| + // go one beat async to defeat IronMenuBehavior
|
| + // autorefocus-on-no-selection timeout
|
| + this.async(function() {
|
| + if (this._defaultFocusAsync) {
|
| + this.cancelAsync(this._defaultFocusAsync);
|
| + this._defaultFocusAsync = null;
|
| + }
|
| + }, 1);
|
| + },
|
|
|
| - if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
| - parent = parent.host;
|
| - }
|
| + _affectScroll: function(dx) {
|
| + this.$.tabsContainer.scrollLeft += dx;
|
|
|
| - return parent;
|
| - },
|
| + var scrollLeft = this.$.tabsContainer.scrollLeft;
|
|
|
| - /**
|
| - * The horizontal align value, accounting for the RTL/LTR text direction.
|
| - */
|
| - get _localeHorizontalAlign() {
|
| - if (this._isRTL) {
|
| - // In RTL, "left" becomes "right".
|
| - if (this.horizontalAlign === 'right') {
|
| - return 'left';
|
| - }
|
| - if (this.horizontalAlign === 'left') {
|
| - return 'right';
|
| - }
|
| - }
|
| - return this.horizontalAlign;
|
| - },
|
| + this._leftHidden = scrollLeft === 0;
|
| + this._rightHidden = scrollLeft === this._tabContainerScrollSize;
|
| + },
|
|
|
| - attached: function() {
|
| - // Memoize this to avoid expensive calculations & relayouts.
|
| - this._isRTL = window.getComputedStyle(this).direction == 'rtl';
|
| - this.positionTarget = this.positionTarget || this._defaultPositionTarget;
|
| - if (this.autoFitOnAttach) {
|
| - if (window.getComputedStyle(this).display === 'none') {
|
| - setTimeout(function() {
|
| - this.fit();
|
| - }.bind(this));
|
| - } else {
|
| - this.fit();
|
| - }
|
| - }
|
| - },
|
| + _onLeftScrollButtonDown: function() {
|
| + this._scrollToLeft();
|
| + this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDelay);
|
| + },
|
|
|
| - /**
|
| - * Positions and fits the element into the `fitInto` element.
|
| - */
|
| - fit: function() {
|
| - this.position();
|
| - this.constrain();
|
| - this.center();
|
| - },
|
| + _onRightScrollButtonDown: function() {
|
| + this._scrollToRight();
|
| + this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDelay);
|
| + },
|
|
|
| - /**
|
| - * Memoize information needed to position and size the target element.
|
| - * @suppress {deprecated}
|
| - */
|
| - _discoverInfo: function() {
|
| - if (this._fitInfo) {
|
| - return;
|
| - }
|
| - var target = window.getComputedStyle(this);
|
| - var sizer = window.getComputedStyle(this.sizingTarget);
|
| + _onScrollButtonUp: function() {
|
| + clearInterval(this._holdJob);
|
| + this._holdJob = null;
|
| + },
|
|
|
| - this._fitInfo = {
|
| - inlineStyle: {
|
| - top: this.style.top || '',
|
| - left: this.style.left || '',
|
| - position: this.style.position || ''
|
| - },
|
| - sizerInlineStyle: {
|
| - maxWidth: this.sizingTarget.style.maxWidth || '',
|
| - maxHeight: this.sizingTarget.style.maxHeight || '',
|
| - boxSizing: this.sizingTarget.style.boxSizing || ''
|
| - },
|
| - positionedBy: {
|
| - vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
|
| - 'bottom' : null),
|
| - horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
|
| - 'right' : null)
|
| - },
|
| - sizedBy: {
|
| - height: sizer.maxHeight !== 'none',
|
| - width: sizer.maxWidth !== 'none',
|
| - minWidth: parseInt(sizer.minWidth, 10) || 0,
|
| - minHeight: parseInt(sizer.minHeight, 10) || 0
|
| - },
|
| - margin: {
|
| - top: parseInt(target.marginTop, 10) || 0,
|
| - right: parseInt(target.marginRight, 10) || 0,
|
| - bottom: parseInt(target.marginBottom, 10) || 0,
|
| - left: parseInt(target.marginLeft, 10) || 0
|
| + _scrollToLeft: function() {
|
| + this._affectScroll(-this._step);
|
| + },
|
| +
|
| + _scrollToRight: function() {
|
| + this._affectScroll(this._step);
|
| + },
|
| +
|
| + _tabChanged: function(tab, old) {
|
| + if (!tab) {
|
| + // Remove the bar without animation.
|
| + this.$.selectionBar.classList.remove('expand');
|
| + this.$.selectionBar.classList.remove('contract');
|
| + this._positionBar(0, 0);
|
| + return;
|
| }
|
| - };
|
|
|
| - // Support these properties until they are removed.
|
| - if (this.verticalOffset) {
|
| - this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffset;
|
| - this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
|
| - this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
|
| - this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px';
|
| - }
|
| - if (this.horizontalOffset) {
|
| - this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOffset;
|
| - this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
|
| - this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
|
| - this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
|
| - }
|
| - },
|
| + var r = this.$.tabsContent.getBoundingClientRect();
|
| + var w = r.width;
|
| + var tabRect = tab.getBoundingClientRect();
|
| + var tabOffsetLeft = tabRect.left - r.left;
|
|
|
| - /**
|
| - * Resets the target element's position and size constraints, and clear
|
| - * the memoized data.
|
| - */
|
| - resetFit: function() {
|
| - var info = this._fitInfo || {};
|
| - for (var property in info.sizerInlineStyle) {
|
| - this.sizingTarget.style[property] = info.sizerInlineStyle[property];
|
| - }
|
| - for (var property in info.inlineStyle) {
|
| - this.style[property] = info.inlineStyle[property];
|
| - }
|
| + this._pos = {
|
| + width: this._calcPercent(tabRect.width, w),
|
| + left: this._calcPercent(tabOffsetLeft, w)
|
| + };
|
|
|
| - this._fitInfo = null;
|
| - },
|
| + if (this.noSlide || old == null) {
|
| + // Position the bar without animation.
|
| + this.$.selectionBar.classList.remove('expand');
|
| + this.$.selectionBar.classList.remove('contract');
|
| + this._positionBar(this._pos.width, this._pos.left);
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
|
| - * the element or the `fitInto` element has been resized, or if any of the
|
| - * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
|
| - * It preserves the scroll position of the sizingTarget.
|
| - */
|
| - refit: function() {
|
| - var scrollLeft = this.sizingTarget.scrollLeft;
|
| - var scrollTop = this.sizingTarget.scrollTop;
|
| - this.resetFit();
|
| - this.fit();
|
| - this.sizingTarget.scrollLeft = scrollLeft;
|
| - this.sizingTarget.scrollTop = scrollTop;
|
| - },
|
| + var oldRect = old.getBoundingClientRect();
|
| + var oldIndex = this.items.indexOf(old);
|
| + var index = this.items.indexOf(tab);
|
| + var m = 5;
|
|
|
| - /**
|
| - * Positions the element according to `horizontalAlign, verticalAlign`.
|
| - */
|
| - position: function() {
|
| - if (!this.horizontalAlign && !this.verticalAlign) {
|
| - // needs to be centered, and it is done after constrain.
|
| - return;
|
| - }
|
| - this._discoverInfo();
|
| + // bar animation: expand
|
| + this.$.selectionBar.classList.add('expand');
|
|
|
| - this.style.position = 'fixed';
|
| - // Need border-box for margin/padding.
|
| - this.sizingTarget.style.boxSizing = 'border-box';
|
| - // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
|
| - this.style.left = '0px';
|
| - this.style.top = '0px';
|
| + var moveRight = oldIndex < index;
|
| + var isRTL = this._isRTL;
|
| + if (isRTL) {
|
| + moveRight = !moveRight;
|
| + }
|
|
|
| - var rect = this.getBoundingClientRect();
|
| - var positionRect = this.__getNormalizedRect(this.positionTarget);
|
| - var fitRect = this.__getNormalizedRect(this.fitInto);
|
| + if (moveRight) {
|
| + this._positionBar(this._calcPercent(tabRect.left + tabRect.width - oldRect.left, w) - m,
|
| + this._left);
|
| + } else {
|
| + this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tabRect.left, w) - m,
|
| + this._calcPercent(tabOffsetLeft, w) + m);
|
| + }
|
|
|
| - var margin = this._fitInfo.margin;
|
| + if (this.scrollable) {
|
| + this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft);
|
| + }
|
| + },
|
|
|
| - // Consider the margin as part of the size for position calculations.
|
| - var size = {
|
| - width: rect.width + margin.left + margin.right,
|
| - height: rect.height + margin.top + margin.bottom
|
| - };
|
| + _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) {
|
| + var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft;
|
| + if (l < 0) {
|
| + this.$.tabsContainer.scrollLeft += l;
|
| + } else {
|
| + l += (tabWidth - this.$.tabsContainer.offsetWidth);
|
| + if (l > 0) {
|
| + this.$.tabsContainer.scrollLeft += l;
|
| + }
|
| + }
|
| + },
|
|
|
| - var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, fitRect);
|
| + _calcPercent: function(w, w0) {
|
| + return 100 * w / w0;
|
| + },
|
|
|
| - var left = position.left + margin.left;
|
| - var top = position.top + margin.top;
|
| + _positionBar: function(width, left) {
|
| + width = width || 0;
|
| + left = left || 0;
|
|
|
| - // Use original size (without margin).
|
| - var right = Math.min(fitRect.right - margin.right, left + rect.width);
|
| - var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
|
| + this._width = width;
|
| + this._left = left;
|
| + this.transform(
|
| + 'translateX(' + left + '%) scaleX(' + (width / 100) + ')',
|
| + this.$.selectionBar);
|
| + },
|
|
|
| - var minWidth = this._fitInfo.sizedBy.minWidth;
|
| - var minHeight = this._fitInfo.sizedBy.minHeight;
|
| - if (left < margin.left) {
|
| - left = margin.left;
|
| - if (right - left < minWidth) {
|
| - left = right - minWidth;
|
| - }
|
| - }
|
| - if (top < margin.top) {
|
| - top = margin.top;
|
| - if (bottom - top < minHeight) {
|
| - top = bottom - minHeight;
|
| + _onBarTransitionEnd: function(e) {
|
| + var cl = this.$.selectionBar.classList;
|
| + // bar animation: expand -> contract
|
| + if (cl.contains('expand')) {
|
| + cl.remove('expand');
|
| + cl.add('contract');
|
| + this._positionBar(this._pos.width, this._pos.left);
|
| + // bar animation done
|
| + } else if (cl.contains('contract')) {
|
| + cl.remove('contract');
|
| }
|
| }
|
| + });
|
| +(function() {
|
| + 'use strict';
|
|
|
| - this.sizingTarget.style.maxWidth = (right - left) + 'px';
|
| - this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
|
| + Polymer.IronA11yAnnouncer = Polymer({
|
| + is: 'iron-a11y-announcer',
|
|
|
| - // Remove the offset caused by any stacking context.
|
| - this.style.left = (left - rect.left) + 'px';
|
| - this.style.top = (top - rect.top) + 'px';
|
| - },
|
| + properties: {
|
|
|
| - /**
|
| - * Constrains the size of the element to `fitInto` by setting `max-height`
|
| - * and/or `max-width`.
|
| - */
|
| - constrain: function() {
|
| - if (this.horizontalAlign || this.verticalAlign) {
|
| - return;
|
| - }
|
| - this._discoverInfo();
|
| + /**
|
| + * The value of mode is used to set the `aria-live` attribute
|
| + * for the element that will be announced. Valid values are: `off`,
|
| + * `polite` and `assertive`.
|
| + */
|
| + mode: {
|
| + type: String,
|
| + value: 'polite'
|
| + },
|
|
|
| - var info = this._fitInfo;
|
| - // position at (0px, 0px) if not already positioned, so we can measure the natural size.
|
| - if (!info.positionedBy.vertically) {
|
| - this.style.position = 'fixed';
|
| - this.style.top = '0px';
|
| - }
|
| - if (!info.positionedBy.horizontally) {
|
| - this.style.position = 'fixed';
|
| - this.style.left = '0px';
|
| - }
|
| + _text: {
|
| + type: String,
|
| + value: ''
|
| + }
|
| + },
|
|
|
| - // need border-box for margin/padding
|
| - this.sizingTarget.style.boxSizing = 'border-box';
|
| - // constrain the width and height if not already set
|
| - var rect = this.getBoundingClientRect();
|
| - if (!info.sizedBy.height) {
|
| - this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
|
| + created: function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = this;
|
| + }
|
| +
|
| + document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
|
| + },
|
| +
|
| + /**
|
| + * Cause a text string to be announced by screen readers.
|
| + *
|
| + * @param {string} text The text that should be announced.
|
| + */
|
| + announce: function(text) {
|
| + this._text = '';
|
| + this.async(function() {
|
| + this._text = text;
|
| + }, 100);
|
| + },
|
| +
|
| + _onIronAnnounce: function(event) {
|
| + if (event.detail && event.detail.text) {
|
| + this.announce(event.detail.text);
|
| + }
|
| + }
|
| + });
|
| +
|
| + Polymer.IronA11yAnnouncer.instance = null;
|
| +
|
| + Polymer.IronA11yAnnouncer.requestAvailability = function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
|
| + }
|
| +
|
| + document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
|
| + };
|
| + })();
|
| +/**
|
| + * Singleton IronMeta instance.
|
| + */
|
| + Polymer.IronValidatableBehaviorMeta = null;
|
| +
|
| + /**
|
| + * `Use Polymer.IronValidatableBehavior` to implement an element that validates user input.
|
| + * Use the related `Polymer.IronValidatorBehavior` to add custom validation logic to an iron-input.
|
| + *
|
| + * By default, an `<iron-form>` element validates its fields when the user presses the submit button.
|
| + * To validate a form imperatively, call the form's `validate()` method, which in turn will
|
| + * call `validate()` on all its children. By using `Polymer.IronValidatableBehavior`, your
|
| + * custom element will get a public `validate()`, which
|
| + * will return the validity of the element, and a corresponding `invalid` attribute,
|
| + * which can be used for styling.
|
| + *
|
| + * To implement the custom validation logic of your element, you must override
|
| + * the protected `_getValidity()` method of this behaviour, rather than `validate()`.
|
| + * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/simple-element.html)
|
| + * for an example.
|
| + *
|
| + * ### Accessibility
|
| + *
|
| + * Changing the `invalid` property, either manually or by calling `validate()` will update the
|
| + * `aria-invalid` attribute.
|
| + *
|
| + * @demo demo/index.html
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.IronValidatableBehavior = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Name of the validator to use.
|
| + */
|
| + validator: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * True if the last call to `validate` is invalid.
|
| + */
|
| + invalid: {
|
| + notify: true,
|
| + reflectToAttribute: true,
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * This property is deprecated and should not be used. Use the global
|
| + * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead.
|
| + */
|
| + _validatorMeta: {
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * Namespace for this validator. This property is deprecated and should
|
| + * not be used. For all intents and purposes, please consider it a
|
| + * read-only, config-time property.
|
| + */
|
| + validatorType: {
|
| + type: String,
|
| + value: 'validator'
|
| + },
|
| +
|
| + _validator: {
|
| + type: Object,
|
| + computed: '__computeValidator(validator)'
|
| }
|
| - if (!info.sizedBy.width) {
|
| - this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
|
| + },
|
| +
|
| + observers: [
|
| + '_invalidChanged(invalid)'
|
| + ],
|
| +
|
| + registered: function() {
|
| + Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validator'});
|
| + },
|
| +
|
| + _invalidChanged: function() {
|
| + if (this.invalid) {
|
| + this.setAttribute('aria-invalid', 'true');
|
| + } else {
|
| + this.removeAttribute('aria-invalid');
|
| }
|
| },
|
|
|
| /**
|
| - * @protected
|
| - * @deprecated
|
| + * @return {boolean} True if the validator `validator` exists.
|
| */
|
| - _sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| - this.__sizeDimension(rect, positionedBy, start, end, extent);
|
| + hasValidator: function() {
|
| + return this._validator != null;
|
| },
|
|
|
| /**
|
| - * @private
|
| + * Returns true if the `value` is valid, and updates `invalid`. If you want
|
| + * your element to have custom validation logic, do not override this method;
|
| + * override `_getValidity(value)` instead.
|
| +
|
| + * @param {Object} value The value to be validated. By default, it is passed
|
| + * to the validator's `validate()` function, if a validator is set.
|
| + * @return {boolean} True if `value` is valid.
|
| */
|
| - __sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| - var info = this._fitInfo;
|
| - var fitRect = this.__getNormalizedRect(this.fitInto);
|
| - var max = extent === 'Width' ? fitRect.width : fitRect.height;
|
| - var flip = (positionedBy === end);
|
| - var offset = flip ? max - rect[end] : rect[start];
|
| - var margin = info.margin[flip ? start : end];
|
| - var offsetExtent = 'offset' + extent;
|
| - var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
|
| - this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingOffset) + 'px';
|
| + validate: function(value) {
|
| + this.invalid = !this._getValidity(value);
|
| + return !this.invalid;
|
| },
|
|
|
| /**
|
| - * Centers horizontally and vertically if not already positioned. This also sets
|
| - * `position:fixed`.
|
| + * Returns true if `value` is valid. By default, it is passed
|
| + * to the validator's `validate()` function, if a validator is set. You
|
| + * should override this method if you want to implement custom validity
|
| + * logic for your element.
|
| + *
|
| + * @param {Object} value The value to be validated.
|
| + * @return {boolean} True if `value` is valid.
|
| */
|
| - center: function() {
|
| - if (this.horizontalAlign || this.verticalAlign) {
|
| - return;
|
| - }
|
| - this._discoverInfo();
|
|
|
| - var positionedBy = this._fitInfo.positionedBy;
|
| - if (positionedBy.vertically && positionedBy.horizontally) {
|
| - // Already positioned.
|
| - return;
|
| - }
|
| - // Need position:fixed to center
|
| - this.style.position = 'fixed';
|
| - // Take into account the offset caused by parents that create stacking
|
| - // contexts (e.g. with transform: translate3d). Translate to 0,0 and
|
| - // measure the bounding rect.
|
| - if (!positionedBy.vertically) {
|
| - this.style.top = '0px';
|
| - }
|
| - if (!positionedBy.horizontally) {
|
| - this.style.left = '0px';
|
| - }
|
| - // It will take in consideration margins and transforms
|
| - var rect = this.getBoundingClientRect();
|
| - var fitRect = this.__getNormalizedRect(this.fitInto);
|
| - if (!positionedBy.vertically) {
|
| - var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
|
| - this.style.top = top + 'px';
|
| - }
|
| - if (!positionedBy.horizontally) {
|
| - var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
|
| - this.style.left = left + 'px';
|
| + _getValidity: function(value) {
|
| + if (this.hasValidator()) {
|
| + return this._validator.validate(value);
|
| }
|
| + return true;
|
| },
|
|
|
| - __getNormalizedRect: function(target) {
|
| - if (target === document.documentElement || target === window) {
|
| - return {
|
| - top: 0,
|
| - left: 0,
|
| - width: window.innerWidth,
|
| - height: window.innerHeight,
|
| - right: window.innerWidth,
|
| - bottom: window.innerHeight
|
| - };
|
| - }
|
| - return target.getBoundingClientRect();
|
| - },
|
| + __computeValidator: function() {
|
| + return Polymer.IronValidatableBehaviorMeta &&
|
| + Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
|
| + }
|
| + };
|
| +/*
|
| +`<iron-input>` adds two-way binding and custom validators using `Polymer.IronValidatorBehavior`
|
| +to `<input>`.
|
| +
|
| +### Two-way binding
|
| +
|
| +By default you can only get notified of changes to an `input`'s `value` due to user input:
|
| +
|
| + <input value="{{myValue::input}}">
|
| +
|
| +`iron-input` adds the `bind-value` property that mirrors the `value` property, and can be used
|
| +for two-way data binding. `bind-value` will notify if it is changed either by user input or by script.
|
| +
|
| + <input is="iron-input" bind-value="{{myValue}}">
|
| +
|
| +### Custom validators
|
| +
|
| +You can use custom validators that implement `Polymer.IronValidatorBehavior` with `<iron-input>`.
|
| +
|
| + <input is="iron-input" validator="my-custom-validator">
|
| +
|
| +### Stopping invalid input
|
| +
|
| +It may be desirable to only allow users to enter certain characters. You can use the
|
| +`prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
|
| +is separate from validation, and `allowed-pattern` does not affect how the input is validated.
|
| +
|
| + \x3c!-- only allow characters that match [0-9] --\x3e
|
| + <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
|
| +
|
| +@hero hero.svg
|
| +@demo demo/index.html
|
| +*/
|
| +
|
| + Polymer({
|
| +
|
| + is: 'iron-input',
|
| +
|
| + extends: 'input',
|
| +
|
| + behaviors: [
|
| + Polymer.IronValidatableBehavior
|
| + ],
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Use this property instead of `value` for two-way data binding.
|
| + */
|
| + bindValue: {
|
| + observer: '_bindValueChanged',
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * Set to true to prevent the user from entering invalid input. If `allowedPattern` is set,
|
| + * any character typed by the user will be matched against that pattern, and rejected if it's not a match.
|
| + * Pasted input will have each character checked individually; if any character
|
| + * doesn't match `allowedPattern`, the entire pasted string will be rejected.
|
| + * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
|
| + */
|
| + preventInvalidInput: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Regular expression that list the characters allowed as input.
|
| + * This pattern represents the allowed characters for the field; as the user inputs text,
|
| + * each individual character will be checked against the pattern (rather than checking
|
| + * the entire value as a whole). The recommended format should be a list of allowed characters;
|
| + * for example, `[a-zA-Z0-9.+-!;:]`
|
| + */
|
| + allowedPattern: {
|
| + type: String,
|
| + observer: "_allowedPatternChanged"
|
| + },
|
| +
|
| + _previousValidInput: {
|
| + type: String,
|
| + value: ''
|
| + },
|
| +
|
| + _patternAlreadyChecked: {
|
| + type: Boolean,
|
| + value: false
|
| + }
|
| +
|
| + },
|
| +
|
| + listeners: {
|
| + 'input': '_onInput',
|
| + 'keypress': '_onKeypress'
|
| + },
|
| +
|
| + /** @suppress {checkTypes} */
|
| + registered: function() {
|
| + // Feature detect whether we need to patch dispatchEvent (i.e. on FF and IE).
|
| + if (!this._canDispatchEventOnDisabled()) {
|
| + this._origDispatchEvent = this.dispatchEvent;
|
| + this.dispatchEvent = this._dispatchEventFirefoxIE;
|
| + }
|
| + },
|
| +
|
| + created: function() {
|
| + Polymer.IronA11yAnnouncer.requestAvailability();
|
| + },
|
| +
|
| + _canDispatchEventOnDisabled: function() {
|
| + var input = document.createElement('input');
|
| + var canDispatch = false;
|
| + input.disabled = true;
|
| +
|
| + input.addEventListener('feature-check-dispatch-event', function() {
|
| + canDispatch = true;
|
| + });
|
| +
|
| + try {
|
| + input.dispatchEvent(new Event('feature-check-dispatch-event'));
|
| + } catch(e) {}
|
| +
|
| + return canDispatch;
|
| + },
|
| +
|
| + _dispatchEventFirefoxIE: function() {
|
| + // Due to Firefox bug, events fired on disabled form controls can throw
|
| + // errors; furthermore, neither IE nor Firefox will actually dispatch
|
| + // events from disabled form controls; as such, we toggle disable around
|
| + // the dispatch to allow notifying properties to notify
|
| + // See issue #47 for details
|
| + var disabled = this.disabled;
|
| + this.disabled = false;
|
| + this._origDispatchEvent.apply(this, arguments);
|
| + this.disabled = disabled;
|
| + },
|
| +
|
| + get _patternRegExp() {
|
| + var pattern;
|
| + if (this.allowedPattern) {
|
| + pattern = new RegExp(this.allowedPattern);
|
| + } else {
|
| + switch (this.type) {
|
| + case 'number':
|
| + pattern = /[0-9.,e-]/;
|
| + break;
|
| + }
|
| + }
|
| + return pattern;
|
| + },
|
| +
|
| + ready: function() {
|
| + this.bindValue = this.value;
|
| + },
|
| +
|
| + /**
|
| + * @suppress {checkTypes}
|
| + */
|
| + _bindValueChanged: function() {
|
| + if (this.value !== this.bindValue) {
|
| + this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
|
| + }
|
| + // manually notify because we don't want to notify until after setting value
|
| + this.fire('bind-value-changed', {value: this.bindValue});
|
| + },
|
| +
|
| + _allowedPatternChanged: function() {
|
| + // Force to prevent invalid input when an `allowed-pattern` is set
|
| + this.preventInvalidInput = this.allowedPattern ? true : false;
|
| + },
|
| +
|
| + _onInput: function() {
|
| + // Need to validate each of the characters pasted if they haven't
|
| + // been validated inside `_onKeypress` already.
|
| + if (this.preventInvalidInput && !this._patternAlreadyChecked) {
|
| + var valid = this._checkPatternValidity();
|
| + if (!valid) {
|
| + this._announceInvalidCharacter('Invalid string of characters not entered.');
|
| + this.value = this._previousValidInput;
|
| + }
|
| + }
|
| +
|
| + this.bindValue = this.value;
|
| + this._previousValidInput = this.value;
|
| + this._patternAlreadyChecked = false;
|
| + },
|
| +
|
| + _isPrintable: function(event) {
|
| + // What a control/printable character is varies wildly based on the browser.
|
| + // - most control characters (arrows, backspace) do not send a `keypress` event
|
| + // in Chrome, but the *do* on Firefox
|
| + // - in Firefox, when they do send a `keypress` event, control chars have
|
| + // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
|
| + // - printable characters always send a keypress event.
|
| + // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
|
| + // always matches the charCode.
|
| + // None of this makes any sense.
|
| +
|
| + // For these keys, ASCII code == browser keycode.
|
| + var anyNonPrintable =
|
| + (event.keyCode == 8) || // backspace
|
| + (event.keyCode == 9) || // tab
|
| + (event.keyCode == 13) || // enter
|
| + (event.keyCode == 27); // escape
|
| +
|
| + // For these keys, make sure it's a browser keycode and not an ASCII code.
|
| + var mozNonPrintable =
|
| + (event.keyCode == 19) || // pause
|
| + (event.keyCode == 20) || // caps lock
|
| + (event.keyCode == 45) || // insert
|
| + (event.keyCode == 46) || // delete
|
| + (event.keyCode == 144) || // num lock
|
| + (event.keyCode == 145) || // scroll lock
|
| + (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, home, arrows
|
| + (event.keyCode > 111 && event.keyCode < 124); // fn keys
|
| +
|
| + return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
|
| + },
|
| +
|
| + _onKeypress: function(event) {
|
| + if (!this.preventInvalidInput && this.type !== 'number') {
|
| + return;
|
| + }
|
| + var regexp = this._patternRegExp;
|
| + if (!regexp) {
|
| + return;
|
| + }
|
| +
|
| + // Handle special keys and backspace
|
| + if (event.metaKey || event.ctrlKey || event.altKey)
|
| + return;
|
| +
|
| + // Check the pattern either here or in `_onInput`, but not in both.
|
| + this._patternAlreadyChecked = true;
|
| +
|
| + var thisChar = String.fromCharCode(event.charCode);
|
| + if (this._isPrintable(event) && !regexp.test(thisChar)) {
|
| + event.preventDefault();
|
| + this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.');
|
| + }
|
| + },
|
| +
|
| + _checkPatternValidity: function() {
|
| + var regexp = this._patternRegExp;
|
| + if (!regexp) {
|
| + return true;
|
| + }
|
| + for (var i = 0; i < this.value.length; i++) {
|
| + if (!regexp.test(this.value[i])) {
|
| + return false;
|
| + }
|
| + }
|
| + return true;
|
| + },
|
| +
|
| + /**
|
| + * Returns true if `value` is valid. The validator provided in `validator` will be used first,
|
| + * then any constraints.
|
| + * @return {boolean} True if the value is valid.
|
| + */
|
| + validate: function() {
|
| + // First, check what the browser thinks. Some inputs (like type=number)
|
| + // behave weirdly and will set the value to "" if something invalid is
|
| + // entered, but will set the validity correctly.
|
| + var valid = this.checkValidity();
|
| +
|
| + // Only do extra checking if the browser thought this was valid.
|
| + if (valid) {
|
| + // Empty, required input is invalid
|
| + if (this.required && this.value === '') {
|
| + valid = false;
|
| + } else if (this.hasValidator()) {
|
| + valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
| + }
|
| + }
|
| +
|
| + this.invalid = !valid;
|
| + this.fire('iron-input-validate');
|
| + return valid;
|
| + },
|
| +
|
| + _announceInvalidCharacter: function(message) {
|
| + this.fire('iron-announce', { text: message });
|
| + }
|
| + });
|
| +
|
| + /*
|
| + The `iron-input-validate` event is fired whenever `validate()` is called.
|
| + @event iron-input-validate
|
| + */
|
| +Polymer({
|
| + is: 'paper-input-container',
|
| +
|
| + properties: {
|
| + /**
|
| + * Set to true to disable the floating label. The label disappears when the input value is
|
| + * not null.
|
| + */
|
| + noLabelFloat: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to always float the floating label.
|
| + */
|
| + alwaysFloatLabel: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * The attribute to listen for value changes on.
|
| + */
|
| + attrForValue: {
|
| + type: String,
|
| + value: 'bind-value'
|
| + },
|
| +
|
| + /**
|
| + * Set to true to auto-validate the input value when it changes.
|
| + */
|
| + autoValidate: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * True if the input is invalid. This property is set automatically when the input value
|
| + * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
|
| + */
|
| + invalid: {
|
| + observer: '_invalidChanged',
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * True if the input has focus.
|
| + */
|
| + focused: {
|
| + readOnly: true,
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true
|
| + },
|
| +
|
| + _addons: {
|
| + type: Array
|
| + // do not set a default value here intentionally - it will be initialized lazily when a
|
| + // distributed child is attached, which may occur before configuration for this element
|
| + // in polyfill.
|
| + },
|
| +
|
| + _inputHasContent: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + _inputSelector: {
|
| + type: String,
|
| + value: 'input,textarea,.paper-input-input'
|
| + },
|
| +
|
| + _boundOnFocus: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._onFocus.bind(this);
|
| + }
|
| + },
|
| +
|
| + _boundOnBlur: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._onBlur.bind(this);
|
| + }
|
| + },
|
| +
|
| + _boundOnInput: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._onInput.bind(this);
|
| + }
|
| + },
|
| +
|
| + _boundValueChanged: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._onValueChanged.bind(this);
|
| + }
|
| + }
|
| + },
|
| +
|
| + listeners: {
|
| + 'addon-attached': '_onAddonAttached',
|
| + 'iron-input-validate': '_onIronInputValidate'
|
| + },
|
| +
|
| + get _valueChangedEvent() {
|
| + return this.attrForValue + '-changed';
|
| + },
|
| +
|
| + get _propertyForValue() {
|
| + return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
|
| + },
|
| +
|
| + get _inputElement() {
|
| + return Polymer.dom(this).querySelector(this._inputSelector);
|
| + },
|
| +
|
| + get _inputElementValue() {
|
| + return this._inputElement[this._propertyForValue] || this._inputElement.value;
|
| + },
|
| +
|
| + ready: function() {
|
| + if (!this._addons) {
|
| + this._addons = [];
|
| + }
|
| + this.addEventListener('focus', this._boundOnFocus, true);
|
| + this.addEventListener('blur', this._boundOnBlur, true);
|
| + },
|
| +
|
| + attached: function() {
|
| + if (this.attrForValue) {
|
| + this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
|
| + } else {
|
| + this.addEventListener('input', this._onInput);
|
| + }
|
| +
|
| + // Only validate when attached if the input already has a value.
|
| + if (this._inputElementValue != '') {
|
| + this._handleValueAndAutoValidate(this._inputElement);
|
| + } else {
|
| + this._handleValue(this._inputElement);
|
| + }
|
| + },
|
| +
|
| + _onAddonAttached: function(event) {
|
| + if (!this._addons) {
|
| + this._addons = [];
|
| + }
|
| + var target = event.target;
|
| + if (this._addons.indexOf(target) === -1) {
|
| + this._addons.push(target);
|
| + if (this.isAttached) {
|
| + this._handleValue(this._inputElement);
|
| + }
|
| + }
|
| + },
|
| +
|
| + _onFocus: function() {
|
| + this._setFocused(true);
|
| + },
|
| +
|
| + _onBlur: function() {
|
| + this._setFocused(false);
|
| + this._handleValueAndAutoValidate(this._inputElement);
|
| + },
|
| +
|
| + _onInput: function(event) {
|
| + this._handleValueAndAutoValidate(event.target);
|
| + },
|
| +
|
| + _onValueChanged: function(event) {
|
| + this._handleValueAndAutoValidate(event.target);
|
| + },
|
| +
|
| + _handleValue: function(inputElement) {
|
| + var value = this._inputElementValue;
|
| +
|
| + // type="number" hack needed because this.value is empty until it's valid
|
| + if (value || value === 0 || (inputElement.type === 'number' && !inputElement.checkValidity())) {
|
| + this._inputHasContent = true;
|
| + } else {
|
| + this._inputHasContent = false;
|
| + }
|
| +
|
| + this.updateAddons({
|
| + inputElement: inputElement,
|
| + value: value,
|
| + invalid: this.invalid
|
| + });
|
| + },
|
| +
|
| + _handleValueAndAutoValidate: function(inputElement) {
|
| + if (this.autoValidate) {
|
| + var valid;
|
| + if (inputElement.validate) {
|
| + valid = inputElement.validate(this._inputElementValue);
|
| + } else {
|
| + valid = inputElement.checkValidity();
|
| + }
|
| + this.invalid = !valid;
|
| + }
|
| +
|
| + // Call this last to notify the add-ons.
|
| + this._handleValue(inputElement);
|
| + },
|
| +
|
| + _onIronInputValidate: function(event) {
|
| + this.invalid = this._inputElement.invalid;
|
| + },
|
| +
|
| + _invalidChanged: function() {
|
| + if (this._addons) {
|
| + this.updateAddons({invalid: this.invalid});
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Call this to update the state of add-ons.
|
| + * @param {Object} state Add-on state.
|
| + */
|
| + updateAddons: function(state) {
|
| + for (var addon, index = 0; addon = this._addons[index]; index++) {
|
| + addon.update(state);
|
| + }
|
| + },
|
| +
|
| + _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
|
| + var cls = 'input-content';
|
| + if (!noLabelFloat) {
|
| + var label = this.querySelector('label');
|
| +
|
| + if (alwaysFloatLabel || _inputHasContent) {
|
| + cls += ' label-is-floating';
|
| + // If the label is floating, ignore any offsets that may have been
|
| + // applied from a prefix element.
|
| + this.$.labelAndInputContainer.style.position = 'static';
|
| +
|
| + if (invalid) {
|
| + cls += ' is-invalid';
|
| + } else if (focused) {
|
| + cls += " label-is-highlighted";
|
| + }
|
| + } else {
|
| + // When the label is not floating, it should overlap the input element.
|
| + if (label) {
|
| + this.$.labelAndInputContainer.style.position = 'relative';
|
| + }
|
| + }
|
| + } else {
|
| + if (_inputHasContent) {
|
| + cls += ' label-is-hidden';
|
| + }
|
| + }
|
| + return cls;
|
| + },
|
| +
|
| + _computeUnderlineClass: function(focused, invalid) {
|
| + var cls = 'underline';
|
| + if (invalid) {
|
| + cls += ' is-invalid';
|
| + } else if (focused) {
|
| + cls += ' is-highlighted'
|
| + }
|
| + return cls;
|
| + },
|
| +
|
| + _computeAddOnContentClass: function(focused, invalid) {
|
| + var cls = 'add-on-content';
|
| + if (invalid) {
|
| + cls += ' is-invalid';
|
| + } else if (focused) {
|
| + cls += ' is-highlighted'
|
| + }
|
| + return cls;
|
| + }
|
| + });
|
| +/** @polymerBehavior */
|
| + Polymer.PaperSpinnerBehavior = {
|
| +
|
| + listeners: {
|
| + 'animationend': '__reset',
|
| + 'webkitAnimationEnd': '__reset'
|
| + },
|
| +
|
| + properties: {
|
| + /**
|
| + * Displays the spinner.
|
| + */
|
| + active: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true,
|
| + observer: '__activeChanged'
|
| + },
|
| +
|
| + /**
|
| + * Alternative text content for accessibility support.
|
| + * If alt is present, it will add an aria-label whose content matches alt when active.
|
| + * If alt is not present, it will default to 'loading' as the alt value.
|
| + */
|
| + alt: {
|
| + type: String,
|
| + value: 'loading',
|
| + observer: '__altChanged'
|
| + },
|
| +
|
| + __coolingDown: {
|
| + type: Boolean,
|
| + value: false
|
| + }
|
| + },
|
| +
|
| + __computeContainerClasses: function(active, coolingDown) {
|
| + return [
|
| + active || coolingDown ? 'active' : '',
|
| + coolingDown ? 'cooldown' : ''
|
| + ].join(' ');
|
| + },
|
| +
|
| + __activeChanged: function(active, old) {
|
| + this.__setAriaHidden(!active);
|
| + this.__coolingDown = !active && old;
|
| + },
|
| +
|
| + __altChanged: function(alt) {
|
| + // user-provided `aria-label` takes precedence over prototype default
|
| + if (alt === this.getPropertyInfo('alt').value) {
|
| + this.alt = this.getAttribute('aria-label') || alt;
|
| + } else {
|
| + this.__setAriaHidden(alt==='');
|
| + this.setAttribute('aria-label', alt);
|
| + }
|
| + },
|
| +
|
| + __setAriaHidden: function(hidden) {
|
| + var attr = 'aria-hidden';
|
| + if (hidden) {
|
| + this.setAttribute(attr, 'true');
|
| + } else {
|
| + this.removeAttribute(attr);
|
| + }
|
| + },
|
| +
|
| + __reset: function() {
|
| + this.active = false;
|
| + this.__coolingDown = false;
|
| + }
|
| + };
|
| +Polymer({
|
| + is: 'paper-spinner-lite',
|
| +
|
| + behaviors: [
|
| + Polymer.PaperSpinnerBehavior
|
| + ]
|
| + });
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * Implements an incremental search field which can be shown and hidden.
|
| + * Canonical implementation is <cr-search-field>.
|
| + * @polymerBehavior
|
| + */
|
| +var CrSearchFieldBehavior = {
|
| + properties: {
|
| + label: {
|
| + type: String,
|
| + value: '',
|
| + },
|
| +
|
| + clearLabel: {
|
| + type: String,
|
| + value: '',
|
| + },
|
| +
|
| + showingSearch: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + observer: 'showingSearchChanged_',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /** @private */
|
| + lastValue_: {
|
| + type: String,
|
| + value: '',
|
| + },
|
| + },
|
| +
|
| + /**
|
| + * @abstract
|
| + * @return {!HTMLInputElement} The input field element the behavior should
|
| + * use.
|
| + */
|
| + getSearchInput: function() {},
|
| +
|
| + /**
|
| + * @return {string} The value of the search field.
|
| + */
|
| + getValue: function() {
|
| + return this.getSearchInput().value;
|
| + },
|
| +
|
| + /**
|
| + * Sets the value of the search field.
|
| + * @param {string} value
|
| + */
|
| + setValue: function(value) {
|
| + // Use bindValue when setting the input value so that changes propagate
|
| + // correctly.
|
| + this.getSearchInput().bindValue = value;
|
| + this.onValueChanged_(value);
|
| + },
|
| +
|
| + showAndFocus: function() {
|
| + this.showingSearch = true;
|
| + this.focus_();
|
| + },
|
| +
|
| + /** @private */
|
| + focus_: function() {
|
| + this.getSearchInput().focus();
|
| + },
|
| +
|
| + onSearchTermSearch: function() {
|
| + this.onValueChanged_(this.getValue());
|
| + },
|
| +
|
| + /**
|
| + * Updates the internal state of the search field based on a change that has
|
| + * already happened.
|
| + * @param {string} newValue
|
| + * @private
|
| + */
|
| + onValueChanged_: function(newValue) {
|
| + if (newValue == this.lastValue_)
|
| + return;
|
| +
|
| + this.fire('search-changed', newValue);
|
| + this.lastValue_ = newValue;
|
| + },
|
| +
|
| + onSearchTermKeydown: function(e) {
|
| + if (e.key == 'Escape')
|
| + this.showingSearch = false;
|
| + },
|
| +
|
| + /** @private */
|
| + showingSearchChanged_: function() {
|
| + if (this.showingSearch) {
|
| + this.focus_();
|
| + return;
|
| + }
|
| +
|
| + this.setValue('');
|
| + this.getSearchInput().blur();
|
| + }
|
| +};
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +// TODO(tsergeant): Add tests for cr-toolbar-search-field.
|
| +Polymer({
|
| + is: 'cr-toolbar-search-field',
|
| +
|
| + behaviors: [CrSearchFieldBehavior],
|
| +
|
| + properties: {
|
| + narrow: {
|
| + type: Boolean,
|
| + reflectToAttribute: true,
|
| + },
|
| +
|
| + // Prompt text to display in the search field.
|
| + label: String,
|
| +
|
| + // Tooltip to display on the clear search button.
|
| + clearLabel: String,
|
| +
|
| + // When true, show a loading spinner to indicate that the backend is
|
| + // processing the search. Will only show if the search field is open.
|
| + spinnerActive: {
|
| + type: Boolean,
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /** @private */
|
| + hasSearchText_: Boolean,
|
| + },
|
| +
|
| + listeners: {
|
| + 'tap': 'showSearch_',
|
| + 'searchInput.bind-value-changed': 'onBindValueChanged_',
|
| + },
|
| +
|
| + /** @return {!HTMLInputElement} */
|
| + getSearchInput: function() {
|
| + return this.$.searchInput;
|
| + },
|
| +
|
| + /** @return {boolean} */
|
| + isSearchFocused: function() {
|
| + return this.$.searchTerm.focused;
|
| + },
|
| +
|
| + /**
|
| + * @param {boolean} narrow
|
| + * @return {number}
|
| + * @private
|
| + */
|
| + computeIconTabIndex_: function(narrow) {
|
| + return narrow ? 0 : -1;
|
| + },
|
| +
|
| + /**
|
| + * @param {boolean} spinnerActive
|
| + * @param {boolean} showingSearch
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + isSpinnerShown_: function(spinnerActive, showingSearch) {
|
| + return spinnerActive && showingSearch;
|
| + },
|
| +
|
| + /** @private */
|
| + onInputBlur_: function() {
|
| + if (!this.hasSearchText_)
|
| + this.showingSearch = false;
|
| + },
|
| +
|
| + /**
|
| + * Update the state of the search field whenever the underlying input value
|
| + * changes. Unlike onsearch or onkeypress, this is reliably called immediately
|
| + * after any change, whether the result of user input or JS modification.
|
| + * @private
|
| + */
|
| + onBindValueChanged_: function() {
|
| + var newValue = this.$.searchInput.bindValue;
|
| + this.hasSearchText_ = newValue != '';
|
| + if (newValue != '')
|
| + this.showingSearch = true;
|
| + },
|
| +
|
| + /**
|
| + * @param {Event} e
|
| + * @private
|
| + */
|
| + showSearch_: function(e) {
|
| + if (e.target != this.$.clearSearch)
|
| + this.showingSearch = true;
|
| + },
|
| +
|
| + /**
|
| + * @param {Event} e
|
| + * @private
|
| + */
|
| + hideSearch_: function(e) {
|
| + this.showingSearch = false;
|
| + e.stopPropagation();
|
| + }
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +Polymer({
|
| + is: 'cr-toolbar',
|
| +
|
| + properties: {
|
| + // Name to display in the toolbar, in titlecase.
|
| + pageName: String,
|
| +
|
| + // Prompt text to display in the search field.
|
| + searchPrompt: String,
|
| +
|
| + // Tooltip to display on the clear search button.
|
| + clearLabel: String,
|
| +
|
| + // Value is proxied through to cr-toolbar-search-field. When true,
|
| + // the search field will show a processing spinner.
|
| + spinnerActive: Boolean,
|
| +
|
| + // Controls whether the menu button is shown at the start of the menu.
|
| + showMenu: {
|
| + type: Boolean,
|
| + reflectToAttribute: true,
|
| + value: true
|
| + },
|
| +
|
| + /** @private */
|
| + narrow_: {
|
| + type: Boolean,
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /** @private */
|
| + showingSearch_: {
|
| + type: Boolean,
|
| + reflectToAttribute: true,
|
| + },
|
| + },
|
| +
|
| + /** @return {!CrToolbarSearchFieldElement} */
|
| + getSearchField: function() {
|
| + return this.$.search;
|
| + },
|
| +
|
| + /** @private */
|
| + onMenuTap_: function(e) {
|
| + this.fire('cr-menu-tap');
|
| + }
|
| +});
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +Polymer({
|
| + is: 'history-toolbar',
|
| + properties: {
|
| + // Number of history items currently selected.
|
| + count: {
|
| + type: Number,
|
| + value: 0,
|
| + observer: 'changeToolbarView_'
|
| + },
|
| +
|
| + // True if 1 or more history items are selected. When this value changes
|
| + // the background colour changes.
|
| + itemsSelected_: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + // The most recent term entered in the search field. Updated incrementally
|
| + // as the user types.
|
| + searchTerm: {
|
| + type: String,
|
| + notify: true,
|
| + },
|
| +
|
| + // True if the backend is processing and a spinner should be shown in the
|
| + // toolbar.
|
| + spinnerActive: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + hasDrawer: {
|
| + type: Boolean,
|
| + observer: 'hasDrawerChanged_',
|
| + reflectToAttribute: true,
|
| + },
|
| +
|
| + // Whether domain-grouped history is enabled.
|
| + isGroupedMode: {
|
| + type: Boolean,
|
| + reflectToAttribute: true,
|
| + },
|
| +
|
| + // The period to search over. Matches BrowsingHistoryHandler::Range.
|
| + groupedRange: {
|
| + type: Number,
|
| + value: 0,
|
| + reflectToAttribute: true,
|
| + notify: true
|
| + },
|
| +
|
| + // The start time of the query range.
|
| + queryStartTime: String,
|
| +
|
| + // The end time of the query range.
|
| + queryEndTime: String,
|
| + },
|
| +
|
| + /**
|
| + * Changes the toolbar background color depending on whether any history items
|
| + * are currently selected.
|
| + * @private
|
| + */
|
| + changeToolbarView_: function() {
|
| + this.itemsSelected_ = this.count > 0;
|
| + },
|
| +
|
| + /**
|
| + * When changing the search term externally, update the search field to
|
| + * reflect the new search term.
|
| + * @param {string} search
|
| + */
|
| + setSearchTerm: function(search) {
|
| + if (this.searchTerm == search)
|
| + return;
|
| +
|
| + this.searchTerm = search;
|
| + var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
|
| + .getSearchField();
|
| + searchField.showAndFocus();
|
| + searchField.setValue(search);
|
| + },
|
| +
|
| + /**
|
| + * @param {!CustomEvent} event
|
| + * @private
|
| + */
|
| + onSearchChanged_: function(event) {
|
| + this.searchTerm = /** @type {string} */ (event.detail);
|
| + },
|
| +
|
| + onClearSelectionTap_: function() {
|
| + this.fire('unselect-all');
|
| + },
|
| +
|
| + onDeleteTap_: function() {
|
| + this.fire('delete-selected');
|
| + },
|
| +
|
| + get searchBar() {
|
| + return this.$['main-toolbar'].getSearchField();
|
| + },
|
| +
|
| + showSearchField: function() {
|
| + /** @type {!CrToolbarElement} */(this.$['main-toolbar'])
|
| + .getSearchField()
|
| + .showAndFocus();
|
| + },
|
| +
|
| + /**
|
| + * If the user is a supervised user the delete button is not shown.
|
| + * @private
|
| + */
|
| + deletingAllowed_: function() {
|
| + return loadTimeData.getBoolean('allowDeletingHistory');
|
| + },
|
| +
|
| + numberOfItemsSelected_: function(count) {
|
| + return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : '';
|
| + },
|
| +
|
| + getHistoryInterval_: function(queryStartTime, queryEndTime) {
|
| + // TODO(calamity): Fix the format of these dates.
|
| + return loadTimeData.getStringF(
|
| + 'historyInterval', queryStartTime, queryEndTime);
|
| + },
|
| +
|
| + /** @private */
|
| + hasDrawerChanged_: function() {
|
| + this.updateStyles();
|
| + },
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the
|
| + * dialog is closed via close(), a 'close' event is fired. If the dialog is
|
| + * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event.
|
| + * Additionally clients can inspect the dialog's |returnValue| property inside
|
| + * the 'close' event listener to determine whether it was canceled or just
|
| + * closed, where a truthy value means success, and a falsy value means it was
|
| + * canceled.
|
| + */
|
| +Polymer({
|
| + is: 'cr-dialog',
|
| + extends: 'dialog',
|
| +
|
| + cancel: function() {
|
| + this.fire('cancel');
|
| + HTMLDialogElement.prototype.close.call(this, '');
|
| + },
|
| +
|
| + /**
|
| + * @param {string=} opt_returnValue
|
| + * @override
|
| + */
|
| + close: function(opt_returnValue) {
|
| + HTMLDialogElement.prototype.close.call(this, 'success');
|
| + },
|
| +
|
| + /** @return {!PaperIconButtonElement} */
|
| + getCloseButton: function() {
|
| + return this.$.close;
|
| + },
|
| +});
|
| +/**
|
| +`Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
|
| +optionally centers it in the window or another element.
|
| +
|
| +The element will only be sized and/or positioned if it has not already been sized and/or positioned
|
| +by CSS.
|
| +
|
| +CSS properties | Action
|
| +-----------------------------|-------------------------------------------
|
| +`position` set | Element is not centered horizontally or vertically
|
| +`top` or `bottom` set | Element is not vertically centered
|
| +`left` or `right` set | Element is not horizontally centered
|
| +`max-height` set | Element respects `max-height`
|
| +`max-width` set | Element respects `max-width`
|
| +
|
| +`Polymer.IronFitBehavior` can position an element into another element using
|
| +`verticalAlign` and `horizontalAlign`. This will override the element's css position.
|
| +
|
| + <div class="container">
|
| + <iron-fit-impl vertical-align="top" horizontal-align="auto">
|
| + Positioned into the container
|
| + </iron-fit-impl>
|
| + </div>
|
| +
|
| +Use `noOverlap` to position the element around another element without overlapping it.
|
| +
|
| + <div class="container">
|
| + <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
|
| + Positioned around the container
|
| + </iron-fit-impl>
|
| + </div>
|
| +
|
| +@demo demo/index.html
|
| +@polymerBehavior
|
| +*/
|
| +
|
| + Polymer.IronFitBehavior = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
|
| + * but it can be set to a child element. This is useful, for example, for implementing a
|
| + * scrolling region inside the element.
|
| + * @type {!Element}
|
| + */
|
| + sizingTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * The element to fit `this` into.
|
| + */
|
| + fitInto: {
|
| + type: Object,
|
| + value: window
|
| + },
|
| +
|
| + /**
|
| + * Will position the element around the positionTarget without overlapping it.
|
| + */
|
| + noOverlap: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * The element that should be used to position the element. If not set, it will
|
| + * default to the parent node.
|
| + * @type {!Element}
|
| + */
|
| + positionTarget: {
|
| + type: Element
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the element horizontally
|
| + * relative to the `positionTarget`. Possible values are "left", "right", "auto".
|
| + */
|
| + horizontalAlign: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the element vertically
|
| + * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
|
| + */
|
| + verticalAlign: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * If true, it will use `horizontalAlign` and `verticalAlign` values as preferred alignment
|
| + * and if there's not enough space, it will pick the values which minimize the cropping.
|
| + */
|
| + dynamicAlign: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * The same as setting margin-left and margin-right css properties.
|
| + * @deprecated
|
| + */
|
| + horizontalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * The same as setting margin-top and margin-bottom css properties.
|
| + * @deprecated
|
| + */
|
| + verticalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * Set to true to auto-fit on attach.
|
| + */
|
| + autoFitOnAttach: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /** @type {?Object} */
|
| + _fitInfo: {
|
| + type: Object
|
| + }
|
| + },
|
| +
|
| + get _fitWidth() {
|
| + var fitWidth;
|
| + if (this.fitInto === window) {
|
| + fitWidth = this.fitInto.innerWidth;
|
| + } else {
|
| + fitWidth = this.fitInto.getBoundingClientRect().width;
|
| + }
|
| + return fitWidth;
|
| + },
|
| +
|
| + get _fitHeight() {
|
| + var fitHeight;
|
| + if (this.fitInto === window) {
|
| + fitHeight = this.fitInto.innerHeight;
|
| + } else {
|
| + fitHeight = this.fitInto.getBoundingClientRect().height;
|
| + }
|
| + return fitHeight;
|
| + },
|
| +
|
| + get _fitLeft() {
|
| + var fitLeft;
|
| + if (this.fitInto === window) {
|
| + fitLeft = 0;
|
| + } else {
|
| + fitLeft = this.fitInto.getBoundingClientRect().left;
|
| + }
|
| + return fitLeft;
|
| + },
|
| +
|
| + get _fitTop() {
|
| + var fitTop;
|
| + if (this.fitInto === window) {
|
| + fitTop = 0;
|
| + } else {
|
| + fitTop = this.fitInto.getBoundingClientRect().top;
|
| + }
|
| + return fitTop;
|
| + },
|
| +
|
| + /**
|
| + * The element that should be used to position the element,
|
| + * if no position target is configured.
|
| + */
|
| + get _defaultPositionTarget() {
|
| + var parent = Polymer.dom(this).parentNode;
|
| +
|
| + if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
| + parent = parent.host;
|
| + }
|
| +
|
| + return parent;
|
| + },
|
| +
|
| + /**
|
| + * The horizontal align value, accounting for the RTL/LTR text direction.
|
| + */
|
| + get _localeHorizontalAlign() {
|
| + if (this._isRTL) {
|
| + // In RTL, "left" becomes "right".
|
| + if (this.horizontalAlign === 'right') {
|
| + return 'left';
|
| + }
|
| + if (this.horizontalAlign === 'left') {
|
| + return 'right';
|
| + }
|
| + }
|
| + return this.horizontalAlign;
|
| + },
|
| +
|
| + attached: function() {
|
| + // Memoize this to avoid expensive calculations & relayouts.
|
| + this._isRTL = window.getComputedStyle(this).direction == 'rtl';
|
| + this.positionTarget = this.positionTarget || this._defaultPositionTarget;
|
| + if (this.autoFitOnAttach) {
|
| + if (window.getComputedStyle(this).display === 'none') {
|
| + setTimeout(function() {
|
| + this.fit();
|
| + }.bind(this));
|
| + } else {
|
| + this.fit();
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Positions and fits the element into the `fitInto` element.
|
| + */
|
| + fit: function() {
|
| + this.position();
|
| + this.constrain();
|
| + this.center();
|
| + },
|
| +
|
| + /**
|
| + * Memoize information needed to position and size the target element.
|
| + * @suppress {deprecated}
|
| + */
|
| + _discoverInfo: function() {
|
| + if (this._fitInfo) {
|
| + return;
|
| + }
|
| + var target = window.getComputedStyle(this);
|
| + var sizer = window.getComputedStyle(this.sizingTarget);
|
| +
|
| + this._fitInfo = {
|
| + inlineStyle: {
|
| + top: this.style.top || '',
|
| + left: this.style.left || '',
|
| + position: this.style.position || ''
|
| + },
|
| + sizerInlineStyle: {
|
| + maxWidth: this.sizingTarget.style.maxWidth || '',
|
| + maxHeight: this.sizingTarget.style.maxHeight || '',
|
| + boxSizing: this.sizingTarget.style.boxSizing || ''
|
| + },
|
| + positionedBy: {
|
| + vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
|
| + 'bottom' : null),
|
| + horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
|
| + 'right' : null)
|
| + },
|
| + sizedBy: {
|
| + height: sizer.maxHeight !== 'none',
|
| + width: sizer.maxWidth !== 'none',
|
| + minWidth: parseInt(sizer.minWidth, 10) || 0,
|
| + minHeight: parseInt(sizer.minHeight, 10) || 0
|
| + },
|
| + margin: {
|
| + top: parseInt(target.marginTop, 10) || 0,
|
| + right: parseInt(target.marginRight, 10) || 0,
|
| + bottom: parseInt(target.marginBottom, 10) || 0,
|
| + left: parseInt(target.marginLeft, 10) || 0
|
| + }
|
| + };
|
| +
|
| + // Support these properties until they are removed.
|
| + if (this.verticalOffset) {
|
| + this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffset;
|
| + this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
|
| + this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
|
| + this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px';
|
| + }
|
| + if (this.horizontalOffset) {
|
| + this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOffset;
|
| + this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
|
| + this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
|
| + this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Resets the target element's position and size constraints, and clear
|
| + * the memoized data.
|
| + */
|
| + resetFit: function() {
|
| + var info = this._fitInfo || {};
|
| + for (var property in info.sizerInlineStyle) {
|
| + this.sizingTarget.style[property] = info.sizerInlineStyle[property];
|
| + }
|
| + for (var property in info.inlineStyle) {
|
| + this.style[property] = info.inlineStyle[property];
|
| + }
|
| +
|
| + this._fitInfo = null;
|
| + },
|
| +
|
| + /**
|
| + * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
|
| + * the element or the `fitInto` element has been resized, or if any of the
|
| + * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
|
| + * It preserves the scroll position of the sizingTarget.
|
| + */
|
| + refit: function() {
|
| + var scrollLeft = this.sizingTarget.scrollLeft;
|
| + var scrollTop = this.sizingTarget.scrollTop;
|
| + this.resetFit();
|
| + this.fit();
|
| + this.sizingTarget.scrollLeft = scrollLeft;
|
| + this.sizingTarget.scrollTop = scrollTop;
|
| + },
|
| +
|
| + /**
|
| + * Positions the element according to `horizontalAlign, verticalAlign`.
|
| + */
|
| + position: function() {
|
| + if (!this.horizontalAlign && !this.verticalAlign) {
|
| + // needs to be centered, and it is done after constrain.
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + this.style.position = 'fixed';
|
| + // Need border-box for margin/padding.
|
| + this.sizingTarget.style.boxSizing = 'border-box';
|
| + // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
|
| + this.style.left = '0px';
|
| + this.style.top = '0px';
|
| +
|
| + var rect = this.getBoundingClientRect();
|
| + var positionRect = this.__getNormalizedRect(this.positionTarget);
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| +
|
| + var margin = this._fitInfo.margin;
|
| +
|
| + // Consider the margin as part of the size for position calculations.
|
| + var size = {
|
| + width: rect.width + margin.left + margin.right,
|
| + height: rect.height + margin.top + margin.bottom
|
| + };
|
| +
|
| + var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, fitRect);
|
| +
|
| + var left = position.left + margin.left;
|
| + var top = position.top + margin.top;
|
| +
|
| + // Use original size (without margin).
|
| + var right = Math.min(fitRect.right - margin.right, left + rect.width);
|
| + var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
|
| +
|
| + var minWidth = this._fitInfo.sizedBy.minWidth;
|
| + var minHeight = this._fitInfo.sizedBy.minHeight;
|
| + if (left < margin.left) {
|
| + left = margin.left;
|
| + if (right - left < minWidth) {
|
| + left = right - minWidth;
|
| + }
|
| + }
|
| + if (top < margin.top) {
|
| + top = margin.top;
|
| + if (bottom - top < minHeight) {
|
| + top = bottom - minHeight;
|
| + }
|
| + }
|
| +
|
| + this.sizingTarget.style.maxWidth = (right - left) + 'px';
|
| + this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
|
| +
|
| + // Remove the offset caused by any stacking context.
|
| + this.style.left = (left - rect.left) + 'px';
|
| + this.style.top = (top - rect.top) + 'px';
|
| + },
|
| +
|
| + /**
|
| + * Constrains the size of the element to `fitInto` by setting `max-height`
|
| + * and/or `max-width`.
|
| + */
|
| + constrain: function() {
|
| + if (this.horizontalAlign || this.verticalAlign) {
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + var info = this._fitInfo;
|
| + // position at (0px, 0px) if not already positioned, so we can measure the natural size.
|
| + if (!info.positionedBy.vertically) {
|
| + this.style.position = 'fixed';
|
| + this.style.top = '0px';
|
| + }
|
| + if (!info.positionedBy.horizontally) {
|
| + this.style.position = 'fixed';
|
| + this.style.left = '0px';
|
| + }
|
| +
|
| + // need border-box for margin/padding
|
| + this.sizingTarget.style.boxSizing = 'border-box';
|
| + // constrain the width and height if not already set
|
| + var rect = this.getBoundingClientRect();
|
| + if (!info.sizedBy.height) {
|
| + this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
|
| + }
|
| + if (!info.sizedBy.width) {
|
| + this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @protected
|
| + * @deprecated
|
| + */
|
| + _sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| + this.__sizeDimension(rect, positionedBy, start, end, extent);
|
| + },
|
| +
|
| + /**
|
| + * @private
|
| + */
|
| + __sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| + var info = this._fitInfo;
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| + var max = extent === 'Width' ? fitRect.width : fitRect.height;
|
| + var flip = (positionedBy === end);
|
| + var offset = flip ? max - rect[end] : rect[start];
|
| + var margin = info.margin[flip ? start : end];
|
| + var offsetExtent = 'offset' + extent;
|
| + var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
|
| + this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingOffset) + 'px';
|
| + },
|
| +
|
| + /**
|
| + * Centers horizontally and vertically if not already positioned. This also sets
|
| + * `position:fixed`.
|
| + */
|
| + center: function() {
|
| + if (this.horizontalAlign || this.verticalAlign) {
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + var positionedBy = this._fitInfo.positionedBy;
|
| + if (positionedBy.vertically && positionedBy.horizontally) {
|
| + // Already positioned.
|
| + return;
|
| + }
|
| + // Need position:fixed to center
|
| + this.style.position = 'fixed';
|
| + // Take into account the offset caused by parents that create stacking
|
| + // contexts (e.g. with transform: translate3d). Translate to 0,0 and
|
| + // measure the bounding rect.
|
| + if (!positionedBy.vertically) {
|
| + this.style.top = '0px';
|
| + }
|
| + if (!positionedBy.horizontally) {
|
| + this.style.left = '0px';
|
| + }
|
| + // It will take in consideration margins and transforms
|
| + var rect = this.getBoundingClientRect();
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| + if (!positionedBy.vertically) {
|
| + var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
|
| + this.style.top = top + 'px';
|
| + }
|
| + if (!positionedBy.horizontally) {
|
| + var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
|
| + this.style.left = left + 'px';
|
| + }
|
| + },
|
| +
|
| + __getNormalizedRect: function(target) {
|
| + if (target === document.documentElement || target === window) {
|
| + return {
|
| + top: 0,
|
| + left: 0,
|
| + width: window.innerWidth,
|
| + height: window.innerHeight,
|
| + right: window.innerWidth,
|
| + bottom: window.innerHeight
|
| + };
|
| + }
|
| + return target.getBoundingClientRect();
|
| + },
|
| +
|
| + __getCroppedArea: function(position, size, fitRect) {
|
| + var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
|
| + var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
|
| + return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height;
|
| + },
|
| +
|
| +
|
| + __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
|
| + // All the possible configurations.
|
| + // Ordered as top-left, top-right, bottom-left, bottom-right.
|
| + var positions = [{
|
| + verticalAlign: 'top',
|
| + horizontalAlign: 'left',
|
| + top: positionRect.top,
|
| + left: positionRect.left
|
| + }, {
|
| + verticalAlign: 'top',
|
| + horizontalAlign: 'right',
|
| + top: positionRect.top,
|
| + left: positionRect.right - size.width
|
| + }, {
|
| + verticalAlign: 'bottom',
|
| + horizontalAlign: 'left',
|
| + top: positionRect.bottom - size.height,
|
| + left: positionRect.left
|
| + }, {
|
| + verticalAlign: 'bottom',
|
| + horizontalAlign: 'right',
|
| + top: positionRect.bottom - size.height,
|
| + left: positionRect.right - size.width
|
| + }];
|
| +
|
| + if (this.noOverlap) {
|
| + // Duplicate.
|
| + for (var i = 0, l = positions.length; i < l; i++) {
|
| + var copy = {};
|
| + for (var key in positions[i]) {
|
| + copy[key] = positions[i][key];
|
| + }
|
| + positions.push(copy);
|
| + }
|
| + // Horizontal overlap only.
|
| + positions[0].top = positions[1].top += positionRect.height;
|
| + positions[2].top = positions[3].top -= positionRect.height;
|
| + // Vertical overlap only.
|
| + positions[4].left = positions[6].left += positionRect.width;
|
| + positions[5].left = positions[7].left -= positionRect.width;
|
| + }
|
| +
|
| + // Consider auto as null for coding convenience.
|
| + vAlign = vAlign === 'auto' ? null : vAlign;
|
| + hAlign = hAlign === 'auto' ? null : hAlign;
|
| +
|
| + var position;
|
| + for (var i = 0; i < positions.length; i++) {
|
| + var pos = positions[i];
|
| +
|
| + // If both vAlign and hAlign are defined, return exact match.
|
| + // For dynamicAlign and noOverlap we'll have more than one candidate, so
|
| + // we'll have to check the croppedArea to make the best choice.
|
| + if (!this.dynamicAlign && !this.noOverlap &&
|
| + pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
|
| + position = pos;
|
| + break;
|
| + }
|
| +
|
| + // Align is ok if alignment preferences are respected. If no preferences,
|
| + // it is considered ok.
|
| + var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
|
| + (!hAlign || pos.horizontalAlign === hAlign);
|
| +
|
| + // Filter out elements that don't match the alignment (if defined).
|
| + // With dynamicAlign, we need to consider all the positions to find the
|
| + // one that minimizes the cropped area.
|
| + if (!this.dynamicAlign && !alignOk) {
|
| + continue;
|
| + }
|
| +
|
| + position = position || pos;
|
| + pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
|
| + var diff = pos.croppedArea - position.croppedArea;
|
| + // Check which crops less. If it crops equally, check if align is ok.
|
| + if (diff < 0 || (diff === 0 && alignOk)) {
|
| + position = pos;
|
| + }
|
| + // If not cropped and respects the align requirements, keep it.
|
| + // This allows to prefer positions overlapping horizontally over the
|
| + // ones overlapping vertically.
|
| + if (position.croppedArea === 0 && alignOk) {
|
| + break;
|
| + }
|
| + }
|
| +
|
| + return position;
|
| + }
|
| +
|
| + };
|
| +(function() {
|
| +'use strict';
|
| +
|
| + Polymer({
|
| +
|
| + is: 'iron-overlay-backdrop',
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Returns true if the backdrop is opened.
|
| + */
|
| + opened: {
|
| + reflectToAttribute: true,
|
| + type: Boolean,
|
| + value: false,
|
| + observer: '_openedChanged'
|
| + }
|
| +
|
| + },
|
| +
|
| + listeners: {
|
| + 'transitionend': '_onTransitionend'
|
| + },
|
| +
|
| + created: function() {
|
| + // Used to cancel previous requestAnimationFrame calls when opened changes.
|
| + this.__openedRaf = null;
|
| + },
|
| +
|
| + attached: function() {
|
| + this.opened && this._openedChanged(this.opened);
|
| + },
|
| +
|
| + /**
|
| + * Appends the backdrop to document body if needed.
|
| + */
|
| + prepare: function() {
|
| + if (this.opened && !this.parentNode) {
|
| + Polymer.dom(document.body).appendChild(this);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Shows the backdrop.
|
| + */
|
| + open: function() {
|
| + this.opened = true;
|
| + },
|
| +
|
| + /**
|
| + * Hides the backdrop.
|
| + */
|
| + close: function() {
|
| + this.opened = false;
|
| + },
|
| +
|
| + /**
|
| + * Removes the backdrop from document body if needed.
|
| + */
|
| + complete: function() {
|
| + if (!this.opened && this.parentNode === document.body) {
|
| + Polymer.dom(this.parentNode).removeChild(this);
|
| + }
|
| + },
|
| +
|
| + _onTransitionend: function(event) {
|
| + if (event && event.target === this) {
|
| + this.complete();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @param {boolean} opened
|
| + * @private
|
| + */
|
| + _openedChanged: function(opened) {
|
| + if (opened) {
|
| + // Auto-attach.
|
| + this.prepare();
|
| + } else {
|
| + // Animation might be disabled via the mixin or opacity custom property.
|
| + // If it is disabled in other ways, it's up to the user to call complete.
|
| + var cs = window.getComputedStyle(this);
|
| + if (cs.transitionDuration === '0s' || cs.opacity == 0) {
|
| + this.complete();
|
| + }
|
| + }
|
| +
|
| + if (!this.isAttached) {
|
| + return;
|
| + }
|
| +
|
| + // Always cancel previous requestAnimationFrame.
|
| + if (this.__openedRaf) {
|
| + window.cancelAnimationFrame(this.__openedRaf);
|
| + this.__openedRaf = null;
|
| + }
|
| + // Force relayout to ensure proper transitions.
|
| + this.scrollTop = this.scrollTop;
|
| + this.__openedRaf = window.requestAnimationFrame(function() {
|
| + this.__openedRaf = null;
|
| + this.toggleClass('opened', this.opened);
|
| + }.bind(this));
|
| + }
|
| + });
|
| +
|
| +})();
|
| +/**
|
| + * @struct
|
| + * @constructor
|
| + * @private
|
| + */
|
| + Polymer.IronOverlayManagerClass = function() {
|
| + /**
|
| + * Used to keep track of the opened overlays.
|
| + * @private {Array<Element>}
|
| + */
|
| + this._overlays = [];
|
| +
|
| + /**
|
| + * iframes have a default z-index of 100,
|
| + * so this default should be at least that.
|
| + * @private {number}
|
| + */
|
| + this._minimumZ = 101;
|
| +
|
| + /**
|
| + * Memoized backdrop element.
|
| + * @private {Element|null}
|
| + */
|
| + this._backdropElement = null;
|
| +
|
| + // Enable document-wide tap recognizer.
|
| + Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
|
| +
|
| + document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
|
| + document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
|
| + };
|
| +
|
| + Polymer.IronOverlayManagerClass.prototype = {
|
| +
|
| + constructor: Polymer.IronOverlayManagerClass,
|
| +
|
| + /**
|
| + * The shared backdrop element.
|
| + * @type {!Element} backdropElement
|
| + */
|
| + get backdropElement() {
|
| + if (!this._backdropElement) {
|
| + this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| + }
|
| + return this._backdropElement;
|
| + },
|
| +
|
| + /**
|
| + * The deepest active element.
|
| + * @type {!Element} activeElement the active element
|
| + */
|
| + get deepActiveElement() {
|
| + // document.activeElement can be null
|
| + // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
| + // In case of null, default it to document.body.
|
| + var active = document.activeElement || document.body;
|
| + while (active.root && Polymer.dom(active.root).activeElement) {
|
| + active = Polymer.dom(active.root).activeElement;
|
| + }
|
| + return active;
|
| + },
|
| +
|
| + /**
|
| + * Brings the overlay at the specified index to the front.
|
| + * @param {number} i
|
| + * @private
|
| + */
|
| + _bringOverlayAtIndexToFront: function(i) {
|
| + var overlay = this._overlays[i];
|
| + if (!overlay) {
|
| + return;
|
| + }
|
| + var lastI = this._overlays.length - 1;
|
| + var currentOverlay = this._overlays[lastI];
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| + lastI--;
|
| + }
|
| + // If already the top element, return.
|
| + if (i >= lastI) {
|
| + return;
|
| + }
|
| + // Update z-index to be on top.
|
| + var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
| + if (this._getZ(overlay) <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| + }
|
| +
|
| + // Shift other overlays behind the new on top.
|
| + while (i < lastI) {
|
| + this._overlays[i] = this._overlays[i + 1];
|
| + i++;
|
| + }
|
| + this._overlays[lastI] = overlay;
|
| + },
|
| +
|
| + /**
|
| + * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
|
| + * Also updates the backdrop z-index.
|
| + * @param {!Element} overlay
|
| + */
|
| + addOrRemoveOverlay: function(overlay) {
|
| + if (overlay.opened) {
|
| + this.addOverlay(overlay);
|
| + } else {
|
| + this.removeOverlay(overlay);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Tracks overlays for z-index and focus management.
|
| + * Ensures the last added overlay with always-on-top remains on top.
|
| + * @param {!Element} overlay
|
| + */
|
| + addOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i >= 0) {
|
| + this._bringOverlayAtIndexToFront(i);
|
| + this.trackBackdrop();
|
| + return;
|
| + }
|
| + var insertionIndex = this._overlays.length;
|
| + var currentOverlay = this._overlays[insertionIndex - 1];
|
| + var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
|
| + var newZ = this._getZ(overlay);
|
| +
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| + // This bumps the z-index of +2.
|
| + this._applyOverlayZ(currentOverlay, minimumZ);
|
| + insertionIndex--;
|
| + // Update minimumZ to match previous overlay's z-index.
|
| + var previousOverlay = this._overlays[insertionIndex - 1];
|
| + minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
|
| + }
|
| +
|
| + // Update z-index and insert overlay.
|
| + if (newZ <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| + }
|
| + this._overlays.splice(insertionIndex, 0, overlay);
|
| +
|
| + this.trackBackdrop();
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} overlay
|
| + */
|
| + removeOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i === -1) {
|
| + return;
|
| + }
|
| + this._overlays.splice(i, 1);
|
| +
|
| + this.trackBackdrop();
|
| + },
|
| +
|
| + /**
|
| + * Returns the current overlay.
|
| + * @return {Element|undefined}
|
| + */
|
| + currentOverlay: function() {
|
| + var i = this._overlays.length - 1;
|
| + return this._overlays[i];
|
| + },
|
| +
|
| + /**
|
| + * Returns the current overlay z-index.
|
| + * @return {number}
|
| + */
|
| + currentOverlayZ: function() {
|
| + return this._getZ(this.currentOverlay());
|
| + },
|
| +
|
| + /**
|
| + * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
| + * This does not effect the z-index of any existing overlays.
|
| + * @param {number} minimumZ
|
| + */
|
| + ensureMinimumZ: function(minimumZ) {
|
| + this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
| + },
|
| +
|
| + focusOverlay: function() {
|
| + var current = /** @type {?} */ (this.currentOverlay());
|
| + if (current) {
|
| + current._applyFocus();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Updates the backdrop z-index.
|
| + */
|
| + trackBackdrop: function() {
|
| + var overlay = this._overlayWithBackdrop();
|
| + // Avoid creating the backdrop if there is no overlay with backdrop.
|
| + if (!overlay && !this._backdropElement) {
|
| + return;
|
| + }
|
| + this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
|
| + this.backdropElement.opened = !!overlay;
|
| + },
|
| +
|
| + /**
|
| + * @return {Array<Element>}
|
| + */
|
| + getBackdrops: function() {
|
| + var backdrops = [];
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + backdrops.push(this._overlays[i]);
|
| + }
|
| + }
|
| + return backdrops;
|
| + },
|
| +
|
| + /**
|
| + * Returns the z-index for the backdrop.
|
| + * @return {number}
|
| + */
|
| + backdropZ: function() {
|
| + return this._getZ(this._overlayWithBackdrop()) - 1;
|
| + },
|
| +
|
| + /**
|
| + * Returns the first opened overlay that has a backdrop.
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayWithBackdrop: function() {
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + return this._overlays[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Calculates the minimum z-index for the overlay.
|
| + * @param {Element=} overlay
|
| + * @private
|
| + */
|
| + _getZ: function(overlay) {
|
| + var z = this._minimumZ;
|
| + if (overlay) {
|
| + var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
|
| + // Check if is a number
|
| + // Number.isNaN not supported in IE 10+
|
| + if (z1 === z1) {
|
| + z = z1;
|
| + }
|
| + }
|
| + return z;
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} element
|
| + * @param {number|string} z
|
| + * @private
|
| + */
|
| + _setZ: function(element, z) {
|
| + element.style.zIndex = z;
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} overlay
|
| + * @param {number} aboveZ
|
| + * @private
|
| + */
|
| + _applyOverlayZ: function(overlay, aboveZ) {
|
| + this._setZ(overlay, aboveZ + 2);
|
| + },
|
| +
|
| + /**
|
| + * Returns the deepest overlay in the path.
|
| + * @param {Array<Element>=} path
|
| + * @return {Element|undefined}
|
| + * @suppress {missingProperties}
|
| + * @private
|
| + */
|
| + _overlayInPath: function(path) {
|
| + path = path || [];
|
| + for (var i = 0; i < path.length; i++) {
|
| + if (path[i]._manager === this) {
|
| + return path[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the click event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureClick: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + // Check if clicked outside of top overlay.
|
| + if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
|
| + overlay._onCaptureClick(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the focus event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureFocus: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + overlay._onCaptureFocus(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures TAB and ESC keyboard events are delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureKeyDown: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
|
| + overlay._onCaptureEsc(event);
|
| + } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
|
| + overlay._onCaptureTab(event);
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns if the overlay1 should be behind overlay2.
|
| + * @param {!Element} overlay1
|
| + * @param {!Element} overlay2
|
| + * @return {boolean}
|
| + * @suppress {missingProperties}
|
| + * @private
|
| + */
|
| + _shouldBeBehindOverlay: function(overlay1, overlay2) {
|
| + return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
|
| + }
|
| + };
|
| +
|
| + Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
|
| +(function() {
|
| +'use strict';
|
| +
|
| +/**
|
| +Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
|
| +on top of other content. It includes an optional backdrop, and can be used to implement a variety
|
| +of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once.
|
| +
|
| +See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
|
| +for an example.
|
| +
|
| +### Closing and canceling
|
| +
|
| +An overlay may be hidden by closing or canceling. The difference between close and cancel is user
|
| +intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
|
| +it will cancel whenever the user taps outside it or presses the escape key. This behavior is
|
| +configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties.
|
| +`close()` should be called explicitly by the implementer when the user interacts with a control
|
| +in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled'
|
| +event. Call `preventDefault` on this event to prevent the overlay from closing.
|
| +
|
| +### Positioning
|
| +
|
| +By default the element is sized and positioned to fit and centered inside the window. You can
|
| +position and size it manually using CSS. See `Polymer.IronFitBehavior`.
|
| +
|
| +### Backdrop
|
| +
|
| +Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
|
| +appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
|
| +options.
|
| +
|
| +In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
|
| +Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes)
|
| +to achieve a different behavior.
|
| +
|
| +### Limitations
|
| +
|
| +The element is styled to appear on top of other content by setting its `z-index` property. You
|
| +must ensure no element has a stacking context with a higher `z-index` than its parent stacking
|
| +context. You should place this element as a child of `<body>` whenever possible.
|
| +
|
| +@demo demo/index.html
|
| +@polymerBehavior Polymer.IronOverlayBehavior
|
| +*/
|
| +
|
| + Polymer.IronOverlayBehaviorImpl = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * True if the overlay is currently displayed.
|
| + */
|
| + opened: {
|
| + observer: '_openedChanged',
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * True if the overlay was canceled when it was last closed.
|
| + */
|
| + canceled: {
|
| + observer: '_canceledChanged',
|
| + readOnly: true,
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to display a backdrop behind the overlay. It traps the focus
|
| + * within the light DOM of the overlay.
|
| + */
|
| + withBackdrop: {
|
| + observer: '_withBackdropChanged',
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable auto-focusing the overlay or child nodes with
|
| + * the `autofocus` attribute` when the overlay is opened.
|
| + */
|
| + noAutoFocus: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable canceling the overlay with the ESC key.
|
| + */
|
| + noCancelOnEscKey: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable canceling the overlay by clicking outside it.
|
| + */
|
| + noCancelOnOutsideClick: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Contains the reason(s) this overlay was last closed (see `iron-overlay-closed`).
|
| + * `IronOverlayBehavior` provides the `canceled` reason; implementers of the
|
| + * behavior can provide other reasons in addition to `canceled`.
|
| + */
|
| + closingReason: {
|
| + // was a getter before, but needs to be a property so other
|
| + // behaviors can override this.
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * Set to true to enable restoring of focus when overlay is closed.
|
| + */
|
| + restoreFocusOnClose: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to keep overlay always on top.
|
| + */
|
| + alwaysOnTop: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Shortcut to access to the overlay manager.
|
| + * @private
|
| + * @type {Polymer.IronOverlayManagerClass}
|
| + */
|
| + _manager: {
|
| + type: Object,
|
| + value: Polymer.IronOverlayManager
|
| + },
|
| +
|
| + /**
|
| + * The node being focused.
|
| + * @type {?Node}
|
| + */
|
| + _focusedChild: {
|
| + type: Object
|
| + }
|
| +
|
| + },
|
| +
|
| + listeners: {
|
| + 'iron-resize': '_onIronResize'
|
| + },
|
| +
|
| + /**
|
| + * The backdrop element.
|
| + * @type {Element}
|
| + */
|
| + get backdropElement() {
|
| + return this._manager.backdropElement;
|
| + },
|
| +
|
| + /**
|
| + * Returns the node to give focus to.
|
| + * @type {Node}
|
| + */
|
| + get _focusNode() {
|
| + return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
|
| + },
|
| +
|
| + /**
|
| + * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
|
| + * This is used to retrieve which is the first and last focusable nodes in order
|
| + * to wrap the focus for overlays `with-backdrop`.
|
| + *
|
| + * If you know what is your content (specifically the first and last focusable children),
|
| + * you can override this method to return only `[firstFocusable, lastFocusable];`
|
| + * @type {Array<Node>}
|
| + * @protected
|
| + */
|
| + get _focusableNodes() {
|
| + // Elements that can be focused even if they have [disabled] attribute.
|
| + var FOCUSABLE_WITH_DISABLED = [
|
| + 'a[href]',
|
| + 'area[href]',
|
| + 'iframe',
|
| + '[tabindex]',
|
| + '[contentEditable=true]'
|
| + ];
|
| +
|
| + // Elements that cannot be focused if they have [disabled] attribute.
|
| + var FOCUSABLE_WITHOUT_DISABLED = [
|
| + 'input',
|
| + 'select',
|
| + 'textarea',
|
| + 'button'
|
| + ];
|
| +
|
| + // Discard elements with tabindex=-1 (makes them not focusable).
|
| + var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
|
| + ':not([tabindex="-1"]),' +
|
| + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') +
|
| + ':not([disabled]):not([tabindex="-1"])';
|
| +
|
| + var focusables = Polymer.dom(this).querySelectorAll(selector);
|
| + if (this.tabIndex >= 0) {
|
| + // Insert at the beginning because we might have all elements with tabIndex = 0,
|
| + // and the overlay should be the first of the list.
|
| + focusables.splice(0, 0, this);
|
| + }
|
| + // Sort by tabindex.
|
| + return focusables.sort(function (a, b) {
|
| + if (a.tabIndex === b.tabIndex) {
|
| + return 0;
|
| + }
|
| + if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
|
| + return 1;
|
| + }
|
| + return -1;
|
| + });
|
| + },
|
| +
|
| + ready: function() {
|
| + // Used to skip calls to notifyResize and refit while the overlay is animating.
|
| + this.__isAnimating = false;
|
| + // with-backdrop needs tabindex to be set in order to trap the focus.
|
| + // If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
|
| + this.__shouldRemoveTabIndex = false;
|
| + // Used for wrapping the focus on TAB / Shift+TAB.
|
| + this.__firstFocusableNode = this.__lastFocusableNode = null;
|
| + // Used by __onNextAnimationFrame to cancel any previous callback.
|
| + this.__raf = null;
|
| + // Focused node before overlay gets opened. Can be restored on close.
|
| + this.__restoreFocusNode = null;
|
| + this._ensureSetup();
|
| + },
|
| +
|
| + attached: function() {
|
| + // Call _openedChanged here so that position can be computed correctly.
|
| + if (this.opened) {
|
| + this._openedChanged(this.opened);
|
| + }
|
| + this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
|
| + },
|
| +
|
| + detached: function() {
|
| + Polymer.dom(this).unobserveNodes(this._observer);
|
| + this._observer = null;
|
| + if (this.__raf) {
|
| + window.cancelAnimationFrame(this.__raf);
|
| + this.__raf = null;
|
| + }
|
| + this._manager.removeOverlay(this);
|
| + },
|
| +
|
| + /**
|
| + * Toggle the opened state of the overlay.
|
| + */
|
| + toggle: function() {
|
| + this._setCanceled(false);
|
| + this.opened = !this.opened;
|
| + },
|
| +
|
| + /**
|
| + * Open the overlay.
|
| + */
|
| + open: function() {
|
| + this._setCanceled(false);
|
| + this.opened = true;
|
| + },
|
| +
|
| + /**
|
| + * Close the overlay.
|
| + */
|
| + close: function() {
|
| + this._setCanceled(false);
|
| + this.opened = false;
|
| + },
|
| +
|
| + /**
|
| + * Cancels the overlay.
|
| + * @param {Event=} event The original event
|
| + */
|
| + cancel: function(event) {
|
| + var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
|
| + if (cancelEvent.defaultPrevented) {
|
| + return;
|
| + }
|
| +
|
| + this._setCanceled(true);
|
| + this.opened = false;
|
| + },
|
| +
|
| + _ensureSetup: function() {
|
| + if (this._overlaySetup) {
|
| + return;
|
| + }
|
| + this._overlaySetup = true;
|
| + this.style.outline = 'none';
|
| + this.style.display = 'none';
|
| + },
|
| +
|
| + /**
|
| + * Called when `opened` changes.
|
| + * @param {boolean=} opened
|
| + * @protected
|
| + */
|
| + _openedChanged: function(opened) {
|
| + if (opened) {
|
| + this.removeAttribute('aria-hidden');
|
| + } else {
|
| + this.setAttribute('aria-hidden', 'true');
|
| + }
|
| +
|
| + // Defer any animation-related code on attached
|
| + // (_openedChanged gets called again on attached).
|
| + if (!this.isAttached) {
|
| + return;
|
| + }
|
| +
|
| + this.__isAnimating = true;
|
| +
|
| + // Use requestAnimationFrame for non-blocking rendering.
|
| + this.__onNextAnimationFrame(this.__openedChanged);
|
| + },
|
| +
|
| + _canceledChanged: function() {
|
| + this.closingReason = this.closingReason || {};
|
| + this.closingReason.canceled = this.canceled;
|
| + },
|
| +
|
| + _withBackdropChanged: function() {
|
| + // If tabindex is already set, no need to override it.
|
| + if (this.withBackdrop && !this.hasAttribute('tabindex')) {
|
| + this.setAttribute('tabindex', '-1');
|
| + this.__shouldRemoveTabIndex = true;
|
| + } else if (this.__shouldRemoveTabIndex) {
|
| + this.removeAttribute('tabindex');
|
| + this.__shouldRemoveTabIndex = false;
|
| + }
|
| + if (this.opened && this.isAttached) {
|
| + this._manager.trackBackdrop();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * tasks which must occur before opening; e.g. making the element visible.
|
| + * @protected
|
| + */
|
| + _prepareRenderOpened: function() {
|
| + // Store focused node.
|
| + this.__restoreFocusNode = this._manager.deepActiveElement;
|
| +
|
| + // Needed to calculate the size of the overlay so that transitions on its size
|
| + // will have the correct starting points.
|
| + this._preparePositioning();
|
| + this.refit();
|
| + this._finishPositioning();
|
| +
|
| + // Safari will apply the focus to the autofocus element when displayed
|
| + // for the first time, so we make sure to return the focus where it was.
|
| + if (this.noAutoFocus && document.activeElement === this._focusNode) {
|
| + this._focusNode.blur();
|
| + this.__restoreFocusNode.focus();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Tasks which cause the overlay to actually open; typically play an animation.
|
| + * @protected
|
| + */
|
| + _renderOpened: function() {
|
| + this._finishRenderOpened();
|
| + },
|
| +
|
| + /**
|
| + * Tasks which cause the overlay to actually close; typically play an animation.
|
| + * @protected
|
| + */
|
| + _renderClosed: function() {
|
| + this._finishRenderClosed();
|
| + },
|
| +
|
| + /**
|
| + * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
|
| + * @protected
|
| + */
|
| + _finishRenderOpened: function() {
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| +
|
| + // Store it so we don't query too much.
|
| + var focusableNodes = this._focusableNodes;
|
| + this.__firstFocusableNode = focusableNodes[0];
|
| + this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
|
| +
|
| + this.fire('iron-overlay-opened');
|
| + },
|
| +
|
| + /**
|
| + * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
|
| + * @protected
|
| + */
|
| + _finishRenderClosed: function() {
|
| + // Hide the overlay.
|
| + this.style.display = 'none';
|
| + // Reset z-index only at the end of the animation.
|
| + this.style.zIndex = '';
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| + this.fire('iron-overlay-closed', this.closingReason);
|
| + },
|
| +
|
| + _preparePositioning: function() {
|
| + this.style.transition = this.style.webkitTransition = 'none';
|
| + this.style.transform = this.style.webkitTransform = 'none';
|
| + this.style.display = '';
|
| + },
|
| +
|
| + _finishPositioning: function() {
|
| + // First, make it invisible & reactivate animations.
|
| + this.style.display = 'none';
|
| + // Force reflow before re-enabling animations so that they don't start.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| + this.style.transition = this.style.webkitTransition = '';
|
| + this.style.transform = this.style.webkitTransform = '';
|
| + // Now that animations are enabled, make it visible again
|
| + this.style.display = '';
|
| + // Force reflow, so that following animations are properly started.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| + },
|
| +
|
| + /**
|
| + * Applies focus according to the opened state.
|
| + * @protected
|
| + */
|
| + _applyFocus: function() {
|
| + if (this.opened) {
|
| + if (!this.noAutoFocus) {
|
| + this._focusNode.focus();
|
| + }
|
| + }
|
| + else {
|
| + this._focusNode.blur();
|
| + this._focusedChild = null;
|
| + // Restore focus.
|
| + if (this.restoreFocusOnClose && this.__restoreFocusNode) {
|
| + this.__restoreFocusNode.focus();
|
| + }
|
| + this.__restoreFocusNode = null;
|
| + // If many overlays get closed at the same time, one of them would still
|
| + // be the currentOverlay even if already closed, and would call _applyFocus
|
| + // infinitely, so we check for this not to be the current overlay.
|
| + var currentOverlay = this._manager.currentOverlay();
|
| + if (currentOverlay && this !== currentOverlay) {
|
| + currentOverlay._applyFocus();
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Cancels (closes) the overlay. Call when click happens outside the overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureClick: function(event) {
|
| + if (!this.noCancelOnOutsideClick) {
|
| + this.cancel(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureFocus: function (event) {
|
| + if (!this.withBackdrop) {
|
| + return;
|
| + }
|
| + var path = Polymer.dom(event).path;
|
| + if (path.indexOf(this) === -1) {
|
| + event.stopPropagation();
|
| + this._applyFocus();
|
| + } else {
|
| + this._focusedChild = path[0];
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Handles the ESC key event and cancels (closes) the overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureEsc: function(event) {
|
| + if (!this.noCancelOnEscKey) {
|
| + this.cancel(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Handles TAB key events to track focus changes.
|
| + * Will wrap focus for overlays withBackdrop.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureTab: function(event) {
|
| + if (!this.withBackdrop) {
|
| + return;
|
| + }
|
| + // TAB wraps from last to first focusable.
|
| + // Shift + TAB wraps from first to last focusable.
|
| + var shift = event.shiftKey;
|
| + var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
|
| + var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
|
| + var shouldWrap = false;
|
| + if (nodeToCheck === nodeToSet) {
|
| + // If nodeToCheck is the same as nodeToSet, it means we have an overlay
|
| + // with 0 or 1 focusables; in either case we still need to trap the
|
| + // focus within the overlay.
|
| + shouldWrap = true;
|
| + } else {
|
| + // In dom=shadow, the manager will receive focus changes on the main
|
| + // root but not the ones within other shadow roots, so we can't rely on
|
| + // _focusedChild, but we should check the deepest active element.
|
| + var focusedNode = this._manager.deepActiveElement;
|
| + // If the active element is not the nodeToCheck but the overlay itself,
|
| + // it means the focus is about to go outside the overlay, hence we
|
| + // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
|
| + shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
|
| + }
|
| +
|
| + if (shouldWrap) {
|
| + // When the overlay contains the last focusable element of the document
|
| + // and it's already focused, pressing TAB would move the focus outside
|
| + // the document (e.g. to the browser search bar). Similarly, when the
|
| + // overlay contains the first focusable element of the document and it's
|
| + // already focused, pressing Shift+TAB would move the focus outside the
|
| + // document (e.g. to the browser search bar).
|
| + // In both cases, we would not receive a focus event, but only a blur.
|
| + // In order to achieve focus wrapping, we prevent this TAB event and
|
| + // force the focus. This will also prevent the focus to temporarily move
|
| + // outside the overlay, which might cause scrolling.
|
| + event.preventDefault();
|
| + this._focusedChild = nodeToSet;
|
| + this._applyFocus();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Refits if the overlay is opened and not animating.
|
| + * @protected
|
| + */
|
| + _onIronResize: function() {
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.__onNextAnimationFrame(this.refit);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Will call notifyResize if overlay is opened.
|
| + * Can be overridden in order to avoid multiple observers on the same node.
|
| + * @protected
|
| + */
|
| + _onNodesChange: function() {
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.notifyResize();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Tasks executed when opened changes: prepare for the opening, move the
|
| + * focus, update the manager, render opened/closed.
|
| + * @private
|
| + */
|
| + __openedChanged: function() {
|
| + if (this.opened) {
|
| + // Make overlay visible, then add it to the manager.
|
| + this._prepareRenderOpened();
|
| + this._manager.addOverlay(this);
|
| + // Move the focus to the child node with [autofocus].
|
| + this._applyFocus();
|
| +
|
| + this._renderOpened();
|
| + } else {
|
| + // Remove overlay, then restore the focus before actually closing.
|
| + this._manager.removeOverlay(this);
|
| + this._applyFocus();
|
| +
|
| + this._renderClosed();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Executes a callback on the next animation frame, overriding any previous
|
| + * callback awaiting for the next animation frame. e.g.
|
| + * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
|
| + * `callback1` will never be invoked.
|
| + * @param {!Function} callback Its `this` parameter is the overlay itself.
|
| + * @private
|
| + */
|
| + __onNextAnimationFrame: function(callback) {
|
| + if (this.__raf) {
|
| + window.cancelAnimationFrame(this.__raf);
|
| + }
|
| + var self = this;
|
| + this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
|
| + self.__raf = null;
|
| + callback.call(self);
|
| + });
|
| + }
|
| +
|
| + };
|
| +
|
| + /** @polymerBehavior */
|
| + Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| +
|
| + /**
|
| + * Fired after the overlay opens.
|
| + * @event iron-overlay-opened
|
| + */
|
| +
|
| + /**
|
| + * Fired when the overlay is canceled, but before it is closed.
|
| + * @event iron-overlay-canceled
|
| + * @param {Event} event The closing of the overlay can be prevented
|
| + * by calling `event.preventDefault()`. The `event.detail` is the original event that
|
| + * originated the canceling (e.g. ESC keyboard event or click event outside the overlay).
|
| + */
|
| +
|
| + /**
|
| + * Fired after the overlay closes.
|
| + * @event iron-overlay-closed
|
| + * @param {Event} event The `event.detail` is the `closingReason` property
|
| + * (contains `canceled`, whether the overlay was canceled).
|
| + */
|
| +
|
| +})();
|
| +/**
|
| + * `Polymer.NeonAnimatableBehavior` is implemented by elements containing animations for use with
|
| + * elements implementing `Polymer.NeonAnimationRunnerBehavior`.
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.NeonAnimatableBehavior = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Animation configuration. See README for more info.
|
| + */
|
| + animationConfig: {
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * Convenience property for setting an 'entry' animation. Do not set `animationConfig.entry`
|
| + * manually if using this. The animated node is set to `this` if using this property.
|
| + */
|
| + entryAnimation: {
|
| + observer: '_entryAnimationChanged',
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * Convenience property for setting an 'exit' animation. Do not set `animationConfig.exit`
|
| + * manually if using this. The animated node is set to `this` if using this property.
|
| + */
|
| + exitAnimation: {
|
| + observer: '_exitAnimationChanged',
|
| + type: String
|
| + }
|
| +
|
| + },
|
| +
|
| + _entryAnimationChanged: function() {
|
| + this.animationConfig = this.animationConfig || {};
|
| + this.animationConfig['entry'] = [{
|
| + name: this.entryAnimation,
|
| + node: this
|
| + }];
|
| + },
|
| +
|
| + _exitAnimationChanged: function() {
|
| + this.animationConfig = this.animationConfig || {};
|
| + this.animationConfig['exit'] = [{
|
| + name: this.exitAnimation,
|
| + node: this
|
| + }];
|
| + },
|
| +
|
| + _copyProperties: function(config1, config2) {
|
| + // shallowly copy properties from config2 to config1
|
| + for (var property in config2) {
|
| + config1[property] = config2[property];
|
| + }
|
| + },
|
| +
|
| + _cloneConfig: function(config) {
|
| + var clone = {
|
| + isClone: true
|
| + };
|
| + this._copyProperties(clone, config);
|
| + return clone;
|
| + },
|
| +
|
| + _getAnimationConfigRecursive: function(type, map, allConfigs) {
|
| + if (!this.animationConfig) {
|
| + return;
|
| + }
|
| +
|
| + if(this.animationConfig.value && typeof this.animationConfig.value === 'function') {
|
| + this._warn(this._logf('playAnimation', "Please put 'animationConfig' inside of your components 'properties' object instead of outside of it."));
|
| + return;
|
| + }
|
| +
|
| + // type is optional
|
| + var thisConfig;
|
| + if (type) {
|
| + thisConfig = this.animationConfig[type];
|
| + } else {
|
| + thisConfig = this.animationConfig;
|
| + }
|
| +
|
| + if (!Array.isArray(thisConfig)) {
|
| + thisConfig = [thisConfig];
|
| + }
|
| +
|
| + // iterate animations and recurse to process configurations from child nodes
|
| + if (thisConfig) {
|
| + for (var config, index = 0; config = thisConfig[index]; index++) {
|
| + if (config.animatable) {
|
| + config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
|
| + } else {
|
| + if (config.id) {
|
| + var cachedConfig = map[config.id];
|
| + if (cachedConfig) {
|
| + // merge configurations with the same id, making a clone lazily
|
| + if (!cachedConfig.isClone) {
|
| + map[config.id] = this._cloneConfig(cachedConfig)
|
| + cachedConfig = map[config.id];
|
| + }
|
| + this._copyProperties(cachedConfig, config);
|
| + } else {
|
| + // put any configs with an id into a map
|
| + map[config.id] = config;
|
| + }
|
| + } else {
|
| + allConfigs.push(config);
|
| + }
|
| + }
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
|
| + * an animation with an optional type. Elements implementing `Polymer.NeonAnimatableBehavior`
|
| + * should define the property `animationConfig`, which is either a configuration object
|
| + * or a map of animation type to array of configuration objects.
|
| + */
|
| + getAnimationConfig: function(type) {
|
| + var map = {};
|
| + var allConfigs = [];
|
| + this._getAnimationConfigRecursive(type, map, allConfigs);
|
| + // append the configurations saved in the map to the array
|
| + for (var key in map) {
|
| + allConfigs.push(map[key]);
|
| + }
|
| + return allConfigs;
|
| + }
|
| +
|
| + };
|
| +/**
|
| + * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
|
| + *
|
| + * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
|
| + */
|
| + Polymer.NeonAnimationRunnerBehaviorImpl = {
|
| +
|
| + _configureAnimations: function(configs) {
|
| + var results = [];
|
| + if (configs.length > 0) {
|
| + for (var config, index = 0; config = configs[index]; index++) {
|
| + var neonAnimation = document.createElement(config.name);
|
| + // is this element actually a neon animation?
|
| + if (neonAnimation.isNeonAnimation) {
|
| + var result = null;
|
| + // configuration or play could fail if polyfills aren't loaded
|
| + try {
|
| + result = neonAnimation.configure(config);
|
| + // Check if we have an Effect rather than an Animation
|
| + if (typeof result.cancel != 'function') {
|
| + result = document.timeline.play(result);
|
| + }
|
| + } catch (e) {
|
| + result = null;
|
| + console.warn('Couldnt play', '(', config.name, ').', e);
|
| + }
|
| + if (result) {
|
| + results.push({
|
| + neonAnimation: neonAnimation,
|
| + config: config,
|
| + animation: result,
|
| + });
|
| + }
|
| + } else {
|
| + console.warn(this.is + ':', config.name, 'not found!');
|
| + }
|
| + }
|
| + }
|
| + return results;
|
| + },
|
| +
|
| + _shouldComplete: function(activeEntries) {
|
| + var finished = true;
|
| + for (var i = 0; i < activeEntries.length; i++) {
|
| + if (activeEntries[i].animation.playState != 'finished') {
|
| + finished = false;
|
| + break;
|
| + }
|
| + }
|
| + return finished;
|
| + },
|
| +
|
| + _complete: function(activeEntries) {
|
| + for (var i = 0; i < activeEntries.length; i++) {
|
| + activeEntries[i].neonAnimation.complete(activeEntries[i].config);
|
| + }
|
| + for (var i = 0; i < activeEntries.length; i++) {
|
| + activeEntries[i].animation.cancel();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Plays an animation with an optional `type`.
|
| + * @param {string=} type
|
| + * @param {!Object=} cookie
|
| + */
|
| + playAnimation: function(type, cookie) {
|
| + var configs = this.getAnimationConfig(type);
|
| + if (!configs) {
|
| + return;
|
| + }
|
| + this._active = this._active || {};
|
| + if (this._active[type]) {
|
| + this._complete(this._active[type]);
|
| + delete this._active[type];
|
| + }
|
| +
|
| + var activeEntries = this._configureAnimations(configs);
|
| +
|
| + if (activeEntries.length == 0) {
|
| + this.fire('neon-animation-finish', cookie, {bubbles: false});
|
| + return;
|
| + }
|
| +
|
| + this._active[type] = activeEntries;
|
| +
|
| + for (var i = 0; i < activeEntries.length; i++) {
|
| + activeEntries[i].animation.onfinish = function() {
|
| + if (this._shouldComplete(activeEntries)) {
|
| + this._complete(activeEntries);
|
| + delete this._active[type];
|
| + this.fire('neon-animation-finish', cookie, {bubbles: false});
|
| + }
|
| + }.bind(this);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Cancels the currently running animations.
|
| + */
|
| + cancelAnimation: function() {
|
| + for (var k in this._animations) {
|
| + this._animations[k].cancel();
|
| + }
|
| + this._animations = {};
|
| + }
|
| + };
|
| +
|
| + /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */
|
| + Polymer.NeonAnimationRunnerBehavior = [
|
| + Polymer.NeonAnimatableBehavior,
|
| + Polymer.NeonAnimationRunnerBehaviorImpl
|
| + ];
|
| +/**
|
| + * Use `Polymer.NeonAnimationBehavior` to implement an animation.
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.NeonAnimationBehavior = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Defines the animation timing.
|
| + */
|
| + animationTiming: {
|
| + type: Object,
|
| + value: function() {
|
| + return {
|
| + duration: 500,
|
| + easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
| + fill: 'both'
|
| + }
|
| + }
|
| + }
|
| +
|
| + },
|
| +
|
| + /**
|
| + * Can be used to determine that elements implement this behavior.
|
| + */
|
| + isNeonAnimation: true,
|
| +
|
| + /**
|
| + * Do any animation configuration here.
|
| + */
|
| + // configure: function(config) {
|
| + // },
|
| +
|
| + /**
|
| + * Returns the animation timing by mixing in properties from `config` to the defaults defined
|
| + * by the animation.
|
| + */
|
| + timingFromConfig: function(config) {
|
| + if (config.timing) {
|
| + for (var property in config.timing) {
|
| + this.animationTiming[property] = config.timing[property];
|
| + }
|
| + }
|
| + return this.animationTiming;
|
| + },
|
| +
|
| + /**
|
| + * Sets `transform` and `transformOrigin` properties along with the prefixed versions.
|
| + */
|
| + setPrefixedProperty: function(node, property, value) {
|
| + var map = {
|
| + 'transform': ['webkitTransform'],
|
| + 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin']
|
| + };
|
| + var prefixes = map[property];
|
| + for (var prefix, index = 0; prefix = prefixes[index]; index++) {
|
| + node.style[prefix] = value;
|
| + }
|
| + node.style[property] = value;
|
| + },
|
| +
|
| + /**
|
| + * Called when the animation finishes.
|
| + */
|
| + complete: function() {}
|
| +
|
| + };
|
| +Polymer({
|
| +
|
| + is: 'opaque-animation',
|
| +
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
| +
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + this._effect = new KeyframeEffect(node, [
|
| + {'opacity': '1'},
|
| + {'opacity': '1'}
|
| + ], this.timingFromConfig(config));
|
| + node.style.opacity = '0';
|
| + return this._effect;
|
| + },
|
| +
|
| + complete: function(config) {
|
| + config.node.style.opacity = '';
|
| + }
|
| +
|
| + });
|
| +(function() {
|
| + 'use strict';
|
| + // Used to calculate the scroll direction during touch events.
|
| + var LAST_TOUCH_POSITION = {
|
| + pageX: 0,
|
| + pageY: 0
|
| + };
|
| + // Used to avoid computing event.path and filter scrollable nodes (better perf).
|
| + var ROOT_TARGET = null;
|
| + var SCROLLABLE_NODES = [];
|
| +
|
| + /**
|
| + * The IronDropdownScrollManager is intended to provide a central source
|
| + * of authority and control over which elements in a document are currently
|
| + * allowed to scroll.
|
| + */
|
| +
|
| + Polymer.IronDropdownScrollManager = {
|
| +
|
| + /**
|
| + * The current element that defines the DOM boundaries of the
|
| + * scroll lock. This is always the most recently locking element.
|
| + */
|
| + get currentLockingElement() {
|
| + return this._lockingElements[this._lockingElements.length - 1];
|
| + },
|
| +
|
| + /**
|
| + * Returns true if the provided element is "scroll locked", which is to
|
| + * say that it cannot be scrolled via pointer or keyboard interactions.
|
| + *
|
| + * @param {HTMLElement} element An HTML element instance which may or may
|
| + * not be scroll locked.
|
| + */
|
| + elementIsScrollLocked: function(element) {
|
| + var currentLockingElement = this.currentLockingElement;
|
| +
|
| + if (currentLockingElement === undefined)
|
| + return false;
|
| +
|
| + var scrollLocked;
|
| +
|
| + if (this._hasCachedLockedElement(element)) {
|
| + return true;
|
| + }
|
| +
|
| + if (this._hasCachedUnlockedElement(element)) {
|
| + return false;
|
| + }
|
| +
|
| + scrollLocked = !!currentLockingElement &&
|
| + currentLockingElement !== element &&
|
| + !this._composedTreeContains(currentLockingElement, element);
|
| +
|
| + if (scrollLocked) {
|
| + this._lockedElementCache.push(element);
|
| + } else {
|
| + this._unlockedElementCache.push(element);
|
| + }
|
| +
|
| + return scrollLocked;
|
| + },
|
| +
|
| + /**
|
| + * Push an element onto the current scroll lock stack. The most recently
|
| + * pushed element and its children will be considered scrollable. All
|
| + * other elements will not be scrollable.
|
| + *
|
| + * Scroll locking is implemented as a stack so that cases such as
|
| + * dropdowns within dropdowns are handled well.
|
| + *
|
| + * @param {HTMLElement} element The element that should lock scroll.
|
| + */
|
| + pushScrollLock: function(element) {
|
| + // Prevent pushing the same element twice
|
| + if (this._lockingElements.indexOf(element) >= 0) {
|
| + return;
|
| + }
|
| +
|
| + if (this._lockingElements.length === 0) {
|
| + this._lockScrollInteractions();
|
| + }
|
| +
|
| + this._lockingElements.push(element);
|
| +
|
| + this._lockedElementCache = [];
|
| + this._unlockedElementCache = [];
|
| + },
|
| +
|
| + /**
|
| + * Remove an element from the scroll lock stack. The element being
|
| + * removed does not need to be the most recently pushed element. However,
|
| + * the scroll lock constraints only change when the most recently pushed
|
| + * element is removed.
|
| + *
|
| + * @param {HTMLElement} element The element to remove from the scroll
|
| + * lock stack.
|
| + */
|
| + removeScrollLock: function(element) {
|
| + var index = this._lockingElements.indexOf(element);
|
| +
|
| + if (index === -1) {
|
| + return;
|
| + }
|
| +
|
| + this._lockingElements.splice(index, 1);
|
| +
|
| + this._lockedElementCache = [];
|
| + this._unlockedElementCache = [];
|
| +
|
| + if (this._lockingElements.length === 0) {
|
| + this._unlockScrollInteractions();
|
| + }
|
| + },
|
| +
|
| + _lockingElements: [],
|
| +
|
| + _lockedElementCache: null,
|
| +
|
| + _unlockedElementCache: null,
|
| +
|
| + _hasCachedLockedElement: function(element) {
|
| + return this._lockedElementCache.indexOf(element) > -1;
|
| + },
|
| +
|
| + _hasCachedUnlockedElement: function(element) {
|
| + return this._unlockedElementCache.indexOf(element) > -1;
|
| + },
|
| +
|
| + _composedTreeContains: function(element, child) {
|
| + // NOTE(cdata): This method iterates over content elements and their
|
| + // corresponding distributed nodes to implement a contains-like method
|
| + // that pierces through the composed tree of the ShadowDOM. Results of
|
| + // this operation are cached (elsewhere) on a per-scroll-lock basis, to
|
| + // guard against potentially expensive lookups happening repeatedly as
|
| + // a user scrolls / touchmoves.
|
| + var contentElements;
|
| + var distributedNodes;
|
| + var contentIndex;
|
| + var nodeIndex;
|
| +
|
| + if (element.contains(child)) {
|
| + return true;
|
| + }
|
| +
|
| + contentElements = Polymer.dom(element).querySelectorAll('content');
|
| +
|
| + for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) {
|
| +
|
| + distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes();
|
| +
|
| + for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
|
| +
|
| + if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
|
| + return true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + return false;
|
| + },
|
| +
|
| + _scrollInteractionHandler: function(event) {
|
| + // Avoid canceling an event with cancelable=false, e.g. scrolling is in
|
| + // progress and cannot be interrupted.
|
| + if (event.cancelable && this._shouldPreventScrolling(event)) {
|
| + event.preventDefault();
|
| + }
|
| + // If event has targetTouches (touch event), update last touch position.
|
| + if (event.targetTouches) {
|
| + var touch = event.targetTouches[0];
|
| + LAST_TOUCH_POSITION.pageX = touch.pageX;
|
| + LAST_TOUCH_POSITION.pageY = touch.pageY;
|
| + }
|
| + },
|
| +
|
| + _lockScrollInteractions: function() {
|
| + this._boundScrollHandler = this._boundScrollHandler ||
|
| + this._scrollInteractionHandler.bind(this);
|
| + // Modern `wheel` event for mouse wheel scrolling:
|
| + document.addEventListener('wheel', this._boundScrollHandler, true);
|
| + // Older, non-standard `mousewheel` event for some FF:
|
| + document.addEventListener('mousewheel', this._boundScrollHandler, true);
|
| + // IE:
|
| + document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true);
|
| + // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
|
| + document.addEventListener('touchstart', this._boundScrollHandler, true);
|
| + // Mobile devices can scroll on touch move:
|
| + document.addEventListener('touchmove', this._boundScrollHandler, true);
|
| + },
|
| +
|
| + _unlockScrollInteractions: function() {
|
| + document.removeEventListener('wheel', this._boundScrollHandler, true);
|
| + document.removeEventListener('mousewheel', this._boundScrollHandler, true);
|
| + document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
|
| + document.removeEventListener('touchstart', this._boundScrollHandler, true);
|
| + document.removeEventListener('touchmove', this._boundScrollHandler, true);
|
| + },
|
| +
|
| + /**
|
| + * Returns true if the event causes scroll outside the current locking
|
| + * element, e.g. pointer/keyboard interactions, or scroll "leaking"
|
| + * outside the locking element when it is already at its scroll boundaries.
|
| + * @param {!Event} event
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + _shouldPreventScrolling: function(event) {
|
| +
|
| + // Update if root target changed. For touch events, ensure we don't
|
| + // update during touchmove.
|
| + var target = Polymer.dom(event).rootTarget;
|
| + if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
|
| + ROOT_TARGET = target;
|
| + SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
|
| + }
|
| +
|
| + // Prevent event if no scrollable nodes.
|
| + if (!SCROLLABLE_NODES.length) {
|
| + return true;
|
| + }
|
| + // Don't prevent touchstart event inside the locking element when it has
|
| + // scrollable nodes.
|
| + if (event.type === 'touchstart') {
|
| + return false;
|
| + }
|
| + // Get deltaX/Y.
|
| + var info = this._getScrollInfo(event);
|
| + // Prevent if there is no child that can scroll.
|
| + return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY);
|
| + },
|
| +
|
| + /**
|
| + * Returns an array of scrollable nodes up to the current locking element,
|
| + * which is included too if scrollable.
|
| + * @param {!Array<Node>} nodes
|
| + * @return {Array<Node>} scrollables
|
| + * @private
|
| + */
|
| + _getScrollableNodes: function(nodes) {
|
| + var scrollables = [];
|
| + var lockingIndex = nodes.indexOf(this.currentLockingElement);
|
| + // Loop from root target to locking element (included).
|
| + for (var i = 0; i <= lockingIndex; i++) {
|
| + var node = nodes[i];
|
| + // Skip document fragments.
|
| + if (node.nodeType === 11) {
|
| + continue;
|
| + }
|
| + // Check inline style before checking computed style.
|
| + var style = node.style;
|
| + if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
|
| + style = window.getComputedStyle(node);
|
| + }
|
| + if (style.overflow === 'scroll' || style.overflow === 'auto') {
|
| + scrollables.push(node);
|
| + }
|
| + }
|
| + return scrollables;
|
| + },
|
| +
|
| + /**
|
| + * Returns the node that is scrolling. If there is no scrolling,
|
| + * returns undefined.
|
| + * @param {!Array<Node>} nodes
|
| + * @param {number} deltaX Scroll delta on the x-axis
|
| + * @param {number} deltaY Scroll delta on the y-axis
|
| + * @return {Node|undefined}
|
| + * @private
|
| + */
|
| + _getScrollingNode: function(nodes, deltaX, deltaY) {
|
| + // No scroll.
|
| + if (!deltaX && !deltaY) {
|
| + return;
|
| + }
|
| + // Check only one axis according to where there is more scroll.
|
| + // Prefer vertical to horizontal.
|
| + var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
|
| + for (var i = 0; i < nodes.length; i++) {
|
| + var node = nodes[i];
|
| + var canScroll = false;
|
| + if (verticalScroll) {
|
| + // delta < 0 is scroll up, delta > 0 is scroll down.
|
| + canScroll = deltaY < 0 ? node.scrollTop > 0 :
|
| + node.scrollTop < node.scrollHeight - node.clientHeight;
|
| + } else {
|
| + // delta < 0 is scroll left, delta > 0 is scroll right.
|
| + canScroll = deltaX < 0 ? node.scrollLeft > 0 :
|
| + node.scrollLeft < node.scrollWidth - node.clientWidth;
|
| + }
|
| + if (canScroll) {
|
| + return node;
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns scroll `deltaX` and `deltaY`.
|
| + * @param {!Event} event The scroll event
|
| + * @return {{
|
| + * deltaX: number The x-axis scroll delta (positive: scroll right,
|
| + * negative: scroll left, 0: no scroll),
|
| + * deltaY: number The y-axis scroll delta (positive: scroll down,
|
| + * negative: scroll up, 0: no scroll)
|
| + * }} info
|
| + * @private
|
| + */
|
| + _getScrollInfo: function(event) {
|
| + var info = {
|
| + deltaX: event.deltaX,
|
| + deltaY: event.deltaY
|
| + };
|
| + // Already available.
|
| + if ('deltaX' in event) {
|
| + // do nothing, values are already good.
|
| + }
|
| + // Safari has scroll info in `wheelDeltaX/Y`.
|
| + else if ('wheelDeltaX' in event) {
|
| + info.deltaX = -event.wheelDeltaX;
|
| + info.deltaY = -event.wheelDeltaY;
|
| + }
|
| + // Firefox has scroll info in `detail` and `axis`.
|
| + else if ('axis' in event) {
|
| + info.deltaX = event.axis === 1 ? event.detail : 0;
|
| + info.deltaY = event.axis === 2 ? event.detail : 0;
|
| + }
|
| + // On mobile devices, calculate scroll direction.
|
| + else if (event.targetTouches) {
|
| + var touch = event.targetTouches[0];
|
| + // Touch moves from right to left => scrolling goes right.
|
| + info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
|
| + // Touch moves from down to up => scrolling goes down.
|
| + info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
|
| + }
|
| + return info;
|
| + }
|
| + };
|
| + })();
|
| +(function() {
|
| + 'use strict';
|
| +
|
| + Polymer({
|
| + is: 'iron-dropdown',
|
| +
|
| + behaviors: [
|
| + Polymer.IronControlState,
|
| + Polymer.IronA11yKeysBehavior,
|
| + Polymer.IronOverlayBehavior,
|
| + Polymer.NeonAnimationRunnerBehavior
|
| + ],
|
| +
|
| + properties: {
|
| + /**
|
| + * The orientation against which to align the dropdown content
|
| + * horizontally relative to the dropdown trigger.
|
| + * Overridden from `Polymer.IronFitBehavior`.
|
| + */
|
| + horizontalAlign: {
|
| + type: String,
|
| + value: 'left',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the dropdown content
|
| + * vertically relative to the dropdown trigger.
|
| + * Overridden from `Polymer.IronFitBehavior`.
|
| + */
|
| + verticalAlign: {
|
| + type: String,
|
| + value: 'top',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * An animation config. If provided, this will be used to animate the
|
| + * opening of the dropdown.
|
| + */
|
| + openAnimationConfig: {
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * An animation config. If provided, this will be used to animate the
|
| + * closing of the dropdown.
|
| + */
|
| + closeAnimationConfig: {
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * If provided, this will be the element that will be focused when
|
| + * the dropdown opens.
|
| + */
|
| + focusTarget: {
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable animations when opening and closing the
|
| + * dropdown.
|
| + */
|
| + noAnimations: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * By default, the dropdown will constrain scrolling on the page
|
| + * to itself when opened.
|
| + * Set to true in order to prevent scroll from being constrained
|
| + * to the dropdown when it opens.
|
| + */
|
| + allowOutsideScroll: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Callback for scroll events.
|
| + * @type {Function}
|
| + * @private
|
| + */
|
| + _boundOnCaptureScroll: {
|
| + type: Function,
|
| + value: function() {
|
| + return this._onCaptureScroll.bind(this);
|
| + }
|
| + }
|
| + },
|
| +
|
| + listeners: {
|
| + 'neon-animation-finish': '_onNeonAnimationFinish'
|
| + },
|
| +
|
| + observers: [
|
| + '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
|
| + ],
|
| +
|
| + /**
|
| + * The element that is contained by the dropdown, if any.
|
| + */
|
| + get containedElement() {
|
| + return Polymer.dom(this.$.content).getDistributedNodes()[0];
|
| + },
|
| +
|
| + /**
|
| + * The element that should be focused when the dropdown opens.
|
| + * @deprecated
|
| + */
|
| + get _focusTarget() {
|
| + return this.focusTarget || this.containedElement;
|
| + },
|
| +
|
| + ready: function() {
|
| + // Memoized scrolling position, used to block scrolling outside.
|
| + this._scrollTop = 0;
|
| + this._scrollLeft = 0;
|
| + // Used to perform a non-blocking refit on scroll.
|
| + this._refitOnScrollRAF = null;
|
| + },
|
| +
|
| + detached: function() {
|
| + this.cancelAnimation();
|
| + Polymer.IronDropdownScrollManager.removeScrollLock(this);
|
| + },
|
| +
|
| + /**
|
| + * Called when the value of `opened` changes.
|
| + * Overridden from `IronOverlayBehavior`
|
| + */
|
| + _openedChanged: function() {
|
| + if (this.opened && this.disabled) {
|
| + this.cancel();
|
| + } else {
|
| + this.cancelAnimation();
|
| + this.sizingTarget = this.containedElement || this.sizingTarget;
|
| + this._updateAnimationConfig();
|
| + this._saveScrollPosition();
|
| + if (this.opened) {
|
| + document.addEventListener('scroll', this._boundOnCaptureScroll);
|
| + !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScrollLock(this);
|
| + } else {
|
| + document.removeEventListener('scroll', this._boundOnCaptureScroll);
|
| + Polymer.IronDropdownScrollManager.removeScrollLock(this);
|
| + }
|
| + Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + */
|
| + _renderOpened: function() {
|
| + if (!this.noAnimations && this.animationConfig.open) {
|
| + this.$.contentWrapper.classList.add('animating');
|
| + this.playAnimation('open');
|
| + } else {
|
| + Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + */
|
| + _renderClosed: function() {
|
| +
|
| + if (!this.noAnimations && this.animationConfig.close) {
|
| + this.$.contentWrapper.classList.add('animating');
|
| + this.playAnimation('close');
|
| + } else {
|
| + Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Called when animation finishes on the dropdown (when opening or
|
| + * closing). Responsible for "completing" the process of opening or
|
| + * closing the dropdown by positioning it or setting its display to
|
| + * none.
|
| + */
|
| + _onNeonAnimationFinish: function() {
|
| + this.$.contentWrapper.classList.remove('animating');
|
| + if (this.opened) {
|
| + this._finishRenderOpened();
|
| + } else {
|
| + this._finishRenderClosed();
|
| + }
|
| + },
|
| +
|
| + _onCaptureScroll: function() {
|
| + if (!this.allowOutsideScroll) {
|
| + this._restoreScrollPosition();
|
| + } else {
|
| + this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrollRAF);
|
| + this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(this));
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Memoizes the scroll position of the outside scrolling element.
|
| + * @private
|
| + */
|
| + _saveScrollPosition: function() {
|
| + if (document.scrollingElement) {
|
| + this._scrollTop = document.scrollingElement.scrollTop;
|
| + this._scrollLeft = document.scrollingElement.scrollLeft;
|
| + } else {
|
| + // Since we don't know if is the body or html, get max.
|
| + this._scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
|
| + this._scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Resets the scroll position of the outside scrolling element.
|
| + * @private
|
| + */
|
| + _restoreScrollPosition: function() {
|
| + if (document.scrollingElement) {
|
| + document.scrollingElement.scrollTop = this._scrollTop;
|
| + document.scrollingElement.scrollLeft = this._scrollLeft;
|
| + } else {
|
| + // Since we don't know if is the body or html, set both.
|
| + document.documentElement.scrollTop = this._scrollTop;
|
| + document.documentElement.scrollLeft = this._scrollLeft;
|
| + document.body.scrollTop = this._scrollTop;
|
| + document.body.scrollLeft = this._scrollLeft;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Constructs the final animation config from different properties used
|
| + * to configure specific parts of the opening and closing animations.
|
| + */
|
| + _updateAnimationConfig: function() {
|
| + var animations = (this.openAnimationConfig || []).concat(this.closeAnimationConfig || []);
|
| + for (var i = 0; i < animations.length; i++) {
|
| + animations[i].node = this.containedElement;
|
| + }
|
| + this.animationConfig = {
|
| + open: this.openAnimationConfig,
|
| + close: this.closeAnimationConfig
|
| + };
|
| + },
|
| +
|
| + /**
|
| + * Updates the overlay position based on configured horizontal
|
| + * and vertical alignment.
|
| + */
|
| + _updateOverlayPosition: function() {
|
| + if (this.isAttached) {
|
| + // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
|
| + this.notifyResize();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Apply focus to focusTarget or containedElement
|
| + */
|
| + _applyFocus: function () {
|
| + var focusTarget = this.focusTarget || this.containedElement;
|
| + if (focusTarget && this.opened && !this.noAutoFocus) {
|
| + focusTarget.focus();
|
| + } else {
|
| + Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
|
| + }
|
| + }
|
| + });
|
| + })();
|
| +Polymer({
|
|
|
| - __getCroppedArea: function(position, size, fitRect) {
|
| - var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
|
| - var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
|
| - return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height;
|
| - },
|
| + is: 'fade-in-animation',
|
|
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
|
|
| - __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
|
| - // All the possible configurations.
|
| - // Ordered as top-left, top-right, bottom-left, bottom-right.
|
| - var positions = [{
|
| - verticalAlign: 'top',
|
| - horizontalAlign: 'left',
|
| - top: positionRect.top,
|
| - left: positionRect.left
|
| - }, {
|
| - verticalAlign: 'top',
|
| - horizontalAlign: 'right',
|
| - top: positionRect.top,
|
| - left: positionRect.right - size.width
|
| - }, {
|
| - verticalAlign: 'bottom',
|
| - horizontalAlign: 'left',
|
| - top: positionRect.bottom - size.height,
|
| - left: positionRect.left
|
| - }, {
|
| - verticalAlign: 'bottom',
|
| - horizontalAlign: 'right',
|
| - top: positionRect.bottom - size.height,
|
| - left: positionRect.right - size.width
|
| - }];
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + this._effect = new KeyframeEffect(node, [
|
| + {'opacity': '0'},
|
| + {'opacity': '1'}
|
| + ], this.timingFromConfig(config));
|
| + return this._effect;
|
| + }
|
|
|
| - if (this.noOverlap) {
|
| - // Duplicate.
|
| - for (var i = 0, l = positions.length; i < l; i++) {
|
| - var copy = {};
|
| - for (var key in positions[i]) {
|
| - copy[key] = positions[i][key];
|
| - }
|
| - positions.push(copy);
|
| - }
|
| - // Horizontal overlap only.
|
| - positions[0].top = positions[1].top += positionRect.height;
|
| - positions[2].top = positions[3].top -= positionRect.height;
|
| - // Vertical overlap only.
|
| - positions[4].left = positions[6].left += positionRect.width;
|
| - positions[5].left = positions[7].left -= positionRect.width;
|
| - }
|
| + });
|
| +Polymer({
|
|
|
| - // Consider auto as null for coding convenience.
|
| - vAlign = vAlign === 'auto' ? null : vAlign;
|
| - hAlign = hAlign === 'auto' ? null : hAlign;
|
| + is: 'fade-out-animation',
|
|
|
| - var position;
|
| - for (var i = 0; i < positions.length; i++) {
|
| - var pos = positions[i];
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
|
|
| - // If both vAlign and hAlign are defined, return exact match.
|
| - // For dynamicAlign and noOverlap we'll have more than one candidate, so
|
| - // we'll have to check the croppedArea to make the best choice.
|
| - if (!this.dynamicAlign && !this.noOverlap &&
|
| - pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
|
| - position = pos;
|
| - break;
|
| - }
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + this._effect = new KeyframeEffect(node, [
|
| + {'opacity': '1'},
|
| + {'opacity': '0'}
|
| + ], this.timingFromConfig(config));
|
| + return this._effect;
|
| + }
|
|
|
| - // Align is ok if alignment preferences are respected. If no preferences,
|
| - // it is considered ok.
|
| - var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
|
| - (!hAlign || pos.horizontalAlign === hAlign);
|
| + });
|
| +Polymer({
|
| + is: 'paper-menu-grow-height-animation',
|
|
|
| - // Filter out elements that don't match the alignment (if defined).
|
| - // With dynamicAlign, we need to consider all the positions to find the
|
| - // one that minimizes the cropped area.
|
| - if (!this.dynamicAlign && !alignOk) {
|
| - continue;
|
| - }
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
|
|
| - position = position || pos;
|
| - pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
|
| - var diff = pos.croppedArea - position.croppedArea;
|
| - // Check which crops less. If it crops equally, check if align is ok.
|
| - if (diff < 0 || (diff === 0 && alignOk)) {
|
| - position = pos;
|
| - }
|
| - // If not cropped and respects the align requirements, keep it.
|
| - // This allows to prefer positions overlapping horizontally over the
|
| - // ones overlapping vertically.
|
| - if (position.croppedArea === 0 && alignOk) {
|
| - break;
|
| - }
|
| - }
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + var rect = node.getBoundingClientRect();
|
| + var height = rect.height;
|
|
|
| - return position;
|
| + this._effect = new KeyframeEffect(node, [{
|
| + height: (height / 2) + 'px'
|
| + }, {
|
| + height: height + 'px'
|
| + }], this.timingFromConfig(config));
|
| +
|
| + return this._effect;
|
| }
|
| + });
|
|
|
| - };
|
| -(function() {
|
| -'use strict';
|
| + Polymer({
|
| + is: 'paper-menu-grow-width-animation',
|
| +
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
| +
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + var rect = node.getBoundingClientRect();
|
| + var width = rect.width;
|
| +
|
| + this._effect = new KeyframeEffect(node, [{
|
| + width: (width / 2) + 'px'
|
| + }, {
|
| + width: width + 'px'
|
| + }], this.timingFromConfig(config));
|
| +
|
| + return this._effect;
|
| + }
|
| + });
|
|
|
| Polymer({
|
| + is: 'paper-menu-shrink-width-animation',
|
|
|
| - is: 'iron-overlay-backdrop',
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
|
|
| - properties: {
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + var rect = node.getBoundingClientRect();
|
| + var width = rect.width;
|
|
|
| - /**
|
| - * Returns true if the backdrop is opened.
|
| - */
|
| - opened: {
|
| - reflectToAttribute: true,
|
| - type: Boolean,
|
| - value: false,
|
| - observer: '_openedChanged'
|
| - }
|
| + this._effect = new KeyframeEffect(node, [{
|
| + width: width + 'px'
|
| + }, {
|
| + width: width - (width / 20) + 'px'
|
| + }], this.timingFromConfig(config));
|
|
|
| - },
|
| + return this._effect;
|
| + }
|
| + });
|
|
|
| - listeners: {
|
| - 'transitionend': '_onTransitionend'
|
| - },
|
| + Polymer({
|
| + is: 'paper-menu-shrink-height-animation',
|
|
|
| - created: function() {
|
| - // Used to cancel previous requestAnimationFrame calls when opened changes.
|
| - this.__openedRaf = null;
|
| - },
|
| + behaviors: [
|
| + Polymer.NeonAnimationBehavior
|
| + ],
|
|
|
| - attached: function() {
|
| - this.opened && this._openedChanged(this.opened);
|
| - },
|
| + configure: function(config) {
|
| + var node = config.node;
|
| + var rect = node.getBoundingClientRect();
|
| + var height = rect.height;
|
| + var top = rect.top;
|
|
|
| - /**
|
| - * Appends the backdrop to document body if needed.
|
| - */
|
| - prepare: function() {
|
| - if (this.opened && !this.parentNode) {
|
| - Polymer.dom(document.body).appendChild(this);
|
| - }
|
| - },
|
| + this.setPrefixedProperty(node, 'transformOrigin', '0 0');
|
|
|
| - /**
|
| - * Shows the backdrop.
|
| - */
|
| - open: function() {
|
| - this.opened = true;
|
| - },
|
| + this._effect = new KeyframeEffect(node, [{
|
| + height: height + 'px',
|
| + transform: 'translateY(0)'
|
| + }, {
|
| + height: height / 2 + 'px',
|
| + transform: 'translateY(-20px)'
|
| + }], this.timingFromConfig(config));
|
|
|
| - /**
|
| - * Hides the backdrop.
|
| - */
|
| - close: function() {
|
| - this.opened = false;
|
| - },
|
| + return this._effect;
|
| + }
|
| + });
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - /**
|
| - * Removes the backdrop from document body if needed.
|
| - */
|
| - complete: function() {
|
| - if (!this.opened && this.parentNode === document.body) {
|
| - Polymer.dom(this.parentNode).removeChild(this);
|
| - }
|
| - },
|
| +/** Same as paper-menu-button's custom easing cubic-bezier param. */
|
| +var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)';
|
|
|
| - _onTransitionend: function(event) {
|
| - if (event && event.target === this) {
|
| - this.complete();
|
| - }
|
| +Polymer({
|
| + is: 'cr-shared-menu',
|
| +
|
| + behaviors: [Polymer.IronA11yKeysBehavior],
|
| +
|
| + properties: {
|
| + menuOpen: {
|
| + type: Boolean,
|
| + observer: 'menuOpenChanged_',
|
| + value: false,
|
| },
|
|
|
| /**
|
| - * @param {boolean} opened
|
| - * @private
|
| + * The contextual item that this menu was clicked for.
|
| + * e.g. the data used to render an item in an <iron-list> or <dom-repeat>
|
| + * @type {?Object}
|
| */
|
| - _openedChanged: function(opened) {
|
| - if (opened) {
|
| - // Auto-attach.
|
| - this.prepare();
|
| - } else {
|
| - // Animation might be disabled via the mixin or opacity custom property.
|
| - // If it is disabled in other ways, it's up to the user to call complete.
|
| - var cs = window.getComputedStyle(this);
|
| - if (cs.transitionDuration === '0s' || cs.opacity == 0) {
|
| - this.complete();
|
| - }
|
| + itemData: {
|
| + type: Object,
|
| + value: null,
|
| + },
|
| +
|
| + /** @override */
|
| + keyEventTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this.$.menu;
|
| }
|
| + },
|
|
|
| - if (!this.isAttached) {
|
| - return;
|
| + openAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-in-animation',
|
| + timing: {
|
| + delay: 50,
|
| + duration: 200
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-width-animation',
|
| + timing: {
|
| + delay: 50,
|
| + duration: 150,
|
| + easing: SLIDE_CUBIC_BEZIER
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-height-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 275,
|
| + easing: SLIDE_CUBIC_BEZIER
|
| + }
|
| + }];
|
| }
|
| + },
|
|
|
| - // Always cancel previous requestAnimationFrame.
|
| - if (this.__openedRaf) {
|
| - window.cancelAnimationFrame(this.__openedRaf);
|
| - this.__openedRaf = null;
|
| + closeAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-out-animation',
|
| + timing: {
|
| + duration: 150
|
| + }
|
| + }];
|
| }
|
| - // Force relayout to ensure proper transitions.
|
| - this.scrollTop = this.scrollTop;
|
| - this.__openedRaf = window.requestAnimationFrame(function() {
|
| - this.__openedRaf = null;
|
| - this.toggleClass('opened', this.opened);
|
| - }.bind(this));
|
| }
|
| - });
|
| + },
|
|
|
| -})();
|
| -/**
|
| - * @struct
|
| - * @constructor
|
| - * @private
|
| + keyBindings: {
|
| + 'tab': 'onTabPressed_',
|
| + },
|
| +
|
| + listeners: {
|
| + 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_',
|
| + },
|
| +
|
| + /**
|
| + * The last anchor that was used to open a menu. It's necessary for toggling.
|
| + * @private {?Element}
|
| */
|
| - Polymer.IronOverlayManagerClass = function() {
|
| - /**
|
| - * Used to keep track of the opened overlays.
|
| - * @private {Array<Element>}
|
| - */
|
| - this._overlays = [];
|
| + lastAnchor_: null,
|
|
|
| - /**
|
| - * iframes have a default z-index of 100,
|
| - * so this default should be at least that.
|
| - * @private {number}
|
| - */
|
| - this._minimumZ = 101;
|
| + /**
|
| + * The first focusable child in the menu's light DOM.
|
| + * @private {?Element}
|
| + */
|
| + firstFocus_: null,
|
|
|
| - /**
|
| - * Memoized backdrop element.
|
| - * @private {Element|null}
|
| - */
|
| - this._backdropElement = null;
|
| + /**
|
| + * The last focusable child in the menu's light DOM.
|
| + * @private {?Element}
|
| + */
|
| + lastFocus_: null,
|
|
|
| - // Enable document-wide tap recognizer.
|
| - Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this));
|
| + /** @override */
|
| + attached: function() {
|
| + window.addEventListener('resize', this.closeMenu.bind(this));
|
| + },
|
|
|
| - document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
|
| - document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
|
| - };
|
| + /** Closes the menu. */
|
| + closeMenu: function() {
|
| + if (this.root.activeElement == null) {
|
| + // Something else has taken focus away from the menu. Do not attempt to
|
| + // restore focus to the button which opened the menu.
|
| + this.$.dropdown.restoreFocusOnClose = false;
|
| + }
|
| + this.menuOpen = false;
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype = {
|
| + /**
|
| + * Opens the menu at the anchor location.
|
| + * @param {!Element} anchor The location to display the menu.
|
| + * @param {!Object} itemData The contextual item's data.
|
| + */
|
| + openMenu: function(anchor, itemData) {
|
| + if (this.lastAnchor_ == anchor && this.menuOpen)
|
| + return;
|
|
|
| - constructor: Polymer.IronOverlayManagerClass,
|
| + if (this.menuOpen)
|
| + this.closeMenu();
|
|
|
| - /**
|
| - * The shared backdrop element.
|
| - * @type {!Element} backdropElement
|
| - */
|
| - get backdropElement() {
|
| - if (!this._backdropElement) {
|
| - this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| - }
|
| - return this._backdropElement;
|
| - },
|
| + this.itemData = itemData;
|
| + this.lastAnchor_ = anchor;
|
| + this.$.dropdown.restoreFocusOnClose = true;
|
|
|
| - /**
|
| - * The deepest active element.
|
| - * @type {!Element} activeElement the active element
|
| - */
|
| - get deepActiveElement() {
|
| - // document.activeElement can be null
|
| - // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
| - // In case of null, default it to document.body.
|
| - var active = document.activeElement || document.body;
|
| - while (active.root && Polymer.dom(active.root).activeElement) {
|
| - active = Polymer.dom(active.root).activeElement;
|
| - }
|
| - return active;
|
| - },
|
| + var focusableChildren = Polymer.dom(this).querySelectorAll(
|
| + '[tabindex]:not([hidden]),button:not([hidden])');
|
| + if (focusableChildren.length > 0) {
|
| + this.$.dropdown.focusTarget = focusableChildren[0];
|
| + this.firstFocus_ = focusableChildren[0];
|
| + this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
|
| + }
|
|
|
| - /**
|
| - * Brings the overlay at the specified index to the front.
|
| - * @param {number} i
|
| - * @private
|
| - */
|
| - _bringOverlayAtIndexToFront: function(i) {
|
| - var overlay = this._overlays[i];
|
| - if (!overlay) {
|
| - return;
|
| - }
|
| - var lastI = this._overlays.length - 1;
|
| - var currentOverlay = this._overlays[lastI];
|
| - // Ensure always-on-top overlay stays on top.
|
| - if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| - lastI--;
|
| - }
|
| - // If already the top element, return.
|
| - if (i >= lastI) {
|
| - return;
|
| - }
|
| - // Update z-index to be on top.
|
| - var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
| - if (this._getZ(overlay) <= minimumZ) {
|
| - this._applyOverlayZ(overlay, minimumZ);
|
| - }
|
| + // Move the menu to the anchor.
|
| + this.$.dropdown.positionTarget = anchor;
|
| + this.menuOpen = true;
|
| + },
|
|
|
| - // Shift other overlays behind the new on top.
|
| - while (i < lastI) {
|
| - this._overlays[i] = this._overlays[i + 1];
|
| - i++;
|
| - }
|
| - this._overlays[lastI] = overlay;
|
| - },
|
| + /**
|
| + * Toggles the menu for the anchor that is passed in.
|
| + * @param {!Element} anchor The location to display the menu.
|
| + * @param {!Object} itemData The contextual item's data.
|
| + */
|
| + toggleMenu: function(anchor, itemData) {
|
| + if (anchor == this.lastAnchor_ && this.menuOpen)
|
| + this.closeMenu();
|
| + else
|
| + this.openMenu(anchor, itemData);
|
| + },
|
|
|
| - /**
|
| - * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
|
| - * Also updates the backdrop z-index.
|
| - * @param {!Element} overlay
|
| - */
|
| - addOrRemoveOverlay: function(overlay) {
|
| - if (overlay.opened) {
|
| - this.addOverlay(overlay);
|
| - } else {
|
| - this.removeOverlay(overlay);
|
| - }
|
| - },
|
| + /**
|
| + * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
|
| + * the first element with a nonzero tabindex to the last such element.
|
| + * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
|
| + * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
|
| + * @param {CustomEvent} e
|
| + */
|
| + onTabPressed_: function(e) {
|
| + if (!this.firstFocus_ || !this.lastFocus_)
|
| + return;
|
|
|
| - /**
|
| - * Tracks overlays for z-index and focus management.
|
| - * Ensures the last added overlay with always-on-top remains on top.
|
| - * @param {!Element} overlay
|
| - */
|
| - addOverlay: function(overlay) {
|
| - var i = this._overlays.indexOf(overlay);
|
| - if (i >= 0) {
|
| - this._bringOverlayAtIndexToFront(i);
|
| - this.trackBackdrop();
|
| - return;
|
| - }
|
| - var insertionIndex = this._overlays.length;
|
| - var currentOverlay = this._overlays[insertionIndex - 1];
|
| - var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
|
| - var newZ = this._getZ(overlay);
|
| + var toFocus;
|
| + var keyEvent = e.detail.keyboardEvent;
|
| + if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_)
|
| + toFocus = this.lastFocus_;
|
| + else if (keyEvent.target == this.lastFocus_)
|
| + toFocus = this.firstFocus_;
|
|
|
| - // Ensure always-on-top overlay stays on top.
|
| - if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| - // This bumps the z-index of +2.
|
| - this._applyOverlayZ(currentOverlay, minimumZ);
|
| - insertionIndex--;
|
| - // Update minimumZ to match previous overlay's z-index.
|
| - var previousOverlay = this._overlays[insertionIndex - 1];
|
| - minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
|
| - }
|
| + if (!toFocus)
|
| + return;
|
|
|
| - // Update z-index and insert overlay.
|
| - if (newZ <= minimumZ) {
|
| - this._applyOverlayZ(overlay, minimumZ);
|
| - }
|
| - this._overlays.splice(insertionIndex, 0, overlay);
|
| + e.preventDefault();
|
| + toFocus.focus();
|
| + },
|
|
|
| - this.trackBackdrop();
|
| + /**
|
| + * Ensure the menu is reset properly when it is closed by the dropdown (eg,
|
| + * clicking outside).
|
| + * @private
|
| + */
|
| + menuOpenChanged_: function() {
|
| + if (!this.menuOpen) {
|
| + this.itemData = null;
|
| + this.lastAnchor_ = null;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Prevent focus restoring when tapping outside the menu. This stops the
|
| + * focus moving around unexpectedly when closing the menu with the mouse.
|
| + * @param {CustomEvent} e
|
| + * @private
|
| + */
|
| + onOverlayCanceled_: function(e) {
|
| + if (e.detail.type == 'tap')
|
| + this.$.dropdown.restoreFocusOnClose = false;
|
| + },
|
| +});
|
| +/** @polymerBehavior Polymer.PaperItemBehavior */
|
| + Polymer.PaperItemBehaviorImpl = {
|
| + hostAttributes: {
|
| + role: 'option',
|
| + tabindex: '0'
|
| + }
|
| + };
|
| +
|
| + /** @polymerBehavior */
|
| + Polymer.PaperItemBehavior = [
|
| + Polymer.IronButtonState,
|
| + Polymer.IronControlState,
|
| + Polymer.PaperItemBehaviorImpl
|
| + ];
|
| +Polymer({
|
| + is: 'paper-item',
|
| +
|
| + behaviors: [
|
| + Polymer.PaperItemBehavior
|
| + ]
|
| + });
|
| +Polymer({
|
| +
|
| + is: 'iron-collapse',
|
| +
|
| + behaviors: [
|
| + Polymer.IronResizableBehavior
|
| + ],
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * If true, the orientation is horizontal; otherwise is vertical.
|
| + *
|
| + * @attribute horizontal
|
| + */
|
| + horizontal: {
|
| + type: Boolean,
|
| + value: false,
|
| + observer: '_horizontalChanged'
|
| + },
|
| +
|
| + /**
|
| + * Set opened to true to show the collapse element and to false to hide it.
|
| + *
|
| + * @attribute opened
|
| + */
|
| + opened: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + observer: '_openedChanged'
|
| + },
|
| +
|
| + /**
|
| + * Set noAnimation to true to disable animations
|
| + *
|
| + * @attribute noAnimation
|
| + */
|
| + noAnimation: {
|
| + type: Boolean
|
| + },
|
| +
|
| + },
|
| +
|
| + get dimension() {
|
| + return this.horizontal ? 'width' : 'height';
|
| },
|
|
|
| /**
|
| - * @param {!Element} overlay
|
| + * `maxWidth` or `maxHeight`.
|
| + * @private
|
| */
|
| - removeOverlay: function(overlay) {
|
| - var i = this._overlays.indexOf(overlay);
|
| - if (i === -1) {
|
| - return;
|
| - }
|
| - this._overlays.splice(i, 1);
|
| -
|
| - this.trackBackdrop();
|
| + get _dimensionMax() {
|
| + return this.horizontal ? 'maxWidth' : 'maxHeight';
|
| },
|
|
|
| /**
|
| - * Returns the current overlay.
|
| - * @return {Element|undefined}
|
| + * `max-width` or `max-height`.
|
| + * @private
|
| */
|
| - currentOverlay: function() {
|
| - var i = this._overlays.length - 1;
|
| - return this._overlays[i];
|
| + get _dimensionMaxCss() {
|
| + return this.horizontal ? 'max-width' : 'max-height';
|
| },
|
|
|
| - /**
|
| - * Returns the current overlay z-index.
|
| - * @return {number}
|
| - */
|
| - currentOverlayZ: function() {
|
| - return this._getZ(this.currentOverlay());
|
| + hostAttributes: {
|
| + role: 'group',
|
| + 'aria-hidden': 'true',
|
| + 'aria-expanded': 'false'
|
| },
|
|
|
| - /**
|
| - * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
| - * This does not effect the z-index of any existing overlays.
|
| - * @param {number} minimumZ
|
| - */
|
| - ensureMinimumZ: function(minimumZ) {
|
| - this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
| + listeners: {
|
| + transitionend: '_transitionEnd'
|
| },
|
|
|
| - focusOverlay: function() {
|
| - var current = /** @type {?} */ (this.currentOverlay());
|
| - if (current) {
|
| - current._applyFocus();
|
| - }
|
| + attached: function() {
|
| + // It will take care of setting correct classes and styles.
|
| + this._transitionEnd();
|
| },
|
|
|
| /**
|
| - * Updates the backdrop z-index.
|
| + * Toggle the opened state.
|
| + *
|
| + * @method toggle
|
| */
|
| - trackBackdrop: function() {
|
| - var overlay = this._overlayWithBackdrop();
|
| - // Avoid creating the backdrop if there is no overlay with backdrop.
|
| - if (!overlay && !this._backdropElement) {
|
| - return;
|
| - }
|
| - this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
|
| - this.backdropElement.opened = !!overlay;
|
| + toggle: function() {
|
| + this.opened = !this.opened;
|
| },
|
|
|
| - /**
|
| - * @return {Array<Element>}
|
| - */
|
| - getBackdrops: function() {
|
| - var backdrops = [];
|
| - for (var i = 0; i < this._overlays.length; i++) {
|
| - if (this._overlays[i].withBackdrop) {
|
| - backdrops.push(this._overlays[i]);
|
| - }
|
| - }
|
| - return backdrops;
|
| + show: function() {
|
| + this.opened = true;
|
| },
|
|
|
| - /**
|
| - * Returns the z-index for the backdrop.
|
| - * @return {number}
|
| - */
|
| - backdropZ: function() {
|
| - return this._getZ(this._overlayWithBackdrop()) - 1;
|
| + hide: function() {
|
| + this.opened = false;
|
| },
|
|
|
| /**
|
| - * Returns the first opened overlay that has a backdrop.
|
| - * @return {Element|undefined}
|
| - * @private
|
| + * Updates the size of the element.
|
| + * @param {string} size The new value for `maxWidth`/`maxHeight` as css property value, usually `auto` or `0px`.
|
| + * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
|
| */
|
| - _overlayWithBackdrop: function() {
|
| - for (var i = 0; i < this._overlays.length; i++) {
|
| - if (this._overlays[i].withBackdrop) {
|
| - return this._overlays[i];
|
| - }
|
| + updateSize: function(size, animated) {
|
| + // No change!
|
| + var curSize = this.style[this._dimensionMax];
|
| + if (curSize === size || (size === 'auto' && !curSize)) {
|
| + return;
|
| }
|
| - },
|
|
|
| - /**
|
| - * Calculates the minimum z-index for the overlay.
|
| - * @param {Element=} overlay
|
| - * @private
|
| - */
|
| - _getZ: function(overlay) {
|
| - var z = this._minimumZ;
|
| - if (overlay) {
|
| - var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
|
| - // Check if is a number
|
| - // Number.isNaN not supported in IE 10+
|
| - if (z1 === z1) {
|
| - z = z1;
|
| - }
|
| + this._updateTransition(false);
|
| + // If we can animate, must do some prep work.
|
| + if (animated && !this.noAnimation && this._isDisplayed) {
|
| + // Animation will start at the current size.
|
| + var startSize = this._calcSize();
|
| + // For `auto` we must calculate what is the final size for the animation.
|
| + // After the transition is done, _transitionEnd will set the size back to `auto`.
|
| + if (size === 'auto') {
|
| + this.style[this._dimensionMax] = '';
|
| + size = this._calcSize();
|
| + }
|
| + // Go to startSize without animation.
|
| + this.style[this._dimensionMax] = startSize;
|
| + // Force layout to ensure transition will go. Set scrollTop to itself
|
| + // so that compilers won't remove it.
|
| + this.scrollTop = this.scrollTop;
|
| + // Enable animation.
|
| + this._updateTransition(true);
|
| + }
|
| + // Set the final size.
|
| + if (size === 'auto') {
|
| + this.style[this._dimensionMax] = '';
|
| + } else {
|
| + this.style[this._dimensionMax] = size;
|
| }
|
| - return z;
|
| },
|
|
|
| /**
|
| - * @param {!Element} element
|
| - * @param {number|string} z
|
| - * @private
|
| + * enableTransition() is deprecated, but left over so it doesn't break existing code.
|
| + * Please use `noAnimation` property instead.
|
| + *
|
| + * @method enableTransition
|
| + * @deprecated since version 1.0.4
|
| */
|
| - _setZ: function(element, z) {
|
| - element.style.zIndex = z;
|
| + enableTransition: function(enabled) {
|
| + Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.');
|
| + this.noAnimation = !enabled;
|
| },
|
|
|
| - /**
|
| - * @param {!Element} overlay
|
| - * @param {number} aboveZ
|
| - * @private
|
| - */
|
| - _applyOverlayZ: function(overlay, aboveZ) {
|
| - this._setZ(overlay, aboveZ + 2);
|
| + _updateTransition: function(enabled) {
|
| + this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s';
|
| },
|
|
|
| - /**
|
| - * Returns the deepest overlay in the path.
|
| - * @param {Array<Element>=} path
|
| - * @return {Element|undefined}
|
| - * @suppress {missingProperties}
|
| - * @private
|
| - */
|
| - _overlayInPath: function(path) {
|
| - path = path || [];
|
| - for (var i = 0; i < path.length; i++) {
|
| - if (path[i]._manager === this) {
|
| - return path[i];
|
| - }
|
| - }
|
| + _horizontalChanged: function() {
|
| + this.style.transitionProperty = this._dimensionMaxCss;
|
| + var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxWidth';
|
| + this.style[otherDimension] = '';
|
| + this.updateSize(this.opened ? 'auto' : '0px', false);
|
| },
|
|
|
| - /**
|
| - * Ensures the click event is delegated to the right overlay.
|
| - * @param {!Event} event
|
| - * @private
|
| - */
|
| - _onCaptureClick: function(event) {
|
| - var overlay = /** @type {?} */ (this.currentOverlay());
|
| - // Check if clicked outside of top overlay.
|
| - if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
|
| - overlay._onCaptureClick(event);
|
| + _openedChanged: function() {
|
| + this.setAttribute('aria-expanded', this.opened);
|
| + this.setAttribute('aria-hidden', !this.opened);
|
| +
|
| + this.toggleClass('iron-collapse-closed', false);
|
| + this.toggleClass('iron-collapse-opened', false);
|
| + this.updateSize(this.opened ? 'auto' : '0px', true);
|
| +
|
| + // Focus the current collapse.
|
| + if (this.opened) {
|
| + this.focus();
|
| + }
|
| + if (this.noAnimation) {
|
| + this._transitionEnd();
|
| }
|
| },
|
|
|
| - /**
|
| - * Ensures the focus event is delegated to the right overlay.
|
| - * @param {!Event} event
|
| - * @private
|
| - */
|
| - _onCaptureFocus: function(event) {
|
| - var overlay = /** @type {?} */ (this.currentOverlay());
|
| - if (overlay) {
|
| - overlay._onCaptureFocus(event);
|
| + _transitionEnd: function() {
|
| + if (this.opened) {
|
| + this.style[this._dimensionMax] = '';
|
| }
|
| + this.toggleClass('iron-collapse-closed', !this.opened);
|
| + this.toggleClass('iron-collapse-opened', this.opened);
|
| + this._updateTransition(false);
|
| + this.notifyResize();
|
| },
|
|
|
| /**
|
| - * Ensures TAB and ESC keyboard events are delegated to the right overlay.
|
| - * @param {!Event} event
|
| + * Simplistic heuristic to detect if element has a parent with display: none
|
| + *
|
| * @private
|
| */
|
| - _onCaptureKeyDown: function(event) {
|
| - var overlay = /** @type {?} */ (this.currentOverlay());
|
| - if (overlay) {
|
| - if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
|
| - overlay._onCaptureEsc(event);
|
| - } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
|
| - overlay._onCaptureTab(event);
|
| - }
|
| + get _isDisplayed() {
|
| + var rect = this.getBoundingClientRect();
|
| + for (var prop in rect) {
|
| + if (rect[prop] !== 0) return true;
|
| }
|
| + return false;
|
| },
|
|
|
| - /**
|
| - * Returns if the overlay1 should be behind overlay2.
|
| - * @param {!Element} overlay1
|
| - * @param {!Element} overlay2
|
| - * @return {boolean}
|
| - * @suppress {missingProperties}
|
| - * @private
|
| - */
|
| - _shouldBeBehindOverlay: function(overlay1, overlay2) {
|
| - return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
|
| + _calcSize: function() {
|
| + return this.getBoundingClientRect()[this.dimension] + 'px';
|
| }
|
| - };
|
| -
|
| - Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
|
| -(function() {
|
| -'use strict';
|
|
|
| + });
|
| /**
|
| -Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
|
| -on top of other content. It includes an optional backdrop, and can be used to implement a variety
|
| -of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once.
|
| -
|
| -See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
|
| -for an example.
|
| -
|
| -### Closing and canceling
|
| -
|
| -An overlay may be hidden by closing or canceling. The difference between close and cancel is user
|
| -intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
|
| -it will cancel whenever the user taps outside it or presses the escape key. This behavior is
|
| -configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties.
|
| -`close()` should be called explicitly by the implementer when the user interacts with a control
|
| -in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled'
|
| -event. Call `preventDefault` on this event to prevent the overlay from closing.
|
| + Polymer.IronFormElementBehavior enables a custom element to be included
|
| + in an `iron-form`.
|
|
|
| -### Positioning
|
| -
|
| -By default the element is sized and positioned to fit and centered inside the window. You can
|
| -position and size it manually using CSS. See `Polymer.IronFitBehavior`.
|
| -
|
| -### Backdrop
|
| -
|
| -Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
|
| -appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
|
| -options.
|
| -
|
| -In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
|
| -Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes)
|
| -to achieve a different behavior.
|
| -
|
| -### Limitations
|
| -
|
| -The element is styled to appear on top of other content by setting its `z-index` property. You
|
| -must ensure no element has a stacking context with a higher `z-index` than its parent stacking
|
| -context. You should place this element as a child of `<body>` whenever possible.
|
| -
|
| -@demo demo/index.html
|
| -@polymerBehavior Polymer.IronOverlayBehavior
|
| -*/
|
| -
|
| - Polymer.IronOverlayBehaviorImpl = {
|
| + @demo demo/index.html
|
| + @polymerBehavior
|
| + */
|
| + Polymer.IronFormElementBehavior = {
|
|
|
| properties: {
|
| -
|
| - /**
|
| - * True if the overlay is currently displayed.
|
| - */
|
| - opened: {
|
| - observer: '_openedChanged',
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true
|
| - },
|
| -
|
| /**
|
| - * True if the overlay was canceled when it was last closed.
|
| + * Fired when the element is added to an `iron-form`.
|
| + *
|
| + * @event iron-form-element-register
|
| */
|
| - canceled: {
|
| - observer: '_canceledChanged',
|
| - readOnly: true,
|
| - type: Boolean,
|
| - value: false
|
| - },
|
|
|
| /**
|
| - * Set to true to display a backdrop behind the overlay. It traps the focus
|
| - * within the light DOM of the overlay.
|
| + * Fired when the element is removed from an `iron-form`.
|
| + *
|
| + * @event iron-form-element-unregister
|
| */
|
| - withBackdrop: {
|
| - observer: '_withBackdropChanged',
|
| - type: Boolean
|
| - },
|
|
|
| /**
|
| - * Set to true to disable auto-focusing the overlay or child nodes with
|
| - * the `autofocus` attribute` when the overlay is opened.
|
| + * The name of this element.
|
| */
|
| - noAutoFocus: {
|
| - type: Boolean,
|
| - value: false
|
| + name: {
|
| + type: String
|
| },
|
|
|
| /**
|
| - * Set to true to disable canceling the overlay with the ESC key.
|
| + * The value for this element.
|
| */
|
| - noCancelOnEscKey: {
|
| - type: Boolean,
|
| - value: false
|
| + value: {
|
| + notify: true,
|
| + type: String
|
| },
|
|
|
| /**
|
| - * Set to true to disable canceling the overlay by clicking outside it.
|
| + * Set to true to mark the input as required. If used in a form, a
|
| + * custom element that uses this behavior should also use
|
| + * Polymer.IronValidatableBehavior and define a custom validation method.
|
| + * Otherwise, a `required` element will always be considered valid.
|
| + * It's also strongly recommended to provide a visual style for the element
|
| + * when its value is invalid.
|
| */
|
| - noCancelOnOutsideClick: {
|
| + required: {
|
| type: Boolean,
|
| value: false
|
| },
|
|
|
| /**
|
| - * Contains the reason(s) this overlay was last closed (see `iron-overlay-closed`).
|
| - * `IronOverlayBehavior` provides the `canceled` reason; implementers of the
|
| - * behavior can provide other reasons in addition to `canceled`.
|
| + * The form that the element is registered to.
|
| */
|
| - closingReason: {
|
| - // was a getter before, but needs to be a property so other
|
| - // behaviors can override this.
|
| + _parentForm: {
|
| type: Object
|
| - },
|
| + }
|
| + },
|
| +
|
| + attached: function() {
|
| + // Note: the iron-form that this element belongs to will set this
|
| + // element's _parentForm property when handling this event.
|
| + this.fire('iron-form-element-register');
|
| + },
|
|
|
| + detached: function() {
|
| + if (this._parentForm) {
|
| + this._parentForm.fire('iron-form-element-unregister', {target: this});
|
| + }
|
| + }
|
| +
|
| + };
|
| +/**
|
| + * Use `Polymer.IronCheckedElementBehavior` to implement a custom element
|
| + * that has a `checked` property, which can be used for validation if the
|
| + * element is also `required`. Element instances implementing this behavior
|
| + * will also be registered for use in an `iron-form` element.
|
| + *
|
| + * @demo demo/index.html
|
| + * @polymerBehavior Polymer.IronCheckedElementBehavior
|
| + */
|
| + Polymer.IronCheckedElementBehaviorImpl = {
|
| +
|
| + properties: {
|
| /**
|
| - * Set to true to enable restoring of focus when overlay is closed.
|
| + * Fired when the checked state changes.
|
| + *
|
| + * @event iron-change
|
| */
|
| - restoreFocusOnClose: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
|
|
| /**
|
| - * Set to true to keep overlay always on top.
|
| + * Gets or sets the state, `true` is checked and `false` is unchecked.
|
| */
|
| - alwaysOnTop: {
|
| - type: Boolean
|
| + checked: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true,
|
| + notify: true,
|
| + observer: '_checkedChanged'
|
| },
|
|
|
| /**
|
| - * Shortcut to access to the overlay manager.
|
| - * @private
|
| - * @type {Polymer.IronOverlayManagerClass}
|
| + * If true, the button toggles the active state with each tap or press
|
| + * of the spacebar.
|
| */
|
| - _manager: {
|
| - type: Object,
|
| - value: Polymer.IronOverlayManager
|
| + toggles: {
|
| + type: Boolean,
|
| + value: true,
|
| + reflectToAttribute: true
|
| },
|
|
|
| - /**
|
| - * The node being focused.
|
| - * @type {?Node}
|
| - */
|
| - _focusedChild: {
|
| - type: Object
|
| + /* Overriden from Polymer.IronFormElementBehavior */
|
| + value: {
|
| + type: String,
|
| + value: 'on',
|
| + observer: '_valueChanged'
|
| }
|
| -
|
| },
|
|
|
| - listeners: {
|
| - 'iron-resize': '_onIronResize'
|
| + observers: [
|
| + '_requiredChanged(required)'
|
| + ],
|
| +
|
| + created: function() {
|
| + // Used by `iron-form` to handle the case that an element with this behavior
|
| + // doesn't have a role of 'checkbox' or 'radio', but should still only be
|
| + // included when the form is serialized if `this.checked === true`.
|
| + this._hasIronCheckedElementBehavior = true;
|
| },
|
|
|
| /**
|
| - * The backdrop element.
|
| - * @type {Element}
|
| + * Returns false if the element is required and not checked, and true otherwise.
|
| + * @param {*=} _value Ignored.
|
| + * @return {boolean} true if `required` is false or if `checked` is true.
|
| */
|
| - get backdropElement() {
|
| - return this._manager.backdropElement;
|
| + _getValidity: function(_value) {
|
| + return this.disabled || !this.required || this.checked;
|
| },
|
|
|
| /**
|
| - * Returns the node to give focus to.
|
| - * @type {Node}
|
| + * Update the aria-required label when `required` is changed.
|
| */
|
| - get _focusNode() {
|
| - return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
|
| + _requiredChanged: function() {
|
| + if (this.required) {
|
| + this.setAttribute('aria-required', 'true');
|
| + } else {
|
| + this.removeAttribute('aria-required');
|
| + }
|
| },
|
|
|
| /**
|
| - * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
|
| - * This is used to retrieve which is the first and last focusable nodes in order
|
| - * to wrap the focus for overlays `with-backdrop`.
|
| - *
|
| - * If you know what is your content (specifically the first and last focusable children),
|
| - * you can override this method to return only `[firstFocusable, lastFocusable];`
|
| - * @type {Array<Node>}
|
| - * @protected
|
| + * Fire `iron-changed` when the checked state changes.
|
| */
|
| - get _focusableNodes() {
|
| - // Elements that can be focused even if they have [disabled] attribute.
|
| - var FOCUSABLE_WITH_DISABLED = [
|
| - 'a[href]',
|
| - 'area[href]',
|
| - 'iframe',
|
| - '[tabindex]',
|
| - '[contentEditable=true]'
|
| - ];
|
| + _checkedChanged: function() {
|
| + this.active = this.checked;
|
| + this.fire('iron-change');
|
| + },
|
|
|
| - // Elements that cannot be focused if they have [disabled] attribute.
|
| - var FOCUSABLE_WITHOUT_DISABLED = [
|
| - 'input',
|
| - 'select',
|
| - 'textarea',
|
| - 'button'
|
| - ];
|
| + /**
|
| + * Reset value to 'on' if it is set to `undefined`.
|
| + */
|
| + _valueChanged: function() {
|
| + if (this.value === undefined || this.value === null) {
|
| + this.value = 'on';
|
| + }
|
| + }
|
| + };
|
|
|
| - // Discard elements with tabindex=-1 (makes them not focusable).
|
| - var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
|
| - ':not([tabindex="-1"]),' +
|
| - FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') +
|
| - ':not([disabled]):not([tabindex="-1"])';
|
| + /** @polymerBehavior Polymer.IronCheckedElementBehavior */
|
| + Polymer.IronCheckedElementBehavior = [
|
| + Polymer.IronFormElementBehavior,
|
| + Polymer.IronValidatableBehavior,
|
| + Polymer.IronCheckedElementBehaviorImpl
|
| + ];
|
| +/**
|
| + * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element
|
| + * that has a `checked` property similar to `Polymer.IronCheckedElementBehavior`
|
| + * and is compatible with having a ripple effect.
|
| + * @polymerBehavior Polymer.PaperCheckedElementBehavior
|
| + */
|
| + Polymer.PaperCheckedElementBehaviorImpl = {
|
| + /**
|
| + * Synchronizes the element's checked state with its ripple effect.
|
| + */
|
| + _checkedChanged: function() {
|
| + Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this);
|
| + if (this.hasRipple()) {
|
| + if (this.checked) {
|
| + this._ripple.setAttribute('checked', '');
|
| + } else {
|
| + this._ripple.removeAttribute('checked');
|
| + }
|
| + }
|
| + },
|
|
|
| - var focusables = Polymer.dom(this).querySelectorAll(selector);
|
| - if (this.tabIndex >= 0) {
|
| - // Insert at the beginning because we might have all elements with tabIndex = 0,
|
| - // and the overlay should be the first of the list.
|
| - focusables.splice(0, 0, this);
|
| + /**
|
| + * Synchronizes the element's `active` and `checked` state.
|
| + */
|
| + _buttonStateChanged: function() {
|
| + Polymer.PaperRippleBehavior._buttonStateChanged.call(this);
|
| + if (this.disabled) {
|
| + return;
|
| }
|
| - // Sort by tabindex.
|
| - return focusables.sort(function (a, b) {
|
| - if (a.tabIndex === b.tabIndex) {
|
| - return 0;
|
| + if (this.isAttached) {
|
| + this.checked = this.active;
|
| + }
|
| + }
|
| + };
|
| +
|
| + /** @polymerBehavior Polymer.PaperCheckedElementBehavior */
|
| + Polymer.PaperCheckedElementBehavior = [
|
| + Polymer.PaperInkyFocusBehavior,
|
| + Polymer.IronCheckedElementBehavior,
|
| + Polymer.PaperCheckedElementBehaviorImpl
|
| + ];
|
| +Polymer({
|
| + is: 'paper-checkbox',
|
| +
|
| + behaviors: [
|
| + Polymer.PaperCheckedElementBehavior
|
| + ],
|
| +
|
| + hostAttributes: {
|
| + role: 'checkbox',
|
| + 'aria-checked': false,
|
| + tabindex: 0
|
| + },
|
| +
|
| + properties: {
|
| + /**
|
| + * Fired when the checked state changes due to user interaction.
|
| + *
|
| + * @event change
|
| + */
|
| +
|
| + /**
|
| + * Fired when the checked state changes.
|
| + *
|
| + * @event iron-change
|
| + */
|
| + ariaActiveAttribute: {
|
| + type: String,
|
| + value: 'aria-checked'
|
| }
|
| - if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
|
| - return 1;
|
| + },
|
| +
|
| + _computeCheckboxClass: function(checked, invalid) {
|
| + var className = '';
|
| + if (checked) {
|
| + className += 'checked ';
|
| }
|
| - return -1;
|
| - });
|
| - },
|
| + if (invalid) {
|
| + className += 'invalid';
|
| + }
|
| + return className;
|
| + },
|
|
|
| - ready: function() {
|
| - // Used to skip calls to notifyResize and refit while the overlay is animating.
|
| - this.__isAnimating = false;
|
| - // with-backdrop needs tabindex to be set in order to trap the focus.
|
| - // If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
|
| - this.__shouldRemoveTabIndex = false;
|
| - // Used for wrapping the focus on TAB / Shift+TAB.
|
| - this.__firstFocusableNode = this.__lastFocusableNode = null;
|
| - // Used by __onNextAnimationFrame to cancel any previous callback.
|
| - this.__raf = null;
|
| - // Focused node before overlay gets opened. Can be restored on close.
|
| - this.__restoreFocusNode = null;
|
| - this._ensureSetup();
|
| - },
|
| + _computeCheckmarkClass: function(checked) {
|
| + return checked ? '' : 'hidden';
|
| + },
|
| +
|
| + // create ripple inside the checkboxContainer
|
| + _createRipple: function() {
|
| + this._rippleContainer = this.$.checkboxContainer;
|
| + return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this);
|
| + }
|
| +
|
| + });
|
| +Polymer({
|
| + is: 'paper-icon-button-light',
|
| + extends: 'button',
|
| +
|
| + behaviors: [
|
| + Polymer.PaperRippleBehavior
|
| + ],
|
| +
|
| + listeners: {
|
| + 'down': '_rippleDown',
|
| + 'up': '_rippleUp',
|
| + 'focus': '_rippleDown',
|
| + 'blur': '_rippleUp',
|
| + },
|
| +
|
| + _rippleDown: function() {
|
| + this.getRipple().downAction();
|
| + },
|
| +
|
| + _rippleUp: function() {
|
| + this.getRipple().upAction();
|
| + },
|
| +
|
| + /**
|
| + * @param {...*} var_args
|
| + */
|
| + ensureRipple: function(var_args) {
|
| + var lastRipple = this._ripple;
|
| + Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments);
|
| + if (this._ripple && this._ripple !== lastRipple) {
|
| + this._ripple.center = true;
|
| + this._ripple.classList.add('circle');
|
| + }
|
| + }
|
| + });
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +cr.define('cr.icon', function() {
|
| + /**
|
| + * @return {!Array<number>} The scale factors supported by this platform for
|
| + * webui resources.
|
| + */
|
| + function getSupportedScaleFactors() {
|
| + var supportedScaleFactors = [];
|
| + if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
|
| + // All desktop platforms support zooming which also updates the
|
| + // renderer's device scale factors (a.k.a devicePixelRatio), and
|
| + // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
|
| + // image-set on these platforms so that the renderer can pick the
|
| + // closest image for the current device scale factor.
|
| + supportedScaleFactors.push(1);
|
| + supportedScaleFactors.push(2);
|
| + } else {
|
| + // For other platforms that use fixed device scale factor, use
|
| + // the window's device pixel ratio.
|
| + // TODO(oshima): Investigate if Android/iOS need to use image-set.
|
| + supportedScaleFactors.push(window.devicePixelRatio);
|
| + }
|
| + return supportedScaleFactors;
|
| + }
|
| +
|
| + /**
|
| + * Returns the URL of the image, or an image set of URLs for the profile
|
| + * avatar. Default avatars have resources available for multiple scalefactors,
|
| + * whereas the GAIA profile image only comes in one size.
|
| + *
|
| + * @param {string} path The path of the image.
|
| + * @return {string} The url, or an image set of URLs of the avatar image.
|
| + */
|
| + function getProfileAvatarIcon(path) {
|
| + var chromeThemePath = 'chrome://theme';
|
| + var isDefaultAvatar =
|
| + (path.slice(0, chromeThemePath.length) == chromeThemePath);
|
| + return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path);
|
| + }
|
| +
|
| + /**
|
| + * Generates a CSS -webkit-image-set for a chrome:// url.
|
| + * An entry in the image set is added for each of getSupportedScaleFactors().
|
| + * The scale-factor-specific url is generated by replacing the first instance
|
| + * of 'scalefactor' in |path| with the numeric scale factor.
|
| + * @param {string} path The URL to generate an image set for.
|
| + * 'scalefactor' should be a substring of |path|.
|
| + * @return {string} The CSS -webkit-image-set.
|
| + */
|
| + function imageset(path) {
|
| + var supportedScaleFactors = getSupportedScaleFactors();
|
| +
|
| + var replaceStartIndex = path.indexOf('scalefactor');
|
| + if (replaceStartIndex < 0)
|
| + return url(path);
|
| +
|
| + var s = '';
|
| + for (var i = 0; i < supportedScaleFactors.length; ++i) {
|
| + var scaleFactor = supportedScaleFactors[i];
|
| + var pathWithScaleFactor = path.substr(0, replaceStartIndex) +
|
| + scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length);
|
| +
|
| + s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
|
| +
|
| + if (i != supportedScaleFactors.length - 1)
|
| + s += ', ';
|
| + }
|
| + return '-webkit-image-set(' + s + ')';
|
| + }
|
| +
|
| + /**
|
| + * A regular expression for identifying favicon URLs.
|
| + * @const {!RegExp}
|
| + */
|
| + var FAVICON_URL_REGEX = /\.ico$/i;
|
| +
|
| + /**
|
| + * Creates a CSS -webkit-image-set for a favicon request.
|
| + * @param {string} url Either the URL of the original page or of the favicon
|
| + * itself.
|
| + * @param {number=} opt_size Optional preferred size of the favicon.
|
| + * @param {string=} opt_type Optional type of favicon to request. Valid values
|
| + * are 'favicon' and 'touch-icon'. Default is 'favicon'.
|
| + * @return {string} -webkit-image-set for the favicon.
|
| + */
|
| + function getFaviconImageSet(url, opt_size, opt_type) {
|
| + var size = opt_size || 16;
|
| + var type = opt_type || 'favicon';
|
| +
|
| + return imageset(
|
| + 'chrome://' + type + '/size/' + size + '@scalefactorx/' +
|
| + // Note: Literal 'iconurl' must match |kIconURLParameter| in
|
| + // components/favicon_base/favicon_url_parser.cc.
|
| + (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url);
|
| + }
|
| +
|
| + return {
|
| + getSupportedScaleFactors: getSupportedScaleFactors,
|
| + getProfileAvatarIcon: getProfileAvatarIcon,
|
| + getFaviconImageSet: getFaviconImageSet,
|
| + };
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - attached: function() {
|
| - // Call _openedChanged here so that position can be computed correctly.
|
| - if (this.opened) {
|
| - this._openedChanged(this.opened);
|
| - }
|
| - this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
|
| - },
|
| +/**
|
| + * @fileoverview Defines a singleton object, md_history.BrowserService, which
|
| + * provides access to chrome.send APIs.
|
| + */
|
|
|
| - detached: function() {
|
| - Polymer.dom(this).unobserveNodes(this._observer);
|
| - this._observer = null;
|
| - if (this.__raf) {
|
| - window.cancelAnimationFrame(this.__raf);
|
| - this.__raf = null;
|
| +cr.define('md_history', function() {
|
| + /** @constructor */
|
| + function BrowserService() {
|
| + /** @private {Array<!HistoryEntry>} */
|
| + this.pendingDeleteItems_ = null;
|
| + /** @private {PromiseResolver} */
|
| + this.pendingDeletePromise_ = null;
|
| + }
|
| +
|
| + BrowserService.prototype = {
|
| + /**
|
| + * @param {!Array<!HistoryEntry>} items
|
| + * @return {Promise<!Array<!HistoryEntry>>}
|
| + */
|
| + deleteItems: function(items) {
|
| + if (this.pendingDeleteItems_ != null) {
|
| + // There's already a deletion in progress, reject immediately.
|
| + return new Promise(function(resolve, reject) { reject(items); });
|
| }
|
| - this._manager.removeOverlay(this);
|
| +
|
| + var removalList = items.map(function(item) {
|
| + return {
|
| + url: item.url,
|
| + timestamps: item.allTimestamps
|
| + };
|
| + });
|
| +
|
| + this.pendingDeleteItems_ = items;
|
| + this.pendingDeletePromise_ = new PromiseResolver();
|
| +
|
| + chrome.send('removeVisits', removalList);
|
| +
|
| + return this.pendingDeletePromise_.promise;
|
| },
|
|
|
| /**
|
| - * Toggle the opened state of the overlay.
|
| + * @param {!string} url
|
| */
|
| - toggle: function() {
|
| - this._setCanceled(false);
|
| - this.opened = !this.opened;
|
| + removeBookmark: function(url) {
|
| + chrome.send('removeBookmark', [url]);
|
| },
|
|
|
| /**
|
| - * Open the overlay.
|
| + * @param {string} sessionTag
|
| */
|
| - open: function() {
|
| - this._setCanceled(false);
|
| - this.opened = true;
|
| + openForeignSessionAllTabs: function(sessionTag) {
|
| + chrome.send('openForeignSession', [sessionTag]);
|
| },
|
|
|
| /**
|
| - * Close the overlay.
|
| + * @param {string} sessionTag
|
| + * @param {number} windowId
|
| + * @param {number} tabId
|
| + * @param {MouseEvent} e
|
| */
|
| - close: function() {
|
| - this._setCanceled(false);
|
| - this.opened = false;
|
| + openForeignSessionTab: function(sessionTag, windowId, tabId, e) {
|
| + chrome.send('openForeignSession', [
|
| + sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey,
|
| + e.ctrlKey, e.metaKey, e.shiftKey
|
| + ]);
|
| },
|
|
|
| /**
|
| - * Cancels the overlay.
|
| - * @param {Event=} event The original event
|
| + * @param {string} sessionTag
|
| */
|
| - cancel: function(event) {
|
| - var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
|
| - if (cancelEvent.defaultPrevented) {
|
| - return;
|
| - }
|
| -
|
| - this._setCanceled(true);
|
| - this.opened = false;
|
| + deleteForeignSession: function(sessionTag) {
|
| + chrome.send('deleteForeignSession', [sessionTag]);
|
| },
|
|
|
| - _ensureSetup: function() {
|
| - if (this._overlaySetup) {
|
| - return;
|
| - }
|
| - this._overlaySetup = true;
|
| - this.style.outline = 'none';
|
| - this.style.display = 'none';
|
| + openClearBrowsingData: function() {
|
| + chrome.send('clearBrowsingData');
|
| },
|
|
|
| /**
|
| - * Called when `opened` changes.
|
| - * @param {boolean=} opened
|
| - * @protected
|
| + * @param {boolean} successful
|
| + * @private
|
| */
|
| - _openedChanged: function(opened) {
|
| - if (opened) {
|
| - this.removeAttribute('aria-hidden');
|
| - } else {
|
| - this.setAttribute('aria-hidden', 'true');
|
| - }
|
| -
|
| - // Defer any animation-related code on attached
|
| - // (_openedChanged gets called again on attached).
|
| - if (!this.isAttached) {
|
| + resolveDelete_: function(successful) {
|
| + if (this.pendingDeleteItems_ == null ||
|
| + this.pendingDeletePromise_ == null) {
|
| return;
|
| }
|
|
|
| - this.__isAnimating = true;
|
| + if (successful)
|
| + this.pendingDeletePromise_.resolve(this.pendingDeleteItems_);
|
| + else
|
| + this.pendingDeletePromise_.reject(this.pendingDeleteItems_);
|
|
|
| - // Use requestAnimationFrame for non-blocking rendering.
|
| - this.__onNextAnimationFrame(this.__openedChanged);
|
| + this.pendingDeleteItems_ = null;
|
| + this.pendingDeletePromise_ = null;
|
| },
|
| + };
|
|
|
| - _canceledChanged: function() {
|
| - this.closingReason = this.closingReason || {};
|
| - this.closingReason.canceled = this.canceled;
|
| - },
|
| + cr.addSingletonGetter(BrowserService);
|
|
|
| - _withBackdropChanged: function() {
|
| - // If tabindex is already set, no need to override it.
|
| - if (this.withBackdrop && !this.hasAttribute('tabindex')) {
|
| - this.setAttribute('tabindex', '-1');
|
| - this.__shouldRemoveTabIndex = true;
|
| - } else if (this.__shouldRemoveTabIndex) {
|
| - this.removeAttribute('tabindex');
|
| - this.__shouldRemoveTabIndex = false;
|
| - }
|
| - if (this.opened && this.isAttached) {
|
| - this._manager.trackBackdrop();
|
| - }
|
| - },
|
| + return {BrowserService: BrowserService};
|
| +});
|
|
|
| - /**
|
| - * tasks which must occur before opening; e.g. making the element visible.
|
| - * @protected
|
| - */
|
| - _prepareRenderOpened: function() {
|
| - // Store focused node.
|
| - this.__restoreFocusNode = this._manager.deepActiveElement;
|
| +/**
|
| + * Called by the history backend when deletion was succesful.
|
| + */
|
| +function deleteComplete() {
|
| + md_history.BrowserService.getInstance().resolveDelete_(true);
|
| +}
|
|
|
| - // Needed to calculate the size of the overlay so that transitions on its size
|
| - // will have the correct starting points.
|
| - this._preparePositioning();
|
| - this.refit();
|
| - this._finishPositioning();
|
| +/**
|
| + * Called by the history backend when the deletion failed.
|
| + */
|
| +function deleteFailed() {
|
| + md_history.BrowserService.getInstance().resolveDelete_(false);
|
| +};
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - // Safari will apply the focus to the autofocus element when displayed
|
| - // for the first time, so we make sure to return the focus where it was.
|
| - if (this.noAutoFocus && document.activeElement === this._focusNode) {
|
| - this._focusNode.blur();
|
| - this.__restoreFocusNode.focus();
|
| - }
|
| - },
|
| +Polymer({
|
| + is: 'history-searched-label',
|
|
|
| - /**
|
| - * Tasks which cause the overlay to actually open; typically play an animation.
|
| - * @protected
|
| - */
|
| - _renderOpened: function() {
|
| - this._finishRenderOpened();
|
| - },
|
| + properties: {
|
| + // The text to show in this label.
|
| + title: String,
|
|
|
| - /**
|
| - * Tasks which cause the overlay to actually close; typically play an animation.
|
| - * @protected
|
| - */
|
| - _renderClosed: function() {
|
| - this._finishRenderClosed();
|
| - },
|
| + // The search term to bold within the title.
|
| + searchTerm: String,
|
| + },
|
|
|
| - /**
|
| - * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
|
| - * @protected
|
| - */
|
| - _finishRenderOpened: function() {
|
| - this.notifyResize();
|
| - this.__isAnimating = false;
|
| + observers: ['setSearchedTextToBold_(title, searchTerm)'],
|
|
|
| - // Store it so we don't query too much.
|
| - var focusableNodes = this._focusableNodes;
|
| - this.__firstFocusableNode = focusableNodes[0];
|
| - this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
|
| + /**
|
| + * Updates the page title. If a search term is specified, highlights any
|
| + * occurrences of the search term in bold.
|
| + * @private
|
| + */
|
| + setSearchedTextToBold_: function() {
|
| + var i = 0;
|
| + var titleElem = this.$.container;
|
| + var titleText = this.title;
|
|
|
| - this.fire('iron-overlay-opened');
|
| - },
|
| + if (this.searchTerm == '' || this.searchTerm == null) {
|
| + titleElem.textContent = titleText;
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
|
| - * @protected
|
| - */
|
| - _finishRenderClosed: function() {
|
| - // Hide the overlay.
|
| - this.style.display = 'none';
|
| - // Reset z-index only at the end of the animation.
|
| - this.style.zIndex = '';
|
| - this.notifyResize();
|
| - this.__isAnimating = false;
|
| - this.fire('iron-overlay-closed', this.closingReason);
|
| - },
|
| + var re = new RegExp(quoteString(this.searchTerm), 'gim');
|
| + var match;
|
| + titleElem.textContent = '';
|
| + while (match = re.exec(titleText)) {
|
| + if (match.index > i)
|
| + titleElem.appendChild(document.createTextNode(
|
| + titleText.slice(i, match.index)));
|
| + i = re.lastIndex;
|
| + // Mark the highlighted text in bold.
|
| + var b = document.createElement('b');
|
| + b.textContent = titleText.substring(match.index, i);
|
| + titleElem.appendChild(b);
|
| + }
|
| + if (i < titleText.length)
|
| + titleElem.appendChild(
|
| + document.createTextNode(titleText.slice(i)));
|
| + },
|
| +});
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - _preparePositioning: function() {
|
| - this.style.transition = this.style.webkitTransition = 'none';
|
| - this.style.transform = this.style.webkitTransform = 'none';
|
| - this.style.display = '';
|
| - },
|
| +cr.define('md_history', function() {
|
| + var HistoryItem = Polymer({
|
| + is: 'history-item',
|
|
|
| - _finishPositioning: function() {
|
| - // First, make it invisible & reactivate animations.
|
| - this.style.display = 'none';
|
| - // Force reflow before re-enabling animations so that they don't start.
|
| - // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| - this.scrollTop = this.scrollTop;
|
| - this.style.transition = this.style.webkitTransition = '';
|
| - this.style.transform = this.style.webkitTransform = '';
|
| - // Now that animations are enabled, make it visible again
|
| - this.style.display = '';
|
| - // Force reflow, so that following animations are properly started.
|
| - // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| - this.scrollTop = this.scrollTop;
|
| - },
|
| + properties: {
|
| + // Underlying HistoryEntry data for this item. Contains read-only fields
|
| + // from the history backend, as well as fields computed by history-list.
|
| + item: {type: Object, observer: 'showIcon_'},
|
|
|
| - /**
|
| - * Applies focus according to the opened state.
|
| - * @protected
|
| - */
|
| - _applyFocus: function() {
|
| - if (this.opened) {
|
| - if (!this.noAutoFocus) {
|
| - this._focusNode.focus();
|
| - }
|
| - }
|
| - else {
|
| - this._focusNode.blur();
|
| - this._focusedChild = null;
|
| - // Restore focus.
|
| - if (this.restoreFocusOnClose && this.__restoreFocusNode) {
|
| - this.__restoreFocusNode.focus();
|
| - }
|
| - this.__restoreFocusNode = null;
|
| - // If many overlays get closed at the same time, one of them would still
|
| - // be the currentOverlay even if already closed, and would call _applyFocus
|
| - // infinitely, so we check for this not to be the current overlay.
|
| - var currentOverlay = this._manager.currentOverlay();
|
| - if (currentOverlay && this !== currentOverlay) {
|
| - currentOverlay._applyFocus();
|
| - }
|
| - }
|
| - },
|
| + // Search term used to obtain this history-item.
|
| + searchTerm: {type: String},
|
|
|
| - /**
|
| - * Cancels (closes) the overlay. Call when click happens outside the overlay.
|
| - * @param {!Event} event
|
| - * @protected
|
| - */
|
| - _onCaptureClick: function(event) {
|
| - if (!this.noCancelOnOutsideClick) {
|
| - this.cancel(event);
|
| - }
|
| - },
|
| + selected: {type: Boolean, notify: true},
|
|
|
| - /**
|
| - * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
|
| - * @param {!Event} event
|
| - * @protected
|
| - */
|
| - _onCaptureFocus: function (event) {
|
| - if (!this.withBackdrop) {
|
| - return;
|
| - }
|
| - var path = Polymer.dom(event).path;
|
| - if (path.indexOf(this) === -1) {
|
| - event.stopPropagation();
|
| - this._applyFocus();
|
| - } else {
|
| - this._focusedChild = path[0];
|
| - }
|
| + isFirstItem: {type: Boolean, reflectToAttribute: true},
|
| +
|
| + isCardStart: {type: Boolean, reflectToAttribute: true},
|
| +
|
| + isCardEnd: {type: Boolean, reflectToAttribute: true},
|
| +
|
| + // True if the item is being displayed embedded in another element and
|
| + // should not manage its own borders or size.
|
| + embedded: {type: Boolean, reflectToAttribute: true},
|
| +
|
| + hasTimeGap: {type: Boolean},
|
| +
|
| + numberOfItems: {type: Number}
|
| },
|
|
|
| /**
|
| - * Handles the ESC key event and cancels (closes) the overlay.
|
| - * @param {!Event} event
|
| - * @protected
|
| + * When a history-item is selected the toolbar is notified and increases
|
| + * or decreases its count of selected items accordingly.
|
| + * @private
|
| */
|
| - _onCaptureEsc: function(event) {
|
| - if (!this.noCancelOnEscKey) {
|
| - this.cancel(event);
|
| - }
|
| + onCheckboxSelected_: function() {
|
| + this.fire('history-checkbox-select', {
|
| + countAddition: this.$.checkbox.checked ? 1 : -1
|
| + });
|
| },
|
|
|
| /**
|
| - * Handles TAB key events to track focus changes.
|
| - * Will wrap focus for overlays withBackdrop.
|
| - * @param {!Event} event
|
| - * @protected
|
| + * Remove bookmark of current item when bookmark-star is clicked.
|
| + * @private
|
| */
|
| - _onCaptureTab: function(event) {
|
| - if (!this.withBackdrop) {
|
| + onRemoveBookmarkTap_: function() {
|
| + if (!this.item.starred)
|
| return;
|
| - }
|
| - // TAB wraps from last to first focusable.
|
| - // Shift + TAB wraps from first to last focusable.
|
| - var shift = event.shiftKey;
|
| - var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
|
| - var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
|
| - var shouldWrap = false;
|
| - if (nodeToCheck === nodeToSet) {
|
| - // If nodeToCheck is the same as nodeToSet, it means we have an overlay
|
| - // with 0 or 1 focusables; in either case we still need to trap the
|
| - // focus within the overlay.
|
| - shouldWrap = true;
|
| - } else {
|
| - // In dom=shadow, the manager will receive focus changes on the main
|
| - // root but not the ones within other shadow roots, so we can't rely on
|
| - // _focusedChild, but we should check the deepest active element.
|
| - var focusedNode = this._manager.deepActiveElement;
|
| - // If the active element is not the nodeToCheck but the overlay itself,
|
| - // it means the focus is about to go outside the overlay, hence we
|
| - // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
|
| - shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
|
| - }
|
|
|
| - if (shouldWrap) {
|
| - // When the overlay contains the last focusable element of the document
|
| - // and it's already focused, pressing TAB would move the focus outside
|
| - // the document (e.g. to the browser search bar). Similarly, when the
|
| - // overlay contains the first focusable element of the document and it's
|
| - // already focused, pressing Shift+TAB would move the focus outside the
|
| - // document (e.g. to the browser search bar).
|
| - // In both cases, we would not receive a focus event, but only a blur.
|
| - // In order to achieve focus wrapping, we prevent this TAB event and
|
| - // force the focus. This will also prevent the focus to temporarily move
|
| - // outside the overlay, which might cause scrolling.
|
| - event.preventDefault();
|
| - this._focusedChild = nodeToSet;
|
| - this._applyFocus();
|
| - }
|
| + if (this.$$('#bookmark-star') == this.root.activeElement)
|
| + this.$['menu-button'].focus();
|
| +
|
| + md_history.BrowserService.getInstance()
|
| + .removeBookmark(this.item.url);
|
| + this.fire('remove-bookmark-stars', this.item.url);
|
| },
|
|
|
| /**
|
| - * Refits if the overlay is opened and not animating.
|
| - * @protected
|
| + * Fires a custom event when the menu button is clicked. Sends the details
|
| + * of the history item and where the menu should appear.
|
| */
|
| - _onIronResize: function() {
|
| - if (this.opened && !this.__isAnimating) {
|
| - this.__onNextAnimationFrame(this.refit);
|
| - }
|
| + onMenuButtonTap_: function(e) {
|
| + this.fire('toggle-menu', {
|
| + target: Polymer.dom(e).localTarget,
|
| + item: this.item,
|
| + });
|
| +
|
| + // Stops the 'tap' event from closing the menu when it opens.
|
| + e.stopPropagation();
|
| },
|
|
|
| /**
|
| - * Will call notifyResize if overlay is opened.
|
| - * Can be overridden in order to avoid multiple observers on the same node.
|
| - * @protected
|
| + * Set the favicon image, based on the URL of the history item.
|
| + * @private
|
| */
|
| - _onNodesChange: function() {
|
| - if (this.opened && !this.__isAnimating) {
|
| - this.notifyResize();
|
| - }
|
| + showIcon_: function() {
|
| + this.$.icon.style.backgroundImage =
|
| + cr.icon.getFaviconImageSet(this.item.url);
|
| + },
|
| +
|
| + selectionNotAllowed_: function() {
|
| + return !loadTimeData.getBoolean('allowDeletingHistory');
|
| },
|
|
|
| /**
|
| - * Tasks executed when opened changes: prepare for the opening, move the
|
| - * focus, update the manager, render opened/closed.
|
| + * Generates the title for this history card.
|
| + * @param {number} numberOfItems The number of items in the card.
|
| + * @param {string} search The search term associated with these results.
|
| * @private
|
| */
|
| - __openedChanged: function() {
|
| - if (this.opened) {
|
| - // Make overlay visible, then add it to the manager.
|
| - this._prepareRenderOpened();
|
| - this._manager.addOverlay(this);
|
| - // Move the focus to the child node with [autofocus].
|
| - this._applyFocus();
|
| -
|
| - this._renderOpened();
|
| - } else {
|
| - // Remove overlay, then restore the focus before actually closing.
|
| - this._manager.removeOverlay(this);
|
| - this._applyFocus();
|
| + cardTitle_: function(numberOfItems, historyDate, search) {
|
| + if (!search)
|
| + return this.item.dateRelativeDay;
|
|
|
| - this._renderClosed();
|
| - }
|
| + var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults';
|
| + return loadTimeData.getStringF('foundSearchResults', numberOfItems,
|
| + loadTimeData.getString(resultId), search);
|
| },
|
|
|
| /**
|
| - * Executes a callback on the next animation frame, overriding any previous
|
| - * callback awaiting for the next animation frame. e.g.
|
| - * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
|
| - * `callback1` will never be invoked.
|
| - * @param {!Function} callback Its `this` parameter is the overlay itself.
|
| - * @private
|
| + * Crop long item titles to reduce their effect on layout performance. See
|
| + * crbug.com/621347.
|
| + * @param {string} title
|
| + * @return {string}
|
| */
|
| - __onNextAnimationFrame: function(callback) {
|
| - if (this.__raf) {
|
| - window.cancelAnimationFrame(this.__raf);
|
| - }
|
| - var self = this;
|
| - this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
|
| - self.__raf = null;
|
| - callback.call(self);
|
| - });
|
| + cropItemTitle_: function(title) {
|
| + return (title.length > TITLE_MAX_LENGTH) ?
|
| + title.substr(0, TITLE_MAX_LENGTH) :
|
| + title;
|
| }
|
| + });
|
| +
|
| + /**
|
| + * Check whether the time difference between the given history item and the
|
| + * next one is large enough for a spacer to be required.
|
| + * @param {Array<HistoryEntry>} visits
|
| + * @param {number} currentIndex
|
| + * @param {string} searchedTerm
|
| + * @return {boolean} Whether or not time gap separator is required.
|
| + * @private
|
| + */
|
| + HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) {
|
| + if (currentIndex >= visits.length - 1 || visits.length == 0)
|
| + return false;
|
| +
|
| + var currentItem = visits[currentIndex];
|
| + var nextItem = visits[currentIndex + 1];
|
|
|
| + if (searchedTerm)
|
| + return currentItem.dateShort != nextItem.dateShort;
|
| +
|
| + return currentItem.time - nextItem.time > BROWSING_GAP_TIME &&
|
| + currentItem.dateRelativeDay == nextItem.dateRelativeDay;
|
| };
|
|
|
| - /** @polymerBehavior */
|
| - Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| + return { HistoryItem: HistoryItem };
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @typedef {{domain: string,
|
| + * visits: !Array<HistoryEntry>,
|
| + * rendered: boolean,
|
| + * expanded: boolean}}
|
| + */
|
| +var HistoryDomain;
|
| +
|
| +/**
|
| + * @typedef {{title: string,
|
| + * domains: !Array<HistoryDomain>}}
|
| + */
|
| +var HistoryGroup;
|
| +
|
| +// TODO(calamity): Support selection by refactoring selection out of
|
| +// history-list and into history-app.
|
| +Polymer({
|
| + is: 'history-grouped-list',
|
| +
|
| + properties: {
|
| + // An array of history entries in reverse chronological order.
|
| + historyData: {
|
| + type: Array,
|
| + },
|
| +
|
| + /**
|
| + * @type {Array<HistoryGroup>}
|
| + */
|
| + groupedHistoryData_: {
|
| + type: Array,
|
| + },
|
| +
|
| + searchedTerm: {
|
| + type: String,
|
| + value: ''
|
| + },
|
| +
|
| + range: {
|
| + type: Number,
|
| + },
|
| +
|
| + queryStartTime: String,
|
| + queryEndTime: String,
|
| + },
|
| +
|
| + observers: [
|
| + 'updateGroupedHistoryData_(range, historyData)'
|
| + ],
|
|
|
| /**
|
| - * Fired after the overlay opens.
|
| - * @event iron-overlay-opened
|
| + * Make a list of domains from visits.
|
| + * @param {!Array<!HistoryEntry>} visits
|
| + * @return {!Array<!HistoryDomain>}
|
| */
|
| + createHistoryDomains_: function(visits) {
|
| + var domainIndexes = {};
|
| + var domains = [];
|
| +
|
| + // Group the visits into a dictionary and generate a list of domains.
|
| + for (var i = 0, visit; visit = visits[i]; i++) {
|
| + var domain = visit.domain;
|
| + if (domainIndexes[domain] == undefined) {
|
| + domainIndexes[domain] = domains.length;
|
| + domains.push({
|
| + domain: domain,
|
| + visits: [],
|
| + expanded: false,
|
| + rendered: false,
|
| + });
|
| + }
|
| + domains[domainIndexes[domain]].visits.push(visit);
|
| + }
|
| + var sortByVisits = function(a, b) {
|
| + return b.visits.length - a.visits.length;
|
| + };
|
| + domains.sort(sortByVisits);
|
| +
|
| + return domains;
|
| + },
|
| +
|
| + updateGroupedHistoryData_: function() {
|
| + if (this.historyData.length == 0) {
|
| + this.groupedHistoryData_ = [];
|
| + return;
|
| + }
|
| +
|
| + if (this.range == HistoryRange.WEEK) {
|
| + // Group each day into a list of results.
|
| + var days = [];
|
| + var currentDayVisits = [this.historyData[0]];
|
| +
|
| + var pushCurrentDay = function() {
|
| + days.push({
|
| + title: this.searchedTerm ? currentDayVisits[0].dateShort :
|
| + currentDayVisits[0].dateRelativeDay,
|
| + domains: this.createHistoryDomains_(currentDayVisits),
|
| + });
|
| + }.bind(this);
|
| +
|
| + var visitsSameDay = function(a, b) {
|
| + if (this.searchedTerm)
|
| + return a.dateShort == b.dateShort;
|
| +
|
| + return a.dateRelativeDay == b.dateRelativeDay;
|
| + }.bind(this);
|
| +
|
| + for (var i = 1; i < this.historyData.length; i++) {
|
| + var visit = this.historyData[i];
|
| + if (!visitsSameDay(visit, currentDayVisits[0])) {
|
| + pushCurrentDay();
|
| + currentDayVisits = [];
|
| + }
|
| + currentDayVisits.push(visit);
|
| + }
|
| + pushCurrentDay();
|
| +
|
| + this.groupedHistoryData_ = days;
|
| + } else if (this.range == HistoryRange.MONTH) {
|
| + // Group each all visits into a single list.
|
| + this.groupedHistoryData_ = [{
|
| + title: this.queryStartTime + ' – ' + this.queryEndTime,
|
| + domains: this.createHistoryDomains_(this.historyData)
|
| + }];
|
| + }
|
| + },
|
|
|
| /**
|
| - * Fired when the overlay is canceled, but before it is closed.
|
| - * @event iron-overlay-canceled
|
| - * @param {Event} event The closing of the overlay can be prevented
|
| - * by calling `event.preventDefault()`. The `event.detail` is the original event that
|
| - * originated the canceling (e.g. ESC keyboard event or click event outside the overlay).
|
| + * @param {{model:Object, currentTarget:IronCollapseElement}} e
|
| */
|
| + toggleDomainExpanded_: function(e) {
|
| + var collapse = e.currentTarget.parentNode.querySelector('iron-collapse');
|
| + e.model.set('domain.rendered', true);
|
| +
|
| + // Give the history-items time to render.
|
| + setTimeout(function() { collapse.toggle() }, 0);
|
| + },
|
|
|
| /**
|
| - * Fired after the overlay closes.
|
| - * @event iron-overlay-closed
|
| - * @param {Event} event The `event.detail` is the `closingReason` property
|
| - * (contains `canceled`, whether the overlay was canceled).
|
| + * Check whether the time difference between the given history item and the
|
| + * next one is large enough for a spacer to be required.
|
| + * @param {number} groupIndex
|
| + * @param {number} domainIndex
|
| + * @param {number} itemIndex
|
| + * @return {boolean} Whether or not time gap separator is required.
|
| + * @private
|
| */
|
| + needsTimeGap_: function(groupIndex, domainIndex, itemIndex) {
|
| + var visits =
|
| + this.groupedHistoryData_[groupIndex].domains[domainIndex].visits;
|
|
|
| -})();
|
| + return md_history.HistoryItem.needsTimeGap(
|
| + visits, itemIndex, this.searchedTerm);
|
| + },
|
| +
|
| + hasResults_: function(historyDataLength) {
|
| + return historyDataLength > 0;
|
| + },
|
| +
|
| + getWebsiteIconStyle_: function(domain) {
|
| + return 'background-image: ' +
|
| + cr.icon.getFaviconImageSet(domain.visits[0].url);
|
| + },
|
| +
|
| + getDropdownIcon_: function(expanded) {
|
| + return expanded ? 'cr:expand-less' : 'cr:expand-more';
|
| + },
|
| +
|
| + noResultsMessage_: function(searchedTerm) {
|
| + var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
|
| + return loadTimeData.getString(messageId);
|
| + },
|
| +});
|
| /**
|
| - * `Polymer.NeonAnimatableBehavior` is implemented by elements containing animations for use with
|
| - * elements implementing `Polymer.NeonAnimationRunnerBehavior`.
|
| + * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll events from a
|
| + * designated scroll target.
|
| + *
|
| + * Elements that consume this behavior can override the `_scrollHandler`
|
| + * method to add logic on the scroll event.
|
| + *
|
| + * @demo demo/scrolling-region.html Scrolling Region
|
| + * @demo demo/document.html Document Element
|
| * @polymerBehavior
|
| */
|
| - Polymer.NeonAnimatableBehavior = {
|
| + Polymer.IronScrollTargetBehavior = {
|
|
|
| properties: {
|
|
|
| /**
|
| - * Animation configuration. See README for more info.
|
| - */
|
| - animationConfig: {
|
| - type: Object
|
| - },
|
| -
|
| - /**
|
| - * Convenience property for setting an 'entry' animation. Do not set `animationConfig.entry`
|
| - * manually if using this. The animated node is set to `this` if using this property.
|
| - */
|
| - entryAnimation: {
|
| - observer: '_entryAnimationChanged',
|
| - type: String
|
| - },
|
| -
|
| - /**
|
| - * Convenience property for setting an 'exit' animation. Do not set `animationConfig.exit`
|
| - * manually if using this. The animated node is set to `this` if using this property.
|
| - */
|
| - exitAnimation: {
|
| - observer: '_exitAnimationChanged',
|
| - type: String
|
| + * Specifies the element that will handle the scroll event
|
| + * on the behalf of the current element. This is typically a reference to an element,
|
| + * but there are a few more posibilities:
|
| + *
|
| + * ### Elements id
|
| + *
|
| + *```html
|
| + * <div id="scrollable-element" style="overflow: auto;">
|
| + * <x-element scroll-target="scrollable-element">
|
| + * \x3c!-- Content--\x3e
|
| + * </x-element>
|
| + * </div>
|
| + *```
|
| + * In this case, the `scrollTarget` will point to the outer div element.
|
| + *
|
| + * ### Document scrolling
|
| + *
|
| + * For document scrolling, you can use the reserved word `document`:
|
| + *
|
| + *```html
|
| + * <x-element scroll-target="document">
|
| + * \x3c!-- Content --\x3e
|
| + * </x-element>
|
| + *```
|
| + *
|
| + * ### Elements reference
|
| + *
|
| + *```js
|
| + * appHeader.scrollTarget = document.querySelector('#scrollable-element');
|
| + *```
|
| + *
|
| + * @type {HTMLElement}
|
| + */
|
| + scrollTarget: {
|
| + type: HTMLElement,
|
| + value: function() {
|
| + return this._defaultScrollTarget;
|
| + }
|
| }
|
| -
|
| },
|
|
|
| - _entryAnimationChanged: function() {
|
| - this.animationConfig = this.animationConfig || {};
|
| - this.animationConfig['entry'] = [{
|
| - name: this.entryAnimation,
|
| - node: this
|
| - }];
|
| - },
|
| + observers: [
|
| + '_scrollTargetChanged(scrollTarget, isAttached)'
|
| + ],
|
|
|
| - _exitAnimationChanged: function() {
|
| - this.animationConfig = this.animationConfig || {};
|
| - this.animationConfig['exit'] = [{
|
| - name: this.exitAnimation,
|
| - node: this
|
| - }];
|
| - },
|
| + _scrollTargetChanged: function(scrollTarget, isAttached) {
|
| + var eventTarget;
|
|
|
| - _copyProperties: function(config1, config2) {
|
| - // shallowly copy properties from config2 to config1
|
| - for (var property in config2) {
|
| - config1[property] = config2[property];
|
| + if (this._oldScrollTarget) {
|
| + eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScrollTarget;
|
| + eventTarget.removeEventListener('scroll', this._boundScrollHandler);
|
| + this._oldScrollTarget = null;
|
| }
|
| - },
|
| -
|
| - _cloneConfig: function(config) {
|
| - var clone = {
|
| - isClone: true
|
| - };
|
| - this._copyProperties(clone, config);
|
| - return clone;
|
| - },
|
|
|
| - _getAnimationConfigRecursive: function(type, map, allConfigs) {
|
| - if (!this.animationConfig) {
|
| + if (!isAttached) {
|
| return;
|
| }
|
| + // Support element id references
|
| + if (scrollTarget === 'document') {
|
|
|
| - if(this.animationConfig.value && typeof this.animationConfig.value === 'function') {
|
| - this._warn(this._logf('playAnimation', "Please put 'animationConfig' inside of your components 'properties' object instead of outside of it."));
|
| - return;
|
| - }
|
| + this.scrollTarget = this._doc;
|
|
|
| - // type is optional
|
| - var thisConfig;
|
| - if (type) {
|
| - thisConfig = this.animationConfig[type];
|
| - } else {
|
| - thisConfig = this.animationConfig;
|
| - }
|
| + } else if (typeof scrollTarget === 'string') {
|
|
|
| - if (!Array.isArray(thisConfig)) {
|
| - thisConfig = [thisConfig];
|
| - }
|
| + this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
|
| + Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
|
|
| - // iterate animations and recurse to process configurations from child nodes
|
| - if (thisConfig) {
|
| - for (var config, index = 0; config = thisConfig[index]; index++) {
|
| - if (config.animatable) {
|
| - config.animatable._getAnimationConfigRecursive(config.type || type, map, allConfigs);
|
| - } else {
|
| - if (config.id) {
|
| - var cachedConfig = map[config.id];
|
| - if (cachedConfig) {
|
| - // merge configurations with the same id, making a clone lazily
|
| - if (!cachedConfig.isClone) {
|
| - map[config.id] = this._cloneConfig(cachedConfig)
|
| - cachedConfig = map[config.id];
|
| - }
|
| - this._copyProperties(cachedConfig, config);
|
| - } else {
|
| - // put any configs with an id into a map
|
| - map[config.id] = config;
|
| - }
|
| - } else {
|
| - allConfigs.push(config);
|
| - }
|
| - }
|
| - }
|
| + } else if (this._isValidScrollTarget()) {
|
| +
|
| + eventTarget = scrollTarget === this._doc ? window : scrollTarget;
|
| + this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
|
| + this._oldScrollTarget = scrollTarget;
|
| +
|
| + eventTarget.addEventListener('scroll', this._boundScrollHandler);
|
| }
|
| },
|
|
|
| /**
|
| - * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this method to configure
|
| - * an animation with an optional type. Elements implementing `Polymer.NeonAnimatableBehavior`
|
| - * should define the property `animationConfig`, which is either a configuration object
|
| - * or a map of animation type to array of configuration objects.
|
| + * Runs on every scroll event. Consumer of this behavior may override this method.
|
| + *
|
| + * @protected
|
| */
|
| - getAnimationConfig: function(type) {
|
| - var map = {};
|
| - var allConfigs = [];
|
| - this._getAnimationConfigRecursive(type, map, allConfigs);
|
| - // append the configurations saved in the map to the array
|
| - for (var key in map) {
|
| - allConfigs.push(map[key]);
|
| - }
|
| - return allConfigs;
|
| - }
|
| + _scrollHandler: function scrollHandler() {},
|
|
|
| - };
|
| -/**
|
| - * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations.
|
| - *
|
| - * @polymerBehavior Polymer.NeonAnimationRunnerBehavior
|
| - */
|
| - Polymer.NeonAnimationRunnerBehaviorImpl = {
|
| + /**
|
| + * The default scroll target. Consumers of this behavior may want to customize
|
| + * the default scroll target.
|
| + *
|
| + * @type {Element}
|
| + */
|
| + get _defaultScrollTarget() {
|
| + return this._doc;
|
| + },
|
|
|
| - _configureAnimations: function(configs) {
|
| - var results = [];
|
| - if (configs.length > 0) {
|
| - for (var config, index = 0; config = configs[index]; index++) {
|
| - var neonAnimation = document.createElement(config.name);
|
| - // is this element actually a neon animation?
|
| - if (neonAnimation.isNeonAnimation) {
|
| - var result = null;
|
| - // configuration or play could fail if polyfills aren't loaded
|
| - try {
|
| - result = neonAnimation.configure(config);
|
| - // Check if we have an Effect rather than an Animation
|
| - if (typeof result.cancel != 'function') {
|
| - result = document.timeline.play(result);
|
| - }
|
| - } catch (e) {
|
| - result = null;
|
| - console.warn('Couldnt play', '(', config.name, ').', e);
|
| - }
|
| - if (result) {
|
| - results.push({
|
| - neonAnimation: neonAnimation,
|
| - config: config,
|
| - animation: result,
|
| - });
|
| - }
|
| - } else {
|
| - console.warn(this.is + ':', config.name, 'not found!');
|
| - }
|
| - }
|
| - }
|
| - return results;
|
| + /**
|
| + * Shortcut for the document element
|
| + *
|
| + * @type {Element}
|
| + */
|
| + get _doc() {
|
| + return this.ownerDocument.documentElement;
|
| },
|
|
|
| - _shouldComplete: function(activeEntries) {
|
| - var finished = true;
|
| - for (var i = 0; i < activeEntries.length; i++) {
|
| - if (activeEntries[i].animation.playState != 'finished') {
|
| - finished = false;
|
| - break;
|
| - }
|
| + /**
|
| + * Gets the number of pixels that the content of an element is scrolled upward.
|
| + *
|
| + * @type {number}
|
| + */
|
| + get _scrollTop() {
|
| + if (this._isValidScrollTarget()) {
|
| + return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop;
|
| }
|
| - return finished;
|
| + return 0;
|
| },
|
|
|
| - _complete: function(activeEntries) {
|
| - for (var i = 0; i < activeEntries.length; i++) {
|
| - activeEntries[i].neonAnimation.complete(activeEntries[i].config);
|
| - }
|
| - for (var i = 0; i < activeEntries.length; i++) {
|
| - activeEntries[i].animation.cancel();
|
| + /**
|
| + * Gets the number of pixels that the content of an element is scrolled to the left.
|
| + *
|
| + * @type {number}
|
| + */
|
| + get _scrollLeft() {
|
| + if (this._isValidScrollTarget()) {
|
| + return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft;
|
| }
|
| + return 0;
|
| },
|
|
|
| /**
|
| - * Plays an animation with an optional `type`.
|
| - * @param {string=} type
|
| - * @param {!Object=} cookie
|
| + * Sets the number of pixels that the content of an element is scrolled upward.
|
| + *
|
| + * @type {number}
|
| */
|
| - playAnimation: function(type, cookie) {
|
| - var configs = this.getAnimationConfig(type);
|
| - if (!configs) {
|
| - return;
|
| - }
|
| - this._active = this._active || {};
|
| - if (this._active[type]) {
|
| - this._complete(this._active[type]);
|
| - delete this._active[type];
|
| + set _scrollTop(top) {
|
| + if (this.scrollTarget === this._doc) {
|
| + window.scrollTo(window.pageXOffset, top);
|
| + } else if (this._isValidScrollTarget()) {
|
| + this.scrollTarget.scrollTop = top;
|
| }
|
| + },
|
|
|
| - var activeEntries = this._configureAnimations(configs);
|
| -
|
| - if (activeEntries.length == 0) {
|
| - this.fire('neon-animation-finish', cookie, {bubbles: false});
|
| - return;
|
| + /**
|
| + * Sets the number of pixels that the content of an element is scrolled to the left.
|
| + *
|
| + * @type {number}
|
| + */
|
| + set _scrollLeft(left) {
|
| + if (this.scrollTarget === this._doc) {
|
| + window.scrollTo(left, window.pageYOffset);
|
| + } else if (this._isValidScrollTarget()) {
|
| + this.scrollTarget.scrollLeft = left;
|
| }
|
| + },
|
|
|
| - this._active[type] = activeEntries;
|
| + /**
|
| + * Scrolls the content to a particular place.
|
| + *
|
| + * @method scroll
|
| + * @param {number} left The left position
|
| + * @param {number} top The top position
|
| + */
|
| + scroll: function(left, top) {
|
| + if (this.scrollTarget === this._doc) {
|
| + window.scrollTo(left, top);
|
| + } else if (this._isValidScrollTarget()) {
|
| + this.scrollTarget.scrollLeft = left;
|
| + this.scrollTarget.scrollTop = top;
|
| + }
|
| + },
|
|
|
| - for (var i = 0; i < activeEntries.length; i++) {
|
| - activeEntries[i].animation.onfinish = function() {
|
| - if (this._shouldComplete(activeEntries)) {
|
| - this._complete(activeEntries);
|
| - delete this._active[type];
|
| - this.fire('neon-animation-finish', cookie, {bubbles: false});
|
| - }
|
| - }.bind(this);
|
| + /**
|
| + * Gets the width of the scroll target.
|
| + *
|
| + * @type {number}
|
| + */
|
| + get _scrollTargetWidth() {
|
| + if (this._isValidScrollTarget()) {
|
| + return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth;
|
| }
|
| + return 0;
|
| },
|
|
|
| /**
|
| - * Cancels the currently running animations.
|
| + * Gets the height of the scroll target.
|
| + *
|
| + * @type {number}
|
| */
|
| - cancelAnimation: function() {
|
| - for (var k in this._animations) {
|
| - this._animations[k].cancel();
|
| + get _scrollTargetHeight() {
|
| + if (this._isValidScrollTarget()) {
|
| + return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight;
|
| }
|
| - this._animations = {};
|
| + return 0;
|
| + },
|
| +
|
| + /**
|
| + * Returns true if the scroll target is a valid HTMLElement.
|
| + *
|
| + * @return {boolean}
|
| + */
|
| + _isValidScrollTarget: function() {
|
| + return this.scrollTarget instanceof HTMLElement;
|
| }
|
| };
|
| +(function() {
|
|
|
| - /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */
|
| - Polymer.NeonAnimationRunnerBehavior = [
|
| - Polymer.NeonAnimatableBehavior,
|
| - Polymer.NeonAnimationRunnerBehaviorImpl
|
| - ];
|
| -/**
|
| - * Use `Polymer.NeonAnimationBehavior` to implement an animation.
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.NeonAnimationBehavior = {
|
| + var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
|
| + var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
|
| + var DEFAULT_PHYSICAL_COUNT = 3;
|
| + var HIDDEN_Y = '-10000px';
|
| + var DEFAULT_GRID_SIZE = 200;
|
| + var SECRET_TABINDEX = -100;
|
| +
|
| + Polymer({
|
| +
|
| + is: 'iron-list',
|
|
|
| properties: {
|
|
|
| /**
|
| - * Defines the animation timing.
|
| + * An array containing items determining how many instances of the template
|
| + * to stamp and that that each template instance should bind to.
|
| */
|
| - animationTiming: {
|
| + items: {
|
| + type: Array
|
| + },
|
| +
|
| + /**
|
| + * The max count of physical items the pool can extend to.
|
| + */
|
| + maxPhysicalCount: {
|
| + type: Number,
|
| + value: 500
|
| + },
|
| +
|
| + /**
|
| + * The name of the variable to add to the binding scope for the array
|
| + * element associated with a given template instance.
|
| + */
|
| + as: {
|
| + type: String,
|
| + value: 'item'
|
| + },
|
| +
|
| + /**
|
| + * The name of the variable to add to the binding scope with the index
|
| + * for the row.
|
| + */
|
| + indexAs: {
|
| + type: String,
|
| + value: 'index'
|
| + },
|
| +
|
| + /**
|
| + * The name of the variable to add to the binding scope to indicate
|
| + * if the row is selected.
|
| + */
|
| + selectedAs: {
|
| + type: String,
|
| + value: 'selected'
|
| + },
|
| +
|
| + /**
|
| + * When true, the list is rendered as a grid. Grid items must have
|
| + * fixed width and height set via CSS. e.g.
|
| + *
|
| + * ```html
|
| + * <iron-list grid>
|
| + * <template>
|
| + * <div style="width: 100px; height: 100px;"> 100x100 </div>
|
| + * </template>
|
| + * </iron-list>
|
| + * ```
|
| + */
|
| + grid: {
|
| + type: Boolean,
|
| + value: false,
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * When true, tapping a row will select the item, placing its data model
|
| + * in the set of selected items retrievable via the selection property.
|
| + *
|
| + * Note that tapping focusable elements within the list item will not
|
| + * result in selection, since they are presumed to have their * own action.
|
| + */
|
| + selectionEnabled: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * When `multiSelection` is false, this is the currently selected item, or `null`
|
| + * if no item is selected.
|
| + */
|
| + selectedItem: {
|
| type: Object,
|
| - value: function() {
|
| - return {
|
| - duration: 500,
|
| - easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
| - fill: 'both'
|
| - }
|
| - }
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * When `multiSelection` is true, this is an array that contains the selected items.
|
| + */
|
| + selectedItems: {
|
| + type: Object,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * When `true`, multiple items may be selected at once (in this case,
|
| + * `selected` is an array of currently selected items). When `false`,
|
| + * only one item may be selected at a time.
|
| + */
|
| + multiSelection: {
|
| + type: Boolean,
|
| + value: false
|
| }
|
| + },
|
| +
|
| + observers: [
|
| + '_itemsChanged(items.*)',
|
| + '_selectionEnabledChanged(selectionEnabled)',
|
| + '_multiSelectionChanged(multiSelection)',
|
| + '_setOverflow(scrollTarget)'
|
| + ],
|
| +
|
| + behaviors: [
|
| + Polymer.Templatizer,
|
| + Polymer.IronResizableBehavior,
|
| + Polymer.IronA11yKeysBehavior,
|
| + Polymer.IronScrollTargetBehavior
|
| + ],
|
|
|
| + keyBindings: {
|
| + 'up': '_didMoveUp',
|
| + 'down': '_didMoveDown',
|
| + 'enter': '_didEnter'
|
| },
|
|
|
| /**
|
| - * Can be used to determine that elements implement this behavior.
|
| + * The ratio of hidden tiles that should remain in the scroll direction.
|
| + * Recommended value ~0.5, so it will distribute tiles evely in both directions.
|
| */
|
| - isNeonAnimation: true,
|
| + _ratio: 0.5,
|
|
|
| /**
|
| - * Do any animation configuration here.
|
| + * The padding-top value for the list.
|
| */
|
| - // configure: function(config) {
|
| - // },
|
| + _scrollerPaddingTop: 0,
|
|
|
| /**
|
| - * Returns the animation timing by mixing in properties from `config` to the defaults defined
|
| - * by the animation.
|
| + * This value is the same as `scrollTop`.
|
| */
|
| - timingFromConfig: function(config) {
|
| - if (config.timing) {
|
| - for (var property in config.timing) {
|
| - this.animationTiming[property] = config.timing[property];
|
| - }
|
| - }
|
| - return this.animationTiming;
|
| - },
|
| + _scrollPosition: 0,
|
|
|
| /**
|
| - * Sets `transform` and `transformOrigin` properties along with the prefixed versions.
|
| + * The sum of the heights of all the tiles in the DOM.
|
| */
|
| - setPrefixedProperty: function(node, property, value) {
|
| - var map = {
|
| - 'transform': ['webkitTransform'],
|
| - 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin']
|
| - };
|
| - var prefixes = map[property];
|
| - for (var prefix, index = 0; prefix = prefixes[index]; index++) {
|
| - node.style[prefix] = value;
|
| - }
|
| - node.style[property] = value;
|
| - },
|
| + _physicalSize: 0,
|
|
|
| /**
|
| - * Called when the animation finishes.
|
| + * The average `offsetHeight` of the tiles observed till now.
|
| */
|
| - complete: function() {}
|
| -
|
| - };
|
| -Polymer({
|
| + _physicalAverage: 0,
|
|
|
| - is: 'opaque-animation',
|
| + /**
|
| + * The number of tiles which `offsetHeight` > 0 observed until now.
|
| + */
|
| + _physicalAverageCount: 0,
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + /**
|
| + * The Y position of the item rendered in the `_physicalStart`
|
| + * tile relative to the scrolling list.
|
| + */
|
| + _physicalTop: 0,
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - this._effect = new KeyframeEffect(node, [
|
| - {'opacity': '1'},
|
| - {'opacity': '1'}
|
| - ], this.timingFromConfig(config));
|
| - node.style.opacity = '0';
|
| - return this._effect;
|
| - },
|
| + /**
|
| + * The number of items in the list.
|
| + */
|
| + _virtualCount: 0,
|
|
|
| - complete: function(config) {
|
| - config.node.style.opacity = '';
|
| - }
|
| + /**
|
| + * A map between an item key and its physical item index
|
| + */
|
| + _physicalIndexForKey: null,
|
|
|
| - });
|
| -(function() {
|
| - 'use strict';
|
| - // Used to calculate the scroll direction during touch events.
|
| - var LAST_TOUCH_POSITION = {
|
| - pageX: 0,
|
| - pageY: 0
|
| - };
|
| - // Used to avoid computing event.path and filter scrollable nodes (better perf).
|
| - var ROOT_TARGET = null;
|
| - var SCROLLABLE_NODES = [];
|
| + /**
|
| + * The estimated scroll height based on `_physicalAverage`
|
| + */
|
| + _estScrollHeight: 0,
|
|
|
| /**
|
| - * The IronDropdownScrollManager is intended to provide a central source
|
| - * of authority and control over which elements in a document are currently
|
| - * allowed to scroll.
|
| + * The scroll height of the dom node
|
| */
|
| + _scrollHeight: 0,
|
|
|
| - Polymer.IronDropdownScrollManager = {
|
| + /**
|
| + * The height of the list. This is referred as the viewport in the context of list.
|
| + */
|
| + _viewportHeight: 0,
|
|
|
| - /**
|
| - * The current element that defines the DOM boundaries of the
|
| - * scroll lock. This is always the most recently locking element.
|
| - */
|
| - get currentLockingElement() {
|
| - return this._lockingElements[this._lockingElements.length - 1];
|
| - },
|
| + /**
|
| + * The width of the list. This is referred as the viewport in the context of list.
|
| + */
|
| + _viewportWidth: 0,
|
|
|
| - /**
|
| - * Returns true if the provided element is "scroll locked", which is to
|
| - * say that it cannot be scrolled via pointer or keyboard interactions.
|
| - *
|
| - * @param {HTMLElement} element An HTML element instance which may or may
|
| - * not be scroll locked.
|
| - */
|
| - elementIsScrollLocked: function(element) {
|
| - var currentLockingElement = this.currentLockingElement;
|
| + /**
|
| + * An array of DOM nodes that are currently in the tree
|
| + * @type {?Array<!TemplatizerNode>}
|
| + */
|
| + _physicalItems: null,
|
|
|
| - if (currentLockingElement === undefined)
|
| - return false;
|
| + /**
|
| + * An array of heights for each item in `_physicalItems`
|
| + * @type {?Array<number>}
|
| + */
|
| + _physicalSizes: null,
|
|
|
| - var scrollLocked;
|
| + /**
|
| + * A cached value for the first visible index.
|
| + * See `firstVisibleIndex`
|
| + * @type {?number}
|
| + */
|
| + _firstVisibleIndexVal: null,
|
|
|
| - if (this._hasCachedLockedElement(element)) {
|
| - return true;
|
| - }
|
| + /**
|
| + * A cached value for the last visible index.
|
| + * See `lastVisibleIndex`
|
| + * @type {?number}
|
| + */
|
| + _lastVisibleIndexVal: null,
|
|
|
| - if (this._hasCachedUnlockedElement(element)) {
|
| - return false;
|
| - }
|
| + /**
|
| + * A Polymer collection for the items.
|
| + * @type {?Polymer.Collection}
|
| + */
|
| + _collection: null,
|
|
|
| - scrollLocked = !!currentLockingElement &&
|
| - currentLockingElement !== element &&
|
| - !this._composedTreeContains(currentLockingElement, element);
|
| + /**
|
| + * True if the current item list was rendered for the first time
|
| + * after attached.
|
| + */
|
| + _itemsRendered: false,
|
|
|
| - if (scrollLocked) {
|
| - this._lockedElementCache.push(element);
|
| - } else {
|
| - this._unlockedElementCache.push(element);
|
| - }
|
| + /**
|
| + * The page that is currently rendered.
|
| + */
|
| + _lastPage: null,
|
|
|
| - return scrollLocked;
|
| - },
|
| + /**
|
| + * The max number of pages to render. One page is equivalent to the height of the list.
|
| + */
|
| + _maxPages: 3,
|
|
|
| - /**
|
| - * Push an element onto the current scroll lock stack. The most recently
|
| - * pushed element and its children will be considered scrollable. All
|
| - * other elements will not be scrollable.
|
| - *
|
| - * Scroll locking is implemented as a stack so that cases such as
|
| - * dropdowns within dropdowns are handled well.
|
| - *
|
| - * @param {HTMLElement} element The element that should lock scroll.
|
| - */
|
| - pushScrollLock: function(element) {
|
| - // Prevent pushing the same element twice
|
| - if (this._lockingElements.indexOf(element) >= 0) {
|
| - return;
|
| - }
|
| + /**
|
| + * The currently focused physical item.
|
| + */
|
| + _focusedItem: null,
|
|
|
| - if (this._lockingElements.length === 0) {
|
| - this._lockScrollInteractions();
|
| - }
|
| + /**
|
| + * The index of the `_focusedItem`.
|
| + */
|
| + _focusedIndex: -1,
|
|
|
| - this._lockingElements.push(element);
|
| + /**
|
| + * The the item that is focused if it is moved offscreen.
|
| + * @private {?TemplatizerNode}
|
| + */
|
| + _offscreenFocusedItem: null,
|
|
|
| - this._lockedElementCache = [];
|
| - this._unlockedElementCache = [];
|
| - },
|
| + /**
|
| + * The item that backfills the `_offscreenFocusedItem` in the physical items
|
| + * list when that item is moved offscreen.
|
| + */
|
| + _focusBackfillItem: null,
|
|
|
| - /**
|
| - * Remove an element from the scroll lock stack. The element being
|
| - * removed does not need to be the most recently pushed element. However,
|
| - * the scroll lock constraints only change when the most recently pushed
|
| - * element is removed.
|
| - *
|
| - * @param {HTMLElement} element The element to remove from the scroll
|
| - * lock stack.
|
| - */
|
| - removeScrollLock: function(element) {
|
| - var index = this._lockingElements.indexOf(element);
|
| + /**
|
| + * The maximum items per row
|
| + */
|
| + _itemsPerRow: 1,
|
|
|
| - if (index === -1) {
|
| - return;
|
| - }
|
| + /**
|
| + * The width of each grid item
|
| + */
|
| + _itemWidth: 0,
|
|
|
| - this._lockingElements.splice(index, 1);
|
| + /**
|
| + * The height of the row in grid layout.
|
| + */
|
| + _rowHeight: 0,
|
|
|
| - this._lockedElementCache = [];
|
| - this._unlockedElementCache = [];
|
| + /**
|
| + * The bottom of the physical content.
|
| + */
|
| + get _physicalBottom() {
|
| + return this._physicalTop + this._physicalSize;
|
| + },
|
|
|
| - if (this._lockingElements.length === 0) {
|
| - this._unlockScrollInteractions();
|
| - }
|
| - },
|
| + /**
|
| + * The bottom of the scroll.
|
| + */
|
| + get _scrollBottom() {
|
| + return this._scrollPosition + this._viewportHeight;
|
| + },
|
|
|
| - _lockingElements: [],
|
| + /**
|
| + * The n-th item rendered in the last physical item.
|
| + */
|
| + get _virtualEnd() {
|
| + return this._virtualStart + this._physicalCount - 1;
|
| + },
|
|
|
| - _lockedElementCache: null,
|
| + /**
|
| + * The height of the physical content that isn't on the screen.
|
| + */
|
| + get _hiddenContentSize() {
|
| + var size = this.grid ? this._physicalRows * this._rowHeight : this._physicalSize;
|
| + return size - this._viewportHeight;
|
| + },
|
|
|
| - _unlockedElementCache: null,
|
| + /**
|
| + * The maximum scroll top value.
|
| + */
|
| + get _maxScrollTop() {
|
| + return this._estScrollHeight - this._viewportHeight + this._scrollerPaddingTop;
|
| + },
|
|
|
| - _hasCachedLockedElement: function(element) {
|
| - return this._lockedElementCache.indexOf(element) > -1;
|
| - },
|
| + /**
|
| + * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
|
| + */
|
| + _minVirtualStart: 0,
|
|
|
| - _hasCachedUnlockedElement: function(element) {
|
| - return this._unlockedElementCache.indexOf(element) > -1;
|
| - },
|
| + /**
|
| + * The largest n-th value for an item such that it can be rendered in `_physicalStart`.
|
| + */
|
| + get _maxVirtualStart() {
|
| + return Math.max(0, this._virtualCount - this._physicalCount);
|
| + },
|
|
|
| - _composedTreeContains: function(element, child) {
|
| - // NOTE(cdata): This method iterates over content elements and their
|
| - // corresponding distributed nodes to implement a contains-like method
|
| - // that pierces through the composed tree of the ShadowDOM. Results of
|
| - // this operation are cached (elsewhere) on a per-scroll-lock basis, to
|
| - // guard against potentially expensive lookups happening repeatedly as
|
| - // a user scrolls / touchmoves.
|
| - var contentElements;
|
| - var distributedNodes;
|
| - var contentIndex;
|
| - var nodeIndex;
|
| + /**
|
| + * The n-th item rendered in the `_physicalStart` tile.
|
| + */
|
| + _virtualStartVal: 0,
|
|
|
| - if (element.contains(child)) {
|
| - return true;
|
| - }
|
| + set _virtualStart(val) {
|
| + this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
|
| + },
|
|
|
| - contentElements = Polymer.dom(element).querySelectorAll('content');
|
| + get _virtualStart() {
|
| + return this._virtualStartVal || 0;
|
| + },
|
|
|
| - for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) {
|
| + /**
|
| + * The k-th tile that is at the top of the scrolling list.
|
| + */
|
| + _physicalStartVal: 0,
|
|
|
| - distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes();
|
| + set _physicalStart(val) {
|
| + this._physicalStartVal = val % this._physicalCount;
|
| + if (this._physicalStartVal < 0) {
|
| + this._physicalStartVal = this._physicalCount + this._physicalStartVal;
|
| + }
|
| + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| + },
|
|
|
| - for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
|
| + get _physicalStart() {
|
| + return this._physicalStartVal || 0;
|
| + },
|
|
|
| - if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
|
| - return true;
|
| - }
|
| - }
|
| - }
|
| + /**
|
| + * The number of tiles in the DOM.
|
| + */
|
| + _physicalCountVal: 0,
|
|
|
| - return false;
|
| - },
|
| + set _physicalCount(val) {
|
| + this._physicalCountVal = val;
|
| + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| + },
|
|
|
| - _scrollInteractionHandler: function(event) {
|
| - // Avoid canceling an event with cancelable=false, e.g. scrolling is in
|
| - // progress and cannot be interrupted.
|
| - if (event.cancelable && this._shouldPreventScrolling(event)) {
|
| - event.preventDefault();
|
| - }
|
| - // If event has targetTouches (touch event), update last touch position.
|
| - if (event.targetTouches) {
|
| - var touch = event.targetTouches[0];
|
| - LAST_TOUCH_POSITION.pageX = touch.pageX;
|
| - LAST_TOUCH_POSITION.pageY = touch.pageY;
|
| - }
|
| - },
|
| + get _physicalCount() {
|
| + return this._physicalCountVal;
|
| + },
|
|
|
| - _lockScrollInteractions: function() {
|
| - this._boundScrollHandler = this._boundScrollHandler ||
|
| - this._scrollInteractionHandler.bind(this);
|
| - // Modern `wheel` event for mouse wheel scrolling:
|
| - document.addEventListener('wheel', this._boundScrollHandler, true);
|
| - // Older, non-standard `mousewheel` event for some FF:
|
| - document.addEventListener('mousewheel', this._boundScrollHandler, true);
|
| - // IE:
|
| - document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true);
|
| - // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
|
| - document.addEventListener('touchstart', this._boundScrollHandler, true);
|
| - // Mobile devices can scroll on touch move:
|
| - document.addEventListener('touchmove', this._boundScrollHandler, true);
|
| - },
|
| + /**
|
| + * The k-th tile that is at the bottom of the scrolling list.
|
| + */
|
| + _physicalEnd: 0,
|
|
|
| - _unlockScrollInteractions: function() {
|
| - document.removeEventListener('wheel', this._boundScrollHandler, true);
|
| - document.removeEventListener('mousewheel', this._boundScrollHandler, true);
|
| - document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
|
| - document.removeEventListener('touchstart', this._boundScrollHandler, true);
|
| - document.removeEventListener('touchmove', this._boundScrollHandler, true);
|
| - },
|
| + /**
|
| + * An optimal physical size such that we will have enough physical items
|
| + * to fill up the viewport and recycle when the user scrolls.
|
| + *
|
| + * This default value assumes that we will at least have the equivalent
|
| + * to a viewport of physical items above and below the user's viewport.
|
| + */
|
| + get _optPhysicalSize() {
|
| + if (this.grid) {
|
| + return this._estRowsInView * this._rowHeight * this._maxPages;
|
| + }
|
| + return this._viewportHeight * this._maxPages;
|
| + },
|
|
|
| - /**
|
| - * Returns true if the event causes scroll outside the current locking
|
| - * element, e.g. pointer/keyboard interactions, or scroll "leaking"
|
| - * outside the locking element when it is already at its scroll boundaries.
|
| - * @param {!Event} event
|
| - * @return {boolean}
|
| - * @private
|
| - */
|
| - _shouldPreventScrolling: function(event) {
|
| + get _optPhysicalCount() {
|
| + return this._estRowsInView * this._itemsPerRow * this._maxPages;
|
| + },
|
|
|
| - // Update if root target changed. For touch events, ensure we don't
|
| - // update during touchmove.
|
| - var target = Polymer.dom(event).rootTarget;
|
| - if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
|
| - ROOT_TARGET = target;
|
| - SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
|
| - }
|
| + /**
|
| + * True if the current list is visible.
|
| + */
|
| + get _isVisible() {
|
| + return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight);
|
| + },
|
|
|
| - // Prevent event if no scrollable nodes.
|
| - if (!SCROLLABLE_NODES.length) {
|
| - return true;
|
| - }
|
| - // Don't prevent touchstart event inside the locking element when it has
|
| - // scrollable nodes.
|
| - if (event.type === 'touchstart') {
|
| - return false;
|
| - }
|
| - // Get deltaX/Y.
|
| - var info = this._getScrollInfo(event);
|
| - // Prevent if there is no child that can scroll.
|
| - return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY);
|
| - },
|
| + /**
|
| + * Gets the index of the first visible item in the viewport.
|
| + *
|
| + * @type {number}
|
| + */
|
| + get firstVisibleIndex() {
|
| + if (this._firstVisibleIndexVal === null) {
|
| + var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddingTop);
|
|
|
| - /**
|
| - * Returns an array of scrollable nodes up to the current locking element,
|
| - * which is included too if scrollable.
|
| - * @param {!Array<Node>} nodes
|
| - * @return {Array<Node>} scrollables
|
| - * @private
|
| - */
|
| - _getScrollableNodes: function(nodes) {
|
| - var scrollables = [];
|
| - var lockingIndex = nodes.indexOf(this.currentLockingElement);
|
| - // Loop from root target to locking element (included).
|
| - for (var i = 0; i <= lockingIndex; i++) {
|
| - var node = nodes[i];
|
| - // Skip document fragments.
|
| - if (node.nodeType === 11) {
|
| - continue;
|
| - }
|
| - // Check inline style before checking computed style.
|
| - var style = node.style;
|
| - if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
|
| - style = window.getComputedStyle(node);
|
| - }
|
| - if (style.overflow === 'scroll' || style.overflow === 'auto') {
|
| - scrollables.push(node);
|
| - }
|
| - }
|
| - return scrollables;
|
| - },
|
| + this._firstVisibleIndexVal = this._iterateItems(
|
| + function(pidx, vidx) {
|
| + physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
|
|
| - /**
|
| - * Returns the node that is scrolling. If there is no scrolling,
|
| - * returns undefined.
|
| - * @param {!Array<Node>} nodes
|
| - * @param {number} deltaX Scroll delta on the x-axis
|
| - * @param {number} deltaY Scroll delta on the y-axis
|
| - * @return {Node|undefined}
|
| - * @private
|
| - */
|
| - _getScrollingNode: function(nodes, deltaX, deltaY) {
|
| - // No scroll.
|
| - if (!deltaX && !deltaY) {
|
| - return;
|
| - }
|
| - // Check only one axis according to where there is more scroll.
|
| - // Prefer vertical to horizontal.
|
| - var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
|
| - for (var i = 0; i < nodes.length; i++) {
|
| - var node = nodes[i];
|
| - var canScroll = false;
|
| - if (verticalScroll) {
|
| - // delta < 0 is scroll up, delta > 0 is scroll down.
|
| - canScroll = deltaY < 0 ? node.scrollTop > 0 :
|
| - node.scrollTop < node.scrollHeight - node.clientHeight;
|
| - } else {
|
| - // delta < 0 is scroll left, delta > 0 is scroll right.
|
| - canScroll = deltaX < 0 ? node.scrollLeft > 0 :
|
| - node.scrollLeft < node.scrollWidth - node.clientWidth;
|
| - }
|
| - if (canScroll) {
|
| - return node;
|
| - }
|
| - }
|
| - },
|
| + if (physicalOffset > this._scrollPosition) {
|
| + return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
|
| + }
|
| + // Handle a partially rendered final row in grid mode
|
| + if (this.grid && this._virtualCount - 1 === vidx) {
|
| + return vidx - (vidx % this._itemsPerRow);
|
| + }
|
| + }) || 0;
|
| + }
|
| + return this._firstVisibleIndexVal;
|
| + },
|
|
|
| - /**
|
| - * Returns scroll `deltaX` and `deltaY`.
|
| - * @param {!Event} event The scroll event
|
| - * @return {{
|
| - * deltaX: number The x-axis scroll delta (positive: scroll right,
|
| - * negative: scroll left, 0: no scroll),
|
| - * deltaY: number The y-axis scroll delta (positive: scroll down,
|
| - * negative: scroll up, 0: no scroll)
|
| - * }} info
|
| - * @private
|
| - */
|
| - _getScrollInfo: function(event) {
|
| - var info = {
|
| - deltaX: event.deltaX,
|
| - deltaY: event.deltaY
|
| - };
|
| - // Already available.
|
| - if ('deltaX' in event) {
|
| - // do nothing, values are already good.
|
| - }
|
| - // Safari has scroll info in `wheelDeltaX/Y`.
|
| - else if ('wheelDeltaX' in event) {
|
| - info.deltaX = -event.wheelDeltaX;
|
| - info.deltaY = -event.wheelDeltaY;
|
| - }
|
| - // Firefox has scroll info in `detail` and `axis`.
|
| - else if ('axis' in event) {
|
| - info.deltaX = event.axis === 1 ? event.detail : 0;
|
| - info.deltaY = event.axis === 2 ? event.detail : 0;
|
| - }
|
| - // On mobile devices, calculate scroll direction.
|
| - else if (event.targetTouches) {
|
| - var touch = event.targetTouches[0];
|
| - // Touch moves from right to left => scrolling goes right.
|
| - info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
|
| - // Touch moves from down to up => scrolling goes down.
|
| - info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
|
| + /**
|
| + * Gets the index of the last visible item in the viewport.
|
| + *
|
| + * @type {number}
|
| + */
|
| + get lastVisibleIndex() {
|
| + if (this._lastVisibleIndexVal === null) {
|
| + if (this.grid) {
|
| + var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._itemsPerRow - 1;
|
| + this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex);
|
| + } else {
|
| + var physicalOffset = this._physicalTop;
|
| + this._iterateItems(function(pidx, vidx) {
|
| + if (physicalOffset < this._scrollBottom) {
|
| + this._lastVisibleIndexVal = vidx;
|
| + } else {
|
| + // Break _iterateItems
|
| + return true;
|
| + }
|
| + physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
| + });
|
| }
|
| - return info;
|
| }
|
| - };
|
| - })();
|
| -(function() {
|
| - 'use strict';
|
| + return this._lastVisibleIndexVal;
|
| + },
|
|
|
| - Polymer({
|
| - is: 'iron-dropdown',
|
| + get _defaultScrollTarget() {
|
| + return this;
|
| + },
|
| + get _virtualRowCount() {
|
| + return Math.ceil(this._virtualCount / this._itemsPerRow);
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.IronControlState,
|
| - Polymer.IronA11yKeysBehavior,
|
| - Polymer.IronOverlayBehavior,
|
| - Polymer.NeonAnimationRunnerBehavior
|
| - ],
|
| + get _estRowsInView() {
|
| + return Math.ceil(this._viewportHeight / this._rowHeight);
|
| + },
|
|
|
| - properties: {
|
| - /**
|
| - * The orientation against which to align the dropdown content
|
| - * horizontally relative to the dropdown trigger.
|
| - * Overridden from `Polymer.IronFitBehavior`.
|
| - */
|
| - horizontalAlign: {
|
| - type: String,
|
| - value: 'left',
|
| - reflectToAttribute: true
|
| - },
|
| + get _physicalRows() {
|
| + return Math.ceil(this._physicalCount / this._itemsPerRow);
|
| + },
|
|
|
| - /**
|
| - * The orientation against which to align the dropdown content
|
| - * vertically relative to the dropdown trigger.
|
| - * Overridden from `Polymer.IronFitBehavior`.
|
| - */
|
| - verticalAlign: {
|
| - type: String,
|
| - value: 'top',
|
| - reflectToAttribute: true
|
| - },
|
| + ready: function() {
|
| + this.addEventListener('focus', this._didFocus.bind(this), true);
|
| + },
|
|
|
| - /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * opening of the dropdown.
|
| - */
|
| - openAnimationConfig: {
|
| - type: Object
|
| - },
|
| + attached: function() {
|
| + this.updateViewportBoundaries();
|
| + this._render();
|
| + // `iron-resize` is fired when the list is attached if the event is added
|
| + // before attached causing unnecessary work.
|
| + this.listen(this, 'iron-resize', '_resizeHandler');
|
| + },
|
|
|
| - /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * closing of the dropdown.
|
| - */
|
| - closeAnimationConfig: {
|
| - type: Object
|
| - },
|
| + detached: function() {
|
| + this._itemsRendered = false;
|
| + this.unlisten(this, 'iron-resize', '_resizeHandler');
|
| + },
|
|
|
| - /**
|
| - * If provided, this will be the element that will be focused when
|
| - * the dropdown opens.
|
| - */
|
| - focusTarget: {
|
| - type: Object
|
| - },
|
| + /**
|
| + * Set the overflow property if this element has its own scrolling region
|
| + */
|
| + _setOverflow: function(scrollTarget) {
|
| + this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
|
| + this.style.overflow = scrollTarget === this ? 'auto' : '';
|
| + },
|
|
|
| - /**
|
| - * Set to true to disable animations when opening and closing the
|
| - * dropdown.
|
| - */
|
| - noAnimations: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + /**
|
| + * Invoke this method if you dynamically update the viewport's
|
| + * size or CSS padding.
|
| + *
|
| + * @method updateViewportBoundaries
|
| + */
|
| + updateViewportBoundaries: function() {
|
| + this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
|
| + parseInt(window.getComputedStyle(this)['padding-top'], 10);
|
|
|
| - /**
|
| - * By default, the dropdown will constrain scrolling on the page
|
| - * to itself when opened.
|
| - * Set to true in order to prevent scroll from being constrained
|
| - * to the dropdown when it opens.
|
| - */
|
| - allowOutsideScroll: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + this._viewportHeight = this._scrollTargetHeight;
|
| + if (this.grid) {
|
| + this._updateGridMetrics();
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Callback for scroll events.
|
| - * @type {Function}
|
| - * @private
|
| - */
|
| - _boundOnCaptureScroll: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onCaptureScroll.bind(this);
|
| - }
|
| - }
|
| - },
|
| + /**
|
| + * Update the models, the position of the
|
| + * items in the viewport and recycle tiles as needed.
|
| + */
|
| + _scrollHandler: function() {
|
| + // clamp the `scrollTop` value
|
| + var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
|
| + var delta = scrollTop - this._scrollPosition;
|
| + var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
|
| + var ratio = this._ratio;
|
| + var recycledTiles = 0;
|
| + var hiddenContentSize = this._hiddenContentSize;
|
| + var currentRatio = ratio;
|
| + var movingUp = [];
|
|
|
| - listeners: {
|
| - 'neon-animation-finish': '_onNeonAnimationFinish'
|
| - },
|
| + // track the last `scrollTop`
|
| + this._scrollPosition = scrollTop;
|
|
|
| - observers: [
|
| - '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
|
| - ],
|
| + // clear cached visible indexes
|
| + this._firstVisibleIndexVal = null;
|
| + this._lastVisibleIndexVal = null;
|
|
|
| - /**
|
| - * The element that is contained by the dropdown, if any.
|
| - */
|
| - get containedElement() {
|
| - return Polymer.dom(this.$.content).getDistributedNodes()[0];
|
| - },
|
| + scrollBottom = this._scrollBottom;
|
| + physicalBottom = this._physicalBottom;
|
| +
|
| + // random access
|
| + if (Math.abs(delta) > this._physicalSize) {
|
| + this._physicalTop += delta;
|
| + recycledTiles = Math.round(delta / this._physicalAverage);
|
| + }
|
| + // scroll up
|
| + else if (delta < 0) {
|
| + var topSpace = scrollTop - this._physicalTop;
|
| + var virtualStart = this._virtualStart;
|
| +
|
| + recycledTileSet = [];
|
| +
|
| + kth = this._physicalEnd;
|
| + currentRatio = topSpace / hiddenContentSize;
|
| +
|
| + // move tiles from bottom to top
|
| + while (
|
| + // approximate `currentRatio` to `ratio`
|
| + currentRatio < ratio &&
|
| + // recycle less physical items than the total
|
| + recycledTiles < this._physicalCount &&
|
| + // ensure that these recycled tiles are needed
|
| + virtualStart - recycledTiles > 0 &&
|
| + // ensure that the tile is not visible
|
| + physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom
|
| + ) {
|
| +
|
| + tileHeight = this._getPhysicalSizeIncrement(kth);
|
| + currentRatio += tileHeight / hiddenContentSize;
|
| + physicalBottom -= tileHeight;
|
| + recycledTileSet.push(kth);
|
| + recycledTiles++;
|
| + kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
|
| + }
|
| +
|
| + movingUp = recycledTileSet;
|
| + recycledTiles = -recycledTiles;
|
| + }
|
| + // scroll down
|
| + else if (delta > 0) {
|
| + var bottomSpace = physicalBottom - scrollBottom;
|
| + var virtualEnd = this._virtualEnd;
|
| + var lastVirtualItemIndex = this._virtualCount-1;
|
| +
|
| + recycledTileSet = [];
|
| +
|
| + kth = this._physicalStart;
|
| + currentRatio = bottomSpace / hiddenContentSize;
|
| +
|
| + // move tiles from top to bottom
|
| + while (
|
| + // approximate `currentRatio` to `ratio`
|
| + currentRatio < ratio &&
|
| + // recycle less physical items than the total
|
| + recycledTiles < this._physicalCount &&
|
| + // ensure that these recycled tiles are needed
|
| + virtualEnd + recycledTiles < lastVirtualItemIndex &&
|
| + // ensure that the tile is not visible
|
| + this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop
|
| + ) {
|
| +
|
| + tileHeight = this._getPhysicalSizeIncrement(kth);
|
| + currentRatio += tileHeight / hiddenContentSize;
|
| +
|
| + this._physicalTop += tileHeight;
|
| + recycledTileSet.push(kth);
|
| + recycledTiles++;
|
| + kth = (kth + 1) % this._physicalCount;
|
| + }
|
| + }
|
| +
|
| + if (recycledTiles === 0) {
|
| + // Try to increase the pool if the list's client height isn't filled up with physical items
|
| + if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
|
| + this._increasePoolIfNeeded();
|
| + }
|
| + } else {
|
| + this._virtualStart = this._virtualStart + recycledTiles;
|
| + this._physicalStart = this._physicalStart + recycledTiles;
|
| + this._update(recycledTileSet, movingUp);
|
| + }
|
| + },
|
|
|
| - /**
|
| - * The element that should be focused when the dropdown opens.
|
| - * @deprecated
|
| - */
|
| - get _focusTarget() {
|
| - return this.focusTarget || this.containedElement;
|
| - },
|
| + /**
|
| + * Update the list of items, starting from the `_virtualStart` item.
|
| + * @param {!Array<number>=} itemSet
|
| + * @param {!Array<number>=} movingUp
|
| + */
|
| + _update: function(itemSet, movingUp) {
|
| + // manage focus
|
| + this._manageFocus();
|
| + // update models
|
| + this._assignModels(itemSet);
|
| + // measure heights
|
| + this._updateMetrics(itemSet);
|
| + // adjust offset after measuring
|
| + if (movingUp) {
|
| + while (movingUp.length) {
|
| + var idx = movingUp.pop();
|
| + this._physicalTop -= this._getPhysicalSizeIncrement(idx);
|
| + }
|
| + }
|
| + // update the position of the items
|
| + this._positionItems();
|
| + // set the scroller size
|
| + this._updateScrollerSize();
|
| + // increase the pool of physical items
|
| + this._increasePoolIfNeeded();
|
| + },
|
|
|
| - ready: function() {
|
| - // Memoized scrolling position, used to block scrolling outside.
|
| - this._scrollTop = 0;
|
| - this._scrollLeft = 0;
|
| - // Used to perform a non-blocking refit on scroll.
|
| - this._refitOnScrollRAF = null;
|
| - },
|
| + /**
|
| + * Creates a pool of DOM elements and attaches them to the local dom.
|
| + */
|
| + _createPool: function(size) {
|
| + var physicalItems = new Array(size);
|
|
|
| - detached: function() {
|
| - this.cancelAnimation();
|
| - Polymer.IronDropdownScrollManager.removeScrollLock(this);
|
| - },
|
| + this._ensureTemplatized();
|
|
|
| - /**
|
| - * Called when the value of `opened` changes.
|
| - * Overridden from `IronOverlayBehavior`
|
| - */
|
| - _openedChanged: function() {
|
| - if (this.opened && this.disabled) {
|
| - this.cancel();
|
| - } else {
|
| - this.cancelAnimation();
|
| - this.sizingTarget = this.containedElement || this.sizingTarget;
|
| - this._updateAnimationConfig();
|
| - this._saveScrollPosition();
|
| - if (this.opened) {
|
| - document.addEventListener('scroll', this._boundOnCaptureScroll);
|
| - !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScrollLock(this);
|
| - } else {
|
| - document.removeEventListener('scroll', this._boundOnCaptureScroll);
|
| - Polymer.IronDropdownScrollManager.removeScrollLock(this);
|
| - }
|
| - Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
|
| - }
|
| - },
|
| + for (var i = 0; i < size; i++) {
|
| + var inst = this.stamp(null);
|
| + // First element child is item; Safari doesn't support children[0]
|
| + // on a doc fragment
|
| + physicalItems[i] = inst.root.querySelector('*');
|
| + Polymer.dom(this).appendChild(inst.root);
|
| + }
|
| + return physicalItems;
|
| + },
|
|
|
| - /**
|
| - * Overridden from `IronOverlayBehavior`.
|
| - */
|
| - _renderOpened: function() {
|
| - if (!this.noAnimations && this.animationConfig.open) {
|
| - this.$.contentWrapper.classList.add('animating');
|
| - this.playAnimation('open');
|
| - } else {
|
| - Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
|
| - }
|
| - },
|
| + /**
|
| + * Increases the pool of physical items only if needed.
|
| + *
|
| + * @return {boolean} True if the pool was increased.
|
| + */
|
| + _increasePoolIfNeeded: function() {
|
| + // Base case 1: the list has no height.
|
| + if (this._viewportHeight === 0) {
|
| + return false;
|
| + }
|
| + // Base case 2: If the physical size is optimal and the list's client height is full
|
| + // with physical items, don't increase the pool.
|
| + var isClientHeightFull = this._physicalBottom >= this._scrollBottom && this._physicalTop <= this._scrollPosition;
|
| + if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
|
| + return false;
|
| + }
|
| + // this value should range between [0 <= `currentPage` <= `_maxPages`]
|
| + var currentPage = Math.floor(this._physicalSize / this._viewportHeight);
|
|
|
| - /**
|
| - * Overridden from `IronOverlayBehavior`.
|
| - */
|
| - _renderClosed: function() {
|
| + if (currentPage === 0) {
|
| + // fill the first page
|
| + this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
|
| + } else if (this._lastPage !== currentPage && isClientHeightFull) {
|
| + // paint the page and defer the next increase
|
| + // wait 16ms which is rough enough to get paint cycle.
|
| + Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, this._itemsPerRow), 16));
|
| + } else {
|
| + // fill the rest of the pages
|
| + this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow));
|
| + }
|
|
|
| - if (!this.noAnimations && this.animationConfig.close) {
|
| - this.$.contentWrapper.classList.add('animating');
|
| - this.playAnimation('close');
|
| - } else {
|
| - Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
|
| - }
|
| - },
|
| + this._lastPage = currentPage;
|
|
|
| - /**
|
| - * Called when animation finishes on the dropdown (when opening or
|
| - * closing). Responsible for "completing" the process of opening or
|
| - * closing the dropdown by positioning it or setting its display to
|
| - * none.
|
| - */
|
| - _onNeonAnimationFinish: function() {
|
| - this.$.contentWrapper.classList.remove('animating');
|
| - if (this.opened) {
|
| - this._finishRenderOpened();
|
| - } else {
|
| - this._finishRenderClosed();
|
| - }
|
| - },
|
| + return true;
|
| + },
|
|
|
| - _onCaptureScroll: function() {
|
| - if (!this.allowOutsideScroll) {
|
| - this._restoreScrollPosition();
|
| - } else {
|
| - this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrollRAF);
|
| - this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(this));
|
| - }
|
| - },
|
| + /**
|
| + * Increases the pool size.
|
| + */
|
| + _increasePool: function(missingItems) {
|
| + var nextPhysicalCount = Math.min(
|
| + this._physicalCount + missingItems,
|
| + this._virtualCount - this._virtualStart,
|
| + Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT)
|
| + );
|
| + var prevPhysicalCount = this._physicalCount;
|
| + var delta = nextPhysicalCount - prevPhysicalCount;
|
|
|
| - /**
|
| - * Memoizes the scroll position of the outside scrolling element.
|
| - * @private
|
| - */
|
| - _saveScrollPosition: function() {
|
| - if (document.scrollingElement) {
|
| - this._scrollTop = document.scrollingElement.scrollTop;
|
| - this._scrollLeft = document.scrollingElement.scrollLeft;
|
| - } else {
|
| - // Since we don't know if is the body or html, get max.
|
| - this._scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
|
| - this._scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
|
| - }
|
| - },
|
| + if (delta <= 0) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Resets the scroll position of the outside scrolling element.
|
| - * @private
|
| - */
|
| - _restoreScrollPosition: function() {
|
| - if (document.scrollingElement) {
|
| - document.scrollingElement.scrollTop = this._scrollTop;
|
| - document.scrollingElement.scrollLeft = this._scrollLeft;
|
| - } else {
|
| - // Since we don't know if is the body or html, set both.
|
| - document.documentElement.scrollTop = this._scrollTop;
|
| - document.documentElement.scrollLeft = this._scrollLeft;
|
| - document.body.scrollTop = this._scrollTop;
|
| - document.body.scrollLeft = this._scrollLeft;
|
| - }
|
| - },
|
| + [].push.apply(this._physicalItems, this._createPool(delta));
|
| + [].push.apply(this._physicalSizes, new Array(delta));
|
|
|
| - /**
|
| - * Constructs the final animation config from different properties used
|
| - * to configure specific parts of the opening and closing animations.
|
| - */
|
| - _updateAnimationConfig: function() {
|
| - var animations = (this.openAnimationConfig || []).concat(this.closeAnimationConfig || []);
|
| - for (var i = 0; i < animations.length; i++) {
|
| - animations[i].node = this.containedElement;
|
| - }
|
| - this.animationConfig = {
|
| - open: this.openAnimationConfig,
|
| - close: this.closeAnimationConfig
|
| - };
|
| - },
|
| + this._physicalCount = prevPhysicalCount + delta;
|
|
|
| - /**
|
| - * Updates the overlay position based on configured horizontal
|
| - * and vertical alignment.
|
| - */
|
| - _updateOverlayPosition: function() {
|
| - if (this.isAttached) {
|
| - // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
|
| - this.notifyResize();
|
| - }
|
| - },
|
| + // update the physical start if we need to preserve the model of the focused item.
|
| + // In this situation, the focused item is currently rendered and its model would
|
| + // have changed after increasing the pool if the physical start remained unchanged.
|
| + if (this._physicalStart > this._physicalEnd &&
|
| + this._isIndexRendered(this._focusedIndex) &&
|
| + this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
|
| + this._physicalStart = this._physicalStart + delta;
|
| + }
|
| + this._update();
|
| + },
|
|
|
| - /**
|
| - * Apply focus to focusTarget or containedElement
|
| - */
|
| - _applyFocus: function () {
|
| - var focusTarget = this.focusTarget || this.containedElement;
|
| - if (focusTarget && this.opened && !this.noAutoFocus) {
|
| - focusTarget.focus();
|
| - } else {
|
| - Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
|
| - }
|
| - }
|
| - });
|
| - })();
|
| -Polymer({
|
| + /**
|
| + * Render a new list of items. This method does exactly the same as `update`,
|
| + * but it also ensures that only one `update` cycle is created.
|
| + */
|
| + _render: function() {
|
| + var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
|
|
|
| - is: 'fade-in-animation',
|
| + if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
|
| + this._lastPage = 0;
|
| + this._update();
|
| + this._itemsRendered = true;
|
| + }
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + /**
|
| + * Templetizes the user template.
|
| + */
|
| + _ensureTemplatized: function() {
|
| + if (!this.ctor) {
|
| + // Template instance props that should be excluded from forwarding
|
| + var props = {};
|
| + props.__key__ = true;
|
| + props[this.as] = true;
|
| + props[this.indexAs] = true;
|
| + props[this.selectedAs] = true;
|
| + props.tabIndex = true;
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - this._effect = new KeyframeEffect(node, [
|
| - {'opacity': '0'},
|
| - {'opacity': '1'}
|
| - ], this.timingFromConfig(config));
|
| - return this._effect;
|
| - }
|
| + this._instanceProps = props;
|
| + this._userTemplate = Polymer.dom(this).querySelector('template');
|
|
|
| - });
|
| -Polymer({
|
| + if (this._userTemplate) {
|
| + this.templatize(this._userTemplate);
|
| + } else {
|
| + console.warn('iron-list requires a template to be provided in light-dom');
|
| + }
|
| + }
|
| + },
|
|
|
| - is: 'fade-out-animation',
|
| + /**
|
| + * Implements extension point from Templatizer mixin.
|
| + */
|
| + _getStampedChildren: function() {
|
| + return this._physicalItems;
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + /**
|
| + * Implements extension point from Templatizer
|
| + * Called as a side effect of a template instance path change, responsible
|
| + * for notifying items.<key-for-instance>.<path> change up to host.
|
| + */
|
| + _forwardInstancePath: function(inst, path, value) {
|
| + if (path.indexOf(this.as + '.') === 0) {
|
| + this.notifyPath('items.' + inst.__key__ + '.' +
|
| + path.slice(this.as.length + 1), value);
|
| + }
|
| + },
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - this._effect = new KeyframeEffect(node, [
|
| - {'opacity': '1'},
|
| - {'opacity': '0'}
|
| - ], this.timingFromConfig(config));
|
| - return this._effect;
|
| - }
|
| + /**
|
| + * Implements extension point from Templatizer mixin
|
| + * Called as side-effect of a host property change, responsible for
|
| + * notifying parent path change on each row.
|
| + */
|
| + _forwardParentProp: function(prop, value) {
|
| + if (this._physicalItems) {
|
| + this._physicalItems.forEach(function(item) {
|
| + item._templateInstance[prop] = value;
|
| + }, this);
|
| + }
|
| + },
|
|
|
| - });
|
| -Polymer({
|
| - is: 'paper-menu-grow-height-animation',
|
| + /**
|
| + * Implements extension point from Templatizer
|
| + * Called as side-effect of a host path change, responsible for
|
| + * notifying parent.<path> path change on each row.
|
| + */
|
| + _forwardParentPath: function(path, value) {
|
| + if (this._physicalItems) {
|
| + this._physicalItems.forEach(function(item) {
|
| + item._templateInstance.notifyPath(path, value, true);
|
| + }, this);
|
| + }
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + /**
|
| + * Called as a side effect of a host items.<key>.<path> path change,
|
| + * responsible for notifying item.<path> changes.
|
| + */
|
| + _forwardItemPath: function(path, value) {
|
| + if (!this._physicalIndexForKey) {
|
| + return;
|
| + }
|
| + var dot = path.indexOf('.');
|
| + var key = path.substring(0, dot < 0 ? path.length : dot);
|
| + var idx = this._physicalIndexForKey[key];
|
| + var offscreenItem = this._offscreenFocusedItem;
|
| + var el = offscreenItem && offscreenItem._templateInstance.__key__ === key ?
|
| + offscreenItem : this._physicalItems[idx];
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - var rect = node.getBoundingClientRect();
|
| - var height = rect.height;
|
| + if (!el || el._templateInstance.__key__ !== key) {
|
| + return;
|
| + }
|
| + if (dot >= 0) {
|
| + path = this.as + '.' + path.substring(dot+1);
|
| + el._templateInstance.notifyPath(path, value, true);
|
| + } else {
|
| + // Update selection if needed
|
| + var currentItem = el._templateInstance[this.as];
|
| + if (Array.isArray(this.selectedItems)) {
|
| + for (var i = 0; i < this.selectedItems.length; i++) {
|
| + if (this.selectedItems[i] === currentItem) {
|
| + this.set('selectedItems.' + i, value);
|
| + break;
|
| + }
|
| + }
|
| + } else if (this.selectedItem === currentItem) {
|
| + this.set('selectedItem', value);
|
| + }
|
| + el._templateInstance[this.as] = value;
|
| + }
|
| + },
|
|
|
| - this._effect = new KeyframeEffect(node, [{
|
| - height: (height / 2) + 'px'
|
| - }, {
|
| - height: height + 'px'
|
| - }], this.timingFromConfig(config));
|
| + /**
|
| + * Called when the items have changed. That is, ressignments
|
| + * to `items`, splices or updates to a single item.
|
| + */
|
| + _itemsChanged: function(change) {
|
| + if (change.path === 'items') {
|
| + // reset items
|
| + this._virtualStart = 0;
|
| + this._physicalTop = 0;
|
| + this._virtualCount = this.items ? this.items.length : 0;
|
| + this._collection = this.items ? Polymer.Collection.get(this.items) : null;
|
| + this._physicalIndexForKey = {};
|
| + this._firstVisibleIndexVal = null;
|
| + this._lastVisibleIndexVal = null;
|
|
|
| - return this._effect;
|
| - }
|
| - });
|
| + this._resetScrollPosition(0);
|
| + this._removeFocusedItem();
|
| + // create the initial physical items
|
| + if (!this._physicalItems) {
|
| + this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, this._virtualCount));
|
| + this._physicalItems = this._createPool(this._physicalCount);
|
| + this._physicalSizes = new Array(this._physicalCount);
|
| + }
|
|
|
| - Polymer({
|
| - is: 'paper-menu-grow-width-animation',
|
| + this._physicalStart = 0;
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + } else if (change.path === 'items.splices') {
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - var rect = node.getBoundingClientRect();
|
| - var width = rect.width;
|
| + this._adjustVirtualIndex(change.value.indexSplices);
|
| + this._virtualCount = this.items ? this.items.length : 0;
|
|
|
| - this._effect = new KeyframeEffect(node, [{
|
| - width: (width / 2) + 'px'
|
| - }, {
|
| - width: width + 'px'
|
| - }], this.timingFromConfig(config));
|
| + } else {
|
| + // update a single item
|
| + this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
|
| + return;
|
| + }
|
|
|
| - return this._effect;
|
| - }
|
| - });
|
| + this._itemsRendered = false;
|
| + this._debounceTemplate(this._render);
|
| + },
|
|
|
| - Polymer({
|
| - is: 'paper-menu-shrink-width-animation',
|
| + /**
|
| + * @param {!Array<!PolymerSplice>} splices
|
| + */
|
| + _adjustVirtualIndex: function(splices) {
|
| + splices.forEach(function(splice) {
|
| + // deselect removed items
|
| + splice.removed.forEach(this._removeItem, this);
|
| + // We only need to care about changes happening above the current position
|
| + if (splice.index < this._virtualStart) {
|
| + var delta = Math.max(
|
| + splice.addedCount - splice.removed.length,
|
| + splice.index - this._virtualStart);
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + this._virtualStart = this._virtualStart + delta;
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - var rect = node.getBoundingClientRect();
|
| - var width = rect.width;
|
| + if (this._focusedIndex >= 0) {
|
| + this._focusedIndex = this._focusedIndex + delta;
|
| + }
|
| + }
|
| + }, this);
|
| + },
|
|
|
| - this._effect = new KeyframeEffect(node, [{
|
| - width: width + 'px'
|
| - }, {
|
| - width: width - (width / 20) + 'px'
|
| - }], this.timingFromConfig(config));
|
| + _removeItem: function(item) {
|
| + this.$.selector.deselect(item);
|
| + // remove the current focused item
|
| + if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
|
| + this._removeFocusedItem();
|
| + }
|
| + },
|
|
|
| - return this._effect;
|
| - }
|
| - });
|
| + /**
|
| + * Executes a provided function per every physical index in `itemSet`
|
| + * `itemSet` default value is equivalent to the entire set of physical indexes.
|
| + *
|
| + * @param {!function(number, number)} fn
|
| + * @param {!Array<number>=} itemSet
|
| + */
|
| + _iterateItems: function(fn, itemSet) {
|
| + var pidx, vidx, rtn, i;
|
|
|
| - Polymer({
|
| - is: 'paper-menu-shrink-height-animation',
|
| + if (arguments.length === 2 && itemSet) {
|
| + for (i = 0; i < itemSet.length; i++) {
|
| + pidx = itemSet[i];
|
| + vidx = this._computeVidx(pidx);
|
| + if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| + return rtn;
|
| + }
|
| + }
|
| + } else {
|
| + pidx = this._physicalStart;
|
| + vidx = this._virtualStart;
|
|
|
| - behaviors: [
|
| - Polymer.NeonAnimationBehavior
|
| - ],
|
| + for (; pidx < this._physicalCount; pidx++, vidx++) {
|
| + if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| + return rtn;
|
| + }
|
| + }
|
| + for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
|
| + if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
| + return rtn;
|
| + }
|
| + }
|
| + }
|
| + },
|
|
|
| - configure: function(config) {
|
| - var node = config.node;
|
| - var rect = node.getBoundingClientRect();
|
| - var height = rect.height;
|
| - var top = rect.top;
|
| + /**
|
| + * Returns the virtual index for a given physical index
|
| + *
|
| + * @param {number} pidx Physical index
|
| + * @return {number}
|
| + */
|
| + _computeVidx: function(pidx) {
|
| + if (pidx >= this._physicalStart) {
|
| + return this._virtualStart + (pidx - this._physicalStart);
|
| + }
|
| + return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
|
| + },
|
|
|
| - this.setPrefixedProperty(node, 'transformOrigin', '0 0');
|
| + /**
|
| + * Assigns the data models to a given set of items.
|
| + * @param {!Array<number>=} itemSet
|
| + */
|
| + _assignModels: function(itemSet) {
|
| + this._iterateItems(function(pidx, vidx) {
|
| + var el = this._physicalItems[pidx];
|
| + var inst = el._templateInstance;
|
| + var item = this.items && this.items[vidx];
|
|
|
| - this._effect = new KeyframeEffect(node, [{
|
| - height: height + 'px',
|
| - transform: 'translateY(0)'
|
| - }, {
|
| - height: height / 2 + 'px',
|
| - transform: 'translateY(-20px)'
|
| - }], this.timingFromConfig(config));
|
| + if (item != null) {
|
| + inst[this.as] = item;
|
| + inst.__key__ = this._collection.getKey(item);
|
| + inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
|
| + inst[this.indexAs] = vidx;
|
| + inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
|
| + this._physicalIndexForKey[inst.__key__] = pidx;
|
| + el.removeAttribute('hidden');
|
| + } else {
|
| + inst.__key__ = null;
|
| + el.setAttribute('hidden', '');
|
| + }
|
| + }, itemSet);
|
| + },
|
|
|
| - return this._effect;
|
| - }
|
| - });
|
| -(function() {
|
| - 'use strict';
|
| + /**
|
| + * Updates the height for a given set of items.
|
| + *
|
| + * @param {!Array<number>=} itemSet
|
| + */
|
| + _updateMetrics: function(itemSet) {
|
| + // Make sure we distributed all the physical items
|
| + // so we can measure them
|
| + Polymer.dom.flush();
|
|
|
| - var config = {
|
| - ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
|
| - MAX_ANIMATION_TIME_MS: 400
|
| - };
|
| + var newPhysicalSize = 0;
|
| + var oldPhysicalSize = 0;
|
| + var prevAvgCount = this._physicalAverageCount;
|
| + var prevPhysicalAvg = this._physicalAverage;
|
|
|
| - var PaperMenuButton = Polymer({
|
| - is: 'paper-menu-button',
|
| + this._iterateItems(function(pidx, vidx) {
|
|
|
| - /**
|
| - * Fired when the dropdown opens.
|
| - *
|
| - * @event paper-dropdown-open
|
| - */
|
| + oldPhysicalSize += this._physicalSizes[pidx] || 0;
|
| + this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
|
| + newPhysicalSize += this._physicalSizes[pidx];
|
| + this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
|
|
|
| - /**
|
| - * Fired when the dropdown closes.
|
| - *
|
| - * @event paper-dropdown-close
|
| - */
|
| + }, itemSet);
|
|
|
| - behaviors: [
|
| - Polymer.IronA11yKeysBehavior,
|
| - Polymer.IronControlState
|
| - ],
|
| + this._viewportHeight = this._scrollTargetHeight;
|
| + if (this.grid) {
|
| + this._updateGridMetrics();
|
| + this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
|
| + } else {
|
| + this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
|
| + }
|
|
|
| - properties: {
|
| - /**
|
| - * True if the content is currently displayed.
|
| - */
|
| - opened: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - observer: '_openedChanged'
|
| - },
|
| + // update the average if we measured something
|
| + if (this._physicalAverageCount !== prevAvgCount) {
|
| + this._physicalAverage = Math.round(
|
| + ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
|
| + this._physicalAverageCount);
|
| + }
|
| + },
|
|
|
| - /**
|
| - * The orientation against which to align the menu dropdown
|
| - * horizontally relative to the dropdown trigger.
|
| - */
|
| - horizontalAlign: {
|
| - type: String,
|
| - value: 'left',
|
| - reflectToAttribute: true
|
| - },
|
| + _updateGridMetrics: function() {
|
| + this._viewportWidth = this.$.items.offsetWidth;
|
| + // Set item width to the value of the _physicalItems offsetWidth
|
| + this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoundingClientRect().width : DEFAULT_GRID_SIZE;
|
| + // Set row height to the value of the _physicalItems offsetHeight
|
| + this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetHeight : DEFAULT_GRID_SIZE;
|
| + // If in grid mode compute how many items with exist in each row
|
| + this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / this._itemWidth) : this._itemsPerRow;
|
| + },
|
|
|
| - /**
|
| - * The orientation against which to align the menu dropdown
|
| - * vertically relative to the dropdown trigger.
|
| - */
|
| - verticalAlign: {
|
| - type: String,
|
| - value: 'top',
|
| - reflectToAttribute: true
|
| - },
|
| + /**
|
| + * Updates the position of the physical items.
|
| + */
|
| + _positionItems: function() {
|
| + this._adjustScrollPosition();
|
|
|
| - /**
|
| - * If true, the `horizontalAlign` and `verticalAlign` properties will
|
| - * be considered preferences instead of strict requirements when
|
| - * positioning the dropdown and may be changed if doing so reduces
|
| - * the area of the dropdown falling outside of `fitInto`.
|
| - */
|
| - dynamicAlign: {
|
| - type: Boolean
|
| - },
|
| + var y = this._physicalTop;
|
|
|
| - /**
|
| - * A pixel value that will be added to the position calculated for the
|
| - * given `horizontalAlign`. Use a negative value to offset to the
|
| - * left, or a positive value to offset to the right.
|
| - */
|
| - horizontalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| - },
|
| + if (this.grid) {
|
| + var totalItemWidth = this._itemsPerRow * this._itemWidth;
|
| + var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
|
|
|
| - /**
|
| - * A pixel value that will be added to the position calculated for the
|
| - * given `verticalAlign`. Use a negative value to offset towards the
|
| - * top, or a positive value to offset towards the bottom.
|
| - */
|
| - verticalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| - },
|
| + this._iterateItems(function(pidx, vidx) {
|
|
|
| - /**
|
| - * If true, the dropdown will be positioned so that it doesn't overlap
|
| - * the button.
|
| - */
|
| - noOverlap: {
|
| - type: Boolean
|
| - },
|
| + var modulus = vidx % this._itemsPerRow;
|
| + var x = Math.floor((modulus * this._itemWidth) + rowOffset);
|
|
|
| - /**
|
| - * Set to true to disable animations when opening and closing the
|
| - * dropdown.
|
| - */
|
| - noAnimations: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
|
|
|
| - /**
|
| - * Set to true to disable automatically closing the dropdown after
|
| - * a selection has been made.
|
| - */
|
| - ignoreSelect: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + if (this._shouldRenderNextRow(vidx)) {
|
| + y += this._rowHeight;
|
| + }
|
|
|
| - /**
|
| - * Set to true to enable automatically closing the dropdown after an
|
| - * item has been activated, even if the selection did not change.
|
| - */
|
| - closeOnActivate: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + });
|
| + } else {
|
| + this._iterateItems(function(pidx, vidx) {
|
|
|
| - /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * opening of the dropdown.
|
| - */
|
| - openAnimationConfig: {
|
| - type: Object,
|
| - value: function() {
|
| - return [{
|
| - name: 'fade-in-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 200
|
| - }
|
| - }, {
|
| - name: 'paper-menu-grow-width-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 150,
|
| - easing: config.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }, {
|
| - name: 'paper-menu-grow-height-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 275,
|
| - easing: config.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }];
|
| - }
|
| - },
|
| + this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
|
| + y += this._physicalSizes[pidx];
|
|
|
| - /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * closing of the dropdown.
|
| - */
|
| - closeAnimationConfig: {
|
| - type: Object,
|
| - value: function() {
|
| - return [{
|
| - name: 'fade-out-animation',
|
| - timing: {
|
| - duration: 150
|
| - }
|
| - }, {
|
| - name: 'paper-menu-shrink-width-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 50,
|
| - easing: config.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }, {
|
| - name: 'paper-menu-shrink-height-animation',
|
| - timing: {
|
| - duration: 200,
|
| - easing: 'ease-in'
|
| - }
|
| - }];
|
| - }
|
| - },
|
| + });
|
| + }
|
| + },
|
|
|
| - /**
|
| - * By default, the dropdown will constrain scrolling on the page
|
| - * to itself when opened.
|
| - * Set to true in order to prevent scroll from being constrained
|
| - * to the dropdown when it opens.
|
| - */
|
| - allowOutsideScroll: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + _getPhysicalSizeIncrement: function(pidx) {
|
| + if (!this.grid) {
|
| + return this._physicalSizes[pidx];
|
| + }
|
| + if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
|
| + return 0;
|
| + }
|
| + return this._rowHeight;
|
| + },
|
| +
|
| + /**
|
| + * Returns, based on the current index,
|
| + * whether or not the next index will need
|
| + * to be rendered on a new row.
|
| + *
|
| + * @param {number} vidx Virtual index
|
| + * @return {boolean}
|
| + */
|
| + _shouldRenderNextRow: function(vidx) {
|
| + return vidx % this._itemsPerRow === this._itemsPerRow - 1;
|
| + },
|
|
|
| - /**
|
| - * Whether focus should be restored to the button when the menu closes.
|
| - */
|
| - restoreFocusOnClose: {
|
| - type: Boolean,
|
| - value: true
|
| - },
|
| + /**
|
| + * Adjusts the scroll position when it was overestimated.
|
| + */
|
| + _adjustScrollPosition: function() {
|
| + var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
|
| + Math.min(this._scrollPosition + this._physicalTop, 0);
|
|
|
| - /**
|
| - * This is the element intended to be bound as the focus target
|
| - * for the `iron-dropdown` contained by `paper-menu-button`.
|
| - */
|
| - _dropdownContent: {
|
| - type: Object
|
| - }
|
| - },
|
| + if (deltaHeight) {
|
| + this._physicalTop = this._physicalTop - deltaHeight;
|
| + // juking scroll position during interial scrolling on iOS is no bueno
|
| + if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) {
|
| + this._resetScrollPosition(this._scrollTop - deltaHeight);
|
| + }
|
| + }
|
| + },
|
|
|
| - hostAttributes: {
|
| - role: 'group',
|
| - 'aria-haspopup': 'true'
|
| - },
|
| + /**
|
| + * Sets the position of the scroll.
|
| + */
|
| + _resetScrollPosition: function(pos) {
|
| + if (this.scrollTarget) {
|
| + this._scrollTop = pos;
|
| + this._scrollPosition = this._scrollTop;
|
| + }
|
| + },
|
|
|
| - listeners: {
|
| - 'iron-activate': '_onIronActivate',
|
| - 'iron-select': '_onIronSelect'
|
| - },
|
| + /**
|
| + * Sets the scroll height, that's the height of the content,
|
| + *
|
| + * @param {boolean=} forceUpdate If true, updates the height no matter what.
|
| + */
|
| + _updateScrollerSize: function(forceUpdate) {
|
| + if (this.grid) {
|
| + this._estScrollHeight = this._virtualRowCount * this._rowHeight;
|
| + } else {
|
| + this._estScrollHeight = (this._physicalBottom +
|
| + Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage);
|
| + }
|
|
|
| - /**
|
| - * The content element that is contained by the menu button, if any.
|
| - */
|
| - get contentElement() {
|
| - return Polymer.dom(this.$.content).getDistributedNodes()[0];
|
| - },
|
| + forceUpdate = forceUpdate || this._scrollHeight === 0;
|
| + forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
|
| + forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this._estScrollHeight;
|
|
|
| - /**
|
| - * Toggles the drowpdown content between opened and closed.
|
| - */
|
| - toggle: function() {
|
| - if (this.opened) {
|
| - this.close();
|
| - } else {
|
| - this.open();
|
| - }
|
| - },
|
| + // amortize height adjustment, so it won't trigger repaints very often
|
| + if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
|
| + this.$.items.style.height = this._estScrollHeight + 'px';
|
| + this._scrollHeight = this._estScrollHeight;
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Make the dropdown content appear as an overlay positioned relative
|
| - * to the dropdown trigger.
|
| - */
|
| - open: function() {
|
| - if (this.disabled) {
|
| - return;
|
| - }
|
| + /**
|
| + * Scroll to a specific item in the virtual list regardless
|
| + * of the physical items in the DOM tree.
|
| + *
|
| + * @method scrollToItem
|
| + * @param {(Object)} item The item to be scrolled to
|
| + */
|
| + scrollToItem: function(item){
|
| + return this.scrollToIndex(this.items.indexOf(item));
|
| + },
|
|
|
| - this.$.dropdown.open();
|
| - },
|
| + /**
|
| + * Scroll to a specific index in the virtual list regardless
|
| + * of the physical items in the DOM tree.
|
| + *
|
| + * @method scrollToIndex
|
| + * @param {number} idx The index of the item
|
| + */
|
| + scrollToIndex: function(idx) {
|
| + if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
|
| + return;
|
| + }
|
|
|
| - /**
|
| - * Hide the dropdown content.
|
| - */
|
| - close: function() {
|
| - this.$.dropdown.close();
|
| - },
|
| + Polymer.dom.flush();
|
|
|
| - /**
|
| - * When an `iron-select` event is received, the dropdown should
|
| - * automatically close on the assumption that a value has been chosen.
|
| - *
|
| - * @param {CustomEvent} event A CustomEvent instance with type
|
| - * set to `"iron-select"`.
|
| - */
|
| - _onIronSelect: function(event) {
|
| - if (!this.ignoreSelect) {
|
| - this.close();
|
| - }
|
| - },
|
| + idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
|
| + // update the virtual start only when needed
|
| + if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
|
| + this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
|
| + }
|
| + // manage focus
|
| + this._manageFocus();
|
| + // assign new models
|
| + this._assignModels();
|
| + // measure the new sizes
|
| + this._updateMetrics();
|
|
|
| - /**
|
| - * Closes the dropdown when an `iron-activate` event is received if
|
| - * `closeOnActivate` is true.
|
| - *
|
| - * @param {CustomEvent} event A CustomEvent of type 'iron-activate'.
|
| - */
|
| - _onIronActivate: function(event) {
|
| - if (this.closeOnActivate) {
|
| - this.close();
|
| - }
|
| - },
|
| + // estimate new physical offset
|
| + var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
|
| + this._physicalTop = estPhysicalTop;
|
|
|
| - /**
|
| - * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
|
| - * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
|
| - *
|
| - * @param {boolean} opened True if the dropdown is opened, otherwise false.
|
| - * @param {boolean} oldOpened The previous value of `opened`.
|
| - */
|
| - _openedChanged: function(opened, oldOpened) {
|
| - if (opened) {
|
| - // TODO(cdata): Update this when we can measure changes in distributed
|
| - // children in an idiomatic way.
|
| - // We poke this property in case the element has changed. This will
|
| - // cause the focus target for the `iron-dropdown` to be updated as
|
| - // necessary:
|
| - this._dropdownContent = this.contentElement;
|
| - this.fire('paper-dropdown-open');
|
| - } else if (oldOpened != null) {
|
| - this.fire('paper-dropdown-close');
|
| - }
|
| - },
|
| + var currentTopItem = this._physicalStart;
|
| + var currentVirtualItem = this._virtualStart;
|
| + var targetOffsetTop = 0;
|
| + var hiddenContentSize = this._hiddenContentSize;
|
|
|
| - /**
|
| - * If the dropdown is open when disabled becomes true, close the
|
| - * dropdown.
|
| - *
|
| - * @param {boolean} disabled True if disabled, otherwise false.
|
| - */
|
| - _disabledChanged: function(disabled) {
|
| - Polymer.IronControlState._disabledChanged.apply(this, arguments);
|
| - if (disabled && this.opened) {
|
| - this.close();
|
| - }
|
| - },
|
| + // scroll to the item as much as we can
|
| + while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
|
| + targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(currentTopItem);
|
| + currentTopItem = (currentTopItem + 1) % this._physicalCount;
|
| + currentVirtualItem++;
|
| + }
|
| + // update the scroller size
|
| + this._updateScrollerSize(true);
|
| + // update the position of the items
|
| + this._positionItems();
|
| + // set the new scroll position
|
| + this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop);
|
| + // increase the pool of physical items if needed
|
| + this._increasePoolIfNeeded();
|
| + // clear cached visible index
|
| + this._firstVisibleIndexVal = null;
|
| + this._lastVisibleIndexVal = null;
|
| + },
|
| +
|
| + /**
|
| + * Reset the physical average and the average count.
|
| + */
|
| + _resetAverage: function() {
|
| + this._physicalAverage = 0;
|
| + this._physicalAverageCount = 0;
|
| + },
|
|
|
| - __onIronOverlayCanceled: function(event) {
|
| - var uiEvent = event.detail;
|
| - var target = Polymer.dom(uiEvent).rootTarget;
|
| - var trigger = this.$.trigger;
|
| - var path = Polymer.dom(uiEvent).path;
|
| + /**
|
| + * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
|
| + * when the element is resized.
|
| + */
|
| + _resizeHandler: function() {
|
| + // iOS fires the resize event when the address bar slides up
|
| + if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100) {
|
| + return;
|
| + }
|
| + // In Desktop Safari 9.0.3, if the scroll bars are always shown,
|
| + // changing the scroll position from a resize handler would result in
|
| + // the scroll position being reset. Waiting 1ms fixes the issue.
|
| + Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() {
|
| + this.updateViewportBoundaries();
|
| + this._render();
|
|
|
| - if (path.indexOf(trigger) > -1) {
|
| - event.preventDefault();
|
| - }
|
| + if (this._itemsRendered && this._physicalItems && this._isVisible) {
|
| + this._resetAverage();
|
| + this.scrollToIndex(this.firstVisibleIndex);
|
| }
|
| - });
|
| -
|
| - Object.keys(config).forEach(function (key) {
|
| - PaperMenuButton[key] = config[key];
|
| - });
|
| + }.bind(this), 1));
|
| + },
|
|
|
| - Polymer.PaperMenuButton = PaperMenuButton;
|
| - })();
|
| -/**
|
| - * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has keyboard focus.
|
| - *
|
| - * @polymerBehavior Polymer.PaperInkyFocusBehavior
|
| - */
|
| - Polymer.PaperInkyFocusBehaviorImpl = {
|
| - observers: [
|
| - '_focusedChanged(receivedFocusFromKeyboard)'
|
| - ],
|
| + _getModelFromItem: function(item) {
|
| + var key = this._collection.getKey(item);
|
| + var pidx = this._physicalIndexForKey[key];
|
|
|
| - _focusedChanged: function(receivedFocusFromKeyboard) {
|
| - if (receivedFocusFromKeyboard) {
|
| - this.ensureRipple();
|
| - }
|
| - if (this.hasRipple()) {
|
| - this._ripple.holdDown = receivedFocusFromKeyboard;
|
| + if (pidx != null) {
|
| + return this._physicalItems[pidx]._templateInstance;
|
| }
|
| + return null;
|
| },
|
|
|
| - _createRipple: function() {
|
| - var ripple = Polymer.PaperRippleBehavior._createRipple();
|
| - ripple.id = 'ink';
|
| - ripple.setAttribute('center', '');
|
| - ripple.classList.add('circle');
|
| - return ripple;
|
| - }
|
| - };
|
| -
|
| - /** @polymerBehavior Polymer.PaperInkyFocusBehavior */
|
| - Polymer.PaperInkyFocusBehavior = [
|
| - Polymer.IronButtonState,
|
| - Polymer.IronControlState,
|
| - Polymer.PaperRippleBehavior,
|
| - Polymer.PaperInkyFocusBehaviorImpl
|
| - ];
|
| -Polymer({
|
| - is: 'paper-icon-button',
|
| + /**
|
| + * Gets a valid item instance from its index or the object value.
|
| + *
|
| + * @param {(Object|number)} item The item object or its index
|
| + */
|
| + _getNormalizedItem: function(item) {
|
| + if (this._collection.getKey(item) === undefined) {
|
| + if (typeof item === 'number') {
|
| + item = this.items[item];
|
| + if (!item) {
|
| + throw new RangeError('<item> not found');
|
| + }
|
| + return item;
|
| + }
|
| + throw new TypeError('<item> should be a valid item');
|
| + }
|
| + return item;
|
| + },
|
|
|
| - hostAttributes: {
|
| - role: 'button',
|
| - tabindex: '0'
|
| - },
|
| + /**
|
| + * Select the list item at the given index.
|
| + *
|
| + * @method selectItem
|
| + * @param {(Object|number)} item The item object or its index
|
| + */
|
| + selectItem: function(item) {
|
| + item = this._getNormalizedItem(item);
|
| + var model = this._getModelFromItem(item);
|
|
|
| - behaviors: [
|
| - Polymer.PaperInkyFocusBehavior
|
| - ],
|
| + if (!this.multiSelection && this.selectedItem) {
|
| + this.deselectItem(this.selectedItem);
|
| + }
|
| + if (model) {
|
| + model[this.selectedAs] = true;
|
| + }
|
| + this.$.selector.select(item);
|
| + this.updateSizeForItem(item);
|
| + },
|
|
|
| - properties: {
|
| - /**
|
| - * The URL of an image for the icon. If the src property is specified,
|
| - * the icon property should not be.
|
| - */
|
| - src: {
|
| - type: String
|
| - },
|
| + /**
|
| + * Deselects the given item list if it is already selected.
|
| + *
|
|
|
| - /**
|
| - * Specifies the icon name or index in the set of icons available in
|
| - * the icon's icon set. If the icon property is specified,
|
| - * the src property should not be.
|
| - */
|
| - icon: {
|
| - type: String
|
| - },
|
| + * @method deselect
|
| + * @param {(Object|number)} item The item object or its index
|
| + */
|
| + deselectItem: function(item) {
|
| + item = this._getNormalizedItem(item);
|
| + var model = this._getModelFromItem(item);
|
|
|
| - /**
|
| - * Specifies the alternate text for the button, for accessibility.
|
| - */
|
| - alt: {
|
| - type: String,
|
| - observer: "_altChanged"
|
| - }
|
| - },
|
| + if (model) {
|
| + model[this.selectedAs] = false;
|
| + }
|
| + this.$.selector.deselect(item);
|
| + this.updateSizeForItem(item);
|
| + },
|
|
|
| - _altChanged: function(newValue, oldValue) {
|
| - var label = this.getAttribute('aria-label');
|
| + /**
|
| + * Select or deselect a given item depending on whether the item
|
| + * has already been selected.
|
| + *
|
| + * @method toggleSelectionForItem
|
| + * @param {(Object|number)} item The item object or its index
|
| + */
|
| + toggleSelectionForItem: function(item) {
|
| + item = this._getNormalizedItem(item);
|
| + if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
|
| + this.deselectItem(item);
|
| + } else {
|
| + this.selectItem(item);
|
| + }
|
| + },
|
|
|
| - // Don't stomp over a user-set aria-label.
|
| - if (!label || oldValue == label) {
|
| - this.setAttribute('aria-label', newValue);
|
| + /**
|
| + * Clears the current selection state of the list.
|
| + *
|
| + * @method clearSelection
|
| + */
|
| + clearSelection: function() {
|
| + function unselect(item) {
|
| + var model = this._getModelFromItem(item);
|
| + if (model) {
|
| + model[this.selectedAs] = false;
|
| }
|
| }
|
| - });
|
| -// Copyright 2016 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
|
|
| -/**
|
| - * Implements an incremental search field which can be shown and hidden.
|
| - * Canonical implementation is <cr-search-field>.
|
| - * @polymerBehavior
|
| - */
|
| -var CrSearchFieldBehavior = {
|
| - properties: {
|
| - label: {
|
| - type: String,
|
| - value: '',
|
| + if (Array.isArray(this.selectedItems)) {
|
| + this.selectedItems.forEach(unselect, this);
|
| + } else if (this.selectedItem) {
|
| + unselect.call(this, this.selectedItem);
|
| + }
|
| +
|
| + /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
|
| },
|
|
|
| - clearLabel: {
|
| - type: String,
|
| - value: '',
|
| + /**
|
| + * Add an event listener to `tap` if `selectionEnabled` is true,
|
| + * it will remove the listener otherwise.
|
| + */
|
| + _selectionEnabledChanged: function(selectionEnabled) {
|
| + var handler = selectionEnabled ? this.listen : this.unlisten;
|
| + handler.call(this, this, 'tap', '_selectionHandler');
|
| },
|
|
|
| - showingSearch: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - observer: 'showingSearchChanged_',
|
| - reflectToAttribute: true
|
| + /**
|
| + * Select an item from an event object.
|
| + */
|
| + _selectionHandler: function(e) {
|
| + var model = this.modelForElement(e.target);
|
| + if (!model) {
|
| + return;
|
| + }
|
| + var modelTabIndex, activeElTabIndex;
|
| + var target = Polymer.dom(e).path[0];
|
| + var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).activeElement;
|
| + var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.indexAs])];
|
| + // Safari does not focus certain form controls via mouse
|
| + // https://bugs.webkit.org/show_bug.cgi?id=118043
|
| + if (target.localName === 'input' ||
|
| + target.localName === 'button' ||
|
| + target.localName === 'select') {
|
| + return;
|
| + }
|
| + // Set a temporary tabindex
|
| + modelTabIndex = model.tabIndex;
|
| + model.tabIndex = SECRET_TABINDEX;
|
| + activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
|
| + model.tabIndex = modelTabIndex;
|
| + // Only select the item if the tap wasn't on a focusable child
|
| + // or the element bound to `tabIndex`
|
| + if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SECRET_TABINDEX) {
|
| + return;
|
| + }
|
| + this.toggleSelectionForItem(model[this.as]);
|
| },
|
|
|
| - /** @private */
|
| - lastValue_: {
|
| - type: String,
|
| - value: '',
|
| + _multiSelectionChanged: function(multiSelection) {
|
| + this.clearSelection();
|
| + this.$.selector.multi = multiSelection;
|
| },
|
| - },
|
|
|
| - /**
|
| - * @abstract
|
| - * @return {!HTMLInputElement} The input field element the behavior should
|
| - * use.
|
| - */
|
| - getSearchInput: function() {},
|
| + /**
|
| + * Updates the size of an item.
|
| + *
|
| + * @method updateSizeForItem
|
| + * @param {(Object|number)} item The item object or its index
|
| + */
|
| + updateSizeForItem: function(item) {
|
| + item = this._getNormalizedItem(item);
|
| + var key = this._collection.getKey(item);
|
| + var pidx = this._physicalIndexForKey[key];
|
|
|
| - /**
|
| - * @return {string} The value of the search field.
|
| - */
|
| - getValue: function() {
|
| - return this.getSearchInput().value;
|
| - },
|
| + if (pidx != null) {
|
| + this._updateMetrics([pidx]);
|
| + this._positionItems();
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Sets the value of the search field.
|
| - * @param {string} value
|
| - */
|
| - setValue: function(value) {
|
| - // Use bindValue when setting the input value so that changes propagate
|
| - // correctly.
|
| - this.getSearchInput().bindValue = value;
|
| - this.onValueChanged_(value);
|
| - },
|
| + /**
|
| + * Creates a temporary backfill item in the rendered pool of physical items
|
| + * to replace the main focused item. The focused item has tabIndex = 0
|
| + * and might be currently focused by the user.
|
| + *
|
| + * This dynamic replacement helps to preserve the focus state.
|
| + */
|
| + _manageFocus: function() {
|
| + var fidx = this._focusedIndex;
|
|
|
| - showAndFocus: function() {
|
| - this.showingSearch = true;
|
| - this.focus_();
|
| - },
|
| + if (fidx >= 0 && fidx < this._virtualCount) {
|
| + // if it's a valid index, check if that index is rendered
|
| + // in a physical item.
|
| + if (this._isIndexRendered(fidx)) {
|
| + this._restoreFocusedItem();
|
| + } else {
|
| + this._createFocusBackfillItem();
|
| + }
|
| + } else if (this._virtualCount > 0 && this._physicalCount > 0) {
|
| + // otherwise, assign the initial focused index.
|
| + this._focusedIndex = this._virtualStart;
|
| + this._focusedItem = this._physicalItems[this._physicalStart];
|
| + }
|
| + },
|
|
|
| - /** @private */
|
| - focus_: function() {
|
| - this.getSearchInput().focus();
|
| - },
|
| + _isIndexRendered: function(idx) {
|
| + return idx >= this._virtualStart && idx <= this._virtualEnd;
|
| + },
|
|
|
| - onSearchTermSearch: function() {
|
| - this.onValueChanged_(this.getValue());
|
| - },
|
| + _isIndexVisible: function(idx) {
|
| + return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
|
| + },
|
|
|
| - /**
|
| - * Updates the internal state of the search field based on a change that has
|
| - * already happened.
|
| - * @param {string} newValue
|
| - * @private
|
| - */
|
| - onValueChanged_: function(newValue) {
|
| - if (newValue == this.lastValue_)
|
| - return;
|
| + _getPhysicalIndex: function(idx) {
|
| + return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
|
| + },
|
|
|
| - this.fire('search-changed', newValue);
|
| - this.lastValue_ = newValue;
|
| - },
|
| + _focusPhysicalItem: function(idx) {
|
| + if (idx < 0 || idx >= this._virtualCount) {
|
| + return;
|
| + }
|
| + this._restoreFocusedItem();
|
| + // scroll to index to make sure it's rendered
|
| + if (!this._isIndexRendered(idx)) {
|
| + this.scrollToIndex(idx);
|
| + }
|
|
|
| - onSearchTermKeydown: function(e) {
|
| - if (e.key == 'Escape')
|
| - this.showingSearch = false;
|
| - },
|
| + var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
|
| + var model = physicalItem._templateInstance;
|
| + var focusable;
|
|
|
| - /** @private */
|
| - showingSearchChanged_: function() {
|
| - if (this.showingSearch) {
|
| - this.focus_();
|
| - return;
|
| - }
|
| + // set a secret tab index
|
| + model.tabIndex = SECRET_TABINDEX;
|
| + // check if focusable element is the physical item
|
| + if (physicalItem.tabIndex === SECRET_TABINDEX) {
|
| + focusable = physicalItem;
|
| + }
|
| + // search for the element which tabindex is bound to the secret tab index
|
| + if (!focusable) {
|
| + focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET_TABINDEX + '"]');
|
| + }
|
| + // restore the tab index
|
| + model.tabIndex = 0;
|
| + // focus the focusable element
|
| + this._focusedIndex = idx;
|
| + focusable && focusable.focus();
|
| + },
|
|
|
| - this.setValue('');
|
| - this.getSearchInput().blur();
|
| - }
|
| -};
|
| -(function() {
|
| - 'use strict';
|
| + _removeFocusedItem: function() {
|
| + if (this._offscreenFocusedItem) {
|
| + Polymer.dom(this).removeChild(this._offscreenFocusedItem);
|
| + }
|
| + this._offscreenFocusedItem = null;
|
| + this._focusBackfillItem = null;
|
| + this._focusedItem = null;
|
| + this._focusedIndex = -1;
|
| + },
|
|
|
| - Polymer.IronA11yAnnouncer = Polymer({
|
| - is: 'iron-a11y-announcer',
|
| + _createFocusBackfillItem: function() {
|
| + var pidx, fidx = this._focusedIndex;
|
| + if (this._offscreenFocusedItem || fidx < 0) {
|
| + return;
|
| + }
|
| + if (!this._focusBackfillItem) {
|
| + // create a physical item, so that it backfills the focused item.
|
| + var stampedTemplate = this.stamp(null);
|
| + this._focusBackfillItem = stampedTemplate.root.querySelector('*');
|
| + Polymer.dom(this).appendChild(stampedTemplate.root);
|
| + }
|
| + // get the physical index for the focused index
|
| + pidx = this._getPhysicalIndex(fidx);
|
|
|
| - properties: {
|
| + if (pidx != null) {
|
| + // set the offcreen focused physical item
|
| + this._offscreenFocusedItem = this._physicalItems[pidx];
|
| + // backfill the focused physical item
|
| + this._physicalItems[pidx] = this._focusBackfillItem;
|
| + // hide the focused physical
|
| + this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
|
| + }
|
| + },
|
|
|
| - /**
|
| - * The value of mode is used to set the `aria-live` attribute
|
| - * for the element that will be announced. Valid values are: `off`,
|
| - * `polite` and `assertive`.
|
| - */
|
| - mode: {
|
| - type: String,
|
| - value: 'polite'
|
| - },
|
| + _restoreFocusedItem: function() {
|
| + var pidx, fidx = this._focusedIndex;
|
|
|
| - _text: {
|
| - type: String,
|
| - value: ''
|
| - }
|
| - },
|
| + if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
|
| + return;
|
| + }
|
| + // assign models to the focused index
|
| + this._assignModels();
|
| + // get the new physical index for the focused index
|
| + pidx = this._getPhysicalIndex(fidx);
|
|
|
| - created: function() {
|
| - if (!Polymer.IronA11yAnnouncer.instance) {
|
| - Polymer.IronA11yAnnouncer.instance = this;
|
| - }
|
| + if (pidx != null) {
|
| + // flip the focus backfill
|
| + this._focusBackfillItem = this._physicalItems[pidx];
|
| + // restore the focused physical item
|
| + this._physicalItems[pidx] = this._offscreenFocusedItem;
|
| + // reset the offscreen focused item
|
| + this._offscreenFocusedItem = null;
|
| + // hide the physical item that backfills
|
| + this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
|
| + }
|
| + },
|
|
|
| - document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
|
| - },
|
| + _didFocus: function(e) {
|
| + var targetModel = this.modelForElement(e.target);
|
| + var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
|
| + var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
|
| + var fidx = this._focusedIndex;
|
|
|
| - /**
|
| - * Cause a text string to be announced by screen readers.
|
| - *
|
| - * @param {string} text The text that should be announced.
|
| - */
|
| - announce: function(text) {
|
| - this._text = '';
|
| - this.async(function() {
|
| - this._text = text;
|
| - }, 100);
|
| - },
|
| + if (!targetModel || !focusedModel) {
|
| + return;
|
| + }
|
| + if (focusedModel === targetModel) {
|
| + // if the user focused the same item, then bring it into view if it's not visible
|
| + if (!this._isIndexVisible(fidx)) {
|
| + this.scrollToIndex(fidx);
|
| + }
|
| + } else {
|
| + this._restoreFocusedItem();
|
| + // restore tabIndex for the currently focused item
|
| + focusedModel.tabIndex = -1;
|
| + // set the tabIndex for the next focused item
|
| + targetModel.tabIndex = 0;
|
| + fidx = targetModel[this.indexAs];
|
| + this._focusedIndex = fidx;
|
| + this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
|
|
|
| - _onIronAnnounce: function(event) {
|
| - if (event.detail && event.detail.text) {
|
| - this.announce(event.detail.text);
|
| - }
|
| + if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
|
| + this._update();
|
| }
|
| - });
|
| + }
|
| + },
|
|
|
| - Polymer.IronA11yAnnouncer.instance = null;
|
| + _didMoveUp: function() {
|
| + this._focusPhysicalItem(this._focusedIndex - 1);
|
| + },
|
|
|
| - Polymer.IronA11yAnnouncer.requestAvailability = function() {
|
| - if (!Polymer.IronA11yAnnouncer.instance) {
|
| - Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
|
| - }
|
| + _didMoveDown: function(e) {
|
| + // disable scroll when pressing the down key
|
| + e.detail.keyboardEvent.preventDefault();
|
| + this._focusPhysicalItem(this._focusedIndex + 1);
|
| + },
|
|
|
| - document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
|
| - };
|
| - })();
|
| -/**
|
| - * Singleton IronMeta instance.
|
| - */
|
| - Polymer.IronValidatableBehaviorMeta = null;
|
| + _didEnter: function(e) {
|
| + this._focusPhysicalItem(this._focusedIndex);
|
| + this._selectionHandler(e.detail.keyboardEvent);
|
| + }
|
| + });
|
|
|
| - /**
|
| - * `Use Polymer.IronValidatableBehavior` to implement an element that validates user input.
|
| - * Use the related `Polymer.IronValidatorBehavior` to add custom validation logic to an iron-input.
|
| - *
|
| - * By default, an `<iron-form>` element validates its fields when the user presses the submit button.
|
| - * To validate a form imperatively, call the form's `validate()` method, which in turn will
|
| - * call `validate()` on all its children. By using `Polymer.IronValidatableBehavior`, your
|
| - * custom element will get a public `validate()`, which
|
| - * will return the validity of the element, and a corresponding `invalid` attribute,
|
| - * which can be used for styling.
|
| - *
|
| - * To implement the custom validation logic of your element, you must override
|
| - * the protected `_getValidity()` method of this behaviour, rather than `validate()`.
|
| - * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/simple-element.html)
|
| - * for an example.
|
| - *
|
| - * ### Accessibility
|
| - *
|
| - * Changing the `invalid` property, either manually or by calling `validate()` will update the
|
| - * `aria-invalid` attribute.
|
| - *
|
| - * @demo demo/index.html
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.IronValidatableBehavior = {
|
| +})();
|
| +Polymer({
|
| +
|
| + is: 'iron-scroll-threshold',
|
|
|
| properties: {
|
|
|
| /**
|
| - * Name of the validator to use.
|
| + * Distance from the top (or left, for horizontal) bound of the scroller
|
| + * where the "upper trigger" will fire.
|
| */
|
| - validator: {
|
| - type: String
|
| + upperThreshold: {
|
| + type: Number,
|
| + value: 100
|
| },
|
|
|
| /**
|
| - * True if the last call to `validate` is invalid.
|
| + * Distance from the bottom (or right, for horizontal) bound of the scroller
|
| + * where the "lower trigger" will fire.
|
| */
|
| - invalid: {
|
| - notify: true,
|
| - reflectToAttribute: true,
|
| - type: Boolean,
|
| - value: false
|
| + lowerThreshold: {
|
| + type: Number,
|
| + value: 100
|
| },
|
|
|
| /**
|
| - * This property is deprecated and should not be used. Use the global
|
| - * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead.
|
| + * Read-only value that tracks the triggered state of the upper threshold.
|
| */
|
| - _validatorMeta: {
|
| - type: Object
|
| + upperTriggered: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + readOnly: true
|
| },
|
|
|
| /**
|
| - * Namespace for this validator. This property is deprecated and should
|
| - * not be used. For all intents and purposes, please consider it a
|
| - * read-only, config-time property.
|
| + * Read-only value that tracks the triggered state of the lower threshold.
|
| */
|
| - validatorType: {
|
| - type: String,
|
| - value: 'validator'
|
| + lowerTriggered: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + readOnly: true
|
| },
|
|
|
| - _validator: {
|
| - type: Object,
|
| - computed: '__computeValidator(validator)'
|
| + /**
|
| + * True if the orientation of the scroller is horizontal.
|
| + */
|
| + horizontal: {
|
| + type: Boolean,
|
| + value: false
|
| }
|
| },
|
|
|
| + behaviors: [
|
| + Polymer.IronScrollTargetBehavior
|
| + ],
|
| +
|
| observers: [
|
| - '_invalidChanged(invalid)'
|
| + '_setOverflow(scrollTarget)',
|
| + '_initCheck(horizontal, isAttached)'
|
| ],
|
|
|
| - registered: function() {
|
| - Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validator'});
|
| + get _defaultScrollTarget() {
|
| + return this;
|
| + },
|
| +
|
| + _setOverflow: function(scrollTarget) {
|
| + this.style.overflow = scrollTarget === this ? 'auto' : '';
|
| + },
|
| +
|
| + _scrollHandler: function() {
|
| + // throttle the work on the scroll event
|
| + var THROTTLE_THRESHOLD = 200;
|
| + if (!this.isDebouncerActive('_checkTheshold')) {
|
| + this.debounce('_checkTheshold', function() {
|
| + this.checkScrollThesholds();
|
| + }, THROTTLE_THRESHOLD);
|
| + }
|
| },
|
|
|
| - _invalidChanged: function() {
|
| - if (this.invalid) {
|
| - this.setAttribute('aria-invalid', 'true');
|
| - } else {
|
| - this.removeAttribute('aria-invalid');
|
| + _initCheck: function(horizontal, isAttached) {
|
| + if (isAttached) {
|
| + this.debounce('_init', function() {
|
| + this.clearTriggers();
|
| + this.checkScrollThesholds();
|
| + });
|
| }
|
| },
|
|
|
| /**
|
| - * @return {boolean} True if the validator `validator` exists.
|
| + * Checks the scroll thresholds.
|
| + * This method is automatically called by iron-scroll-threshold.
|
| + *
|
| + * @method checkScrollThesholds
|
| */
|
| - hasValidator: function() {
|
| - return this._validator != null;
|
| + checkScrollThesholds: function() {
|
| + if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
|
| + return;
|
| + }
|
| + var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTop;
|
| + var lowerScrollValue = this.horizontal ?
|
| + this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scrollLeft :
|
| + this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._scrollTop;
|
| +
|
| + // Detect upper threshold
|
| + if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
|
| + this._setUpperTriggered(true);
|
| + this.fire('upper-threshold');
|
| + }
|
| + // Detect lower threshold
|
| + if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
|
| + this._setLowerTriggered(true);
|
| + this.fire('lower-threshold');
|
| + }
|
| },
|
|
|
| /**
|
| - * Returns true if the `value` is valid, and updates `invalid`. If you want
|
| - * your element to have custom validation logic, do not override this method;
|
| - * override `_getValidity(value)` instead.
|
| + * Clear the upper and lower threshold states.
|
| + *
|
| + * @method clearTriggers
|
| + */
|
| + clearTriggers: function() {
|
| + this._setUpperTriggered(false);
|
| + this._setLowerTriggered(false);
|
| + }
|
|
|
| - * @param {Object} value The value to be validated. By default, it is passed
|
| - * to the validator's `validate()` function, if a validator is set.
|
| - * @return {boolean} True if `value` is valid.
|
| + /**
|
| + * Fires when the lower threshold has been reached.
|
| + *
|
| + * @event lower-threshold
|
| */
|
| - validate: function(value) {
|
| - this.invalid = !this._getValidity(value);
|
| - return !this.invalid;
|
| - },
|
|
|
| /**
|
| - * Returns true if `value` is valid. By default, it is passed
|
| - * to the validator's `validate()` function, if a validator is set. You
|
| - * should override this method if you want to implement custom validity
|
| - * logic for your element.
|
| + * Fires when the upper threshold has been reached.
|
| *
|
| - * @param {Object} value The value to be validated.
|
| - * @return {boolean} True if `value` is valid.
|
| + * @event upper-threshold
|
| */
|
|
|
| - _getValidity: function(value) {
|
| - if (this.hasValidator()) {
|
| - return this._validator.validate(value);
|
| - }
|
| - return true;
|
| + });
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +Polymer({
|
| + is: 'history-list',
|
| +
|
| + properties: {
|
| + // The search term for the current query. Set when the query returns.
|
| + searchedTerm: {
|
| + type: String,
|
| + value: '',
|
| },
|
|
|
| - __computeValidator: function() {
|
| - return Polymer.IronValidatableBehaviorMeta &&
|
| - Polymer.IronValidatableBehaviorMeta.byKey(this.validator);
|
| - }
|
| - };
|
| -/*
|
| -`<iron-input>` adds two-way binding and custom validators using `Polymer.IronValidatorBehavior`
|
| -to `<input>`.
|
| + lastSearchedTerm_: String,
|
|
|
| -### Two-way binding
|
| + querying: Boolean,
|
|
|
| -By default you can only get notified of changes to an `input`'s `value` due to user input:
|
| + // An array of history entries in reverse chronological order.
|
| + historyData_: Array,
|
|
|
| - <input value="{{myValue::input}}">
|
| + resultLoadingDisabled_: {
|
| + type: Boolean,
|
| + value: false,
|
| + },
|
| + },
|
|
|
| -`iron-input` adds the `bind-value` property that mirrors the `value` property, and can be used
|
| -for two-way data binding. `bind-value` will notify if it is changed either by user input or by script.
|
| + listeners: {
|
| + 'infinite-list.scroll': 'notifyListScroll_',
|
| + 'remove-bookmark-stars': 'removeBookmarkStars_',
|
| + },
|
|
|
| - <input is="iron-input" bind-value="{{myValue}}">
|
| + /** @override */
|
| + attached: function() {
|
| + // It is possible (eg, when middle clicking the reload button) for all other
|
| + // resize events to fire before the list is attached and can be measured.
|
| + // Adding another resize here ensures it will get sized correctly.
|
| + /** @type {IronListElement} */(this.$['infinite-list']).notifyResize();
|
| + },
|
|
|
| -### Custom validators
|
| + /**
|
| + * Remove bookmark star for history items with matching URLs.
|
| + * @param {{detail: !string}} e
|
| + * @private
|
| + */
|
| + removeBookmarkStars_: function(e) {
|
| + var url = e.detail;
|
|
|
| -You can use custom validators that implement `Polymer.IronValidatorBehavior` with `<iron-input>`.
|
| + if (this.historyData_ === undefined)
|
| + return;
|
|
|
| - <input is="iron-input" validator="my-custom-validator">
|
| + for (var i = 0; i < this.historyData_.length; i++) {
|
| + if (this.historyData_[i].url == url)
|
| + this.set('historyData_.' + i + '.starred', false);
|
| + }
|
| + },
|
|
|
| -### Stopping invalid input
|
| + /**
|
| + * Disables history result loading when there are no more history results.
|
| + */
|
| + disableResultLoading: function() {
|
| + this.resultLoadingDisabled_ = true;
|
| + },
|
|
|
| -It may be desirable to only allow users to enter certain characters. You can use the
|
| -`prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
|
| -is separate from validation, and `allowed-pattern` does not affect how the input is validated.
|
| + /**
|
| + * Adds the newly updated history results into historyData_. Adds new fields
|
| + * for each result.
|
| + * @param {!Array<!HistoryEntry>} historyResults The new history results.
|
| + */
|
| + addNewResults: function(historyResults) {
|
| + var results = historyResults.slice();
|
| + /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold'])
|
| + .clearTriggers();
|
| +
|
| + if (this.lastSearchedTerm_ != this.searchedTerm) {
|
| + this.resultLoadingDisabled_ = false;
|
| + if (this.historyData_)
|
| + this.splice('historyData_', 0, this.historyData_.length);
|
| + this.fire('unselect-all');
|
| + this.lastSearchedTerm_ = this.searchedTerm;
|
| + }
|
|
|
| - \x3c!-- only allow characters that match [0-9] --\x3e
|
| - <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
|
| + if (this.historyData_) {
|
| + // If we have previously received data, push the new items onto the
|
| + // existing array.
|
| + results.unshift('historyData_');
|
| + this.push.apply(this, results);
|
| + } else {
|
| + // The first time we receive data, use set() to ensure the iron-list is
|
| + // initialized correctly.
|
| + this.set('historyData_', results);
|
| + }
|
| + },
|
|
|
| -@hero hero.svg
|
| -@demo demo/index.html
|
| -*/
|
| + /**
|
| + * Cycle through each entry in historyData_ and set all items to be
|
| + * unselected.
|
| + * @param {number} overallItemCount The number of checkboxes selected.
|
| + */
|
| + unselectAllItems: function(overallItemCount) {
|
| + if (this.historyData_ === undefined)
|
| + return;
|
|
|
| - Polymer({
|
| + for (var i = 0; i < this.historyData_.length; i++) {
|
| + if (this.historyData_[i].selected) {
|
| + this.set('historyData_.' + i + '.selected', false);
|
| + overallItemCount--;
|
| + if (overallItemCount == 0)
|
| + break;
|
| + }
|
| + }
|
| + },
|
|
|
| - is: 'iron-input',
|
| + /**
|
| + * Remove the given |items| from the list. Expected to be called after the
|
| + * items are removed from the backend.
|
| + * @param {!Array<!HistoryEntry>} removalList
|
| + * @private
|
| + */
|
| + removeDeletedHistory_: function(removalList) {
|
| + // This set is only for speed. Note that set inclusion for objects is by
|
| + // reference, so this relies on the HistoryEntry objects never being copied.
|
| + var deletedItems = new Set(removalList);
|
| + var splices = [];
|
| +
|
| + for (var i = this.historyData_.length - 1; i >= 0; i--) {
|
| + var item = this.historyData_[i];
|
| + if (deletedItems.has(item)) {
|
| + // Removes the selected item from historyData_. Use unshift so
|
| + // |splices| ends up in index order.
|
| + splices.unshift({
|
| + index: i,
|
| + removed: [item],
|
| + addedCount: 0,
|
| + object: this.historyData_,
|
| + type: 'splice'
|
| + });
|
| + this.historyData_.splice(i, 1);
|
| + }
|
| + }
|
| + // notifySplices gives better performance than individually splicing as it
|
| + // batches all of the updates together.
|
| + this.notifySplices('historyData_', splices);
|
| + },
|
|
|
| - extends: 'input',
|
| + /**
|
| + * Performs a request to the backend to delete all selected items. If
|
| + * successful, removes them from the view. Does not prompt the user before
|
| + * deleting -- see <history-list-container> for a version of this method which
|
| + * does prompt.
|
| + */
|
| + deleteSelected: function() {
|
| + var toBeRemoved = this.historyData_.filter(function(item) {
|
| + return item.selected;
|
| + });
|
| + md_history.BrowserService.getInstance()
|
| + .deleteItems(toBeRemoved)
|
| + .then(function(items) {
|
| + this.removeDeletedHistory_(items);
|
| + this.fire('unselect-all');
|
| + }.bind(this));
|
| + },
|
|
|
| - behaviors: [
|
| - Polymer.IronValidatableBehavior
|
| - ],
|
| + /**
|
| + * Called when the page is scrolled to near the bottom of the list.
|
| + * @private
|
| + */
|
| + loadMoreData_: function() {
|
| + if (this.resultLoadingDisabled_ || this.querying)
|
| + return;
|
|
|
| - properties: {
|
| + this.fire('load-more-history');
|
| + },
|
|
|
| - /**
|
| - * Use this property instead of `value` for two-way data binding.
|
| - */
|
| - bindValue: {
|
| - observer: '_bindValueChanged',
|
| - type: String
|
| - },
|
| + /**
|
| + * Check whether the time difference between the given history item and the
|
| + * next one is large enough for a spacer to be required.
|
| + * @param {HistoryEntry} item
|
| + * @param {number} index The index of |item| in |historyData_|.
|
| + * @param {number} length The length of |historyData_|.
|
| + * @return {boolean} Whether or not time gap separator is required.
|
| + * @private
|
| + */
|
| + needsTimeGap_: function(item, index, length) {
|
| + return md_history.HistoryItem.needsTimeGap(
|
| + this.historyData_, index, this.searchedTerm);
|
| + },
|
|
|
| - /**
|
| - * Set to true to prevent the user from entering invalid input. If `allowedPattern` is set,
|
| - * any character typed by the user will be matched against that pattern, and rejected if it's not a match.
|
| - * Pasted input will have each character checked individually; if any character
|
| - * doesn't match `allowedPattern`, the entire pasted string will be rejected.
|
| - * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
|
| - */
|
| - preventInvalidInput: {
|
| - type: Boolean
|
| - },
|
| + hasResults: function(historyDataLength) {
|
| + return historyDataLength > 0;
|
| + },
|
|
|
| - /**
|
| - * Regular expression that list the characters allowed as input.
|
| - * This pattern represents the allowed characters for the field; as the user inputs text,
|
| - * each individual character will be checked against the pattern (rather than checking
|
| - * the entire value as a whole). The recommended format should be a list of allowed characters;
|
| - * for example, `[a-zA-Z0-9.+-!;:]`
|
| - */
|
| - allowedPattern: {
|
| - type: String,
|
| - observer: "_allowedPatternChanged"
|
| - },
|
| + noResultsMessage_: function(searchedTerm, isLoading) {
|
| + if (isLoading)
|
| + return '';
|
| + var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults';
|
| + return loadTimeData.getString(messageId);
|
| + },
|
|
|
| - _previousValidInput: {
|
| - type: String,
|
| - value: ''
|
| - },
|
| + /**
|
| + * True if the given item is the beginning of a new card.
|
| + * @param {HistoryEntry} item
|
| + * @param {number} i Index of |item| within |historyData_|.
|
| + * @param {number} length
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + isCardStart_: function(item, i, length) {
|
| + if (length == 0 || i > length - 1)
|
| + return false;
|
| + return i == 0 ||
|
| + this.historyData_[i].dateRelativeDay !=
|
| + this.historyData_[i - 1].dateRelativeDay;
|
| + },
|
|
|
| - _patternAlreadyChecked: {
|
| - type: Boolean,
|
| - value: false
|
| - }
|
| + /**
|
| + * True if the given item is the end of a card.
|
| + * @param {HistoryEntry} item
|
| + * @param {number} i Index of |item| within |historyData_|.
|
| + * @param {number} length
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + isCardEnd_: function(item, i, length) {
|
| + if (length == 0 || i > length - 1)
|
| + return false;
|
| + return i == length - 1 ||
|
| + this.historyData_[i].dateRelativeDay !=
|
| + this.historyData_[i + 1].dateRelativeDay;
|
| + },
|
|
|
| - },
|
| + /**
|
| + * @param {number} index
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + isFirstItem_: function(index) {
|
| + return index == 0;
|
| + },
|
|
|
| - listeners: {
|
| - 'input': '_onInput',
|
| - 'keypress': '_onKeypress'
|
| - },
|
| + /**
|
| + * @private
|
| + */
|
| + notifyListScroll_: function() {
|
| + this.fire('history-list-scrolled');
|
| + },
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - /** @suppress {checkTypes} */
|
| - registered: function() {
|
| - // Feature detect whether we need to patch dispatchEvent (i.e. on FF and IE).
|
| - if (!this._canDispatchEventOnDisabled()) {
|
| - this._origDispatchEvent = this.dispatchEvent;
|
| - this.dispatchEvent = this._dispatchEventFirefoxIE;
|
| - }
|
| - },
|
| +Polymer({
|
| + is: 'history-list-container',
|
|
|
| - created: function() {
|
| - Polymer.IronA11yAnnouncer.requestAvailability();
|
| - },
|
| + properties: {
|
| + // The path of the currently selected page.
|
| + selectedPage_: String,
|
|
|
| - _canDispatchEventOnDisabled: function() {
|
| - var input = document.createElement('input');
|
| - var canDispatch = false;
|
| - input.disabled = true;
|
| + // Whether domain-grouped history is enabled.
|
| + grouped: Boolean,
|
|
|
| - input.addEventListener('feature-check-dispatch-event', function() {
|
| - canDispatch = true;
|
| - });
|
| + /** @type {!QueryState} */
|
| + queryState: Object,
|
|
|
| - try {
|
| - input.dispatchEvent(new Event('feature-check-dispatch-event'));
|
| - } catch(e) {}
|
| + /** @type {!QueryResult} */
|
| + queryResult: Object,
|
| + },
|
|
|
| - return canDispatch;
|
| - },
|
| + observers: [
|
| + 'groupedRangeChanged_(queryState.range)',
|
| + ],
|
|
|
| - _dispatchEventFirefoxIE: function() {
|
| - // Due to Firefox bug, events fired on disabled form controls can throw
|
| - // errors; furthermore, neither IE nor Firefox will actually dispatch
|
| - // events from disabled form controls; as such, we toggle disable around
|
| - // the dispatch to allow notifying properties to notify
|
| - // See issue #47 for details
|
| - var disabled = this.disabled;
|
| - this.disabled = false;
|
| - this._origDispatchEvent.apply(this, arguments);
|
| - this.disabled = disabled;
|
| - },
|
| + listeners: {
|
| + 'history-list-scrolled': 'closeMenu_',
|
| + 'load-more-history': 'loadMoreHistory_',
|
| + 'toggle-menu': 'toggleMenu_',
|
| + },
|
| +
|
| + /**
|
| + * @param {HistoryQuery} info An object containing information about the
|
| + * query.
|
| + * @param {!Array<HistoryEntry>} results A list of results.
|
| + */
|
| + historyResult: function(info, results) {
|
| + this.initializeResults_(info, results);
|
| + this.closeMenu_();
|
| +
|
| + if (this.selectedPage_ == 'grouped-list') {
|
| + this.$$('#grouped-list').historyData = results;
|
| + return;
|
| + }
|
| +
|
| + var list = /** @type {HistoryListElement} */(this.$['infinite-list']);
|
| + list.addNewResults(results);
|
| + if (info.finished)
|
| + list.disableResultLoading();
|
| + },
|
| +
|
| + /**
|
| + * Queries the history backend for results based on queryState.
|
| + * @param {boolean} incremental Whether the new query should continue where
|
| + * the previous query stopped.
|
| + */
|
| + queryHistory: function(incremental) {
|
| + var queryState = this.queryState;
|
| + // Disable querying until the first set of results have been returned. If
|
| + // there is a search, query immediately to support search query params from
|
| + // the URL.
|
| + var noResults = !this.queryResult || this.queryResult.results == null;
|
| + if (queryState.queryingDisabled ||
|
| + (!this.queryState.searchTerm && noResults)) {
|
| + return;
|
| + }
|
|
|
| - get _patternRegExp() {
|
| - var pattern;
|
| - if (this.allowedPattern) {
|
| - pattern = new RegExp(this.allowedPattern);
|
| - } else {
|
| - switch (this.type) {
|
| - case 'number':
|
| - pattern = /[0-9.,e-]/;
|
| - break;
|
| - }
|
| - }
|
| - return pattern;
|
| - },
|
| + // Close any open dialog if a new query is initiated.
|
| + if (!incremental && this.$.dialog.open)
|
| + this.$.dialog.close();
|
|
|
| - ready: function() {
|
| - this.bindValue = this.value;
|
| - },
|
| + this.set('queryState.querying', true);
|
| + this.set('queryState.incremental', incremental);
|
|
|
| - /**
|
| - * @suppress {checkTypes}
|
| - */
|
| - _bindValueChanged: function() {
|
| - if (this.value !== this.bindValue) {
|
| - this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue === false) ? '' : this.bindValue;
|
| - }
|
| - // manually notify because we don't want to notify until after setting value
|
| - this.fire('bind-value-changed', {value: this.bindValue});
|
| - },
|
| + var lastVisitTime = 0;
|
| + if (incremental) {
|
| + var lastVisit = this.queryResult.results.slice(-1)[0];
|
| + lastVisitTime = lastVisit ? lastVisit.time : 0;
|
| + }
|
|
|
| - _allowedPatternChanged: function() {
|
| - // Force to prevent invalid input when an `allowed-pattern` is set
|
| - this.preventInvalidInput = this.allowedPattern ? true : false;
|
| - },
|
| + var maxResults =
|
| + queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0;
|
| + chrome.send('queryHistory', [
|
| + queryState.searchTerm, queryState.groupedOffset, queryState.range,
|
| + lastVisitTime, maxResults
|
| + ]);
|
| + },
|
|
|
| - _onInput: function() {
|
| - // Need to validate each of the characters pasted if they haven't
|
| - // been validated inside `_onKeypress` already.
|
| - if (this.preventInvalidInput && !this._patternAlreadyChecked) {
|
| - var valid = this._checkPatternValidity();
|
| - if (!valid) {
|
| - this._announceInvalidCharacter('Invalid string of characters not entered.');
|
| - this.value = this._previousValidInput;
|
| - }
|
| - }
|
| + unselectAllItems: function(count) {
|
| + /** @type {HistoryListElement} */ (this.$['infinite-list'])
|
| + .unselectAllItems(count);
|
| + },
|
|
|
| - this.bindValue = this.value;
|
| - this._previousValidInput = this.value;
|
| - this._patternAlreadyChecked = false;
|
| - },
|
| + /**
|
| + * Delete all the currently selected history items. Will prompt the user with
|
| + * a dialog to confirm that the deletion should be performed.
|
| + */
|
| + deleteSelectedWithPrompt: function() {
|
| + if (!loadTimeData.getBoolean('allowDeletingHistory'))
|
| + return;
|
|
|
| - _isPrintable: function(event) {
|
| - // What a control/printable character is varies wildly based on the browser.
|
| - // - most control characters (arrows, backspace) do not send a `keypress` event
|
| - // in Chrome, but the *do* on Firefox
|
| - // - in Firefox, when they do send a `keypress` event, control chars have
|
| - // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
|
| - // - printable characters always send a keypress event.
|
| - // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode
|
| - // always matches the charCode.
|
| - // None of this makes any sense.
|
| + this.$.dialog.showModal();
|
| + },
|
|
|
| - // For these keys, ASCII code == browser keycode.
|
| - var anyNonPrintable =
|
| - (event.keyCode == 8) || // backspace
|
| - (event.keyCode == 9) || // tab
|
| - (event.keyCode == 13) || // enter
|
| - (event.keyCode == 27); // escape
|
| + /**
|
| + * @param {HistoryRange} range
|
| + * @private
|
| + */
|
| + groupedRangeChanged_: function(range) {
|
| + this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ?
|
| + 'infinite-list' : 'grouped-list';
|
|
|
| - // For these keys, make sure it's a browser keycode and not an ASCII code.
|
| - var mozNonPrintable =
|
| - (event.keyCode == 19) || // pause
|
| - (event.keyCode == 20) || // caps lock
|
| - (event.keyCode == 45) || // insert
|
| - (event.keyCode == 46) || // delete
|
| - (event.keyCode == 144) || // num lock
|
| - (event.keyCode == 145) || // scroll lock
|
| - (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, home, arrows
|
| - (event.keyCode > 111 && event.keyCode < 124); // fn keys
|
| + this.queryHistory(false);
|
| + },
|
|
|
| - return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
|
| - },
|
| + /** @private */
|
| + loadMoreHistory_: function() { this.queryHistory(true); },
|
|
|
| - _onKeypress: function(event) {
|
| - if (!this.preventInvalidInput && this.type !== 'number') {
|
| - return;
|
| - }
|
| - var regexp = this._patternRegExp;
|
| - if (!regexp) {
|
| - return;
|
| - }
|
| + /**
|
| + * @param {HistoryQuery} info
|
| + * @param {!Array<HistoryEntry>} results
|
| + * @private
|
| + */
|
| + initializeResults_: function(info, results) {
|
| + if (results.length == 0)
|
| + return;
|
|
|
| - // Handle special keys and backspace
|
| - if (event.metaKey || event.ctrlKey || event.altKey)
|
| - return;
|
| + var currentDate = results[0].dateRelativeDay;
|
|
|
| - // Check the pattern either here or in `_onInput`, but not in both.
|
| - this._patternAlreadyChecked = true;
|
| + for (var i = 0; i < results.length; i++) {
|
| + // Sets the default values for these fields to prevent undefined types.
|
| + results[i].selected = false;
|
| + results[i].readableTimestamp =
|
| + info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort;
|
|
|
| - var thisChar = String.fromCharCode(event.charCode);
|
| - if (this._isPrintable(event) && !regexp.test(thisChar)) {
|
| - event.preventDefault();
|
| - this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.');
|
| + if (results[i].dateRelativeDay != currentDate) {
|
| + currentDate = results[i].dateRelativeDay;
|
| }
|
| - },
|
| + }
|
| + },
|
|
|
| - _checkPatternValidity: function() {
|
| - var regexp = this._patternRegExp;
|
| - if (!regexp) {
|
| - return true;
|
| - }
|
| - for (var i = 0; i < this.value.length; i++) {
|
| - if (!regexp.test(this.value[i])) {
|
| - return false;
|
| - }
|
| - }
|
| - return true;
|
| - },
|
| + /** @private */
|
| + onDialogConfirmTap_: function() {
|
| + this.$['infinite-list'].deleteSelected();
|
| + this.$.dialog.close();
|
| + },
|
|
|
| - /**
|
| - * Returns true if `value` is valid. The validator provided in `validator` will be used first,
|
| - * then any constraints.
|
| - * @return {boolean} True if the value is valid.
|
| - */
|
| - validate: function() {
|
| - // First, check what the browser thinks. Some inputs (like type=number)
|
| - // behave weirdly and will set the value to "" if something invalid is
|
| - // entered, but will set the validity correctly.
|
| - var valid = this.checkValidity();
|
| + /** @private */
|
| + onDialogCancelTap_: function() {
|
| + this.$.dialog.close();
|
| + },
|
|
|
| - // Only do extra checking if the browser thought this was valid.
|
| - if (valid) {
|
| - // Empty, required input is invalid
|
| - if (this.required && this.value === '') {
|
| - valid = false;
|
| - } else if (this.hasValidator()) {
|
| - valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
| - }
|
| - }
|
| + /**
|
| + * Closes the overflow menu.
|
| + * @private
|
| + */
|
| + closeMenu_: function() {
|
| + /** @type {CrSharedMenuElement} */(this.$.sharedMenu).closeMenu();
|
| + },
|
|
|
| - this.invalid = !valid;
|
| - this.fire('iron-input-validate');
|
| - return valid;
|
| - },
|
| + /**
|
| + * Opens the overflow menu unless the menu is already open and the same button
|
| + * is pressed.
|
| + * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e
|
| + * @private
|
| + */
|
| + toggleMenu_: function(e) {
|
| + var target = e.detail.target;
|
| + /** @type {CrSharedMenuElement} */(this.$.sharedMenu).toggleMenu(
|
| + target, e.detail.item);
|
| + },
|
|
|
| - _announceInvalidCharacter: function(message) {
|
| - this.fire('iron-announce', { text: message });
|
| - }
|
| - });
|
| + /** @private */
|
| + onMoreFromSiteTap_: function() {
|
| + var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
|
| + this.fire('search-domain', {domain: menu.itemData.domain});
|
| + menu.closeMenu();
|
| + },
|
| +
|
| + /** @private */
|
| + onRemoveFromHistoryTap_: function() {
|
| + var menu = /** @type {CrSharedMenuElement} */(this.$.sharedMenu);
|
| + md_history.BrowserService.getInstance()
|
| + .deleteItems([menu.itemData])
|
| + .then(function(items) {
|
| + this.$['infinite-list'].removeDeletedHistory_(items);
|
| + // This unselect-all is to reset the toolbar when deleting a selected
|
| + // item. TODO(tsergeant): Make this automatic based on observing list
|
| + // modifications.
|
| + this.fire('unselect-all');
|
| + }.bind(this));
|
| + menu.closeMenu();
|
| + },
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - /*
|
| - The `iron-input-validate` event is fired whenever `validate()` is called.
|
| - @event iron-input-validate
|
| - */
|
| Polymer({
|
| - is: 'paper-input-container',
|
| + is: 'history-synced-device-card',
|
|
|
| - properties: {
|
| - /**
|
| - * Set to true to disable the floating label. The label disappears when the input value is
|
| - * not null.
|
| - */
|
| - noLabelFloat: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + properties: {
|
| + // Name of the synced device.
|
| + device: String,
|
|
|
| - /**
|
| - * Set to true to always float the floating label.
|
| - */
|
| - alwaysFloatLabel: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + // When the device information was last updated.
|
| + lastUpdateTime: String,
|
|
|
| - /**
|
| - * The attribute to listen for value changes on.
|
| - */
|
| - attrForValue: {
|
| - type: String,
|
| - value: 'bind-value'
|
| - },
|
| + /**
|
| + * The list of tabs open for this device.
|
| + * @type {!Array<!ForeignSessionTab>}
|
| + */
|
| + tabs: {
|
| + type: Array,
|
| + value: function() { return []; },
|
| + observer: 'updateIcons_'
|
| + },
|
|
|
| - /**
|
| - * Set to true to auto-validate the input value when it changes.
|
| - */
|
| - autoValidate: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + /**
|
| + * The indexes where a window separator should be shown. The use of a
|
| + * separate array here is necessary for window separators to appear
|
| + * correctly in search. See http://crrev.com/2022003002 for more details.
|
| + * @type {!Array<number>}
|
| + */
|
| + separatorIndexes: Array,
|
|
|
| - /**
|
| - * True if the input is invalid. This property is set automatically when the input value
|
| - * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
|
| - */
|
| - invalid: {
|
| - observer: '_invalidChanged',
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + // Whether the card is open.
|
| + cardOpen_: {type: Boolean, value: true},
|
|
|
| - /**
|
| - * True if the input has focus.
|
| - */
|
| - focused: {
|
| - readOnly: true,
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true
|
| - },
|
| + searchTerm: String,
|
|
|
| - _addons: {
|
| - type: Array
|
| - // do not set a default value here intentionally - it will be initialized lazily when a
|
| - // distributed child is attached, which may occur before configuration for this element
|
| - // in polyfill.
|
| - },
|
| + // Internal identifier for the device.
|
| + sessionTag: String,
|
| + },
|
|
|
| - _inputHasContent: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| + /**
|
| + * Open a single synced tab. Listens to 'click' rather than 'tap'
|
| + * to determine what modifier keys were pressed.
|
| + * @param {DomRepeatClickEvent} e
|
| + * @private
|
| + */
|
| + openTab_: function(e) {
|
| + var tab = /** @type {ForeignSessionTab} */(e.model.tab);
|
| + md_history.BrowserService.getInstance().openForeignSessionTab(
|
| + this.sessionTag, tab.windowId, tab.sessionId, e);
|
| + e.preventDefault();
|
| + },
|
| +
|
| + /**
|
| + * Toggles the dropdown display of synced tabs for each device card.
|
| + */
|
| + toggleTabCard: function() {
|
| + this.$.collapse.toggle();
|
| + this.$['dropdown-indicator'].icon =
|
| + this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more';
|
| + },
|
| +
|
| + /**
|
| + * When the synced tab information is set, the icon associated with the tab
|
| + * website is also set.
|
| + * @private
|
| + */
|
| + updateIcons_: function() {
|
| + this.async(function() {
|
| + var icons = Polymer.dom(this.root).querySelectorAll('.website-icon');
|
| +
|
| + for (var i = 0; i < this.tabs.length; i++) {
|
| + icons[i].style.backgroundImage =
|
| + cr.icon.getFaviconImageSet(this.tabs[i].url);
|
| + }
|
| + });
|
| + },
|
| +
|
| + /** @private */
|
| + isWindowSeparatorIndex_: function(index, separatorIndexes) {
|
| + return this.separatorIndexes.indexOf(index) != -1;
|
| + },
|
|
|
| - _inputSelector: {
|
| - type: String,
|
| - value: 'input,textarea,.paper-input-input'
|
| - },
|
| + /**
|
| + * @param {boolean} cardOpen
|
| + * @return {string}
|
| + */
|
| + getCollapseTitle_: function(cardOpen) {
|
| + return cardOpen ? loadTimeData.getString('collapseSessionButton') :
|
| + loadTimeData.getString('expandSessionButton');
|
| + },
|
|
|
| - _boundOnFocus: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onFocus.bind(this);
|
| - }
|
| - },
|
| + /**
|
| + * @param {CustomEvent} e
|
| + * @private
|
| + */
|
| + onMenuButtonTap_: function(e) {
|
| + this.fire('toggle-menu', {
|
| + target: Polymer.dom(e).localTarget,
|
| + tag: this.sessionTag
|
| + });
|
| + e.stopPropagation(); // Prevent iron-collapse.
|
| + },
|
| +});
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - _boundOnBlur: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onBlur.bind(this);
|
| - }
|
| - },
|
| +/**
|
| + * @typedef {{device: string,
|
| + * lastUpdateTime: string,
|
| + * separatorIndexes: !Array<number>,
|
| + * timestamp: number,
|
| + * tabs: !Array<!ForeignSessionTab>,
|
| + * tag: string}}
|
| + */
|
| +var ForeignDeviceInternal;
|
|
|
| - _boundOnInput: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onInput.bind(this);
|
| - }
|
| - },
|
| +Polymer({
|
| + is: 'history-synced-device-manager',
|
|
|
| - _boundValueChanged: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onValueChanged.bind(this);
|
| - }
|
| - }
|
| + properties: {
|
| + /**
|
| + * @type {?Array<!ForeignSession>}
|
| + */
|
| + sessionList: {
|
| + type: Array,
|
| + observer: 'updateSyncedDevices'
|
| },
|
|
|
| - listeners: {
|
| - 'addon-attached': '_onAddonAttached',
|
| - 'iron-input-validate': '_onIronInputValidate'
|
| + searchTerm: {
|
| + type: String,
|
| + observer: 'searchTermChanged'
|
| },
|
|
|
| - get _valueChangedEvent() {
|
| - return this.attrForValue + '-changed';
|
| + /**
|
| + * An array of synced devices with synced tab data.
|
| + * @type {!Array<!ForeignDeviceInternal>}
|
| + */
|
| + syncedDevices_: {
|
| + type: Array,
|
| + value: function() { return []; }
|
| },
|
|
|
| - get _propertyForValue() {
|
| - return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
|
| + /** @private */
|
| + signInState_: {
|
| + type: Boolean,
|
| + value: loadTimeData.getBoolean('isUserSignedIn'),
|
| },
|
|
|
| - get _inputElement() {
|
| - return Polymer.dom(this).querySelector(this._inputSelector);
|
| + /** @private */
|
| + guestSession_: {
|
| + type: Boolean,
|
| + value: loadTimeData.getBoolean('isGuestSession'),
|
| },
|
|
|
| - get _inputElementValue() {
|
| - return this._inputElement[this._propertyForValue] || this._inputElement.value;
|
| - },
|
| + /** @private */
|
| + fetchingSyncedTabs_: {
|
| + type: Boolean,
|
| + value: false,
|
| + }
|
| + },
|
|
|
| - ready: function() {
|
| - if (!this._addons) {
|
| - this._addons = [];
|
| - }
|
| - this.addEventListener('focus', this._boundOnFocus, true);
|
| - this.addEventListener('blur', this._boundOnBlur, true);
|
| - },
|
| + listeners: {
|
| + 'toggle-menu': 'onToggleMenu_',
|
| + },
|
|
|
| - attached: function() {
|
| - if (this.attrForValue) {
|
| - this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
|
| - } else {
|
| - this.addEventListener('input', this._onInput);
|
| - }
|
| + /** @override */
|
| + attached: function() {
|
| + // Update the sign in state.
|
| + chrome.send('otherDevicesInitialized');
|
| + },
|
|
|
| - // Only validate when attached if the input already has a value.
|
| - if (this._inputElementValue != '') {
|
| - this._handleValueAndAutoValidate(this._inputElement);
|
| - } else {
|
| - this._handleValue(this._inputElement);
|
| - }
|
| - },
|
| + /**
|
| + * @param {!ForeignSession} session
|
| + * @return {!ForeignDeviceInternal}
|
| + */
|
| + createInternalDevice_: function(session) {
|
| + var tabs = [];
|
| + var separatorIndexes = [];
|
| + for (var i = 0; i < session.windows.length; i++) {
|
| + var windowId = session.windows[i].sessionId;
|
| + var newTabs = session.windows[i].tabs;
|
| + if (newTabs.length == 0)
|
| + continue;
|
| +
|
| + newTabs.forEach(function(tab) {
|
| + tab.windowId = windowId;
|
| + });
|
|
|
| - _onAddonAttached: function(event) {
|
| - if (!this._addons) {
|
| - this._addons = [];
|
| - }
|
| - var target = event.target;
|
| - if (this._addons.indexOf(target) === -1) {
|
| - this._addons.push(target);
|
| - if (this.isAttached) {
|
| - this._handleValue(this._inputElement);
|
| + var windowAdded = false;
|
| + if (!this.searchTerm) {
|
| + // Add all the tabs if there is no search term.
|
| + tabs = tabs.concat(newTabs);
|
| + windowAdded = true;
|
| + } else {
|
| + var searchText = this.searchTerm.toLowerCase();
|
| + for (var j = 0; j < newTabs.length; j++) {
|
| + var tab = newTabs[j];
|
| + if (tab.title.toLowerCase().indexOf(searchText) != -1) {
|
| + tabs.push(tab);
|
| + windowAdded = true;
|
| + }
|
| }
|
| }
|
| - },
|
| + if (windowAdded && i != session.windows.length - 1)
|
| + separatorIndexes.push(tabs.length - 1);
|
| + }
|
| + return {
|
| + device: session.name,
|
| + lastUpdateTime: '– ' + session.modifiedTime,
|
| + separatorIndexes: separatorIndexes,
|
| + timestamp: session.timestamp,
|
| + tabs: tabs,
|
| + tag: session.tag,
|
| + };
|
| + },
|
|
|
| - _onFocus: function() {
|
| - this._setFocused(true);
|
| - },
|
| + onSignInTap_: function() {
|
| + chrome.send('SyncSetupShowSetupUI');
|
| + chrome.send('SyncSetupStartSignIn', [false]);
|
| + },
|
|
|
| - _onBlur: function() {
|
| - this._setFocused(false);
|
| - this._handleValueAndAutoValidate(this._inputElement);
|
| - },
|
| + onToggleMenu_: function(e) {
|
| + this.$.menu.toggleMenu(e.detail.target, e.detail.tag);
|
| + },
|
|
|
| - _onInput: function(event) {
|
| - this._handleValueAndAutoValidate(event.target);
|
| - },
|
| + onOpenAllTap_: function() {
|
| + md_history.BrowserService.getInstance().openForeignSessionAllTabs(
|
| + this.$.menu.itemData);
|
| + this.$.menu.closeMenu();
|
| + },
|
|
|
| - _onValueChanged: function(event) {
|
| - this._handleValueAndAutoValidate(event.target);
|
| - },
|
| + onDeleteSessionTap_: function() {
|
| + md_history.BrowserService.getInstance().deleteForeignSession(
|
| + this.$.menu.itemData);
|
| + this.$.menu.closeMenu();
|
| + },
|
|
|
| - _handleValue: function(inputElement) {
|
| - var value = this._inputElementValue;
|
| + /** @private */
|
| + clearDisplayedSyncedDevices_: function() {
|
| + this.syncedDevices_ = [];
|
| + },
|
|
|
| - // type="number" hack needed because this.value is empty until it's valid
|
| - if (value || value === 0 || (inputElement.type === 'number' && !inputElement.checkValidity())) {
|
| - this._inputHasContent = true;
|
| - } else {
|
| - this._inputHasContent = false;
|
| - }
|
| + /**
|
| + * Decide whether or not should display no synced tabs message.
|
| + * @param {boolean} signInState
|
| + * @param {number} syncedDevicesLength
|
| + * @param {boolean} guestSession
|
| + * @return {boolean}
|
| + */
|
| + showNoSyncedMessage: function(
|
| + signInState, syncedDevicesLength, guestSession) {
|
| + if (guestSession)
|
| + return true;
|
|
|
| - this.updateAddons({
|
| - inputElement: inputElement,
|
| - value: value,
|
| - invalid: this.invalid
|
| - });
|
| - },
|
| + return signInState && syncedDevicesLength == 0;
|
| + },
|
|
|
| - _handleValueAndAutoValidate: function(inputElement) {
|
| - if (this.autoValidate) {
|
| - var valid;
|
| - if (inputElement.validate) {
|
| - valid = inputElement.validate(this._inputElementValue);
|
| - } else {
|
| - valid = inputElement.checkValidity();
|
| - }
|
| - this.invalid = !valid;
|
| - }
|
| + /**
|
| + * Shows the signin guide when the user is not signed in and not in a guest
|
| + * session.
|
| + * @param {boolean} signInState
|
| + * @param {boolean} guestSession
|
| + * @return {boolean}
|
| + */
|
| + showSignInGuide: function(signInState, guestSession) {
|
| + return !signInState && !guestSession;
|
| + },
|
|
|
| - // Call this last to notify the add-ons.
|
| - this._handleValue(inputElement);
|
| - },
|
| + /**
|
| + * Decide what message should be displayed when user is logged in and there
|
| + * are no synced tabs.
|
| + * @param {boolean} fetchingSyncedTabs
|
| + * @return {string}
|
| + */
|
| + noSyncedTabsMessage: function(fetchingSyncedTabs) {
|
| + return loadTimeData.getString(
|
| + fetchingSyncedTabs ? 'loading' : 'noSyncedResults');
|
| + },
|
|
|
| - _onIronInputValidate: function(event) {
|
| - this.invalid = this._inputElement.invalid;
|
| - },
|
| + /**
|
| + * Replaces the currently displayed synced tabs with |sessionList|. It is
|
| + * common for only a single session within the list to have changed, We try to
|
| + * avoid doing extra work in this case. The logic could be more intelligent
|
| + * about updating individual tabs rather than replacing whole sessions, but
|
| + * this approach seems to have acceptable performance.
|
| + * @param {?Array<!ForeignSession>} sessionList
|
| + */
|
| + updateSyncedDevices: function(sessionList) {
|
| + this.fetchingSyncedTabs_ = false;
|
|
|
| - _invalidChanged: function() {
|
| - if (this._addons) {
|
| - this.updateAddons({invalid: this.invalid});
|
| - }
|
| - },
|
| + if (!sessionList)
|
| + return;
|
|
|
| - /**
|
| - * Call this to update the state of add-ons.
|
| - * @param {Object} state Add-on state.
|
| - */
|
| - updateAddons: function(state) {
|
| - for (var addon, index = 0; addon = this._addons[index]; index++) {
|
| - addon.update(state);
|
| + // First, update any existing devices that have changed.
|
| + var updateCount = Math.min(sessionList.length, this.syncedDevices_.length);
|
| + for (var i = 0; i < updateCount; i++) {
|
| + var oldDevice = this.syncedDevices_[i];
|
| + if (oldDevice.tag != sessionList[i].tag ||
|
| + oldDevice.timestamp != sessionList[i].timestamp) {
|
| + this.splice(
|
| + 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i]));
|
| }
|
| - },
|
| + }
|
|
|
| - _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
|
| - var cls = 'input-content';
|
| - if (!noLabelFloat) {
|
| - var label = this.querySelector('label');
|
| + // Then, append any new devices.
|
| + for (var i = updateCount; i < sessionList.length; i++) {
|
| + this.push('syncedDevices_', this.createInternalDevice_(sessionList[i]));
|
| + }
|
| + },
|
|
|
| - if (alwaysFloatLabel || _inputHasContent) {
|
| - cls += ' label-is-floating';
|
| - // If the label is floating, ignore any offsets that may have been
|
| - // applied from a prefix element.
|
| - this.$.labelAndInputContainer.style.position = 'static';
|
| + /**
|
| + * Get called when user's sign in state changes, this will affect UI of synced
|
| + * tabs page. Sign in promo gets displayed when user is signed out, and
|
| + * different messages are shown when there are no synced tabs.
|
| + * @param {boolean} isUserSignedIn
|
| + */
|
| + updateSignInState: function(isUserSignedIn) {
|
| + // If user's sign in state didn't change, then don't change message or
|
| + // update UI.
|
| + if (this.signInState_ == isUserSignedIn)
|
| + return;
|
|
|
| - if (invalid) {
|
| - cls += ' is-invalid';
|
| - } else if (focused) {
|
| - cls += " label-is-highlighted";
|
| - }
|
| - } else {
|
| - // When the label is not floating, it should overlap the input element.
|
| - if (label) {
|
| - this.$.labelAndInputContainer.style.position = 'relative';
|
| - }
|
| - }
|
| - } else {
|
| - if (_inputHasContent) {
|
| - cls += ' label-is-hidden';
|
| + this.signInState_ = isUserSignedIn;
|
| +
|
| + // User signed out, clear synced device list and show the sign in promo.
|
| + if (!isUserSignedIn) {
|
| + this.clearDisplayedSyncedDevices_();
|
| + return;
|
| + }
|
| + // User signed in, show the loading message when querying for synced
|
| + // devices.
|
| + this.fetchingSyncedTabs_ = true;
|
| + },
|
| +
|
| + searchTermChanged: function(searchTerm) {
|
| + this.clearDisplayedSyncedDevices_();
|
| + this.updateSyncedDevices(this.sessionList);
|
| + }
|
| +});
|
| +/**
|
| + `iron-selector` is an element which can be used to manage a list of elements
|
| + that can be selected. Tapping on the item will make the item selected. The `selected` indicates
|
| + which item is being selected. The default is to use the index of the item.
|
| +
|
| + Example:
|
| +
|
| + <iron-selector selected="0">
|
| + <div>Item 1</div>
|
| + <div>Item 2</div>
|
| + <div>Item 3</div>
|
| + </iron-selector>
|
| +
|
| + If you want to use the attribute value of an element for `selected` instead of the index,
|
| + set `attrForSelected` to the name of the attribute. For example, if you want to select item by
|
| + `name`, set `attrForSelected` to `name`.
|
| +
|
| + Example:
|
| +
|
| + <iron-selector attr-for-selected="name" selected="foo">
|
| + <div name="foo">Foo</div>
|
| + <div name="bar">Bar</div>
|
| + <div name="zot">Zot</div>
|
| + </iron-selector>
|
| +
|
| + You can specify a default fallback with `fallbackSelection` in case the `selected` attribute does
|
| + not match the `attrForSelected` attribute of any elements.
|
| +
|
| + Example:
|
| +
|
| + <iron-selector attr-for-selected="name" selected="non-existing"
|
| + fallback-selection="default">
|
| + <div name="foo">Foo</div>
|
| + <div name="bar">Bar</div>
|
| + <div name="default">Default</div>
|
| + </iron-selector>
|
| +
|
| + Note: When the selector is multi, the selection will set to `fallbackSelection` iff
|
| + the number of matching elements is zero.
|
| +
|
| + `iron-selector` is not styled. Use the `iron-selected` CSS class to style the selected element.
|
| +
|
| + Example:
|
| +
|
| + <style>
|
| + .iron-selected {
|
| + background: #eee;
|
| }
|
| - }
|
| - return cls;
|
| - },
|
| + </style>
|
|
|
| - _computeUnderlineClass: function(focused, invalid) {
|
| - var cls = 'underline';
|
| - if (invalid) {
|
| - cls += ' is-invalid';
|
| - } else if (focused) {
|
| - cls += ' is-highlighted'
|
| - }
|
| - return cls;
|
| - },
|
| + ...
|
| +
|
| + <iron-selector selected="0">
|
| + <div>Item 1</div>
|
| + <div>Item 2</div>
|
| + <div>Item 3</div>
|
| + </iron-selector>
|
| +
|
| + @demo demo/index.html
|
| + */
|
| +
|
| + Polymer({
|
| +
|
| + is: 'iron-selector',
|
| +
|
| + behaviors: [
|
| + Polymer.IronMultiSelectableBehavior
|
| + ]
|
|
|
| - _computeAddOnContentClass: function(focused, invalid) {
|
| - var cls = 'add-on-content';
|
| - if (invalid) {
|
| - cls += ' is-invalid';
|
| - } else if (focused) {
|
| - cls += ' is-highlighted'
|
| - }
|
| - return cls;
|
| - }
|
| });
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
|
|
| -var SearchField = Polymer({
|
| - is: 'cr-search-field',
|
| -
|
| - behaviors: [CrSearchFieldBehavior],
|
| +Polymer({
|
| + is: 'history-side-bar',
|
|
|
| properties: {
|
| - value_: String,
|
| - },
|
| + selectedPage: {
|
| + type: String,
|
| + notify: true
|
| + },
|
|
|
| - /** @return {!HTMLInputElement} */
|
| - getSearchInput: function() {
|
| - return this.$.searchInput;
|
| + route: Object,
|
| +
|
| + showFooter: Boolean,
|
| +
|
| + // If true, the sidebar is contained within an app-drawer.
|
| + drawer: {
|
| + type: Boolean,
|
| + reflectToAttribute: true
|
| + },
|
| },
|
|
|
| /** @private */
|
| - clearSearch_: function() {
|
| - this.setValue('');
|
| - this.getSearchInput().focus();
|
| + onSelectorActivate_: function() {
|
| + this.fire('history-close-drawer');
|
| },
|
|
|
| - /** @private */
|
| - toggleShowingSearch_: function() {
|
| - this.showingSearch = !this.showingSearch;
|
| + /**
|
| + * Relocates the user to the clear browsing data section of the settings page.
|
| + * @param {Event} e
|
| + * @private
|
| + */
|
| + onClearBrowsingDataTap_: function(e) {
|
| + md_history.BrowserService.getInstance().openClearBrowsingData();
|
| + e.preventDefault();
|
| },
|
| +
|
| + /**
|
| + * @param {Object} route
|
| + * @private
|
| + */
|
| + getQueryString_: function(route) {
|
| + return window.location.search;
|
| + }
|
| });
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
|
|
| -cr.define('downloads', function() {
|
| - var Toolbar = Polymer({
|
| - is: 'downloads-toolbar',
|
| -
|
| - attached: function() {
|
| - // isRTL() only works after i18n_template.js runs to set <html dir>.
|
| - this.overflowAlign_ = isRTL() ? 'left' : 'right';
|
| - },
|
| -
|
| - properties: {
|
| - downloadsShowing: {
|
| - reflectToAttribute: true,
|
| - type: Boolean,
|
| - value: false,
|
| - observer: 'downloadsShowingChanged_',
|
| - },
|
| +Polymer({
|
| + is: 'history-app',
|
|
|
| - overflowAlign_: {
|
| - type: String,
|
| - value: 'right',
|
| - },
|
| - },
|
| + properties: {
|
| + showSidebarFooter: Boolean,
|
|
|
| - listeners: {
|
| - 'paper-dropdown-close': 'onPaperDropdownClose_',
|
| - 'paper-dropdown-open': 'onPaperDropdownOpen_',
|
| - },
|
| + // The id of the currently selected page.
|
| + selectedPage_: {type: String, value: 'history', observer: 'unselectAll'},
|
|
|
| - /** @return {boolean} Whether removal can be undone. */
|
| - canUndo: function() {
|
| - return this.$['search-input'] != this.shadowRoot.activeElement;
|
| - },
|
| + // Whether domain-grouped history is enabled.
|
| + grouped_: {type: Boolean, reflectToAttribute: true},
|
|
|
| - /** @return {boolean} Whether "Clear all" should be allowed. */
|
| - canClearAll: function() {
|
| - return !this.$['search-input'].getValue() && this.downloadsShowing;
|
| + /** @type {!QueryState} */
|
| + queryState_: {
|
| + type: Object,
|
| + value: function() {
|
| + return {
|
| + // Whether the most recent query was incremental.
|
| + incremental: false,
|
| + // A query is initiated by page load.
|
| + querying: true,
|
| + queryingDisabled: false,
|
| + _range: HistoryRange.ALL_TIME,
|
| + searchTerm: '',
|
| + // TODO(calamity): Make history toolbar buttons change the offset
|
| + groupedOffset: 0,
|
| +
|
| + set range(val) { this._range = Number(val); },
|
| + get range() { return this._range; },
|
| + };
|
| + }
|
| },
|
|
|
| - onFindCommand: function() {
|
| - this.$['search-input'].showAndFocus();
|
| + /** @type {!QueryResult} */
|
| + queryResult_: {
|
| + type: Object,
|
| + value: function() {
|
| + return {
|
| + info: null,
|
| + results: null,
|
| + sessionList: null,
|
| + };
|
| + }
|
| },
|
|
|
| - /** @private */
|
| - closeMoreActions_: function() {
|
| - this.$.more.close();
|
| - },
|
| + // Route data for the current page.
|
| + routeData_: Object,
|
|
|
| - /** @private */
|
| - downloadsShowingChanged_: function() {
|
| - this.updateClearAll_();
|
| - },
|
| + // The query params for the page.
|
| + queryParams_: Object,
|
|
|
| - /** @private */
|
| - onClearAllTap_: function() {
|
| - assert(this.canClearAll());
|
| - downloads.ActionService.getInstance().clearAll();
|
| - },
|
| + // True if the window is narrow enough for the page to have a drawer.
|
| + hasDrawer_: Boolean,
|
| + },
|
|
|
| - /** @private */
|
| - onPaperDropdownClose_: function() {
|
| - window.removeEventListener('resize', assert(this.boundClose_));
|
| - },
|
| + observers: [
|
| + // routeData_.page <=> selectedPage
|
| + 'routeDataChanged_(routeData_.page)',
|
| + 'selectedPageChanged_(selectedPage_)',
|
|
|
| - /**
|
| - * @param {!Event} e
|
| - * @private
|
| - */
|
| - onItemBlur_: function(e) {
|
| - var menu = /** @type {PaperMenuElement} */(this.$$('paper-menu'));
|
| - if (menu.items.indexOf(e.relatedTarget) >= 0)
|
| - return;
|
| + // queryParams_.q <=> queryState.searchTerm
|
| + 'searchTermChanged_(queryState_.searchTerm)',
|
| + 'searchQueryParamChanged_(queryParams_.q)',
|
|
|
| - this.$.more.restoreFocusOnClose = false;
|
| - this.closeMoreActions_();
|
| - this.$.more.restoreFocusOnClose = true;
|
| - },
|
| + ],
|
|
|
| - /** @private */
|
| - onPaperDropdownOpen_: function() {
|
| - this.boundClose_ = this.boundClose_ || this.closeMoreActions_.bind(this);
|
| - window.addEventListener('resize', this.boundClose_);
|
| - },
|
| + // TODO(calamity): Replace these event listeners with data bound properties.
|
| + listeners: {
|
| + 'cr-menu-tap': 'onMenuTap_',
|
| + 'history-checkbox-select': 'checkboxSelected',
|
| + 'unselect-all': 'unselectAll',
|
| + 'delete-selected': 'deleteSelected',
|
| + 'search-domain': 'searchDomain_',
|
| + 'history-close-drawer': 'closeDrawer_',
|
| + },
|
|
|
| - /**
|
| - * @param {!CustomEvent} event
|
| - * @private
|
| - */
|
| - onSearchChanged_: function(event) {
|
| - downloads.ActionService.getInstance().search(
|
| - /** @type {string} */ (event.detail));
|
| - this.updateClearAll_();
|
| - },
|
| + /** @override */
|
| + ready: function() {
|
| + this.grouped_ = loadTimeData.getBoolean('groupByDomain');
|
|
|
| - /** @private */
|
| - onOpenDownloadsFolderTap_: function() {
|
| - downloads.ActionService.getInstance().openDownloadsFolder();
|
| - },
|
| + cr.ui.decorate('command', cr.ui.Command);
|
| + document.addEventListener('canExecute', this.onCanExecute_.bind(this));
|
| + document.addEventListener('command', this.onCommand_.bind(this));
|
|
|
| - /** @private */
|
| - updateClearAll_: function() {
|
| - this.$$('#actions .clear-all').hidden = !this.canClearAll();
|
| - this.$$('paper-menu .clear-all').hidden = !this.canClearAll();
|
| - },
|
| - });
|
| + // Redirect legacy search URLs to URLs compatible with material history.
|
| + if (window.location.hash) {
|
| + window.location.href = window.location.href.split('#')[0] + '?' +
|
| + window.location.hash.substr(1);
|
| + }
|
| + },
|
|
|
| - return {Toolbar: Toolbar};
|
| -});
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| + /** @private */
|
| + onMenuTap_: function() {
|
| + var drawer = this.$$('#drawer');
|
| + if (drawer)
|
| + drawer.toggle();
|
| + },
|
|
|
| -cr.define('downloads', function() {
|
| - var Manager = Polymer({
|
| - is: 'downloads-manager',
|
| + /**
|
| + * Listens for history-item being selected or deselected (through checkbox)
|
| + * and changes the view of the top toolbar.
|
| + * @param {{detail: {countAddition: number}}} e
|
| + */
|
| + checkboxSelected: function(e) {
|
| + var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
|
| + toolbar.count += e.detail.countAddition;
|
| + },
|
|
|
| - properties: {
|
| - hasDownloads_: {
|
| - observer: 'hasDownloadsChanged_',
|
| - type: Boolean,
|
| - },
|
| + /**
|
| + * Listens for call to cancel selection and loops through all items to set
|
| + * checkbox to be unselected.
|
| + * @private
|
| + */
|
| + unselectAll: function() {
|
| + var listContainer =
|
| + /** @type {HistoryListContainerElement} */ (this.$['history']);
|
| + var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
|
| + listContainer.unselectAllItems(toolbar.count);
|
| + toolbar.count = 0;
|
| + },
|
|
|
| - items_: {
|
| - type: Array,
|
| - value: function() { return []; },
|
| - },
|
| - },
|
| + deleteSelected: function() {
|
| + this.$.history.deleteSelectedWithPrompt();
|
| + },
|
|
|
| - hostAttributes: {
|
| - loading: true,
|
| - },
|
| + /**
|
| + * @param {HistoryQuery} info An object containing information about the
|
| + * query.
|
| + * @param {!Array<HistoryEntry>} results A list of results.
|
| + */
|
| + historyResult: function(info, results) {
|
| + this.set('queryState_.querying', false);
|
| + this.set('queryResult_.info', info);
|
| + this.set('queryResult_.results', results);
|
| + var listContainer =
|
| + /** @type {HistoryListContainerElement} */ (this.$['history']);
|
| + listContainer.historyResult(info, results);
|
| + },
|
|
|
| - listeners: {
|
| - 'downloads-list.scroll': 'onListScroll_',
|
| - },
|
| + /**
|
| + * Fired when the user presses 'More from this site'.
|
| + * @param {{detail: {domain: string}}} e
|
| + */
|
| + searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); },
|
|
|
| - observers: [
|
| - 'itemsChanged_(items_.*)',
|
| - ],
|
| + /**
|
| + * @param {Event} e
|
| + * @private
|
| + */
|
| + onCanExecute_: function(e) {
|
| + e = /** @type {cr.ui.CanExecuteEvent} */(e);
|
| + switch (e.command.id) {
|
| + case 'find-command':
|
| + e.canExecute = true;
|
| + break;
|
| + case 'slash-command':
|
| + e.canExecute = !this.$.toolbar.searchBar.isSearchFocused();
|
| + break;
|
| + case 'delete-command':
|
| + e.canExecute = this.$.toolbar.count > 0;
|
| + break;
|
| + }
|
| + },
|
|
|
| - /** @private */
|
| - clearAll_: function() {
|
| - this.set('items_', []);
|
| - },
|
| + /**
|
| + * @param {string} searchTerm
|
| + * @private
|
| + */
|
| + searchTermChanged_: function(searchTerm) {
|
| + this.set('queryParams_.q', searchTerm || null);
|
| + this.$['history'].queryHistory(false);
|
| + },
|
|
|
| - /** @private */
|
| - hasDownloadsChanged_: function() {
|
| - if (loadTimeData.getBoolean('allowDeletingHistory'))
|
| - this.$.toolbar.downloadsShowing = this.hasDownloads_;
|
| + /**
|
| + * @param {string} searchQuery
|
| + * @private
|
| + */
|
| + searchQueryParamChanged_: function(searchQuery) {
|
| + this.$.toolbar.setSearchTerm(searchQuery || '');
|
| + },
|
|
|
| - if (this.hasDownloads_) {
|
| - this.$['downloads-list'].fire('iron-resize');
|
| - } else {
|
| - var isSearching = downloads.ActionService.getInstance().isSearching();
|
| - var messageToShow = isSearching ? 'noSearchResults' : 'noDownloads';
|
| - this.$['no-downloads'].querySelector('span').textContent =
|
| - loadTimeData.getString(messageToShow);
|
| - }
|
| - },
|
| + /**
|
| + * @param {Event} e
|
| + * @private
|
| + */
|
| + onCommand_: function(e) {
|
| + if (e.command.id == 'find-command' || e.command.id == 'slash-command')
|
| + this.$.toolbar.showSearchField();
|
| + if (e.command.id == 'delete-command')
|
| + this.deleteSelected();
|
| + },
|
|
|
| - /**
|
| - * @param {number} index
|
| - * @param {!Array<!downloads.Data>} list
|
| - * @private
|
| - */
|
| - insertItems_: function(index, list) {
|
| - this.splice.apply(this, ['items_', index, 0].concat(list));
|
| - this.updateHideDates_(index, index + list.length);
|
| - this.removeAttribute('loading');
|
| - },
|
| + /**
|
| + * @param {!Array<!ForeignSession>} sessionList Array of objects describing
|
| + * the sessions from other devices.
|
| + * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
|
| + */
|
| + setForeignSessions: function(sessionList, isTabSyncEnabled) {
|
| + if (!isTabSyncEnabled)
|
| + return;
|
|
|
| - /** @private */
|
| - itemsChanged_: function() {
|
| - this.hasDownloads_ = this.items_.length > 0;
|
| - },
|
| + this.set('queryResult_.sessionList', sessionList);
|
| + },
|
|
|
| - /**
|
| - * @param {Event} e
|
| - * @private
|
| - */
|
| - onCanExecute_: function(e) {
|
| - e = /** @type {cr.ui.CanExecuteEvent} */(e);
|
| - switch (e.command.id) {
|
| - case 'undo-command':
|
| - e.canExecute = this.$.toolbar.canUndo();
|
| - break;
|
| - case 'clear-all-command':
|
| - e.canExecute = this.$.toolbar.canClearAll();
|
| - break;
|
| - case 'find-command':
|
| - e.canExecute = true;
|
| - break;
|
| - }
|
| - },
|
| + /**
|
| + * Update sign in state of synced device manager after user logs in or out.
|
| + * @param {boolean} isUserSignedIn
|
| + */
|
| + updateSignInState: function(isUserSignedIn) {
|
| + var syncedDeviceManagerElem =
|
| + /** @type {HistorySyncedDeviceManagerElement} */this
|
| + .$$('history-synced-device-manager');
|
| + if (syncedDeviceManagerElem)
|
| + syncedDeviceManagerElem.updateSignInState(isUserSignedIn);
|
| + },
|
|
|
| - /**
|
| - * @param {Event} e
|
| - * @private
|
| - */
|
| - onCommand_: function(e) {
|
| - if (e.command.id == 'clear-all-command')
|
| - downloads.ActionService.getInstance().clearAll();
|
| - else if (e.command.id == 'undo-command')
|
| - downloads.ActionService.getInstance().undo();
|
| - else if (e.command.id == 'find-command')
|
| - this.$.toolbar.onFindCommand();
|
| - },
|
| + /**
|
| + * @param {string} selectedPage
|
| + * @return {boolean}
|
| + * @private
|
| + */
|
| + syncedTabsSelected_: function(selectedPage) {
|
| + return selectedPage == 'syncedTabs';
|
| + },
|
|
|
| - /** @private */
|
| - onListScroll_: function() {
|
| - var list = this.$['downloads-list'];
|
| - if (list.scrollHeight - list.scrollTop - list.offsetHeight <= 100) {
|
| - // Approaching the end of the scrollback. Attempt to load more items.
|
| - downloads.ActionService.getInstance().loadMore();
|
| - }
|
| - },
|
| + /**
|
| + * @param {boolean} querying
|
| + * @param {boolean} incremental
|
| + * @param {string} searchTerm
|
| + * @return {boolean} Whether a loading spinner should be shown (implies the
|
| + * backend is querying a new search term).
|
| + * @private
|
| + */
|
| + shouldShowSpinner_: function(querying, incremental, searchTerm) {
|
| + return querying && !incremental && searchTerm != '';
|
| + },
|
|
|
| - /** @private */
|
| - onLoad_: function() {
|
| - cr.ui.decorate('command', cr.ui.Command);
|
| - document.addEventListener('canExecute', this.onCanExecute_.bind(this));
|
| - document.addEventListener('command', this.onCommand_.bind(this));
|
| + /**
|
| + * @param {string} page
|
| + * @private
|
| + */
|
| + routeDataChanged_: function(page) {
|
| + this.selectedPage_ = page;
|
| + },
|
|
|
| - downloads.ActionService.getInstance().loadMore();
|
| - },
|
| + /**
|
| + * @param {string} selectedPage
|
| + * @private
|
| + */
|
| + selectedPageChanged_: function(selectedPage) {
|
| + this.set('routeData_.page', selectedPage);
|
| + },
|
|
|
| - /**
|
| - * @param {number} index
|
| - * @private
|
| - */
|
| - removeItem_: function(index) {
|
| - this.splice('items_', index, 1);
|
| - this.updateHideDates_(index, index);
|
| - this.onListScroll_();
|
| - },
|
| + /**
|
| + * This computed binding is needed to make the iron-pages selector update when
|
| + * the synced-device-manager is instantiated for the first time. Otherwise the
|
| + * fallback selection will continue to be used after the corresponding item is
|
| + * added as a child of iron-pages.
|
| + * @param {string} selectedPage
|
| + * @param {Array} items
|
| + * @return {string}
|
| + * @private
|
| + */
|
| + getSelectedPage_(selectedPage, items) {
|
| + return selectedPage;
|
| + },
|
|
|
| - /**
|
| - * @param {number} start
|
| - * @param {number} end
|
| - * @private
|
| - */
|
| - updateHideDates_: function(start, end) {
|
| - for (var i = start; i <= end; ++i) {
|
| - var current = this.items_[i];
|
| - if (!current)
|
| - continue;
|
| - var prev = this.items_[i - 1];
|
| - current.hideDate = !!prev && prev.date_string == current.date_string;
|
| - }
|
| - },
|
| + /** @private */
|
| + closeDrawer_: function() {
|
| + var drawer = this.$$('#drawer');
|
| + if (drawer)
|
| + drawer.close();
|
| + },
|
| +});
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
|
|
| - /**
|
| - * @param {number} index
|
| - * @param {!downloads.Data} data
|
| - * @private
|
| - */
|
| - updateItem_: function(index, data) {
|
| - this.set('items_.' + index, data);
|
| - this.updateHideDates_(index, index);
|
| - var list = /** @type {!IronListElement} */(this.$['downloads-list']);
|
| - list.updateSizeForItem(index);
|
| - },
|
| - });
|
| +// Send the history query immediately. This allows the query to process during
|
| +// the initial page startup.
|
| +chrome.send('queryHistory', ['', 0, 0, 0, RESULTS_PER_PAGE]);
|
| +chrome.send('getForeignSessions');
|
|
|
| - Manager.clearAll = function() {
|
| - Manager.get().clearAll_();
|
| - };
|
| +/** @type {Promise} */
|
| +var upgradePromise = null;
|
| +/** @type {boolean} */
|
| +var resultsRendered = false;
|
|
|
| - /** @return {!downloads.Manager} */
|
| - Manager.get = function() {
|
| - return /** @type {!downloads.Manager} */(
|
| - queryRequiredElement('downloads-manager'));
|
| - };
|
| +/**
|
| + * @return {!Promise<!HistoryAppElement>} Resolves once the history-app has been
|
| + * fully upgraded.
|
| + */
|
| +function waitForHistoryApp() {
|
| + if (!upgradePromise) {
|
| + upgradePromise = new Promise(function(resolve, reject) {
|
| + if (window.Polymer && Polymer.isInstance &&
|
| + Polymer.isInstance(document.querySelector('history-app'))) {
|
| + resolve(/** @type {!HistoryAppElement} */(
|
| + document.querySelector('history-app')));
|
| + } else {
|
| + $('bundle').addEventListener('load', function() {
|
| + resolve(/** @type {!HistoryAppElement} */(
|
| + document.querySelector('history-app')));
|
| + });
|
| + }
|
| + });
|
| + }
|
| + return upgradePromise;
|
| +}
|
|
|
| - Manager.insertItems = function(index, list) {
|
| - Manager.get().insertItems_(index, list);
|
| - };
|
| +// Chrome Callbacks-------------------------------------------------------------
|
|
|
| - Manager.onLoad = function() {
|
| - Manager.get().onLoad_();
|
| - };
|
| +/**
|
| + * Our history system calls this function with results from searches.
|
| + * @param {HistoryQuery} info An object containing information about the query.
|
| + * @param {!Array<HistoryEntry>} results A list of results.
|
| + */
|
| +function historyResult(info, results) {
|
| + waitForHistoryApp().then(function(historyApp) {
|
| + historyApp.historyResult(info, results);
|
| + document.body.classList.remove('loading');
|
| +
|
| + if (!resultsRendered) {
|
| + resultsRendered = true;
|
| + // requestAnimationFrame allows measurement immediately before the next
|
| + // repaint, but after the first page of <iron-list> items has stamped.
|
| + requestAnimationFrame(function() {
|
| + chrome.send(
|
| + 'metricsHandler:recordTime',
|
| + ['History.ResultsRenderedTime', window.performance.now()]);
|
| + });
|
| + }
|
| + });
|
| +}
|
|
|
| - Manager.removeItem = function(index) {
|
| - Manager.get().removeItem_(index);
|
| - };
|
| +/**
|
| + * Called by the history backend after receiving results and after discovering
|
| + * the existence of other forms of browsing history.
|
| + * @param {boolean} hasSyncedResults Whether there are synced results.
|
| + * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
|
| + * a sentence about the existence of other forms of browsing history.
|
| + */
|
| +function showNotification(
|
| + hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
|
| + // TODO(msramek): |hasSyncedResults| was used in the old WebUI to show
|
| + // the message about other signed-in devices. This message does not exist
|
| + // in the MD history anymore, so the parameter is not needed. Remove it
|
| + // when WebUI is removed and this becomes the only client of
|
| + // BrowsingHistoryHandler.
|
| + waitForHistoryApp().then(function(historyApp) {
|
| + historyApp.showSidebarFooter = includeOtherFormsOfBrowsingHistory;
|
| + });
|
| +}
|
|
|
| - Manager.updateItem = function(index, data) {
|
| - Manager.get().updateItem_(index, data);
|
| - };
|
| +/**
|
| + * Receives the synced history data. An empty list means that either there are
|
| + * no foreign sessions, or tab sync is disabled for this profile.
|
| + * |isTabSyncEnabled| makes it possible to distinguish between the cases.
|
| + *
|
| + * @param {!Array<!ForeignSession>} sessionList Array of objects describing the
|
| + * sessions from other devices.
|
| + * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
|
| + */
|
| +function setForeignSessions(sessionList, isTabSyncEnabled) {
|
| + waitForHistoryApp().then(function(historyApp) {
|
| + historyApp.setForeignSessions(sessionList, isTabSyncEnabled);
|
| + });
|
| +}
|
|
|
| - return {Manager: Manager};
|
| -});
|
| -// Copyright 2015 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| +/**
|
| + * Called when the history is deleted by someone else.
|
| + */
|
| +function historyDeleted() {
|
| +}
|
|
|
| -window.addEventListener('load', downloads.Manager.onLoad);
|
| +/**
|
| + * Called by the history backend after user's sign in state changes.
|
| + * @param {boolean} isUserSignedIn Whether user is signed in or not now.
|
| + */
|
| +function updateSignInState(isUserSignedIn) {
|
| + waitForHistoryApp().then(function(historyApp) {
|
| + historyApp.updateSignInState(isUserSignedIn);
|
| + });
|
| +};
|
|
|